简介:这份PDF为ISO/IEC 13818-1:2019《信息技术——运动图像及其伴音信息的通用编码 第1部分:系统》完整英文版标准原文,共305页,面向从事数字电视、流媒体传输、多媒体系统开发与标准研究的工程师、研究人员及高年级学生。标准系统阐述了MPEG-2系统层的核心规范,涵盖系统架构、编码格式、音视频信号处理、同步与时序控制等关键模块,并明确了编码器、解码器、系统控制器及存储组件间的交互与传输机制。凭借对时钟同步、节目复用及时戳等细节的权威定义,该标准是理解传输流(TS)与节目流(PS)格式、实现音视频高质量传输与播放不可或缺的基础参考资料。资源包含1个PDF文件,压缩包整体大小约20.93MB,便于收藏与查阅。已有326人获取学习,适合需要直接对照国际标准原文进行学术研究或工程落地的读者。
1. ISO/IEC 13818-1 到底管什么:一份不直接编码的编码标准
做流媒体开发的同行应该都有过这种时刻:拿着一条 DVB 或者 IPTV 的码流文件,播放器花屏、音画不同步、搜台只出黑屏,用 FFprobe 看半天只看到一堆 PID 和 table_id,说不清问题出在哪一层。这时候翻到 ISO/IEC 13818-1 这份标准——正确叫法是 Generic coding of moving pictures and associated audio information: Systems——你才会意识到,视频编码本身根本不归它管,它管的是更底层、也更折磨人的部分:怎么把视频、音频、字幕、私有数据打成包,怎么在一条流里交织传输,怎么用时间戳让它们解码时重新对齐。这份标准定义了 MPEG-2 Systems,也就是我们天天挂在嘴边的 TS 流和 PS 流的全部语法和语义。它适合三类人:写播放器或解码器内核的、做码流分析仪或采集网关的、以及被运营商抓去排查直播卡顿的。一个反直觉的结论先放在这:2019 年的第七版,和 1994 年发布的第一版相比,核心语法几乎没有变化——变的是它承载的内容对象,H.264、H.265、JPEG 2000、DASH 这些后来者,全都被它收编成了"允许承载的负载"。所以这份文档过期不失效,到今天它依然是数字电视、IPTV、安防录像系统里最通用的那条底线。
2. 为什么是 2019 第七版:版本脉络与 305 页阅读地图
2.1 第七版到底是什么:替换关系与增补内容
ISO/IEC 13818-1 的版本脉络看着乱,其实规律很明显。第一版 1994 年发布,对应 ITU-T H.222.0。此后每轮大改就是一次换版,中间夹着一堆 Amendment 和 Technical Corrigendum。2019 年这一版是第七版,它做了一件关键的事:直接取消并替换第六版(2018 版),并且在替换时把 2018 版之后的 Amendment 1:2018 的内容吸收了进来。换句话说,你手里如果拿着 2018 版再加一个补丁文件,那两份合的集合约等于这一份 305 页的第七版,但阅读体验完全不同——标准不是给你做补丁叠加用的,合订本才是。
第七版的正文开头就写明它由 ITU-T 起草,编号为 Rec. ITU-T H.222.0 (08/2018),在 ISO/IEC 这边则走 JTC 1/SC 29 的流程审批。所以你在很多工具和论文里会看到"H.222.0"和"13818-1"混用。引用标准时写哪一个都有理,但如果要精确到条款号,两边页码编排是完全一致的,这点不用纠结。
版本选择上我的建议很直接:能用第七版就别用旧版。原因不是新版多出了多少语法表,而是你在和别人对齐问题、截图报 bug 时,条款号对不上最浪费生命。旧版里那些 Amendment 的条款编号是插进去的,经常出现"2.6.5 后面跟着 2.6.5.1 和 2.6.5.2,然后跳到 2.6.6"这种跳变,而合订版已经把编号整理顺了。
2.2 305 页的阅读地图:先看哪、后看哪、可以跳哪
拿到一份 300 多页的英文标准,第一反应是硬啃,这其实是最差策略。13818-1 的结构是 Section 1 加 Section 2,Section 1 只有 Scope 和 Normative references,加起来三页不到。真正的主体在 Section 2,里面有定义、语法描述方法、然后是两大块硬骨头:2.4 Transport stream bitstream requirements 和 2.5 Program stream bitstream requirements。再往后是 2.6,Program and program element descriptors,这节是所有 descriptor 的字典,篇幅最大,但它不是读的,是查的。
我建议的阅读顺序是这样的:先把 2.4 的 TS 部分整体过一遍,重点看 TS 包的 188 字节布局、adaptation field 的语义和 PCR 的编码方式;然后跳到 2.7 Restrictions on the multiplexed stream semantics,这一节讲的是复用约束,比如同一节目内视频音频的 PTS 间隔限制,这是排查音画不同步的第一现场;接着看 2.11 Carriage of ISO/IEC 14496 data,也就是 TS 流里怎么封装 H.264/H.265,这是现代播放器开发绕不开的部分;最后才把 2.6 当作字典随查随用。
章节阅读优先级可以按下面这张表规划:
| 章节范围 | 内容主题 | 阅读策略 |
|---|---|---|
| Section 1 (1.1-1.2) | 范围与引用标准 | 通读,约 10 分钟 |
| 2.1-2.3 | 定义、缩写、语法记法 | 第一次可跳过,遇到再看 |
| 2.4 | TS 流全部语法语义 | 重点精读,配合实际码流验证 |
| 2.5 | PS 流全部语法语义 | 做 DVD/录制节目才需要细读 |
| 2.6 | descriptor 字典 | 按需查询,不建议通读 |
| 2.7 | 复用约束与语义限制 | 排查问题时优先翻这里 |
| 2.11-2.13 | 承载 H.264/H.265、metadata | 写播放器必读 |
注意 2.4 里有一个很容易被新手忽略的点:TS 包是固定 188 字节,但标准允许在特定条件下用 204 字节的变体(多出 16 字节 RS 校验),比如 DVB-C 里常见。这个在标准正文里写的是"transport stream packet may be extended",很多人在解析时默认 188,遇到 204 长度的文件直接解析错位,后文会专门讲这个坑。
3. PS/TS/PES 三件套:复用与同步的核心机制
3.1 Program Stream 与 Transport Stream 的选型逻辑
13818-1 的最核心决策就是定义了两条平行的复用流:Program Stream(节目流)和 Transport Stream(传输流)。两者的底层构建单元都是 PES 包,也就是 Packetized Elementary Stream 包,区别在于 PES 包之后怎么切、怎么封装。
Program Stream 的设计目标是"近乎无错误的环境",典型的场景是 DVD 光盘。它的特点是把一组共享同一时间基准的 PES 包按顺序组织成一个个 Pack,每个 Pack 以 Pack Start Code 开头,后面跟着系统头、PES 包、填充数据。PS 流适合软件处理和本地回放,因为它的结构是顺序流式的,随机访问靠导航包,不需要额外的时间刻度和节目表维护。PS 流里没有 PID 这个概念,识别流靠 Stream ID——0xE0 开头是视频、0xC0 开头是音频,这个设计简单到粗暴。
而 Transport Stream 是为"错误容易发生"的环境设计的,也就是有线、卫星、地面广播和 IP 网络。TS 流将 PES 包切成固定 188 字节的小包,每个小包带一个 PID,通过 PID 来区分它属于哪个节目、哪种流。更重要的是,TS 流天然支持多节目复用:多个节目可以共享一条 TS,每个节目有自己的节目时钟基准,接收端通过解码 PSI(Program Specific Information)表来发现节目。
选型逻辑一句话就能说清:做本地文件回放或 DVD 抓轨,处理的是 PS 流;做直播、广播、安防平台接入,处理的是 TS 流。现代从业者面对 99% 的场景都是 TS,PS 流通常只出现在老旧的采集设备和 DVD 相关的兼容需求里。标准里定了一条很实际的原则:TS 流即使出现丢包或误码,接收机也能凭借 PSI 表重新同步,而 PS 流一旦出错,恢复的成本高得多。
3.2 PES 包:TS 与 PS 共同的结构单元
PES 包是理解整个 13818-1 的钥匙。无论是 PS 还是 TS,压缩层的数据(也就是真正的 H.264 码流或 AAC 码流)都不会直接裸露在系统层里,而是先被打包成 PES 包,PES 包的负载部分才是编码后的音视频数据。PES 头的核心职责是承载时间信息。
PES 头里有四个关键字段值得记住:
| 字段 | 长度 | 作用 |
|---|---|---|
| stream_id | 8 bit | 标识流类型,视频通常 0xE0 |
| PTS | 33 bit | 解码后呈现时间戳,单位是 90 kHz |
| DTS | 33 bit | 解码时间戳,仅在 B 帧场景下出现 |
| PES_header_data_length | 8 bit | 后面可选字段的总长度 |
注意 PTS 计数器的单位是 90 kHz,也就是每个时间戳增量是 1/90000 秒,大约 11.1 微秒。PCR 的时间基准同样用 90 kHz 作为基础频率,但 PCR 是 42 bit 的计数器,前 33 位以 90 kHz 计,后 9 位以 27 MHz 计。这个 27 MHz 和 90 kHz 的比例关系(正好 300 倍)是很多时序问题的根源——后面排查音画不同步时会反复用到。
写解析器时最容易被骗的地方是 PTS 的 33 bit 会有回绕:2^33 / 90000 ≈ 26.5 小时,如果长时间播放一个直播流,时间戳跨过回绕点,直接用差值判断先后顺序会得出完全错误的结论。标准里对解码器有明确的约束,要求按回绕感知的方式处理 PTS 差值,但很多第三方解析工具并没有这么干。你能做的就是在自己的代码里预留这个逻辑,下文会给实现。
3.3 PSI 表:PAT、PMT 与节目的发现机制
TS 流的节目信息完全靠 PSI 表来承载。PSI 分四张表:PAT(Program Association Table)、PMT(Program Map Table)、CAT(Conditional Access Table)和 NIT 等扩展表。PAT 固定分配 PID 0x00,它的负载里列着当前 TS 内的所有节目编号和对应的 PMT PID。拿到 PAT 之后,根据节目号找到 PMT 的 PID,再读 PMT,PMT 里才会列出这个节目包含哪些流:视频、音频、字幕各在哪个 PID,以及 PCR PID 用哪一个。
这个三级跳是解析 TS 流最基本的路数。很多新手第一次解析时直接拿一个视频 PID 去抓包,却忽略了必须先经过 PAT 和 PMT 才能知道哪个 PID 是视频。更隐蔽的问题是:PMT 里的 PCR_PID 字段经常和视频 PID 不一致,有的复用器会把 PCR 单独放在一个空流 PID 上,或者放在音频 PID 上。如果解析器默认"PCR_PID = 视频 PID",遇到这种流就会在时钟恢复上踩坑。
PMT 的 table_id 固定是 0x02,section_length 字段指示这个 section 的有效长度。菜单位于 section 内的 program_info_length 之后,每个 es_info_length 又描述了每个流对应的 descriptor。descriptor 是标准里留给私有数据的栖息地,注册表项通过 registration_descriptor 的 format_identifier 区分归属。13818-1 里明确规定:descriptor 里可以带任意私有数据,但长度字段必须严格计算,否则整个 section 的解析都会错乱。这个"长度字段错一位,后面全错位"的特性,是码流解析翻车的第一大原因。
4. 把条款变成代码:从解析 PID 到调 PCR 参数
4.1 用 FFprobe 快速验证一条 TS 流的节目结构
标准在手,第一步不是写代码,是先验证手头这条码流的真实结构。FFprobe 内置了完整的 MPEG-TS demuxer 和对 PSI 表的解析能力,用它把 PAT/PMT 打出来是最快的摸底方式。下面这个命令直接列出所有节目和流:
ffprobe -v error -show_programs -show_streams -select_streams v:0 input.ts逻辑说明:-show_programs让 ffprobe 解析并输出 PSI 里的节目信息,-show_streams输出每个 PID 的流详情,-select_streams v:0只保留第一个视频流以减小输出噪音。跑完你会看到类似program_id=1、stream_index=0、codec_name=h264、time_base=1/90000的输出,这些信息直接对应 PMT 里的流条目。
参数说明:如果希望连 PCR PID 一起打出来,可以追加-show_entries program=pcr_pid:stream=pid,codec_name来精确控制输出字段。FFprobe 显示的时间基如果是 1/90000,说明 PTS 是按标准走的;如果显示 1/1000,说明码流里用了非标的时间戳频率,这种流在很多播放器里会出现进度条跳动的现象。
4.2 手写一个最小 PMT 解析片段
FFprobe 能帮你确认码流是好的,但毕竟是个黑匣子。真到要排查私有 descriptor 或者诊断复用器问题时,还得自己解析。下面是 PMT 解析的核心片段,只处理最基本的 section 头,但能跑通:
def parse_pmt(data: bytes) -> dict: if data[0] != 0x02: raise ValueError(f"not a PMT section, table_id={data[0]:#x}") # section_length 是标准字段,占 12 bit,从第 1 字节的低 4 位开始 section_length = ((data[1] & 0x0F) << 8) | data[2] # 该字段只包含从 program_number 开始的字节数 program_number = (data[3] << 8) | data[4] version_number = (data[5] >> 1) & 0x1F current_next = data[5] & 0x01 pcr_pid = ((data[8] & 0x1F) << 8) | data[9] program_info_length = ((data[10] & 0x0F) << 8) | data[11] program_info_start = 12 program_info_end = program_info_start + program_info_length # program_info 里是节目级 descriptor,跳过 idx = program_info_end streams = [] while idx + 4 < len(data) and idx - 3 < section_length: stream_type = data[idx] elementary_pid = ((data[idx + 1] & 0x1F) << 8) | data[idx + 2] es_info_length = ((data[idx + 3] & 0x0F) << 8) | data[idx + 4] streams.append({ "stream_type": stream_type, "pid": elementary_pid, "es_info_length": es_info_length, }) idx += 5 + es_info_length return { "program_number": program_number, "version": version_number, "current_next": current_next, "pcr_pid": pcr_pid, "streams": streams, }逻辑说明:PMT 的 section 布局是固定的——table_id 占一字节,section_length 占两字节但只有 12 bit 有效,program_number 两字节,接着是 version、current_next、section_number 等字段,然后第 8、9 字节才是 PCR_PID。注意 PCR_PID 的取值可能和第一个视频流的 PID 不同,这个字段是独立的。解析时把所有流条目循环读完,每个条目都是 5 字节头加一段 ES_info 描述符,ES_info 的长度由 es_info_length 给出。整个循环的终止条件是位置越过 section_length 的边界。
参数说明:section_length的掩码处理是这里最容易错的地方——它的高 4 位其实在 data[1] 的低 4 位里,所以必须data[1] & 0x0F再左移 8 位。如果你直接用data[1] << 8,会把保留位的值也带进来,轻则解析错位,重则整个 section 长度直接翻倍。另外version_number是 5 bit 的,它在加 1 到 32 时会回绕成 0,做 PSI 缓存更新时一定要按回绕处理,不然 32 次更新之后你的缓存就永远认为版本没变过。
4.3 stream_type 到编码格式的映射表
解析 PMT 后拿到的 stream_type 是数字,必须映射成编码格式才知道这个流是 H.264 还是 AAC。13818-1 沿用了 13818-1 里注册的 stream_type 值,日常开发最常碰到的几个:
| stream_type | 含义 |
|---|---|
| 0x01 | MPEG-1 视频 |
| 0x02 | MPEG-2 视频 |
| 0x0F | AAC 音频(ADTS) |
| 0x1B | H.264 / AVC 视频 |
| 0x24 | HEVC / H.265 视频 |
| 0x06 | 私有数据(PES 里带私有头) |
| 0x81-0xFF | 用户私有,需结合 registration descriptor |
这里有个非常关键的坑:stream_type 0x06 代表私有数据,但很多复用器把字幕、EPG、甚至加密后的音视频都藏在 0x06 里。你光看 stream_type 是分不出来里面是什么的,必须再检查 PMT 里对应流条目有没有 registration_descriptor,它的 format_identifier 才能告诉你这路流到底属于谁。13818-1 的 2.6 章节里定义了 registration_descriptor 的完整语法,这也是我在前面说 descriptor 是字典的原因——它不是用来读的,是遇到了问题再去查的。
5. 踩坑实录:TS 流开发最常翻车的五个现场
5.1 PCR 不连续导致解码器反复重启
现象:播放一条自建的 TS 流,画面每隔十几秒卡一下,日志里反复出现"buffer underflow"或者"resync"。
原因:PCR 是解码器的时钟基准,它的值应当严格单调递增,增量与真实时间成正比。如果复用过程中 PCR 被重写、或者两个节目拼接时没有重新生成 PCR,解码器会认为时钟发生了跳变,于是重新对齐缓冲,表现出来就是周期性卡顿。
解决:用 FFprobe 验证 PCR 连续性。跑一条命令看一眼 PCR 增量是否均匀:
ffprobe -v error -show_entries packet=pts_time,flags input.ts | head -50如果发现 PCR 增量有大幅跳变,直接用 TS 复用工具(如 mpegtsmux 或自研打包器)对整条流重新生成一次 PCR,不要试图手工修,手工修 42 bit 计数器很容易因为进位处理出错埋下更大的雷。
5.2 把 PCR_PID 默认当成视频 PID
现象:解码器报"no valid PCR",画面有但声音不同步,唇形对不上。
原因:PMT 里的 PCR_PID 字段独立存在,并不保证等于视频 PID。有的复用器为了让音频解码优先建立时钟,把 PCR 放在音频流上;更有甚者单独开一路空流专门传 PCR。解析器如果写死"PCR = 第一个视频 PID",在遇到合法但不常规的码流时就直接失效。
解决:按标准走完整流程——先读 PAT,再读 PMT,取出 PCR_PID 字段后把它单独保存。不要用任何默认值兜底。在解析器里,PCR_PID 必须在拿到 PMT 之后显式赋值,如果值为 0x1FFF 表示无 PCR,这时候要触发告警而不是静默忽略。
5.3 加密流里 0x06 私有数据全是密文,拿不到 PTS
现象:抓包分析一个加密节目,在 PMT 里看到视频和音频 PID 都是 0x06,负载全是不可读的二进制。
原因:这就是典型的 CSA 加密 TS。加密发生在 PES 层,PES 头里的 PTS/DTS 也会被加密或置零,只有 PSI 表是明文。外部工具看不出任何流结构。
解决:不要试图解析密文,直接从 PMT 读取 PCR_PID 和加密标志,确认是否包含 CA_descriptor。如果要做分析,需要先接入解密模块得到明文再解析。这条坑的真正价值在于提醒你:看到 0x06 别急着当私有数据跳过,先查 CA descriptor。
5.4 188 字节对齐假设失效
现象:某些文件解析到一半开始全部错位,table_id 出现大量乱值。
原因:TS 流在部分传输系统里会扩展成 204 字节,多出的 16 字节是 RS 纠错码,位于包尾。如果你的读取逻辑固定按 188 字节步进扫描,遇到这种流会把 204 字节包里的校验尾巴当成下一个包的开头,导致从某个位置开始连续解析错误。
解决:读取时不要硬编码 188。标准允许的合法包长有 188、204 等种。稳妥做法是先读取前 4 个字节,检查 sync_byte 0x47 是否对齐;如果第一个包的 0x47 在偏移 188、204 处都能找到,按最小公倍数去判断实际包长。这也是为什么很多老工程师反对用"固定步长快进"的方式扫流的原因。
5.5 节目名编码用了 UTF-8 导致 EPG 乱码
现象:EPG 里节目名显示成一片乱码,英文正常,中文全是问号。
原因:DVB 标准里节目名字符串默认用 UTF-8 编码,但很多老式复用器实际输出的是 ISO-8859-1 或者 GBK。NIT/SDT 表里的字符编码是跟着表里的 encoding_type 走的,解析器不检查这个字段直接按 UTF-8 解码就会出现乱码。
解决:解析 SDT 的 service name 之前,先读旁边字节的 encoding 标志;如果标记不是 UTF-8,回退到自定义字符集转换。遇到这种码流不要改解析器默认编码,而是老老实实把原字节打出来看一眼再定策略——这是标准的兼容性要求,不是解析器的 bug。
6. 进阶验证:不写解析器也能做的合规性自查
6.1 四步命令行自检套路
有没有解析器源码,都能用现成工具做一次 TS 合规性体检。我长期用这套四步检查法,跑完基本能定位 80% 的复用层问题。
第一步,检查 PAT 和 PMT 是否齐全:
ffprobe -v error -show_programs input.ts 2>&1 | grep -E "program_id|pcr_pid"这一步确认码流里有节目信息表,且 PCR_PID 存在且合法。注意 PCR_PID 不能是 0x1FFF,否则解码器无法恢复时钟。
第二步,检查 PID 的流类型是否匹配实际负载:
ffprobe -v error -show_streams input.ts 2>&1 | grep -E "codec_name|profile|level"这里会输出每个流实际解码出来的编码格式。如果 PMT 里标的是 H.264,实际 codec_name 却是 mpeg2video,说明复用器在 PMT 里写了错误的 stream_type,是典型的复用错误。
第三步,检查时间戳回绕逻辑(这一步可以写成脚本):
ffprobe -v error -show_entries packet=pts_time,flags -of csv input.ts \ | awk -F',' 'NR>1 { if ($1 != "N/A") { diff = $1 - prev; if (diff < -90000) print "wrap or jump", $1, prev } prev = $1 }'这里的 awk 逻辑是:如果相邻 PTS 差值为负且超过 90000(1 秒),说明要么发生了回绕要么 PTS 跳变。正常直播流的 PTS 差值应当恒为非负小值。
第四步,验证音频和视频 PTS 是否交织:
ffprobe -v error -show_entries packet=pts_time,stream_index -of csv input.ts | awk -F',' '{print $2, $3}' | sort -k2 -n | head -30音频和视频的 PTS 应该交错上升,不能出现所有视频 PTS 在前、所有音频 PTS 在后的情况。如果是后者,播放器会一直缓冲音频直到视频追上,表现出来就是启动慢。
6.2 排查流程之外的三个习惯
第一,解复用器里的所有长度字段都按"读到的值 + 已消费的字节"双重校验,防止错位继续蔓延。第二,对 PCR 的增量做统计而不是只看瞬时值,均值和方差才是判断时钟稳定性的依据。第三,所有版本比较逻辑都按 5 bit 或 33 bit 回绕处理,不要直接比较大小。
说个我自己的习惯:从那以后,我每次拿到一条来源不明的 TS 流,做的第一件事永远是先跑一遍上面这套四步,把 PAT/PMT/时间戳的截图存下来再动手改代码。很多玄学 bug,其实都是码流复用参数不干净导致的,早五分钟验证,能省下后面一整个下午的解码器调试。希望帮到你——毕竟 MEPG 2 系统层是条老河,但年年都有人掉进同一个坑里。
本文还有配套的精品资源,点击获取