ShiftMediaProject项目介绍—ffplay工具RTSP设置连接和接收超时

前言

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