美文网首页
jsmpeg系列三 源码buffer.js对Uint8Array

jsmpeg系列三 源码buffer.js对Uint8Array

作者: 合肥黑 | 来源:发表于2018-09-28 13:38 被阅读127次
一、回顾

1.在jsmpeg系列一 基础知识 字节序 ArrayBuffer TypedArray中,Uint8Array是针对byte(也就是8bit)操作,当我们想针对bit读取时,就需要对Uint8Array再次封装了。

比如,在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,但必须是连续的

看一下jsmpeg源码中的ts.js有这样一段:

    var transportError = this.bits.read(1),
        payloadStart = this.bits.read(1),
        transportPriority = this.bits.read(1),
        pid = this.bits.read(13),
        transportScrambling = this.bits.read(2),
        adaptationField = this.bits.read(2),
        continuityCounter = this.bits.read(4);

基本上,连变量名都一样,很明显是在读ts header。并且read(13)这样的参数,传入的是要读取的bit数量

二、先看一下buffer.js的写入

1.appendSingleBuffer

BitBuffer.prototype.appendSingleBuffer = function(buffer) {
    buffer = buffer instanceof Uint8Array
        ? buffer 
        : new Uint8Array(buffer);
    
    this.bytes.set(buffer, this.byteLength);
    this.byteLength += buffer.length;
};

这里可以回顾length和byteLength的区别,一个是成员总个数,一个是字节总长度,字节总长度=成员总个数*每个成员占用的字节长度。本类使用的是Uint8Array,意味着与arraybuffer的单位一致,即length和byteLength相同。

2.write

BitBuffer.prototype.write = function(buffers) {
    var isArrayOfBuffers = (typeof(buffers[0]) === 'object'),
        totalLength = 0,
        available = this.bytes.length - this.byteLength;

    // Calculate total byte length
    if (isArrayOfBuffers) {
        var totalLength = 0;
        for (var i = 0; i < buffers.length; i++) {
            totalLength += buffers[i].byteLength;
        }
    }
    else {
        totalLength = buffers.byteLength;
    }

    // Do we need to resize or evict?
    if (totalLength > available) {
        if (this.mode === BitBuffer.MODE.EXPAND) {
            var newSize = Math.max(
                this.bytes.length * 2,
                totalLength - available
            );
            this.resize(newSize)
        }
        else {
            this.evict(totalLength);
        }
    }

    if (isArrayOfBuffers) {
        for (var i = 0; i < buffers.length; i++) {
            this.appendSingleBuffer(buffers[i]);
        }
    }
    else {
        this.appendSingleBuffer(buffers);
    }
};

这里先判断剩下的空间够不够,如果不够,要么resize,要么evict

3.MODE
搜索代码,可以看到MODE与options.streaming有关

    var bufferSize = options.videoBufferSize || 512*1024;
    var bufferMode = options.streaming
        ? JSMpeg.BitBuffer.MODE.EVICT
        : JSMpeg.BitBuffer.MODE.EXPAND;

    this.bits = new JSMpeg.BitBuffer(bufferSize, bufferMode);

options.streaming在默认情况下,只有websocket时才为true。也就是使用了EVICT模式。

    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;
    }

4.this.resize(newSize)
resize就是var newBytes = new Uint8Array(size);,简单粗暴地扩大容量。并且使用this.index = Math.min(this.index, this.byteLength << 3);把index标记放到了末尾。说明一下,index变量是对应bit的一个标记,可以参见read方法:

BitBuffer.prototype.read = function(count) {
    var value = this.peek(count);
    this.index += count;
    return value;
};

上面提到pid = this.bits.read(13)读取ts header的pid,可以看到每读取count位后,index就会右移count个位置。

5.this.evict(totalLength)

BitBuffer.prototype.evict = function(sizeNeeded) {
    var bytePos = this.index >> 3,
        available = this.bytes.length - this.byteLength;
    
    // If the current index is the write position, we can simply reset both
    // to 0. Also reset (and throw away yet unread data) if we won't be able
    // to fit the new data in even after a normal eviction.
    if (
        this.index === this.byteLength << 3 ||
        sizeNeeded > available + bytePos // emergency evac
    ) {
        this.byteLength = 0;
        this.index = 0;
        return;
    }
    else if (bytePos === 0) {
        // Nothing read yet - we can't evict anything
        return;
    }
    
    // Some browsers don't support copyWithin() yet - we may have to do 
    // it manually using set and a subarray
    if (this.bytes.copyWithin) {
        this.bytes.copyWithin(0, bytePos, this.byteLength);
    }
    else {
        this.bytes.set(this.bytes.subarray(bytePos, this.byteLength));
    }

    this.byteLength = this.byteLength - bytePos;
    this.index -= bytePos << 3;
    return;
};

这里第一个if判断意思是,如果数据已经全部读过了,很好办,直接把数据清干净,会在appendSingleBuffer写入新数据。这里我的疑问是appendSingleBuffer最终还是调用set方法,那么仍然写不下怎么办,会不会报错 Uncaught RangeError: Source is too large at Uint8Array.set (<anonymous>)

