3. FFmpeg基础回顾:核心数据结构与编解码流程
做硬件加速编解码,说白了就是跟FFmpeg的这几个核心结构体打交道。AVFormatContext、AVCodecContext、AVFrame、AVPacket——你天天都会见到它们。我刚开始接触FFmpeg时,总觉得这些结构体太多太乱,后来踩了几个坑才摸清楚它们各自管什么。
这一节,咱们就把这几个核心数据结构掰开揉碎,再走一遍解码和编码的基本流程。命令行和API调用我都会讲到。
3.1 四大核心数据结构
先看一张总览图,把这四个结构体的关系理清楚。
3.2 AVFormatContext — 容器层管家
AVFormatContext 管的是容器格式。MP4、TS、FLV、MKV——这些封装格式的读写都由它负责。我个人习惯把它理解成「文件/流的总控制器」。
它里面最重要的几个成员:
- nb_streams / streams[]:流数组,一个文件里可能有视频流、音频流、字幕流
- duration:总时长,单位是AV_TIME_BASE(微秒)
- bit_rate:整体码率
- pb:AVIOContext,底层I/O操作
关键API:
avformat_open_input()— 打开文件/流avformat_find_stream_info()— 读取流信息avformat_close_input()— 关闭释放
我的经验:做硬件加速时,经常需要从AVFormatContext里拿到流的编码信息。比如判断视频流是不是H.264,再决定要不要走硬件解码。我曾经在一个项目中,没检查流类型就直接送硬件,结果音频流也送进去了——解码器直接崩了。
3.3 AVCodecContext — 编解码器的大脑
AVCodecContext 是编解码的核心配置。它告诉解码器:分辨率多少、帧率多少、码率多少、用哪种硬件加速。
常用的字段:
| 字段 | 说明 | 典型值 |
|---|---|---|
codec_id |
编码器类型 | AV_CODEC_ID_H264 |
width / height |
视频宽高 | 1920 x 1080 |
pix_fmt |
像素格式 | AV_PIX_FMT_YUV420P |
bit_rate |
目标码率 | 4000000 (4Mbps) |
time_base |
时间基 | {1, 25} |
hw_device_ctx |
硬件设备上下文 | VAAPI/CUDA/MediaCodec |
避坑指南:我曾经在配置硬件加速时,忘了设置 hw_device_ctx,结果解码器虽然初始化成功了,但一直走软解。性能完全没提上来。检查了半天才发现是这个字段没赋值。记住:硬件加速不是自动开启的,你得手动告诉FFmpeg用哪个设备。
3.4 AVPacket — 压缩数据包
AVPacket 存的是压缩后的数据。对视频来说,就是H.264/H.265的ES流。它不直接管理内存,而是通过引用计数机制共享数据。
核心成员:
- data / size:压缩数据指针和大小
- pts / dts:显示时间戳和解码时间戳
- duration:该包的持续时间
- stream_index:属于哪个流
- flags:关键帧标记等
常用API:
av_packet_alloc()— 分配包结构av_read_frame()— 从容器读取一个包av_packet_unref()— 释放引用
3.5 AVFrame — 原始图像/音频帧
AVFrame 存的是解码后的原始数据。视频就是YUV或RGB像素数据,音频就是PCM采样数据。
视频帧的关键字段:
- data[] / linesize[]:图像数据指针数组和每行字节数
- width / height:分辨率
- format:像素格式
- pts:显示时间戳
- key_frame:是否为关键帧
- hw_frames_ctx:硬件帧上下文(硬件加速时用)
注意:硬件加速时,AVFrame的data[]里存的不是CPU内存地址,而是GPU显存的句柄。你不能直接拿它当普通内存用。我见过有人直接memcpy硬件帧的data——结果拷贝了一堆垃圾数据出来。
3.6 解码基本流程
解码流程,说白了就是:读包 → 送解码器 → 取帧。我画个流程图你就明白了。
代码示例:
// 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 = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
// 3. 初始化解码器
AVCodecContext *dec_ctx = avcodec_alloc_context3(NULL);
avcodec_parameters_to_context(dec_ctx, fmt_ctx->streams[video_stream]->codecpar);
const AVCodec *decoder = avcodec_find_decoder(dec_ctx->codec_id);
avcodec_open2(dec_ctx, decoder, NULL);
// 4. 解码循环
AVPacket *pkt = av_packet_alloc();
AVFrame *frame = av_frame_alloc();
while (av_read_frame(fmt_ctx, pkt) >= 0) {
if (pkt->stream_index == video_stream) {
avcodec_send_packet(dec_ctx, pkt);
while (avcodec_receive_frame(dec_ctx, frame) >= 0) {
// 处理解码后的帧
// 比如显示、保存、或者送编码器
}
}
av_packet_unref(pkt);
}
// 5. 冲刷解码器
avcodec_send_packet(dec_ctx, NULL);
while (avcodec_receive_frame(dec_ctx, frame) >= 0) {
// 处理剩余帧
}
// 6. 清理
av_frame_free(&frame);
av_packet_free(&pkt);
avcodec_free_context(&dec_ctx);
avformat_close_input(&fmt_ctx);
我曾经踩过的坑:解码循环里,avcodec_send_packet 和 avcodec_receive_frame 是「生产者-消费者」模式。一个包可能产生多个帧(比如B帧),也可能一个包还不够(比如大帧分片)。所以必须用内层while循环把帧全部取完。我早期写代码时只调了一次avcodec_receive_frame,结果丢了一半的帧。
3.7 编码基本流程
编码流程跟解码正好反过来:送原始帧 → 取压缩包。
代码示例:
// 1. 创建输出上下文
AVFormatContext *out_fmt_ctx = NULL;
avformat_alloc_output_context2(&out_fmt_ctx, NULL, NULL, "output.mp4");
// 2. 添加视频流
AVStream *out_stream = avformat_new_stream(out_fmt_ctx, NULL);
AVCodecContext *enc_ctx = avcodec_alloc_context3(NULL);
enc_ctx->codec_id = AV_CODEC_ID_H264;
enc_ctx->width = 1920;
enc_ctx->height = 1080;
enc_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
enc_ctx->time_base = (AVRational){1, 25};
// 3. 打开编码器
const AVCodec *encoder = avcodec_find_encoder(AV_CODEC_ID_H264);
avcodec_open2(enc_ctx, encoder, NULL);
// 4. 写入文件头
avformat_write_header(out_fmt_ctx, NULL);
// 5. 编码循环
AVFrame *frame = av_frame_alloc();
// ... 填充frame数据(从摄像头、文件或GPU读取)
avcodec_send_frame(enc_ctx, frame);
AVPacket *pkt = av_packet_alloc();
while (avcodec_receive_packet(enc_ctx, pkt) >= 0) {
av_interleaved_write_frame(out_fmt_ctx, pkt);
av_packet_unref(pkt);
}
// 6. 冲刷编码器
avcodec_send_frame(enc_ctx, NULL);
while (avcodec_receive_packet(enc_ctx, pkt) >= 0) {
av_interleaved_write_frame(out_fmt_ctx, pkt);
av_packet_unref(pkt);
}
// 7. 写文件尾并清理
av_write_trailer(out_fmt_ctx);
av_frame_free(&frame);
av_packet_free(&pkt);
avcodec_free_context(&enc_ctx);
avformat_free_context(out_fmt_ctx);
3.8 命令行与API调用方式对比
FFmpeg有两种使用方式:命令行和API。做硬件加速开发,你肯定要用API。但命令行可以用来快速验证。
| 场景 | 命令行 | API |
|---|---|---|
| 快速转码测试 | ffmpeg -i input.mp4 output.avi |
avcodec_send_packet / receive_frame |
| 硬件加速解码 | ffmpeg -hwaccel vaapi -i input.mp4 output.yuv |
设置 hw_device_ctx |
| 提取裸流 | ffmpeg -i input.mp4 -vcodec copy -an output.h264 |
av_read_frame + 写文件 |
| 查看流信息 | ffprobe input.mp4 |
avformat_find_stream_info |
我的建议:调试阶段先用命令行确认你的输入文件没问题。比如用 ffprobe 看看编码格式、分辨率、帧率对不对。确认无误后再写API代码。这样可以排除「文件本身有问题」这个变量。
3.9 小结
这一节我们把FFmpeg的四个核心结构体过了一遍,也走通了编解码的基本流程。你想想看,其实核心逻辑就两句话:
- 解码:AVPacket(压缩数据)→ AVFrame(原始数据)
- 编码:AVFrame(原始数据)→ AVPacket(压缩数据)
AVFormatContext管容器,AVCodecContext管编解码参数。这两个上下文通过AVStream连接起来。