20、FFmpeg解码:使用FFmpeg解码视频帧、音频帧、格式转换

说实话,FFmpeg解码这块,是音视频开发里最绕不开的硬骨头。我记得刚入行那会儿,对着API文档啃了整整一周,才把整个解码流程跑通。今天我就把这些年踩过的坑、总结的经验,一次性给你讲透。

20.1 解码前的准备工作

在开始解码之前,你得先搞清楚一件事:FFmpeg的解码器不是拿来就能用的。它需要先初始化,再打开,最后才能干活。我见过太多新手直接跳过初始化步骤,结果程序跑起来就崩。

核心流程:

  1. 注册所有编解码器(avcodec_register_all)
  2. 查找解码器(avcodec_find_decoder)
  3. 分配解码器上下文(avcodec_alloc_context3)
  4. 打开解码器(avcodec_open2)

你想想看,这四步缺一步都不行。尤其是第三步,很多人以为分配了上下文就能直接用,其实还得把流参数填进去。

// 初始化解码器
avcodec_register_all();

// 查找H.264解码器
AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) {
    // 我遇到过这种情况,有些FFmpeg版本没编译H.264解码器
    fprintf(stderr, "Codec not found\n");
    return -1;
}

// 分配上下文
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
    fprintf(stderr, "Could not allocate codec context\n");
    return -1;
}

// 设置参数(从流中获取)
codec_ctx->width = 1920;
codec_ctx->height = 1080;
codec_ctx->time_base = (AVRational){1, 25};

// 打开解码器
if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
    fprintf(stderr, "Could not open codec\n");
    return -1;
}

注意:avcodec_register_all()在FFmpeg 4.0之后已经废弃了,新版本会自动注册。但为了兼容老版本,我建议你还是加上。

20.2 视频帧解码实战

视频帧解码,说白了就是把压缩的H.264/H.265数据,变成你能看到的YUV或RGB像素。这个过程分两步:先送压缩数据进去,再取解码后的帧出来。

为什么会这样?因为FFmpeg内部有缓冲区,你送一包数据进去,它可能吐出一帧,也可能吐出多帧。尤其是B帧存在的时候,解码顺序和显示顺序是不一样的。

// 准备输入数据包
AVPacket *pkt = av_packet_alloc();
pkt->data = compressed_data;  // 你的H.264数据
pkt->size = compressed_size;

// 准备输出帧
AVFrame *frame = av_frame_alloc();

// 发送压缩数据到解码器
int ret = avcodec_send_packet(codec_ctx, pkt);
if (ret < 0) {
    fprintf(stderr, "Error sending packet\n");
    return -1;
}

// 循环接收解码后的帧
while (ret >= 0) {
    ret = avcodec_receive_frame(codec_ctx, frame);
    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
        break;
    } else if (ret < 0) {
        fprintf(stderr, "Error during decoding\n");
        break;
    }
    
    // 这里frame就是解码后的视频帧
    // 可以拿去渲染或保存
    process_video_frame(frame);
}

// 清理
av_packet_free(&pkt);
av_frame_free(&frame);

个人经验:avcodec_receive_frame返回EAGAIN时,别慌。这说明解码器需要更多数据才能输出帧。继续送包就行。我曾经在这里卡了两天,一直以为是解码器坏了。

20.3 音频帧解码详解

音频解码和视频解码的流程基本一样,但有几个关键区别。音频帧的采样率、声道数、样本格式这些参数,必须在解码前就设置好。

我记得有一次做直播项目,音频解码出来全是噪音。查了半天,原来是声道布局没设置对。你想想看,立体声当单声道解,不出噪音才怪。

// 音频解码器初始化
AVCodec *audio_codec = avcodec_find_decoder(AV_CODEC_ID_AAC);
AVCodecContext *audio_ctx = avcodec_alloc_context3(audio_codec);

// 关键参数设置
audio_ctx->sample_rate = 44100;
audio_ctx->channel_layout = AV_CH_LAYOUT_STEREO;
audio_ctx->channels = av_get_channel_layout_nb_channels(audio_ctx->channel_layout);
audio_ctx->sample_fmt = AV_SAMPLE_FLT;  // 浮点格式

avcodec_open2(audio_ctx, audio_codec, NULL);

// 解码循环
AVPacket *audio_pkt = av_packet_alloc();
AVFrame *audio_frame = av_frame_alloc();

while (read_audio_packet(audio_pkt)) {
    avcodec_send_packet(audio_ctx, audio_pkt);
    
    while (avcodec_receive_frame(audio_ctx, audio_frame) == 0) {
        // audio_frame->nb_samples 是当前帧的样本数
        // audio_frame->data[0] 是音频数据
        // 注意:音频数据可能是交错格式,也可能是平面格式
        process_audio_frame(audio_frame);
    }
}

