Merge pull request #262 from videojs/id3-enhancements
Id3 enhancements
Showing
7 changed files
with
320 additions
and
67 deletions
... | @@ -166,12 +166,19 @@ embedded as [ID3 tags](http://id3.org/id3v2.3.0). When a stream is | ... | @@ -166,12 +166,19 @@ embedded as [ID3 tags](http://id3.org/id3v2.3.0). When a stream is |
166 | encountered with embedded metadata, an [in-band metadata text | 166 | encountered with embedded metadata, an [in-band metadata text |
167 | track](https://html.spec.whatwg.org/multipage/embedded-content.html#text-track-in-band-metadata-track-dispatch-type) | 167 | track](https://html.spec.whatwg.org/multipage/embedded-content.html#text-track-in-band-metadata-track-dispatch-type) |
168 | will automatically be created and populated with cues as they are | 168 | will automatically be created and populated with cues as they are |
169 | encountered in the stream. Only UTF-8 encoded | 169 | encountered in the stream. UTF-8 encoded |
170 | [TXXX](http://id3.org/id3v2.3.0#User_defined_text_information_frame) | 170 | [TXXX](http://id3.org/id3v2.3.0#User_defined_text_information_frame) |
171 | and [WXXX](http://id3.org/id3v2.3.0#User_defined_URL_link_frame) ID3 | 171 | and [WXXX](http://id3.org/id3v2.3.0#User_defined_URL_link_frame) ID3 |
172 | frames are currently mapped to cue points. There are lots of guides | 172 | frames are mapped to cue points and their values set as the cue |
173 | and references to using text tracks [around the | 173 | text. Cues are created for all other frame types and the data is |
174 | web](http://www.html5rocks.com/en/tutorials/track/basics/). | 174 | attached to the generated cue: |
175 | |||
176 | ```js | ||
177 | cue.frame.data | ||
178 | ``` | ||
179 | |||
180 | There are lots of guides and references to using text tracks [around | ||
181 | the web](http://www.html5rocks.com/en/tutorials/track/basics/). | ||
175 | 182 | ||
176 | ### Testing | 183 | ### Testing |
177 | 184 | ... | ... |
... | @@ -6,12 +6,24 @@ | ... | @@ -6,12 +6,24 @@ |
6 | (function(window, videojs, undefined) { | 6 | (function(window, videojs, undefined) { |
7 | 'use strict'; | 7 | 'use strict'; |
8 | var | 8 | var |
9 | parseString = function(bytes, start, end) { | 9 | // return a percent-encoded representation of the specified byte range |
10 | // @see http://en.wikipedia.org/wiki/Percent-encoding | ||
11 | percentEncode = function(bytes, start, end) { | ||
10 | var i, result = ''; | 12 | var i, result = ''; |
11 | for (i = start; i < end; i++) { | 13 | for (i = start; i < end; i++) { |
12 | result += '%' + ('00' + bytes[i].toString(16)).slice(-2); | 14 | result += '%' + ('00' + bytes[i].toString(16)).slice(-2); |
13 | } | 15 | } |
14 | return window.decodeURIComponent(result); | 16 | return result; |
17 | }, | ||
18 | // return the string representation of the specified byte range, | ||
19 | // interpreted as UTf-8. | ||
20 | parseUtf8 = function(bytes, start, end) { | ||
21 | return window.decodeURIComponent(percentEncode(bytes, start, end)); | ||
22 | }, | ||
23 | // return the string representation of the specified byte range, | ||
24 | // interpreted as ISO-8859-1. | ||
25 | parseIso88591 = function(bytes, start, end) { | ||
26 | return window.unescape(percentEncode(bytes, start, end)); | ||
15 | }, | 27 | }, |
16 | tagParsers = { | 28 | tagParsers = { |
17 | 'TXXX': function(tag) { | 29 | 'TXXX': function(tag) { |
... | @@ -24,8 +36,8 @@ | ... | @@ -24,8 +36,8 @@ |
24 | for (i = 1; i < tag.data.length; i++) { | 36 | for (i = 1; i < tag.data.length; i++) { |
25 | if (tag.data[i] === 0) { | 37 | if (tag.data[i] === 0) { |
26 | // parse the text fields | 38 | // parse the text fields |
27 | tag.description = parseString(tag.data, 1, i); | 39 | tag.description = parseUtf8(tag.data, 1, i); |
28 | tag.value = parseString(tag.data, i + 1, tag.data.length); | 40 | tag.value = parseUtf8(tag.data, i + 1, tag.data.length); |
29 | break; | 41 | break; |
30 | } | 42 | } |
31 | } | 43 | } |
... | @@ -40,24 +52,45 @@ | ... | @@ -40,24 +52,45 @@ |
40 | for (i = 1; i < tag.data.length; i++) { | 52 | for (i = 1; i < tag.data.length; i++) { |
41 | if (tag.data[i] === 0) { | 53 | if (tag.data[i] === 0) { |
42 | // parse the description and URL fields | 54 | // parse the description and URL fields |
43 | tag.description = parseString(tag.data, 1, i); | 55 | tag.description = parseUtf8(tag.data, 1, i); |
44 | tag.url = parseString(tag.data, i + 1, tag.data.length); | 56 | tag.url = parseUtf8(tag.data, i + 1, tag.data.length); |
45 | break; | 57 | break; |
46 | } | 58 | } |
47 | } | 59 | } |
60 | }, | ||
61 | 'PRIV': function(tag) { | ||
62 | var i; | ||
63 | |||
64 | for (i = 0; i < tag.data.length; i++) { | ||
65 | if (tag.data[i] === 0) { | ||
66 | // parse the description and URL fields | ||
67 | tag.owner = parseIso88591(tag.data, 0, i); | ||
68 | break; | ||
69 | } | ||
70 | } | ||
71 | tag.privateData = tag.data.subarray(i + 1); | ||
48 | } | 72 | } |
49 | }, | 73 | }, |
50 | MetadataStream; | 74 | MetadataStream; |
51 | 75 | ||
52 | MetadataStream = function(options) { | 76 | MetadataStream = function(options) { |
53 | var settings = { | 77 | var |
54 | debug: !!(options && options.debug), | 78 | settings = { |
55 | 79 | debug: !!(options && options.debug), | |
56 | // the bytes of the program-level descriptor field in MP2T | 80 | |
57 | // see ISO/IEC 13818-1:2013 (E), section 2.6 "Program and | 81 | // the bytes of the program-level descriptor field in MP2T |
58 | // program element descriptors" | 82 | // see ISO/IEC 13818-1:2013 (E), section 2.6 "Program and |
59 | descriptor: options && options.descriptor | 83 | // program element descriptors" |
60 | }, i; | 84 | descriptor: options && options.descriptor |
85 | }, | ||
86 | // the total size in bytes of the ID3 tag being parsed | ||
87 | tagSize = 0, | ||
88 | // tag data that is not complete enough to be parsed | ||
89 | buffer = [], | ||
90 | // the total number of bytes currently in the buffer | ||
91 | bufferSize = 0, | ||
92 | i; | ||
93 | |||
61 | MetadataStream.prototype.init.call(this); | 94 | MetadataStream.prototype.init.call(this); |
62 | 95 | ||
63 | // calculate the text track in-band metadata track dispatch type | 96 | // calculate the text track in-band metadata track dispatch type |
... | @@ -70,73 +103,111 @@ | ... | @@ -70,73 +103,111 @@ |
70 | } | 103 | } |
71 | 104 | ||
72 | this.push = function(chunk) { | 105 | this.push = function(chunk) { |
73 | var tagSize, frameStart, frameSize, frame; | 106 | var tag, frameStart, frameSize, frame, i; |
74 | 107 | ||
75 | // ignore events that don't look like ID3 data | 108 | // ignore events that don't look like ID3 data |
76 | if (chunk.data.length < 10 || | 109 | if (buffer.length === 0 && |
77 | chunk.data[0] !== 'I'.charCodeAt(0) || | 110 | (chunk.data.length < 10 || |
78 | chunk.data[1] !== 'D'.charCodeAt(0) || | 111 | chunk.data[0] !== 'I'.charCodeAt(0) || |
79 | chunk.data[2] !== '3'.charCodeAt(0)) { | 112 | chunk.data[1] !== 'D'.charCodeAt(0) || |
113 | chunk.data[2] !== '3'.charCodeAt(0))) { | ||
80 | if (settings.debug) { | 114 | if (settings.debug) { |
81 | videojs.log('Skipping unrecognized metadata stream'); | 115 | videojs.log('Skipping unrecognized metadata packet'); |
82 | } | 116 | } |
83 | return; | 117 | return; |
84 | } | 118 | } |
85 | 119 | ||
120 | // add this chunk to the data we've collected so far | ||
121 | buffer.push(chunk); | ||
122 | bufferSize += chunk.data.byteLength; | ||
123 | |||
124 | // grab the size of the entire frame from the ID3 header | ||
125 | if (buffer.length === 1) { | ||
126 | // the frame size is transmitted as a 28-bit integer in the | ||
127 | // last four bytes of the ID3 header. | ||
128 | // The most significant bit of each byte is dropped and the | ||
129 | // results concatenated to recover the actual value. | ||
130 | tagSize = (chunk.data[6] << 21) | | ||
131 | (chunk.data[7] << 14) | | ||
132 | (chunk.data[8] << 7) | | ||
133 | (chunk.data[9]); | ||
134 | |||
135 | // ID3 reports the tag size excluding the header but it's more | ||
136 | // convenient for our comparisons to include it | ||
137 | tagSize += 10; | ||
138 | } | ||
139 | |||
140 | // if the entire frame has not arrived, wait for more data | ||
141 | if (bufferSize < tagSize) { | ||
142 | return; | ||
143 | } | ||
144 | |||
145 | // collect the entire frame so it can be parsed | ||
146 | tag = { | ||
147 | data: new Uint8Array(tagSize), | ||
148 | frames: [], | ||
149 | pts: buffer[0].pts, | ||
150 | dts: buffer[0].dts | ||
151 | }; | ||
152 | for (i = 0; i < tagSize;) { | ||
153 | tag.data.set(buffer[0].data, i); | ||
154 | i += buffer[0].data.byteLength; | ||
155 | bufferSize -= buffer[0].data.byteLength; | ||
156 | buffer.shift(); | ||
157 | } | ||
158 | |||
86 | // find the start of the first frame and the end of the tag | 159 | // find the start of the first frame and the end of the tag |
87 | tagSize = chunk.data.byteLength; | ||
88 | frameStart = 10; | 160 | frameStart = 10; |
89 | if (chunk.data[5] & 0x40) { | 161 | if (tag.data[5] & 0x40) { |
90 | // advance the frame start past the extended header | 162 | // advance the frame start past the extended header |
91 | frameStart += 4; // header size field | 163 | frameStart += 4; // header size field |
92 | frameStart += (chunk.data[10] << 24) | | 164 | frameStart += (tag.data[10] << 24) | |
93 | (chunk.data[11] << 16) | | 165 | (tag.data[11] << 16) | |
94 | (chunk.data[12] << 8) | | 166 | (tag.data[12] << 8) | |
95 | (chunk.data[13]); | 167 | (tag.data[13]); |
96 | 168 | ||
97 | // clip any padding off the end | 169 | // clip any padding off the end |
98 | tagSize -= (chunk.data[16] << 24) | | 170 | tagSize -= (tag.data[16] << 24) | |
99 | (chunk.data[17] << 16) | | 171 | (tag.data[17] << 16) | |
100 | (chunk.data[18] << 8) | | 172 | (tag.data[18] << 8) | |
101 | (chunk.data[19]); | 173 | (tag.data[19]); |
102 | } | 174 | } |
103 | 175 | ||
104 | // adjust the PTS values to align with the video and audio | 176 | // adjust the PTS values to align with the video and audio |
105 | // streams | 177 | // streams |
106 | if (this.timestampOffset) { | 178 | if (this.timestampOffset) { |
107 | chunk.pts -= this.timestampOffset; | 179 | tag.pts -= this.timestampOffset; |
108 | chunk.dts -= this.timestampOffset; | 180 | tag.dts -= this.timestampOffset; |
109 | } | 181 | } |
110 | 182 | ||
111 | // parse one or more ID3 frames | 183 | // parse one or more ID3 frames |
112 | // http://id3.org/id3v2.3.0#ID3v2_frame_overview | 184 | // http://id3.org/id3v2.3.0#ID3v2_frame_overview |
113 | chunk.frames = []; | ||
114 | do { | 185 | do { |
115 | // determine the number of bytes in this frame | 186 | // determine the number of bytes in this frame |
116 | frameSize = (chunk.data[frameStart + 4] << 24) | | 187 | frameSize = (tag.data[frameStart + 4] << 24) | |
117 | (chunk.data[frameStart + 5] << 16) | | 188 | (tag.data[frameStart + 5] << 16) | |
118 | (chunk.data[frameStart + 6] << 8) | | 189 | (tag.data[frameStart + 6] << 8) | |
119 | (chunk.data[frameStart + 7]); | 190 | (tag.data[frameStart + 7]); |
120 | if (frameSize < 1) { | 191 | if (frameSize < 1) { |
121 | return videojs.log('Malformed ID3 frame encountered. Skipping metadata parsing.'); | 192 | return videojs.log('Malformed ID3 frame encountered. Skipping metadata parsing.'); |
122 | } | 193 | } |
123 | 194 | ||
124 | frame = { | 195 | frame = { |
125 | id: String.fromCharCode(chunk.data[frameStart]) + | 196 | id: String.fromCharCode(tag.data[frameStart], |
126 | String.fromCharCode(chunk.data[frameStart + 1]) + | 197 | tag.data[frameStart + 1], |
127 | String.fromCharCode(chunk.data[frameStart + 2]) + | 198 | tag.data[frameStart + 2], |
128 | String.fromCharCode(chunk.data[frameStart + 3]), | 199 | tag.data[frameStart + 3]), |
129 | data: chunk.data.subarray(frameStart + 10, frameStart + frameSize + 10) | 200 | data: tag.data.subarray(frameStart + 10, frameStart + frameSize + 10) |
130 | }; | 201 | }; |
131 | if (tagParsers[frame.id]) { | 202 | if (tagParsers[frame.id]) { |
132 | tagParsers[frame.id](frame); | 203 | tagParsers[frame.id](frame); |
133 | } | 204 | } |
134 | chunk.frames.push(frame); | 205 | tag.frames.push(frame); |
135 | 206 | ||
136 | frameStart += 10; // advance past the frame header | 207 | frameStart += 10; // advance past the frame header |
137 | frameStart += frameSize; // advance past the frame body | 208 | frameStart += frameSize; // advance past the frame body |
138 | } while (frameStart < tagSize); | 209 | } while (frameStart < tagSize); |
139 | this.trigger('data', chunk); | 210 | this.trigger('data', tag); |
140 | }; | 211 | }; |
141 | }; | 212 | }; |
142 | MetadataStream.prototype = new videojs.Hls.Stream(); | 213 | MetadataStream.prototype = new videojs.Hls.Stream(); | ... | ... |
... | @@ -368,12 +368,6 @@ | ... | @@ -368,12 +368,6 @@ |
368 | aacStream.setNextTimeStamp(pts, | 368 | aacStream.setNextTimeStamp(pts, |
369 | pesPacketSize, | 369 | pesPacketSize, |
370 | dataAlignmentIndicator); | 370 | dataAlignmentIndicator); |
371 | } else { | ||
372 | self.metadataStream.push({ | ||
373 | pts: pts, | ||
374 | dts: dts, | ||
375 | data: data.subarray(offset) | ||
376 | }); | ||
377 | } | 371 | } |
378 | } | 372 | } |
379 | 373 | ||
... | @@ -381,6 +375,12 @@ | ... | @@ -381,6 +375,12 @@ |
381 | aacStream.writeBytes(data, offset, end - offset); | 375 | aacStream.writeBytes(data, offset, end - offset); |
382 | } else if (pid === self.stream.programMapTable[STREAM_TYPES.h264]) { | 376 | } else if (pid === self.stream.programMapTable[STREAM_TYPES.h264]) { |
383 | h264Stream.writeBytes(data, offset, end - offset); | 377 | h264Stream.writeBytes(data, offset, end - offset); |
378 | } else if (pid === self.stream.programMapTable[STREAM_TYPES.metadata]) { | ||
379 | self.metadataStream.push({ | ||
380 | pts: pts, | ||
381 | dts: dts, | ||
382 | data: data.subarray(offset) | ||
383 | }); | ||
384 | } | 384 | } |
385 | } else if (self.stream.pmtPid === pid) { | 385 | } else if (self.stream.pmtPid === pid) { |
386 | // similarly to the PAT, jump to the first byte of the section | 386 | // similarly to the PAT, jump to the first byte of the section | ... | ... |
... | @@ -99,7 +99,7 @@ videojs.Hls.prototype.src = function(src) { | ... | @@ -99,7 +99,7 @@ videojs.Hls.prototype.src = function(src) { |
99 | } | 99 | } |
100 | 100 | ||
101 | metadataStream.on('data', function(metadata) { | 101 | metadataStream.on('data', function(metadata) { |
102 | var i, frame, time, hexDigit; | 102 | var i, cue, frame, time, hexDigit; |
103 | 103 | ||
104 | // create the metadata track if this is the first ID3 tag we've | 104 | // create the metadata track if this is the first ID3 tag we've |
105 | // seen | 105 | // seen |
... | @@ -118,7 +118,9 @@ videojs.Hls.prototype.src = function(src) { | ... | @@ -118,7 +118,9 @@ videojs.Hls.prototype.src = function(src) { |
118 | for (i = 0; i < metadata.frames.length; i++) { | 118 | for (i = 0; i < metadata.frames.length; i++) { |
119 | frame = metadata.frames[i]; | 119 | frame = metadata.frames[i]; |
120 | time = metadata.pts / 1000; | 120 | time = metadata.pts / 1000; |
121 | textTrack.addCue(new window.VTTCue(time, time, frame.value || frame.url)); | 121 | cue = new window.VTTCue(time, time, frame.value || frame.url || ''); |
122 | cue.frame = frame; | ||
123 | textTrack.addCue(cue); | ||
122 | } | 124 | } |
123 | }); | 125 | }); |
124 | })(); | 126 | })(); | ... | ... |
... | @@ -60,11 +60,13 @@ | ... | @@ -60,11 +60,13 @@ |
60 | ], frames), | 60 | ], frames), |
61 | size; | 61 | size; |
62 | 62 | ||
63 | // size is stored as a sequence of four 7-bit integers with the | ||
64 | // high bit of each byte set to zero | ||
63 | size = result.length - 10; | 65 | size = result.length - 10; |
64 | result[6] = (size >>> 24) & 0xff; | 66 | result[6] = (size >>> 21) & 0x7f; |
65 | result[7] = (size >>> 16) & 0xff; | 67 | result[7] = (size >>> 14) & 0x7f; |
66 | result[8] = (size >>> 8) & 0xff; | 68 | result[8] = (size >>> 7) & 0x7f; |
67 | result[9] = (size) & 0xff; | 69 | result[9] = (size) & 0x7f; |
68 | 70 | ||
69 | return result; | 71 | return result; |
70 | }; | 72 | }; |
... | @@ -206,7 +208,7 @@ | ... | @@ -206,7 +208,7 @@ |
206 | equal(events[0].dts, 100, 'translated dts'); | 208 | equal(events[0].dts, 100, 'translated dts'); |
207 | }); | 209 | }); |
208 | 210 | ||
209 | test('parses TXXX tags', function() { | 211 | test('parses TXXX frames', function() { |
210 | var events = []; | 212 | var events = []; |
211 | metadataStream.on('data', function(event) { | 213 | metadataStream.on('data', function(event) { |
212 | events.push(event); | 214 | events.push(event); |
... | @@ -227,11 +229,12 @@ | ... | @@ -227,11 +229,12 @@ |
227 | 229 | ||
228 | equal(events.length, 1, 'parsed one tag'); | 230 | equal(events.length, 1, 'parsed one tag'); |
229 | equal(events[0].frames.length, 1, 'parsed one frame'); | 231 | equal(events[0].frames.length, 1, 'parsed one frame'); |
232 | equal(events[0].frames[0].id, 'TXXX', 'parsed the frame id'); | ||
230 | equal(events[0].frames[0].description, 'get done', 'parsed the description'); | 233 | equal(events[0].frames[0].description, 'get done', 'parsed the description'); |
231 | equal(events[0].frames[0].value, '{ "key": "value" }', 'parsed the value'); | 234 | equal(events[0].frames[0].value, '{ "key": "value" }', 'parsed the value'); |
232 | }); | 235 | }); |
233 | 236 | ||
234 | test('parses WXXX tags', function() { | 237 | test('parses WXXX frames', function() { |
235 | var events = [], url = 'http://example.com/path/file?abc=7&d=4#ty'; | 238 | var events = [], url = 'http://example.com/path/file?abc=7&d=4#ty'; |
236 | metadataStream.on('data', function(event) { | 239 | metadataStream.on('data', function(event) { |
237 | events.push(event); | 240 | events.push(event); |
... | @@ -252,11 +255,12 @@ | ... | @@ -252,11 +255,12 @@ |
252 | 255 | ||
253 | equal(events.length, 1, 'parsed one tag'); | 256 | equal(events.length, 1, 'parsed one tag'); |
254 | equal(events[0].frames.length, 1, 'parsed one frame'); | 257 | equal(events[0].frames.length, 1, 'parsed one frame'); |
258 | equal(events[0].frames[0].id, 'WXXX', 'parsed the frame id'); | ||
255 | equal(events[0].frames[0].description, '', 'parsed the description'); | 259 | equal(events[0].frames[0].description, '', 'parsed the description'); |
256 | equal(events[0].frames[0].url, url, 'parsed the value'); | 260 | equal(events[0].frames[0].url, url, 'parsed the value'); |
257 | }); | 261 | }); |
258 | 262 | ||
259 | test('parses TXXX tags with characters that have a single-digit hexadecimal representation', function() { | 263 | test('parses TXXX frames with characters that have a single-digit hexadecimal representation', function() { |
260 | var events = [], value = String.fromCharCode(7); | 264 | var events = [], value = String.fromCharCode(7); |
261 | metadataStream.on('data', function(event) { | 265 | metadataStream.on('data', function(event) { |
262 | events.push(event); | 266 | events.push(event); |
... | @@ -280,6 +284,138 @@ | ... | @@ -280,6 +284,138 @@ |
280 | 'parsed the single-digit character'); | 284 | 'parsed the single-digit character'); |
281 | }); | 285 | }); |
282 | 286 | ||
287 | test('parses PRIV frames', function() { | ||
288 | var | ||
289 | events = [], | ||
290 | payload = stringToInts('arbitrary data may be included in the payload ' + | ||
291 | 'of a PRIV frame'); | ||
292 | |||
293 | metadataStream.on('data', function(event) { | ||
294 | events.push(event); | ||
295 | }); | ||
296 | |||
297 | metadataStream.push({ | ||
298 | trackId: 7, | ||
299 | pts: 1000, | ||
300 | dts: 900, | ||
301 | |||
302 | // header | ||
303 | data: new Uint8Array(id3Tag(id3Frame('PRIV', | ||
304 | stringToCString('priv-owner@example.com'), | ||
305 | payload))) | ||
306 | }); | ||
307 | |||
308 | equal(events.length, 1, 'parsed a tag'); | ||
309 | equal(events[0].frames.length, 1, 'parsed a frame'); | ||
310 | equal(events[0].frames[0].id, 'PRIV', 'frame id is PRIV'); | ||
311 | equal(events[0].frames[0].owner, 'priv-owner@example.com', 'parsed the owner'); | ||
312 | deepEqual(new Uint8Array(events[0].frames[0].privateData), | ||
313 | new Uint8Array(payload), | ||
314 | 'parsed the frame private data'); | ||
315 | |||
316 | }); | ||
317 | |||
318 | test('parses tags split across pushes', function() { | ||
319 | var | ||
320 | events = [], | ||
321 | owner = stringToCString('owner@example.com'), | ||
322 | payload = stringToInts('A TS packet is 188 bytes in length so that it can' + | ||
323 | ' be easily transmitted over ATM networks, an ' + | ||
324 | 'important medium at one time. We want to be sure' + | ||
325 | ' that ID3 frames larger than a TS packet are ' + | ||
326 | 'properly re-assembled.'), | ||
327 | tag = new Uint8Array(id3Tag(id3Frame('PRIV', owner, payload))), | ||
328 | front = tag.subarray(0, 100), | ||
329 | back = tag.subarray(100); | ||
330 | |||
331 | metadataStream.on('data', function(event) { | ||
332 | events.push(event); | ||
333 | }); | ||
334 | |||
335 | metadataStream.push({ | ||
336 | trackId: 7, | ||
337 | pts: 1000, | ||
338 | dts: 900, | ||
339 | data: front | ||
340 | }); | ||
341 | |||
342 | equal(events.length, 0, 'parsed zero tags'); | ||
343 | |||
344 | metadataStream.push({ | ||
345 | trackId: 7, | ||
346 | pts: 1000, | ||
347 | dts: 900, | ||
348 | data: back | ||
349 | }); | ||
350 | |||
351 | equal(events.length, 1, 'parsed a tag'); | ||
352 | equal(events[0].frames.length, 1, 'parsed a frame'); | ||
353 | equal(events[0].frames[0].data.byteLength, | ||
354 | owner.length + payload.length, | ||
355 | 'collected data across pushes'); | ||
356 | |||
357 | // parses subsequent fragmented tags | ||
358 | tag = new Uint8Array(id3Tag(id3Frame('PRIV', | ||
359 | owner, payload, payload))); | ||
360 | front = tag.subarray(0, 188); | ||
361 | back = tag.subarray(188); | ||
362 | metadataStream.push({ | ||
363 | trackId: 7, | ||
364 | pts: 2000, | ||
365 | dts: 2000, | ||
366 | data: front | ||
367 | }); | ||
368 | metadataStream.push({ | ||
369 | trackId: 7, | ||
370 | pts: 2000, | ||
371 | dts: 2000, | ||
372 | data: back | ||
373 | }); | ||
374 | equal(events.length, 2, 'parsed a subseqent frame'); | ||
375 | }); | ||
376 | |||
377 | test('ignores tags when the header is fragmented', function() { | ||
378 | |||
379 | var | ||
380 | events = [], | ||
381 | tag = new Uint8Array(id3Tag(id3Frame('PRIV', | ||
382 | stringToCString('owner@example.com'), | ||
383 | stringToInts('payload')))), | ||
384 | // split the 10-byte ID3 tag header in half | ||
385 | front = tag.subarray(0, 5), | ||
386 | back = tag.subarray(5); | ||
387 | |||
388 | metadataStream.on('data', function(event) { | ||
389 | events.push(event); | ||
390 | }); | ||
391 | |||
392 | metadataStream.push({ | ||
393 | trackId: 7, | ||
394 | pts: 1000, | ||
395 | dts: 900, | ||
396 | data: front | ||
397 | }); | ||
398 | metadataStream.push({ | ||
399 | trackId: 7, | ||
400 | pts: 1000, | ||
401 | dts: 900, | ||
402 | data: back | ||
403 | }); | ||
404 | |||
405 | equal(events.length, 0, 'parsed zero tags'); | ||
406 | |||
407 | metadataStream.push({ | ||
408 | trackId: 7, | ||
409 | pts: 1500, | ||
410 | dts: 1500, | ||
411 | data: new Uint8Array(id3Tag(id3Frame('PRIV', | ||
412 | stringToCString('owner2'), | ||
413 | stringToInts('payload2')))) | ||
414 | }); | ||
415 | equal(events.length, 1, 'parsed one tag'); | ||
416 | equal(events[0].frames[0].owner, 'owner2', 'dropped the first tag'); | ||
417 | }); | ||
418 | |||
283 | // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track | 419 | // 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() { | 420 | test('constructs the dispatch type', function() { |
285 | metadataStream = new videojs.Hls.MetadataStream({ | 421 | metadataStream = new videojs.Hls.MetadataStream({ | ... | ... |
... | @@ -177,7 +177,8 @@ | ... | @@ -177,7 +177,8 @@ |
177 | // sync_byte | 177 | // sync_byte |
178 | result.push(0x47); | 178 | result.push(0x47); |
179 | // transport_error_indicator payload_unit_start_indicator transport_priority PID | 179 | // transport_error_indicator payload_unit_start_indicator transport_priority PID |
180 | result.push((settings.pid & 0x1f) << 8 | 0x40); | 180 | result.push((settings.pid & 0x1f) << 8 | |
181 | (settings.payloadUnitStartIndicator ? 0x40 : 0x00)); | ||
181 | result.push(settings.pid & 0xff); | 182 | result.push(settings.pid & 0xff); |
182 | // transport_scrambling_control adaptation_field_control continuity_counter | 183 | // transport_scrambling_control adaptation_field_control continuity_counter |
183 | result.push(0x10); | 184 | result.push(0x10); |
... | @@ -226,6 +227,29 @@ | ... | @@ -226,6 +227,29 @@ |
226 | equal(parser.stream.programMapTable[0x15], 0x02, 'metadata is PID 2'); | 227 | equal(parser.stream.programMapTable[0x15], 0x02, 'metadata is PID 2'); |
227 | }); | 228 | }); |
228 | 229 | ||
230 | test('recognizes subsequent metadata packets after the payload start', function() { | ||
231 | var packets = []; | ||
232 | parser.metadataStream.push = function(packet) { | ||
233 | packets.push(packet); | ||
234 | }; | ||
235 | parser.parseSegmentBinaryData(new Uint8Array(makePacket({ | ||
236 | programs: { | ||
237 | 0x01: [0x01] | ||
238 | } | ||
239 | }).concat(makePacket({ | ||
240 | pid: 0x01, | ||
241 | pids: { | ||
242 | // Rec. ITU-T H.222.0 (06/2012), Table 2-34 | ||
243 | 0x02: 0x15 // Metadata carried in PES packets | ||
244 | } | ||
245 | })).concat(makePacket({ | ||
246 | pid: 0x02, | ||
247 | payloadUnitStartIndicator: false | ||
248 | })))); | ||
249 | |||
250 | equal(packets.length, 1, 'parsed non-payload metadata packet'); | ||
251 | }); | ||
252 | |||
229 | test('parses the first bipbop segment', function() { | 253 | test('parses the first bipbop segment', function() { |
230 | parser.parseSegmentBinaryData(window.bcSegment); | 254 | parser.parseSegmentBinaryData(window.bcSegment); |
231 | 255 | ... | ... |
... | @@ -1112,11 +1112,15 @@ test('exposes in-band metadata events as cues', function() { | ... | @@ -1112,11 +1112,15 @@ test('exposes in-band metadata events as cues', function() { |
1112 | pts: 2000, | 1112 | pts: 2000, |
1113 | data: new Uint8Array([]), | 1113 | data: new Uint8Array([]), |
1114 | frames: [{ | 1114 | frames: [{ |
1115 | type: 'TXXX', | 1115 | id: 'TXXX', |
1116 | value: 'cue text' | 1116 | value: 'cue text' |
1117 | }, { | 1117 | }, { |
1118 | type: 'WXXX', | 1118 | id: 'WXXX', |
1119 | url: 'http://example.com' | 1119 | url: 'http://example.com' |
1120 | }, { | ||
1121 | id: 'PRIV', | ||
1122 | owner: 'owner@example.com', | ||
1123 | privateData: new Uint8Array([1, 2, 3]) | ||
1120 | }] | 1124 | }] |
1121 | }); | 1125 | }); |
1122 | }; | 1126 | }; |
... | @@ -1128,7 +1132,7 @@ test('exposes in-band metadata events as cues', function() { | ... | @@ -1128,7 +1132,7 @@ test('exposes in-band metadata events as cues', function() { |
1128 | track = player.textTracks()[0]; | 1132 | track = player.textTracks()[0]; |
1129 | equal(track.kind, 'metadata', 'kind is metadata'); | 1133 | equal(track.kind, 'metadata', 'kind is metadata'); |
1130 | equal(track.inBandMetadataTrackDispatchType, '15010203BB', 'set the dispatch type'); | 1134 | equal(track.inBandMetadataTrackDispatchType, '15010203BB', 'set the dispatch type'); |
1131 | equal(track.cues.length, 2, 'created two cues'); | 1135 | equal(track.cues.length, 3, 'created three cues'); |
1132 | equal(track.cues[0].startTime, 2, 'cue starts at 2 seconds'); | 1136 | equal(track.cues[0].startTime, 2, 'cue starts at 2 seconds'); |
1133 | equal(track.cues[0].endTime, 2, 'cue ends at 2 seconds'); | 1137 | equal(track.cues[0].endTime, 2, 'cue ends at 2 seconds'); |
1134 | equal(track.cues[0].pauseOnExit, false, 'cue does not pause on exit'); | 1138 | equal(track.cues[0].pauseOnExit, false, 'cue does not pause on exit'); |
... | @@ -1138,6 +1142,15 @@ test('exposes in-band metadata events as cues', function() { | ... | @@ -1138,6 +1142,15 @@ test('exposes in-band metadata events as cues', function() { |
1138 | equal(track.cues[1].endTime, 2, 'cue ends at 2 seconds'); | 1142 | equal(track.cues[1].endTime, 2, 'cue ends at 2 seconds'); |
1139 | equal(track.cues[1].pauseOnExit, false, 'cue does not pause on exit'); | 1143 | equal(track.cues[1].pauseOnExit, false, 'cue does not pause on exit'); |
1140 | equal(track.cues[1].text, 'http://example.com', 'set cue text'); | 1144 | equal(track.cues[1].text, 'http://example.com', 'set cue text'); |
1145 | |||
1146 | equal(track.cues[2].startTime, 2, 'cue starts at 2 seconds'); | ||
1147 | equal(track.cues[2].endTime, 2, 'cue ends at 2 seconds'); | ||
1148 | equal(track.cues[2].pauseOnExit, false, 'cue does not pause on exit'); | ||
1149 | equal(track.cues[2].text, '', 'did not set cue text'); | ||
1150 | equal(track.cues[2].frame.owner, 'owner@example.com', 'set the owner'); | ||
1151 | deepEqual(track.cues[2].frame.privateData, | ||
1152 | new Uint8Array([1, 2, 3]), | ||
1153 | 'set the private data'); | ||
1141 | }); | 1154 | }); |
1142 | 1155 | ||
1143 | test('drops tags before the target timestamp when seeking', function() { | 1156 | test('drops tags before the target timestamp when seeking', function() { | ... | ... |
-
Please register or sign in to post a comment