Qt音视频开发08ffmpeg内核优化(极速打开超时回调实时响应)
一、前言
最初编写这套视频解析组件的时候,面对的场景是视频监控行业,对应设备都是网络监控摄像机,传过来的都是rtsp这种视频流,做过这一块的人都知道,打开某个视频流默认耗时比较大,基本上在2s左右,那是因为ffmpeg接口内部读取的最大数据量 formatCtx->probesize(从源文件中读取的最大字节数)值是5000000,导致这里卡很久最耗时,可以调小来加快打开速度。还有一个参数就是从文件中读取的最大时长 formatCtx->max_analyze_duration,改成5个单位即可,5 * AV_TIME_BASE。当然这参数也不是一层不变的,需要根据实际的网络状态好坏来设置,ffmpeg内部接口值很大就是尽量考虑了网络环境很差的情况,所以默认值很大。基本上改了这两个参数以后在局域网中打开1080P的主码流都只要0.5s左右,比之前的2s提升了3倍以上。
除了对打开速度进行特别优化之外,还有一块就是超时回调,毕竟实时的视频流这种,严重依赖网络环境的好坏,一旦网络环境不好,或者网络设备坏了,网线拔了,很容易卡主读取,在用 avformat_open_input 打开视频流的阶段也特别容易卡主很久,windows上默认30s左右,这么久肯定不能接受,所以需要有个机制可以自由控制最大等待时间,设置超时回调 formatCtx->interrupt_callback.callback 就很有必要的。
以前采用过的策略是打开前去主动连接对应的IP地址和端口,通了说明设备在线,这种策略只适用于打开的时候,如果是运行过程中网络环境变坏了或者网线拔了之类的,依然识别不到,不可能开个定时器或者线程去检测,那样就太垃圾了,所以采用超时回调的做法是最好的最万能的。二、效果图
四、相关代码bool FFmpegThread::initInput() { //实例化格式处理上下文 formatCtx = avformat_alloc_context(); //设置超时回调(有些不存在的地址或者网络不好的情况下要卡很久) formatCtx->interrupt_callback.callback = FFmpegHelper::avinterruptCallBackFun; formatCtx->interrupt_callback.opaque = this; //先判断是否是本地摄像头 AVInputFormatx *ifmt = NULL; if (videoType == VideoType_Camera) { #if defined(Q_OS_WIN) //ifmt = av_find_input_format("vfwcap"); ifmt = av_find_input_format("dshow"); #elif defined(Q_OS_LINUX) //可以打开cheese程序查看本地摄像头(如果是在虚拟机中需要设置usb选项3.1) //ifmt = av_find_input_format("v4l2"); ifmt = av_find_input_format("video4linux2"); #elif defined(Q_OS_MAC) ifmt = av_find_input_format("avfoundation"); #endif } //打开输入(通过标志位控制回调那边做超时判断) //其他地方调用 formatCtx->url formatCtx->filename 可以拿到设置的地址(两个变量值一样) tryOpen = true; QByteArray urlData = VideoHelper::getRightUrl(videoType, videoUrl).toUtf8(); int result = avformat_open_input(&formatCtx, urlData.data(), ifmt, &options); tryOpen = false; if (result < 0) { debug("打开出错", "错误: " + FFmpegHelper::getError(result)); return false; } //根据自己项目需要开启下面部分代码加快视频流打开速度 //开启后由于值太小可能会出现部分视频流获取不到分辨率 if (decodeType == DecodeType_Fast2 && videoType == VideoType_Rtsp) { //接口内部读取的最大数据量(从源文件中读取的最大字节数) //默认值5000000导致这里卡很久最耗时(可以调小来加快打开速度) formatCtx->probesize = 50000; //从文件中读取的最大时长(单位为 AV_TIME_BASE units) formatCtx->max_analyze_duration = 5 * AV_TIME_BASE; //内部读取的数据包不放入缓冲区 //formatCtx->flags |= AVFMT_FLAG_NOBUFFER; } //获取流信息 result = avformat_find_stream_info(formatCtx, NULL); if (result < 0) { debug("找流失败", "错误: " + FFmpegHelper::getError(result)); return false; } //解码格式 formatName = formatCtx->iformat->name; //某些格式比如视频流不做音视频同步(响应速度快) if (formatName == "rtsp" || videoUrl.endsWith(".sdp")) { useSync = false; } //设置了最快速度则不启用音视频同步 if (decodeType == DecodeType_Fast2) { useSync = false; } //有些格式不支持硬解码 if (formatName.contains("rm") || formatName.contains("avi") || formatName.contains("webm")) { hardware = "none"; } //本地摄像头设备解码出来的直接就是yuv显示不需要硬解码 if (videoType == VideoType_Camera) { useSync = false; hardware = "none"; } //过低版本不支持硬解码 #if (FFMPEG_VERSION_MAJOR < 3) hardware = "none"; #endif //发送文件时长信号(这里获取到的是秒) duration = formatCtx->duration / AV_TIME_BASE; duration = duration * 1000; if (getIsFile()) { //文件必须要音视频同步 useSync = true; emit receiveDuration(duration); } QString msg = QString("格式: %1 时长: %2 秒 加速: %3").arg(formatName).arg(duration / 1000).arg(hardware); debug("文件信息", msg); return true; } int FFmpegHelper::avinterruptCallBackFun(void *ctx) { #ifdef videoffmpeg FFmpegThread *thread = (FFmpegThread *)ctx; //2021-9-29 增加先判断是否尝试停止线程,有时候不存在的地址反复打开关闭会卡主导致崩溃 //多了这个判断可以立即停止 if (thread->getTryStop()) { thread->debug("主动停止", ""); return 1; } //打开超时判定和读取超时判定 if (thread->getTryOpen()) { //时间差值=当前时间-开始解码的时间(单位微秒) qint64 offset = av_gettime() - thread->getStartTime(); int timeout = thread->getConnectTimeout() * 1000; //没有设定对应值的话限定最小值3秒 timeout = (timeout <= 0 ? 3000000 : timeout); if (offset > timeout) { //thread->debug("打开超时", QString("设置: %1 当前: %2").arg(timeout).arg(offset)); return 1; } } else if (thread->getTryRead()) { //时间差值=当前时间-最后一次读取的时间(单位毫秒) QDateTime now = QDateTime::currentDateTime(); qint64 offset = thread->getLastTime().msecsTo(now); int timeout = thread->getReadTimeout(); //没有设定对应值的话限定最小值3秒 timeout = (timeout <= 0 ? 3000 : timeout); if (offset > timeout) { //thread->debug("读取超时", QString("设置: %1 当前: %2").arg(timeout).arg(offset)); return 1; } } #endif return 0; }