Merge pull request #221 from videojs/feature/in-band-metadata
In-Band Metadata
Showing
13 changed files
with
747 additions
and
136 deletions
... | @@ -24,12 +24,13 @@ module.exports = function(grunt) { | ... | @@ -24,12 +24,13 @@ module.exports = function(grunt) { |
24 | dist: { | 24 | dist: { |
25 | nonull: true, | 25 | nonull: true, |
26 | src: ['src/videojs-hls.js', | 26 | src: ['src/videojs-hls.js', |
27 | 'src/stream.js', | ||
27 | 'src/flv-tag.js', | 28 | 'src/flv-tag.js', |
28 | 'src/exp-golomb.js', | 29 | 'src/exp-golomb.js', |
29 | 'src/h264-stream.js', | 30 | 'src/h264-stream.js', |
30 | 'src/aac-stream.js', | 31 | 'src/aac-stream.js', |
32 | 'src/metadata-stream.js', | ||
31 | 'src/segment-parser.js', | 33 | 'src/segment-parser.js', |
32 | 'src/stream.js', | ||
33 | 'src/m3u8/m3u8-parser.js', | 34 | 'src/m3u8/m3u8-parser.js', |
34 | 'src/xhr.js', | 35 | 'src/xhr.js', |
35 | 'src/playlist-loader.js', | 36 | 'src/playlist-loader.js', | ... | ... |
... | @@ -155,6 +155,20 @@ the actual rendering quality change does not occur simultaneously with | ... | @@ -155,6 +155,20 @@ the actual rendering quality change does not occur simultaneously with |
155 | this event; a new segment must be requested and the existing buffer | 155 | this event; a new segment must be requested and the existing buffer |
156 | depleted first. | 156 | depleted first. |
157 | 157 | ||
158 | ### In-Band Metadata | ||
159 | The HLS tech supports [timed | ||
160 | metadata](https://developer.apple.com/library/ios/#documentation/AudioVideo/Conceptual/HTTP_Live_Streaming_Metadata_Spec/Introduction/Introduction.html) | ||
161 | embedded as [ID3 tags](http://id3.org/id3v2.3.0). When a stream is | ||
162 | encountered with embedded metadata, an [in-band metadata text | ||
163 | track](https://html.spec.whatwg.org/multipage/embedded-content.html#text-track-in-band-metadata-track-dispatch-type) | ||
164 | will automatically be created and populated with cues as they are | ||
165 | encountered in the stream. Only UTF-8 encoded | ||
166 | [TXXX](http://id3.org/id3v2.3.0#User_defined_text_information_frame) | ||
167 | and [WXXX](http://id3.org/id3v2.3.0#User_defined_URL_link_frame) ID3 | ||
168 | frames are currently mapped to cue points. There are lots of guides | ||
169 | and references to using text tracks [around the | ||
170 | web](http://www.html5rocks.com/en/tutorials/track/basics/). | ||
171 | |||
158 | ### Testing | 172 | ### Testing |
159 | 173 | ||
160 | For testing, you can either run `npm test` or use `grunt` directly. | 174 | For testing, you can either run `npm test` or use `grunt` directly. | ... | ... |
... | @@ -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 | ... | ... |
src/metadata-stream.js
0 → 100644
1 | /** | ||
2 | * Accepts program elementary stream (PES) data events and parses out | ||
3 | * ID3 metadata from them, if present. | ||
4 | * @see http://id3.org/id3v2.3.0 | ||
5 | */ | ||
6 | (function(window, videojs, undefined) { | ||
7 | 'use strict'; | ||
8 | var | ||
9 | parseString = function(bytes, start, end) { | ||
10 | var i, result = ''; | ||
11 | for (i = start; i < end; i++) { | ||
12 | result += '%' + ('00' + bytes[i].toString(16)).slice(-2); | ||
13 | } | ||
14 | return window.decodeURIComponent(result); | ||
15 | }, | ||
16 | tagParsers = { | ||
17 | 'TXXX': function(tag) { | ||
18 | var i; | ||
19 | if (tag.data[0] !== 3) { | ||
20 | // ignore frames with unrecognized character encodings | ||
21 | return; | ||
22 | } | ||
23 | |||
24 | for (i = 1; i < tag.data.length; i++) { | ||
25 | if (tag.data[i] === 0) { | ||
26 | // parse the text fields | ||
27 | tag.description = parseString(tag.data, 1, i); | ||
28 | tag.value = parseString(tag.data, i + 1, tag.data.length); | ||
29 | break; | ||
30 | } | ||
31 | } | ||
32 | }, | ||
33 | 'WXXX': function(tag) { | ||
34 | var i; | ||
35 | if (tag.data[0] !== 3) { | ||
36 | // ignore frames with unrecognized character encodings | ||
37 | return; | ||
38 | } | ||
39 | |||
40 | for (i = 1; i < tag.data.length; i++) { | ||
41 | if (tag.data[i] === 0) { | ||
42 | // parse the description and URL fields | ||
43 | tag.description = parseString(tag.data, 1, i); | ||
44 | tag.url = parseString(tag.data, i + 1, tag.data.length); | ||
45 | break; | ||
46 | } | ||
47 | } | ||
48 | } | ||
49 | }, | ||
50 | MetadataStream; | ||
51 | |||
52 | MetadataStream = function(options) { | ||
53 | var settings = { | ||
54 | debug: !!(options && options.debug), | ||
55 | |||
56 | // the bytes of the program-level descriptor field in MP2T | ||
57 | // see ISO/IEC 13818-1:2013 (E), section 2.6 "Program and | ||
58 | // program element descriptors" | ||
59 | descriptor: options && options.descriptor | ||
60 | }, i; | ||
61 | MetadataStream.prototype.init.call(this); | ||
62 | |||
63 | // calculate the text track in-band metadata track dispatch type | ||
64 | // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track | ||
65 | this.dispatchType = videojs.Hls.SegmentParser.STREAM_TYPES.metadata.toString(16); | ||
66 | if (settings.descriptor) { | ||
67 | for (i = 0; i < settings.descriptor.length; i++) { | ||
68 | this.dispatchType += ('00' + settings.descriptor[i].toString(16)).slice(-2); | ||
69 | } | ||
70 | } | ||
71 | |||
72 | this.push = function(chunk) { | ||
73 | var tagSize, frameStart, frameSize, frame; | ||
74 | |||
75 | // ignore events that don't look like ID3 data | ||
76 | if (chunk.data.length < 10 || | ||
77 | chunk.data[0] !== 'I'.charCodeAt(0) || | ||
78 | chunk.data[1] !== 'D'.charCodeAt(0) || | ||
79 | chunk.data[2] !== '3'.charCodeAt(0)) { | ||
80 | if (settings.debug) { | ||
81 | videojs.log('Skipping unrecognized metadata stream'); | ||
82 | } | ||
83 | return; | ||
84 | } | ||
85 | |||
86 | // find the start of the first frame and the end of the tag | ||
87 | tagSize = chunk.data.byteLength; | ||
88 | frameStart = 10; | ||
89 | if (chunk.data[5] & 0x40) { | ||
90 | // advance the frame start past the extended header | ||
91 | frameStart += 4; // header size field | ||
92 | frameStart += (chunk.data[10] << 24) | | ||
93 | (chunk.data[11] << 16) | | ||
94 | (chunk.data[12] << 8) | | ||
95 | (chunk.data[13]); | ||
96 | |||
97 | // clip any padding off the end | ||
98 | tagSize -= (chunk.data[16] << 24) | | ||
99 | (chunk.data[17] << 16) | | ||
100 | (chunk.data[18] << 8) | | ||
101 | (chunk.data[19]); | ||
102 | } | ||
103 | |||
104 | // adjust the PTS values to align with the video and audio | ||
105 | // streams | ||
106 | if (this.timestampOffset) { | ||
107 | chunk.pts -= this.timestampOffset; | ||
108 | chunk.dts -= this.timestampOffset; | ||
109 | } | ||
110 | |||
111 | // parse one or more ID3 frames | ||
112 | // http://id3.org/id3v2.3.0#ID3v2_frame_overview | ||
113 | chunk.frames = []; | ||
114 | do { | ||
115 | // determine the number of bytes in this frame | ||
116 | frameSize = (chunk.data[frameStart + 4] << 24) | | ||
117 | (chunk.data[frameStart + 5] << 16) | | ||
118 | (chunk.data[frameStart + 6] << 8) | | ||
119 | (chunk.data[frameStart + 7]); | ||
120 | if (frameSize < 1) { | ||
121 | return videojs.log('Malformed ID3 frame encountered. Skipping metadata parsing.'); | ||
122 | } | ||
123 | |||
124 | frame = { | ||
125 | id: String.fromCharCode(chunk.data[frameStart]) + | ||
126 | String.fromCharCode(chunk.data[frameStart + 1]) + | ||
127 | String.fromCharCode(chunk.data[frameStart + 2]) + | ||
128 | String.fromCharCode(chunk.data[frameStart + 3]), | ||
129 | data: chunk.data.subarray(frameStart + 10, frameStart + frameSize + 10) | ||
130 | }; | ||
131 | if (tagParsers[frame.id]) { | ||
132 | tagParsers[frame.id](frame); | ||
133 | } | ||
134 | chunk.frames.push(frame); | ||
135 | |||
136 | frameStart += 10; // advance past the frame header | ||
137 | frameStart += frameSize; // advance past the frame body | ||
138 | } while (frameStart < tagSize); | ||
139 | this.trigger('data', chunk); | ||
140 | }; | ||
141 | }; | ||
142 | MetadataStream.prototype = new videojs.Hls.Stream(); | ||
143 | |||
144 | videojs.Hls.MetadataStream = MetadataStream; | ||
145 | })(window, window.videojs); |
... | @@ -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,23 +401,26 @@ | ... | @@ -383,23 +401,26 @@ |
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 |
402 | ESInfolength = (data[offset + 3] & 0x0F) << 8 | data[offset + 4]; | 419 | ESInfolength = (data[offset + 3] & 0x0F) << 8 | data[offset + 4]; |
420 | // capture the stream descriptor for metadata streams | ||
421 | if (streamType === STREAM_TYPES.metadata) { | ||
422 | self.metadataStream.descriptor = new Uint8Array(data.subarray(offset + 5, offset + 5 + ESInfolength)); | ||
423 | } | ||
403 | // move to the first byte after the end of this entry | 424 | // move to the first byte after the end of this entry |
404 | offset += 5 + ESInfolength; | 425 | offset += 5 + ESInfolength; |
405 | pmtSectionLength -= 5 + ESInfolength; | 426 | pmtSectionLength -= 5 + ESInfolength; |
... | @@ -435,7 +456,8 @@ | ... | @@ -435,7 +456,8 @@ |
435 | videojs.Hls.SegmentParser.MP2T_PACKET_LENGTH = MP2T_PACKET_LENGTH = 188; | 456 | videojs.Hls.SegmentParser.MP2T_PACKET_LENGTH = MP2T_PACKET_LENGTH = 188; |
436 | videojs.Hls.SegmentParser.STREAM_TYPES = STREAM_TYPES = { | 457 | videojs.Hls.SegmentParser.STREAM_TYPES = STREAM_TYPES = { |
437 | h264: 0x1b, | 458 | h264: 0x1b, |
438 | adts: 0x0f | 459 | adts: 0x0f, |
460 | metadata: 0x15 | ||
439 | }; | 461 | }; |
440 | 462 | ||
441 | })(window); | 463 | })(window); | ... | ... |
... | @@ -74,6 +74,44 @@ videojs.Hls.prototype.src = function(src) { | ... | @@ -74,6 +74,44 @@ videojs.Hls.prototype.src = function(src) { |
74 | this.segmentBuffer_ = []; | 74 | this.segmentBuffer_ = []; |
75 | this.segmentParser_ = new videojs.Hls.SegmentParser(); | 75 | this.segmentParser_ = new videojs.Hls.SegmentParser(); |
76 | 76 | ||
77 | // if the stream contains ID3 metadata, expose that as a metadata | ||
78 | // text track | ||
79 | (function() { | ||
80 | var | ||
81 | metadataStream = tech.segmentParser_.metadataStream, | ||
82 | textTrack; | ||
83 | |||
84 | // only expose metadata tracks to video.js versions that support | ||
85 | // dynamic text tracks (4.12+) | ||
86 | if (!tech.player().addTextTrack) { | ||
87 | return; | ||
88 | } | ||
89 | |||
90 | metadataStream.on('data', function(metadata) { | ||
91 | var i, frame, time, hexDigit; | ||
92 | |||
93 | // create the metadata track if this is the first ID3 tag we've | ||
94 | // seen | ||
95 | if (!textTrack) { | ||
96 | textTrack = tech.player().addTextTrack('metadata', 'Timed Metadata'); | ||
97 | |||
98 | // build the dispatch type from the stream descriptor | ||
99 | // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track | ||
100 | textTrack.inBandMetadataTrackDispatchType = videojs.Hls.SegmentParser.STREAM_TYPES.metadata.toString(16).toUpperCase(); | ||
101 | for (i = 0; i < metadataStream.descriptor.length; i++) { | ||
102 | hexDigit = ('00' + metadataStream.descriptor[i].toString(16).toUpperCase()).slice(-2); | ||
103 | textTrack.inBandMetadataTrackDispatchType += hexDigit; | ||
104 | } | ||
105 | } | ||
106 | |||
107 | for (i = 0; i < metadata.frames.length; i++) { | ||
108 | frame = metadata.frames[i]; | ||
109 | time = metadata.pts / 1000; | ||
110 | textTrack.addCue(new window.VTTCue(time, time, frame.value || frame.url)); | ||
111 | } | ||
112 | }); | ||
113 | })(); | ||
114 | |||
77 | // load the MediaSource into the player | 115 | // load the MediaSource into the player |
78 | this.mediaSource.addEventListener('sourceopen', videojs.bind(this, this.handleSourceOpen)); | 116 | this.mediaSource.addEventListener('sourceopen', videojs.bind(this, this.handleSourceOpen)); |
79 | 117 | ... | ... |
... | @@ -80,14 +80,15 @@ module.exports = function(config) { | ... | @@ -80,14 +80,15 @@ module.exports = function(config) { |
80 | '../node_modules/pkcs7/dist/pkcs7.unpad.js', | 80 | '../node_modules/pkcs7/dist/pkcs7.unpad.js', |
81 | '../test/karma-qunit-shim.js', | 81 | '../test/karma-qunit-shim.js', |
82 | '../src/videojs-hls.js', | 82 | '../src/videojs-hls.js', |
83 | '../src/xhr.js', | 83 | '../src/stream.js', |
84 | '../src/flv-tag.js', | 84 | '../src/flv-tag.js', |
85 | '../src/exp-golomb.js', | 85 | '../src/exp-golomb.js', |
86 | '../src/h264-stream.js', | 86 | '../src/h264-stream.js', |
87 | '../src/aac-stream.js', | 87 | '../src/aac-stream.js', |
88 | '../src/metadata-stream.js', | ||
88 | '../src/segment-parser.js', | 89 | '../src/segment-parser.js', |
89 | '../src/stream.js', | ||
90 | '../src/m3u8/m3u8-parser.js', | 90 | '../src/m3u8/m3u8-parser.js', |
91 | '../src/xhr.js', | ||
91 | '../src/playlist-loader.js', | 92 | '../src/playlist-loader.js', |
92 | '../src/decrypter.js', | 93 | '../src/decrypter.js', |
93 | '../tmp/manifests.js', | 94 | '../tmp/manifests.js', | ... | ... |
... | @@ -44,14 +44,15 @@ module.exports = function(config) { | ... | @@ -44,14 +44,15 @@ module.exports = function(config) { |
44 | '../node_modules/pkcs7/dist/pkcs7.unpad.js', | 44 | '../node_modules/pkcs7/dist/pkcs7.unpad.js', |
45 | '../test/karma-qunit-shim.js', | 45 | '../test/karma-qunit-shim.js', |
46 | '../src/videojs-hls.js', | 46 | '../src/videojs-hls.js', |
47 | '../src/xhr.js', | 47 | '../src/stream.js', |
48 | '../src/flv-tag.js', | 48 | '../src/flv-tag.js', |
49 | '../src/exp-golomb.js', | 49 | '../src/exp-golomb.js', |
50 | '../src/h264-stream.js', | 50 | '../src/h264-stream.js', |
51 | '../src/aac-stream.js', | 51 | '../src/aac-stream.js', |
52 | '../src/metadata-stream.js', | ||
52 | '../src/segment-parser.js', | 53 | '../src/segment-parser.js', |
53 | '../src/stream.js', | ||
54 | '../src/m3u8/m3u8-parser.js', | 54 | '../src/m3u8/m3u8-parser.js', |
55 | '../src/xhr.js', | ||
55 | '../src/playlist-loader.js', | 56 | '../src/playlist-loader.js', |
56 | '../src/decrypter.js', | 57 | '../src/decrypter.js', |
57 | '../tmp/manifests.js', | 58 | '../tmp/manifests.js', | ... | ... |
test/metadata-stream_test.js
0 → 100644
1 | (function(window, videojs, undefined) { | ||
2 | 'use strict'; | ||
3 | /* | ||
4 | ======== A Handy Little QUnit Reference ======== | ||
5 | http://api.qunitjs.com/ | ||
6 | |||
7 | Test methods: | ||
8 | module(name, {[setup][ ,teardown]}) | ||
9 | test(name, callback) | ||
10 | expect(numberOfAssertions) | ||
11 | stop(increment) | ||
12 | start(decrement) | ||
13 | Test assertions: | ||
14 | ok(value, [message]) | ||
15 | equal(actual, expected, [message]) | ||
16 | notEqual(actual, expected, [message]) | ||
17 | deepEqual(actual, expected, [message]) | ||
18 | notDeepEqual(actual, expected, [message]) | ||
19 | strictEqual(actual, expected, [message]) | ||
20 | notStrictEqual(actual, expected, [message]) | ||
21 | throws(block, [expected], [message]) | ||
22 | */ | ||
23 | |||
24 | var metadataStream, stringToInts, stringToCString, id3Tag, id3Frame; | ||
25 | |||
26 | module('MetadataStream', { | ||
27 | setup: function() { | ||
28 | metadataStream = new videojs.Hls.MetadataStream(); | ||
29 | } | ||
30 | }); | ||
31 | |||
32 | test('can construct a MetadataStream', function() { | ||
33 | ok(metadataStream, 'does not return null'); | ||
34 | }); | ||
35 | |||
36 | stringToInts = function(string) { | ||
37 | var result = [], i; | ||
38 | for (i = 0; i < string.length; i++) { | ||
39 | result[i] = string.charCodeAt(i); | ||
40 | } | ||
41 | return result; | ||
42 | }; | ||
43 | |||
44 | stringToCString = function(string) { | ||
45 | return stringToInts(string).concat([0x00]); | ||
46 | }; | ||
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 | |||
72 | id3Frame = function(type) { | ||
73 | var result = stringToInts(type).concat([ | ||
74 | 0x00, 0x00, 0x00, 0x00, // size | ||
75 | 0xe0, 0x00 // flags. tag/file alter preservation, read-only | ||
76 | ]), | ||
77 | size = result.length - 10; | ||
78 | |||
79 | // append the fields of the ID3 frame | ||
80 | result = result.concat.apply(result, Array.prototype.slice.call(arguments, 1)); | ||
81 | |||
82 | // set the size | ||
83 | size = result.length - 10; | ||
84 | result[4] = (size >>> 24); | ||
85 | result[5] = (size >>> 16) & 0xff; | ||
86 | result[6] = (size >>> 8) & 0xff; | ||
87 | result[7] = (size) & 0xff; | ||
88 | |||
89 | return result; | ||
90 | }; | ||
91 | |||
92 | test('parses simple ID3 metadata out of PES packets', function() { | ||
93 | var | ||
94 | events = [], | ||
95 | wxxxPayload = [ | ||
96 | 0x00 // text encoding. ISO-8859-1 | ||
97 | ].concat(stringToCString('ad tag URL'), // description | ||
98 | stringToInts('http://example.com/ad?v=1234&q=7')), // value | ||
99 | id3Bytes, | ||
100 | size; | ||
101 | |||
102 | metadataStream.on('data', function(event) { | ||
103 | events.push(event); | ||
104 | }); | ||
105 | |||
106 | id3Bytes = new Uint8Array(stringToInts('ID3').concat([ | ||
107 | 0x03, 0x00, // version 3.0 of ID3v2 (aka ID3v.2.3.0) | ||
108 | 0x40, // flags. include an extended header | ||
109 | 0x00, 0x00, 0x00, 0x00, // size. set later | ||
110 | |||
111 | // extended header | ||
112 | 0x00, 0x00, 0x00, 0x06, // extended header size. no CRC | ||
113 | 0x00, 0x00, // extended flags | ||
114 | 0x00, 0x00, 0x00, 0x02, // size of padding | ||
115 | |||
116 | // frame 0 | ||
117 | // http://id3.org/id3v2.3.0#User_defined_text_information_frame | ||
118 | ], id3Frame('WXXX', | ||
119 | wxxxPayload), // value | ||
120 | // frame 1 | ||
121 | // custom tag | ||
122 | id3Frame('XINF', | ||
123 | [ | ||
124 | 0x04, 0x03, 0x02, 0x01 // arbitrary data | ||
125 | ]), [ | ||
126 | 0x00, 0x00 // padding | ||
127 | ])); | ||
128 | |||
129 | // set header size field | ||
130 | size = id3Bytes.byteLength - 10; | ||
131 | id3Bytes[6] = (size >>> 21) & 0x7f; | ||
132 | id3Bytes[7] = (size >>> 14) & 0x7f; | ||
133 | id3Bytes[8] = (size >>> 7) & 0x7f; | ||
134 | id3Bytes[9] = (size) & 0x7f; | ||
135 | |||
136 | metadataStream.push({ | ||
137 | trackId: 7, | ||
138 | pts: 1000, | ||
139 | dts: 1000, | ||
140 | |||
141 | // header | ||
142 | data: id3Bytes | ||
143 | }); | ||
144 | |||
145 | equal(events.length, 1, 'parsed one tag'); | ||
146 | equal(events[0].frames.length, 2, 'parsed two frames'); | ||
147 | equal(events[0].frames[0].id, 'WXXX', 'parsed a WXXX frame'); | ||
148 | deepEqual(new Uint8Array(events[0].frames[0].data), | ||
149 | new Uint8Array(wxxxPayload), | ||
150 | 'attached the frame payload'); | ||
151 | equal(events[0].frames[1].id, 'XINF', 'parsed a user-defined frame'); | ||
152 | deepEqual(new Uint8Array(events[0].frames[1].data), | ||
153 | new Uint8Array([0x04, 0x03, 0x02, 0x01]), | ||
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'); | ||
157 | }); | ||
158 | |||
159 | test('skips non-ID3 metadata events', function() { | ||
160 | var events = []; | ||
161 | metadataStream.on('data', function(event) { | ||
162 | events.push(event); | ||
163 | }); | ||
164 | |||
165 | metadataStream.push({ | ||
166 | trackId: 7, | ||
167 | pts: 1000, | ||
168 | dts: 1000, | ||
169 | |||
170 | // header | ||
171 | data: new Uint8Array([0]) | ||
172 | }); | ||
173 | |||
174 | equal(events.length, 0, 'did not emit an event'); | ||
175 | }); | ||
176 | |||
177 | // missing cases: | ||
178 | // unsynchronization | ||
179 | // CRC | ||
180 | // no extended header | ||
181 | // compressed frames | ||
182 | // encrypted frames | ||
183 | // frame groups | ||
184 | // too large/small tag size values | ||
185 | // too large/small frame size values | ||
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 | 0x03, // utf-8 | ||
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 | 0x03, // utf-8 | ||
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 | 0x03, // utf-8 | ||
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 | |||
283 | // 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() { | ||
285 | metadataStream = new videojs.Hls.MetadataStream({ | ||
286 | descriptor: new Uint8Array([0x03, 0x02, 0x01, 0x00]) | ||
287 | }); | ||
288 | |||
289 | equal(metadataStream.dispatchType, '1503020100', 'built the dispatch type'); | ||
290 | }); | ||
291 | |||
292 | })(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 | ... | ... |
... | @@ -21,14 +21,15 @@ | ... | @@ -21,14 +21,15 @@ |
21 | <!-- HLS plugin --> | 21 | <!-- HLS plugin --> |
22 | <script src="../src/videojs-hls.js"></script> | 22 | <script src="../src/videojs-hls.js"></script> |
23 | <script src="../src/xhr.js"></script> | 23 | <script src="../src/xhr.js"></script> |
24 | <script src="../src/stream.js"></script> | ||
24 | <script src="../src/flv-tag.js"></script> | 25 | <script src="../src/flv-tag.js"></script> |
25 | <script src="../src/exp-golomb.js"></script> | 26 | <script src="../src/exp-golomb.js"></script> |
26 | <script src="../src/h264-stream.js"></script> | 27 | <script src="../src/h264-stream.js"></script> |
27 | <script src="../src/aac-stream.js"></script> | 28 | <script src="../src/aac-stream.js"></script> |
29 | <script src="../src/metadata-stream.js"></script> | ||
28 | <script src="../src/segment-parser.js"></script> | 30 | <script src="../src/segment-parser.js"></script> |
29 | 31 | ||
30 | <!-- M3U8 --> | 32 | <!-- M3U8 --> |
31 | <script src="../src/stream.js"></script> | ||
32 | <script src="../src/m3u8/m3u8-parser.js"></script> | 33 | <script src="../src/m3u8/m3u8-parser.js"></script> |
33 | <script src="../src/playlist-loader.js"></script> | 34 | <script src="../src/playlist-loader.js"></script> |
34 | <script src="../node_modules/pkcs7/dist/pkcs7.unpad.js"></script> | 35 | <script src="../node_modules/pkcs7/dist/pkcs7.unpad.js"></script> |
... | @@ -55,6 +56,7 @@ | ... | @@ -55,6 +56,7 @@ |
55 | <script src="h264-stream_test.js"></script> | 56 | <script src="h264-stream_test.js"></script> |
56 | <script src="exp-golomb_test.js"></script> | 57 | <script src="exp-golomb_test.js"></script> |
57 | <script src="flv-tag_test.js"></script> | 58 | <script src="flv-tag_test.js"></script> |
59 | <script src="metadata-stream_test.js"></script> | ||
58 | <script src="m3u8_test.js"></script> | 60 | <script src="m3u8_test.js"></script> |
59 | <script src="playlist-loader_test.js"></script> | 61 | <script src="playlist-loader_test.js"></script> |
60 | <script src="decrypter_test.js"></script> | 62 | <script src="decrypter_test.js"></script> | ... | ... |
... | @@ -110,6 +110,9 @@ var | ... | @@ -110,6 +110,9 @@ var |
110 | this.getNextTag = function() { | 110 | this.getNextTag = function() { |
111 | return tags.shift(); | 111 | return tags.shift(); |
112 | }; | 112 | }; |
113 | this.metadataStream = { | ||
114 | on: Function.prototype | ||
115 | }; | ||
113 | }; | 116 | }; |
114 | }; | 117 | }; |
115 | 118 | ||
... | @@ -897,6 +900,9 @@ test('flushes the parser after each segment', function() { | ... | @@ -897,6 +900,9 @@ test('flushes the parser after each segment', function() { |
897 | flushes++; | 900 | flushes++; |
898 | }; | 901 | }; |
899 | this.tagsAvailable = function() {}; | 902 | this.tagsAvailable = function() {}; |
903 | this.metadataStream = { | ||
904 | on: Function.prototype | ||
905 | }; | ||
900 | }; | 906 | }; |
901 | 907 | ||
902 | player.src({ | 908 | player.src({ |
... | @@ -910,6 +916,52 @@ test('flushes the parser after each segment', function() { | ... | @@ -910,6 +916,52 @@ test('flushes the parser after each segment', function() { |
910 | strictEqual(flushes, 1, 'tags are flushed at the end of a segment'); | 916 | strictEqual(flushes, 1, 'tags are flushed at the end of a segment'); |
911 | }); | 917 | }); |
912 | 918 | ||
919 | test('exposes in-band metadata events as cues', function() { | ||
920 | var track; | ||
921 | player.src({ | ||
922 | src: 'manifest/media.m3u8', | ||
923 | type: 'application/vnd.apple.mpegurl' | ||
924 | }); | ||
925 | openMediaSource(player); | ||
926 | |||
927 | player.hls.segmentParser_.parseSegmentBinaryData = function() { | ||
928 | // fake out a descriptor | ||
929 | player.hls.segmentParser_.metadataStream.descriptor = new Uint8Array([ | ||
930 | 1, 2, 3, 0xbb | ||
931 | ]); | ||
932 | // trigger a metadata event | ||
933 | player.hls.segmentParser_.metadataStream.trigger('data', { | ||
934 | pts: 2000, | ||
935 | data: new Uint8Array([]), | ||
936 | frames: [{ | ||
937 | type: 'TXXX', | ||
938 | value: 'cue text' | ||
939 | }, { | ||
940 | type: 'WXXX', | ||
941 | url: 'http://example.com' | ||
942 | }] | ||
943 | }); | ||
944 | }; | ||
945 | |||
946 | standardXHRResponse(requests[0]); | ||
947 | standardXHRResponse(requests[1]); | ||
948 | |||
949 | equal(player.textTracks().length, 1, 'created a text track'); | ||
950 | track = player.textTracks()[0]; | ||
951 | equal(track.kind, 'metadata', 'kind is metadata'); | ||
952 | equal(track.inBandMetadataTrackDispatchType, '15010203BB', 'set the dispatch type'); | ||
953 | equal(track.cues.length, 2, 'created two cues'); | ||
954 | equal(track.cues[0].startTime, 2, 'cue starts at 2 seconds'); | ||
955 | equal(track.cues[0].endTime, 2, 'cue ends at 2 seconds'); | ||
956 | equal(track.cues[0].pauseOnExit, false, 'cue does not pause on exit'); | ||
957 | equal(track.cues[0].text, 'cue text', 'set cue text'); | ||
958 | |||
959 | equal(track.cues[1].startTime, 2, 'cue starts at 2 seconds'); | ||
960 | equal(track.cues[1].endTime, 2, 'cue ends at 2 seconds'); | ||
961 | equal(track.cues[1].pauseOnExit, false, 'cue does not pause on exit'); | ||
962 | equal(track.cues[1].text, 'http://example.com', 'set cue text'); | ||
963 | }); | ||
964 | |||
913 | test('drops tags before the target timestamp when seeking', function() { | 965 | test('drops tags before the target timestamp when seeking', function() { |
914 | var i = 10, | 966 | var i = 10, |
915 | tags = [], | 967 | tags = [], | ... | ... |
-
Please register or sign in to post a comment