02b31a5e by Brandon Casey

separate m3u8 from HLS, and depend m3u8-parser package(#734)

1 parent b6ad6cc9
...@@ -83,6 +83,7 @@ ...@@ -83,6 +83,7 @@
83 "test/" 83 "test/"
84 ], 84 ],
85 "dependencies": { 85 "dependencies": {
86 "m3u8-parser": "^1.0.2",
86 "pkcs7": "^0.2.2", 87 "pkcs7": "^0.2.2",
87 "video.js": "^5.10.1", 88 "video.js": "^5.10.1",
88 "videojs-contrib-media-sources": "^3.1.0", 89 "videojs-contrib-media-sources": "^3.1.0",
......
1 /**
2 * @file m3u8/index.js
3 *
4 * Utilities for parsing M3U8 files. If the entire manifest is available,
5 * `Parser` will create an object representation with enough detail for managing
6 * playback. `ParseStream` and `LineStream` are lower-level parsing primitives
7 * that do not assume the entirety of the manifest is ready and expose a
8 * ReadableStream-like interface.
9 */
10
11 import LineStream from './line-stream';
12 import ParseStream from './parse-stream';
13 import Parser from './parser';
14
15 export default {
16 LineStream,
17 ParseStream,
18 Parser
19 };
1 /**
2 * @file m3u8/line-stream.js
3 */
4 import Stream from '../stream';
5
6 /**
7 * A stream that buffers string input and generates a `data` event for each
8 * line.
9 *
10 * @class LineStream
11 * @extends Stream
12 */
13 export default class LineStream extends Stream {
14 constructor() {
15 super();
16 this.buffer = '';
17 }
18
19 /**
20 * Add new data to be parsed.
21 *
22 * @param {String} data the text to process
23 */
24 push(data) {
25 let nextNewline;
26
27 this.buffer += data;
28 nextNewline = this.buffer.indexOf('\n');
29
30 for (; nextNewline > -1; nextNewline = this.buffer.indexOf('\n')) {
31 this.trigger('data', this.buffer.substring(0, nextNewline));
32 this.buffer = this.buffer.substring(nextNewline + 1);
33 }
34 }
35 }
1 /**
2 * @file m3u8/parse-stream.js
3 */
4 import Stream from '../stream';
5
6 /**
7 * "forgiving" attribute list psuedo-grammar:
8 * attributes -> keyvalue (',' keyvalue)*
9 * keyvalue -> key '=' value
10 * key -> [^=]*
11 * value -> '"' [^"]* '"' | [^,]*
12 */
13 const attributeSeparator = function() {
14 let key = '[^=]*';
15 let value = '"[^"]*"|[^,]*';
16 let keyvalue = '(?:' + key + ')=(?:' + value + ')';
17
18 return new RegExp('(?:^|,)(' + keyvalue + ')');
19 };
20
21 /**
22 * Parse attributes from a line given the seperator
23 *
24 * @param {String} attributes the attibute line to parse
25 */
26 const parseAttributes = function(attributes) {
27 // split the string using attributes as the separator
28 let attrs = attributes.split(attributeSeparator());
29 let i = attrs.length;
30 let result = {};
31 let attr;
32
33 while (i--) {
34 // filter out unmatched portions of the string
35 if (attrs[i] === '') {
36 continue;
37 }
38
39 // split the key and value
40 attr = (/([^=]*)=(.*)/).exec(attrs[i]).slice(1);
41 // trim whitespace and remove optional quotes around the value
42 attr[0] = attr[0].replace(/^\s+|\s+$/g, '');
43 attr[1] = attr[1].replace(/^\s+|\s+$/g, '');
44 attr[1] = attr[1].replace(/^['"](.*)['"]$/g, '$1');
45 result[attr[0]] = attr[1];
46 }
47 return result;
48 };
49
50 /**
51 * A line-level M3U8 parser event stream. It expects to receive input one
52 * line at a time and performs a context-free parse of its contents. A stream
53 * interpretation of a manifest can be useful if the manifest is expected to
54 * be too large to fit comfortably into memory or the entirety of the input
55 * is not immediately available. Otherwise, it's probably much easier to work
56 * with a regular `Parser` object.
57 *
58 * Produces `data` events with an object that captures the parser's
59 * interpretation of the input. That object has a property `tag` that is one
60 * of `uri`, `comment`, or `tag`. URIs only have a single additional
61 * property, `line`, which captures the entirety of the input without
62 * interpretation. Comments similarly have a single additional property
63 * `text` which is the input without the leading `#`.
64 *
65 * Tags always have a property `tagType` which is the lower-cased version of
66 * the M3U8 directive without the `#EXT` or `#EXT-X-` prefix. For instance,
67 * `#EXT-X-MEDIA-SEQUENCE` becomes `media-sequence` when parsed. Unrecognized
68 * tags are given the tag type `unknown` and a single additional property
69 * `data` with the remainder of the input.
70 *
71 * @class ParseStream
72 * @extends Stream
73 */
74 export default class ParseStream extends Stream {
75 constructor() {
76 super();
77 }
78
79 /**
80 * Parses an additional line of input.
81 *
82 * @param {String} line a single line of an M3U8 file to parse
83 */
84 push(line) {
85 let match;
86 let event;
87
88 // strip whitespace
89 line = line.replace(/^[\u0000\s]+|[\u0000\s]+$/g, '');
90 if (line.length === 0) {
91 // ignore empty lines
92 return;
93 }
94
95 // URIs
96 if (line[0] !== '#') {
97 this.trigger('data', {
98 type: 'uri',
99 uri: line
100 });
101 return;
102 }
103
104 // Comments
105 if (line.indexOf('#EXT') !== 0) {
106 this.trigger('data', {
107 type: 'comment',
108 text: line.slice(1)
109 });
110 return;
111 }
112
113 // strip off any carriage returns here so the regex matching
114 // doesn't have to account for them.
115 line = line.replace('\r', '');
116
117 // Tags
118 match = (/^#EXTM3U/).exec(line);
119 if (match) {
120 this.trigger('data', {
121 type: 'tag',
122 tagType: 'm3u'
123 });
124 return;
125 }
126 match = (/^#EXTINF:?([0-9\.]*)?,?(.*)?$/).exec(line);
127 if (match) {
128 event = {
129 type: 'tag',
130 tagType: 'inf'
131 };
132 if (match[1]) {
133 event.duration = parseFloat(match[1]);
134 }
135 if (match[2]) {
136 event.title = match[2];
137 }
138 this.trigger('data', event);
139 return;
140 }
141 match = (/^#EXT-X-TARGETDURATION:?([0-9.]*)?/).exec(line);
142 if (match) {
143 event = {
144 type: 'tag',
145 tagType: 'targetduration'
146 };
147 if (match[1]) {
148 event.duration = parseInt(match[1], 10);
149 }
150 this.trigger('data', event);
151 return;
152 }
153 match = (/^#ZEN-TOTAL-DURATION:?([0-9.]*)?/).exec(line);
154 if (match) {
155 event = {
156 type: 'tag',
157 tagType: 'totalduration'
158 };
159 if (match[1]) {
160 event.duration = parseInt(match[1], 10);
161 }
162 this.trigger('data', event);
163 return;
164 }
165 match = (/^#EXT-X-VERSION:?([0-9.]*)?/).exec(line);
166 if (match) {
167 event = {
168 type: 'tag',
169 tagType: 'version'
170 };
171 if (match[1]) {
172 event.version = parseInt(match[1], 10);
173 }
174 this.trigger('data', event);
175 return;
176 }
177 match = (/^#EXT-X-MEDIA-SEQUENCE:?(\-?[0-9.]*)?/).exec(line);
178 if (match) {
179 event = {
180 type: 'tag',
181 tagType: 'media-sequence'
182 };
183 if (match[1]) {
184 event.number = parseInt(match[1], 10);
185 }
186 this.trigger('data', event);
187 return;
188 }
189 match = (/^#EXT-X-DISCONTINUITY-SEQUENCE:?(\-?[0-9.]*)?/).exec(line);
190 if (match) {
191 event = {
192 type: 'tag',
193 tagType: 'discontinuity-sequence'
194 };
195 if (match[1]) {
196 event.number = parseInt(match[1], 10);
197 }
198 this.trigger('data', event);
199 return;
200 }
201 match = (/^#EXT-X-PLAYLIST-TYPE:?(.*)?$/).exec(line);
202 if (match) {
203 event = {
204 type: 'tag',
205 tagType: 'playlist-type'
206 };
207 if (match[1]) {
208 event.playlistType = match[1];
209 }
210 this.trigger('data', event);
211 return;
212 }
213 match = (/^#EXT-X-BYTERANGE:?([0-9.]*)?@?([0-9.]*)?/).exec(line);
214 if (match) {
215 event = {
216 type: 'tag',
217 tagType: 'byterange'
218 };
219 if (match[1]) {
220 event.length = parseInt(match[1], 10);
221 }
222 if (match[2]) {
223 event.offset = parseInt(match[2], 10);
224 }
225 this.trigger('data', event);
226 return;
227 }
228 match = (/^#EXT-X-ALLOW-CACHE:?(YES|NO)?/).exec(line);
229 if (match) {
230 event = {
231 type: 'tag',
232 tagType: 'allow-cache'
233 };
234 if (match[1]) {
235 event.allowed = !(/NO/).test(match[1]);
236 }
237 this.trigger('data', event);
238 return;
239 }
240 match = (/^#EXT-X-STREAM-INF:?(.*)$/).exec(line);
241 if (match) {
242 event = {
243 type: 'tag',
244 tagType: 'stream-inf'
245 };
246 if (match[1]) {
247 event.attributes = parseAttributes(match[1]);
248
249 if (event.attributes.RESOLUTION) {
250 let split = event.attributes.RESOLUTION.split('x');
251 let resolution = {};
252
253 if (split[0]) {
254 resolution.width = parseInt(split[0], 10);
255 }
256 if (split[1]) {
257 resolution.height = parseInt(split[1], 10);
258 }
259 event.attributes.RESOLUTION = resolution;
260 }
261 if (event.attributes.BANDWIDTH) {
262 event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10);
263 }
264 if (event.attributes['PROGRAM-ID']) {
265 event.attributes['PROGRAM-ID'] = parseInt(event.attributes['PROGRAM-ID'], 10);
266 }
267 }
268 this.trigger('data', event);
269 return;
270 }
271 match = (/^#EXT-X-MEDIA:?(.*)$/).exec(line);
272 if (match) {
273 event = {
274 type: 'tag',
275 tagType: 'media'
276 };
277 if (match[1]) {
278 event.attributes = parseAttributes(match[1]);
279 }
280 this.trigger('data', event);
281 return;
282 }
283 match = (/^#EXT-X-ENDLIST/).exec(line);
284 if (match) {
285 this.trigger('data', {
286 type: 'tag',
287 tagType: 'endlist'
288 });
289 return;
290 }
291 match = (/^#EXT-X-DISCONTINUITY/).exec(line);
292 if (match) {
293 this.trigger('data', {
294 type: 'tag',
295 tagType: 'discontinuity'
296 });
297 return;
298 }
299 match = (/^#EXT-X-KEY:?(.*)$/).exec(line);
300 if (match) {
301 event = {
302 type: 'tag',
303 tagType: 'key'
304 };
305 if (match[1]) {
306 event.attributes = parseAttributes(match[1]);
307 // parse the IV string into a Uint32Array
308 if (event.attributes.IV) {
309 if (event.attributes.IV.substring(0, 2).toLowerCase() === '0x') {
310 event.attributes.IV = event.attributes.IV.substring(2);
311 }
312
313 event.attributes.IV = event.attributes.IV.match(/.{8}/g);
314 event.attributes.IV[0] = parseInt(event.attributes.IV[0], 16);
315 event.attributes.IV[1] = parseInt(event.attributes.IV[1], 16);
316 event.attributes.IV[2] = parseInt(event.attributes.IV[2], 16);
317 event.attributes.IV[3] = parseInt(event.attributes.IV[3], 16);
318 event.attributes.IV = new Uint32Array(event.attributes.IV);
319 }
320 }
321 this.trigger('data', event);
322 return;
323 }
324
325 // unknown tag type
326 this.trigger('data', {
327 type: 'tag',
328 data: line.slice(4, line.length)
329 });
330 }
331 }
1 /**
2 * @file m3u8/parser.js
3 */
4 import Stream from '../stream' ;
5 import LineStream from './line-stream';
6 import ParseStream from './parse-stream';
7 import {mergeOptions} from 'video.js';
8
9 /**
10 * A parser for M3U8 files. The current interpretation of the input is
11 * exposed as a property `manifest` on parser objects. It's just two lines to
12 * create and parse a manifest once you have the contents available as a string:
13 *
14 * ```js
15 * var parser = new videojs.m3u8.Parser();
16 * parser.push(xhr.responseText);
17 * ```
18 *
19 * New input can later be applied to update the manifest object by calling
20 * `push` again.
21 *
22 * The parser attempts to create a usable manifest object even if the
23 * underlying input is somewhat nonsensical. It emits `info` and `warning`
24 * events during the parse if it encounters input that seems invalid or
25 * requires some property of the manifest object to be defaulted.
26 *
27 * @class Parser
28 * @extends Stream
29 */
30 export default class Parser extends Stream {
31 constructor() {
32 super();
33 this.lineStream = new LineStream();
34 this.parseStream = new ParseStream();
35 this.lineStream.pipe(this.parseStream);
36 /* eslint-disable consistent-this */
37 let self = this;
38 /* eslint-enable consistent-this */
39 let uris = [];
40 let currentUri = {};
41 let key;
42 let noop = function() {};
43 let defaultMediaGroups = {
44 'AUDIO': {},
45 'VIDEO': {},
46 'CLOSED-CAPTIONS': {},
47 'SUBTITLES': {}
48 };
49 // group segments into numbered timelines delineated by discontinuities
50 let currentTimeline = 0;
51
52 // the manifest is empty until the parse stream begins delivering data
53 this.manifest = {
54 allowCache: true,
55 discontinuityStarts: []
56 };
57
58 // update the manifest with the m3u8 entry from the parse stream
59 this.parseStream.on('data', function(entry) {
60 let mediaGroup;
61 let rendition;
62
63 ({
64 tag() {
65 // switch based on the tag type
66 (({
67 'allow-cache'() {
68 this.manifest.allowCache = entry.allowed;
69 if (!('allowed' in entry)) {
70 this.trigger('info', {
71 message: 'defaulting allowCache to YES'
72 });
73 this.manifest.allowCache = true;
74 }
75 },
76 byterange() {
77 let byterange = {};
78
79 if ('length' in entry) {
80 currentUri.byterange = byterange;
81 byterange.length = entry.length;
82
83 if (!('offset' in entry)) {
84 this.trigger('info', {
85 message: 'defaulting offset to zero'
86 });
87 entry.offset = 0;
88 }
89 }
90 if ('offset' in entry) {
91 currentUri.byterange = byterange;
92 byterange.offset = entry.offset;
93 }
94 },
95 endlist() {
96 this.manifest.endList = true;
97 },
98 inf() {
99 if (!('mediaSequence' in this.manifest)) {
100 this.manifest.mediaSequence = 0;
101 this.trigger('info', {
102 message: 'defaulting media sequence to zero'
103 });
104 }
105 if (!('discontinuitySequence' in this.manifest)) {
106 this.manifest.discontinuitySequence = 0;
107 this.trigger('info', {
108 message: 'defaulting discontinuity sequence to zero'
109 });
110 }
111 if (entry.duration > 0) {
112 currentUri.duration = entry.duration;
113 }
114
115 if (entry.duration === 0) {
116 currentUri.duration = 0.01;
117 this.trigger('info', {
118 message: 'updating zero segment duration to a small value'
119 });
120 }
121
122 this.manifest.segments = uris;
123 },
124 key() {
125 if (!entry.attributes) {
126 this.trigger('warn', {
127 message: 'ignoring key declaration without attribute list'
128 });
129 return;
130 }
131 // clear the active encryption key
132 if (entry.attributes.METHOD === 'NONE') {
133 key = null;
134 return;
135 }
136 if (!entry.attributes.URI) {
137 this.trigger('warn', {
138 message: 'ignoring key declaration without URI'
139 });
140 return;
141 }
142 if (!entry.attributes.METHOD) {
143 this.trigger('warn', {
144 message: 'defaulting key method to AES-128'
145 });
146 }
147
148 // setup an encryption key for upcoming segments
149 key = {
150 method: entry.attributes.METHOD || 'AES-128',
151 uri: entry.attributes.URI
152 };
153
154 if (typeof entry.attributes.IV !== 'undefined') {
155 key.iv = entry.attributes.IV;
156 }
157 },
158 'media-sequence'() {
159 if (!isFinite(entry.number)) {
160 this.trigger('warn', {
161 message: 'ignoring invalid media sequence: ' + entry.number
162 });
163 return;
164 }
165 this.manifest.mediaSequence = entry.number;
166 },
167 'discontinuity-sequence'() {
168 if (!isFinite(entry.number)) {
169 this.trigger('warn', {
170 message: 'ignoring invalid discontinuity sequence: ' + entry.number
171 });
172 return;
173 }
174 this.manifest.discontinuitySequence = entry.number;
175 currentTimeline = entry.number;
176 },
177 'playlist-type'() {
178 if (!(/VOD|EVENT/).test(entry.playlistType)) {
179 this.trigger('warn', {
180 message: 'ignoring unknown playlist type: ' + entry.playlist
181 });
182 return;
183 }
184 this.manifest.playlistType = entry.playlistType;
185 },
186 'stream-inf'() {
187 this.manifest.playlists = uris;
188 this.manifest.mediaGroups =
189 this.manifest.mediaGroups || defaultMediaGroups;
190
191 if (!entry.attributes) {
192 this.trigger('warn', {
193 message: 'ignoring empty stream-inf attributes'
194 });
195 return;
196 }
197
198 if (!currentUri.attributes) {
199 currentUri.attributes = {};
200 }
201 currentUri.attributes = mergeOptions(currentUri.attributes,
202 entry.attributes);
203 },
204 media() {
205 this.manifest.mediaGroups =
206 this.manifest.mediaGroups || defaultMediaGroups;
207
208 if (!(entry.attributes &&
209 entry.attributes.TYPE &&
210 entry.attributes['GROUP-ID'] &&
211 entry.attributes.NAME)) {
212 this.trigger('warn', {
213 message: 'ignoring incomplete or missing media group'
214 });
215 return;
216 }
217
218 // find the media group, creating defaults as necessary
219 let mediaGroupType = this.manifest.mediaGroups[entry.attributes.TYPE];
220
221 mediaGroupType[entry.attributes['GROUP-ID']] =
222 mediaGroupType[entry.attributes['GROUP-ID']] || {};
223 mediaGroup = mediaGroupType[entry.attributes['GROUP-ID']];
224
225 // collect the rendition metadata
226 rendition = {
227 default: (/yes/i).test(entry.attributes.DEFAULT)
228 };
229 if (rendition.default) {
230 rendition.autoselect = true;
231 } else {
232 rendition.autoselect = (/yes/i).test(entry.attributes.AUTOSELECT);
233 }
234 if (entry.attributes.LANGUAGE) {
235 rendition.language = entry.attributes.LANGUAGE;
236 }
237 if (entry.attributes.URI) {
238 rendition.uri = entry.attributes.URI;
239 }
240
241 // insert the new rendition
242 mediaGroup[entry.attributes.NAME] = rendition;
243 },
244 discontinuity() {
245 currentTimeline += 1;
246 currentUri.discontinuity = true;
247 this.manifest.discontinuityStarts.push(uris.length);
248 },
249 targetduration() {
250 if (!isFinite(entry.duration) || entry.duration < 0) {
251 this.trigger('warn', {
252 message: 'ignoring invalid target duration: ' + entry.duration
253 });
254 return;
255 }
256 this.manifest.targetDuration = entry.duration;
257 },
258 totalduration() {
259 if (!isFinite(entry.duration) || entry.duration < 0) {
260 this.trigger('warn', {
261 message: 'ignoring invalid total duration: ' + entry.duration
262 });
263 return;
264 }
265 this.manifest.totalDuration = entry.duration;
266 }
267 })[entry.tagType] || noop).call(self);
268 },
269 uri() {
270 currentUri.uri = entry.uri;
271 uris.push(currentUri);
272
273 // if no explicit duration was declared, use the target duration
274 if (this.manifest.targetDuration &&
275 !('duration' in currentUri)) {
276 this.trigger('warn', {
277 message: 'defaulting segment duration to the target duration'
278 });
279 currentUri.duration = this.manifest.targetDuration;
280 }
281 // annotate with encryption information, if necessary
282 if (key) {
283 currentUri.key = key;
284 }
285 currentUri.timeline = currentTimeline;
286
287 // prepare for the next URI
288 currentUri = {};
289 },
290 comment() {
291 // comments are not important for playback
292 }
293 })[entry.type].call(self);
294 });
295
296 }
297
298 /**
299 * Parse the input string and update the manifest object.
300 *
301 * @param {String} chunk a potentially incomplete portion of the manifest
302 */
303 push(chunk) {
304 this.lineStream.push(chunk);
305 }
306
307 /**
308 * Flush any remaining input. This can be handy if the last line of an M3U8
309 * manifest did not contain a trailing newline but the file has been
310 * completely received.
311 */
312 end() {
313 // flush any buffered input
314 this.lineStream.push('\n');
315 }
316
317 }
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
8 import resolveUrl from './resolve-url'; 8 import resolveUrl from './resolve-url';
9 import {mergeOptions} from 'video.js'; 9 import {mergeOptions} from 'video.js';
10 import Stream from './stream'; 10 import Stream from './stream';
11 import m3u8 from './m3u8'; 11 import m3u8 from 'm3u8-parser';
12 12
13 /** 13 /**
14 * Returns a new array of segments that is the result of merging 14 * Returns a new array of segments that is the result of merging
......
...@@ -11,7 +11,7 @@ import xhrFactory from './xhr'; ...@@ -11,7 +11,7 @@ import xhrFactory from './xhr';
11 import {Decrypter, AsyncStream, decrypt} from './decrypter'; 11 import {Decrypter, AsyncStream, decrypt} from './decrypter';
12 import utils from './bin-utils'; 12 import utils from './bin-utils';
13 import {MediaSource, URL} from 'videojs-contrib-media-sources'; 13 import {MediaSource, URL} from 'videojs-contrib-media-sources';
14 import m3u8 from './m3u8'; 14 import m3u8 from 'm3u8-parser';
15 import videojs from 'video.js'; 15 import videojs from 'video.js';
16 import MasterPlaylistController from './master-playlist-controller'; 16 import MasterPlaylistController from './master-playlist-controller';
17 import Config from './config'; 17 import Config from './config';
......
1 import {ParseStream, LineStream, Parser} from '../src/m3u8';
2 import QUnit from 'qunit';
3 import testDataExpected from './test-expected.js';
4 import testDataManifests from './test-manifests.js';
5
6 QUnit.module('LineStream', {
7 beforeEach() {
8 this.lineStream = new LineStream();
9 }
10 });
11 QUnit.test('empty inputs produce no tokens', function() {
12 let data = false;
13
14 this.lineStream.on('data', function() {
15 data = true;
16 });
17 this.lineStream.push('');
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'
540 }
541 }, 'parsed a valid key');
542
543 manifest = '#EXT-X-KEY:URI="https://example.com/key#1",METHOD=FutureType-1024\n';
544 this.lineStream.push(manifest);
545 QUnit.ok(element, 'an event was triggered');
546 QUnit.deepEqual(element, {
547 type: 'tag',
548 tagType: 'key',
549 attributes: {
550 METHOD: 'FutureType-1024',
551 URI: 'https://example.com/key#1'
552 }
553 }, 'parsed the attribute list independent of order');
554
555 manifest = '#EXT-X-KEY:IV=1234567890abcdef1234567890abcdef\n';
556 this.lineStream.push(manifest);
557 QUnit.ok(element.attributes.IV, 'detected an IV attribute');
558 QUnit.deepEqual(element.attributes.IV, new Uint32Array([
559 0x12345678,
560 0x90abcdef,
561 0x12345678,
562 0x90abcdef
563 ]), 'parsed an IV value');
564 });
565
566 QUnit.test('parses minimal #EXT-X-KEY tags', function() {
567 let manifest = '#EXT-X-KEY:\n';
568 let element;
569
570 this.parseStream.on('data', function(elem) {
571 element = elem;
572 });
573 this.lineStream.push(manifest);
574
575 QUnit.ok(element, 'an event was triggered');
576 QUnit.deepEqual(element, {
577 type: 'tag',
578 tagType: 'key'
579 }, 'parsed a minimal key tag');
580 });
581
582 QUnit.test('parses lightly-broken #EXT-X-KEY tags', function() {
583 let manifest = '#EXT-X-KEY:URI=\'https://example.com/single-quote\',METHOD=AES-128\n';
584 let element;
585
586 this.parseStream.on('data', function(elem) {
587 element = elem;
588 });
589 this.lineStream.push(manifest);
590
591 QUnit.strictEqual(element.attributes.URI,
592 'https://example.com/single-quote',
593 'parsed a single-quoted uri');
594
595 element = null;
596 manifest = '#EXT-X-KEYURI="https://example.com/key",METHOD=AES-128\n';
597 this.lineStream.push(manifest);
598 QUnit.strictEqual(element.tagType, 'key', 'parsed the tag type');
599 QUnit.strictEqual(element.attributes.URI,
600 'https://example.com/key',
601 'inferred a colon after the tag type');
602
603 element = null;
604 manifest = '#EXT-X-KEY: URI = "https://example.com/key",METHOD=AES-128\n';
605 this.lineStream.push(manifest);
606 QUnit.strictEqual(element.attributes.URI,
607 'https://example.com/key',
608 'trims and removes quotes around the URI');
609 });
610
611 QUnit.test('parses prefixed with 0x or 0X #EXT-X-KEY:IV tags', function() {
612 let manifest;
613 let element;
614
615 this.parseStream.on('data', function(elem) {
616 element = elem;
617 });
618
619 manifest = '#EXT-X-KEY:IV=0x1234567890abcdef1234567890abcdef\n';
620 this.lineStream.push(manifest);
621 QUnit.ok(element.attributes.IV, 'detected an IV attribute');
622 QUnit.deepEqual(element.attributes.IV, new Uint32Array([
623 0x12345678,
624 0x90abcdef,
625 0x12345678,
626 0x90abcdef
627 ]), 'parsed an IV value with 0x');
628
629 manifest = '#EXT-X-KEY:IV=0X1234567890abcdef1234567890abcdef\n';
630 this.lineStream.push(manifest);
631 QUnit.ok(element.attributes.IV, 'detected an IV attribute');
632 QUnit.deepEqual(element.attributes.IV, new Uint32Array([
633 0x12345678,
634 0x90abcdef,
635 0x12345678,
636 0x90abcdef
637 ]), 'parsed an IV value with 0X');
638 });
639
640 QUnit.test('ignores empty lines', function() {
641 let manifest = '\n';
642 let event = false;
643
644 this.parseStream.on('data', function() {
645 event = true;
646 });
647 this.lineStream.push(manifest);
648
649 QUnit.ok(!event, 'no event is triggered');
650 });
651
652 QUnit.module('m3u8 parser');
653
654 QUnit.test('can be constructed', function() {
655 QUnit.notStrictEqual(typeof new Parser(), 'undefined', 'parser is defined');
656 });
657
658 QUnit.module('m3u8s');
659
660 QUnit.test('parses static manifests as expected', function() {
661 let key;
662
663 for (key in testDataManifests) {
664 if (testDataExpected[key]) {
665 let parser = new Parser();
666
667 parser.push(testDataManifests[key]);
668 QUnit.deepEqual(parser.manifest,
669 testDataExpected[key],
670 key + '.m3u8 was parsed correctly'
671 );
672 }
673 }
674 });