d7019d06 by David LaPalomento

Merge pull request #534 from BrandonOCasey/browserify-p2

browserify-p2: m3u8, stream, and stub
2 parents b0caca88 c2ce93db
...@@ -34,5 +34,5 @@ dist-test/ ...@@ -34,5 +34,5 @@ dist-test/
34 docs/api/ 34 docs/api/
35 es5/ 35 es5/
36 tmp 36 tmp
37 test/data/manifests.js 37 test/test-manifests.js
38 test/data/expected.js 38 test/test-expected.js
......
...@@ -40,14 +40,17 @@ ...@@ -40,14 +40,17 @@
40 </label> 40 </label>
41 <button type=submit>Load</button> 41 <button type=submit>Load</button>
42 </form> 42 </form>
43 <ul>
44 <li><a href="/test/">Run unit tests in browser.</a></li>
45 <li><a href="/docs/api/">Read generated docs.</a></li>
46 </ul>
43 47
44 <script src="/node_modules/video.js/dist/video.js"></script> 48 <script src="/node_modules/video.js/dist/video.js"></script>
45 <script src="/node_modules/videojs-contrib-media-sources/dist/videojs-media-sources.js"></script> 49 <script src="/node_modules/videojs-contrib-media-sources/dist/videojs-media-sources.js"></script>
46 <script src="/node_modules/pkcs7/dist/pkcs7.unpad.js"></script> 50 <script src="/node_modules/pkcs7/dist/pkcs7.unpad.js"></script>
47 <script src="/src/videojs-hls.js"></script> 51 <script src="/src/videojs-contrib-hls.js"></script>
48 <script src="/src/xhr.js"></script> 52 <script src="/src/xhr.js"></script>
49 <script src="/src/stream.js"></script> 53 <script src="/dist/videojs-contrib-hls.js"></script>
50 <script src="/src/m3u8/m3u8-parser.js"></script>
51 <script src="/src/playlist.js"></script> 54 <script src="/src/playlist.js"></script>
52 <script src="/src/playlist-loader.js"></script> 55 <script src="/src/playlist-loader.js"></script>
53 <script src="/src/decrypter.js"></script> 56 <script src="/src/decrypter.js"></script>
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
2 "name": "videojs-contrib-hls", 2 "name": "videojs-contrib-hls",
3 "version": "1.3.5", 3 "version": "1.3.5",
4 "description": "Play back HLS with video.js, even where it's not natively supported", 4 "description": "Play back HLS with video.js, even where it's not natively supported",
5 "main": "es5/videojs-hls.js", 5 "main": "es5/stub.js",
6 "engines": { 6 "engines": {
7 "node": ">= 0.10.12" 7 "node": ">= 0.10.12"
8 }, 8 },
...@@ -13,16 +13,17 @@ ...@@ -13,16 +13,17 @@
13 "scripts": { 13 "scripts": {
14 "prebuild": "npm run clean", 14 "prebuild": "npm run clean",
15 "build": "npm-run-all -p build:*", 15 "build": "npm-run-all -p build:*",
16 "build:manifest": "node -e \"var b=require('./scripts/manifest-data.js'); b.build();\"",
17 "build:js": "npm-run-all build:js:babel build:js:browserify build:js:bannerize build:js:uglify", 16 "build:js": "npm-run-all build:js:babel build:js:browserify build:js:bannerize build:js:uglify",
18 "build:js:babel": "babel src -d es5", 17 "build:js:babel": "babel src -d es5",
19 "build:js:bannerize": "bannerize dist/videojs-contrib-hls.js --banner=scripts/banner.ejs", 18 "build:js:bannerize": "bannerize dist/videojs-contrib-hls.js --banner=scripts/banner.ejs",
20 "build:js:browserify": "browserify . -s src/videojs-hls.js -o dist/videojs-contrib-hls.js", 19 "build:js:browserify": "browserify . -s videojs-contrib-hls -o dist/videojs-contrib-hls.js",
21 "build:js:uglify": "uglifyjs dist/videojs-contrib-hls.js --comments --mangle --compress -o dist/videojs-contrib-hls.min.js", 20 "build:js:uglify": "uglifyjs dist/videojs-contrib-hls.js --comments --mangle --compress -o dist/videojs-contrib-hls.min.js",
22 "build:test": "node scripts/build-test.js", 21 "build:test": "npm-run-all build:test:manifest build:test:js",
23 "clean": "npm-run-all clean:*", 22 "build:test:js": "node scripts/build-test.js",
23 "build:test:manifest": "node -e \"var b=require('./scripts/manifest-data.js'); b.build();\"",
24 "clean": "npm-run-all -p clean:*",
24 "clean:build": "node -e \"var s=require('shelljs'),d=['dist','dist-test','es5'];s.rm('-rf',d);s.mkdir('-p',d);\"", 25 "clean:build": "node -e \"var s=require('shelljs'),d=['dist','dist-test','es5'];s.rm('-rf',d);s.mkdir('-p',d);\"",
25 "clean:manifest": "node -e \"var b=require('./scripts/manifest-data.js'); b.clean();\"", 26 "clean:test": "node -e \"var b=require('./scripts/manifest-data.js'); b.clean();\"",
26 "docs": "npm-run-all docs:*", 27 "docs": "npm-run-all docs:*",
27 "docs:api": "jsdoc src -r -d docs/api", 28 "docs:api": "jsdoc src -r -d docs/api",
28 "docs:toc": "doctoc README.md", 29 "docs:toc": "doctoc README.md",
...@@ -39,9 +40,10 @@ ...@@ -39,9 +40,10 @@
39 "preversion": "npm test", 40 "preversion": "npm test",
40 "version": "npm run build", 41 "version": "npm run build",
41 "watch": "npm-run-all -p watch:*", 42 "watch": "npm-run-all -p watch:*",
42 "watch:manifest": "node -e \"var b=require('./scripts/manifest-data.js'); b.watch();\"", 43 "watch:js": "watchify src/stub.js -t babelify -v -o dist/videojs-contrib-hls.js",
43 "watch:js": "watchify src/videojs-hls.js -t babelify -v -o dist/videojs-contrib-hls.js", 44 "watch:test": "npm-run-all -p watch:test:*",
44 "watch:test": "node scripts/watch-test.js", 45 "watch:test:js": "node scripts/watch-test.js",
46 "watch:test:manifest": "node -e \"var b=require('./scripts/manifest-data.js'); b.watch();\"",
45 "prepublish": "npm run build" 47 "prepublish": "npm run build"
46 }, 48 },
47 "keywords": [ 49 "keywords": [
...@@ -69,7 +71,8 @@ ...@@ -69,7 +71,8 @@
69 "test/karma", 71 "test/karma",
70 "scripts", 72 "scripts",
71 "utils", 73 "utils",
72 "test/data" 74 "test/test-manifests.js",
75 "test/test-expected.js"
73 ] 76 ]
74 }, 77 },
75 "files": [ 78 "files": [
......
...@@ -2,7 +2,7 @@ var browserify = require('browserify'); ...@@ -2,7 +2,7 @@ var browserify = require('browserify');
2 var fs = require('fs'); 2 var fs = require('fs');
3 var glob = require('glob'); 3 var glob = require('glob');
4 4
5 glob('test/**/*.test.js', function(err, files) { 5 glob('test/{m3u8,stub}.test.js', function(err, files) {
6 browserify(files) 6 browserify(files)
7 .transform('babelify') 7 .transform('babelify')
8 .bundle() 8 .bundle()
......
1 var fs = require('fs'); 1 var fs = require('fs');
2 var path = require('path'); 2 var path = require('path');
3 3
4 var basePath = path.resolve(__dirname + '/..'); 4 var basePath = path.resolve(__dirname, '..');
5 var testDataDir = basePath + '/test/data'; 5 var testDataDir = path.join(basePath,'test');
6 var manifestDir = basePath + '/utils/manifest'; 6 var manifestDir = path.join(basePath, 'utils', 'manifest');
7 var manifestFilepath = testDataDir + '/manifests.js'; 7 var manifestFilepath = path.join(testDataDir, 'test-manifests.js');
8 var expectedFilepath = testDataDir + '/expected.js'; 8 var expectedFilepath = path.join(testDataDir, 'test-expected.js');
9
10 9
11 var build = function() { 10 var build = function() {
12 var manifests = 'window.manifests = {\n'; 11 var manifests = 'export default {\n';
13 var expected = 'window.expected = {\n'; 12 var expected = 'export default {\n';
14 13
15 var files = fs.readdirSync(manifestDir); 14 var files = fs.readdirSync(manifestDir);
16 while (files.length > 0) { 15 while (files.length > 0) {
......
...@@ -3,7 +3,7 @@ var fs = require('fs'); ...@@ -3,7 +3,7 @@ var fs = require('fs');
3 var glob = require('glob'); 3 var glob = require('glob');
4 var watchify = require('watchify'); 4 var watchify = require('watchify');
5 5
6 glob('test/**/*.test.js', function(err, files) { 6 glob('test/{m3u8,stub}.test.js', function(err, files) {
7 var b = browserify(files, { 7 var b = browserify(files, {
8 cache: {}, 8 cache: {},
9 packageCache: {}, 9 packageCache: {},
......
1 {
2 "curly": true,
3 "eqeqeq": true,
4 "globals": {
5 "console": true
6 },
7 "immed": true,
8 "latedef": true,
9 "newcap": true,
10 "noarg": true,
11 "sub": true,
12 "undef": true,
13 "unused": true,
14 "boss": true,
15 "eqnull": true,
16 "browser": true
17 }
...@@ -5,111 +5,108 @@ ...@@ -5,111 +5,108 @@
5 * that do not assume the entirety of the manifest is ready and expose a 5 * that do not assume the entirety of the manifest is ready and expose a
6 * ReadableStream-like interface. 6 * ReadableStream-like interface.
7 */ 7 */
8 (function(videojs, parseInt, isFinite, mergeOptions, undefined) {
9 var
10 noop = function() {},
11
12 // "forgiving" attribute list psuedo-grammar:
13 // attributes -> keyvalue (',' keyvalue)*
14 // keyvalue -> key '=' value
15 // key -> [^=]*
16 // value -> '"' [^"]* '"' | [^,]*
17 attributeSeparator = (function() {
18 var
19 key = '[^=]*',
20 value = '"[^"]*"|[^,]*',
21 keyvalue = '(?:' + key + ')=(?:' + value + ')';
22
23 return new RegExp('(?:^|,)(' + keyvalue + ')');
24 })(),
25 parseAttributes = function(attributes) {
26 var
27 // split the string using attributes as the separator
28 attrs = attributes.split(attributeSeparator),
29 i = attrs.length,
30 result = {},
31 attr;
32
33 while (i--) {
34 // filter out unmatched portions of the string
35 if (attrs[i] === '') {
36 continue;
37 }
38 8
39 // split the key and value 9 import Stream from '../stream';
40 attr = /([^=]*)=(.*)/.exec(attrs[i]).slice(1); 10 import {mergeOptions} from 'video.js';
41 // trim whitespace and remove optional quotes around the value 11 /**
42 attr[0] = attr[0].replace(/^\s+|\s+$/g, ''); 12 * A stream that buffers string input and generates a `data` event for each
43 attr[1] = attr[1].replace(/^\s+|\s+$/g, ''); 13 * line.
44 attr[1] = attr[1].replace(/^['"](.*)['"]$/g, '$1'); 14 */
45 result[attr[0]] = attr[1]; 15 export class LineStream extends Stream {
46 } 16 constructor() {
47 return result; 17 super();
48 }, 18 this.buffer = '';
49 Stream = videojs.Hls.Stream, 19 }
50 LineStream,
51 ParseStream,
52 Parser;
53 20
54 /** 21 /**
55 * A stream that buffers string input and generates a `data` event for each 22 * Add new data to be parsed.
56 * line. 23 * @param data {string} the text to process
57 */ 24 */
58 LineStream = function() { 25 push(data) {
59 var buffer = ''; 26 let nextNewline;
60 LineStream.prototype.init.call(this); 27
61 28 this.buffer += data;
62 /** 29 nextNewline = this.buffer.indexOf('\n');
63 * Add new data to be parsed. 30
64 * @param data {string} the text to process 31 for (; nextNewline > -1; nextNewline = this.buffer.indexOf('\n')) {
65 */ 32 this.trigger('data', this.buffer.substring(0, nextNewline));
66 this.push = function(data) { 33 this.buffer = this.buffer.substring(nextNewline + 1);
67 var nextNewline; 34 }
68 35 }
69 buffer += data; 36 }
70 nextNewline = buffer.indexOf('\n'); 37
71 38 // "forgiving" attribute list psuedo-grammar:
72 for (; nextNewline > -1; nextNewline = buffer.indexOf('\n')) { 39 // attributes -> keyvalue (',' keyvalue)*
73 this.trigger('data', buffer.substring(0, nextNewline)); 40 // keyvalue -> key '=' value
74 buffer = buffer.substring(nextNewline + 1); 41 // key -> [^=]*
75 } 42 // value -> '"' [^"]* '"' | [^,]*
76 }; 43 const attributeSeparator = function() {
77 }; 44 let key = '[^=]*';
78 LineStream.prototype = new Stream(); 45 let value = '"[^"]*"|[^,]*';
46 let keyvalue = '(?:' + key + ')=(?:' + value + ')';
47
48 return new RegExp('(?:^|,)(' + keyvalue + ')');
49 };
50
51 const parseAttributes = function(attributes) {
52 // split the string using attributes as the separator
53 let attrs = attributes.split(attributeSeparator());
54 let i = attrs.length;
55 let result = {};
56 let attr;
57
58 while (i--) {
59 // filter out unmatched portions of the string
60 if (attrs[i] === '') {
61 continue;
62 }
63
64 // split the key and value
65 attr = (/([^=]*)=(.*)/).exec(attrs[i]).slice(1);
66 // trim whitespace and remove optional quotes around the value
67 attr[0] = attr[0].replace(/^\s+|\s+$/g, '');
68 attr[1] = attr[1].replace(/^\s+|\s+$/g, '');
69 attr[1] = attr[1].replace(/^['"](.*)['"]$/g, '$1');
70 result[attr[0]] = attr[1];
71 }
72 return result;
73 };
74
75 /**
76 * A line-level M3U8 parser event stream. It expects to receive input one
77 * line at a time and performs a context-free parse of its contents. A stream
78 * interpretation of a manifest can be useful if the manifest is expected to
79 * be too large to fit comfortably into memory or the entirety of the input
80 * is not immediately available. Otherwise, it's probably much easier to work
81 * with a regular `Parser` object.
82 *
83 * Produces `data` events with an object that captures the parser's
84 * interpretation of the input. That object has a property `tag` that is one
85 * of `uri`, `comment`, or `tag`. URIs only have a single additional
86 * property, `line`, which captures the entirety of the input without
87 * interpretation. Comments similarly have a single additional property
88 * `text` which is the input without the leading `#`.
89 *
90 * Tags always have a property `tagType` which is the lower-cased version of
91 * the M3U8 directive without the `#EXT` or `#EXT-X-` prefix. For instance,
92 * `#EXT-X-MEDIA-SEQUENCE` becomes `media-sequence` when parsed. Unrecognized
93 * tags are given the tag type `unknown` and a single additional property
94 * `data` with the remainder of the input.
95 */
96 export class ParseStream extends Stream {
97 constructor() {
98 super();
99 }
79 100
80 /**
81 * A line-level M3U8 parser event stream. It expects to receive input one
82 * line at a time and performs a context-free parse of its contents. A stream
83 * interpretation of a manifest can be useful if the manifest is expected to
84 * be too large to fit comfortably into memory or the entirety of the input
85 * is not immediately available. Otherwise, it's probably much easier to work
86 * with a regular `Parser` object.
87 *
88 * Produces `data` events with an object that captures the parser's
89 * interpretation of the input. That object has a property `tag` that is one
90 * of `uri`, `comment`, or `tag`. URIs only have a single additional
91 * property, `line`, which captures the entirety of the input without
92 * interpretation. Comments similarly have a single additional property
93 * `text` which is the input without the leading `#`.
94 *
95 * Tags always have a property `tagType` which is the lower-cased version of
96 * the M3U8 directive without the `#EXT` or `#EXT-X-` prefix. For instance,
97 * `#EXT-X-MEDIA-SEQUENCE` becomes `media-sequence` when parsed. Unrecognized
98 * tags are given the tag type `unknown` and a single additional property
99 * `data` with the remainder of the input.
100 */
101 ParseStream = function() {
102 ParseStream.prototype.init.call(this);
103 };
104 ParseStream.prototype = new Stream();
105 /** 101 /**
106 * Parses an additional line of input. 102 * Parses an additional line of input.
107 * @param line {string} a single line of an M3U8 file to parse 103 * @param line {string} a single line of an M3U8 file to parse
108 */ 104 */
109 ParseStream.prototype.push = function(line) { 105 push(line) {
110 var match, event; 106 let match;
107 let event;
111 108
112 //strip whitespace 109 // strip whitespace
113 line = line.replace(/^[\u0000\s]+|[\u0000\s]+$/g, ''); 110 line = line.replace(/^[\u0000\s]+|[\u0000\s]+$/g, '');
114 if (line.length === 0) { 111 if (line.length === 0) {
115 // ignore empty lines 112 // ignore empty lines
...@@ -134,12 +131,12 @@ ...@@ -134,12 +131,12 @@
134 return; 131 return;
135 } 132 }
136 133
137 //strip off any carriage returns here so the regex matching 134 // strip off any carriage returns here so the regex matching
138 //doesn't have to account for them. 135 // doesn't have to account for them.
139 line = line.replace('\r',''); 136 line = line.replace('\r', '');
140 137
141 // Tags 138 // Tags
142 match = /^#EXTM3U/.exec(line); 139 match = (/^#EXTM3U/).exec(line);
143 if (match) { 140 if (match) {
144 this.trigger('data', { 141 this.trigger('data', {
145 type: 'tag', 142 type: 'tag',
...@@ -271,18 +268,16 @@ ...@@ -271,18 +268,16 @@
271 event.attributes = parseAttributes(match[1]); 268 event.attributes = parseAttributes(match[1]);
272 269
273 if (event.attributes.RESOLUTION) { 270 if (event.attributes.RESOLUTION) {
274 (function() { 271 let split = event.attributes.RESOLUTION.split('x');
275 var 272 let resolution = {};
276 split = event.attributes.RESOLUTION.split('x'), 273
277 resolution = {}; 274 if (split[0]) {
278 if (split[0]) { 275 resolution.width = parseInt(split[0], 10);
279 resolution.width = parseInt(split[0], 10); 276 }
280 } 277 if (split[1]) {
281 if (split[1]) { 278 resolution.height = parseInt(split[1], 10);
282 resolution.height = parseInt(split[1], 10); 279 }
283 } 280 event.attributes.RESOLUTION = resolution;
284 event.attributes.RESOLUTION = resolution;
285 })();
286 } 281 }
287 if (event.attributes.BANDWIDTH) { 282 if (event.attributes.BANDWIDTH) {
288 event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10); 283 event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10);
...@@ -320,7 +315,7 @@ ...@@ -320,7 +315,7 @@
320 event.attributes = parseAttributes(match[1]); 315 event.attributes = parseAttributes(match[1]);
321 // parse the IV string into a Uint32Array 316 // parse the IV string into a Uint32Array
322 if (event.attributes.IV) { 317 if (event.attributes.IV) {
323 if (event.attributes.IV.substring(0,2) === '0x') { 318 if (event.attributes.IV.substring(0, 2) === '0x') {
324 event.attributes.IV = event.attributes.IV.substring(2); 319 event.attributes.IV = event.attributes.IV.substring(2);
325 } 320 }
326 321
...@@ -341,37 +336,40 @@ ...@@ -341,37 +336,40 @@
341 type: 'tag', 336 type: 'tag',
342 data: line.slice(4, line.length) 337 data: line.slice(4, line.length)
343 }); 338 });
344 }; 339 }
345 340 }
346 /**
347 * A parser for M3U8 files. The current interpretation of the input is
348 * exposed as a property `manifest` on parser objects. It's just two lines to
349 * create and parse a manifest once you have the contents available as a string:
350 *
351 * ```js
352 * var parser = new videojs.m3u8.Parser();
353 * parser.push(xhr.responseText);
354 * ```
355 *
356 * New input can later be applied to update the manifest object by calling
357 * `push` again.
358 *
359 * The parser attempts to create a usable manifest object even if the
360 * underlying input is somewhat nonsensical. It emits `info` and `warning`
361 * events during the parse if it encounters input that seems invalid or
362 * requires some property of the manifest object to be defaulted.
363 */
364 Parser = function() {
365 var
366 self = this,
367 uris = [],
368 currentUri = {},
369 key;
370 Parser.prototype.init.call(this);
371 341
342 /**
343 * A parser for M3U8 files. The current interpretation of the input is
344 * exposed as a property `manifest` on parser objects. It's just two lines to
345 * create and parse a manifest once you have the contents available as a string:
346 *
347 * ```js
348 * var parser = new videojs.m3u8.Parser();
349 * parser.push(xhr.responseText);
350 * ```
351 *
352 * New input can later be applied to update the manifest object by calling
353 * `push` again.
354 *
355 * The parser attempts to create a usable manifest object even if the
356 * underlying input is somewhat nonsensical. It emits `info` and `warning`
357 * events during the parse if it encounters input that seems invalid or
358 * requires some property of the manifest object to be defaulted.
359 */
360 export class Parser extends Stream {
361 constructor() {
362 super();
372 this.lineStream = new LineStream(); 363 this.lineStream = new LineStream();
373 this.parseStream = new ParseStream(); 364 this.parseStream = new ParseStream();
374 this.lineStream.pipe(this.parseStream); 365 this.lineStream.pipe(this.parseStream);
366 /* eslint-disable consistent-this */
367 let self = this;
368 /* eslint-enable consistent-this */
369 let uris = [];
370 let currentUri = {};
371 let key;
372 let noop = function() {};
375 373
376 // the manifest is empty until the parse stream begins delivering data 374 // the manifest is empty until the parse stream begins delivering data
377 this.manifest = { 375 this.manifest = {
...@@ -382,10 +380,10 @@ ...@@ -382,10 +380,10 @@
382 // update the manifest with the m3u8 entry from the parse stream 380 // update the manifest with the m3u8 entry from the parse stream
383 this.parseStream.on('data', function(entry) { 381 this.parseStream.on('data', function(entry) {
384 ({ 382 ({
385 tag: function() { 383 tag() {
386 // switch based on the tag type 384 // switch based on the tag type
387 (({ 385 (({
388 'allow-cache': function() { 386 'allow-cache'() {
389 this.manifest.allowCache = entry.allowed; 387 this.manifest.allowCache = entry.allowed;
390 if (!('allowed' in entry)) { 388 if (!('allowed' in entry)) {
391 this.trigger('info', { 389 this.trigger('info', {
...@@ -394,8 +392,9 @@ ...@@ -394,8 +392,9 @@
394 this.manifest.allowCache = true; 392 this.manifest.allowCache = true;
395 } 393 }
396 }, 394 },
397 'byterange': function() { 395 byterange() {
398 var byterange = {}; 396 let byterange = {};
397
399 if ('length' in entry) { 398 if ('length' in entry) {
400 currentUri.byterange = byterange; 399 currentUri.byterange = byterange;
401 byterange.length = entry.length; 400 byterange.length = entry.length;
...@@ -412,10 +411,10 @@ ...@@ -412,10 +411,10 @@
412 byterange.offset = entry.offset; 411 byterange.offset = entry.offset;
413 } 412 }
414 }, 413 },
415 'endlist': function() { 414 endlist() {
416 this.manifest.endList = true; 415 this.manifest.endList = true;
417 }, 416 },
418 'inf': function() { 417 inf() {
419 if (!('mediaSequence' in this.manifest)) { 418 if (!('mediaSequence' in this.manifest)) {
420 this.manifest.mediaSequence = 0; 419 this.manifest.mediaSequence = 0;
421 this.trigger('info', { 420 this.trigger('info', {
...@@ -435,7 +434,7 @@ ...@@ -435,7 +434,7 @@
435 this.manifest.segments = uris; 434 this.manifest.segments = uris;
436 435
437 }, 436 },
438 'key': function() { 437 key() {
439 if (!entry.attributes) { 438 if (!entry.attributes) {
440 this.trigger('warn', { 439 this.trigger('warn', {
441 message: 'ignoring key declaration without attribute list' 440 message: 'ignoring key declaration without attribute list'
...@@ -465,11 +464,11 @@ ...@@ -465,11 +464,11 @@
465 uri: entry.attributes.URI 464 uri: entry.attributes.URI
466 }; 465 };
467 466
468 if (entry.attributes.IV !== undefined) { 467 if (typeof entry.attributes.IV !== 'undefined') {
469 key.iv = entry.attributes.IV; 468 key.iv = entry.attributes.IV;
470 } 469 }
471 }, 470 },
472 'media-sequence': function() { 471 'media-sequence'() {
473 if (!isFinite(entry.number)) { 472 if (!isFinite(entry.number)) {
474 this.trigger('warn', { 473 this.trigger('warn', {
475 message: 'ignoring invalid media sequence: ' + entry.number 474 message: 'ignoring invalid media sequence: ' + entry.number
...@@ -478,7 +477,7 @@ ...@@ -478,7 +477,7 @@
478 } 477 }
479 this.manifest.mediaSequence = entry.number; 478 this.manifest.mediaSequence = entry.number;
480 }, 479 },
481 'discontinuity-sequence': function() { 480 'discontinuity-sequence'() {
482 if (!isFinite(entry.number)) { 481 if (!isFinite(entry.number)) {
483 this.trigger('warn', { 482 this.trigger('warn', {
484 message: 'ignoring invalid discontinuity sequence: ' + entry.number 483 message: 'ignoring invalid discontinuity sequence: ' + entry.number
...@@ -487,7 +486,7 @@ ...@@ -487,7 +486,7 @@
487 } 486 }
488 this.manifest.discontinuitySequence = entry.number; 487 this.manifest.discontinuitySequence = entry.number;
489 }, 488 },
490 'playlist-type': function() { 489 'playlist-type'() {
491 if (!(/VOD|EVENT/).test(entry.playlistType)) { 490 if (!(/VOD|EVENT/).test(entry.playlistType)) {
492 this.trigger('warn', { 491 this.trigger('warn', {
493 message: 'ignoring unknown playlist type: ' + entry.playlist 492 message: 'ignoring unknown playlist type: ' + entry.playlist
...@@ -496,7 +495,7 @@ ...@@ -496,7 +495,7 @@
496 } 495 }
497 this.manifest.playlistType = entry.playlistType; 496 this.manifest.playlistType = entry.playlistType;
498 }, 497 },
499 'stream-inf': function() { 498 'stream-inf'() {
500 this.manifest.playlists = uris; 499 this.manifest.playlists = uris;
501 500
502 if (!entry.attributes) { 501 if (!entry.attributes) {
...@@ -512,11 +511,11 @@ ...@@ -512,11 +511,11 @@
512 currentUri.attributes = mergeOptions(currentUri.attributes, 511 currentUri.attributes = mergeOptions(currentUri.attributes,
513 entry.attributes); 512 entry.attributes);
514 }, 513 },
515 'discontinuity': function() { 514 discontinuity() {
516 currentUri.discontinuity = true; 515 currentUri.discontinuity = true;
517 this.manifest.discontinuityStarts.push(uris.length); 516 this.manifest.discontinuityStarts.push(uris.length);
518 }, 517 },
519 'targetduration': function() { 518 targetduration() {
520 if (!isFinite(entry.duration) || entry.duration < 0) { 519 if (!isFinite(entry.duration) || entry.duration < 0) {
521 this.trigger('warn', { 520 this.trigger('warn', {
522 message: 'ignoring invalid target duration: ' + entry.duration 521 message: 'ignoring invalid target duration: ' + entry.duration
...@@ -525,7 +524,7 @@ ...@@ -525,7 +524,7 @@
525 } 524 }
526 this.manifest.targetDuration = entry.duration; 525 this.manifest.targetDuration = entry.duration;
527 }, 526 },
528 'totalduration': function() { 527 totalduration() {
529 if (!isFinite(entry.duration) || entry.duration < 0) { 528 if (!isFinite(entry.duration) || entry.duration < 0) {
530 this.trigger('warn', { 529 this.trigger('warn', {
531 message: 'ignoring invalid total duration: ' + entry.duration 530 message: 'ignoring invalid total duration: ' + entry.duration
...@@ -536,7 +535,7 @@ ...@@ -536,7 +535,7 @@
536 } 535 }
537 })[entry.tagType] || noop).call(self); 536 })[entry.tagType] || noop).call(self);
538 }, 537 },
539 uri: function() { 538 uri() {
540 currentUri.uri = entry.uri; 539 currentUri.uri = entry.uri;
541 uris.push(currentUri); 540 uris.push(currentUri);
542 541
...@@ -556,33 +555,36 @@ ...@@ -556,33 +555,36 @@
556 // prepare for the next URI 555 // prepare for the next URI
557 currentUri = {}; 556 currentUri = {};
558 }, 557 },
559 comment: function() { 558 comment() {
560 // comments are not important for playback 559 // comments are not important for playback
561 } 560 }
562 })[entry.type].call(self); 561 })[entry.type].call(self);
563 }); 562 });
564 }; 563
565 Parser.prototype = new Stream(); 564 }
565
566 /** 566 /**
567 * Parse the input string and update the manifest object. 567 * Parse the input string and update the manifest object.
568 * @param chunk {string} a potentially incomplete portion of the manifest 568 * @param chunk {string} a potentially incomplete portion of the manifest
569 */ 569 */
570 Parser.prototype.push = function(chunk) { 570 push(chunk) {
571 this.lineStream.push(chunk); 571 this.lineStream.push(chunk);
572 }; 572 }
573
573 /** 574 /**
574 * Flush any remaining input. This can be handy if the last line of an M3U8 575 * Flush any remaining input. This can be handy if the last line of an M3U8
575 * manifest did not contain a trailing newline but the file has been 576 * manifest did not contain a trailing newline but the file has been
576 * completely received. 577 * completely received.
577 */ 578 */
578 Parser.prototype.end = function() { 579 end() {
579 // flush any buffered input 580 // flush any buffered input
580 this.lineStream.push('\n'); 581 this.lineStream.push('\n');
581 }; 582 }
582 583
583 window.videojs.m3u8 = { 584 }
584 LineStream: LineStream, 585
585 ParseStream: ParseStream, 586 export default {
586 Parser: Parser 587 LineStream,
587 }; 588 ParseStream,
588 })(window.videojs, window.parseInt, window.isFinite, window.videojs.mergeOptions); 589 Parser
590 };
......
...@@ -2,73 +2,84 @@ ...@@ -2,73 +2,84 @@
2 * A lightweight readable stream implemention that handles event dispatching. 2 * A lightweight readable stream implemention that handles event dispatching.
3 * Objects that inherit from streams should call init in their constructors. 3 * Objects that inherit from streams should call init in their constructors.
4 */ 4 */
5 (function(videojs, undefined) { 5 export default class Stream {
6 var Stream = function() { 6 constructor() {
7 this.init = function() { 7 this.init();
8 var listeners = {}; 8 }
9 /** 9
10 * Add a listener for a specified event type. 10 init() {
11 * @param type {string} the event name 11 this.listeners = {};
12 * @param listener {function} the callback to be invoked when an event of 12 }
13 * the specified type occurs 13
14 */ 14 /**
15 this.on = function(type, listener) { 15 * Add a listener for a specified event type.
16 if (!listeners[type]) { 16 * @param type {string} the event name
17 listeners[type] = []; 17 * @param listener {function} the callback to be invoked when an event of
18 } 18 * the specified type occurs
19 listeners[type].push(listener); 19 */
20 }; 20 on(type, listener) {
21 /** 21 if (!this.listeners[type]) {
22 * Remove a listener for a specified event type. 22 this.listeners[type] = [];
23 * @param type {string} the event name 23 }
24 * @param listener {function} a function previously registered for this 24 this.listeners[type].push(listener);
25 * type of event through `on` 25 }
26 */ 26
27 this.off = function(type, listener) { 27 /**
28 var index; 28 * Remove a listener for a specified event type.
29 if (!listeners[type]) { 29 * @param type {string} the event name
30 return false; 30 * @param listener {function} a function previously registered for this
31 } 31 * type of event through `on`
32 index = listeners[type].indexOf(listener); 32 */
33 listeners[type].splice(index, 1); 33 off(type, listener) {
34 return index > -1; 34 let index;
35 }; 35
36 /** 36 if (!this.listeners[type]) {
37 * Trigger an event of the specified type on this stream. Any additional 37 return false;
38 * arguments to this function are passed as parameters to event listeners. 38 }
39 * @param type {string} the event name 39 index = this.listeners[type].indexOf(listener);
40 */ 40 this.listeners[type].splice(index, 1);
41 this.trigger = function(type) { 41 return index > -1;
42 var callbacks, i, length, args; 42 }
43 callbacks = listeners[type]; 43
44 if (!callbacks) { 44 /**
45 return; 45 * Trigger an event of the specified type on this stream. Any additional
46 } 46 * arguments to this function are passed as parameters to event listeners.
47 // Slicing the arguments on every invocation of this method 47 * @param type {string} the event name
48 // can add a significant amount of overhead. Avoid the 48 */
49 // intermediate object creation for the common case of a 49 trigger(type) {
50 // single callback argument 50 let callbacks;
51 if (arguments.length === 2) { 51 let i;
52 length = callbacks.length; 52 let length;
53 for (i = 0; i < length; ++i) { 53 let args;
54 callbacks[i].call(this, arguments[1]); 54
55 } 55 callbacks = this.listeners[type];
56 } else { 56 if (!callbacks) {
57 args = Array.prototype.slice.call(arguments, 1); 57 return;
58 length = callbacks.length; 58 }
59 for (i = 0; i < length; ++i) { 59 // Slicing the arguments on every invocation of this method
60 callbacks[i].apply(this, args); 60 // can add a significant amount of overhead. Avoid the
61 } 61 // intermediate object creation for the common case of a
62 } 62 // single callback argument
63 }; 63 if (arguments.length === 2) {
64 /** 64 length = callbacks.length;
65 * Destroys the stream and cleans up. 65 for (i = 0; i < length; ++i) {
66 */ 66 callbacks[i].call(this, arguments[1]);
67 this.dispose = function() { 67 }
68 listeners = {}; 68 } else {
69 }; 69 args = Array.prototype.slice.call(arguments, 1);
70 }; 70 length = callbacks.length;
71 }; 71 for (i = 0; i < length; ++i) {
72 callbacks[i].apply(this, args);
73 }
74 }
75 }
76
77 /**
78 * Destroys the stream and cleans up.
79 */
80 dispose() {
81 this.listeners = {};
82 }
72 /** 83 /**
73 * Forwards all `data` events on this stream to the destination stream. The 84 * Forwards all `data` events on this stream to the destination stream. The
74 * destination stream should provide a method `push` to receive the data 85 * destination stream should provide a method `push` to receive the data
...@@ -76,11 +87,9 @@ ...@@ -76,11 +87,9 @@
76 * @param destination {stream} the stream that will receive all `data` events 87 * @param destination {stream} the stream that will receive all `data` events
77 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 88 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options
78 */ 89 */
79 Stream.prototype.pipe = function(destination) { 90 pipe(destination) {
80 this.on('data', function(data) { 91 this.on('data', function(data) {
81 destination.push(data); 92 destination.push(data);
82 }); 93 });
83 }; 94 }
84 95 }
85 videojs.Hls.Stream = Stream;
86 })(window.videojs);
......
1 import m3u8 from './m3u8';
2 import Stream from './stream';
3 import videojs from 'video.js';
4
5 if(typeof window.videojs.Hls === 'undefined') {
6 videojs.Hls = {};
7 }
8 videojs.Hls.Stream = Stream;
9 videojs.m3u8 = m3u8;
10
...@@ -16,22 +16,16 @@ ...@@ -16,22 +16,16 @@
16 <script src="/node_modules/video.js/dist/video.js"></script> 16 <script src="/node_modules/video.js/dist/video.js"></script>
17 <script src="/node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script> 17 <script src="/node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script>
18 18
19 <script src="/src/videojs-hls.js"></script> 19 <script src="/src/videojs-contrib-hls.js"></script>
20 <script src="/src/xhr.js"></script> 20 <script src="/src/xhr.js"></script>
21 <script src="/src/stream.js"></script> 21 <script src="/dist/videojs-contrib-hls.js"></script>
22 <script src="/src/m3u8/m3u8-parser.js"></script>
23 <script src="/src/playlist.js"></script> 22 <script src="/src/playlist.js"></script>
24 <script src="/src/playlist-loader.js"></script> 23 <script src="/src/playlist-loader.js"></script>
25 <script src="/src/decrypter.js"></script> 24 <script src="/src/decrypter.js"></script>
26 <script src="/src/bin-utils.js"></script> 25 <script src="/src/bin-utils.js"></script>
27 26
28 <script src="/test/data/manifests.js"></script> 27 <script src="/test/videojs-contrib-hls.test.js"></script>
29 <script src="/test/data/expected.js"></script> 28 <script src="/dist-test/videojs-contrib-hls.js"></script>
30 <script src="/test/data/ts-segment-bc.js"></script>
31
32
33 <script src="/test/videojs-hls.test.js"></script>
34 <script src="/test/m3u8.test.js"></script>
35 <script src="/test/playlist.test.js"></script> 29 <script src="/test/playlist.test.js"></script>
36 <script src="/test/playlist-loader.test.js"></script> 30 <script src="/test/playlist-loader.test.js"></script>
37 <script src="/test/decrypter.test.js"></script> 31 <script src="/test/decrypter.test.js"></script>
......
...@@ -2,8 +2,7 @@ var merge = require('lodash-compat/object/merge'); ...@@ -2,8 +2,7 @@ var merge = require('lodash-compat/object/merge');
2 2
3 var DEFAULTS = { 3 var DEFAULTS = {
4 basePath: '../..', 4 basePath: '../..',
5 //frameworks: ['browserify', 'qunit'], 5 frameworks: ['browserify', 'qunit'],
6 frameworks: ['qunit'],
7 6
8 7
9 files: [ 8 files: [
...@@ -16,20 +15,19 @@ var DEFAULTS = { ...@@ -16,20 +15,19 @@ var DEFAULTS = {
16 'node_modules/pkcs7/dist/pkcs7.unpad.js', 15 'node_modules/pkcs7/dist/pkcs7.unpad.js',
17 'node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js', 16 'node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js',
18 17
19 'src/videojs-hls.js', 18 // these two stub old functionality
19 'src/videojs-contrib-hls.js',
20 'src/xhr.js', 20 'src/xhr.js',
21 'src/stream.js', 21 'dist/videojs-contrib-hls.js',
22 'src/m3u8/m3u8-parser.js', 22
23 'src/playlist.js', 23 'src/playlist.js',
24 'src/playlist-loader.js', 24 'src/playlist-loader.js',
25 'src/decrypter.js', 25 'src/decrypter.js',
26 'src/bin-utils.js', 26 'src/bin-utils.js',
27 27
28 'test/data/manifests.js', 28 'test/stub.test.js',
29 'test/data/expected.js',
30 'test/data/ts-segment-bc.js',
31 29
32 'test/videojs-hls.test.js', 30 'test/videojs-contrib-hls.test.js',
33 'test/m3u8.test.js', 31 'test/m3u8.test.js',
34 'test/playlist.test.js', 32 'test/playlist.test.js',
35 'test/playlist-loader.test.js', 33 'test/playlist-loader.test.js',
...@@ -44,12 +42,12 @@ var DEFAULTS = { ...@@ -44,12 +42,12 @@ var DEFAULTS = {
44 ], 42 ],
45 43
46 plugins: [ 44 plugins: [
47 // 'karma-browserify', 45 'karma-browserify',
48 'karma-qunit' 46 'karma-qunit'
49 ], 47 ],
50 48
51 preprocessors: { 49 preprocessors: {
52 // 'test/**/*.js': ['browserify'] 50 'test/{stub,m3u8}.test.js': ['browserify']
53 }, 51 },
54 52
55 reporters: ['dots'], 53 reporters: ['dots'],
...@@ -59,18 +57,16 @@ var DEFAULTS = { ...@@ -59,18 +57,16 @@ var DEFAULTS = {
59 singleRun: true, 57 singleRun: true,
60 concurrency: Infinity, 58 concurrency: Infinity,
61 59
62 /*
63 browserify: { 60 browserify: {
64 debug: true, 61 debug: true,
65 transform: [ 62 transform: [
66 'babelify', 63 'babelify',
67 'browserify-shim' 64 'browserify-shim'
68 ], 65 ],
69 noparse: [ 66 noParse: [
70 'test/data/**', 67 'test/data/**',
71 ] 68 ]
72 } 69 }
73 */
74 }; 70 };
75 71
76 /** 72 /**
......
...@@ -29,6 +29,7 @@ module.exports = function(config) { ...@@ -29,6 +29,7 @@ module.exports = function(config) {
29 postDetection: function(availableBrowsers) { 29 postDetection: function(availableBrowsers) {
30 var safariIndex = availableBrowsers.indexOf('Safari'); 30 var safariIndex = availableBrowsers.indexOf('Safari');
31 if(safariIndex !== -1) { 31 if(safariIndex !== -1) {
32 console.log("Not running safari it is/was broken");
32 availableBrowsers.splice(safariIndex, 1); 33 availableBrowsers.splice(safariIndex, 1);
33 } 34 }
34 return availableBrowsers; 35 return availableBrowsers;
......
1 (function(window, undefined) { 1 import {ParseStream, LineStream, Parser} from '../src/m3u8';
2 var 2 import QUnit from 'qunit';
3 //manifestController = this.manifestController, 3 import testDataExpected from './test-expected.js';
4 m3u8 = window.videojs.m3u8, 4 import testDataManifests from './test-manifests.js';
5 ParseStream = m3u8.ParseStream, 5
6 parseStream, 6 QUnit.module('LineStream', {
7 LineStream = m3u8.LineStream, 7 beforeEach() {
8 lineStream, 8 this.lineStream = new LineStream();
9 Parser = m3u8.Parser, 9 }
10 parser; 10 });
11 11 QUnit.test('empty inputs produce no tokens', function() {
12 /* 12 let data = false;
13 M3U8 Test Suite 13
14 */ 14 this.lineStream.on('data', function() {
15 15 data = true;
16 QUnit.module('LineStream', { 16 });
17 setup: function() { 17 this.lineStream.push('');
18 lineStream = new LineStream(); 18 QUnit.ok(!data, 'no tokens were produced');
19 });
20 QUnit.test('splits on newlines', function() {
21 let lines = [];
22
23 this.lineStream.on('data', function(line) {
24 lines.push(line);
25 });
26 this.lineStream.push('#EXTM3U\nmovie.ts\n');
27
28 QUnit.strictEqual(2, lines.length, 'two lines are ready');
29 QUnit.strictEqual('#EXTM3U', lines.shift(), 'the first line is the first token');
30 QUnit.strictEqual('movie.ts', lines.shift(), 'the second line is the second token');
31 });
32 QUnit.test('empty lines become empty strings', function() {
33 let lines = [];
34
35 this.lineStream.on('data', function(line) {
36 lines.push(line);
37 });
38 this.lineStream.push('\n\n');
39
40 QUnit.strictEqual(2, lines.length, 'two lines are ready');
41 QUnit.strictEqual('', lines.shift(), 'the first line is empty');
42 QUnit.strictEqual('', lines.shift(), 'the second line is empty');
43 });
44 QUnit.test('handles lines broken across appends', function() {
45 let lines = [];
46
47 this.lineStream.on('data', function(line) {
48 lines.push(line);
49 });
50 this.lineStream.push('#EXTM');
51 QUnit.strictEqual(0, lines.length, 'no lines are ready');
52
53 this.lineStream.push('3U\nmovie.ts\n');
54 QUnit.strictEqual(2, lines.length, 'two lines are ready');
55 QUnit.strictEqual('#EXTM3U', lines.shift(), 'the first line is the first token');
56 QUnit.strictEqual('movie.ts', lines.shift(), 'the second line is the second token');
57 });
58 QUnit.test('stops sending events after deregistering', function() {
59 let temporaryLines = [];
60 let temporary = function(line) {
61 temporaryLines.push(line);
62 };
63 let permanentLines = [];
64 let permanent = function(line) {
65 permanentLines.push(line);
66 };
67
68 this.lineStream.on('data', temporary);
69 this.lineStream.on('data', permanent);
70 this.lineStream.push('line one\n');
71 QUnit.strictEqual(temporaryLines.length,
72 permanentLines.length,
73 'both callbacks receive the event');
74
75 QUnit.ok(this.lineStream.off('data', temporary), 'a listener was removed');
76 this.lineStream.push('line two\n');
77 QUnit.strictEqual(1, temporaryLines.length, 'no new events are received');
78 QUnit.strictEqual(2, permanentLines.length, 'new events are still received');
79 });
80
81 QUnit.module('ParseStream', {
82 beforeEach() {
83 this.lineStream = new LineStream();
84 this.parseStream = new ParseStream();
85 this.lineStream.pipe(this.parseStream);
86 }
87 });
88 QUnit.test('parses comment lines', function() {
89 let manifest = '# a line that starts with a hash mark without "EXT" is a comment\n';
90 let element;
91
92 this.parseStream.on('data', function(elem) {
93 element = elem;
94 });
95 this.lineStream.push(manifest);
96
97 QUnit.ok(element, 'an event was triggered');
98 QUnit.strictEqual(element.type, 'comment', 'the type is comment');
99 QUnit.strictEqual(element.text,
100 manifest.slice(1, manifest.length - 1),
101 'the comment text is parsed');
102 });
103 QUnit.test('parses uri lines', function() {
104 let manifest = 'any non-blank line that does not start with a hash-mark is a URI\n';
105 let element;
106
107 this.parseStream.on('data', function(elem) {
108 element = elem;
109 });
110 this.lineStream.push(manifest);
111
112 QUnit.ok(element, 'an event was triggered');
113 QUnit.strictEqual(element.type, 'uri', 'the type is uri');
114 QUnit.strictEqual(element.uri,
115 manifest.substring(0, manifest.length - 1),
116 'the uri text is parsed');
117 });
118 QUnit.test('parses unknown tag types', function() {
119 let manifest = '#EXT-X-EXAMPLE-TAG:some,additional,stuff\n';
120 let element;
121
122 this.parseStream.on('data', function(elem) {
123 element = elem;
124 });
125 this.lineStream.push(manifest);
126
127 QUnit.ok(element, 'an event was triggered');
128 QUnit.strictEqual(element.type, 'tag', 'the type is tag');
129 QUnit.strictEqual(element.data,
130 manifest.slice(4, manifest.length - 1),
131 'unknown tag data is preserved');
132 });
133
134 // #EXTM3U
135 QUnit.test('parses #EXTM3U tags', function() {
136 let manifest = '#EXTM3U\n';
137 let element;
138
139 this.parseStream.on('data', function(elem) {
140 element = elem;
141 });
142 this.lineStream.push(manifest);
143
144 QUnit.ok(element, 'an event was triggered');
145 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
146 QUnit.strictEqual(element.tagType, 'm3u', 'the tag type is m3u');
147 });
148
149 // #EXTINF
150 QUnit.test('parses minimal #EXTINF tags', function() {
151 let manifest = '#EXTINF\n';
152 let element;
153
154 this.parseStream.on('data', function(elem) {
155 element = elem;
156 });
157 this.lineStream.push(manifest);
158
159 QUnit.ok(element, 'an event was triggered');
160 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
161 QUnit.strictEqual(element.tagType, 'inf', 'the tag type is inf');
162 });
163 QUnit.test('parses #EXTINF tags with durations', function() {
164 let manifest = '#EXTINF:15\n';
165 let element;
166
167 this.parseStream.on('data', function(elem) {
168 element = elem;
169 });
170 this.lineStream.push(manifest);
171
172 QUnit.ok(element, 'an event was triggered');
173 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
174 QUnit.strictEqual(element.tagType, 'inf', 'the tag type is inf');
175 QUnit.strictEqual(element.duration, 15, 'the duration is parsed');
176 QUnit.ok(!('title' in element), 'no title is parsed');
177
178 manifest = '#EXTINF:21,\n';
179 this.lineStream.push(manifest);
180
181 QUnit.ok(element, 'an event was triggered');
182 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
183 QUnit.strictEqual(element.tagType, 'inf', 'the tag type is inf');
184 QUnit.strictEqual(element.duration, 21, 'the duration is parsed');
185 QUnit.ok(!('title' in element), 'no title is parsed');
186 });
187 QUnit.test('parses #EXTINF tags with a duration and title', function() {
188 let manifest = '#EXTINF:13,Does anyone really use the title attribute?\n';
189 let element;
190
191 this.parseStream.on('data', function(elem) {
192 element = elem;
193 });
194 this.lineStream.push(manifest);
195
196 QUnit.ok(element, 'an event was triggered');
197 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
198 QUnit.strictEqual(element.tagType, 'inf', 'the tag type is inf');
199 QUnit.strictEqual(element.duration, 13, 'the duration is parsed');
200 QUnit.strictEqual(element.title,
201 manifest.substring(manifest.indexOf(',') + 1, manifest.length - 1),
202 'the title is parsed');
203 });
204 QUnit.test('parses #EXTINF tags with carriage returns', function() {
205 let manifest = '#EXTINF:13,Does anyone really use the title attribute?\r\n';
206 let element;
207
208 this.parseStream.on('data', function(elem) {
209 element = elem;
210 });
211 this.lineStream.push(manifest);
212
213 QUnit.ok(element, 'an event was triggered');
214 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
215 QUnit.strictEqual(element.tagType, 'inf', 'the tag type is inf');
216 QUnit.strictEqual(element.duration, 13, 'the duration is parsed');
217 QUnit.strictEqual(element.title,
218 manifest.substring(manifest.indexOf(',') + 1, manifest.length - 2),
219 'the title is parsed');
220 });
221
222 // #EXT-X-TARGETDURATION
223 QUnit.test('parses minimal #EXT-X-TARGETDURATION tags', function() {
224 let manifest = '#EXT-X-TARGETDURATION\n';
225 let element;
226
227 this.parseStream.on('data', function(elem) {
228 element = elem;
229 });
230 this.lineStream.push(manifest);
231
232 QUnit.ok(element, 'an event was triggered');
233 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
234 QUnit.strictEqual(element.tagType, 'targetduration', 'the tag type is targetduration');
235 QUnit.ok(!('duration' in element), 'no duration is parsed');
236 });
237 QUnit.test('parses #EXT-X-TARGETDURATION with duration', function() {
238 let manifest = '#EXT-X-TARGETDURATION:47\n';
239 let element;
240
241 this.parseStream.on('data', function(elem) {
242 element = elem;
243 });
244 this.lineStream.push(manifest);
245
246 QUnit.ok(element, 'an event was triggered');
247 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
248 QUnit.strictEqual(element.tagType, 'targetduration', 'the tag type is targetduration');
249 QUnit.strictEqual(element.duration, 47, 'the duration is parsed');
250 });
251
252 // #EXT-X-VERSION
253 QUnit.test('parses minimal #EXT-X-VERSION tags', function() {
254 let manifest = '#EXT-X-VERSION:\n';
255 let element;
256
257 this.parseStream.on('data', function(elem) {
258 element = elem;
259 });
260 this.lineStream.push(manifest);
261
262 QUnit.ok(element, 'an event was triggered');
263 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
264 QUnit.strictEqual(element.tagType, 'version', 'the tag type is version');
265 QUnit.ok(!('version' in element), 'no version is present');
266 });
267 QUnit.test('parses #EXT-X-VERSION with a version', function() {
268 let manifest = '#EXT-X-VERSION:99\n';
269 let element;
270
271 this.parseStream.on('data', function(elem) {
272 element = elem;
273 });
274 this.lineStream.push(manifest);
275
276 QUnit.ok(element, 'an event was triggered');
277 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
278 QUnit.strictEqual(element.tagType, 'version', 'the tag type is version');
279 QUnit.strictEqual(element.version, 99, 'the version is parsed');
280 });
281
282 // #EXT-X-MEDIA-SEQUENCE
283 QUnit.test('parses minimal #EXT-X-MEDIA-SEQUENCE tags', function() {
284 let manifest = '#EXT-X-MEDIA-SEQUENCE\n';
285 let element;
286
287 this.parseStream.on('data', function(elem) {
288 element = elem;
289 });
290 this.lineStream.push(manifest);
291
292 QUnit.ok(element, 'an event was triggered');
293 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
294 QUnit.strictEqual(element.tagType, 'media-sequence', 'the tag type is media-sequence');
295 QUnit.ok(!('number' in element), 'no number is present');
296 });
297 QUnit.test('parses #EXT-X-MEDIA-SEQUENCE with sequence numbers', function() {
298 let manifest = '#EXT-X-MEDIA-SEQUENCE:109\n';
299 let element;
300
301 this.parseStream.on('data', function(elem) {
302 element = elem;
303 });
304 this.lineStream.push(manifest);
305
306 QUnit.ok(element, 'an event was triggered');
307 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
308 QUnit.strictEqual(element.tagType, 'media-sequence', 'the tag type is media-sequence');
309 QUnit.ok(element.number, 109, 'the number is parsed');
310 });
311
312 // #EXT-X-PLAYLIST-TYPE
313 QUnit.test('parses minimal #EXT-X-PLAYLIST-TYPE tags', function() {
314 let manifest = '#EXT-X-PLAYLIST-TYPE:\n';
315 let element;
316
317 this.parseStream.on('data', function(elem) {
318 element = elem;
319 });
320 this.lineStream.push(manifest);
321
322 QUnit.ok(element, 'an event was triggered');
323 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
324 QUnit.strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
325 QUnit.ok(!('playlistType' in element), 'no playlist type is present');
326 });
327 QUnit.test('parses #EXT-X-PLAYLIST-TYPE with mutability info', function() {
328 let manifest = '#EXT-X-PLAYLIST-TYPE:EVENT\n';
329 let element;
330
331 this.parseStream.on('data', function(elem) {
332 element = elem;
333 });
334 this.lineStream.push(manifest);
335
336 QUnit.ok(element, 'an event was triggered');
337 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
338 QUnit.strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
339 QUnit.strictEqual(element.playlistType, 'EVENT', 'the playlist type is EVENT');
340
341 manifest = '#EXT-X-PLAYLIST-TYPE:VOD\n';
342 this.lineStream.push(manifest);
343 QUnit.ok(element, 'an event was triggered');
344 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
345 QUnit.strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
346 QUnit.strictEqual(element.playlistType, 'VOD', 'the playlist type is VOD');
347
348 manifest = '#EXT-X-PLAYLIST-TYPE:nonsense\n';
349 this.lineStream.push(manifest);
350 QUnit.ok(element, 'an event was triggered');
351 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
352 QUnit.strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
353 QUnit.strictEqual(element.playlistType, 'nonsense', 'the playlist type is parsed');
354 });
355
356 // #EXT-X-BYTERANGE
357 QUnit.test('parses minimal #EXT-X-BYTERANGE tags', function() {
358 let manifest = '#EXT-X-BYTERANGE\n';
359 let element;
360
361 this.parseStream.on('data', function(elem) {
362 element = elem;
363 });
364 this.lineStream.push(manifest);
365
366 QUnit.ok(element, 'an event was triggered');
367 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
368 QUnit.strictEqual(element.tagType, 'byterange', 'the tag type is byterange');
369 QUnit.ok(!('length' in element), 'no length is present');
370 QUnit.ok(!('offset' in element), 'no offset is present');
371 });
372 QUnit.test('parses #EXT-X-BYTERANGE with length and offset', function() {
373 let manifest = '#EXT-X-BYTERANGE:45\n';
374 let element;
375
376 this.parseStream.on('data', function(elem) {
377 element = elem;
378 });
379 this.lineStream.push(manifest);
380
381 QUnit.ok(element, 'an event was triggered');
382 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
383 QUnit.strictEqual(element.tagType, 'byterange', 'the tag type is byterange');
384 QUnit.strictEqual(element.length, 45, 'length is parsed');
385 QUnit.ok(!('offset' in element), 'no offset is present');
386
387 manifest = '#EXT-X-BYTERANGE:108@16\n';
388 this.lineStream.push(manifest);
389 QUnit.ok(element, 'an event was triggered');
390 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
391 QUnit.strictEqual(element.tagType, 'byterange', 'the tag type is byterange');
392 QUnit.strictEqual(element.length, 108, 'length is parsed');
393 QUnit.strictEqual(element.offset, 16, 'offset is parsed');
394 });
395
396 // #EXT-X-ALLOW-CACHE
397 QUnit.test('parses minimal #EXT-X-ALLOW-CACHE tags', function() {
398 let manifest = '#EXT-X-ALLOW-CACHE:\n';
399 let element;
400
401 this.parseStream.on('data', function(elem) {
402 element = elem;
403 });
404 this.lineStream.push(manifest);
405
406 QUnit.ok(element, 'an event was triggered');
407 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
408 QUnit.strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache');
409 QUnit.ok(!('allowed' in element), 'no allowed is present');
410 });
411 QUnit.test('parses valid #EXT-X-ALLOW-CACHE tags', function() {
412 let manifest = '#EXT-X-ALLOW-CACHE:YES\n';
413 let element;
414
415 this.parseStream.on('data', function(elem) {
416 element = elem;
417 });
418 this.lineStream.push(manifest);
419
420 QUnit.ok(element, 'an event was triggered');
421 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
422 QUnit.strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache');
423 QUnit.ok(element.allowed, 'allowed is parsed');
424
425 manifest = '#EXT-X-ALLOW-CACHE:NO\n';
426 this.lineStream.push(manifest);
427
428 QUnit.ok(element, 'an event was triggered');
429 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
430 QUnit.strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache');
431 QUnit.ok(!element.allowed, 'allowed is parsed');
432 });
433 // #EXT-X-STREAM-INF
434 QUnit.test('parses minimal #EXT-X-STREAM-INF tags', function() {
435 let manifest = '#EXT-X-STREAM-INF\n';
436 let element;
437
438 this.parseStream.on('data', function(elem) {
439 element = elem;
440 });
441 this.lineStream.push(manifest);
442
443 QUnit.ok(element, 'an event was triggered');
444 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
445 QUnit.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
446 QUnit.ok(!('attributes' in element), 'no attributes are present');
447 });
448 QUnit.test('parses #EXT-X-STREAM-INF with common attributes', function() {
449 let manifest = '#EXT-X-STREAM-INF:BANDWIDTH=14400\n';
450 let element;
451
452 this.parseStream.on('data', function(elem) {
453 element = elem;
454 });
455 this.lineStream.push(manifest);
456
457 QUnit.ok(element, 'an event was triggered');
458 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
459 QUnit.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
460 QUnit.strictEqual(element.attributes.BANDWIDTH, 14400, 'bandwidth is parsed');
461
462 manifest = '#EXT-X-STREAM-INF:PROGRAM-ID=7\n';
463 this.lineStream.push(manifest);
464
465 QUnit.ok(element, 'an event was triggered');
466 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
467 QUnit.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
468 QUnit.strictEqual(element.attributes['PROGRAM-ID'], 7, 'program-id is parsed');
469
470 manifest = '#EXT-X-STREAM-INF:RESOLUTION=396x224\n';
471 this.lineStream.push(manifest);
472
473 QUnit.ok(element, 'an event was triggered');
474 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
475 QUnit.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
476 QUnit.strictEqual(element.attributes.RESOLUTION.width, 396, 'width is parsed');
477 QUnit.strictEqual(element.attributes.RESOLUTION.height, 224, 'heigth is parsed');
478
479 manifest = '#EXT-X-STREAM-INF:CODECS="avc1.4d400d, mp4a.40.2"\n';
480 this.lineStream.push(manifest);
481
482 QUnit.ok(element, 'an event was triggered');
483 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
484 QUnit.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
485 QUnit.strictEqual(element.attributes.CODECS,
486 'avc1.4d400d, mp4a.40.2',
487 'codecs are parsed');
488 });
489 QUnit.test('parses #EXT-X-STREAM-INF with arbitrary attributes', function() {
490 let manifest = '#EXT-X-STREAM-INF:NUMERIC=24,ALPHA=Value,MIXED=123abc\n';
491 let element;
492
493 this.parseStream.on('data', function(elem) {
494 element = elem;
495 });
496 this.lineStream.push(manifest);
497
498 QUnit.ok(element, 'an event was triggered');
499 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
500 QUnit.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
501 QUnit.strictEqual(element.attributes.NUMERIC, '24', 'numeric attributes are parsed');
502 QUnit.strictEqual(element.attributes.ALPHA,
503 'Value',
504 'alphabetic attributes are parsed');
505 QUnit.strictEqual(element.attributes.MIXED, '123abc', 'mixed attributes are parsed');
506 });
507 // #EXT-X-ENDLIST
508 QUnit.test('parses #EXT-X-ENDLIST tags', function() {
509 let manifest = '#EXT-X-ENDLIST\n';
510 let element;
511
512 this.parseStream.on('data', function(elem) {
513 element = elem;
514 });
515 this.lineStream.push(manifest);
516
517 QUnit.ok(element, 'an event was triggered');
518 QUnit.strictEqual(element.type, 'tag', 'the line type is tag');
519 QUnit.strictEqual(element.tagType, 'endlist', 'the tag type is stream-inf');
520 });
521
522 // #EXT-X-KEY
523 QUnit.test('parses valid #EXT-X-KEY tags', function() {
524 let manifest =
525 '#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"\n';
526 let element;
527
528 this.parseStream.on('data', function(elem) {
529 element = elem;
530 });
531 this.lineStream.push(manifest);
532
533 QUnit.ok(element, 'an event was triggered');
534 QUnit.deepEqual(element, {
535 type: 'tag',
536 tagType: 'key',
537 attributes: {
538 METHOD: 'AES-128',
539 URI: 'https://priv.example.com/key.php?r=52'
19 } 540 }
20 }); 541 }, 'parsed a valid key');
21 test('empty inputs produce no tokens', function() { 542
22 var data = false; 543 manifest = '#EXT-X-KEY:URI="https://example.com/key#1",METHOD=FutureType-1024\n';
23 lineStream.on('data', function() { 544 this.lineStream.push(manifest);
24 data = true; 545 QUnit.ok(element, 'an event was triggered');
25 }); 546 QUnit.deepEqual(element, {
26 lineStream.push(''); 547 type: 'tag',
27 ok(!data, 'no tokens were produced'); 548 tagType: 'key',
28 }); 549 attributes: {
29 test('splits on newlines', function() { 550 METHOD: 'FutureType-1024',
30 var lines = []; 551 URI: 'https://example.com/key#1'
31 lineStream.on('data', function(line) {
32 lines.push(line);
33 });
34 lineStream.push('#EXTM3U\nmovie.ts\n');
35
36 strictEqual(2, lines.length, 'two lines are ready');
37 strictEqual('#EXTM3U', lines.shift(), 'the first line is the first token');
38 strictEqual('movie.ts', lines.shift(), 'the second line is the second token');
39 });
40 test('empty lines become empty strings', function() {
41 var lines = [];
42 lineStream.on('data', function(line) {
43 lines.push(line);
44 });
45 lineStream.push('\n\n');
46
47 strictEqual(2, lines.length, 'two lines are ready');
48 strictEqual('', lines.shift(), 'the first line is empty');
49 strictEqual('', lines.shift(), 'the second line is empty');
50 });
51 test('handles lines broken across appends', function() {
52 var lines = [];
53 lineStream.on('data', function(line) {
54 lines.push(line);
55 });
56 lineStream.push('#EXTM');
57 strictEqual(0, lines.length, 'no lines are ready');
58
59 lineStream.push('3U\nmovie.ts\n');
60 strictEqual(2, lines.length, 'two lines are ready');
61 strictEqual('#EXTM3U', lines.shift(), 'the first line is the first token');
62 strictEqual('movie.ts', lines.shift(), 'the second line is the second token');
63 });
64 test('stops sending events after deregistering', function() {
65 var
66 temporaryLines = [],
67 temporary = function(line) {
68 temporaryLines.push(line);
69 },
70 permanentLines = [],
71 permanent = function(line) {
72 permanentLines.push(line);
73 };
74
75 lineStream.on('data', temporary);
76 lineStream.on('data', permanent);
77 lineStream.push('line one\n');
78 strictEqual(temporaryLines.length, permanentLines.length, 'both callbacks receive the event');
79
80 ok(lineStream.off('data', temporary), 'a listener was removed');
81 lineStream.push('line two\n');
82 strictEqual(1, temporaryLines.length, 'no new events are received');
83 strictEqual(2, permanentLines.length, 'new events are still received');
84 });
85
86 QUnit.module('ParseStream', {
87 setup: function() {
88 lineStream = new LineStream();
89 parseStream = new ParseStream();
90 lineStream.pipe(parseStream);
91 } 552 }
92 }); 553 }, 'parsed the attribute list independent of order');
93 test('parses comment lines', function() { 554
94 var 555 manifest = '#EXT-X-KEY:IV=1234567890abcdef1234567890abcdef\n';
95 manifest = '# a line that starts with a hash mark without "EXT" is a comment\n', 556 this.lineStream.push(manifest);
96 element; 557 QUnit.ok(element.attributes.IV, 'detected an IV attribute');
97 parseStream.on('data', function(elem) { 558 QUnit.deepEqual(element.attributes.IV, new Uint32Array([
98 element = elem; 559 0x12345678,
99 }); 560 0x90abcdef,
100 lineStream.push(manifest); 561 0x12345678,
101 562 0x90abcdef
102 ok(element, 'an event was triggered'); 563 ]), 'parsed an IV value');
103 strictEqual(element.type, 'comment', 'the type is comment'); 564 });
104 strictEqual(element.text, 565
105 manifest.slice(1, manifest.length - 1), 566 QUnit.test('parses minimal #EXT-X-KEY tags', function() {
106 'the comment text is parsed'); 567 let manifest = '#EXT-X-KEY:\n';
107 }); 568 let element;
108 test('parses uri lines', function() { 569
109 var 570 this.parseStream.on('data', function(elem) {
110 manifest = 'any non-blank line that does not start with a hash-mark is a URI\n', 571 element = elem;
111 element; 572 });
112 parseStream.on('data', function(elem) { 573 this.lineStream.push(manifest);
113 element = elem; 574
114 }); 575 QUnit.ok(element, 'an event was triggered');
115 lineStream.push(manifest); 576 QUnit.deepEqual(element, {
116 577 type: 'tag',
117 ok(element, 'an event was triggered'); 578 tagType: 'key'
118 strictEqual(element.type, 'uri', 'the type is uri'); 579 }, 'parsed a minimal key tag');
119 strictEqual(element.uri, 580 });
120 manifest.substring(0, manifest.length - 1), 581
121 'the uri text is parsed'); 582 QUnit.test('parses lightly-broken #EXT-X-KEY tags', function() {
122 }); 583 let manifest = '#EXT-X-KEY:URI=\'https://example.com/single-quote\',METHOD=AES-128\n';
123 test('parses unknown tag types', function() { 584 let element;
124 var 585
125 manifest = '#EXT-X-EXAMPLE-TAG:some,additional,stuff\n', 586 this.parseStream.on('data', function(elem) {
126 element; 587 element = elem;
127 parseStream.on('data', function(elem) { 588 });
128 element = elem; 589 this.lineStream.push(manifest);
129 }); 590
130 lineStream.push(manifest); 591 QUnit.strictEqual(element.attributes.URI,
131 592 'https://example.com/single-quote',
132 ok(element, 'an event was triggered'); 593 'parsed a single-quoted uri');
133 strictEqual(element.type, 'tag', 'the type is tag'); 594
134 strictEqual(element.data, 595 element = null;
135 manifest.slice(4, manifest.length - 1), 596 manifest = '#EXT-X-KEYURI="https://example.com/key",METHOD=AES-128\n';
136 'unknown tag data is preserved'); 597 this.lineStream.push(manifest);
137 }); 598 QUnit.strictEqual(element.tagType, 'key', 'parsed the tag type');
138 599 QUnit.strictEqual(element.attributes.URI,
139 // #EXTM3U 600 'https://example.com/key',
140 test('parses #EXTM3U tags', function() { 601 'inferred a colon after the tag type');
141 var 602
142 manifest = '#EXTM3U\n', 603 element = null;
143 element; 604 manifest = '#EXT-X-KEY: URI = "https://example.com/key",METHOD=AES-128\n';
144 parseStream.on('data', function(elem) { 605 this.lineStream.push(manifest);
145 element = elem; 606 QUnit.strictEqual(element.attributes.URI,
146 }); 607 'https://example.com/key',
147 lineStream.push(manifest); 608 'trims and removes quotes around the URI');
148 609 });
149 ok(element, 'an event was triggered'); 610
150 strictEqual(element.type, 'tag', 'the line type is tag'); 611 QUnit.test('ignores empty lines', function() {
151 strictEqual(element.tagType, 'm3u', 'the tag type is m3u'); 612 let manifest = '\n';
152 }); 613 let event = false;
153 614
154 // #EXTINF 615 this.parseStream.on('data', function() {
155 test('parses minimal #EXTINF tags', function() { 616 event = true;
156 var 617 });
157 manifest = '#EXTINF\n', 618 this.lineStream.push(manifest);
158 element; 619
159 parseStream.on('data', function(elem) { 620 QUnit.ok(!event, 'no event is triggered');
160 element = elem; 621 });
161 }); 622
162 lineStream.push(manifest); 623 QUnit.module('m3u8 parser');
163 624
164 ok(element, 'an event was triggered'); 625 QUnit.test('can be constructed', function() {
165 strictEqual(element.type, 'tag', 'the line type is tag'); 626 QUnit.notStrictEqual(typeof new Parser(), 'undefined', 'parser is defined');
166 strictEqual(element.tagType, 'inf', 'the tag type is inf'); 627 });
167 }); 628
168 test('parses #EXTINF tags with durations', function() { 629 QUnit.module('m3u8s');
169 var 630
170 manifest = '#EXTINF:15\n', 631 QUnit.test('parses static manifests as expected', function() {
171 element; 632 let key;
172 parseStream.on('data', function(elem) { 633
173 element = elem; 634 for (key in testDataManifests) {
174 }); 635 if (testDataExpected[key]) {
175 lineStream.push(manifest); 636 let parser = new Parser();
176 637
177 ok(element, 'an event was triggered'); 638 parser.push(testDataManifests[key]);
178 strictEqual(element.type, 'tag', 'the line type is tag'); 639 QUnit.deepEqual(parser.manifest,
179 strictEqual(element.tagType, 'inf', 'the tag type is inf'); 640 testDataExpected[key],
180 strictEqual(element.duration, 15, 'the duration is parsed'); 641 key + '.m3u8 was parsed correctly'
181 ok(!('title' in element), 'no title is parsed'); 642 );
182
183 manifest = '#EXTINF:21,\n';
184 lineStream.push(manifest);
185
186 ok(element, 'an event was triggered');
187 strictEqual(element.type, 'tag', 'the line type is tag');
188 strictEqual(element.tagType, 'inf', 'the tag type is inf');
189 strictEqual(element.duration, 21, 'the duration is parsed');
190 ok(!('title' in element), 'no title is parsed');
191 });
192 test('parses #EXTINF tags with a duration and title', function() {
193 var
194 manifest = '#EXTINF:13,Does anyone really use the title attribute?\n',
195 element;
196 parseStream.on('data', function(elem) {
197 element = elem;
198 });
199 lineStream.push(manifest);
200
201 ok(element, 'an event was triggered');
202 strictEqual(element.type, 'tag', 'the line type is tag');
203 strictEqual(element.tagType, 'inf', 'the tag type is inf');
204 strictEqual(element.duration, 13, 'the duration is parsed');
205 strictEqual(element.title,
206 manifest.substring(manifest.indexOf(',') + 1, manifest.length - 1),
207 'the title is parsed');
208 });
209 test('parses #EXTINF tags with carriage returns', function() {
210 var
211 manifest = '#EXTINF:13,Does anyone really use the title attribute?\r\n',
212 element;
213 parseStream.on('data', function(elem) {
214 element = elem;
215 });
216 lineStream.push(manifest);
217
218 ok(element, 'an event was triggered');
219 strictEqual(element.type, 'tag', 'the line type is tag');
220 strictEqual(element.tagType, 'inf', 'the tag type is inf');
221 strictEqual(element.duration, 13, 'the duration is parsed');
222 strictEqual(element.title,
223 manifest.substring(manifest.indexOf(',') + 1, manifest.length - 2),
224 'the title is parsed');
225 });
226
227 // #EXT-X-TARGETDURATION
228 test('parses minimal #EXT-X-TARGETDURATION tags', function() {
229 var
230 manifest = '#EXT-X-TARGETDURATION\n',
231 element;
232 parseStream.on('data', function(elem) {
233 element = elem;
234 });
235 lineStream.push(manifest);
236
237 ok(element, 'an event was triggered');
238 strictEqual(element.type, 'tag', 'the line type is tag');
239 strictEqual(element.tagType, 'targetduration', 'the tag type is targetduration');
240 ok(!('duration' in element), 'no duration is parsed');
241 });
242 test('parses #EXT-X-TARGETDURATION with duration', function() {
243 var
244 manifest = '#EXT-X-TARGETDURATION:47\n',
245 element;
246 parseStream.on('data', function(elem) {
247 element = elem;
248 });
249 lineStream.push(manifest);
250
251 ok(element, 'an event was triggered');
252 strictEqual(element.type, 'tag', 'the line type is tag');
253 strictEqual(element.tagType, 'targetduration', 'the tag type is targetduration');
254 strictEqual(element.duration, 47, 'the duration is parsed');
255 });
256
257 // #EXT-X-VERSION
258 test('parses minimal #EXT-X-VERSION tags', function() {
259 var
260 manifest = '#EXT-X-VERSION:\n',
261 element;
262 parseStream.on('data', function(elem) {
263 element = elem;
264 });
265 lineStream.push(manifest);
266
267 ok(element, 'an event was triggered');
268 strictEqual(element.type, 'tag', 'the line type is tag');
269 strictEqual(element.tagType, 'version', 'the tag type is version');
270 ok(!('version' in element), 'no version is present');
271 });
272 test('parses #EXT-X-VERSION with a version', function() {
273 var
274 manifest = '#EXT-X-VERSION:99\n',
275 element;
276 parseStream.on('data', function(elem) {
277 element = elem;
278 });
279 lineStream.push(manifest);
280
281 ok(element, 'an event was triggered');
282 strictEqual(element.type, 'tag', 'the line type is tag');
283 strictEqual(element.tagType, 'version', 'the tag type is version');
284 strictEqual(element.version, 99, 'the version is parsed');
285 });
286
287 // #EXT-X-MEDIA-SEQUENCE
288 test('parses minimal #EXT-X-MEDIA-SEQUENCE tags', function() {
289 var
290 manifest = '#EXT-X-MEDIA-SEQUENCE\n',
291 element;
292 parseStream.on('data', function(elem) {
293 element = elem;
294 });
295 lineStream.push(manifest);
296
297 ok(element, 'an event was triggered');
298 strictEqual(element.type, 'tag', 'the line type is tag');
299 strictEqual(element.tagType, 'media-sequence', 'the tag type is media-sequence');
300 ok(!('number' in element), 'no number is present');
301 });
302 test('parses #EXT-X-MEDIA-SEQUENCE with sequence numbers', function() {
303 var
304 manifest = '#EXT-X-MEDIA-SEQUENCE:109\n',
305 element;
306 parseStream.on('data', function(elem) {
307 element = elem;
308 });
309 lineStream.push(manifest);
310
311 ok(element, 'an event was triggered');
312 strictEqual(element.type, 'tag', 'the line type is tag');
313 strictEqual(element.tagType, 'media-sequence', 'the tag type is media-sequence');
314 ok(element.number, 109, 'the number is parsed');
315 });
316
317 // #EXT-X-PLAYLIST-TYPE
318 test('parses minimal #EXT-X-PLAYLIST-TYPE tags', function() {
319 var
320 manifest = '#EXT-X-PLAYLIST-TYPE:\n',
321 element;
322 parseStream.on('data', function(elem) {
323 element = elem;
324 });
325 lineStream.push(manifest);
326
327 ok(element, 'an event was triggered');
328 strictEqual(element.type, 'tag', 'the line type is tag');
329 strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
330 ok(!('playlistType' in element), 'no playlist type is present');
331 });
332 test('parses #EXT-X-PLAYLIST-TYPE with mutability info', function() {
333 var
334 manifest = '#EXT-X-PLAYLIST-TYPE:EVENT\n',
335 element;
336 parseStream.on('data', function(elem) {
337 element = elem;
338 });
339 lineStream.push(manifest);
340
341 ok(element, 'an event was triggered');
342 strictEqual(element.type, 'tag', 'the line type is tag');
343 strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
344 strictEqual(element.playlistType, 'EVENT', 'the playlist type is EVENT');
345
346 manifest = '#EXT-X-PLAYLIST-TYPE:VOD\n';
347 lineStream.push(manifest);
348 ok(element, 'an event was triggered');
349 strictEqual(element.type, 'tag', 'the line type is tag');
350 strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
351 strictEqual(element.playlistType, 'VOD', 'the playlist type is VOD');
352
353 manifest = '#EXT-X-PLAYLIST-TYPE:nonsense\n';
354 lineStream.push(manifest);
355 ok(element, 'an event was triggered');
356 strictEqual(element.type, 'tag', 'the line type is tag');
357 strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
358 strictEqual(element.playlistType, 'nonsense', 'the playlist type is parsed');
359 });
360
361 // #EXT-X-BYTERANGE
362 test('parses minimal #EXT-X-BYTERANGE tags', function() {
363 var
364 manifest = '#EXT-X-BYTERANGE\n',
365 element;
366 parseStream.on('data', function(elem) {
367 element = elem;
368 });
369 lineStream.push(manifest);
370
371 ok(element, 'an event was triggered');
372 strictEqual(element.type, 'tag', 'the line type is tag');
373 strictEqual(element.tagType, 'byterange', 'the tag type is byterange');
374 ok(!('length' in element), 'no length is present');
375 ok(!('offset' in element), 'no offset is present');
376 });
377 test('parses #EXT-X-BYTERANGE with length and offset', function() {
378 var
379 manifest = '#EXT-X-BYTERANGE:45\n',
380 element;
381 parseStream.on('data', function(elem) {
382 element = elem;
383 });
384 lineStream.push(manifest);
385
386 ok(element, 'an event was triggered');
387 strictEqual(element.type, 'tag', 'the line type is tag');
388 strictEqual(element.tagType, 'byterange', 'the tag type is byterange');
389 strictEqual(element.length, 45, 'length is parsed');
390 ok(!('offset' in element), 'no offset is present');
391
392 manifest = '#EXT-X-BYTERANGE:108@16\n';
393 lineStream.push(manifest);
394 ok(element, 'an event was triggered');
395 strictEqual(element.type, 'tag', 'the line type is tag');
396 strictEqual(element.tagType, 'byterange', 'the tag type is byterange');
397 strictEqual(element.length, 108, 'length is parsed');
398 strictEqual(element.offset, 16, 'offset is parsed');
399 });
400
401 // #EXT-X-ALLOW-CACHE
402 test('parses minimal #EXT-X-ALLOW-CACHE tags', function() {
403 var
404 manifest = '#EXT-X-ALLOW-CACHE:\n',
405 element;
406 parseStream.on('data', function(elem) {
407 element = elem;
408 });
409 lineStream.push(manifest);
410
411 ok(element, 'an event was triggered');
412 strictEqual(element.type, 'tag', 'the line type is tag');
413 strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache');
414 ok(!('allowed' in element), 'no allowed is present');
415 });
416 test('parses valid #EXT-X-ALLOW-CACHE tags', function() {
417 var
418 manifest = '#EXT-X-ALLOW-CACHE:YES\n',
419 element;
420 parseStream.on('data', function(elem) {
421 element = elem;
422 });
423 lineStream.push(manifest);
424
425 ok(element, 'an event was triggered');
426 strictEqual(element.type, 'tag', 'the line type is tag');
427 strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache');
428 ok(element.allowed, 'allowed is parsed');
429
430 manifest = '#EXT-X-ALLOW-CACHE:NO\n';
431 lineStream.push(manifest);
432
433 ok(element, 'an event was triggered');
434 strictEqual(element.type, 'tag', 'the line type is tag');
435 strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache');
436 ok(!element.allowed, 'allowed is parsed');
437 });
438 // #EXT-X-STREAM-INF
439 test('parses minimal #EXT-X-STREAM-INF tags', function() {
440 var
441 manifest = '#EXT-X-STREAM-INF\n',
442 element;
443 parseStream.on('data', function(elem) {
444 element = elem;
445 });
446 lineStream.push(manifest);
447
448 ok(element, 'an event was triggered');
449 strictEqual(element.type, 'tag', 'the line type is tag');
450 strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
451 ok(!('attributes' in element), 'no attributes are present');
452 });
453 test('parses #EXT-X-STREAM-INF with common attributes', function() {
454 var
455 manifest = '#EXT-X-STREAM-INF:BANDWIDTH=14400\n',
456 element;
457 parseStream.on('data', function(elem) {
458 element = elem;
459 });
460 lineStream.push(manifest);
461
462 ok(element, 'an event was triggered');
463 strictEqual(element.type, 'tag', 'the line type is tag');
464 strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
465 strictEqual(element.attributes.BANDWIDTH, 14400, 'bandwidth is parsed');
466
467 manifest = '#EXT-X-STREAM-INF:PROGRAM-ID=7\n';
468 lineStream.push(manifest);
469
470 ok(element, 'an event was triggered');
471 strictEqual(element.type, 'tag', 'the line type is tag');
472 strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
473 strictEqual(element.attributes['PROGRAM-ID'], 7, 'program-id is parsed');
474
475 manifest = '#EXT-X-STREAM-INF:RESOLUTION=396x224\n';
476 lineStream.push(manifest);
477
478 ok(element, 'an event was triggered');
479 strictEqual(element.type, 'tag', 'the line type is tag');
480 strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
481 strictEqual(element.attributes.RESOLUTION.width, 396, 'width is parsed');
482 strictEqual(element.attributes.RESOLUTION.height, 224, 'heigth is parsed');
483
484 manifest = '#EXT-X-STREAM-INF:CODECS="avc1.4d400d, mp4a.40.2"\n';
485 lineStream.push(manifest);
486
487 ok(element, 'an event was triggered');
488 strictEqual(element.type, 'tag', 'the line type is tag');
489 strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
490 strictEqual(element.attributes.CODECS,
491 'avc1.4d400d, mp4a.40.2',
492 'codecs are parsed');
493 });
494 test('parses #EXT-X-STREAM-INF with arbitrary attributes', function() {
495 var
496 manifest = '#EXT-X-STREAM-INF:NUMERIC=24,ALPHA=Value,MIXED=123abc\n',
497 element;
498 parseStream.on('data', function(elem) {
499 element = elem;
500 });
501 lineStream.push(manifest);
502
503 ok(element, 'an event was triggered');
504 strictEqual(element.type, 'tag', 'the line type is tag');
505 strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
506 strictEqual(element.attributes.NUMERIC, '24', 'numeric attributes are parsed');
507 strictEqual(element.attributes.ALPHA, 'Value', 'alphabetic attributes are parsed');
508 strictEqual(element.attributes.MIXED, '123abc', 'mixed attributes are parsed');
509 });
510 // #EXT-X-ENDLIST
511 test('parses #EXT-X-ENDLIST tags', function() {
512 var
513 manifest = '#EXT-X-ENDLIST\n',
514 element;
515 parseStream.on('data', function(elem) {
516 element = elem;
517 });
518 lineStream.push(manifest);
519
520 ok(element, 'an event was triggered');
521 strictEqual(element.type, 'tag', 'the line type is tag');
522 strictEqual(element.tagType, 'endlist', 'the tag type is stream-inf');
523 });
524
525 // #EXT-X-KEY
526 test('parses valid #EXT-X-KEY tags', function() {
527 var
528 manifest = '#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"\n',
529 element;
530 parseStream.on('data', function(elem) {
531 element = elem;
532 });
533 lineStream.push(manifest);
534
535 ok(element, 'an event was triggered');
536 deepEqual(element, {
537 type: 'tag',
538 tagType: 'key',
539 attributes: {
540 METHOD: 'AES-128',
541 URI: 'https://priv.example.com/key.php?r=52'
542 }
543 }, 'parsed a valid key');
544
545 manifest = '#EXT-X-KEY:URI="https://example.com/key#1",METHOD=FutureType-1024\n';
546 lineStream.push(manifest);
547 ok(element, 'an event was triggered');
548 deepEqual(element, {
549 type: 'tag',
550 tagType: 'key',
551 attributes: {
552 METHOD: 'FutureType-1024',
553 URI: 'https://example.com/key#1'
554 }
555 }, 'parsed the attribute list independent of order');
556
557 manifest = '#EXT-X-KEY:IV=1234567890abcdef1234567890abcdef\n';
558 lineStream.push(manifest);
559 ok(element.attributes.IV, 'detected an IV attribute');
560 deepEqual(element.attributes.IV, new Uint32Array([
561 0x12345678,
562 0x90abcdef,
563 0x12345678,
564 0x90abcdef
565 ]), 'parsed an IV value');
566 });
567
568 test('parses minimal #EXT-X-KEY tags', function() {
569 var
570 manifest = '#EXT-X-KEY:\n',
571 element;
572 parseStream.on('data', function(elem) {
573 element = elem;
574 });
575 lineStream.push(manifest);
576
577 ok(element, 'an event was triggered');
578 deepEqual(element, {
579 type: 'tag',
580 tagType: 'key'
581 }, 'parsed a minimal key tag');
582 });
583
584 test('parses lightly-broken #EXT-X-KEY tags', function() {
585 var
586 manifest = '#EXT-X-KEY:URI=\'https://example.com/single-quote\',METHOD=AES-128\n',
587 element;
588 parseStream.on('data', function(elem) {
589 element = elem;
590 });
591 lineStream.push(manifest);
592
593 strictEqual(element.attributes.URI,
594 'https://example.com/single-quote',
595 'parsed a single-quoted uri');
596
597 element = null;
598 manifest = '#EXT-X-KEYURI="https://example.com/key",METHOD=AES-128\n';
599 lineStream.push(manifest);
600 strictEqual(element.tagType, 'key', 'parsed the tag type');
601 strictEqual(element.attributes.URI,
602 'https://example.com/key',
603 'inferred a colon after the tag type');
604
605 element = null;
606 manifest = '#EXT-X-KEY: URI = "https://example.com/key",METHOD=AES-128\n';
607 lineStream.push(manifest);
608 strictEqual(element.attributes.URI,
609 'https://example.com/key',
610 'trims and removes quotes around the URI');
611 });
612
613 test('ignores empty lines', function() {
614 var
615 manifest = '\n',
616 event = false;
617 parseStream.on('data', function() {
618 event = true;
619 });
620 lineStream.push(manifest);
621
622 ok(!event, 'no event is triggered');
623 });
624
625 QUnit.module('m3u8 parser');
626
627 test('can be constructed', function() {
628 notStrictEqual(new Parser(), undefined, 'parser is defined');
629 });
630
631 QUnit.module('m3u8s');
632
633 test('parses static manifests as expected', function() {
634 var key;
635 for (key in window.manifests) {
636 if (window.expected[key]) {
637 parser = new Parser();
638 parser.push(window.manifests[key]);
639 deepEqual(parser.manifest,
640 window.expected[key],
641 key + '.m3u8 was parsed correctly');
642 }
643 } 643 }
644 }); 644 }
645 645 });
646 })(window, window.console);
......
1 import manifests from './test-manifests';
2 import expected from './test-expected';
3 window.manifests = manifests;
4 window.expected = expected;
5