资讯动态

32位Qt MinGW下FFmpeg 3.4预编译库集成与解码避坑

发布时间:2026/10/27 4:36:32 来源:尧图企业网站定制
简介ffmpeg-3.4-mingw32 是面向 32 位 Windows 平台、由 MinGW32 工具链编译的 FFmpeg 3.4 开发包适合使用 Qt 与 MinGW 构建音视频播放、转码或推流功能的 C/C 开发者。它解决的是在不引入 Visual Studio 依赖的前提下快速获得一套可直接链接的编解码库的问题涵盖 libavcodec、libavformat、libavfilter、libswscale、libswresample 等核心模块。压缩包为 7z 格式共 170 个文件体积约 2.62MB107 个 h 头文件与 6 个 dll、6 个 a 导入库、6 个 lib、6 个 def 定义文件共同构成编译与运行所需的完整环境6 个 pc 文件便于通过 pkg-config 配置工程21 个 c 文件中的 transcoding.c、muxing.c、demuxing_decoding.c、transcode_aac.c 等示例可直接作为接口调用范本另有 makefile、readme 与 ffpreset 预设文件辅助构建与参数调试。目前已有 229 人学习下载适合希望把 FFmpeg 嵌入 Qt 桌面应用或需要一份轻量 32 位编译产物做二次开发的读者对照参考。1. 32 位 Qt 工程找不到能用的 FFmpeg一份 MinGW 预编译库的定位老项目维护最难受的不是代码烂是环境配不齐。Qt 的 MinGW 32 位 Kit 摆在那儿业务上要抽视频帧做缩略图一搜 ffmpeg 预编译包十有八九是 64 位 MSVC 版本拖进工程不是链接期报一堆 undefined reference就是编译过了运行崩在 0xc000007b。ffmpeg-3.4-mingw32 这份资源解决的就是这一段它把 ffmpeg 3.4 按 32 位 MinGW 工具链编好include、lib、bin 三件套齐全直接挂到 Qt 的 .pro 里就能编。适合两类人还在维护 32 位 Qt 上位机、需要在老工控机上跑视频解析的以及想避过自己搭交叉编译环境、从零编一份 3.4 的人。2. 拆开目录看结构include、lib、bin 三件套怎么对应拿到一个预编译包第一件事不是往工程里塞是先看清楚它给了什么。32 位 MinGW 版的目录结构和 Linux 下的 FFmpeg 安装目录几乎一致理解了这个对应关系后面配 .pro 的时候就不会瞎猜路径。2.1 三件套与八个库的对应关系把包解开正常会看到三个顶层目录。include 下按库名分子目录lib 下是链接期要用的导入库或静态库bin 下是运行期要用的 DLL。路径内容什么时候用include/libavformat、libavcodec、libavutil、libswscale、libswresample 等公共头文件每个库一个子目录加进 INCLUDEPATHlib/libavformat.dll.a 一类动态链接的导入库LIBS 里 -lavformatlib/libavformat.a 一类静态库如果这个包是静态编的LIBS 同上但要补系统库bin/avformat-57.dll 一类运行时动态库拷到 exe 同目录FFmpeg 不是一个库是八个库拼起来的。Qt 工程里常见只用到五个但得知道谁依赖谁库负责什么本次场景是否必需libavformat容器解析、解复用、读包必需libavcodec编解码器必需libavutil内存、帧、像素格式、时间基等基础工具必需被上面两个依赖libswscale像素格式转换、缩放必需转 QImage 要用libswresample音频重采样做音频才用libavfilter滤镜图一般不用除非要 scale/crop 滤镜链libavdevice摄像头、采集卡采集场景用libpostproc老式后处理基本不用判断这个包是动态还是静态很简单lib 目录里如果是一堆 .dll.a那就是动态版bin 目录里一定有对应 DLL如果只有体积很大的 .a 且没有 DLL那就是静态版链接时要额外补 ws2_32、secur32、bcrypt 之类的系统库。两种包的 .pro 写法差别不小先确认清楚再动手能省掉后面一半的排查时间。提示不确定版本时不要靠记忆直接打开 include/libavutil/avutil.h看 LIBAVUTIL_VERSION_MAJOR 这几个宏比任何下载页描述都准。2.2 为什么这个场景必须锁死 32 位 MinGW有人会问都什么年代了还用 32 位。答案不在技术先进与否在现场设备。老工控机、老一体机、某些专用外设驱动只跑 32 位系统Qt 那套 Kit 又是当年配好的 mingw 32 位重新装一套 64 位 MSVC 环境意味着整个工程的构建脚本、第三方库、部署流程全要重来。改一行环境配置的成本往往比业务代码本身还高。第二层原因是 ABI。MSVC 编出来的 .lib 给 MinGW 链接符号修饰规则不一样直接链不上反过来也一样。所以库的编译器必须和 Qt Kit 的编译器家族对齐MinGW 的库只能配 MinGW 的 Kit位数也必须一致——32 位 Kit 配 32 位库64 位 Kit 配 64 位库混着来就是链接期或运行期的各种玄学。这里有个容易忽略的好消息FFmpeg 是纯 C 写的不依赖 libstdc。这意味着它对 MinGW 的 gcc 版本没那么挑剔只要大版本别差太远通常不会出现编译期过、运行期 C 运行时冲突的问题。这一点比接一个 C 写的第三方库要省心得多也是我倾向于在这种老项目里直接用预编译 FFmpeg 而不是自己重编的原因。当然代价也有纯 C 库意味着它对外暴露的全是 C 结构体没有 RAII所有 AVFrame、AVPacket、AVCodecContext 都要手工释放写错一个就是内存泄漏在 32 位进程里泄漏的后果比 64 位明显得多——32 位用户态地址空间只有 2GB解几段 1080p 就能把堆撑爆。2.3 3.4 的 API 边界哪些函数还能用哪些已经埋雷3.4 是个分水岭前后的版本网上教程新旧混杂照着 4.x、5.x 的文章写代码在 3.4 上大概率编不过。几条必须记牢的边界av_register_all()在 3.4 里依然存在而且需要调用。它从 4.0 开始被标记弃用后面的版本直接删了所以你在新教程里看不到它但在这份库里不调用它某些封装格式会解析失败。解码接口有两条路。老路是avcodec_decode_video2()一个包进一帧出逻辑直观新路是avcodec_send_packet()/avcodec_receive_frame()从 3.1 起就有。3.4 上两条都能走我一般先用老路跑通因为它对解码器内部还有缓存帧需要 flush这件事的处理更符合直觉——传空包进去就能把尾巴吐出来。AVStream::codec这个字段在 3.4 里还在直接用也能跑但它已经是弃用状态。正确做法是avcodec_alloc_context3()配avcodec_parameters_to_context()从codecpar拷参数。这么写的好处是将来换 4.x、5.x 时不用重写这段。音频相关的字段差异更大。3.4 用的是channels加channel_layout两个字段ch_layout这个 AVChannelLayout 结构要到很后面的版本才有。谁要是把新版本的音频代码抄过来编译期就会卡在找不到成员上。另外注意AVFrame::pkt_pts3.4 里它还在用来取当前帧对应的显示时间戳配合best_effort_timestamp一起用比较稳。这个字段在新版本里已经改名换姓属于典型的版本一换就编不过的位置。3. 接进 Qt 工程.pro 写法、链接顺序与 DLL 部署结构看明白了接下来是把它挂进 qmake 工程。这一步的坑集中在两个地方链接顺序以及运行期 DLL 到底该放哪儿。前者决定能不能编过后者决定编过了能不能跑起来。3.1 INCLUDEPATH 与 LIBS 的写法建议单独建一个 ffmpeg.pri用相对路径引用别写死绝对路径——这种老项目经常要在几台机器之间来回拷。第三方库统一放在工程目录下的 3rdparty 里路径就能跟着工程走。# ffmpeg.pri FFMPEG_DIR $$PWD/3rdparty/ffmpeg-3.4-mingw32 INCLUDEPATH $$FFMPEG_DIR/include DEPENDPATH $$FFMPEG_DIR/include # 依赖方在前被依赖方在后 LIBS -L$$FFMPEG_DIR/lib \ -lavformat \ -lavcodec \ -lswresample \ -lswscale \ -lavutil链接顺序不是随便排的。GNU ld 处理静态库和导入库时是单向扫描遇到一个未定义符号它只会去后面列出的库里找。avformat 依赖 avcodecavcodec 依赖 avutil所以 avformat 必须写在 avcodec 前面avutil 必须放最后。顺序反了报错就是一堆 undefined reference toav_...看着像库没链上其实是顺序不对。如果这个包是静态版还要补系统库常见做法是LIBS -lws2_32 -lsecur32 -lbcrypt -lole32 -luuid -lshell32 -lwinmm具体要补哪些取决于编译时的配置报错缺什么补什么别一次全加上加多了反而可能引入符号冲突。工程里再include(ffmpeg.pri)就行。有些团队习惯在 .pro 里用contains(QT_ARCH, i386)判断位数我一般不加这层——位数不匹配的时候链接器会直接告诉你加了判断反而容易掩盖问题。3.2 头文件引入方式与两个必须定义的宏FFmpeg 的头文件本身都带extern C守卫理论上直接 include 就能用。但工程里如果还有别的 C 头跟它混在一个编译单元里包一层更保险也符合大多数人的阅读习惯。// ffmpeg_common.h #ifndef FFMPEG_COMMON_H #define FFMPEG_COMMON_H #define __STDC_CONSTANT_MACROS // 老版本 libstdc 下 INT64_C 一类的宏需要它 #define __STDC_FORMAT_MACROS extern C { #include libavformat/avformat.h #include libavcodec/avcodec.h #include libswscale/swscale.h #include libswresample/swresample.h #include libavutil/imgutils.h #include libavutil/opt.h #include libavutil/time.h } #endif__STDC_CONSTANT_MACROS是历史遗留。FFmpeg 的avutil/common.h里用到了INT64_C、UINT64_C这类宏而某些老版本 libstdc 在 C 模式下会把这些宏屏蔽掉结果就是编译报INT64_C was not declared。现在的 gcc 大多不需要了但加上一行没坏处尤其是在 gcc 版本不明的老 Kit 上。统一走这一个头好处是后面所有源文件都不用再重复写 extern C也方便在换包的时候只改一个地方。3.3 DLL 部署别指望 windeployqtQt 自带的 windeployqt 只认 Qt 自己的库第三方 DLL 它一概不管。这一步翻车特别常见开发机上能跑因为 DLL 在 PATH 里或者就在构建目录旁边拷到现场机器上就不行双击没反应或者弹一个缺 DLL 的对话框。最简单的办法是手工把 bin 下所有 DLL 拷到 exe 同目录。麻烦但不出错。工程多点的话用 qmake 的后置命令自动化win32 { FF_BIN $$shell_path($$FFMPEG_DIR/bin) DEST_DIR $$shell_path($$OUT_PWD/$$DESTDIR) QMAKE_POST_LINK $$quote(cmd /c xcopy /y /q \$$FF_BIN\\*.dll\ \$$DEST_DIR\\\ nul) }$$DESTDIR在不同构建配置下可能是空、debug 或 release所以这里用$$OUT_PWD/$$DESTDIR拼出来而不是直接写死 debug 目录。 nul是为了别让 xcopy 的输出把编译日志刷满。拷 DLL 的时候有个细节debug 和 release 的 exe 目录是两个都要拷。有人只在 debug 下测通过就交付release 包漏了 DLL现场直接打脸。3.4 第一段能跑的验证代码读视频元信息配置完先别急着写解码跑一个最小验证打开文件、打印时长、分辨率、编码器名。这段代码能过说明编译、链接、DLL 部署三关全通了。#include ffmpeg_common.h #include QDebug #include QString bool probeMedia(const QString file) { av_register_all(); // 3.4 必须先注册 AVFormatContext *fmt nullptr; // Windows 下路径必须转 UTF-8中文路径全靠这一句 if (avformat_open_input(fmt, file.toUtf8().constData(), nullptr, nullptr) 0) { qWarning() open failed: file; return false; } if (avformat_find_stream_info(fmt, nullptr) 0) { qWarning() find_stream_info failed; avformat_close_input(fmt); return false; } int vIdx av_find_best_stream(fmt, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0); qDebug() duration(ms): fmt-duration / (AV_TIME_BASE / 1000) bitrate: fmt-bit_rate; if (vIdx 0) { AVStream *st fmt-streams[vIdx]; AVCodecParameters *par st-codecpar; AVCodec *dec avcodec_find_decoder(par-codec_id); qDebug() video: par-width x par-height codec: (dec ? dec-name : unknown) fps: av_q2d(st-avg_frame_rate); } avformat_close_input(fmt); // 传指针的地址函数内部置空 return true; }几个点值得说明。file.toUtf8().constData()这句不是可选项Windows 下avformat_open_input走的是标准 C 文件接口收到的是 UTF-8 字节串QString 直接toLocal8Bit在中文路径上会打不开——这个坑后面还会展开。avformat_close_input(fmt)传的是指针的地址函数会把fmt置成 nullptr所以后面不需要再手动置空。整个 FFmpeg 的释放接口都是这个风格av_frame_free(frame)、av_packet_free(pkt)、avcodec_free_context(ctx)一律传地址。记住了这一点能少写一半的野指针。fmt-duration的单位是 AV_TIME_BASE1000000换算成毫秒要除 1000。有些人直接拿它当秒用结果算出来的时长差三个数量级还以为是库的问题。4. 解一帧出来从 avformat_open_input 到 QImage 的完整链路元信息读通之后真正的活儿是解码。这条链路固定是六步打开输入、找流、开解码器、读包、解码、转格式。每一步都有对应的释放动作漏一个就是泄漏。4.1 打开输入、找流、开解码器前面 probe 那段代码已经走了前三步的一半这里补齐开解码器的部分。关键是用codecpar拷参数而不是直接摸stream-codec。AVFormatContext *fmt nullptr; if (avformat_open_input(fmt, file.toUtf8().constData(), nullptr, nullptr) 0) return false; if (avformat_find_stream_info(fmt, nullptr) 0) { avformat_close_input(fmt); return false; } int vIdx av_find_best_stream(fmt, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0); if (vIdx 0) { avformat_close_input(fmt); return false; } AVCodecContext *dec avcodec_alloc_context3(nullptr); // 先分配空上下文 avcodec_parameters_to_context(dec, fmt-streams[vIdx]-codecpar); // 再灌参数 dec-thread_count 1; // 32 位进程先别开多线程 AVCodec *codec avcodec_find_decoder(dec-codec_id); if (!codec || avcodec_open2(dec, codec, nullptr) 0) { avcodec_free_context(dec); avformat_close_input(fmt); return false; }avcodec_alloc_context3(nullptr)传空指针是合法的后面avcodec_parameters_to_context会把 width、height、pix_fmt、extradata 这些全填上。这比直接dec stream-codec多写两行但换版本的时候不用改。thread_count 1这行在 32 位进程里我一般会显式关掉多线程解码。FFmpeg 的帧级多线程会额外开几个解码上下文每个都带自己的缓冲区1080p 素材下内存占用能翻两三倍。32 位进程只有 2GB 用户态地址空间片段一长就容易bad_alloc。如果确定素材分辨率不高可以放开成 2 或 0自动但要有心理准备。4.2 解码循环与 avcodec_decode_video2 的收尾语义读包和解码是两层循环外层av_read_frame一个个取包内层判断这个包是不是视频流的是就送进解码器。AVPacket *pkt av_packet_alloc(); AVFrame *pic av_frame_alloc(); bool got false; while (!got av_read_frame(fmt, pkt) 0) { if (pkt-stream_index vIdx) { int ret 0; if (avcodec_decode_video2(dec, pic, ret, pkt) 0 ret 0) { got true; // pic 里现在是一帧有效数据 } } av_packet_unref(pkt); // 每次循环都要 unref不能等循环结束 }这里有两个容易写错的地方。第一av_packet_unref(pkt)必须放在循环体内、每次读完就调而不是等整个循环结束再调一次。av_read_frame每次会往 pkt 里填新的缓冲区不 unref 就是一路泄漏。第二avcodec_decode_video2的返回值ret表示这一帧是否完整解出来了可能为 0——因为 B 帧的存在送进去一个包不一定马上出一帧。所以判断条件是ret 0而不是 0。很多人写成 0就拿着一帧空数据去转格式结果要么崩要么出来一张花屏。循环结束后还有一步收尾解码器内部可能还压着几帧没吐出来要传一个空包进去把它挤干净。// flush传 nullptr 作为包反复取直到没有帧产出 while (true) { int ret 0; if (avcodec_decode_video2(dec, pic, ret, nullptr) 0 || ret 0) break; // 这里处理 pic }只有当你要解完整段视频时这步才重要只取首帧的话读完第一帧就可以跳出了。但如果是做抽第 N 帧或者逐帧导出的功能漏了 flush 就会少最后几帧而且这个 bug 很隐蔽——正常视频的尾巴一般是黑场或片尾字幕少几帧不容易被发现。4.3 sws_scale 转 RGB888 并落到 QImage解码出来的AVFrame是 YUV 格式Qt 显示不了必须转成 RGB。这一步用 libswscale。SwsContext *sws nullptr; QByteArray buf; QImage image; sws sws_getCachedContext(sws, pic-width, pic-height, dec-pix_fmt, // 源 pic-width, pic-height, AV_PIX_FMT_RGB24, // 目标 SWS_BILINEAR, nullptr, nullptr, nullptr); int bufSize av_image_get_buffer_size(AV_PIX_FMT_RGB24, pic-width, pic-height, 1); buf.resize(bufSize); uint8_t *dstData[4] { reinterpret_castuint8_t *(buf.data()), nullptr, nullptr, nullptr }; int dstLinesize[4] { pic-width * 3, 0, 0, 0 }; sws_scale(sws, pic-data, pic-linesize, 0, pic-height, dstData, dstLinesize); image QImage(reinterpret_castconst uchar *(buf.constData()), pic-width, pic-height, pic-width * 3, QImage::Format_RGB888).copy(); // copy 不能省sws_getCachedContext和sws_getContext的区别在于前者在参数不变时会复用已有的上下文。逐帧处理时一定要用带 Cached 的版本否则每帧新建一个 SwsContext开销大到离谱而且很容易忘记释放。copy()这一句是重点。QImage 有个构造函数接受外部缓冲区指针但它不接管所有权只是持有一个指针。buf是局部变量函数一返回内存就失效了这时候再 paint 出去要么花屏要么崩。加一个copy()让 QImage 自己深拷一份代价是一次内存拷贝换来的是安全。dstLinesize[0] width * 3是 RGB24 的行字节数因为一个像素三字节且没有行对齐填充。如果目标格式是 RGB32这里就是width * 4。写错了会得到一张斜切的图——这是判断行跨度问题最直观的现象。4.4 关键参数与内存归属对照表写这类代码脑子里要有一张谁分配、谁释放的表不然一定会漏。对象分配释放备注AVFormatContextavformat_open_inputavformat_close_input(fmt)传地址内部置空AVCodecContextavcodec_alloc_context3avcodec_free_context(ctx)传地址AVPacketav_packet_allocav_packet_free(pkt)循环内还要 av_packet_unrefAVFrameav_frame_allocav_frame_free(frame)不持有 data 时很轻SwsContextsws_getCachedContextsws_freeContext用 Cached 版可跨帧复用AVPacket 内部缓冲av_read_frame 自动填av_packet_unref每次循环都要调几个常用字段的取值也要心里有数dec-pix_fmt是解码器实际输出的像素格式用它喂给 sws 而不是用codecpar-format后者只是容器里声明的值实际解码可能不一样pic-linesize[0]是源数据的行跨度可能大于 width绝对不能想当然地按 width 算st-time_base是流的时间基通常是个分数pts 乘它才得到秒。5. 避坑与排查32 位 FFmpeg 在 Qt 里翻车的五个现场下面这五条基本都是我在实际项目里真撞过的按出现什么现象、为什么会这样、怎么解决的顺序写遇到了直接对号入座。5.1 现象链接期一串 undefined reference toav_register_all原因通常是两个库的顺序排错了或者 Kit 的位数和库不匹配。前者的报错里往往能看到某个库里的符号被另一个库引用后者更隐蔽——32 位的 .dll.a 被 64 位链接器读到会直接跳过报的也是一模一样的未定义。解决先把 LIBS 改成-lavformat -lavcodec -lswresample -lswscale -lavutil这个顺序试一遍。还不行就看编译器实际调用的命令行Qt Creator 的编译输出里能看到完整的 g 参数确认-L指向的路径确实是这个 32 位包的 lib 目录而且 Kit 是 MinGW 32-bit。这一步别猜直接看输出。5.2 现象编译通过双击 exe 弹应用程序无法正常启动 0xc000007b原因几乎一定是 32/64 位 DLL 混用。系统里装了 64 位的 FFmpegPATH 里有它的 bin 目录而你的 exe 是 32 位的加载器找到 64 位 DLL 就报这个错。另一种可能是 Qt 的 DLL 版本不对比如用了 64 位 Qt 的 Qt5Core.dll。解决不要依赖 PATH。把这份包 bin 目录下的 DLL 全部拷到 exe 同目录确保同目录下没有其他来源的 FFmpeg DLL。用 Process Explorer 或者 depends 一类工具看实际加载了哪个路径的 DLL比猜快得多。还有个土办法把 exe 拷到一个干净的空目录只放确认要用的 DLL能跑就说明是环境干扰。5.3 现象运行时崩在 avcodec_open2 或者第一次 malloc 就挂原因多半是运行库不一致。静态链接的 FFmpeg 可能用了不同的 CRT 配置跟 Qt 这边的 MinGW 运行时冲突也可能是 32 位地址空间被撑爆只是崩的位置恰好在 malloc 上。解决先确认包是动态版还是静态版。动态版的话bin目录下除了 av*.dll可能还需要一并拷 MinGW 的运行时 DLLlibgcc_s_dw2-1.dll、libwinpthread-1.dll 之类具体看 DLL 的依赖。如果确认是内存问题把thread_count降到 1检查解码循环里有没有漏掉av_packet_unref再拿一个 30 秒的短视频对比测试。5.4 现象QImage 显示出来是花屏、绿屏或者颜色明显偏原因分三种。一是把 YUV 数据直接塞进了 QImage根本没走 sws_scale二是走了 sws 但没copy()缓冲区已经失效三是 dstLinesize 算错了RGB24 写成了 width 而不是 width * 3。解决先打印dec-pix_fmt和pic-width/height/linesize[0]确认源数据正常。然后重点查 dstLinesize这个参数错的表现很有特征——图像会呈现规律的斜向错位而不是全屏噪点。如果是整屏绿色基本是像素格式判断错了比如 source 传的是 AV_PIX_FMT_YUV420P 但实际数据是 NV12。5.5 现象英文路径一切正常中文路径 avformat_open_input 返回失败原因是字符编码。Windows 下avformat_open_input最终走标准 C 的文件打开接口在 MinGW 环境下它按 UTF-8 解释传入的字节串而 QString 转toLocal8Bit得到的是 GBK 字节路径里带中文就对不上。解决统一用file.toUtf8().constData()。如果是通过 QFileDialog 拿到的路径QString 本身就是正确的 Unicode只需要在传给 FFmpeg 前转一次编码。反过来如果要把 FFmpeg 里的元数据字符串比如标题、作者显示到界面上那串是 UTF-8 字节要用QString::fromUtf8()还原别直接QString(...)。6. 时间戳校准让解码出来的帧对上播放时钟只取首帧的场景到上一章就够了但凡是做成播放器或者逐帧分析工具迟早要面对一个问题解出来的帧顺序对时间对不上。表现出来就是画面和声音越走越偏或者拖动进度条之后画面跳一下。根子在时间基。容器里每个流有自己的time_base帧上的 pts 是这个时间基下的整数计数不是毫秒也不是秒。换算必须走av_q2d// 把帧的 pts 换算成秒 double frameSeconds(const AVFrame *f, const AVStream *st) { int64_t ts f-best_effort_timestamp; if (ts AV_NOPTS_VALUE) ts f-pkt_pts; // 3.4 里这个字段还在新版本已换名 if (ts AV_NOPTS_VALUE) return -1.0; // 拿不到时间戳交给上层兜底 return ts * av_q2d(st-time_base); }best_effort_timestamp是 FFmpeg 帮你算过的最可信时间戳优先用它拿不到再退回pkt_pts。两个都没有说明这个容器里确实没写时间信息只能按帧率累加这时候就不要再纠结精度了。有了秒数之后播放时钟我一般用QElapsedTimer自己维护一个而不是跟着系统时间跑QElapsedTimer clock; clock.start(); double baseMedia 0.0; // 播放起点对应的媒体时间 // 每帧算出这一帧应该出现在什么时候 double target baseMedia clock.elapsed() / 1000.0; double frameTs frameSeconds(pic, fmt-streams[vIdx]); if (frameTs target 0.04) { // 快了 40ms 以上等一等 QThread::msleep(static_castunsigned long((frameTs - target) * 1000)); } else if (frameTs target - 0.15) { // 慢了 150ms 以上直接丢帧 continue; }40ms 和 150ms 这两个阈值是可以调的前者决定最多等多一会儿后者决定容忍多大的滞后。窗口太小会频繁 sleep 导致卡顿太大会让音画不同步变得明显。经验值是按一帧的时长取上下界25fps 素材一帧 40ms刚好对上。验证这套逻辑是否靠谱我用一个笨但有效的办法拿同一段素材用外部工具读出的总帧数和时长做基准然后用自己写的循环跑一遍比对三个数——解出来的帧数、首帧时间戳、末帧时间戳。三者对得上说明时间轴处理没问题帧数少了多半是漏了 flush首帧时间戳明显不为零且不是容器起始偏移那是时间基用错了。检查项期望结果不符时的排查方向解码帧数与外部工具读到的帧数一致是否漏了 flush 空包首帧 pts接近流的 start_time时间基是否用错pts 序列单调不减是否用了 dts 而非 pts末帧时间接近容器时长是否中途丢包从那以后我每接一个新的 FFmpeg 预编译包都强制先跑一遍这套自检探元信息、解首帧、数帧数、打时间戳四步走完再写业务代码。这套流程看着笨但它能把位数、链接、DLL、像素格式、时间基这几类问题全部挡在业务逻辑之前省下来的调试时间远超写这几行验证代码的成本。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