fabd2276 by David LaPalomento

Merge pull request #262 from videojs/id3-enhancements

Id3 enhancements
2 parents 1811ffc3 fd6c3d02
...@@ -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() {
......