ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

32位MinGW版FFmpeg 3.4接入Qt与解码编码实践

32位MinGW版FFmpeg 3.4接入Qt与解码编码实践

简介:这是一份面向32位Windows平台、可配合Qt与MinGW环境使用的FFmpeg 3.4轻量级开发包,定位于帮助C/C++开发者接入音视频编解码、转码、封装与解封装能力,适用于播放器、转码工具和媒体处理软件的原型开发与课程实践。压缩包共170个文件,大小约2.62MB,包含107个头文件、21个C源码,以及def、a、dll、lib、pc等链接库与配置类型,另有ffpreset预设、说明文本和少量可执行文件,便于完成编译链接与环境验证,也兼顾开发接口与运行库,适合对资源占用较敏感的场景。预览中的AAC转码、封装、解封装等示例源码,可辅助理解FFmpeg接口调用流程,并结合头文件和导入库用于Qt工程集成、功能裁剪与二次开发。已有229人学习,适合需要快速搭建32位开发环境、查阅示例代码和排查编译链接问题的初中级开发者参考。

1. 32 位 MinGW 版的 FFmpeg 3.4:一个还没退役的组合

接手一套工控上位机的维护时,最先卡住的往往不是业务逻辑,而是工具链。Qt 5.x 配 MinGW 32 位,跑在只装得动 32 位系统的旧机器上,现场要求把本地 MP4 转码、把摄像头流截帧存盘。这时候想用最新 FFmpeg,先得自己搭一套 i686 交叉编译环境啃一整天编译脚本;第三方预编译包又大多来自 MSVC 体系,混进 MinGW 工程要额外一层麻烦。ffmpeg-3.4-mingw32这类资源解决的就是这段落差:一份用 32 位 MinGW 工具链编好的 FFmpeg 3.4 库,头文件、导入库、运行时 DLL 都在,解压进工程就能写avformat_open_input。它适合两类人:被 32 位环境绑死、又需要稳定音视频解码的桌面/Qt 开发者,以及想先把链路跑通、再考虑升级方案的工程实践者。下面按拆包、接入、跑通、踩坑的顺序讲。

2. 拆开压缩包:目录结构、库依赖与上手前的验证

拿到一个预编译库,最忌讳的是直接往工程里一塞然后开始改代码。先花十分钟把包拆明白,后面能省掉大量 "为什么链接不上" 的排查时间。这一章讲清 MinGW 发行版的库文件组织方式、怎么在命令行里确认这套库真的是 32 位、以及 FFmpeg 3.4 在 API 上有哪些和 4.x/5.x 不一样的地方需要提前知道。

2.1 一个 MinGW 发行版的库文件是怎么组织的

常见布局是三个目录:include/、lib/、bin/。include/下是libavcodec/、libavformat/、libavutil/、libswscale/、libswresample/、libavfilter/、libavdevice/、libpostproc/八套头文件目录;lib/下放导入库(import library),MinGW 版本通常同时给.a和.lib两套,内容等价,只是后缀方便不同构建系统识别;bin/下是运行时 DLL,文件名带主版本号,比如avcodec-57.dll、avformat-57.dll、avutil-55.dll、swscale-4.dll、swresample-2.dll。

FFmpeg 的模块之间有严格的依赖方向,链接顺序必须自顶向下:

模块3.4 对应的 DLL依赖谁
libavdeviceavdevice-57.dllavformat、avcodec、avutil
libavfilteravfilter-6.dllavformat、avcodec、swscale、swresample、avutil
libavformatavformat-57.dllavcodec、avutil
libavcodecavcodec-57.dllswresample、avutil
libswscaleswscale-4.dllavutil
libswresampleswresample-2.dllavutil
libavutilavutil-55.dll无

这张表的用处很直接:gcc/ld是单趟扫描符号的,被依赖的库必须写在依赖者的右边。只做解码 + 像素格式转换的话,-lavformat -lavcodec -lswresample -lswscale -lavutil这个顺序就够了;如果顺手加了-lavfilter却放在-lavformat后面,很容易冒出一串undefined reference to 'av_...',而报错的符号其实来自 avformat。

