适用人群:C/C++ 开发者、音视频处理入门到进阶、工业数采项目中需要音视频能力的边缘设备开发者 技术栈:C 语言实现,提供 C API 与命令行双形态
1. 背景
视频格式的"混乱"是历史常态:同一段视频由封装格式 (MP4/MKV/FLV/TS...)和编码格式(H.264/H.265/VP9/AV1...)两个正交维度描述,再加上音频编码(AAC/MP3/Opus...)、字幕、流媒体协议(RTMP/HLS/DASH...)的排列组合,单靠手写解析器维护一个项目几乎是灾难。
FFmpeg 之所以成为"多媒体领域的事实标准",在于它用一个统一框架吃下了这条长尾:
- 全格式覆盖:数百种解封装/封装格式、编解码器、滤镜,ffmpeg -formats 一屏都列不完;
- 跨平台:纯 C 编写,Windows/Linux/macOS/Android/iOS/嵌入式全平台可编译;
- 双形态:命令行 ffmpeg 适合快速验证与批处理;libavcodec / libavformat / libavfilter 等 C API 适合深度集成------OBS、VLC、Chrome、Android、几乎所有视频播放器的底层都在用它;
- 硬件加速生态:NVENC/NVDEC、VAAPI、QSV、VideoToolbox、MediaCodec、AMF 全覆盖。
对工业数采场景而言,FFmpeg 的价值同样直接:摄像头/网络流(RTSP)取流 → 转码/抽帧 → 本地落盘或推流上云,是一条完整的"视觉数据采集"链路,与传统的传感器点位数采互补。
2. 架构全景
FFmpeg 由一套共享基础设施 + 多个功能库 + 三个命令行工具组成。
2.1 六大核心库
| 库 | 全称 | 职责 | 开发者最常用 API |
|---|---|---|---|
| libavutil | AV Util | 基础工具:内存、日志、数学、像素/采样格式、时间基、AVFrame/AVPacket/AVBuffer | av_frame_alloc/ref/unref、av_log、av_rescale_q、AVRational |
| libavcodec | AV Codec | 编解码器核心:H.264/H.265/VP9/AV1/AAC/MP3... 与硬件加速 | avcodec_send_packet/receive_frame |
| libavformat | AV Format | 解封装/封装/协议:MP4/MKV/FLV/TS/RTSP/HLS... | avformat_open_input/read_frame/write_frame |
| libavfilter | AV Filter | 滤镜图:缩放/裁剪/水印/转场/音频 EQ... | avfilter_graph_*、av_buffersrc_add_frame |
| libswscale | SW Scale | 像素格式与尺寸转换(CPU) | sws_getContext/sws_scale |
| libswresample | SW Resample | 音频重采样与声道/采样率转换 | swr_alloc/swr_convert |
2.2 三件套工具
- ffmpeg:转码/转封装/滤镜/推流主工具;
- ffprobe:探针工具,读取媒体文件/流的元信息(格式、编码、时长、帧率、码率),调试时第一选择;
- ffplay:极简播放器,快速预览。
2.3 构建与安装
bash
# Windows(推荐从 gyan.dev 或 BtbN 下载预编译包,加入 PATH)
ffmpeg -version
# Ubuntu
sudo apt install ffmpeg
# 从源码构建(最小可用)
./configure --enable-shared --disable-everything --enable-decoder=h264 --enable-demuxer=mov --enable-parser=h264
make -j$(nproc)
提示:正式集成建议用 --disable-everything 按需裁剪组件,二进制体积可从 100+MB 降到几 MB,这是嵌入式设备的标准做法。
3. 核心概念
这几个概念不搞懂,写 C 代码必踩坑,是 FFmpeg 的"心智门槛"。
3.1 封装格式 vs 编码格式
- 编码(Codec):像素/采样数据如何压缩。H.264 是编码器,压缩的是帧。
- 封装(Container):压缩后的数据如何打包成文件/流。MP4 是容器,描述的是"H.264 视频轨 + AAC 音频轨 + 元数据"怎么排布。
ffprobe 输出中 codec_name 是编码,format_name 是容器:
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'input.mp4':
Duration: 00:00:10.00, bitrate: 1000 kb/s
Stream #0:0: Video: h264 (High) ... ← 编码
Stream #0:1: Audio: aac (LC) ... ← 编码
上面的 mov,mp4,... 就是容器。
3.2 时间基(Time Base)与 AVRational
FFmpeg 不用浮点秒表示时间,而是用有理数时间基 AVRational { num, den },即每秒 den 个 tick、每 tick 为 num/den 秒。
cpp
AVRational tb = {1, 90000}; // 90000Hz 时钟,常见于 MPEG-TS
double seconds = pts * av_q2d(tb); // pts 数值 × 1/90000 = 秒
关键坑:不同层的时间基不一样------容器层(AVStream.time_base)、编解码器层(AVCodecContext.time_base)、滤镜层各有各的基。跨层换算必须用 av_rescale_q(),禁止直接乘除。
cpp
// 把 AVPacket.pts(stream 时间基)换算成毫秒
int64_t ms = av_rescale_q(pkt->pts, stream->time_base, (AVRational){1, 1000});
3.3 PTS / DTS
- PTS(Presentation Timestamp) :帧显示时间;
- DTS(Decode Timestamp) :帧解码顺序时间。
B 帧存在时两者不同(I/P 帧先解、B 帧后解但先显示),因此 DTS 单调不减,PTS 不一定。B 帧常见于 H.264 High Profile;流式转码如不需要 B 帧可用 -bf 0。
3.4 滤镜图(Filter Graph)
滤镜 = 对帧做变换的节点,节点连成有向图。最简单的图是 scale=1280:720,复杂图如:
input(视频) → scale → watermark → output
input(音频) → volume → output
滤镜图在 libavfilter 中由 AVFilterGraph、AVFilterContext(节点)、AVFilterLink(边)表示,数据通过 av_buffersrc_add_frame(进)与 av_buffersink_get_frame(出)流动。命令行里用逗号(同一链)和分号(并行链)分隔。
3.5 AVFrame / AVPacket 与引用计数
- AVPacket:编码后的数据(压缩字节),一头在解封装侧、一头在编码侧;
- AVFrame:解码后/编码前的数据(原始像素/采样),或解码器输出的 YUV 帧。
两者都不是"一次性 malloc 的结构",而是引用计数的:av_frame_ref() 只增加引用不拷贝数据,av_frame_unref() 释放本引用,数据在最后一个引用释放时才真正回收
cpp
AVFrame *dst = av_frame_alloc();
av_frame_ref(dst, src); // 浅拷贝 + 引用 +1
// ... 使用 dst ...
av_frame_unref(dst); // 引用 -1,src 仍可用
永远不要手工 free 帧数据,这是内存管理的第一铁律。
4. API 说明
4.1 解封装(libavformat)核心 API
cpp
// 1. 打开输入
AVFormatContext *fmt_ctx = NULL;
if (avformat_open_input(&fmt_ctx, url, NULL, NULL) < 0) { /* 失败 */ }
// 2. 读取流信息(流数量、时长、元数据)
avformat_find_stream_info(fmt_ctx, NULL);
// 3. 遍历流,找到视频流
int v_idx = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
// 4. 循环读取 AVPacket
AVPacket *pkt = av_packet_alloc();
while (av_read_frame(fmt_ctx, pkt) >= 0) {
if (pkt->stream_index == v_idx) { /* 处理视频包 */ }
av_packet_unref(pkt);
}
// 5. 收尾
avformat_close_input(&fmt_ctx);
av_packet_free(&pkt);
4.2 解码(libavcodec)核心 API
新版 API 采用 send/receive 推拉模型:发送一个压缩包,循环接收零到多个帧(解码器有内部缓冲,可能一次输入产出多帧、也可能需要多包才能出一帧)。
cpp
const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
AVCodecContext *dec_ctx = avcodec_alloc_context3(codec);
// 关键:把流的参数(SPS/PPS、宽高、时间基)拷给解码器
avcodec_parameters_to_context(dec_ctx, stream->codecpar);
avcodec_open2(dec_ctx, codec, NULL);
// 推拉循环
avcodec_send_packet(dec_ctx, pkt); // 输入压缩包
while (avcodec_receive_frame(dec_ctx, frame) >= 0) {
// 拿到原始帧:frame->data[0..3]、frame->linesize[0..3](YUV420 为例)
// frame->pts 是解码器时间基下的显示时间
}
avcodec_receive_frame 返回值语义:
- 0:拿到一帧,继续循环;
- AVERROR(EAGAIN):当前输入还不够出帧,需要继续喂包;
- AVERROR_EOF:输入结束,解码器内部缓冲已全部冲刷。
4.3 编码(libavcodec)核心 API
cpp
const AVCodec *enc = avcodec_find_encoder(AV_CODEC_ID_H264);
AVCodecContext *enc_ctx = avcodec_alloc_context3(enc);
enc_ctx->width = 1280; enc_ctx->height = 720;
enc_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
enc_ctx->time_base = (AVRational){1, 25}; // 帧率 25fps
enc_ctx->framerate = (AVRational){25, 1};
enc_ctx->bit_rate = 2'000'000; // 2Mbps
// x264 私有参数(通过字典传递)
av_opt_set(enc_ctx->priv_data, "preset", "fast", 0);
avcodec_open2(enc_ctx, enc, NULL);
// 推拉模型:送原始帧,收压缩包
avcodec_send_frame(enc_ctx, frame);
while (avcodec_receive_packet(enc_ctx, pkt) >= 0) {
// pkt 为编码后的压缩数据,可写入容器
}
4.4 封装输出(libavformat)核心 API
cpp
AVFormatContext *out_ctx = NULL;
avformat_alloc_output_context2(&out_ctx, NULL, NULL, "out.mp4");
// 创建视频流
AVStream *out_stream = avformat_new_stream(out_ctx, NULL);
avcodec_parameters_from_context(out_stream->codecpar, enc_ctx);
// 打开输出
avio_open(&out_ctx->pb, "out.mp4", AVIO_FLAG_WRITE);
avformat_write_header(out_ctx, NULL);
// 写包(注意包的时间基要换算到流时间基)
pkt->stream_index = out_stream->index;
av_packet_rescale_ts(pkt, enc_ctx->time_base, out_stream->time_base);
av_interleaved_write_frame(out_ctx, pkt); // 交错写帧,自动排序
// 收尾
av_write_trailer(out_ctx);
avio_closep(&out_ctx->pb);
avformat_free_context(out_ctx);
4.5 滤镜(libavfilter)最小骨架
cpp
// 1. 建图
AVFilterGraph *graph = avfilter_graph_alloc();
// 2. 建输入节点(buffer)
AVFilterContext *src_ctx = NULL;
avfilter_graph_create_filter(&src_ctx, avfilter_get_by_name("buffer"),
"in", "video_size=1280x720:pix_fmt=yuv420p:time_base=1/25:frame_rate=25/1", NULL, graph);
// 3. 建输出节点(buffersink)
AVFilterContext *sink_ctx = NULL;
avfilter_graph_create_filter(&sink_ctx, avfilter_get_by_name("buffersink"),
"out", NULL, NULL, graph);
// 4. 中间滤镜:scale
AVFilterContext *scale_ctx = NULL;
avfilter_graph_create_filter(&scale_ctx, avfilter_get_by_name("scale"),
"scale", "640:360", NULL, graph);
// 5. 连边:in -> scale -> out
avfilter_link(src_ctx, 0, scale_ctx, 0);
avfilter_link(scale_ctx, 0, sink_ctx, 0);
avfilter_graph_config(graph, NULL);
// 6. 喂帧/收帧
av_buffersrc_add_frame_flags(src_ctx, frame, AV_BUFFERSRC_FLAG_KEEP_REF);
AVFrame *out = av_frame_alloc();
av_buffersink_get_frame(sink_ctx, out);
4.6 像素转换(libswscale)与音频重采样(libswresample)
cpp
// 像素格式/尺寸转换(如 YUV420P 1280x720 -> RGB24 640x360)
struct SwsContext *sws = sws_getContext(1280, 720, AV_PIX_FMT_YUV420P,
640, 360, AV_PIX_FMT_RGB24,
SWS_BILINEAR, NULL, NULL, NULL);
sws_scale(sws, frame->data, frame->linesize, 0, 720, rgb_data, rgb_linesize);
sws_freeContext(sws);
// 音频重采样(如 44100Hz 立体声 -> 48000Hz 单声道)
SwrContext *swr = swr_alloc();
av_opt_set_int(swr, "in_channel_layout", AV_CH_LAYOUT_STEREO, 0);
av_opt_set_int(swr, "out_channel_layout", AV_CH_LAYOUT_MONO, 0);
av_opt_set_int(swr, "in_sample_rate", 44100, 0);
av_opt_set_int(swr, "out_sample_rate", 48000, 0);
av_opt_set_sample_fmt(swr, "in_sample_fmt", AV_SAMPLE_FMT_FLTP, 0);
av_opt_set_sample_fmt(swr, "out_sample_fmt", AV_SAMPLE_FMT_FLT, 0);
swr_init(swr);
swr_convert(swr, out_data, out_nb_samples, in_data, in_nb_samples);
swr_free(&swr);
4.7 错误处理
FFmpeg 错误码统一为负的 AVERROR 值(本质是 errno 的负映射)。调试时把错误码转成可读文本:
cpp
char errbuf[AV_ERROR_MAX_STRING_SIZE];
av_strerror(errcode, errbuf, sizeof(errbuf));
av_log(NULL, AV_LOG_ERROR, "错误: %s\n", errbuf);
常见:AVERROR(EAGAIN)、AVERROR_EOF、AVERROR(ENOMEM)、AVERROR_INVALIDDATA。
5. 详细使用
5.1 命令行实战(高频场景)
bash
# 转码:H.264 视频 + AAC 音频 封装为 MP4
ffmpeg -i input.mkv -c:v libx264 -preset fast -crf 23 -c:a aac -b:a 128k output.mp4
# 只换封装,不重新编码(秒级完成)
ffmpeg -i input.mkv -c copy output.mp4
# 裁剪前 10 秒
ffmpeg -ss 00:00:05 -t 10 -i input.mp4 -c copy output.mp4
# 抽帧:每 2 秒存一张 JPEG
ffmpeg -i input.mp4 -vf fps=1/2 frame_%03d.jpg
# 缩放到 720p 并加水印
ffmpeg -i input.mp4 -vf "scale=-2:720,drawtext=text='Hello':x=10:y=10:fontsize=24:fontcolor=white" output.mp4
# RTSP 摄像头取流实时转码推流到 RTMP
ffmpeg -rtsp_transport tcp -i "rtsp://user:pass@192.168.1.10:554/stream" \
-c:v libx264 -preset ultrafast -tune zerolatency -f flv "rtmp://push.example.com/live/cam01"
# HLS 直播切片(3 秒一片)
ffmpeg -i input.mp4 -c:v libx264 -hls_time 3 -hls_list_size 0 -f hls out.m3u8
# 音视频分离
ffmpeg -i input.mp4 -vn audio.aac -an video.mp4 # 注意:-vn 去掉视频,-an 去掉音频
# 视频转 GIF
ffmpeg -i input.mp4 -vf "fps=10,scale=320:-1" -loop 0 out.gif
5.2 C 语言完整转码管线(可编译骨架)
下面是一个最小可用的转码程序骨架,串起"解封装 → 解码 → 缩放 → 编码 → 封装"整条链(已省略逐行错误处理,完整工程请按 4.x 的 API 逐段补齐):
cpp
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
#include <libavutil/opt.h>
#include <libavutil/imgutils.h>
int main(int argc, char *argv[]) {
const char *src_file = argv[1], *dst_file = argv[2];
// ---------- 解封装 ----------
AVFormatContext *in_fmt = NULL;
avformat_open_input(&in_fmt, src_file, NULL, NULL);
avformat_find_stream_info(in_fmt, NULL);
int v_in = av_find_best_stream(in_fmt, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
AVStream *in_stream = in_fmt->streams[v_in];
// ---------- 解码器 ----------
const AVCodec *dec = avcodec_find_decoder(in_stream->codecpar->codec_id);
AVCodecContext *dec_ctx = avcodec_alloc_context3(dec);
avcodec_parameters_to_context(dec_ctx, in_stream->codecpar);
avcodec_open2(dec_ctx, dec, NULL);
// ---------- 编码器(H.264 720p) ----------
const AVCodec *enc = avcodec_find_encoder(AV_CODEC_ID_H264);
AVCodecContext *enc_ctx = avcodec_alloc_context3(enc);
enc_ctx->width = 1280; enc_ctx->height = 720;
enc_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
enc_ctx->time_base = (AVRational){1, 25};
enc_ctx->framerate = (AVRational){25, 1};
av_opt_set(enc_ctx->priv_data, "preset", "fast", 0);
avcodec_open2(enc_ctx, enc, NULL);
// ---------- 封装输出 ----------
AVFormatContext *out_fmt = NULL;
avformat_alloc_output_context2(&out_fmt, NULL, NULL, dst_file);
AVStream *out_stream = avformat_new_stream(out_fmt, NULL);
avcodec_parameters_from_context(out_stream->codecpar, enc_ctx);
avio_open(&out_fmt->pb, dst_file, AVIO_FLAG_WRITE);
avformat_write_header(out_fmt, NULL);
// ---------- 像素转换(输入帧 -> 720p YUV) ----------
struct SwsContext *sws = sws_getContext(dec_ctx->width, dec_ctx->height, dec_ctx->pix_fmt,
enc_ctx->width, enc_ctx->height, enc_ctx->pix_fmt,
SWS_BILINEAR, NULL, NULL, NULL);
// ---------- 主循环 ----------
AVPacket *pkt = av_packet_alloc();
AVFrame *frame = av_frame_alloc();
AVFrame *scaled = av_frame_alloc();
scaled->format = enc_ctx->pix_fmt;
scaled->width = enc_ctx->width; scaled->height = enc_ctx->height;
av_frame_get_buffer(scaled, 0);
int64_t pts = 0;
while (av_read_frame(in_fmt, pkt) >= 0) {
if (pkt->stream_index == v_in) {
avcodec_send_packet(dec_ctx, pkt);
while (avcodec_receive_frame(dec_ctx, frame) >= 0) {
sws_scale(sws, frame->data, frame->linesize, 0,
dec_ctx->height, scaled->data, scaled->linesize);
scaled->pts = pts++; // 重新生成时间戳
avcodec_send_frame(enc_ctx, scaled);
while (avcodec_receive_packet(enc_ctx, pkt) >= 0) {
pkt->stream_index = out_stream->index;
av_packet_rescale_ts(pkt, enc_ctx->time_base, out_stream->time_base);
av_interleaved_write_frame(out_fmt, pkt);
}
}
}
av_packet_unref(pkt);
}
// flush 编码器尾帧
avcodec_send_frame(enc_ctx, NULL);
while (avcodec_receive_packet(enc_ctx, pkt) >= 0) {
pkt->stream_index = out_stream->index;
av_packet_rescale_ts(pkt, enc_ctx->time_base, out_stream->time_base);
av_interleaved_write_frame(out_fmt, pkt);
}
// ---------- 收尾 ----------
av_write_trailer(out_fmt);
avio_closep(&out_fmt->pb);
avformat_free_context(out_fmt);
avcodec_free_context(&dec_ctx);
avcodec_free_context(&enc_ctx);
avformat_close_input(&in_fmt);
sws_freeContext(sws);
av_frame_free(&frame);
av_frame_free(&scaled);
av_packet_free(&pkt);
return 0;
}
5.3 工业数采结合场景:RTSP 取流抽帧 + 推流
cpp
// 核心思路:RTSP 源 -> 解码 -> 抽关键帧(或按 fps 抽帧)-> JPEG 编码 -> 上报/落盘
// 1) 打开 RTSP(TCP 传输更稳)
AVDictionary *opts = NULL;
av_dict_set(&opts, "rtsp_transport", "tcp", 0);
av_dict_set(&opts, "stimeout", "3000000", 0); // 3 秒超时
avformat_open_input(&fmt_ctx, "rtsp://192.168.1.10:554/stream", NULL, &opts);
// 2) 解码后每 N 帧编码一张 JPEG
AVCodecContext *jpeg_ctx = /* avcodec_find_encoder(AV_CODEC_ID_MJPEG) ... */;
// 3) JPEG 数据可直接写 Kafka/TDengine/对象存储,或经 MQTT 上报
// 4) 断流检测:av_read_frame 返回 AVERROR_EOF / ETIMEDOUT 后按退避策略重连
6. 常错点
按"最容易踩、后果最重"排序:
| 坑点 | 后果 | 正确做法 | |
|---|---|---|---|
| 1 | 时间基混用:直接用 pkt->pts 乘除秒 | 音画不同步、时长错乱 | 一律 av_rescale_q 换算 |
| 2 | 忘 avcodec_parameters_to_context | 解码失败/花屏 | open2 前必拷参数 |
| 3 | 手工 free AVFrame/AVPacket 数据 | 双重释放崩溃 | 用 av_frame_unref/av_packet_unref |
| 4 | 只 av_frame_alloc 不 av_frame_get_buffer | 解码器写入野指针崩溃 | 解码帧由解码器分配;自建帧先 get_buffer |
| 5 | avcodec_receive_frame 返回 EAGAIN 就放弃 | 丢帧 | EAGAIN 是正常状态,继续喂包 |
| 6 | 包写到输出前不 rescale_ts | 输出文件时长错误/播放器快进 | 编码时间基 → 流时间基换算 |
| 7 | -c copy 时源与目标容器不兼容 | 输出损坏 | 先 ffprobe 确认 codec tag 兼容 |
| 8 | 输入像素格式与编码器要求不一致直接 send_frame | 编码失败 | 先用 sws_scale 转 YUV420P(多数编码器要求) |
| 9 | 滤镜图不调 avfilter_graph_config | 图不生效/崩溃 | 建图后必须 config |
| 10 | av_buffersrc_add_frame 后还继续用 frame | 数据被滤镜改坏 | 加 AV_BUFFERSRC_FLAG_KEEP_REF 或拷贝 |
| 11 | RTSP 默认 UDP 传输 | 丢包花屏/卡顿 | -rtsp_transport tcp |
| 12 | 解码循环只处理视频包不 unref 音频包 | 内存暴涨 | 每个 pkt 用后必 unref |
| 13 | 多线程访问同一 AVCodecContext | 未定义行为/崩溃 | 编解码器上下文线程亲和,加锁或每线程一实例 |
| 14 | 硬解/硬编不检查设备初始化 | 静默回退 CPU 性能暴跌 | 检查 open2 返回码与 hw_frames_ctx |
| 15 | B 帧场景下用 DTS 当 PTS 显示 | 播放顺序错乱 | 显示用 PTS,写入用解码排序 |
| 16 | 忘 flush:不 avcodec_send_frame(enc_ctx, NULL) | 末尾丢几帧 | 编码结束必须 flush |
| 17 | -ss 放在 -i 前/后语义不同 | 剪切位置不对 | 前面是"快进到时间点再解",后面是"解到时间点再剪" |
| 18 | HLS 切片时间基准与 -hls_time 不符 | 直播延迟/列表异常 | 注意 GOP 对齐,加 -g 帧率×秒数 |
| 19 | 音视频同步只对齐起始 PTS 不持续校正 | 越播越不同步 | 以音频为时钟,视频持续追赶或等待 |
| 20 | 忽略 av_read_frame 的 EOF/超时 | 死循环/无限重连风暴 | 区分正常 EOF 与错误,退避重连 |
6.1 两个经典疑难详解
画面花屏但文件时长正常:多半是编码器输入像素格式不对(比如编码器要 NV12 你给了 YUV420P),或 avcodec_parameters_to_context 缺失导致 SPS/PPS 解析错位。先用 ffprobe -show_streams 确认 pix_fmt,再用 ffmpeg -pix_fmts 查编码器支持列表。
音画不同步:转码时最常见原因是时间基换算出错或 pts 重生成逻辑不一致。排查步骤:ffprobe -show_frames 对比音视频 pkt_pts_time 序列,检查首帧差值是否恒定;若差值恒定说明起始不同步,若递增说明时钟漂移。
7. 性能优化
- 按需裁剪组件:--disable-everything + 只开需要的 demuxer/decoder/encoder,减小体积与初始化开销;
- 线程化:解码 avcodec_open2 前设 dec_ctx->thread_count(x264 编码器默认已多线程);多路流并行解码用多线程/多进程;
- 零拷贝路径:解码帧直接送滤镜/编码器,避免 av_frame_clone 后改数据;
- 硬件加速:NVIDIA 用 NVENC/NVDEC(h264_nvenc),Intel 用 QSV(h264_qsv),Linux 通用 VAAPI;纯 CPU 场景 -preset 从 slow 降到 fast/veryfast 可显著提速;
- 转封装优先于转码:能 -c copy 就不要重编码,吞吐量差一个数量级;
- 滤镜图合并:多个 scale/crop 合并进一张图,避免多次像素格式转换;
- 音频格式对齐:解码后直接按编码器格式 swr_convert,避免中间格式转换;
- 缓冲调优:推流场景 -max_delay、-re(实时读)按网络带宽调整,-g GOP 与切片对齐。
8. 总结
五句话带走:
- 容器 ≠ 编码:排查问题先 ffprobe 分清 format_name 与 codec_name;
- 时间基是一切:PTS/DTS 换算永远用 av_rescale_q,跨层必转;
- 引用计数内存模型:AVFrame/AVPacket 只 ref/unref,不手工 free;
- send/receive 推拉模型:EAGAIN 是正常状态,EOF 要 flush 冲刷缓冲;
- 先命令行验证、再 C API 集成:ffmpeg -vf 能跑通的效果,转成 C 就是一段可对照的滤镜图代码。
FFmpeg 是"读文档不如读代码、读代码不如跑命令"的典型项目------每一个命令行参数背后都是 libav* 的一串 API 调用,用 ffmpeg -h full 和 ffprobe 做探针,是学习它最快的路径。
9. FAQ
| 问题 | 速答 |
|---|---|
| 视频转码 CPU 占用 100% 正常吗? | 软件编码正常;要降占用用硬件编码或低 preset |
| -c copy 为什么有时报错? | 目标容器不支持源编码的 codec tag,需转码 |
| 如何判断文件是什么编码? | ffprobe -show_streams file 看 codec_name |
| RTSP 画面花屏怎么处理? | 换 TCP 传输、加大 -analyzeduration、检查设备码流 |
| 抽帧用什么格式最快? | MJPEG 编码器抽 JPEG,比 PNG 快且体积小 |
| FFmpeg 可以商用吗? | 核心是 LGPL/GPL,按自己是否修改源码/链接方式选许可证,商用前评估 LGPL 动态链接 |
| HLS 直播延迟大怎么调? | -hls_time 1-2、-hls_flags independent_segments、减小 GOP |
| 如何实时预览滤镜效果? | ffplay -vf "scale=640:360,drawtext=..." input.mp4 |
| 多路 RTSP 同时转码怎么规划? | 每路一个进程/线程组,独立 AVFormatContext,必要时 GPU 硬解多路 |
| 音频声道数变了但时长不变正常吗? | 正常,重采样只改变采样布局不影响内容时长 |