一、问题背景
1.1 帧类型回顾
在视频编码中,常见的帧类型有三种:
| 帧类型 | 全称 | 参考关系 | 压缩率 | 依赖 |
|---|---|---|---|---|
| I帧 | Intra帧 | 仅参考自身 | 最低 | 无依赖 |
| P帧 | Predictive帧 | 参考前面的帧 | 中等 | 依赖前面的I/P帧 |
| B帧 | Bi-predictive帧 | 参考前后帧 | 最高 | 依赖前后的I/P帧 |
1.2 核心矛盾
B帧的压缩效率最高,但它需要同时参考前面的帧和后面的帧才能完成解码。这就产生了一个根本性的矛盾:
- 编码器必须先编码后面的帧,才能编码B帧
- 但采集和显示的顺序是先有前面的帧,后有后面的帧
- 这导致编码顺序 ≠ 显示顺序
二、问题示例
2.1 采集顺序(原始顺序)
假设采集到的帧序列如下(括号内为显示顺序PTS):
I(0) → B(1) → B(2) → P(3) → B(4) → B(5) → P(6) → ...
这是摄像头/文件读取的原始顺序,也是最终需要显示的顺序。
2.2 编码器困境
当编码器按顺序处理时:
步骤1: 收到 I(0) → 可以编码(无需参考其他帧)
步骤2: 收到 B(1) → 无法编码!需要后面的 P(3)
步骤3: 收到 B(2) → 无法编码!需要后面的 P(3)
步骤4: 收到 P(3) → 可以编码(只需参考前面的 I(0))
问题:B(1)和B(2)需要P(3)的信息,但P(3)还没编码,编码器不知道它的内容。
2.3 解决方案:重排
编码器改变处理顺序:
1. 编码 I(0) → 输出
2. 跳过 B(1)、B(2),先编码 P(3) → 输出
3. 现在 P(3) 已知,回头编码 B(1)、B(2) → 输出
最终码流顺序(DTS) :
I(0) → P(3) → B(1) → B(2) → P(6) → B(4) → B(5) → ...
三、技术概念
3.1 DTS 和 PTS
为解决重排问题,视频容器引入了两个时间戳概念:
| 概念 | 全称 | 含义 | 用途 |
|---|---|---|---|
| DTS | Decoding Time Stamp | 解码时间戳 | 告诉解码器按什么顺序接收/解码数据 |
| PTS | Presentation Time Stamp | 显示时间戳 | 告诉播放器按什么顺序显示画面 |
3.2 工作流程对比
| 阶段 | 顺序 | 使用的时序 |
|---|---|---|
| 采集 | I B B P B B P ... | 真实时间顺序 |
| 编码器输入 | I B B P B B P ... | 按PTS顺序 |
| 编码器输出 | I P B B P B B ... | 按DTS顺序(重排后) |
| 解码器输入 | I P B B P B B ... | 按DTS顺序 |
| 解码器输出 | I B B P B B P ... | 按PTS顺序(重排回来) |
| 显示 | I B B P B B P ... | 按PTS顺序 |
四、具体示例
4.1 五帧序列示例
假设有5帧:I(1) B(2) B(3) P(4) B(5)
编码器工作流程:
1. 收到 I(1):直接编码 → 输出 I(1)
2. 收到 B(2):无法编码,放入缓冲区
3. 收到 B(3):无法编码,放入缓冲区
4. 收到 P(4):可以编码(参考 I(1))→ 输出 P(4)
5. 缓冲区有 B(2)、B(3),现在 P(4) 已知 → 编码并输出 B(2)、B(3)
6. 收到 B(5):无法编码(需要后面的帧)
7. 收到下一个 P 帧后,再回头编码 B(5)
最终码流:
顺序: I(1) → P(4) → B(2) → B(3) → ...
DTS: 1 2 3 4
PTS: 1 4 2 3
4.2 解码器工作流程
解码器按DTS顺序接收和解码:
1. 解码 I(1):准备就绪,PTS=1 → 立即显示
2. 解码 P(4):解码完成,但PTS=4,需要等待B(2)、B(3) → 放入缓冲区
3. 解码 B(2):解码完成,PTS=2 → 缓冲区有PTS=4的帧,按PTS排序后输出B(2)
4. 解码 B(3):解码完成,PTS=3 → 输出B(3)
5. 缓冲区只剩P(4),PTS=4 → 输出P(4)
五、特殊情况
5.1 没有B帧的情况
如果编码时禁用B帧,只有I帧和P帧:
采集顺序: I(1) → P(2) → P(3) → P(4)
编码顺序: I(1) → P(2) → P(3) → P(4) (无需重排)
DTS = PTS
因为P帧只需要参考前面的帧,不需要等待后面的帧。
5.2 不同B帧数量
常见的GOP(Group of Pictures)结构:
| GOP结构 | 采集顺序 | 码流顺序 | 重排延迟 |
|---|---|---|---|
| I P P P | I P P P | I P P P | 0帧 |
| I B P | I B P | I P B | 1帧 |
| I B B P | I B B P | I P B B | 2帧 |
| I B B B P | I B B B P | I P B B B | 3帧 |
六、编程实践要点
6.1 编码时设置PTS
c
// 假设编码第i帧
frame->pts = i; // 设置显示顺序
avcodec_send_frame(enc_ctx, frame);
编码器会根据GOP结构自动计算DTS并重排。
6.2 解码时读取PTS
c
avcodec_send_packet(dec_ctx, packet);
while (avcodec_receive_frame(dec_ctx, frame) == 0) {
int64_t pts = frame->pts; // 解码器已按PTS重排
// pts 已经是正确的显示顺序
display_frame(frame);
}
注意:avcodec_receive_frame 返回的帧已经按PTS重排,无需手动处理。
七、常见问题与解答
Q1:为什么需要B帧?直接不用不行吗?
答:B帧的压缩效率最高,可以显著减少码率。使用B帧相比只使用P帧,在相同画质下可减少30%-50%的码率。
Q2:重排会增加延迟吗?
答:会。B帧越多,重排延迟越大。例如IBBP结构会有2帧的编码延迟。实时通信(如视频通话)通常禁用B帧或只使用1个B帧。
Q3:如何判断一个视频流是否使用了B帧?
答:检查视频流的PTS和DTS。如果所有帧的PTS都等于DTS,说明没有B帧;如果存在PTS ≠ DTS的情况,说明有B帧。
Q4:解码器如何知道PTS和DTS?
答:PTS和DTS存储在容器格式中:
- MP4:存储在stts、ctts atom中
- TS:存储在PES头的PTS/DTS字段
- FLV:存储在Tag头中
Q5:播放器如何处理PTS?
答:播放器根据PTS差值计算每帧的显示时长:
text
delay = (next_pts - current_pts) / time_base
然后通过av_usleep()或类似机制控制显示时机。
八、总结
| 核心要点 | 说明 |
|---|---|
| 根本原因 | B帧需要参考未来的帧,导致编码顺序必须改变 |
| 解决方案 | 编码器重排输出顺序,解码器根据PTS重排回来 |
| 关键概念 | DTS(解码顺序)、PTS(显示顺序) |
| 适用范围 | 任何使用B帧的视频编码标准(H.264、H.265、AV1等) |
| 性能代价 | 增加编码延迟,需要更大的解码缓冲区 |
| 实际应用 | 点播、直播(允许延迟)使用B帧;实时通话禁用或限制B帧 |
一句话总结:B帧参考未来 → 需要先编未来的帧 → 码流顺序重排 → 需要PTS/DTS来区分解码和显示顺序。