08ba349e by brandonocasey

browserify-p2: m3u8, stream, and stub

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