第9章:编码器 API:从打开输出到创建流

⚖️

本章导师:包青天

核心方法论:铁面无私

「本官办案,只看证据。API 返回的每一个错误码,都是呈堂证供——你敢忽略它,它就敢让你的程序在线上崩溃。这一章我们按素材的九步流程走一遍编码器初始化:每一步的返回值都必须检查,这就是 C API 世界里的铁面无私。」

本章前置:需要第 1-4 章的容器/编码/时间基概念,以及命令行转码的直觉。第 9-13 章是一整条 C API 主线:本章搭好"打开输出 → 创建流 → 找编码器 → 配参数 → 打开编码器"的骨架,第 10 章深入参数,第 11 章讲 send/receive 循环,第 12 章讲 flush 收尾,第 13 章把输入输出换成自定义 IO。跟着走,五章之后你就能手写一个转码器。

9.1 API 全景:三个库的分工与对象模型

第 2 章里我们一直在敲命令行:ffmpeg -i in.mp4 -c:v libx264 out.mp4。这条命令背后发生了什么?FFmpeg 的官方命令行工具本身就是一个巨大的 C 程序,它调用的是三个核心库的 API——你完全可以用同样的 API 写自己的转码器,嵌入业务逻辑(加水印、传进度、从内存读、传到云端)。

三个库的分工,一句话各一个:

职责对应命令行概念
libavformat封装与解封装:读写容器(mp4/mkv/ts…),拆装"档案袋"输入输出文件、流
libavcodec编解码:压缩与解压帧数据,干最重的活-c:v / -c:a
libavutil工具箱:内存管理、有理数、日志、通道布局等公共设施一切基础设施

这四个结构体是整条 API 主线的"四世同堂",从大到小:

// 对象模型:从外到内
AVFormatContext *ofmt;   // ① 封装器:整个输出文件("档案袋")
    AVStream *st;          // ② 流:文件里的一条轨(视频/音频各一条)
        AVCodecContext *enc; // ③ 编解码上下文:编码器的"工作台",参数都摆这
            AVCodec *codec;     // ④ 编解码器:干活的"师傅"(libx264 / aac…)

对象模型的理解顺序:先找师傅(codec),再摆工作台(codec_ctx),然后把工作台挂到一条流(stream)上,最后把流放进档案袋(format_ctx)。但 API 的调用顺序恰好相反——因为你得先有档案袋才能往里面放流,有了流才知道这条流要什么师傅。这就是素材《ffmpeg-编码的步骤》记录的九步流程,本章一步步走完它。

// 命令行 ↔ API 对照:同一件事的两种说法
// 命令行(第 2 章)                 // C API(本章起)
ffmpeg -i in.wav -c:a aac out.aac    = avformat_alloc_output_context2(&ofmt, NULL, NULL, "out.aac")
                                     + avformat_new_stream(ofmt, NULL)
-c:a aac                            = avcodec_find_encoder_by_name("aac")
-b:a 64k                            = enc_ctx->bit_rate = 64000

这张对照表是全章的"地图":每一行命令行选项,都能在 API 里找到对应的一个或几个调用。往后读到某个 API 函数想不起用途时,回头看看命令行你用过的那条指令——它们说的是同一件事。

tip:素材(2019 年)写的是音频编码(AAC),本章沿用。视频编码流程结构完全一样,只是参数从采样率换成宽高和像素格式——学完音频,视频自然就会。

9.2 第一步:打开输出文件流

一切从 avformat_alloc_output_context2 开始。它做两件事:分配一个 AVFormatContext,并且根据输出文件名猜出封装器(.aac → ADTS、.mp4 → MP4、.mkv → Matroska)。第三个参数传 NULL 就是"你看着办"。

AVFormatContext *ofmt_ctx = NULL;
int ret = avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, "out.aac");
if (ret < 0) {
    av_log(NULL, AV_LOG_ERROR, "无法创建输出上下文: %s\n", av_err2str(ret));
    return ret;
}
printf("oformat = %s\n", ofmt_ctx->oformat->name);  // 猜对了哪个封装器

注意包青天式写法:每一步的返回值都要检查av_err2str 把错误码转成人类可读文本——这是排查问题的第一手证据。本机实测(FFmpeg 8.1.1):

# 实测输出(2026-08-08,Apple clang 21 / arm64)
$ gcc enc_steps.c $(pkg-config --cflags --libs libavformat libavcodec libavutil) -o enc_steps && ./enc_steps
[ok  ] avformat_alloc_output_context2
       oformat = adts        <- 看到 out.aac 就自动选了 ADTS 封装器

pkg-config 一行搞定三个库的头文件路径和链接参数——这是编译 FFmpeg 程序的标准姿势,本章所有示例都用它编译。

warning:素材原文这里写的是 av_log(); 空调用——占位符没填参数。真正的 av_log 需要级别、上下文和格式串,直接抄素材会编译不过。本章所有示例都是修好、跑通的版本。

9.3 第二步:创建输出音频流

