5、FFmpeg解码实战:FFmpeg解码流程、AVPacket与AVFrame、音频解码、视频解码

各位同学,欢迎来到第五章。前面几章我们把FFmpeg的编译、基础数据结构都过了一遍。今天咱们来点真格的——解码实战

说实话,我当年刚接触FFmpeg解码时,最头疼的就是搞不清AVPacket和AVFrame到底谁先谁后。代码跑起来要么花屏,要么没声音。后来踩了无数坑,才把整个流程理顺。今天我就把这点压箱底的经验全倒出来。

5.1 解码核心流程:别被API吓到

FFmpeg的解码,说白了就三步:打开 -> 喂数据 -> 取结果。但中间有几个关键节点,我习惯用一张图来记。

FFmpeg 解码核心流程 avformat_open_input() avformat_find_stream_info() avcodec_find_decoder() avcodec_open2() 循环:av_read_frame() → avcodec_send_packet() → avcodec_receive_frame()

嗯,这张图我画了好几次才满意。你看,前面四个步骤都是准备工作,真正的解码在最后那个循环里。我个人习惯把最后三步叫做「喂-拉-取」模型。

5.2 AVPacket 与 AVFrame:解码前后的「容器」

这两个结构体,是解码过程中最重要的两个「容器」。我刚开始学的时候,总把它们搞混。后来想了个笨办法:AVPacket是压缩后的数据,AVFrame是解码后的数据

5.2.1 AVPacket:压缩数据包

AVPacket 里装的是什么?说白了就是编码器吐出来的原始码流。比如 H.264 的 NALU,或者 AAC 的 ADTS 帧。它有几个关键字段:

字段 说明
uint8_t *data 压缩数据指针
int size 数据大小
int64_t pts 显示时间戳
int64_t dts 解码时间戳
int stream_index 所属流索引
💡 小提示:AVPacket 的生命周期很短。av_read_frame() 拿到后,用完记得 av_packet_unref() 释放。我曾经因为忘记释放,在解码 4K 视频时内存直接飙到 2GB,手机烫得能煎鸡蛋。

5.2.2 AVFrame:解码后的「画面」或「声音」

AVFrame 就丰富多了。对于视频,它存的是像素数据;对于音频,它存的是 PCM 采样数据。我挑几个常用的字段说说:

字段 说明
uint8_t *data[8] 数据指针数组(YUV 或 PCM)
int linesize[8] 每行字节数(对齐后的宽度)
int width, height 视频宽高
int nb_samples 音频采样数
int format 像素格式或采样格式
int64_t pts 显示时间戳
⚠️ 注意:AVFrame 的 linesize 不一定等于 width * bytes_per_pixel。因为内存对齐,linesize 可能比实际宽度大。如果你直接按 width 去拷贝数据,画面会「歪掉」。我踩过这个坑,花了两天才定位到问题。

5.3 视频解码实战:从 H.264 到 YUV

好了,理论说够了,咱们直接上代码。视频解码的核心逻辑,其实就是那个「喂-拉-取」循环。


// 1. 打开输入文件
AVFormatContext *fmt_ctx = NULL;
avformat_open_input(&fmt_ctx, "input.mp4", NULL, NULL);
avformat_find_stream_info(fmt_ctx, NULL);

// 2. 找到视频流
int video_stream_idx = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
AVStream *video_stream = fmt_ctx->streams[video_stream_idx];

// 3. 打开解码器
const AVCodec *decoder = avcodec_find_decoder(video_stream->codecpar->codec_id);
AVCodecContext *codec_ctx = avcodec_alloc_context3(decoder);
avcodec_parameters_to_context(codec_ctx, video_stream->codecpar);
avcodec_open2(codec_ctx, decoder, NULL);

// 4. 分配 packet 和 frame
AVPacket *pkt = av_packet_alloc();
AVFrame *frame = av_frame_alloc();

// 5. 解码循环
while (av_read_frame(fmt_ctx, pkt) >= 0) {
    if (pkt->stream_index == video_stream_idx) {
        // 喂数据给解码器
        int ret = avcodec_send_packet(codec_ctx, pkt);
        if (ret < 0) continue;

        // 拉取解码后的帧
        while (ret >= 0) {
            ret = avcodec_receive_frame(codec_ctx, frame);
            if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
            if (ret < 0) break;

            // 到这里,frame 里就是 YUV 数据了
            // 你可以渲染、保存或做其他处理
            // 比如:save_yuv_frame(frame, width, height);
        }
    }
    av_packet_unref(pkt);
}

