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.
Showing
4 changed files
with
101 additions
and
10 deletions
... | @@ -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 = [], | ... | ... |
-
Please register or sign in to post a comment