MPEG-TS 封装格式

参考:

分析工具:

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。

posted @ 2022-04-27 20:24  小夕nike  阅读(1180)  评论(0编辑  收藏  举报