1dc8dc12 by David LaPalomento

Buffer ID3 tags split across TS packets

If the entire ID3 frame isn't available in a single metadatastream push, store everything up and check with the combined data next time. Ignore tags that do not include at least the tag header in the first TS packet.
1 parent 3bf53170
...@@ -6,23 +6,24 @@ ...@@ -6,23 +6,24 @@
6 (function(window, videojs, undefined) { 6 (function(window, videojs, undefined) {
7 'use strict'; 7 'use strict';
8 var 8 var
9 // return the string representation of the specified byte range, 9 // return a percent-encoded representation of the specified byte range
10 // interpreted as UTf-8. 10 // @see http://en.wikipedia.org/wiki/Percent-encoding
11 parseUtf8 = function(bytes, start, end) { 11 percentEncode = function(bytes, start, end) {
12 var i, result = ''; 12 var i, result = '';
13 for (i = start; i < end; i++) { 13 for (i = start; i < end; i++) {
14 result += '%' + ('00' + bytes[i].toString(16)).slice(-2); 14 result += '%' + ('00' + bytes[i].toString(16)).slice(-2);
15 } 15 }
16 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));
17 }, 22 },
18 // return the string representation of the specified byte range, 23 // return the string representation of the specified byte range,
19 // interpreted as ISO-8859-1. 24 // interpreted as ISO-8859-1.
20 parseIso88591 = function(bytes, start, end) { 25 parseIso88591 = function(bytes, start, end) {
21 var i, result = ''; 26 return window.unescape(percentEncode(bytes, start, end));
22 for (i = start; i < end; i++) {
23 result += '%' + ('00' + bytes[i].toString(16)).slice(-2);
24 }
25 return window.unescape(result);
26 }, 27 },
27 tagParsers = { 28 tagParsers = {
28 'TXXX': function(tag) { 29 'TXXX': function(tag) {
...@@ -73,14 +74,23 @@ ...@@ -73,14 +74,23 @@
73 MetadataStream; 74 MetadataStream;
74 75
75 MetadataStream = function(options) { 76 MetadataStream = function(options) {
76 var settings = { 77 var
78 settings = {
77 debug: !!(options && options.debug), 79 debug: !!(options && options.debug),
78 80
79 // the bytes of the program-level descriptor field in MP2T 81 // the bytes of the program-level descriptor field in MP2T
80 // see ISO/IEC 13818-1:2013 (E), section 2.6 "Program and 82 // see ISO/IEC 13818-1:2013 (E), section 2.6 "Program and
81 // program element descriptors" 83 // program element descriptors"
82 descriptor: options && options.descriptor 84 descriptor: options && options.descriptor
83 }, i; 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
84 MetadataStream.prototype.init.call(this); 94 MetadataStream.prototype.init.call(this);
85 95
86 // calculate the text track in-band metadata track dispatch type 96 // calculate the text track in-band metadata track dispatch type
...@@ -93,73 +103,110 @@ ...@@ -93,73 +103,110 @@
93 } 103 }
94 104
95 this.push = function(chunk) { 105 this.push = function(chunk) {
96 var tagSize, frameStart, frameSize, frame; 106 var tag, frameStart, frameSize, frame, i;
97 107
98 // ignore events that don't look like ID3 data 108 // ignore events that don't look like ID3 data
99 if (chunk.data.length < 10 || 109 if (buffer.length === 0 &&
110 (chunk.data.length < 10 ||
100 chunk.data[0] !== 'I'.charCodeAt(0) || 111 chunk.data[0] !== 'I'.charCodeAt(0) ||
101 chunk.data[1] !== 'D'.charCodeAt(0) || 112 chunk.data[1] !== 'D'.charCodeAt(0) ||
102 chunk.data[2] !== '3'.charCodeAt(0)) { 113 chunk.data[2] !== '3'.charCodeAt(0))) {
103 if (settings.debug) { 114 if (settings.debug) {
104 videojs.log('Skipping unrecognized metadata stream'); 115 videojs.log('Skipping unrecognized metadata packet');
116 }
117 return;
118 }
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;
105 } 138 }
139
140 // if the entire frame has not arrived, wait for more data
141 if (bufferSize < tagSize) {
106 return; 142 return;
107 } 143 }
108 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 buffer.shift();
156 }
157
109 // find the start of the first frame and the end of the tag 158 // find the start of the first frame and the end of the tag
110 tagSize = chunk.data.byteLength;
111 frameStart = 10; 159 frameStart = 10;
112 if (chunk.data[5] & 0x40) { 160 if (tag.data[5] & 0x40) {
113 // advance the frame start past the extended header 161 // advance the frame start past the extended header
114 frameStart += 4; // header size field 162 frameStart += 4; // header size field
115 frameStart += (chunk.data[10] << 24) | 163 frameStart += (tag.data[10] << 24) |
116 (chunk.data[11] << 16) | 164 (tag.data[11] << 16) |
117 (chunk.data[12] << 8) | 165 (tag.data[12] << 8) |
118 (chunk.data[13]); 166 (tag.data[13]);
119 167
120 // clip any padding off the end 168 // clip any padding off the end
121 tagSize -= (chunk.data[16] << 24) | 169 tagSize -= (tag.data[16] << 24) |
122 (chunk.data[17] << 16) | 170 (tag.data[17] << 16) |
123 (chunk.data[18] << 8) | 171 (tag.data[18] << 8) |
124 (chunk.data[19]); 172 (tag.data[19]);
125 } 173 }
126 174
127 // adjust the PTS values to align with the video and audio 175 // adjust the PTS values to align with the video and audio
128 // streams 176 // streams
129 if (this.timestampOffset) { 177 if (this.timestampOffset) {
130 chunk.pts -= this.timestampOffset; 178 tag.pts -= this.timestampOffset;
131 chunk.dts -= this.timestampOffset; 179 tag.dts -= this.timestampOffset;
132 } 180 }
133 181
134 // parse one or more ID3 frames 182 // parse one or more ID3 frames
135 // http://id3.org/id3v2.3.0#ID3v2_frame_overview 183 // http://id3.org/id3v2.3.0#ID3v2_frame_overview
136 chunk.frames = [];
137 do { 184 do {
138 // determine the number of bytes in this frame 185 // determine the number of bytes in this frame
139 frameSize = (chunk.data[frameStart + 4] << 24) | 186 frameSize = (tag.data[frameStart + 4] << 24) |
140 (chunk.data[frameStart + 5] << 16) | 187 (tag.data[frameStart + 5] << 16) |
141 (chunk.data[frameStart + 6] << 8) | 188 (tag.data[frameStart + 6] << 8) |
142 (chunk.data[frameStart + 7]); 189 (tag.data[frameStart + 7]);
143 if (frameSize < 1) { 190 if (frameSize < 1) {
144 return videojs.log('Malformed ID3 frame encountered. Skipping metadata parsing.'); 191 return videojs.log('Malformed ID3 frame encountered. Skipping metadata parsing.');
145 } 192 }
146 193
147 frame = { 194 frame = {
148 id: String.fromCharCode(chunk.data[frameStart]) + 195 id: String.fromCharCode(tag.data[frameStart],
149 String.fromCharCode(chunk.data[frameStart + 1]) + 196 tag.data[frameStart + 1],
150 String.fromCharCode(chunk.data[frameStart + 2]) + 197 tag.data[frameStart + 2],
151 String.fromCharCode(chunk.data[frameStart + 3]), 198 tag.data[frameStart + 3]),
152 data: chunk.data.subarray(frameStart + 10, frameStart + frameSize + 10) 199 data: tag.data.subarray(frameStart + 10, frameStart + frameSize + 10)
153 }; 200 };
154 if (tagParsers[frame.id]) { 201 if (tagParsers[frame.id]) {
155 tagParsers[frame.id](frame); 202 tagParsers[frame.id](frame);
156 } 203 }
157 chunk.frames.push(frame); 204 tag.frames.push(frame);
158 205
159 frameStart += 10; // advance past the frame header 206 frameStart += 10; // advance past the frame header
160 frameStart += frameSize; // advance past the frame body 207 frameStart += frameSize; // advance past the frame body
161 } while (frameStart < tagSize); 208 } while (frameStart < tagSize);
162 this.trigger('data', chunk); 209 this.trigger('data', tag);
163 }; 210 };
164 }; 211 };
165 MetadataStream.prototype = new videojs.Hls.Stream(); 212 MetadataStream.prototype = new videojs.Hls.Stream();
......
...@@ -206,7 +206,7 @@ ...@@ -206,7 +206,7 @@
206 equal(events[0].dts, 100, 'translated dts'); 206 equal(events[0].dts, 100, 'translated dts');
207 }); 207 });
208 208
209 test('parses TXXX tags', function() { 209 test('parses TXXX frames', function() {
210 var events = []; 210 var events = [];
211 metadataStream.on('data', function(event) { 211 metadataStream.on('data', function(event) {
212 events.push(event); 212 events.push(event);
...@@ -232,7 +232,7 @@ ...@@ -232,7 +232,7 @@
232 equal(events[0].frames[0].value, '{ "key": "value" }', 'parsed the value'); 232 equal(events[0].frames[0].value, '{ "key": "value" }', 'parsed the value');
233 }); 233 });
234 234
235 test('parses WXXX tags', function() { 235 test('parses WXXX frames', function() {
236 var events = [], url = 'http://example.com/path/file?abc=7&d=4#ty'; 236 var events = [], url = 'http://example.com/path/file?abc=7&d=4#ty';
237 metadataStream.on('data', function(event) { 237 metadataStream.on('data', function(event) {
238 events.push(event); 238 events.push(event);
...@@ -258,7 +258,7 @@ ...@@ -258,7 +258,7 @@
258 equal(events[0].frames[0].url, url, 'parsed the value'); 258 equal(events[0].frames[0].url, url, 'parsed the value');
259 }); 259 });
260 260
261 test('parses TXXX tags with characters that have a single-digit hexadecimal representation', function() { 261 test('parses TXXX frames with characters that have a single-digit hexadecimal representation', function() {
262 var events = [], value = String.fromCharCode(7); 262 var events = [], value = String.fromCharCode(7);
263 metadataStream.on('data', function(event) { 263 metadataStream.on('data', function(event) {
264 events.push(event); 264 events.push(event);
...@@ -282,7 +282,7 @@ ...@@ -282,7 +282,7 @@
282 'parsed the single-digit character'); 282 'parsed the single-digit character');
283 }); 283 });
284 284
285 test('parses PRIV tags', function() { 285 test('parses PRIV frames', function() {
286 var 286 var
287 events = [], 287 events = [],
288 payload = stringToInts('arbitrary data may be included in the payload ' + 288 payload = stringToInts('arbitrary data may be included in the payload ' +
...@@ -313,6 +313,88 @@ ...@@ -313,6 +313,88 @@
313 313
314 }); 314 });
315 315
316 test('parses tags split across pushes', function() {
317 var
318 events = [],
319 owner = stringToCString('owner@example.com'),
320 payload = stringToInts('A TS packet is 188 bytes in length so that it can' +
321 ' be easily transmitted over ATM networks, an ' +
322 'important medium at one time. We want to be sure' +
323 ' that ID3 frames larger than a TS packet are ' +
324 'properly re-assembled.'),
325 tag = new Uint8Array(id3Tag(id3Frame('PRIV', owner, payload))),
326 front = tag.subarray(0, 100),
327 back = tag.subarray(100);
328
329 metadataStream.on('data', function(event) {
330 events.push(event);
331 });
332
333 metadataStream.push({
334 trackId: 7,
335 pts: 1000,
336 dts: 900,
337 data: front
338 });
339
340 equal(events.length, 0, 'parsed zero tags');
341
342 metadataStream.push({
343 trackId: 7,
344 pts: 1000,
345 dts: 900,
346 data: back
347 });
348
349 equal(events.length, 1, 'parsed a tag');
350 equal(events[0].frames.length, 1, 'parsed a frame');
351 equal(events[0].frames[0].data.byteLength,
352 owner.length + payload.length,
353 'collected data across pushes');
354 });
355
356 test('ignores tags when the header is fragmented', function() {
357
358 var
359 events = [],
360 tag = new Uint8Array(id3Tag(id3Frame('PRIV',
361 stringToCString('owner@example.com'),
362 stringToInts('payload')))),
363 // split the 10-byte ID3 tag header in half
364 front = tag.subarray(0, 5),
365 back = tag.subarray(5);
366
367 metadataStream.on('data', function(event) {
368 events.push(event);
369 });
370
371 metadataStream.push({
372 trackId: 7,
373 pts: 1000,
374 dts: 900,
375 data: front
376 });
377 metadataStream.push({
378 trackId: 7,
379 pts: 1000,
380 dts: 900,
381 data: back
382 });
383
384 equal(events.length, 0, 'parsed zero tags');
385
386 metadataStream.push({
387 trackId: 7,
388 pts: 1500,
389 dts: 1500,
390 data: new Uint8Array(id3Tag(id3Frame('PRIV',
391 stringToCString('owner2'),
392 stringToInts('payload2'))))
393 });
394 equal(events.length, 1, 'parsed one tag');
395 equal(events[0].frames[0].owner, 'owner2', 'dropped the first tag');
396 });
397
316 // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track 398 // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track
317 test('constructs the dispatch type', function() { 399 test('constructs the dispatch type', function() {
318 metadataStream = new videojs.Hls.MetadataStream({ 400 metadataStream = new videojs.Hls.MetadataStream({
......