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.
Showing
6 changed files
with
377 additions
and
138 deletions
... | @@ -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 | ... | ... |
-
Please register or sign in to post a comment