include/目录里还有一个容易忽略的点:3.4 的头文件内部大量使用#include "libavutil/xxx.h"这种相对路径,所以INCLUDEPATH要指到include/这一层,不能指到include/libavcodec/。指错了会看到libavutil/common.h: No such file or directory。

2.2 用命令行验证这套库是不是 32 位、能不能链接

别信目录名,用工具确认。MinGW 环境里objdump和gcc -print-multiarch都可用,一条命令看架构:

# 查看导入库的目标架构,输出里应出现 pe-i386 / i386 字样 objdump -f /path/to/ffmpeg/lib/libavcodec.a | head -n 5 # 查看 DLL 的机器类型,32 位是 0x014c,64 位是 0x8664 objdump -f /path/to/ffmpeg/bin/avcodec-57.dll | head -n 3

如果输出是file format pe-i386或architecture: i386,说明确实是 32 位;出现pe-x86-64或x86-64就是拿错了包,后面gcc -m32链接一定会报file format not recognized或符号找不到。

第二步做一次最小链接验证,别一上来就开 Qt Creator。写个只有main的空程序,把头文件和库全带上:

/* check_link.c —— 只验证头文件和符号能不能对上,不跑逻辑 */ #include <stdio.h> #include <libavformat/avformat.h> #include <libavcodec/avcodec.h> #include <libavutil/avutil.h> int main(void) { /* av_register_all 是 3.x 的初始化入口,4.0 之后被移除 */ av_register_all(); printf("avformat %u.%u.%u\n", LIBAVFORMAT_VERSION_MAJOR, LIBAVFORMAT_VERSION_MINOR, LIBAVFORMAT_VERSION_MICRO); return 0; }
# 编译 + 链接 + 运行,一条命令验证整条链路 gcc -m32 check_link.c -o check_link.exe \ -I/path/to/ffmpeg/include \ -L/path/to/ffmpeg/lib \ -lavformat -lavcodec -lswresample -lswscale -lavutil # 把 bin 目录加进 PATH 再运行,否则会弹 "找不到 avformat-57.dll" PATH=/path/to/ffmpeg/bin:$PATH ./check_link.exe

这段验证的价值在于把问题分层。编译报错说明INCLUDEPATH不对;链接报错说明库路径、库名或顺序不对;编译链接都过、一运行就闪退说明 DLL 没找到。三个阶段的错误信息完全不同,分开验证比在 Qt 里对着collect2.exe: error: ld returned 1 exit status猜要快得多。

参数上唯一需要留意的是-m32:MinGW-w64 的 32 位发行版(比如常见的 i686 前缀工具链)默认就产出 32 位,加不加都行;如果是 x86_64 前缀的工具链,-m32需要 multilib 支持,多数发行版并不带,这时候别硬扛,换 32 位工具链更省事。

2.3 版本 3.4 的 API 边界:哪些函数还能用、哪些已经没了

这不是一篇 API 历史考据,但有几处差异必须在动手前记住,否则从网上抄来的代码有一半编译不过。3.4 属于 3.x 分支末期,处在老 API 和新 API 并存的阶段:

av_register_all()、avformat_network_init()仍然需要显式调用,4.0 起前者被移除、后者变成可选。解码仍然可以用avcodec_decode_video2(ctx, frame, &got, pkt),这个函数在 3.4 里正常工作(4.0 标记废弃,5.0 移除);同时avcodec_send_packet/avcodec_receive_frame在 3.4 里也已经可用,两者可以混着理解,但不要在同一个解码上下文里混用。

流参数从AVStream::codec(旧的AVCodecContext*)迁移到AVStream::codecpar(AVCodecParameters*)这件事在 3.4 已经完成,必须走avcodec_parameters_to_context(ctx, stream->codecpar)这条路,直接读stream->codec会编译报错——这是从 2.x 时代老教程抄代码时最常见的一处断裂。

AVPacket的内存管理也换了:av_packet_alloc()/av_packet_free()配套使用,读帧后要av_packet_unref();老的av_init_packet+ 手动av_free_packet虽然还能编译,但容易漏掉buf引用计数,在长时间运行的转码循环里表现为内存缓慢上涨。我一般在新代码里一律用 alloc/free 那套,老接口只在维护存量代码时保留。

