jsmpeg系列三 源码buffer.js对Uint8Array
一、回顾
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的分析。