档案袋有了,往里放一条流。avformat_new_stream 在容器里注册一条流并返回它的 AVStream——这时候它还是一条"空流",不知道自己要编什么码。

AVStream *audio_stream = avformat_new_stream(ofmt_ctx, NULL);
if (!audio_stream) {
    av_log(NULL, AV_LOG_ERROR, "无法创建音频流\n");
    return -1;
}
printf("stream index = %d\n", audio_stream->index);  // 这条流在容器里的编号(通常 0)

第二个参数传 NULL 表示"流的信息稍后从编解码器上下文拷贝"——对应素材第七步的 avcodec_parameters_from_context。也就是说:先创建空流,配好编码器,最后把参数灌回流

# 实测
[ok  ] avformat_new_stream

tip:一个容器可以有多条流(视频 + 音频 + 字幕)。转码时循环调用 avformat_new_stream 就能建多条——每条流一个 index,后面写包时靠它告诉封装器"这个包属于哪条轨"。

9.4 第三步:找编码器

现在要给流请师傅。素材的写法是 avcodec_find_encoder(AV_CODEC_ID_AAC)——按编码器 ID 找。这个函数在现行版本已经标记 deprecated(弃用),推荐按名字找,更精确:

// 素材旧写法(已弃用,但仍可用):按 ID 找
AVCodec *enc_old = avcodec_find_encoder(AV_CODEC_ID_AAC);

// 现行推荐写法:按名字找(更精确,还能找到 libx264 这类第三方编码器)
const AVCodec *encoder = avcodec_find_encoder_by_name("aac");
if (!encoder) {
    av_log(NULL, AV_LOG_ERROR, "找不到 aac 编码器——检查 ffmpeg 编译时是否启用了它\n");
    return -1;
}

为什么弃用按 ID 找?因为一个 ID 背后可能挂多个实现(AAC 有原生的 aac、Apple 的 aac_at、FDK 的 libfdk_aac……),find_encoder 返回"第一个",结果可能不是你想要的。按名字找就是点名要谁。回顾第 2 章 -c:a aac-c:a aac_at——命令行的 -c:a 后面跟的正是编码器名字,API 里就是 avcodec_find_encoder_by_name 的第一个参数。

# 实测
[ok  ] avcodec_find_encoder_by_name("aac")

warning:素材第三步直接 avcodec_find_encoder(AV_CODEC_ID_AAC) 且不检查空指针。如果 ffmpeg 编译时没启用 AAC 编码器(少见但可能),返回 NULL,下一步 avcodec_alloc_context3(NULL) 会直接崩溃。包青天规矩:找完必查。

9.5 第四到六步:分配上下文与参数设置

师傅请来了,给他摆工作台。avcodec_alloc_context3 分配 AVCodecContext,然后逐项设置参数。素材这部分的参数写法在现行版本有一处重要变化

AVCodecContext *enc_ctx = avcodec_alloc_context3(encoder);
if (!enc_ctx) { av_log(NULL, AV_LOG_ERROR, "分配上下文失败\n"); return -1; }

// —— 参数设置(素材旧写法 → 现行写法)——
enc_ctx->bit_rate   = 64000;      // 目标码率 64kbps(第 10 章:目标不是承诺)
enc_ctx->sample_rate = 44100;     // 采样率 44.1kHz

// 旧:enc_ctx->channel_layout = 2;           // 直接写整数,早已弃用
// 新:通道布局用 AVChannelLayout 结构体
av_channel_layout_default(&enc_ctx->ch_layout, 2);  // 双声道

enc_ctx->sample_fmt = encoder->sample_fmts[0];        // 用编码器支持的第一种采样格式
enc_ctx->time_base = (AVRational){1, enc_ctx->sample_rate};  // 时间基跟采样率走

逐条解读(柯南视角,数据流会怎么变):

# 实测
[ok  ] 参数设置(bit_rate/sample_rate/ch_layout/sample_fmt/time_base)
       sample_fmt = fltp, channels = 2

warning:素材第五步还手动设了 enc_ctx->channels = av_get_channel_layout_nb_channels(...)——这个字段在现行版本同样弃用了,通道数由 ch_layout.nb_channels 自动推导(上面实测打印出 channels = 2 就是它)。照抄旧代码会得到一堆 deprecation 警告。

tip:另一个版本差异彩蛋——本机编译时 encoder->sample_fmts 本身也报了 deprecated 警告。现行 FFmpeg 正把"编码器能力表"从 AVCodec 结构体搬到运行时查询(avcodec_get_supported_config)。用 sample_fmts[0] 目前仍然工作(只是警告),但新代码建议走运行时查询——这正是素材(2019)→ 现行(2026)API 演进的活标本。

9.6 第七到九步:打开编码器、收尾三项

工作台摆好了,师傅上岗:avcodec_open2。它会检查参数自洽性(采样率、通道数、采样格式是否匹配),任何矛盾都在这一步暴露。打开之后有三件收尾小事,素材第七到九步:

// 第七步:打开编码器
int ret = avcodec_open2(enc_ctx, encoder, NULL);
if (ret < 0) {
    av_log(NULL, AV_LOG_ERROR, "打开编码器失败: %s\n", av_err2str(ret));
    return ret;
}

