5351582b by David LaPalomento

Fix loadWord in ExpGolomb and code cleanup

loadWord wasn't incrementing the position in the byte stream so fix it so that the accounting is taken care of. Replace a bunch of single-letter variable names with more descriptive terms. Add some comments. Add tests for exponential golomb parsing.
1 parent b2ed4313
/<!DOCTYPE html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
......
(function(window) {
/**
* Parser for exponential Golomb codes, a variable-bitwidth number encoding
* scheme used by h264.
*/
window.videojs.hls.ExpGolomb = function(workingData) {
var
// the number of bytes left to examine in workingData
workingBytesAvailable = workingData.byteLength,
// the current word being examined
workingWord, // :uint
workingWord = 0, // :uint
// the number of bits left to examine in the current word
workingBitsAvailable; // :uint;
workingBitsAvailable = 0; // :uint;
// ():uint
this.length = function() {
......@@ -21,15 +25,24 @@ window.videojs.hls.ExpGolomb = function(workingData) {
return (8 * workingBytesAvailable) + workingBitsAvailable;
};
this.logStuff = function() {
console.log('bits', workingBitsAvailable, 'word', (workingWord >>> 0));
};
// ():void
this.loadWord = function() {
var
position = workingData.byteLength - workingBytesAvailable,
workingBytes = new Uint8Array(4),
availableBytes = Math.min(4, workingBytesAvailable);
// console.assert(availableBytes > 0);
if (availableBytes === 0) {
throw new Error('no bytes available');
}
workingBytes.set(workingData.subarray(0, availableBytes));
workingBytes.set(workingData.subarray(position,
position + availableBytes));
workingWord = new DataView(workingBytes.buffer).getUint32(0);
// track the amount of workingData that has been processed
......@@ -37,23 +50,23 @@ window.videojs.hls.ExpGolomb = function(workingData) {
workingBytesAvailable -= availableBytes;
};
// (size:int):void
this.skipBits = function(size) {
// (count:int):void
this.skipBits = function(count) {
var skipBytes; // :int
if (workingBitsAvailable > size) {
workingWord <<= size;
workingBitsAvailable -= size;
if (workingBitsAvailable > count) {
workingWord <<= count;
workingBitsAvailable -= count;
} else {
size -= workingBitsAvailable;
skipBytes = size / 8;
count -= workingBitsAvailable;
skipBytes = count / 8;
size -= (skipBytes * 8);
workingData.position += skipBytes;
count -= (skipBytes * 8);
workingBytesAvailable -= skipBytes;
this.loadWord();
workingWord <<= size;
workingBitsAvailable -= size;
workingWord <<= count;
workingBitsAvailable -= count;
}
};
......@@ -63,17 +76,17 @@ window.videojs.hls.ExpGolomb = function(workingData) {
bits = Math.min(workingBitsAvailable, size), // :uint
valu = workingWord >>> (32 - bits); // :uint
console.assert(32 > size, 'Cannot read more than 32 bits at a time');
console.assert(size < 32, 'Cannot read more than 32 bits at a time');
workingBitsAvailable -= bits;
if (0 < workingBitsAvailable) {
if (workingBitsAvailable > 0) {
workingWord <<= bits;
} else {
} else if (workingBytesAvailable > 0) {
this.loadWord();
}
bits = size - bits;
if (0 < bits) {
if (bits > 0) {
return valu << bits | this.readBits(bits);
} else {
return valu;
......@@ -82,18 +95,19 @@ window.videojs.hls.ExpGolomb = function(workingData) {
// ():uint
this.skipLeadingZeros = function() {
var clz; // :uint
for (clz = 0 ; clz < workingBitsAvailable ; ++clz) {
if (0 !== (workingWord & (0x80000000 >>> clz))) {
workingWord <<= clz;
workingBitsAvailable -= clz;
return clz;
var leadingZeroCount; // :uint
for (leadingZeroCount = 0 ; leadingZeroCount < workingBitsAvailable ; ++leadingZeroCount) {
if (0 !== (workingWord & (0x80000000 >>> leadingZeroCount))) {
// the first bit of working word is 1
workingWord <<= leadingZeroCount;
workingBitsAvailable -= leadingZeroCount;
return leadingZeroCount;
}
}
// we exhausted workingWord and still have not found a 1
this.loadWord();
return clz + this.skipLeadingZeros();
this.loadWord();
return leadingZeroCount + this.skipLeadingZeros();
};
// ():void
......
......@@ -105,7 +105,8 @@ hls.FlvTag = function(type, extraData) {
// Rewind to the marker and write the size
if (this.length === adHoc + 4) {
this.length -= 4; // we started a nal unit, but didnt write one, so roll back the 4 byte size value
// we started a nal unit, but didnt write one, so roll back the 4 byte size value
this.length -= 4;
} else if (adHoc > 0) {
nalStart = adHoc + 4;
nalLength = this.length - nalStart;
......@@ -207,13 +208,16 @@ hls.FlvTag = function(type, extraData) {
len = this.length - 11;
// write the DataSize field
this.bytes[ 1] = (len & 0x00FF0000) >>> 16;
this.bytes[ 2] = (len & 0x0000FF00) >>> 8;
this.bytes[ 3] = (len & 0x000000FF) >>> 0;
// write the Timestamp
this.bytes[ 4] = (this.pts & 0x00FF0000) >>> 16;
this.bytes[ 5] = (this.pts & 0x0000FF00) >>> 8;
this.bytes[ 6] = (this.pts & 0x000000FF) >>> 0;
this.bytes[ 7] = (this.pts & 0xFF000000) >>> 24;
// write the StreamID
this.bytes[ 8] = 0;
this.bytes[ 9] = 0;
this.bytes[10] = 0;
......@@ -230,9 +234,9 @@ hls.FlvTag = function(type, extraData) {
};
};
hls.FlvTag.AUDIO_TAG = 0x08; // :uint
hls.FlvTag.VIDEO_TAG = 0x09; // :uint
hls.FlvTag.METADATA_TAG = 0x12; // :uint
hls.FlvTag.AUDIO_TAG = 0x08; // == 8, :uint
hls.FlvTag.VIDEO_TAG = 0x09; // == 9, :uint
hls.FlvTag.METADATA_TAG = 0x12; // == 18, :uint
// (tag:ByteArray):Boolean {
hls.FlvTag.isAudioFrame = function(tag) {
......
......@@ -55,6 +55,19 @@
}
};
/**
* NAL unit
* |- NAL header -|------ RBSP ------|
*
* NAL unit: Network abstraction layer unit. The combination of a NAL
* header and an RBSP.
* NAL header: the encapsulation unit for transport-specific metadata in
* an h264 stream.
* RBSP: raw bit-stream payload. The actual encoded video data.
*
* SPS: sequence parameter set. Part of the RBSP. Metadata to be applied
* to a complete video sequence, like width and height.
*/
this.getSps0Rbsp = function() { // :ByteArray
// remove emulation bytes. Is this nesessary? is there ever emulation
// bytes in the SPS?
......@@ -62,22 +75,20 @@
spsCount = 0,
sps0 = this.sps[0], // :ByteArray
rbspCount = 0,
s, // :uint
e, // :uint
rbsp, // :ByteArray
o; // :uint
s = 1;
e = sps0.byteLength - 2;
rbsp = new Uint8Array(sps0.byteLength); // new ByteArray();
for (o = s ; o < e ;) {
if (3 !== sps0[o + 2]) {
o += 3;
} else if (0 !== sps0[o + 1]) {
o += 2;
} else if (0 !== sps0[o + 0]) {
o += 1;
start = 1, // :uint
end = sps0.byteLength - 2, // :uint
rbsp = new Uint8Array(sps0.byteLength), // :ByteArray
offset = 0; // :uint
// H264 requires emulation bytes (0x03) be dropped to interpret NAL
// units. For instance, 0x8a03b4 should be read as 0x8ab4.
for (offset = start ; offset < end ;) {
if (3 !== sps0[offset + 2]) {
offset += 3;
} else if (0 !== sps0[offset + 1]) {
offset += 2;
} else if (0 !== sps0[offset + 0]) {
offset += 1;
} else {
console.log('found emulation bytes');
......@@ -85,21 +96,22 @@
spsCount += 2;
rbspCount += 2;
if (o > s) {
if (offset > start) {
// If there are bytes to write, write them
rbsp.set(sps0.subarray(0, o - s), rbspCount);
spsCount += o - s;
rbspCount += o - s;
rbsp.set(sps0.subarray(start, offset - start), rbspCount);
spsCount += offset - start;
rbspCount += offset - start;
}
// skip the emulation bytes
o += 3;
s = o;
offset += 3;
start = offset;
}
}
// copy any remaining bytes
rbsp.set(sps0.subarray(spsCount), rbspCount); // sps0.readBytes(rbsp, rbsp.length);
return rbsp;
};
......@@ -122,10 +134,10 @@
frame_mbs_only_flag, // :int
frame_cropping_flag, // :Boolean
frame_crop_left_offset, // :int
frame_crop_right_offset, // :int
frame_crop_top_offset, // :int
frame_crop_bottom_offset, // :int
frame_crop_left_offset = 0, // :int
frame_crop_right_offset = 0, // :int
frame_crop_top_offset = 0, // :int
frame_crop_bottom_offset = 0, // :int
width,
height;
......@@ -202,8 +214,8 @@
frame_crop_bottom_offset = expGolomb.readUnsignedExpGolomb();
}
width = ((pic_width_in_mbs_minus1 +1)*16) - frame_crop_left_offset*2 - frame_crop_right_offset*2;
height = ((2 - frame_mbs_only_flag)* (pic_height_in_map_units_minus1 +1) * 16) - (frame_crop_top_offset * 2) - (frame_crop_bottom_offset * 2);
width = ((pic_width_in_mbs_minus1 + 1) * 16) - frame_crop_left_offset * 2 - frame_crop_right_offset * 2;
height = ((2 - frame_mbs_only_flag) * (pic_height_in_map_units_minus1 + 1) * 16) - (frame_crop_top_offset * 2) - (frame_crop_bottom_offset * 2);
tag.writeMetaDataDouble("videocodecid", 7);
tag.writeMetaDataDouble("width", width);
......@@ -318,20 +330,25 @@
};
// (data:ByteArray, o:int, l:int):void
this.writeBytes = function(data, o, l) {
this.writeBytes = function(data, offset, length) {
var
nalUnitSize, // :uint
s, // :uint
e, // :uint
start, // :uint
end, // :uint
t; // :int
if (l <= 0) {
// default argument values
offset = offset || 0;
length = length || 0;
if (length <= 0) {
// data is empty so there's nothing to write
return;
}
// scan through the bytes until we find the start code (0x000001) for a
// NAL unit and then begin writing it out
// strip NAL start codes as we go
switch (state) {
default:
/* falls through */
......@@ -341,11 +358,11 @@
case 1:
// A NAL unit may be split across two TS packets. Look back a bit to
// make sure the prefix of the start code wasn't already written out.
if (data[o] <= 1) {
if (data[offset] <= 1) {
nalUnitSize = h264Frame ? h264Frame.nalUnitSize() : 0;
if (nalUnitSize >= 1 && h264Frame.negIndex(1) === 0) {
// ?? ?? 00 | O[01] ?? ??
if (1 === data[o] && 2 <= nalUnitSize && 0 === h264Frame.negIndex(2)) {
if (1 === data[offset] && 2 <= nalUnitSize && 0 === h264Frame.negIndex(2)) {
// ?? 00 00 : 01
if (3 <= nalUnitSize && 0 === h264Frame.negIndex(3)) {
h264Frame.length -= 3; // 00 00 00 : 01
......@@ -354,10 +371,10 @@
}
state = 3;
return this.writeBytes(data, o + 1, l - 1);
return this.writeBytes(data, offset + 1, length - 1);
}
if (1 < l && 0 === data[o] && 1 === data[o + 1]) {
if (1 < length && 0 === data[offset] && 1 === data[offset + 1]) {
// ?? 00 | 00 01
if (2 <= nalUnitSize && 0 === h264Frame.negIndex(2)) {
h264Frame.length -= 2; // 00 00 : 00 01
......@@ -366,14 +383,17 @@
}
state = 3;
return this.writeBytes(data, o + 2, l - 2);
return this.writeBytes(data, offset + 2, length - 2);
}
if (2 < l && 0 === data[o] && 0 === data[o + 1] && 1 === data[o + 2]) {
if (2 < length
&& 0 === data[offset]
&& 0 === data[offset + 1]
&& 1 === data[offset + 2]) {
// 00 | 00 00 01
h264Frame.length -= 1;
state = 3;
return this.writeBytes(data, o + 3, l - 3);
return this.writeBytes(data, offset + 3, length - 3);
}
}
}
......@@ -382,45 +402,45 @@
state = 2;
/* falls through */
case 2: // Look for start codes in data
s = o; // s = Start
e = s + l; // e = End
for (t = e - 3 ; o < t ;) {
if (1 < data[o + 2]) {
o += 3; // if data[o + 2] is greater than 1, there is no way a start code can begin before o+3
} else if (0 !== data[o + 1]) {
o += 2;
} else if (0 !== data[o]) {
o += 1;
start = offset;
end = start + length;
for (t = end - 3 ; offset < t ;) {
if (1 < data[offset + 2]) {
offset += 3; // if data[offset + 2] is greater than 1, there is no way a start code can begin before offset+3
} else if (0 !== data[offset + 1]) {
offset += 2;
} else if (0 !== data[offset]) {
offset += 1;
} else {
// If we get here we have 00 00 00 or 00 00 01
if (1 === data[o + 2]) {
if (o > s) {
h264Frame.writeBytes(data, s, o - s);
if (1 === data[offset + 2]) {
if (offset > start) {
h264Frame.writeBytes(data, start, offset - start);
}
state = 3;
o += 3;
return this.writeBytes(data, o, e - o);
offset += 3;
return this.writeBytes(data, offset, end - offset);
}
if (e - o >= 4 && 0 === data[o + 2] && 1 === data[o + 3]) {
if (o > s) {
h264Frame.writeBytes(data, s, o - s);
if (end - offset >= 4 && 0 === data[offset + 2] && 1 === data[offset + 3]) {
if (offset > start) {
h264Frame.writeBytes(data, start, offset - start);
}
state = 3;
o += 4;
return this.writeBytes(data, o, e - o);
offset += 4;
return this.writeBytes(data, offset, end - offset);
}
// We are at the end of the buffer, or we have 3 NULLS followed by
// something that is not a 1, either way we can step forward by at
// least 3
o += 3;
offset += 3;
}
}
// We did not find any start codes. Try again next packet
state = 1;
h264Frame.writeBytes(data, s, l);
h264Frame.writeBytes(data, start, length);
return;
case 3:
// The next byte is the first byte of a NAL Unit
......@@ -447,7 +467,7 @@
}
// setup to begin processing the new NAL unit
nalUnitType = data[o] & 0x1F;
nalUnitType = data[offset] & 0x1F;
if (h264Frame && 9 === nalUnitType) {
this.finishFrame(); // We are starting a new access unit. Flush the previous one
}
......@@ -461,7 +481,7 @@
h264Frame.startNalUnit();
state = 2; // We know there will not be an overlapping start code, so we can skip that test
return this.writeBytes(data, o, l);
return this.writeBytes(data, offset, length);
/*--------------------------------------------------------------------------------------------------------------------*/
} // switch
};
......
(function(window) {
/*
======== A Handy Little QUnit Reference ========
http://api.qunitjs.com/
Test methods:
module(name, {[setup][ ,teardown]})
test(name, callback)
expect(numberOfAssertions)
stop(increment)
start(decrement)
Test assertions:
ok(value, [message])
equal(actual, expected, [message])
notEqual(actual, expected, [message])
deepEqual(actual, expected, [message])
notDeepEqual(actual, expected, [message])
strictEqual(actual, expected, [message])
notStrictEqual(actual, expected, [message])
throws(block, [expected], [message])
*/
var
buffer,
expGolomb,
view;
module('Exponential Golomb coding');
test('small numbers are coded correctly', function() {
var
expected = [
[0xF8, 0],
[0x5F, 1],
[0x7F, 2],
[0x27, 3],
[0x2F, 4],
[0x37, 5],
[0x3F, 6],
[0x11, 7],
[0x13, 8],
[0x15, 9]
],
i = expected.length,
result;
while (i--) {
buffer = new Uint8Array([expected[i][0]]);
expGolomb = new window.videojs.hls.ExpGolomb(buffer);
result = expGolomb.readUnsignedExpGolomb();
equal(expected[i][1], result, expected[i][0] + ' is decoded to ' + expected[i][1]);
}
});
test('drops working data as it is parsed', function() {
var expGolomb = new window.videojs.hls.ExpGolomb(new Uint8Array([0x00, 0xFF]));
expGolomb.skipBits(8);
equal(8, expGolomb.bitsAvailable(), '8 bits remain');
equal(0xFF, expGolomb.readBits(8), 'the second byte is read');
});
test('drops working data when skipping leading zeros', function() {
var expGolomb = new window.videojs.hls.ExpGolomb(new Uint8Array([0x00, 0x00, 0x00, 0x00, 0xFF]));
equal(32, expGolomb.skipLeadingZeros(), '32 leading zeros are dropped');
equal(8, expGolomb.bitsAvailable(), '8 bits remain');
equal(0xFF, expGolomb.readBits(8), 'the second byte is read');
});
test('drops working data when skipping leading zeros', function() {
var expGolomb = new window.videojs.hls.ExpGolomb(new Uint8Array([0x15, 0xab, 0x40, 0xc8, 0xFF]));
equal(3, expGolomb.skipLeadingZeros(), '3 leading zeros are dropped');
equal((8 * 4) + 5, expGolomb.bitsAvailable(), '37 bits remain');
expGolomb.skipBits(1);
equal(0x5a, expGolomb.readBits(8), 'the next bits are read');
});
})(this);
......@@ -51,6 +51,7 @@
<script src="../src/bin-utils.js"></script>
<script src="video-js-hls_test.js"></script>
<script src="exp-golomb_test.js"></script>
</head>
<body>
<div id="qunit"></div>
......
......@@ -65,16 +65,6 @@
console.log('h264 tags:', parser.stats.h264Tags(),
'aac tags:', parser.stats.aacTags());
console.log(videojs.hls.utils.hexDump(parser.getFlvHeader()));
for (i = 0; i < 4; ++i) {
parser.getNextTag();
}
console.log(videojs.hls.utils.tagDump(parser.getNextTag()));
console.log('bad tag:');
for (i = 0; i < 3; ++i) {
console.log(videojs.hls.utils.tagDump(parser.getNextTag()));
}
});
testAudioTag = function(tag) {
......@@ -100,7 +90,10 @@
frameType = (byte & 0xF0) >>> 4,
codecId = byte & 0x0F,
packetType = tag.bytes[12],
compositionTime = (tag.view.getInt32(13) & 0xFFFFFF00) >> 8;
compositionTime = (tag.view.getInt32(13) & 0xFFFFFF00) >> 8,
nalHeader;
// payload starts at tag.bytes[16]
console.log(frameType);
......@@ -109,7 +102,7 @@
'the frame type should be valid');
equal(7, codecId, 'the codec ID is AVC for h264');
ok(packetType <=2 && packetType >= 0, 'the packet type is within [0, 2]');
ok(packetType <= 2 && packetType >= 0, 'the packet type is within [0, 2]');
if (packetType !== 1) {
equal(0,
compositionTime,
......@@ -117,7 +110,25 @@
}
// TODO: the rest of the bytes are an NLU unit
if (packetType === 0) {
// AVC decoder configuration record
} else {
// NAL units
testNalUnit(tag.bytes.subarray(16));
}
};
testNalUnit = function(bytes) {
var
nalHeader = bytes[0],
unitType = nalHeader & 0x1F;
equal(0, (nalHeader & 0x80) >>> 7, 'the first bit is always 0');
// equal(90, (nalHeader & 0x60) >>> 5, 'the NAL reference indicator is something');
// ok(unitType > 0, 'NAL unit type ' + unitType + ' is greater than 0');
// ok(unitType < 22 , 'NAL unit type ' + unitType + ' is less than 22');
};
asciiFromBytes = function(bytes) {
var
......@@ -168,6 +179,7 @@
offset++;
} else {
// number
ok(!isNaN(tag.view.getFloat64(offset)), 'the value is not NaN');
offset += 8;
}
}
......