separate m3u8 from HLS, and depend m3u8-parser package(#734)
Showing
8 changed files
with
3 additions
and
1378 deletions
... | @@ -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", | ... | ... |
src/m3u8/index.js
deleted
100644 → 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 | }; |
src/m3u8/line-stream.js
deleted
100644 → 0
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 | } |
src/m3u8/parse-stream.js
deleted
100644 → 0
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 | } |
src/m3u8/parser.js
deleted
100644 → 0
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'; | ... | ... |
test/m3u8.test.js
deleted
100644 → 0
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 | }); |
-
Please register or sign in to post a comment