前言
FFmpeg的avformat_open_input函数和av_read_frame函数默认是阻塞的,意味着如果RTSP服务器不在线的情况下,ffplay工具会一直在avformat_open_input函数内部不断尝试连接服务器。
ffplay退出阻塞的机制
decode_interrupt_cb函数主要是为了允许用户中断退出
当用户按下键盘的q,abort_request将会赋值为1,在上述两个函数中,退出循环
登录后复制
cpp
static int decode_interrupt_cb(void *ctx)
{
VideoState *is = ctx;
return is->abort_request;
}
登录后复制
cpp
ic->interrupt_callback.callback = decode_interrupt_cb;
ic->interrupt_callback.opaque = is;
连接超时
代码堆栈
登录后复制
cpp
ffplayd.exe!decode_interrupt_cb(void * ctx) 行 2713 C
ffplayd.exe!ff_check_interrupt(AVIOInterruptCB * cb) 行 667 C
ffplayd.exe!ff_poll_interrupt(pollfd * p, unsigned long nfds, int timeout, AVIOInterruptCB * cb) 行 166 C
ffplayd.exe!ff_connect_parallel(addrinfo * addrs, int timeout_ms_per_address, int parallel, URLContext * h, int * fd, void(*)(void *, int) customize_fd, void * customize_ctx) 行 461 C
ffplayd.exe!tcp_open(URLContext * h, const char * uri, int flags) 行 198 C
ffplayd.exe!ffurl_connect(URLContext * uc, AVDictionary * * options) 行 205 C
ffplayd.exe!ffurl_open_whitelist(URLContext * * puc, const char * filename, int flags, const AVIOInterruptCB * int_cb, AVDictionary * * options, const char * whitelist, const char * blacklist, URLContext * parent) 行 345 C
ffplayd.exe!ff_rtsp_connect(AVFormatContext * s) 行 1841 C
ffplayd.exe!rtsp_read_header(AVFormatContext * s) 行 726 C
> ffplayd.exe!avformat_open_input(AVFormatContext * * ps, const char * filename, AVInputFormat * fmt, AVDictionary * * options) 行 631 C
ffplayd.exe!read_thread(void * arg) 行 2780 C
ffplayd.exe!SDL_RunThread(void * data) 行 283 C
ffplayd.exe!RunThread(void * data) 行 91 C
ffplayd.exe!RunThreadViaBeginThreadEx(void * data) 行 106 C
ucrtbased.dll!00007ffbf3f14fb8() 未知
ucrtbased.dll!00007ffbf3f14bf1() 未知
kernel32.dll!00007ffc6c1e7c24() 未知
ntdll.dll!00007ffc6d74d721() 未知
核心阻塞代码
登录后复制
cpp
static int ff_poll_interrupt(struct pollfd *p, nfds_t nfds, int timeout,
AVIOInterruptCB *cb)
{
//#define POLLING_TIME 100 /// Time in milliseconds between interrupt check
//timeout在传递进来之前,会先除以1000
int runs = timeout / POLLING_TIME;
int ret = 0;
do {
if (ff_check_interrupt(cb))
return AVERROR_EXIT;
ret = poll(p, nfds, POLLING_TIME);
if (ret != 0) {
if (ret < 0)
ret = ff_neterrno();
if (ret == AVERROR(EINTR))
continue;
break;
}
} while (timeout <= 0 || runs-- > 0);
if (!ret)
return AVERROR(ETIMEDOUT);
return ret;
}
默认情况下,不设置连接超时,FFmpeg API ff_poll_interrupt函数会循环调用poll函数不断尝试连接服务器。如果服务器处于离线状态,函数avformat_open_input会一直处于阻塞状态。因此需要设置连接超时,将传递参数给到timeout变量,在尝试一定次数以后,退出该循环
接收超时
默认情况下,不设置接收超时,服务器由于某种问题,虽然保持跟客户端的连接,但是没有发送任何的数据过来,函数av_read_frame会一直处于阻塞状态,直到服务器发送数据过来,或者由于连接异常断开,才会正常返回
代码堆栈
登录后复制
cpp
ffplayd.exe!ff_check_interrupt(AVIOInterruptCB * cb) 行 666 C
ffplayd.exe!ff_network_wait_fd_timeout(int fd, int write, __int64 timeout, AVIOInterruptCB * int_cb) 行 84 C
ffplayd.exe!tcp_read(URLContext * h, unsigned char * buf, int size) 行 240 C
ffplayd.exe!retry_transfer_wrapper(URLContext * h, unsigned char * buf, int size, int size_min, int(*)(URLContext *, unsigned char *, int) transfer_func) 行 376 C
ffplayd.exe!ffurl_read_complete(URLContext * h, unsigned char * buf, int size) 行 419 C
ffplayd.exe!ff_rtsp_tcp_read_packet(AVFormatContext * s, RTSPStream * * prtsp_st, unsigned char * buf, int buf_size) 行 771 C
ffplayd.exe!read_packet(AVFormatContext * s, RTSPStream * * rtsp_st, RTSPStream * first_queue_st, __int64 wait_end) 行 2111 C
ffplayd.exe!ff_rtsp_fetch_packet(AVFormatContext * s, AVPacket * pkt) 行 2202 C
ffplayd.exe!rtsp_read_packet(AVFormatContext * s, AVPacket * pkt) 行 879 C
ffplayd.exe!ff_read_packet(AVFormatContext * s, AVPacket * pkt) 行 856 C
ffplayd.exe!read_frame_internal(AVFormatContext * s, AVPacket * pkt) 行 1582 C
ffplayd.exe!av_read_frame(AVFormatContext * s, AVPacket * pkt) 行 1776 C
ffplayd.exe!read_thread(void * arg) 行 3009 C
ffplayd.exe!SDL_RunThread(void * data) 行 283 C
ffplayd.exe!RunThread(void * data) 行 91 C
ffplayd.exe!RunThreadViaBeginThreadEx(void * data) 行 106 C
ucrtbased.dll!00007fff78404fb8() 未知
ucrtbased.dll!00007fff78404bf1() 未知
kernel32.dll!00007fffc9f77c24() 未知
ntdll.dll!00007fffcacad721() 未知
登录后复制
cpp
int ff_network_wait_fd_timeout(int fd, int write, int64_t timeout, AVIOInterruptCB *int_cb)
{
int ret;
int64_t wait_start = 0;
while (1) {
if (ff_check_interrupt(int_cb))
return AVERROR_EXIT;
ret = ff_network_wait_fd(fd, write);
if (ret != AVERROR(EAGAIN))
return ret;
if (timeout > 0) {
if (!wait_start)
wait_start = av_gettime_relative();
else if (av_gettime_relative() - wait_start > timeout)
return AVERROR(ETIMEDOUT);
}
}
}
登录后复制
cpp
static inline int retry_transfer_wrapper(URLContext *h, uint8_t *buf,
int size, int size_min,
int (*transfer_func)(URLContext *h,
uint8_t *buf,
int size))
{
int ret, len;
int fast_retries = 5;
int64_t wait_since = 0;
len = 0;
while (len < size_min) {
if (ff_check_interrupt(&h->interrupt_callback))
return AVERROR_EXIT;
ret = transfer_func(h, buf + len, size - len);
if (ret == AVERROR(EINTR))
continue;
if (h->flags & AVIO_FLAG_NONBLOCK)
return ret;
if (ret == AVERROR(EAGAIN)) {
ret = 0;
if (fast_retries) {
fast_retries--;
} else {
if (h->rw_timeout) {
if (!wait_since)
wait_since = av_gettime_relative();
else if (av_gettime_relative() > wait_since + h->rw_timeout)
return AVERROR(EIO);
}
av_usleep(1000);
}
} else if (ret == AVERROR_EOF)
return (len > 0) ? len : AVERROR_EOF;
else if (ret < 0)
return ret;
if (ret) {
fast_retries = FFMAX(fast_retries, 2);
wait_since = 0;
}
len += ret;
}
return len;
}
上面两个函数都有机会执行interrupt_callback回调函数
原理说明AVFormatContext结构体中interrupt_callback函数会被av_read_frame重复调用,当interrupt_callback返回1的时候,av_read_frame将会立刻返回,退出阻塞状态,当interrupt_callback返回0的时候,av_read_frame会继续等待数据
设置连接超时
libavformat版本小于59
参数设置说明
登录后复制
cpp
const AVOption ff_rtsp_options[]
#if FF_API_OLD_RTSP_OPTIONS
{ "timeout", "set maximum timeout (in seconds) to wait for incoming connections (-1 is infinite, imply flag listen) (deprecated, use listen_timeout)", OFFSET(initial_timeout), AV_OPT_TYPE_INT, {.i64 = -1}, INT_MIN, INT_MAX, DEC },
{ "stimeout", "set timeout (in microseconds) of socket TCP I/O operations", OFFSET(stimeout), AV_OPT_TYPE_INT, {.i64 = 0}, INT_MIN, INT_MAX, DEC },
#else
{ "timeout", "set timeout (in microseconds) of socket TCP I/O operations", OFFSET(stimeout), AV_OPT_TYPE_INT, {.i64 = 0}, INT_MIN, INT_MAX, DEC },
#endif
设置stimeout参数,单位是微秒,10的负六次方秒
libavformat版本大于等于59
参数设置说明
登录后复制
cpp
{ "timeout", "set timeout (in microseconds) of socket I/O operations", OFFSET(stimeout), AV_OPT_TYPE_INT64, {.i64 = 0}, INT_MIN, INT64_MAX, DEC }, COMMON_OPTS(),
设置timeout参数,单位是微秒,10的负六次方秒
例子说明(FFmpeg版本7.0.1)
登录后复制
cpp
AVFormatContext* pAVFormatContext = NULL;
AVDictionary* opts = NULL;
av_dict_set(&opts, "timeout", "1500000", 0);//设置连接超时1.5秒
avformat_open_input(&pAVFormatContext, "rtsp://192.168.18.204:554/h264/ch1/main/av_stream", NULL, &opts);
av_dict_free(&opts);
设置AVFormatContext变量的interrupt_callback中断实现阻塞退出
超时退出阻塞连接和接收例子
登录后复制
cpp
static int InterruptCallback(void* pContext)
{
rtsp* pRTSP = (rtsp*)pContext;
if (nullptr == pRTSP) return 0;
std::int64_t nCurTime = std::chrono::time_point_cast<std::chrono::milliseconds>(std::chrono::system_clock::now()).time_since_epoch().count();
if (nCurTime - pRTSP->m_nReadTimeout >= 1500)
{
std::cout<<"[{0}] 超时1.5秒无流,退出阻塞读取" << std::endl;
return 1;
}
return 0;
}
AVFormatContext* pAVFormatContext = avformat_alloc_context();
pAVFormatContext->interrupt_callback.callback = InterruptCallback;
pAVFormatContext->interrupt_callback.opaque = this;
m_nReadTimeout = std::chrono::time_point_cast<std::chrono::milliseconds>(std::chrono::system_clock::now()).time_since_epoch().count();
avformat_open_input(&pAVFormatContext, strURL.c_str(), NULL, &opts);
接收缓存设置
av_dict_set(&opts, "buffer_size", "1024000", 0); // 设置缓冲区大小
设置过大,会占用内存,例如设置为9000000,会每一个链接占用9M的内存空间,当前1920*1080分辨率设置为1024000
max delay reached. need to consume packet
网络不畅的情况下,等待RTP数据超时
登录后复制
cpp
[rtsp @ 0x7f14117220] max delay reached. need to consume packet
[rtsp @ 0x7f14117220] RTP: missed 4 packets