FFmpeg 开源多媒体处理框架深度解析:从命令行到 libav* 全家桶

适用人群: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. 性能优化

  1. 按需裁剪组件:--disable-everything + 只开需要的 demuxer/decoder/encoder,减小体积与初始化开销;
  2. 线程化:解码 avcodec_open2 前设 dec_ctx->thread_count(x264 编码器默认已多线程);多路流并行解码用多线程/多进程;
  3. 零拷贝路径:解码帧直接送滤镜/编码器,避免 av_frame_clone 后改数据;
  4. 硬件加速:NVIDIA 用 NVENC/NVDEC(h264_nvenc),Intel 用 QSV(h264_qsv),Linux 通用 VAAPI;纯 CPU 场景 -preset 从 slow 降到 fast/veryfast 可显著提速;
  5. 转封装优先于转码:能 -c copy 就不要重编码,吞吐量差一个数量级;
  6. 滤镜图合并:多个 scale/crop 合并进一张图,避免多次像素格式转换;
  7. 音频格式对齐:解码后直接按编码器格式 swr_convert,避免中间格式转换;
  8. 缓冲调优:推流场景 -max_delay、-re(实时读)按网络带宽调整,-g GOP 与切片对齐。

8. 总结

五句话带走:

  1. 容器 ≠ 编码:排查问题先 ffprobe 分清 format_name 与 codec_name;
  2. 时间基是一切:PTS/DTS 换算永远用 av_rescale_q,跨层必转;
  3. 引用计数内存模型:AVFrame/AVPacket 只 ref/unref,不手工 free;
  4. send/receive 推拉模型:EAGAIN 是正常状态,EOF 要 flush 冲刷缓冲;
  5. 先命令行验证、再 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 硬解多路
音频声道数变了但时长不变正常吗? 正常,重采样只改变采样布局不影响内容时长
相关推荐
m0_547486661 小时前
《数据结构:基于C语言的描述》全套PPT课件2026
c语言·数据结构
启友玩AI2 小时前
微安级唤醒、毫安级识别:基于 CI3422 的 AI 眼镜语音交互开发实战
c语言·人工智能·科技·嵌入式硬件·ai·ar·语音识别
wuminyu2 小时前
stackChunkOop内部结构与GC屏障的配合工作原理剖析
java·linux·c语言·jvm·c++
小π军2 小时前
C 和 C++ 是否都支持函数的重载?
java·c语言·c++
miofly2 小时前
GitHub 日榜趋势速报 | 2026-10-13
开源·github
刘发财13 小时前
前端如何生成10000页PDF文件?
前端·javascript·开源
Erishen14 小时前
Lume C11编译器内部架构:从树形解释器到双LLVM后端的设计之旅
架构·开源·编译器
冬奇Lab14 小时前
一天一个开源项目(第234篇):CC Switch —— 一个 App 管理十款 AI 编程工具的 Provider、MCP、Skills 和 Prompts
人工智能·开源·claude
小蠢驴打代码14 小时前
给 Codex Plan 模式加一本决策账本:问过什么、选了什么,都进仓库
开源·openai·ai编程