音视频学习(三十七):pts和dts
概念
PTS(Presentation Time Stamp)显示时间戳
- 表示:该帧应该在什么时间被显示/播放。
- 主要用于:同步音频与视频,控制播放节奏。
- 举例:视频帧 A 的 PTS 是 300ms,表示应在视频播放第 300 毫秒时显示。
DTS(Decoding Time Stamp)解码时间戳
- 表示:该帧应该在什么时间被解码。
- 主要用于:解码器按正确顺序解码帧(尤其对于有 B 帧的视频)。
作用
因为视频编码中存在帧的重排序:
编码时为了压缩率更高,视频通常采用如下三种帧类型:
- I帧(关键帧):可以独立解码。
- P帧(预测帧):依赖前面的帧。
- B帧(双向预测帧):依赖前后帧。
这就导致:
显示顺序 ≠ 解码顺序
因此,必须使用 PTS 控制显示顺序,使用 DTS 控制解码顺序。
示例
假设视频帧顺序如下(按显示顺序):
帧类型 | 显示顺序 (PTS) | 解码顺序 (DTS) |
---|---|---|
I | 0 | 0 |
B | 1 | 2 |
B | 2 | 3 |
P | 3 | 1 |
解码器必须这样处理:
- 先解码 I(DTS=0)
- 再解码 P(DTS=1)
- 然后可以解码 B(DTS=2,依赖 I 和 P)
- 然后解码下一帧 B(DTS=3)
但播放时按照 PTS 排序:I → B → B → P
赋值
1. 没有B帧的情况(PTS = DTS)
例如只使用 I 帧 + P 帧:
int64_t pts = frame_index * frame_interval; // 单位:时间基(如1/90000)
int64_t dts = pts;
frame_interval = 时间基 / 帧率
,例如:1/25fps => 每帧间隔 3600(如果时间基是 90000)
2. 有B帧的情况(PTS ≠ DTS)
例如 IBBP 结构(假设 GOP = IPBBP):
- 编码顺序:I P B B P
- 显示顺序:I B B P P
帧类型 | 编码顺序 | DTS | PTS |
---|---|---|---|
I | 0 | 0 | 0 |
P | 1 | 1 | 3 |
B | 2 | 2 | 1 |
B | 3 | 3 | 2 |
P | 4 | 4 | 4 |
通常:PTS = 显示顺序 * frame_interval,DTS = 编码顺序 * frame_interval。
这种关系在 H.264/H.265 中通过 AVC reorder buffer 或 Decoding Order Number (DON) 推导。
示例
- 帧率:25fps → 每帧间隔 3600(以时间基
1/90000
为例) - GOP结构:
I B B P
循环 - 显示顺序:
I B B P I B B P ...
- 编码顺序:
I P B B I P B B ...
代码(c++):
#include <iostream>
#include <vector>
#include <string>
#include <map>enum FrameType {I_FRAME,P_FRAME,B_FRAME
};struct Frame {int display_index; // 显示顺序编号int encode_index; // 编码顺序编号int64_t pts; // 显示时间戳int64_t dts; // 解码时间戳FrameType type;
};// 模拟一个 GOP(IBBP)
std::vector<Frame> generate_gop_with_bframes(int gop_size, int frame_interval) {std::vector<Frame> display_order; // 显示顺序std::vector<Frame> encode_order; // 编码顺序// 构造 GOP:显示顺序 I B B P(假设gop_size是4或其倍数)for (int gop_index = 0; gop_index < gop_size / 4; ++gop_index) {int base = gop_index * 4;display_order.push_back({base + 0, -1, 0, 0, I_FRAME});display_order.push_back({base + 1, -1, 0, 0, B_FRAME});display_order.push_back({base + 2, -1, 0, 0, B_FRAME});display_order.push_back({base + 3, -1, 0, 0, P_FRAME});}// 编码顺序为 I P B Bfor (int gop_index = 0; gop_index < gop_size / 4; ++gop_index) {int base = gop_index * 4;encode_order.push_back({base + 0, -1, 0, 0, I_FRAME});encode_order.push_back({base + 3, -1, 0, 0, P_FRAME});encode_order.push_back({base + 1, -1, 0, 0, B_FRAME});encode_order.push_back({base + 2, -1, 0, 0, B_FRAME});}// 构建最终帧列表,赋值PTS(按显示顺序)和 DTS(按编码顺序)std::map<int, Frame> frame_map;for (size_t i = 0; i < display_order.size(); ++i) {int display_index = display_order[i].display_index;frame_map[display_index].display_index = display_index;frame_map[display_index].pts = i * frame_interval;frame_map[display_index].type = display_order[i].type;}for (size_t i = 0; i < encode_order.size(); ++i) {int display_index = encode_order[i].display_index;frame_map[display_index].encode_index = i;frame_map[display_index].dts = i * frame_interval;}// 按编码顺序输出结果std::vector<Frame> result;for (const auto& [_, frame] : frame_map) {result.push_back(frame);}// 根据 DTS 排序std::sort(result.begin(), result.end(), [](const Frame& a, const Frame& b) {return a.dts < b.dts;});return result;
}std::string frame_type_str(FrameType type) {switch (type) {case I_FRAME: return "I";case P_FRAME: return "P";case B_FRAME: return "B";default: return "?";}
}int main() {int gop_size = 8; // 2组 IBBPint fps = 25;int time_base = 90000;int frame_interval = time_base / fps;std::vector<Frame> frames = generate_gop_with_bframes(gop_size, frame_interval);std::cout << "Idx\tType\tPTS\t\tDTS\n";for (const auto& f : frames) {std::cout << f.display_index << "\t" << frame_type_str(f.type)<< "\t" << f.pts << "\t" << f.dts << "\n";}return 0;
}
结果输出:
Idx Type PTS DTS
0 I 0 0
3 P 10800 3600
1 B 3600 7200
2 B 7200 10800
4 I 14400 14400
7 P 25200 18000
5 B 18000 21600
6 B 21600 25200
时间基
概念
时间基是一个分数,表示时间戳的单位,即:
1 time_base = 1 / 秒数
通俗讲:
如果时间基是
{1, 25}
,则时间戳单位为 1/25 秒,每递增 1,表示时间过去了 1/25 秒(即一帧)。
使用场景
场景 | 用途说明 |
---|---|
编码器 time_base | 控制 frame->pts 的单位 |
AVStream time_base | 控制 packet->pts/dts 在封装文件中的时间戳单位 |
音视频同步 | 不同 stream 间通过统一时间基换算进行对齐 |
时间戳转换 | 不同模块交互时需要 av_rescale_q() 进行换算 |
常见时间基含义
时间基 {num, den} | 表示含义 | 常见用途 |
---|---|---|
{1, 25} | 每单位 = 1/25 秒 | 25fps 视频 |
{1, 1000} | 每单位 = 1ms | MP4/FLV 封装层常用 |
{1, 90000} | 每单位 = 1/90000 秒 | MPEG-TS、RTSP 常用 |
{1, 48000} | 每单位 = 一个采样周期 | 48kHz 音频 |
{1, AV_TIME_BASE} | 每单位 = 微秒(1/1000000s) | FFmpeg 通用时间单位 |
设置建议
视频编码器(AVCodecContext)
codec_ctx->time_base = (AVRational){1, fps};
场景 | 示例 |
---|---|
25fps 视频 | {1, 25} |
30fps 视频 | {1, 30} |
60fps 视频 | {1, 60} |
表示每帧间隔 1/25、1/30 秒,
AVFrame->pts
从 0 开始每帧加 1。
音频编码器
codec_ctx->time_base = (AVRational){1, sample_rate};
场景 | 示例 |
---|---|
48kHz 音频 | {1, 48000} |
44.1kHz 音频 | {1, 44100} |
表示每个采样点对应 1/48000 秒,
AVFrame->pts
表示采样点偏移量。
封装层 AVStream(非常重要!)
stream->time_base = (AVRational){1, 1000}; // 推荐毫秒单位
// 或者 MPEG TS 常用:
stream->time_base = (AVRational){1, 90000};
容器格式 | 建议 time_base |
---|---|
MP4、FLV | {1, 1000} (毫秒) |
MPEG-TS | {1, 90000} |
MKV | {1, 1000} 或自动适配 |
所有
AVPacket->pts/dts
必须用av_rescale_q()
将帧时间戳从 codec_ctx 的 time_base 转换为 stream 的 time_base。
时间戳换算方式(必做)
pkt.pts = av_rescale_q(frame->pts, codec_ctx->time_base, stream->time_base);
pkt.dts = pkt.pts;
如果有 B 帧,还需考虑 DTS 与 PTS 的排序差异。
音视频同步建议
音频和视频流的时间戳必须使用统一时间轴对齐,即都转为同一 stream time_base 参与比较:
video_pts_in_ms = av_rescale_q(video_pkt.pts, video_stream->time_base, {1, 1000});
audio_pts_in_ms = av_rescale_q(audio_pkt.pts, audio_stream->time_base, {1, 1000});
设置错误的后果
错误类型 | 可能后果 |
---|---|
time_base 设置过小或过大 | 时间戳溢出、精度不足、播放不同步 |
编解码器与封装器 time_base 不匹配 | 封装时间戳错误、seek异常、播放异常 |
不进行时间基转换 | 时间戳错乱,播放器无法正确识别帧时间 |
不同 stream 使用不统一单位 | 音视频同步失败,seek 错位 |
总结
模块 | 推荐时间基 | 备注 |
---|---|---|
视频编码器 | {1, fps} | 如 25fps = {1, 25} |
音频编码器 | {1, sample_rate} | 如 48kHz = {1, 48000} |
视频流封装 | {1, 1000} | 单位为毫秒,通用适配性好 |
音频流封装 | {1, 1000} | 同上 |
MPEG TS 封装 | {1, 90000} | 对应 MPEG 时间基 |
时间戳转换函数 | av_rescale_q() | 必须用于编码→封装间的换算 |
影响
控制解码顺序(DTS)
描述:
- DTS 决定帧在解码器中何时被解码,尤其在存在 B 帧(双向预测帧)时。
影响:
- 解码顺序错误将导致解码失败或花屏,因为 B 帧依赖前后帧数据,必须在参考帧解码后才能处理。
- 没有正确处理 DTS 会导致视频数据丢帧或无法还原图像。
控制播放顺序(PTS)
描述:
- PTS 决定帧在播放器中何时显示。
影响:
- 如果 PTS 错乱,视频会播放顺序错乱、跳帧、画面抖动。
- 音视频同步将失败,导致“嘴型不对”,“声画不同步”。
音视频同步(PTS)
描述:
- 音频和视频通过 PTS 对齐,实现声画同步。
影响:
- 若视频帧 PTS 落后于音频帧,会导致“声音先到画面后到”;
- 若 PTS 提前或延迟跳变,导致“卡顿”、“快进”或“时间线错乱”。
播放器缓冲管理(PTS + DTS)
描述:
播放器使用 PTS 判断是否需要缓冲、跳帧或提前渲染;用 DTS 提前解码数据填充缓冲区。
影响:
- DTS 错误可能导致解码器提前耗尽帧,导致播放中断。
- PTS 不连续或跳变会导致播放卡顿、缓冲策略错误。
B帧重排序处理(PTS ≠ DTS)
描述:
编码器会将帧重排序以压缩效率最大化(I-P-B结构),导致 PTS ≠ DTS。
影响:
- 解码器必须根据 DTS 顺序输入数据,但播放器必须按 PTS 顺序显示。
- 如播放器或解码器未处理重排序,会导致播放异常,如花屏、跳帧。
封装格式要求(TS、MP4、FLV等)
描述:
不同封装格式对 PTS 和 DTS 有不同要求:
- MPEG-TS 通常包含 PTS 和 DTS;
- MP4 要求明确记录帧的 PTS 和 DTS;
- FLV 仅含 PTS,要求解码器自己推算 DTS。
影响:
- 若格式需要 DTS 而未提供,播放器解码会出错;
- 在流媒体中时间戳错误影响服务端/客户端播放稳定性。
播放器时间轴与播放速率控制(PTS)
描述:
播放器以 PTS 为时间基准,控制渲染速率(帧率)、快进/慢放等行为。
影响:
- 时间戳不单调递增将破坏时间轴,导致跳帧、时间错乱;
- 快进时无法准确跳转至目标 PTS 会导致 seek 错位。
硬件解码器行为(DTS)
描述:
一些硬件解码器(如 GPU 解码)依赖 DTS 正确排序,确保流水线处理。
影响:
- 错误 DTS 会导致解码器 crash、丢帧、处理错误。
转码与重新封装流程依赖 PTS/DTS
描述:
转码器/复用器如 FFmpeg 需要依靠 PTS/DTS 判断帧间顺序与时长。
影响:
- 时间戳错误会导致输出文件乱序、播放错乱或封装失败。
时间基转换和流同步影响(PTS)
描述:
不同编码器/封装器的 time_base 不同,PTS/DTS 需要合理缩放转换。
影响:
- 转换错误会导致输出帧率不对、时间戳跳变或超大。
对比
影响点 | PTS(播放) | DTS(解码) |
---|---|---|
控制显示顺序 | 主要依据 | 不相关 |
控制解码顺序 | 不相关 | 主要依据 |
B帧重排序 | 决定实际显示顺序 | 决定输入解码顺序 |
音视频同步 | 用于声画对齐 | 无直接作用 |
播放器缓冲控制 | 决定渲染点 | 提前解码填充缓冲区 |
seek/快进精准定位 | 关键依据 | 不直接参与 |
格式封装要求 | 必需项 | 某些格式必须 |
硬件解码器正确性 | 依赖 DTS 重排序结果 | 必须严格准确 |
转码/封装时间戳计算 | 控制封装播放顺序 | 控制帧顺序完整性 |