FFmpeg 入门 101 — 高层架构概览与简易播放器实现
> 原文:FFmpeg 101 - Igalia
> 代码仓库:ffmpeg-101
FFmpeg 包含什么
FFmpeg 由一套工具和库组成。
命令行工具
这些工具可以编码/解码/转码多种音视频格式,还能通过网络推流。
- ffmpeg:在不同多媒体格式之间转换的命令行工具
- ffplay:基于 SDL 和 FFmpeg 库的简易播放器
- ffprobe:简易多媒体流分析器
库
这些库可以集成到你自己的产品中,实现相同功能。
| 库 | 功能 |
|---|---|
| **libavformat** | I/O 和 封装/解封装(muxing/demuxing) |
| **libavcodec** | 编码/解码 |
| **libavfilter** | 基于图的原始媒体滤镜 |
| **libavdevice** | 输入/输出设备 |
| **libavutil** | 通用多媒体工具函数 |
| **libswresample** | 音频重采样、采样格式转换和混音 |
| **libswscale** | 颜色转换和图像缩放 |
| **libpostproc** | 视频后处理(去块效应/降噪滤镜) |
FFmpeg 简易播放器
FFmpeg 最基本的用法:把一个多媒体流(来自文件或网络)解封装成音频流和视频流,然后把这些流解码成原始音频和视频数据。
核心数据结构
FFmpeg 用以下结构体管理媒体流:
| 结构体 | 作用 |
|---|---|
| **AVFormatContext** | 高层结构体,提供流的同步、元数据和封装管理 |
| **AVStream** | 一条连续的流(音频或视频) |
| **AVCodec** | 定义数据如何编码和解码 |
| **AVPacket** | 流中的编码数据(压缩的) |
| **AVFrame** | 解码后的数据(原始视频帧或原始音频采样) |
处理流程
解封装和解码遵循这个逻辑:
多媒体文件 → AVFormatContext(解封装)→ AVPacket(编码包)→ AVCodecContext(解码)→ 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 中。
AVStream* stream = format_context->streams[i];
// 根据流的 codec_id 查找兼容的解码器
const AVCodec* codec = avcodec_find_decoder(stream->codecpar->codec_id);
if (!codec) {
fprintf(stderr, "不支持的编解码器\n");
continue;
}
printf(" Codec: %s, bitrate: %" PRId64 "\n", codec->name, stream->codecpar->bit_rate);
if (codec->type == AVMEDIA_TYPE_VIDEO) {
printf(" 分辨率: %dx%d\n", stream->codecpar->width, stream->codecpar->height);
} else if (codec->type == AVMEDIA_TYPE_AUDIO) {
printf(" 声道数: %d, 采样率: %d Hz\n",
stream->codecpar->ch_layout.nb_channels,
stream->codecpar->sample_rate);
}
> 💡 你也可以创建自己的编解码器——只需创建一个 FFCodec 结构体实例,然后在 libavcodec/allcodecs.c 中注册为 extern const FFCodec。但那是另一个话题了。
第三步:初始化解码器上下文
有了正确的编解码器和从 AVStream 中提取的参数后,就可以分配 AVCodecContext 结构体用于解码。
重要:记住你要解码的流的索引号——后面解封装出来的包需要靠它来识别属于哪条流。
以下代码选择文件中第一条视频流:
// 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)
{
// 解封装后的包用 stream_index 标识它来自哪条 AVStream
printf("Packet received for stream %02d, pts: %" PRId64 "\n",
packet->stream_index, packet->pts);
// 只解码之前识别出的第一条视频流
if (packet->stream_index == first_video_stream_index)
{
// 把编码包送进解码器
int res = avcodec_send_packet(codec_context, packet);
if (res < 0) {
fprintf(stderr, "无法将包送入解码器: %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, "从解码器接收帧时出错: %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() 时会自动解引用并回收
}
}
// 解引用包内部内容,回收给下一个解封装包使用
av_packet_unref(packet);
}
// 释放之前为各 FFmpeg 结构体分配的内存
end:
av_packet_free(&packet);
av_frame_free(&frame);
avcodec_free_context(&codec_context);
avformat_close_input(&format_context);
构建和运行
pip3 install meson ninja
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
Type: video
FourCC: avc1
Codec: h264, bitrate: 47094
Video resolution: 206x80
---- Stream 01
Time base: 1/44100
Type: audio
FourCC: mp4a
Codec: aac, bitrate: 112000
Audio: 2 channels, sample rate: 44100 Hz
Frame 01, type: I, format: 0, pts: 000, keyframe: true
Frame 02, type: P, format: 0, pts: 100, keyframe: false
Frame 03, type: P, format: 0, pts: 200, keyframe: false
...
要点总结
整个流程就四步:
1. avformat_open_input → 打开文件
2. avcodec_find_decoder → 找解码器
3. avcodec_send_packet → 喂数据
4. avcodec_receive_frame → 取帧
理解了这个 send/receive 的 FIFO 模型,FFmpeg 的 C API 就不神秘了。后续可以在 AVFrame 上做任何事:渲染、转码、加滤镜、写文件。