提示:如果你手上的第三方教程出现avcodec_encode_video、avcodec_decode_audio3、avcodec_alloc_frame,那是 1.x 时代的接口,3.4 里已经全部不存在,照抄必然编译失败,别浪费时间怀疑环境。

3. 接到 Qt 工程里:.pro 配置、链接顺序与编译宏

Qt + MinGW 的组合在 Windows 上有个绕不开的现实:qmake 生成的是 MinGW 的 Makefile,最终调用的是g++和ld,所以对库的要求跟命令行验证时一致。但.pro文件里的LIBS展开顺序、C 头文件在 C++ 里的编译宏、以及运行时 DLL 的部署位置,这三件事各有各的坑。这一章把它们逐个拆开。

3.1 .pro 文件的完整写法与链接顺序原理

一个能用的.pro长这样:

# 工程基础配置 QT += core gui CONFIG += c++11 console TARGET = player_demo TEMPLATE = app # 头文件搜索路径,指到 include 这一层 INCLUDEPATH += $$PWD/third_party/ffmpeg/include # 库搜索路径 + 链接顺序:上层在前,底层在后 LIBS += -L$$PWD/third_party/ffmpeg/lib \ -lavformat \ -lavcodec \ -lswresample \ -lswscale \ -lavutil

$$PWD是.pro文件所在目录,用相对路径的好处是工程换机器不用改配置。三个参数层面要说清楚:

CONFIG += console在 Windows 下会保留控制台窗口,调试阶段非常有用——FFmpeg 的日志通过av_log_set_callback输出的内容默认打到 stderr,没有控制台就只能靠写文件。发布时再去掉这一行。

INCLUDEPATH只写一个include根目录,不要在代码里写#include "libavcodec/avcodec.h"之外的路径前缀,头文件内部的相对引用才能正常展开。

LIBS里的-L和-l顺序不能颠倒,也不可能靠"多写几遍"来糊弄。我见过有人在末尾追加一遍-lavcodec来"修复"未定义符号,在ld的单趟扫描规则下,这种补丁偶尔生效偶尔不生效,取决于符号在库内的物理位置,属于典型的玄学修复。正确做法是按 2.1 那张依赖表排好序,一次到位。

3.2 头文件引入方式与__STDC_CONSTANT_MACROS

C++ 工程里引 C 头文件必须包extern "C",否则 C++ 的名字修饰规则会让链接器找不到av_register_all这类符号。标准写法是一个集中头文件:

/* ffmpeg_headers.h —— 全工程只在这里包一次 extern "C" */ #ifndef FFMPEG_HEADERS_H #define FFMPEG_HEADERS_H /* 3.x 在 C++ 下编译需要这个宏,否则 UINT64_C 等宏未定义 */ #ifndef __STDC_CONSTANT_MACROS #define __STDC_CONSTANT_MACROS #endif extern "C" { #include <libavformat/avformat.h> #include <libavcodec/avcodec.h> #include <libavutil/avutil.h> #include <libavutil/imgutils.h> #include <libavutil/opt.h> #include <libswscale/swscale.h> #include <libswresample/swresample.h> } #endif /* FFMPEG_HEADERS_H */

__STDC_CONSTANT_MACROS这个宏不是可选项。FFmpeg 的libavutil/common.h里有INT64_C、UINT64_C这类宏,C++ 标准规定这些宏在 C++ 模式下默认不定义,只有显式开启__STDC_CONSTANT_MACROS(C++11 之前)或__STDC_LIMIT_MACROS才可见。不定义它,报错信息是'UINT64_C' was not declared in this scope,看起来跟 FFmpeg 毫无关系,新手很容易在这里卡半天。

另一种做法是在.pro里全局加DEFINES += __STDC_CONSTANT_MACROS,效果一样。我倾向于写在头文件里,因为这样这个依赖跟着头文件走,别人把源文件拷走也能编过。

3.3 运行时 DLL 部署与调试器配置

编译链接通过只是第一步,运行阶段最常见的是进程一启动就退出、没有任何提示。原因基本只有一个:avformat-57.dll这一串 DLL 不在进程的搜索路径里。

Qt Creator 默认工作目录是构建目录,PATH继承自 IDE 启动时的环境。三种部署方式,按场景选:

开发阶段最省事的做法是在.pro里加拷贝规则,每次构建把 DLL 同步到输出目录:

