9a3c3356 by David LaPalomento

Parse frame payloads for WXXX and TXXX tags

Decode the fields of simple ID3 tags. Adjust the starting PTS value for the metadata stream so that it aligns with video and audio streams.
1 parent baa54acc
...@@ -18,13 +18,14 @@ ...@@ -18,13 +18,14 @@
18 <!-- segment handling --> 18 <!-- segment handling -->
19 <script src="src/xhr.js"></script> 19 <script src="src/xhr.js"></script>
20 <script src="src/flv-tag.js"></script> 20 <script src="src/flv-tag.js"></script>
21 <script src="src/stream.js"></script>
21 <script src="src/exp-golomb.js"></script> 22 <script src="src/exp-golomb.js"></script>
22 <script src="src/h264-stream.js"></script> 23 <script src="src/h264-stream.js"></script>
23 <script src="src/aac-stream.js"></script> 24 <script src="src/aac-stream.js"></script>
25 <script src="src/metadata-stream.js"></script>
24 <script src="src/segment-parser.js"></script> 26 <script src="src/segment-parser.js"></script>
25 27
26 <!-- m3u8 handling --> 28 <!-- m3u8 handling -->
27 <script src="src/stream.js"></script>
28 <script src="src/m3u8/m3u8-parser.js"></script> 29 <script src="src/m3u8/m3u8-parser.js"></script>
29 <script src="src/playlist-loader.js"></script> 30 <script src="src/playlist-loader.js"></script>
30 31
......
...@@ -5,16 +5,61 @@ ...@@ -5,16 +5,61 @@
5 */ 5 */
6 (function(window, videojs, undefined) { 6 (function(window, videojs, undefined) {
7 'use strict'; 7 'use strict';
8 var defaults = { 8 var
9 defaults = {
9 debug: false 10 debug: false
10 }, MetadataStream; 11 },
12 parseString = function(bytes, start, end) {
13 var i, result = '';
14 for (i = start; i < end; i++) {
15 result += '%' + ('00' + bytes[i].toString(16)).slice(-2);
16 }
17 return window.decodeURIComponent(result);
18 },
19 tagParsers = {
20 'TXXX': function(tag) {
21 var i;
22 if (tag.data[0] !== 3) {
23 // ignore frames with unrecognized character encodings
24 return;
25 }
26
27 for (i = 1; i < tag.data.length; i++) {
28 if (tag.data[i] === 0) {
29 // parse the text fields
30 tag.description = parseString(tag.data, 1, i);
31 tag.value = parseString(tag.data, i + 1, tag.data.length);
32 break;
33 }
34 }
35 },
36 'WXXX': function(tag) {
37 var i;
38 if (tag.data[0] !== 3) {
39 // ignore frames with unrecognized character encodings
40 return;
41 }
42
43 for (i = 1; i < tag.data.length; i++) {
44 if (tag.data[i] === 0) {
45 // parse the description and URL fields
46 tag.description = parseString(tag.data, 1, i);
47 tag.url = parseString(tag.data, i + 1, tag.data.length);
48 break;
49 }
50 }
51 }
52 },
53 MetadataStream;
11 54
12 MetadataStream = function(options) { 55 MetadataStream = function(options) {
13 var settings = videojs.util.mergeOptions(defaults, options); 56 var settings = {
57 debug: !!(options && options.debug)
58 };
14 MetadataStream.prototype.init.call(this); 59 MetadataStream.prototype.init.call(this);
15 60
16 this.push = function(chunk) { 61 this.push = function(chunk) {
17 var tagSize, frameStart, frameSize; 62 var tagSize, frameStart, frameSize, frame;
18 63
19 // ignore events that don't look like ID3 data 64 // ignore events that don't look like ID3 data
20 if (chunk.data.length < 10 || 65 if (chunk.data.length < 10 ||
...@@ -45,6 +90,13 @@ ...@@ -45,6 +90,13 @@
45 (chunk.data[19]); 90 (chunk.data[19]);
46 } 91 }
47 92
93 // adjust the PTS values to align with the video and audio
94 // streams
95 if (this.timestampOffset) {
96 chunk.pts -= this.timestampOffset;
97 chunk.dts -= this.timestampOffset;
98 }
99
48 // parse one or more ID3 frames 100 // parse one or more ID3 frames
49 // http://id3.org/id3v2.3.0#ID3v2_frame_overview 101 // http://id3.org/id3v2.3.0#ID3v2_frame_overview
50 chunk.frames = []; 102 chunk.frames = [];
...@@ -58,13 +110,17 @@ ...@@ -58,13 +110,17 @@
58 return videojs.log('Malformed ID3 frame encountered. Skipping metadata parsing.'); 110 return videojs.log('Malformed ID3 frame encountered. Skipping metadata parsing.');
59 } 111 }
60 112
61 chunk.frames.push({ 113 frame = {
62 id: String.fromCharCode(chunk.data[frameStart]) + 114 id: String.fromCharCode(chunk.data[frameStart]) +
63 String.fromCharCode(chunk.data[frameStart + 1]) + 115 String.fromCharCode(chunk.data[frameStart + 1]) +
64 String.fromCharCode(chunk.data[frameStart + 2]) + 116 String.fromCharCode(chunk.data[frameStart + 2]) +
65 String.fromCharCode(chunk.data[frameStart + 3]), 117 String.fromCharCode(chunk.data[frameStart + 3]),
66 data: chunk.data.subarray(frameStart + 10, frameStart + frameSize + 10) 118 data: chunk.data.subarray(frameStart + 10, frameStart + frameSize + 10)
67 }); 119 };
120 if (tagParsers[frame.id]) {
121 tagParsers[frame.id](frame);
122 }
123 chunk.frames.push(frame);
68 124
69 frameStart += 10; // advance past the frame header 125 frameStart += 10; // advance past the frame header
70 frameStart += frameSize; // advance past the frame body 126 frameStart += frameSize; // advance past the frame body
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
4 FlvTag = videojs.Hls.FlvTag, 4 FlvTag = videojs.Hls.FlvTag,
5 H264Stream = videojs.Hls.H264Stream, 5 H264Stream = videojs.Hls.H264Stream,
6 AacStream = videojs.Hls.AacStream, 6 AacStream = videojs.Hls.AacStream,
7 MetadataStream = videojs.Hls.MetadataStream,
7 MP2T_PACKET_LENGTH, 8 MP2T_PACKET_LENGTH,
8 STREAM_TYPES; 9 STREAM_TYPES;
9 10
...@@ -27,6 +28,9 @@ ...@@ -27,6 +28,9 @@
27 programMapTable: {} 28 programMapTable: {}
28 }; 29 };
29 30
31 // allow in-band metadata to be observed
32 self.metadataStream = new MetadataStream();
33
30 // For information on the FLV format, see 34 // For information on the FLV format, see
31 // http://download.macromedia.com/f4v/video_file_format_spec_v10_1.pdf. 35 // http://download.macromedia.com/f4v/video_file_format_spec_v10_1.pdf.
32 // Technically, this function returns the header and a metadata FLV tag 36 // Technically, this function returns the header and a metadata FLV tag
...@@ -287,7 +291,8 @@ ...@@ -287,7 +291,8 @@
287 self.stream.pmtPid = (data[offset + 2] & 0x1F) << 8 | data[offset + 3]; 291 self.stream.pmtPid = (data[offset + 2] & 0x1F) << 8 | data[offset + 3];
288 } 292 }
289 } else if (pid === self.stream.programMapTable[STREAM_TYPES.h264] || 293 } else if (pid === self.stream.programMapTable[STREAM_TYPES.h264] ||
290 pid === self.stream.programMapTable[STREAM_TYPES.adts]) { 294 pid === self.stream.programMapTable[STREAM_TYPES.adts] ||
295 pid === self.stream.programMapTable[STREAM_TYPES.metadata]) {
291 if (pusi) { 296 if (pusi) {
292 // comment out for speed 297 // comment out for speed
293 if (0x00 !== data[offset + 0] || 0x00 !== data[offset + 1] || 0x01 !== data[offset + 2]) { 298 if (0x00 !== data[offset + 0] || 0x00 !== data[offset + 1] || 0x01 !== data[offset + 2]) {
...@@ -328,9 +333,16 @@ ...@@ -328,9 +333,16 @@
328 dts /= 45; 333 dts /= 45;
329 } 334 }
330 } 335 }
336
331 // Skip past "optional" portion of PTS header 337 // Skip past "optional" portion of PTS header
332 offset += pesHeaderLength; 338 offset += pesHeaderLength;
333 339
340 // align the metadata stream PTS values with the start of
341 // the other elementary streams
342 if (!self.metadataStream.timestampOffset) {
343 self.metadataStream.timestampOffset = pts;
344 }
345
334 if (pid === self.stream.programMapTable[STREAM_TYPES.h264]) { 346 if (pid === self.stream.programMapTable[STREAM_TYPES.h264]) {
335 h264Stream.setNextTimeStamp(pts, 347 h264Stream.setNextTimeStamp(pts,
336 dts, 348 dts,
...@@ -339,6 +351,12 @@ ...@@ -339,6 +351,12 @@
339 aacStream.setNextTimeStamp(pts, 351 aacStream.setNextTimeStamp(pts,
340 pesPacketSize, 352 pesPacketSize,
341 dataAlignmentIndicator); 353 dataAlignmentIndicator);
354 } else {
355 self.metadataStream.push({
356 pts: pts,
357 dts: dts,
358 data: data.subarray(offset)
359 });
342 } 360 }
343 } 361 }
344 362
...@@ -383,19 +401,18 @@ ...@@ -383,19 +401,18 @@
383 // the PID for this entry 401 // the PID for this entry
384 elementaryPID = (data[offset + 1] & 0x1F) << 8 | data[offset + 2]; 402 elementaryPID = (data[offset + 1] & 0x1F) << 8 | data[offset + 2];
385 403
386 if (streamType === STREAM_TYPES.h264) { 404 if (streamType === STREAM_TYPES.h264 &&
387 if (self.stream.programMapTable[streamType] && 405 self.stream.programMapTable[streamType] &&
388 self.stream.programMapTable[streamType] !== elementaryPID) { 406 self.stream.programMapTable[streamType] !== elementaryPID) {
389 throw new Error("Program has more than 1 video stream"); 407 throw new Error("Program has more than 1 video stream");
390 } 408 } else if (streamType === STREAM_TYPES.adts &&
391 self.stream.programMapTable[streamType] = elementaryPID; 409 self.stream.programMapTable[streamType] &&
392 } else if (streamType === STREAM_TYPES.adts) {
393 if (self.stream.programMapTable[streamType] &&
394 self.stream.programMapTable[streamType] !== elementaryPID) { 410 self.stream.programMapTable[streamType] !== elementaryPID) {
395 throw new Error("Program has more than 1 audio Stream"); 411 throw new Error("Program has more than 1 audio Stream");
396 } 412 }
413 // add the stream type entry to the map
397 self.stream.programMapTable[streamType] = elementaryPID; 414 self.stream.programMapTable[streamType] = elementaryPID;
398 } 415
399 // TODO add support for MP3 audio 416 // TODO add support for MP3 audio
400 417
401 // the length of the entry descriptor 418 // the length of the entry descriptor
...@@ -435,7 +452,8 @@ ...@@ -435,7 +452,8 @@
435 videojs.Hls.SegmentParser.MP2T_PACKET_LENGTH = MP2T_PACKET_LENGTH = 188; 452 videojs.Hls.SegmentParser.MP2T_PACKET_LENGTH = MP2T_PACKET_LENGTH = 188;
436 videojs.Hls.SegmentParser.STREAM_TYPES = STREAM_TYPES = { 453 videojs.Hls.SegmentParser.STREAM_TYPES = STREAM_TYPES = {
437 h264: 0x1b, 454 h264: 0x1b,
438 adts: 0x0f 455 adts: 0x0f,
456 metadata: 0x15
439 }; 457 };
440 458
441 })(window); 459 })(window);
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
21 throws(block, [expected], [message]) 21 throws(block, [expected], [message])
22 */ 22 */
23 23
24 var metadataStream, stringToInts, stringToCString, id3Frame; 24 var metadataStream, stringToInts, stringToCString, id3Tag, id3Frame;
25 25
26 module('MetadataStream', { 26 module('MetadataStream', {
27 setup: function() { 27 setup: function() {
...@@ -45,6 +45,30 @@ ...@@ -45,6 +45,30 @@
45 return stringToInts(string).concat([0x00]); 45 return stringToInts(string).concat([0x00]);
46 }; 46 };
47 47
48 id3Tag = function() {
49 var
50 frames = Array.prototype.concat.apply([], Array.prototype.slice.call(arguments)),
51 result = stringToInts('ID3').concat([
52 0x03, 0x00, // version 3.0 of ID3v2 (aka ID3v.2.3.0)
53 0x40, // flags. include an extended header
54 0x00, 0x00, 0x00, 0x00, // size. set later
55
56 // extended header
57 0x00, 0x00, 0x00, 0x06, // extended header size. no CRC
58 0x00, 0x00, // extended flags
59 0x00, 0x00, 0x00, 0x02 // size of padding
60 ], frames),
61 size;
62
63 size = result.length - 10;
64 result[6] = (size >>> 24) & 0xff;
65 result[7] = (size >>> 16) & 0xff;
66 result[8] = (size >>> 8) & 0xff;
67 result[9] = (size) & 0xff
68
69 return result;
70 }
71
48 id3Frame = function(type) { 72 id3Frame = function(type) {
49 var result = stringToInts(type).concat([ 73 var result = stringToInts(type).concat([
50 0x00, 0x00, 0x00, 0x00, // size 74 0x00, 0x00, 0x00, 0x00, // size
...@@ -128,6 +152,8 @@ ...@@ -128,6 +152,8 @@
128 deepEqual(new Uint8Array(events[0].frames[1].data), 152 deepEqual(new Uint8Array(events[0].frames[1].data),
129 new Uint8Array([0x04, 0x03, 0x02, 0x01]), 153 new Uint8Array([0x04, 0x03, 0x02, 0x01]),
130 'attached the frame payload'); 154 'attached the frame payload');
155 equal(events[0].pts, 1000, 'did not modify the PTS');
156 equal(events[0].dts, 1000, 'did not modify the PTS');
131 }); 157 });
132 158
133 test('skips non-ID3 metadata events', function() { 159 test('skips non-ID3 metadata events', function() {
...@@ -158,4 +184,100 @@ ...@@ -158,4 +184,100 @@
158 // too large/small tag size values 184 // too large/small tag size values
159 // too large/small frame size values 185 // too large/small frame size values
160 186
187 test('translates PTS and DTS values based on the timestamp offset', function() {
188 var events = [];
189 metadataStream.on('data', function(event) {
190 events.push(event);
191 });
192
193 metadataStream.timestampOffset = 800;
194
195 metadataStream.push({
196 trackId: 7,
197 pts: 1000,
198 dts: 900,
199
200 // header
201 data: new Uint8Array(id3Tag(id3Frame('XFFF', [0]), [0x00, 0x00]))
202 });
203
204 equal(events.length, 1, 'emitted an event');
205 equal(events[0].pts, 200, 'translated pts');
206 equal(events[0].dts, 100, 'translated dts');
207 });
208
209 test('parses TXXX tags', function() {
210 var events = [];
211 metadataStream.on('data', function(event) {
212 events.push(event);
213 });
214
215 metadataStream.push({
216 trackId: 7,
217 pts: 1000,
218 dts: 900,
219
220 // header
221 data: new Uint8Array(id3Tag(id3Frame('TXXX',
222 0x00,
223 stringToCString('get done'),
224 stringToInts('{ "key": "value" }')),
225 [0x00, 0x00]))
226 });
227
228 equal(events.length, 1, 'parsed one tag');
229 equal(events[0].frames.length, 1, 'parsed one frame');
230 equal(events[0].frames[0].description, 'get done', 'parsed the description');
231 equal(events[0].frames[0].value, '{ "key": "value" }', 'parsed the value');
232 });
233
234 test('parses WXXX tags', function() {
235 var events = [], url = 'http://example.com/path/file?abc=7&d=4#ty';
236 metadataStream.on('data', function(event) {
237 events.push(event);
238 });
239
240 metadataStream.push({
241 trackId: 7,
242 pts: 1000,
243 dts: 900,
244
245 // header
246 data: new Uint8Array(id3Tag(id3Frame('WXXX',
247 0x00,
248 stringToCString(''),
249 stringToInts(url)),
250 [0x00, 0x00]))
251 });
252
253 equal(events.length, 1, 'parsed one tag');
254 equal(events[0].frames.length, 1, 'parsed one frame');
255 equal(events[0].frames[0].description, '', 'parsed the description');
256 equal(events[0].frames[0].url, url, 'parsed the value');
257 });
258
259 test('parses TXXX tags with characters that have a single-digit hexadecimal representation', function() {
260 var events = [], value = String.fromCharCode(7);
261 metadataStream.on('data', function(event) {
262 events.push(event);
263 });
264
265 metadataStream.push({
266 trackId: 7,
267 pts: 1000,
268 dts: 900,
269
270 // header
271 data: new Uint8Array(id3Tag(id3Frame('TXXX',
272 0x00,
273 stringToCString(''),
274 stringToInts(value)),
275 [0x00, 0x00]))
276 });
277
278 equal(events[0].frames[0].value,
279 value,
280 'parsed the single-digit character');
281 });
282
161 })(window, window.videojs); 283 })(window, window.videojs);
......
...@@ -109,11 +109,18 @@ ...@@ -109,11 +109,18 @@
109 </div> 109 </div>
110 110
111 111
112 <script>
113 window.videojs = {
114 Hls: {}
115 };
116 </script>
112 <!-- transmuxing --> 117 <!-- transmuxing -->
118 <script src="../../src/stream.js"></script>
113 <script src="../../src/flv-tag.js"></script> 119 <script src="../../src/flv-tag.js"></script>
114 <script src="../../src/exp-golomb.js"></script> 120 <script src="../../src/exp-golomb.js"></script>
115 <script src="../../src/h264-stream.js"></script> 121 <script src="../../src/h264-stream.js"></script>
116 <script src="../../src/aac-stream.js"></script> 122 <script src="../../src/aac-stream.js"></script>
123 <script src="../../src/metadata-stream.js"></script>
117 <script src="../../src/segment-parser.js"></script> 124 <script src="../../src/segment-parser.js"></script>
118 <script src="../../node_modules/pkcs7/dist/pkcs7.unpad.js"></script> 125 <script src="../../node_modules/pkcs7/dist/pkcs7.unpad.js"></script>
119 <script src="../../src/decrypter.js"></script> 126 <script src="../../src/decrypter.js"></script>
......
...@@ -29,6 +29,11 @@ ...@@ -29,6 +29,11 @@
29 29
30 extend = window.videojs.util.mergeOptions, 30 extend = window.videojs.util.mergeOptions,
31 31
32 makePat,
33 makePsi,
34 makePmt,
35 makePacket,
36
32 testAudioTag, 37 testAudioTag,
33 testVideoTag, 38 testVideoTag,
34 testScriptTag, 39 testScriptTag,
...@@ -54,14 +59,15 @@ ...@@ -54,14 +59,15 @@
54 deepEqual(expectedHeader, header, 'the rest of the header is correct'); 59 deepEqual(expectedHeader, header, 'the rest of the header is correct');
55 }); 60 });
56 61
57 test('parses PMTs with program descriptors', function() { 62 // Create a PMT packet
58 var 63 // @return {Array} bytes
59 makePmt = function(options) { 64 makePmt = function(options) {
60 var 65 var
61 result = [], 66 result = [],
62 entryCount = 0, 67 entryCount = 0,
63 k, 68 k,
64 sectionLength; 69 sectionLength;
70
65 for (k in options.pids) { 71 for (k in options.pids) {
66 entryCount++; 72 entryCount++;
67 } 73 }
...@@ -108,11 +114,15 @@ ...@@ -108,11 +114,15 @@
108 // CRC_32 114 // CRC_32
109 result.push([0x00, 0x00, 0x00, 0x00]); // invalid CRC but we don't check it 115 result.push([0x00, 0x00, 0x00, 0x00]); // invalid CRC but we don't check it
110 return result; 116 return result;
111 }, 117 };
118
119 // Create a PAT packet
120 // @return {Array} bytes
112 makePat = function(options) { 121 makePat = function(options) {
113 var 122 var
114 result = [], 123 result = [],
115 k; 124 k;
125
116 // table_id 126 // table_id
117 result.push(0x00); 127 result.push(0x00);
118 // section_syntax_indicator '0' reserved section_length 128 // section_syntax_indicator '0' reserved section_length
...@@ -136,7 +146,10 @@ ...@@ -136,7 +146,10 @@
136 result.push(options.programs[k] & 0xff); 146 result.push(options.programs[k] & 0xff);
137 } 147 }
138 return result; 148 return result;
139 }, 149 };
150
151 // Create a PAT or PMT packet based on the specified options
152 // @return {Array} bytes
140 makePsi = function(options) { 153 makePsi = function(options) {
141 var result = []; 154 var result = [];
142 155
...@@ -148,7 +161,10 @@ ...@@ -148,7 +161,10 @@
148 return result.concat(makePat(options)); 161 return result.concat(makePat(options));
149 } 162 }
150 return result.concat(makePmt(options)); 163 return result.concat(makePmt(options));
151 }, 164 };
165
166 // Construct an M2TS packet
167 // @return {Array} bytes
152 makePacket = function(options) { 168 makePacket = function(options) {
153 var 169 var
154 result = [], 170 result = [],
...@@ -170,7 +186,10 @@ ...@@ -170,7 +186,10 @@
170 // ensure the resulting packet is the correct size 186 // ensure the resulting packet is the correct size
171 result.length = window.videojs.Hls.SegmentParser.MP2T_PACKET_LENGTH; 187 result.length = window.videojs.Hls.SegmentParser.MP2T_PACKET_LENGTH;
172 return result; 188 return result;
173 }, 189 };
190
191 test('parses PMTs with program descriptors', function() {
192 var
174 h264Type = window.videojs.Hls.SegmentParser.STREAM_TYPES.h264, 193 h264Type = window.videojs.Hls.SegmentParser.STREAM_TYPES.h264,
175 adtsType = window.videojs.Hls.SegmentParser.STREAM_TYPES.adts; 194 adtsType = window.videojs.Hls.SegmentParser.STREAM_TYPES.adts;
176 195
...@@ -191,6 +210,22 @@ ...@@ -191,6 +210,22 @@
191 strictEqual(parser.stream.programMapTable[adtsType], 0x03, 'audio is PID 3'); 210 strictEqual(parser.stream.programMapTable[adtsType], 0x03, 'audio is PID 3');
192 }); 211 });
193 212
213 test('recognizes metadata streams', function() {
214 parser.parseSegmentBinaryData(new Uint8Array(makePacket({
215 programs: {
216 0x01: [0x01]
217 }
218 }).concat(makePacket({
219 pid: 0x01,
220 pids: {
221 // Rec. ITU-T H.222.0 (06/2012), Table 2-34
222 0x02: 0x15 // Metadata carried in PES packets
223 }
224 }))));
225
226 equal(parser.stream.programMapTable[0x15], 0x02, 'metadata is PID 2');
227 });
228
194 test('parses the first bipbop segment', function() { 229 test('parses the first bipbop segment', function() {
195 parser.parseSegmentBinaryData(window.bcSegment); 230 parser.parseSegmentBinaryData(window.bcSegment);
196 231
......