43a47b68 by David LaPalomento

Merge pull request #221 from videojs/feature/in-band-metadata

In-Band Metadata
2 parents e686ddde 0e85b0f0
......@@ -24,12 +24,13 @@ module.exports = function(grunt) {
dist: {
nonull: true,
src: ['src/videojs-hls.js',
'src/stream.js',
'src/flv-tag.js',
'src/exp-golomb.js',
'src/h264-stream.js',
'src/aac-stream.js',
'src/metadata-stream.js',
'src/segment-parser.js',
'src/stream.js',
'src/m3u8/m3u8-parser.js',
'src/xhr.js',
'src/playlist-loader.js',
......
......@@ -155,6 +155,20 @@ the actual rendering quality change does not occur simultaneously with
this event; a new segment must be requested and the existing buffer
depleted first.
### In-Band Metadata
The HLS tech supports [timed
metadata](https://developer.apple.com/library/ios/#documentation/AudioVideo/Conceptual/HTTP_Live_Streaming_Metadata_Spec/Introduction/Introduction.html)
embedded as [ID3 tags](http://id3.org/id3v2.3.0). When a stream is
encountered with embedded metadata, an [in-band metadata text
track](https://html.spec.whatwg.org/multipage/embedded-content.html#text-track-in-band-metadata-track-dispatch-type)
will automatically be created and populated with cues as they are
encountered in the stream. Only UTF-8 encoded
[TXXX](http://id3.org/id3v2.3.0#User_defined_text_information_frame)
and [WXXX](http://id3.org/id3v2.3.0#User_defined_URL_link_frame) ID3
frames are currently mapped to cue points. There are lots of guides
and references to using text tracks [around the
web](http://www.html5rocks.com/en/tutorials/track/basics/).
### Testing
For testing, you can either run `npm test` or use `grunt` directly.
......
......@@ -18,13 +18,14 @@
<!-- segment handling -->
<script src="src/xhr.js"></script>
<script src="src/flv-tag.js"></script>
<script src="src/stream.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/metadata-stream.js"></script>
<script src="src/segment-parser.js"></script>
<!-- m3u8 handling -->
<script src="src/stream.js"></script>
<script src="src/m3u8/m3u8-parser.js"></script>
<script src="src/playlist-loader.js"></script>
......
/**
* Accepts program elementary stream (PES) data events and parses out
* ID3 metadata from them, if present.
* @see http://id3.org/id3v2.3.0
*/
(function(window, videojs, undefined) {
'use strict';
var
parseString = function(bytes, start, end) {
var i, result = '';
for (i = start; i < end; i++) {
result += '%' + ('00' + bytes[i].toString(16)).slice(-2);
}
return window.decodeURIComponent(result);
},
tagParsers = {
'TXXX': function(tag) {
var i;
if (tag.data[0] !== 3) {
// ignore frames with unrecognized character encodings
return;
}
for (i = 1; i < tag.data.length; i++) {
if (tag.data[i] === 0) {
// parse the text fields
tag.description = parseString(tag.data, 1, i);
tag.value = parseString(tag.data, i + 1, tag.data.length);
break;
}
}
},
'WXXX': function(tag) {
var i;
if (tag.data[0] !== 3) {
// ignore frames with unrecognized character encodings
return;
}
for (i = 1; i < tag.data.length; i++) {
if (tag.data[i] === 0) {
// parse the description and URL fields
tag.description = parseString(tag.data, 1, i);
tag.url = parseString(tag.data, i + 1, tag.data.length);
break;
}
}
}
},
MetadataStream;
MetadataStream = function(options) {
var settings = {
debug: !!(options && options.debug),
// the bytes of the program-level descriptor field in MP2T
// see ISO/IEC 13818-1:2013 (E), section 2.6 "Program and
// program element descriptors"
descriptor: options && options.descriptor
}, i;
MetadataStream.prototype.init.call(this);
// calculate the text track in-band metadata track dispatch type
// https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track
this.dispatchType = videojs.Hls.SegmentParser.STREAM_TYPES.metadata.toString(16);
if (settings.descriptor) {
for (i = 0; i < settings.descriptor.length; i++) {
this.dispatchType += ('00' + settings.descriptor[i].toString(16)).slice(-2);
}
}
this.push = function(chunk) {
var tagSize, frameStart, frameSize, frame;
// ignore events that don't look like ID3 data
if (chunk.data.length < 10 ||
chunk.data[0] !== 'I'.charCodeAt(0) ||
chunk.data[1] !== 'D'.charCodeAt(0) ||
chunk.data[2] !== '3'.charCodeAt(0)) {
if (settings.debug) {
videojs.log('Skipping unrecognized metadata stream');
}
return;
}
// find the start of the first frame and the end of the tag
tagSize = chunk.data.byteLength;
frameStart = 10;
if (chunk.data[5] & 0x40) {
// advance the frame start past the extended header
frameStart += 4; // header size field
frameStart += (chunk.data[10] << 24) |
(chunk.data[11] << 16) |
(chunk.data[12] << 8) |
(chunk.data[13]);
// clip any padding off the end
tagSize -= (chunk.data[16] << 24) |
(chunk.data[17] << 16) |
(chunk.data[18] << 8) |
(chunk.data[19]);
}
// adjust the PTS values to align with the video and audio
// streams
if (this.timestampOffset) {
chunk.pts -= this.timestampOffset;
chunk.dts -= this.timestampOffset;
}
// parse one or more ID3 frames
// http://id3.org/id3v2.3.0#ID3v2_frame_overview
chunk.frames = [];
do {
// determine the number of bytes in this frame
frameSize = (chunk.data[frameStart + 4] << 24) |
(chunk.data[frameStart + 5] << 16) |
(chunk.data[frameStart + 6] << 8) |
(chunk.data[frameStart + 7]);
if (frameSize < 1) {
return videojs.log('Malformed ID3 frame encountered. Skipping metadata parsing.');
}
frame = {
id: String.fromCharCode(chunk.data[frameStart]) +
String.fromCharCode(chunk.data[frameStart + 1]) +
String.fromCharCode(chunk.data[frameStart + 2]) +
String.fromCharCode(chunk.data[frameStart + 3]),
data: chunk.data.subarray(frameStart + 10, frameStart + frameSize + 10)
};
if (tagParsers[frame.id]) {
tagParsers[frame.id](frame);
}
chunk.frames.push(frame);
frameStart += 10; // advance past the frame header
frameStart += frameSize; // advance past the frame body
} while (frameStart < tagSize);
this.trigger('data', chunk);
};
};
MetadataStream.prototype = new videojs.Hls.Stream();
videojs.Hls.MetadataStream = MetadataStream;
})(window, window.videojs);
......@@ -4,6 +4,7 @@
FlvTag = videojs.Hls.FlvTag,
H264Stream = videojs.Hls.H264Stream,
AacStream = videojs.Hls.AacStream,
MetadataStream = videojs.Hls.MetadataStream,
MP2T_PACKET_LENGTH,
STREAM_TYPES;
......@@ -27,6 +28,9 @@
programMapTable: {}
};
// allow in-band metadata to be observed
self.metadataStream = new MetadataStream();
// For information on the FLV format, see
// http://download.macromedia.com/f4v/video_file_format_spec_v10_1.pdf.
// Technically, this function returns the header and a metadata FLV tag
......@@ -287,7 +291,8 @@
self.stream.pmtPid = (data[offset + 2] & 0x1F) << 8 | data[offset + 3];
}
} else if (pid === self.stream.programMapTable[STREAM_TYPES.h264] ||
pid === self.stream.programMapTable[STREAM_TYPES.adts]) {
pid === self.stream.programMapTable[STREAM_TYPES.adts] ||
pid === self.stream.programMapTable[STREAM_TYPES.metadata]) {
if (pusi) {
// comment out for speed
if (0x00 !== data[offset + 0] || 0x00 !== data[offset + 1] || 0x01 !== data[offset + 2]) {
......@@ -328,9 +333,16 @@
dts /= 45;
}
}
// Skip past "optional" portion of PTS header
offset += pesHeaderLength;
// align the metadata stream PTS values with the start of
// the other elementary streams
if (!self.metadataStream.timestampOffset) {
self.metadataStream.timestampOffset = pts;
}
if (pid === self.stream.programMapTable[STREAM_TYPES.h264]) {
h264Stream.setNextTimeStamp(pts,
dts,
......@@ -339,6 +351,12 @@
aacStream.setNextTimeStamp(pts,
pesPacketSize,
dataAlignmentIndicator);
} else {
self.metadataStream.push({
pts: pts,
dts: dts,
data: data.subarray(offset)
});
}
}
......@@ -383,23 +401,26 @@
// the PID for this entry
elementaryPID = (data[offset + 1] & 0x1F) << 8 | data[offset + 2];
if (streamType === STREAM_TYPES.h264) {
if (self.stream.programMapTable[streamType] &&
self.stream.programMapTable[streamType] !== elementaryPID) {
throw new Error("Program has more than 1 video stream");
}
self.stream.programMapTable[streamType] = elementaryPID;
} else if (streamType === STREAM_TYPES.adts) {
if (self.stream.programMapTable[streamType] &&
self.stream.programMapTable[streamType] !== elementaryPID) {
throw new Error("Program has more than 1 audio Stream");
}
self.stream.programMapTable[streamType] = elementaryPID;
if (streamType === STREAM_TYPES.h264 &&
self.stream.programMapTable[streamType] &&
self.stream.programMapTable[streamType] !== elementaryPID) {
throw new Error("Program has more than 1 video stream");
} else if (streamType === STREAM_TYPES.adts &&
self.stream.programMapTable[streamType] &&
self.stream.programMapTable[streamType] !== elementaryPID) {
throw new Error("Program has more than 1 audio Stream");
}
// add the stream type entry to the map
self.stream.programMapTable[streamType] = elementaryPID;
// TODO add support for MP3 audio
// the length of the entry descriptor
ESInfolength = (data[offset + 3] & 0x0F) << 8 | data[offset + 4];
// capture the stream descriptor for metadata streams
if (streamType === STREAM_TYPES.metadata) {
self.metadataStream.descriptor = new Uint8Array(data.subarray(offset + 5, offset + 5 + ESInfolength));
}
// move to the first byte after the end of this entry
offset += 5 + ESInfolength;
pmtSectionLength -= 5 + ESInfolength;
......@@ -435,7 +456,8 @@
videojs.Hls.SegmentParser.MP2T_PACKET_LENGTH = MP2T_PACKET_LENGTH = 188;
videojs.Hls.SegmentParser.STREAM_TYPES = STREAM_TYPES = {
h264: 0x1b,
adts: 0x0f
adts: 0x0f,
metadata: 0x15
};
})(window);
......
......@@ -74,6 +74,44 @@ videojs.Hls.prototype.src = function(src) {
this.segmentBuffer_ = [];
this.segmentParser_ = new videojs.Hls.SegmentParser();
// if the stream contains ID3 metadata, expose that as a metadata
// text track
(function() {
var
metadataStream = tech.segmentParser_.metadataStream,
textTrack;
// only expose metadata tracks to video.js versions that support
// dynamic text tracks (4.12+)
if (!tech.player().addTextTrack) {
return;
}
metadataStream.on('data', function(metadata) {
var i, frame, time, hexDigit;
// create the metadata track if this is the first ID3 tag we've
// seen
if (!textTrack) {
textTrack = tech.player().addTextTrack('metadata', 'Timed Metadata');
// build the dispatch type from the stream descriptor
// https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track
textTrack.inBandMetadataTrackDispatchType = videojs.Hls.SegmentParser.STREAM_TYPES.metadata.toString(16).toUpperCase();
for (i = 0; i < metadataStream.descriptor.length; i++) {
hexDigit = ('00' + metadataStream.descriptor[i].toString(16).toUpperCase()).slice(-2);
textTrack.inBandMetadataTrackDispatchType += hexDigit;
}
}
for (i = 0; i < metadata.frames.length; i++) {
frame = metadata.frames[i];
time = metadata.pts / 1000;
textTrack.addCue(new window.VTTCue(time, time, frame.value || frame.url));
}
});
})();
// load the MediaSource into the player
this.mediaSource.addEventListener('sourceopen', videojs.bind(this, this.handleSourceOpen));
......
......@@ -80,14 +80,15 @@ module.exports = function(config) {
'../node_modules/pkcs7/dist/pkcs7.unpad.js',
'../test/karma-qunit-shim.js',
'../src/videojs-hls.js',
'../src/xhr.js',
'../src/stream.js',
'../src/flv-tag.js',
'../src/exp-golomb.js',
'../src/h264-stream.js',
'../src/aac-stream.js',
'../src/metadata-stream.js',
'../src/segment-parser.js',
'../src/stream.js',
'../src/m3u8/m3u8-parser.js',
'../src/xhr.js',
'../src/playlist-loader.js',
'../src/decrypter.js',
'../tmp/manifests.js',
......
......@@ -44,14 +44,15 @@ module.exports = function(config) {
'../node_modules/pkcs7/dist/pkcs7.unpad.js',
'../test/karma-qunit-shim.js',
'../src/videojs-hls.js',
'../src/xhr.js',
'../src/stream.js',
'../src/flv-tag.js',
'../src/exp-golomb.js',
'../src/h264-stream.js',
'../src/aac-stream.js',
'../src/metadata-stream.js',
'../src/segment-parser.js',
'../src/stream.js',
'../src/m3u8/m3u8-parser.js',
'../src/xhr.js',
'../src/playlist-loader.js',
'../src/decrypter.js',
'../tmp/manifests.js',
......
(function(window, videojs, undefined) {
'use strict';
/*
======== 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 metadataStream, stringToInts, stringToCString, id3Tag, id3Frame;
module('MetadataStream', {
setup: function() {
metadataStream = new videojs.Hls.MetadataStream();
}
});
test('can construct a MetadataStream', function() {
ok(metadataStream, 'does not return null');
});
stringToInts = function(string) {
var result = [], i;
for (i = 0; i < string.length; i++) {
result[i] = string.charCodeAt(i);
}
return result;
};
stringToCString = function(string) {
return stringToInts(string).concat([0x00]);
};
id3Tag = function() {
var
frames = Array.prototype.concat.apply([], Array.prototype.slice.call(arguments)),
result = stringToInts('ID3').concat([
0x03, 0x00, // version 3.0 of ID3v2 (aka ID3v.2.3.0)
0x40, // flags. include an extended header
0x00, 0x00, 0x00, 0x00, // size. set later
// extended header
0x00, 0x00, 0x00, 0x06, // extended header size. no CRC
0x00, 0x00, // extended flags
0x00, 0x00, 0x00, 0x02 // size of padding
], frames),
size;
size = result.length - 10;
result[6] = (size >>> 24) & 0xff;
result[7] = (size >>> 16) & 0xff;
result[8] = (size >>> 8) & 0xff;
result[9] = (size) & 0xff;
return result;
};
id3Frame = function(type) {
var result = stringToInts(type).concat([
0x00, 0x00, 0x00, 0x00, // size
0xe0, 0x00 // flags. tag/file alter preservation, read-only
]),
size = result.length - 10;
// append the fields of the ID3 frame
result = result.concat.apply(result, Array.prototype.slice.call(arguments, 1));
// set the size
size = result.length - 10;
result[4] = (size >>> 24);
result[5] = (size >>> 16) & 0xff;
result[6] = (size >>> 8) & 0xff;
result[7] = (size) & 0xff;
return result;
};
test('parses simple ID3 metadata out of PES packets', function() {
var
events = [],
wxxxPayload = [
0x00 // text encoding. ISO-8859-1
].concat(stringToCString('ad tag URL'), // description
stringToInts('http://example.com/ad?v=1234&q=7')), // value
id3Bytes,
size;
metadataStream.on('data', function(event) {
events.push(event);
});
id3Bytes = new Uint8Array(stringToInts('ID3').concat([
0x03, 0x00, // version 3.0 of ID3v2 (aka ID3v.2.3.0)
0x40, // flags. include an extended header
0x00, 0x00, 0x00, 0x00, // size. set later
// extended header
0x00, 0x00, 0x00, 0x06, // extended header size. no CRC
0x00, 0x00, // extended flags
0x00, 0x00, 0x00, 0x02, // size of padding
// frame 0
// http://id3.org/id3v2.3.0#User_defined_text_information_frame
], id3Frame('WXXX',
wxxxPayload), // value
// frame 1
// custom tag
id3Frame('XINF',
[
0x04, 0x03, 0x02, 0x01 // arbitrary data
]), [
0x00, 0x00 // padding
]));
// set header size field
size = id3Bytes.byteLength - 10;
id3Bytes[6] = (size >>> 21) & 0x7f;
id3Bytes[7] = (size >>> 14) & 0x7f;
id3Bytes[8] = (size >>> 7) & 0x7f;
id3Bytes[9] = (size) & 0x7f;
metadataStream.push({
trackId: 7,
pts: 1000,
dts: 1000,
// header
data: id3Bytes
});
equal(events.length, 1, 'parsed one tag');
equal(events[0].frames.length, 2, 'parsed two frames');
equal(events[0].frames[0].id, 'WXXX', 'parsed a WXXX frame');
deepEqual(new Uint8Array(events[0].frames[0].data),
new Uint8Array(wxxxPayload),
'attached the frame payload');
equal(events[0].frames[1].id, 'XINF', 'parsed a user-defined frame');
deepEqual(new Uint8Array(events[0].frames[1].data),
new Uint8Array([0x04, 0x03, 0x02, 0x01]),
'attached the frame payload');
equal(events[0].pts, 1000, 'did not modify the PTS');
equal(events[0].dts, 1000, 'did not modify the PTS');
});
test('skips non-ID3 metadata events', function() {
var events = [];
metadataStream.on('data', function(event) {
events.push(event);
});
metadataStream.push({
trackId: 7,
pts: 1000,
dts: 1000,
// header
data: new Uint8Array([0])
});
equal(events.length, 0, 'did not emit an event');
});
// missing cases:
// unsynchronization
// CRC
// no extended header
// compressed frames
// encrypted frames
// frame groups
// too large/small tag size values
// too large/small frame size values
test('translates PTS and DTS values based on the timestamp offset', function() {
var events = [];
metadataStream.on('data', function(event) {
events.push(event);
});
metadataStream.timestampOffset = 800;
metadataStream.push({
trackId: 7,
pts: 1000,
dts: 900,
// header
data: new Uint8Array(id3Tag(id3Frame('XFFF', [0]), [0x00, 0x00]))
});
equal(events.length, 1, 'emitted an event');
equal(events[0].pts, 200, 'translated pts');
equal(events[0].dts, 100, 'translated dts');
});
test('parses TXXX tags', function() {
var events = [];
metadataStream.on('data', function(event) {
events.push(event);
});
metadataStream.push({
trackId: 7,
pts: 1000,
dts: 900,
// header
data: new Uint8Array(id3Tag(id3Frame('TXXX',
0x03, // utf-8
stringToCString('get done'),
stringToInts('{ "key": "value" }')),
[0x00, 0x00]))
});
equal(events.length, 1, 'parsed one tag');
equal(events[0].frames.length, 1, 'parsed one frame');
equal(events[0].frames[0].description, 'get done', 'parsed the description');
equal(events[0].frames[0].value, '{ "key": "value" }', 'parsed the value');
});
test('parses WXXX tags', function() {
var events = [], url = 'http://example.com/path/file?abc=7&d=4#ty';
metadataStream.on('data', function(event) {
events.push(event);
});
metadataStream.push({
trackId: 7,
pts: 1000,
dts: 900,
// header
data: new Uint8Array(id3Tag(id3Frame('WXXX',
0x03, // utf-8
stringToCString(''),
stringToInts(url)),
[0x00, 0x00]))
});
equal(events.length, 1, 'parsed one tag');
equal(events[0].frames.length, 1, 'parsed one frame');
equal(events[0].frames[0].description, '', 'parsed the description');
equal(events[0].frames[0].url, url, 'parsed the value');
});
test('parses TXXX tags with characters that have a single-digit hexadecimal representation', function() {
var events = [], value = String.fromCharCode(7);
metadataStream.on('data', function(event) {
events.push(event);
});
metadataStream.push({
trackId: 7,
pts: 1000,
dts: 900,
// header
data: new Uint8Array(id3Tag(id3Frame('TXXX',
0x03, // utf-8
stringToCString(''),
stringToInts(value)),
[0x00, 0x00]))
});
equal(events[0].frames[0].value,
value,
'parsed the single-digit character');
});
// https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track
test('constructs the dispatch type', function() {
metadataStream = new videojs.Hls.MetadataStream({
descriptor: new Uint8Array([0x03, 0x02, 0x01, 0x00])
});
equal(metadataStream.dispatchType, '1503020100', 'built the dispatch type');
});
})(window, window.videojs);
......@@ -109,11 +109,18 @@
</div>
<script>
window.videojs = {
Hls: {}
};
</script>
<!-- transmuxing -->
<script src="../../src/stream.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/metadata-stream.js"></script>
<script src="../../src/segment-parser.js"></script>
<script src="../../node_modules/pkcs7/dist/pkcs7.unpad.js"></script>
<script src="../../src/decrypter.js"></script>
......
......@@ -29,6 +29,11 @@
extend = window.videojs.util.mergeOptions,
makePat,
makePsi,
makePmt,
makePacket,
testAudioTag,
testVideoTag,
testScriptTag,
......@@ -54,123 +59,137 @@
deepEqual(expectedHeader, header, 'the rest of the header is correct');
});
// Create a PMT packet
// @return {Array} bytes
makePmt = function(options) {
var
result = [],
entryCount = 0,
k,
sectionLength;
for (k in options.pids) {
entryCount++;
}
// table_id
result.push(0x02);
// section_syntax_indicator '0' reserved section_length
// 13 + (program_info_length) + (n * 5 + ES_info_length[n])
sectionLength = 13 + (5 * entryCount) + 17;
result.push(0x80 | (0xF00 & sectionLength >>> 8));
result.push(sectionLength & 0xFF);
// program_number
result.push(0x00);
result.push(0x01);
// reserved version_number current_next_indicator
result.push(0x01);
// section_number
result.push(0x00);
// last_section_number
result.push(0x00);
// reserved PCR_PID
result.push(0xe1);
result.push(0x00);
// reserved program_info_length
result.push(0xf0);
result.push(0x11); // hard-coded 17 byte descriptor
// program descriptors
result = result.concat([
0x25, 0x0f, 0xff, 0xff,
0x49, 0x44, 0x33, 0x20,
0xff, 0x49, 0x44, 0x33,
0x20, 0x00, 0x1f, 0x00,
0x01
]);
for (k in options.pids) {
// stream_type
result.push(options.pids[k]);
// reserved elementary_PID
result.push(0xe0 | (k & 0x1f00) >>> 8);
result.push(k & 0xff);
// reserved ES_info_length
result.push(0xf0);
result.push(0x00); // ES_info_length = 0
}
// CRC_32
result.push([0x00, 0x00, 0x00, 0x00]); // invalid CRC but we don't check it
return result;
};
// Create a PAT packet
// @return {Array} bytes
makePat = function(options) {
var
result = [],
k;
// table_id
result.push(0x00);
// section_syntax_indicator '0' reserved section_length
result.push(0x80);
result.push(0x0d); // section_length for one program
// transport_stream_id
result.push(0x00);
result.push(0x00);
// reserved version_number current_next_indicator
result.push(0x01); // current_next_indicator is 1
// section_number
result.push(0x00);
// last_section_number
result.push(0x00);
for (k in options.programs) {
// program_number
result.push((k & 0xFF00) >>> 8);
result.push(k & 0x00FF);
// reserved program_map_pid
result.push((options.programs[k] & 0x1f00) >>> 8);
result.push(options.programs[k] & 0xff);
}
return result;
};
// Create a PAT or PMT packet based on the specified options
// @return {Array} bytes
makePsi = function(options) {
var result = [];
// pointer_field
if (options.payloadUnitStartIndicator) {
result.push(0x00);
}
if (options.programs) {
return result.concat(makePat(options));
}
return result.concat(makePmt(options));
};
// Construct an M2TS packet
// @return {Array} bytes
makePacket = function(options) {
var
result = [],
settings = extend({
payloadUnitStartIndicator: true,
pid: 0x00
}, options);
// header
// sync_byte
result.push(0x47);
// transport_error_indicator payload_unit_start_indicator transport_priority PID
result.push((settings.pid & 0x1f) << 8 | 0x40);
result.push(settings.pid & 0xff);
// transport_scrambling_control adaptation_field_control continuity_counter
result.push(0x10);
result = result.concat(makePsi(settings));
// ensure the resulting packet is the correct size
result.length = window.videojs.Hls.SegmentParser.MP2T_PACKET_LENGTH;
return result;
};
test('parses PMTs with program descriptors', function() {
var
makePmt = function(options) {
var
result = [],
entryCount = 0,
k,
sectionLength;
for (k in options.pids) {
entryCount++;
}
// table_id
result.push(0x02);
// section_syntax_indicator '0' reserved section_length
// 13 + (program_info_length) + (n * 5 + ES_info_length[n])
sectionLength = 13 + (5 * entryCount) + 17;
result.push(0x80 | (0xF00 & sectionLength >>> 8));
result.push(sectionLength & 0xFF);
// program_number
result.push(0x00);
result.push(0x01);
// reserved version_number current_next_indicator
result.push(0x01);
// section_number
result.push(0x00);
// last_section_number
result.push(0x00);
// reserved PCR_PID
result.push(0xe1);
result.push(0x00);
// reserved program_info_length
result.push(0xf0);
result.push(0x11); // hard-coded 17 byte descriptor
// program descriptors
result = result.concat([
0x25, 0x0f, 0xff, 0xff,
0x49, 0x44, 0x33, 0x20,
0xff, 0x49, 0x44, 0x33,
0x20, 0x00, 0x1f, 0x00,
0x01
]);
for (k in options.pids) {
// stream_type
result.push(options.pids[k]);
// reserved elementary_PID
result.push(0xe0 | (k & 0x1f00) >>> 8);
result.push(k & 0xff);
// reserved ES_info_length
result.push(0xf0);
result.push(0x00); // ES_info_length = 0
}
// CRC_32
result.push([0x00, 0x00, 0x00, 0x00]); // invalid CRC but we don't check it
return result;
},
makePat = function(options) {
var
result = [],
k;
// table_id
result.push(0x00);
// section_syntax_indicator '0' reserved section_length
result.push(0x80);
result.push(0x0d); // section_length for one program
// transport_stream_id
result.push(0x00);
result.push(0x00);
// reserved version_number current_next_indicator
result.push(0x01); // current_next_indicator is 1
// section_number
result.push(0x00);
// last_section_number
result.push(0x00);
for (k in options.programs) {
// program_number
result.push((k & 0xFF00) >>> 8);
result.push(k & 0x00FF);
// reserved program_map_pid
result.push((options.programs[k] & 0x1f00) >>> 8);
result.push(options.programs[k] & 0xff);
}
return result;
},
makePsi = function(options) {
var result = [];
// pointer_field
if (options.payloadUnitStartIndicator) {
result.push(0x00);
}
if (options.programs) {
return result.concat(makePat(options));
}
return result.concat(makePmt(options));
},
makePacket = function(options) {
var
result = [],
settings = extend({
payloadUnitStartIndicator: true,
pid: 0x00
}, options);
// header
// sync_byte
result.push(0x47);
// transport_error_indicator payload_unit_start_indicator transport_priority PID
result.push((settings.pid & 0x1f) << 8 | 0x40);
result.push(settings.pid & 0xff);
// transport_scrambling_control adaptation_field_control continuity_counter
result.push(0x10);
result = result.concat(makePsi(settings));
// ensure the resulting packet is the correct size
result.length = window.videojs.Hls.SegmentParser.MP2T_PACKET_LENGTH;
return result;
},
h264Type = window.videojs.Hls.SegmentParser.STREAM_TYPES.h264,
adtsType = window.videojs.Hls.SegmentParser.STREAM_TYPES.adts;
......@@ -191,6 +210,22 @@
strictEqual(parser.stream.programMapTable[adtsType], 0x03, 'audio is PID 3');
});
test('recognizes metadata streams', function() {
parser.parseSegmentBinaryData(new Uint8Array(makePacket({
programs: {
0x01: [0x01]
}
}).concat(makePacket({
pid: 0x01,
pids: {
// Rec. ITU-T H.222.0 (06/2012), Table 2-34
0x02: 0x15 // Metadata carried in PES packets
}
}))));
equal(parser.stream.programMapTable[0x15], 0x02, 'metadata is PID 2');
});
test('parses the first bipbop segment', function() {
parser.parseSegmentBinaryData(window.bcSegment);
......
......@@ -21,14 +21,15 @@
<!-- HLS plugin -->
<script src="../src/videojs-hls.js"></script>
<script src="../src/xhr.js"></script>
<script src="../src/stream.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/metadata-stream.js"></script>
<script src="../src/segment-parser.js"></script>
<!-- M3U8 -->
<script src="../src/stream.js"></script>
<script src="../src/m3u8/m3u8-parser.js"></script>
<script src="../src/playlist-loader.js"></script>
<script src="../node_modules/pkcs7/dist/pkcs7.unpad.js"></script>
......@@ -55,6 +56,7 @@
<script src="h264-stream_test.js"></script>
<script src="exp-golomb_test.js"></script>
<script src="flv-tag_test.js"></script>
<script src="metadata-stream_test.js"></script>
<script src="m3u8_test.js"></script>
<script src="playlist-loader_test.js"></script>
<script src="decrypter_test.js"></script>
......
......@@ -110,6 +110,9 @@ var
this.getNextTag = function() {
return tags.shift();
};
this.metadataStream = {
on: Function.prototype
};
};
};
......@@ -897,6 +900,9 @@ test('flushes the parser after each segment', function() {
flushes++;
};
this.tagsAvailable = function() {};
this.metadataStream = {
on: Function.prototype
};
};
player.src({
......@@ -910,6 +916,52 @@ test('flushes the parser after each segment', function() {
strictEqual(flushes, 1, 'tags are flushed at the end of a segment');
});
test('exposes in-band metadata events as cues', function() {
var track;
player.src({
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
player.hls.segmentParser_.parseSegmentBinaryData = function() {
// fake out a descriptor
player.hls.segmentParser_.metadataStream.descriptor = new Uint8Array([
1, 2, 3, 0xbb
]);
// trigger a metadata event
player.hls.segmentParser_.metadataStream.trigger('data', {
pts: 2000,
data: new Uint8Array([]),
frames: [{
type: 'TXXX',
value: 'cue text'
}, {
type: 'WXXX',
url: 'http://example.com'
}]
});
};
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
equal(player.textTracks().length, 1, 'created a text track');
track = player.textTracks()[0];
equal(track.kind, 'metadata', 'kind is metadata');
equal(track.inBandMetadataTrackDispatchType, '15010203BB', 'set the dispatch type');
equal(track.cues.length, 2, 'created two cues');
equal(track.cues[0].startTime, 2, 'cue starts at 2 seconds');
equal(track.cues[0].endTime, 2, 'cue ends at 2 seconds');
equal(track.cues[0].pauseOnExit, false, 'cue does not pause on exit');
equal(track.cues[0].text, 'cue text', 'set cue text');
equal(track.cues[1].startTime, 2, 'cue starts at 2 seconds');
equal(track.cues[1].endTime, 2, 'cue ends at 2 seconds');
equal(track.cues[1].pauseOnExit, false, 'cue does not pause on exit');
equal(track.cues[1].text, 'http://example.com', 'set cue text');
});
test('drops tags before the target timestamp when seeking', function() {
var i = 10,
tags = [],
......