# 构建后自动拷贝 FFmpeg 运行库到可执行文件旁边 FFMPEG_BIN = $$PWD/third_party/ffmpeg/bin win32 { DLLS = $$files($$FFMPEG_BIN/*.dll) for(dll, DLLS) { QMAKE_POST_LINK += $$quote(cmd /c copy /y \"$$shell_path($$dll)\" \"$$shell_path($$OUT_PWD/release)\" $$escape_expand(\\n\\t)) } }

$$files()是 qmake 的通配函数,QMAKE_POST_LINK在链接完成后执行,$$OUT_PWD是构建输出目录。注意$$escape_expand(\\n\\t)的作用是让多条命令在生成的 Makefile 里分行,不加会被拼成一行导致第二条失败。

发布阶段则把所有 DLL 和 exe 放在同一目录,或者用windeployqt把 Qt 的运行库一起收拢。这里有个细节:windeployqt只处理 Qt 自己的依赖,FFmpeg 的 DLL 要自己拷。

调试器配置是另一件事。MinGW 的gdb在加载 FFmpeg 时偶尔会因为缺少调试信息而在某些栈帧上显示??,这是正常的——预编译库不带.debug段。如果需要在崩溃时定位到 FFmpeg 内部,可以临时把bin/下的avcodec-57.dll换成带调试符号的版本,但工程里通常不需要。

注意:不要把ffmpeg/bin直接加进系统环境变量PATH来"解决" DLL 问题。同一台机器上如果还有别的软件带着不同版本的 FFmpeg DLL,会按 PATH 顺序加载,先命中的那个版本可能就是错的,表现为程序在别人机器上能跑、在原机器上崩。DLL 跟 exe 同目录是最稳的顺序。

4. 跑通第一条解码链路:从 MP4 到 QImage

配置通了,接下来验证这套库到底能不能干活。选解码是因为它最容易观察到结果——一帧画面出来了就是成了,不出来就是哪里错了,没有中间态。这一章按 3.4 的 API 走一遍完整解码序列,然后处理sws_scale转 RGB32 时的行对齐问题,最后反过来把一帧 QImage 推成 H.264,验证编码方向也能用。

4.1 3.4 版的解码调用序列

一次完整的解码由固定的六步构成,顺序错了任何一步都会返回负数:

  1. avformat_open_input打开容器,avformat_find_stream_info补齐流参数;
  2. av_find_best_stream找到视频流的索引和对应的AVCodec;
  3. avcodec_alloc_context3建解码上下文,avcodec_parameters_to_context把codecpar里的参数灌进去,avcodec_open2打开;
  4. 循环av_read_frame读包,只处理视频流,丢给avcodec_decode_video2;
  5. got_picture为真时拿到AVFrame,交给sws_scale转格式;
  6. 收尾阶段给解码器送一个空包 flush,把 B 帧缓冲的帧取出来,然后逐级释放。

第 6 步很容易被漏掉。H.264 有 B 帧,解码器内部会缓存若干帧等参考帧到齐,文件读到 EOF 时缓冲区里还压着几帧。不 flush 的话,一个 10 秒的片子可能只解出前 9 秒多,最后几帧永远出不来。3.4 里的做法是把一个空的AVPacket(data = NULL, size = 0)送进avcodec_decode_video2,直到返回 0 且got_picture为假。

4.2 完整代码:封装一个最简解码器类

把上面的流程封成类,方便后面复用。这里给的是最简版本,去掉了业务逻辑,只保留解码和转 RGB:

