TS Demux 插件知识文档

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。

相关推荐
Json____17 天前
vue3-商城管理系统-前端静态网站
前端·vue3·ts·商城纯静态
Huanzhi_Lin1 个月前
关于V8/MajorGC/MinorGC——性能优化
javascript·性能优化·ts·js·v8·新生代·老生代
belldeep2 个月前
nodejs:Vite + Svelte + ts 入门示例
typescript·node.js·ts·vite·svelte
却道天凉_好个秋2 个月前
音视频学习(九十二):ts封装
音视频·ts
zh_xuan3 个月前
React Native Demo
android·javascript·react native·ts
我讲个笑话你可别哭啊4 个月前
鸿蒙ArkTS快速入门
前端·ts·arkts·鸿蒙·方舟开发框架
cui_win4 个月前
企业级中后台开源解决方案汇总
开源·vue3·ts
麷飞花4 个月前
TypeScript问题
前端·javascript·vscode·typescript·ts
千里马-horse5 个月前
Rect Native bridging 源码分析--AString.h
c++·ts·rn·jsi