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) 组成:

ts使用axios封装 ts流封装_取值

  • 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 时钟作为自己的时钟源,让自己的时钟走得跟编码端一样快,这样看起来就是正常速度播放了。