前端阅读 05月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?集成分为三步:安装开发包、配置构建系统、链接库文件。安装开发包不同平台的安装方式:# Ubuntu/Debiansudo apt install libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev# macOSbrew 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_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: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 库之间存在依赖关系,必须按依赖顺序从左到右排列:-lavformat -lavcodec -lswscale -lswresample -lavutil -lm -lz -lpthreadlibavformat 依赖 libavcodec,libavcodec 依赖 libavutil,所以 libavformat 必须在前面。如果顺序反了,会报 undefined reference to avformat_open_input 之类的错误。Windows 上额外注意:需要把 FFmpeg 的 bin 目录加到 PATH,或在项目属性中设置 LIBRARY_PATH 和 INCLUDE 环境变量。如何用 FFmpeg API 解码视频帧?这是最常考的代码题。解码流程分五步:打开文件 → 查找流 → 打开解码器 → 读包解码 → 释放资源。#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 编码视频?解码的反向过程——编码同样高频出现。核心差异在于需要手动设置编码参数、管理时间戳。// 编码器初始化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 支持线程级并行解码,但默认不开启。设置方式: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_xxxA: 99% 是链接顺序问题。确保 -lavformat 在 -lavcodec 前面,-lavcodec 在 -lavutil 前面。用 pkg-config --libs libavformat 查看正确的链接顺序。Q: 运行时报 Cannot open input fileA: 检查文件路径是否正确。Windows 上注意反斜杠问题,建议统一用正斜杠。另外确认文件格式是否被 FFmpeg 支持:ffmpeg -formats | grep mp4。Q: 解码出的帧颜色不对A: 缺少像素格式转换。解码输出通常是 YUV 格式,显示需要 RGB。用 libswscale 的 sws_scale() 转换。Q: 音视频不同步A: 时间戳管理问题。必须用 av_packet_rescale_ts() 在编码/复用时重新计算时间基,不能直接用解码帧的 PTS。实际项目中的最佳实践错误处理不能偷懒:每个 FFmpeg API 调用的返回值都要检查。生产环境建议封装统一的错误处理宏:#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() 重置后复用。