TS Demux 插件知识文档
基于 GStreamer gst-plugins-bad/gst/mpegtsdemux 插件源码整理,涵盖 MPEG-2 Transport Stream 容器格式关键信息及 tsdemux 元素调用流程。
零、MPEG-TS 是什么,为什么要有它
定位
MPEG-2 Transport Stream (TS) 是一种面向传输的容器格式,最初为卫星/有线/地面广播设计,将视频、音频、字幕等多路数据拆分为固定 188 字节包,适合在有误码的信道上传输。后来被广泛用于 IPTV、HLS、蓝光(BDMV/M2TS)等场景。
文件扩展名约定:.ts(标准传输流)、.m2ts(蓝光,192字节包)、.mts(AVCHD 摄像机)。
为什么选 TS 而不是 MP4/MKV
| 场景 | 选 TS | 选 MP4/MKV |
|---|---|---|
| 卫星/有线广播 | 固定包+CC校验+CRC,天然容错 | 无内置容错,丢包即崩溃 |
| IPTV/HLS | 标准传输协议,无缝衔接 | HLS 基于 TS 分片 |
| 蓝光 BDMV | M2TS 是标准格式 | 不支持 |
| 多节目复用 | PAT/PMT 原生多节目 | 不支持 |
| 本地文件播放 | 可用但无帧级索引,Seek 慢 | 帧级索引,Seek 精确 |
| 在线流媒体(DASH) | 不适用 | ISO BMFF (MP4) 是标准基础 |
| DRM 版权保护 | CA_descriptor + ECM/EMM,生态碎片化 | Widevine/PlayReady/FairPlay 成熟 |
一句话总结:TS 用固定包和冗余校验换传输可靠性,MP4/MKV 用索引和变长结构换存储效率和随机访问。
一、MPEG-TS 容器格式概览
1.1 TS 包结构 ★★★
每个 TS 包固定长度(标准 188B),结构如下:
TS Packet (188 bytes):
┌─────────────────────────────────────────────────────────────┐
│ Header (4B) │ Adaptation Field (0~) │ Payload (0~) │
└─────────────────────────────────────────────────────────────┘
TS 包头 (4 字节) 比特级结构
Byte 0: sync_byte = 0x47 (固定)
Byte 1: ┌─────┬──────┬──────────┐
│ TEI │ PUSI │ Priority │ ← 高3位
│ 1bit│ 1bit │ 1bit │
└─────┴──────┴──────────┘
┌───────────────────────┐
│ PID[12:11] │ ← 低5位 (PID高2位)
└───────────────────────┘
Byte 2: PID[10:0] ← PID低11位
→ PID = ((byte1 & 0x1F) << 8) | byte2 (13-bit, 0~8191)
Byte 3: ┌──────────────┬──────┬────┐
│Scrambling(2b)│ AFC │ CC │
│ [7:6] │[5:4] │[3:0]│
└──────────────┴──────┴────┘
各字段详解:
| 字段 | 位 | 值/含义 |
|---|---|---|
| sync_byte | Byte0 [7:0] | 固定 0x47,每隔 packetsize 字节出现一个 |
| TEI (transport_error_indicator) | Byte1 [7] | 1=不可纠误码,包应丢弃 |
| PUSI (payload_unit_start_indicator) | Byte1 [6] | 1=PES/Section 起始包 |
| transport_priority | Byte1 [5] | 1=比同 PID 包更高优先级 |
| PID | Byte1[4:0]+Byte2 [7:0] | 13-bit 包标识符 |
| transport_scrambling_control | Byte3 [7:6] | 00=未加密; 01=保留; 10=偶密钥; 11=奇密钥 |
| adaptation_field_control | Byte3 [5:4] | 00=保留; 01=仅Payload; 10=仅AF; 11=AF+Payload |
| continuity_counter | Byte3 [3:0] | 4-bit 循环计数(0~15),仅含Payload时递增 |
代码解析 (mpegts_packetizer_parse_packet):
c
// 跳过 sync_byte (0x47)
tmp = *data++; // Byte 1
packet->payload_unit_start_indicator = tmp & 0x40;
packet->pid = GST_READ_UINT16_BE(data) & 0x1FFF; // Byte 1-2
data += 2;
tmp = *data++; // Byte 3
packet->scram_afc_cc = tmp;
// scrambling = tmp & 0xC0, AFC = tmp & 0x30, CC = tmp & 0x0F
字节序 : TS 包所有字段均为大端序 (Big-Endian)。
PID 特殊值
| PID | 含义 |
|---|---|
| 0x0000 | PAT (节目关联表) |
| 0x0001 | CAT (条件访问表) |
| 0x0002 | TSDT (传输流描述表) |
| 0x0010 | NIT (网络信息表) |
| 0x0011 | SDT/BAT (业务描述表/业务群关联表) |
| 0x0012 | EIT (事件信息表) |
| 0x0014 | TDT/TOT (时间表) |
| 0x1FFF | 空包 (stuffing) |
1.2 Adaptation Field ★★
Adaptation Field 紧跟 TS 包头之后,AFC=10 或 11 时存在。用于填充、PCR 传输、随机访问标识等。
比特级结构
Adaptation Field:
┌──────────┬────────┬──────────────────────────────────────┐
│Length(1B)│Flags(1B)│ Optional Fields (按 flag 存在) │
└──────────┴────────┴──────────────────────────────────────┘
Byte 0: adaptation_field_length
= 后续字节数 (不含自身)
= 0 时仅做 stuffing,无 flags
有效范围: 0~183 (无payload), 0~182 (有payload)
Byte 1: Flags
┌──────────────┬──────┬─────────┬─────┬─────────┬──────────┬──────────┬──────────┐
│Discontinuity │ RAI │ES Priority│PCR │ OPCR │Splicing │Transport │Extension │
│indicator │ │ │flag │ flag │Point flag│Priv Data │flag │
│bit7 │bit6 │bit5 │bit4 │bit3 │bit2 │bit1 │bit0 │
└──────────────┴──────┴─────────┴─────┴─────────┴──────────┴──────────┴──────────┘
各字段详解:
| Flag | 位 | 含义 |
|---|---|---|
| discontinuity_indicator | [7] | 1=PCR 或 CC 不连续 |
| random_access_indicator (RAI) | [6] | 1=关键帧/随机访问点起始 ★ Seek 定位用 |
| elementary_stream_priority_indicator | [5] | 1=此包携带优先数据 |
| PCR_flag | [4] | 1=6 字节 PCR 跟随 ★ |
| OPCR_flag | [3] | 1=6 字节 OPCR 跟随 |
| splicing_point_flag | [2] | 1=1 字节 splice_countdown 跟随 |
| transport_private_data_flag | [1] | 1=私有数据跟随 |
| adaptation_field_extension_flag | [0] | 1=扩展数据跟随 |
PCR 编码 (6 字节, PCR_flag=1 时)
PCR (6 字节):
┌─────────────────────────────────────────────────────────┐
│ program_clock_reference_base (33bit) │reserved(6bit)│ext(9bit) │
│ → 90kHz 精度 │ │→27MHz精度│
└─────────────────────────────────────────────────────────┘
字节级编码:
Bytes 0-3: PCR base[32:1] (32-bit BE)
Byte 4: PCR base[0] (bit7) | reserved[5:0] (bits6-1) | PCR ext[8] (bit0)
Byte 5: PCR ext[7:0]
代码解析 (mpegts_packetizer_compute_pcr):
pcr1 = GST_READ_UINT32_BE(data); // bytes 0-3
pcr2 = GST_READ_UINT16_BE(data + 4); // bytes 4-5
pcr_base = (pcr1 << 1) | ((pcr2 & 0x8000) >> 15) // 33-bit base
pcr_ext = pcr2 & 0x01FF // 9-bit extension
PCR = pcr_base × 300 + pcr_ext // 27MHz 时钟单位
→ PCRTIME_TO_GSTTIME(pcr) = pcr × 1000 / 27 // 转换为纳秒
其他可选字段
| 字段 | 长度 | 条件 | 说明 |
|---|---|---|---|
| OPCR | 6B | OPCR_flag=1 | 原始 PCR,编码同 PCR,仅日志用 |
| splice_countdown | 1B | splicing_point_flag=1 | 倒计数,0=拼接点 |
| transport_private_data | 1+N B | transport_private_data_flag=1 | 1字节长度 + N字节私有数据 |
| extension | 1+N B | extension_flag=1 | 1字节长度 + flags + 可选 ltw/piecewise_rate/seamless_splice |
代码常量: MPEGTS_AFC_PCR_FLAG=0x10, MPEGTS_AFC_OPCR_FLAG=0x08, MPEGTS_AFC_SPLICING_POINT_FLAG=0x04, MPEGTS_AFC_TRANSPORT_PRIVATE_DATA_FLAG=0x02, MPEGTS_AFC_EXTENSION_FLAG=0x01
1.3 TS 包尺寸变体
| 格式 | 包大小 | 额外字节 | 场景 |
|---|---|---|---|
| 标准 TS | 188B | 无 | DVB/ATSC/IPTV/HLS |
| M2TS | 192B | 4B 时间码前缀 | 蓝光 BDMV |
| DVB ASI | 204B | 16B FEC | DVB-ASI 接口 |
| ATSC | 208B | 20B FEC | ATSC 地面广播 |
代码常量: MPEGTS_NORMAL_PACKETSIZE=188, MPEGTS_M2TS_PACKETSIZE=192, MPEGTS_DVB_ASI_PACKETSIZE=204, MPEGTS_ATSC_PACKETSIZE=208
1.4 PAT 结构 ★★★
PAT (Program Association Table) 是 TS 流的入口点,固定在 PID 0x0000 上传输,告诉接收端每个节目号对应的 PMT 在哪个 PID 上。
PAT Section 比特级结构
PAT Section (Long Section, table_id=0x00):
┌────────────┬─────────────────────┬──────────────────────┬──────────┐
│ Header(8B) │ Program Entries(N×4B)│ ... │ CRC32(4B)│
└────────────┴─────────────────────┴──────────────────────┴──────────┘
Header (8 字节):
┌──────────┬──────────────────────────────────┬─────────────────────┐
│table_id │section_syntax|private|reserved| │transport_stream_id │
│ 8bit │indicator(1) |ind(1) |2bit │ │16bit │
│=0x00 │section_length(12bit) │ │
│ │=后续总长度(含CRC) │ │
├──────────┼──────────────────────────────────┼─────────────────────┤
│reserved │version_number│current_next│section│last_section_number │
│2bit │5bit │indicator(1)│_num(8)│8bit │
│=0b11 │0~31 循环 │1=当前有效 │通常=0 │通常=0 │
└──────────┴──────────────────────────────────┴─────────────────────┘
字节级编码:
Byte 0: table_id = 0x00
Byte 1: 0xB0 | (section_length >> 8) & 0x0F
bit7 = section_syntax_indicator = 1
bit6 = private_indicator = 0
bit5-4 = reserved = 0b11
Byte 2: section_length & 0xFF
Byte 3-4: transport_stream_id (大端)
Byte 5: 0xC0 | (version_number << 1) | current_next_indicator
bit7-6 = reserved = 0b11
Byte 6: section_number
Byte 7: last_section_number
Program Entry (每条 4 字节,重复 N 次):
┌───────────────────┬───────────────────────────────────┐
│ program_number │ reserved(3bit) │ PID(13bit) │
│ 16bit │ =0b111 │ │
└───────────────────┴───────────────────────────────────┘
program_number = 0x0000 → PID 指向 NIT (网络信息表)
program_number ≠ 0 → PID 指向该节目的 PMT
CRC32 (4 字节):
IEEE 802.3 CRC32,覆盖 table_id 到 CRC 前的所有字节
校验: _calc_crc32(data, section_length) == 0
最小长度: 12 字节 (8 Header + 4 CRC,无节目条目)
条目数计算: nb_programs = (section_length - 8 - 4) / 4
PAT 在代码中的处理
数据结构:
GstMpegtsSection --- 通用 Section 容器
├── section_type = GST_MPEGTS_SECTION_PAT
├── pid = 0x0000
├── table_id = 0x00
├── subtable_extension = transport_stream_id
├── version_number / current_next_indicator
└── cached_parsed → GPtrArray of GstMpegtsPatProgram
GstMpegtsPatProgram --- PAT 节目条目
├── program_number (guint16)
└── network_or_program_map_PID (guint16, 低13位有效)
解析流程:
mpegts_packetizer_push_section()
→ gst_mpegts_section_new() 解析 Header (8B)
→ gst_mpegts_section_get_pat()
→ __common_section_checks(min_size=12, CRC校验)
→ _parse_pat()
├── data = section->data + 8 (跳过已解析的 Header)
├── end = section->data + section_length
├── nb_programs = (end - 4 - data) / 4
└── 逐条读取: program_number(16b) + PID(13b, &0x1FFF)
应用流程 (mpegts_base_apply_pat):
1. 解析 PAT → GPtrArray<GstMpegtsPatProgram>
2. 替换 base->pat (旧 PAT 保留用于对比)
3. 遍历新 PAT 条目:
├── program_number=0 → NIT 条目,跳过
├── 节目已存在:
│ ├── PMT PID 变化 → 取消旧 PID 的 known_psi,注册新 PID
│ └── PMT PID 不变 → 无操作
└── 节目不存在 → mpegts_base_add_program() 创建
4. 遍历旧 PAT 条目:
├── patcount > 0 → 仍被新 PAT 引用,保留
└── patcount = 0 → 节目已移除,deactivate + 释放
1.5 PSI/SI 表总览 ★★
PSI (Program Specific Information) 是 TS 的节目描述信息,通过 Section 传输:
TS 层级结构:
PAT (PID 0x0000)
├── program_number → PMT PID
│
PMT (PID 由 PAT 指定)
├── program_number
├── pcr_pid ★ 节目 PCR 所在 PID
├── program_info_length + descriptors
└── streams[] ★ 每个基本流的信息
├── stream_type ★ 编码类型
├── elementary_PID ★ 此流的 PID
├── ES_info_length + descriptors
│ ├── ISO_639_language_descriptor (语言)
│ ├── registration_descriptor (如 'HDMV'/'AC-3')
│ ├── DVB AC-3/EAC3/AAC/LPCM descriptor
│ └── ...
└── (可以有多个 stream 条目)
其他 PSI/SI 表:
├── CAT (PID 0x0001) --- 条件访问表 (CA/DRM)
├── TSDT (PID 0x0002) --- 传输流描述表
├── NIT (PID 0x0010) --- 网络信息表 (DVB)
├── SDT (PID 0x0011) --- 业务描述表 (DVB)
├── EIT (PID 0x0012) --- 事件信息表 (EPG)
└── TDT/TOT (PID 0x0014) --- 时间表
Section 通用头 (Long Section 共用)
PAT、PMT、CAT、SDT、EIT 等 Long Section 共享相同的 8 字节头部:
Section Header (8 字节, 所有 Long Section 共用):
┌──────────┬──────────────────────────────────┬─────────────────────┐
│table_id │section_syntax|private|reserved| │subtable_extension │
│8bit │indicator(1) |ind(1) |2bit │ │16bit │
│ │section_length(12bit) │(PAT:ts_id/PMT:pgm_num)│
├──────────┼──────────────────────────────────┼─────────────────────┤
│reserved │version_number│current_next│section│last_section_number │
│2bit │5bit │indicator(1)│_num(8)│8bit │
│=0b11 │0~31 循环 │1=当前有效 │ │ │
└──────────┴──────────────────────────────────┴─────────────────────┘
字节级编码 (所有 Long Section 通用):
Byte 0: table_id
Byte 1: 0xB0 | (section_length >> 8) & 0x0F
bit7 = section_syntax_indicator = 1
bit6 = private_indicator = 0
bit5-4 = reserved = 0b11
Byte 2: section_length & 0xFF
Byte 3-4: subtable_extension (大端)
Byte 5: 0xC0 | (version_number << 1) | current_next_indicator
Byte 6: section_number
Byte 7: last_section_number
代码解析 (gst_mpegts_section_new):
section->short_section = (*data & 0x80) == 0x00
section->section_length = section_length + 3 // +3 包含 table_id + 2B length
section->subtable_extension = GST_READ_UINT16_BE(data)
section->version_number = tmp >> 1 & 0x1f
section->current_next_indicator = tmp & 0x01
1.6 PMT 结构 ★★★
PMT (Program Map Table) 描述一个节目包含哪些基本流、各自的 PID 和编码类型,以及节目的 PCR PID。
PMT Section 比特级结构
PMT Section (Long Section, table_id=0x02):
┌──────────┬──────────┬──────────────────┬───────────────┬──────────┐
│Header(8B)│PCR_PID(2B)│Prog Desc(var) │Streams(N×5+var)│CRC32(4B)│
└──────────┴──────────┴──────────────────┴───────────────┴──────────┘
PMT Body (紧跟 Section Header 的 8 字节):
Bytes 8-9: PCR PID
┌──────────┬──────────────┐
│reserved │ PCR_PID │
│3bit=0b111│ 13bit │
└──────────┴──────────────┘
→ pcr_pid = GST_READ_UINT16_BE(data) & 0x1FFF
Bytes 10-11: program_info_length + 节目级描述符
┌──────────┬────────────────────┐
│reserved │program_info_length │
│4bit=0b1111│ 12bit │
└──────────┴────────────────────┘
→ program_info_length = GST_READ_UINT16_BE(data) & 0x0FFF
→ 后跟 program_info_length 字节的节目级描述符
Stream Entry (重复 N 次, 每条至少 5 字节 + 描述符):
┌────────────┬──────────┬──────────────┬──────────────┬──────────────┐
│stream_type │reserved │elementary_PID│reserved │ES_info_length│
│8bit │3bit=0b111│13bit │4bit=0b1111 │12bit │
├────────────┴──────────┴──────────────┴──────────────┴──────────────┤
│ ES descriptors (ES_info_length 字节) │
└─────────────────────────────────────────────────────────────────────┘
代码解析 (_parse_pmt):
pmt->program_number = section->subtable_extension // 来自 Section Header
data += 8; // 跳过 Section Header
pmt->pcr_pid = GST_READ_UINT16_BE(data) & 0x1FFF; data += 2;
program_info_length = GST_READ_UINT16_BE(data) & 0x0FFF; data += 2;
pmt->descriptors = gst_mpegts_parse_descriptors(data, program_info_length);
data += program_info_length;
while (data <= end - 4 - 5): // -4 CRC, -5 最小 stream entry
stream->stream_type = *data++; // 1 byte
stream->pid = GST_READ_UINT16_BE(data) & 0x1FFF; data += 2; // 2 bytes
stream_info_length = GST_READ_UINT16_BE(data) & 0x0FFF; data += 2; // 2 bytes
stream->descriptors = gst_mpegts_parse_descriptors(data, stream_info_length);
data += stream_info_length;
// 校验: data == end - 4
最小长度: 16 字节 (8 Header + 2 PCR_PID + 2 program_info_length + 4 CRC)
PMT 数据结构
GstMpegtsPMT:
├── program_number (guint16) 来自 subtable_extension
├── pcr_pid (guint16) 节目 PCR 所在 PID
├── descriptors (GPtrArray) 节目级描述符列表
└── streams (GPtrArray) GstMpegtsPMTStream 列表
GstMpegtsPMTStream:
├── stream_type (guint8) 编码类型 (见 1.7 映射表)
├── pid (guint16) 基本流 PID
└── descriptors (GPtrArray) 流级描述符列表
Descriptor 通用结构
Descriptor (所有 PSI Section 中的描述符共用):
┌──────────┬──────────┬──────────────────────┐
│tag(1B) │length(1B)│descriptor_body(var) │
└──────────┴──────────┴──────────────────────┘
tag = 0x7F 时为扩展描述符, body 首字节为 tag_extension
length = body 字节数 (不含 tag 和 length 自身)
常见 Descriptor:
┌──────────┬──────────────────────────────────────┐
│ Tag │ 名称/用途 │
├──────────┼──────────────────────────────────────┤
│ 0x05 │ registration_descriptor (4CC标识) │
│ 0x0A │ ISO_639_language_descriptor │
│ 0x52 │ stream_identifier_descriptor │
│ 0x56 │ teletext_descriptor │
│ 0x59 │ subtitling_descriptor │
│ 0x6A │ AC-3 descriptor (DVB) │
│ 0x7A │ enhanced AC-3 descriptor (DVB) │
│ 0x7B │ DTS descriptor (DVB) │
│ 0x7C │ AAC descriptor (DVB) │
│ 0x7F │ extension_descriptor (含 tag_ext) │
└──────────┴──────────────────────────────────────┘
1.7 stream_type 映射 ★★
PMT 中 stream_type 标识基本流编码格式:
| stream_type | 标准 | 编码 | GStreamer Caps |
|---|---|---|---|
| 0x01 | ISO/IEC 11172-2 | MPEG-1 Video | video/mpeg mpegversion=1 |
| 0x02 | ISO/IEC 13818-2 | MPEG-2 Video | video/mpeg mpegversion=2 |
| 0x03 | ISO/IEC 11172-3 | MPEG-1 Audio | audio/mpeg mpegversion=1 |
| 0x04 | ISO/IEC 13818-3 | MPEG-2 Audio | audio/mpeg mpegversion=1 |
| 0x05 | ISO/IEC 13818-1 | Private Sections | --- |
| 0x06 | ISO/IEC 13818-1 | PES Private Data | 需 descriptor 辅助判断 |
| 0x0F | ISO/IEC 13818-7 | AAC Audio | audio/mpeg mpegversion=4 |
| 0x10 | ISO/IEC 14496-2 | MPEG-4 Visual | video/mpeg mpegversion=4 |
| 0x11 | ISO/IEC 14496-3 | AAC LATM Audio | audio/mpeg stream-format=loas |
| 0x1B | ISO/IEC 14496-10 | H.264/AVC | video/x-h264 |
| 0x1C | ISO/IEC 14496-3 | AAC ADTS Audio | audio/mpeg stream-format=adts |
| 0x1D | ISO/IEC 14496-1 | MPEG-4 SL/PES | --- |
| 0x21 | ITU-T H.265 | HEVC/H.265 | video/x-h265 |
| 0x24 | HEVC temporal video | HEVC (时域子层) | video/x-h265 |
| 0x80 | HDMV | LPCM (蓝光) | audio/x-private-ts-lpcm |
| 0x81 | HDMV | AC-3 (蓝光) | audio/x-ac3 |
| 0x82 | HDMV | DTS (蓝光) | audio/x-dts |
| 0x83 | HDMV | TrueHD (蓝光) | audio/x-true-hd |
| 0x84 | HDMV | E-AC3 (蓝光) | audio/x-eac3 |
| 0x85 | HDMV | DTS-HD (蓝光) | audio/x-dts |
| 0x86 | HDMV | DTS-HD MA (蓝光) | audio/x-dts |
| 0x90 | HDMV | PGS 字幕 (蓝光) | subpicture/x-pgs |
| 0xEA | RP227 | VC-1 | video/x-wmv wmvversion=3 |
| 0xD1 | --- | Dirac | video/x-dirac |
关键点 :
stream_type=0x06(PES Private) 是最常见的歧义类型,需结合 registration_descriptor 或 DVB descriptor 判断实际编码。
1.8 PES 包结构 ★★★
PES (Packetized Elementary Stream) 承载实际音视频数据,由多个 TS 包的 Payload 组装而成。PUSI=1 的 TS 包标识一个 PES 包的起始。
PES 包固定头 (6 字节)
PES Packet:
┌──────────────┬─────────────┬────────────┬──────────────────┐
│ Start Code │ Stream ID │ PES Length │ Optional Header │
│ 0x000001 │ 1B │ 2B │ + Payload Data │
└──────────────┴─────────────┴────────────┴──────────────────┘
Byte 0-2: Start Code Prefix = 0x000001
校验: (val32 & 0xFFFFFF00) == 0x00000100
Byte 3: Stream ID
0xBC = program_stream_map
0xBD = private_stream_1 (AC3/DTS/LPCM 等私有流)
0xBE = padding_stream
0xBF = private_stream_2
0xC0~0xDF = MPEG-1/2 Audio (stream_number = ID & 0x1F)
0xE0~0xEF = MPEG-1/2 Video (stream_number = ID & 0x0F)
0xF0 = ECM, 0xF1 = EMM, 0xF2 = DSMCC
0xFD = extended_stream_id (HEVC/VC-1 等)
0xFF = program_stream_directory
Byte 4-5: PES Packet Length (16-bit BE)
= 后续字节总数 (含 Optional Header + Payload)
= 0 表示长度未指定 (视频流常见)
非零时: 实际总长 = value + 6
跳过可选头的 Stream ID
以下 Stream ID 没有 可选 PES Header,PES Length 后直接是 Payload:
0xBC, 0xBE, 0xBF, 0xF0~0xF2, 0xF8, 0xFF
PES 可选头 (3 + header_data_length 字节)
Byte 6 --- PES Header Flags 1:
┌──────┬──────────┬─────────┬────────┬──────────┬──────────┐
│Marker│Scrambling│Priority │Data │Copyright │Original/ │
│2bit │Control │ │Align │ │Copy │
│=10 │2bit │1bit │1bit │1bit │1bit │
└──────┴──────────┴─────────┴────────┴──────────┴──────────┘
校验: (val8 & 0xC0) != 0x80 → 错误
scrambling_control: 00=未加密, 01=保留, 10=偶密钥, 11=奇密钥
flags = val8 & 0x0F (低4位存入 PESHeaderFlags)
Byte 7 --- PES Header Flags 2:
┌──────────┬─────┬───────┬──────────┬──────────┬──────────┬──────────┬──────────┐
│PTS_DTS │ESCR │ES_rate│DSM_trick │Add_copy │PES_CRC │PES │
│flags(2b) │flag │flag │mode_flag │info_flag│flag │ext_flag │
│[7:6] │[5] │[4] │[3] │[2] │[1] │[0] │
└──────────┴─────┴───────┴──────────┴──────────┴──────────┴──────────┴──────────┘
PTS_DTS_flags: 00=无, 01=禁止, 10=仅PTS, 11=PTS+DTS
Byte 8 --- PES Header Data Length:
8-bit 值,表示后续可选字段的字节总数 (不含自身)
→ 完整头大小 = 9 + header_data_length
其中 9 = 3(prefix+sid) + 2(length) + 3(这三字节) + 1(本字节)
PTS/DTS 33-bit 编码 (5 字节/个)
PTS 编码 (5 字节, PTS_DTS_flags=10 或 11 时存在):
Byte 0: [0010|PTS[32:30]|marker] ← '0010'=仅PTS; '0011'=PTS(在PTS+DTS时)
高4位标识: 0010=PTS_only, 0011=PTS_in_PTS+DTS
Byte 1: PTS[29:22]
Byte 2: [PTS[21:15]|marker]
Byte 3: PTS[14:7]
Byte 4: [PTS[6:0]|marker]
DTS 编码 (5 字节, PTS_DTS_flags=11 时存在):
格式同 PTS,但 Byte 0 高4位 = 0001
代码解码 (READ_TS 宏, gstmpegdefs.h):
target = ((guint64)(*data++ & 0x0E)) << 29; // byte0[4:1] → PTS[32:30]
target |= ((guint64)(*data++ )) << 22; // byte1 → PTS[29:22]
target |= ((guint64)(*data++ & 0xFE)) << 14; // byte2[7:1] → PTS[21:15]
target |= ((guint64)(*data++ )) << 7; // byte3 → PTS[14:7]
target |= ((guint64)(*data++ & 0xFE)) >> 1; // byte4[7:1] → PTS[6:0]
marker bit (LSB of byte 0,2,4) 必须为 1,否则跳转到 lost_sync
转换: MPEGTIME_TO_GSTTIME(pts) = pts × 100000 / 9
其他可选字段
| 字段 | 长度 | 条件 | 说明 |
|---|---|---|---|
| ESCR | 6B | ESCR_flag=1 | 编码同 PCR (base×300+ext),42-bit |
| ES_rate | 3B | ES_rate_flag=1 | 22-bit,值×50 = 字节/秒 |
| DSM trick mode | 1B | DSM_trick_mode_flag=1 | trick_mode_control[7:5]: 0=快进,1=慢放,2=冻结,3=快退,4=慢退 |
| additional_copy_info | 1B | additional_copy_info_flag=1 | bit7=marker, bits[6:0]=copy_info |
| previous_PES_packet_CRC | 2B | PES_CRC_flag=1 | 16-bit BE |
| PES extension | 变长 | PES_extension_flag=1 | 见下表 |
PES Extension (PES_extension_flag=1 时)
Extension Flags Byte:
┌──────────┬──────────┬──────────┬──────────┬──────────┬──────────┐
│PES_private│pack_hdr │pgm_pkt_ │P-STD_buf │reserved │PES_ext_2 │
│data_flag │flag │seq_cnt │flag │3bit │flag │
│[7] │[6] │flag[5] │[4] │[3:1] │[0] │
└──────────┴──────────┴──────────┴──────────┴──────────┴──────────┘
可选字段 (按 flag 存在):
PES_private_data_flag=1: 16 字节私有数据
pack_header_field_flag=1: 1字节size + pack header数据
program_packet_sequence_counter_flag=1: 2字节
P-STD_buffer_flag=1: 2字节
bit[7:6]=01, bit[5]=scale(0=×128,1=×1024), bits[4:0]+next[7:0]=size(13bit)
PES_extension_flag_2=1: 1字节长度 + stream_id_extension_data
→ stream_id_extension 用于 HEVC 等扩展流 ID
PESHeader 代码结构
PESHeader (pesparse.h):
├── stream_id (guint8)
├── packet_length (guint32) PES 包长度 (0=未指定)
├── header_size (guint16) 完整 PES 头大小
├── scrambling_control (guint8) 加密控制
├── flags (PESHeaderFlags) PRIORITY/DATA_ALIGNMENT/COPYRIGHT/ORIGINAL
├── PTS (guint64) -1=不存在
├── DTS (guint64) -1=不存在
├── ESCR (guint64) -1=不存在
├── ES_rate (guint32) 0=不存在
├── trick_mode (PESTrickModeControl)
├── stream_id_extension (guint8) HEVC 等用
└── extension_field_length (gsize)
1.9 时间系统 ★★★
TS 有三层时间体系,理解它们是调试 A/V 同步的基础:
PCR (Program Clock Reference):
- 27MHz 精度 (42-bit: 33-bit base × 300 + 9-bit ext)
- 由 PAT→PMT→pcr_pid 指定哪个 PID 传输 PCR
- 每个节目独立一个 PCR-PID
- 正常频率: 至少每 100ms 一个 PCR
PTS (Presentation Time Stamp):
- 90kHz 精度 (33-bit)
- 嵌入 PES Header 中
- 表示该帧应被显示的时间
- 33-bit 回绕周期: 2^33 / 90000 ≈ 26.5 小时
DTS (Decoding Time Stamp):
- 90kHz 精度 (33-bit)
- 嵌入 PES Header 中
- 表示该帧应被解码的时间
- 仅 B 帧重排场景下 DTS ≠ PTS
转换公式:
MPEGTIME_TO_GSTTIME(t) = t × 100000 / 9
PCRTIME_TO_GSTTIME(t) = t × 1000 / 27
GSTTIME_TO_PCRTIME(t) = t × 300 × 9 / 100000
PCR_SECOND = 27000000 (27MHz × 1s)
PTS_DTS_MAX_VALUE = 2^33 (回绕阈值)
二、关键技术要点
完整的包结构、PSI/SI 表见上方第一节。本节仅补充开发调试中需特别关注的技术细节。
2.1 PCR/PTS/DTS 的关系 ★★★
TS 流有三层时间,理解它们的关联是调试 A/V 同步的根基:
编码端 (发送方):
┌─────────────────────────────────────────────────────────┐
│ 编码器系统时钟 (27MHz STC) │
│ ├── PCR: 采样自 STC,写入 Adaptation Field │
│ │ PCR = STC_base(33bit,90kHz) × 300 + ext(9bit,27MHz) │
│ │ 至少每 100ms 发送一次 (由 PMT→pcr_pid 指定) │
│ │ │
│ ├── PTS: 帧的显示时间,采样自 STC,写入 PES Header │
│ │ PTS = STC_base(33bit) 精度: 90kHz │
│ │ 每个 PES 包一个 (关键帧/音频帧必须有) │
│ │ │
│ └── DTS: 帧的解码时间,采样自 STC,写入 PES Header │
│ DTS = STC_base(33bit) 精度: 90kHz
│ 仅 B 帧重排时 DTS ≠ PTS; 否则 DTS == PTS
└─────────────────────────────────────────────────────────────┘
解码端 (接收方):
┌─────────────────────────────────────────────────────────┐
│ 本地系统时钟 (27MHz) │
│ └── 锁相到 PCR → 恢复编码端时钟 → 用 PTS/DTS 同步 │
└─────────────────────────────────────────────────────────┘
核心关系:
| 关系 | 说明 |
|---|---|
| PCR 是锚点 | PCR 携带编码端的绝对时钟,接收端据此恢复 27MHz 系统时钟 |
| PTS/DTS 是刻度 | PTS/DTS 的值就是编码端 STC 在该帧显示/解码时刻的采样值 |
| PTS = PCR_base 的同源值 | PTS(90kHz) × 300 = 对应时刻的 PCR(27MHz),它们共享同一个时间基准 |
| PCR → 输出时间 | 接收端用 PCR 建立偏移映射,将 PTS/DTS 转换为 GStreamer 运行时间 |
| DTS ≤ PTS | 解码必须先于显示;B 帧重排时 DTS < PTS,差值 = 重排延迟 |
时间轴示意:
编码端 STC 时间轴 (27MHz):
├── PCR₁ ────── PCR₂ ────── PCR₃ ────── PCR₄ ───→
│ ↑ ↑
│ DTS_A DTS_B PTS_A PTS_B
│ (解码I帧) (解码P帧) (显示I帧) (显示P帧)
│
│ B帧重排示例: DTS_B < PTS_A (P帧先解码,I帧后显示)
│ 无B帧时: DTS == PTS
PCR 与 PTS/DTS 的数值关系:
假设某帧 PTS = 90000 (1秒 @ 90kHz)
则对应 PCR ≈ 90000 × 300 = 27,000,000 (1秒 @ 27MHz)
GStreamer 输出时间 = PTS × 100000/9 = 1,000,000,000 ns = 1秒
回绕:
PTS/DTS 33-bit (@ 90kHz): 2^33 / 90000 ≈ 95443秒 ≈ 26.5小时回绕一次
PCR base 33-bit (@ 90kHz): 同 PTS/DTS,26.5小时回绕一次
PCR 42-bit (@ 27MHz): 2^42 / 27000000 ≈ 1620秒 ≈ 27分钟回绕一次
注意: PCR base 与 PTS/DTS 共用 33-bit @ 90kHz,回绕周期相同 (26.5h);
PCR ext (9-bit @ 27MHz) 在 base 不变时每 2^9 / 27000000 ≈ 189μs 回绕一次,
但实际编码中 ext 始终与 base 配对使用,完整 42-bit PCR 的理论回绕周期为 27 分钟。
在 GStreamer 代码中,pcroffset 按完整 42-bit PCR 值累加,因此实际回绕周期取决于 base 的 26.5h。
代码中的转换链:
1. 原始值 (从码流读取):
PCR = base × 300 + ext (27MHz, 42-bit)
PTS/DTS (90kHz, 33-bit)
2. PTS/DTS → GStreamer 时间 (ns):
MPEGTIME_TO_GSTTIME(pts) = pts × 100000 / 9
3. PCR → GStreamer 时间 (ns):
PCRTIME_TO_GSTTIME(pcr) = pcr × 1000 / 27
4. GStreamer 时间 → PCR 时间:
GSTTIME_TO_PCRTIME(time) = time × 300 × 9 / 100000
5. 最终输出 (通过 packetizer):
Push 模式: output_time = pts_gst - skew (EPTLA 偏移)
Pull 模式: output_time = pts_gst - pcr_offset + extra_shift (PCR 组偏移)
2.2 PCR 同步与校验 PTS/DTS ★★★
PTS/DTS 的原始值 (90kHz) 是编码端系统时钟的采样值,无法直接作为 GStreamer 输出时间戳------必须通过 PCR 锚点将它们同步到 GStreamer 运行时钟 。整个转换链分两层:Packetizer 层 (pts_to_ts) 做 PCR 域→GStreamer 时钟域的映射,TSDemux 层 (record_pts/record_dts) 做一致性校验。
转换链总览
码流原始值 Packetizer 层 TSDemux 层
────────── ────────────── ────────────
PTS (90kHz) ──→ MPEGTIME_TO_GSTTIME ──→ pts_to_ts() ──→ record_pts()
(×100000/9) (PCR同步) (一致性校验)
│
DTS (90kHz) ──→ MPEGTIME_TO_GSTTIME ──→ pts_to_ts() ──→ record_dts()
(×100000/9) (PCR同步) (一致性校验)
为什么时钟同步在 demux 而非 decoder 中做
传统硬件方案:
TS Demux → 输出原始PES (PTS原值) → 解码器自带PLL锁相PCR → 用恢复的时钟调度解码/显示
GStreamer 管线模型:
tsdemux → 输出 GstBuffer (必须带 running_time) → decoder → sink (按pipeline时钟调度显示)
↑
sink 不做PCR时钟恢复
只按 buffer 上的时间戳播放
GStreamer 管线只有一个时钟 (通常是音频设备硬件时钟),所有元素的 buffer 时间戳必须对齐到这个时钟。下游 decoder/sink 不做 PCR 时钟恢复,只认 buffer 上的 running_time。
tsdemux 是管线中唯一同时接触两个时钟域的元素:
- 码流时钟域: PCR, PTS, DTS (从TS包解析)
- GStreamer时钟域: base_time, 上游segment时间 (从pipeline传入)
因此时钟同步必须在 demux 中完成: 将编码端 PTS/DTS 映射为 GStreamer running_time,下游才能正确调度。
为什么 Push 模式需要 skew
时钟恢复 (硬件 PLL 方案):
仅用 PCR 值的变化率,通过锁相环恢复编码端 27MHz 时钟
→ 不依赖到达时间,网络抖动完全无关
时间戳映射 (GStreamer 软件方案):
需要: PTS(编码端时钟) → running_time(GStreamer时钟)
唯一可用对齐点: PCR值 ↔ 该PCR包到达GStreamer的时间(time)
问题: time 包含网络抖动,不是理想到达时间
→ delta = recv_diff - send_diff = 时钟频差(想测) + 网络抖动(干扰)
→ skew 用滑动窗口最小值尽量逼近真实频差
skew 的计算: calculate_skew() --- EPTLA 算法
每次收到 PCR 时调用: calculate_skew(pcr, pcrtime, time)
步骤1: 计算核心观测量 delta
gstpcrtime = PCRTIME_TO_GSTTIME(pcrtime) + pcroffset // 加回绕偏移后的PCR时间
send_diff = gstpcrtime - base_pcrtime // 编码端发了多久 (码流时钟)
recv_diff = time - base_time // 接收端收了多久 (GStreamer时钟)
delta = recv_diff - send_diff // 两端时间差 = 频差 + 抖动
恒定延迟不影响: recv_diff = (到达时刻ₙ - 到达时刻₀), 固定延迟被消掉
网络抖动影响: 某包排队久 → 到达晚 → recv_diff 偏大 → delta 偏大
抖动只会让 delta 偏大,不会偏小 → 取最小值最接近真实频差
步骤2: 滑动窗口取最小值 (window[512])
填充阶段 (window_filling=TRUE):
→ 逐个填入 delta,记录 window_min
→ skew 趋近 window_min (抛物线权重: 初期缓慢,后期快速)
→ perc = max(发送时间进度, 窗口填充进度) × 100
→ skew = (perc² × window_min + (10000 - perc²) × skew) / 10000
→ 填满后: skew = window_min
滑动阶段 (window_filling=FALSE):
→ 新 delta 替换最旧值 (环形缓冲)
→ 更新 window_min: 新值≤min → 新min; 旧值==min → 全窗口扫描
→ skew = (window_min + 124 × skew) / 125 ← IIR低通滤波, 时间常数≈125采样
步骤3: 异常处理
┌──────────────┬──────────────────────────────┬──────────────────────┐
│ 情况 │ 条件 │ 处理 │
├──────────────┼──────────────────────────────┼──────────────────────┤
│ PCR 回绕 │ 新PCR<旧PCR 且差值>MAX/2 │ pcroffset += MAX │
│ PCR 重置 │ PCR反向>15s 且有有效time │ 反推pcroffset使对齐 │
│ 小抖动 │ PCR反向<1s │ 忽略,跳过本次 │
│ delta 跳变 │ |delta-skew| > discont_thresh │ resync重置窗口 │
│ 时间倒流 │ out_time < prev_out_time │ 使用上一次的时间 │
└──────────────┴──────────────────────────────┴──────────────────────┘
PCR 重置的反推公式:
假设 send_diff == recv_diff (对齐点成立)
→ (corrected)gstpcrtime = time - base_time + base_pcrtime
→ pcroffset += time - base_time + base_pcrtime - gstpcrtime
为什么取最小值而非均值: 网络抖动只让 delta 偏大 (包晚到),不会偏小。window_min ≈ 抖动最小的采样点 ≈ 真实时钟频差。均值会被抖动偏置拉高。
Packetizer 层: pts_to_ts() --- PCR 同步核心
根据模式走不同路径:
Push 模式 (calculate_skew=TRUE):
输入: pts_gst = MPEGTIME_TO_GSTTIME(pts) // 已转为 ns
pcr_pid = 节目的 PCR PID
步骤1: 初始偏移
res = pts_gst + pcrtable->pcroffset + extra_shift
// pcroffset: PCR 回绕累积偏移 (每次回绕 += PCR_GST_MAX_VALUE)
// extra_shift: 额外偏移 (segment 调整等)
步骤2: 15s 拒绝校验
if |res - last_pcrtime| > 15s:
→ res = GST_CLOCK_TIME_NONE // 与最近 PCR 差距过大,丢弃
// 意义: PTS 跳变超过 15s 极可能是码流错误,而非回绕
步骤3: skew 偏移修正
tmp = base_time + skew
// base_time: 首个有效 PCR 对应的输入时间 (上游 segment 时间)
// skew: EPTLA 算法计算的时钟偏移 (编码器/本地时钟频差)
if tmp + res >= base_pcrtime: // 正常情况
res += tmp - base_pcrtime
// 输出 = PTS + (base_time + skew - base_pcrtime)
// = PTS - base_pcrtime + base_time + skew
// = PTS 相对于首个 PCR 的偏移 + 运行时间起点 + 频差修正
elif |tmp + res + PCR_MAX - base_pcrtime| < PCR_MAX/2: // 回绕
res += tmp + PCR_GST_MAX_VALUE - base_pcrtime
// PTS 回绕处理: 补偿一个完整的 PCR 周期
else: // 异常
res = GST_CLOCK_TIME_NONE // 不可修复,丢弃
步骤4: 最终输出
res += switch_time_diff // (上游代码的额外修正)
Pull 模式 (calculate_offset=TRUE):
步骤1: 确定参考 PCR 组
if current->group 存在:
refpcr = group->first_pcr // 当前组的第一个 PCR
refpcroffset = group->pcr_offset // 组的累积偏移
else:
遍历 groups 列表,找 offset 所在的组
步骤2: 回绕判定
if pts < refpcr 且差值 > 1s:
pts += PCR_GST_MAX_VALUE // PTS 回绕补偿
elif 差值 <= 1s:
refpcr = G_MAXINT64 // PTS 恰好在组起始前,不判回绕
步骤3: 线性计算
res = pts - PCRTIME_TO_GSTTIME(refpcr) + PCRTIME_TO_GSTTIME(refpcroffset)
// 输出 = PTS - 组内基准PCR + 组的累积偏移
// = PTS 相对于组内基准的时间差 + 前面所有组的累积时间
TSDemux 层: record_pts/record_dts --- 一致性校验
pts_to_ts() 返回后,record_pts() 做最终校验:
record_pts():
raw_pts = pts (90kHz 原始值)
stream->pts = pts_to_ts(MPEGTIME_TO_GSTTIME(pts), pcr_pid) // 上面算出的 res
健壮性校验 (5s 一致性):
if |stream->pts - stream->dts| > 5s:
→ 丢弃 PTS,使用 DTS 代替
→ raw_pts = raw_dts
→ stream->pts = stream->dts
// 意义: PTS/DTS 正常差值 < 1s (B帧重排延迟),
// 超过 5s 说明 PTS 同步失败,DTS 更可靠
mpeg_pts_offset 计算:
if out_segment.format == TIME:
offset = GSTTIME_TO_MPEGTIME(segment_to_running_time(pts)) - raw_pts
mpeg_pts_offset = offset & 0x1FFFFFFFF
// 用于后续帧的时间戳连续性保证
record_dts():
同理调用 pts_to_ts(),但无 5s 校验 (DTS 通常更可靠)
PCR 同步机制汇总
| 校验点 | 位置 | 条件 | 处理 | 意义 |
|---|---|---|---|---|
| 15s 拒绝 | pts_to_ts (Push) | |res - last_pcrtime| > 15s | 返回 INVALID | PTS 跳变异常,可能是码流错误 |
| 回绕判定 | pts_to_ts (Push) | tmp+res < base_pcrtime 且差值 < PCR_MAX/2 | 补偿 PCR_MAX | PTS 33-bit 回绕 (26.5h) |
| 回绕判定 | pts_to_ts (Pull) | pts < refpcr 且差值 > 1s | pts += PCR_MAX | Pull 模式下的回绕处理 |
| 异常兜底 | pts_to_ts (Push) | 不满足正常/回绕任一条件 | 返回 INVALID | 无法确定是正常还是回绕 |
| 5s 一致性 | record_pts | |PTS - DTS| > 5s | 丢弃 PTS 用 DTS | PTS 同步失败时的保护 |
| PCR 回绕 | calculate_skew | 新 PCR < 上一个 PCR 且差值大 | pcroffset += PCR_MAX | 维持 PCR 单调递增 |
| PCR 重置 | calculate_skew | PCR 反向跳变 > 15s 且有有效时间 | 从输入时间重新计算 | 码流不连续 (切换频道等) |
| PCR 抖动 | calculate_skew | |差值| < 1s | 忽略 | 避免微小抖动影响偏移 |
同步流程图
PTS/DTS 原始值 (90kHz, 编码端时钟)
│
▼
MPEGTIME_TO_GSTTIME (→ ns, 单位统一)
│
▼
┌──────────────────────────────┐
│ pts_to_ts() │
│ ┌─────────────────────────┐ │
│ │ res = pts + pcroffset │ │ ← PCR 回绕累积
│ └──────────┬──────────────┘ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ |res-last_pcrtime|>15s? │ │ ← 15s 拒绝校验
│ │ YES → 返回 INVALID │ │
│ └──────────┬──────────────┘ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ Push: +skew偏移 │ │ ← EPTLA 频差同步
│ │ Pull: -refpcr+offset │ │ ← PCR 组偏移同步
│ └──────────┬──────────────┘ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ 回绕检测 + 兜底 │ │ ← 33-bit 回绕/异常
│ └──────────┬──────────────┘ │
└─────────────┼────────────────┘
▼
编码端时钟 → GStreamer 运行时钟 同步完成
│
▼
┌──────────────────────────────┐
│ record_pts/dts() │
│ ┌─────────────────────────┐ │
│ │ |PTS-DTS| > 5s? │ │ ← 一致性校验
│ │ YES → PTS=DTS │ │
│ └──────────┬──────────────┘ │
│ ▼ │
│ 计算 mpeg_pts_offset │ ← 保证后续帧连续
└─────────────┼────────────────┘
▼
GStreamer 输出时间戳 (ns)
一句话总结 : PCR 是时钟锚点,pts_to_ts() 用 PCR 建立偏移映射,将编码端 PTS/DTS 时钟同步到 GStreamer 运行时钟,过程中通过 15s 拒绝、回绕判定、异常兜底三重校验保证合理性;record_pts() 最后用 5s 一致性校验兜底,同步失败时回退到 DTS。
2.3 Section 重组 ★★
TS 包的 Payload 可能只包含 Section 的一部分,需要跨包重组:
Section 重组流程 (mpegts_packetizer_push_section):
1. PUSI=1 时: 读取 pointer_field,跳过前一个 Section 的尾部
2. 读取 Section Header: table_id + section_length
3. 跨包累积 section_data,直到 section_offset >= section_length
4. 校验 CRC32
5. 版本号检查: version_number 变化表示 Section 更新
6. 段号检查: section_number / last_section_number 确保完整
MpegTSPacketizerStream (每 PID):
├── continuity_counter 连续性计数
├── section_data 正在重组的 Section 数据
├── section_offset 当前偏移
├── table_id/version_number 段标识
├── subtables 已见子表列表 (防重复)
└── offset 上游偏移
去重机制: MpegTSPacketizerStreamSubtable.seen_section[256位] 位图记录已见 section_number,同一 table_id + subtable_extension + version_number + section_number 不重复处理。
2.4 PES 重组与帧推送 ★★★
PES 重组状态机 (PendingPacketState):
EMPTY → HEADER → BUFFER → (push) → EMPTY
↘ DISCONT → (丢弃)
流程:
1. 收到 PUSI=1 的 TS 包:
→ 先推送上一个 PES (push_pending_data)
→ 状态切换到 HEADER
2. HEADER 状态:
→ 解析 PES Header (mpegts_parse_pes_header)
→ 提取 PTS/DTS
→ 分配输出缓冲
→ 状态切换到 BUFFER
3. BUFFER 状态:
→ 将后续 TS 包 Payload 追加到缓冲
→ 当 current_size >= expected_size 或 >= MAX_PES_PAYLOAD(32MB) 时:
→ 推送完整帧 (push_pending_data)
4. DISCONT 状态:
→ 丢弃所有数据直到下一个 PUSI=1
连续性计数器校验:
- 仅对含 Payload 的包递增 (AFC=01 或 11)
- CC 不连续: 标记 discont,重置 PES 重组状态
- 首包: CC 可为 0~15 任意值 (CONTINUITY_UNSET=255)
2.5 关键帧检测 ★★
Seek 后需要从关键帧开始输出,不同编码的检测方式:
H.264 关键帧检测 (scan_keyframe_h264):
遍历 NAL Unit:
→ SPS (type=7) / PPS (type=8): 保存
→ IDR Slice: 解析 slice header,GST_H264_IS_I_SLICE 且 first_mb_in_slice==0 = 关键帧 ✓
→ 找到 SPS+PPS+关键帧: 按 SPS,PPS,SEI,关键帧顺序组装到 stream->data
H.265 / JP2K / MPEG Video:
上游代码仅实现了 H.264 的 scan_function
其他编码无 scan_function 时返回 TRUE (不做关键帧过滤)
2.6 三种时间戳模式 ★★★
| 模式 | 触发条件 | 时间戳计算 | 场景 |
|---|---|---|---|
| 实时推送 | 上游 segment 格式=TIME | EPTLA 时钟偏移算法 | DVB/UDP/RTP 直播 |
| 非实时推送 | 上游 segment 格式≠TIME | PCR 参考基准 + 偏移 | HLS/DLNA |
| 随机访问拉取 | Pull 模式 | PCR-偏移映射表 | 本地文件/蓝光 |
2.7 PTS/DTS 记录与校验 ★
gst_ts_demux_record_pts():
raw_pts = pts (90kHz 原始值)
pts = mpegts_packetizer_pts_to_ts(MPEGTIME_TO_GSTTIME(pts), pcr_pid)
健壮性检查: 如果 |PTS - DTS| > 5s,丢弃 PTS,使用 DTS 代替
mpeg_pts_offset 计算: 用于下游 SCTE35 插播时间转换
gst_ts_demux_record_dts():
同理,转换 DTS 到 GStreamer 时间
- 若
pending_ts=TRUE(尚无有效观测),缓冲作为 PendingBuffer 排队 - 当所有流都有观测后,
check_pending_buffers()刷新排队缓冲
2.8 Pending Buffer 机制 ★★
首次播放或 Seek 后,流可能还没有有效 PCR/PTS,此时不能直接输出 buffer。
触发条件: stream->pending_ts = TRUE (流创建/Seek后默认为TRUE)
→ 不推送 buffer,而是存入 stream->pending 链表 (PendingBuffer)
→ PendingBuffer 保存: data, size, raw_pts(90kHz), raw_dts(90kHz), offset
解除条件: check_pending_buffers()
→ 任一流获得有效 PTS 后触发
→ 调用 mpegts_packetizer_set_current_pcr_offset() 设置全局 PCR 偏移
→ 对每个 pending buffer 重新调用 pts_to_ts() 计算时间戳
→ 推送所有排队的 buffer
超时保护: 如果累积 >500ms 的 pending 数据仍无 PCR:
→ 设置 base->ignore_pcr = TRUE
→ pcr_pid 改为 0x1fff (禁用 PCR 时钟)
→ 对输入 segment 的 start/stop 施加 2s 偏移补偿
→ 后续帧直接用 MPEGTIME_TO_GSTTIME 转换,不走 pts_to_ts
2.9 不连续性处理 ★★
TS 流中存在三层不连续,处理方式不同:
1. TS 包级不连续 (Push 模式)
→ DISCONT 标记的 GstBuffer 到达
→ mpegts_packetizer_flush(hard=TRUE): 丢弃所有 PCR 观测
→ mpegts_packetizer_clear: 重置解析状态
→ 重新建立 base_time/base_pcrtime
2. CC 不连续 (连续性计数器)
→ gst_ts_demux_queue_data() 中检测
→ CC 应 +1 递增 (15→0 回绕),重复包(同CC)静默丢弃
→ 不匹配: GST_ELEMENT_WARNING + state = PENDING_PACKET_DISCONT
→ 丢弃所有数据直到下一个 PUSI=1
3. 输出 buffer 标记
→ stream->discont = TRUE: 下一个输出 buffer 带 GST_BUFFER_FLAG_DISCONT
→ 触发时机: 流创建、Seek、flush、CC 错误
→ 推送后自动清除
2.10 PTS 跳变处理 ★★
PTS 跳变指转换后的输出时间戳异常偏离预期,代码在多个层级防御:
1. pts_to_ts 中 15s 拒绝 (Push 模式)
if |res - last_pcrtime| > 15s:
→ res = GST_CLOCK_TIME_NONE // 丢弃该帧时间戳
场景: 码流拼接、PTS 突然回跳、编码器 bug
限制: pcr_pid == 0x1fff 时跳过此检查 (ignore_pcr 模式)
2. pts_to_ts 中异常兜底 (Push 模式)
if 不满足正常分支 且 不满足回绕分支:
→ res = GST_CLOCK_TIME_NONE // 无法判断,丢弃
场景: PTS 跳变方向不明 (不是回绕也不是正常增长)
3. record_pts 中 5s 一致性
if |PTS - DTS| > 5s:
→ PTS = DTS // 用 DTS 替代
场景: PTS 被错误修正,或码流 PTS/DTS 不一致
DTS 严格单调递增 (解码顺序),比 PTS 更可靠
4. calculate_skew 中 out_time 倒流保护
三种判定为倒流:
├── send_diff 上升但 out_time 下降 → skew 修正过度
├── send_diff 下降但 out_time 上升 → PCR 回绕未识别
└── send_diff 不变 → 重复 PCR
处理: out_time = prev_out_time (用前值保证单调)
5. calculate_skew 中 delta 跳变重同步
if |delta - skew| > pcr_discont_threshold:
→ mpegts_packetizer_resync(): 重置 base_time/base_pcrtime/skew
→ 重新填充滑动窗口
场景: 频道切换、码流拼接导致 PCR/PTS 基准突变
PTS 跳变的典型场景与对应处理:
| 场景 | 跳变特征 | 处理层级 | 结果 |
|---|---|---|---|
| 码流拼接 (广告插入) | PTS 突变到新基准 | delta 跳变 → resync | 重建时间基准 |
| 编码器 bug (PTS 跳变大值) | |PTS-LastPCR| > 15s | 15s 拒绝 | 帧无时间戳 |
| PTS/DTS 不一致 | |PTS-DTS| > 5s | 5s 一致性 | PTS 回退到 DTS |
| 33-bit 回绕 | PTS 从大值跳到小值 | 回绕判定 | 补偿 PCR_MAX |
| PCR 抖动导致 skew 偏移 | out_time 倒流 | 倒流保护 | 用前值 |
2.11 PCR 中断处理 ★★
PCR 中断指节目 PCR-PID 上不再有 PCR 包到达,或 PCR 间隔远超标准要求的 100ms。
场景1: 开播就没有 PCR (或 PCR 迟到)
┌─────────────────────────────────────────────────┐
│ 数据到达 → pending_ts=TRUE → buffer 排队等待 │
│ │
│ check_pending_buffers() 每次推送帧时检查: │
│ 有流拿到有效 PTS/DTS? │
│ ├── YES → 设置 PCR offset, 刷新所有 pending │
│ └── NO → 累积 pending 数据 > 500ms? │
│ ├── NO → 继续等 │
│ └── YES → 放弃 PCR ★ │
│ ignore_pcr = TRUE │
│ pcr_pid = 0x1fff │
│ 后续帧直接 MPEGTIME_TO_GSTTIME │
│ segment start/stop + 2s 补偿 │
└─────────────────────────────────────────────────┘
场景2: 播了一段时间后 PCR 断供
┌─────────────────────────────────────────────────┐
│ 已有 base_time + skew 基准 │
│ │
│ pts_to_ts() 公式不依赖实时 PCR: │
│ res = pts + pcroffset + extra_shift │
│ res += (base_time + skew) - base_pcrtime │
│ → skew 冻结在最后一个值,短期可继续工作 │
│ │
│ 问题1: skew 无法更新 │
│ → 两端时钟漂移累积 (慢漂 ~1ms/min) │
│ → 几分钟内可接受,长时间后 A/V 不同步 │
│ │
│ 问题2: 15s 拒绝校验可能误杀 ★ │
│ → last_pcrtime 冻结在最后一个 PCR │
│ → 正常帧的 res 持续增长 │
│ → |res - last_pcrtime| 最终 > 15s │
│ → 所有帧时间戳变 INVALID! │
│ → 直到 pending 数据 > 500ms 触发 ignore_pcr │
│ → pcr_pid 改为 0x1fff,跳过 15s 检查 │
└─────────────────────────────────────────────────┘
场景3: PCR 间隔不均匀 (偶尔有 PCR)
┌─────────────────────────────────────────────────┐
│ calculate_skew() 仅在有 PCR 时调用 │
│ → skew 和 base 在每个 PCR 到达时更新 │
│ → 间隔内的帧用已建立的 skew 转换 │
│ → PCR 间隔越大,skew 对实际频差的跟踪越滞后 │
│ │
│ 如果间隔期间发生 PTS 回绕: │
│ → 15s 拒绝可能误判 (因为 last_pcrtime 过旧) │
│ → 等 PCR 到达后 calculate_skew 才能检测回绕 │
└─────────────────────────────────────────────────┘
PCR 中断的三个阶段:
| 阶段 | 条件 | skew 状态 | 15s 检查 | 时间戳可用性 |
|---|---|---|---|---|
| 正常 | PCR 持续到达 | 持续更新 | 正常工作 | 可靠 |
| 冻结 | PCR 断供 < 15s | 冻结但可用 | res - last_pcrtime < 15s | 可靠 |
| 危险 | PCR 断供 15s~ | 冻结,漂移累积 | 误杀所有帧 | 全部 INVALID |
| 退化 | > 500ms pending | 忽略 | pcr_pid=0x1fff 跳过 | 直接映射,无频差修正 |
一句话总结: PCR 中断后代码依赖冻结的 skew 短期工作,但 15s 后 15s 拒绝校验会误杀所有帧,最终通过 ignore_pcr 机制退化为无 PCR 直接映射。这是上游代码的已知缺陷------15s 拒绝没有考虑"PCR 过旧"与"PTS 异常"的区别。
2.11 Duration 计算 ★★
gst_ts_demux_get_duration():
1. 首选: 上游 duration query (TIME 格式)
2. Pull 模式: mpegts_packetizer_offset_to_ts(file_size, pcr_pid)
→ 在最后的 PCROffsetGroup 中线性插值
3. adjust_duration: 使用 last_valid_offset 代替原始 file_size
(避免文件末尾填充数据干扰)
4. 无效 PCR 兜底: file_size × target / known_duration (比例估算)
2.12 Pull 模式 PCR 组回绕处理 ★★
Pull 模式用 PCROffsetGroup 维护 PCR 历史,组间回绕处理比 Push 模式更复杂:
组创建时:
→ 新组标记 PCR_GROUP_FLAG_ESTIMATED (偏移待确认)
→ 根据与前组的关系判定类型:
├── PCR_GST_MAX_VALUE × 90% < 差值: WRAPOVER (33-bit 回绕)
│ pcr_offset = prev.pcr_offset + MAX - prev.first_pcr + cur.first_pcr
├── PCR 反向 > 500ms: RESET (码流不连续)
│ pcr_offset = prev.last_pcr + 500~1000ms
└── 正常递增: 继承 prev.pcr_offset + 差值
组再评估 (_reevaluate_group_pcr_offset):
→ 创建新组时触发
→ 遍历所有 ESTIMATED 组,根据前组 bitrate 重新计算 pcr_offset
→ 前后组 bitrate 差异 < 10%: 确认偏移,清除 ESTIMATED 标记
→ 差异 ≥ 10%: 保持 ESTIMATED,等待更多观测
意义: Pull 模式无法像 Push 模式实时调整,需要在组边界做一次性修正
2.13 多节目与节目切换 ★
PAT/PMT 更新时节目处理 (mpegts_base_apply_pmt):
├── 新 PMT PID 与旧的不同: 取消旧 PID 注册,注册新 PID
└── 完全新建节目: mpegts_base_add_program()
节目切换 (同一 program_number 的新节目):
├── mpegts_base_steal_program(): 旧节目存入 demux->previous_program
├── can_remove_program 返回 FALSE: 保留旧节目直到排空
├── 旧节目的流继续 push_pending_data 排空
└── 排空后 deactivate 旧节目
Pad 命名避免冲突:
→ program_generation (4-bit 计数器) 递增
→ Pad 名: video_%x_%05x (%x = program_generation)
动态流 (decodebin3 场景):
→ streams_aware = TRUE 时,用 mpegts_base_update_program()
→ 增删单个 Pad,而非整体替换节目
2.14 稀疏流 Gap 事件 ★
字幕/元数据等稀疏流长时间无数据输出,会导致管线卡住。
gst_ts_demux_check_and_sync_streams():
→ 每次推送 PES 数据时检查所有流
→ 如果某流自上次检查后无输出 (nb_out_buffers == gap_ref_buffers):
→ 且无 pending 数据
→ 发送 gst_event_new_gap(time, 0) 保持管线活跃
→ 音频流偶尔也需 Gap (如 AC-3 同步帧间隔大时)
2.15 SCTE35 插播事件 ★
handle_psi() 中处理 GST_MPEGTS_SECTION_SCTE_SIT:
→ 解析 SCTE35 Splice Insert / Time Signal
→ splice_time 从 MPEG 时间域转换:
running_time = pts_to_ts(MPEGTIME_TO_GSTTIME(splice_time), pcr_pid)
→ 创建 running-time-map Structure:
├── "running-time": 转换后的运行时间
└── "mpeg-pts-offset": demux->mpeg_pts_offset (下游用于反推)
→ 作为自定义事件推送到所有 src pad
2.16 Flow Combiner 多 Pad 流控 ★
demux->flowcombiner 聚合所有活跃 Pad 的 flow return:
→ Pad 创建时: gst_flow_combiner_add_pad()
→ Pad 移除时: gst_flow_combiner_remove_pad()
→ 每次推送后: gst_flow_combiner_update_pad() 更新该 Pad 的状态
→ 组合规则: 任一 Pad 返回非 OK → 上报最严重的错误
→ 确保 EOS/ERROR 正确传播到上游
三、端到端播放流程
从打开文件到出画面的完整链路:
1. 模式选择
└── mpegts_base_sink_activate()
├── 支持 Pull → GST_PAD_MODE_PULL (GstTask 驱动 mpegts_base_loop)
└── 不支持 → GST_PAD_MODE_PUSH (mpegts_base_chain 回调)
2. 扫描同步 (Pull 模式)
└── mpegts_base_scan(): 搜索 0x47 同步字节,确定 packet_size
→ 拉取 64KB 块,累积 >=5 个 PCR 后完成
→ 读文件末尾,查找最后一个 PCR (计算 Duration)
3. PAT 解析
└── PID 0x0000 → mpegts_base_apply_pat()
→ 创建/更新 MpegTSBaseProgram (program_number → pmt_pid)
4. PMT 解析
└── PAT 指定的 PID → mpegts_base_apply_pmt()
→ 解析 stream_type、elementary_PID、pcr_pid
→ 激活节目: mpegts_base_activate_program()
→ 为每个 stream 调用 klass->stream_added()
→ TSDemux 中: create_pad_for_stream() → 创建 Pad + Caps
→ 调用 klass->program_started()
5. 数据循环
└── Push: mpegts_base_chain() → packetizer_push() → 逐包处理
Pull: mpegts_base_loop() → pull_range(100×packetsize) → mpegts_base_chain()
│
├── PSI 包 (known_psi): → push_section() → handle_psi()
├── PES 包 (is_pes): → klass->push() → gst_ts_demux_push()
│ └── gst_ts_demux_handle_packet()
│ ├── PUSI=1: push 上一个 PES → 切换到 HEADER
│ └── 追加 Payload → PES 重组 → 推送帧
└── 未知 PID: 根据 push_unknown 决定是否推送
6. 帧推送 (gst_ts_demux_push_pending_data)
└── PES 重组完成后:
├── 解析 PES Header → 提取 PTS/DTS
├── 时间戳转换: pts_to_ts() → GST 时间
├── 关键帧检测 (seek 时)
├── 设置 buffer flags (DISCONT/DELTA_UNIT/HEADER)
├── 设置 buffer timestamps/duration
└── gst_pad_push() → flowcombiner 聚合结果
7. Seek 处理
└── mpegts_base_handle_seek_event()
├── Push 模式: 上游 seek 或 klass->seek()
└── Pull 模式:
├── 发送 flush_start/flush_end
├── klass->seek() → gst_ts_demux_do_seek()
│ ├── ts_to_offset() 转换目标时间为字节偏移
│ ├── 回退 SEEK_TIMESTAMP_OFFSET (2.5s) 查找关键帧
│ └── 设置 base->seek_offset
├── 重启 mpegts_base_loop()
└── 从新偏移继续读取
8. 结束
└── EOS → 排空 (drain) 所有待推送数据 → 发送 EOS event → 暂停 task
关键路径上的性能瓶颈:
- PCR 观测建立(步骤2-4): Pull 模式需扫描文件首尾找 PCR,Push 模式需等待上游推送
- PES 重组(步骤5-6): 每帧都经历 Header 解析 + 时间戳转换,是热路径
- Seek 定位(步骤7): 无帧级索引,需回退 2.5s + 关键帧搜索,大 GOP 时延迟高
四、stream_type 与 Caps 完整映射
tsdemux 在 create_pad_for_stream() 中根据 stream_type + descriptor 创建 GStreamer Caps:
4.1 蓝光 (registration_id == DRF_ID_HDMV)
| stream_type | Caps | 说明 |
|---|---|---|
| 0x80 | audio/x-private-ts-lpcm | LPCM |
| 0x81 | audio/x-ac3 或 audio/x-eac3 (按 bsid 判断) | AC-3/E-AC3 |
| 0x83 | audio/x-true-hd | TrueHD |
| 0x84/0x87 | audio/x-eac3 | E-AC3 |
| 0x85/0x86 | audio/x-dts | DTS-HD/MA |
| 0x90 | subpicture/x-pgs | PGS 字幕 |
4.2 标准类型
| stream_type | Caps | 说明 |
|---|---|---|
| 0x01 | video/mpeg, mpegversion=1 | MPEG-1 Video |
| 0x02 | video/mpeg, mpegversion=2 | MPEG-2 Video |
| 0x03/0x04 | audio/mpeg, mpegversion=1 | MPEG-1/2 Audio |
| 0x06 (PES Private) | --- | 按 descriptor 分发,见下表 |
| 0x0F | audio/mpeg, mpegversion=4 | AAC |
| 0x10 | video/mpeg, mpegversion=4 | MPEG-4 Visual |
| 0x11 | audio/mpeg, mpegversion=4, stream-format=loas | AAC LATM |
| 0x1B | video/x-h264, stream-format=byte-stream | H.264 |
| 0x1C | audio/mpeg, mpegversion=4, stream-format=adts | AAC ADTS |
| 0x21 | video/x-h265, stream-format=byte-stream | HEVC |
| 0x24 | video/x-h265, stream-format=byte-stream | HEVC temporal |
| 0xD1 | video/x-dirac | Dirac |
| 0xEA | video/x-wmv, wmvversion=3, format=WVC1 | VC-1 |
4.3 stream_type=0x06 按 descriptor 分发
| Descriptor | Caps | 说明 |
|---|---|---|
| DVB AC-3 | audio/x-ac3 | |
| DVB Enhanced AC-3 | audio/x-eac3 | |
| DVB AC-4 extension | audio/x-ac4 | |
| DVB Teletext | application/x-teletext | |
| DVB Subtitling | subpicture/x-dvb | |
| reg_id DTS1/DTS2/DTS3 | audio/x-dts | |
| reg_id S302M | audio/x-smpte-302m | |
| reg_id OPUS | audio/x-opus | |
| reg_id HEVC | video/x-h265, stream-format=byte-stream | |
| reg_id KLVA | meta/x-klv, parsed=true | |
| reg_id AC4 | audio/x-ac4 |
五、代码架构
5.1 核心数据结构
继承关系:GstElement → MpegTSBase (基类, PAT/PMT/PSI) → GstTSDemux (解复用) / MpegTSParse2 (解析)
辅助模块:Packetizer (mpegtspacketizer.h/c, TS包解析+PCR追踪)、PES Parser (pesparse.h/c)、MPEG Defs (gstmpegdefs.h, 时间转换宏)、MPEG Desc (gstmpegdesc.h)
MpegTSBase --- 基类:
├── mode MpegTSBaseMode (SCANNING/SEEKING/STREAMING/PUSHING)
├── seek_offset Pull 模式当前读取偏移
├── packetsize 缓存的 TS 包大小
├── programs GPtrArray --- 活跃节目列表
├── pat GPtrArray --- PAT 条目
├── packetizer MpegTSPacketizer2 --- 包解析器
├── known_psi bit array --- 已知 PSI PID
├── is_pes bit array --- 已知 PES PID
├── seen_pat 是否已收到 PAT
├── segment 上游 segment
├── out_segment 下游 segment (子类使用)
├── ignore_pcr 是否忽略 PCR
├── streams_aware 父 bin 是否支持动态流
└── push_data/push_section/push_unknown --- 数据推送控制
MpegTSBaseClass 虚方法:
├── push() 推送 TS 包给子类
├── inspect_packet() 检查包 (可选)
├── push_event() 推送事件
├── handle_psi() 处理 PSI section
├── program_started/stopped() 节目生命周期
├── stream_added/removed() 流生命周期
├── find_timestamps() Pull 模式找 PCR
├── seek() Seek 实现
├── drain()/flush() 数据排空/刷新
└── input_done() 输入处理完成通知
GstTSDemux --- 解复用器:
├── requested_program_number 请求的节目号 (-1=自动)
├── program_number 当前节目号
├── program MpegTSBaseProgram* --- 当前节目
├── previous_program 切换中的旧节目
├── segment_event 待发送的 segment 事件
├── global_tags 全局标签
├── duration 流总时长
├── rate 播放速率
├── flowcombiner 多 Pad 流组合器
├── mpeg_pts_offset PTS 与输出运行时间的偏移
├── last_seek_offset 关键帧回退搜索用
├── latency 延迟 (默认 700ms)
└── lock (GMutex) 保护 segment_event
TSDemuxStream --- 每 PID 的流状态:
├── pad GstPad --- 输出 Pad
├── active Pad 是否已添加
├── sparse 是否稀疏流 (字幕/元数据)
├── state PendingPacketState (EMPTY/HEADER/BUFFER/DISCONT)
├── data/expected_size/current_size --- PES 数据组装缓冲
├── pts/dts 当前 PTS/DTS (GStreamer 时间)
├── raw_pts/raw_dts 当前 PTS/DTS (90kHz 原始值)
├── need_newsegment 是否需发送 newsegment
├── discont 下一 buffer 是否标记 DISCONT
├── first_pts 首个 PTS (用于 newsegment 计算)
├── continuity_counter 连续性计数器
├── pending 待处理 buffer 列表 (PendingBuffer)
├── scan_function 关键帧扫描函数
├── needs_keyframe Seek 后是否需等待关键帧
└── target_pes_substream 目标 PES 子流 (TrueHD/DTS-HD)
MpegTSPacketizer2 --- 包解析器:
├── adapter GstAdapter --- 数据缓冲
├── streams MpegTSPacketizerStream** --- 每 PID 流状态
├── packet_size 当前包大小
├── offset 当前偏移
├── calculate_skew 是否计算时钟偏移 (Push+TIME 模式)
├── calculate_offset 是否计算偏移映射 (Pull 模式)
├── pcrtablelut[0x2000] PID→PCR 表查找表 (O(1))
├── observations[256] 最多 256 个 PCR 通道
├── last_pts/last_dts 最后收到的 PTS/DTS
└── extra_shift 额外时间偏移
MpegTSPCR --- 每 PCR-PID 的观测数据:
├── pid
├── base_time/base_pcrtime EPTLA 算法基准
├── window[512] 偏移窗口
├── skew 计算出的时钟偏移
├── pcroffset PCR 回绕偏移
├── groups (GList*) PCROffsetGroup 列表
└── current (PCROffsetCurrent*) 当前观测窗口
PCROffsetGroup --- PCR 观测组:
├── first_pcr/first_offset 组内基准
├── values[] PCROffset 数组
├── pcr_offset 累积 PCR 偏移 (含回绕)
└── flags CLOSED/ESTIMATED/RESET/WRAPOVER
六、核心流程
6.1 初始化与模式选择
gst_ts_demux_init()
├── 调用 MpegTSBase 初始化
├── 设置 push_data=TRUE, push_section=FALSE
└── 初始化 flowcombiner, lock 等
mpegts_base_sink_activate()
└── gst_pad_check_pull_range()
├── 支持 → 激活 PULL 模式 (GstTask 驱动 mpegts_base_loop)
└── 不支持 → 激活 PUSH 模式 (mpegts_base_chain 回调)
6.2 Pull 模式主循环
mpegts_base_loop()
│
├── SCANNING: mpegts_base_scan()
│ ├── pull 64kB 块,搜索 0x47 同步字节
│ ├── 确定包大小 (188/192/204/208)
│ ├── 累积 >=5 个 PCR
│ └── 扫描文件末尾找最后一个 PCR (Duration)
│
├── STREAMING:
│ ├── gst_pad_pull_range(seek_offset, 100×packetsize)
│ ├── mpegts_base_chain()
│ └── seek_offset += buffer_size
│
└── SEEKING: 直接切换到 STREAMING
6.3 Push 模式数据流
mpegts_base_chain(pad, buffer)
│
├── DISCONT 标记 → drain + flush + 可能清除 packetizer
├── mpegts_packetizer_push(buf)
│
└── while PACKET_OK:
├── mpegts_packetizer_next_packet()
├── inspect_packet() vfunc
├── if is_pes(pid): klass->push(packet, NULL)
├── elif known_psi(pid): parse section → handle_psi()
└── elif push_unknown: klass->push(packet, NULL)
│
└── input_done() vfunc
6.4 轨道解析与 Pad 创建
create_pad_for_stream()
│
├── 根据 stream_type + registration_id + descriptor 确定 Caps
│ ├── 蓝光: 检查 HDMV registration → 蓝光专用映射
│ ├── 0x06: 按 descriptor 逐一匹配 (AC3/EAC3/DTS/Opus/HEVC/KLVA/...)
│ └── 标准: 直接按 stream_type 映射
│
├── 创建 GstPad (video_%x_%05x / audio_%x_%05x / subpicture_%x_%05x)
├── 设置 Caps, tags (language, codec info)
├── gst_element_add_pad() + gst_flow_combiner_add_pad()
└── 设置 scan_function (仅 Pull+H264 时)
6.5 Seek 流程
Pull 模式 Seek
mpegts_base_handle_seek_event()
│
├── 解析 seek event (rate, format, flags, start/stop)
├── Push 模式: 转发上游; 或 klass->seek()
│
├── Pull 模式:
│ ├── flush=TRUE: 发 flush_start → drain → flush(hard=TRUE)
│ ├── 暂停 task, 获取 STREAM_LOCK
│ ├── klass->seek() → gst_ts_demux_do_seek()
│ │ ├── 时间→偏移: mpegts_packetizer_ts_to_offset()
│ │ │ → 在 groups 中查找 → 线性插值
│ │ ├── 回退 SEEK_TIMESTAMP_OFFSET (2.5s)
│ │ └── 设置 base->seek_offset, last_seek_offset
│ │
│ ├── flush=TRUE: 发 flush_stop
│ ├── 为每个流: needs_keyframe=(ACCURATE flag), need_newsegment=TRUE
│ └── gst_pad_start_task() → 重启 loop
│
└── loop 从 seek_offset 开始读取:
→ 丢弃非关键帧 (needs_keyframe 标志)
→ 遇到关键帧后正常推送
ts_to_offset: 时间→字节偏移的核心转换
上游仅做一次 PCR Group 插值估算,不做 PTS 校验。完整流程:
目标时间 ts (GST_TIME, ns)
│
├── querypcr = GSTTIME_TO_PCRTIME(ts) ← 转成 27MHz PCR 单位
│
├── 遍历 pcrtable->groups 链表,找 querypcr 落在哪个区间:
│ ├── 先检查 current group (正在填充的组)
│ └── 再遍历 groups 链表,找 prevgroup 和 nextgroup
│
├── 取端点值:
│ ├── firstpcr = prevgroup.pcr_offset + prevgroup.last_value.pcr
│ ├── firstoffset = prevgroup.first_offset + prevgroup.last_value.offset
│ ├── lastpcr = nextgroup.pcr_offset + nextgroup.last_value.pcr
│ └── lastoffset = nextgroup.first_offset + nextgroup.last_value.offset
│
└── 线性插值:
offset = firstoffset + (querypcr - firstpcr) × (lastoffset - firstoffset) / (lastpcr - firstpcr)
offset 位置开始读取数据,找到第一个关键帧。(精度差)
精度限制:
- 相邻两个 PCR 观测点之间按码率线性估算
- CBR 流精度高(码率恒定),VBR 流偏差大(码率波动)
- PCR 越密集精度越高,PCR 间隔大时偏差可达数秒
- 无帧级索引,一次估算后不做 PTS 校验(上游)
优化迭代 PTS 校验:
ts_to_offset() → 粗偏移
├── offset_conv_to_pts()
│ ├── 拉取 pull_range 数据
│ ├── 解析 PES 头提取 PTS
│ └── 返回 current_pts (GST_TIME 单位)
│
├── 比较 current_pts 与 target:
│ ├── current < target - 阈值 → 前向搜索
│ │ └── 重新拉取解析
│ ├── current > target + 阈值 → 后向搜索
│ │ └── 重新拉取解析
│ └── 差值 < 阈值 → 确认偏移
│
├── 保护机制:
│ ├── 最大尝试次数 → 回退到粗偏移
│ ├── 强制推出搜索 → 外部取消
│ ├── PTS跳变异常 → 跳过该点
代价 :每次 seek 可能多次 pull_range I/O,大文件耗时长;CBR 假设在 VBR 流上偏差大。
关键帧回退搜索
gst_ts_demux_push_pending_data() 中:
if (stream->needs_keyframe):
├── 调用 stream->scan_function() 检测关键帧
├── 找到: needs_keyframe=FALSE, 正常推送
└── 未找到:
├── base->seek_offset = last_seek_offset - 200 × packetsize
├── base->mode = BASE_MODE_SEEKING
├── mpegts_packetizer_flush(hard=FALSE)
├── 重置所有流为 PENDING_PACKET_EMPTY
└── return GST_FLOW_REWINDING → loop 从更早偏移重试
6.6 Newsegment 计算
calculate_and_push_newsegment():
│
├── Push + TIME 模式:
│ → 直接转发上游 segment
│
├── Push + 非TIME 模式:
│ → 以第一个 PAT/PMT 时的 PCR 为时间 0
│ → 输出 segment.start = 0
│
└── Pull 模式:
→ 用 packetizer 的 offset_to_ts 计算起始时间
→ segment.start = first_pts
七、Pull 模式 vs Push 模式对比
| 特性 | Pull 模式 | Push 模式 |
|---|---|---|
| 入口函数 | mpegts_base_loop (GstTask) |
mpegts_base_chain (回调) |
| 数据源 | gst_pad_pull_range --- 随机访问 |
上游推送 GstBuffer |
| 模式状态 | SCANNING → STREAMING | 始终 PUSHING |
| 时间戳 | calculate_offset=TRUE (PCR-偏移映射) |
calculate_skew=TRUE (EPTLA) |
| Seek | 直接修改 seek_offset 重启 loop | 转发 seek event 到上游 |
| Duration | 扫描首尾 PCR 计算 | 依赖上游或 PCR 差值估算 |
| 包大小检测 | 主动扫描确定 | 被动接收确定 |
| 首次 PCR | find_timestamps() 主动查找 |
等待上游推送 |
| 拉取粒度 | 100 × packetsize/次 (约 18.8KB) | 上游决定 |
| 关键帧搜索 | 回退 + scan_function 逐帧检测 | 依赖上游 |
八、性能优化
8.1 I/O 层优化
Pull 批量拉取: 每次拉取 100 × packetsize (≈18.8KB),减少 pull_range 系统调用。
PCR 观测快速查找:
pcrtablelut[0x2000]直接映射 PID → observation index,O(1) 查找observations[256]最多 256 个 PCR 通道
8.2 查找优化
PCR Group 二分搜索: offset_to_ts/ts_to_offset 在排序后的 PCROffsetGroup 中二分查找,O(log N)。
PCR Group 内二分: 在 values[] 中二分查找最近的 PCROffset。
线性插值 : 找到相邻两个观测点后,用 gst_util_uint64_scale() 做精确插值,而非取最近邻。
8.3 解析优化
Section 版本号缓存 : seen_section[256位] 位图记录已见 section_number,避免重复处理同一 Section。
PES 帧级跳过 : Seek 时 needs_keyframe 标志直接跳过非关键帧 PES,不做完整解码。
已解析标记 : seen_pat 确保 PAT 处理逻辑正确触发。
8.4 内存保护
MAX_PES_PAYLOAD (32MB): 单个 PES 包超过 32MB 时强制输出,防止内存溢出。超出时按 32MB 分块输出。
PCR 观测窗口 : MAX_WINDOW=512 限制 EPTLA 滑动窗口大小。
PCROffsetGroup 预分配 : DEFAULT_ALLOCATED_OFFSET=16 初始预分配 16 个 PCROffset,减少 realloc。
PES 缓冲动态增长: 初始分配 8192 字节或 expected_size,按需翻倍增长 (g_realloc)。
8.5 性能优化总结
| 优化类型 | 关键机制 | 效果 |
|---|---|---|
| I/O 批量 | 100×packetsize 拉取 | 减少 pull_range 调用 |
| 时间戳查找 | PCR Group 二分搜索 + 线性插值 | O(N) → O(log N),精度高 |
| PID 查找 | pcrtablelut 直接映射 | O(1) |
| 重复检测 | Section 版本号+段号位图 | 避免重复处理 |
| 帧级跳过 | needs_keyframe + scan_function | Seek 时跳过非关键帧 |
| 内存安全 | 32MB PES 上限 + 512 窗口上限 + 动态增长 | 防止 OOM |
九、FFmpeg vs GStreamer TS/PCR 处理对比
FFmpeg (libavformat/mpegts.c) 和 GStreamer (gst/mpegtsdemux) 对 TS 的解析策略差异巨大,根本原因在于架构模型不同。
9.1 核心架构差异
GStreamer Pipeline 模型 :每个 buffer 必须携带 running_time,demux 是唯一同时拥有 PCR 时钟域和 GStreamer 时钟域的元素,因此时钟同步必须在 demux 中完成。
FFmpeg 传统模型:demuxer 只管解封装,PTS/DTS 原样传出,时钟同步由播放器框架(如 ffplay)在解码/渲染时完成。
9.2 PCR 处理对比
| 机制 | GStreamer tsdemux | FFmpeg mpegts |
|---|---|---|
| PCR 存储 | per-program MpegTSPCR,含窗口/skew |
per-filter tss->last_pcr,仅存最新值 |
| EPTLA/skew 算法 | 512 滑动窗口 + IIR (calculate_skew) |
无 |
| pts_to_ts 转换 | Push(skew) / Pull(PCR Group offset) 两套 | 无,PTS/DTS 原样传出 |
| 15s 异常拒绝 | 有 | 无 |
| PCR 回绕处理 | pcroffset += PCR_GST_MAX_VALUE |
无显式处理,依赖 int64 不溢出 |
| Pending buffer 机制 | 500ms 阈值 → ignore_pcr | 无 |
| out_time 倒退保护 | 有 | 无 |
| Teletext PTS 修正 | 走 pts_to_ts 通用路径 | PCR 直接钳位:DTS<PCR → 设为 PCR;DTS 超 PCR 130ms → 钳位 |
| SCTE35 时间戳 | 走 pts_to_ts 通用路径 | pkt->pts = last_pcr / 300,直接用 PCR |
FFmpeg 的 PCR 仅用于两处:Teletext PTS 修正和 SCTE35 时间戳打点。核心播放流程完全不依赖 PCR。
9.3 数据结构对比
| 维度 | GStreamer | FFmpeg |
|---|---|---|
| PID 过滤 | 哈希表/链表 | 扁平数组 pids[8192],O(1) 查找 |
| PCR 存储粒度 | per-program | per-filter(任意 PID 的 PCR 可直接查) |
| PES 缓冲 | 自行管理 | AVBufferPool 32 级 power-of-2 分配器 |
| Section 缓冲 | 动态分配 | MAX_SECTION_SIZE=4096 固定缓冲 |
9.4 容错策略对比
设计哲学 :GStreamer 偏向精确 ,FFmpeg 偏向容错。
| 场景 | GStreamer | FFmpeg |
|---|---|---|
| CC 失败 | 可能丢包或纠正 | 只打 AV_PKT_FLAG_CORRUPT,数据照传 |
| CRC 校验 | 严格校验或忽略 | 可信度计数器 crc_validity[],初始 100 分,CRC 错误减分,低于 -10 自动放行 |
| TEI 标志 | 处理后丢弃 | 打 AV_PKT_FLAG_CORRUPT,数据照传 |
| discontinuity | 立即重置 PES 状态、清空缓冲 | 只跳过 CC 检查,不做 PES/缓冲区重置,仅 seek 时才全局 flush |
| EOF | 无显式 flush | 扫描所有 PID,将未完成的 PES 作为最后一个包发出,避免丢尾 |
| CA/加扰 | 解析 CAT,暴露 CA 信息 | 完全不解析 CAT(PID 0x0001),无 CA section filter |
9.5 Seek 对比
| 维度 | GStreamer tsdemux | FFmpeg mpegts |
|---|---|---|
| 索引 | 无,PCR Group 线性插值 | av_add_index_entry() 建立关键帧索引 |
| 时间→偏移 | ts_to_offset() PCR Group 插值 |
mpegts_get_dts() 实际读帧建索引 |
| 关键帧扫描 | H.264 SPS+PPS+IDR (上游),H.265 I帧 | 不做,交给解码器 |
| 负速率 | 不支持 | 支持 |
| seek 后校验 | 迭代 PTS 搜索修正偏移 | 无校验 |
| PMT 版本变更 | 流重建 | merge_pmt_versions 按 stream_identifier 或 PMT 位置索引复用 AVStream |
9.6 其他差异
自动探测模式 :FFmpeg 有 auto_guess 模式------未注册 PID 出现 PUSI 时自动创建 PES 流,比 GStreamer 更激进。
包大小自适应 :FFmpeg 在 resync 时可动态切换包大小(188/192/204),通过 analyze() 对三种候选打分。GStreamer 包大小在初始化阶段确定,运行中一般不切换。
Opus/DOVI/ARIB:FFmpeg 有完整的 Opus extradata 构建、Dolby Vision 配置解析、ARIB 字幕识别等扩展描述符处理。GStreamer 也有类似处理但路径不同。
9.7 总结
| GStreamer tsdemux | FFmpeg mpegts | |
|---|---|---|
| 核心职责 | 解析 + 时钟同步 + seek | 仅解析 |
| 设计哲学 | 保证管线同步正确 | 尽量不丢数据 |
| PCR 角色 | 核心时间基准 | 辅助(teletext/SCTE35) |
| 复杂度 | 高(EPTLA/skew/pts_to_ts/PCR Group/pending buffer) | 低(存储即用) |
一句话总结:GStreamer 的 tsdemux 是"解析+同步"一体化的管线节点,FFmpeg 的 mpegts 是纯解析器------职责边界不同导致复杂度差异。GStreamer 的复杂是管线模型的必然代价,不是过度设计。
十、与 MKV/MP4 的设计取舍对比
不只是格式差异,更关键的是理解为什么这样设计:
| 维度 | TS | MKV | MP4 | 设计意图 |
|---|---|---|---|---|
| 包结构 | 固定 188B,有同步字节 | EBML 变长元素 | Box 变长结构 | TS 为容错传输设计(固定包+同步=丢包后快速恢复);MKV/MP4 为存储设计(变长=空间效率) |
| 容错 | 每包 CC 校验 + Section CRC | 无内置校验 | 无内置校验 | TS 面向不可靠信道;MKV/MP4 假设存储可靠 |
| 索引 | 无帧级索引,依赖 PCR | Cues 簇级索引 | stbl 帧级索引 | TS 为流式传输(无需索引);MP4 为随机访问(精确 seek);MKV 折中 |
| 时间基准 | PCR(27MHz) + PTS/DTS(90kHz) | TimecodeScale + Cluster TC | 每轨 timescale | TS 多层时间适配实时同步;MP4 每轨独立精度高;MKV 全局简单 |
| 多节目 | PAT/PMT 原生多节目 | 不支持 | 不支持 | TS 为广播复用设计(一路流多节目);MKV/MP4 为单节目设计 |
| Seek | PCR-偏移映射 + 线性搜索 | Cues 二分 + 簇内扫描 | stts/stsz 精确计算 | TS Seek 最慢(无索引);MP4 最快(帧级索引);MKV 中等 |
| 编码标识 | stream_type(1B) + descriptor | 字符串 CodecID | FourCC(4B) | TS 紧凑但歧义多(0x06);MKV 可扩展性好;MP4 需注册但解析快 |
| 加密 | CA_descriptor + ECM/EMM | WebM AES-CTR | cenc/sinf 多方案 | TS 广播DRM生态碎片化;MKV 简单但生态弱;MP4 DRM 生态成熟 |
| 帧级信息 | PES 内嵌,需解析编码头 | Block flags/keyframe 位 | stss/stbl 表 | TS 帧信息在编码层(需额外解析);MKV/MP4 在容器层(直接可读) |
| 适用场景 | 广播传输/HLS/蓝光 | 开源视频/多字幕 | 流媒体/设备兼容 | 各自面向核心场景优化 |
一句话总结设计哲学:TS 为"在不可靠信道上可靠传输"而设计------固定包+CC+CRC+PCR 提供强容错;MKV 为"开放灵活的容器"设计------字符串ID+变长结构提供高可扩展性;MP4 为"精确随机访问"设计------帧级索引+每轨 timescale 提供精确 Seek。