简介:面向32位Windows下采用MinGW工具链和Qt框架的开发者,这份FFmpeg 3.4预编译精简包解决了在不安装Visual Studio的前提下进行音视频编解码、格式转换与流媒体处理的开发需求。压缩包共170个文件,主体为107个C/C++头文件、21个C语言示例源码、6个DLL及配套的静态导入库(a/lib),并包含def模块定义、pc配置、ffpreset预置参数、ffprobe可执行程序以及makefile和readme,整体体积仅2.62MB,便于快速集成。已有227人学习下载。从内容预览可看到libavutil、libavcodec、libavformat等核心库的导入库,以及transcode_aac、muxing、demuxing_decoding等演示工程,覆盖音频转码、封装复用和解封装解码等常见流程;作为lite版本,它侧重解码能力,对内存与CPU占用更友好,适合在Qt界面程序或轻量桌面工具中直接调用。资源还提供了makefile与readme说明,配合示例源码可快速搭建编译环境;开发时需注意该包面向32位体系,在64位系统下应保持应用架构一致。
1. 为什么你需要这个 ffmpeg-3.4-mingw32:把 32 位 Qt 工程的坑一次填平
做 Qt 开发的人大概都碰过这个场景:手里维护的工程还在用 32 位 MinGW 编译链,同事丢过来一段视频解码的需求,你说「没问题,用 FFmpeg」,结果翻遍全网发现多数组件要么是 MSVC 版、要么是 64 位,要么就是需要自己拿 MSYS 从头编译三天三夜。这个ffmpeg-3.4-mingw32就是用来堵这个窟窿的:一套 32 位 MinGW 下直接能用的 FFmpeg 3.4 预编译库,头文件、链接库、运行时 DLL 齐全,拿来接进 Qt 的 .pro 文件就能跑。它解决的痛点很明确:让还在用 32 位 Qt + MinGW 的老项目,不用重写编译环境,就能做视频解码、格式探测、抽帧、转封装这类常见任务。写这篇东西,是想让你知道这份资源拿回来后第一步该看什么、怎么接、接到第几步容易翻车。
2. 资源包形态与选型:目录结构、DLL 布局和 64 位替代
拿到ffmpeg-3.4-mingw32,先别急着往工程里拖,把三层目录认清楚,后面能少走三小时弯路。这类预编译包通常解压出来就是三个目录:include放头文件,lib放的是 MinGW 链接器能识别的.a导入库,bin放的是带版本后缀的.dll运行库。Qt 工程在 Windows 下编译时只能直接消费这三样,所以目录结构本身就是一个「能不能用」的暗示。
2.1 一眼看清 include、lib、bin 各是干什么的
include下面按模块分目录,libavformat、libavcodec、libavutil、libswscale、libswresample各一个头文件子目录。写代码时extern "C"包住#include <libavformat/avformat.h>和#include <libavcodec/avcodec.h>就够起步用。
lib目录里的文件名很有迷惑性。MinGW 联库时用的不是avformat.lib这种 MSVC 风格,而是libavformat.a、libavcodec.a、libavutil.a。qmake 的LIBS写-lavformat,链接器会自动补上lib前缀和.a后缀。如果你在 Windows 资源管理器里看到一堆libavformat-7.dll.a样子的文件,那是真正链接要用的导入库;.dll.a是给 DLL 导出符号做桥接用的。
bin目录则是一堆带数字后缀的avformat-57.dll、avcodec-57.dll之类的运行库。这个数字不是随便写的,它对应 FFmpeg 3.4 的 ABI 版本。程序启动时 Windows 会按 PATH 顺序找这些 DLL,这就是后面「能编译、跑不起来」的根源。
2.2 为什么 32 位 MinGW 的 FFmpeg 这么难找:ABI 与工具链的双重限制
FFmpeg 在 Windows 上的预编译版本,大量是 MSVC 编译的,因为音视频领域很多工具链默认走 Visual Studio。MinGW 用的是 GCC 的 libgcc、libstdc++ 运行时,对 C 接口的 FFmpeg 来说,MSVC 版不是完全不能链接,但经常踩到内存分配器不一致、结构体对齐方式不对、异常处理机制不同之类的坑,跑起来大概率在avformat_open_input里面当场崩溃。64 位版本又接不进 32 位 Qt 程序,因为指针宽度不一样,单个结构体里的字段偏移都变了。
ffmpeg-3.4-mingw32的价值就在这里:它是在 32 位 MinGW 工具链下编译出来的,ABI 对齐了 Qt 5 的编译环境。至于版本号 3.4,属于 FFmpeg 4.0 之前的老接口时代,那个版本的 API 特征很典型:av_register_all()还需要显式调用,avcodec_decode_video2还能直接用。很多老项目里跑了好几年的代码就是为 3.x 写的,用新版库反而要改一大片。
2.3 拿到资源后别急着接,先跑一遍版本验证
我拿到这类预编译包的第一习惯是,先找个空目录把 include、lib、bin 解开放好,然后写一个一行代码的qmake工程,只链接avutil,打印av_version_info()。这一步不是多余,它能提前暴露两件事:这个包到底是 MinGW 还是 MSVC 编译的,以及你的 Qt Kit 是 32 位还是 64 位。
#include <QtCore/QString> #include <QDebug> extern "C" { #include <libavutil/avutil.h> } int main(int argc, char *argv[]) { (void)argc; (void)argv; av_register_all(); // 3.4 里必须调用一次,4.0 起变成空函数 return qDebug() .noquote() .space() << QString("FFmpeg version:") << av_version_info() ? 0 : 1; }代码里av_register_all()是关键,FFmpeg 3.4 之前不调用它,很多 muxer、demuxer 没有注册,打开文件会直接返回Protocol not found或者Function not implemented,而且这个错误和配置无关,属于「必踩且不好查」的坑。av_version_info()返回的是字符串,例如3.4.8,能直接告诉我们头文件和运行库是否来自同一次打包。如果这里编译都失败,那问题基本出在LIBS顺序或缺少__STDC_CONSTANT_MACROS,这部分放到第 4 章专门说。
3. 在 Qt 工程里顺手接上 FFmpeg:qmake 链接配置与第一段解码代码
这章是重点,按「配置 .pro → 写代码 → 跑通拿文件信息」的顺序来。不建议一上来就写完整解码循环,先做最小链路:打开一个文件,打印流格式,关闭。这条链路能验证头文件、导入库、运行库三个环节都没问题。
3.1 .pro 文件的实际写法:LIBS 顺序和 DEFINES 的细节
把解压后的整个ffmpeg-3.4-mingw32目录放在工程同级,然后在 .pro 里加上下面这段。
INCLUDEPATH += $$PWD/ffmpeg-3.4-mingw32/include LIBS += -L$$PWD/ffmpeg-3.4-mingw32/lib \ -lavformat \ -lavcodec \ -lavutil \ -lswscale \ -lswresample DEFINES += __STDC_CONSTANT_MACROS这里两个地方容易出问题。第一个是-lavformat必须放在-lavcodec和-lavutil前面,因为链接器在处理静态库和导入库时,会从左到右解析未定义符号。avformat里的函数调用了avcodec和avutil里的符号,把被依赖的库放在后面,链接器才能用后面的库去解释前面留下的悬空符号。如果把顺序颠倒,avcodec先处理完毕,等到avformat需要avcodec_alloc_context3时,链接器已经把这个符号标记为 unresolved,直接报undefined reference。第二个是__STDC_CONSTANT_MACROS,MinGW 的某些标准库头文件里,INT64_C、UINT64_C这类宏默认不展开,FFmpeg 头文件里大量使用这些常量,不定义这个宏会在头文件编译阶段报错,错误信息是INT64_C was not declared in this scope,看起来完全不像链接问题,其实根本到不了链接那步。
3.2 第一段能跑的代码:打开文件、打印流信息、安全释放
下面这段代码没有任何编解码动作,就是打开一个本地视频文件,把流信息打到 Qt 的控制台。
#include <QDebug> #include <QFile> #include <QString> extern "C" { #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libavutil/error.h> } static const char *pathToLocal(const QString &filePath) { // Windows 下 FFmpeg 的 avformat_open_input 接收本地编码路径 // 用 QFile::encodeName 解决路径里中文/空格导致的打开失败 return QFile::encodeName(filePath).constData(); } int inspectMedia(const QString &filePath) { av_register_all(); AVFormatContext *fmtCtx = nullptr; int ret = avformat_open_input(&fmtCtx, pathToLocal(filePath), nullptr, nullptr); if (ret != 0) { char errbuf[AV_ERROR_MAX_STRING_SIZE] = { 0 }; av_strerror(ret, errbuf, sizeof(errbuf)); qDebug() << "open failed:" << errbuf; return -1; } ret = avformat_find_stream_info(fmtCtx, nullptr); if (ret < 0) { qDebug() << "find stream info failed"; avformat_close_input(&fmtCtx); return -1; } av_dump_format(fmtCtx, 0, qPrintable(filePath), 0); qDebug() << "stream count:" << fmtCtx->nb_streams; avformat_close_input(&fmtCtx); return 0; }avformat_open_input是走整个库的第一步,它的第二个参数就是文件路径。这里用QFile::encodeName而不是qPrintable,是个容易忽略但很实际的坑:FFmpeg 3.4 在 Windows 上把filename当成多字节字符串,Qt 的 QString 是 UTF-16 内部表示,直接qPrintable转出来的字符串在中文路径下会变成问号,文件打不开。QFile::encodeName会按当前 locale 编码,同时保留空格和中文,这才是 Windows 本地路径的正确递法。
av_dump_format是一个既适合调试也适合验证链接是否成功的函数,它会往 stderr 打完整媒体信息,包括时长、比特率、每个流的编码器和分辨率。nb_streams能直观确认 FFmpeg 已经解析到几个流。
3.3 av_register_all 与 AVFormatContext 的声明周期
FFmpeg 3.4 还很依赖av_register_all(),它做的事情是注册所有内置的封装器、解封装器、编解码器和协议处理器。虽然在调用avformat_open_input之前,库已经做了一次隐式初始化,但在 3.4 版本里这个隐式初始化只注册了个别协议,很多实际使用的 muxer/demuxer 还没进去。常见表现是avformat_open_input返回-1330790764,十六进制是错误码,对应Function not implemented。
AVFormatContext的生命周期要求是:avformat_open_input分配并填充它,用完由avformat_close_input释放,不能直接av_free(fmtCtx),否则内部缓冲没有释放,长时间跑会产生内存泄漏。这种不调用长时间也看不出毛病,只是内存增长缓慢,很多半路接手的人会当成自己的业务泄漏去查,最后根源在释放方式不对。从一上手就用标准释放流程,能省掉后面字符串替换、转封装时的一大堆幺蛾子。
4. 排查避坑:MinGW 32 位下链接顺序、宏定义和 DLL 路径的五个真实问题
这章全是实际操作里最常见、最不容易一眼看穿的报错。每一条都是从「现象 → 原因 → 解决」三个角度拆开说,适合把.pro配置完编译失败时逐条对照查。
4.1 undefined reference to 'avformat_open_input':库顺序背锅
现象:qmake 编译到链接阶段,控制台刷出一串undefined reference to 'avformat_open_input'或者avcodec_alloc_context3。
原因:.pro里LIBS顺序不对。MinGW 的链接器是单遍解析,从左到右处理,-lavformat放在-lavcodec -lavutil后面时,链接器检查完avformat里的未定义符号,再看到后面avcodec库才会去解析,但avformat已经出局,晚期符号无法回填。另外,如果同时引了 MSVC 版的.lib,MinGW 链接器直接忽略,也会产生类似报错。
解决:把依赖关系写成从高一层到低一层:avformat→avcodec→avutil,中间可以有swscale、swresample,它们都依赖avutil,所以avutil永远放在末尾。调整后再编译,这串错误会迅速消失。
4.2 报错 INT64_C was not declared:宏定义缺失
现象:#include <libavutil/mem.h>或者包含libavformat/avformat.h时,编译报INT64_C' was not declared in this scope,集中在头文件的常量表达式处。
原因:在 MinGW 的某些环境下,C++ 编译器没有一进来就启用 C99 标准常量宏,FFmpeg 头文件里为了表示 64 位时间戳常量,大量用到INT64_C(0x...),只有预先定义__STDC_CONSTANT_MACROS才在 C++ 模式下把宏展开。这和第三方库无关,是标准库实现差异。
解决:在.pro里加DEFINES += __STDC_CONSTANT_MACROS,或者编译命令加-D__STDC_CONSTANT_MACROS。加了之后如果还有其他UINT64_C报错,说明你用的是老版本标准库,需要在头文件之前#define,但DEFINES已经覆盖绝大多数场景。
4.3 编译能过,运行起不来:找不到 DLL
现象:Qt Creator 里程序直接报错The code execution cannot proceed because avcodec-57.dll was not found,或者双击 exe 没反应。
原因:编译链接用的是lib目录下的导入库,运行时需要的是bin目录下的 DLL。这个 DLL 不在目标 exe 同目录,也不在 PATH 里,Windows 找不到就拒绝启动。Qt 工程的构建目录默认和资源目录不在一起,所以这一步极其常见。
解决:两种办法任选,我一般两个都做。第一,项目发布时把bin下所有 DLL 拷贝到 exe 目录;第二,开发调试时在 Qt Creator 的 Run 页面里把ffmpeg-3.4-mingw32/bin塞进PATH环境变量。如果只做第二种,发布时照样丢 DLL,所以拷 DLL 这步不能省。32 位程序必须用 32 位 DLL,64 位程序不能用这里的文件,否则 Windows 会直接扔一个不兼容错误。
4.4 打开带空格或中文路径的文件返回错误,英文路径没问题
现象:同一段代码,测试文件名video.mp4正常,换成D:/我的视频/test 1.mp4直接返回打开失败,错误码多为-2或者-22。
原因:FFmpeg 3.4 在 Windows 上通过fopen类函数打开文件,这个接口接收的是本地代码页字符串。qPrintable把 QString 转成 UTF-8,在中文 locale 下 UTF-8 字节序列和实际文件名不对应,导致文件找不到;空格的问题更直接,某些解封装器按 URL 解析,空格没做编码转换,协议层解析失败。
解决:使用QFile::encodeName得到本地编码路径,并且避免手动拼 URL 式路径。如果你的路径来自用户输入,建议在入口处做一层处理:带易混淆字符的路径先复制到无空格英文临时目录,再交给 FFmpeg。这不是玄学,是老接手老项目的视频处理工具必须考虑的边界。
4.5 解码时偶发内存崩溃:把 extern "C" 漏了
现象:程序运行一段时间后崩溃在avcodec_decode_video2里,调用栈显示乱七八糟,看不出和业务代码的关系。单步调试时又很难复现,偶发性强。
原因:libavformat等 C 库在 C++ 环境下如果没有用extern "C"包裹头文件,编译时函数名会被 Name Mangling,链接阶段通常就报错了,所以这个情况在链接期已经拦住。真正容易漏的是结构体指针类型不匹配,MinGW 32 位下默认结构体对齐方式是 4 字节,FFmpeg 老版本默认也是 4,但如果某个全局编译选项开了#pragma pack(1),所有结构体偏移量都变了,库外部写入的字段和内部读取位置错位,内存访问越界。
解决:确认extern "C"没漏,同时检查整个工程的编译选项里没有全局#pragma pack,最好在av_register_all()之后打印avcodec_configuration()里听编译参数,和包的编译参数对齐。这类问题一换环境就消失,所以调试时很难定性,见到随机崩溃先把字节对齐这个嫌疑排除。
5. 顺着代码走一遍:抽帧存图与重新封装的完整流程
配置跑通以后,下一步最常用的两个功能就是「从视频里抽一帧存成图」和「把一个封装格式转成另一个」。挑这两个来讲,是因为它们覆盖了解码、缩放、编码、写文件四条主线,做完基本对 FFmpeg 3.4 的核心 API 有数了。
5.1 找到视频流并打开解码器
拿到AVFormatContext之后,先遍历流找到视频流,并用它初始化解码器。
int findVideoStream(AVFormatContext *fmtCtx) { for (unsigned int i = 0; i < fmtCtx->nb_streams; i++) { if (fmtCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { return static_cast<int>(i); } } return -1; } void openDecoder(AVFormatContext *fmtCtx, int streamIdx, AVCodecContext **codecCtx) { AVCodecParameters *par = fmtCtx->streams[streamIdx]->codecpar; const AVCodec *decoder = avcodec_find_decoder(par->codec_id); if (!decoder) { qWarning() << "decoder not found" << par->codec_id; return; } *codecCtx = avcodec_alloc_context3(decoder); avcodec_parameters_to_context(*codecCtx, par); if (avcodec_open2(*codecCtx, decoder, nullptr) < 0) { qWarning() << "open decoder failed"; avcodec_free_context(codecCtx); } }注意传参里有二级指针AVCodecContext **,这是为了在函数创建失败时能把指针置空,防止外面留着野指针。avcodec_parameters_to_context是 3.4 里的标准做法,把流里的编码参数拷贝到私有的 codec context,这一步不能省,不然解码器不知道分辨率、像素格式。avcodec_find_decoder返回的指针不需要手动释放,它是静态注册的,真正要释放的是avcodec_alloc_context3分出来的codecCtx。
5.2 循环读取解码帧并转成 QImage
解码是一个循环:先从AVFormatContext里读包,再把包喂给解码器,解出来的AVFrame从 YUV 转成 RGB,最后交给 QImage。FFmpeg 3.4 同时提供老接口avcodec_decode_video2和新接口avcodec_send_packet/avcodec_receive_frame,我习惯用后者,因为能正确表达「一次 send 可能对应多次 receive」,语义更接近现代解码器。
bool grabFrame(AVFormatContext *fmtCtx, AVCodecContext *codecCtx, int streamIdx, QString outPath) { AVPacket pkt; AVFrame *frame = av_frame_alloc(); AVFrame *rgbFrame = av_frame_alloc(); SwsContext *sws = nullptr; bool done = false; av_init_packet(&pkt); while (av_read_frame(fmtCtx, &pkt) >= 0) { if (pkt.stream_index != streamIdx) { av_packet_unref(&pkt); continue; } int ret = avcodec_send_packet(codecCtx, &pkt); if (ret != 0 && ret != AVERROR(EAGAIN)) { break; } while (avcodec_receive_frame(codecCtx, frame) == 0) { if (sws == nullptr) { sws = sws_getContext(frame->width, frame->height, codecCtx->pix_fmt, frame->width, frame->height, AV_PIX_FMT_RGB32, SWS_BILINEAR, nullptr, nullptr, nullptr); } sws_scale(sws, frame->data, frame->linesize, 0, frame->height, rgbFrame->data, rgbFrame->linesize); QImage img(rgbFrame->data[0], frame->width, frame->height, rgbFrame->linesize[0], QImage::Format_RGB32); img.save(outPath); done = true; break; } av_packet_unref(&pkt); if (done) break; } sws_freeContext(sws); av_frame_free(&frame); av_frame_free(&rgbFrame); return done; }这段代码对 32 位环境有两点敏感:QImage::Format_RGB32在字节序上是小端 BGRX,和AV_PIX_FMT_RGB32一致,但如果你用Format_ARGB32,红色和蓝色通道会互换;另外 RGB32 的linesize是 4 字节对齐后的宽度,QImage构造传入的bytesPerLine直接用rgbFrame->linesize[0]就对了,不要自己算。如果只取第一帧,读完即break,后续流的 packet 会被av_packet_unref慢慢清理,不会泄漏。av_frame_alloc分配的是框架,内部数据缓冲区会在解码时自动管理,所以rgbFrame不用手动去分配 row buffer,由 swscale 帮我们写入。
5.3 重新封装:不重新编码的容器转换
如果只想把 MKV 转成 MP4,或把 TS 转成 FLV,不需要转码,只做「流复制」。核心步骤是建立输出 context、手动添加 stream、逐个 packet 写入,代码骨架如下。
int remux(const QString &srcPath, const QString &dstPath) { av_register_all(); AVFormatContext *inCtx = nullptr; if (avformat_open_input(&inCtx, QFile::encodeName(srcPath).constData(), nullptr, nullptr) < 0) { return -1; } avformat_find_stream_info(inCtx, nullptr); AVFormatContext *outCtx = nullptr; avformat_alloc_output_context2(&outCtx, nullptr, nullptr, QFile::encodeName(dstPath).constData()); if (!outCtx) { avformat_close_input(&inCtx); return -1; } for (unsigned int i = 0; i < inCtx->nb_streams; i++) { AVStream *inStream = inCtx->streams[i]; AVStream *outStream = avformat_new_stream(outCtx, nullptr); avcodec_parameters_copy(outStream->codecpar, inStream->codecpar); outStream->codecpar->codec_tag = 0; } avformat_write_header(outCtx, nullptr); AVPacket pkt; while (av_read_frame(inCtx, &pkt) >= 0) { av_packet_rescale_ts(&pkt, inCtx->streams[pkt.stream_index]->time_base, outCtx->streams[pkt.stream_index]->time_base); pkt.pos = -1; av_interleaved_write_frame(outCtx, &pkt); av_packet_unref(&pkt); } av_write_trailer(outCtx); avformat_close_input(&inCtx); avformat_free_context(outCtx); return 0; }这个流程里最像玄学的两行是outStream->codecpar->codec_tag = 0和pkt.pos = -1。codec_tag是封装格式特有的四字符编码,从旧容器拷到新容器时不一定是合法的,重置成 0 让新 muxer 自己去填;pkt.pos = -1防止把输入文件的偏移写到输出文件的索引里。av_packet_rescale_ts负责把时间戳从输入流的 time_base 换算到输出流的 time_base,这一步不做,播放进度会乱。这个 remux 方案适合音画同步要求不高、输入输出时间基差异不大的场景,如果你的源文件很特殊,还是按需打开参考文档逐个调。
6. 进一步把它封成 QML 可用的解码器:重视版本验证与线程边界
FFmpeg 的解码默认是阻塞的,拿到的QImage如果直接在主线程里处理,视频一长界面立刻卡死。常见的做法是把它封装到一个QObject派生类里,用QThread跑解码循环,每解出一帧通过信号把QImage发给 QML。核心边界很简单:不能在工作线程里碰 UI 对象,但QImage本身是隐式共享类型,可以安全跨线程发信号传递。
class VideoDecoder : public QObject { Q_OBJECT public slots: void startDecode(const QString &path) { // 这里跑 ffmpeg 解码,每帧 emit frameReady(image) } signals: void frameReady(const QImage &image); void finished(); };工作线程里要保护两个状态:上一帧还没被 UI 消费完,下一帧就过来,QImage 的隐式共享会让两个对象共享内存,虽然不至于崩溃,但 UI 显示的数据可能被下一帧覆盖。我习惯在 emit 之前image.detach()强制拷贝一份,代价是多一次像素拷贝,但换来 QML 侧显示的确定性。这个细节在 32 位环境上更重要,因为内存池小,分配压力大,用 detach 能有效阻止解码线程反复改共享缓冲。
封装完成后,建议在工程里加一个隐藏的验证入口:一个菜单项或者命令行参数,调用av_version_info()把链接的库版本打出来。同时跑一个已知可解码的短素材,观察输出帧数和预期是否一致。从那以后,我每换一个 Qt Kit 或重装系统,都会强制走一遍「av_version_info → 打开测试视频 → 打印帧数」三步,只要这三步通,库就算真的在这个环境里落地了。对还在吃 32 位 MinGW 老本的项目,顺手接这套库并不难,难的是提前把顺序、宏、路径这些零碎先排掉。希望这篇能帮你少排一轮坑。
本文还有配套的精品资源,点击获取