音频格式说明:

格式 说明 常见场景
AV_SAMPLE_FMT_FLT 浮点型,平面格式 AAC解码常用
AV_SAMPLE_FMT_S16 16位整型,交错格式 PCM/WAV常用
AV_SAMPLE_FMT_FLTP 浮点型,平面格式 FFmpeg内部常用

20.4 格式转换:从YUV到RGB

解码出来的视频帧通常是YUV格式,但显示设备需要RGB。这时候就需要做格式转换。FFmpeg提供了SwsContext来做这件事。

嗯,这里要注意:SwsContext的创建开销很大,千万别在每帧都创建。我见过有人这么干,结果CPU直接拉满,播放器卡成PPT。

// 创建格式转换上下文(只创建一次!)
struct SwsContext *sws_ctx = sws_getContext(
    src_width, src_height, AV_PIX_FMT_YUV420P,  // 源格式
    dst_width, dst_height, AV_PIX_FMT_RGB24,    // 目标格式
    SWS_BILINEAR, NULL, NULL, NULL
);

// 准备目标缓冲区
uint8_t *dst_data[4];
int dst_linesize[4];
av_image_alloc(dst_data, dst_linesize, 
               dst_width, dst_height, AV_PIX_FMT_RGB24, 1);

// 执行转换
sws_scale(sws_ctx, 
          (const uint8_t *const *)frame->data, frame->linesize,
          0, src_height,
          dst_data, dst_linesize);

// 现在dst_data[0]里就是RGB数据了
// 可以直接送给OpenGL或者SDL渲染

// 程序结束时清理
av_freep(&dst_data[0]);
sws_freeContext(sws_ctx);

避坑指南:我曾经在转换4K视频时,忘记考虑内存对齐问题。av_image_alloc分配的内存是自动对齐的,但如果你自己malloc,记得要16字节对齐,否则SWS会报错。

20.5 音频重采样

音频解码出来后,采样率、声道数、样本格式可能跟你的播放设备不匹配。这时候就需要用swr_convert做重采样。

// 创建重采样上下文
SwrContext *swr_ctx = swr_alloc_set_opts(NULL,
    AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_S16, 44100,  // 目标格式
    audio_ctx->channel_layout, audio_ctx->sample_fmt, audio_ctx->sample_rate,  // 源格式
    0, NULL);

swr_init(swr_ctx);

// 计算目标缓冲区大小
int dst_nb_samples = av_rescale_rnd(
    swr_get_delay(swr_ctx, audio_ctx->sample_rate) + audio_frame->nb_samples,
    44100, audio_ctx->sample_rate, AV_ROUND_UP);

uint8_t *dst_data;
av_samples_alloc(&dst_data, NULL, 2, dst_nb_samples, 
                 AV_SAMPLE_FMT_S16, 0);

// 执行重采样
int ret = swr_convert(swr_ctx, &dst_data, dst_nb_samples,
                      (const uint8_t **)audio_frame->data, 
                      audio_frame->nb_samples);

// 现在dst_data里就是重采样后的PCM数据
// 可以直接送给音频设备播放

我的习惯:重采样参数尽量在初始化时就定好,不要在播放过程中频繁切换。如果确实需要动态改变,记得先调用swr_close再重新初始化。

20.6 完整解码流程总结

说了这么多,其实整个解码流程可以归纳成一张图。我画了个SVG,把核心步骤和数据结构的关系展示出来,你看一眼就明白了。

FFmpeg解码完整流程 压缩数据 (AVPacket) avcodec_send_packet 发送到解码器 avcodec_receive_frame 循环接收解码帧 ┣━━ 视频帧 ━━┫ 视频帧 (AVFrame) YUV → RGB (sws_scale) 音频帧 (AVFrame) 重采样 (swr_convert) 字幕帧 (AVFrame) 文本/位图渲染 输入数据 解码过程 输出处理

你看这张图,从左到右就是数据流动的方向。压缩数据进来,经过解码器变成原始帧,然后根据类型做不同的后处理。整个流程其实不复杂,但每个环节都有它的坑。

最后说一句:解码器的内存管理一定要小心。AVPacket和AVFrame的生命周期要搞清楚,该释放的时候别犹豫。我见过太多内存泄漏的问题,都是因为忘了调用av_packet_unref或者av_frame_unref。

核心要点回顾:

  • 解码器初始化四步走:注册→查找→分配上下文→打开
  • 视频解码用avcodec_send_packet + avcodec_receive_frame循环
  • 音频解码注意采样率、声道数、样本格式的匹配
  • 格式转换用sws_scale,重采样用swr_convert
  • 所有资源用完必须释放,防止内存泄漏
公众号:蓝海资料掘金营,微信deep3321