5a33fc57 by brandonocasey

browserify-p4: playlist*, xhr, and resolve-url

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