4b96b9ca by David LaPalomento

Merge pull request #8 from dlapalomento/feature/perf-fixes

Grow FLV tags as necessary and make some performance tweaks
2 parents 64f30e95 961e0073
......@@ -46,7 +46,7 @@ module.exports = function(grunt) {
},
},
qunit: {
files: ['test/**/*.html']
files: ['test/**/*.html', '!test/perf.html']
},
jshint: {
gruntfile: {
......
......@@ -52,7 +52,7 @@
videojs.options.flash.swf = 'node_modules/videojs-media-sources/video-js-with-mse.swf';
video = videojs('video',{},function(){
this.playbackController = new window.videojs.hls.HLSPlaybackController(this);
this.playbackController.loadManifest('http://localhost:7070/test/basic-playback/zencoder/gogo/manifest.m3u8', function(data) {
this.playbackController.loadManifest('test/fixtures/prog_index.m3u8', function(data) {
console.log(data);
});
});
......
(function(window) {
var hls = window.videojs.hls;
var
hls = window.videojs.hls,
// commonly used metadata properties
widthBytes = new Uint8Array('width'.length),
heightBytes = new Uint8Array('height'.length),
videocodecidBytes = new Uint8Array('videocodecid'.length),
i;
// calculating the bytes of common metadata names ahead of time makes the
// corresponding writes faster because we don't have to loop over the
// characters
// re-test with test/perf.html if you're planning on changing this
for (i in 'width') {
widthBytes[i] = 'width'.charCodeAt(i);
}
for (i in 'height') {
heightBytes[i] = 'height'.charCodeAt(i);
}
for (i in 'videocodecid') {
videocodecidBytes[i] = 'videocodecid'.charCodeAt(i);
}
// (type:uint, extraData:Boolean = false) extends ByteArray
hls.FlvTag = function(type, extraData) {
var
// Counter if this is a metadata tag, nal start marker if this is a video
// tag. unused if this is an audio tag
adHoc = 0; // :uint
adHoc = 0, // :uint
// checks whether the FLV tag has enough capacity to accept the proposed
// write and re-allocates the internal buffers if necessary
prepareWrite = function(flv, count) {
var
bytes,
minLength = flv.position + count;
if (minLength < flv.bytes.byteLength) {
// there's enough capacity so do nothing
return;
}
// allocate a new buffer and copy over the data that will not be modified
bytes = new Uint8Array(minLength * 2);
bytes.set(flv.bytes.subarray(0, flv.position), 0);
flv.bytes = bytes;
flv.view = new DataView(flv.bytes.buffer);
};
this.keyFrame = false; // :Boolean
......@@ -27,7 +66,6 @@ hls.FlvTag = function(type, extraData) {
throw("Error Unknown TagType");
}
// XXX: I have no idea if 16k is enough to buffer arbitrary FLV tags
this.bytes = new Uint8Array(16384);
this.view = new DataView(this.bytes.buffer);
this.bytes[0] = type;
......@@ -37,24 +75,26 @@ hls.FlvTag = function(type, extraData) {
// presentation timestamp
this.pts = 0;
// decoder timestamp
this.dts = 0;
this.dts = 0;
// ByteArray#writeBytes(bytes:ByteArray, offset:uint = 0, length:uint = 0)
this.writeBytes = function(bytes, offset, length) {
offset = offset || 0;
var
start = offset || 0,
end;
length = length || bytes.byteLength;
end = start + length;
prepareWrite(this, length);
this.bytes.set(bytes.subarray(start, end), this.position);
try {
this.bytes.set(bytes.subarray(offset, offset + length), this.position);
} catch(e) {
throw e;
}
this.position += length;
this.length = Math.max(this.length, this.position);
};
// ByteArray#writeByte(value:int):void
this.writeByte = function(byte) {
prepareWrite(this, 1);
this.bytes[this.position] = byte;
this.position++;
this.length = Math.max(this.length, this.position);
......@@ -62,6 +102,7 @@ hls.FlvTag = function(type, extraData) {
// ByteArray#writeShort(value:int):void
this.writeShort = function(short) {
prepareWrite(this, 2);
this.view.setUint16(this.position, short);
this.position += 2;
this.length = Math.max(this.length, this.position);
......@@ -80,7 +121,7 @@ hls.FlvTag = function(type, extraData) {
if (adHoc === 0) {
return 0;
}
return this.length - (adHoc + 4);
};
......@@ -115,7 +156,7 @@ hls.FlvTag = function(type, extraData) {
this.position = this.length;
if (nalContainer) {
// Add the tag to the NAL unit
// Add the tag to the NAL unit
nalContainer.push(this.bytes.subarray(nalStart, nalStart + nalLength));
}
}
......@@ -123,20 +164,47 @@ hls.FlvTag = function(type, extraData) {
adHoc = 0;
};
/**
* Write out a 64-bit floating point valued metadata property. This method is
* called frequently during a typical parse and needs to be fast.
*/
// (key:String, val:Number):void
this.writeMetaDataDouble = function(key, val) {
var i;
prepareWrite(this, 2 + key.length + 9);
// write size of property name
this.view.setUint16(this.position, key.length);
this.position += 2;
for (i in key) {
console.assert(key.charCodeAt(i) < 255);
this.bytes[this.position] = key.charCodeAt(i);
this.position++;
// this next part looks terrible but it improves parser throughput by
// 10kB/s in my testing
// write property name
if (key === 'width') {
this.bytes.set(widthBytes, this.position);
this.position += 5;
} else if (key === 'height') {
this.bytes.set(heightBytes, this.position);
this.position += 6;
} else if (key === 'videocodecid') {
this.bytes.set(videocodecidBytes, this.position);
this.position += 12;
} else {
for (i in key) {
this.bytes[this.position] = key.charCodeAt(i);
this.position++;
}
}
this.view.setUint8(this.position, 0x00);
// skip null byte
this.position++;
// write property value
this.view.setFloat64(this.position, val);
this.position += 8;
// update flv tag length
this.length = Math.max(this.length, this.position);
++adHoc;
};
......@@ -144,13 +212,16 @@ hls.FlvTag = function(type, extraData) {
// (key:String, val:Boolean):void
this.writeMetaDataBoolean = function(key, val) {
var i;
prepareWrite(this, 2);
this.view.setUint16(this.position, key.length);
this.position += 2;
for (i in key) {
console.assert(key.charCodeAt(i) < 255);
prepareWrite(this, 1);
this.bytes[this.position] = key.charCodeAt(i);
this.position++;
}
prepareWrite(this, 2);
this.view.setUint8(this.position, 0x01);
this.position++;
this.view.setUint8(this.position, val ? 0x01 : 0x00);
......@@ -197,10 +268,8 @@ hls.FlvTag = function(type, extraData) {
this.position++;
this.view.setUint32(this.position, adHoc);
this.position = this.length;
// this.bytes.set([0, 0, 9], this.position);
// this.position += 3;
this.view.setUint32(this.position, 0x09); // End Data Tag
this.position += 4;
this.bytes.set([0, 0, 9], this.position);
this.position += 3; // End Data Tag
this.length = this.position;
break;
}
......@@ -261,7 +330,7 @@ hls.FlvTag.isKeyFrame = function(tag) {
if (hls.FlvTag.isAudioFrame(tag)) {
return true;
}
if (hls.FlvTag.isMetaData(tag)) {
return true;
}
......
/*
* h264-stream
*
*
*
* Copyright (c) 2013 Brightcove
* All rights reserved.
......@@ -62,7 +62,7 @@
* 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.
* an h264 stream. Exactly one byte.
* RBSP: raw bit-stream payload. The actual encoded video data.
*
* SPS: sequence parameter set. Part of the RBSP. Metadata to be applied
......@@ -128,7 +128,7 @@
pic_width_in_mbs_minus1, // :int
pic_height_in_map_units_minus1, // :int
frame_mbs_only_flag, // :int
frame_cropping_flag, // :Boolean
......@@ -283,17 +283,17 @@
oldExtraData = new H264ExtraData(), // :H264ExtraData
newExtraData = new H264ExtraData(), // :H264ExtraData
nalUnitType = -1, // :int
state; // :uint;
this.tags = [];
//(pts:uint, dts:uint, dataAligned:Boolean):void
this.setNextTimeStamp = function(pts, dts, dataAligned) {
if (0>pts_delta) {
// We assume the very first pts is less than 0x8FFFFFFF (max signed
// We assume the very first pts is less than 0x8FFFFFFF (max signed
// int32)
pts_delta = pts;
}
......@@ -361,38 +361,40 @@
// 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[offset] <= 1) {
nalUnitSize = h264Frame ? h264Frame.nalUnitSize() : 0;
nalUnitSize = h264Frame ? h264Frame.nalUnitSize() : 0;
if (nalUnitSize >= 1 && h264Frame.negIndex(1) === 0) {
// ?? ?? 00 | O[01] ?? ??
if (1 === data[offset] && 2 <= nalUnitSize && 0 === h264Frame.negIndex(2)) {
if (data[offset] === 1 &&
nalUnitSize >= 2 &&
h264Frame.negIndex(2) === 0) {
// ?? 00 00 : 01
if (3 <= nalUnitSize && 0 === h264Frame.negIndex(3)) {
h264Frame.length -= 3; // 00 00 00 : 01
} else {
h264Frame.length -= 2; // 00 00 : 01
}
state = 3;
return this.writeBytes(data, offset + 1, length - 1);
}
if (1 < length && 0 === data[offset] && 1 === data[offset + 1]) {
if (length > 1 && data[offset] === 0 && data[offset + 1] === 1) {
// ?? 00 | 00 01
if (2 <= nalUnitSize && 0 === h264Frame.negIndex(2)) {
if (nalUnitSize >= 2 && h264Frame.negIndex(2) === 0) {
h264Frame.length -= 2; // 00 00 : 00 01
} else {
h264Frame.length -= 1; // 00 : 00 01
}
state = 3;
return this.writeBytes(data, offset + 2, length - 2);
}
if (2 < length &&
0 === data[offset] &&
0 === data[offset + 1] &&
1 === data[offset + 2]) {
// 00 | 00 00 01
if (length > 2 &&
data[offset] === 0 &&
data[offset + 1] === 0 &&
data[offset + 2] === 1) {
// 00 : 00 00 01
h264Frame.length -= 1;
state = 3;
return this.writeBytes(data, offset + 3, length - 3);
......@@ -403,19 +405,22 @@
// bytes a second time. But that case will be VERY rare
state = 2;
/* falls through */
case 2: // Look for start codes in data
case 2:
// Look for start codes in the data from the current offset forward
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]) {
for (t = end - 3; offset < t;) {
if (data[offset + 2] > 1) {
// if data[offset + 2] is greater than 1, there is no way a start
// code can begin before offset + 3
offset += 3;
} else if (data[offset + 1] !== 0) {
offset += 2;
} else if (0 !== data[offset]) {
} else if (data[offset] !== 0) {
offset += 1;
} else {
// If we get here we have 00 00 00 or 00 00 01
if (1 === data[offset + 2]) {
if (data[offset + 2] === 1) {
if (offset > start) {
h264Frame.writeBytes(data, start, offset - start);
}
......@@ -424,7 +429,9 @@
return this.writeBytes(data, offset, end - offset);
}
if (end - offset >= 4 && 0 === data[offset + 2] && 1 === data[offset + 3]) {
if (end - offset >= 4 &&
data[offset + 2] === 0 &&
data[offset + 3] === 1) {
if (offset > start) {
h264Frame.writeBytes(data, start, offset - start);
}
......@@ -482,9 +489,10 @@
}
h264Frame.startNalUnit();
state = 2; // We know there will not be an overlapping start code, so we can skip that test
// We know there will not be an overlapping start code, so we can skip
// that test
state = 2;
return this.writeBytes(data, offset, length);
/*--------------------------------------------------------------------------------------------------------------------*/
} // switch
};
};
......
......@@ -39,7 +39,7 @@
duration = duration || 0;
audio = audio === undefined? true : audio;
video = video === undefined? true : video;
// signature
head.setUint8(0, 0x46); // 'F'
head.setUint8(1, 0x4c); // 'L'
......@@ -47,7 +47,7 @@
// version
head.setUint8(3, 0x01);
// flags
head.setUint8(4, (audio ? 0x04 : 0x00) | (video ? 0x01 : 0x00));
......@@ -74,7 +74,7 @@
return result;
};
self.flushTags = function() {
h264Stream.finishFrame();
};
......@@ -161,7 +161,7 @@
streamBuffer.set(dataSlice, streamBufferByteCount);
parseTSPacket(streamBuffer);
// reset the buffer
streamBuffer = new Uint8Array(m2tsPacketSize);
streamBufferByteCount = 0;
......@@ -174,7 +174,7 @@
// If there is no sync byte skip forward until we find one
// TODO if we find a sync byte, look 188 bytes in the future (if
// possible). If there is not a sync byte there, keep looking
dataPosition++;
dataPosition++;
}
// base case: not enough data to parse a m2ts packet
......@@ -205,19 +205,18 @@
// packet!
parseTSPacket = function(data) { // :ByteArray):Boolean {
var
s = 0, //:uint
o = s, // :uint
e = o + m2tsPacketSize, // :uint
offset = 0, // :uint
end = offset + m2tsPacketSize, // :uint
// Don't look for a sync byte. We handle that in
// parseSegmentBinaryData()
// Payload Unit Start Indicator
pusi = !!(data[o + 1] & 0x40), // mask: 0100 0000
pusi = !!(data[offset + 1] & 0x40), // mask: 0100 0000
// PacketId
pid = (data[o + 1] & 0x1F) << 8 | data[o + 2], // mask: 0001 1111
afflag = (data[o + 3] & 0x30 ) >>> 4,
pid = (data[offset + 1] & 0x1F) << 8 | data[offset + 2], // mask: 0001 1111
afflag = (data[offset + 3] & 0x30 ) >>> 4,
aflen, // :uint
patTableId, // :int
......@@ -242,29 +241,29 @@
// Continuity Counter we could use this for sanity check, and
// corrupt stream detection
// cc = (data[o + 3] & 0x0F);
// cc = (data[offset + 3] & 0x0F);
// Done with TS header
offset += 4;
// Done with TS header
o += 4;
if (afflag > 0x01) { // skip most of the adaption field
aflen = data[o];
o += aflen + 1;
aflen = data[offset];
offset += aflen + 1;
}
if (0x0000 === pid) {
// always test for PMT first! (becuse other variables default to 0)
// if pusi is set we must skip X bytes (PSI pointer field)
o += pusi ? 1 + data[o] : 0;
patTableId = data[o];
offset += pusi ? 1 + data[offset] : 0;
patTableId = data[offset];
console.assert(0x00 === patTableId, 'patTableId should be 0x00');
patCurrentNextIndicator = !!(data[o + 5] & 0x01);
patCurrentNextIndicator = !!(data[offset + 5] & 0x01);
if (patCurrentNextIndicator) {
patSectionLength = (data[o + 1] & 0x0F) << 8 | data[o + 2];
o += 8; // skip past PSI header
patSectionLength = (data[offset + 1] & 0x0F) << 8 | data[offset + 2];
offset += 8; // skip past PSI header
// We currently only support streams with 1 program
patSectionLength = (patSectionLength - 9) / 4;
......@@ -273,26 +272,26 @@
}
// if we ever support more that 1 program (unlikely) loop over them here
// var programNumber = data[o + 0] << 8 | data[o + 1];
// var programId = (data[o+2] & 0x1F) << 8 | data[o + 3];
pmtPid = (data[o + 2] & 0x1F) << 8 | data[o + 3];
// var programNumber = data[offset + 0] << 8 | data[offset + 1];
// var programId = (data[offset+2] & 0x1F) << 8 | data[offset + 3];
pmtPid = (data[offset + 2] & 0x1F) << 8 | data[offset + 3];
}
// We could test the CRC here to detect corruption with extra CPU cost
} else if (videoPid === pid || audioPid === pid) {
if (pusi) {
// comment out for speed
if (0x00 !== data[o + 0] || 0x00 !== data[o + 1] || 0x01 !== data[o + 2]) {
if (0x00 !== data[offset + 0] || 0x00 !== data[offset + 1] || 0x01 !== data[offset + 2]) {
// look for PES start code
throw new Error("PES did not begin with start code");
}
// var sid:int = data[o+3]; // StreamID
pesPacketSize = (data[o + 4] << 8) | data[o + 5];
dataAlignmentIndicator = !!((data[o + 6] & 0x04) >>> 2);
ptsDtsIndicator = (data[o + 7] & 0xC0) >>> 6;
pesHeaderLength = data[o + 8]; // TODO sanity check header length
o += 9; // Skip past PES header
// var sid:int = data[offset+3]; // StreamID
pesPacketSize = (data[offset + 4] << 8) | data[offset + 5];
dataAlignmentIndicator = (data[offset + 6] & 0x04) !== 0;
ptsDtsIndicator = data[offset + 7];
pesHeaderLength = data[offset + 8]; // TODO sanity check header length
offset += 9; // Skip past PES header
// PTS and DTS are normially stored as a 33 bit number.
// JavaScript does not have a integer type larger than 32 bit
......@@ -300,26 +299,28 @@
// so what we are going to do instead, is drop the least
// significant bit (the same as dividing by two) then we can
// divide by 45 (45 * 2 = 90) to get ms.
if (ptsDtsIndicator & 0x03) {
pts = (data[o + 0] & 0x0E) << 28
| (data[o + 1] & 0xFF) << 21
| (data[o + 2] & 0xFE) << 13
| (data[o + 3] & 0xFF) << 6
| (data[o + 4] & 0xFE) >>> 2;
if (ptsDtsIndicator & 0xC0) {
// the PTS and DTS are not written out directly. For information on
// how they are encoded, see
// http://dvd.sourceforge.net/dvdinfo/pes-hdr.html
pts = (data[offset + 0] & 0x0E) << 28
| (data[offset + 1] & 0xFF) << 21
| (data[offset + 2] & 0xFE) << 13
| (data[offset + 3] & 0xFF) << 6
| (data[offset + 4] & 0xFE) >>> 2;
pts /= 45;
if (ptsDtsIndicator & 0x01) {// DTS
dts = (data[o + 5] & 0x0E ) << 28
| (data[o + 6] & 0xFF ) << 21
| (data[o + 7] & 0xFE ) << 13
| (data[o + 8] & 0xFF ) << 6
| (data[o + 9] & 0xFE ) >>> 2;
dts = pts;
if (ptsDtsIndicator & 0x40) {// DTS
dts = (data[offset + 5] & 0x0E ) << 28
| (data[offset + 6] & 0xFF ) << 21
| (data[offset + 7] & 0xFE ) << 13
| (data[offset + 8] & 0xFF ) << 6
| (data[offset + 9] & 0xFE ) >>> 2;
dts /= 45;
} else {
dts = pts;
}
}
// Skip past "optional" portion of PTS header
o += pesHeaderLength;
offset += pesHeaderLength;
if (videoPid === pid) {
// Stash this frame for future use.
......@@ -336,31 +337,31 @@
}
if (audioPid === pid) {
aacStream.writeBytes(data, o, e - o);
aacStream.writeBytes(data, offset, end - offset);
} else if (videoPid === pid) {
h264Stream.writeBytes(data, o, e - o);
h264Stream.writeBytes(data, offset, end - offset);
}
} else if (pmtPid === pid) {
// TODO sanity check data[o]
// TODO sanity check data[offset]
// if pusi is set we must skip X bytes (PSI pointer field)
o += (pusi ? 1 + data[o] : 0);
pmtTableId = data[o];
offset += (pusi ? 1 + data[offset] : 0);
pmtTableId = data[offset];
console.assert(0x02 === pmtTableId);
pmtCurrentNextIndicator = !!(data[o + 5] & 0x01);
pmtCurrentNextIndicator = !!(data[offset + 5] & 0x01);
if (pmtCurrentNextIndicator) {
audioPid = videoPid = 0;
pmtSectionLength = (data[o + 1] & 0x0F) << 8 | data[o + 2];
pmtSectionLength = (data[offset + 1] & 0x0F) << 8 | data[offset + 2];
// skip CRC and PSI data we dont care about
pmtSectionLength -= 13;
pmtSectionLength -= 13;
o += 12; // skip past PSI header and some PMT data
offset += 12; // skip past PSI header and some PMT data
while (0 < pmtSectionLength) {
streamType = data[o + 0];
elementaryPID = (data[o + 1] & 0x1F) << 8 | data[o + 2];
ESInfolength = (data[o + 3] & 0x0F) << 8 | data[o + 4];
o += 5 + ESInfolength;
streamType = data[offset + 0];
elementaryPID = (data[offset + 1] & 0x1F) << 8 | data[offset + 2];
ESInfolength = (data[offset + 3] & 0x0F) << 8 | data[offset + 4];
offset += 5 + ESInfolength;
pmtSectionLength -= 5 + ESInfolength;
if (0x1B === streamType) {
......@@ -398,4 +399,4 @@
}
};
};
})(this);
})(window);
......
......@@ -44,4 +44,17 @@ test('writeShort writes a two byte sequence', function() {
'the value is written');
});
test('writeBytes grows the internal byte array dynamically', function() {
var
tag = new FlvTag(FlvTag.VIDEO_TAG),
tooManyBytes = new Uint8Array(tag.bytes.byteLength + 1);
try {
tag.writeBytes(tooManyBytes);
ok(true, 'the buffer grew to fit the data');
} catch(e) {
ok(!e, 'the buffer should grow');
}
});
})(this);
......
<!doctype html>
<html>
<head>
<title>MPEG-TS Parser Performance Workbench</title>
<!-- video.js -->
<script src="../node_modules/video.js/video.dev.js"></script>
<!-- HLS plugin -->
<script src="../src/video-js-hls.js"></script>
<script src="../src/flv-tag.js"></script>
<script src="../src/exp-golomb.js"></script>
<script src="../src/h264-stream.js"></script>
<script src="../src/aac-stream.js"></script>
<script src="../src/segment-parser.js"></script>
<!-- MPEG-TS segment -->
<script src="tsSegment-bc.js"></script>
<style>
.desc {
background-color: #ddd;
border: thin solid #333;
padding: 8px;
}
</style>
</head>
<body>
<p class="desc">Select your number of iterations and then press "Run" to begin parsing MPEG-TS packets into FLV tags. This page can be handy for identifying segment parser performance bottlenecks.</p>
<form>
<input name="iterations" min="1" type="number" value="1">
<button type="sumbit">Run</button>
</form>
<table>
<thead>
<th>Iterations</th><th>Time</th><th>MB/second</th>
</thead>
<tbody class="results"></tbody>
</table>
<script>
var
button = document.querySelector('button'),
input = document.querySelector('input'),
results = document.querySelector('.results'),
reportResults = function(count, elapsed) {
var
row = document.createElement('tr'),
countCell = document.createElement('td'),
elapsedCell = document.createElement('td'),
throughputCell = document.createElement('td');
countCell.innerText = count;
elapsedCell.innerText = elapsed;
throughputCell.innerText = (((bcSegment.byteLength * count * 1000) / elapsed) / (Math.pow(2, 20))).toFixed(3);
row.appendChild(countCell);
row.appendChild(elapsedCell);
row.appendChild(throughputCell);
results.insertBefore(row, results.firstChild);
};
button.addEventListener('click', function(event) {
var
iterations = input.value,
parser = new window.videojs.hls.SegmentParser(),
start;
// setup
start = +new Date();
while (iterations--) {
// parse the segment
parser.parseSegmentBinaryData(window.bcSegment);
// finalize all the FLV tags
while (parser.tagsAvailable()) {
parser.getNextTag();
}
}
// report
reportResults(input.value, (+new Date()) - start);
// don't actually submit the form
event.preventDefault();
}, false);
</script>
</body>
</html>