/* simple_decoder.h */ #ifndef SIMPLE_DECODER_H #define SIMPLE_DECODER_H #include <QImage> #include <QString> #include "ffmpeg_headers.h" class SimpleDecoder { public: SimpleDecoder(); ~SimpleDecoder(); bool open(const QString &filePath); // 打开文件,成功返回 true bool nextFrame(QImage &outImage); // 取下一帧并转 RGB32,无帧返回 false void close(); private: bool decodePacket(AVPacket *pkt, QImage &outImage); AVFormatContext *m_fmtCtx = nullptr; AVCodecContext *m_codecCtx = nullptr; int m_streamIdx = -1; AVPacket *m_packet = nullptr; AVFrame *m_frame = nullptr; SwsContext *m_swsCtx = nullptr; bool m_eof = false; // 文件读完后进入 flush 阶段 }; #endif
/* simple_decoder.cpp —— 核心解码逻辑 */ #include "simple_decoder.h" SimpleDecoder::SimpleDecoder() {} SimpleDecoder::~SimpleDecoder() { close(); } bool SimpleDecoder::open(const QString &filePath) { /* 3.x 必须显式注册,4.0 之后移除 */ av_register_all(); /* Windows 下路径要转本地编码,直接传 QString 的 utf8 会失败 */ QByteArray path = filePath.toLocal8Bit(); if (avformat_open_input(&m_fmtCtx, path.constData(), nullptr, nullptr) != 0) return false; if (avformat_find_stream_info(m_fmtCtx, nullptr) < 0) return false; /* 找到最好的视频流,同时拿到解码器 */ const AVCodec *codec = nullptr; m_streamIdx = av_find_best_stream(m_fmtCtx, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0); if (m_streamIdx < 0 || !codec) return false; m_codecCtx = avcodec_alloc_context3(codec); /* 3.x 必须走 codecpar 拷贝,stream->codec 已不可用 */ avcodec_parameters_to_context(m_codecCtx, m_fmtCtx->streams[m_streamIdx]->codecpar); /* 单线程解码,线程数设 0 让 FFmpeg 自己决定;调试时可固定为 1 便于定位 */ m_codecCtx->thread_count = 1; if (avcodec_open2(m_codecCtx, codec, nullptr) < 0) return false; m_packet = av_packet_alloc(); m_frame = av_frame_alloc(); return m_packet && m_frame; } bool SimpleDecoder::decodePacket(AVPacket *pkt, QImage &outImage) { int got = 0; /* 3.4 里 avcodec_decode_video2 仍可用,返回值为消耗的字节数 */ int ret = avcodec_decode_video2(m_codecCtx, m_frame, &got, pkt); if (ret < 0 || !got) return false; /* 第一帧拿到后再建 sws 上下文,避免拿不到 pix_fmt 时尺寸未知 */ if (!m_swsCtx) { m_swsCtx = sws_getContext(m_frame->width, m_frame->height, static_cast<AVPixelFormat>(m_frame->format), m_frame->width, m_frame->height, AV_PIX_FMT_BGRA, SWS_BILINEAR, nullptr, nullptr, nullptr); if (!m_swsCtx) return false; } /* Format_RGB32 在小端机器上的内存布局就是 BGRA,正好对应 AV_PIX_FMT_BGRA */ outImage = QImage(m_frame->width, m_frame->height, QImage::Format_RGB32); uint8_t *dstData[4] = { outImage.bits(), nullptr, nullptr, nullptr }; /* 关键:用 bytesPerLine() 而不是 width*4,Qt 会按 4 字节对齐补行尾 */ int dstLinesize[4] = { static_cast<int>(outImage.bytesPerLine()), 0, 0, 0 }; sws_scale(m_swsCtx, m_frame->data, m_frame->linesize, 0, m_frame->height, dstData, dstLinesize); return true; } bool SimpleDecoder::nextFrame(QImage &outImage) { while (true) { if (!m_eof) { int ret = av_read_frame(m_fmtCtx, m_packet); if (ret < 0) { /* 读完了,进入 flush 阶段,送空包把缓冲的帧挤出来 */ m_eof = true; av_packet_unref(m_packet); m_packet->data = nullptr; m_packet->size = 0; if (decodePacket(m_packet, outImage)) return true; return false; } if (m_packet->stream_index != m_streamIdx) { av_packet_unref(m_packet); /* 音频包等直接丢,不释放会漏内存 */ continue; } bool ok = decodePacket(m_packet, outImage); av_packet_unref(m_packet); if (ok) return true; } else { /* flush 阶段持续送空包,直到解码器不再吐帧 */ if (decodePacket(m_packet, outImage)) return true; return false; } } } void SimpleDecoder::close() { if (m_swsCtx) { sws_freeContext(m_swsCtx); m_swsCtx = nullptr; } if (m_frame) { av_frame_free(&m_frame); } if (m_packet) { av_packet_free(&m_packet); } if (m_codecCtx) { avcodec_free_context(&m_codecCtx); } if (m_fmtCtx) { avformat_close_input(&m_fmtCtx); } }

