5月28日 02:01

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 和正确的链接顺序:

cmake
cmake_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

cmake
find_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 依赖 libavcodeclibavcodec 依赖 libavutil,所以 libavformat 必须在前面。如果顺序反了,会报 undefined reference to avformat_open_input 之类的错误。

Windows 上额外注意:需要把 FFmpeg 的 bin 目录加到 PATH,或在项目属性中设置 LIBRARY_PATHINCLUDE 环境变量。

如何用 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 支持线程级并行解码,但默认不开启。设置方式:

c
enc_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。用 libswscalesws_scale() 转换。

Q: 音视频不同步 A: 时间戳管理问题。必须用 av_packet_rescale_ts() 在编码/复用时重新计算时间基,不能直接用解码帧的 PTS。

实际项目中的最佳实践

  1. 错误处理不能偷懒:每个 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)
  1. 日志分级:开发阶段设 av_log_set_level(AV_LOG_DEBUG),生产环境设 AV_LOG_WARNINGAV_LOG_ERROR。FFmpeg 默认日志级别太低,会输出大量信息。

  2. 资源释放用 goto 模式:C 语言没有 defer,用 goto cleanup 是 FFmpeg 社区推荐的方式,确保任何错误路径都能正确释放已分配的资源。

  3. API 版本兼容:FFmpeg 不同版本之间 API 有变化。用 LIBAVCODEC_VERSION_MAJOR 等宏做版本判断,或在 CMake 中检测。FFmpeg 6.0 之后 avcodec_find_decoder() 返回 const AVCodec*,之前是非 const。

  4. 避免在热路径中分配内存av_frame_alloc()av_packet_alloc() 应在循环外调用,循环内用 av_frame_unref()av_packet_unref() 重置后复用。

标签:FFmpeg