ced9b85e by Tom Johnson
2 parents 0f3916fb 5351582b
/<!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();
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);
......@@ -69,6 +69,7 @@
<script src="video-js-hls_test.js"></script>
<script src="exp-golomb_test.js"></script>
</head>
<body>
<div id="qunit"></div>
......
......@@ -67,16 +67,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) {
......@@ -102,7 +92,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]
// XXX: I'm not sure that frame types 3-5 are invalid
......@@ -110,7 +103,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,
......@@ -118,8 +111,26 @@
}
// 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
string = [],
......@@ -169,6 +180,7 @@
offset++;
} else {
// number
ok(!isNaN(tag.view.getFloat64(offset)), 'the value is not NaN');
offset += 8;
}
}
......