几处参数值得单独说。thread_count = 1不是性能建议,而是调试手段:多线程解码下avcodec_decode_video2的返回时机和帧序在某些编码上会变,定位花屏问题时先单线程复现,确认是解码逻辑问题还是线程调度问题。

toLocal8Bit()也不能省。avformat_open_input在 Windows 上走的是fopen系的窄字符接口,接收的是本地 ANSI 编码。直接传QString::toUtf8()的字节,路径里有中文时打开失败,报的错是No such file or directory,但文件明明存在——这个坑非常经典。

m_packet->data = nullptr这种写法利用了av_packet_unref之后data已被置空、size归零的特性,实际上av_packet_unref会把整个包重置成一个合法的空包,可以直接复用。不要在这里av_packet_free再av_packet_alloc,循环里频繁分配释放会拖慢整体吞吐。

4.3sws_scale转 RGB32 与行对齐

sws_scale的签名里最关键的是 source/destination 的linesize数组。FFmpeg 的AVFrame每一行之间可能有 padding(用于 SIMD 对齐),所以frame->linesize[0]通常大于width * 4;Qt 的QImage出于同样的原因,bytesPerLine()也可能大于width * 4。

两边都用各自的真实行宽,sws_scale才知道每一行从哪开始。如果这里图省事写成:

int dstLinesize[4] = { m_frame->width * 4, 0, 0, 0 }; // 错误示范

当width * 4恰好是 4 的倍数但bytesPerLine()里带了额外对齐时,输出图右侧会出现一条宽度不固定的彩色竖带——因为sws_scale按错误的行宽往右写,把行尾的 padding 也当成了有效像素。4:3、16:9 这类常见比例大多凑巧没事,换成 1:1 或者奇数宽度的素材就翻车,所以从一开始就按正确写法来。

像素格式的选择上,AV_PIX_FMT_BGRA对应QImage::Format_RGB32(小端机器),AV_PIX_FMT_RGBA对应QImage::Format_ARGB32(注意 Qt 的 ARGB32 在小端内存里也是 BGRA 顺序,容易混淆)。如果希望减少一次通道交换,AV_PIX_FMT_RGB32这个宏在 FFmpeg 里就是"本地字节序的 RGB32",跟QImage::Format_RGB32的内存布局完全一致,可以直接用。

提示:sws_scale返回的是输出行数,正常应等于m_frame->height。如果返回值小于高度,通常是目标缓冲区不够大或者dstLinesize设错了,别直接当成功处理。

4.4 编码方向:把 QImage 推成 H.264

解码跑通之后,编码方向值得一起验证,因为这套 3.4 的库如果只带解码器没带编码器(有些精简发行版会裁),要到这一步才会发现。检查方式是avcodec_find_encoder(AV_CODEC_ID_H264)返回非空。

编码侧的关键在于像素格式。libx264只接受 YUV420P(或 NV12 等),QImage 是 RGB32,所以要多一次sws_scale做 RGB → YUV420P 的转换,而且 YUV420P 的 U/V 平面是width/2 × height/2,宽高必须是偶数,奇数尺寸的图要先裁到偶数。

/* 编码一帧的骨架,上下文创建部分省略,重点看转换和送帧顺序 */ AVFrame *yuvFrame = av_frame_alloc(); yuvFrame->format = AV_PIX_FMT_YUV420P; yuvFrame->width = srcImage.width() & ~1; /* 向下取偶 */ yuvFrame->height = srcImage.height() & ~1; av_frame_get_buffer(yuvFrame, 32); /* 32 字节对齐,x264 要求 */ SwsContext *toYuv = sws_getContext( srcImage.width(), srcImage.height(), AV_PIX_FMT_BGRA, yuvFrame->width, yuvFrame->height, AV_PIX_FMT_YUV420P, SWS_BILINEAR, nullptr, nullptr, nullptr); const uint8_t *srcData[4] = { srcImage.bits(), nullptr, nullptr, nullptr }; int srcLinesize[4] = { static_cast<int>(srcImage.bytesPerLine()), 0, 0, 0 }; sws_scale(toYuv, srcData, srcLinesize, 0, yuvFrame->height, yuvFrame->data, yuvFrame->linesize); /* 3.4 编码用 avcodec_encode_video2,和 avcodec_decode_video2 对称 */ AVPacket *pkt = av_packet_alloc(); int got = 0; avcodec_encode_video2(encCtx, pkt, yuvFrame, &got); if (got) { /* 必须设置 pts,否则 x264 报 "non-strictly-monotonic PTS" */ pkt->pts = frameIndex++; av_interleaved_write_frame(outFmtCtx, pkt); av_packet_unref(pkt); }

