简介:面向音视频开发者的 FFmpeg n5.1.2 预编译开发库,适用于 Windows 64 位环境下进行音视频编解码、转封装、转码、滤镜处理与流媒体分析,也可作为学习编码器库、容器格式库、过滤器库和通用工具库等核心模块的实践基础。压缩包共 377 个文件、约 9.12MB,以 274 个头文件和 23 个源文件为主体,配合动态链接库、静态库及 pkg-config 配置文件,分别对应接口声明、程序链接与工程集成,另含命令行程序、编码预设和手册页,目录按可执行文件、头文件、库文件与共享资源分区组织,结构清晰,便于接入现有工程或按需裁剪。目前已有 2464 人学习。读者可快速搭建可编译、可链接的开发环境,结合 MinGW64 编译配置说明理解构建流程,并借助数据包、格式上下文、编解码上下文与帧对象等核心概念,完成直播推流、本地媒体转换和实时视频分析等任务。
1. 拿到一份 FFmpeg n5.1.2 开发库,先别急着编译
做 FFmpeg 音视频开发的人,第一次尝试基本都花在“把库编出来”这件事上:装工具链、拉源码、等半小时、最后卡在某个 configure 报错。所以一份已经编好的 n5.1.2 开发库,价值不在于省那几十分钟,而在于它把“编译环境”这个变量从你的问题里彻底拿掉了——你的代码跑不通,就一定是你代码或链接配置的问题,不会出现“到底是编译器的问题还是 API 的问题”这种黑匣子局面。
这份资源就是标准的预编译开发库:头文件、导入/静态库、动态库三件套,覆盖常见的桌面平台和编译器组合。适合两类人:一是刚接触音视频、想先跑通“打开文件→解出一帧”这条链路的新手;二是手上有个小工具要做转码、截图、抽帧,不想为此维护一套 FFmpeg 构建脚本的熟手。不适合的是要改 FFmpeg 内部行为、自己裁解码器的人——那种场景还是得自己编。下面按“认清文件→接进工程→跑通解码→避开坑”的顺序走一遍。
2. 拆开压缩包:include 与 lib 里到底装了什么
很多人解压完看一眼目录就急着写 CMakeLists,结果链接阶段一堆“未定义符号”,回头再翻文件已经分不清哪个.lib是导入库、哪个是静态库。先把这三类文件的关系理清楚,后面能省掉一半排查时间。
2.1 三个目录对应三种文件形态
开发库的目录结构基本是固定的,只是不同发行包命名略有差异:
| 目录 | 内容 | 作用 |
|---|---|---|
include/ | libavcodec/、libavformat/、libavutil/、libswscale/、libswresample/、libavfilter/等子目录 | 编译期需要的头文件 |
lib/ | avcodec.lib、libavcodec.so、libavcodec.a、libavcodec.dll.a等 | 链接期需要的库文件 |
bin/ | avcodec-59.dll之类的动态库 | 运行期需要的动态库(Windows 上尤为关键) |
关键区分点在lib/里。静态库是把代码整个塞进你的可执行文件,体积大但不依赖外部文件;动态库的导入库(import library)只有符号跳转表,真实代码在bin/的 DLL 或系统的.so里。Windows 上两者的扩展名都是.lib,光看名字分不出来——导入库通常只有几十 KB,静态库动辄几 MB。我一般先看一眼文件大小,几十 KB 的那些就是导入库,必须配合bin/里的 DLL 一起用;几 MB 那些才是静态库,链接时要把依赖链全部列出来。
Linux 下就直观得多:libavcodec.so是链接时用的符号链接,libavcodec.so.59是真实文件,libavcodec.a是静态库。有些包只给.so.59没给.so软链,这时候-lavcodec会直接找不到库,得自己补一条软链或者改成写全路径。
提示:不要在同一工程里混用静态库和动态库。一半静态链接 avcodec、一半动态加载 avformat,最后会出现两套
libavutil全局状态,运行时报的错会非常难判断。
2.2 n5.1.2 这个版本号真正的含义
n5.1.2里的n是 FFmpeg 官方发布 tag 的前缀习惯,5.1.2依次是主版本、次版本、修订号。这个编号对你最直接的影响在 ABI 层:FFmpeg 的各个子库有自己的 soname 主版本号,同一个大版本内不变。也就是说,5.1.x系列内部的库文件二进制兼容,你换一个 5.1.3 的 DLL 理论上不用重编程序;但跨到 6.x,soname 变了,DLL 文件名跟着变,程序启动就会直接找不到库。
API 层的变化比 ABI 更值得注意。5.x 这一代砍掉了一批老接口,网上大量 2016~2019 年的教程代码在这份库上根本编不过。常见的新旧对应关系如下:
| 老写法 | n5.1.2 里的状态 |
|---|---|
av_register_all()/avcodec_register_all() | 已移除,不需要注册,直接调avcodec_find_decoder |
avcodec_decode_video2()/avcodec_decode_audio4() | 已移除,改用avcodec_send_packet+avcodec_receive_frame |
avcodec_encode_video2()/avcodec_encode_audio2() | 已移除,改用avcodec_send_frame+avcodec_receive_packet |
AVCodecContext::channels/channel_layout | 已弃用,改用ch_layout(AVChannelLayout结构体) |
swr_alloc_set_opts() | 已弃用,改用swr_alloc_set_opts2() |
av_init_packet()手动初始化 | 不再使用,统一走av_packet_alloc()+av_packet_unref() |
这张表建议直接贴在调试时看得到的墙上。绝大多数“照着教程写编译不过”的情况,答案都在这六行里。
2.3 三条命令验明正身
解压之后别急着写代码,先用两条命令确认库的真实形态,尤其是别人转手给你的包,可能被谁替换过文件。
Linux 下用readelf和nm:
# 1) 看 SONAME,确认这份 .so 期望被以什么名字加载 readelf -d lib/libavcodec.so.59 | head -5 # 输出里会有一行 SONAME Library soname: [libavcodec.so.59] # 2) 确认关键符号存在(5.x 的 send/receive 接口) nm -D --defined-only lib/libavcodec.so.59 | grep -E 'avcodec_(send_packet|receive_frame)' # 两个符号都出现在输出里,说明这份库确实是 5.x 的 API # 3) 确认头文件与库版本对得上 grep -E 'LIBAVCODEC_VERSION_(MAJOR|MINOR|MICRO)' include/libavcodec/version.hWindows 下换成dumpbin,逻辑一样:
# 查看导入库导出了哪些符号 dumpbin /exports lib\avcodec.lib | findstr "avcodec_send_packet" # 查看 DLL 的依赖,确认它依赖的 avutil 版本号与目录里的一致 dumpbin /dependents bin\avcodec-59.dll第三步的version.h最容易被忽略。有些包的include/和lib/是分批拷进来的,头文件是 5.1.2、库文件却是 5.0 的,编译期一切正常,运行到某个新加的字段就崩。头文件里的LIBAVCODEC_VERSION_MAJOR应该和bin/里 DLL 的主版本号对得上(59 对 59),对不上就说明这个包是拼出来的。
3. 把库接进工程:CMake 与 IDE 两条路
库认清了,接下来是把它接进你自己的工程。这一步翻车率最高,原因往往不是 CMake 写错,而是链接顺序、库类型选择、头文件包含路径这三件事里有一件没搞对。
3.1 CMakeLists 里的查找、包含与链接顺序
我一般不用find_package去碰运气找系统里的 FFmpeg——一旦机器上装过别的版本,找到的就不是这份。直接把库根目录写死成相对路径,工程自带一份,谁拉下来都能编。
cmake_minimum_required(VERSION 3.16) project(ffdemo LANGUAGES CXX) # 1) 开发库根目录,跟着工程一起走 set(FFMPEG_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/ffmpeg-n5.1.2") # 2) 头文件目录有两种常见布局:include/ 或 include/ffmpeg/ if(EXISTS "${FFMPEG_ROOT}/include/libavcodec/avcodec.h") set(FFMPEG_INCLUDE_DIR "${FFMPEG_ROOT}/include") elseif(EXISTS "${FFMPEG_ROOT}/include/ffmpeg/libavcodec/avcodec.h") set(FFMPEG_INCLUDE_DIR "${FFMPEG_ROOT}/include/ffmpeg") else() message(FATAL_ERROR "找不到 avcodec.h,检查 FFMPEG_ROOT 是否指对") endif() # 3) 按依赖顺序列出组件:被依赖的放后面 set(FF_COMPONENTS avformat avcodec swscale swresample avfilter avutil) set(FF_LIBS "") foreach(comp ${FF_COMPONENTS}) find_library(FF_LIB_${comp} NAMES ${comp} lib${comp} HINTS "${FFMPEG_ROOT}/lib" NO_DEFAULT_PATH) # 只在指定目录找,避免找到系统里的版本 if(NOT FF_LIB_${comp}) message(FATAL_ERROR "缺少组件: ${comp}") endif() list(APPEND FF_LIBS ${FF_LIB_${comp}}) endforeach() add_executable(ffdemo main.cpp) target_include_directories(ffdemo PRIVATE ${FFMPEG_INCLUDE_DIR}) target_link_libraries(ffdemo PRIVATE ${FF_LIBS}) # 4) Windows 下把 DLL 拷到可执行文件旁边,省得手动配 Path if(WIN32) file(GLOB FF_DLLS "${FFMPEG_ROOT}/bin/*.dll") add_custom_command(TARGET ffdemo POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${FF_DLLS} $<TARGET_FILE_DIR:ffdemo>) endif()这段脚本里有三个参数值得解释。NO_DEFAULT_PATH是关键,加上它以后find_library只在HINTS指定的目录里找,不会因为机器上装了另一份 FFmpeg 而链接到错误版本——这是“我的机器上能编、同事机器上编不过”最常见的原因。FF_COMPONENTS的顺序就是静态链接时的依赖顺序:avformat用到avcodec,avcodec用到avutil,swscale依赖swresample和avutil,所以avutil必须放最后。POST_BUILD的拷贝命令只对 Windows 有意义,Linux 和类 Unix 平台走的是运行时库搜索路径,不需要这一步。
动态链接时这个顺序其实无所谓,因为.so/.dll自带依赖信息;静态链接时顺序错了就是成片的undefined reference。如果你只拿到静态库又不想调顺序,Linux 下可以用-Wl,--start-group ... -Wl,--end-group把库包起来让链接器反复扫描,代价是链接变慢,工程大了会明显。
3.2 头文件与旧 API 的取舍
包含路径定了之后,头文件的写法有两个细节。第一,FFmpeg 的头文件里已经带了extern "C"保护,在 C++ 里直接#include <libavcodec/avcodec.h>就行,不用自己再包一层;自己包反而在某些版本上会因为嵌套出错。第二,如果你的工程里混着.c和.cpp,记得确认.c文件确实按 C 编译——CMake 里project(... LANGUAGES C CXX)两个都要写,否则.c会被当成 C++ 编,链接时函数签名对不上。
// main.cpp —— C++ 工程里正确的包含方式 #include <libavformat/avformat.h> #include <libavcodec/avcodec.h> #include <libavutil/avutil.h> #include <libswscale/swscale.h> // 不需要手写 extern "C",头文件内部已处理至于那些已经移除的老接口,不要试图用宏或兼容层去糊。网上流传的#define av_register_all() ((void)0)这种“空实现补丁”能让编译通过,但代码逻辑还是旧的,后面在别的地方照样崩。老老实实按 5.x 的 send/receive 模型重写,工作量其实比想象中小——一个解码循环也就三十行。至于弃用警告(deprecated),-Wno-deprecated-declarations只能把警告藏起来,那些字段随时可能在下个小版本消失,看到警告就当待办事项处理。
3.3 最小验证程序:打印版本与解码器清单
工程配好之后先别写业务逻辑,跑一个十几行的验证程序。它要同时验证三件事:头文件和库版本一致、动态库能加载、符号能调到。
// verify.cpp —— 三条命令能跑通,说明库接对了 #include <libavutil/avutil.h> #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <cstdio> int main() { // 1) 运行时版本:来自动态库,不是头文件 printf("av_version_info : %s\n", av_version_info()); printf("avcodec : %d.%d.%d\n", AV_VERSION_MAJOR(avcodec_version()), AV_VERSION_MINOR(avcodec_version()), AV_VERSION_MICRO(avcodec_version())); // 2) 枚举所有解码器,验证符号与遍历接口都正常 const AVCodec *codec = nullptr; void *iter = nullptr; int decoders = 0, encoders = 0; while ((codec = av_codec_iterate(&iter)) != nullptr) { if (av_codec_is_decoder(codec)) ++decoders; if (av_codec_is_encoder(codec)) ++encoders; } printf("decoders = %d, encoders = %d\n", decoders, encoders); return 0; }av_version_info()返回的是字符串,来自库内部而不是头文件,这是判断“运行的是不是这份库”的唯一可靠依据——头文件里的版本宏只能证明编译期用的是哪份头文件。如果打印出来的版本和你目录里的对不上,说明系统里还有另一份 FFmpeg 被先加载了,去查LD_LIBRARY_PATH和ldconfig -p | grep avcodec。
解码器数量这一行也很有用。带 GPL 组件的完整构建通常在几百个解码器这个量级,如果打印出来只有个位数,说明这份库是裁剪过的精简版,你后面想解的某个编码格式可能根本没编进去。遇到这种情况不用怀疑代码,先换一份完整构建的库。
4. 最小解码链路:从 avformat_open_input 到一帧 AVFrame
验证程序跑通只说明库能用,真正动手是从这里开始。解码这条链路有固定顺序,顺序错了返回值会很含糊,所以先把调用次序和目标说清楚。
4.1 打开输入、找流、开解码器的顺序
完整顺序是:打开容器 → 读流信息 → 找目标流 → 用流参数建解码上下文 → 打开解码器 → 循环送包收帧。每一步的返回值都必须检查,FFmpeg 的错误码用av_strerror能翻译成人话,调试时非常省时间。
// decode_one.cpp —— 解出视频流的前 N 帧 extern "C" { } #include <libavformat/avformat.h> #include <libavcodec/avcodec.h> #include <libavutil/error.h> #include <cstdio> static void print_err(const char *tag, int ret) { char buf[256]; av_strerror(ret, buf, sizeof(buf)); printf("[%s] ret=%d (%s)\n", tag, ret, buf); } int decode_video(const char *path, int max_frames) { AVFormatContext *fmt = nullptr; int ret = avformat_open_input(&fmt, path, nullptr, nullptr); if (ret < 0) { print_err("open_input", ret); return ret; } // 探测时长、帧率、编码参数;本地文件一般很快 ret = avformat_find_stream_info(fmt, nullptr); if (ret < 0) { print_err("find_stream_info", ret); goto end; } // 找“最合适”的视频流,它会顺带做一次解码器可用性判断 const AVCodec *dec = nullptr; int vidx = av_find_best_stream(fmt, AVMEDIA_TYPE_VIDEO, -1, -1, &dec, 0); if (vidx < 0) { print_err("find_best_stream", vidx); ret = vidx; goto end; } AVStream *st = fmt->streams[vidx]; AVCodecContext *ctx = avcodec_alloc_context3(dec); if (!ctx) { ret = AVERROR(ENOMEM); goto end; } // 把流参数搬进解码上下文,不要手动逐字段赋值 ret = avcodec_parameters_to_context(ctx, st->codecpar); if (ret < 0) { print_err("params_to_context", ret); avcodec_free_context(&ctx); goto end; } // 关键:告诉解码器包时间戳用的是流时间基,否则 pts 换算全错 ctx->pkt_timebase = st->time_base; ret = avcodec_open2(ctx, dec, nullptr); if (ret < 0) { print_err("open2", ret); avcodec_free_context(&ctx); goto end; } printf("stream: %dx%d, pix_fmt=%d, tb=%d/%d\n", ctx->width, ctx->height, ctx->pix_fmt, st->time_base.num, st->time_base.den); // ... 送包收帧循环见 4.2 avcodec_free_context(&ctx); end: avformat_close_input(&fmt); return ret; }这里有个顺序不能颠倒:avcodec_alloc_context3(dec)必须带解码器指针,而不是先 alloc 一个空的再avcodec_open2里传解码器——5.x 要求上下文和设备状态绑定,空传会让某些硬件解码器初始化失败。avcodec_parameters_to_context也要在open2之前完成,它负责把codecpar里的分辨率、像素格式、extradata(比如 H.264 的 SPS/PPS)搬过来;extradata 没搬过去,解码出来的画面会是一片绿。
ctx->pkt_timebase = st->time_base这一行很多示例代码省略了,但它决定了avcodec_receive_frame出来的frame->pts用什么时间基解释。省掉它,某些流上frame->pts会是AV_NOPTS_VALUE,你后面算时间就是一堆负数。
4.2 AVPacket 与 AVFrame 的归属关系
送包收帧的循环是 5.x 的核心,写法必须严格按“送一个、收到 EAGAIN 为止”来:
AVPacket *pkt = av_packet_alloc(); AVFrame *frame = av_frame_alloc(); int got = 0; while (av_read_frame(fmt, pkt) >= 0) { if (pkt->stream_index != vidx) { av_packet_unref(pkt); continue; } ret = avcodec_send_packet(ctx, pkt); // EAGAIN 表示解码器缓冲满了,先把帧收完再送 if (ret < 0 && ret != AVERROR(EAGAIN)) { print_err("send_packet", ret); av_packet_unref(pkt); break; } while ((ret = avcodec_receive_frame(ctx, frame)) == 0) { // 这里 frame 是有效的一帧,用完必须 unref,不能 free printf("frame %d: pts=%lld, key=%d, fmt=%d\n", got++, (long long)frame->pts, !!(frame->flags & AV_FRAME_FLAG_KEY), frame->format); av_frame_unref(frame); if (got >= max_frames) break; } if (ret != AVERROR(EAGAIN) && ret != AVERROR_EOF) { print_err("receive_frame", ret); } av_packet_unref(pkt); if (got >= max_frames) break; } // 收尾:送一个空包冲刷解码器缓存,否则最后几帧永远出不来 avcodec_send_packet(ctx, nullptr); while (avcodec_receive_frame(ctx, frame) == 0) { printf("flush frame %d: pts=%lld\n", got++, (long long)frame->pts); av_frame_unref(frame); } av_frame_free(&frame); av_packet_free(&pkt);内存归属是这里最容易被误解的部分。av_read_frame填入的AVPacket内部带引用计数,av_packet_unref只是减引用,不是释放结构体本身;结构体由av_packet_alloc分配、av_packet_free释放。AVFrame同理,frame->data[]指向的是引用计数的缓冲区,你绝对不能free(frame->data[0]),也不能delete[],只能av_frame_unref。我见过有人为了“省内存”手动释放frame->buf,结果在下一个包上直接段错误。
冲刷那一段不能省。解码器内部有 B 帧重排序缓存,不发空包它不会把剩下的帧吐出来,表现出来就是“视频最后几百毫秒不见了”。这个问题非常隐蔽,因为前 99% 的帧都正常。
还有一个细节:AV_FRAME_FLAG_KEY是 5.x 的写法,老代码里的frame->key_frame字段已经弃用。这类字段迁移在 5.x 里有一批,编译时看到 deprecation 警告基本就是它们。
4.3 时间基换算:别让 pts 在你的代码里失真
拿到frame->pts之后最常见的错误是直接把它当毫秒用。pts的单位是时间基,视频流常见1/1000(毫秒)也有1/90000(TS 流)或者1/30(帧号),同一个数值在不同容器里含义完全不同。
// 把帧时间戳换算成毫秒,避免浮点累积误差 static int64_t pts_to_ms(int64_t pts, AVRational tb) { if (pts == AV_NOPTS_VALUE) return AV_NOPTS_VALUE; // av_rescale_q 做整数比例换算,内部用 64 位防溢出 return av_rescale_q(pts, tb, (AVRational){1, 1000}); } // 用法:优先用 best_effort_timestamp,它会把缺失的 pts 从 dts 推算出来 int64_t ts = frame->best_effort_timestamp; if (ts == AV_NOPTS_VALUE) ts = frame->pts; int64_t ms = pts_to_ms(ts, ctx->pkt_timebase);为什么强调av_rescale_q而不是pts * av_q2d(tb) * 1000:av_q2d返回 double,长时间序列上误差会累积,一部两小时的片子跑下来能偏出几十毫秒,音画同步就是从这里开始飘的。av_rescale_q全程整数运算,还带四舍五入选项(av_rescale_q_rnd),精度可控。
best_effort_timestamp也值得单独说。有些容器只给了 dts 没给 pts,或者中间有几帧的 pts 是AV_NOPTS_VALUE,best_effort_timestamp会尝试用帧率和 dts 推算一个合理值,比自己写回退逻辑靠谱。至于时间基本身,用ctx->pkt_timebase而不是st->time_base更稳妥——两者在大多数情况下相同,但解码器在某些封装格式下会改写前者。
5. 避坑与排查:链接、运行、解码结果三类翻车现场
前面几章是顺利路线,这一章是翻车路线。下面五条都是我在实际工程里反复遇到的,按发生阶段排列,每条给出现象、原因和具体动作。
5.1 链接期:符号找不到,八成是顺序和库类型
现象:Linux 下编译到链接阶段,报一屏undefined reference to 'avcodec_send_packet',但-lavcodec明明加了。
原因:静态链接时库的顺序敏感。g++ main.cpp -lavcodec -lavformat -lavutil这个顺序里,avformat引用的符号在它后面的avcodec里能找着,但avcodec引用的avutil符号在-lavutil之前已经扫过了,链接器不会回头再扫。另外,如果目录里同时有.so和.a,链接器默认优先选.so,你以为在静态链接其实不是。
解决:按依赖倒序排列,avformat avcodec swscale swresample avfilter avutil,被依赖的永远在后。想强制静态链接就写全路径lib/libavcodec.a,别用-l。工程复杂、顺序实在理不清的时候,用-Wl,--start-group ... -Wl,--end-group包住,让链接器循环扫描直到符号全部解析。
5.2 运行期:编译过了,一启动就提示找不到动态库
现象:Windows 下双击 exe 弹出“找不到 avcodec-59.dll”;Linux 下报error while loading shared libraries: libavcodec.so.59: cannot open shared object file。
原因:动态库不在加载器的搜索路径里。Windows 按 exe 所在目录、当前目录、系统目录、Path 的顺序找;Linux 按RPATH、LD_LIBRARY_PATH、/etc/ld.so.cache、系统目录的顺序找。你在 IDE 里跑没问题,是因为 IDE 帮你把工作目录设到了bin/;拷到别的机器上就露馅了。
解决:Windows 用 3.1 里那段POST_BUILD把 DLL 拷到输出目录,一劳永逸。Linux 编译时加-Wl,-rpath,'$ORIGIN/lib',让可执行文件在自身目录下的lib/里找,注意$ORIGIN要用单引号包住,否则会被 shell 提前展开成空字符串。排查时用ldd ./ffdemo | grep av看实际加载的是哪个路径的文件,比猜快得多。
5.3 运行期:版本串了,加载到系统里的旧库
现象:程序能启动,但一调用就崩,或者av_version_info()打印出来的版本和目录里的对不上;Linux 下偶尔报version 'LIBAVCODEC_59' not found。
原因:系统里装过别的 FFmpeg(很多桌面发行版自带),动态加载器先找到了系统那份。最典型的症状是头文件是 5.1.2、运行时库是 4.x,结构体布局变了,访问ctx->ch_layout这种新字段就踩到别的内存上。
解决:先用ldd看实际路径,再用LD_LIBRARY_PATH强制指向自带目录验证。根本上还是靠RPATH/rpath把搜索路径钉死在可执行文件旁边。已经发布的二进制可以用patchelf --set-rpath '$ORIGIN/lib' ./ffdemo事后改,改完ldd复核一遍。Windows 上对应的问题是Path里有别的 FFmpeg 目录,顺序比 exe 目录靠前,只能靠拷 DLL 到本地目录压过去。
5.4 解码结果:花屏、绿屏、条纹
现象:能解出帧,但画面上半部分正常、下半部分是绿块,或者整帧噪点。
原因:三种可能,按概率排序。第一,avcodec_parameters_to_context没调,extradata(SPS/PPS)没传过去,解码器不知道自己该按什么参数解。第二,sws_scale的目标缓冲区行宽没按linesize对齐,你按width * 3分配、它按对齐后的linesize[0]写入,越界部分就是花屏。第三,还是用了avcodec_decode_video2那套老接口的移植代码,包边界处理错了。
解决:先确认ctx->extradata非空、extradata_size大于 0;再检查sws_getContext之后av_image_alloc分配的缓冲区用的是dst_linesize而不是手算的宽度乘字节数。转换完写文件时也要按linesize[0]逐行拷,不能整块fwrite,因为linesize通常大于width * 3。
// 正确的 sws_scale 目标缓冲区分配与写出 uint8_t *dst_data[4] = {nullptr}; int dst_linesize[4] = {0}; av_image_alloc(dst_data, dst_linesize, ctx->width, ctx->height, AV_PIX_FMT_BGR24, 1); // 对齐字节数 1,按行紧排 sws_scale(sws, frame->data, frame->linesize, 0, ctx->height, dst_data, dst_linesize); // 逐行写,行宽用 dst_linesize[0],不是 width*3 for (int y = 0; y < ctx->height; ++y) fwrite(dst_data[0] + y * dst_linesize[0], 1, ctx->width * 3, fp); av_freep(&dst_data[0]);5.5 时间戳:音画不同步、播放速度不对
现象:抽出来的帧时间戳是负数或者巨大值,按它排出来的顺序乱;音视频各解各的,合起来差几百毫秒。
原因:两个坑叠加。一是没做时间基换算,直接把pts当毫秒;二是音频和视频用了各自流的时间基换算到毫秒,但落到了不同的取整方向,误差逐帧累积。还有一种情况是pkt->pts为AV_NOPTS_VALUE时没做回退,代码里出现INT64_MIN参与运算导致溢出。
解决:统一在“输出毫秒”这一层做换算,视频用frame->best_effort_timestamp配ctx->pkt_timebase,音频用frame->pts配音频流的time_base,两边都用av_rescale_q而不是浮点。遇到AV_NOPTS_VALUE直接跳过这一帧的时间计算,用上一帧时间加帧间隔补上,别让它进算术运算。排查时把每帧的pts/dts/duration和换算后的毫秒都打出来,对着ffprobe的输出核一遍,偏差从第几帧开始一目了然。
6. 进阶:把这份库封装成可复用的解码模块
链路跑通之后,下一步一定是“把这堆 C 接口藏起来”。我一般封装成一个 RAII 的VideoDecoder类,构造时完成打开与探测,nextFrame()返回一帧,析构时按相反顺序释放。这样做的好处是所有goto cleanup都消失了,异常路径也不会漏释放。
封装时最容易写错的是状态机。解码器有三个状态需要显式记录:是否已打开、是否已冲刷、是否已到文件尾。这三个状态决定了nextFrame()该继续读包、该送空包、还是该返回false。我一般用一个枚举加一个flush_done布尔量,比用AVERROR_EOF反复试探清楚得多。
| 状态 | 触发条件 | nextFrame 的行为 |
|---|---|---|
Opened | avcodec_open2成功 | 循环av_read_frame+send_packet |
Draining | 读包返回 EOF | 送一次nullptr,循环receive_frame |
Eof | 冲刷后receive_frame返回AVERROR_EOF | 直接返回false |
验证封装是否正确,我有个笨办法但很好用:拿一个固定时长的测试文件,用ffprobe拿到总帧数和时长,然后用模块把整段解一遍,比对三个数字——解出的帧数、第一帧和最后一帧的时间戳、总耗时。帧数差一两帧通常是正常的(B 帧重排和容器尾部信息导致),差几十帧就说明冲刷逻辑没写对;首尾时间戳对不上说明时间基换算有问题。这套对数方法比肉眼看画面靠谱得多,尤其是在没有图形界面的环境里。
还有个小技巧:把解码器的日志级别调到AV_LOG_WARNING,然后注册一个av_log_set_callback,把 FFmpeg 自己的告警收集起来。很多问题(比如“Could not find codec parameters”、“non-existing PPS 0 referenced”)它自己早就说了,只是默认输出到 stderr 被忽略了。我把这些日志接到自己的日志系统里之后,定位问题的速度大概快了一倍。
最后说个血泪教训。早期我图省事,在类里直接存AVFrame*的裸指针返回给调用方,想着“反正是引用计数,用完 unref 就行”。结果调用方某条分支忘了 unref,跑长视频的时候内存一路涨到几个 G,最后 OOM。从那以后我每次返回帧都用av_frame_clone拷一份所有权明确的出来,或者干脆在回调里处理完再返回,绝不让裸指针跨过模块边界。现在每写一个新解码模块,我都会先用一个两小时的长文件跑一遍内存监控,确认曲线是平的才继续往下做。希望帮到你。
本文还有配套的精品资源,点击获取