简介:一份面向Qt5.8开发者的FFmpeg音视频处理集成资源包,解决在Qt框架下配置FFmpeg库并完成编码、解码、转封装等操作的问题。压缩包共186个文件,大小40.9MB,包含116个头文件(h)、23个C源代码文件、16个动态链接库(dll)以及配套的def、a/lib、exe等文件;其中ffmpeglib目录提供include头文件与静态库,dll文件夹存放libavcodec等核心运行时组件,目录结构清晰,便于按需引用。已有400人学习。资源特别包含了音频转码、流处理等参考C代码,并针对Qt Creator的.pro文件配置、子线程处理I/O、QProcess调用ffmpeg命令行等关键场景给出可落地方法,适合需要在Qt5.8中快速集成FFmpeg进行音视频处理的初中级开发者参考。
1. 直接开干:把 FFmpeg 塞进 Qt 5.8 之前先搞清这几件事
做 Qt 5.8 开发的人,早晚会撞上“要在界面里播视频”这个需求,而 Qt 5.8 自带的多媒体组件在格式兼容和帧处理上都很憋屈,于是大家都会想到把 FFmpeg 开发库引进来。真正卡住你的通常不是解码算法,而是 Qt 5.8 的工程配置、FFmpeg 库版本匹配、以及内存释放这三个环节——我在拿到一套 Qt5.8 + FFmpeg 的开发包时,第一件事就是把这几个坑提前踩平。这篇文章适合要在 Qt 5.8 里做视频预览、流媒体拉流、帧截图或自定义渲染的开发者,让你少走弯路,照着配置就能跑起来。
2. 为什么是 FFmpeg + Qt 5.8:库选型与播放链路拆解
2.1 Qt 5.8 里播放视频的三条路:哪条真正适合 FFmpeg
Qt 5.8 自己提供了 QMediaPlayer,走的是系统解码后端,在 Windows 上依赖 DirectShow 那套东西。你用它播个 MP4 封装、H.264 编码的视频问题不大,但一旦碰见 H.265、MKV 封装、多音轨、RTSP 流,或者你想对视频帧做逐帧处理,它就明显不够用了。或者说,你根本拿不到原始解码帧,只能在 QVideoWidget 里干瞪眼。
第二条路是 QML 里的 MediaPlayer,本质上跟第一条路是同一个后端,而且只在 QML 环境里顺手,在纯 Widgets 工程里缝合起来很别扭。第三条路才是本文说的:把 FFmpeg 开发库直接链进 Qt 5.8 工程,自己接管解码、格式转换,再把帧转成 QImage 显示到界面控件。这条路学习成本高一点,但换来的是可控性:H.264、H.265、MPEG-4、VP8、RTSP、本地文件统统能解,而且每一帧的像素数据都握在你手里,后续做截图、滤镜、缩放、分析都方便。
我个人做某跨平台系统的视频预览模块时,就是直接从这套方案下手的。真正的分水岭不在于是不是用 FFmpeg,而在于你愿意接收多少底层细节。先给你打个预防针:FFmpeg 里大量使用 C 语言的内存管理模式,稍不注意就是泄漏和崩溃,这会在后面第 5 章里专门讲。
2.2 解码链路拆开看:AVFormatContext 到 QImage 的完整流转
如果不看代码,先看数据流,很多混乱会提前消解。一个视频文件或网络流进入 FFmpeg 后,你会经历这几层:
- 打开输入源,得到 AVFormatContext,它管着容器格式,比如 MP4、TS、FLV。
- 从上下文里找到视频流,拿到 AVStream,再从 AVStream 的 codecpar 里拿到编码参数。
- 根据编码参数打开解码器 AVCodecContext,H.264 对应解码器就是 libx264 的解码端。
- 循环读取 AVPacket,把压缩数据喂给解码器,通过 avcodec_send_packet 和 avcodec_receive_frame 拿到 AVFrame。
- AVFrame 此时还是 YUV 之类的原始像素格式,而 Qt 的 QImage 需要 RGB 或 RGBA,所以中间必须用 sws_scale 转换格式。
- 把转换后的 RGB 数据包成 QImage,再交给主线程刷新到界面上。
这一条链路上最容易懵的有两处:一是老版本的 FFmpeg 解码 API 是 avcodec_decode_video2 这种同步形式,而新版本 SDK 里已经改成 send/receive 的异步形式,网上大量旧博文用的是旧接口,拿到新库直接编译不过。二是 AVFrame 里拿到的 YUV 数据不能直接画,强制转成 RGB 会让颜色诡异,不转又没法显示。
我拿到这套开发库资源后,先扫了一眼示例里的解码接口风格,就确定它是新式 API 还是兼容老式 API,这样能少走半天弯路。下载到本地后,建议第一件事就是把示例工程里的解码循环部分读一遍,确认跟你手上的 FFmpeg 版本匹配。
2.3 关键对比:FFmpeg 的硬解与软解在 Qt 5.8 下的取舍
很多人一开始就追硬解,觉得硬解性能好。但硬解在 Qt 5.8 这个年代的技术栈里是个大坑。首先,FFmpeg 的硬解方案是模块化的,Windows 上通常是 DXVA2,Linux 上可能是 VDPAU 或者 VAAPI,而且每个驱动版本表现都不一样。我见过某实验室的同事在 Linux 机器上为了启用硬解,折腾驱动和 FFmpeg 编译选项就花了两天,最终还因为显卡太老导致花屏。
参数对比我列一个表:
| 方案 | CPU 占用 | 延迟 | 兼容性 | 实施难度 |
|---|---|---|---|---|
| 软解 | 高 | 稍高 | 最好 | 低 |
| 硬解 DXVA2 | 低 | 较低 | 仅限 Windows | 中 |
| 硬解 VAAPI | 低 | 较低 | 仅限 Linux 特定驱动 | 高 |
| 硬解 CUDA | 低 | 很低 | 仅限 N 卡 | 高(需要额外 SDK) |
软解其实在绝大多数场景下够用:1080p 的 H.264 视频,软解帧率能到 60 帧甚至更高,完全够界面显示。只有当你要同时解码多路流,比如监控墙的 4 画面、9 画面时,软解的 CPU 占用才可能成为瓶颈。我的建议是:第一版先实现软解,把整条链路跑通,再考虑硬解。因为硬解的坑通常不在于解码器本身,而在于把 GPU 上的帧拿回内存后,像素格式转换和线程同步又会引入新的问题。
3. 编译与接入:把 FFmpeg 开发库绑进 Qt 5.8 工程
3.1 拿到 FFmpeg 开发库后第一件事:核对版本位数
网上能下载到的 FFmpeg 开发库有不少打包形式,有的带 include、lib、bin 三个目录,有的只有源码让你自己编译。你在 Qt 5.8 里用,优先找已经编译好的二进制开发包,省下的编译时间够你把示例工程跑两遍。
但拿到手后不能直接往工程里塞,先核对三件事:
- 位数:你的 Qt 5.8 是 32 位还是 64 位,FFmpeg 开发库就必须是同样的位数。常见翻车现场是 Qt 用的是 32 位套件,下载的 FFmpeg 是 64 位,编译链接时要么找不到库,要么运行时崩溃。
- 编译器版本:在 Windows 上,Qt 5.8 的套件分 MSVC 和 MinGW 两套。MSVC 编译的 Qt 工程不能直接链接 MinGW 编译的 FFmpeg 库,反之亦然。尤其注意 lib 目录里是 .lib 还是 .a 后缀,这能快速判断库的生成工具链。
- 依赖顺序:FFmpeg 之间是有依赖层的,链接时 avformat 依赖 avcodec 和 avutil,avcodec 依赖 avutil,swscale 依赖 avutil,顺序错了会报一堆 undefined reference。
我自己常用的核对命令是在 FFmpeg 的 bin 目录里跑一下 ffmpeg -version,看它打印的编译配置里是 “--enable-shared” 还是 “--enable-static”。如果是静态库,链接出来的 exe 很大,但部署简单;如果是动态库,exe 小,但要记得把 bin 目录下的所有 dll 一起拷走。
3.2 工程配置:Pro 文件怎么写才不踩坑
拿到库之后,在 Qt 5.8 的 .pro 文件里做如下配置。假设你的 FFmpeg 开发库放在 D:/sdk/ffmpeg 目录下。
QT += core gui greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = VideoDemo TEMPLATE = app CONFIG += c++11 # FFmpeg 开发库路径,请替换为你的实际目录 FFMPEG_PATH = D:/sdk/ffmpeg INCLUDEPATH += $$FFMPEG_PATH/include LIBS += -L$$FFMPEG_PATH/lib \ -lavformat \ -lavcodec \ -lavutil \ -lswscale \ -lswresample这段配置里有几个关键点。INCLUDEPATH 指向 include 目录,这样 avformat.h、avcodec.h 这些头文件才能被找到,注意 FFmpeg 的头文件是按模块分目录的,比如 libavcodec 子目录里的 avcodec.h,所以 include 路径要路径到 FFmpeg 的根目录,而不是某一层。LIBS 里 -L 指定库目录,-l 指定库名,Linux 下对应 libavformat.so,Windows 下对应 avformat.lib,但 qmake 的写法是一样的。
链接顺序非常重要:avformat 用到了 avcodec 的符号,avcodec 用到了 avutil 的符号,所以被依赖的库要写在后面。如果你把 -lavutil 写在最前面,大概率会遇到未定义符号。另外 Debug 和 Release 共用一个 FFmpeg 库是可以的,FFmpeg 不像 Qt 那样区分 debug 和 release 库,但前提是你下载的开发库本身带了调试符号,否则 Release 下崩了查起来会很痛苦。
配置完 .pro 之后,先编译一个空壳工程试试水:包含 avformat.h,调用一个最简单的函数 av_version_info() 并把返回值打印出来。这一步能快速验证头文件路径和链接路径是否正确,避免后面写了一大堆代码才发现基础环境有问题。
3.3 初始化与释放:代码里最容易崩的两行
FFmpeg 在不同版本里初始化方式不一样。旧版本需要在开头调用 av_register_all(),新版本打开输入源时会自动注册协议和解码器,这行调用已经变成空操作。示例代码里常能看到兼容写法:
#include <QApplication> #include <QDebug> extern "C" { #include <libavformat/avformat.h> #include <libavcodec/avcodec.h> } int main(int argc, char *argv[]) { QApplication a(argc, argv); // 新版本 SDK 下可留可不留,留着兼容旧代码也无害 av_register_all(); avformat_network_init(); qDebug() << "FFmpeg version:" << av_version_info(); return a.exec(); }代码里有两个容易被忽略的细节。第一,C++ 工程里 include FFmpeg 头文件时,必须用 extern "C" 包裹,否则链接阶段会因为 C++ 名字修饰找不到符号,报一堆 undefined reference。第二,avformat_network_init() 只在需要访问网络流时调用,但本地文件也调用不碍事。释放端的习惯也最好从一开始养成:初始化了多少,退出时就逆序释放多少,avformat_network_init 调用后要记得 avformat_network_deinit,av_register_all 在新版本里则没有对应的反注册函数。
这套 Qt5.8 + FFmpeg 开发库资源包里,核心就是把这几个初始化和释放配对都写成标准模板,你直接抄进自己的工程就行。接下来到第 4 章,我会把解码一帧并显示出来这件事写完整。
4. 解码一帧显示一帧:Qt 5.8 下的可复现实战代码
4.1 打开输入源:从本地文件到实时流的两行核心差异
打开输入源的代码不算多,但参数细节很多。本地文件用这组调用:
AVFormatContext *fmtCtx = nullptr; // filename 是 UTF-8 编码的本地文件路径 int ret = avformat_open_input(&fmtCtx, filename, nullptr, nullptr); if (ret < 0) { qDebug() << "无法打开文件:" << ret; return; } ret = avformat_find_stream_info(fmtCtx, nullptr); if (ret < 0) { qDebug() << "找不到流信息"; avformat_close_input(&fmtCtx); return; } // 拿到视频流的索引,-1 表示找不到 int videoStreamIdx = av_find_best_stream(fmtCtx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0); if (videoStreamIdx < 0) { qDebug() << "视频流不存在"; avformat_close_input(&fmtCtx); return; }第一个参数是输出参数,传入指针的指针,函数内部会分配好 AVFormatContext 内存。第二个参数是文件名或 URL。第三个参数用于指定输入格式,比如强制 demuxer,写 nullptr 就是让 FFmpeg 自己根据文件头探测。第四个参数是 AVDictionary 指针,用来传各种选项,本地文件一般用不到。
流媒体场景下,同样是打开输入源,但需要在打开前设置网络选项,最常见的写法是这样:
AVFormatContext *fmtCtx = nullptr; AVDictionary *opt = nullptr; // rtsp 走 tcp 更稳定,不至于因为 udp 丢包花屏 av_dict_set(&opt, "rtsp_transport", "tcp", 0); // 连接超时时间,单位是微秒,3000000 表示 3 秒 av_dict_set(&opt, "stimeout", "3000000", 0); int ret = avformat_open_input(&fmtCtx, url, nullptr, &opt); if (ret < 0) { qDebug() << "无法打开网络流:" << ret; return; } av_dict_free(&opt);rtsp_transport 这个参数的设置经验是:udp 延迟低但容易丢包,tcp 稳定但多一层握手开销。本地局域网内播 RTSP,我一般优先用 tcp,避免画面的花屏和马赛克被误认为解码器问题。stimeout 是个容易被忽略的参数,不设的话,RTSP 地址不可达时程序会在网络层卡死几十秒甚至更久,实际体验就像界面无响应。这两个参数不写,你在本地文件场景看不出来问题,一旦切到网络流就立刻翻车。
4.2 解码循环里做什么:AVPacket 与 AVFrame
打开输入源并拿到视频流索引后,下一步是根据流的编码信息找解码器。解码器上下文从流的 codecpar 里拷贝出来,注意新版本 API 是 avcodec_parameters_to_context,老代码里直接给 codecCtx 赋值的方式已经过时了。
AVCodecParameters *codecPar = fmtCtx->streams[videoStreamIdx]->codecpar; AVCodec *decoder = avcodec_find_decoder(codecPar->codec_id); if (!decoder) { qDebug() << "找不到对应解码器"; avformat_close_input(&fmtCtx); return; } AVCodecContext *decCtx = avcodec_alloc_context3(decoder); avcodec_parameters_to_context(decCtx, codecPar); decCtx->thread_count = 4; int ret = avcodec_open2(decCtx, decoder, nullptr); if (ret < 0) { qDebug() << "解码器打开失败"; return; }thread_count 这个参数值得聊一聊。默认值是 1,即单线程解码。对于 1080p 以上分辨率,特别是 4K 视频,单线程软解很容易成为瓶颈。设成 4 或 8 能让 FFmpeg 内部多线程解码,但也不是越大越好——解码器会拆分帧组并行处理,线程太多时线程切换开销可能反超收益。我一般先设 4,再根据 CPU 核心数微调。另外,thread_count 只是给解码器的帧级并行提供上限,并不意味着每一帧都会被多个线程处理,它主要对 B 帧较多的 H.264 流有效。
解码循环的核心代码:
AVPacket *pkt = av_packet_alloc(); AVFrame *frame = av_frame_alloc(); while (av_read_frame(fmtCtx, pkt) >= 0) { if (pkt->stream_index == videoStreamIdx) { // 把压缩数据送进解码器 int ret = avcodec_send_packet(decCtx, pkt); if (ret < 0) { // 常见情况:数据不完整或解码器内部状态异常 av_packet_unref(pkt); continue; } // 循环取帧,一次 send 可能对应多次 receive while (avcodec_receive_frame(decCtx, frame) == 0) { // 此时 frame 里就是解码后的原始图像数据 // 后续转 QImage 在这里做 processFrame(decCtx, frame); } } av_packet_unref(pkt); } // 冲刷解码器内部的缓冲帧 avcodec_send_packet(decCtx, nullptr); while (avcodec_receive_frame(decCtx, frame) == 0) { processFrame(decCtx, frame); }这段代码有两条必须养成的习惯。
第一,AVPacket 用完必须 av_packet_unref,否则每次循环都会累积引用计数,内存用量肉眼可见地上涨。第二,发完所有包之后,一定要再用 avcodec_send_packet 传一个 nullptr,把解码器内部缓冲的最后几帧冲刷出来,如果不做这一步,视频最后的几帧画面会缺失,处理视频文件时不明显,处理长流时尾部数据看起来就像被“吃掉”了。
avcodec_send_packet 与 avcodec_receive_frame 是一对 API,返回值的处理非常关键。send 返回 AVERROR(EAGAIN) 表示内部队列满了,需要先调用 receive 取走帧再继续 send;receive 返回 AVERROR(EAGAIN) 表示当前没有完整帧可输出,这是正常情况,不是错误。很多新手在这里看到 EAGAIN 就当错误处理,直接中断,导致画面一卡一卡的。
4.3 转成 QImage:像素格式对齐的一个关键坑
解码器输出的 AVFrame,像素格式最常见的是 AV_PIX_FMT_YUV420P。QImage 不认识这个格式,直接包装的话,显示出来是绿油油或者花屏。必须用 sws_scale 做一次色彩空间和像素格式转换:
#include <libswscale/swscale.h> static SwsContext *swsCtx = nullptr; static AVFrame *rgbFrame = nullptr; static uint8_t *rgbBuffer[4] = {nullptr}; static int rgbLinesize[4] = {0}; void ensureSwsContext(AVCodecContext *decCtx) { if (!swsCtx) { swsCtx = sws_getContext( decCtx->width, decCtx->height, decCtx->pix_fmt, decCtx->width, decCtx->height, AV_PIX_FMT_RGB24, SWS_BILINEAR, nullptr, nullptr, nullptr); } if (!rgbFrame) { rgbFrame = av_frame_alloc(); // 分配目标格式的缓冲区 av_image_alloc(rgbBuffer, rgbLinesize, decCtx->width, decCtx->height, AV_PIX_FMT_RGB24, 1); } } void processFrame(AVCodecContext *decCtx, AVFrame *frame) { ensureSwsContext(decCtx); // 执行转换,把 YUV 数据转成 RGB24 sws_scale(swsCtx, frame->data, frame->linesize, 0, decCtx->height, rgbBuffer, rgbLinesize); // 包装成 QImage,注意第三行字节数用 rgbLinesize[0] QImage img(rgbBuffer[0], decCtx->width, decCtx->height, rgbLinesize[0], QImage::Format_RGB888); emit frameReady(img.copy()); }这里有一个非常隐蔽的坑:QImage 的构造函数有多个重载,其中一版会直接引用你给的 buffer 内存而不会拷贝。如果 buffer 是 av_image_alloc 分配的内存,后续帧继续 sws_scale 会把这块内存覆盖,前面发出的 QImage 也会跟着变化。因此在跨线程投递 QImage 时,建议直接 img.copy() 做深拷贝,牺牲一点性能换稳定。如果确认只在单线程内使用 QImage 并且下一帧覆盖前已经绘制完毕,可以不 copy,但线程同步一旦出错,花屏和蓝屏就轮番来。
SWS_BILINEAR 这个缩放算法是质量和性能的折中:比 nearest 平滑,又比 lanczos 快很多。如果你要做的是视频缩略图,用 SWS_FAST_BILINEAR 省 CPU;如果要放大 4K 原图,试试 SWS_LANCZOS,清晰度提升肉眼可辨,但耗时也会增加。
4.4 刷新到界面:信号槽模式 vs 定时器模式
解码和界面显示不能在同一个线程做死循环。QT 主线程要处理事件循环,你在主线程里写一个 while 循环不断读帧,窗口会直接无响应,拖动窗口像在玩 PPT。常见的做法是用 QThread 放解码循环,解码完一帧就通过信号槽把 QImage 丢给主线程。
这里贴一个标准解码线程的骨架:
// 解码工作线程 class DecodeThread : public QThread { Q_OBJECT signals: void frameReady(const QImage &image); void decodeFailed(const QString &message); protected: void run() override { // 4.1 节的打开输入源代码放这里 // 4.2 节的解码循环放这里 // 在 processFrame 中 emit frameReady(img) } }; // 主线程 UI 控件 class VideoWidget : public QLabel { Q_OBJECT public slots: void updateFrame(const QImage &img) { // 显示最近一帧 this->setPixmap(QPixmap::fromImage(img)); } };用信号槽模式的好处是线程安全,QImage 在信号传递过程中由 Qt 的事件队列管理,不会出现双线程同时写显存导致的花屏。但这个模式也有性能死角:信号每帧都发射,如果视频帧率是 60 帧,主线程每秒要处理 60 次 QPixmap 转换,对于大型窗口缩放来说开销不小。
我遇到高帧率情况时,常用定时器模式替代:解码线程里只保留最新的一帧 QImage,不清空队列,主线程用 QTimer 每 40 毫秒去取一次“当前最新帧”并刷新显示。这样做能把绘制次数降到 25 帧每秒,视觉上却几乎不影响流畅度,因为解码线程的帧已经被丢弃了一部分,UI 只显示你最关心的那一帧。两种模式没有绝对优劣,但确认好边界才能选得准:信号槽适合低分辨率、低帧率、帧率稳定的场景;定时器模式适合高帧率、高分辨率、以及解码耗时不稳定的网络流场景。
5. 避坑手册:Qt 5.8 + FFmpeg 最常见的六类翻车现场
做 Qt 5.8 + FFmpeg 集成,最让人头疼的不是功能写不出来,而是莫名其妙地崩、闪、花。下面按我实际踩过的顺序,把这六类高频问题归纳成“现象 → 原因 → 解决”,每条都来自真实调试记录。
5.1 DLL 加载失败:程序起不来,提示找不到 avcodec-*.dll
现象:编译过了,一运行就弹窗说找不到 avformat-xxx.dll 或 avcodec-xxx.dll,程序直接退出。很多人以为是自己工程配置错了,实际上问题出在 FFmpeg 的动态库没被放进 exe 的搜索路径。
原因:FFmpeg 开发库的 bin 目录里有一堆 dll,Qt 5.8 的 windeployqt 工具只会部署 Qt 自己的运行库,不认识 FFmpeg 的 dll。你如果在开发环境里直接跑,Qt Creator 可能通过 PATH 找到了 FFmpeg bin 目录,所以没事;但打包后部署到别的机器,搜不到 dll 立刻崩。
解决:把 FFmpeg 的 bin 目录下所有 dll 复制到 exe 同级目录,或者用 windeployqt 之后手动补拷。我自己的习惯是写一个 deploy.bat,把 Qt 的 windeployqt、FFmpeg 的 dll 拷贝、以及其它第三方库的拷贝统一执行,这样每次构建后跑一遍就能得到完整可运行的目录。注意别只拷 avformat 和 avcodec,swscale、swresample、avutil 一个都不能少,否则启动时一样炸。
5.2 解码后花屏或绿屏
现象:解码能跑通,但画面是花屏、绿屏、或者色彩奇怪到完全不可用。
原因:两种情况最常见。一是你直接把 YUV 数据当 RGB 包装进 QImage,中间跳过了 sws_scale,颜色必然不对。二是 sws_getContext 里源宽高和像素格式与实际帧不一致,尤其是有时候取的是 AVCodecContext 里的宽高,但某些流这个字段可能为 0 或不精确,需要从 codecpar 获取实际值。还有一种是解码后没有处理 frame 的 data 和 linesize,直接认为每行字节数是 width × 像素字节数,忽略了行对齐 padding,导致图像产生斜向撕裂。
解决:先查 sws_getContext 的参数是否与实际帧一致,打印 frame->format、frame->width、frame->height 跟解码上下文对比。如果换流后花屏,很大概率是分辨率和像素格式中途变了(比如 RTSP 码流切换分辨率),需要动态重建 SwsContext。我一般在每帧转换前检查这几个值,一旦变了就释放旧的 swsCtx 重新创建,代价很低但能规避大量看不太懂的显示问题。
5.3 退出时崩溃:程序关闭瞬间崩在未知地址
现象:视频播放一切正常,关窗口时直接崩溃,调用栈飘忽不定,有时在 free,有时在 avcodec_close,有时干脆在 Qt 的事件循环里。
原因:典型的释放顺序错乱。退出时主线程销毁窗口,解码线程还在读帧,两个线程同时访问 AVFormatContext 和 AVCodecContext,自然就崩了。要么是解码线程没停干净就释放了资源,要么是释放顺序跟初始化顺序不一致,FFmpeg 内部对资源依赖顺序很敏感,先关了 avcodec 再关 avformat 是常踩的坑。
解决:严格遵守三步顺序:先通知解码线程退出并等待它结束,再关闭解码器上下文,最后关闭输入上下文。具体到代码上,在析构函数里这样做:
~VideoWidget() { // 1. 先发停止信号,让解码循环退出 m_stop = true; if (m_decodeThread) { m_decodeThread->wait(1000); // 最多等 1 秒 } // 2. 此时才释放解码器资源 if (decCtx) { avcodec_free_context(&decCtx); } // 3. 最后释放输入上下文 if (fmtCtx) { avformat_close_input(&fmtCtx); } }这里有个容易忽略的点:解码线程的 while 循环在 av_read_frame 阻塞时,如果你只是把 m_stop 置 true,线程可能卡在网络读取上无法及时退出。所以网络流场景必须给 avformat_open_input 设置超时参数,并且解码循环里最好用 av_read_frame 的非阻塞变体或者加上 interrupt callback,否则 wait(1000) 会超时,然后你就被迫在线程还活着的时候释放资源,或者干脆确认崩溃。
5.4 内存泄漏:内存占用随播放时长不断增加
现象:播放时间越长,任务管理器里的内存占用越高,换视频文件也不回落。
原因:最常见的是 AVPacket 和 AVFrame 没有正确释放。AVPacket 在 av_read_frame 里会自动分配内部缓冲,需要 av_packet_unref;AVFrame 从解码器拿到的是内部引用,也需要 av_frame_unref。很多新手用完就丢,忘了这两步,内存就像漏水的桶。另外,如果你每次循环都重新调用 av_image_alloc 分配转换缓冲区而不释放,泄漏速度会更快。
解决:养成对称的释放习惯。av_packet_alloc 配 av_packet_free,av_frame_alloc 配 av_frame_free,av_read_frame 之后配 av_packet_unref。凡是调用 xxx_alloc 的地方,都要在合适位置补上 xxx_free。我在自检时会在解码循环里每 300 帧打印一次当前进程的 Rss 内存,如果稳定在某个水平,说明没有明显泄漏;如果一路直线上涨,就回去检查哪个 alloc 没有对应的 free。
5.5 中文文件路径打不开
现象:英文名文件一切正常,中文名文件打开返回负数,打不开或者打开后没有流信息。
原因:Windows 下 FFmpeg 的文件打开依赖本地文件名编码,而 QString 里保存的是 Unicode。直接把 QString 转成 UTF-8 传进去,Windows 的文件系统 API 可能不认识 UTF-8 编码的中文,导致 avformat_open_input 失败。这在 X86 和 MinGW 环境下特别常见。
解决:稳妥的做法是用 QFile 在本地文件名上先做一次复制,再打开临时文件;或者把 QString 转成本地 8 位编码。我自己的做法是简单粗暴:先试 UTF-8,如果打开失败再试 fromLocal8Bit,两者都不行就考虑用 QFile 复制临时文件再解码。不要迷信某一种方案在所有中文环境下都能通,因为 Windows 的系统代码页可能不同。
5.6 网络流卡死或长时间无响应
现象:打开 RTSP 流时,程序卡住十几秒才返回错误,或者在播放过程中突然画面停住、拖拽刷新也没反应。
原因:RTSP 连接建立涉及网络握手和媒体协商,默认超时时间很长。FFmpeg 里影响这个消息的选项比较多,常见有 rtsp_transport、stimeout、rw_timeout。只设置了 stimeout 有时不起作用,因为 stimeout 在部分协议栈中不带底层 I/O 限制,还需要设置 rw_timeout 或者通过 interrupt_callback 做上层超时控制。
解决:我采用的组合拳是:设置 rtsp_transport=tcp 保证传输层稳定,设置 rw_timeout=3000000 限制读写超时,再用 AVDictionary 把 avformat_open_input 的底层超时控制住。如果这样还卡死,就得用自定义 interrupt callback,在回调里定期检查当前时间与开始时间的差值,超过阈值就返回 1 中断阻塞的 IO 操作。这个 callback 是 FFmpeg 留出的官方“后悔药”通道,必须配合解码上下文设置,才能做到真正可中断。
6. 进阶验证:把延迟与同步做成可测量的固化习惯
如果只是把画面播出来,前面几章已经足够。但要交付一套能被别人放心用的东西,还差最后一步:让播放延迟、丢帧行为、音画同步变得可测量、可验证。我自己在完成某跨平台系统的播放模块后,并没有直接提交代码,而是先补上了这三件小事。
第一,用打点日志把每帧耗时量化。在解码循环里包一圈 chrono 计时,统计每一步的耗时。把解码耗时、sws_scale 耗时分别输出到控制台,用均值和中位数判断是否还有性能瓶颈。当 CPU 占用高但画面卡顿时,如果解码耗时不大,真正的瓶颈可能出在 UI 线程的绘制或信号槽频率上,这时通过日志可以快速定位到具体环节,而不是“觉得程序慢”。
第二,给音画同步留一个轻量处理机制。如果你只是做视频预览,对音频不敏感,那不需要完整的同步方案,但至少要知道 FFmpeg 的输出帧里带有 PTS 时间戳。简单做法是以音频时钟为主时钟,视频帧如果落后就丢几帧;或者反过来以视频为主时钟,音频延迟就塞静音。我这里不做全乎,但建议把 PTS 打印出来对照实际播放时间,能帮你判断延迟的根源到底是解码队列堆积,还是网络传输延迟。
第三,自检清单跑一遍。我每次拿到新 FFmpeg 开发库或者升级 Qt 子版本,都会强制走一遍以下流程:编译空壳验证版本位数和链接顺序、播放一段 4K H.264 文件、播放一段 RTSP 网络流、中文文件名视频、退出程序观察是否有崩溃和内存泄漏。这五关跑完,我对这套工程才算真正放心。曾经有一次我升级 FFmpeg 版本后,只改了链接配置就交付给 A同学测试,结果他的机器上没有新版 dll,直接闪退,那一次之后我才把这套自检流程固化下来。
希望这组配置、代码和避坑经验能帮你省下几个晚上的调试时间,也欢迎你把自测中发现的新坑记录到自己的清单里,慢慢变成你自己的工程习惯。
本文还有配套的精品资源,点击获取