f928ea8c by David LaPalomento

Expose ID3 metadata as cues on an in-band metadata text track

Listen for metadata events during transmuxing and expose each ID3 frame as a cue at the appropriate time. Assume TXXX and WXXX frames will use UTF-8 encoded text. Start work on exposing the dispatch type for the generated text track.
1 parent 9a3c3356
......@@ -6,9 +6,6 @@
(function(window, videojs, undefined) {
'use strict';
var
defaults = {
debug: false
},
parseString = function(bytes, start, end) {
var i, result = '';
for (i = start; i < end; i++) {
......@@ -54,10 +51,24 @@
MetadataStream = function(options) {
var settings = {
debug: !!(options && options.debug)
};
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;
......
......@@ -74,6 +74,29 @@ 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 textTrack;
tech.segmentParser_.metadataStream.on('data', function(metadata) {
var i, frame, time;
// create the metadata track if this is the first ID3 tag we've
// seen
if (!textTrack) {
textTrack = tech.player().addTextTrack('metadata', 'Timed Metadata');
textTrack.inBandMetadataTrackDispatchType = metadata.dispatchType;
}
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));
......
......@@ -64,10 +64,10 @@
result[6] = (size >>> 24) & 0xff;
result[7] = (size >>> 16) & 0xff;
result[8] = (size >>> 8) & 0xff;
result[9] = (size) & 0xff
result[9] = (size) & 0xff;
return result;
}
};
id3Frame = function(type) {
var result = stringToInts(type).concat([
......@@ -219,7 +219,7 @@
// header
data: new Uint8Array(id3Tag(id3Frame('TXXX',
0x00,
0x03, // utf-8
stringToCString('get done'),
stringToInts('{ "key": "value" }')),
[0x00, 0x00]))
......@@ -244,7 +244,7 @@
// header
data: new Uint8Array(id3Tag(id3Frame('WXXX',
0x00,
0x03, // utf-8
stringToCString(''),
stringToInts(url)),
[0x00, 0x00]))
......@@ -269,7 +269,7 @@
// header
data: new Uint8Array(id3Tag(id3Frame('TXXX',
0x00,
0x03, // utf-8
stringToCString(''),
stringToInts(value)),
[0x00, 0x00]))
......@@ -280,4 +280,13 @@
'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);
......
......@@ -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,48 @@ 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() {
player.hls.segmentParser_.metadataStream.trigger('data', {
pts: 2000,
dispatchType: '15010203',
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, '15010203', '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 = [],
......