ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Qt 5.8 集成 FFmpeg:Windows 下的环境搭建与解码实战

Qt 5.8 集成 FFmpeg:Windows 下的环境搭建与解码实战 简介面向Qt5.8开发者的FFmpeg集成资源包适合需要在Qt项目中引入音视频编解码、格式转换与流媒体处理能力的开发者。包内包含完整的FFmpeg开发依赖116个头文件与23个C源码示例用于接口调用与二次开发16个DLL及8个lib、8个a分别对应运行时动态库与编译期链接库另有8个def、3个exe及makefile等辅助文件压缩包整体40.9MB目录结构清晰。已有399人学习下载。资源重点展示了动态库路径配置、pro文件中的INCLUDEPATH与LIBS写法并提供了基于QProcess调用可执行程序以及直接使用FFmpeg API两种集成思路可帮助读者快速搭好开发环境、避免版本不兼容等常见坑适用于在Qt5.8下开发播放器、音视频编辑工具或流媒体客户端等场景。 手头有项目是用 Qt 5.8 做 Windows 桌面客户端需要播放 RTSP 视频流和本地音视频文件。一开始想省事直接用 QMediaPlayer结果在 Windows 上折腾一圈解码器支持、播放延迟、格式兼容这些问题接二连三冒出来尤其是“Qt5.8 音乐播放不了”这类问题网上随手一搜一大片。最后老老实实换方案直接用 FFmpeg 开发库自己做解码播放地址一下子通了。这篇文章不做纯理论科普也不贴一堆官方文档翻译就按要求把整套环境搭建、工程配置、代码接入和常见坑位完整写一遍。目标场景是 Windows Qt 5.8MinGW 32 位 FFmpeg 4.x 开发库适合正在做音视频播放器、视频监控客户端、或者想在 Qt 里集成 FFmpeg 但没摸清门路的开发者参考。1. 整体设计思路与选型原因1.1 为什么是 FFmpeg 而不是 Qt 自带模块Qt 的 Qt Multimedia 模块在 Windows 平台上默认走的是系统 DirectShow 后端Qt 5.8 时代还没有切换到 Windows Media Foundation这在播放 mp4、flv 这类常见封装格式时问题不大但一旦涉及 RTSP 取流、自定义协议、特殊编码格式或者需要低延迟播放基本就是当场阵亡。更麻烦的是不同 Windows 系统自带的解码器参差不齐在开发机上能放拿到客户机器上可能直接黑屏无声。FFmpeg 的优势在于解码能力完全自包含不依赖操作系统解码器H.264、H.265、AAC、MP3 这些主流的编解码格式全覆盖。而且 FFmpeg 的库结构非常清晰avformat 管封装解析、avcodec 管编解码、swscale 管像素格式转换、swresample 管音频重采样使用起来各司其职也方便按需裁剪。1.2 Qt 5.8 版本的特殊性Qt 5.8 是 2017 年初发布的 LTS 版本虽然现在看版本有点老但很多工业控制器、嵌入式工控机厂商至今仍在使用基于它二次开发的定制库所以它依然有很庞大的存量用户。Qt 5.8 官方提供的 Windows 安装包分为 MSVC 和 MinGW 两套工具链。这里要特别说明我最终选择了 MinGW 32 位工具链而不是 MSVC原因有三FFmpeg 官方发布的 Windows 版本中有专门针对 MinGW 编译器编译的 dev 包导入库格式直接用就行不用像 MSVC 那样还要自己生成 .lib。MinGW 工具链配置相对简单qmake 一个 .pro 文件搞定不用额外维护 Visual Studio 的解决方案。很多嵌入式平台交叉编译链本身就是 GCC 系统一用 MinGW 可以最大程度复用代码和构建脚本。1.3 架构规划整个集成方案按如下层次划分解码层调用 FFmpeg 核心库负责打开输入、读取帧、解码音视频数据。转换层用 swscale 做视频像素格式与分辨率转换用 swresample 做音频采样格式与采样率转换。显示/播放层视频推送到 QLabel 或 QOpenGLWidget 渲染显示音频输出到 QAudioOutput 播放。业务层封装播放控制接口打开、播放、暂停、停止、跳转对上层提供简洁的 Qt 风格 API。这个分层的好处是FFmpeg 的相关操作全部集中在解码层和转换层后期如果换版本或做硬件解码只需要替换底层实现不需要动 UI 部分。2. FFmpeg 开发库准备与目录结构2.1 下载 FFmpeg 开发库先明确一个概念FFmpeg 官方发布包分三个版本Shared 版包含 exe、dll 以及对应导入库MinGW 下是 .dll.a适合开发时调试和动态发布。Dev 版仅包含头文件和导入库/静态库文件不包含 exe 和 dll适合自己编译静态链接的场景。Full 版Shared 版 Dev 版 额外工具集最省心。我第一次做集成时直接下载了 Full 版反正体积大不了多少头文件、导入库、dll、exe 全都齐了。需要特别注意的是编译器版本匹配在 FFmpeg 下载页要选择 win32 架构下 GCC 编译的版本通常是 win32 和 win64 下的 gpl 共享版本而不是选择 msvc 版本。选错的话MinGW 的链接器无法直接识别 MSVC 生成的 .lib 导入库。解压后建立的目录结构尽量规范我习惯统一放到一个第三方库目录里例如 D:\ThirdParty\三个文件夹分别是 include、lib、bin内容如下D:\ThirdParty\FFmpeg\ ├── include\ │ ├── libavcodec\ │ ├── libavformat\ │ ├── libavutil\ │ ├── libswscale\ │ └── libswresample\ ├── lib\ │ ├── libavcodec.dll.a │ ├── libavformat.dll.a │ ├── libavutil.dll.a │ ├── libswscale.dll.a │ └── libswresample.dll.a └── bin\ ├── avcodec-58.dll ├── avformat-58.dll ├── avutil-56.dll ├── swscale-5.dll └── swresample-3.dll2.2 32 位还是 64 位如果你的 Qt 安装的是 MinGW 32 位版本那 FFmpeg 也必须选 32 位版本win32否则链接时会出现无法解析的符号错误甚至直接报“file format not recognized”。同理如果 Qt 是 MinGW 64 位就选 win64 版本。这里有个容易踩的坑很多开发者电脑上同时装了 32 位和 64 位的 Qt环境变量里的 PATH 如果先指向了 64 位目录而项目实际用的 qmake 是 32 位版本构建时会报一堆莫名其妙的错误。稳妥做法是在 Qt Creator 里指定确定版本的 Qt Kit命令行构建时也显式调用对应路径下的 qmake.exe。2.3 PATH 环境变量的配置虽然发布程序时会把 FFmpeg 的 dll 文件拷贝到 exe 同目录但开发调试阶段建议把 FFmpeg 的 bin 目录加入系统 PATH 环境变量这样程序在 Qt Creator 里直接运行时能快速找到 dll省去每次手动拷贝的麻烦。加入 PATH 之后可以在命令行执行以下命令验证是否配置成功ffmpeg -version能正常打印版本号说明 bin 目录生效了。这一步不是必须的但能节省开发期间的不少时间。3. Qt 5.8 工程配置与链接细节3.1 .pro 文件中的配置Qt 工程文件是核心配置入口。以我的项目为例核心配置如下QT core gui greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET FfmpegDemo TEMPLATE app CONFIG c11 # FFmpeg 路径 FFMPEG_HOME D:/ThirdParty/FFmpeg INCLUDEPATH $$FFMPEG_HOME/include LIBS -L$$FFMPEG_HOME/lib \ -lavcodec \ -lavformat \ -lavutil \ -lswscale \ -lswresample这里有个关键点MinGW 下链接 FFmpeg 导入库时不需要写完整的 .dll.a 后缀直接写库名的核心部分就行比如-lavcodec会自动去找 libavcodec.dll.a。不要画蛇添足写成-llibavcodec那是 MSVC 的写法。3.2 extern C 的必要性FFmpeg 是用 C 语言写的在 C 工程里包含头文件时必须用 extern C 包裹否则链接阶段会全部报未定义引用。标准写法extern C { #include libavcodec/avcodec.h #include libavformat/avformat.h #include libavutil/avutil.h #include libswscale/swscale.h #include libswresample/swresample.h }Qt 5.8 默认的 C 编译器如果直接#include这些头文件不去做 extern C 保护编译能过链接必挂。这也算新手最容易踩的第一个坑。3.3 运行时 dll 拷贝策略开发调试阶段可以用 PATH 环境变量临时解决 dll 搜索问题但发布程序时一定要把 FFmpeg bin 目录下的 dll 文件全部拷贝到 exe 同目录。比较稳妥的做法是写一个脚本构建完成后自动执行copy D:\ThirdParty\FFmpeg\bin\*.dll %~dp0output\debug\ copy D:\ThirdParty\FFmpeg\bin\*.dll %~dp0output\release\或者用 Qt Creator 的构建步骤里加一个自定义命令。不要想着用 QLibrary::load 在程序里动态加载虽然能行但每次要手动管理加载顺序而且 Qt 5.8 里开发库的版本号随 FFmpeg 版本变化较大硬编码 dll 名会非常脆弱调试起来也麻烦。另外值得强调的是Qt 自带的 windeployqt 工具只能部署 Qt 自身的运行库不会帮你处理 FFmpeg 的 dll所以这一步不能省。3.4 头文件包含的路径细节开发库头文件里有#include libavcodec/avcodec.h这种写法而 FFmpeg 源码头文件内部还会通过相对路径去引用公共头文件。因此 INCLUDEPATH 只需要指定到 include 这一层目录就够了不要写成 include/libavcodec否则里面的相对路径引用会全部失效。4. 核心代码实现与调用流程4.1 版本信息验证环境配置完成后第一个代码验证是打印 FFmpeg 版本信息确认库可以正常链接和调用#include QDebug #include QApplication extern C { #include libavcodec/avcodec.h #include libavformat/avformat.h #include libavutil/avutil.h } int main(int argc, char *argv[]) { QApplication a(argc, argv); qDebug() FFmpeg Version: av_version_info(); qDebug() avcodec license: avcodec_license(); qDebug() avformat version: avformat_version(); qDebug() avutil version: avutil_version(); return 0; }编译运行后如果 Debug 窗口能看到版本字符串说明头文件引入、库路径、导入库链接全都正常接下来的开发就在这个基础上逐步扩展。4.2 视频解码流程的最小实现接下来展示一段最小可运行的视频解码逻辑输入一个视频文件路径逐帧解码并打印每帧信息。这个流程是整个播放器的骨架后续所有功能都从这里展开。#include QFileDialog #include QDebug extern C { #include libavformat/avformat.h #include libavcodec/avcodec.h #include libswscale/swscale.h #include libavutil/imgutils.h } void decodeVideo(const QString filePath) { AVFormatContext *fmtCtx nullptr; int ret avformat_open_input(fmtCtx, filePath.toStdString().c_str(), nullptr, nullptr); if (ret 0) { qDebug() open input failed: ret; return; } // 查找流信息 if (avformat_find_stream_info(fmtCtx, nullptr) 0) { qDebug() find stream info failed; avformat_close_input(fmtCtx); return; } // 找到视频流 int videoStreamIndex -1; for (unsigned int i 0; i fmtCtx-nb_streams; i) { if (fmtCtx-streams[i]-codecpar-codec_type AVMEDIA_TYPE_VIDEO) { videoStreamIndex static_castint(i); break; } } if (videoStreamIndex -1) { qDebug() no video stream found; avformat_close_input(fmtCtx); return; } // 获取解码器 AVCodecParameters *codecPar fmtCtx-streams[videoStreamIndex]-codecpar; const AVCodec *decoder avcodec_find_decoder(codecPar-codec_id); if (!decoder) { qDebug() decoder not found; avformat_close_input(fmtCtx); return; } // 打开解码器 AVCodecContext *codecCtx avcodec_alloc_context3(decoder); avcodec_parameters_to_context(codecCtx, codecPar); if (avcodec_open2(codecCtx, decoder, nullptr) 0) { qDebug() open decoder failed; avcodec_free_context(codecCtx); avformat_close_input(fmtCtx); return; } // 逐帧解码 AVPacket pkt; AVFrame *frame av_frame_alloc(); av_init_packet(pkt); pkt.data nullptr; pkt.size 0; while (av_read_frame(fmtCtx, pkt) 0) { if (pkt.stream_index videoStreamIndex) { ret avcodec_send_packet(codecCtx, pkt); if (ret 0) { while (ret 0) { ret avcodec_receive_frame(codecCtx, frame); if (ret 0) { qDebug() Decoded frame, width: codecCtx-width height: codecCtx-height; } } } } av_packet_unref(pkt); } // 资源清理 av_frame_free(frame); avcodec_free_context(codecCtx); avformat_close_input(fmtCtx); }这段代码在整个项目里算“发起点”级别的代码用了版本兼容性较好的 API。如果用的是 FFmpeg 3.x 及以下版本AVCodecContext 的创建方式会有所不同但 FFmpeg 4.x 之后avcodec_send_packet/avcodec_receive_frame这种新接口是标准做法建议直接按新接口写。4.3 与 Qt 界面的结合解码线程和渲染线程不建议混在同一线程里否则网络流缓冲不够或解码速度跟不上时界面会卡死。我习惯的做法是解码在 QThread 里跑解码出来的视频帧转成 QImage 后通过信号槽发送到主线程再在 QLabel 上显示。帧转换为 QImage 的代码片段使用 swscaleSwsContext *swsCtx sws_getContext( frame-width, frame-height, (AVPixelFormat)frame-format, frame-width, frame-height, AV_PIX_FMT_RGB24, SWS_BILINEAR, nullptr, nullptr, nullptr); QImage img(frame-width, frame-height, QImage::Format_RGB888); uint8_t *dstData[4] { img.bits(), nullptr, nullptr, nullptr }; int dstLinesize[4] { img.bytesPerLine(), 0, 0, 0 }; sws_scale(swsCtx, frame-data, frame-linesize, 0, frame-height, dstData, dstLinesize); sws_freeContext(swsCtx);这里有个细节QImage::Format_RGB888 在内存中和 FFmpeg 的 RGB24 存储布局一致但由于 QImage 的行字节数可能不等于 width * 3所以在传给 sws_scale 时目标行字节数必须用 img.bytesPerLine() 而不是 width * 3否则图像会错位。音频的播放流程类似解码出的 AVFrame 通过 swresample 重采样转成 QAudioOutput 需要的 PCM 格式然后写入音频输出设备。Qt 5.8 的 QAudioOutput 用起来不算复杂但音频数据缓冲策略需要花心思写得太简单容易出现爆音或延迟。如果项目以视频监控为主可以先不管音频把画面传输稳定了再加音频模块。5. 常见问题与排查技巧实录我在集成过程中和帮同事排查问题时收集了下面这些高频问题按频率从高到低排列问题现象根本原因解决方案编译报错undefined reference toavformat_open_input头文件未包 extern C或链接库顺序不对检查 extern C 包裹FFmpeg 库的链接顺序调整通常 avformat 放最前avcodec 和 avutil 放后面编译报错file not recognized: File format not recognized库文件架构不匹配32 位 MinGW 链接了 64 位库重新下载 win32 版本的 FFmpeg dev 包程序启动提示找不到 avcodec-58.dll开发环境下 dll 路径未包含或发布时未拷贝开发时配置 PATH 环境变量发布时手动拷贝 dll 到 exe 目录能编译但运行时解码失败 ret -1094995529解码器未找到或输入格式不受支持用 av_strerror 把错误码转成字符串确认 FFmpeg 是 full/gpl 版本而不是精简版视频黑屏但音频正常sws_scale 参数传错或 QImage 像素格式设置不对检查 AVPixelFormat 对应关系先用固定分辨率测试确认基础流程再处理尺寸自适应打开 RTSP 流卡住很长时间没有设置超时参数TCP/UDP 切换问题avformat_open_input 的 options 参数里设置 stimeout 和 rtsp_transport如下代码RTSP 流超时设置参考AVDictionary *opts nullptr; av_dict_set(opts, rtsp_transport, tcp, 0); av_dict_set(opts, stimeout, 3000000, 0); // 单位微秒3秒 avformat_open_input(fmtCtx, url.toStdString().c_str(), nullptr, opts);另外还有几个容易忽略的小问题在 Qt 5.8 下使用 FFmpeg 4.x 时av_register_all() 这个方法已经被标记为废弃不需要调用。不要看到网上老教程写了就在新代码里照抄既不会报错也没什么用纯属多余操作。Debug 版程序如果信息输出闪烁特别快在耗时较长的解码循环里适当加 sleep 或帧率控制不然会把 CPU 吃满。这在播放本地高帧率视频时尤其明显。还有一次遇到出诡异问题程序在开发机上正常拷贝到另一台电脑直接崩溃直指 FFmpeg 内部。后来查出来是两台的显卡驱动版本差异Qt 渲染部分用了不兼容的绘制路径。建议视频显示这块如果条件允许尽量用 QOpenGLWidget 替代 QLabel性能和兼容性会好很多。但这也引出一个新问题QOpenGLWidget 的跨线程 QoS 处理要仔细想清楚我现在用的方案是解码线程直接生成 QImage 投递到主线程显示虽然多了一次内存拷贝但在 1080P 分辨率下测试 CPU 占用率在接受范围内换来的是代码简单稳定。6. 经验总结与后续扩展方向拿 Qt 5.8 接 FFmpeg 开发库这件事整个链路打通不难但每一步都有讲究。版本匹配是第一关卡编译器、位数、库版本只要错一个后续调试成本直线上升工程配置是第二关卡头文件路径、库链接写法、dll 部署方式这些看起来琐碎却能卡住人一整天最后才是代码组织层面的问题。我在实际开发中还摸索出了一个稳妥的后备方案入门验证阶段别急着上 UI先写一个纯控制台程序把解码流程跑通确认 FFmpeg 库本身没问题再往 Qt 工程里迁移。这样即使出问题排查范围也会很小不会牵扯进 Qt 界面线程的干扰项。关于后续能力扩展基于这套基础可以继续做FFmpeg 命令行的封装调用比如 m4s 转 mp4、破损 AVI 文件的修复场景、自定义拉流程序、视频转码工具、甚至配合网络协议把裸流数据推送到流媒体服务端。我目前正在做的方向是基于这套播放内核封装一个支持多路视频同时解码显示的模块核心思路是不改动上面提到的解码层代码只在上层加一个采集调度管理器。这套方案的扩展性还算不错有类似需求的话可以参考同样的分层思路。本文还有配套的精品资源点击获取
返回列表