目录
一、基本概念
二、TS结构
三、各小结构解析
3.1 TS文件
四、pes层结构
五、es层结构
5.1 h.264视频的es层
5.2 aac音频的es层
六、 TS封包
之前都是别人总结的,现在感觉看了那么多之后也有了自己的认识,还是Mark一下吧,方便以后回顾。
一、基本概念
关于ts的封包,ts的封装格式要比flv更复杂,主要的数据单元是ts包,每个包有pid,一个包固定大小普通没有crc的为188,主要分为三类ts包,pat,pmt,pes,pat就是第一个包,当解析的时候会在ts包列表里找pid为0x0的包,就是pat包,pat大概作用就是入口的意思,pat里面有pmt包的pid,pmt里面存储的是流的包的pid,比如指定音频包pid是0x102,视频包pid是0x101,后面的0x102和0x101的包就是pes包了,将pes包解析并合并出原始流,就能解码播放了。
ES(Elementary Stream)流是基本码流,包含音频、视频、数据的连续码流。直接从编码器出来的数据流,可以是编码过的视频数据流(H.264,MJPEG等),音频数据流(AAC),或其他编码数据流的统称。ES流经过PES打包器之后,被转换成PES包。
PES(Packet Elementary Stream 分组的ES)ES形成的分组称为PES分组,是用来传递ES的一种数据结构。PES流是ES流经过PES打包器处理后形成的数据流,在这个过程中完成了将ES流分组、打包、加入包头信息等操作(对ES流的第一次打包)。PES流的基本单位是PES包。PES包由包头和payload组成。
TS(Transport Stream)流,也叫传输流。是在pes层上加入了数据流识别和传输的必要信息。是由固定长度的188字节的包组成。含有独立是一个或者多个program,一个program又可以包含多个视频,音频和文字信息的ES流。每个ES流会有不同的PID标示。为了分析这些ES流,TS有些固定的PID来间隔发送Program和ES信息表格:PAT表和PMT表。
二、TS结构
- 一、开始还是看一下TS的格式吧,在TS文件里结构如下图:
上面是一张ts的结构图:都是一个个transport packet组成,每个都是188byte。可以是AAC、H264、 PAT、PMT等;
每个TS都有PID和payload,188-payload就是TS header的大小,counter表示这个帧里transport packet的个数;
三、各小结构解析
ts文件分为三次:ts层(Transport Stream)、pes层(Packet Elemental Stream)、es层(Elementary Stream)。es层就是音视频数据,pes层是在音视频数据上加了时间戳等数据帧的说明信息,ts层是在pes层上加入了数据流识别和传输的必要信息。
3.1 TS文件
ts包(Packet)大小固定为188字节,ts层分为三个部分:ts header、adaptation field、payload。ts header固定4个字节;adaptation field可能存在也可能不存在,主要作用是给不足188字节的数据做填充;payload是pes数据。
TS流:
+-+-+-+-+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| TS | = | Packet 1 | Packet 2 | Packet 3 | ... | Packet n-1| Packet n |
+-+-+-+-+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
一个Packet: 4bytes 184bytes
+-+-+-+-+-+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Packet | = | Packet header | Packet data |
+-+-+-+-+-+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
实际上一个transport packet(Packet)里面并不是一直都是上面那样的,可能会有adaption filed 存放其他信息,
自适应区(adaption filed )的长度要包含传输错误指示符标识的一个字节。pcr是节目时钟参考,pcr、dts、pts都是对同一个系统时钟的采样值,pcr是递增的,因此可以将其设置为dts值,音频数据不需要pcr。如果没有字段,ipad是可以播放的,但vlc无法播放。打包ts流时PAT和PMT表是没有adaptation field的,不够的长度直接补0xff即可。视频流和音频流都需要加adaptation field,通常加在一个帧的第一个ts包和最后一个ts包里,中间的ts包不加。
如下:
4 byte x byte 184-x byte
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ts header | adaptation field | payload(pes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
3.3.1 ts header 结构
sync_byte | 8b | 同步字节,固定为0x47 |
transport_error_indicator | 1b | 传输错误指示符,表明在ts头的adapt域后由一个无用字节,通常都为0,这个字节算在adapt域长度内 |
payload_unit_start_indicator | 1b | 负载单元起始标示符,一个完整的数据包开始时标记为1 |
transport_priority | 1b | 传输优先级,0为低优先级,1为高优先级,通常取0 |
pid | 13b | pid值 |
transport_scrambling_control | 2b | 传输加扰控制,00表示未加密 |
adaptation_field_control | 2b | 是否包含自适应区,‘00’保留;‘01’为无自适应域,仅含有效负载;‘10’为仅含自适应域,无有效负载;‘11’为同时带有自适应域和有效负载。 |
continuity_counter | 4b | 递增计数器,从0-f,起始值不一定取0,但必须是连续的 |
一个重要的PID值
ts层的内容是通过PID值来标识的,主要内容包括:PAT表、PMT表、音频流、视频流。解析ts流要先找到PAT表,只要找到PAT就可以找到PMT,然后就可以找到音视频流了。PAT表的PID值固定为0。PAT表和PMT表需要定期插入ts流,因为用户随时可能加入ts流,这个间隔比较小,通常每隔几个视频帧就要加入PAT和PMT。PAT和PMT表是必须的,还可以加入其它表如SDT(业务描述表)等,不过hls流只要有PAT和PMT就可以播放了。
- PAT表:他主要的作用就是指明了PMT表的PID值。
- PMT表:他主要的作用就是指明了音视频流的PID值。
- 音频流/视频流:承载音视频内容
- 可以不错的参考:https://onelib.biz/doc/stb/course/begin.html#part3_2
PID取值 | PID值使用描述 |
0x0000 | 节目关联表(program association table, PAT) |
0x0001 | 条件访问表(conditional access table, CAT) |
0x0002 | 传送流描述表(transport stream description table, TSDT) |
0x0003~0x000F | 保留 |
0x0010~0x1FFE | 可以分配为network PID, Program map PID, elementary PID, 或其它 |
0x1FFF | 空包(8191) |
好、来分析下面第一个行的4个字节的ts header;
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
47 | 41 | 01 | 10
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
0X47 | 0100 0001 | 0000 0000 | 0001 0000
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
3.3.2 adaptation field结构
adaptation_field_length | 1B | 自适应域长度,后面的字节数 |
flag | 1B | 取0x50表示包含PCR或0x40表示不包含PCR |
PCR | 5B | Program Clock Reference,节目时钟参考,用于恢复出与编码端一致的系统时序时钟STC(System Time Clock)。 |
stuffing_bytes | xB | 填充字节,取值0xff |
自适应区的长度要包含传输错误指示符标识的一个字节。pcr是节目时钟参考,pcr、dts、pts都是对同一个系统时钟的采样值,pcr是递增的,因此可以将其设置为dts值,音频数据不需要pcr。如果没有字段,ipad是可以播放的,但vlc无法播放。打包ts流时PAT和PMT表是没有adaptation field的,不够的长度直接补0xff即可。视频流和音频流都需要加adaptation field,通常加在一个帧的第一个ts包和最后一个ts包里,中间的ts包不加。
PAT/PMT类型包(Packet)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ts header | PAT/PMT | Stuffing Bytss |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
视频/音频类型包(Packet),一帧视频/音频数据被拆分成N个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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
PAT和PMT
PAT(Program Associate Table)格式 节目关联表
table_id | 8b | PAT表固定为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校验码 |
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校验码 |
四、pes层结构
pes层(ts层中payload)是在每一个视频/音频帧上加入了时间戳等信息,pes包内容很多,我们只留下最常用的。
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| pes header| optional pes header | pes payload |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
6Byte 3~259Byte max 65526Byte
pes header格式:
pes start code | 3B | 开始码,固定为0x000001 |
stream id | 1B | 音频取值(0xc0-0xdf),通常为0xc0 视频取值(0xe0-0xef),通常为0xe0 |
pes packet length | 2B | 后面pes数据的长度,0表示长度不限制, 只有视频数据长度会超过0xffff |
flag | 1B | 通常取值0x80,表示数据不加密、无优先级、备份的数据 |
flag | 1B | 取值0x80表示只含有pts,取值0xc0表示含有pts和dts |
pes data length | 1B | 后面数据的长度,取值5或10 |
pts | 5B | 33bit值 |
dts | 5B | 33bit值 |
pts是显示时间戳、dts是解码时间戳,视频数据两种时间戳都需要,音频数据的pts和dts相同,所以只需要pts。有pts和dts两种时间戳是B帧引起的,I帧和P帧的pts等于dts。如果一个视频没有B帧,则pts永远和dts相同。从文件中顺序读取视频帧,取出的帧顺序和dts顺序相同。dts算法比较简单,初始值 + 增量即可,pts计算比较复杂,需要在dts的基础上加偏移量。
音频的pes中只有pts(同dts),视频的I、P帧两种时间戳都要有,视频B帧只要pts(同dts)。打包pts和dts就需要知道视频帧类型,但是通过容器格式我们是无法判断帧类型的,必须解析h.264内容才可以获取帧类型。
举例说明:
------------------------------>
I P B B B P
1 2 3 4 5 6 读取顺序
1 2 3 4 5 6 dts顺序
1 5 3 2 4 6 pts顺序
点播视频dts算法:
dts = 初始值 + 90000 / video_frame_rate,初始值可以随便指定,但是最好不要取0,video_frame_rate就是帧率,比如23、30。
pts和dts是以timescale为单位的,1s = 90000 time scale , 一帧就应该是90000/video_frame_rate 个timescale。
用一帧的timescale除以采样频率就可以转换为一帧的播放时长
点播音频dts算法:
dts = 初始值 + (90000 * audio_samples_per_frame) / audio_sample_rate,audio_samples_per_frame这个值与编解码相关,aac取值1024,mp3取值1158,audio_sample_rate是采样率,比如24000、41000。AAC一帧解码出来是每声道1024个sample,也就是说一帧的时长为1024/sample_rate秒。所以每一帧时间戳依次0,1024/sample_rate,...,1024*n/sample_rate秒。
直播视频的dts和pts应该直接用直播数据流中的时间,不应该按公式计算。
五、es层结构
es层(pes payload)指的就是音视频数据。
video:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| start code(4 byte)| nalu header(1 byte) | h264 data(x byte) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
audio:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| adts header(7 byte) | aac data(x byte) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
5.1 h.264视频的es层
打包h.264数据我们必须给视频数据加上一个nalu(Network Abstraction Layer unit),nalu包括start code和nalu header,start code固定为0x00000001(帧开始)或0x000001(帧中)。h.264的数据是由slice组成的,slice的内容包括:视频、sps、pps等。nalu type决定了后面的h.264数据内容。
nalu header:
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|F|NRI| Type |
+-+-+-+-+-+-+-+-+
F: 占1bit,forbidden_zero_bit,h.264规定必须取0,禁止位,当网络发现NAL单元有比特错误时可设置该比特为1,以便接收方纠错或丢掉该单元。
NRI: 占2bit,nal_ref_idc,取值0~3,指示这个nalu的重要性,I帧、sps、pps通常取3,P帧通常取2,B帧通常取0,nal重要性指示,标志该NAL单元的重要性,值越大,越重要,解码器在解码处理不过来的时候,可以丢掉重要性为0的NALU。
Type:占5bit, nal_unit_type:0=未使用 1=非IDR图像片,IDR指关键帧
2=片分区A 3=片分区B
4=片分区C 5=IDR图像片,即关键帧
6=补充增强信息单元(SEI) 7=SPS序列参数集
8=PPS图像参数集 9=分解符
10=序列结束 11=码流结束
12=填充
13~23=保留 24~31=未使用
打包es层数据时pes头和es数据之间要加入一个type=9的nalu,关键帧slice前必须要加入type=7和type=8的nalu,而且是紧邻
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| pes header| nalu(0x09)| 1byte | nalu | | nalu(0x67)| | nalu(0x68)| | nalu(0x65)| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
随意 其他 内容 SPS 内容 PPS 内容 I帧 内容
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| pes header| nalu(0x09)| 1byte | nalu | | nalu(0x41)| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
随意 其他 内容 P帧 内容
F | 1b | forbidden_zero_bit,h.264规定必须取0 |
NRI | 2b | nal_ref_idc,取值0~3,指示这个nalu的重要性,I帧、sps、pps通常取3,P帧通常取2,B帧通常取0 |
Type | 5b | 参考下表 |
nal_unit_type | 说明 |
0 | 未使用 |
1 | 非IDR图像片,IDR指关键帧 |
2 | 片分区A |
3 | 片分区B |
4 | 片分区C |
5 | IDR图像片,即关键帧 |
6 | 补充增强信息单元(SEI) |
7 | SPS序列参数集 |
8 | PPS图像参数集 |
9 | 分解符 |
10 | 序列结束 |
11 | 码流结束 |
12 | 填充 |
13~23 | 保留 |
24~31 | 未使用 |
5.2 aac音频的es层
打包aac音频必须加上一个adts(Audio Data Transport Stream)头,共7Byte,adts包括fixed_header和variable_header两部分,各28bit。
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 |
variable_header
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 |
六、 TS封包
一个PAT包含整个TS流的信息,其中里面有一张表,比较重要的两个属性 program_number和program_map_PID,可能出现多对,
每一对program_number表示一个节目,而与该program_number对应的program_map_PID则表示该节目对应的流信息应该放在一个PMT表中,
而该PMT表的PID应该与这里的program_map_PID相等。
一个PMT中描述了流的类型,其中0x0f表示AAC音频,而0x1b表示H264视频,除这两种之外还有其他流,例如字幕。
elementay_PID表示该流的数据应该存放在以该PID为表示的TS包中。
+-+-+-+-+-+-+-+-+-+-+-+
| PAT |
| |
| program_number 5 |___
| program_map_PID 10 | |
| | |
| program_number 6 |___|__
| program_map_PID 11 | | |
| | | |
| program_number 7 | | |
| program_map_PID 12 | | |
| | | |
| ... | | |
| | | |
+-+-+-+-+-+-+-+-+-+-+-+ | |
| |
+-+-+-+-+-+-+-+-+-+-+-+ | |
| PMT | | |
| TS Header PID = 10 |<—— |
| | |
| stream_type 0x0f |______|__________________0x0f表示AAC音频,下方AAC数据打包PID=20,
| elementary_PID 20 | |
| stream_type 0x1b |______|__________________0x1b表示H264视频,下方H264数据打包PID=22
| elementary_PID 22 | |
| | |
+-+-+-+-+-+-+-+-+-+-+-+ |
|
+-+-+-+-+-+-+-+-+-+-+-+ |
| PMT | |
| TS Header PID = 11 |<—————
| |
| stream_type 0x0f |
| elementary_PID 23 |
| stream_type 0x1b |
| elementary_PID 24 |
| |
+-+-+-+-+-+-+-+-+-+-+-+
裸ACC数据:
+-+-+-+-+-+-+-+-+-+-+-+
| AAC |
+-+-+-+-+-+-+-+-+-+-+-+
添加PES头的ACC数据:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| AAC PES | AAC |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
添加TS头将PES分割之后的TS包,假设正好分割成2个TS包,包大小固定188字节,不够用adaptation域填充一般填充0xFF:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|TS | AAC PES | AAC 1 |TS | adaptation| AAC 2 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|- - - - Packet 1 - - - |- - - - - Packet 2 - - - - - |
<假设 PID = 20 的TS包>
裸H264数据,一帧:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| H264 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
添加PES头之后的H264数据,一帧表示一个PES包:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| H264 PES| H264 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
将一个PES包分割之后,分别添加TS头之后的TS包,这里假设分割成3个TS包,每个包固定大小188字节(包含TS包头在内),
最后一个包不够188字节使用adaptaion域填充0xFF:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|TS | H264 PES| H264 1|TS | H264 2 |TS | adaptation| H264 3 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|- - - Packet 1 - - - |- - Packet 2 - - | - - - - Packet 3 - - - -|
<假设 PID = 22 的TS包>
现在回头看下面这个是不是有些头绪了