// 6. 清理
av_frame_free(&frame);
av_packet_free(&pkt);
avcodec_free_context(&codec_ctx);
avformat_close_input(&fmt_ctx);

这段代码我几乎每个项目都会复用。你注意看那个内层 while 循环——avcodec_send_packet 一次,avcodec_receive_frame 可能多次。为什么?因为有些解码器(比如某些硬件解码器)会缓存帧,一次喂入可能吐出多帧。反过来,也可能喂多次才吐一帧。所以这个「一喂多取」的模式是最稳妥的。

5.4 音频解码实战:从 AAC 到 PCM

音频解码和视频解码的流程几乎一模一样。区别在于:音频的 AVFrame 里装的是 PCM 采样数据,而且有声道、采样率这些概念。


// 找到音频流
int audio_stream_idx = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
AVStream *audio_stream = fmt_ctx->streams[audio_stream_idx];

// 打开音频解码器
const AVCodec *audio_decoder = avcodec_find_decoder(audio_stream->codecpar->codec_id);
AVCodecContext *audio_codec_ctx = avcodec_alloc_context3(audio_decoder);
avcodec_parameters_to_context(audio_codec_ctx, audio_stream->codecpar);
avcodec_open2(audio_codec_ctx, audio_decoder, NULL);

// 解码循环(和视频一样)
while (av_read_frame(fmt_ctx, pkt) >= 0) {
    if (pkt->stream_index == audio_stream_idx) {
        avcodec_send_packet(audio_codec_ctx, pkt);
        while (avcodec_receive_frame(audio_codec_ctx, frame) >= 0) {
            // frame->data[0] 就是 PCM 数据
            // frame->nb_samples 是采样点数
            // frame->channels 是声道数
            // 你可以播放、混音或保存为 WAV
        }
    }
    av_packet_unref(pkt);
}
✅ 关键点:音频解码后,frame->data 的布局取决于采样格式。如果是 planar 格式(如 AV_SAMPLE_FMT_FLTP),每个声道的数据是分开存放的;如果是 interleaved 格式(如 AV_SAMPLE_FMT_FLT),所有声道交错存放在 data[0] 里。我建议你解码后统一转成 interleaved 格式,方便后续处理。

5.5 避坑指南:我踩过的那些坑

做解码这么多年,有些坑我几乎每个项目都会遇到。今天一次性说清楚:

  • 时间戳乱跳:av_read_frame 拿到的 pkt->pts 可能是乱的,尤其是 B 帧多的视频。解决办法是用 av_packet_rescale_ts() 统一转成微秒时间基。
  • 解码器没刷新:文件读完后,要调用一次 avcodec_send_packet(NULL) 来刷新解码器缓冲区。否则最后几帧可能丢。
  • 内存泄漏:AVPacket 和 AVFrame 一定要配对使用 av_packet_unref 和 av_frame_unref。我见过有人只 alloc 不 free,跑 10 分钟就 OOM。
  • 硬件解码回退:如果用了硬件解码器(如 MediaCodec),记得加个 fallback 到软件解码的逻辑。有些设备硬件解码器不支持某些 profile。

嗯,说到硬件解码,我记得有一次在某个低端平板上,硬解 H.265 一直报错,最后发现是驱动 bug。后来我加了个「硬解失败自动切软解」的逻辑,才算彻底解决。

5.6 本章小结

这一章我们走通了 FFmpeg 解码的完整流程。从 AVPacket 到 AVFrame,从视频到音频,核心就是那个「喂-拉-取」循环。你只要记住:av_read_frame 拿压缩包,avcodec_send_packet 喂给解码器,avcodec_receive_frame 取解码结果。剩下的就是处理各种边界情况。

下一章我们会深入音视频同步,那才是真正考验功底的地方。到时候我会分享一个我在直播项目中用的同步算法,保证让你眼前一亮。


公众号:蓝海资料掘金营,微信deep3321