
简介面向Qt5.8开发者的FFmpeg集成资源包适用于需要实现音视频编解码、格式转换与滤镜处理的项目。压缩包共186个文件包含116个C头文件、23个C源文件、16个DLL动态库、8个静态库.a/.lib、8个DEF导出文件及3个可执行工具。头文件负责提供API声明静态库用于编译期链接动态库确保运行时调用FFmpeg的libavcodec、libavformat等核心组件示例源文件transcode_aac.c与transcoding.c演示了音频提取和转码的完整流程。资源包总大小40.9MB已有400人学习下载。借助该资源可快速完成Qt5.8项目的.pro配置避开自行编译FFmpeg的繁琐步骤直接上手开发跨平台音视频应用附带的readme与makefile还能辅助搭建构建环境、排查库版本与链接错误。1. Qt5.8嵌进FFmpeg开发库先让最小用例跑起来Qt5.8这些年被吐槽最多的是它的C标准支持停在半中间但把FFmpeg开发库塞进Qt工程这件事其实没有想象中复杂。我拆过的类似资源包里真正让新手卡住的往往不是解码算法而是第一步的环境匹配——MinGW还是MSVC、动态库还是静态库、头文件和导入库该往哪儿放。只要这三件事对了后面从打开视频到弹出第一帧基本就一路绿灯。这篇笔记从qmake配置讲起覆盖解码、显示、音频和五个我实际踩过的坑。适合正在把FFmpeg集成进Qt 5.8老项目、想先跑通最小用例的人。2. 先配环境再写代码qmake、FFmpeg包与链接器之间的三角关系2.1 编译器匹配MinGW包别塞进MSVC工程Qt 5.8的安装包分成MinGW版和MSVC版这决定了你后续下载哪个FFmpeg构建。很多人看网上帖子直接下了一个ffmpeg-4.4-full_build-shared结果放到MSVC工程里链接报一排“unresolved external symbol”或者运行时直接弹0xc000007b。原因就一句话导入库和导入表是按不同ABI生成的MinGW的.a导入库没法被MSVC的link.exe消费反过来也一样。所以下载FFmpeg时先看两件事编译器线和位数。Dev包里有include头文件Shared包里有bin下的dll和lib下的导入库把两个包解压合并到一个3rd/ffmpeg目录下是最省心的做法。如果你只是做播放器不需要自己编译FFmpeg源码直接用预编译的shared包就行。2.2 .pro四行配置与dll自拷技巧qmake工程里接入FFmpeg核心就四件事告诉编译器头文件在哪、链接库文件叫什么、构建完了把dll拷到运行目录、别让中文路径参与链接。下面这段.pro文件我用了很多次Qt 5.8可以直接编译不用改任何变量。# QtFFmpegDemo.pro QT core gui greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET QtFFmpegDemo TEMPLATE app CONFIG c11 # FFmpeg 解压后的目录需要包含 include/lib/bin 三个子目录 FFMPEG_PREFIX $$PWD/3rd/ffmpeg INCLUDEPATH $$FFMPEG_PREFIX/include LIBS -L$$FFMPEG_PREFIX/lib LIBS -lavformat -lavcodec -lavutil -lswscale -lswresample win32 { CONFIG(debug, debug|release) { DLL_DEST $$OUT_PWD/debug } else { DLL_DEST $$OUT_PWD/release } QMAKE_POST_LINK $$QMAKE_COPY_DIR $$shell_path($$FFMPEG_PREFIX/bin) $$shell_path($$DLL_DEST) }这里有个细节容易被忽略链接库的排列顺序。MinGW的ld在解析符号时是单遍扫描-lavformat依赖-lavcodec而-lavcodec又依赖-lavutil和-lswresample所以被依赖的库必须放在后面。如果你把-lavutil写到最前面链接时偶尔会报一堆莫名其妙的undefine查半天才发现是顺序问题。QMAKE_POST_LINK那段是把bin目录下所有dll复制到输出目录。注意$$shell_path会把路径转成Windows认识的斜杠格式这在Qt 5.8上很关键否则路径里的/偶尔会让cmd卡住。2.3 验证库能用跑一个只打开文件的最小用例环境配好先别写界面。我习惯先做一个纯控制台用例只调avformat_open_input打开一个视频文件。这一步能同时验证头文件、导入库、dll运行时依赖三条链路通不通排查成本是最低的。#include QCoreApplication #include QDebug extern C { #include libavformat/avformat.h } int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); const char *file test.mp4; AVFormatContext *fmt nullptr; int ret avformat_open_input(fmt, file, nullptr, nullptr); if (ret 0) { qDebug() open failed, ret ret; return 1; } qDebug() streams: fmt-nb_streams; avformat_close_input(fmt); return 0; }如果这一步运行后能看到streams: 2这样的输出说明头文件路径、导入库名称、dll运行时加载三件事全部正常。如果报error while loading shared libraries去检查输出目录里avformat-*.dll在不在如果报LNK2019去检查lib目录里导入库的名称和你LIBS 里写的是否一致。这个最小用例会占用你整个项目最初的十分钟但它能省掉后面排查环境问题的几个小时。3. 解码主流程从avformat到AVFrame要过的三关3.1 解码放线程QThread与信号里传QImage的约定把解码逻辑直接写进QWidget构造函数或按钮槽函数里是新手最常见的做法。它的问题不是不能跑而是视频文件一旦有点大、或者网络流卡顿界面会直接冻结因为你把av_read_frame这种阻塞操作放到UI线程里了。Qt的UI线程要做事件循环一个几十毫秒的阻塞都可能被用户感知成卡顿。正确姿势是让解码跑在独立线程里。这里我推荐QThread加信号抛帧而不是继承QThread然后重写run。虽然重写run也能跑但跨线程传数据时信号槽才是Qt保证线程安全的正统手段。下面这个类结构是我验证过的写法class DecodeThread : public QThread { Q_OBJECT public: explicit DecodeThread(QObject *parent nullptr); void setInputFile(const QString file); void stopDecode() { m_stop true; } signals: void frameReady(QImage image); void audioSamples(QByteArray pcm); void errorOccurred(const QString msg); protected: void run() override; private: QString m_file; volatile bool m_stop false; };信号里不要传指针要传QImage值。QImage是隐式共享的按值传递在队列连接里跨线程是安全的而传指针容易在UI线程还没用完时被解码线程释放掉出现悬空指针。frameReady(QImage image)这种写法Qt内部会做一次深拷贝到事件队列吗不一定但QImage自己管理内存的分片不错只要你在emit之后不要再碰这个QImage对象就行。3.2 open_input、find_stream_info、open2三行初始化背后各有参数解码线程的第一步是初始化FFmpeg全局环境并打开文件。把下面代码放进run()的开头avformat_network_init(); AVFormatContext *fmtCtx nullptr; const char *file m_file.toLocal8Bit().constData(); if (avformat_open_input(fmtCtx, file, nullptr, nullptr) ! 0) { emit errorOccurred(QStringLiteral(打不开文件)); return; } if (avformat_find_stream_info(fmtCtx, nullptr) 0) { avformat_close_input(fmtCtx); emit errorOccurred(QStringLiteral(找不到流信息)); return; }avformat_open_input的第三个参数为nullptr时FFmpeg会根据文件头自动探测格式。如果打开的是RTSP或HTTP流这里默认会阻塞直到连接成功或超时我在第5章说假死时会展开。第四个参数是AVDictionary的地址可以传字典定制参数比如设置stimeout控制网络读超时。avformat_find_stream_info不是可选项它会把流的基础信息读出来比如分辨率、编码器ID、帧率。你后续建解码器全靠它填充的codecpar。这两步做完就该定位视频流和音频流的索引了int videoIndex -1; int audioIndex -1; for (unsigned i 0; i fmtCtx-nb_streams; i) { AVCodecParameters *par fmtCtx-streams[i]-codecpar; if (par-codec_type AVMEDIA_TYPE_VIDEO videoIndex 0) { videoIndex static_castint(i); } else if (par-codec_type AVMEDIA_TYPE_AUDIO audioIndex 0) { audioIndex static_castint(i); } } if (videoIndex 0 audioIndex 0) { avformat_close_input(fmtCtx); emit errorOccurred(QStringLiteral(没有视频也没有音频)); return; }这里我建议用AVCodecParameters而非直接操作AVCodecContext因为codecpar是封装层暴露的参数标准接口不跟具体解码器实例耦合。你要拿编码器ID就访问par-codec_id要拿宽高就访问par-width、par-height。接下来按索引建解码上下文。注意旧代码里经常出现的avcodec_decode_video2在新版本已废弃你下载的FFmpeg如果是3.x之后的版本用send/receive这组API更稳AVCodec *codec avcodec_find_decoder( fmtCtx-streams[videoIndex]-codecpar-codec_id); if (!codec) { emit errorOccurred(QStringLiteral(找不到视频解码器)); return; } AVCodecContext *vctx avcodec_alloc_context3(codec); avcodec_parameters_to_context(vctx, fmtCtx-streams[videoIndex]-codecpar); vctx-thread_count 2; if (avcodec_open2(vctx, codec, nullptr) 0) { emit errorOccurred(QStringLiteral(打开视频解码器失败)); avcodec_free_context(vctx); avformat_close_input(fmtCtx); return; }thread_count设成2或4能让部分解码器走多线程解码。对1080p视频设成4通常比设成1快20%左右但如果你的目标平台CPU核心本来就少或者你同时跑好几路视频设成2更稳。音频解码器同理在后面第4章复用这套逻辑只是codec_type换成音频。3.3 读帧循环与send/receive的正确顺序解码循环是播放器的心脏。先看代码结构再看循环里每次要做什么AVPacket packet; AVFrame *frame av_frame_alloc(); while (!m_stop) { int ret av_read_frame(fmtCtx, packet); if (ret AVERROR_EOF) { break; } if (ret 0) { av_packet_unref(packet); continue; } if (packet.stream_index videoIndex) { int sendRet avcodec_send_packet(vctx, packet); if (sendRet 0) { while (avcodec_receive_frame(vctx, frame) 0) { QImage img convertToRgb(frame, vctx); if (!img.isNull()) { emit frameReady(img); } av_frame_unref(frame); } } } else if (packet.stream_index audioIndex) { // 音频处理见第4章 } av_packet_unref(packet); } av_frame_free(frame);av_read_frame每次返回一个AVPacket注意它返回的packet可能不属于你想要的流比如存在字幕流、数据流。在拿到packet后先看stream_index对不对再丢进解码器这个判断避免了无谓的解码开销。avcodec_send_packet与avcodec_receive_frame的关系要理解对send负责把压缩数据送进解码器内部的输入队列receive负责把解码出来的原始帧取走。send一次receive可能有好几帧B帧存在时尤其明显所以receive要用while循环取干净。这句av_frame_unref(frame)不能省。avcodec_receive_frame返回的frame内部是有引用计数的如果你不管它下一轮receive会直接在同一个frame上覆盖写先前的data还是指向旧缓冲区最终导致内存泄漏。每次取完帧unref一次把引用计数归还给解码器。4. 显示与出声让FFmpeg的帧长成Qt的控件4.1 AVFrame转QImagesws_scale与stride对齐FFmpeg解出来的原始视频帧通常是YUV420P而Qt的QImage不支持YUV你必须先转成RGB格式。负责转换的是libswscale调用sws_scale完成颜色空间转换和缩放。这里有一个多数人第一次都会踩的坑目标缓冲区的行字节数必须用QImage::bytesPerLine()告诉sws不能直接写width*3。因为QImage内部为了对齐可能让每行字节数比width*3多几个字节如果你给sws一个错误的linesize转换出来的图像在左侧会有一条彩边或者像被撕开的错位图。QImage convertToRgb(AVFrame *frame, AVCodecContext *vctx) { SwsContext *sws sws_getContext( vctx-width, vctx-height, static_castAVPixelFormat(frame-format), vctx-width, vctx-height, AV_PIX_FMT_RGB24, SWS_BILINEAR, nullptr, nullptr, nullptr); if (!sws) { return QImage(); } QImage img(vctx-width, vctx-height, QImage::Format_RGB888); uint8_t *dstData[1] { img.bits() }; int dstLinesize[1] { img.bytesPerLine() }; sws_scale(sws, frame-data, frame-linesize, 0, vctx-height, dstData, dstLinesize); sws_freeContext(sws); return img; }在解码循环里每帧都调用sws_getContext会带来不必要的性能损耗因为获取context的过程需要做一堆查表和初始化。我一般的做法是把SwsContext创建挪到run()方法里在解码循环外只创建一次循环内只调sws_scale线程结束时统一释放。上面这段写法是为了示例直观如果你要压性能记得移出去。4.2 上屏选型QLabel适合调试QOpenGLWidget适合多路转好后的QImage怎么显示到界面上有两个常见的选项它们的取舍很直接上屏方式适用场景代价备注QLabel setPixmap单路、原型验证、截图调试每次QImage转QPixmap有一次拷贝代码最少几行就能出画面QOpenGLWidget 纹理上传多路视频、缩放、任意变换需要管GL上下文和纹理生命周期画面流畅适合做拼接墙如果只是验证解码链路通不通先用QLabel。把frameReady信号连到槽函数里ui-label-setPixmap(QPixmap::fromImage(img))即可。这一步需要把QImage保持在QPixmap转换的局部作用域内避免QPixmap拿到的数据区被释放。等你要做多路预览或者对视频做任意角度旋转就得上QOpenGLWidget。基本思路是在paintGL里用glTexImage2D上传RGB数据然后画一个纹理四边形。QImage作为纹理源时注意它的bytesPerLine可能不等于width*3上传时要用GL_UNPACK_ROW_LENGTH指定行间距否则图像会斜着显示。这部分代码跟平台相关等真到那一步再细做不迟。4.3 音频出口swresample接QAudioOutput的配置清单音频解码和视频解码的前半段相似找音频流索引建解码器。后半段多一步重采样因为解码出来的PCM可能是浮点或32位整型而Qt的QAudioOutput通常直接吃16位小端PCM。中间用libswresample转格式。先配QAudioOutputQAudioFormat fmt; fmt.setSampleRate(44100); fmt.setChannelCount(2); fmt.setSampleSize(16); fmt.setCodec(audio/pcm); fmt.setByteOrder(QAudioFormat::LittleEndian); fmt.setSampleType(QAudioFormat::SignedInt); QAudioOutput *audioOut new QAudioOutput(fmt, this); QIODevice *audioDev audioOut-start();这里的采样率44100是一个约定值不是从文件里自动读出来的。在实际项目里我会从音频流的codecpar里取原始采样率和声道数重采样到这里设置的44100双声道。这样无论视频文件是48k还是44.1k采样进QAudioOutput前都是统一格式。重采样代码SwrContext *swr swr_alloc(); av_opt_set_int(swr, in_channel_layout, actx-channel_layout, 0); av_opt_set_int(swr, in_sample_rate, actx-sample_rate, 0); av_opt_set_int(swr, in_sample_fmt, actx-sample_fmt, 0); av_opt_set_int(swr, out_channel_layout, AV_CH_LAYOUT_STEREO, 0); av_opt_set_int(swr, out_sample_rate, 44100, 0); av_opt_set_int(swr, out_sample_fmt, AV_SAMPLE_FMT_S16, 0); swr_init(swr);每次拿到音频帧后int outSamples frame-nb_samples; int outBytesTotal av_samples_get_buffer_size(nullptr, 2, outSamples, AV_SAMPLE_FMT_S16, 1); uint8_t *outBuf (uint8_t *)av_malloc(outBytesTotal); uint8_t *outData[1] { outBuf }; int converted swr_convert(swr, outData, outSamples, (const uint8_t **)frame-data, frame-nb_samples); if (converted 0) { QByteArray pcm((const char *)outBuf, converted * 2 * sizeof(int16_t)); audioDev-write(pcm); } av_free(outBuf);audioDev-write是异步的QIODevice内部有缓冲所以不用担心write阻塞。但要注意如果音频帧很小而write频率很高QAudioOutput内部缓冲会积累延迟表现为声音越来越滞后。我一般会在第一次write前用audioOut-setBufferSize(4096)给个合理初值具体数值要根据你的采样率和帧大小来回试。5. FFmpeg Qt实战常见问题翻车、死锁与内存泄漏5.1 花屏或绿屏像素格式没转就上屏现象视频能播但画面是一片惨绿或交错条纹完全看不出内容。 原因直接拿YUV420P的AVFrame-data[0]塞给了QImage或者在上一步转换时给sws传了错误的行字节数。 解决第一步先确认frame-format是不是AV_PIX_FMT_YUV420P如果是必须经sws_scale转成RGB24再上屏。第二步检查dstLinesize是不是用的img.bytesPerLine()很多示例代码直接写width*3AVFrame的linesize和QImage的stride不匹配就会出现纵向错位、看起来像被切割的画面。我用img.bytesPerLine()之后再没碰到过这类问题。5.2 假死或卡住网络流打开时阻塞UI线程现象点开一个RTSP地址后整个界面像被冻住十几秒偶尔直接失去响应。 原因avformat_open_input对网络流是阻塞操作如果网络不通或服务端不响应它会在内部等很久而你又恰好把它放在槽函数里。 解决把avformat_open_input和av_read_frame都放到解码线程里跑这是结构性解法。另外可以在open之前设置超时字典参数AVDictionary *opts nullptr; av_dict_set(opts, stimeout, 3000000, 0); // 单位微秒3秒 avformat_open_input(fmtCtx, url, nullptr, opts); av_dict_free(opts);stimeout只对TCP系协议生效UDP的RTSP不一定听它。UDP场景我会在open前先发一个探测请求或者让用户选择用TCP模式连接。5.3 内存越涨越大AVFrame与AVPacket没有成对释放现象播放器跑20分钟任务管理器里内存从80MB涨到500MB。 原因解码循环里每帧都调了av_frame_alloc()却没调av_frame_free()或者调了av_packet_unref()却忘了在某条分支里av_read_frame读出来的packet还在。FFmpeg的缓冲区是引用计数的漏掉一次unref就漏一块引用GC大部分时候不会立刻触发但这块内存也永远回不到释放流程。 解决我的习惯是所有av_read_frame的packet在循环结束前统一av_packet_unref所有receive_frame得到的frame立即处理完av_frame_unref。在异常分支return之前把已经alloc出来的AVFrame释放掉。代码里凡是有av_frame_alloc的必须有一个av_frame_free与之对应如果你用了RAII智能指针包装这块就自动安全了。5.4 中文字符串打不开文件toLocal8Bit的取舍现象文件路径带中文avformat_open_input返回负值而英文路径一切正常。 原因QString内部是UTF-16Windows下FFmpeg的avformat_open_input期望的是本地代码页的窄字符串通常是GBK直接toUtf8()传进去FFmpeg在Windows上会把它按本地字符集解释中文就被拆成乱码了。 解决在Windows下用m_file.toLocal8Bit().constData()这会把QString按当前系统区域编码转成窄字符串中文路径就能正常打开。Linux或macOS下toLocal8Bit()通常等于UTF-8可以放心用。不要用toStdString()它在Windows下走的还是本地ANSI行为跟toLocal8Bit类似但语义上不明确可读性差。5.5 音频爆音或断流swr的缓冲参数对不上现象画面前面跑了声音却一顿一顿偶尔还有刺耳的爆破音。 原因QAudioOutput的采样率、声道数、位深配置和swr输出不一致。比如QAudioFormat设了双声道swr却按单声道转换输出的PCM字节数只有预期的一半QAudioOutput就会读乱。 解决把QAudioFormat的每一行都写全主流的组合是44100Hz、2声道、S16、LittleEndian、SignedInt。swr端固定套AV_CH_LAYOUT_STEREO和AV_SAMPLE_FMT_S16。如果视频源的采样率不是44100swr会自动重采样别自己手动改QAudioFormat的sampleRate去匹配源那样反而会因为转换误差造成频繁入队和丢数据。爆音如果还出现去检查swr_convert的返回值是不是等于frame-nb_samples持续不相等说明输入参数有问题。6. 让播放器更稳的收官笔记硬解、低延迟与例行自查6.1 硬解开关可以做成运行时选项硬解不是必须的。我给播放器加硬解能力的方式是运行时动态切换启动时尝试创建av_hwdevice_ctx_create成功才把vctx-hw_device_ctx挂上去失败就回退软解绝不让硬解失败卡死整个播放流程。开启硬解后avcodec_receive_frame返回的frame-format可能变成硬件像素格式此时要先用av_hwframe_transfer_data把帧拷回普通内存再走sws转RGB。6.2 低延迟直播流的解码参数拉RTSP这类直播场景解码器的延迟主要来自缓冲。我一般会在avformat_open_input前设置下面几个参数AVDictionary *opts nullptr; av_dict_set(opts, fflags, nobuffer, 0); av_dict_set(opts, probesize, 32000, 0); av_dict_set(opts, max_delay, 100000, 0); // 100ms avformat_open_input(fmtCtx, url, nullptr, opts); av_dict_free(opts);nobuffer把全局缓冲压到最小probesize缩小探测数据量max_delay限制最多排队100ms。这几个值调参时要有心理准备设太小会让网络抖动直接体现在画面上设太大又回到秒级延迟没有万能值得根据你的网络质量现场试。6.3 例行自查清单我现在每接一个新的FFmpeg版本或新Qt版本都会先花五分钟跑一遍这套自查第一编译能过不等于运行能过先跑最小用例验证dll加载第二解码线程的退出标志要在析构函数里置true并等wait()结束再销毁对象否则线程退出时访问到已释放的QAudioOutput会崩溃第三所有库版本号和路径写进README免得半年后自己都忘了当时装的是哪一套第四检查一下输出目录的dll是不是跟源目录完全一致必要时做一次hash校验。从那以后我每次接手新的FFmpeg集成任务都强制自己先走一遍上面这套流程先最小用例、再解码循环、再音频上屏。这个习惯帮我省下了太多排查时间。希望这篇笔记也能帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。