第一个if判断,还有个或条件,新来的数据长度大于未读数据加上剩余空间,也是全清空,即使还有数据未读完,未读数据也不要了。

但是,在下面的else if中,如果发现bypePos==0,即数据完全没读过,则什么也不做,不清数据了,相当于evict直接return。这里也有疑问,什么也不做的话,appendSingleBuffer写入会不会报错。

再往下处理,就是数据读了一部分,把剩下未读的用subarray截取出来,重新set回this.bytes的起点上。这里也能看出EVICT模式,是在写入新数据时,不断清除读取过的数据,避免内存占用太大。

videoBufferSize – when streaming, size in bytes for the video decode buffer. Default 512*1024 (512kb). You may have to increase this for very high bitrates.
audioBufferSize – when streaming, size in bytes for the audio decode buffer. Default 128*1024 (128kb). You may have to increase this for very high bitrates.

上面引用的是官方说明,也是使用EVICT模式的两个地方。看来一般情况下是够用的,除非是very high bitrates

三、buffer.js的读取
BitBuffer.prototype.peek = function(count) {
    var offset = this.index;
    var value = 0;
    while (count) {
        var currentByte = this.bytes[offset >> 3],
            remaining = 8 - (offset & 7), // remaining bits in byte
            read = remaining < count ? remaining : count, // bits in this run
            shift = remaining - read,
            mask = (0xff >> (8-read));

        value = (value << read) | ((currentByte & (mask << shift)) >> shift);

        offset += read;
        count -= read;
    }

    return value;
}

这里用一个例子看一下逻辑,假如this.index=11,也就是说在上一次peek中,只读到了this.bytes索引1的8位数据中的第3位。再假设这次要读取2位,即count=2。

1.currentByte = this.bytes[offset >> 3]
这里offset右移3位,相当于除以8,并且取整,这样就拿到了当前offset对应的this.bytes索引。本例offset=11,对应二进制是1011,右移3位变成1,说明当前offset对应的8位数据是this.bytes[1]。拿到currentByte后,下一步目标就是把这个8位数据中的第4位和第5位读出来。

2.remaining = 8 - (offset & 7), // remaining bits in byte
read = remaining < count ? remaining : count, // bits in this run
这里在计算当前索引值里还剩下几位没读,如果剩下的不够count,就直接把剩下的先读完。offset & 7意味着不管offset跑了多远,也只看后三位,也相当于对8求余了。比如11&7=3,说明当前这个8位值,已经读了3位,还剩下5位没读。当前count=2,要读2位,足够用。

3.shift = remaining - read,
mask = (0xff >> (8-read));
mask就是拿到一堆1,1的数量根据要读的read数量来定,除了这堆1之外,其它全部弄成0。这样通过0把原始数据不相干的信息抹掉,通过1把想干数据过滤出来。比如本例中,read=2,那么mask右移了6位后,只剩下2个1了。到目前得到的值:

currentByte=this.bytes[1],
remaining=8-(11&7)=8-3=5
read=2
shift=5-2=3
mask=3

假如currentByte=xxx xxxxx,那么currentByte & (mask << shift)就是xxx xxxxx & 000 11000=000 xx 000,是不是正好把想要的第4位和第5位给过滤出来了。然后((currentByte & (mask << shift)) >> shift),即把000 xx 000再右移3位,这样就剩下000xx了,即我们想要的目标值。

4.offset += read;
count -= read;
这里如果读完了,whilde(count)就退出了,最终的值就是value = (value << read) | ((currentByte & (mask << shift)) >> shift);因为value=0,左半部分可以忽略。

如果没读完,继续循环。假如我们最开始要读的count不是2,而是8。这样第1次循环,会把this.bytes[1]剩余的5位全部读掉。然后执行count -= read后,count=3。即第2次循环,要再读3位。这样的话,第1次读出来的值处于高位,就需要向左移动3位,再把第二次读出来的3位放进来。这就是左半部分(value << read) |的意图。

四、其它方法
//跳过count
BitBuffer.prototype.skip = function(count) {
    return (this.index += count);
};
//回退count,重读
BitBuffer.prototype.rewind = function(count) {
    this.index = Math.max(this.index - count, 0);
};
//还有没有count这么多数据
BitBuffer.prototype.has = function(count) {
    return ((this.byteLength << 3) - this.index) >= count;
};

BitBuffer.prototype.findNextStartCode = function() {    
    for (var i = (this.index+7 >> 3); i < this.byteLength; i++) {
        if(
            this.bytes[i] == 0x00 &&
            this.bytes[i+1] == 0x00 &&
            this.bytes[i+2] == 0x01
        ) {
            this.index = (i+4) << 3;
            return this.bytes[i+3];
        }
    }
    this.index = (this.byteLength << 3);
    return -1;
};

BitBuffer.prototype.findStartCode = function(code) {
    var current = 0;
    while (true) {
        current = this.findNextStartCode();
        if (current === code || current === -1) {
            return current;
        }
    }
    return -1;
};

最后findStartCode方法查找0x000001,可以参考jsmpeg系列四 源码ts.js TS格式解析流程关于PES的分析。

相关文章

网友评论

      本文标题:jsmpeg系列三 源码buffer.js对Uint8Array

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