FFmpeg C API集成:解码、编码和最容易踩的坑
FFmpeg 的命令行工具够用的话,没人愿意碰它的 C API——函数多、生命周期复杂、版本间 API 变动频繁。但当你要做实时流处理、自定义滤镜链、或者把音视频能力嵌入产品时,命令行就不够了。这篇文章把 FFmpeg API 集成的核心流程讲清楚:从打开文件到解码、从编码到输出,以及最容易踩的坑。
核心库和职责
| 库 | 职责 | 你什么时候会用到 |
|---|---|---|
| libavformat | 封装格式(容器)读写 | 打开文件、读写 MP4/MKV/FLV 等 |
| libavcodec | 编解码 | 解码 H.264/AAC,编码 H.265/Opus |
| libavutil | 工具函数 | 内存分配、数学运算、日志 |
| libswscale | 图像缩放和色彩转换 | YUV → RGB、分辨率缩放 |
| libswresample | 音频重采样 | 采样率转换、声道布局转换 |
| libavfilter | 滤镜 | 视频加水印、音频降噪 |
不需要全部链接,按需引入。只做解码的话,libavformat + libavcodec + libavutil 就够。
解码流程:从文件到原始帧
1. 初始化
c#include <libavformat/avformat.h> #include <libavcodec/avcodec.h> // FFmpeg 4.0+ 不需要手动注册,老版本需要: // av_register_all(); avformat_network_init(); // 如果要处理网络流(RTMP/HLS)
版本差异是最大的坑之一。FFmpeg 4.0 废弃了 av_register_all(),5.0 废弃了 avcodec_register_all(),新版自动注册所有内置编解码器。如果你的代码还要兼容 3.x,加版本判断:
c#if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58, 0, 0) av_register_all(); #endif
2. 打开文件、找流
cAVFormatContext *fmt_ctx = NULL; int ret = avformat_open_input(&fmt_ctx, "input.mp4", NULL, NULL); if (ret < 0) { char errbuf[128]; av_strerror(ret, errbuf, sizeof(errbuf)); fprintf(stderr, "无法打开文件: %s\n", errbuf); return -1; } avformat_find_stream_info(fmt_ctx, NULL);
avformat_open_input 只打开文件头,不读帧数据。avformat_find_stream_info 读几帧探测流信息(编码器、分辨率、帧率),如果省略这步,后续 codecpar 里的信息可能不完整。
3. 找视频流、打开解码器
c// 找第一个视频流 int video_idx = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); if (video_idx < 0) { fprintf(stderr, "没找到视频流\n"); return -1; } // 打开解码器 AVCodecParameters *codecpar = fmt_ctx->streams[video_idx]->codecpar; const AVCodec *codec = avcodec_find_decoder(codecpar->codec_id); AVCodecContext *codec_ctx = avcodec_alloc_context3(codec); avcodec_parameters_to_context(codec_ctx, codecpar); avcodec_open2(codec_ctx, codec, NULL);
av_find_best_stream 比 手动遍历 nb_streams 更好——它会根据流的质量和语言偏好选最优的。
4. 读取和解码帧
FFmpeg 3.1+ 使用 send/receive 模型:
cAVPacket *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; } ret = avcodec_send_packet(codec_ctx, pkt); if (ret < 0 && ret != AVERROR(EAGAIN) && ret != AVERROR_EOF) { break; } while (ret >= 0) { ret = avcodec_receive_frame(codec_ctx, frame); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break; if (ret < 0) break; // frame->data[0] 就是 YUV 数据 // frame->width, frame->height, frame->format 可用 process_frame(frame); } av_packet_unref(pkt); }
send/receive 是异步的:一个 packet 可能产生多个 frame(如 B 帧重排序),也可能多个 packet 才产出一个 frame(音频解码缓冲)。EAGAIN 不是错误,意思是"再发一个 packet 过来"。
5. 冲刷解码器
读取完所有帧后,解码器里可能还缓存着几帧,需要冲刷:
cavcodec_send_packet(codec_ctx, NULL); // NULL 触发冲刷 while (avcodec_receive_frame(codec_ctx, frame) != AVERROR_EOF) { process_frame(frame); }
编码流程:从原始帧到文件
编码是解码的逆过程,但多了输出容器的初始化:
c// 创建输出上下文 AVFormatContext *out_ctx = NULL; avformat_alloc_output_context2(&out_ctx, NULL, NULL, "output.mp4"); // 添加视频流 AVStream *out_stream = avformat_new_stream(out_ctx, NULL); const AVCodec *encoder = avcodec_find_encoder(AV_CODEC_ID_H264); AVCodecContext *enc_ctx = avcodec_alloc_context3(encoder); enc_ctx->width = 1280; enc_ctx->height = 720; enc_ctx->time_base = (AVRational){1, 25}; // 时间基准 enc_ctx->framerate = (AVRational){25, 1}; enc_ctx->pix_fmt = AV_PIX_FMT_YUV420P; enc_ctx->gop_size = 10; enc_ctx->max_b_frames = 1; // 如果输出是 MP4,需要把编码器参数拷到流里 avcodec_open2(enc_ctx, encoder, NULL); avcodec_parameters_from_context(out_stream->codecpar, enc_ctx); // 打开输出文件 avio_open(&out_ctx->pb, "output.mp4", AVIO_FLAG_WRITE); avformat_write_header(out_ctx, NULL);
编码帧和写文件:
cAVPacket *enc_pkt = av_packet_alloc(); // 对每个帧: avcodec_send_frame(enc_ctx, frame); while (avcodec_receive_packet(enc_ctx, enc_pkt) == 0) { enc_pkt->stream_index = out_stream->index; av_interleaved_write_frame(out_ctx, enc_pkt); av_packet_unref(enc_pkt); } // 冲刷编码器 avcodec_send_frame(enc_ctx, NULL); while (avcodec_receive_packet(enc_ctx, enc_pkt) == 0) { enc_pkt->stream_index = out_stream->index; av_interleaved_write_frame(out_ctx, enc_pkt); av_packet_unref(enc_pkt); } // 写文件尾 av_write_trailer(out_ctx);
av_interleaved_write_frame 会自动按 DTS 排序后写入,比 av_write_frame 更安全。如果你不确定 DTS/PTS 的关系,用 interleaved 版本。
资源释放
FFmpeg 的资源释放顺序不能乱——先释放依赖项,再释放容器:
cav_frame_free(&frame); av_packet_free(&pkt); avcodec_free_context(&codec_ctx); avformat_close_input(&fmt_ctx); // 输入 avcodec_free_context(&enc_ctx); av_write_trailer(out_ctx); avformat_close_input(&out_ctx); // 输出(如果用 avformat_close_input) // 或者: avio_closep(&out_ctx->pb); avformat_free_context(out_ctx);
忘写 av_write_trailer 的话,MP4 文件的 moov atom 不会被写入,播放器无法打开。
错误处理的正确姿势
FFmpeg 的错误码是负数,用 av_strerror 转成可读字符串:
cchar errbuf[AV_ERROR_MAX_STRING_SIZE] = {0}; if (ret < 0) { av_strerror(ret, errbuf, sizeof(errbuf)); fprintf(stderr, "错误: %s (code=%d)\n", errbuf, ret); }
常见错误码:
AVERROR(EAGAIN)— 需要更多输入,不是真错误AVERROR_EOF— 流结束,正常退出条件AVERROR(EINVAL)— 参数无效,检查传参AVERROR(ENOMEM)— 内存不足,检查是否有泄漏AVERROR_EXIT— 被 callback 终止
线程安全
FFmpeg API 大部分不是线程安全的。多线程环境下:
- 每个
AVCodecContext只能被一个线程使用 AVFormatContext的读写操作需要加锁av_log是线程安全的,可以放心在多线程中使用- 推荐模式:一个线程负责读取和解码,通过队列把帧传给另一个线程编码
编译链接
bash# 查看链接需要的库 pkg-config --libs libavformat libavcodec libavutil # 典型编译命令 gcc -o myapp myapp.c $(pkg-config --cflags --libs libavformat libavcodec libavutil libswscale)
静态链接时注意依赖顺序:libavformat 依赖 libavcodec,libavcodec 依赖 libavutil,链接顺序要反过来写。