// 第八步:codec_tag 清零——流级别覆盖封装器默认的编码标签
audio_stream->codecpar->codec_tag = 0;

// 第九步:把编码器参数(码率/采样率/格式等)拷贝到流的 codecpar
ret = avcodec_parameters_from_context(audio_stream->codecpar, enc_ctx);
if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "拷贝参数失败\n"); return ret; }

等等,素材第六步不是 GLOBAL_HEADER 吗?对,它在打开编码器之前设置:

// 第六步:全局头标志——部分封装器要求编解码参数放容器头而非每个包
if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
    enc_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

这个判断的意思是:先问封装器"你要不要全局头?"。MP4 要(extradata 写进文件头),ADTS 不要(每帧自带头)。avcodec_open2 会读这个标志决定把 extradata 放哪。实测 ADTS 封装器没有 AVFMT_GLOBALHEADER,标志不置位——符合预期。

# 实测(完整九步)
[ok  ] avformat_alloc_output_context2        oformat = adts
[ok  ] avformat_new_stream
[ok  ] avcodec_find_encoder_by_name("aac")
[ok  ] avcodec_alloc_context3
[ok  ] 参数设置(...)                          sample_fmt = fltp, channels = 2
[ok  ] GLOBAL_HEADER flag
[ok  ] avcodec_open2                          frame_size = 1024
[ok  ] codec_tag = 0
[ok  ] avcodec_parameters_from_context

=== 九步全部就绪,可以开始编码了(第 11 章 send/receive)===

frame_size = 1024 是编码器揭晓的关键数字:AAC 每次吃 1024 个采样样本,产出一个压缩包。你的数据流必须按这个尺寸喂给它——第 11 章的 send/receive 循环就是干这个的

warning:素材把 codec_tag 设 0 的理由没写。实际原因:avformat_new_stream 创建的流有时会带上封装器默认的 codec_tag,显式清零避免与 avcodec_parameters_from_context 拷贝进来的真实参数打架。照抄就对了,但要知道它在防什么。

tip:到这里,骨架已经立起来了。还差三件事才能真正产出文件:① avio_open 打开实际输出(第 13 章会换成自定义 IO);② avformat_write_header 写容器头;③ 循环喂数据 + av_write_trailer 收尾——第 11、12 章专讲。

章末练习

练习 1:对象模型连线 入门

把四个结构体和它们的"人话比喻"配对:AVFormatContext / AVStream / AVCodecContext / AVCodec,对应:档案袋 / 一条轨 / 工作台 / 师傅。

提示

从外到内:档案袋装轨,轨上摆工作台,工作台请师傅。

参考答案

AVFormatContext=档案袋,AVStream=一条轨,AVCodecContext=工作台,AVCodec=师傅。

练习 2:找编码器两种写法 进阶

素材用 avcodec_find_encoder(AV_CODEC_ID_AAC),现行推荐 avcodec_find_encoder_by_name("aac")。请说明:① 为什么按 ID 找可能"找错师傅"?② 想用 libx264 编码视频,按名字找应该传什么字符串?

提示

想想一个 ID 背后挂几个实现;再想想命令行 -c:v 后面跟什么。

参考答案

① 同一 ID(如 AAC)有多个实现(aac/aac_at/libfdk_aac),按 ID 返回"第一个",不一定是你要的。② avcodec_find_encoder_by_name("libx264")——与 -c:v libx264 同名。

练习 3:GLOBAL_HEADER 的判断 进阶

写出判断逻辑:什么条件下 enc_ctx->flags 要置 AV_CODEC_FLAG_GLOBAL_HEADER?ADTS 和 MP4 分别是置还是不置?

提示

看封装器的 oformat->flags 有没有 AVFMT_GLOBALHEADER

参考答案

if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) enc_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;。MP4 有该标志→置位;ADTS 没有→不置位。

练习 4:把九步补全成可运行程序 挑战

本章的 enc_steps.c 跑到九步就收尾了。请在此基础上补上 avio_open + avformat_write_header,并在打开编码器后打印 frame_size。编译运行,验证输出的 .aac 文件用 ffprobe 能看到 aac 编码、44100 Hz。

提示

avio_open(&ofmt_ctx->pb, "out.aac", AVIO_FLAG_WRITE) 在 write_header 前调用;头文件和链接参数用 pkg-config --cflags --libs libavformat libavcodec libavutil

参考答案

补三行:avio_open(&ofmt_ctx->pb, "out.aac", AVIO_FLAG_WRITE)avformat_write_header(ofmt_ctx, NULL);结尾 av_write_trailer(ofmt_ctx) + avio_closep(&ofmt_ctx->pb)。ffprobe 应显示 Audio: aac (LC), 44100 Hz, stereo, fltp

下一章预告:第 10 章《编码器配置:参数设置的艺术》——bit_rate 真的是目标吗?sample_fmt 为什么必须听编码器的?第 11 章进入 send/receive 循环,把数据真正喂给编码器。