1 | import QUnit from 'qunit';
|
2 | import videojs from 'video.js';
|
3 | import xhrFactory from '../src/xhr';
|
4 | import Config from '../src/config';
|
5 | import {
|
6 | playlistWithDuration,
|
7 | useFakeEnvironment,
|
8 | useFakeMediaSource
|
9 | } from './test-helpers.js';
|
10 | import { MasterPlaylistController } from '../src/master-playlist-controller';
|
11 | import SyncController from '../src/sync-controller';
|
12 | import Decrypter from '../src/decrypter-worker';
|
13 | import worker from 'webworkify';
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 | export 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 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 | export 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 |
|
83 |
|
84 |
|
85 |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 |
|
91 |
|
92 |
|
93 | export const LoaderCommonFactory = (LoaderConstructor,
|
94 | loaderSettings,
|
95 | loaderBeforeEach) => {
|
96 | let loader;
|
97 |
|
98 | QUnit.module('Loader Common', function(hooks) {
|
99 | hooks.beforeEach(function(assert) {
|
100 |
|
101 |
|
102 | loader = new LoaderConstructor(LoaderCommonSettings.call(this, loaderSettings), {});
|
103 |
|
104 | loaderBeforeEach(loader);
|
105 |
|
106 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
256 |
|
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 |
|
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 |
|
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 |
|
397 | loader.mediaIndex = 1;
|
398 |
|
399 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
459 | buffered = videojs.createTimeRanges([]);
|
460 | this.updateend();
|
461 |
|
462 |
|
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 |
|
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 |
|
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 |
|
514 | buffered = videojs.createTimeRanges([]);
|
515 | this.updateend();
|
516 |
|
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 |
|
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 |
|
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 |
|
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 |
|
578 |
|
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 |
|
602 |
|
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 |
|
632 | loader.playlist_.segments.forEach(segment => segment.empty = false);
|
633 | };
|
634 |
|
635 |
|
636 | this.currentTime = 31;
|
637 | loader.playlist(playlistWithDuration(50, {
|
638 | mediaSequence: 0,
|
639 | endList: false
|
640 | }));
|
641 |
|
642 | loader.load();
|
643 |
|
644 |
|
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 |
|
673 |
|
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 |
|
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 |
|
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 |
|
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 |
|
924 | let playlistUpdated = playlistWithDuration(40);
|
925 |
|
926 | playlistUpdated.segments.shift();
|
927 | playlistUpdated.mediaSequence++;
|
928 | loader.playlist(playlistUpdated);
|
929 |
|
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 |
|
950 |
|
951 |
|
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 |
|
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 |
|
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 | };
|