av_frame_get_buffer的第二个参数是对齐字节数,传 32 是给 SIMD 优化留空间,传 1 也能跑但性能下降。pkt->pts必须单调递增,3.4 里avcodec_encode_video2不帮你算,自己维护一个帧计数器最省心。时间基换算在avcodec_parameters_to_context之后用encCtx->time_base = {1, fps}显式设好,别指望默认值——默认值不一定和容器时间基一致,写出来的文件播放速度会不对。

5. 避坑与排查:32 位 MinGW 下最常翻车的五件事

前面几章讲的是"应该怎么做",这一章讲"做不成的时候怎么查"。下面五条都是在实际工程里反复出现的问题,按现象、原因、解决的顺序写,可以当检查表用。

5.1 链接期:undefined reference的三种来源

现象:编译通过,链接阶段冒出一屏undefined reference to 'avformat_open_input'或者'av_register_all',collect2.exe: error: ld returned 1 exit status。

原因分三类,按出现频率排。第一类是LIBS顺序反了,-lavutil写在-lavformat前面,ld扫描到 avformat 时 avutil 已经处理完,符号没被引用就被丢弃。第二类是库路径压根没写对,-L指到了lib/的上一级或者指到了一个空目录,ld找不到libavformat.a就跳过,不报"库不存在"而报"符号未定义",非常有误导性。第三类是真拿错了包,库是 MSVC 编译的.lib,MinGW 的ld能识别 PE 格式但不认 MSVC 的名字修饰约定,链接时报的符号名对不上。

解决:先按 2.2 的命令行验证法在终端里跑一遍,把 Qt 排除在外。命令行能过就是.pro的问题,命令行走不过就是库的问题。检查顺序固定为:-L路径存在且有.a文件 → 库名拼写(-lavformat对应libavformat.a)→ 顺序按依赖表排 → 是否混入了 MSVC 的库。

5.2 运行期:缺 DLL、0xc000007b与路径中文

现象:编译链接全部通过,双击 exe 或从 Qt Creator 启动,进程毫无反应地退出,或者弹一个0xc000007b的错误框。

原因:进程启动时加载器找不到avformat-57.dll,不同 Windows 版本的表现不一样——有的静默退出,有的报模块错误。0xc000007b是 32/64 位不匹配的典型码,说明找到的 DLL 是 64 位的,或者依赖链里某一环是 64 位。还有一个隐蔽来源:exe 同目录下混进了另一套 FFmpeg 的 DLL(比如别的软件留下的),版本号相同但编译方式不同,加载后在某些函数指针上崩溃。

解决:exe 目录下必须凑齐一套同源的 DLL,不要靠系统PATH。用dumpbin /dependents或者 MinGW 的objdump -p xxx.exe | grep "DLL Name"看依赖列表,逐个核对。0xc000007b时用objdump -f检查每个 DLL 的架构,找出那个 x86-64 的。中文路径的问题在 4.2 已经提过,统一转toLocal8Bit()。

5.3 解码期:花屏、绿边与 B 帧延迟

现象:画面能出来,但右侧有彩色竖条,或者整帧偏绿,或者视频播到倒数几秒就没了。

原因:绿边是sws_scale的目标linesize用错了(4.3 讲过),QImage的bytesPerLine()大于width * 4时,按width * 4写会把行尾的 padding 区域覆盖成相邻行的数据,表现为右侧一条歪斜的彩带。整帧偏绿通常是像素格式对错了通道顺序,把AV_PIX_FMT_BGRA和AV_PIX_FMT_RGBA搞混,红蓝通道交换,人脸发青、天空发红。最后几秒丢失是没做 flush,解码器缓冲区里的帧没取出来。

