20、FFmpeg解码:使用FFmpeg解码视频帧、音频帧、格式转换
说实话,FFmpeg解码这块,是音视频开发里最绕不开的硬骨头。我记得刚入行那会儿,对着API文档啃了整整一周,才把整个解码流程跑通。今天我就把这些年踩过的坑、总结的经验,一次性给你讲透。
20.1 解码前的准备工作
在开始解码之前,你得先搞清楚一件事:FFmpeg的解码器不是拿来就能用的。它需要先初始化,再打开,最后才能干活。我见过太多新手直接跳过初始化步骤,结果程序跑起来就崩。
核心流程:
- 注册所有编解码器(avcodec_register_all)
- 查找解码器(avcodec_find_decoder)
- 分配解码器上下文(avcodec_alloc_context3)
- 打开解码器(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,把核心步骤和数据结构的关系展示出来,你看一眼就明白了。
你看这张图,从左到右就是数据流动的方向。压缩数据进来,经过解码器变成原始帧,然后根据类型做不同的后处理。整个流程其实不复杂,但每个环节都有它的坑。
最后说一句:解码器的内存管理一定要小心。AVPacket和AVFrame的生命周期要搞清楚,该释放的时候别犹豫。我见过太多内存泄漏的问题,都是因为忘了调用av_packet_unref或者av_frame_unref。
核心要点回顾:
- 解码器初始化四步走:注册→查找→分配上下文→打开
- 视频解码用avcodec_send_packet + avcodec_receive_frame循环
- 音频解码注意采样率、声道数、样本格式的匹配
- 格式转换用sws_scale,重采样用swr_convert
- 所有资源用完必须释放,防止内存泄漏