一、TS HEADER
参考
TS科普 2 包头
TS流格式学习
Ts流解析中难点说明
百度文库 最直白明了的TS流分析
在jsmpeg系列二 TS码流 PAT PMT有提到TS header的结构,下面重点介绍其中几个。
名称 | 长度 | 说明 |
---|---|---|
sync_byte | 8bit | 同步字节,固定为0x47 |
transport_error_indicator | 1bit | 传输错误指示符,表明在ts头的adapt域后由一个无用字节,通常都为0,这个字节算在adapt域长度内 |
payload_unit_start_indicator | 1bit | 负载单元起始标示符,一个完整的数据包开始时标记为1 |
transport_priority | 1bit | 传输优先级,0为低优先级,1为高优先级,通常取0 |
pid | 13bit | pid值(Packet ID号码,唯一的号码对应不同的包) |
transport_scrambling_control | 2bit | 传输加扰控制,00表示未加密 |
adaptation_field_control | 2bit | 是否包含自适应区,‘00’保留;‘01’为无自适应域,仅含有效负载;‘10’为仅含自适应域,无有效负载;‘11’为同时带有自适应域和有效负载。 |
continuity_counter | 4bit | 递增计数器,从0-f,起始值不一定取0,但必须是连续的 |
1.sync_byte 0x47
用UltraEdit打开的一个TS流,我们发现每隔188个字节就有一个47(可以看做是包头)
2.transport_error_indicator
错误指示位,如果为1,表明该包有错误。
3.payload_unit_start_indicator起始符
要说明的是一个Ts包(188字节)往往是放不下一个PES包的,那就需要截取发送,那么截取出来的包中,肯定有的是含有包头的,但是有的是不含有包头的, 这个区分是靠这个字段的。所以在解析的时候,如果他置1,那么他后面的就是一个包头,既然是包头,那就可以进一步的解析。
-
该TS包的payload为PES:如果标志位为1,则该TS包的payload第一个Byte是某一PES包的第一个byte;如果标志位为0,则没有PES包从该TS包开始。
-
该TS包的payload为PSI/SI:如果标志位为1,则该TS包的第一个byte是pointer_field,pointer_field指向某一PSI section的第一个byte开始位置;如果标志位为0,则不存在pointer_field且此TS包不包含任何PSI section的第一个byte
以上两种情况,先说一下第二种情况。在jsmpeg系列二 TS码流 PAT PMT解析PAT部分中,曾经出现过包头为47 40 00 1C 00 00 B0 1D...
解析PID=0x0000,这说明是PAT表。然后payload_unit_start_indicator=1,说明在包头后需要除去一个字节才是有效数据。当时并未说明原因,现在看起来这个00正是第二种情况提到的pointer_field(程序特殊信息指针)。读到这个pointer_field=0后,需要跳0后,才是PSI section的第一个byte开始位置,这相当于没有跳,所以说只需要去除pointer_field本身这个字节,就是有效数据了。
4.adaptation_field_control
指出TS packet header后是否跟adaption field 和/或 payload.
- 00 reserved for future use by ISO/IEC
- 01 no adaptation_field,payload only
- 10 adaptation_field only,no payload
- 11 adaptation_field followed by payload
结论:首先应该通过adaption_field_control 调整字段来确认,有没有调整字段,有跳过(获取调整字段里面的length),在确认了调整字段后指针已经指向了有效负荷区。
ts.js中parsePacket方法有如下片断做印证:
// Extract current payload
if (adaptationField & 0x1) {
if ((adaptationField & 0x2)) {
var adaptationFieldLength = this.bits.read(8);
this.bits.skip(adaptationFieldLength << 3);
}
...
首先,& 0x1
的判断,就滤掉了00 和 10这两种情况,也就是说没有payload后面就不用执行了。然后& 0x2
就是锁定11这种情况,即payload前面还有adaptation_field。根据上面的图,可以this.bits.read(8)获得adaptationFieldLength,然后跳过去。
5.continuity_counter
连续计数器。相同PID的TS包此值比前一个包增加1,达到最大值15后从0开始。但若adaptation_field_control的值为00或10时不增1。另外,重复包(duplicate packet)也不增1。重复包是指除了PCR(如果有)以外整个包都与前一个包一样的包。
如果除了以上特殊情况,还出现不连续,说明有包丢失。
6.payload
Payload包括PES\PSI/SI,但同一packet只包含PES或PSI/SI。PES以0x000001开始,如果开始字节不是0x000001,则内容是PSI,PSI部分以1个字节的pointer_field开始。
if (transport_packet_header.adaptation_field_control & 0x02)// 10 11
//10’仅含调整字段,无有效负载;‘11’调整字段后为 有效负载。
{
size = * buff + 1; //adaptation_field(buff);
buff += size; // 跳过调整字段
leng -= size; // 剩下的包长度
}
if (transport_packet_header.adaptation_field_control & 0x01)
{ // 01 只有有效负载
if (buff[0] == 0x00 && buff[1] == 0x00 && buff[2] == 0x01)
{//pes包的包头是 0x 00 00 01
//log("dvbstrPESstream_ID");
pes_packet(buff, leng,& transport_packet_header);
}
else {
//PSI 数据。
//核心点:这里的pointer只在PES或者PSI的开始包中有,
//其大小为8位,其值为从这里到真正有效负载开始的距离,
//而是不是开始包的判断哪当然是依靠payload_unit_start_indicato字段。
int pointer = * buff + 1;
// printf("zhangfeionline__%d\n",pointer);
if (transport_packet_header.payload_unit_start_indicator)
// 是开始包,那么里面就包含了一个字节的
// pointer_field(程序特殊信息指针),需要解析出跳过
{
buff += pointer;
leng -= pointer;
}
// PSI
...
再来个例子,有个188bytes的TS包是47 40 10 37 01 00 00 40...
:
(1)ts header 47 40 10 37,解析出来PID=0x010,说明是个NIT表,adaptation_field_control=11b,说明adaptation_field followed by payload
(2)然后去读adaptationFieldLength,读到了第5个字节01,也就是说要跳1字节才能到达payload。把第6字节00跳过,来到了第7字节00
(3)payload_unit_start_indicator=1,并且该表是NIT,属于PSI第二种情况,第一个字节是pointer_field,即第7字节00,不用跳了,第8字节40即NIT section。
二、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值 |
关于pes start code开始码,固定为0x000001,可以参考buffer.js的findStartCode方法。
关于stream id,音频取值(0xc0-0xdf),通常为0xc0;视频取值(0xe0-0xef),通常为0xe0,可以参考ts.js常量
TS.STREAM = {
PACK_HEADER: 0xBA,
SYSTEM_HEADER: 0xBB,
PROGRAM_MAP: 0xBC,
PRIVATE_1: 0xBD,
PADDING: 0xBE,
PRIVATE_2: 0xBF,
AUDIO_1: 0xC0,
VIDEO_1: 0xE0,
DIRECTORY: 0xFF
};
以下参考TS协议解析第三部分(PES),对第1个PES包47 48 14 10 00 00 01 C0 01 88 80 80 05 21 00 01 96 07 FF FD 85 00 33 22...
解析
1.ts header
47 48 14 10
解析
- pid = 0x814,在PMT中查找音频是program_map_PID为0x814
- payload_unit_start_indicator=1,有包头,也就是帧头
- adaptation_field_control=01,no adaptation_field,payload only
- continuity_counter=0000
2.pes start code
找到了00 00 01起始码
3.stream id
47 48 14 10 00 00 01 C0
4.pes packet length
0x01 88,即十进制的392。也就是这帧长度是392字节。
5.flag
80:1000 0000
10:默认规定
00:PES加扰控制
0:PES优先级
0:数据定位指示符
0:版权
0:原始的或复制的
6.flag
80:1000 0000
10:PTS_DTS_flags,10代表后面将会有PTS信息。
000000:分别代表其他6个标志,0表示后面没有对应的信息。
7.pes data length 05
PES头数据长度,表示后面还有0x05个字节,之后就是一帧的数据内容。
PES头数据具体包含哪些内容有前面的标志位来确定,哪些信息得标志位1,就包含哪些信息。排列顺序分别是PTS DTS ESCR ES速率 DSM特技方式 附件的复制信息 前PES的CRC PES 扩展,如果还有多余的字节没用,就用填充字节0xFF填充。本例子中,PES头数据只包含PTS数据。
8.pts
21 00 01 96 07:5个字节总共40位
9.帧数据
从96 07后面的FF FD 85 00 33 22...
这些都是MP3格式数据。
10.上面解析完第1个PES包后,又找到47开头的第二个PES包,如上图。对ts header47 08 14 11
解析
- pid = 0x814
- payload_unit_start_indicator = 0 表示不是帧头,不含PES包头数据,只有PES负载(PES负载就是一帧数据)
- adaptation_field_control=01,no adaptation_field,payload only
- continuity_counter=0001
11.帧数据
第二个PES包,去除包头后,68 4D 8C...
全是MP3格式数据。
三、解析流程
1.TS.prototype.resync
不复制源码了,大概是遍历this.bits,以0x47为开头,去找到5个连续的包。
2.TS.prototype.parsePacket
这个方法上面已经解析了一部分,再过一下。先调用resync去同步,然后读取Ts header.判断出有payload后,通过nextBytesAreStartCode去找PES开始码0x000001,找到了就把这几个数跳过去this.bits.skip(24)
。
下面的数据就是PES包解析了,参考本文第二部分的格式说明。
3.计算PTS
在jsmpeg系列二 TS码流 PAT PMT有提到PTS的概念,但是没有写具体算法。ts.js中这段代码是有点奇怪的:
// The Presentation Timestamp is encoded as 33(!) bit
// integer, but has a "marker bit" inserted at weird places
// in between, making the whole thing 5 bytes in size.
// You can't make this shit up...
this.bits.skip(4);
var p32_30 = this.bits.read(3);
this.bits.skip(1);
var p29_15 = this.bits.read(15);
this.bits.skip(1);
var p14_0 = this.bits.read(15);
this.bits.skip(1);
// Can't use bit shifts here; we need 33 bits of precision,
// so we're using JavaScript's double number type. Also
// divide by the 90khz clock to get the pts in seconds.
pts = (p32_30 * 1073741824 + p29_15 * 32768 + p14_0)/90000;
这里搜索到向高手请教MPEG2码流(TS流)系列问题一:PTS怎么用,原文如下
摘录一段《13818-1》 P65页里面,对PTS、DTS都有的情形:
if (PTS_DTS_flags ==‘11’ ) {
'0011' 4 bslbf
PTS [32..30] 3 bslbf
marker_bit 1 bslbf
PTS [29..15] 15 bslbf
marker_bit 1 bslbf
PTS [14..0] 15 bslbf
marker_bit 1 bslbf
'0001' 4 bslbf
DTS [32..30] 3 bslbf
marker_bit 1 bslbf
DTS [29..15] 15 bslbf
marker_bit 1 bslbf
DTS [14..0] 15 bslbf
marker_bit 1 bslbf
}
从上面摘录可见,PTS和DTS的格式相同,都是由一个3 bits和两个15 bits组成,之间用两个1 bit的“marker_bit”分开。刚好经过一下午暴搜,再加上MPEG-2 TS packet analyser软件的帮助,我也大概了解到,闹了半天PTS/DTS就是一个33 bits的整形数,那中间的“marker_bit”木有用,是用来跳过的。
4.detect if the PES packet is complete 这一段参考注释,没细看
if (streamId) {
// Attempt to detect if the PES packet is complete. For Audio (and
// other) packets, we received a total packet length with the PES
// header, so we can check the current length.
// For Video packets, we have to guess the end by detecting if this
// TS packet was padded - there's no good reason to pad a TS packet
// in between, but it might just fit exactly. If this fails, we can
// only wait for the next PES header for that stream.
var pi = this.pesPacketInfo[streamId];
if (pi) {
var start = this.bits.index >> 3;
var complete = this.packetAddData(pi, start, end);
var hasPadding = !payloadStart && (adaptationField & 0x2);
if (complete || (this.guessVideoFrameEnd && hasPadding)) {
this.packetComplete(pi);
}
}
}
5.packetComplete
TS.prototype.packetComplete = function(pi) {
pi.destination.write(pi.pts, pi.buffers);
pi.totalLength = 0;
pi.currentLength = 0;
pi.buffers = [];
};
通过pi.destination把解析好的数据传递出去。
四、ts.js对外部提供的调用
1.write
在player.js中
var Player = function(url, options) {
this.options = options || {};
if (options.source) {
this.source = new options.source(url, options);
options.streaming = !!this.source.streaming;
}
else if (url.match(/^wss?:\/\//)) {
this.source = new JSMpeg.Source.WebSocket(url, options);
options.streaming = true;
}
else if (options.progressive !== false) {
this.source = new JSMpeg.Source.AjaxProgressive(url, options);
options.streaming = false;
}
else {
this.source = new JSMpeg.Source.Ajax(url, options);
options.streaming = false;
}
this.maxAudioLag = options.maxAudioLag || 0.25;
this.loop = options.loop !== false;
this.autoplay = !!options.autoplay || options.streaming;
this.demuxer = new JSMpeg.Demuxer.TS(options);
this.source.connect(this.demuxer);
...
这里指定了几种不同的source,当然也可以在options中自定义。牵涉到三个类:
- websocket.js
- ajax-progressive.js
- ajax.js
progressive -
whether to load data in chunks (static files only).When enabled, playback can begin before the whole source has been completely loaded. Default true.
这三个类都提供了一致的接口,提供给player.js调用。比如上面的this.source.connect(this.demuxer);
,还有后面的this.source.start();
ajax的两个方式都是以XMLHttpRequest下载数据,这里细节不上源码了,仅以websocket.js为例:
WSSource.prototype.connect = function(destination) {
this.destination = destination;
};
WSSource.prototype.onMessage = function(ev) {
if (this.destination) {
this.destination.write(ev.data);
}
};
可以看到connect方法把JSMpeg.Demuxer.TS给传入到不同的source里。最终在收到二进制数据后,转交给JSMpeg.Demuxer.TS的write方法去处理。
2.connect
在player.js中
if (options.video !== false) {
this.video = new JSMpeg.Decoder.MPEG1Video(options);
this.renderer = !options.disableGl && JSMpeg.Renderer.WebGL.IsSupported()
? new JSMpeg.Renderer.WebGL(options)
: new JSMpeg.Renderer.Canvas2D(options);
this.demuxer.connect(JSMpeg.Demuxer.TS.STREAM.VIDEO_1, this.video);
this.video.connect(this.renderer);
}
if (options.audio !== false && JSMpeg.AudioOutput.WebAudio.IsSupported()) {
this.audio = new JSMpeg.Decoder.MP2Audio(options);
this.audioOut = new JSMpeg.AudioOutput.WebAudio(options);
this.demuxer.connect(JSMpeg.Demuxer.TS.STREAM.AUDIO_1, this.audio);
this.audio.connect(this.audioOut);
}
可以看出,demuxer把视频和音频的解码器给连接起来。
TS.prototype.connect = function(streamId, destination) {
this.pesPacketInfo[streamId] = {
destination: destination,
currentLength: 0,
totalLength: 0,
pts: 0,
buffers: []
};
};
3.write
TS.prototype.write = function(buffer) {
if (this.leftoverBytes) {
var totalLength = buffer.byteLength + this.leftoverBytes.byteLength;
this.bits = new JSMpeg.BitBuffer(totalLength);
this.bits.write([this.leftoverBytes, buffer]);
}
else {
this.bits = new JSMpeg.BitBuffer(buffer);
}
while (this.bits.has(188 << 3) && this.parsePacket()) {}
var leftoverCount = this.bits.byteLength - (this.bits.index >> 3);
this.leftoverBytes = leftoverCount > 0
? this.bits.bytes.subarray(this.bits.index >> 3)
: null;
};
上述代码188<<3相当于188*8,即判断只要还有完整的TS包,就一直parsePacket。如果没有完整包,则把剩余的数据放入leftoverBytes,在下次执行write写入时,把数据拼起来继续解析。
4.总结
全文至此,已经看到parsePacket最终用传入的destination继续解析了。destination一个是JSMpeg.Decoder.MPEG1Video,另一个是JSMpeg.Decoder.MP2Audio,后续系列文章将会看一下这两个类。
五、TS 流解码过程概述
参考
TS流解码分析之I,P,B帧以及PTS,DTS
TS文件解析流程
打包TS流
将H264与AAC打包Ipad可播放的TS流的总结
- 获取TS中的PAT,从PAT表里面找到所有的PMT表的map_id。
- 注意1:PAT表并不一定在文件的起始位置,TS流这种对于电视直播的Live流需要保证在任何时间打开电视你都能看到画面,所以PAT表是被随机插到TS流的Packet中的,比如间隔10帧插一个PAT表和PMT表。所以TS流文件的第一个TS Packet可能是一个PES包,但是这个PES包更可能是续包,它没有解码器需要的Header,所以这种包可以在播放中被忽略,因为它可能是录制前一帧的I、P、B包的一个断包,根本解码不出数据;
- 注意2:记得检测PAT中的current_next_indicator这个flag,如果这个flag被置1,则忽略本次读到的这个PAT包,继续往下搜索PAT包;
- 注意3:如果PAT包因为容纳的PMT的map_id很多,一个TS Packet的188个字节或许放不完,则last_section_number不是0了,你得根据当前的section_number(第一个是0),然后不断的搜索下去,把TS Packet去掉头后的数据组合成一个完整的PAT表;
- 获取TS中的PMT,建立流id表。
在通过PAT表找到所有的PMT表的id后,则需要开始继续跑文件,查找PMT表了,一般情况下,PMT表在TS文件中的位置跟在PAT表的后面,但是也有不同,所以我推荐在查找PAT表完成后,把指针Seek到文件的0位置,从头开始查找PMT表。这样可能能更快的找到PMT表也说不定,当然你用当前的位置继续向下找PMT表也是没问题的。
- 注意1:PMT表也有跟PAT表一样的分段特性,一样检查last_section_number这个是不是有情况。也有current_next_indicator的特性,都得检查;
- 注意2:当PAT表里提供了多张PMT表的id后,则表明文件是一个多视频、多音频流混合的文件;
- 根据PMT可以知道当前网络中传输的视频(音频)类型(H264),相应的PID,PCR的PID等信息。
在搜索完所有PMT表后,保存其中的流类型和流id,此时我们有一张表,表里保存了所有的视频流id和音频流id,下面我们把文件指针Seek到0,我们开始一点点的查找TS Packet。在这之前有一些需要注意的地方:
- 确定你要播放的视频和音频流:因为文件中可能有多个视频、音频流,并且这些流的编码也不同,比如日本的电视在播放时会用1080i的MPEG2和240P+360P的H264同时传输,这样录制下来的TS流则会有3个视频流(id),并且音频也是传输3条,也就是有6条流,但是我们在PC或者碟机中播放的时候,一般都是播放一条视频和一条音频,则我们必需根据用户选择播放那条视频和音频(如果你希望让用户选择的话),比如我们希望播放MPEG2的视频,所以在不断的跑读TS Packet的过程中,我们要忽略掉除了MPEG2流的视频id,那些全部Skip即可,音频同理。
- 如何查找一个音频\视频帧的头,以及它的长度:这个问题也比较简单,在跑TS Packet的过程中,找到PES包,如果TS头表明payload_unit_start_indicator为1,则这个PES包此流id的某一帧起始包,去掉PES头后的ES流就是编码后的流的起始数据。而后面的针对这条流的PES包,只要没有payload_unit_start_indicator标志,都是这个包的续包,这些续包把头去掉后,跟上一个包的数据组合起来,就一个编码后的ES数据。
这里有一个需要注意的,在找到一个包表明它是payload_unit_start_indicator后,往下查找可能会查找到其他流id的payload_unit_start_indicator的PES包。。。要分别组合。
- 设置demux 模块的视频Filter 为相应视频的PID和stream type等。
- 从视频Demux Filter 后得到的TS数据包中的payload 数据就是 one piece of PES,在TS header中有一些关于此 payload属于哪个 PES的 第多少个数据包。
- 拼接好的PES包的包头会有 PTS,DTS信息,去掉PES的header就是 ES。PTS,DTS信息在 pes头部,当PTS_DTS_flag = ‘10’时,有PTS,当是‘11’时,PTS,DTS都有。
- 直接将ES包送给decoder就可以进行解码。解码出来的数据就是一帧一帧的视频数据,这些数据至少应当与PES中的PTS关联一下,以便进行视音频同步。
- I,B,P 帧就在ES中,通过picture_header()的picture_start_code来辨别是哪个帧。
网友评论