解决:目标linesize一律用QImage::bytesPerLine();像素格式对照 4.3 那张小表确认;nextFrame在av_read_frame返回小于 0 之后必须持续送空包直到不再出帧,代码骨架见 4.2 的nextFrame。

5.4 内存期:32 位地址空间与 4K 素材

现象:处理小分辨率素材一切正常,换到 1080p 以上的长视频,跑几分钟后崩,崩溃点随机,有时在av_read_frame,有时在 Qt 的图像绘制里。

原因:32 位进程的用户态地址空间只有 2GB 左右,还要被 Qt、显卡驱动、系统 DLL 占掉一块。一帧 4K 的 BGRA 是3840 × 2160 × 4 ≈ 31.6MB,加上 YUV 原始帧、sws_scale的内部缓冲、解码器的参考帧队列,单帧开销能到 100MB 量级。如果每帧都 new 一个QImage又不及时释放,堆碎片化之后即使总量没到 2GB 也会分配失败。

解决:三件事。第一,sws_getContext只创建一次,不要每帧建了再销毁——这个上下文内部有缓冲区,反复创建销毁是内存碎片的头号来源。第二,QImage复用同一个对象,SimpleDecoder那种每次outImage = QImage(...)的写法在 demo 里没问题,正式工程应该让调用方传一个预先分配好的缓冲区,或者用帧池管理。第三,真的需要处理 4K 素材时,先把分辨率降到 720p 解码(sws_scale里目标宽高设小),或者干脆评估 64 位工具链的可行性。

5.5 编码期:时间戳与 GOP 设置

现象:编码出来的 H.264 文件能播,但播放器上拖动进度条位置不对,或者画面卡顿、帧率明显不对。

原因:pkt->pts没设、设成 0、或者没有单调递增。avcodec_encode_video2在 3.4 里不做自动推算,pts缺省是AV_NOPTS_VALUE,落进文件后播放器只能按码率猜位置,拖动就不准。另一种情况是time_base没跟容器对齐,编码器用1/25、容器用1/1000,写出去的时间戳差 40 倍,播放速度直接错乱。

解决:encCtx->time_base = AVRational{1, fps}显式设置,pkt->pts = frameIndex++维护单调计数,av_interleaved_write_frame之前不要手动改pts。GOP 相关的encCtx->gop_size一般设成fps * 2(两秒一个关键帧),设太大会导致拖动时定位慢,设太小文件体积涨。另外encCtx->max_b_frames在需要低延迟的场合(比如实时推流)设成 0,避免编码器内部再引入一层缓冲延迟。

6. 进阶:把这套老库用稳的几个习惯

围绕 3.4 这个版本做工程,最实用的习惯是给运行环境加一层"自检"。启动时用avcodec_configuration()打印一遍编译配置,确认--enable-libx264、--enable-decoder=h264这些关键开关真的开着——精简发行版经常裁掉编码器,等到用户点了"导出"才报错就太晚了。再配一份avcodec_find_decoder_by_name("h264")/avcodec_find_encoder(AV_CODEC_ID_H264)的存在性检查,缺哪个就在启动阶段提示,而不是让程序跑到一半崩。

另一个习惯是给日志分级。av_log_set_level(AV_LOG_WARNING)在发布版里避免日志刷屏,调试时调到AV_LOG_DEBUG,并且用av_log_set_callback把输出接到 Qt 的日志系统里,这样 FFmpeg 内部报的Invalid NAL unit size、non-existing PPS这类信息才能和业务日志对上时间线,排查现场问题时有据可查。

帧池的做法值得单独提一句:维护一个固定大小的QImage队列,解码器复用队首对象写入,消费端用完归还。在 32 位地址空间紧张的机器上,这一招能把长时间运行的崩溃率压下来一大截。参数上,池大小取 3~5 帧就够,太多反而占内存。

我最早接这套库的时候,图省事跳过了 2.2 那个命令行验证,直接在 Qt Creator 里改.pro,结果在一个链接顺序上耗了整整一个下午——报错的符号指向 avcodec,实际问题出在 avformat 的位置。从那以后我每次拿到预编译库,都强制先在终端里用最小main跑一遍编译链接运行三连,十分钟的事,能把后面几小时的不确定性砍掉。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表