FFmpeg 入门 101 — 高层架构概览与简易播放器实现

> 原文:FFmpeg 101 - Igalia

> 代码仓库:ffmpeg-101

FFmpeg 包含什么

FFmpeg 由一套工具和库组成。

命令行工具

这些工具可以编码/解码/转码多种音视频格式,还能通过网络推流。

这些库可以集成到你自己的产品中,实现相同功能。

功能
**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 库提供,使用 AVFormatContextAVStream 结构体存储信息。


// 为格式上下文分配内存
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* 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);

构建和运行

需要 mesonninja


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 上做任何事:渲染、转码、加滤镜、写文件。