
简介这份资源是 ISO/IEC 13818-1:2019《信息技术——运动图像及其伴音信息的通用编码——第1部分系统》的完整英文版 PDF文件总数仅 1 个共 305 页压缩包约 20.93MB内容完整、可检索复制。标准面向数字电视、流媒体、音视频编解码及多媒体系统领域的工程师、研发人员和标准研究者是处理 MPEG-2 系统层、TS/PS 封装、节目流与传输流同步、时间戳与缓冲区管理等问题的权威依据。PDF 正文系统介绍了系统架构、编码器与解码器职责、音频和视频信号处理、同步与时序控制等核心规范同时说明该第七版取代第六版并纳入 AMD1且与 ITU-T H.222.0 保持对应关系进一步给出系统目标解码器、节目流和传输流的数据结构细节。资源还包含专利与商标权声明、术语和定义、附录等完整内容适合用于协议分析、标准对照、系统开发与学术研究。该标准在数字广播、IPTV、视频点播、蓝光光盘等场景中均有应用对从事相关协议实现或验证的读者尤为实用已有 328 人下载学习便于快速查阅和精读原文。1. 为什么 305 页的 ISO/IEC 13818-1 是音视频工程师绕不开的底稿ISO/IEC 13818-1:2019 第七版也就是 ITU-T H.222.0是整个 MPEG-2 Systems 层的完整定义——你平时遇到的 TS 流、PS 流、PES 包、PAT/PMT、PCR 同步、多节目复用权威出处全在这 305 页里。做流媒体封装、数字电视前端、播放器解复用、编码器复用器的人迟早要回头翻它。它不是讲视频压缩本身的而是讲压缩完的 ES 流怎么打包、怎么交织、怎么带上时间戳、怎么让接收端按序解码并保持音画同步。适合谁想搞懂 TS 流每个字节含义的开发者、被音画不同步折磨的播放器工程师、需要按标准实现复用/解复用的嵌入式同事以及准备做 DVB/ATSC/蓝光相关项目的团队。这篇我按自己读这份标准的实际路线拆开TS/PES 语法、PSI 节目信息、时间戳时钟模型最后附上排查技巧和验证手段。2. TS 流与 PES 层从 188 字节的包结构开始动手解析2.1 TS 包固定 188 字节先看头四个字节整个 MPEG-2 Systems 的系统层分两层外层是系统层内层是压缩层。系统层把视频 ES、音频 ES、字幕、私有数据等交织成一个流接收端再按 PID 把它们拆出来。TS 流Transport Stream和 PS 流Program Stream是两种封装格式PS 适合几乎无差错的环境比如光盘存储支持软件处理TS 更适合有差错的环境比如广播信道因为每个包独立可同步、可纠错、可切换节目。TS 包固定 188 字节是有意为之——正好匹配 ATM 信元承载和 DVB 的 RS 纠错块对齐。一个包的结构是 4 字节头 可选 adaptation field payload。头四个字节里最关键是这几位字段长度说明sync_byte8 bit固定 0x47用于接收端同步transport_error_indicator1 bit传输层置 1表示该包有错payload_unit_start_indicator1 bit置 1 表示 payload 起始是一个 PES 包或 PSI 段的第一个字节PID13 bit包的流标识0x0000 是 PAT0x0011 是 SDT 等0x1FFF 是空包transport_scrambling_control2 bit加扰指示adaptation_field_control2 bit01 只有 payload10 只有 adaptation field11 两者都有continuity_counter4 bit同 PID 的包计数接收端用于丢包检测新手最容易搞混的是 PID 和节目号的关系PID 只是传输层的复用标签节目号是 PSI 层的东西。两者通过 PAT/PMT 映射不是一回事。另一个容易忽略的是 continuity_counter 只有 4 bit模 16 递增如果 adaptation_field_control 为 00 是不允许的包会被接收端丢弃。2.2 PES 包ES 流进入系统层的入口压缩后的视频 ES 流不是直接塞进 TS payload 的而是先打成 PES 包。PES 头最关键的是 packet_start_code_prefix固定 0x000001和 stream_id。stream_id 决定这个 PES 承载的是视频0xE0~0xEF、音频0xC0~0xDF还是其他类型。PTS/DTS 字段也在 PES 头里视频一般 PTS 和 DTS 都写音频基本只写 PTS。PES 可长可短视频 PES 通常一帧一个包所以 PES_packet_length 对视频经常是 0表示长度不限定音频则有明确长度。写解析器的时候要注意PES 头后面有若干可选字段PTS 是 5 字节、DTS 是 5 字节且要处理 33 位时间戳分散存储的位操作。PES 头解析完剩下的就是原始 ES 数据可以直接喂给解码器。2.3 第一个 TS 解析脚本定位同步并提取 PES 头下面给一个我平时用来快速检查 TS 流的 Python 脚本骨架它能完成三件事同步定位、按 PID 分类统计、提取指定 PID 的 PES 头信息。import sys from collections import Counter TS_PACKET_SIZE 188 SYNC_BYTE 0x47 def find_sync(data, start0): # 连续找到 3 个 0x47 才认为同步成功避免 payload 里的 0x47 干扰 count 0 i start while i len(data) - TS_PACKET_SIZE * 3: if data[i] SYNC_BYTE: ok True for j in range(1, 4): if data[i j * TS_PACKET_SIZE] ! SYNC_BYTE: ok False break if ok: return i i 1 return -1 def parse_packet(pkt): # 4 字节头解析 sync pkt[0] tei (pkt[1] 7) 0x01 pusi (pkt[1] 6) 0x01 pid ((pkt[1] 0x1F) 8) | pkt[2] afc (pkt[3] 4) 0x03 cc pkt[3] 0x0F return sync, tei, pusi, pid, afc, cc def main(path, target_pid): with open(path, rb) as f: data f.read() sync_pos find_sync(data) if sync_pos 0: print(未找到有效的 TS 同步) return print(f同步偏移: {sync_pos}) pkt_count (len(data) - sync_pos) // TS_PACKET_SIZE pid_counter Counter() last_cc {} loss_count 0 for i in range(pkt_count): offset sync_pos i * TS_PACKET_SIZE pkt data[offset:offset TS_PACKET_SIZE] sync, tei, pusi, pid, afc, cc parse_packet(pkt) if sync ! SYNC_BYTE: continue pid_counter[pid] 1 if pid in last_cc: if (last_cc[pid] 1) % 16 ! cc: loss_count 1 last_cc[pid] cc if pid target_pid and pusi: # 从 payload 解析 PES 头 payload_offset 4 if afc 0x03 or afc 0x02: af_len pkt[4] payload_offset 1 af_len if pkt[payload_offset:payload_offset 3] b\x00\x00\x01: stream_id pkt[payload_offset 3] pes_len (pkt[payload_offset 4] 8) | pkt[payload_offset 5] flags pkt[payload_offset 7] pts_dts_flag (flags 6) 0x03 print(fPID 0x{pid:04X} PES stream_id0x{stream_id:02X} flen{pes_len} pts_dts_flag{pts_dts_flag}) print(f总包数: {pkt_count}) print(fPID 分布 (前10): {pid_counter.most_common(10)}) if loss_count: print(fcontinuity_counter 跳变次数: {loss_count}) if __name__ __main__: main(sys.argv[1], int(sys.argv[2], 16))逻辑说明find_sync 函数要求连续 3 个包都落在 188 字节间隔的 0x47 上才确认同步偏移这一步是为了把 payload 里的伪 0x47 过滤掉实际产品里这个同步逻辑会更严格大部分硬件解复用器也是这样实现的。parse_packet 把 4 字节头的各个字段拆开重点提取 PID 和 adaptation_field_control。continuity_counter 连续性检查用于统计丢包这在分析劣化信道的录制文件时很有用。最后针对目标 PID在 payload_unit_start_indicator 为 1 的包上尝试解析 PES 头。参数说明target_pid 参数用十六进制传入比如看视频流传 0x1011看音频流传对应的 PMT 里查到的 PID。afc 为 0x02 或 0x03 时需先跳过 adaptation fieldadaptation_field_length 字节本身不算在负载长度里这个细节容易漏。PES 头的 flags 字节第 6、7 位是 PTS_DTS_flags如果是 0b10 就是只有 PTS0b11 是 PTSDTS。3. PSI 与描述子PAT 和 PMT 怎么把节目带出来3.1 四张基础表的职责分配TS 流承载的是一堆 PID 的包接收端怎么知道哪个 PID 是视频、哪个是音频、哪个属于哪个节目靠 PSIProgram Specific Information。标准里定义了 PAT节目关联表table_id0x00、CAT条件接收表table_id0x01、PMT节目映射表table_id0x02、TSDT传输流描述表table_id0x03。PAT 固定走 PID 0x0000这是写死的接收端开机先找它。PAT 的作用是列出这个 TS 流里所有节目号以及每个节目对应的 PMT PID。PMT 再针对每个节目列出该节目包含的流视频流 PID、音频流 PID、stream_type0x01 是 MPEG-1 视频0x02 是 MPEG-2 视频0x1B 是 H.2640x24 是 HEVC以及各种描述子。所以解析路径是先读 PID 0x0000 拿到 PAT找到目标 program_number 对应的 PMT PID再读 PMT 拿到音视频 PID。一个常见的理解误区PID 0x0000 是 PAT但 PMT 的 PID 不是固定值由 PAT 动态指定。CAT 里的 EMM 流和 CA_descriptor 是条件接收用的免费流通常没有 CAT。TSDT 在单节目流里经常不出现多节目流里用来描述整个 TS 的全局信息。3.2 段section语法与 CRC32 校验PSI 数据按段传输。一个段有固定的头结构table_id 占 1 字节section_length 占 12 bit 且只算从 section_number 到 CRC 之前的字节数。段可以跨多个 TS 包这就是为什么 payload_unit_start_indicator 为 1 的包表示新段开始。标准要求 PAT 和 PMT 每秒至少重复发送几次这是为了接收端能快速捕捉也是测试 checklist 里的常驻项。段尾还有 4 字节 CRC32用的是 MPEG-2 定义的 CRC32 多项式不是 zlib 的那个。解析器必须校验 CRC否则一个位错误可能让你拿到一份错误的节目映射。很多开源播放器为了性能会跳过 CRC 校验但做测试仪器或专业解码器的人不会省这一步。3.3 一个 PAT/PMT 解析流程示例这里给出一个不依赖第三方库的轻量解析流程直接用上一章的 TS 包解析函数往里接def parse_section(data, start): # 输入: TS payload 起始位置; 输出: section 或 None table_id data[start] section_length ((data[start 1] 0x0F) 8) | data[start 2] section_data data[start:start 3 section_length] if len(section_data) 3 section_length: return None # 这里可以做 CRC32 校验略 return section_data def parse_pat(section): # PAT: 从第 8 字节起是 program_number PMT PID 对 tsid (section[3] 8) | section[4] entries [] pos 8 while pos 4 len(section) - 4: program_number (section[pos] 8) | section[pos 1] pmt_pid ((section[pos 2] 0x1F) 8) | section[pos 3] entries.append((program_number, pmt_pid)) pos 4 return tsid, entries def parse_pmt(section): # PMT: 第 7 字节低 3 位 第 8 字节是 PCR PID pcr_pid ((section[6] 0x07) 8) | section[7] program_info_length ((section[8] 0x0F) 8) | section[9] streams [] pos 10 program_info_length while pos 5 len(section) - 4: stream_type section[pos] pid ((section[pos 1] 0x1F) 8) | section[pos 2] es_info_length ((section[pos 3] 0x0F) 8) | section[pos 4] streams.append((stream_type, pid)) pos 5 es_info_length return pcr_pid, streams逻辑说明parse_section 拿到的 section_length 不包含 table_id 和 length 字段本身这是新手容易数错偏移的根源。PAT 解析的循环条件是 pos 4 len(section) - 4最后 4 字节是 CRC不能把 CRC 当节目条目。PMT 解析要注意 PCR PID 字段的位置在 section 的固定偏移处program_info_length 之后才是流条目循环。参数说明如果 PAT 里出现 program_number 为 0x0000 的条目它是 network PIDNIT 的 PID不是节目。PMT 里 stream_type 的完整列表要对照标准 Table 2-34不同版本支持范围不一样第七版把 HEVC、JPEG 2000 等新类型都纳入了。解析到的是 13 位 PID和 TS 头的 PID 字段位宽一致不需要额外处理。3.4 描述子descriptor_tag 决定一切描述子挂在 PMT 的 program_info 或 ES_info 里结构是 descriptor_tag1 字节 descriptor_length1 字节 data。tag 0x05 是 registration descriptor里面 4 字节是格式标识符tag 0x0A 是 video stream descriptor0x0B 是 audio stream descriptor0x28 是 HEVC descriptor。私有描述子 tag 从 0x40 到 0xFF 可以由厂商自定义遇到不认识的就跳过 length 字节继续解析下一个这是标准解析器的通用安全策略。实际项目里描述子信息往往比 stream_type 更有用AC-3 音频在 stream_type 里是 0x81私有但靠 AC-3 descriptortag 0x6A才能拿到声道数、sample rate code 这些细节。DVB 和 ATSC 各自扩展了一堆描述子ISO 13818-1 只定义基础部分具体广播标准再往上加。4. 时间同步模型PCR、PTS、DTS 与 27 MHz 时钟逻辑4.1 为什么是 27 MHzSTC 与分频关系标准里对定时系统的核心是节目时钟编码端有一个 27 MHz 的系统时钟称为 STCSystem Time Clock。这个 27 MHz 不是随便选的它是 90 kHz 的 300 倍而 90 kHz 的分辨率对应约 11.1 微秒足够音视频帧级的同步判断。PCR 在 TS 层的 adaptation field 里传输PTS/DTS 在 PES 头里传输三者都溯源到同一个 STC。PCR 是 33 位 base90 kHz加 9 位 extension27 MHz完整值算出来是 base * 300 extension。接收端用 PCR 恢复本地时钟PTS/DTS 则用于在正确时刻呈现解码后的帧。如果 PCR 断了或跳变超过容限接收端的锁相环会失锁表现为音画抖动或缓冲异常。4.2 时间戳字段的位级布局PTS 和 DTS 是 33 位但在 PES 头里各占 5 字节33 位被拆成 3 段存放。很多解析翻车就在这不是简单的按字节大端读出。正确的拼法是取 5 字节第一位是固定前缀 0011PTS或 0001DTS有效位是 bits[32:30]3 bit、bits[29:15]15 bit、bits[14:0]15 bit。时间戳差值的意义也要说清楚PTS 表示该帧的呈现时间DTS 表示解码时间。B 帧因为重排序解码顺序和显示顺序不同所以 PTS 和 DTS 会不一样I 帧和 P 帧通常相等。对音频来说解码即呈现一般只有 PTS。4.3 PCR 和 PTS 的约束间隔、回绕与缓冲模型标准对 PCR 插入间隔的要求是两个 PCR 之间的时间差不大于 0.1 秒。超过这个值接收端时钟恢复精度就会劣化。PTS 的间隔要求更宽松但同一节目内不能长时间没有 PTS否则播放器无法确定呈现时刻。33 位时间戳在 90 kHz 下约 26.5 小时回绕一次处理回绕要按模运算比较差值不能用普通减法。STDSystem Target Decoder缓冲模型规定解码器有传输缓冲、复用缓冲和解码缓冲PTS/DTS 与 PCR 的差值必须让数据流的到达和取出满足这些缓冲不溢出、不下溢。实际调试音画不同步多数问题出在复用器没有按 STD 模型计算而不是时间戳本身错了。4.4 时间戳计算与校验的一个代码片段def pts_from_header(buf, pos): # buf[pos:pos5] 是 5 字节 PTS 字段按标准位布局拼接 b0 buf[pos] b1 buf[pos 1] b2 buf[pos 2] b3 buf[pos 3] b4 buf[pos 4] pts ((b0 0x0E) 29) | (b1 22) | ((b2 0xFE) 14) | (b3 7) | (b4 1) return pts def pts_delta(pts1, pts2): # 模 2^33 的差值处理回绕 return (pts2 - pts1) 0x1FFFFFFFF逻辑说明第一个函数把 5 字节压成 33 位。b0 的低 1 位和 b2 的低 1 位是固定标志位必须忽略只取有效位。这种位操作方式在标准第 2.4.3.7 节有精确描述做完后用已知样本验证一次就不会再错。第二个函数做回绕安全的差值计算掩码 0x1FFFFFFFF 正好是 33 位全 1保证差值为正。参数说明PTS 单位是 90 kHz tick算实际时间用差值除以 90000 得到秒。比如 PTS 差 4500 就是 0.05 秒正好一帧 PAL 视频间隔。PCR 的 27 MHz extension 要单独处理33 位 base 回绕周期约 26.5 小时在长录制的流里必须考虑。5. 实操常见问题与排查解析 MPEG-2 TS 流的现场坑5.1 同步丢失解析到一半全是乱码现象用前面那个脚本解析一个录制的 TS 文件前半段正常后半段所有包都解析不出来或者 PID 统计出现大量 0x1FFF。原因录制文件可能在某个位置出现了字节丢失或插入。188 字节的固定包长只要错一个字节后续所有包边界全部错位。另一个常见原因是录制工具在拼接文件时把两段流的中间直接接上没有处理两个流之间的垃圾数据。解决不要只定位一次同步就一路解析到底要周期性检查每个包的 sync_byte。遇到连续多个包不是 0x47 就重新调用 find_sync 再对齐。如果是拼接流的场景要在拼接点做一个独立检测一次同步的过程然后把两段流的 PID 统计分别输出对比。5.2 PCR 间隔超限播放器缓冲反复抖动现象自研复用器输出的 TS 流在机顶盒上播放时每隔几秒画面卡顿一次但用 PC 播放器看是好的。原因PC 播放器缓冲大、时钟恢复容忍度高把 PCR 间隔超标的问题掩盖了。机顶盒的时钟恢复更严格PCR 间隔超过 0.1 秒就会失锁。PCR 插入逻辑只做了每个视频帧插入一次但视频帧间隔在低帧率内容下可能超过 100ms。解决插入策略要按时间而不是按帧。常见做法是复用器维护一个 PCR 计数器每 50ms 强制在一个包的 adaptation field 里写入一次 PCR与视频帧边界无关。PCR 值取当前 STC 的实际时刻不能拿前一个包的时间戳硬填。5.3 音画不同步差一个固定值现象播放时音频比视频固定领先约 400ms所有节目都这样。原因复用器对音频 PES 和视频 PES 使用了不同的延迟补偿。编码器输出视频 ES 时有编码延迟音频 ES 基本没有复用器如果只给视频加了延迟而没有在音频 PTS 上也加同样的偏移就会出现固定差。解决在复用器里给音频 PTS 统一加上一个等于视频编码延迟的偏移量或者反过来把视频 PTS 减去编码延迟。具体做法是在复用器的 PES 封装阶段对每个流配置一个 pts_offset 参数调试时先用一个固定值跑通全链路再按帧精度微调。判断基准是同一时刻的 PCR 上音视频 PTS 应指向同一个呈现时刻。5.4 adaptation field 长度解析出错现象解析某些 PID 的包时stream_id 永远不对甚至从 payload 里读出的字节完全不是 PES 头。原因adaptation_field_control 为 0x03 时adaptation_field_length 是包含长度字节本身的。如果解析器把 payload 偏移算成 4 adaptation_field_length或者少算了那个长度字节就会多跳或少跳一个字节后面的 PES 头全部偏移。解决payload 偏移 4TS 头 1adaptation_field_length 字节本身 adaptation_field_length。另外 adaptation_field_length 为 0 时表示没有 adaptation field即使 afc 写的是 0x03 也要按 0 处理这类异常包在某些广播流里真的出现过。5.5 PMT 更新不及时现象节目中途切换了音频编码格式播放器还按旧的 PMT 搜音频 PID导致长时间无声。原因PMT 是按版本号递增来更新的接收端在检测到 version_number 变化后要重新解析 PMT。但有些复用器只改了 PMT 内容忘记递增 version_number接收端根本不会重新解析。解决排查时先用解析脚本连续抓取 PID 0x0000 的 PAT 和对应 PMT PID 的包对比版本号是否在上一次解析时增加了。标准要求 version_number 变化时用 current_next_indicator 配合处理改动后先置 current_next_indicator 为 0 发一版预告再发 current1 的新版本接收端才能平滑切换。6. 落地检查用脚本验证你解析的 TS 流有没有翻车前面几章把 TS 包、PSI、时间戳都拆过一遍最后给一个能当日常体检工具用的校验脚本。验证目标不是能不能播而是是否合规同步是否稳定、PCR 间隔是否超标、PTS 是否回绕处理正确、PAT/PMT 是否周期重复。def validate_ts(path): data open(path, rb).read() pos find_sync(data) if pos 0: return 同步失败文件可能不是 TS 流 pcr_values [] pts_values [] pid_set set() pcr_pid None pmt_pid None last_pcr None max_pcr_interval 0 while pos TS_PACKET_SIZE len(data): pkt data[pos:pos TS_PACKET_SIZE] sync, tei, pusi, pid, afc, cc parse_packet(pkt) pid_set.add(pid) # 检查 PAT if pid 0x0000 and pusi: # 简单找 PMT PID完整实现用 parse_pat pass # 检查 PCR if afc in (0x02, 0x03): af_len pkt[4] if af_len 0: flags5 pkt[5] if flags5 0x10: # PCR_flag pcr_base (pkt[6] 25) | (pkt[7] 17) | (pkt[8] 9) | (pkt[9] 1) | (pkt[10] 7) pcr_ext ((pkt[10] 0x01) 8) | pkt[11] pcr pcr_base * 300 pcr_ext if last_pcr is not None: interval (pcr - last_pcr) / 27000000.0 max_pcr_interval max(max_pcr_interval, interval) last_pcr pcr pos TS_PACKET_SIZE print(fPID 数量: {len(pid_set)}) print(f最大 PCR 间隔: {max_pcr_interval * 1000:.1f} ms) if max_pcr_interval 100: print(警告: PCR 间隔超过 100ms机顶盒可能出现时钟失锁) return 检查完成逻辑说明这个脚本抓两件事——PCR 的 27 MHz 值换算后的最大间隔以及 PID 分布。PCR_flag 在 adaptation field 的第 5 个字节从 TS 头算起第 5 字节的 bit 4为 1 时后面跟 6 字节 PCR 值。PCR base 33 位和 extension 9 位要按位拼接公式是 base * 300 ext转成秒要除以 27000000。最后把最大间隔和 100ms 阈值比较超了就是问题流。参数说明如果需要验证 PTS 回绕可以在这个基础上加一个 PES 头解析分支把相邻两个 PTS 做模 2^33 的差值正常差值应该是帧间隔的整数倍左右。PAT 版本号变化检测也可以加进去记录每次拿到的 version_number发现倒退或跳变就输出告警。把校验脚本固定成一个函数每次改完复用器代码或拿到新录制文件都强制跑一遍这是我处理 TS 流项目的固定习惯——标准书页不能告诉你某个具体流哪里坏了但这个脚本能希望帮到你。本文还有配套的精品资源点击获取