MPEG-TS 封装格式
参考:
- ISO/IEC 13818-1
- https://www.cnblogs.com/jimodetiantang/p/9140808.html
- https://blog.csdn.net/leek5533/article/details/104993932/
分析工具:
- https://easyice.cn/ (easyice)
1. 概述
本篇文章主要记录自己对于 mpeg-ts 流媒体封装标准的理解。
可以使用如下 ffmpeg 命令生成 .m3u8 和 ts 分片:
ffmpeg -re -i test.mp4 -c copy -f hls -hls_list_size 0 -bsf:v h264_mp4toannexb test.m3u8
-hls_list_size 0,用于将所有 .ts 分片都记录在 .m3u8 文件中
-bsf:v h264_mp4toannexb,用于将 mp4 avcc 码流格式转为 annex-b 码流格式
2. mpeg-ts 结构概览
一个 .ts 文件(ts 流)实际上由多个 ts packet 组成,每个 ts packet 大小固定为 188 Bytes。
ts stream:
+-+-+-+-+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| TS | = | Packet 1 | Packet 2 | Packet 3 | ... | Packet n-1| Packet n |
+-+-+-+-+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
ts 流可以抽象成由 ts 层(Transport Stream layer)、pes 层(Packet Elemental Stream layer) 和 es 层(Elementary Stream layer) 组成:
- es 层,由原始音视频流(elementary stream)组成,每个 es packet 包含完整的一个视频或音频帧
- pes 层,在 es 层的基础上加上必要的头信息,形成 pes packet
- ts 层,对 pes packet 按 188 Bytes 的大小限制进行拆分,然后加上必要的头信息;同时,除了 pes 类型的 payload,还有其它类型的 packet payload(如 PSI、SDT 等)
3. ts packet
188 Bytes 的 ts packet 由 ts header、payload 以及可选的 adaptation field 组成:
4 byte x byte 184-x byte
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ts header | adaptation field | payload |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
3.1 ts header
字段 | 类型 | 描述 |
---|---|---|
sync_byte | 8b | 同步字节,固定为 0x47 |
transport_error_indicator | 1b | 传输错误指示符,一般为 0 |
payload_unit_start_indicator | 1b | 拆分负载单元时的起始标示符 |
transport_priority | 1b | 传输优先级,一般为 0(低优先级) |
pid | 13b | pid 值,不同负载唯一标识 |
transport_scrambling_control | 2b | 传输加扰控制,00 表示未加密 |
adaptation_field_control | 2b | 是否包含 adaptaion field: 00 保留 01 无自适应域,仅含有效负载 10 仅含自适应域,无有效负载 11 同时带有自适应域和有效负载 |
continuity_counter | 4b | 拆分负载单元时的递增计数器,起始值不一定取 0,但必须是连续的 |
3.2 ts payload 拆分
由于 ts packet 188 Bytes 大小的限制,所以一个需要对 pes payload 进行拆分,而 ts header 中 payload_unit_start_indicator 标志位用于表示当前的 ts packet 是拆分后的第一个 packet。
同时 ts header 中 continuity_counter 计数器用于对拆分包进行计数,使得接收端能够正确组合出 pes packet。
一般其它类型的 ts payload packet 不会超过 188 Bytes 的大小限制。
3.3 pid
pid 的出现是为了支持 ts 文件中包含多个流。在 flv 文件格式中,只能包含一个音频和一个视频流,而 ts 文件可以通过 pid 支持多个音频和视频流。
pid 取值有如下规定:
取值 | 描述 |
---|---|
0x0000 | 节目关联表(program association table, PAT) |
0x0001 | 条件访问表(conditional access table, CAT) |
0x0002 | 传送流描述表(transport stream description table, TSDT) |
0x0003~0x000F | 保留 |
0x0010~0x1FFE | 自由分配 |
0x1FFF | 空包 |
如上,PAT 的 pid 是固定的,且 PAT 一般会出现在 .ts 文件开头。解析器首先需要根据 PAT 的 pid 找到 PAT,然后根据 PAT 中的信息得到 PMT 的 pid。关于 PAT、PMT 在后面进行论述。
3.4 adaptation field
adaptation field 有两个作用:
- 给 188 Bytes 的 ts packet 做填充
- 携带 PCR 外部时钟
adaptation field 结构如下:
字段 | 类型 | 描述 |
---|---|---|
adaptation_field_length | 8b | 自适应域长度 |
discontinuity_indicator | 1b | 一般为 0 |
random_access_indicator | 1b | 一般为 0 |
elementary_stream_priority_indicator | 1b | 一般为 0 |
PCR_flag | 1b | 携带 pcr 时置 1 |
OPCR_flag | 1b | 一般为 0 |
splicing_point_flag | 1b | 一般为 0 |
transport_private_data_flag | 1b | 一般为 0 |
adaptation_field_extension_flag | 1b | 一般为 0 |
PCR | 40b | 当 PCR_flag=1 时携带,Program Clock Reference,节目时钟参考 |
stuffing_bytes | 不定大小 | 填充字节,取值0xff |
但是针对不同的 ts payload,adaptation field 的应用方式也不同:
- 针对 PAT、PMT,不足 188 Bytes 的部分直接使用 0xff 进行填充,而不会使用 adaptation field(但是也有例外,有的编码器会携带)
- 针对 PES packet,才会使用 adaptation field 做填充
- audio PES packet 不会在 adaptation field 中携带 PCR 字段
- video PES packet 可以选择是否在 adaptation field 中携带 PCR 字段(一般都会携带)
一般 PES packet 被拆分时,会在第一个和最后一个拆分包添加 adaptation field:
PAT/PMT packet:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ts header | PAT/PMT | Stuffing Bytss |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
PES packet:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ts header | adaptation field | payload(pes 1) |-->第1个Packet
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ts header | payload(pes 2) |-->第2个Packet
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ts header | ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ts header | payload(pes n-1) |-->第n-1个Packet
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ts header | adaptation field | payload(pes n) |-->第n个Packet
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
其中:
- 第一个拆分包的 adaptation field 才会携带 pcr 时钟,且主要作用是为了携带 pcr 时钟,而不是为了填充数据
- 最后一个拆分包的 adaptation field 不会携带 pcr 时钟,只做填充用
- 参考 srs/trunk/srs/kernel/srs-kernel_ts.cpp::SrsTsContext::encode_pes() 函数
这里有一个小小的问题需要注意一下,即 adaptation field 最少占用 1 个字节,即 adaptation_field_length 字段,这样最少可以填充 1 个字节。
4. PSI(Program specific information,节目特定信息)
PSI 是多种表的合称,其有如下不同种类的表:
结构名 | PID 值 | 描述 |
---|---|---|
PAT(Program Association Table,节目关联表) | 0x0000 | 解析 .ts 文件的第一步就是找到 PAT 表,然后获取 PMT 表的 PID 值 |
PMT(Program Map Table,节目映射表) | 在 PAT 中给出 | 根据 PMT 表找到 audio pes packet 和 video pes packet 的 PID 值 |
CAT(Conditional Access Table,条件接收表) | 0x0001 | 可忽略 |
NIT(Program Association Table,网络信息表) | PAT 中给出 | 可忽略 |
4.1 PAT 表
字段 | 类型 | 描述 |
---|---|---|
table_id | 8b | 固定为0x00 |
section_syntax_indicator | 1b | 固定为1 |
zero | 1b | 固定为0 |
reserved | 2b | 固定为11 |
section_length | 12b | 后面数据的长度 |
transport_stream_id | 16b | 传输流ID,固定为0x0001 |
reserved | 2b | 固定为11 |
version_number | 5b | 版本号,固定为00000,如果PAT有变化则版本号加1 |
current_next_indicator | 1b | 固定为1,表示这个PAT表可以用,如果为0则要等待下一个PAT表 |
section_number | 8b | 固定为0x00 |
last_section_number | 8b | 固定为0x00 |
开始循环 | ||
program_number | 16b | 节目号为0x0000时表示这是NIT,节目号为0x0001时,表示这是PMT |
reserved | 3b | 固定为111 |
PID | 13b | 节目号对应内容的PID值 |
结束循环 | ||
CRC32 | 32b | 前面数据的CRC32校验码 |
4.2 PMT 表
字段 | 类型 | 描述 |
---|---|---|
table_id | 8b | PMT表取值随意,0x02 |
section_syntax_indicator | 1b | 固定为1 |
zero | 1b | 固定为0 |
reserved | 2b | 固定为11 |
section_length | 12b | 后面数据的长度 |
program_number | 16b | 频道号码,表示当前的PMT关联到的频道,取值0x0001 |
reserved | 2b | 固定为11 |
version_number | 5b | 版本号,固定为00000,如果 PAT 有变化则版本号加1 |
current_next_indicator | 1b | 固定为1 |
section_number | 8b | 固定为0x00 |
last_section_number | 8b | 固定为0x00 |
reserved | 3b | 固定为111 |
PCR_PID | 13b | PCR(节目参考时钟)所在TS分组的PID,指定为视频 PID |
reserved | 4b | 固定为1111 |
program_info_length | 12b | 节目描述信息,指定为0x000表示没有 |
开始循环 | ||
stream_type | 8b | 流类型,标志是 video 还是 audio 还是其他数据,h.264 编码对应 0x1b,aac 编码对应 0x0f,mp3 编码对应 0x03 |
reserved | 3b | 固定为111 |
elementary_PID | 13b | 与 stream_type 对应的 PID |
reserved | 4b | 固定为1111 |
ES_info_length | 12b | 描述信息,指定为0x000表示没有 |
结束循环 | ||
CRC32 | 32b | 前面数据的CRC32校验码 |
5. PES(Packet Elemental Stream,基本流打包)
每个 pes packet 都包含一个完整的视频帧或音频帧,pes packet 结构如下:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| pes header| optional pes header | pes payload |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
6 Byte 3~259 Byte max 65526 Byte
5.1 pes header
6 Bytes 固定的 pes header 结构如下:
字段 | 类型 | 描述 |
---|---|---|
packet_start_code_prefix | 24b | 该字段联合下面的 stream_id 构成了一个 packet 的起始码,指示 packet 的开始,固定 0x000001 |
stream_id | 8b | PES 包中的负载流类型。一般视频为 0xe0,音频为 0xc0 |
PES_packet_length | 16b | PES 包长度,包括此字段后的可选包头和负载的长度 |
optionl pes header 中与 pts、dts 有关的结构如下:
字段 | 类型 | 描述 |
---|---|---|
PTS_DTS_flags | 2b | 10 表示 PES 头部有 PTS 字段,11 表示有 PTS 和 DTS 字段,00 表示都没有,10 被禁止 |
pts | 40b | 实际 pts 长度占用 33b |
dts | 40b | 实际 dts 长度占用 33b |
6. ES(Packet Elemental Stream,基本流打包)
每个 es packet 包含一个完整的音频帧或视频帧:
h264 video:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| start code(4 byte)| nalu header(1 byte) | h264 data(x byte) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
aac audio:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| adts header(7 byte) | aac data(x byte) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
6.1 h264 es packet
h264 es packet 采用 Annex-B 格式封装码流,即使用 0x000001 作为 start code。
注意:
- 封 h264 es packet 不用添加 AUD
- 每个 es packet 应该包含一整帧的所有 slices
- sps、pps、idr 属于一帧,需要封装到一个 es packet 里面,使用同一个 pes header 时间戳
6.2 aac es packet
aac es packet 由 28 bit 的 adts_fixed_header 和 28 bit 的 adts_variable_header 组成,共 7 bytes。
adts_fixed_header 结构如下(共 28 bit):
字段 | 类型 | 描述 |
---|---|---|
syncword | 12b | 固定为0xfff |
id | 1b | 0表示MPEG-4,1表示MPEG-2 |
layer | 2b | 固定为00 |
protection_absent | 1b | 固定为1 |
profile | 2b | 取值0~3,1表示aac |
sampling_frequency_index | 4b | 表示采样率,0: 96000 Hz,1: 88200 Hz,2: 64000 Hz,3:48000 Hz,4: 44100 Hz,5: 32000 Hz,6: 24000 Hz,7: 22050 Hz,8: 16000 Hz,9: 12000 Hz,10: 11025 Hz,11: 8000 Hz,12: 7350 Hz |
private_bit | 1b | 固定为0 |
channel_configuration | 3b | 取值0~7,1: 1 channel: front-center,2: 2 channels: front-left, front-right,3: 3 channels: front-center, front-left, front-right,4: 4 channels: front-center, front-left, front-right, back-center |
original_copy | 1b | 固定为0 |
home | 1b | 固定为0 |
adts_variable_header 结构如下(共 28 bit):
字段 | 类型 | 描述 |
---|---|---|
copyright_identification_bit | 1b | 固定为0 |
copyright_identification_start | 1b | 固定为0 |
aac_frame_length | 13b | 包括adts头在内的音频数据总长度 |
adts_buffer_fullness | 11b | 固定为0x7ff |
number_of_raw_data_blocks_in_frame | 2b | 固定为00 |
7. 时间戳
7.1 pts、dts
header 中结构如下:
pts:
40 bits
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0011 | pts 32..30 | 1 | pts 29..15 | 1 | pts 14..00 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
dts:
40 bits
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0001 | dts 32..30 | 1 | dts 29..15 | 1 | dts 14..00 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
ts 中的时间戳,无论是音频还是视频,时间戳单位都是 1/90000。
7.2 pcr
pcr 属于编码端的时钟,其作用是如果编码端时钟源与解码端时钟源不同步,那么解码端应该采用 pcr 作为自己的时钟源,以同步编码端。
例如编码端时钟是解码端的 2 倍,解码端是正常的物理时钟,这时一个物理世界 5min 的视频,因为编码端时钟源走得太快,那么最后一个视频帧的 dts 就是 10min。播放端直接播放的话,就会播放 10min,显得播放得很慢。所以播放端需要加快播放,方法就是采用 pcr 时钟作为自己的时钟源,让自己的时钟走得跟编码端一样快,这样看起来就是正常速度播放了。
关于播放器中时间戳同步的设计,参考 https://www.cnblogs.com/moonwalk/p/16190871.html。
关于 srs 中对 pcr 的讨论,参考 https://github.com/ossrs/srs/issues/311。