fixed style issues, bare playlist-loader conversion
fixed pre-existing assertion duplication issues
Showing
3 changed files
with
932 additions
and
955 deletions
... | @@ -12,44 +12,16 @@ import Stream from './stream'; | ... | @@ -12,44 +12,16 @@ import Stream from './stream'; |
12 | import m3u8 from './m3u8'; | 12 | import m3u8 from './m3u8'; |
13 | 13 | ||
14 | /** | 14 | /** |
15 | * Returns a new array of segments that is the result of merging | 15 | * Returns a new master playlist that is the result of merging an |
16 | * properties from an older list of segments onto an updated | 16 | * updated media playlist into the original version. If the |
17 | * list. No properties on the updated playlist will be overridden. | 17 | * updated media playlist does not match any of the playlist |
18 | * @param original {array} the outdated list of segments | 18 | * entries in the original master playlist, null is returned. |
19 | * @param update {array} the updated list of segments | 19 | * @param master {object} a parsed master M3U8 object |
20 | * @param offset {number} (optional) the index of the first update | 20 | * @param media {object} a parsed media M3U8 object |
21 | * segment in the original segment list. For non-live playlists, | 21 | * @return {object} a new object that represents the original |
22 | * this should always be zero and does not need to be | 22 | * master playlist with the updated media playlist merged in, or |
23 | * specified. For live playlists, it should be the difference | 23 | * null if the merge produced no change. |
24 | * between the media sequence numbers in the original and updated | 24 | */ |
25 | * playlists. | ||
26 | * @return a list of merged segment objects | ||
27 | */ | ||
28 | const updateSegments = function(original, update, offset) { | ||
29 | let result = update.slice(); | ||
30 | let length; | ||
31 | let i; | ||
32 | |||
33 | offset = offset || 0; | ||
34 | length = Math.min(original.length, update.length + offset); | ||
35 | |||
36 | for (i = offset; i < length; i++) { | ||
37 | result[i - offset] = mergeOptions(original[i], result[i - offset]); | ||
38 | } | ||
39 | return result; | ||
40 | }; | ||
41 | |||
42 | /** | ||
43 | * Returns a new master playlist that is the result of merging an | ||
44 | * updated media playlist into the original version. If the | ||
45 | * updated media playlist does not match any of the playlist | ||
46 | * entries in the original master playlist, null is returned. | ||
47 | * @param master {object} a parsed master M3U8 object | ||
48 | * @param media {object} a parsed media M3U8 object | ||
49 | * @return {object} a new object that represents the original | ||
50 | * master playlist with the updated media playlist merged in, or | ||
51 | * null if the merge produced no change. | ||
52 | */ | ||
53 | const updateMaster = function(master, media) { | 25 | const updateMaster = function(master, media) { |
54 | let changed = false; | 26 | let changed = false; |
55 | let result = mergeOptions(master, {}); | 27 | let result = mergeOptions(master, {}); |
... | @@ -85,383 +57,408 @@ const updateMaster = function(master, media) { | ... | @@ -85,383 +57,408 @@ const updateMaster = function(master, media) { |
85 | return changed ? result : null; | 57 | return changed ? result : null; |
86 | }; | 58 | }; |
87 | 59 | ||
88 | export default class PlaylistLoader extends Stream { | 60 | /** |
89 | constructor(srcUrl, withCredentials) { | 61 | * Returns a new array of segments that is the result of merging |
90 | super(); | 62 | * properties from an older list of segments onto an updated |
91 | this.srcUrl_ = srcUrl; | 63 | * list. No properties on the updated playlist will be overridden. |
92 | this.withCredentials_ = withCredentials; | 64 | * @param original {array} the outdated list of segments |
93 | 65 | * @param update {array} the updated list of segments | |
94 | this.mediaUpdateTimeout_ = null; | 66 | * @param offset {number} (optional) the index of the first update |
95 | 67 | * segment in the original segment list. For non-live playlists, | |
96 | // initialize the loader state | 68 | * this should always be zero and does not need to be |
97 | this.state = 'HAVE_NOTHING'; | 69 | * specified. For live playlists, it should be the difference |
98 | 70 | * between the media sequence numbers in the original and updated | |
99 | // track the time that has expired from the live window | 71 | * playlists. |
100 | // this allows the seekable start range to be calculated even if | 72 | * @return a list of merged segment objects |
101 | // all segments with timing information have expired | 73 | */ |
102 | this.expired_ = 0; | 74 | const updateSegments = function(original, update, offset) { |
103 | 75 | let result = update.slice(); | |
104 | // a flag that disables "expired time"-tracking this setting has | 76 | let length; |
105 | // no effect when not playing a live stream | 77 | let i; |
106 | this.trackExpiredTime_ = false; | ||
107 | |||
108 | if (!this.srcUrl_) { | ||
109 | throw new Error('A non-empty playlist URL is required'); | ||
110 | } | ||
111 | |||
112 | // In a live list, don't keep track of the expired time until | ||
113 | // HLS tells us that "first play" has commenced | ||
114 | this.on('firstplay', function() { | ||
115 | this.trackExpiredTime_ = true; | ||
116 | }); | ||
117 | |||
118 | // live playlist staleness timeout | ||
119 | this.on('mediaupdatetimeout', () => { | ||
120 | if (this.state !== 'HAVE_METADATA') { | ||
121 | // only refresh the media playlist if no other activity is going on | ||
122 | return; | ||
123 | } | ||
124 | |||
125 | this.state = 'HAVE_CURRENT_METADATA'; | ||
126 | this.request_ = XhrModule({ | ||
127 | uri: resolveUrl(this.master.uri, this.media().uri), | ||
128 | withCredentials: this.withCredentials_ | ||
129 | }, (error, request) => { | ||
130 | if (error) { | ||
131 | return this.playlistRequestError_(request, this.media().uri); | ||
132 | } | ||
133 | this.haveMetadata_(request, this.media().uri); | ||
134 | }); | ||
135 | }); | ||
136 | |||
137 | // request the specified URL | ||
138 | this.request_ = XhrModule({ | ||
139 | uri: this.srcUrl_, | ||
140 | withCredentials: this.withCredentials_ | ||
141 | }, (error, request) => { | ||
142 | let parser = new m3u8.Parser(); | ||
143 | let i; | ||
144 | |||
145 | // clear the loader's request reference | ||
146 | this.request_ = null; | ||
147 | |||
148 | if (error) { | ||
149 | this.error = { | ||
150 | status: request.status, | ||
151 | message: 'HLS playlist request error at URL: ' + this.srcUrl_, | ||
152 | responseText: request.responseText, | ||
153 | // MEDIA_ERR_NETWORK | ||
154 | code: 2 | ||
155 | }; | ||
156 | return this.trigger('error'); | ||
157 | } | ||
158 | |||
159 | parser.push(request.responseText); | ||
160 | parser.end(); | ||
161 | |||
162 | this.state = 'HAVE_MASTER'; | ||
163 | |||
164 | parser.manifest.uri = this.srcUrl_; | ||
165 | |||
166 | // loaded a master playlist | ||
167 | if (parser.manifest.playlists) { | ||
168 | this.master = parser.manifest; | ||
169 | |||
170 | // setup by-URI lookups | ||
171 | i = this.master.playlists.length; | ||
172 | while (i--) { | ||
173 | this.master.playlists[this.master.playlists[i].uri] = | ||
174 | this.master.playlists[i]; | ||
175 | } | ||
176 | |||
177 | this.trigger('loadedplaylist'); | ||
178 | if (!this.request_) { | ||
179 | // no media playlist was specifically selected so start | ||
180 | // from the first listed one | ||
181 | this.media(parser.manifest.playlists[0]); | ||
182 | } | ||
183 | return; | ||
184 | } | ||
185 | |||
186 | // loaded a media playlist | ||
187 | // infer a master playlist if none was previously requested | ||
188 | this.master = { | ||
189 | uri: window.location.href, | ||
190 | playlists: [{ | ||
191 | uri: this.srcUrl_ | ||
192 | }] | ||
193 | }; | ||
194 | this.master.playlists[this.srcUrl_] = this.master.playlists[0]; | ||
195 | this.haveMetadata_(request, this.srcUrl_); | ||
196 | return this.trigger('loadedmetadata'); | ||
197 | }); | ||
198 | } | ||
199 | |||
200 | playlistRequestError_(xhr, url, startingState) { | ||
201 | this.setBandwidth(this.request_ || xhr); | ||
202 | |||
203 | // any in-flight request is now finished | ||
204 | this.request_ = null; | ||
205 | |||
206 | if (startingState) { | ||
207 | this.state = startingState; | ||
208 | } | ||
209 | |||
210 | this.error = { | ||
211 | playlist: this.master.playlists[url], | ||
212 | status: xhr.status, | ||
213 | message: 'HLS playlist request error at URL: ' + url, | ||
214 | responseText: xhr.responseText, | ||
215 | code: (xhr.status >= 500) ? 4 : 2 | ||
216 | }; | ||
217 | this.trigger('error'); | ||
218 | } | ||
219 | |||
220 | // update the playlist loader's state in response to a new or | ||
221 | // updated playlist. | ||
222 | haveMetadata_(xhr, url) { | ||
223 | let parser; | ||
224 | let refreshDelay; | ||
225 | let update; | ||
226 | |||
227 | this.setBandwidth(this.request_ || xhr); | ||
228 | |||
229 | // any in-flight request is now finished | ||
230 | this.request_ = null; | ||
231 | |||
232 | this.state = 'HAVE_METADATA'; | ||
233 | |||
234 | parser = new m3u8.Parser(); | ||
235 | parser.push(xhr.responseText); | ||
236 | parser.end(); | ||
237 | parser.manifest.uri = url; | ||
238 | |||
239 | // merge this playlist into the master | ||
240 | update = updateMaster(this.master, parser.manifest); | ||
241 | refreshDelay = (parser.manifest.targetDuration || 10) * 1000; | ||
242 | if (update) { | ||
243 | this.master = update; | ||
244 | this.updateMediaPlaylist_(parser.manifest); | ||
245 | } else { | ||
246 | // if the playlist is unchanged since the last reload, | ||
247 | // try again after half the target duration | ||
248 | refreshDelay /= 2; | ||
249 | } | ||
250 | |||
251 | // refresh live playlists after a target duration passes | ||
252 | if (!this.media().endList) { | ||
253 | this.clearMediaUpdateTimeout_(); | ||
254 | this.mediaUpdateTimeout_ = window.setTimeout(() => { | ||
255 | this.trigger('mediaupdatetimeout'); | ||
256 | }, refreshDelay); | ||
257 | } | ||
258 | |||
259 | this.trigger('loadedplaylist'); | ||
260 | } | ||
261 | |||
262 | clearMediaUpdateTimeout_() { | ||
263 | if (this.mediaUpdateTimeout_) { | ||
264 | window.clearTimeout(this.mediaUpdateTimeout_); | ||
265 | } | ||
266 | } | ||
267 | |||
268 | requestDispose_() { | ||
269 | if (this.request_) { | ||
270 | this.request_.onreadystatechange = null; | ||
271 | this.request_.abort(); | ||
272 | this.request_ = null; | ||
273 | } | ||
274 | } | ||
275 | |||
276 | /** | ||
277 | * Abort any outstanding work and clean up. | ||
278 | */ | ||
279 | dispose() { | ||
280 | this.requestDispose_(); | ||
281 | this.clearMediaUpdateTimeout_(); | ||
282 | super.dispose(); | ||
283 | } | ||
284 | |||
285 | /** | ||
286 | * When called without any arguments, returns the currently | ||
287 | * active media playlist. When called with a single argument, | ||
288 | * triggers the playlist loader to asynchronously switch to the | ||
289 | * specified media playlist. Calling this method while the | ||
290 | * loader is in the HAVE_NOTHING causes an error to be emitted | ||
291 | * but otherwise has no effect. | ||
292 | * @param playlist (optional) {object} the parsed media playlist | ||
293 | * object to switch to | ||
294 | */ | ||
295 | media(playlist) { | ||
296 | let startingState = this.state; | ||
297 | let mediaChange; | ||
298 | |||
299 | // getter | ||
300 | if (!playlist) { | ||
301 | return this.media_; | ||
302 | } | ||
303 | |||
304 | // setter | ||
305 | if (this.state === 'HAVE_NOTHING') { | ||
306 | throw new Error('Cannot switch media playlist from ' + this.state); | ||
307 | } | ||
308 | |||
309 | // find the playlist object if the target playlist has been | ||
310 | // specified by URI | ||
311 | if (typeof playlist === 'string') { | ||
312 | if (!this.master.playlists[playlist]) { | ||
313 | throw new Error('Unknown playlist URI: ' + playlist); | ||
314 | } | ||
315 | playlist = this.master.playlists[playlist]; | ||
316 | } | ||
317 | |||
318 | mediaChange = !this.media_ || playlist.uri !== this.media_.uri; | ||
319 | |||
320 | // switch to fully loaded playlists immediately | ||
321 | if (this.master.playlists[playlist.uri].endList) { | ||
322 | // abort outstanding playlist requests | ||
323 | this.requestDispose_(); | ||
324 | this.state = 'HAVE_METADATA'; | ||
325 | this.media_ = playlist; | ||
326 | |||
327 | // trigger media change if the active media has been updated | ||
328 | if (mediaChange) { | ||
329 | this.trigger('mediachange'); | ||
330 | } | ||
331 | return; | ||
332 | } | ||
333 | |||
334 | // switching to the active playlist is a no-op | ||
335 | if (!mediaChange) { | ||
336 | return; | ||
337 | } | ||
338 | |||
339 | this.state = 'SWITCHING_MEDIA'; | ||
340 | |||
341 | // there is already an outstanding playlist request | ||
342 | if (this.request_) { | ||
343 | if (resolveUrl(this.master.uri, playlist.uri) === this.request_.url) { | ||
344 | // requesting to switch to the same playlist multiple times | ||
345 | // has no effect after the first | ||
346 | return; | ||
347 | } | ||
348 | this.requestDispose_(); | ||
349 | } | ||
350 | |||
351 | // request the new playlist | ||
352 | this.request_ = XhrModule({ | ||
353 | uri: resolveUrl(this.master.uri, playlist.uri), | ||
354 | withCredentials: this.withCredentials_ | ||
355 | }, (error, request) => { | ||
356 | if (error) { | ||
357 | return this.playlistRequestError_(request, playlist.uri, startingState); | ||
358 | } | ||
359 | this.haveMetadata_(request, playlist.uri); | ||
360 | |||
361 | if (error) { | ||
362 | return; | ||
363 | } | ||
364 | |||
365 | // fire loadedmetadata the first time a media playlist is loaded | ||
366 | if (startingState === 'HAVE_MASTER') { | ||
367 | this.trigger('loadedmetadata'); | ||
368 | } else { | ||
369 | this.trigger('mediachange'); | ||
370 | } | ||
371 | }); | ||
372 | } | ||
373 | |||
374 | setBandwidth(xhr) { | ||
375 | this.bandwidth = xhr.bandwidth; | ||
376 | } | ||
377 | /** | ||
378 | * Update the PlaylistLoader state to reflect the changes in an | ||
379 | * update to the current media playlist. | ||
380 | * @param update {object} the updated media playlist object | ||
381 | */ | ||
382 | updateMediaPlaylist_(update) { | ||
383 | let outdated; | ||
384 | let i; | ||
385 | let segment; | ||
386 | |||
387 | outdated = this.media_; | ||
388 | this.media_ = this.master.playlists[update.uri]; | ||
389 | |||
390 | if (!outdated) { | ||
391 | return; | ||
392 | } | ||
393 | |||
394 | // don't track expired time until this flag is truthy | ||
395 | if (!this.trackExpiredTime_) { | ||
396 | return; | ||
397 | } | ||
398 | |||
399 | // if the update was the result of a rendition switch do not | ||
400 | // attempt to calculate expired_ since media-sequences need not | ||
401 | // correlate between renditions/variants | ||
402 | if (update.uri !== outdated.uri) { | ||
403 | return; | ||
404 | } | ||
405 | |||
406 | // try using precise timing from first segment of the updated | ||
407 | // playlist | ||
408 | if (update.segments.length) { | ||
409 | if (typeof update.segments[0].start !== 'undefined') { | ||
410 | this.expired_ = update.segments[0].start; | ||
411 | return; | ||
412 | } else if (typeof update.segments[0].end !== 'undefined') { | ||
413 | this.expired_ = update.segments[0].end - update.segments[0].duration; | ||
414 | return; | ||
415 | } | ||
416 | } | ||
417 | |||
418 | // calculate expired by walking the outdated playlist | ||
419 | i = update.mediaSequence - outdated.mediaSequence - 1; | ||
420 | |||
421 | for (; i >= 0; i--) { | ||
422 | segment = outdated.segments[i]; | ||
423 | 78 | ||
424 | if (!segment) { | 79 | offset = offset || 0; |
425 | // we missed information on this segment completely between | 80 | length = Math.min(original.length, update.length + offset); |
426 | // playlist updates so we'll have to take an educated guess | ||
427 | // once we begin buffering again, any error we introduce can | ||
428 | // be corrected | ||
429 | this.expired_ += outdated.targetDuration || 10; | ||
430 | continue; | ||
431 | } | ||
432 | 81 | ||
433 | if (typeof segment.end !== 'undefined') { | 82 | for (i = offset; i < length; i++) { |
434 | this.expired_ = segment.end; | 83 | result[i - offset] = mergeOptions(original[i], result[i - offset]); |
435 | return; | ||
436 | } | ||
437 | if (typeof segment.start !== 'undefined') { | ||
438 | this.expired_ = segment.start + segment.duration; | ||
439 | return; | ||
440 | } | ||
441 | this.expired_ += segment.duration; | ||
442 | } | ||
443 | } | 84 | } |
85 | return result; | ||
86 | }; | ||
444 | 87 | ||
88 | export default class PlaylistLoader extends Stream { | ||
89 | constructor(srcUrl, withCredentials) { | ||
90 | super(); | ||
91 | let loader = this; | ||
92 | let dispose; | ||
93 | let mediaUpdateTimeout; | ||
94 | let request; | ||
95 | let playlistRequestError; | ||
96 | let haveMetadata; | ||
97 | |||
98 | // a flag that disables "expired time"-tracking this setting has | ||
99 | // no effect when not playing a live stream | ||
100 | this.trackExpiredTime_ = false; | ||
101 | |||
102 | if (!srcUrl) { | ||
103 | throw new Error('A non-empty playlist URL is required'); | ||
104 | } | ||
105 | |||
106 | playlistRequestError = function(xhr, url, startingState) { | ||
107 | loader.setBandwidth(request || xhr); | ||
108 | |||
109 | // any in-flight request is now finished | ||
110 | request = null; | ||
111 | |||
112 | if (startingState) { | ||
113 | loader.state = startingState; | ||
114 | } | ||
115 | |||
116 | loader.error = { | ||
117 | playlist: loader.master.playlists[url], | ||
118 | status: xhr.status, | ||
119 | message: 'HLS playlist request error at URL: ' + url, | ||
120 | responseText: xhr.responseText, | ||
121 | code: (xhr.status >= 500) ? 4 : 2 | ||
122 | }; | ||
123 | loader.trigger('error'); | ||
124 | }; | ||
125 | |||
126 | // update the playlist loader's state in response to a new or | ||
127 | // updated playlist. | ||
128 | |||
129 | haveMetadata = function(xhr, url) { | ||
130 | let parser; | ||
131 | let refreshDelay; | ||
132 | let update; | ||
133 | |||
134 | loader.setBandwidth(request || xhr); | ||
135 | |||
136 | // any in-flight request is now finished | ||
137 | request = null; | ||
138 | loader.state = 'HAVE_METADATA'; | ||
139 | |||
140 | parser = new m3u8.Parser(); | ||
141 | parser.push(xhr.responseText); | ||
142 | parser.end(); | ||
143 | parser.manifest.uri = url; | ||
144 | |||
145 | // merge this playlist into the master | ||
146 | update = updateMaster(loader.master, parser.manifest); | ||
147 | refreshDelay = (parser.manifest.targetDuration || 10) * 1000; | ||
148 | if (update) { | ||
149 | loader.master = update; | ||
150 | loader.updateMediaPlaylist_(parser.manifest); | ||
151 | } else { | ||
152 | // if the playlist is unchanged since the last reload, | ||
153 | // try again after half the target duration | ||
154 | refreshDelay /= 2; | ||
155 | } | ||
156 | |||
157 | // refresh live playlists after a target duration passes | ||
158 | if (!loader.media().endList) { | ||
159 | window.clearTimeout(mediaUpdateTimeout); | ||
160 | mediaUpdateTimeout = window.setTimeout(function() { | ||
161 | loader.trigger('mediaupdatetimeout'); | ||
162 | }, refreshDelay); | ||
163 | } | ||
164 | |||
165 | loader.trigger('loadedplaylist'); | ||
166 | }; | ||
167 | |||
168 | // initialize the loader state | ||
169 | loader.state = 'HAVE_NOTHING'; | ||
170 | |||
171 | // track the time that has expired from the live window | ||
172 | // this allows the seekable start range to be calculated even if | ||
173 | // all segments with timing information have expired | ||
174 | this.expired_ = 0; | ||
175 | |||
176 | // capture the prototype dispose function | ||
177 | dispose = this.dispose; | ||
178 | |||
179 | /** | ||
180 | * Abort any outstanding work and clean up. | ||
181 | */ | ||
182 | loader.dispose = function() { | ||
183 | if (request) { | ||
184 | request.onreadystatechange = null; | ||
185 | request.abort(); | ||
186 | request = null; | ||
187 | } | ||
188 | window.clearTimeout(mediaUpdateTimeout); | ||
189 | dispose.call(this); | ||
190 | }; | ||
191 | |||
192 | /** | ||
193 | * When called without any arguments, returns the currently | ||
194 | * active media playlist. When called with a single argument, | ||
195 | * triggers the playlist loader to asynchronously switch to the | ||
196 | * specified media playlist. Calling this method while the | ||
197 | * loader is in the HAVE_NOTHING causes an error to be emitted | ||
198 | * but otherwise has no effect. | ||
199 | * @param playlist (optional) {object} the parsed media playlist | ||
200 | * object to switch to | ||
201 | */ | ||
202 | loader.media = function(playlist) { | ||
203 | let startingState = loader.state; | ||
204 | let mediaChange; | ||
205 | // getter | ||
206 | if (!playlist) { | ||
207 | return loader.media_; | ||
208 | } | ||
209 | |||
210 | // setter | ||
211 | if (loader.state === 'HAVE_NOTHING') { | ||
212 | throw new Error('Cannot switch media playlist from ' + loader.state); | ||
213 | } | ||
214 | |||
215 | // find the playlist object if the target playlist has been | ||
216 | // specified by URI | ||
217 | if (typeof playlist === 'string') { | ||
218 | if (!loader.master.playlists[playlist]) { | ||
219 | throw new Error('Unknown playlist URI: ' + playlist); | ||
220 | } | ||
221 | playlist = loader.master.playlists[playlist]; | ||
222 | } | ||
223 | |||
224 | mediaChange = !loader.media_ || playlist.uri !== loader.media_.uri; | ||
225 | |||
226 | // switch to fully loaded playlists immediately | ||
227 | if (loader.master.playlists[playlist.uri].endList) { | ||
228 | // abort outstanding playlist requests | ||
229 | if (request) { | ||
230 | request.onreadystatechange = null; | ||
231 | request.abort(); | ||
232 | request = null; | ||
233 | } | ||
234 | loader.state = 'HAVE_METADATA'; | ||
235 | loader.media_ = playlist; | ||
236 | |||
237 | // trigger media change if the active media has been updated | ||
238 | if (mediaChange) { | ||
239 | loader.trigger('mediachange'); | ||
240 | } | ||
241 | return; | ||
242 | } | ||
243 | |||
244 | // switching to the active playlist is a no-op | ||
245 | if (!mediaChange) { | ||
246 | return; | ||
247 | } | ||
248 | |||
249 | loader.state = 'SWITCHING_MEDIA'; | ||
250 | |||
251 | // there is already an outstanding playlist request | ||
252 | if (request) { | ||
253 | if (resolveUrl(loader.master.uri, playlist.uri) === request.url) { | ||
254 | // requesting to switch to the same playlist multiple times | ||
255 | // has no effect after the first | ||
256 | return; | ||
257 | } | ||
258 | request.onreadystatechange = null; | ||
259 | request.abort(); | ||
260 | request = null; | ||
261 | } | ||
262 | |||
263 | // request the new playlist | ||
264 | request = XhrModule({ | ||
265 | uri: resolveUrl(loader.master.uri, playlist.uri), | ||
266 | withCredentials | ||
267 | }, function(error, request) { | ||
268 | if (error) { | ||
269 | return playlistRequestError(request, playlist.uri, startingState); | ||
270 | } | ||
271 | |||
272 | haveMetadata(request, playlist.uri); | ||
273 | |||
274 | // fire loadedmetadata the first time a media playlist is loaded | ||
275 | if (startingState === 'HAVE_MASTER') { | ||
276 | loader.trigger('loadedmetadata'); | ||
277 | } else { | ||
278 | loader.trigger('mediachange'); | ||
279 | } | ||
280 | }); | ||
281 | }; | ||
282 | |||
283 | loader.setBandwidth = function(xhr) { | ||
284 | loader.bandwidth = xhr.bandwidth; | ||
285 | }; | ||
286 | |||
287 | // In a live list, don't keep track of the expired time until | ||
288 | // HLS tells us that "first play" has commenced | ||
289 | loader.on('firstplay', function() { | ||
290 | this.trackExpiredTime_ = true; | ||
291 | }); | ||
292 | |||
293 | // live playlist staleness timeout | ||
294 | loader.on('mediaupdatetimeout', function() { | ||
295 | if (loader.state !== 'HAVE_METADATA') { | ||
296 | // only refresh the media playlist if no other activity is going on | ||
297 | return; | ||
298 | } | ||
299 | |||
300 | loader.state = 'HAVE_CURRENT_METADATA'; | ||
301 | request = XhrModule({ | ||
302 | uri: resolveUrl(loader.master.uri, loader.media().uri), | ||
303 | withCredentials | ||
304 | }, function(error, request) { | ||
305 | if (error) { | ||
306 | return playlistRequestError(request, loader.media().uri); | ||
307 | } | ||
308 | haveMetadata(request, loader.media().uri); | ||
309 | }); | ||
310 | }); | ||
311 | |||
312 | // request the specified URL | ||
313 | request = XhrModule({ | ||
314 | uri: srcUrl, | ||
315 | withCredentials | ||
316 | }, function(error, req) { | ||
317 | let parser; | ||
318 | let i; | ||
319 | |||
320 | // clear the loader's request reference | ||
321 | request = null; | ||
322 | |||
323 | if (error) { | ||
324 | loader.error = { | ||
325 | status: req.status, | ||
326 | message: 'HLS playlist request error at URL: ' + srcUrl, | ||
327 | responseText: req.responseText, | ||
328 | // MEDIA_ERR_NETWORK | ||
329 | code: 2 | ||
330 | }; | ||
331 | return loader.trigger('error'); | ||
332 | } | ||
333 | |||
334 | parser = new m3u8.Parser(); | ||
335 | parser.push(req.responseText); | ||
336 | parser.end(); | ||
337 | |||
338 | loader.state = 'HAVE_MASTER'; | ||
339 | |||
340 | parser.manifest.uri = srcUrl; | ||
341 | |||
342 | // loaded a master playlist | ||
343 | if (parser.manifest.playlists) { | ||
344 | loader.master = parser.manifest; | ||
345 | |||
346 | // setup by-URI lookups | ||
347 | i = loader.master.playlists.length; | ||
348 | while (i--) { | ||
349 | loader.master.playlists[loader.master.playlists[i].uri] = loader.master.playlists[i]; | ||
350 | } | ||
351 | |||
352 | loader.trigger('loadedplaylist'); | ||
353 | if (!request) { | ||
354 | // no media playlist was specifically selected so start | ||
355 | // from the first listed one | ||
356 | loader.media(parser.manifest.playlists[0]); | ||
357 | } | ||
358 | return; | ||
359 | } | ||
360 | |||
361 | // loaded a media playlist | ||
362 | // infer a master playlist if none was previously requested | ||
363 | loader.master = { | ||
364 | uri: window.location.href, | ||
365 | playlists: [{ | ||
366 | uri: srcUrl | ||
367 | }] | ||
368 | }; | ||
369 | loader.master.playlists[srcUrl] = loader.master.playlists[0]; | ||
370 | haveMetadata(req, srcUrl); | ||
371 | return loader.trigger('loadedmetadata'); | ||
372 | }); | ||
373 | } | ||
445 | /** | 374 | /** |
446 | * Determine the index of the segment that contains a specified | 375 | * Update the PlaylistLoader state to reflect the changes in an |
447 | * playback position in the current media playlist. Early versions | 376 | * update to the current media playlist. |
448 | * of the HLS specification require segment durations to be rounded | 377 | * @param update {object} the updated media playlist object |
449 | * to the nearest integer which means it may not be possible to | 378 | */ |
450 | * determine the correct segment for a playback position if that | 379 | updateMediaPlaylist_(update) { |
451 | * position is within .5 seconds of the segment duration. This | 380 | let outdated; |
452 | * function will always return the lower of the two possible indices | 381 | let i; |
453 | * in those cases. | 382 | let segment; |
454 | * | 383 | |
455 | * @param time {number} The number of seconds since the earliest | 384 | outdated = this.media_; |
456 | * possible position to determine the containing segment for | 385 | this.media_ = this.master.playlists[update.uri]; |
457 | * @returns {number} The number of the media segment that contains | 386 | |
458 | * that time position. If the specified playback position is outside | 387 | if (!outdated) { |
459 | * the time range of the current set of media segments, the return | 388 | return; |
460 | * value will be clamped to the index of the segment containing the | 389 | } |
461 | * closest playback position that is currently available. | 390 | |
462 | */ | 391 | // don't track expired time until this flag is truthy |
463 | getMediaIndexForTime_(time) { | 392 | if (!this.trackExpiredTime_) { |
464 | let i; | 393 | return; |
394 | } | ||
395 | |||
396 | // if the update was the result of a rendition switch do not | ||
397 | // attempt to calculate expired_ since media-sequences need not | ||
398 | // correlate between renditions/variants | ||
399 | if (update.uri !== outdated.uri) { | ||
400 | return; | ||
401 | } | ||
402 | |||
403 | // try using precise timing from first segment of the updated | ||
404 | // playlist | ||
405 | if (update.segments.length) { | ||
406 | if (update.segments[0].start !== undefined) { | ||
407 | this.expired_ = update.segments[0].start; | ||
408 | return; | ||
409 | } else if (update.segments[0].end !== undefined) { | ||
410 | this.expired_ = update.segments[0].end - update.segments[0].duration; | ||
411 | return; | ||
412 | } | ||
413 | } | ||
414 | |||
415 | // calculate expired by walking the outdated playlist | ||
416 | i = update.mediaSequence - outdated.mediaSequence - 1; | ||
417 | |||
418 | for (; i >= 0; i--) { | ||
419 | segment = outdated.segments[i]; | ||
420 | |||
421 | if (!segment) { | ||
422 | // we missed information on this segment completely between | ||
423 | // playlist updates so we'll have to take an educated guess | ||
424 | // once we begin buffering again, any error we introduce can | ||
425 | // be corrected | ||
426 | this.expired_ += outdated.targetDuration || 10; | ||
427 | continue; | ||
428 | } | ||
429 | |||
430 | if (segment.end !== undefined) { | ||
431 | this.expired_ = segment.end; | ||
432 | return; | ||
433 | } | ||
434 | if (segment.start !== undefined) { | ||
435 | this.expired_ = segment.start + segment.duration; | ||
436 | return; | ||
437 | } | ||
438 | this.expired_ += segment.duration; | ||
439 | } | ||
440 | } | ||
441 | |||
442 | /** | ||
443 | * Determine the index of the segment that contains a specified | ||
444 | * playback position in the current media playlist. Early versions | ||
445 | * of the HLS specification require segment durations to be rounded | ||
446 | * to the nearest integer which means it may not be possible to | ||
447 | * determine the correct segment for a playback position if that | ||
448 | * position is within .5 seconds of the segment duration. This | ||
449 | * function will always return the lower of the two possible indices | ||
450 | * in those cases. | ||
451 | * | ||
452 | * @param time {number} The number of seconds since the earliest | ||
453 | * possible position to determine the containing segment for | ||
454 | * @returns {number} The number of the media segment that contains | ||
455 | * that time position. If the specified playback position is outside | ||
456 | * the time range of the current set of media segments, the return | ||
457 | * value will be clamped to the index of the segment containing the | ||
458 | * closest playback position that is currently available. | ||
459 | */ | ||
460 | getMediaIndexForTime_(time) { | ||
461 | let i; | ||
465 | let segment; | 462 | let segment; |
466 | let originalTime = time; | 463 | let originalTime = time; |
467 | let numSegments = this.media_.segments.length; | 464 | let numSegments = this.media_.segments.length; |
... | @@ -471,97 +468,99 @@ export default class PlaylistLoader extends Stream { | ... | @@ -471,97 +468,99 @@ export default class PlaylistLoader extends Stream { |
471 | let knownStart; | 468 | let knownStart; |
472 | let knownEnd; | 469 | let knownEnd; |
473 | 470 | ||
474 | if (!this.media_) { | 471 | if (!this.media_) { |
475 | return 0; | 472 | return 0; |
476 | } | 473 | } |
477 | 474 | ||
478 | // when the requested position is earlier than the current set of | 475 | // when the requested position is earlier than the current set of |
479 | // segments, return the earliest segment index | 476 | // segments, return the earliest segment index |
480 | if (time < 0) { | 477 | if (time < 0) { |
481 | return 0; | 478 | return 0; |
482 | } | 479 | } |
483 | 480 | ||
484 | // find segments with known timing information that bound the | 481 | // find segments with known timing information that bound the |
485 | // target time | 482 | // target time |
486 | for (i = 0; i < numSegments; i++) { | 483 | for (i = 0; i < numSegments; i++) { |
487 | segment = this.media_.segments[i]; | 484 | segment = this.media_.segments[i]; |
488 | if (segment.end) { | 485 | if (segment.end) { |
489 | if (segment.end > time) { | 486 | if (segment.end > time) { |
490 | knownEnd = segment.end; | 487 | knownEnd = segment.end; |
491 | endIndex = i; | 488 | endIndex = i; |
492 | break; | 489 | break; |
493 | } else { | 490 | } else { |
494 | knownStart = segment.end; | 491 | knownStart = segment.end; |
495 | startIndex = i + 1; | 492 | startIndex = i + 1; |
496 | } | 493 | } |
497 | } | 494 | } |
498 | } | 495 | } |
499 | 496 | ||
500 | // use the bounds we just found and playlist information to | 497 | // use the bounds we just found and playlist information to |
501 | // estimate the segment that contains the time we are looking for | 498 | // estimate the segment that contains the time we are looking for |
502 | if (typeof startIndex !== 'undefined') { | 499 | if (startIndex !== undefined) { |
503 | // We have a known-start point that is before our desired time so | 500 | // We have a known-start point that is before our desired time so |
504 | // walk from that point forwards | 501 | // walk from that point forwards |
505 | time = time - knownStart; | 502 | time = time - knownStart; |
506 | for (i = startIndex; i < (endIndex || numSegments); i++) { | 503 | for (i = startIndex; i < (endIndex || numSegments); i++) { |
507 | segment = this.media_.segments[i]; | 504 | segment = this.media_.segments[i]; |
508 | time -= segment.duration; | 505 | time -= segment.duration; |
509 | 506 | ||
510 | if (time < 0) { | 507 | if (time < 0) { |
511 | return i; | 508 | return i; |
512 | } | 509 | } |
513 | } | 510 | } |
514 | 511 | ||
515 | if (i >= endIndex) { | 512 | if (i >= endIndex) { |
516 | // We haven't found a segment but we did hit a known end point | 513 | // We haven't found a segment but we did hit a known end point |
517 | // so fallback to interpolating between the segment index | 514 | // so fallback to interpolating between the segment index |
518 | // based on the known span of the timeline we are dealing with | 515 | // based on the known span of the timeline we are dealing with |
519 | // and the number of segments inside that span | 516 | // and the number of segments inside that span |
520 | return startIndex + Math.floor(((originalTime - knownStart) / | 517 | return startIndex + Math.floor( |
521 | (knownEnd - knownStart)) * | 518 | ((originalTime - knownStart) / (knownEnd - knownStart)) * |
522 | (endIndex - startIndex)); | 519 | (endIndex - startIndex)); |
523 | } | 520 | } |
524 | 521 | ||
525 | // We _still_ haven't found a segment so load the last one | 522 | // We _still_ haven't found a segment so load the last one |
526 | return lastSegment; | 523 | return lastSegment; |
527 | } else if (typeof endIndex !== 'undefined') { | 524 | } else if (endIndex !== undefined) { |
528 | // We _only_ have a known-end point that is after our desired time so | 525 | // We _only_ have a known-end point that is after our desired time so |
529 | // walk from that point backwards | 526 | // walk from that point backwards |
530 | time = knownEnd - time; | 527 | time = knownEnd - time; |
531 | for (i = endIndex; i >= 0; i--) { | 528 | for (i = endIndex; i >= 0; i--) { |
532 | segment = this.media_.segments[i]; | 529 | segment = this.media_.segments[i]; |
533 | time -= segment.duration; | 530 | time -= segment.duration; |
534 | 531 | ||
535 | if (time < 0) { | 532 | if (time < 0) { |
536 | return i; | 533 | return i; |
537 | } | 534 | } |
538 | } | 535 | } |
539 | 536 | ||
540 | // We haven't found a segment so load the first one if time is zero | 537 | // We haven't found a segment so load the first one if time is zero |
541 | if (time === 0) { | 538 | if (time === 0) { |
542 | return 0; | 539 | return 0; |
543 | } | 540 | } else { |
544 | return -1; | 541 | return -1; |
545 | } | 542 | } |
546 | // We known nothing so walk from the front of the playlist, | 543 | } else { |
547 | // subtracting durations until we find a segment that contains | 544 | // We known nothing so walk from the front of the playlist, |
548 | // time and return it | 545 | // subtracting durations until we find a segment that contains |
549 | time = time - this.expired_; | 546 | // time and return it |
550 | 547 | time = time - this.expired_; | |
551 | if (time < 0) { | 548 | |
552 | return -1; | 549 | if (time < 0) { |
553 | } | 550 | return -1; |
554 | 551 | } | |
555 | for (i = 0; i < numSegments; i++) { | 552 | |
556 | segment = this.media_.segments[i]; | 553 | for (i = 0; i < numSegments; i++) { |
557 | time -= segment.duration; | 554 | segment = this.media_.segments[i]; |
558 | if (time < 0) { | 555 | time -= segment.duration; |
559 | return i; | 556 | if (time < 0) { |
560 | } | 557 | return i; |
561 | } | 558 | } |
562 | // We are out of possible candidates so load the last one... | 559 | } |
563 | // The last one is the least likely to overlap a buffer and therefore | 560 | // We are out of possible candidates so load the last one... |
564 | // the one most likely to tell us something about the timeline | 561 | // The last one is the least likely to overlap a buffer and therefore |
565 | return lastSegment; | 562 | // the one most likely to tell us something about the timeline |
566 | } | 563 | return lastSegment; |
564 | } | ||
565 | } | ||
567 | } | 566 | } | ... | ... |
... | @@ -35,10 +35,8 @@ const xhr = function(options, callback) { | ... | @@ -35,10 +35,8 @@ const xhr = function(options, callback) { |
35 | response.statusCode !== 200 && | 35 | response.statusCode !== 200 && |
36 | response.statusCode !== 206 && | 36 | response.statusCode !== 206 && |
37 | response.statusCode !== 0) { | 37 | response.statusCode !== 0) { |
38 | error = new Error( | 38 | error = new Error('XHR Failed with a response of: ' + |
39 | 'XHR Failed with a response of: ' + | 39 | (request && (request.response || request.responseText))); |
40 | (request && (request.response || request.responseText)) | ||
41 | ); | ||
42 | } | 40 | } |
43 | 41 | ||
44 | callback(error, request); | 42 | callback(error, request); | ... | ... |
... | @@ -12,10 +12,6 @@ const urlTo = function(path) { | ... | @@ -12,10 +12,6 @@ const urlTo = function(path) { |
12 | .join('/'); | 12 | .join('/'); |
13 | }; | 13 | }; |
14 | 14 | ||
15 | const respond = function(request, string) { | ||
16 | return request.respond(200, null, string); | ||
17 | }; | ||
18 | |||
19 | QUnit.module('Playlist Loader', { | 15 | QUnit.module('Playlist Loader', { |
20 | beforeEach() { | 16 | beforeEach() { |
21 | // fake XHRs | 17 | // fake XHRs |
... | @@ -57,16 +53,16 @@ QUnit.test('starts without any metadata', function() { | ... | @@ -57,16 +53,16 @@ QUnit.test('starts without any metadata', function() { |
57 | QUnit.test('starts with no expired time', function() { | 53 | QUnit.test('starts with no expired time', function() { |
58 | let loader = new PlaylistLoader('media.m3u8'); | 54 | let loader = new PlaylistLoader('media.m3u8'); |
59 | 55 | ||
60 | respond(this.requests.pop(), | 56 | this.requests.pop().respond(200, null, |
61 | '#EXTM3U\n' + | 57 | '#EXTM3U\n' + |
62 | '#EXTINF:10,\n' + | 58 | '#EXTINF:10,\n' + |
63 | '0.ts\n'); | 59 | '0.ts\n'); |
64 | QUnit.equal(loader.expired_, | 60 | QUnit.equal(loader.expired_, |
65 | 0, | 61 | 0, |
66 | 'zero seconds expired'); | 62 | 'zero seconds expired'); |
67 | }); | 63 | }); |
68 | 64 | ||
69 | QUnit.test('this.requests the initial playlist immediately', function() { | 65 | QUnit.test('requests the initial playlist immediately', function() { |
70 | /* eslint-disable no-unused-vars */ | 66 | /* eslint-disable no-unused-vars */ |
71 | let loader = new PlaylistLoader('master.m3u8'); | 67 | let loader = new PlaylistLoader('master.m3u8'); |
72 | /* eslint-enable no-unused-vars */ | 68 | /* eslint-enable no-unused-vars */ |
... | @@ -84,10 +80,10 @@ QUnit.test('moves to HAVE_MASTER after loading a master playlist', function() { | ... | @@ -84,10 +80,10 @@ QUnit.test('moves to HAVE_MASTER after loading a master playlist', function() { |
84 | loader.on('loadedplaylist', function() { | 80 | loader.on('loadedplaylist', function() { |
85 | state = loader.state; | 81 | state = loader.state; |
86 | }); | 82 | }); |
87 | respond(this.requests.pop(), | 83 | this.requests.pop().respond(200, null, |
88 | '#EXTM3U\n' + | 84 | '#EXTM3U\n' + |
89 | '#EXT-X-STREAM-INF:\n' + | 85 | '#EXT-X-STREAM-INF:\n' + |
90 | 'media.m3u8\n'); | 86 | 'media.m3u8\n'); |
91 | QUnit.ok(loader.master, 'the master playlist is available'); | 87 | QUnit.ok(loader.master, 'the master playlist is available'); |
92 | QUnit.strictEqual(state, 'HAVE_MASTER', 'the state at loadedplaylist correct'); | 88 | QUnit.strictEqual(state, 'HAVE_MASTER', 'the state at loadedplaylist correct'); |
93 | }); | 89 | }); |
... | @@ -99,28 +95,27 @@ QUnit.test('jumps to HAVE_METADATA when initialized with a media playlist', func | ... | @@ -99,28 +95,27 @@ QUnit.test('jumps to HAVE_METADATA when initialized with a media playlist', func |
99 | loader.on('loadedmetadata', function() { | 95 | loader.on('loadedmetadata', function() { |
100 | loadedmetadatas++; | 96 | loadedmetadatas++; |
101 | }); | 97 | }); |
102 | respond(this.requests.pop(), | 98 | this.requests.pop().respond(200, null, |
103 | '#EXTM3U\n' + | 99 | '#EXTM3U\n' + |
104 | '#EXTINF:10,\n' + | 100 | '#EXTINF:10,\n' + |
105 | '0.ts\n' + | 101 | '0.ts\n' + |
106 | '#EXT-X-ENDLIST\n'); | 102 | '#EXT-X-ENDLIST\n'); |
107 | QUnit.ok(loader.master, 'infers a master playlist'); | 103 | QUnit.ok(loader.master, 'infers a master playlist'); |
108 | QUnit.ok(loader.media(), 'sets the media playlist'); | 104 | QUnit.ok(loader.media(), 'sets the media playlist'); |
109 | QUnit.ok(loader.media().uri, 'sets the media playlist URI'); | 105 | QUnit.ok(loader.media().uri, 'sets the media playlist URI'); |
110 | QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); | 106 | QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); |
111 | QUnit.strictEqual(this.requests.length, 0, 'no more this.requests are made'); | 107 | QUnit.strictEqual(this.requests.length, 0, 'no more requests are made'); |
112 | QUnit.strictEqual(loadedmetadatas, 1, 'fired one loadedmetadata'); | 108 | QUnit.strictEqual(loadedmetadatas, 1, 'fired one loadedmetadata'); |
113 | }); | 109 | }); |
114 | 110 | ||
115 | QUnit.test( | 111 | QUnit.test('jumps to HAVE_METADATA when initialized with a live media playlist', |
116 | 'jumps to HAVE_METADATA when initialized with a live media playlist', | ||
117 | function() { | 112 | function() { |
118 | let loader = new PlaylistLoader('media.m3u8'); | 113 | let loader = new PlaylistLoader('media.m3u8'); |
119 | 114 | ||
120 | respond(this.requests.pop(), | 115 | this.requests.pop().respond(200, null, |
121 | '#EXTM3U\n' + | 116 | '#EXTM3U\n' + |
122 | '#EXTINF:10,\n' + | 117 | '#EXTINF:10,\n' + |
123 | '0.ts\n'); | 118 | '0.ts\n'); |
124 | QUnit.ok(loader.master, 'infers a master playlist'); | 119 | QUnit.ok(loader.master, 'infers a master playlist'); |
125 | QUnit.ok(loader.media(), 'sets the media playlist'); | 120 | QUnit.ok(loader.media(), 'sets the media playlist'); |
126 | QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); | 121 | QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); |
... | @@ -137,23 +132,23 @@ QUnit.test('moves to HAVE_METADATA after loading a media playlist', function() { | ... | @@ -137,23 +132,23 @@ QUnit.test('moves to HAVE_METADATA after loading a media playlist', function() { |
137 | loader.on('loadedmetadata', function() { | 132 | loader.on('loadedmetadata', function() { |
138 | loadedMetadata++; | 133 | loadedMetadata++; |
139 | }); | 134 | }); |
140 | respond(this.requests.pop(), | 135 | this.requests.pop().respond(200, null, |
141 | '#EXTM3U\n' + | 136 | '#EXTM3U\n' + |
142 | '#EXT-X-STREAM-INF:\n' + | 137 | '#EXT-X-STREAM-INF:\n' + |
143 | 'media.m3u8\n' + | 138 | 'media.m3u8\n' + |
144 | 'alt.m3u8\n'); | 139 | 'alt.m3u8\n'); |
145 | QUnit.strictEqual(loadedPlaylist, 1, 'fired loadedplaylist once'); | 140 | QUnit.strictEqual(loadedPlaylist, 1, 'fired loadedplaylist once'); |
146 | QUnit.strictEqual(loadedMetadata, 0, 'did not fire loadedmetadata'); | 141 | QUnit.strictEqual(loadedMetadata, 0, 'did not fire loadedmetadata'); |
147 | QUnit.strictEqual(this.requests.length, 1, 'this.requests the media playlist'); | 142 | QUnit.strictEqual(this.requests.length, 1, 'requests the media playlist'); |
148 | QUnit.strictEqual(this.requests[0].method, 'GET', 'GETs the media playlist'); | 143 | QUnit.strictEqual(this.requests[0].method, 'GET', 'GETs the media playlist'); |
149 | QUnit.strictEqual(this.requests[0].url, | 144 | QUnit.strictEqual(this.requests[0].url, |
150 | urlTo('media.m3u8'), | 145 | urlTo('media.m3u8'), |
151 | 'this.requests the first playlist'); | 146 | 'requests the first playlist'); |
152 | 147 | ||
153 | respond(this.requests.pop(), | 148 | this.requests.pop().respond(200, null, |
154 | '#EXTM3U\n' + | 149 | '#EXTM3U\n' + |
155 | '#EXTINF:10,\n' + | 150 | '#EXTINF:10,\n' + |
156 | '0.ts\n'); | 151 | '0.ts\n'); |
157 | QUnit.ok(loader.master, 'sets the master playlist'); | 152 | QUnit.ok(loader.master, 'sets the master playlist'); |
158 | QUnit.ok(loader.media(), 'sets the media playlist'); | 153 | QUnit.ok(loader.media(), 'sets the media playlist'); |
159 | QUnit.strictEqual(loadedPlaylist, 2, 'fired loadedplaylist twice'); | 154 | QUnit.strictEqual(loadedPlaylist, 2, 'fired loadedplaylist twice'); |
... | @@ -164,10 +159,10 @@ QUnit.test('moves to HAVE_METADATA after loading a media playlist', function() { | ... | @@ -164,10 +159,10 @@ QUnit.test('moves to HAVE_METADATA after loading a media playlist', function() { |
164 | QUnit.test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', function() { | 159 | QUnit.test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', function() { |
165 | let loader = new PlaylistLoader('live.m3u8'); | 160 | let loader = new PlaylistLoader('live.m3u8'); |
166 | 161 | ||
167 | respond(this.requests.pop(), | 162 | this.requests.pop().respond(200, null, |
168 | '#EXTM3U\n' + | 163 | '#EXTM3U\n' + |
169 | '#EXTINF:10,\n' + | 164 | '#EXTINF:10,\n' + |
170 | '0.ts\n'); | 165 | '0.ts\n'); |
171 | // 10s, one target duration | 166 | // 10s, one target duration |
172 | this.clock.tick(10 * 1000); | 167 | this.clock.tick(10 * 1000); |
173 | QUnit.strictEqual(loader.state, 'HAVE_CURRENT_METADATA', 'the state is correct'); | 168 | QUnit.strictEqual(loader.state, 'HAVE_CURRENT_METADATA', 'the state is correct'); |
... | @@ -180,48 +175,47 @@ QUnit.test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', functi | ... | @@ -180,48 +175,47 @@ QUnit.test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', functi |
180 | QUnit.test('returns to HAVE_METADATA after refreshing the playlist', function() { | 175 | QUnit.test('returns to HAVE_METADATA after refreshing the playlist', function() { |
181 | let loader = new PlaylistLoader('live.m3u8'); | 176 | let loader = new PlaylistLoader('live.m3u8'); |
182 | 177 | ||
183 | respond(this.requests.pop(), | 178 | this.requests.pop().respond(200, null, |
184 | '#EXTM3U\n' + | 179 | '#EXTM3U\n' + |
185 | '#EXTINF:10,\n' + | 180 | '#EXTINF:10,\n' + |
186 | '0.ts\n'); | 181 | '0.ts\n'); |
187 | // 10s, one target duration | 182 | // 10s, one target duration |
188 | this.clock.tick(10 * 1000); | 183 | this.clock.tick(10 * 1000); |
189 | respond(this.requests.pop(), | 184 | this.requests.pop().respond(200, null, |
190 | '#EXTM3U\n' + | 185 | '#EXTM3U\n' + |
191 | '#EXTINF:10,\n' + | 186 | '#EXTINF:10,\n' + |
192 | '1.ts\n'); | 187 | '1.ts\n'); |
193 | QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); | 188 | QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); |
194 | }); | 189 | }); |
195 | 190 | ||
196 | QUnit.test( | 191 | QUnit.test('does not increment expired seconds before firstplay is triggered', |
197 | 'does not increment expired seconds before firstplay is triggered', | ||
198 | function() { | 192 | function() { |
199 | let loader = new PlaylistLoader('live.m3u8'); | 193 | let loader = new PlaylistLoader('live.m3u8'); |
200 | 194 | ||
201 | respond(this.requests.pop(), | 195 | this.requests.pop().respond(200, null, |
202 | '#EXTM3U\n' + | 196 | '#EXTM3U\n' + |
203 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 197 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
204 | '#EXTINF:10,\n' + | 198 | '#EXTINF:10,\n' + |
205 | '0.ts\n' + | 199 | '0.ts\n' + |
206 | '#EXTINF:10,\n' + | 200 | '#EXTINF:10,\n' + |
207 | '1.ts\n' + | 201 | '1.ts\n' + |
208 | '#EXTINF:10,\n' + | 202 | '#EXTINF:10,\n' + |
209 | '2.ts\n' + | 203 | '2.ts\n' + |
210 | '#EXTINF:10,\n' + | 204 | '#EXTINF:10,\n' + |
211 | '3.ts\n'); | 205 | '3.ts\n'); |
212 | // 10s, one target duration | 206 | // 10s, one target duration |
213 | this.clock.tick(10 * 1000); | 207 | this.clock.tick(10 * 1000); |
214 | respond(this.requests.pop(), | 208 | this.requests.pop().respond(200, null, |
215 | '#EXTM3U\n' + | 209 | '#EXTM3U\n' + |
216 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | 210 | '#EXT-X-MEDIA-SEQUENCE:1\n' + |
217 | '#EXTINF:10,\n' + | 211 | '#EXTINF:10,\n' + |
218 | '1.ts\n' + | 212 | '1.ts\n' + |
219 | '#EXTINF:10,\n' + | 213 | '#EXTINF:10,\n' + |
220 | '2.ts\n' + | 214 | '2.ts\n' + |
221 | '#EXTINF:10,\n' + | 215 | '#EXTINF:10,\n' + |
222 | '3.ts\n' + | 216 | '3.ts\n' + |
223 | '#EXTINF:10,\n' + | 217 | '#EXTINF:10,\n' + |
224 | '4.ts\n'); | 218 | '4.ts\n'); |
225 | QUnit.equal(loader.expired_, 0, 'expired one segment'); | 219 | QUnit.equal(loader.expired_, 0, 'expired one segment'); |
226 | }); | 220 | }); |
227 | 221 | ||
... | @@ -229,30 +223,30 @@ QUnit.test('increments expired seconds after a segment is removed', function() { | ... | @@ -229,30 +223,30 @@ QUnit.test('increments expired seconds after a segment is removed', function() { |
229 | let loader = new PlaylistLoader('live.m3u8'); | 223 | let loader = new PlaylistLoader('live.m3u8'); |
230 | 224 | ||
231 | loader.trigger('firstplay'); | 225 | loader.trigger('firstplay'); |
232 | respond(this.requests.pop(), | 226 | this.requests.pop().respond(200, null, |
233 | '#EXTM3U\n' + | 227 | '#EXTM3U\n' + |
234 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 228 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
235 | '#EXTINF:10,\n' + | 229 | '#EXTINF:10,\n' + |
236 | '0.ts\n' + | 230 | '0.ts\n' + |
237 | '#EXTINF:10,\n' + | 231 | '#EXTINF:10,\n' + |
238 | '1.ts\n' + | 232 | '1.ts\n' + |
239 | '#EXTINF:10,\n' + | 233 | '#EXTINF:10,\n' + |
240 | '2.ts\n' + | 234 | '2.ts\n' + |
241 | '#EXTINF:10,\n' + | 235 | '#EXTINF:10,\n' + |
242 | '3.ts\n'); | 236 | '3.ts\n'); |
243 | // 10s, one target duration | 237 | // 10s, one target duration |
244 | this.clock.tick(10 * 1000); | 238 | this.clock.tick(10 * 1000); |
245 | respond(this.requests.pop(), | 239 | this.requests.pop().respond(200, null, |
246 | '#EXTM3U\n' + | 240 | '#EXTM3U\n' + |
247 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | 241 | '#EXT-X-MEDIA-SEQUENCE:1\n' + |
248 | '#EXTINF:10,\n' + | 242 | '#EXTINF:10,\n' + |
249 | '1.ts\n' + | 243 | '1.ts\n' + |
250 | '#EXTINF:10,\n' + | 244 | '#EXTINF:10,\n' + |
251 | '2.ts\n' + | 245 | '2.ts\n' + |
252 | '#EXTINF:10,\n' + | 246 | '#EXTINF:10,\n' + |
253 | '3.ts\n' + | 247 | '3.ts\n' + |
254 | '#EXTINF:10,\n' + | 248 | '#EXTINF:10,\n' + |
255 | '4.ts\n'); | 249 | '4.ts\n'); |
256 | QUnit.equal(loader.expired_, 10, 'expired one segment'); | 250 | QUnit.equal(loader.expired_, 10, 'expired one segment'); |
257 | }); | 251 | }); |
258 | 252 | ||
... | @@ -260,100 +254,98 @@ QUnit.test('increments expired seconds after a discontinuity', function() { | ... | @@ -260,100 +254,98 @@ QUnit.test('increments expired seconds after a discontinuity', function() { |
260 | let loader = new PlaylistLoader('live.m3u8'); | 254 | let loader = new PlaylistLoader('live.m3u8'); |
261 | 255 | ||
262 | loader.trigger('firstplay'); | 256 | loader.trigger('firstplay'); |
263 | respond(this.requests.pop(), | 257 | this.requests.pop().respond(200, null, |
264 | '#EXTM3U\n' + | 258 | '#EXTM3U\n' + |
265 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 259 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
266 | '#EXTINF:10,\n' + | 260 | '#EXTINF:10,\n' + |
267 | '0.ts\n' + | 261 | '0.ts\n' + |
268 | '#EXTINF:3,\n' + | 262 | '#EXTINF:3,\n' + |
269 | '1.ts\n' + | 263 | '1.ts\n' + |
270 | '#EXT-X-DISCONTINUITY\n' + | 264 | '#EXT-X-DISCONTINUITY\n' + |
271 | '#EXTINF:4,\n' + | 265 | '#EXTINF:4,\n' + |
272 | '2.ts\n'); | 266 | '2.ts\n'); |
273 | // 10s, one target duration | 267 | // 10s, one target duration |
274 | this.clock.tick(10 * 1000); | 268 | this.clock.tick(10 * 1000); |
275 | respond(this.requests.pop(), | 269 | this.requests.pop().respond(200, null, |
276 | '#EXTM3U\n' + | 270 | '#EXTM3U\n' + |
277 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | 271 | '#EXT-X-MEDIA-SEQUENCE:1\n' + |
278 | '#EXTINF:3,\n' + | 272 | '#EXTINF:3,\n' + |
279 | '1.ts\n' + | 273 | '1.ts\n' + |
280 | '#EXT-X-DISCONTINUITY\n' + | 274 | '#EXT-X-DISCONTINUITY\n' + |
281 | '#EXTINF:4,\n' + | 275 | '#EXTINF:4,\n' + |
282 | '2.ts\n'); | 276 | '2.ts\n'); |
283 | QUnit.equal(loader.expired_, 10, 'expired one segment'); | 277 | QUnit.equal(loader.expired_, 10, 'expired one segment'); |
284 | 278 | ||
285 | // 10s, one target duration | 279 | // 10s, one target duration |
286 | this.clock.tick(10 * 1000); | 280 | this.clock.tick(10 * 1000); |
287 | respond(this.requests.pop(), | 281 | this.requests.pop().respond(200, null, |
288 | '#EXTM3U\n' + | 282 | '#EXTM3U\n' + |
289 | '#EXT-X-MEDIA-SEQUENCE:2\n' + | 283 | '#EXT-X-MEDIA-SEQUENCE:2\n' + |
290 | '#EXT-X-DISCONTINUITY\n' + | 284 | '#EXT-X-DISCONTINUITY\n' + |
291 | '#EXTINF:4,\n' + | 285 | '#EXTINF:4,\n' + |
292 | '2.ts\n'); | 286 | '2.ts\n'); |
293 | QUnit.equal(loader.expired_, 13, 'no expirations after the discontinuity yet'); | 287 | QUnit.equal(loader.expired_, 13, 'no expirations after the discontinuity yet'); |
294 | 288 | ||
295 | // 10s, one target duration | 289 | // 10s, one target duration |
296 | this.clock.tick(10 * 1000); | 290 | this.clock.tick(10 * 1000); |
297 | respond(this.requests.pop(), | 291 | this.requests.pop().respond(200, null, |
298 | '#EXTM3U\n' + | 292 | '#EXTM3U\n' + |
299 | '#EXT-X-MEDIA-SEQUENCE:3\n' + | 293 | '#EXT-X-MEDIA-SEQUENCE:3\n' + |
300 | '#EXT-X-DISCONTINUITY-SEQUENCE:1\n' + | 294 | '#EXT-X-DISCONTINUITY-SEQUENCE:1\n' + |
301 | '#EXTINF:10,\n' + | 295 | '#EXTINF:10,\n' + |
302 | '3.ts\n'); | 296 | '3.ts\n'); |
303 | QUnit.equal(loader.expired_, 17, 'tracked expiration across the discontinuity'); | 297 | QUnit.equal(loader.expired_, 17, 'tracked expiration across the discontinuity'); |
304 | }); | 298 | }); |
305 | 299 | ||
306 | QUnit.test( | 300 | QUnit.test('tracks expired seconds properly when two discontinuities expire at once', |
307 | 'tracks expired seconds properly when two discontinuities expire at once', | ||
308 | function() { | 301 | function() { |
309 | let loader = new PlaylistLoader('live.m3u8'); | 302 | let loader = new PlaylistLoader('live.m3u8'); |
310 | 303 | ||
311 | loader.trigger('firstplay'); | 304 | loader.trigger('firstplay'); |
312 | respond(this.requests.pop(), | 305 | this.requests.pop().respond(200, null, |
313 | '#EXTM3U\n' + | 306 | '#EXTM3U\n' + |
314 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 307 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
315 | '#EXTINF:4,\n' + | 308 | '#EXTINF:4,\n' + |
316 | '0.ts\n' + | 309 | '0.ts\n' + |
317 | '#EXT-X-DISCONTINUITY\n' + | 310 | '#EXT-X-DISCONTINUITY\n' + |
318 | '#EXTINF:5,\n' + | 311 | '#EXTINF:5,\n' + |
319 | '1.ts\n' + | 312 | '1.ts\n' + |
320 | '#EXT-X-DISCONTINUITY\n' + | 313 | '#EXT-X-DISCONTINUITY\n' + |
321 | '#EXTINF:6,\n' + | 314 | '#EXTINF:6,\n' + |
322 | '2.ts\n' + | 315 | '2.ts\n' + |
323 | '#EXTINF:7,\n' + | 316 | '#EXTINF:7,\n' + |
324 | '3.ts\n'); | 317 | '3.ts\n'); |
325 | this.clock.tick(10 * 1000); | 318 | this.clock.tick(10 * 1000); |
326 | respond(this.requests.pop(), | 319 | this.requests.pop().respond(200, null, |
327 | '#EXTM3U\n' + | 320 | '#EXTM3U\n' + |
328 | '#EXT-X-MEDIA-SEQUENCE:3\n' + | 321 | '#EXT-X-MEDIA-SEQUENCE:3\n' + |
329 | '#EXT-X-DISCONTINUITY-SEQUENCE:2\n' + | 322 | '#EXT-X-DISCONTINUITY-SEQUENCE:2\n' + |
330 | '#EXTINF:7,\n' + | 323 | '#EXTINF:7,\n' + |
331 | '3.ts\n'); | 324 | '3.ts\n'); |
332 | QUnit.equal(loader.expired_, 4 + 5 + 6, 'tracked multiple expiring discontinuities'); | 325 | QUnit.equal(loader.expired_, 4 + 5 + 6, 'tracked multiple expiring discontinuities'); |
333 | }); | 326 | }); |
334 | 327 | ||
335 | QUnit.test( | 328 | QUnit.test('estimates expired if an entire window elapses between live playlist updates', |
336 | 'estimates expired if an entire window elapses between live playlist updates', | ||
337 | function() { | 329 | function() { |
338 | let loader = new PlaylistLoader('live.m3u8'); | 330 | let loader = new PlaylistLoader('live.m3u8'); |
339 | 331 | ||
340 | loader.trigger('firstplay'); | 332 | loader.trigger('firstplay'); |
341 | respond(this.requests.pop(), | 333 | this.requests.pop().respond(200, null, |
342 | '#EXTM3U\n' + | 334 | '#EXTM3U\n' + |
343 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 335 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
344 | '#EXTINF:4,\n' + | 336 | '#EXTINF:4,\n' + |
345 | '0.ts\n' + | 337 | '0.ts\n' + |
346 | '#EXTINF:5,\n' + | 338 | '#EXTINF:5,\n' + |
347 | '1.ts\n'); | 339 | '1.ts\n'); |
348 | 340 | ||
349 | this.clock.tick(10 * 1000); | 341 | this.clock.tick(10 * 1000); |
350 | respond(this.requests.pop(), | 342 | this.requests.pop().respond(200, null, |
351 | '#EXTM3U\n' + | 343 | '#EXTM3U\n' + |
352 | '#EXT-X-MEDIA-SEQUENCE:4\n' + | 344 | '#EXT-X-MEDIA-SEQUENCE:4\n' + |
353 | '#EXTINF:6,\n' + | 345 | '#EXTINF:6,\n' + |
354 | '4.ts\n' + | 346 | '4.ts\n' + |
355 | '#EXTINF:7,\n' + | 347 | '#EXTINF:7,\n' + |
356 | '5.ts\n'); | 348 | '5.ts\n'); |
357 | 349 | ||
358 | QUnit.equal(loader.expired_, | 350 | QUnit.equal(loader.expired_, |
359 | 4 + 5 + (2 * 10), | 351 | 4 + 5 + (2 * 10), |
... | @@ -380,10 +372,10 @@ QUnit.test('errors when an initial media playlist request fails', function() { | ... | @@ -380,10 +372,10 @@ QUnit.test('errors when an initial media playlist request fails', function() { |
380 | loader.on('error', function() { | 372 | loader.on('error', function() { |
381 | errors.push(loader.error); | 373 | errors.push(loader.error); |
382 | }); | 374 | }); |
383 | respond(this.requests.pop(), | 375 | this.requests.pop().respond(200, null, |
384 | '#EXTM3U\n' + | 376 | '#EXTM3U\n' + |
385 | '#EXT-X-STREAM-INF:\n' + | 377 | '#EXT-X-STREAM-INF:\n' + |
386 | 'media.m3u8\n'); | 378 | 'media.m3u8\n'); |
387 | 379 | ||
388 | QUnit.strictEqual(errors.length, 0, 'emitted no errors'); | 380 | QUnit.strictEqual(errors.length, 0, 'emitted no errors'); |
389 | 381 | ||
... | @@ -394,25 +386,24 @@ QUnit.test('errors when an initial media playlist request fails', function() { | ... | @@ -394,25 +386,24 @@ QUnit.test('errors when an initial media playlist request fails', function() { |
394 | }); | 386 | }); |
395 | 387 | ||
396 | // http://tools.ietf.org/html/draft-pantos-http-live-streaming-12#section-6.3.4 | 388 | // http://tools.ietf.org/html/draft-pantos-http-live-streaming-12#section-6.3.4 |
397 | QUnit.test( | 389 | QUnit.test('halves the refresh timeout if a playlist is unchanged since the last reload', |
398 | 'halves the refresh timeout if a playlist is unchanged since the last reload', | ||
399 | function() { | 390 | function() { |
400 | /* eslint-disable no-unused-vars */ | 391 | /* eslint-disable no-unused-vars */ |
401 | let loader = new PlaylistLoader('live.m3u8'); | 392 | let loader = new PlaylistLoader('live.m3u8'); |
402 | /* eslint-enable no-unused-vars */ | 393 | /* eslint-enable no-unused-vars */ |
403 | 394 | ||
404 | respond(this.requests.pop(), | 395 | this.requests.pop().respond(200, null, |
405 | '#EXTM3U\n' + | 396 | '#EXTM3U\n' + |
406 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 397 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
407 | '#EXTINF:10,\n' + | 398 | '#EXTINF:10,\n' + |
408 | '0.ts\n'); | 399 | '0.ts\n'); |
409 | // trigger a refresh | 400 | // trigger a refresh |
410 | this.clock.tick(10 * 1000); | 401 | this.clock.tick(10 * 1000); |
411 | respond(this.requests.pop(), | 402 | this.requests.pop().respond(200, null, |
412 | '#EXTM3U\n' + | 403 | '#EXTM3U\n' + |
413 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 404 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
414 | '#EXTINF:10,\n' + | 405 | '#EXTINF:10,\n' + |
415 | '0.ts\n'); | 406 | '0.ts\n'); |
416 | // half the default target-duration | 407 | // half the default target-duration |
417 | this.clock.tick(5 * 1000); | 408 | this.clock.tick(5 * 1000); |
418 | 409 | ||
... | @@ -426,15 +417,15 @@ QUnit.test('preserves segment metadata across playlist refreshes', function() { | ... | @@ -426,15 +417,15 @@ QUnit.test('preserves segment metadata across playlist refreshes', function() { |
426 | let loader = new PlaylistLoader('live.m3u8'); | 417 | let loader = new PlaylistLoader('live.m3u8'); |
427 | let segment; | 418 | let segment; |
428 | 419 | ||
429 | respond(this.requests.pop(), | 420 | this.requests.pop().respond(200, null, |
430 | '#EXTM3U\n' + | 421 | '#EXTM3U\n' + |
431 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 422 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
432 | '#EXTINF:10,\n' + | 423 | '#EXTINF:10,\n' + |
433 | '0.ts\n' + | 424 | '0.ts\n' + |
434 | '#EXTINF:10,\n' + | 425 | '#EXTINF:10,\n' + |
435 | '1.ts\n' + | 426 | '1.ts\n' + |
436 | '#EXTINF:10,\n' + | 427 | '#EXTINF:10,\n' + |
437 | '2.ts\n'); | 428 | '2.ts\n'); |
438 | // add PTS info to 1.ts | 429 | // add PTS info to 1.ts |
439 | segment = loader.media().segments[1]; | 430 | segment = loader.media().segments[1]; |
440 | segment.minVideoPts = 14; | 431 | segment.minVideoPts = 14; |
... | @@ -443,13 +434,13 @@ QUnit.test('preserves segment metadata across playlist refreshes', function() { | ... | @@ -443,13 +434,13 @@ QUnit.test('preserves segment metadata across playlist refreshes', function() { |
443 | 434 | ||
444 | // trigger a refresh | 435 | // trigger a refresh |
445 | this.clock.tick(10 * 1000); | 436 | this.clock.tick(10 * 1000); |
446 | respond(this.requests.pop(), | 437 | this.requests.pop().respond(200, null, |
447 | '#EXTM3U\n' + | 438 | '#EXTM3U\n' + |
448 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | 439 | '#EXT-X-MEDIA-SEQUENCE:1\n' + |
449 | '#EXTINF:10,\n' + | 440 | '#EXTINF:10,\n' + |
450 | '1.ts\n' + | 441 | '1.ts\n' + |
451 | '#EXTINF:10,\n' + | 442 | '#EXTINF:10,\n' + |
452 | '2.ts\n'); | 443 | '2.ts\n'); |
453 | 444 | ||
454 | QUnit.deepEqual(loader.media().segments[0], segment, 'preserved segment attributes'); | 445 | QUnit.deepEqual(loader.media().segments[0], segment, 'preserved segment attributes'); |
455 | }); | 446 | }); |
... | @@ -463,25 +454,25 @@ QUnit.test('clears the update timeout when switching quality', function() { | ... | @@ -463,25 +454,25 @@ QUnit.test('clears the update timeout when switching quality', function() { |
463 | refreshes++; | 454 | refreshes++; |
464 | }); | 455 | }); |
465 | // deliver the master | 456 | // deliver the master |
466 | respond(this.requests.pop(), | 457 | this.requests.pop().respond(200, null, |
467 | '#EXTM3U\n' + | 458 | '#EXTM3U\n' + |
468 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | 459 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + |
469 | 'live-low.m3u8\n' + | 460 | 'live-low.m3u8\n' + |
470 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | 461 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + |
471 | 'live-high.m3u8\n'); | 462 | 'live-high.m3u8\n'); |
472 | // deliver the low quality playlist | 463 | // deliver the low quality playlist |
473 | respond(this.requests.pop(), | 464 | this.requests.pop().respond(200, null, |
474 | '#EXTM3U\n' + | 465 | '#EXTM3U\n' + |
475 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 466 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
476 | '#EXTINF:10,\n' + | 467 | '#EXTINF:10,\n' + |
477 | 'low-0.ts\n'); | 468 | 'low-0.ts\n'); |
478 | // change to a higher quality playlist | 469 | // change to a higher quality playlist |
479 | loader.media('live-high.m3u8'); | 470 | loader.media('live-high.m3u8'); |
480 | respond(this.requests.pop(), | 471 | this.requests.pop().respond(200, null, |
481 | '#EXTM3U\n' + | 472 | '#EXTM3U\n' + |
482 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 473 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
483 | '#EXTINF:10,\n' + | 474 | '#EXTINF:10,\n' + |
484 | 'high-0.ts\n'); | 475 | 'high-0.ts\n'); |
485 | // trigger a refresh | 476 | // trigger a refresh |
486 | this.clock.tick(10 * 1000); | 477 | this.clock.tick(10 * 1000); |
487 | 478 | ||
... | @@ -493,18 +484,18 @@ QUnit.test('media-sequence updates are considered a playlist change', function() | ... | @@ -493,18 +484,18 @@ QUnit.test('media-sequence updates are considered a playlist change', function() |
493 | let loader = new PlaylistLoader('live.m3u8'); | 484 | let loader = new PlaylistLoader('live.m3u8'); |
494 | /* eslint-enable no-unused-vars */ | 485 | /* eslint-enable no-unused-vars */ |
495 | 486 | ||
496 | respond(this.requests.pop(), | 487 | this.requests.pop().respond(200, null, |
497 | '#EXTM3U\n' + | 488 | '#EXTM3U\n' + |
498 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 489 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
499 | '#EXTINF:10,\n' + | 490 | '#EXTINF:10,\n' + |
500 | '0.ts\n'); | 491 | '0.ts\n'); |
501 | // trigger a refresh | 492 | // trigger a refresh |
502 | this.clock.tick(10 * 1000); | 493 | this.clock.tick(10 * 1000); |
503 | respond(this.requests.pop(), | 494 | this.requests.pop().respond(200, null, |
504 | '#EXTM3U\n' + | 495 | '#EXTM3U\n' + |
505 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | 496 | '#EXT-X-MEDIA-SEQUENCE:1\n' + |
506 | '#EXTINF:10,\n' + | 497 | '#EXTINF:10,\n' + |
507 | '0.ts\n'); | 498 | '0.ts\n'); |
508 | // half the default target-duration | 499 | // half the default target-duration |
509 | this.clock.tick(5 * 1000); | 500 | this.clock.tick(5 * 1000); |
510 | 501 | ||
... | @@ -519,11 +510,11 @@ QUnit.test('emits an error if a media refresh fails', function() { | ... | @@ -519,11 +510,11 @@ QUnit.test('emits an error if a media refresh fails', function() { |
519 | loader.on('error', function() { | 510 | loader.on('error', function() { |
520 | errors++; | 511 | errors++; |
521 | }); | 512 | }); |
522 | respond(this.requests.pop(), | 513 | this.requests.pop().respond(200, null, |
523 | '#EXTM3U\n' + | 514 | '#EXTM3U\n' + |
524 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 515 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
525 | '#EXTINF:10,\n' + | 516 | '#EXTINF:10,\n' + |
526 | '0.ts\n'); | 517 | '0.ts\n'); |
527 | // trigger a refresh | 518 | // trigger a refresh |
528 | this.clock.tick(10 * 1000); | 519 | this.clock.tick(10 * 1000); |
529 | this.requests.pop().respond(500, null, errorResponseText); | 520 | this.requests.pop().respond(500, null, errorResponseText); |
... | @@ -538,26 +529,26 @@ QUnit.test('emits an error if a media refresh fails', function() { | ... | @@ -538,26 +529,26 @@ QUnit.test('emits an error if a media refresh fails', function() { |
538 | QUnit.test('switches media playlists when requested', function() { | 529 | QUnit.test('switches media playlists when requested', function() { |
539 | let loader = new PlaylistLoader('master.m3u8'); | 530 | let loader = new PlaylistLoader('master.m3u8'); |
540 | 531 | ||
541 | respond(this.requests.pop(), | 532 | this.requests.pop().respond(200, null, |
542 | '#EXTM3U\n' + | 533 | '#EXTM3U\n' + |
543 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | 534 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + |
544 | 'low.m3u8\n' + | 535 | 'low.m3u8\n' + |
545 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | 536 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + |
546 | 'high.m3u8\n'); | 537 | 'high.m3u8\n'); |
547 | respond(this.requests.pop(), | 538 | this.requests.pop().respond(200, null, |
548 | '#EXTM3U\n' + | 539 | '#EXTM3U\n' + |
549 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 540 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
550 | '#EXTINF:10,\n' + | 541 | '#EXTINF:10,\n' + |
551 | 'low-0.ts\n'); | 542 | 'low-0.ts\n'); |
552 | 543 | ||
553 | loader.media(loader.master.playlists[1]); | 544 | loader.media(loader.master.playlists[1]); |
554 | QUnit.strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state'); | 545 | QUnit.strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state'); |
555 | 546 | ||
556 | respond(this.requests.pop(), | 547 | this.requests.pop().respond(200, null, |
557 | '#EXTM3U\n' + | 548 | '#EXTM3U\n' + |
558 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 549 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
559 | '#EXTINF:10,\n' + | 550 | '#EXTINF:10,\n' + |
560 | 'high-0.ts\n'); | 551 | 'high-0.ts\n'); |
561 | QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'switched active media'); | 552 | QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'switched active media'); |
562 | QUnit.strictEqual(loader.media(), | 553 | QUnit.strictEqual(loader.media(), |
563 | loader.master.playlists[1], | 554 | loader.master.playlists[1], |
... | @@ -570,38 +561,38 @@ QUnit.test('can switch playlists immediately after the master is downloaded', fu | ... | @@ -570,38 +561,38 @@ QUnit.test('can switch playlists immediately after the master is downloaded', fu |
570 | loader.on('loadedplaylist', function() { | 561 | loader.on('loadedplaylist', function() { |
571 | loader.media('high.m3u8'); | 562 | loader.media('high.m3u8'); |
572 | }); | 563 | }); |
573 | respond(this.requests.pop(), | 564 | this.requests.pop().respond(200, null, |
574 | '#EXTM3U\n' + | 565 | '#EXTM3U\n' + |
575 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | 566 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + |
576 | 'low.m3u8\n' + | 567 | 'low.m3u8\n' + |
577 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | 568 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + |
578 | 'high.m3u8\n'); | 569 | 'high.m3u8\n'); |
579 | QUnit.equal(this.requests[0].url, urlTo('high.m3u8'), 'switched variants immediately'); | 570 | QUnit.equal(this.requests[0].url, urlTo('high.m3u8'), 'switched variants immediately'); |
580 | }); | 571 | }); |
581 | 572 | ||
582 | QUnit.test('can switch media playlists based on URI', function() { | 573 | QUnit.test('can switch media playlists based on URI', function() { |
583 | let loader = new PlaylistLoader('master.m3u8'); | 574 | let loader = new PlaylistLoader('master.m3u8'); |
584 | 575 | ||
585 | respond(this.requests.pop(), | 576 | this.requests.pop().respond(200, null, |
586 | '#EXTM3U\n' + | 577 | '#EXTM3U\n' + |
587 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | 578 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + |
588 | 'low.m3u8\n' + | 579 | 'low.m3u8\n' + |
589 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | 580 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + |
590 | 'high.m3u8\n'); | 581 | 'high.m3u8\n'); |
591 | respond(this.requests.pop(), | 582 | this.requests.pop().respond(200, null, |
592 | '#EXTM3U\n' + | 583 | '#EXTM3U\n' + |
593 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 584 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
594 | '#EXTINF:10,\n' + | 585 | '#EXTINF:10,\n' + |
595 | 'low-0.ts\n'); | 586 | 'low-0.ts\n'); |
596 | 587 | ||
597 | loader.media('high.m3u8'); | 588 | loader.media('high.m3u8'); |
598 | QUnit.strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state'); | 589 | QUnit.strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state'); |
599 | 590 | ||
600 | respond(this.requests.pop(), | 591 | this.requests.pop().respond(200, null, |
601 | '#EXTM3U\n' + | 592 | '#EXTM3U\n' + |
602 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 593 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
603 | '#EXTINF:10,\n' + | 594 | '#EXTINF:10,\n' + |
604 | 'high-0.ts\n'); | 595 | 'high-0.ts\n'); |
605 | QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'switched active media'); | 596 | QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'switched active media'); |
606 | QUnit.strictEqual(loader.media(), | 597 | QUnit.strictEqual(loader.media(), |
607 | loader.master.playlists[1], | 598 | loader.master.playlists[1], |
... | @@ -611,17 +602,17 @@ QUnit.test('can switch media playlists based on URI', function() { | ... | @@ -611,17 +602,17 @@ QUnit.test('can switch media playlists based on URI', function() { |
611 | QUnit.test('aborts in-flight playlist refreshes when switching', function() { | 602 | QUnit.test('aborts in-flight playlist refreshes when switching', function() { |
612 | let loader = new PlaylistLoader('master.m3u8'); | 603 | let loader = new PlaylistLoader('master.m3u8'); |
613 | 604 | ||
614 | respond(this.requests.pop(), | 605 | this.requests.pop().respond(200, null, |
615 | '#EXTM3U\n' + | 606 | '#EXTM3U\n' + |
616 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | 607 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + |
617 | 'low.m3u8\n' + | 608 | 'low.m3u8\n' + |
618 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | 609 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + |
619 | 'high.m3u8\n'); | 610 | 'high.m3u8\n'); |
620 | respond(this.requests.pop(), | 611 | this.requests.pop().respond(200, null, |
621 | '#EXTM3U\n' + | 612 | '#EXTM3U\n' + |
622 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 613 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
623 | '#EXTINF:10,\n' + | 614 | '#EXTINF:10,\n' + |
624 | 'low-0.ts\n'); | 615 | 'low-0.ts\n'); |
625 | this.clock.tick(10 * 1000); | 616 | this.clock.tick(10 * 1000); |
626 | loader.media('high.m3u8'); | 617 | loader.media('high.m3u8'); |
627 | QUnit.strictEqual(this.requests[0].aborted, true, 'aborted refresh request'); | 618 | QUnit.strictEqual(this.requests[0].aborted, true, 'aborted refresh request'); |
... | @@ -633,87 +624,86 @@ QUnit.test('aborts in-flight playlist refreshes when switching', function() { | ... | @@ -633,87 +624,86 @@ QUnit.test('aborts in-flight playlist refreshes when switching', function() { |
633 | QUnit.test('switching to the active playlist is a no-op', function() { | 624 | QUnit.test('switching to the active playlist is a no-op', function() { |
634 | let loader = new PlaylistLoader('master.m3u8'); | 625 | let loader = new PlaylistLoader('master.m3u8'); |
635 | 626 | ||
636 | respond(this.requests.pop(), | 627 | this.requests.pop().respond(200, null, |
637 | '#EXTM3U\n' + | 628 | '#EXTM3U\n' + |
638 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | 629 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + |
639 | 'low.m3u8\n' + | 630 | 'low.m3u8\n' + |
640 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | 631 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + |
641 | 'high.m3u8\n'); | 632 | 'high.m3u8\n'); |
642 | respond(this.requests.pop(), | 633 | this.requests.pop().respond(200, null, |
643 | '#EXTM3U\n' + | 634 | '#EXTM3U\n' + |
644 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 635 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
645 | '#EXTINF:10,\n' + | 636 | '#EXTINF:10,\n' + |
646 | 'low-0.ts\n' + | 637 | 'low-0.ts\n' + |
647 | '#EXT-X-ENDLIST\n'); | 638 | '#EXT-X-ENDLIST\n'); |
648 | loader.media('low.m3u8'); | 639 | loader.media('low.m3u8'); |
649 | 640 | ||
650 | QUnit.strictEqual(this.requests.length, 0, 'no this.requests are sent'); | 641 | QUnit.strictEqual(this.requests.length, 0, 'no requests are sent'); |
651 | }); | 642 | }); |
652 | 643 | ||
653 | QUnit.test('switching to the active live playlist is a no-op', function() { | 644 | QUnit.test('switching to the active live playlist is a no-op', function() { |
654 | let loader = new PlaylistLoader('master.m3u8'); | 645 | let loader = new PlaylistLoader('master.m3u8'); |
655 | 646 | ||
656 | respond(this.requests.pop(), | 647 | this.requests.pop().respond(200, null, |
657 | '#EXTM3U\n' + | 648 | '#EXTM3U\n' + |
658 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | 649 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + |
659 | 'low.m3u8\n' + | 650 | 'low.m3u8\n' + |
660 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | 651 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + |
661 | 'high.m3u8\n'); | 652 | 'high.m3u8\n'); |
662 | respond(this.requests.pop(), | 653 | this.requests.pop().respond(200, null, |
663 | '#EXTM3U\n' + | 654 | '#EXTM3U\n' + |
664 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 655 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
665 | '#EXTINF:10,\n' + | 656 | '#EXTINF:10,\n' + |
666 | 'low-0.ts\n'); | 657 | 'low-0.ts\n'); |
667 | loader.media('low.m3u8'); | 658 | loader.media('low.m3u8'); |
668 | 659 | ||
669 | QUnit.strictEqual(this.requests.length, 0, 'no this.requests are sent'); | 660 | QUnit.strictEqual(this.requests.length, 0, 'no requests are sent'); |
670 | }); | 661 | }); |
671 | 662 | ||
672 | QUnit.test('switches back to loaded playlists without re-requesting them', function() { | 663 | QUnit.test('switches back to loaded playlists without re-requesting them', function() { |
673 | let loader = new PlaylistLoader('master.m3u8'); | 664 | let loader = new PlaylistLoader('master.m3u8'); |
674 | 665 | ||
675 | respond(this.requests.pop(), | 666 | this.requests.pop().respond(200, null, |
676 | '#EXTM3U\n' + | 667 | '#EXTM3U\n' + |
677 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | 668 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + |
678 | 'low.m3u8\n' + | 669 | 'low.m3u8\n' + |
679 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | 670 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + |
680 | 'high.m3u8\n'); | 671 | 'high.m3u8\n'); |
681 | respond(this.requests.pop(), | 672 | this.requests.pop().respond(200, null, |
682 | '#EXTM3U\n' + | 673 | '#EXTM3U\n' + |
683 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 674 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
684 | '#EXTINF:10,\n' + | 675 | '#EXTINF:10,\n' + |
685 | 'low-0.ts\n' + | 676 | 'low-0.ts\n' + |
686 | '#EXT-X-ENDLIST\n'); | 677 | '#EXT-X-ENDLIST\n'); |
687 | loader.media('high.m3u8'); | 678 | loader.media('high.m3u8'); |
688 | respond(this.requests.pop(), | 679 | this.requests.pop().respond(200, null, |
689 | '#EXTM3U\n' + | 680 | '#EXTM3U\n' + |
690 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 681 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
691 | '#EXTINF:10,\n' + | 682 | '#EXTINF:10,\n' + |
692 | 'high-0.ts\n' + | 683 | 'high-0.ts\n' + |
693 | '#EXT-X-ENDLIST\n'); | 684 | '#EXT-X-ENDLIST\n'); |
694 | loader.media('low.m3u8'); | 685 | loader.media('low.m3u8'); |
695 | 686 | ||
696 | QUnit.strictEqual(this.requests.length, 0, 'no outstanding this.requests'); | 687 | QUnit.strictEqual(this.requests.length, 0, 'no outstanding requests'); |
697 | QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'returned to loaded playlist'); | 688 | QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'returned to loaded playlist'); |
698 | }); | 689 | }); |
699 | 690 | ||
700 | QUnit.test( | 691 | QUnit.test('aborts outstanding requests if switching back to an already loaded playlist', |
701 | 'aborts outstanding this.requests if switching back to an already loaded playlist', | ||
702 | function() { | 692 | function() { |
703 | let loader = new PlaylistLoader('master.m3u8'); | 693 | let loader = new PlaylistLoader('master.m3u8'); |
704 | 694 | ||
705 | respond(this.requests.pop(), | 695 | this.requests.pop().respond(200, null, |
706 | '#EXTM3U\n' + | 696 | '#EXTM3U\n' + |
707 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | 697 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + |
708 | 'low.m3u8\n' + | 698 | 'low.m3u8\n' + |
709 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | 699 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + |
710 | 'high.m3u8\n'); | 700 | 'high.m3u8\n'); |
711 | respond(this.requests.pop(), | 701 | this.requests.pop().respond(200, null, |
712 | '#EXTM3U\n' + | 702 | '#EXTM3U\n' + |
713 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 703 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
714 | '#EXTINF:10,\n' + | 704 | '#EXTINF:10,\n' + |
715 | 'low-0.ts\n' + | 705 | 'low-0.ts\n' + |
716 | '#EXT-X-ENDLIST\n'); | 706 | '#EXT-X-ENDLIST\n'); |
717 | loader.media('high.m3u8'); | 707 | loader.media('high.m3u8'); |
718 | loader.media('low.m3u8'); | 708 | loader.media('low.m3u8'); |
719 | 709 | ||
... | @@ -732,23 +722,22 @@ function() { | ... | @@ -732,23 +722,22 @@ function() { |
732 | 'switched to loaded playlist'); | 722 | 'switched to loaded playlist'); |
733 | }); | 723 | }); |
734 | 724 | ||
735 | QUnit.test( | 725 | QUnit.test('does not abort requests when the same playlist is re-requested', |
736 | 'does not abort this.requests when the same playlist is re-requested', | ||
737 | function() { | 726 | function() { |
738 | let loader = new PlaylistLoader('master.m3u8'); | 727 | let loader = new PlaylistLoader('master.m3u8'); |
739 | 728 | ||
740 | respond(this.requests.pop(), | 729 | this.requests.pop().respond(200, null, |
741 | '#EXTM3U\n' + | 730 | '#EXTM3U\n' + |
742 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | 731 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + |
743 | 'low.m3u8\n' + | 732 | 'low.m3u8\n' + |
744 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | 733 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + |
745 | 'high.m3u8\n'); | 734 | 'high.m3u8\n'); |
746 | respond(this.requests.pop(), | 735 | this.requests.pop().respond(200, null, |
747 | '#EXTM3U\n' + | 736 | '#EXTM3U\n' + |
748 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 737 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
749 | '#EXTINF:10,\n' + | 738 | '#EXTINF:10,\n' + |
750 | 'low-0.ts\n' + | 739 | 'low-0.ts\n' + |
751 | '#EXT-X-ENDLIST\n'); | 740 | '#EXT-X-ENDLIST\n'); |
752 | loader.media('high.m3u8'); | 741 | loader.media('high.m3u8'); |
753 | loader.media('high.m3u8'); | 742 | loader.media('high.m3u8'); |
754 | 743 | ||
... | @@ -763,23 +752,22 @@ QUnit.test('throws an error if a media switch is initiated too early', function( | ... | @@ -763,23 +752,22 @@ QUnit.test('throws an error if a media switch is initiated too early', function( |
763 | loader.media('high.m3u8'); | 752 | loader.media('high.m3u8'); |
764 | }, 'threw an error from HAVE_NOTHING'); | 753 | }, 'threw an error from HAVE_NOTHING'); |
765 | 754 | ||
766 | respond(this.requests.pop(), | 755 | this.requests.pop().respond(200, null, |
767 | '#EXTM3U\n' + | 756 | '#EXTM3U\n' + |
768 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | 757 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + |
769 | 'low.m3u8\n' + | 758 | 'low.m3u8\n' + |
770 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | 759 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + |
771 | 'high.m3u8\n'); | 760 | 'high.m3u8\n'); |
772 | }); | 761 | }); |
773 | 762 | ||
774 | QUnit.test( | 763 | QUnit.test('throws an error if a switch to an unrecognized playlist is requested', |
775 | 'throws an error if a switch to an unrecognized playlist is requested', | ||
776 | function() { | 764 | function() { |
777 | let loader = new PlaylistLoader('master.m3u8'); | 765 | let loader = new PlaylistLoader('master.m3u8'); |
778 | 766 | ||
779 | respond(this.requests.pop(), | 767 | this.requests.pop().respond(200, null, |
780 | '#EXTM3U\n' + | 768 | '#EXTM3U\n' + |
781 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | 769 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + |
782 | 'media.m3u8\n'); | 770 | 'media.m3u8\n'); |
783 | 771 | ||
784 | QUnit.throws(function() { | 772 | QUnit.throws(function() { |
785 | loader.media('unrecognized.m3u8'); | 773 | loader.media('unrecognized.m3u8'); |
... | @@ -789,11 +777,11 @@ function() { | ... | @@ -789,11 +777,11 @@ function() { |
789 | QUnit.test('dispose cancels the refresh timeout', function() { | 777 | QUnit.test('dispose cancels the refresh timeout', function() { |
790 | let loader = new PlaylistLoader('live.m3u8'); | 778 | let loader = new PlaylistLoader('live.m3u8'); |
791 | 779 | ||
792 | respond(this.requests.pop(), | 780 | this.requests.pop().respond(200, null, |
793 | '#EXTM3U\n' + | 781 | '#EXTM3U\n' + |
794 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 782 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
795 | '#EXTINF:10,\n' + | 783 | '#EXTINF:10,\n' + |
796 | '0.ts\n'); | 784 | '0.ts\n'); |
797 | loader.dispose(); | 785 | loader.dispose(); |
798 | // a lot of time passes... | 786 | // a lot of time passes... |
799 | this.clock.tick(15 * 1000); | 787 | this.clock.tick(15 * 1000); |
... | @@ -801,25 +789,24 @@ QUnit.test('dispose cancels the refresh timeout', function() { | ... | @@ -801,25 +789,24 @@ QUnit.test('dispose cancels the refresh timeout', function() { |
801 | QUnit.strictEqual(this.requests.length, 0, 'no refresh request was made'); | 789 | QUnit.strictEqual(this.requests.length, 0, 'no refresh request was made'); |
802 | }); | 790 | }); |
803 | 791 | ||
804 | QUnit.test('dispose aborts pending refresh this.requests', function() { | 792 | QUnit.test('dispose aborts pending refresh requests', function() { |
805 | let loader = new PlaylistLoader('live.m3u8'); | 793 | let loader = new PlaylistLoader('live.m3u8'); |
806 | 794 | ||
807 | respond(this.requests.pop(), | 795 | this.requests.pop().respond(200, null, |
808 | '#EXTM3U\n' + | 796 | '#EXTM3U\n' + |
809 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 797 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
810 | '#EXTINF:10,\n' + | 798 | '#EXTINF:10,\n' + |
811 | '0.ts\n'); | 799 | '0.ts\n'); |
812 | this.clock.tick(10 * 1000); | 800 | this.clock.tick(10 * 1000); |
813 | 801 | ||
814 | loader.dispose(); | 802 | loader.dispose(); |
815 | QUnit.ok(this.requests[0].aborted, 'refresh request aborted'); | 803 | QUnit.ok(this.requests[0].aborted, 'refresh request aborted'); |
816 | QUnit.ok( | 804 | QUnit.ok(!this.requests[0].onreadystatechange, |
817 | !this.requests[0].onreadystatechange, | 805 | 'onreadystatechange handler should not exist after dispose called' |
818 | 'onreadystatechange handler should not exist after dispose called' | ||
819 | ); | 806 | ); |
820 | }); | 807 | }); |
821 | 808 | ||
822 | QUnit.test('errors if this.requests take longer than 45s', function() { | 809 | QUnit.test('errors if requests take longer than 45s', function() { |
823 | let loader = new PlaylistLoader('media.m3u8'); | 810 | let loader = new PlaylistLoader('media.m3u8'); |
824 | let errors = 0; | 811 | let errors = 0; |
825 | 812 | ||
... | @@ -839,29 +826,29 @@ QUnit.test('triggers an event when the active media changes', function() { | ... | @@ -839,29 +826,29 @@ QUnit.test('triggers an event when the active media changes', function() { |
839 | loader.on('mediachange', function() { | 826 | loader.on('mediachange', function() { |
840 | mediaChanges++; | 827 | mediaChanges++; |
841 | }); | 828 | }); |
842 | respond(this.requests.pop(), | 829 | this.requests.pop().respond(200, null, |
843 | '#EXTM3U\n' + | 830 | '#EXTM3U\n' + |
844 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | 831 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + |
845 | 'low.m3u8\n' + | 832 | 'low.m3u8\n' + |
846 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | 833 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + |
847 | 'high.m3u8\n'); | 834 | 'high.m3u8\n'); |
848 | respond(this.requests.shift(), | 835 | this.requests.shift().respond(200, null, |
849 | '#EXTM3U\n' + | 836 | '#EXTM3U\n' + |
850 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 837 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
851 | '#EXTINF:10,\n' + | 838 | '#EXTINF:10,\n' + |
852 | 'low-0.ts\n' + | 839 | 'low-0.ts\n' + |
853 | '#EXT-X-ENDLIST\n'); | 840 | '#EXT-X-ENDLIST\n'); |
854 | QUnit.strictEqual(mediaChanges, 0, 'initial selection is not a media change'); | 841 | QUnit.strictEqual(mediaChanges, 0, 'initial selection is not a media change'); |
855 | 842 | ||
856 | loader.media('high.m3u8'); | 843 | loader.media('high.m3u8'); |
857 | QUnit.strictEqual(mediaChanges, 0, 'mediachange does not fire immediately'); | 844 | QUnit.strictEqual(mediaChanges, 0, 'mediachange does not fire immediately'); |
858 | 845 | ||
859 | respond(this.requests.shift(), | 846 | this.requests.shift().respond(200, null, |
860 | '#EXTM3U\n' + | 847 | '#EXTM3U\n' + |
861 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 848 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
862 | '#EXTINF:10,\n' + | 849 | '#EXTINF:10,\n' + |
863 | 'high-0.ts\n' + | 850 | 'high-0.ts\n' + |
864 | '#EXT-X-ENDLIST\n'); | 851 | '#EXT-X-ENDLIST\n'); |
865 | QUnit.strictEqual(mediaChanges, 1, 'fired a mediachange'); | 852 | QUnit.strictEqual(mediaChanges, 1, 'fired a mediachange'); |
866 | 853 | ||
867 | // switch back to an already loaded playlist | 854 | // switch back to an already loaded playlist |
... | @@ -876,16 +863,16 @@ QUnit.test('triggers an event when the active media changes', function() { | ... | @@ -876,16 +863,16 @@ QUnit.test('triggers an event when the active media changes', function() { |
876 | QUnit.test('can get media index by playback position for non-live videos', function() { | 863 | QUnit.test('can get media index by playback position for non-live videos', function() { |
877 | let loader = new PlaylistLoader('media.m3u8'); | 864 | let loader = new PlaylistLoader('media.m3u8'); |
878 | 865 | ||
879 | respond(this.requests.shift(), | 866 | this.requests.shift().respond(200, null, |
880 | '#EXTM3U\n' + | 867 | '#EXTM3U\n' + |
881 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 868 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
882 | '#EXTINF:4,\n' + | 869 | '#EXTINF:4,\n' + |
883 | '0.ts\n' + | 870 | '0.ts\n' + |
884 | '#EXTINF:5,\n' + | 871 | '#EXTINF:5,\n' + |
885 | '1.ts\n' + | 872 | '1.ts\n' + |
886 | '#EXTINF:6,\n' + | 873 | '#EXTINF:6,\n' + |
887 | '2.ts\n' + | 874 | '2.ts\n' + |
888 | '#EXT-X-ENDLIST\n'); | 875 | '#EXT-X-ENDLIST\n'); |
889 | 876 | ||
890 | QUnit.equal(loader.getMediaIndexForTime_(-1), | 877 | QUnit.equal(loader.getMediaIndexForTime_(-1), |
891 | 0, | 878 | 0, |
... | @@ -901,31 +888,30 @@ QUnit.test('can get media index by playback position for non-live videos', funct | ... | @@ -901,31 +888,30 @@ QUnit.test('can get media index by playback position for non-live videos', funct |
901 | QUnit.test('returns the lower index when calculating for a segment boundary', function() { | 888 | QUnit.test('returns the lower index when calculating for a segment boundary', function() { |
902 | let loader = new PlaylistLoader('media.m3u8'); | 889 | let loader = new PlaylistLoader('media.m3u8'); |
903 | 890 | ||
904 | respond(this.requests.shift(), | 891 | this.requests.shift().respond(200, null, |
905 | '#EXTM3U\n' + | 892 | '#EXTM3U\n' + |
906 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 893 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
907 | '#EXTINF:4,\n' + | 894 | '#EXTINF:4,\n' + |
908 | '0.ts\n' + | 895 | '0.ts\n' + |
909 | '#EXTINF:5,\n' + | 896 | '#EXTINF:5,\n' + |
910 | '1.ts\n' + | 897 | '1.ts\n' + |
911 | '#EXT-X-ENDLIST\n'); | 898 | '#EXT-X-ENDLIST\n'); |
912 | QUnit.equal(loader.getMediaIndexForTime_(4), 1, 'rounds up exact matches'); | 899 | QUnit.equal(loader.getMediaIndexForTime_(4), 1, 'rounds up exact matches'); |
913 | QUnit.equal(loader.getMediaIndexForTime_(3.7), 0, 'rounds down'); | 900 | QUnit.equal(loader.getMediaIndexForTime_(3.7), 0, 'rounds down'); |
914 | QUnit.equal(loader.getMediaIndexForTime_(4.5), 1, 'rounds up at 0.5'); | 901 | QUnit.equal(loader.getMediaIndexForTime_(4.5), 1, 'rounds up at 0.5'); |
915 | }); | 902 | }); |
916 | 903 | ||
917 | QUnit.test( | 904 | QUnit.test('accounts for non-zero starting segment time when calculating media index', |
918 | 'accounts for non-zero starting segment time when calculating media index', | ||
919 | function() { | 905 | function() { |
920 | let loader = new PlaylistLoader('media.m3u8'); | 906 | let loader = new PlaylistLoader('media.m3u8'); |
921 | 907 | ||
922 | respond(this.requests.shift(), | 908 | this.requests.shift().respond(200, null, |
923 | '#EXTM3U\n' + | 909 | '#EXTM3U\n' + |
924 | '#EXT-X-MEDIA-SEQUENCE:1001\n' + | 910 | '#EXT-X-MEDIA-SEQUENCE:1001\n' + |
925 | '#EXTINF:4,\n' + | 911 | '#EXTINF:4,\n' + |
926 | '1001.ts\n' + | 912 | '1001.ts\n' + |
927 | '#EXTINF:5,\n' + | 913 | '#EXTINF:5,\n' + |
928 | '1002.ts\n'); | 914 | '1002.ts\n'); |
929 | loader.media().segments[0].end = 154; | 915 | loader.media().segments[0].end = 154; |
930 | 916 | ||
931 | QUnit.equal(loader.getMediaIndexForTime_(0), | 917 | QUnit.equal(loader.getMediaIndexForTime_(0), |
... | @@ -943,9 +929,6 @@ function() { | ... | @@ -943,9 +929,6 @@ function() { |
943 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 2), | 929 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 2), |
944 | 0, | 930 | 0, |
945 | 'calculates within the first segment'); | 931 | 'calculates within the first segment'); |
946 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 2), | ||
947 | 0, | ||
948 | 'calculates within the first segment'); | ||
949 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 4), | 932 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 4), |
950 | 1, | 933 | 1, |
951 | 'calculates within the second segment'); | 934 | 'calculates within the second segment'); |
... | @@ -961,13 +944,13 @@ QUnit.test('prefers precise segment timing when tracking expired time', function | ... | @@ -961,13 +944,13 @@ QUnit.test('prefers precise segment timing when tracking expired time', function |
961 | let loader = new PlaylistLoader('media.m3u8'); | 944 | let loader = new PlaylistLoader('media.m3u8'); |
962 | 945 | ||
963 | loader.trigger('firstplay'); | 946 | loader.trigger('firstplay'); |
964 | respond(this.requests.shift(), | 947 | this.requests.shift().respond(200, null, |
965 | '#EXTM3U\n' + | 948 | '#EXTM3U\n' + |
966 | '#EXT-X-MEDIA-SEQUENCE:1001\n' + | 949 | '#EXT-X-MEDIA-SEQUENCE:1001\n' + |
967 | '#EXTINF:4,\n' + | 950 | '#EXTINF:4,\n' + |
968 | '1001.ts\n' + | 951 | '1001.ts\n' + |
969 | '#EXTINF:5,\n' + | 952 | '#EXTINF:5,\n' + |
970 | '1002.ts\n'); | 953 | '1002.ts\n'); |
971 | // setup the loader with an "imprecise" value as if it had been | 954 | // setup the loader with an "imprecise" value as if it had been |
972 | // accumulating segment durations as they expire | 955 | // accumulating segment durations as they expire |
973 | loader.expired_ = 160; | 956 | loader.expired_ = 160; |
... | @@ -981,11 +964,11 @@ QUnit.test('prefers precise segment timing when tracking expired time', function | ... | @@ -981,11 +964,11 @@ QUnit.test('prefers precise segment timing when tracking expired time', function |
981 | 964 | ||
982 | // trigger a playlist refresh | 965 | // trigger a playlist refresh |
983 | this.clock.tick(10 * 1000); | 966 | this.clock.tick(10 * 1000); |
984 | respond(this.requests.shift(), | 967 | this.requests.shift().respond(200, null, |
985 | '#EXTM3U\n' + | 968 | '#EXTM3U\n' + |
986 | '#EXT-X-MEDIA-SEQUENCE:1002\n' + | 969 | '#EXT-X-MEDIA-SEQUENCE:1002\n' + |
987 | '#EXTINF:5,\n' + | 970 | '#EXTINF:5,\n' + |
988 | '1002.ts\n'); | 971 | '1002.ts\n'); |
989 | QUnit.equal(loader.getMediaIndexForTime_(150 + 4 + 1), | 972 | QUnit.equal(loader.getMediaIndexForTime_(150 + 4 + 1), |
990 | 0, | 973 | 0, |
991 | 'tracks precise expired times'); | 974 | 'tracks precise expired times'); |
... | @@ -994,13 +977,13 @@ QUnit.test('prefers precise segment timing when tracking expired time', function | ... | @@ -994,13 +977,13 @@ QUnit.test('prefers precise segment timing when tracking expired time', function |
994 | QUnit.test('accounts for expired time when calculating media index', function() { | 977 | QUnit.test('accounts for expired time when calculating media index', function() { |
995 | let loader = new PlaylistLoader('media.m3u8'); | 978 | let loader = new PlaylistLoader('media.m3u8'); |
996 | 979 | ||
997 | respond(this.requests.shift(), | 980 | this.requests.shift().respond(200, null, |
998 | '#EXTM3U\n' + | 981 | '#EXTM3U\n' + |
999 | '#EXT-X-MEDIA-SEQUENCE:1001\n' + | 982 | '#EXT-X-MEDIA-SEQUENCE:1001\n' + |
1000 | '#EXTINF:4,\n' + | 983 | '#EXTINF:4,\n' + |
1001 | '1001.ts\n' + | 984 | '1001.ts\n' + |
1002 | '#EXTINF:5,\n' + | 985 | '#EXTINF:5,\n' + |
1003 | '1002.ts\n'); | 986 | '1002.ts\n'); |
1004 | loader.expired_ = 150; | 987 | loader.expired_ = 150; |
1005 | 988 | ||
1006 | QUnit.equal(loader.getMediaIndexForTime_(0), | 989 | QUnit.equal(loader.getMediaIndexForTime_(0), |
... | @@ -1015,9 +998,6 @@ QUnit.test('accounts for expired time when calculating media index', function() | ... | @@ -1015,9 +998,6 @@ QUnit.test('accounts for expired time when calculating media index', function() |
1015 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 2), | 998 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 2), |
1016 | 0, | 999 | 0, |
1017 | 'calculates within the first segment'); | 1000 | 'calculates within the first segment'); |
1018 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 2), | ||
1019 | 0, | ||
1020 | 'calculates within the first segment'); | ||
1021 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 4.5), | 1001 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 4.5), |
1022 | 1, | 1002 | 1, |
1023 | 'calculates within the second segment'); | 1003 | 'calculates within the second segment'); |
... | @@ -1030,11 +1010,11 @@ QUnit.test('does not misintrepret playlists missing newlines at the end', functi | ... | @@ -1030,11 +1010,11 @@ QUnit.test('does not misintrepret playlists missing newlines at the end', functi |
1030 | let loader = new PlaylistLoader('media.m3u8'); | 1010 | let loader = new PlaylistLoader('media.m3u8'); |
1031 | 1011 | ||
1032 | // no newline | 1012 | // no newline |
1033 | respond(this.requests.shift(), | 1013 | this.requests.shift().respond(200, null, |
1034 | '#EXTM3U\n' + | 1014 | '#EXTM3U\n' + |
1035 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 1015 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
1036 | '#EXTINF:10,\n' + | 1016 | '#EXTINF:10,\n' + |
1037 | 'low-0.ts\n' + | 1017 | 'low-0.ts\n' + |
1038 | '#EXT-X-ENDLIST'); | 1018 | '#EXT-X-ENDLIST'); |
1039 | QUnit.ok(loader.media().endList, 'flushed the final line of input'); | 1019 | QUnit.ok(loader.media().endList, 'flushed the final line of input'); |
1040 | }); | 1020 | }); | ... | ... |
-
Please register or sign in to post a comment