UNPKG

39.7 kBJavaScriptView Raw
1import QUnit from 'qunit';
2import videojs from 'video.js';
3import xhrFactory from '../src/xhr';
4import Config from '../src/config';
5import {
6 playlistWithDuration,
7 useFakeEnvironment,
8 useFakeMediaSource
9} from './test-helpers.js';
10import { MasterPlaylistController } from '../src/master-playlist-controller';
11import SyncController from '../src/sync-controller';
12import Decrypter from '../src/decrypter-worker';
13import worker from 'webworkify';
14
15/**
16 * beforeEach and afterEach hooks that should be run segment loader tests regardless of
17 * the type of loader.
18 */
19export const LoaderCommonHooks = {
20 beforeEach(assert) {
21 this.env = useFakeEnvironment(assert);
22 this.clock = this.env.clock;
23 this.requests = this.env.requests;
24 this.mse = useFakeMediaSource();
25 this.currentTime = 0;
26 this.seekable = {
27 length: 0
28 };
29 this.seeking = false;
30 this.hasPlayed = true;
31 this.paused = false;
32 this.playbackRate = 1;
33 this.fakeHls = {
34 xhr: xhrFactory(),
35 tech_: {
36 paused: () => this.paused,
37 playbackRate: () => this.playbackRate,
38 currentTime: () => this.currentTime
39 }
40 };
41 this.tech_ = this.fakeHls.tech_;
42 this.goalBufferLength =
43 MasterPlaylistController.prototype.goalBufferLength.bind(this);
44 this.mediaSource = new videojs.MediaSource();
45 this.mediaSource.trigger('sourceopen');
46 this.syncController = new SyncController();
47 this.decrypter = worker(Decrypter);
48 },
49 afterEach(assert) {
50 this.env.restore();
51 this.mse.restore();
52 this.decrypter.terminate();
53 }
54};
55
56/**
57 * Returns a settings object containing the custom settings provided merged with defaults
58 * for use in constructing a segment loader. This function should be called with the QUnit
59 * test environment the loader will be constructed in for proper this reference.
60 *
61 * @param {Object} settings
62 * custom settings for the loader
63 * @return {Object}
64 * Settings object containing custom settings merged with defaults
65 */
66export const LoaderCommonSettings = function(settings) {
67 return videojs.mergeOptions({
68 hls: this.fakeHls,
69 currentTime: () => this.currentTime,
70 seekable: () => this.seekable,
71 seeking: () => this.seeking,
72 hasPlayed: () => this.hasPlayed,
73 duration: () => this.mediaSource.duration,
74 goalBufferLength: () => this.goalBufferLength(),
75 mediaSource: this.mediaSource,
76 syncController: this.syncController,
77 decrypter: this.decrypter
78 }, settings);
79};
80
81/**
82 * Sets up a QUnit module to run tests that should be run on all segment loader types.
83 * Currently only two types, SegmentLoader and VTTSegmentLoader.
84 *
85 * @param {function(new:SegmentLoader|VTTLoader, Object)} LoaderConstructor
86 * Constructor for segment loader. Takes one parameter, a settings object
87 * @param {Object} loaderSettings
88 * Custom settings to merge with defaults for the provided loader constructor
89 * @param {function(SegmentLoader|VTTLoader)} loaderBeforeEach
90 * Function to be run in the beforeEach after loader creation. Takes one parameter,
91 * the loader for custom modifications to the loader object.
92 */
93export const LoaderCommonFactory = (LoaderConstructor,
94 loaderSettings,
95 loaderBeforeEach) => {
96 let loader;
97
98 QUnit.module('Loader Common', function(hooks) {
99 hooks.beforeEach(function(assert) {
100 // Assume this module is nested and the parent module uses CommonHooks.beforeEach
101
102 loader = new LoaderConstructor(LoaderCommonSettings.call(this, loaderSettings), {});
103
104 loaderBeforeEach(loader);
105
106 // shim updateend trigger to be a noop if the loader has no media source
107 this.updateend = function() {
108 if (loader.mediaSource_) {
109 loader.mediaSource_.sourceBuffers[0].trigger('updateend');
110 }
111 };
112 });
113
114 QUnit.test('fails without required initialization options', function(assert) {
115 /* eslint-disable no-new */
116 assert.throws(function() {
117 new LoaderConstructor();
118 }, 'requires options');
119 assert.throws(function() {
120 new LoaderConstructor({});
121 }, 'requires a currentTime callback');
122 assert.throws(function() {
123 new LoaderConstructor({
124 currentTime() {}
125 });
126 }, 'requires a media source');
127 /* eslint-enable */
128 });
129
130 QUnit.test('calling load is idempotent', function(assert) {
131 loader.playlist(playlistWithDuration(20));
132
133 loader.load();
134 this.clock.tick(1);
135
136 assert.equal(loader.state, 'WAITING', 'moves to the ready state');
137 assert.equal(this.requests.length, 1, 'made one request');
138
139 loader.load();
140 assert.equal(loader.state, 'WAITING', 'still in the ready state');
141 assert.equal(this.requests.length, 1, 'still one request');
142
143 // some time passes and a response is received
144 this.clock.tick(100);
145 this.requests[0].response = new Uint8Array(10).buffer;
146 this.requests.shift().respond(200, null, '');
147 loader.load();
148 assert.equal(this.requests.length, 0, 'load has no effect');
149
150 // verify stats
151 assert.equal(loader.mediaBytesTransferred, 10, '10 bytes');
152 assert.equal(loader.mediaTransferDuration, 100, '100 ms (clock above)');
153 assert.equal(loader.mediaRequests, 1, '1 request');
154 });
155
156 QUnit.test('calling load should unpause', function(assert) {
157 loader.playlist(playlistWithDuration(20));
158 loader.pause();
159
160 loader.load();
161 this.clock.tick(1);
162 assert.equal(loader.paused(), false, 'loading unpauses');
163
164 loader.pause();
165 this.clock.tick(1);
166 this.requests[0].response = new Uint8Array(10).buffer;
167 this.requests.shift().respond(200, null, '');
168
169 assert.equal(loader.paused(), true, 'stayed paused');
170 loader.load();
171 assert.equal(loader.paused(), false, 'unpaused during processing');
172
173 loader.pause();
174
175 this.updateend();
176
177 assert.equal(loader.state, 'READY', 'finished processing');
178 assert.ok(loader.paused(), 'stayed paused');
179
180 loader.load();
181 assert.equal(loader.paused(), false, 'unpaused');
182
183 // verify stats
184 assert.equal(loader.mediaBytesTransferred, 10, '10 bytes');
185 assert.equal(loader.mediaTransferDuration, 1, '1 ms (clock above)');
186 assert.equal(loader.mediaRequests, 1, '1 request');
187 });
188
189 QUnit.test('regularly checks the buffer while unpaused', function(assert) {
190 loader.playlist(playlistWithDuration(90));
191
192 loader.load();
193 this.clock.tick(1);
194
195 // fill the buffer
196 this.clock.tick(1);
197 this.requests[0].response = new Uint8Array(10).buffer;
198 this.requests.shift().respond(200, null, '');
199
200 loader.buffered_ = () => videojs.createTimeRanges([[
201 0, Config.GOAL_BUFFER_LENGTH
202 ]]);
203
204 this.updateend();
205
206 assert.equal(this.requests.length, 0, 'no outstanding requests');
207
208 // play some video to drain the buffer
209 this.currentTime = Config.GOAL_BUFFER_LENGTH;
210 this.clock.tick(10 * 1000);
211 assert.equal(this.requests.length, 1, 'requested another segment');
212
213 // verify stats
214 assert.equal(loader.mediaBytesTransferred, 10, '10 bytes');
215 assert.equal(loader.mediaTransferDuration, 1, '1 ms (clock above)');
216 assert.equal(loader.mediaRequests, 1, '1 request');
217 });
218
219 QUnit.test('does not check the buffer while paused', function(assert) {
220 loader.playlist(playlistWithDuration(90));
221
222 loader.load();
223 this.clock.tick(1);
224
225 loader.pause();
226 this.clock.tick(1);
227 this.requests[0].response = new Uint8Array(10).buffer;
228 this.requests.shift().respond(200, null, '');
229
230 this.updateend();
231
232 this.clock.tick(10 * 1000);
233 assert.equal(this.requests.length, 0, 'did not make a request');
234
235 // verify stats
236 assert.equal(loader.mediaBytesTransferred, 10, '10 bytes');
237 assert.equal(loader.mediaTransferDuration, 1, '1 ms (clock above)');
238 assert.equal(loader.mediaRequests, 1, '1 request');
239 });
240
241 QUnit.test('calculates bandwidth after downloading a segment', function(assert) {
242 loader.playlist(playlistWithDuration(10));
243
244 loader.load();
245 this.clock.tick(1);
246
247 // some time passes and a response is received
248 this.clock.tick(100);
249 this.requests[0].response = new Uint8Array(10).buffer;
250 this.requests.shift().respond(200, null, '');
251
252 assert.equal(loader.bandwidth, (10 / 100) * 8 * 1000, 'calculated bandwidth');
253 assert.equal(loader.roundTrip, 100, 'saves request round trip time');
254
255 // TODO: Bandwidth Stat will be stale??
256 // verify stats
257 assert.equal(loader.mediaBytesTransferred, 10, '10 bytes');
258 assert.equal(loader.mediaTransferDuration, 100, '100 ms (clock above)');
259 });
260
261 QUnit.test('segment request timeouts reset bandwidth', function(assert) {
262 loader.playlist(playlistWithDuration(10));
263
264 loader.load();
265 this.clock.tick(1);
266
267 // a lot of time passes so the request times out
268 this.requests[0].timedout = true;
269 this.clock.tick(100 * 1000);
270
271 assert.equal(loader.bandwidth, 1, 'reset bandwidth');
272 assert.ok(isNaN(loader.roundTrip), 'reset round trip time');
273 });
274
275 QUnit.test('progress on segment requests are redispatched', function(assert) {
276 let progressEvents = 0;
277
278 loader.on('progress', function() {
279 progressEvents++;
280 });
281 loader.playlist(playlistWithDuration(10));
282
283 loader.load();
284 this.clock.tick(1);
285
286 this.requests[0].dispatchEvent({ type: 'progress', target: this.requests[0] });
287 assert.equal(progressEvents, 1, 'triggered progress');
288 });
289
290 QUnit.test('aborts request at progress events if bandwidth is too low',
291 function(assert) {
292 const playlist1 = playlistWithDuration(10, { uri: 'playlist1.m3u8' });
293 const playlist2 = playlistWithDuration(10, { uri: 'playlist2.m3u8' });
294 const playlist3 = playlistWithDuration(10, { uri: 'playlist3.m3u8' });
295 const playlist4 = playlistWithDuration(10, { uri: 'playlist4.m3u8' });
296 const xhrOptions = {
297 timeout: 15000
298 };
299 let bandwidthupdates = 0;
300 let firstProgress = false;
301
302 playlist1.attributes.BANDWIDTH = 18000;
303 playlist2.attributes.BANDWIDTH = 10000;
304 playlist3.attributes.BANDWIDTH = 8888;
305 playlist4.attributes.BANDWIDTH = 7777;
306
307 loader.hls_.playlists = {
308 master: {
309 playlists: [
310 playlist1,
311 playlist2,
312 playlist3,
313 playlist4
314 ]
315 }
316 };
317
318 const oldHandleProgress = loader.handleProgress_.bind(loader);
319
320 loader.handleProgress_ = (event, simpleSegment) => {
321 if (!firstProgress) {
322 firstProgress = true;
323 assert.equal(simpleSegment.stats.firstBytesReceivedAt, Date.now(),
324 'firstBytesReceivedAt timestamp added on first progress event with bytes');
325 }
326 oldHandleProgress(event, simpleSegment);
327 };
328
329 let earlyAborts = 0;
330
331 loader.on('earlyabort', () => earlyAborts++);
332
333 loader.on('bandwidthupdate', () => bandwidthupdates++);
334 loader.playlist(playlist1, xhrOptions);
335 loader.load();
336
337 this.clock.tick(1);
338
339 this.requests[0].dispatchEvent({
340 type: 'progress',
341 target: this.requests[0],
342 loaded: 1
343 });
344
345 assert.equal(bandwidthupdates, 0, 'no bandwidth updates yet');
346 assert.notOk(this.requests[0].aborted, 'request not prematurely aborted');
347 assert.equal(earlyAborts, 0, 'no earlyabort events');
348
349 this.clock.tick(999);
350
351 this.requests[0].dispatchEvent({
352 type: 'progress',
353 target: this.requests[0],
354 loaded: 2000
355 });
356
357 assert.equal(bandwidthupdates, 0, 'no bandwidth updates yet');
358 assert.notOk(this.requests[0].aborted, 'request not prematurely aborted');
359 assert.equal(earlyAborts, 0, 'no earlyabort events');
360
361 this.clock.tick(2);
362
363 this.requests[0].dispatchEvent({
364 type: 'progress',
365 target: this.requests[0],
366 loaded: 2001
367 });
368
369 assert.equal(bandwidthupdates, 0, 'bandwidth not updated');
370 assert.ok(this.requests[0].aborted, 'request aborted');
371 assert.equal(earlyAborts, 1, 'earlyabort event triggered');
372 });
373
374 QUnit.test(
375 'appending a segment when loader is in walk-forward mode triggers bandwidthupdate',
376 function(assert) {
377 let progresses = 0;
378
379 loader.on('bandwidthupdate', function() {
380 progresses++;
381 });
382 loader.playlist(playlistWithDuration(20));
383
384 loader.load();
385 this.clock.tick(1);
386
387 // some time passes and a response is received
388 this.requests[0].response = new Uint8Array(10).buffer;
389 this.requests.shift().respond(200, null, '');
390
391 this.updateend();
392
393 assert.equal(progresses, 0, 'no bandwidthupdate fired');
394
395 this.clock.tick(2);
396 // if mediaIndex is set, then the SegmentLoader is in walk-forward mode
397 loader.mediaIndex = 1;
398
399 // some time passes and a response is received
400 this.requests[0].response = new Uint8Array(10).buffer;
401 this.requests.shift().respond(200, null, '');
402
403 this.updateend();
404
405 assert.equal(progresses, 1, 'fired bandwidthupdate');
406
407 // verify stats
408 assert.equal(loader.mediaBytesTransferred, 20, '20 bytes');
409 assert.equal(loader.mediaRequests, 2, '2 request');
410 });
411
412 QUnit.test('only requests one segment at a time', function(assert) {
413 loader.playlist(playlistWithDuration(10));
414
415 loader.load();
416 this.clock.tick(1);
417
418 // a bunch of time passes without recieving a response
419 this.clock.tick(20 * 1000);
420 assert.equal(this.requests.length, 1, 'only one request was made');
421 });
422
423 QUnit.test('downloads init segments if specified', function(assert) {
424 let playlist = playlistWithDuration(20);
425 let map = {
426 resolvedUri: 'mainInitSegment',
427 byterange: {
428 length: 20,
429 offset: 0
430 }
431 };
432
433 let buffered = videojs.createTimeRanges();
434
435 loader.buffered_ = () => buffered;
436
437 playlist.segments[0].map = map;
438 playlist.segments[1].map = map;
439 loader.playlist(playlist);
440
441 loader.load();
442 this.clock.tick(1);
443
444 assert.equal(this.requests.length, 2, 'made requests');
445
446 // init segment response
447 this.clock.tick(1);
448 assert.equal(this.requests[0].url, 'mainInitSegment', 'requested the init segment');
449 this.requests[0].response = new Uint8Array(20).buffer;
450 this.requests.shift().respond(200, null, '');
451 // 0.ts response
452 this.clock.tick(1);
453 assert.equal(this.requests[0].url, '0.ts',
454 'requested the segment');
455 this.requests[0].response = new Uint8Array(20).buffer;
456 this.requests.shift().respond(200, null, '');
457
458 // append the init segment
459 buffered = videojs.createTimeRanges([]);
460 this.updateend();
461
462 // append the segment
463 buffered = videojs.createTimeRanges([[0, 10]]);
464 this.updateend();
465 this.clock.tick(1);
466
467 assert.equal(this.requests.length, 1, 'made a request');
468 assert.equal(this.requests[0].url, '1.ts',
469 'did not re-request the init segment');
470 });
471
472 QUnit.test('detects init segment changes and downloads it', function(assert) {
473 let playlist = playlistWithDuration(20);
474 let buffered = videojs.createTimeRanges();
475
476 playlist.segments[0].map = {
477 resolvedUri: 'init0',
478 byterange: {
479 length: 20,
480 offset: 0
481 }
482 };
483 playlist.segments[1].map = {
484 resolvedUri: 'init0',
485 byterange: {
486 length: 20,
487 offset: 20
488 }
489 };
490
491 loader.buffered_ = () => buffered;
492 loader.playlist(playlist);
493
494 loader.load();
495 this.clock.tick(1);
496
497 assert.equal(this.requests.length, 2, 'made requests');
498
499 // init segment response
500 this.clock.tick(1);
501 assert.equal(this.requests[0].url, 'init0', 'requested the init segment');
502 assert.equal(this.requests[0].headers.Range, 'bytes=0-19',
503 'requested the init segment byte range');
504 this.requests[0].response = new Uint8Array(20).buffer;
505 this.requests.shift().respond(200, null, '');
506 // 0.ts response
507 this.clock.tick(1);
508 assert.equal(this.requests[0].url, '0.ts',
509 'requested the segment');
510 this.requests[0].response = new Uint8Array(20).buffer;
511 this.requests.shift().respond(200, null, '');
512
513 // append the init segment
514 buffered = videojs.createTimeRanges([]);
515 this.updateend();
516 // append the segment
517 buffered = videojs.createTimeRanges([[0, 10]]);
518 this.updateend();
519 this.clock.tick(1);
520
521 assert.equal(this.requests.length, 2, 'made requests');
522 assert.equal(this.requests[0].url, 'init0', 'requested the init segment');
523 assert.equal(this.requests[0].headers.Range, 'bytes=20-39',
524 'requested the init segment byte range');
525 assert.equal(this.requests[1].url, '1.ts',
526 'did not re-request the init segment');
527 });
528
529 QUnit.test('request error increments mediaRequestsErrored stat', function(assert) {
530 loader.playlist(playlistWithDuration(20));
531
532 loader.load();
533 this.clock.tick(1);
534
535 this.requests.shift().respond(404, null, '');
536
537 // verify stats
538 assert.equal(loader.mediaRequests, 1, '1 request');
539 assert.equal(loader.mediaRequestsErrored, 1, '1 errored request');
540 });
541
542 QUnit.test('request timeout increments mediaRequestsTimedout stat', function(assert) {
543 loader.playlist(playlistWithDuration(20));
544
545 loader.load();
546 this.clock.tick(1);
547 this.requests[0].timedout = true;
548 this.clock.tick(100 * 1000);
549
550 // verify stats
551 assert.equal(loader.mediaRequests, 1, '1 request');
552 assert.equal(loader.mediaRequestsTimedout, 1, '1 timed-out request');
553 });
554
555 QUnit.test('request abort increments mediaRequestsAborted stat', function(assert) {
556 loader.playlist(playlistWithDuration(20));
557
558 loader.load();
559 this.clock.tick(1);
560
561 loader.abort();
562 this.clock.tick(1);
563
564 // verify stats
565 assert.equal(loader.mediaRequests, 1, '1 request');
566 assert.equal(loader.mediaRequestsAborted, 1, '1 aborted request');
567 });
568
569 QUnit.test('SegmentLoader.mediaIndex is adjusted when live playlist is updated',
570 function(assert) {
571 loader.playlist(playlistWithDuration(50, {
572 mediaSequence: 0,
573 endList: false
574 }));
575
576 loader.load();
577 // Start at mediaIndex 2 which means that the next segment we request
578 // should mediaIndex 3
579 loader.mediaIndex = 2;
580 this.clock.tick(1);
581
582 assert.equal(loader.mediaIndex, 2, 'SegmentLoader.mediaIndex starts at 2');
583 assert.equal(this.requests[0].url,
584 '3.ts',
585 'requesting the segment at mediaIndex 3');
586
587 this.requests[0].response = new Uint8Array(10).buffer;
588 this.requests.shift().respond(200, null, '');
589 this.clock.tick(1);
590 this.updateend();
591
592 assert.equal(loader.mediaIndex, 3, 'mediaIndex ends at 3');
593
594 this.clock.tick(1);
595
596 assert.equal(loader.mediaIndex, 3, 'SegmentLoader.mediaIndex starts at 3');
597 assert.equal(this.requests[0].url,
598 '4.ts',
599 'requesting the segment at mediaIndex 4');
600
601 // Update the playlist shifting the mediaSequence by 2 which will result
602 // in a decrement of the mediaIndex by 2 to 1
603 loader.playlist(playlistWithDuration(50, {
604 mediaSequence: 2,
605 endList: false
606 }));
607
608 assert.equal(loader.mediaIndex, 1, 'SegmentLoader.mediaIndex is updated to 1');
609
610 this.requests[0].response = new Uint8Array(10).buffer;
611 this.requests.shift().respond(200, null, '');
612 this.clock.tick(1);
613 this.updateend();
614
615 assert.equal(loader.mediaIndex, 2, 'SegmentLoader.mediaIndex ends at 2');
616 });
617
618 QUnit.test('segmentInfo.mediaIndex is adjusted when live playlist is updated',
619 function(assert) {
620 const handleUpdateEnd_ = loader.handleUpdateEnd_.bind(loader);
621 let expectedLoaderIndex = 3;
622
623 loader.handleUpdateEnd_ = function() {
624 handleUpdateEnd_();
625
626 assert.equal(loader.mediaIndex,
627 expectedLoaderIndex,
628 'SegmentLoader.mediaIndex ends at' + expectedLoaderIndex);
629 loader.mediaIndex = null;
630 loader.fetchAtBuffer_ = false;
631 // remove empty flag that may be added by vtt loader
632 loader.playlist_.segments.forEach(segment => segment.empty = false);
633 };
634
635 // Setting currentTime to 31 so that we start requesting at segment #3
636 this.currentTime = 31;
637 loader.playlist(playlistWithDuration(50, {
638 mediaSequence: 0,
639 endList: false
640 }));
641
642 loader.load();
643 // Start at mediaIndex null which means that the next segment we request
644 // should be based on currentTime (mediaIndex 3)
645 loader.mediaIndex = null;
646 loader.syncPoint_ = {
647 segmentIndex: 0,
648 time: 0
649 };
650 this.clock.tick(1);
651
652 let segmentInfo = loader.pendingSegment_;
653
654 assert.equal(segmentInfo.mediaIndex, 3, 'segmentInfo.mediaIndex starts at 3');
655 assert.equal(this.requests[0].url,
656 '3.ts',
657 'requesting the segment at mediaIndex 3');
658
659 this.requests[0].response = new Uint8Array(10).buffer;
660 this.requests.shift().respond(200, null, '');
661 this.clock.tick(1);
662 this.updateend();
663
664 this.clock.tick(1);
665 segmentInfo = loader.pendingSegment_;
666
667 assert.equal(segmentInfo.mediaIndex, 3, 'segmentInfo.mediaIndex starts at 3');
668 assert.equal(this.requests[0].url,
669 '3.ts',
670 'requesting the segment at mediaIndex 3');
671
672 // Update the playlist shifting the mediaSequence by 2 which will result
673 // in a decrement of the mediaIndex by 2 to 1
674 loader.playlist(playlistWithDuration(50, {
675 mediaSequence: 2,
676 endList: false
677 }));
678
679 assert.equal(segmentInfo.mediaIndex, 1, 'segmentInfo.mediaIndex is updated to 1');
680
681 expectedLoaderIndex = 1;
682 this.requests[0].response = new Uint8Array(10).buffer;
683 this.requests.shift().respond(200, null, '');
684 this.clock.tick(1);
685 this.updateend();
686 });
687
688 QUnit.test('segment 404s should trigger an error', function(assert) {
689 let errors = [];
690
691 loader.playlist(playlistWithDuration(10));
692
693 loader.load();
694 this.clock.tick(1);
695
696 loader.on('error', function(error) {
697 errors.push(error);
698 });
699 this.requests.shift().respond(404, null, '');
700
701 assert.equal(errors.length, 1, 'triggered an error');
702 assert.equal(loader.error().code, 2, 'triggered MEDIA_ERR_NETWORK');
703 assert.ok(loader.error().xhr, 'included the request object');
704 assert.ok(loader.paused(), 'paused the loader');
705 assert.equal(loader.state, 'READY', 'returned to the ready state');
706 });
707
708 QUnit.test('empty segments should trigger an error', function(assert) {
709 let errors = [];
710
711 loader.playlist(playlistWithDuration(10));
712
713 loader.load();
714 this.clock.tick(1);
715
716 loader.on('error', function(error) {
717 errors.push(error);
718 });
719 this.requests[0].response = new Uint8Array(0).buffer;
720 this.requests.shift().respond(200, null, '');
721
722 assert.equal(errors.length, 1, 'triggered an error');
723 assert.equal(loader.error().code, 2, 'triggered MEDIA_ERR_NETWORK');
724 assert.ok(loader.error().xhr, 'included the request object');
725 assert.ok(loader.paused(), 'paused the loader');
726 assert.equal(loader.state, 'READY', 'returned to the ready state');
727 });
728
729 QUnit.test('segment 5xx status codes trigger an error', function(assert) {
730 let errors = [];
731
732 loader.playlist(playlistWithDuration(10));
733
734 loader.load();
735 this.clock.tick(1);
736
737 loader.on('error', function(error) {
738 errors.push(error);
739 });
740 this.requests.shift().respond(500, null, '');
741
742 assert.equal(errors.length, 1, 'triggered an error');
743 assert.equal(loader.error().code, 2, 'triggered MEDIA_ERR_NETWORK');
744 assert.ok(loader.error().xhr, 'included the request object');
745 assert.ok(loader.paused(), 'paused the loader');
746 assert.equal(loader.state, 'READY', 'returned to the ready state');
747 });
748
749 QUnit.test('remains ready if there are no segments', function(assert) {
750 loader.playlist(playlistWithDuration(0));
751
752 loader.load();
753 this.clock.tick(1);
754
755 assert.equal(loader.state, 'READY', 'in the ready state');
756 });
757
758 QUnit.test('dispose cleans up outstanding work', function(assert) {
759 loader.playlist(playlistWithDuration(20));
760
761 loader.load();
762 this.clock.tick(1);
763
764 loader.dispose();
765 assert.ok(this.requests[0].aborted, 'aborted segment request');
766 assert.equal(this.requests.length, 1, 'did not open another request');
767
768 // Check that media source was properly cleaned up if it exists on the loader
769 if (loader.mediaSource_) {
770 loader.mediaSource_.sourceBuffers.forEach((sourceBuffer, i) => {
771 let lastOperation = sourceBuffer.updates_.slice(-1)[0];
772
773 assert.ok(lastOperation.abort, 'aborted source buffer ' + i);
774 });
775 }
776 });
777
778 // ----------
779 // Decryption
780 // ----------
781
782 QUnit.test('calling load with an encrypted segment requests key and segment',
783 function(assert) {
784 assert.equal(loader.state, 'INIT', 'starts in the init state');
785 loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
786 assert.equal(loader.state, 'INIT', 'starts in the init state');
787 assert.ok(loader.paused(), 'starts paused');
788
789 loader.load();
790 this.clock.tick(1);
791
792 assert.equal(loader.state, 'WAITING', 'moves to the ready state');
793 assert.ok(!loader.paused(), 'loading is not paused');
794 assert.equal(this.requests.length, 2, 'requested a segment and key');
795 assert.equal(this.requests[0].url,
796 '0-key.php',
797 'requested the first segment\'s key');
798 assert.equal(this.requests[1].url, '0.ts', 'requested the first segment');
799 });
800
801 QUnit.test('dispose cleans up key requests for encrypted segments', function(assert) {
802 loader.playlist(playlistWithDuration(20, {isEncrypted: true}));
803
804 loader.load();
805 this.clock.tick(1);
806
807 loader.dispose();
808 assert.equal(this.requests.length, 2, 'requested a segment and key');
809 assert.equal(this.requests[0].url,
810 '0-key.php',
811 'requested the first segment\'s key');
812 assert.ok(this.requests[0].aborted, 'aborted the first segment\s key request');
813 assert.equal(this.requests.length, 2, 'did not open another request');
814 });
815
816 QUnit.test('key 404s should trigger an error', function(assert) {
817 let errors = [];
818
819 loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
820
821 loader.load();
822 this.clock.tick(1);
823
824 loader.on('error', function(error) {
825 errors.push(error);
826 });
827 this.requests.shift().respond(404, null, '');
828 this.clock.tick(1);
829
830 assert.equal(errors.length, 1, 'triggered an error');
831 assert.equal(loader.error().code, 2, 'triggered MEDIA_ERR_NETWORK');
832 assert.equal(loader.error().message, 'HLS request errored at URL: 0-key.php',
833 'receieved a key error message');
834 assert.ok(loader.error().xhr, 'included the request object');
835 assert.ok(loader.paused(), 'paused the loader');
836 assert.equal(loader.state, 'READY', 'returned to the ready state');
837 });
838
839 QUnit.test('key 5xx status codes trigger an error', function(assert) {
840 let errors = [];
841
842 loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
843
844 loader.load();
845 this.clock.tick(1);
846
847 loader.on('error', function(error) {
848 errors.push(error);
849 });
850 this.requests.shift().respond(500, null, '');
851
852 assert.equal(errors.length, 1, 'triggered an error');
853 assert.equal(loader.error().code, 2, 'triggered MEDIA_ERR_NETWORK');
854 assert.equal(loader.error().message, 'HLS request errored at URL: 0-key.php',
855 'receieved a key error message');
856 assert.ok(loader.error().xhr, 'included the request object');
857 assert.ok(loader.paused(), 'paused the loader');
858 assert.equal(loader.state, 'READY', 'returned to the ready state');
859 });
860
861 QUnit.test('key request timeouts reset bandwidth', function(assert) {
862 loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
863
864 loader.load();
865 this.clock.tick(1);
866
867 assert.equal(this.requests[0].url,
868 '0-key.php',
869 'requested the first segment\'s key');
870 assert.equal(this.requests[1].url, '0.ts', 'requested the first segment');
871 // a lot of time passes so the request times out
872 this.requests[0].timedout = true;
873 this.clock.tick(100 * 1000);
874
875 assert.equal(loader.bandwidth, 1, 'reset bandwidth');
876 assert.ok(isNaN(loader.roundTrip), 'reset round trip time');
877 });
878
879 QUnit.test('checks the goal buffer configuration every loading opportunity',
880 function(assert) {
881 let playlist = playlistWithDuration(20);
882 let defaultGoal = Config.GOAL_BUFFER_LENGTH;
883 let segmentInfo;
884
885 Config.GOAL_BUFFER_LENGTH = 1;
886 loader.playlist(playlist);
887
888 loader.load();
889
890 segmentInfo = loader.checkBuffer_(videojs.createTimeRanges([[0, 1]]),
891 playlist,
892 null,
893 loader.hasPlayed_(),
894 0,
895 null);
896 assert.ok(!segmentInfo, 'no request generated');
897 Config.GOAL_BUFFER_LENGTH = defaultGoal;
898 });
899
900 QUnit.test(
901 'does not skip over segment if live playlist update occurs while processing',
902 function(assert) {
903 let playlist = playlistWithDuration(40);
904 let buffered = videojs.createTimeRanges();
905
906 loader.buffered_ = () => buffered;
907
908 playlist.endList = false;
909
910 loader.playlist(playlist);
911
912 loader.load();
913 this.clock.tick(1);
914
915 assert.equal(loader.pendingSegment_.uri, '0.ts', 'retrieving first segment');
916 assert.equal(loader.pendingSegment_.segment.uri,
917 '0.ts',
918 'correct segment reference');
919 assert.equal(loader.state, 'WAITING', 'waiting for response');
920
921 this.requests[0].response = new Uint8Array(10).buffer;
922 this.requests.shift().respond(200, null, '');
923 // playlist updated during append
924 let playlistUpdated = playlistWithDuration(40);
925
926 playlistUpdated.segments.shift();
927 playlistUpdated.mediaSequence++;
928 loader.playlist(playlistUpdated);
929 // finish append
930 buffered = videojs.createTimeRanges([[0, 10]]);
931 this.updateend();
932 this.clock.tick(1);
933
934 assert.equal(loader.pendingSegment_.uri, '1.ts', 'retrieving second segment');
935 assert.equal(loader.pendingSegment_.segment.uri,
936 '1.ts',
937 'correct segment reference');
938 assert.equal(loader.state, 'WAITING', 'waiting for response');
939 });
940
941 QUnit.test('processing segment reachable even after playlist update removes it',
942 function(assert) {
943 const handleUpdateEnd_ = loader.handleUpdateEnd_.bind(loader);
944 let expectedURI = '0.ts';
945 let playlist = playlistWithDuration(40);
946 let buffered = videojs.createTimeRanges();
947
948 loader.handleUpdateEnd_ = () => {
949 // we need to check for the right state, as normally handleResponse would throw an
950 // error under failing cases, but sinon swallows it as part of fake XML HTTP
951 // request's response
952 assert.equal(loader.state, 'APPENDING', 'moved to appending state');
953 assert.equal(loader.pendingSegment_.uri, expectedURI, 'correct pending segment');
954 assert.equal(loader.pendingSegment_.segment.uri,
955 expectedURI,
956 'correct segment reference');
957
958 handleUpdateEnd_();
959 };
960
961 loader.buffered_ = () => buffered;
962
963 playlist.endList = false;
964
965 loader.playlist(playlist);
966
967 loader.load();
968 this.clock.tick(1);
969
970 assert.equal(loader.state, 'WAITING', 'in waiting state');
971 assert.equal(loader.pendingSegment_.uri, '0.ts', 'first segment pending');
972 assert.equal(loader.pendingSegment_.segment.uri,
973 '0.ts',
974 'correct segment reference');
975
976 // wrap up the first request to set mediaIndex and start normal live streaming
977 this.requests[0].response = new Uint8Array(10).buffer;
978 this.requests.shift().respond(200, null, '');
979 buffered = videojs.createTimeRanges([[0, 10]]);
980 this.updateend();
981 this.clock.tick(1);
982
983 assert.equal(loader.state, 'WAITING', 'in waiting state');
984 assert.equal(loader.pendingSegment_.uri, '1.ts', 'second segment pending');
985 assert.equal(loader.pendingSegment_.segment.uri,
986 '1.ts',
987 'correct segment reference');
988
989 // playlist updated during waiting
990 let playlistUpdated = playlistWithDuration(40);
991
992 playlistUpdated.segments.shift();
993 playlistUpdated.segments.shift();
994 playlistUpdated.mediaSequence += 2;
995 loader.playlist(playlistUpdated);
996
997 assert.equal(loader.pendingSegment_.uri, '1.ts', 'second segment still pending');
998 assert.equal(loader.pendingSegment_.segment.uri,
999 '1.ts',
1000 'correct segment reference');
1001
1002 expectedURI = '1.ts';
1003 this.requests[0].response = new Uint8Array(10).buffer;
1004 this.requests.shift().respond(200, null, '');
1005 this.updateend();
1006 });
1007
1008 QUnit.test('new playlist always triggers syncinfoupdate', function(assert) {
1009 let playlist = playlistWithDuration(100, { endList: false });
1010 let syncInfoUpdates = 0;
1011
1012 loader.on('syncinfoupdate', () => syncInfoUpdates++);
1013
1014 loader.playlist(playlist);
1015
1016 loader.load();
1017
1018 assert.equal(syncInfoUpdates, 1, 'first playlist triggers an update');
1019 loader.playlist(playlist);
1020 assert.equal(syncInfoUpdates, 2, 'same playlist triggers an update');
1021 playlist = playlistWithDuration(100, { endList: false });
1022 loader.playlist(playlist);
1023 assert.equal(syncInfoUpdates, 3, 'new playlist with same info triggers an update');
1024 playlist.segments[0].start = 10;
1025 playlist = playlistWithDuration(100, { endList: false, mediaSequence: 1 });
1026 loader.playlist(playlist);
1027 assert.equal(syncInfoUpdates,
1028 5,
1029 'new playlist after expiring segment triggers two updates');
1030 });
1031
1032 QUnit.module('Loading Calculation');
1033
1034 QUnit.test('requests the first segment with an empty buffer', function(assert) {
1035
1036 let segmentInfo = loader.checkBuffer_(videojs.createTimeRanges(),
1037 playlistWithDuration(20),
1038 null,
1039 loader.hasPlayed_(),
1040 0,
1041 null);
1042
1043 assert.ok(segmentInfo, 'generated a request');
1044 assert.equal(segmentInfo.uri, '0.ts', 'requested the first segment');
1045 });
1046
1047 QUnit.test('no request if video not played and 1 segment is buffered',
1048 function(assert) {
1049 this.hasPlayed = false;
1050
1051 let segmentInfo = loader.checkBuffer_(videojs.createTimeRanges([[0, 1]]),
1052 playlistWithDuration(20),
1053 0,
1054 loader.hasPlayed_(),
1055 0,
1056 null);
1057
1058 assert.ok(!segmentInfo, 'no request generated');
1059 });
1060
1061 QUnit.test('does not download the next segment if the buffer is full',
1062 function(assert) {
1063 let buffered;
1064 let segmentInfo;
1065
1066 buffered = videojs.createTimeRanges([
1067 [0, 30 + Config.GOAL_BUFFER_LENGTH]
1068 ]);
1069 segmentInfo = loader.checkBuffer_(buffered,
1070 playlistWithDuration(30),
1071 null,
1072 true,
1073 15,
1074 { segmentIndex: 0, time: 0 });
1075
1076 assert.ok(!segmentInfo, 'no segment request generated');
1077 });
1078
1079 QUnit.test('downloads the next segment if the buffer is getting low',
1080 function(assert) {
1081 let buffered;
1082 let segmentInfo;
1083 let playlist = playlistWithDuration(30);
1084
1085 loader.playlist(playlist);
1086
1087 buffered = videojs.createTimeRanges([[0, 19.999]]);
1088 segmentInfo = loader.checkBuffer_(buffered,
1089 playlist,
1090 1,
1091 true,
1092 15,
1093 { segmentIndex: 0, time: 0 });
1094
1095 assert.ok(segmentInfo, 'made a request');
1096 assert.equal(segmentInfo.uri, '2.ts', 'requested the third segment');
1097 });
1098
1099 QUnit.test('stops downloading segments at the end of the playlist', function(assert) {
1100 let buffered;
1101 let segmentInfo;
1102
1103 buffered = videojs.createTimeRanges([[0, 60]]);
1104 segmentInfo = loader.checkBuffer_(buffered,
1105 playlistWithDuration(60),
1106 null,
1107 true,
1108 0,
1109 null);
1110
1111 assert.ok(!segmentInfo, 'no request was made');
1112 });
1113
1114 QUnit.test('stops downloading segments if buffered past reported end of the playlist',
1115 function(assert) {
1116 let buffered;
1117 let segmentInfo;
1118 let playlist;
1119
1120 buffered = videojs.createTimeRanges([[0, 59.9]]);
1121 playlist = playlistWithDuration(60);
1122 playlist.segments[playlist.segments.length - 1].end = 59.9;
1123 segmentInfo = loader.checkBuffer_(buffered,
1124 playlist,
1125 playlist.segments.length - 1,
1126 true,
1127 50,
1128 { segmentIndex: 0, time: 0 });
1129
1130 assert.ok(!segmentInfo, 'no request was made');
1131 });
1132
1133 QUnit.test('doesn\'t allow more than one monitor buffer timer to be set',
1134 function(assert) {
1135 let timeoutCount = this.clock.methods.length;
1136
1137 loader.monitorBuffer_();
1138
1139 assert.equal(this.clock.methods.length,
1140 timeoutCount,
1141 'timeout count remains the same');
1142
1143 loader.monitorBuffer_();
1144
1145 assert.equal(this.clock.methods.length,
1146 timeoutCount,
1147 'timeout count remains the same');
1148
1149 loader.monitorBuffer_();
1150 loader.monitorBuffer_();
1151
1152 assert.equal(this.clock.methods.length,
1153 timeoutCount,
1154 'timeout count remains the same');
1155 });
1156 });
1157};