简介:面向32位 Windows 下使用 Qt 与 MinGW 开发音视频功能的开发者,这份 FFmpeg 3.4 预编译包提供可直接引用的头文件、导入库与动态运行库,适合解码、转码、封装、播放器集成及轻量级二次开发,能减少自行编译和环境适配的成本。压缩包共170个文件,包含107个 h 头文件、21个 c 示例源码,以及 def、a、dll、lib、pc 等链接与 pkg-config 文件,另有 ffpreset 预设、2个 exe 工具、makefile、readme 和 xsd,整体约2.62MB,目录紧凑,便于按模块查阅。已有229人学习下载。包内示例涉及音频转码、封装、解复用与解码等流程,可帮助读者核对 MinGW32 工程配置、排查链接依赖,并据此搭建自己的音视频处理模块。
1. 32 位 Qt 工程接 ffmpeg,为什么绕不开 ffmpeg-3.4-mingw32 这份产物
Qt 的 mingw 32 位套件接 ffmpeg,第一次编译往往出问题的地方在链接,而不是代码本身:网上抓下来的库一半是 MSVC 的 .lib,另一半是 64 位产物,qmake 跑完能出现满屏 undefined reference。ffmpeg-3.4-mingw32 解决的正是这种错位——它是 FFmpeg 3.4 用 32 位 MinGW 工具链编出来的 include、lib、bin 三件套,配 Qt 5 的 mingw530_32 或 mingw730_32 套件能直接链上,不必自己搂 MinGW、msys 重编一遍。它适合两类人:手上还有 32 位 Windows 客户端要维护的,以及被 64 位库的位数错配问题折腾够了、想找个能跑起来的基线的。下面按版本对齐、解封装解码、音频同步、排错、发布核对五段拆开讲。
2. 版本对齐:libavcodec 57 这套号段决定了你能写哪些 API
FFmpeg 3.4 对应的一组主版本号是 libavcodec 57、libavformat 57、libavutil 55、libswscale 4、libswresample 2、libavfilter 6、libavdevice 57。资源里的 DLL 名字之所以是 avcodec-57.dll、avformat-57.dll、avutil-55.dll,就是主版本号直接进了文件名。这件事在工程上不是冷知识:你从别处抄来的代码、搜到的示例,很多是 4.x 甚至 6.x 的写法,直接扔进 3.4 会编译不过或者链接找不到符号。先把号段对齐,后面所有调试才有意义。
2.1 从 57 反推可用 API 的边界
3.4 这个节点比较特殊,新旧两套解码接口同时存在。老的 avcodec_decode_video2 还能用,新的 avcodec_send_packet / avcodec_receive_frame 从 3.1 就进来了,两个都能编译。我一般直接用新的,原因很实在:老接口一次收包一次出帧,遇到 B 帧或者需要重排的时候,你得自己维护缓存,写起来啰嗦;新接口把重排丢回给解码器内部,代码短一截,出错点也少。
需要留神的是那些 3.4 里还没有的东西。av_channel_layout 这一套结构体是 5.x 才有的,3.4 里声道布局还是 uint64_t 的位掩码,配 av_get_default_channel_layout、av_get_channel_layout_nb_channels 这类函数用。avcodec_parameters_to_context 在 3.4 里是有的,从 AVCodecParameters 拷参数到 AVCodecContext 属于标准动作;但 AVStream::codec 这个老字段在很多示例里还活着,能编过不代表该用,参数以 codecpar 为准更稳。另外 refcounted_frames 这个字段 3.4 还有,4.0 之后没了,别把带它的代码当通用写法到处搬。
还有一点容易被忽略:av_register_all 在 3.4 里依然是需要的,虽然它已经在往废弃走的路上。少了这一句,某些老格式会 register 不上,表现是 avformat_open_input 打不开一个明明能播的文件。我一般把它和 avcodec_register_all 放在初始化函数的开头,一次调完拉倒。
运行时校验版本是最省事的自证方式。在 main 里打一行:
extern "C" { #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libavutil/avutil.h> } // 启动时打印一次,确认链进来的确实是 3.4 那一套 qDebug() << "avcodec" << avcodec_version() << "avformat" << avformat_version() << "avutil" << avutil_version();avcodec_version() 返回的是把主、次、微版本按位拼起来的整数,主版本在高 16 位。你不需要背具体数值,只要看它左移 16 位之后是不是 57,就知道手里这份 DLL 是不是 3.4 的产物。这一步我在每个新工程里都会做,因为最容易出现的翻车不是代码写错,而是 PATH 里躺着一份别的版本的同名 DLL,程序跑起来行为诡异,查半天查不到原因。
2.2 MinGW 32 位 kit 与 .dll.a 导入库的链接约定
MinGW 链接 FFmpeg 用的是导入库,文件名形如 libavformat.dll.a,qmake 里写 -lavformat,链接器会自己去拼 lib + 名字 + .dll.a。这里有两个隐形前提:一是这份 .a 必须是 32 位 PE 格式,二是它得和你的 g++ 同属 MinGW 体系,MSVC 编出来的 .lib 在 MinGW 下是链不上的,改后缀也没用。
判断位数不用装额外工具,MinGW 自带的 objdump 就够:
# 在 Qt 的 MinGW 命令行环境里执行,看导入库的目标架构 objdump -f 3rdparty/ffmpeg-3.4-mingw32/lib/libavcodec.dll.a | head -n 8 # 输出里出现 pe-i386 才是 32 位,pe-x86-64 说明拿错包了 # 顺便查运行时依赖,确认是否需要额外补 DLL objdump -p 3rdparty/ffmpeg-3.4-mingw32/bin/avcodec-57.dll | grep "DLL Name"第一条命令看的是 file format,pe-i386 对应 32 位;第二条命令列的是这个 DLL 自己依赖谁。如果输出里出现 libwinpthread-1.dll,说明这份 FFmpeg 是带 pthreads 编的,发布时这个 DLL 也得跟着走,否则程序在别人机器上启动就闪退,而在你开发机上一切正常——因为你的 MinGW 目录里有它。这是我在 32 位项目里见过最多的一类问题:不是找不到 avcodec-57.dll,而是找到了它,它自己又缺东西。
链接顺序方面,MinGW 的 ld 是单遍扫描,被依赖的库要放在后面。写 -lavformat -lavcodec -lswresample -lswscale -lavutil 这个顺序基本不会错,avutil 垫底。顺序写反了在静态链接下必炸,导入库场景下有时能过,但一旦你哪天换成静态库就原形毕露,不如一开始就写对。
头文件不需要你手动套 extern "C"。FFmpeg 的公开头文件自己带了这个保护,手动再包一层不会报错,但容易在嵌套 include 的时候写出奇怪的作用域问题。我的习惯是单独建一个 ffmpeg_headers.h,集中 include,别的地方只引用这一个文件,将来换版本号只改一处。
2.3 目录落位与 .pro 的最小可编译配置
资源是标准三件套结构,落位方式决定了 .pro 写起来顺不顺。我一般把整个目录塞进工程下的 3rdparty,跟平台相关的第三方位放一起,别扔进系统目录:
| 路径 | 内容 | 工程里的角色 |
|---|---|---|
| include/ | libavcodec、libavformat、libavutil、libswscale、libswresample 等头文件目录 | 进 INCLUDEPATH |
| lib/ | libavformat.dll.a、libavcodec.dll.a 等导入库 | 进 LIBS 的 -L 与 -l |
| bin/ | avcodec-57.dll、avformat-57.dll、avutil-55.dll、swscale-4.dll、swresample-2.dll、avfilter-6.dll、avdevice-57.dll | 运行期与 exe 同目录 |
.pro 里最少这么几行就够跑起来:
# 32 位 MinGW 套件:头文件 + 导入库 + 运行期 DLL 复制 FFMPEG_DIR = $$PWD/3rdparty/ffmpeg-3.4-mingw32 INCLUDEPATH += $$FFMPEG_DIR/include # 被依赖的库放后面,avutil 永远垫底 LIBS += -L$$FFMPEG_DIR/lib \ -lavformat -lavcodec -lswresample -lswscale -lavutil win32 { # DESTDIR 为空时输出在 OUT_PWD,非空则是 OUT_PWD/DESTDIR FF_DEST = $$OUT_PWD !isEmpty(DESTDIR): FF_DEST = $$OUT_PWD/$$DESTDIR # 链接完成后把 bin 下的 DLL 拷到可执行文件旁边 for(dll, $$files($$FFMPEG_DIR/bin/*.dll)) { QMAKE_POST_LINK += $$QMAKE_COPY /Y $$shell_path($$dll) \ $$shell_path($$FF_DEST) $$escape_expand(\n\t) } }FFMPEG_DIR 用 $$PWD 起头,保证相对工程文件定位,换机器不会因为绝对路径不同而崩。LIBS 那一行的顺序就是前面说的依赖顺序。QMAKE_POST_LINK 那段是给调试期省事的:每次编译完自动把 DLL 搬到 exe 边上,省得你手动复制完忘了覆盖。注意 $$OUT_PWD 和 DESTDIR 的组合,如果你的工程设置了 DESTDIR 而复制目标还写 OUT_PWD,编译是成功的,运行还是找不到 DLL——这个坑我踩过一次,现象是 qmake 一切正常,双击 exe 弹「无法启动此程序」。
配置完先别急着写业务代码,拿一个空 main 编译一次,确保能过链接。链接过了再往里面填解码流程,出问题的范围就小很多。
3. 解封装到出画面:在 Qt 里跑通一条 32 位解码链
链路本身不复杂:avformat_open_input 打开文件,avformat_find_stream_info 补全流信息,av_find_best_stream 挑出视频流,avcodec_find_decoder 找解码器,avcodec_open2 打开,然后循环 av_read_frame 拿包、送进解码器、收帧、转成 QImage 交给 Qt 画。麻烦全在参数和返回值处理上,32 位环境只是让内存更紧张一点,逻辑上没区别。
3.1 打开输入与流选择的参数怎么给
打开输入这一步,最容易翻车的是路径编码。Windows 上 QString 用 toLocal8Bit 得到的是系统代码页字节,中文路径在 GBK 环境下就是 GBK 字节,而 FFmpeg 内部按 UTF-8 解析路径,结果就是返回一个负数错误码,你看着文件明明在那儿。
bool openInput(const QString &file, AVFormatContext **fmt, int *videoIdx) { *fmt = nullptr; // 统一走 UTF-8,中文路径才不会在 avformat_open_input 里返回 ENOENT QByteArray path = QDir::toNativeSeparators(file).toUtf8(); if (avformat_open_input(fmt, path.constData(), nullptr, nullptr) < 0) return false; // 探测流信息,网络流或残缺文件要给足探测时长 if (avformat_find_stream_info(*fmt, nullptr) < 0) { avformat_close_input(fmt); return false; } // 让 FFmpeg 自己挑最合适的视频流,比自己遍历判断靠谱 *videoIdx = av_find_best_stream(*fmt, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0); return *videoIdx >= 0; }path 用 toNativeSeparators 换成反斜杠只是习惯,FFmpeg 正斜杠反斜杠都认,关键是 toUtf8。avformat_find_stream_info 在本地文件上很快,遇到残缺文件或者网络源会卡住,需要控制的话用 AVDictionary 传 probesize 和 analyzeduration,单位是字节和微秒。av_find_best_stream 的第三个参数是期望的流索引,给 -1 表示不指定,第四个参数 related_stream 用于音频关联视频这类场景,本地播放给 -1 就行。返回负值说明没有视频流,这时候别硬着头皮往下走。
拿到流索引之后,解码器上下文从 codecpar 拷参数,这是 3.4 里的标准写法:
AVCodecContext *decCtx = nullptr; bool openDecoder(AVFormatContext *fmt, int videoIdx, AVCodecContext **out) { AVStream *st = fmt->streams[videoIdx]; const AVCodec *codec = avcodec_find_decoder(st->codecpar->codec_id); if (!codec) return false; AVCodecContext *ctx = avcodec_alloc_context3(codec); if (!ctx) return false; // 从 codecpar 拷参数,别去碰 AVStream::codec 那个老字段 if (avcodec_parameters_to_context(ctx, st->codecpar) < 0) { avcodec_free_context(&ctx); return false; } // 32 位进程地址空间紧张,线程数按核数给,别越大越好 ctx->thread_count = qBound(2, QThread::idealThreadCount(), 4); if (avcodec_open2(ctx, codec, nullptr) < 0) { avcodec_free_context(&ctx); return false; } *out = ctx; return true; }thread_count 这一行值得单独说。FFmpeg 的帧级多线程在 32 位进程里,每个线程都要占一份参考帧缓存,核数多的机器上开满线程,1080p 播放的内存峰值能翻好几倍,32 位地址空间就 2G 出头,很容易在播了几个文件之后 av_frame_alloc 返回空。我一般限制在 2 到 4 之间,具体看分辨率,1080p 给 4 基本够,4K 素材在 32 位进程里本来就不该硬扛。
3.2 解码循环:send_packet / receive_frame 的写法与返回值处理
新接口的关键是理解两个函数的返回值语义。send_packet 返回 0 表示包被收下了,返回 AVERROR(EAGAIN) 表示解码器内部还堵着帧,得先 receive_frame 吐出来再送;receive_frame 返回 0 表示拿到一帧,返回 AVERROR(EAGAIN) 表示暂时没有帧,需要继续送包;返回 AVERROR_EOF 表示流结束了。写错这三个分支,表现就是解码到一半卡死或者丢帧。
void decodeLoop(AVFormatContext *fmt, AVCodecContext *decCtx, int videoIdx) { AVPacket *pkt = av_packet_alloc(); AVFrame *frame = av_frame_alloc(); while (av_read_frame(fmt, pkt) >= 0) { if (pkt->stream_index == videoIdx) { int ret = avcodec_send_packet(decCtx, pkt); // EAGAIN 说明解码器还堵着,先把帧取干净再继续送 if (ret == 0 || ret == AVERROR(EAGAIN)) { while (true) { ret = avcodec_receive_frame(decCtx, frame); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break; if (ret < 0) break; renderFrame(frame); // 交给下一页的 sws_scale av_frame_unref(frame); } } } av_packet_unref(pkt); // 每轮必须归还,否则内存只增不减 } // 冲刷:送一个空包,把解码器里缓存的帧全部取出来 avcodec_send_packet(decCtx, nullptr); while (avcodec_receive_frame(decCtx, frame) == 0) { renderFrame(frame); av_frame_unref(frame); } av_frame_free(&frame); av_packet_free(&pkt); }冲刷那段是很多人漏掉的。视频播完最后一帧就没了,或者拖到结尾少几帧,多半是没送空包。av_packet_unref 和 av_frame_unref 的位置也要注意:unref 不是释放对象,是把引用计数还回去,对象本身留着下次复用,所以循环外才 free。这两句漏一句,播放器跑上十几分钟内存就上去了,32 位进程扛不住。
renderFrame 里做像素格式转换,见下一节。
3.3 sws_scale 转 QImage:stride 与像素格式的对应关系
Qt 要画的东西得是 QImage,FFmpeg 解出来的是 YUV420P 之类的原始格式,中间靠 libswscale 转。转换本身一行调用,坑在 stride 上。
void renderFrame(AVFrame *frame) { static SwsContext *sws = nullptr; static int swsW = 0, swsH = 0; static AVPixelFormat swsFmt = AV_PIX_FMT_NONE; // 分辨率或像素格式变了才重建上下文,别每帧都建 if (swsW != frame->width || swsH != frame->height || swsFmt != frame->format) { sws_freeContext(sws); sws = sws_getContext(frame->width, frame->height, (AVPixelFormat)frame->format, frame->width, frame->height, AV_PIX_FMT_RGB32, SWS_BILINEAR, nullptr, nullptr, nullptr); swsW = frame->width; swsH = frame->height; swsFmt = (AVPixelFormat)frame->format; } QImage img(frame->width, frame->height, QImage::Format_RGB32); // 目标 stride 必须用 QImage 的 bytesPerLine,不能用 width*4 uint8_t *dst[4] = { img.bits(), nullptr, nullptr, nullptr }; int dstStride[4] = { (int)img.bytesPerLine(), 0, 0, 0 }; sws_scale(sws, frame->data, frame->linesize, 0, frame->height, dst, dstStride); emit frameReady(img); }AV_PIX_FMT_RGB32 在小端机器上的内存排布是 B、G、R、X,正好对上 QImage::Format_RGB32 的 0xffRRGGBB 存储顺序,所以这两者是一对,别配成 Format_ARGB32_Premultiplied 再疑惑颜色为什么反了。
dstStride 用 bytesPerLine() 是关键。QImage 每行会做 4 字节对齐,宽度不是 4 的倍数时,bytesPerLine 比 width4 大一点。你要是图省事写 width4,画面就会出现斜向的绿色条纹或者整体错位——这个现象很好认,因为它在某些分辨率下正常、在另一些分辨率下花屏,非常有迷惑性。同理,源端的 linesize 是 FFmpeg 给的,可能带对齐填充,也必须原样传进 sws_scale,不能假设等于宽度。
SwsContext 用 static 缓存,是因为重建一次的开销不小,每帧新建的话 CPU 直接拉满。但缓存要有失效判断,切换视频源、分辨率变了都得重建,不然会读到已经释放的上下文。写成类的成员变量比 static 更干净,我这儿为了示例短一点用了 static,实际工程里会挂到播放器对象上。
3.4 时间戳换算与播放节拍
解出来的帧带的是 pts,单位是流的 time_base,不是秒,也不是毫秒。想按时间显示,必须乘 av_q2d(stream->time_base) 换成秒。用帧序号除以帧率也能动,但可变帧率素材上一定会飘,而且拖动进度条之后完全对不上。
AVStream *st = fmt->streams[videoIdx]; double tb = av_q2d(st->time_base); // best_effort_timestamp 在没有标准 pts 时也能给出可用值 int64_t ts = frame->best_effort_timestamp; double seconds = ts * tb;播放节拍我一般用 QElapsedTimer 做外部时钟,每帧算一次「这帧该在什么时候显示」,差值大于阈值就 sleep,小于阈值就丢帧。不要用 QTimer 定时喂帧,定时器的抖动在 60fps 下很明显,看着就是一顿一顿。32 位进程里 sleep 的精度够用,QThread::usleep 或者条件变量都行,误差控制在 2ms 以内肉眼就看不出来了。
4. 音频与同步:swresample 重采样接 QAudioOutput
音频这条线比视频多一层转换。解码出来是 AVFrame 里的 PCM,格式、采样率、声道数都跟着源走,而声卡要的是固定格式,中间用 libswresample 统一。3.4 的 swr 还是老的 channel_layout 位掩码接口,不是 5.x 那套 AVChannelLayout,写的时候别抄错版本。
4.1 声道布局与采样格式的转换参数
目标格式我一般定成 16 位有符号整数、双声道、48kHz,因为 QAudioOutput 对这个组合的支持最稳,跨机器差异最小。
SwrContext *swr = nullptr; bool initResampler(AVCodecContext *aCtx, int outRate = 48000) { // 3.4 用位掩码描述声道布局,默认取解码器的布局 int64_t inLayout = aCtx->channel_layout ? aCtx->channel_layout : av_get_default_channel_layout(aCtx->channels); int64_t outLayout = AV_CH_LAYOUT_STEREO; swr = swr_alloc_set_opts(nullptr, outLayout, AV_SAMPLE_FMT_S16, outRate, inLayout, aCtx->sample_fmt, aCtx->sample_rate, 0, nullptr); if (!swr || swr_init(swr) < 0) return false; return true; }swr_alloc_set_opts 的参数顺序是「输出布局、输出格式、输出采样率、输入布局、输入格式、输入采样率」,前三后三别搞混,搞混了不报错但声音变调。aCtx->channel_layout 有可能为 0,尤其是某些老编码,这时候用 av_get_default_channel_layout 按声道数推一个,不然 swr_init 会失败。返回负值基本就是布局和声道数对不上。
QAudioFormat 那边对应设置:
QAudioFormat fmt; fmt.setSampleRate(48000); fmt.setChannelCount(2); fmt.setSampleSize(16); fmt.setCodec("audio/pcm"); fmt.setSampleType(QAudioFormat::SignedInt); fmt.setByteOrder(QAudioFormat::LittleEndian);这段和上面的 AV_SAMPLE_FMT_S16 是一套,改一处必须改两处。我见过有人把 swr 输出改成 FLT(浮点),QAudioFormat 还留着 SignedInt,结果就是满耳朵白噪声,音量还特别大。
转换调用本身要注意输出样本数的估算:
// 输入样本数 n 转成输出,按比例放大并留余量 int outSamples = av_rescale_rnd(swr_get_delay(swr, aCtx->sample_rate) + frame->nb_samples, outRate, aCtx->sample_rate, AV_ROUND_UP); uint8_t *outBuf = nullptr; av_samples_alloc(&outBuf, nullptr, 2, outSamples, AV_SAMPLE_FMT_S16, 0); int got = swr_convert(swr, &outBuf, outSamples, (const uint8_t **)frame->data, frame->nb_samples);av_rescale_rnd 加 swr_get_delay 是必须的,重采样不是一一对应,内部有缓冲,不留余量会周期性丢样本,听起来是轻微的咔哒声,几秒钟一次,很难定位。swr_convert 返回值 got 才是真正写进缓冲的样本数,别直接用 outSamples。
4.2 音视频同步的两种取舍
同步策略无非两种:视频跟音频,或者音频跟视频。我基本都选音频主时钟,原因是人对声音的断续极度敏感,对画面掉一两帧几乎无感。音频设备按自己的节奏消费数据,视频那边每帧算出的显示时间跟音频时钟比,超前就等,落后超过阈值就丢帧。
阈值设多少有讲究。低于 20ms 就丢帧,会因为测量误差疯狂丢;高于 100ms 又看得出嘴型不对。我一般设 80ms 作为丢帧门槛,40ms 作为等待门槛,实测下来观感比较自然。音频时钟不要用自己累加的样本数除以采样率,那个值会随着缓冲区漂移,用 QAudioOutput 的 processedUSecs() 更准。
32 位进程里还有一个隐性成本:音频缓冲区不要开太大。缓冲大了抗抖动能力强,但拖动进度条之后要等半天声音才跟上,体验很差。我一般按 200ms 左右配,配合 seek 时的清空动作。
4.3 seek 与 flush:拖进度条必须做的三件事
拖动进度条是播放器里最容易出问题的地方,做完这三步才算完整。
第一步,换算目标时间戳。av_seek_frame 的第三个参数单位是流的 time_base,不是 AV_TIME_BASE,除非你把 stream_index 传 -1。传了流索引就得按流的单位来:
// seconds 是用户拖到的位置(秒) int64_t target = (int64_t)(seconds / av_q2d(st->time_base)); // AVSEEK_FLAG_BACKWARD 保证落在关键帧上,且不会跳过头 av_seek_frame(fmt, videoIdx, target, AVSEEK_FLAG_BACKWARD);第二步,清解码器内部缓存。avcodec_flush_buffers 对视频和音频上下文都要调,漏掉一个就会出现「画面跳过去了但声音还在原处」或者反过来。
第三步,把队列里已经排着的帧丢掉。播放器一般有一到两个队列,seek 之后队列里的老帧如果不清理,会先播半秒旧内容再跳到新位置,看着像是 seek 没生效。这一步在自己的数据结构里做,FFmpeg 管不着。
顺序不能反:先 seek,再 flush,最后清队列。顺序错了会出现花屏,因为解码器还拿着上一个 GOP 的参考帧。
5. 避坑与排查:32 位 MinGW 下最常见的五类翻车
这一节全是踩过的。每一条都按现象、原因、解决三段写,对着查能省不少时间。
5.1 编译期 undefined reference
现象:qmake 跑完,mingw32-make 报一堆 undefined reference to 'avformat_open_input'、'avcodec_alloc_context3'。
原因有三种,按概率排:一是 LIBS 里漏了某个库,最常见是漏 -lswresample,因为视频能跑通就以为音频不用;二是链接顺序不对,-lavutil 写在了前面;三是误用了 MSVC 的 .lib,MinGW 根本不认,但文件在目录里,-l 又能找到同名文件,于是链上了却全是空符号。
解决:先看报错里缺的符号属于哪个库,avformat_ 开头缺 avformat,swr_ 开头缺 swresample。然后确认 LIBS 顺序是 avformat、avcodec、swresample、swscale、avutil。最后用 objdump -f 确认 .dll.a 是 pe-i386。三个都排除了还报错,检查是不是在某个头文件里手动套了 extern "C" 又嵌在 namespace 里,那会导致声明和库里的符号对不上。
5.2 程序启动即退出,或提示找不到 DLL
现象:编译成功,双击 exe 闪一下就没了;或者弹窗说缺少 avcodec-57.dll;或者报 0xc000007b。
原因:0xc000007b 基本可以确定是位数不匹配,32 位 exe 加载了 64 位 DLL,或者反过来。找不到 DLL 则是运行时搜索路径的问题,exe 同目录没有、PATH 里也没有。还有一种隐蔽情况:avcodec-57.dll 找到了,但它依赖 libwinpthread-1.dll,那个没找到,报错信息会指向 avcodec,让你以为是 FFmpeg 的问题。
解决:把 bin 下所有 DLL 复制到 exe 同目录,再用 objdump -p 逐个查 DLL Name,把缺的补上。调试期不要靠改 PATH,PATH 里同时存在多个版本的 FFmpeg 时,加载哪个全看运气,这类问题定位起来极其痛苦。发布时统一走同目录加载,别依赖系统环境。
5.3 画面花屏、绿色斜纹、颜色错乱
现象:能播,但画面有斜向条纹,或者整体偏绿、偏紫,换个分辨率的文件又正常了。
原因:dstStride 写成了 width*4 而不是 QImage::bytesPerLine(),宽度不是 4 的倍数时行对齐不一致。颜色错乱则是像素格式配错,AV_PIX_FMT_RGB32 配了 Format_ARGB32_Premultiplied,或者把 BGR24 当成 RGB24。
解决:stride 一律取 img.bytesPerLine(),源端 linesize 原样传给 sws_scale,不要自己算。像素格式按小端机器的内存排布对:AV_PIX_FMT_RGB32 对应 Format_RGB32,AV_PIX_FMT_BGR24 对应 Format_BGR888(Qt 5.14 之后才有,更早版本只能自己转)。判断花屏属于哪一类有个快招:改变窗口大小不重绘的情况下截图,如果花屏位置固定,是 stride 问题;如果颜色整体不对,是格式问题。
5.4 中文路径打不开文件
现象:avformat_open_input 返回负值,用英文路径一模一样的文件就能打开。
原因:QString 转 QByteArray 用了 toLocal8Bit,在中文 Windows 上得到 GBK 字节,FFmpeg 按 UTF-8 解析路径,找不到文件。
解决:统一 toUtf8。工程里所有传给 FFmpeg 的字符串——文件路径、输出路径、协议 URL——都走同一个转换函数,别这里 toUtf8 那里 toLocal8Bit。封装成一个 QString 到 QByteArray 的小工具,全局只用它,比每次想起来再改可靠得多。
5.5 拖动进度条后花屏或音画不同步
现象:正常播放没问题,一拖进度条就花屏几秒,或者声音和画面彻底错开,越拖越严重。
原因:seek 之后没有 avcodec_flush_buffers,解码器还拿着上一个 GOP 的参考帧去解新位置的数据;或者只 flush 了视频没 flush 音频;再或者自己维护的帧队列没清,旧帧先播出来了。
解决:seek、flush、清队列三件事按顺序做全,视频和音频上下文都要 flush。flush 之后第一帧通常是关键帧,画面会有一次跳变,这是正常的。音画不同步如果 flush 之后仍然存在,检查音频时钟是不是自己累加的样本数,换成设备的 processedUSecs() 就稳了。
6. 发布前的核对:把依赖和版本一次验干净
开发机跑通不代表交付能跑。我现在的习惯是发布前做一次「干净机模拟」:把 exe 和所有 DLL 复制到一个空目录,双击运行,播一个 1080p 视频加一个中文路径的视频,再拖三次进度条。这一步能捞出九成的部署问题。
依赖清单用 objdump 批量导一遍,比一个个点开看快:
# 在 exe 所在目录执行,列出所有 DLL 的对外依赖 for f in *.dll; do echo "== $f" objdump -p "$f" | grep "DLL Name" done | grep -v "KERNEL32\|USER32\|GDI32\|msvcrt\|ADVAPI32\|SHELL32\|ole32\|WINMM"过滤掉系统 DLL 之后,剩下的就是必须随包带的:avcodec-57、avformat-57、avutil-55、swscale-4、swresample-2,按需再加 avfilter-6 和 avdevice-57,以及可能出现的最小运行时依赖。少一个 avutil-55.dll 的现象特别典型——avcodec 加载失败,报错却指向 avcodec,因为 avcodec 依赖 avutil,缺的是下游。
版本核对放在启动日志里,一次 qDebug 打全,现场出问题时让用户把日志发回来,比远程猜快得多:
| 检查项 | 期望值 | 不匹配时的症状 |
|---|---|---|
| avcodec_version() >> 16 | 57 | 接口行为不一致,send/receive 返回值异常 |
| avformat_version() >> 16 | 57 | 打不开某些容器,返回 -1094995529 |
| avutil_version() >> 16 | 55 | 图像转换报参数错误 |
| 进程位数 | 32 位 | 0xc000007b,加载 DLL 失败 |
还有一点,windeployqt 只负责 Qt 自己的库,FFmpeg 的 DLL 它一概不管。跑完 windeployqt 之后必须手动把 bin 下那批 DLL 补进目录,顺序上放在后面做,免得被覆盖。
从那以后我每次交付 32 位客户端,都会先在空目录里跑一遍「中文路径 + 拖进度条 + 播完结尾」这三件事,缺一样都不签字。这套流程救过我至少两次——其中一次是漏了最小运行时依赖,开发机上完全正常。希望帮到你。
本文还有配套的精品资源,点击获取