美文网首页多媒体科技WEB前端开发技术杂谈
jsmpeg系列四 源码ts.js TS格式解析流程

jsmpeg系列四 源码ts.js TS格式解析流程

作者: 合肥黑 | 来源:发表于2018-09-28 15:38 被阅读139次
    一、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
    image.png image.png

    结论:首先应该通过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个PES包
    1.ts header47 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格式数据。

    第2个PES包

    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流的总结

    1. 获取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表;
    1. 获取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后,则表明文件是一个多视频、多音频流混合的文件;
    1. 根据PMT可以知道当前网络中传输的视频(音频)类型(H264),相应的PID,PCR的PID等信息。
    image.png

    在搜索完所有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包。。。要分别组合。
    1. 设置demux 模块的视频Filter 为相应视频的PID和stream type等。
    2. 从视频Demux Filter 后得到的TS数据包中的payload 数据就是 one piece of PES,在TS header中有一些关于此 payload属于哪个 PES的 第多少个数据包。
    3. 拼接好的PES包的包头会有 PTS,DTS信息,去掉PES的header就是 ES。PTS,DTS信息在 pes头部,当PTS_DTS_flag = ‘10’时,有PTS,当是‘11’时,PTS,DTS都有。
    4. 直接将ES包送给decoder就可以进行解码。解码出来的数据就是一帧一帧的视频数据,这些数据至少应当与PES中的PTS关联一下,以便进行视音频同步。
    5. I,B,P 帧就在ES中,通过picture_header()的picture_start_code来辨别是哪个帧。
    image.png

    相关文章

      网友评论

        本文标题:jsmpeg系列四 源码ts.js TS格式解析流程

        本文链接:https://www.haomeiwen.com/subject/fnaeoftx.html