本文翻译自 FFmpeg 101,原载于 Hacker News。
FFmpeg 简介
FFmpeg 是多媒体处理领域的瑞士军刀,无论是视频转码、音频提取,还是流媒体传输,它都能胜任。本文将从架构层面带你快速上手 FFmpeg,理解其核心组件和工作原理。
FFmpeg 包的组成
FFmpeg 由一套工具和库组成,我们可以根据需求选择命令行工具直接使用,或者通过库函数集成到自己的产品中。
FFmpeg 命令行工具
这些工具可以直接在终端中使用:
- ffmpeg:最常用的命令行工具,用于在各种多媒体格式之间转换
- ffplay:基于 SDL 和 FFmpeg 库的简易媒体播放器,适合快速预览
- ffprobe:多媒体流分析器,可以查看文件的编码信息、元数据等
FFmpeg 核心库
如果你想在自己的程序中集成多媒体处理能力,可以使用这些库:
- libavformat:负责 I/O 操作和复用/解复用(muxing/demuxing)
- libavcodec:核心编解码库
- libavfilter:基于图形的滤镜框架,用于处理原始媒体数据
- libavdevice:输入/输出设备支持
- libavutil:通用多媒体工具函数
- libswresample:音频重采样、采样格式转换和音频混音
- libswscale:颜色转换和图像缩放
- libpostproc:视频后处理(去块效应/降噪滤镜)
实现一个简单的 FFmpeg 播放器
FFmpeg 的基本使用场景之一是:从文件或网络获取多媒体流,解复用(demux)成音频流和视频流,然后解码成原始的音频和视频数据。
核心数据结构
FFmpeg 使用以下核心结构来管理媒体流:
- AVFormatContext:高层结构,提供流的同步、元数据和复用功能
- AVStream:表示一个连续的流(音频或视频)
- AVCodec:定义数据的编码和解码方式
- AVPacket:流中的编码数据包
- AVFrame:解码后的数据(原始视频帧或原始音频采样)
处理流程
整个解复用和解码的流程如下:
第一步:打开文件并分析流
下面的代码展示了如何从文件中读取编码的多媒体流,分析其内容并解复用音频和视频流。这些功能由 libavformat 库提供,使用 AVFormatContext 和 AVStream 结构来存储信息。
// 为上下文结构分配内存
AVFormatContext* format_context = avformat_alloc_context();
// 打开多媒体文件(如 mp4 文件或 FFmpeg 识别的任何格式)
avformat_open_input(&format_context, filename, NULL, NULL);
printf("File: %s, format: %s\n", filename, format_context->iformat->name);
// 分析文件内容并识别其中的流
avformat_find_stream_info(format_context, NULL);
// 列出所有流
for (unsigned int i = 0; i < format_context->nb_streams; ++i)
{
AVStream* stream = format_context->streams[i];
printf("---- Stream %02d\n", i);
printf(" Time base: %d/%d\n", stream->time_base.num, stream->time_base.den);
printf(" Framerate: %d/%d\n", stream->r_frame_rate.num, stream->r_frame_rate.den);
printf(" Start time: %" PRId64 "\n", stream->start_time);
printf(" Duration: %" PRId64 "\n", stream->duration);
printf(" Type: %s\n", av_get_media_type_string(stream->codecpar->codec_type));
uint32_t fourcc = stream->codecpar->codec_tag;
printf(" FourCC: %c%c%c%c\n", fourcc & 0xff, (fourcc >> 8) & 0xff, (fourcc >> 16) & 0xff, (fourcc >> 24) & 0xff);
}
// 关闭多媒体文件并释放上下文结构
avformat_close_input(&format_context);
第二步:查找解码器
获取到多媒体文件中的各个流之后,我们需要找到相应的编解码器来将这些流解码成原始的音频和视频数据。所有的编解码器都静态地包含在 libavcodec 中。
💡 扩展知识:你也可以创建自己的编解码器,只需创建一个
FFCodec结构体的实例,并在libavcodec/allcodecs.c中将其注册为extern const FFCodec。不过这是另一个话题了。
要找到与 AVStream 内容对应的编解码器,可以使用以下代码:
// 从之前的流列表循环中获取的 AVStream
AVStream* stream = format_context->streams[i];
// 查找兼容的解码器
const AVCodec* codec = avcodec_find_decoder(stream->codecpar->codec_id);
if (!codec)
{
fprintf(stderr, "Unsupported codec\n");
continue;
}
printf(" Codec: %s, bitrate: %" PRId64 "\n", codec->name, stream->codecpar->bit_rate);
if (codec->type == AVMEDIA_TYPE_VIDEO)
{
printf(" Video resolution: %dx%d\n", stream->codecpar->width, stream->codecpar->height);
}
else if (codec->type == AVMEDIA_TYPE_AUDIO)
{
printf(" Audio: %d channels, sample rate: %d Hz\n",
stream->codecpar->ch_layout.nb_channels,
stream->codecpar->sample_rate);
}
第三步:初始化解码器上下文
有了正确的编解码器和从 AVStream 中提取的编解码器参数,我们现在可以分配 AVCodecContext 结构,用于解码相应的流。
⚠️ 注意:记住要保存我们要解码的流在流列表中的索引,因为后续这个索引将用于标识由
AVFormatContext解复用出来的数据包。
下面的代码将选择多媒体文件中的第一个视频流:
// first_video_stream_index 在之前的流列表循环中确定
int first_video_stream_index = ...;
AVStream* first_video_stream = format_context->streams[first_video_stream_index];
AVCodecParameters* first_video_stream_codec_params = first_video_stream->codecpar;
const AVCodec* first_video_stream_codec = avcodec_find_decoder(first_video_stream_codec_params->codec_id);
// 为解码上下文结构分配内存
AVCodecContext* codec_context = avcodec_alloc_context3(first_video_stream_codec);
// 使用编解码器参数配置解码器
avcodec_parameters_to_context(codec_context, first_video_stream_codec_params);
// 打开解码器
avcodec_open2(codec_context, first_video_stream_codec, NULL);
第四步:解复用和解码
现在我们有了一个运行中的解码器,可以使用 AVFormatContext 结构提取解复用的数据包,并将它们解码成原始视频帧。我们需要两个不同的结构:
AVPacket:包含从输入多媒体文件中提取的编码数据包AVFrame:在AVCodecContext解码前面的数据包后,将包含原始视频帧
// 为编码数据包结构分配内存
AVPacket* packet = av_packet_alloc();
// 为解码帧结构分配内存
AVFrame* frame = av_frame_alloc();
// 从输入多媒体文件中解复用下一个数据包
while (av_read_frame(format_context, packet) >= 0)
{
// 解复用的数据包使用流索引来标识它来自哪个 AVStream
printf("Packet received for stream %02d, pts: %" PRId64 "\n", packet->stream_index, packet->pts);
// 在我们的示例中,我们只解码之前由 first_video_stream_index 标识的第一个视频流
if (packet->stream_index == first_video_stream_index)
{
// 将数据包发送到之前初始化的解码器
int res = avcodec_send_packet(codec_context, packet);
if (res < 0)
{
fprintf(stderr, "Cannot send packet to the decoder: %s\n", av_err2str(res));
break;
}
// 解码器(AVCodecContext)就像一个 FIFO 队列,我们在一端推入编码数据包,
// 需要从另一端轮询来获取解码后的帧。编解码器实现可能使用不同的线程来执行实际的解码。
// 轮询运行中的解码器以获取到目前为止所有可用的解码帧
while (res >= 0)
{
// 获取下一个可用的解码帧
res = avcodec_receive_frame(codec_context, frame);
if (res == AVERROR(EAGAIN) || res == AVERROR_EOF)
{
// 解码器输出队列中没有更多的解码帧,继续下一个编码数据包
break;
}
else if (res < 0)
{
fprintf(stderr, "Error while receiving a frame from the decoder: %s\n", av_err2str(res));
goto end;
}
// 现在 AVFrame 结构包含一个解码后的原始视频帧,我们可以进一步处理它...
printf("Frame %02" PRId64 ", type: %c, format: %d, pts: %03" PRId64 ", keyframe: %s\n",
codec_context->frame_num, av_get_picture_type_char(frame->pict_type), frame->format, frame->pts,
(frame->flags & AV_FRAME_FLAG_KEY) ? "true" : "false");
// AVFrame 的内部内容在下一次调用 avcodec_receive_frame(codec_context, frame) 时会自动 unref 和回收
}
}
// Unref 数据包的内部内容,以便为下一个解复用的数据包回收它
av_packet_unref(packet);
}
// 释放之前为各种 FFmpeg 结构分配的内存
end:
av_packet_free(&packet);
av_frame_free(&frame);
avcodec_free_context(&codec_context);
avformat_close_input(&format_context);
完整流程图
上述代码的工作方式可以用下图总结:
编译和运行示例
完整的示例代码可以在原文仓库中找到。
编译需要 meson 和 ninja。如果你已经安装了 python 和 pip,可以通过 pip3 install meson ninja 轻松安装。
编译步骤:
# 解压示例代码到 ffmpeg-101 文件夹
cd ffmpeg-101
meson setup build # 如果系统中没有安装 FFmpeg,会自动下载正确的版本
ninja -C build # 编译代码
./build/ffmpeg-101 sample.mp4 # 运行
示例输出
File: sample.mp4, format: mov,mp4,m4a,3gp,3g2,mj2
---- Stream 00
Time base: 1/3000
Framerate: 30/1
Start time: 0
Duration: 30000
Type: video
FourCC: avc1
Codec: h264, bitrate: 47094
Video resolution: 206x80
---- Stream 01
Time base: 1/44100
Framerate: 0/0
Start time: 0
Duration: 440320
Type: audio
FourCC: mp4a
Codec: aac, bitrate: 112000
Audio: 2 channels, sample rate: 44100 Hz
Packet received for stream 00, pts: 0
Send video packet to decoder...
Frame 01, type: I, format: 0, pts: 000, keyframe: true
Packet received for stream 00, pts: 100
Send video packet to decoder...
Frame 02, type: P, format: 0, pts: 100, keyframe: false
...
关键要点总结
- FFmpeg 架构清晰:工具层(ffmpeg/ffplay/ffprobe)用于命令行操作,库层(libav*)用于程序集成
- 核心概念:
AVFormatContext管理整个媒体文件AVStream代表单个流(音频或视频)AVPacket是编码数据,AVFrame是解码后的原始数据
- 解码流程:打开文件 → 分析流信息 → 查找解码器 → 初始化解码上下文 → 循环读取数据包 → 发送到解码器 → 接收解码帧
- 解码器是 FIFO 队列:推送编码数据包后,需要轮询获取解码帧,可能存在延迟
- 内存管理:FFmpeg 使用引用计数,记得在适当的时候调用
av_packet_unref()和各种*_free()函数
FFmpeg 是学习多媒体处理的绝佳起点,理解了这些基础概念后,你就可以进一步探索滤镜、编码、流媒体传输等更高级的主题了。