FFmpeg中视频采集编解码显示中帧重排问题详解

5 阅读6分钟

一、问题背景

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

为解决重排问题,视频容器引入了两个时间戳概念:

概念全称含义用途
DTSDecoding Time Stamp解码时间戳告诉解码器按什么顺序接收/解码数据
PTSPresentation 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 PI P P PI P P P0帧
I B PI B PI P B1帧
I B B PI B B PI P B B2帧
I B B B PI B B B PI P B B B3帧

六、编程实践要点

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来区分解码和显示顺序。