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 @@ ...@@ -6,9 +6,6 @@
6 (function(window, videojs, undefined) { 6 (function(window, videojs, undefined) {
7 'use strict'; 7 'use strict';
8 var 8 var
9 defaults = {
10 debug: false
11 },
12 parseString = function(bytes, start, end) { 9 parseString = function(bytes, start, end) {
13 var i, result = ''; 10 var i, result = '';
14 for (i = start; i < end; i++) { 11 for (i = start; i < end; i++) {
...@@ -54,10 +51,24 @@ ...@@ -54,10 +51,24 @@
54 51
55 MetadataStream = function(options) { 52 MetadataStream = function(options) {
56 var settings = { 53 var settings = {
57 debug: !!(options && options.debug) 54 debug: !!(options && options.debug),
58 }; 55
56 // the bytes of the program-level descriptor field in MP2T
57 // see ISO/IEC 13818-1:2013 (E), section 2.6 "Program and
58 // program element descriptors"
59 descriptor: options && options.descriptor
60 }, i;
59 MetadataStream.prototype.init.call(this); 61 MetadataStream.prototype.init.call(this);
60 62
63 // calculate the text track in-band metadata track dispatch type
64 // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track
65 this.dispatchType = videojs.Hls.SegmentParser.STREAM_TYPES.metadata.toString(16);
66 if (settings.descriptor) {
67 for (i = 0; i < settings.descriptor.length; i++) {
68 this.dispatchType += ('00' + settings.descriptor[i].toString(16)).slice(-2);
69 }
70 }
71
61 this.push = function(chunk) { 72 this.push = function(chunk) {
62 var tagSize, frameStart, frameSize, frame; 73 var tagSize, frameStart, frameSize, frame;
63 74
......
...@@ -74,6 +74,29 @@ videojs.Hls.prototype.src = function(src) { ...@@ -74,6 +74,29 @@ videojs.Hls.prototype.src = function(src) {
74 this.segmentBuffer_ = []; 74 this.segmentBuffer_ = [];
75 this.segmentParser_ = new videojs.Hls.SegmentParser(); 75 this.segmentParser_ = new videojs.Hls.SegmentParser();
76 76
77 // if the stream contains ID3 metadata, expose that as a metadata
78 // text track
79 (function() {
80 var textTrack;
81
82 tech.segmentParser_.metadataStream.on('data', function(metadata) {
83 var i, frame, time;
84
85 // create the metadata track if this is the first ID3 tag we've
86 // seen
87 if (!textTrack) {
88 textTrack = tech.player().addTextTrack('metadata', 'Timed Metadata');
89 textTrack.inBandMetadataTrackDispatchType = metadata.dispatchType;
90 }
91
92 for (i = 0; i < metadata.frames.length; i++) {
93 frame = metadata.frames[i];
94 time = metadata.pts / 1000;
95 textTrack.addCue(new window.VTTCue(time, time, frame.value || frame.url));
96 }
97 });
98 })();
99
77 // load the MediaSource into the player 100 // load the MediaSource into the player
78 this.mediaSource.addEventListener('sourceopen', videojs.bind(this, this.handleSourceOpen)); 101 this.mediaSource.addEventListener('sourceopen', videojs.bind(this, this.handleSourceOpen));
79 102
......
...@@ -64,10 +64,10 @@ ...@@ -64,10 +64,10 @@
64 result[6] = (size >>> 24) & 0xff; 64 result[6] = (size >>> 24) & 0xff;
65 result[7] = (size >>> 16) & 0xff; 65 result[7] = (size >>> 16) & 0xff;
66 result[8] = (size >>> 8) & 0xff; 66 result[8] = (size >>> 8) & 0xff;
67 result[9] = (size) & 0xff 67 result[9] = (size) & 0xff;
68 68
69 return result; 69 return result;
70 } 70 };
71 71
72 id3Frame = function(type) { 72 id3Frame = function(type) {
73 var result = stringToInts(type).concat([ 73 var result = stringToInts(type).concat([
...@@ -219,7 +219,7 @@ ...@@ -219,7 +219,7 @@
219 219
220 // header 220 // header
221 data: new Uint8Array(id3Tag(id3Frame('TXXX', 221 data: new Uint8Array(id3Tag(id3Frame('TXXX',
222 0x00, 222 0x03, // utf-8
223 stringToCString('get done'), 223 stringToCString('get done'),
224 stringToInts('{ "key": "value" }')), 224 stringToInts('{ "key": "value" }')),
225 [0x00, 0x00])) 225 [0x00, 0x00]))
...@@ -244,7 +244,7 @@ ...@@ -244,7 +244,7 @@
244 244
245 // header 245 // header
246 data: new Uint8Array(id3Tag(id3Frame('WXXX', 246 data: new Uint8Array(id3Tag(id3Frame('WXXX',
247 0x00, 247 0x03, // utf-8
248 stringToCString(''), 248 stringToCString(''),
249 stringToInts(url)), 249 stringToInts(url)),
250 [0x00, 0x00])) 250 [0x00, 0x00]))
...@@ -269,7 +269,7 @@ ...@@ -269,7 +269,7 @@
269 269
270 // header 270 // header
271 data: new Uint8Array(id3Tag(id3Frame('TXXX', 271 data: new Uint8Array(id3Tag(id3Frame('TXXX',
272 0x00, 272 0x03, // utf-8
273 stringToCString(''), 273 stringToCString(''),
274 stringToInts(value)), 274 stringToInts(value)),
275 [0x00, 0x00])) 275 [0x00, 0x00]))
...@@ -280,4 +280,13 @@ ...@@ -280,4 +280,13 @@
280 'parsed the single-digit character'); 280 'parsed the single-digit character');
281 }); 281 });
282 282
283 // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track
284 test('constructs the dispatch type', function() {
285 metadataStream = new videojs.Hls.MetadataStream({
286 descriptor: new Uint8Array([0x03, 0x02, 0x01, 0x00])
287 });
288
289 equal(metadataStream.dispatchType, '1503020100', 'built the dispatch type');
290 });
291
283 })(window, window.videojs); 292 })(window, window.videojs);
......
...@@ -110,6 +110,9 @@ var ...@@ -110,6 +110,9 @@ var
110 this.getNextTag = function() { 110 this.getNextTag = function() {
111 return tags.shift(); 111 return tags.shift();
112 }; 112 };
113 this.metadataStream = {
114 on: Function.prototype
115 };
113 }; 116 };
114 }; 117 };
115 118
...@@ -897,6 +900,9 @@ test('flushes the parser after each segment', function() { ...@@ -897,6 +900,9 @@ test('flushes the parser after each segment', function() {
897 flushes++; 900 flushes++;
898 }; 901 };
899 this.tagsAvailable = function() {}; 902 this.tagsAvailable = function() {};
903 this.metadataStream = {
904 on: Function.prototype
905 };
900 }; 906 };
901 907
902 player.src({ 908 player.src({
...@@ -910,6 +916,48 @@ test('flushes the parser after each segment', function() { ...@@ -910,6 +916,48 @@ test('flushes the parser after each segment', function() {
910 strictEqual(flushes, 1, 'tags are flushed at the end of a segment'); 916 strictEqual(flushes, 1, 'tags are flushed at the end of a segment');
911 }); 917 });
912 918
919 test('exposes in-band metadata events as cues', function() {
920 var track;
921 player.src({
922 src: 'manifest/media.m3u8',
923 type: 'application/vnd.apple.mpegurl'
924 });
925 openMediaSource(player);
926
927 player.hls.segmentParser_.parseSegmentBinaryData = function() {
928 player.hls.segmentParser_.metadataStream.trigger('data', {
929 pts: 2000,
930 dispatchType: '15010203',
931 data: new Uint8Array([]),
932 frames: [{
933 type: 'TXXX',
934 value: 'cue text'
935 }, {
936 type: 'WXXX',
937 url: 'http://example.com'
938 }]
939 });
940 };
941
942 standardXHRResponse(requests[0]);
943 standardXHRResponse(requests[1]);
944
945 equal(player.textTracks().length, 1, 'created a text track');
946 track = player.textTracks()[0];
947 equal(track.kind, 'metadata', 'kind is metadata');
948 equal(track.inBandMetadataTrackDispatchType, '15010203', 'set the dispatch type');
949 equal(track.cues.length, 2, 'created two cues');
950 equal(track.cues[0].startTime, 2, 'cue starts at 2 seconds');
951 equal(track.cues[0].endTime, 2, 'cue ends at 2 seconds');
952 equal(track.cues[0].pauseOnExit, false, 'cue does not pause on exit');
953 equal(track.cues[0].text, 'cue text', 'set cue text');
954
955 equal(track.cues[1].startTime, 2, 'cue starts at 2 seconds');
956 equal(track.cues[1].endTime, 2, 'cue ends at 2 seconds');
957 equal(track.cues[1].pauseOnExit, false, 'cue does not pause on exit');
958 equal(track.cues[1].text, 'http://example.com', 'set cue text');
959 });
960
913 test('drops tags before the target timestamp when seeking', function() { 961 test('drops tags before the target timestamp when seeking', function() {
914 var i = 10, 962 var i = 10,
915 tags = [], 963 tags = [],
......