3. 编解码器概述

时间戳这东西,说白了就是给每一帧画面、每一段声音贴个标签。告诉播放器:你该在什么时候显示我,什么时候解码我。我早年刚接触多媒体开发时,总觉得这玩意儿不就是个时间数字嘛,有什么难的?直到第一次遇到音画不同步,视频都播到高潮了,声音还在前奏……嗯,从那以后我再也不敢小看 PTS 和 DTS 了。

3.1 PTS 与 DTS:两个时间戳,各司其职

先搞清楚两个基本概念:

  • PTS(Presentation Time Stamp):显示时间戳。告诉解码器,这帧画面应该在哪个时刻呈现给用户。
  • DTS(Decoding Time Stamp):解码时间戳。告诉解码器,这帧数据应该在哪个时刻开始解码。

你可能会问:为什么需要两个?直接解码完就显示不行吗?

问题出在视频编码的参考帧结构上。拿 H.264 来说,B 帧需要参考后面的 P 帧才能解码。这就导致了解码顺序和显示顺序不一致。

核心规律:

  • I 帧和 P 帧:DTS 通常等于 PTS(解码即显示)
  • B 帧:DTS 小于 PTS(先解码,后显示)
  • 显示顺序 = PTS 升序排列
  • 解码顺序 = DTS 升序排列

举个例子,一个典型的 GOP 结构:

编码顺序(DTS): I0  P3  B1  B2  P6  B4  B5
显示顺序(PTS): I0  B1  B2  P3  B4  B5  P6

看到没?B1 和 B2 在编码时排在 P3 后面,但显示时却要回到 P3 前面。这就是 DTS 和 PTS 存在的意义。

3.2 时间戳的计算方式

时间戳不是随便写的数字,它基于一个固定的时间基(Time Base)。

参数 说明 常见值
time_base 时间基,每个 tick 代表的时间 1/90000 秒(MPEG-TS)
1/1000 秒(MP4)
framerate 帧率,每秒多少帧 30 fps、25 fps、60 fps
sample_rate 音频采样率 44100 Hz、48000 Hz

计算方式其实很简单:

// 视频帧 PTS 计算
pts = frame_index * time_base.den / framerate

// 例如:30fps,time_base = 1/90000
// 第 0 帧:0
// 第 1 帧:3000
// 第 2 帧:6000

// 音频帧 PTS 计算
pts = sample_index * time_base.den / sample_rate

// 例如:48000Hz,time_base = 1/90000
// 每帧音频样本数 = 1024
// 第 0 帧:0
// 第 1 帧:1920
// 第 2 帧:3840

我个人习惯:在封装 MP4 时,time_base 用 1/1000 就够了,精度够用且计算简单。但做直播或 TS 流时,一定用 1/90000,否则时间戳累积误差会让你抓狂。我曾经在一个项目中偷懒用了 1/1000,跑了半小时后音画差了 2 秒……

3.3 音视频时钟同步策略

音画同步的核心,就是让音频和视频按照各自的时间戳,在同一个时间轴上对齐播放。这里有三种主流策略:

3.3.1 音频主时钟(Audio Master)

这是最常见的策略。以音频时钟为基准,视频去追赶或等待音频。

  • 原理:音频播放是连续的,由声卡硬件时钟驱动。视频根据音频的播放进度调整自己的显示时间。
  • 优点:人耳对音频抖动的敏感度远高于视频,音频连续性好。
  • 缺点:如果音频时钟本身不准(比如某些廉价声卡),视频会跟着一起乱。

具体做法:

// 伪代码逻辑
while (播放中) {
    audio_clock = 声卡当前播放位置(字节数 / 字节率)
    video_pts = 当前视频帧的 PTS
    
    diff = video_pts - audio_clock
    
    if (diff > 阈值) {
        // 视频快了,丢帧或等待
        跳过当前帧
    } else if (diff < -阈值) {
        // 视频慢了,重复上一帧或加速
        重复显示上一帧
    } else {
        // 正常显示
        显示当前帧
    }
}

3.3.2 视频主时钟(Video Master)

以视频时钟为基准,音频去适配视频。这种策略用得少,但在某些场景下很实用。

  • 适用场景:视频会议、游戏录制,画面流畅度优先于声音。
  • 做法:音频通过重采样或变速来匹配视频帧率。

注意:音频变速会改变音调,需要配合音调矫正算法(如 WSOLA、SoundTouch)。我曾经在做一个低延迟直播项目时试过直接变速,结果声音像唐老鸭……后来加了音调矫正才搞定。

3.3.3 外部主时钟(External Master)

不依赖音视频任何一方,而是用一个独立的高精度时钟(如 NTP、PTP)作为基准。

  • 适用场景:多设备同步播放、分布式系统、专业广播环境。
  • 优点:所有设备都对齐同一个时钟源,精度可达微秒级。
  • 缺点:需要网络支持,实现复杂。

3.4 主时钟选择:怎么选?

我个人的经验是:

场景 推荐主时钟 原因
本地文件播放 音频主时钟 音频硬件时钟稳定,实现简单
直播推流 音频主时钟 保证声音连续,观众对卡顿更敏感
视频会议 视频主时钟 画面流畅优先,音频可做补偿
多屏同步 外部主时钟(NTP) 多设备需要统一时间基准
专业广播 外部主时钟(PTP) 微秒级精度,行业标准

3.5 同步机制的核心流程

下面这张图展示了音视频同步的完整流程:

音视频同步核心流程 音视频数据输入 解封装 / 提取 PTS/DTS 音频解码 视频解码 音频时钟(主时钟) 视频帧缓冲队列 PTS 与时钟比较 同步决策(丢帧/重复/等待) 音视频同步输出 音频时钟作为主时钟参考 视频帧根据差值做动态调整

3.6 实际项目中的避坑指南

做音视频同步,坑不少。我踩过的几个:

  • 时间戳溢出:PTS 是 32 位还是 64 位?32 位在 90kHz 时间基下,大约 13 小时就会溢出。直播流跑一天就出问题。解决方案:用 64 位时间戳,或者做模运算时记录 wrap count。
  • B 帧导致的 PTS 乱序:解码器输出顺序可能不是 PTS 顺序。我建议在显示前先做一次 PTS 排序,否则画面会来回跳。
  • 音频设备时钟漂移:声卡的实际采样率和标称值可能有微小偏差。长时间播放后,音频时钟和系统时钟会拉开差距。我习惯每隔一段时间用系统时钟校准一次音频时钟。
  • 首帧 PTS 不为 0:有些封装格式的首帧 PTS 不是 0,而是某个偏移值。直接拿来做比较会出问题。记得先做归一化处理。

一个小技巧:调试音画同步问题时,我习惯在视频画面上叠加显示当前 PTS 和音频时钟的差值。肉眼看到数字在合理范围内跳动,心里就有底了。如果差值持续增大,说明同步逻辑有问题。

3.7 总结

时间戳和同步机制,说白了就是让音频和视频在时间轴上对齐跳舞。PTS 告诉你怎么跳,DTS 告诉你怎么准备,主时钟告诉谁领舞。理解了这三者的关系,音画同步的问题就解决了一大半。

嗯,这一节就到这里。记住:音频主时钟是默认选择,但不是唯一选择。根据场景灵活切换,才是工程师该有的思路。