TS 数据流分析学习
TS 流、包结构以及同步
1. TS 流: 可以将TS流理解为一种单一码流、混合码流。
单一码流:TS流的基本组成单位是长度为188字节的TS包。
混合码流:TS流有多种数据组成,一个TS包中的数据可以是视频数据、音频数据、填充数据,PSI/SI表格数据.....(唯一的PID对应)
2. TS 包结构分析:
TS 包由包头、有效载荷区组成。(有些包中包括自适应区)。大小: 188 字节
TS包头:4 个字节
同步字节 0x47,用于检测码流是否同步。
包ID: PID, 解码器通过该标志号确定该TS包中的数据属于那种类型。
PCR: 自适应区, 解码器通过该时间参数,进行解码端的时钟重置。
有效载荷: 最高 184 字节
视频、音频 or 其他数据
语法:
五、
HLS,Http Live Streaming 是由Apple公司定义的用于实时流传输的协议,HLS基于HTTP协议实现,传输内容包括两部分,一是M3U8描述文件,二是TS媒体文件。
1、M3U8文件
用文本方式对媒体文件进行描述,由一系列标签组成。
#EXTM3U
#EXT-X-TARGETDURATION:5
#EXTINF:5,
./0.ts
#EXTINF:5,
./1.ts
#EXTM3U:每个M3U8文件第一行必须是这个tag。
#EXT-X-TARGETDURATION:指定最大的媒体段时间长度(秒),#EXTINF中指定的时间长度必须小于或等于这个最大值。该值只能出现一次。
#EXTINF:描述单个媒体文件的长度。后面为媒体文件,如./0.ts
2、ts文件
ts文件为传输流文件,视频编码主要格式h264/mpeg4,音频为acc/MP3。
ts文件分为三层:ts 层 Transport Stream, 就是在pes层加入数据流的识别和传输必须的信息;
pes 层 Packet Elemental Stream,pes层是在音视频数据上加了时间戳等对数据帧的说明信息;
es层 Elementary Stream. es层就是音视频数据。
(1)ts层
ts包大小固定为188字节,ts层分为三个部分:ts header、adaptation field、payload。ts header固定4个字节;adaptation field可能存在也可能不存在,主要作用是给不足188字节的数据做填充;payload是pes数据。
sync_byte | 8 | 同步字节,固定为0x47 |
transport_error_indicator | 1 | 传输错误指示符,表明在ts头的adapt域后由一个无用字节,通常都为0,这个字节算在adapt域长度内 |
payload_unit_start_indicator | 1 | 负载单元起始标示符,一个完整的数据包开始时标记为1, 表示携带的是PSI或PES第一个包 |
transport_priority | 1 | 传输优先级,0为低优先级,1为高优先级,通常取0 |
pid | 13 | pid值 |
transport_scrambling_control | 2 | 传输加扰控制,00表示未加密,TS包中的有效数据未经加扰处理。 |
adaptation_field_control | 2 | 是否包含自适应区,‘00’保留;‘01’为无自适应域,仅含有效负载;‘10’为仅含自适应域,无有效负载;‘11’为同时带有自适应域和有效负载。 |
continuity_counter | 4 | 递增计数器,从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就可以播放了。
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格式
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, 表明这个段是该pat表的第几个段。 |
last_section_number | 8b | 固定为0x00, 表明PAT表一共有多少段。 |
开始循环 | ||
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校验码 |
(2)pes层
pes层是在每一个视频/音频帧上加入了时间戳等信息,pes包内容很多,我们只留下最常用的。
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
dts顺序: 1 2 3 4 5 6
pts顺序: 1 5 3 2 4 6
点播视频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应该直接用直播数据流中的时间,不应该按公式计算。
(3)es层
es层指的就是音视频数据,我们只介绍h.264视频和aac音频。
h.264视频:
打包h.264数据我们必须给视频数据加上一个nalu(Network Abstraction Layer unit),nalu包括nalu header和nalu type,nalu header固定为0x00000001(帧开始)或0x000001(帧中)。h.264的数据是由slice组成的,slice的内容包括:视频、sps、pps等。nalu type决定了后面的h.264数据内容。
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 | 未使用 |
非IDR图像片,IDR指关键帧 | |
2 | 片分区A |
3 | 片分区B |
4 | 片分区C |
IDR图像片,即关键帧 | |
补充增强信息单元(SEI) | |
SPS序列参数集 | |
PPS图像参数集 | |
分解符 | |
10 | 序列结束 |
11 | 码流结束 |
12 | 填充 |
13~23 | 保留 |
24~31 | 未使用 |
红色字体显示的内容是最常用的,打包es层数据时pes头和es数据之间要加入一个type=9的nalu,关键帧slice前必须要加入type=7和type=8的nalu,而且是紧邻。
协议数据单元(protocol data unit,PDU)
TS数据流PAT和PMT分析
typedef struct TS_PAT
{
unsigned table_id : 8; //固定为0x00 ,标志是该表是PAT表
unsigned section_syntax_indicator : 1; //段语法标志位,固定为1
unsigned zero : 1; //0
unsigned reserved_1 : 2; // 保留位
unsigned section_length : 12; //表示从下一个字段开始到CRC32(含)之间有用的字节数
unsigned transport_stream_id : 16; //该传输流的ID,区别于一个网络中其它多路复用的流
unsigned reserved_2 : 2; // 保留位
unsigned version_number : 5; //范围0-31,表示PAT的版本号
unsigned current_next_indicator : 1; //发送的PAT是当前有效还是下一个PAT有效
unsigned section_number : 8; //分段的号码。PAT可能分为多段传输,第一段为00,以后每个分段加1,最多可能有256个分段
unsigned last_section_number : 8; //最后一个分段的号码
std::vector<TS_PAT_Program> program;
unsigned reserved_3 : 3; // 保留位
unsigned network_PID : 13; //网络信息表(NIT)的PID,节目号为0时对应的PID为network_PID
unsigned CRC_32 : 32; //CRC32校验码
} TS_PAT;
//PMT 表结构体
typedef struct TS_PMT
{
unsigned table_id : 8; //固定为0x02, 表示PMT表
unsigned section_syntax_indicator : 1; //固定为0x01
unsigned zero : 1; //0x01
unsigned reserved_1 : 2; //0x03
unsigned section_length : 12; //首先两位bit置为00,它指示段的byte数,由段长度域开始,包含CRC。
unsigned program_number : 16; // 指出该节目对应于可应用的Program map PID
unsigned reserved_2 : 2; //0x03
unsigned version_number : 5; //指出TS流中Program map section的版本号
unsigned current_next_indicator : 1; //当该位置1时,当前传送的Program map section可用;
//当该位置0时,指示当前传送的Program map section不可用,下一个TS流的Program map section有效。
unsigned section_number : 8; //固定为0x00
unsigned last_section_number : 8; //固定为0x00
unsigned reserved_3 : 3; //0x07
unsigned PCR_PID : 13; //指明TS包的PID值,该TS包含有PCR域,
//该PCR值对应于由节目号指定的对应节目。
//如果对于私有数据流的节目定义与PCR无关,这个域的值将为0x1FFF。
unsigned reserved_4 : 4; //预留为0x0F
unsigned program_info_length : 12; //前两位bit为00。该域指出跟随其后对节目信息的描述的byte数。
std::vector<TS_PMT_Stream> PMT_Stream; //每个元素包含8位, 指示特定PID的节目元素包的类型。该处PID由elementary PID指定
unsigned reserved_5 : 3; //0x07
unsigned reserved_6 : 4; //0x0F
unsigned CRC_32 : 32;
} TS_PMT;
:流类型取值说明
取值 |
描述 |
0x00 |
国际标准保留 |
0x01 |
视频 |
0x02 |
视频或受限参数视频流 |
0x03 |
音频 |
0x04 |
音频 |
0x05 |
private_sections |
0x06 |
包含专用数据的PES分组 |
0x07 |
ISO/IEC 13533 MHEG |
0x08 |
|
0x09 |
ITU-T Rec.H.222.1 |
0x0A~0x0D |
GB/T类型 |
0x0E |
GB/T辅助 |
0x0F~0x7F |
GB/T保留 |
0x80~0xFF |
用户专用 |
从TS开始
PAT 节目关联表(PAT Program Association Table): PAT是机顶盒接收的入口点,是它获取数据的开始
结构名 |
中文 |
所定义标准 |
PID |
描述 |
PAT |
节目关联表 |
MPEG2标准 |
0x0000 |
将节目号码和节目映射表PID相关联,是获取数据的开始 |
PMT |
节目映射表 |
MPEG2标准 |
在PAT中指出 |
指定一个或多个节目的PID |
CAT |
条件接收表 |
MPEG2标准 |
0x0001 |
将一个或多个专用EMM流分别与唯一的PID相关联 |
NIT |
网络信息表 |
SI标准 |
PAT中指出 |
描述整个网络,如多少个TS流、频点和调制方式等信息 |
*NOTE:
TS流和PS流的区别:TS流的包结构是长度是固定的;PS流的包结构是可变长度的。这导致了TS流的抵抗传输误码的能力强于PS流(TS码流由于采用了固定长度的包结构,当传输误码破坏了某一TS包的同步信息时,接收机可在固定的位置检测它后面包中的同步信息,从而恢复同步,避免了信息丢失。而PS包由于长度是变化的,一旦某一 PS包的同步信息丢失,接收机无法确定下一包的同步位置,就会造成失步,导致严重的信息丢失。因此,在信道环境较为恶劣,传输误码较高时,一般采用TS码流;而在信道环境较好,传输误码较低时,一般采用PS码流。)
由于TS码流具有较强的抵抗传输误码的能力,因此目前在传输媒体中进行传输的MPEG-2码流基本上都采用了TS码流的包格。
从上图可以看出,视频ES和音频ES通过打包器和共同或独立的系统时间基准形成一个个PES,通过TS复用器复用形成的传输流。注意这里的TS流是位流格式(分析Packet的时候会解释),也即是说TS流是可以按位读取的。
TS流是基于Packet的位流格式,每个包是188个字节(或204个字节,在188个字节后加上了16字节的CRC校验数据,其他格式一样)。整个TS流组成形式如下:
Packet Header(包头)信息说明 |
|||
1 |
sync_byte |
8bits |
同步字节 |
2 |
transport_error_indicator |
1bit |
错误指示信息(1:该包至少有1bits传输错误) |
3 |
payload_unit_start_indicator |
1bit |
负载单元开始标志(packet不满188字节时需填充) |
4 |
transport_priority |
1bit |
传输优先级标志(1:优先级高) |
5 |
PID |
13bits |
Packet ID号码,唯一的号码对应不同的包 |
6 |
transport_scrambling_control |
2bits |
加密标志(00:未加密;其他表示已加密) |
7 |
adaptation_field_control |
2bits |
附加区域控制 |
8 |
continuity_counter |
4bits |
包递增计数器 |
PID是TS流中唯一识别标志,Packet Data是什么内容就是由PID决定的。如果一个TS流中的一个Packet的Packet Header中的PID是0x0000,那么这个Packet的Packet Data就是DVB的PAT表而非其他类型数据(如Video、Audio或其他业务信息)。下表给出了一些表的PID值,这些值是固定的,不允许用于更改。
表 |
PID 值 |
PAT |
0x0000 |
CAT |
0x0001 |
TSDT |
0x0002 |
EIT,ST |
0x0012 |
RST,ST |
0x0013 |
TDT,TOT,ST |
0x0014 |
下面给出了PID字段的取值要求:
值 |
描述 |
0x0000 |
PAT |
0x0001 |
CAT |
0x0002~0x000F |
保留 |
0x0010~0x1FFE |
可赋给network_PID、Program_map_PID、elementary_PID或作其他用途 |
0x1FFF |
空的分组 |
下面以一个TS流的其中一个Packet中的Packet Header为例进行说明:
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
… |
|
Packet(十六进制) |
4 |
7 |
0 |
7 |
e |
5 |
1 |
2 |
… |
||||||||||||||||||||||||
Packet(二进制) |
0 |
1 |
0 |
0 |
0 |
1 |
1 |
1 |
0 |
0 |
0 |
0 |
0 |
1 |
1 |
1 |
1 |
1 |
1 |
0 |
0 |
1 |
0 |
1 |
0 |
0 |
0 |
1 |
0 |
0 |
1 |
0 |
… |
Packet Header 信息 |
1 sync_byte=0x47 |
2 |
3 |
4 |
5 PID=0x07e5 |
6 |
7 |
8 |
… |
sync_byte=01000111, 就是0x47,这是DVB TS规定的同步字节,固定是0x47.
transport_error_indicator=0, 表示当前包没有发生传输错误.
payload_unit_start_indicator=0, 含义参考ISO13818-1标准文档
transport_priority=0, 表示当前包是低优先级.
PID=00111 11100101即0x07e5, Video PID
transport_scrambling_control=00, 表示节目没有加密
adaptation_field_control=01 即0x01,具体含义请参考ISO13818-1
continuity_counte=0010 即0x02,表示当前传送的相同类型的包是第3个
PAT表定义了当前TS流中所有的节目,其PID为0x0000,它是PSI的根节点,要查寻找节目必须从PAT表开始查找。
PAT表携带以下信息:
TS流ID |
transport_stream_id |
该ID标志唯一的流ID |
节目频道号 |
program_number |
该号码标志TS流中的一个频道,该频道可以包含很多的节目(即可以包含多个Video PID和Audio PID) |
PMT的PID |
program_map_PID |
表示本频道使用哪个PID做为PMT的PID,因为可以有很多的频道,因此DVB规定PMT的PID可以由用户自己定义 |
PAT表主要包含频道号码和每一个频道对应的PMT的PID号码,这些信息我们在处理PAT表格的时候会保存起来,以后会使用到这些数据
PAT表描述了当前流的NIT(Network Information Table,网络信息表)中的PID、当前流中有多少不同类型的PMT表及每个PMT表对应的频道号。
(二) PMT表(Program Map Table,节目映射表)(Service Descriptor Table)
1. PMT表的描述
如果一个TS流中含有多个频道,那么就会包含多个PID不同的PMT表。
每一个节目的所有信息必须包含在一个PMT中,但在一个PMT中可以包含多个节目信息。PMT本身的PID由PAT表格提供。
PMT表中包含的数据如下:
(1) 当前频道中包含的所有Video数据的PID
(2) 当前频道中包含的所有Audio数据的PID
(3) 和当前频道关联在一起的其他数据的PID(如数字广播,数据通讯等使用的PID)
只要我们处理了PMT,那么我们就可以获取频道中所有的PID信息,如当前频道包含多少个Video、共多少个Audio和其他数据,还能知道每种数据对应的PID分别是什么。这样如果我们要选择其中一个Video和Audio收看,那么只需要把要收看的节目的Video PID和Audio PID保存起来,在处理Packet的时候进行过滤即可实现。
解复用的意义在于,由于TS流是一种复用的码流,里面混杂了多种类型的包;解复用TS流可以将类型相同的Packet存入相同缓存,分别处理。这样就可以将Video、Audio或者其他业务信息的数据区分开来。
(四) DVB搜台原理以及SDT表(Service Descriptor Table,业务描述表)
机顶盒先调整高频头到一个固定的频率(如498MHZ),如果此频率有数字信号,则COFDM芯片(如MT352)会自动把TS流数据传送给MPEG- 2 decoder。 MPEG-2 decoder先进行数据的同步,也就是等待完整的Packet的到来.然后循环查找是否出现PID== 0x0000的Packet,如果出现了,则马上进入分析PAT的处理,获取了所有的PMT的PID。接着循环查找是否出现PMT,如果发现了,则自动进入PMT分析,获取该频段所有的频道数据并保存。如果没有发现PAT或者没有发现PMT,说明该频段没有信号,进入下一个频率扫描。
在解析TS流的时候,首先寻找PAT表,根据PAT获取所有PMT表的PID;再寻找PMT表,获取该频段所有节目数据并保存。这样,只需要知道节目的PID就可以根据PacketHeade给出的PID过滤出不同的Packet,从而观看不同的节目。这些就是PAT表和PMT表之间的关系。而由于PID是一串枯燥的数字,用户不方便记忆、且容易输错,所以需要有一张表将节目名称和该节目的PID对应起来,DVB设计了SDT表来解决这个问题。 该表格标志一个节目的名称,并且能和PMT中的PID联系起来,这样用户就可以通过直接选择节目名称来选择节目了。
SDT可以提供的信息包括:
(1) 该节目是否在播放中
(2) 该节目是否被加密
(3) 该节目的名称
三、 从PAT开始,走向更远
在本章的学习中,我们发现了一个特点:所有的TS流的解析都是从寻找PAT表开始的,只有找到了PAT表,我们才能继续下一步的解析。因此,在进行了TS流、PAT表和PMT表的初步知识储备后,在接下来的学习中将从PAT表开始,学习更多的PSI/SI相关的表,将走得更远。
TS流的解码过程-ES-PES-DTS-PTS-PCR
http://blog.csdn.net/soulxu/article/details/6167060
TS 流解码过程:
1. 获取TS中的PAT
2. 获取TS中的PMT
3. 根据PMT可以知道当前网络中传输的视频(音频)类型(H264),相应的PID,PCR的PID等信息。
4. 设置demux 模块的视频Filter 为相应视频的PID和stream type等。
5. 从视频Demux Filter 后得到的TS数据包中的payload 数据就是 one piece of PES,在TS header中有一些关于此 payload属于哪个 PES的 第多少个数据包。 因此软件中应该将此payload中的数据copy到PES的buffer中,用于拼接一个PES包。
6. 拼接好的PES包的包头会有 PTS,DTS信息,去掉PES的header就是 ES。
7. 直接将 被被拔掉 PES包头的ES包送给decoder就可以进行解码。解码出来的数据就是一帧一帧的视频数据,这些数据至少应当与PES中的PTS关联一下,以便进行视 音频同步。
8. I,B,B,P 信息是在ES中的。
ES是直接从编码器出来的数据流,可以是编码过的视频数据流,音频数据流,或其他编码数据流的统称。ES流经过PES打 包器之后,被转换成PES包。PES包由包头和payload组成.
在PES层, 主要是在PES包头信息中加入PTS(显 示时间标签)和DTS(解 码时间标签)用于视频、音频同步。其实,Mpeg-2用于视音频同步以及系统时钟恢复的时间标签分别在ES,PES和TS这3个层次 中。在ES层,与同步有关的主要是视频缓冲验证VBV(Video Buffer Verifier),用以防止解码器的缓冲器出现上溢或下溢;在PES层,主要是在PES头 信息里出现的显示时间标签PTS(Presentation Time Stamp)和解码时间标签DTS(Decoding Time Stamp);在TS层 中,TS头信息包含了节目时钟参考PCR(Program Clock Reference),用于恢复出与编码端一致的系统时序时钟STC(System Time Clock)。
基本流程如下:首先MPEG-2压缩编码得到的ES基本流,这个数据流很大,并且只是I,P,B的这些视频帧或音频取样信息,然后加入一些同步信息,打包成长度可变长度的数据包PES,原来是流的格式,现在成了数据包的分割形式。同时要注意的是,ES是只包含一种内容的数据流,如只含视频,或只含音频等,打包之后的PES也是只含一种性质的ES,如 只含视频ES的PES,只 含音频ES的PES等。 可以知道,ES是编码视频数据流或音频数据流,每个ES都由若干个存取单元(AU) 组成,每个视频AU或音频AU都是由头部和编码数据两部分组成,1个AU相当于编码的1幅视频图像或1个音频 帧,也可以说,每个AU实际上是编码数据流的显示单元,即相当于 解码的1幅视频图像或1个音频 帧的取样。PEG-2对视频的压缩产生I帧、P帧、B帧。把 帧顺序I1,P4,B2,B3,P7,B5,B6帧的编码ES,通 过打包并在每个帧中插入 PTS/DTS标志,变成PES。在插入PTS/DTS标志时,由于在B帧PTS和DTS相 等,所以无须在B帧多插入DTS。而对于I帧 和P帧,由 于经过复用后数据包的顺序会发生变化,显示前一定要存储于视频解码器的从新排序缓存器中,经过从新排序后再显示,所以一定要同时插入PTS和 DTS作 为从新排序的依据。
其中,有否PTS/DTS标志,是解决视音频同步显示、防止解码器输入缓存器上溢或下溢的关键所在。PTS表明显示单元出现在系统目标解码器(STD- System Target Decoder)的时间, DTS表明将存取单元全部字节从STD的ES解码缓存器移走的时刻。视频编码图像帧次序为 I1,P4,B2,B3,P7,B5,B6,I10,B8,B9的ES,加 入PTS/DTS后,打包成一个个视频PES包。每个PES包都有一个包头,用于定义PES内的数据内容,提供定时资料。每个I、P、B帧的包头都有一个PTS和DTS,但PTS与DTS对B帧都是 一样的,无须标出B帧的DTS。对I帧和P帧,显示前一定要存储于视频解码器的重新排序缓存器中,经过延迟(重新排序)后再显示,一定要分别标 明PTS和DTS。 例如,解码器输入的图像帧次序为I1,P4,B2,B3,P7,B5,B6,I10,B8,B9,依解码器输出的帧次序,应该P4比B2、B3在先,但显示时P4一定 要比B2、B3在 后,即P4要在提前插入数据流中的时间标志指引下,经过缓存器重新排序,以重建编码前视频帧次序I1,B2,B3,P4,B5,B6,P7,B8,B9,I10。显然,PTS/DTS标志表明对确定事件或确定信息解码的专用时标的存在,依靠专用时标解码器,可知道该确定事件或确定信息开始解码或显示的时刻。例如,PTS/DTS标志可用于确定编码、多路复用、解码、重建的时间。
PCR
PCR是TS里面的,即TS packet的header里面可能会有,他用来指定所期望的该ts packet到达decoder的时间,他 的作用于SCR类似。
DTS, PTS
对于一个ES来说,比如视频,他又许多I,P,B帧,而P, B帧都是以I,P帧作为参考。由于B帧是前向后向参考,因此要对B帧 作decode的话,就必须先decode该B帧后面的P,或者I帧,于是,decode的时间与帧的真正的present的时间就不一致了,按照DTS一 次对各个帧进行decode,然后再按照PTS对各个帧进行展现。
有时候PES包头里面也会有DTS,PTS,对于PTS来说,他代表了这个PES包得payload里面的第一个完整地audio access unit或者video access unit的PTS时间(并不是每个audio/video access unit都带有PTS/DTS,因此,你可以在PES里面指定一个,作为开始)。
PES包头的DTS也是这个原理,只 不过注意的是:对于video来说他的DTS和PTS是可以不一样的,因为B帧的存 在使其顺序可以倒置。而对于audio来说,audio没有双向的预测,他的DTS和PTS可以看成是一个顺序的,因此可一直采用一个,即可只采用PTS。
PES,TS,PS,RTP等流的打包格式解析之PES流
http://blog.csdn.net/appledurian/article/details/70851428
一、PES流
PES流是对原始ES流进行的第一层封装,PES流的基本单位是PES包,由包头和payload组成,ES流即音视频裸流,是从编码器里面出来的原始视频音频流;ES流只包含一种内容,里面是视频或者音频;封装时不对其进行改变,只在前面添加头部,如私有头,解码时,将私有头剥掉,将原始ES码流送进解码器解码,这也是解码通用性,若是修改了,则其他解码器就没法解码了;PES和ES一样,都是单一原始码流,一般我遇到的是一帧数据放在一个PES包里面,但是一个PES包的最大长度为65535字节,因此一帧数据有可能被分为多个PES;其包头格式如下:
可以看出,PES包是由固定包头,可选包头和负载三部分组成,其中固定包头固定6个字节;PES包长度字段占位16bit,最大值为65536,故一帧可能会分为多个PES包;下面依次介绍其每个字段的含义:
Packet start code prefix: 包头起始码,固定为0x000001,占位24bit;
Stream id: (UI)PES包中的负载流类型,一般视频为0xe0,音频为0xc0,占位8bit;
PES packet length: (UI)PES包长度,包括此字节后的可选包头和负载的长度,占位16bit;
Optional PES Header,顺序依次为:
’10’字段: 占位2bit;
PES scrambling control: 加密模式,占2bit;00未加密,01或10或11由用户定义;
PES priority: 有效负载的优先级,占位1bit;值为1比值为0的负载优先级高;
Data alignment indicator: 数据定位指示器,占位1bit;
Copyright: 版权信息,1为有版权,0无版权,占位1bit;
Original or copy: 原始或备份,1为原始,0为备份,占位1bit;
// 后面是7个flags(一般我们关注的就是PTS DTS的标志位):
PTS_DTS_flags :PTS和DTS标志位,占位2bit;10表示首部有PTS字段,11表示有PTS和DTS字段,00表示都没有,01被禁止,不会出现此种情况;
ESCR_flag :ESCR标志,占位1bit;1表示首部有ESCR字段,0则无此字段
ES_rate_flag :ES_rate字段,占位1bit;1表示首部有此字段,0无此字段;
DSM_trick_mode_flag :占位1bit;1表示有8位的DSM_trick_mode_flag字段,0无此字段;
Additional_copy_info_flag :占位1bit;1表示首部有此字段,0表示无此字段;
PES_CRC_flag :占位1bit;置1表示PES分组有CRC字段,0无此字段;
PES_extension_flag :占位1bit;扩展标志位,置1表示有扩展字段,0无此字段;
PES header data length :(UI)PES首部中可选字段和填充字段的长度;占位8bit;可选字段的内容由上面7个flags来进行控制;
Optional fields:可选字段的描述信息区域,其内容由上面的7个flag来控制;
PTS/DTS字段:显示时间戳/解码时间戳,占位40bit,当PTS_DTS_flags == 1x时此字段存在;时间占用33个bit,PTS和DTS的内容是在这40bit中取33位,方式相同;
PTS(presentation time stamp)显示时间戳和DTS(Decoding Time Stamp)解码时间戳,是用来音视频同步的,是打在PES包的包头里面的,PTS/DTS是相对SCR(系统参考)的时间戳,是以90000为单位的,PTS/DTS到ms的转换公式是PTS/90,系统时钟频率(H264采样频率?)为90Khz,所以转换到秒为PTS/90000,所以如果是以ms为单位的播放器,PTS/DTS是要使用公式ms=pts/90来转换才行的,而如果是以时钟频率为单位的话,则直接将PTS/DTS送进去解码即可;如果没有B帧,PTS和DTS的顺序应该是一致的,如果有B帧,则需要先解码P帧,才能解出来B帧,所以需要PTS和DTS来控制解码时间和显示时间;
字节顺序依次:
start_code :起始码,占位4bit;若PTS_DTS_flags == ‘10’,则说明只有PTS,起始码为0010;
若PTS_DTS_flags == ‘11’,则PTS和DTS都存在,PTS的起始码为0011,DTS的起始码为0001;(PTS的起始码后2个bit与flag相同)
PTS[32..30] :占位3bit;
marker_bit :占位1bit;
PTS[29..15] :占位15bit;
marker_bit :占位1bit;
PTS[14..0] :占位15bit;
marker_bit :占位1bit;
PTS/DTS = (PTS1 & 0x0e) << 29 + (PTS2 & 0xfffe) << 14 + (PTS3 & 0xfffe ) >> 1;
ESCR字段 :此字段占位48bit,由33bit的ESCR_base字段和9bit的ESCR_extension字段组成,ESCR_flag == 1时此字段存在;数据依次顺序:
Reserved :保留字段,占位2bit;
ESCR_base[32..30]:占位3bit;
marker_bit :占位1bit;
ESCR_base[29..15]:占位15bit;
marker_bit :占位1bit;
ESCR_base[14..0] :占位15bit;
marker_bit :占位1bit;
ESCR_extension :(UI)占位9bit;周期数,取值范围0~299;循环一次,base+1;
marker_bit :占位1bit;
ES rate字段 :目标解码器接收PES分组字节速率,禁止为0,占位24bit,ES_rate_flag == 1时此字段存在;数据顺序为:
marker_bit :占位1bit;
ES_rate:占位22bit;
marker_bit:占位1bit;
Trick mode control字段:表示哪种trick mode被应用于相应的视频流,占位8个bit,
DSM_trick_mode_flag == 1时此字段存在;其中trick_mode_control占前3个bit,根据其值后面有5个bit的不同内容;
如果trick_mode_control == ‘000’,依次字节顺序为:
field_id:占位2bit;
intra_slice_refresh :占位1bit;
frequency_truncation:占位2bit;
如果trick_mode_control == ‘001’,依次字节顺序为:
rep_cntrl:占位5bit;
如果trick_mode_control == ‘010’,依次字节顺序为:
field_id:占位2bit;
Reserved:占位3bit;
如果trick_mode_control == ‘011’,依次字节顺序为:
field_id:占位2bit;
intra_slice_refresh:占位1bit;
frequency_truncation:占位2bit;
如果trick_mode_control== ‘100’,依次字节顺序为:
rep_cntrl:占位5bit;
其他情况,字节顺序为:
reserved :占位5bit;
Additional copy info字段:占8个bit,Additional_copy_info_flag == 1时此字段存在;数据顺序为:
marker_bit:占位1bit;
copy info字段:占位7bit;表示和版权相关的私有数据;
Previous PES CRC字段:占位16bit字段,包含CRC值,PES_CRC_flag == 1时此字段存在;
PES extension字段:PES扩展字段,PES_extension_flag == 1时此字段存在;内容如下,字节顺序依次为:
PES_private_data_flag:占位1bit,置1表示有私有数据,0则无;
Pack_header_field_flag:占位1bit,置1表示有Pack_header_field字段,0则无;
Program_packet_sequence_counter_flag:占位1bit,置1表示有此字段,0则无;
P-STD_buffer_flag:占位1bit,置1表示有P-STD_buffer字段,0则无此字段;
Reserved字段:3个bit;
PES_extension_flag_2:占位1bit,置1表示有扩展字段,0则无此字段;
Optional field :PES扩展字段的可选字段内容顺序为:
PES_private_data字段:私有数据内容,占位128bit,PES_private_data_flag == 1时此字段存在;
Pack_header_field字段:Pack_header_field_flag == 1时此字段存在;字段组成顺序如下:
Pack_field_length字段:(UI)指定后面的field的长度,占位8bit;
pack_header_field():长度为Pack_field_length指定;
Program_packet_sequence_counter字段:计数器字段,16个bit;当flag字段Program_packet_sequence_counter_flag == 1时此字段存在;字节顺序依次为:
marker_bit:占位1bit;
packet_sequence_counter字段:(UI)占位7bit;
marker_bit:占位1bit;
MPEG1_MPEG2_identifier:占位1bit;置位1表示此PES包的负载来自MPEG1流,置位0表示此PES包的负载来自PS流;
original_stuff_length:(UI)占位6bit;表示PES头部填充字节长度;
P-STD_buffer字段:表示P-STD_buffer内容,占位16bit;P-STD_buffer_flag == '1'时此字段存在;字节顺序依次为:
’01’字段:占位2bit;
P-STD_buffer_scale:占位1bit;表示用来解释后面P-STD_buffer_size字段的比例因子;如果之前的stream_id表示音频流,则此值应为0,若之前的stream_id表示视频流,则此值应为1,对于其他stream类型,此值可以0或1;
P-STD_buffer_size:占位13bit;无符号整数;大于或等于所有P-STD输入缓冲区大小BSn的最大值;若P-STD_buffer_scale == 0,则P-STD_buffer_size以128字节为单位;若P-STD_buffer_scale == 1,则P-STD_buffer_size以1024字节为单位;
PES_extension2字段:扩展字段的扩展字段;占用N*8个bit,PES_extension_flag_2 == '1'时此字段存在;字节顺序依次为:
marker_bit:占位1bit;
PES_extension_field_length:占位7bit,表示扩展区域的长度;
Reserved字段:占位8*PES_extension_field_length个bit;
Stuffing bytes:填充字段,固定为0xFF;不能超过32个字节;
PES_packet_data_byte:PES包负载中的数据,即ES原始流数据;
PES包是TS和PS包封装的基础,TS和PS其实就是对PES包的再一次封装,下篇将讲解一下TS流
上图给出了在一个TS流中PSI各表之间的联系。首先从PAT表出发,获取当前有哪些节目号(program_map_PID),再根据这些节目号找出program_number相同的PMT;获取到PMT后,即可根据elementary_PID和stream_type来确定要过滤哪些含有基本流的TS包,以及这些包里面是什么类型的数据。这样,机顶盒就可以过滤出相应的节目(视频和音频数据)来收看了。
TS总结:
TS解包流程就是现在TS包的包头解出来PAT的PID,然后根据PID找到PAT,并从PAT中解出来每个节目所对应的PMT的PID,再根据PID找到所有节目的PMT,然后从每个节目的PMT中解出来当前节目所对应的不同流类型的TS包的PID,根据这些PID来找到对应的TS包,取出原始视频流,音频流和其他数据等;打包过程则是相反的;
TS头里面的PCR字段是基准时间戳,在音视频解码显示的时候,是根据PES头里面的PTS和DTS字段与其对比,相同就说明该进行解码和显示了;PCR字段是在TS的PMT中指定的PID,只有指定的PID的TS包里面的PCR字段才有用,我们打包的时候使用的是视频的PID中的PCR,只有每帧的第一包TS头里面才会有PCR,而PES头里面的PTS和DTS就是视频和音频的相对时间戳;测试遇到了音视频不同步的问题,原因就是TS打包时,PES头里面的音视频PTS都用了视频的时间戳,而我们在TS解析时是对音频有相对延后的操作,其采用的视频时间戳相对原来是有可能延后了多个视频帧的,所以导致音频有延后;
视频编解码概念:时间戳DTS和PTS的相关分析
http://blog.csdn.net/soaringlee_fighting/article/details/70941896
基本概念:
I frame :帧内编码帧 又称intra picture,I 帧通常是每个 GOP(MPEG 所使用的一种视频压缩技术)的第一个帧,经过适度地压缩,做为随机访问的参考点,可以当成图象。I帧可以看成是一个图像经过压缩后的产物。
P frame: 前向预测编码帧 又称predictive-frame,通过充分将低于图像序列中前面已编码帧的时间冗余信息来压缩传输数据量的编码图像,也叫预测帧;
B frame: 双向预测内插编码帧 又称bi-directional interpolated prediction frame,既考虑与源图像序列前面已编码帧,也顾及源图像序列后面已编码帧之间的时间冗余信息来压缩传输数据量的编码图像,也叫双向预测帧;
PTS:Presentation Time Stamp。PTS主要用于度量解码后的视频帧什么时候被显示出来
DTS:Decode Time Stamp。DTS主要是标识读入内存中的bit流在什么时候开始送入解码器中进行解码。
在没有B帧存在的情况下DTS的顺序和PTS的顺序应该是一样的。
IPB帧的不同:
I frame:自身可以通过视频解压算法解压成一张单独的完整的图片。
P frame:需要参考其前面的一个I frame 或者B frame来生成一张完整的图片。
B frame:则要参考其前一个I或者P帧及其后面的一个P帧来生成一张完整的图片。
两个I frame之间形成一个GOP,在x264中同时可以通过参数来设定bf的大小,即:I 和p或者两个P之间B的数量。
通过上述基本可以说明如果有B frame 存在的情况下一个GOP的最后一个frame一定是P.
DTS和PTS的不同:
DTS主要用于视频的解码,在解码阶段使用.PTS主要用于视频的同步和输出.在display的时候使用.在没有B frame的情况下.DTS和PTS的输出顺序是一样的.
例子:
下面给出一个GOP为15的例子,其解码的参照frame及其解码的顺序都在里面:
如上图:I frame 的解码不依赖于任何的其它的帧.而p frame的解码则依赖于其前面的I frame或者P frame.B frame的解码则依赖于其前的最近的一个I frame或者P frame 及其后的最近的一个P frame.
FFmpeg里有两种时间戳:DTS(Decoding Time Stamp)和PTS(Presentation Time Stamp)。 顾名思义,前者是解码的时间,后者是显示的时间。要仔细理解这两个概念,需要先了解FFmpeg中的packet和frame的概念。
FFmpeg中用AVPacket结构体来描述解码前或编码后的压缩包,用AVFrame结构体来描述解码后或编码前的信号帧。 对于视频来说,AVFrame就是视频的一帧图像。这帧图像什么时候显示给用户,就取决于它的PTS。DTS是AVPacket里的一个成员,表示这个压缩包应该什么时候被解码。 如果视频里各帧的编码是按输入顺序(也就是显示顺序)依次进行的,那么解码和显示时间应该是一致的。可事实上,在大多数编解码标准(如H.264或HEVC)中,编码顺序和输入顺序并不一致。 于是才会需要PTS和DTS这两种不同的时间戳。
PTS - Presentation Timestamp,播放的时间戳。
可以简单地这样理解:
若视频没有B帧,则I和P都是解码后即刻显示。
若视频含有B帧,则I是解码后即刻显示,P是先解码后显示,B是后解码先显示。(B 和P的先、后是相对的)。
TS码流结构分析
http://blog.163.com/elvis1943@126/blog/static/638916982015615101821857/
ES流(Elementary Stream)由三部分组成:
※经MPEG-2视频编码器编码后的图像数据流;
※经MPEG-2音频编码器编码后的声音数据流;
※其他编码数据流;
PES流(Packetized Elementary Stream):PES流是ES流经过PES打包器处理后形成的数据流,在这个过程中完成了将ES流分组、打包、加入包头信息等操作(对ES流的第一次打包)。PES流的基本单位是PES包。
※PS流和TS流是MPEG-2系统规范的两种标准码流。
※PS流用于相对无错环境下的传输与存储(如DVD中),其基本单位是PS包,长度可变。
※TS流用于相对有错环境下的传输与存储(如DVB中),其基本单位是TS包,长度固定188字节。
※ PS流由PS包组成,而一个PS包又由若干个PES包组成(到这里,ES经过了两层的封装)。
※ PS包的包头中包含了同步信息与时钟恢复信息。
※一个PS包最多可包含具有同一时钟基准的16个视频PES包和32个音频PES包。
※ TS流由定长的TS包组成(188字节),而TS包是对PES包的一个重新封装(到这里,ES经过了两层的封装) 。
※ PES包的包头信息依然存在于TS包中。
链接: https://wenku.baidu.com/view/cd092f03a8956bec0875e337.html
https://my.oschina.net/u/727148/blog/666824
http://blog.sina.com.cn/s/blog_6b94d5680101r5l6.html
http://blog.csdn.net/zxh821112/article/details/17587215
http://www.cnblogs.com/hjj801006/p/3837435.html
http://blog.csdn.net/zxh821112/article/details/17587325
TS 数据流分析学习的更多相关文章
- TS数据流PAT和PMT分析(转载)
转自:http://www.cnblogs.com/hjj801006/p/3837435.html TS流,是基于packet的位流格式,每个packet是188个字节或者204个字 节(一般是18 ...
- TS数据流PAT和PMT分析
TS流,是基于packet的位流格式,每个packet是188个字节或者204个字节(一般是188字节,204字节格式是在188字节的packet后面加上16字节的CRC数据,其他格式相同),解析TS ...
- 传送流(TS)的基础知识
数字电视的TS包和TS流的组成和功能 综合考虑几下几个因素: (1)包的长度不能过短,否则包头开销所占比例过大, 导致传输效率下降 (2)包的长度不能过长,否则在丢失同步的情况下恢复同步的 周期过长, ...
- 【腾讯bugly干货分享】HTML 5 视频直播一站式扫盲
本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://bugly.qq.com/bbs/forum.php?mod=viewthread&tid=1277 视频直 ...
- 【腾讯Bugly干货分享】H5 视频直播那些事
本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57a42ee6503dfcb22007ede8 Dev Club 是一个交流移动 ...
- 从0到1打造直播 App
转自http://dev.qq.com/topic/5811d42e7fd6ec467453bf58 概要 分享内容: 互联网内容载体变迁历程,文字——图片/声音——视频——VR/AR——……..从直 ...
- 【腾讯Bugly干货分享】从0到1打造直播 App
本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/5811d42e7fd6ec467453bf58 作者:李智文 概要 分享内容: ...
- H5 视频直播相关技术
一.移动视频直播发展 大家首先来看下面这张图: 可以看到,直播从 PC 到一直发展到移动端,越来越多的直播类 App 上线,同时移动直播进入了前所未有的爆发阶段,但是对于大多数移动直播来说,还是要以 ...
- HLS 协议
HTML 5 视频直播一站式扫盲 本文来自于腾讯bugly开发者社区,原文地址:http://bugly.qq.com/bbs/forum.php?mod=viewthread&tid=1 ...
随机推荐
- centos7 centos6中 更改默认的系统启动级别
centos6中更改默认的启动级别 方法: 1.vi /etc/inittab 2.找到id:x:initdefault:,我的系统是id:3:initdefault:,即默认以字符模式启动. 3.将 ...
- chromium源码阅读--V8 Embbeding
V8是google提供高性能JavaScript解释器,嵌入在chromium里执行JavaScript代码. V8本身是C++实现的,所有嵌入本身毫无压力,一起编译即可,不过作为一个动态语言解释器, ...
- linux基本操作1
ctrl + alt + T 打开命令行 -根目录下home中为用户建的文件夹 cd 加目录名称转到当前目录 .当前目录..上级目录 ls 当前目录下的文件ls -l 显示当前目录下文件的权限 mkd ...
- http短连接大量time wait解决方案
tcp连接是网络编程中最基础的概念,基于不同的使用场景,我们一般区分为“长连接”和“短连接”,长短连接的优点和缺点这里就不详细展开了,有心的同学直接去google查询,本文主要关注如何解决tcp短连接 ...
- PAT 1055 集体照
https://pintia.cn/problem-sets/994805260223102976/problems/994805272021680128 拍集体照时队形很重要,这里对给定的 N 个人 ...
- 基于网络的 Red Hat 无人值守安装
基于网络的 Red Hat 无人值守安装 本文介绍了 PC 平台上的一种快速 Red Hat Linux 安装方案.它具有很高的自动化程度--用户只需手工启动机器并选择从网络启动,就可以完成整个安装过 ...
- 算法(10)Subarray Sum Equals K
题目:在数组中找到一个子数组,让子数组的和是k. 思路:先发发牢骚,这两天做题是卡到不行哇,前一个题折腾了三天,这个题上午又被卡住,一气之下,中午睡觉,下午去了趟公司,竟然把namespace和cgr ...
- perf 对两个map是否重叠的判断,以及函数map_groups__fixup_overlappings代码逻辑
该标题可以抽象出来的问题是:两个前开后闭的区间 rangeA 和 rangeB,如何判断这两个区间是否重叠.这个问题在内核中非常重要,虚拟地址空间的划分需要它,perf中map_group的构建也需要 ...
- Visual Source Safe的使用方法
VSS 的全称为 Visual Source Safe .作为 Microsoft Visual Studio 的一名成员,它主要任务就是负责项目文件的管理,几乎可以适用任何软件项目.管理软件开发中各 ...
- table表头固定问题
table表头固定问题 原生的table表头在表格滚动时候无法固定,可以使用以下的方法进行模拟 1. 双table法 表头和表体各用一个table,这样会产生表格列对不齐的问题,可以使用colgrou ...