目录
[1. 编解码 (Codec)](#1. 编解码 (Codec))
[2. 容器 (Container) 与 流 (Stream)](#2. 容器 (Container) 与 流 (Stream))
[3. 两个核心对象:Packet 与 Frame (最重要!)](#3. 两个核心对象:Packet 与 Frame (最重要!))
[4. 解析器 (Parser)](#4. 解析器 (Parser))
[5. YUV 颜色空间](#5. YUV 颜色空间)
[6. I帧、P帧、B帧 (视频压缩的核心魔法)](#6. I帧、P帧、B帧 (视频压缩的核心魔法))
[7. 进阶:DTS/PTS 与 缓冲机制 (为什么不会乱?)](#7. 进阶:DTS/PTS 与 缓冲机制 (为什么不会乱?))
[1. 准备阶段:找工具 (Setup)](#1. 准备阶段:找工具 (Setup))
[2. 循环读取与切包 (Parsing Loop)](#2. 循环读取与切包 (Parsing Loop))
[3. 解码动作:发送与接收 (Send & Receive)](#3. 解码动作:发送与接收 (Send & Receive))
[4. 保存图像 (Processing)](#4. 保存图像 (Processing))
第一部分:核心音视频概念(小白必读)
在看代码之前,你需要先建立 7 个核心概念的认知。想象你正在经营一家**"冷冻食品加工厂"**。
1. 编解码 (Codec)
-
概念:视频原始数据极大(1秒钟的高清视频可能有几百兆)。为了存储和传输,必须压缩(编码),播放时再解压(解码)。
-
比喻:
-
原始视频 = 新鲜蔬菜(体积大,易坏)。
-
编码 = 脱水干燥,做成压缩蔬菜包(体积小,方便运输)。
-
解码 = 泡水还原,变回新鲜蔬菜(恢复体积,用于食用/观看)。
-
-
代码对应 :
AVCodec(解码器)。
2. 容器 (Container) 与 流 (Stream)
-
概念 :mp4, avi, mkv 只是容器(外壳),里面装着视频流、音频流、字幕流。
-
比喻:
-
容器 = 一个纸箱子。
-
流 = 箱子里装的一袋袋东西(一袋是视频,一袋是音频)。
-
-
代码对应 :本示例比较特殊,它直接读取的是 MPEG1 裸流(没有 MP4 这种外壳),所以代码里没有解封装(Demux)的步骤,直接就是处理流数据。
3. 两个核心对象:Packet 与 Frame (最重要!)
这是音视频开发中最容易混淆,也是最重要的概念。
| 对象 | 术语 | 状态 | 比喻 | 特点 |
|---|---|---|---|---|
| AVPacket | 数据包 | 压缩数据 | 压缩蔬菜块 | 体积小,电脑看不懂,必须解压。 |
| AVFrame | 帧 | 原始数据 | 泡开的蔬菜 | 体积大 (YUV/RGB),包含了具体的像素点,屏幕能直接显示。 |
- 流程 :文件 -> Packet (解压前) -> 解码器 -> Frame (解压后)。
4. 解析器 (Parser)
-
概念:当我们从文件里读数据时,读到的是一长串字节流(010101...)。电脑不知道哪里是一帧的开始,哪里是结束。
-
作用 :Parser 就像一个断句符号,它从乱糟糟的数据流中,精准地切出一个个完整的 Packet。
-
代码对应 :
av_parser_parse2。
5. YUV 颜色空间
-
概念:屏幕通常用 RGB(红绿蓝)显示,但视频通常用 YUV 存储。
-
Y:亮度(灰度图)。人眼对亮度最敏感。
-
U/V:色度(颜色)。
-
-
现象 :示例代码最终保存的是
PGM格式,这是一种灰度图格式。因为它只保存了 Y (亮度) 分量。 -
代码对应 :
frame->data[0](存放 Y 数据)。
6. I帧、P帧、B帧 (视频压缩的核心魔法)
这是理解解码器行为逻辑的关键。为了极致压缩,视频不是每张图都完整存下来。
-
I帧 (Intra Frame) :关键帧。一张完整的照片,不依赖别人。你可以直接看懂。
-
P帧 (Predicted Frame) :前向预测帧。只存储跟前一帧"不一样"的地方(比如背景不动,只记走动的人)。省空间。
-
B帧 (Bi-directional Frame) :双向预测帧 。最省空间!它参考前一帧 和后一帧。
7. 进阶:DTS/PTS 与 缓冲机制 (为什么不会乱?)
这里解答"边吃边吐"是否会乱序的问题。
-
流式处理:播放器不是把整个视频解完才播,而是**"喂几个 Packet,吐几张 Frame"**。
-
DTS (Decoding Time Stamp) :喂食顺序。视频文件内部是按这个存的。为了解出 B 帧,必须先把后面被参考的 P 帧先存进去。
-
PTS (Presentation Time Stamp) :显示顺序。最终吐出来给人看的顺序。
-
防乱逻辑(候车室):
-
目标显示顺序 (PTS):I(1) -> B(2) -> P(3)
-
文件存放顺序 (DTS) :I(1) -> P(3) -> B(2) (因为 B(2) 需要参考 P(3),所以 P(3) 必须先进入解码器)
-
过程 :解码器收到 P(3) 后,发现它是 P 帧且时间靠后,会把它锁在内部缓存(候车室),暂时不吐出来。等收到 B(2) 并解码完成后,再按正确顺序释放。
-
第二部分:代码深度解析
现在我们带着上面的概念,来看这 100 多行代码究竟在干什么。流程图如下:
初始化 -> 读取文件 -> Parser切包 -> 发送Packet给解码器 -> 接收Frame -> 保存图片
1. 准备阶段:找工具 (Setup)
// 1. 找解码器:我要解 MPEG1 格式的视频
codec = avcodec_find_decoder(AV_CODEC_ID_MPEG1VIDEO);
// 2. 创建解析器 (Parser):为了从流里切出 Packet
parser = av_parser_init(codec->id);
// 3. 创建上下文 (Context):这是解码器的"工位",记录各种状态
c = avcodec_alloc_context3(codec);
// 4. 正式打开解码器:工位整理好,工具放好,准备开工
avcodec_open2(c, codec, NULL);
2. 循环读取与切包 (Parsing Loop)
这是代码中最难理解的 while 循环部分。
// 从文件读一大块数据到 buffer (比如 4096 字节)
data_size = fread(inbuf, 1, INBUF_SIZE, f);
while (data_size > 0 || eof) {
// 【核心动作】切包
// parser 帮我们在乱序的 data 中找到一个完整的 Packet
// ret 是 parser 吃掉的字节数
ret = av_parser_parse2(parser, c, &pkt->data, &pkt->size,
data, data_size, ...);
data += ret; // 指针后移,处理剩下的数据
data_size -= ret; // 剩余数据量减少
// 如果切出来一个包 (pkt->size > 0),就拿去解码
if (pkt->size)
decode(c, frame, pkt, outfilename);
}
- 白话解说 :就像吃一条很长的甘蔗。
fread砍下一截拿在手里,parser负责把这一截甘蔗切成一口一口的小块(Packet)。切好一块,就吐出来拿去榨汁(Decode)。
3. 解码动作:发送与接收 (Send & Receive)
这是 FFmpeg 新版本标准解码模式:供需模式。
static void decode(...) {
// 1. 【投喂】把压缩包 (Packet) 扔进解码器 (按 DTS 顺序)
ret = avcodec_send_packet(dec_ctx, pkt);
// 2. 【索取】尝试从解码器里拿出来原始帧 (Frame) (按 PTS 顺序)
while (ret >= 0) {
ret = avcodec_receive_frame(dec_ctx, frame);
// 情况A: 解码器说"我还需要更多 Packet 才能拼出一张图",或者"没数据了"
// 场景举例:你喂进去一个 P(3),解码器把它存入"候车室",此时没有输出,返回 EAGAIN
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
return;
// 情况B: 拿到了一帧画面!(frame 里现在有数据了)
// ... 保存图片 ...
}
}
-
为什么是 while 循环?
-
因为视频编码很复杂。有时候你喂进去 1 个 Packet,解码器就能吐出 1 个 Frame。
-
B 帧场景 :你喂进去 Packet(B帧),解码器会把它先存起来(Buffer),它说"等会儿,我还要看后面的帧才能解这一帧",此时
receive_frame返回 EAGAIN,它暂时不吐数据。 -
I/P 帧场景:等你后来喂进去一个 P 帧,解码器可能一下子把刚才存的 B 帧和现在的 P 帧都吐出来(或者按顺序吐)。
-
所以原则是:一直喂,直到喂进去;一直取,直到取不出来。
-
4. 保存图像 (Processing)
// frame->data[0] 存放的是 Y (亮度/灰度) 分量
// frame->linesize[0] 是这一行数据的内存宽度 (通常大于等于图像宽度,为了内存对齐)
pgm_save(frame->data[0], frame->linesize[0], frame->width, frame->height, buf);
- 这里只保存了黑白画面。如果要保存彩色,需要把
data[1](U) 和data[2](V) 也处理,或者把 YUV 转成 RGB。
总结
这份代码虽然短,但涵盖了音视频开发的万能公式:
数据源 \\rightarrow \\text{Parser (切分)} \\rightarrow \\text{Packet (压缩包/DTS顺序)} \\rightarrow \\text{Decoder (缓冲/重排)} \\rightarrow \\text{Frame (原始画面/PTS顺序)} \\rightarrow \\text{业务逻辑}
希望这份讲解能帮你推开音视频开发的大门!