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 debug: false 9 defaults = {
10 }, MetadataStream; 10 debug: false
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) { 410 self.stream.programMapTable[streamType] !== elementaryPID) {
393 if (self.stream.programMapTable[streamType] && 411 throw new Error("Program has more than 1 audio Stream");
394 self.stream.programMapTable[streamType] !== elementaryPID) {
395 throw new Error("Program has more than 1 audio Stream");
396 }
397 self.stream.programMapTable[streamType] = elementaryPID;
398 } 412 }
413 // add the stream type entry to the map
414 self.stream.programMapTable[streamType] = elementaryPID;
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,123 +59,137 @@ ...@@ -54,123 +59,137 @@
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
62 // Create a PMT packet
63 // @return {Array} bytes
64 makePmt = function(options) {
65 var
66 result = [],
67 entryCount = 0,
68 k,
69 sectionLength;
70
71 for (k in options.pids) {
72 entryCount++;
73 }
74 // table_id
75 result.push(0x02);
76 // section_syntax_indicator '0' reserved section_length
77 // 13 + (program_info_length) + (n * 5 + ES_info_length[n])
78 sectionLength = 13 + (5 * entryCount) + 17;
79 result.push(0x80 | (0xF00 & sectionLength >>> 8));
80 result.push(sectionLength & 0xFF);
81 // program_number
82 result.push(0x00);
83 result.push(0x01);
84 // reserved version_number current_next_indicator
85 result.push(0x01);
86 // section_number
87 result.push(0x00);
88 // last_section_number
89 result.push(0x00);
90 // reserved PCR_PID
91 result.push(0xe1);
92 result.push(0x00);
93 // reserved program_info_length
94 result.push(0xf0);
95 result.push(0x11); // hard-coded 17 byte descriptor
96 // program descriptors
97 result = result.concat([
98 0x25, 0x0f, 0xff, 0xff,
99 0x49, 0x44, 0x33, 0x20,
100 0xff, 0x49, 0x44, 0x33,
101 0x20, 0x00, 0x1f, 0x00,
102 0x01
103 ]);
104 for (k in options.pids) {
105 // stream_type
106 result.push(options.pids[k]);
107 // reserved elementary_PID
108 result.push(0xe0 | (k & 0x1f00) >>> 8);
109 result.push(k & 0xff);
110 // reserved ES_info_length
111 result.push(0xf0);
112 result.push(0x00); // ES_info_length = 0
113 }
114 // CRC_32
115 result.push([0x00, 0x00, 0x00, 0x00]); // invalid CRC but we don't check it
116 return result;
117 };
118
119 // Create a PAT packet
120 // @return {Array} bytes
121 makePat = function(options) {
122 var
123 result = [],
124 k;
125
126 // table_id
127 result.push(0x00);
128 // section_syntax_indicator '0' reserved section_length
129 result.push(0x80);
130 result.push(0x0d); // section_length for one program
131 // transport_stream_id
132 result.push(0x00);
133 result.push(0x00);
134 // reserved version_number current_next_indicator
135 result.push(0x01); // current_next_indicator is 1
136 // section_number
137 result.push(0x00);
138 // last_section_number
139 result.push(0x00);
140 for (k in options.programs) {
141 // program_number
142 result.push((k & 0xFF00) >>> 8);
143 result.push(k & 0x00FF);
144 // reserved program_map_pid
145 result.push((options.programs[k] & 0x1f00) >>> 8);
146 result.push(options.programs[k] & 0xff);
147 }
148 return result;
149 };
150
151 // Create a PAT or PMT packet based on the specified options
152 // @return {Array} bytes
153 makePsi = function(options) {
154 var result = [];
155
156 // pointer_field
157 if (options.payloadUnitStartIndicator) {
158 result.push(0x00);
159 }
160 if (options.programs) {
161 return result.concat(makePat(options));
162 }
163 return result.concat(makePmt(options));
164 };
165
166 // Construct an M2TS packet
167 // @return {Array} bytes
168 makePacket = function(options) {
169 var
170 result = [],
171 settings = extend({
172 payloadUnitStartIndicator: true,
173 pid: 0x00
174 }, options);
175
176 // header
177 // sync_byte
178 result.push(0x47);
179 // transport_error_indicator payload_unit_start_indicator transport_priority PID
180 result.push((settings.pid & 0x1f) << 8 | 0x40);
181 result.push(settings.pid & 0xff);
182 // transport_scrambling_control adaptation_field_control continuity_counter
183 result.push(0x10);
184 result = result.concat(makePsi(settings));
185
186 // ensure the resulting packet is the correct size
187 result.length = window.videojs.Hls.SegmentParser.MP2T_PACKET_LENGTH;
188 return result;
189 };
190
57 test('parses PMTs with program descriptors', function() { 191 test('parses PMTs with program descriptors', function() {
58 var 192 var
59 makePmt = function(options) {
60 var
61 result = [],
62 entryCount = 0,
63 k,
64 sectionLength;
65 for (k in options.pids) {
66 entryCount++;
67 }
68 // table_id
69 result.push(0x02);
70 // section_syntax_indicator '0' reserved section_length
71 // 13 + (program_info_length) + (n * 5 + ES_info_length[n])
72 sectionLength = 13 + (5 * entryCount) + 17;
73 result.push(0x80 | (0xF00 & sectionLength >>> 8));
74 result.push(sectionLength & 0xFF);
75 // program_number
76 result.push(0x00);
77 result.push(0x01);
78 // reserved version_number current_next_indicator
79 result.push(0x01);
80 // section_number
81 result.push(0x00);
82 // last_section_number
83 result.push(0x00);
84 // reserved PCR_PID
85 result.push(0xe1);
86 result.push(0x00);
87 // reserved program_info_length
88 result.push(0xf0);
89 result.push(0x11); // hard-coded 17 byte descriptor
90 // program descriptors
91 result = result.concat([
92 0x25, 0x0f, 0xff, 0xff,
93 0x49, 0x44, 0x33, 0x20,
94 0xff, 0x49, 0x44, 0x33,
95 0x20, 0x00, 0x1f, 0x00,
96 0x01
97 ]);
98 for (k in options.pids) {
99 // stream_type
100 result.push(options.pids[k]);
101 // reserved elementary_PID
102 result.push(0xe0 | (k & 0x1f00) >>> 8);
103 result.push(k & 0xff);
104 // reserved ES_info_length
105 result.push(0xf0);
106 result.push(0x00); // ES_info_length = 0
107 }
108 // CRC_32
109 result.push([0x00, 0x00, 0x00, 0x00]); // invalid CRC but we don't check it
110 return result;
111 },
112 makePat = function(options) {
113 var
114 result = [],
115 k;
116 // table_id
117 result.push(0x00);
118 // section_syntax_indicator '0' reserved section_length
119 result.push(0x80);
120 result.push(0x0d); // section_length for one program
121 // transport_stream_id
122 result.push(0x00);
123 result.push(0x00);
124 // reserved version_number current_next_indicator
125 result.push(0x01); // current_next_indicator is 1
126 // section_number
127 result.push(0x00);
128 // last_section_number
129 result.push(0x00);
130 for (k in options.programs) {
131 // program_number
132 result.push((k & 0xFF00) >>> 8);
133 result.push(k & 0x00FF);
134 // reserved program_map_pid
135 result.push((options.programs[k] & 0x1f00) >>> 8);
136 result.push(options.programs[k] & 0xff);
137 }
138 return result;
139 },
140 makePsi = function(options) {
141 var result = [];
142
143 // pointer_field
144 if (options.payloadUnitStartIndicator) {
145 result.push(0x00);
146 }
147 if (options.programs) {
148 return result.concat(makePat(options));
149 }
150 return result.concat(makePmt(options));
151 },
152 makePacket = function(options) {
153 var
154 result = [],
155 settings = extend({
156 payloadUnitStartIndicator: true,
157 pid: 0x00
158 }, options);
159
160 // header
161 // sync_byte
162 result.push(0x47);
163 // transport_error_indicator payload_unit_start_indicator transport_priority PID
164 result.push((settings.pid & 0x1f) << 8 | 0x40);
165 result.push(settings.pid & 0xff);
166 // transport_scrambling_control adaptation_field_control continuity_counter
167 result.push(0x10);
168 result = result.concat(makePsi(settings));
169
170 // ensure the resulting packet is the correct size
171 result.length = window.videojs.Hls.SegmentParser.MP2T_PACKET_LENGTH;
172 return result;
173 },
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
......