FFmpeg是否提供API?如何在C/C++项目中集成FFmpeg?
FFmpeg 提供了完整的 C 语言 API,这是面试中经常被问到的基础知识。核心 API 分布在 libavformat、libavcodec、libavutil、libswscale、libswresample 五个库中,C/C++ 项目可以直接链接这些库来调用编解码、封装解封装、格式转换等全部功能,无需通过命令行进程通信。
FFmpeg 有哪些核心库?各自的职责是什么?
这是理解 FFmpeg API 的起点。FFmpeg 的模块化设计体现在每个库各司其职:
- libavformat:处理容器格式的读取与写入。MP4、MKV、FLV 等文件的打开、流信息解析、数据包读写都由它负责。核心函数包括
avformat_open_input()、av_read_frame()、avformat_write_header()。 - libavcodec:编解码器的核心。H.264、H.265、AAC、OPUS 等编解码器都封装在这里。
avcodec_find_decoder()、avcodec_send_packet()、avcodec_receive_frame()是解码的关键调用链。 - libavutil:公共工具库,提供内存管理(
av_malloc/av_free)、数学运算(av_clip)、日志(av_log)、字典(AVDictionary)等基础设施。其他库都依赖它。 - libswscale:图像缩放和像素格式转换。将 YUV 数据转为 RGB、调整分辨率等场景必须用它,核心函数是
sws_scale()。 - libswresample:音频重采样、声道布局转换、采样格式转换。处理音频数据时不可或缺,核心函数是
swr_convert()。
面试追问:为什么 FFmpeg 要拆成这么多库而不是一个整体?答案是模块化链接——如果你的项目只需要解码不需要缩放,可以只链接 libavcodec 和 libavformat,不链接 libswscale,减小二进制体积。这在嵌入式和移动端尤其重要。
如何在 C/C++ 项目中集成 FFmpeg?
集成分为三步:安装开发包、配置构建系统、链接库文件。
安装开发包
不同平台的安装方式:
bash# Ubuntu/Debian sudo apt install libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev # macOS brew install ffmpeg # Windows(推荐 vcpkg) vcpkg install ffmpeg
安装后确认头文件存在:/usr/include/libavcodec/avcodec.h(Linux)或 $(brew --prefix ffmpeg)/include/libavcodec/avcodec.h(macOS)。如果头文件找不到,说明装的是运行时包而非开发包。
配置 CMake 构建
CMake 是最常用的构建方式。关键在于 find_package 和正确的链接顺序:
cmakecmake_minimum_required(VERSION 3.10) project(FFmpegApp) # 方式一:使用 CMake 内置的 FindFFmpeg 模块 find_package(FFmpeg REQUIRED COMPONENTS avformat avcodec avutil swscale swresample) add_executable(main main.cpp) target_include_directories(main PRIVATE ${FFMPEG_INCLUDE_DIRS}) target_link_libraries(main PRIVATE ${FFMPEG_LIBAVFORMAT_LIBRARIES} ${FFMPEG_LIBAVCODEC_LIBRARIES} ${FFMPEG_LIBAVUTIL_LIBRARIES} ${FFMPEG_LIBSWSCALE_LIBRARIES} ${FFMPEG_LIBSWRESAMPLE_LIBRARIES} )
如果你的 CMake 版本不支持 FindFFmpeg,可以用 pkg-config:
cmakefind_package(PkgConfig REQUIRED) pkg_check_modules(AVFORMAT REQUIRED libavformat) pkg_check_modules(AVCODEC REQUIRED libavcodec) pkg_check_modules(AVUTIL REQUIRED libavutil) add_executable(main main.cpp) target_compile_options(main PRIVATE ${AVFORMAT_CFLAGS} ${AVCODEC_CFLAGS}) target_link_libraries(main PRIVATE ${AVFORMAT_LIBRARIES} ${AVCODEC_LIBRARIES} ${AVUTIL_LIBRARIES})
链接顺序与常见错误
链接顺序是集成的最大坑。FFmpeg 库之间存在依赖关系,必须按依赖顺序从左到右排列:
shell-lavformat -lavcodec -lswscale -lswresample -lavutil -lm -lz -lpthread
libavformat 依赖 libavcodec,libavcodec 依赖 libavutil,所以 libavformat 必须在前面。如果顺序反了,会报 undefined reference to avformat_open_input 之类的错误。
Windows 上额外注意:需要把 FFmpeg 的 bin 目录加到 PATH,或在项目属性中设置 LIBRARY_PATH 和 INCLUDE 环境变量。
如何用 FFmpeg API 解码视频帧?
这是最常考的代码题。解码流程分五步:打开文件 → 查找流 → 打开解码器 → 读包解码 → 释放资源。
c#include <libavformat/avformat.h> #include <libavcodec/avcodec.h> int main(int argc, char **argv) { if (argc != 2) { fprintf(stderr, "Usage: %s <input>\n", argv[0]); return 1; } // 1. 打开输入文件 AVFormatContext *fmt_ctx = NULL; if (avformat_open_input(&fmt_ctx, argv[1], NULL, NULL) < 0) { fprintf(stderr, "Cannot open input\n"); return 1; } avformat_find_stream_info(fmt_ctx, NULL); // 2. 查找视频流 int video_idx = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); if (video_idx < 0) { fprintf(stderr, "No video stream\n"); avformat_close_input(&fmt_ctx); return 1; } // 3. 打开解码器 const AVCodec *codec = avcodec_find_decoder(fmt_ctx->streams[video_idx]->codecpar->codec_id); AVCodecContext *codec_ctx = avcodec_alloc_context3(codec); avcodec_parameters_to_context(codec_ctx, fmt_ctx->streams[video_idx]->codecpar); avcodec_open2(codec_ctx, codec, NULL); // 4. 读包解码 AVPacket *pkt = av_packet_alloc(); AVFrame *frame = av_frame_alloc(); while (av_read_frame(fmt_ctx, pkt) >= 0) { if (pkt->stream_index != video_idx) { av_packet_unref(pkt); continue; } if (avcodec_send_packet(codec_ctx, pkt) < 0) { av_packet_unref(pkt); continue; } while (avcodec_receive_frame(codec_ctx, frame) == 0) { printf("Frame %d: %dx%d fmt=%d\n", codec_ctx->frame_number, frame->width, frame->height, frame->format); } av_packet_unref(pkt); } // 5. 刷新解码器(处理缓存的帧) avcodec_send_packet(codec_ctx, NULL); while (avcodec_receive_frame(codec_ctx, frame) == 0) { printf("Flushed frame %d\n", codec_ctx->frame_number); } // 6. 释放资源 av_frame_free(&frame); av_packet_free(&pkt); avcodec_free_context(&codec_ctx); avformat_close_input(&fmt_ctx); return 0; }
几个关键细节:
avformat_find_stream_info()不能省略,它填充流信息,否则codecpar中的字段可能不完整。av_find_best_stream()比手动遍历流更可靠,它能处理多视频流的情况。- 解码结束后必须发送 NULL 包来刷新解码器,否则最后几帧会丢失。
- 使用
av_packet_alloc()而不是栈上的AVPacket,这是 FFmpeg 新版推荐的写法。
如何用 FFmpeg API 编码视频?
解码的反向过程——编码同样高频出现。核心差异在于需要手动设置编码参数、管理时间戳。
c// 编码器初始化 const AVCodec *encoder = avcodec_find_encoder(AV_CODEC_ID_H264); AVCodecContext *enc_ctx = avcodec_alloc_context3(encoder); enc_ctx->bit_rate = 400000; enc_ctx->width = 1920; enc_ctx->height = 1080; enc_ctx->time_base = (AVRational){1, 25}; enc_ctx->framerate = (AVRational){25, 1}; enc_ctx->gop_size = 10; enc_ctx->max_b_frames = 1; enc_ctx->pix_fmt = AV_PIX_FMT_YUV420P; // H.264 特定选项 AVDictionary *opts = NULL; av_dict_set(&opts, "preset", "medium", 0); avcodec_open2(enc_ctx, encoder, &opts); av_dict_free(&opts); // 编码循环 AVPacket *pkt = av_packet_alloc(); for (int i = 0; i < frame_count; i++) { // ... 准备 frame 数据 ... frame->pts = i; avcodec_send_frame(enc_ctx, frame); while (avcodec_receive_packet(enc_ctx, pkt) == 0) { // 将 pkt 写入输出文件 av_packet_unref(pkt); } } // 刷新编码器 avcodec_send_frame(enc_ctx, NULL); while (avcodec_receive_packet(enc_ctx, pkt) == 0) { av_packet_unref(pkt); }
编码时最容易踩的坑是 PTS(显示时间戳)。每一帧必须设置递增的 PTS,否则输出文件的时间轴会混乱。时间基 time_base 决定了 PTS 的单位,{1, 25} 表示每秒 25 个单位。
内存管理有哪些容易忽略的要点?
FFmpeg 的 API 是纯 C 设计,没有自动内存管理。每个分配操作都有对应的释放操作,遗漏任何一步都会导致内存泄漏。
| 分配函数 | 释放函数 | 说明 |
|---|---|---|
avformat_open_input() | avformat_close_input() | 关闭输入并释放上下文 |
avcodec_alloc_context3() | avcodec_free_context() | 释放解码器上下文 |
av_frame_alloc() | av_frame_free() | 释放帧 |
av_packet_alloc() | av_packet_free() | 释放包 |
av_malloc() | av_free() | 通用内存分配 |
sws_getContext() | sws_freeContext() | 释放缩放上下文 |
swr_alloc() | swr_free() | 释放重采样上下文 |
特别容易忽略的是 av_packet_unref()。每次 av_read_frame() 后,包内部的数据缓冲区被引用计数加一,必须调用 av_packet_unref() 减引用,否则数据缓冲区永远不会被释放。这不是 C++ 的 RAII,必须手动管理。
另一个常见问题是 avformat_close_input() 会释放 AVFormatContext,之后不要再对它调用 av_free(),否则 double free。
多线程解码需要注意什么?
FFmpeg 支持线程级并行解码,但默认不开启。设置方式:
cenc_ctx->thread_count = 4; // 使用 4 个线程 enc_ctx->thread_type = FF_THREAD_FRAME; // 帧级并行
thread_type 有两个选项:
FF_THREAD_SLICE:片级并行,一帧内多个 slice 并行解码。兼容性好但加速有限。FF_THREAD_FRAME:帧级并行,多帧同时解码。加速明显但延迟更高,需要更多内存缓存帧。
实际使用中,FF_THREAD_FRAME 加速效果更好,但实时场景(如视频会议)应选 FF_THREAD_SLICE 降低延迟。
注意:thread_count 的值不要超过 CPU 核心数,设置为 0 表示 FFmpeg 自动选择。
常见集成问题排查
Q: 编译报 undefined reference to av_xxx
A: 99% 是链接顺序问题。确保 -lavformat 在 -lavcodec 前面,-lavcodec 在 -lavutil 前面。用 pkg-config --libs libavformat 查看正确的链接顺序。
Q: 运行时报 Cannot open input file
A: 检查文件路径是否正确。Windows 上注意反斜杠问题,建议统一用正斜杠。另外确认文件格式是否被 FFmpeg 支持:ffmpeg -formats | grep mp4。
Q: 解码出的帧颜色不对
A: 缺少像素格式转换。解码输出通常是 YUV 格式,显示需要 RGB。用 libswscale 的 sws_scale() 转换。
Q: 音视频不同步
A: 时间戳管理问题。必须用 av_packet_rescale_ts() 在编码/复用时重新计算时间基,不能直接用解码帧的 PTS。
实际项目中的最佳实践
- 错误处理不能偷懒:每个 FFmpeg API 调用的返回值都要检查。生产环境建议封装统一的错误处理宏:
c#define CHECK_ERR(ret, msg) do { \ if (ret < 0) { \ char errbuf[128]; \ av_strerror(ret, errbuf, sizeof(errbuf)); \ fprintf(stderr, "%s: %s\n", msg, errbuf); \ goto cleanup; \ } \ } while(0)
-
日志分级:开发阶段设
av_log_set_level(AV_LOG_DEBUG),生产环境设AV_LOG_WARNING或AV_LOG_ERROR。FFmpeg 默认日志级别太低,会输出大量信息。 -
资源释放用 goto 模式:C 语言没有 defer,用
goto cleanup是 FFmpeg 社区推荐的方式,确保任何错误路径都能正确释放已分配的资源。 -
API 版本兼容:FFmpeg 不同版本之间 API 有变化。用
LIBAVCODEC_VERSION_MAJOR等宏做版本判断,或在 CMake 中检测。FFmpeg 6.0 之后avcodec_find_decoder()返回const AVCodec*,之前是非 const。 -
避免在热路径中分配内存:
av_frame_alloc()和av_packet_alloc()应在循环外调用,循环内用av_frame_unref()和av_packet_unref()重置后复用。