0de2141b by brandonocasey

fixed style issues, bare playlist-loader conversion

fixed pre-existing assertion duplication issues
1 parent a07f52ee
...@@ -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 });
......