Merge pull request #534 from BrandonOCasey/browserify-p2
browserify-p2: m3u8, stream, and stub
Showing
19 changed files
with
954 additions
and
950 deletions
... | @@ -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: {}, | ... | ... |
src/.jshintrc
deleted
100644 → 0
... | @@ -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); | ... | ... |
src/stub.js
0 → 100644
... | @@ -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); | ... | ... |
test/stub.test.js
0 → 100644
-
Please register or sign in to post a comment