UNPKG

107 kBJavaScriptView Raw
1/* eslint-disable max-len */
2
3import document from 'global/document';
4import videojs from 'video.js';
5import Events from 'video.js';
6import QUnit from 'qunit';
7import testDataManifests from './test-manifests.js';
8import {
9 useFakeEnvironment,
10 useFakeMediaSource,
11 createPlayer,
12 openMediaSource,
13 standardXHRResponse,
14 absoluteUrl
15} from './test-helpers.js';
16/* eslint-disable no-unused-vars */
17// we need this so that it can register hls with videojs
18import {HlsSourceHandler, HlsHandler, Hls} from '../src/videojs-contrib-hls';
19import window from 'global/window';
20// we need this so the plugin registers itself
21import 'videojs-contrib-quality-levels';
22/* eslint-enable no-unused-vars */
23
24const Flash = videojs.getTech('Flash');
25const ogHlsHandlerSetupQualityLevels = videojs.HlsHandler.prototype.setupQualityLevels_;
26let nextId = 0;
27
28// do a shallow copy of the properties of source onto the target object
29const merge = function(target, source) {
30 let name;
31
32 for (name in source) {
33 target[name] = source[name];
34 }
35};
36
37QUnit.module('HLS', {
38 beforeEach(assert) {
39 this.env = useFakeEnvironment(assert);
40 this.requests = this.env.requests;
41 this.mse = useFakeMediaSource();
42 this.clock = this.env.clock;
43 this.old = {};
44
45 // mock out Flash features for phantomjs
46 this.old.Flash = videojs.mergeOptions({}, Flash);
47 /* eslint-disable camelcase */
48 Flash.embed = function(swf, flashVars) {
49 let el = document.createElement('div');
50
51 el.id = 'vjs_mock_flash_' + nextId++;
52 el.className = 'vjs-tech vjs-mock-flash';
53 el.duration = Infinity;
54 el.vjs_load = function() {};
55 el.vjs_getProperty = function(attr) {
56 if (attr === 'buffered') {
57 return [[0, 0]];
58 }
59 return el[attr];
60 };
61 el.vjs_setProperty = function(attr, value) {
62 el[attr] = value;
63 };
64 el.vjs_src = function() {};
65 el.vjs_play = function() {};
66 el.vjs_discontinuity = function() {};
67
68 if (flashVars.autoplay) {
69 el.autoplay = true;
70 }
71 if (flashVars.preload) {
72 el.preload = flashVars.preload;
73 }
74
75 el.currentTime = 0;
76
77 return el;
78 };
79 /* eslint-enable camelcase */
80 this.old.FlashSupported = Flash.isSupported;
81 Flash.isSupported = function() {
82 return true;
83 };
84
85 // store functionality that some tests need to mock
86 this.old.GlobalOptions = videojs.mergeOptions(videojs.options);
87
88 // force the HLS tech to run
89 this.old.NativeHlsSupport = videojs.Hls.supportsNativeHls;
90 videojs.Hls.supportsNativeHls = false;
91
92 this.old.Decrypt = videojs.Hls.Decrypter;
93 videojs.Hls.Decrypter = function() {};
94
95 // save and restore browser detection for the Firefox-specific tests
96 this.old.browser = videojs.browser;
97 videojs.browser = videojs.mergeOptions({}, videojs.browser);
98
99 this.standardXHRResponse = (request, data) => {
100 standardXHRResponse(request, data);
101
102 // Because SegmentLoader#fillBuffer_ is now scheduled asynchronously
103 // we have to use clock.tick to get the expected side effects of
104 // SegmentLoader#handleUpdateEnd_
105 this.clock.tick(1);
106 };
107
108 // setup a player
109 this.player = createPlayer();
110 this.clock.tick(1);
111 },
112
113 afterEach() {
114 this.env.restore();
115 this.mse.restore();
116
117 merge(videojs.options, this.old.GlobalOptions);
118 Flash.isSupported = this.old.FlashSupported;
119 merge(Flash, this.old.Flash);
120
121 videojs.Hls.supportsNativeHls = this.old.NativeHlsSupport;
122 videojs.Hls.Decrypter = this.old.Decrypt;
123 videojs.browser = this.old.browser;
124
125 this.player.dispose();
126 }
127});
128
129QUnit.test('deprecation warning is show when using player.hls', function(assert) {
130 let oldWarn = videojs.log.warn;
131 let warning = '';
132 let hlsPlayerAccessEvents = 0;
133
134 this.player.src({
135 src: 'manifest/playlist.m3u8',
136 type: 'application/vnd.apple.mpegurl'
137 });
138
139 this.clock.tick(1);
140
141 this.player.tech_.on('usage', (event) => {
142 if (event.name === 'hls-player-access') {
143 hlsPlayerAccessEvents++;
144 }
145 });
146
147 videojs.log.warn = (text) => {
148 warning = text;
149 };
150 assert.equal(hlsPlayerAccessEvents, 0, 'no hls-player-access event was fired');
151 let hls = this.player.hls;
152
153 assert.equal(hlsPlayerAccessEvents, 1, 'an hls-player-access event was fired');
154 assert.equal(warning, 'player.hls is deprecated. Use player.tech_.hls instead.', 'warning would have been shown');
155 assert.ok(hls, 'an instance of hls is returned by player.hls');
156 videojs.log.warn = oldWarn;
157});
158
159QUnit.test('starts playing if autoplay is specified', function(assert) {
160 this.player.autoplay(true);
161 this.player.src({
162 src: 'manifest/playlist.m3u8',
163 type: 'application/vnd.apple.mpegurl'
164 });
165
166 this.clock.tick(1);
167
168 // make sure play() is called *after* the media source opens
169 openMediaSource(this.player, this.clock);
170
171 this.standardXHRResponse(this.requests[0]);
172 assert.ok(!this.player.paused(), 'not paused');
173});
174
175QUnit.test('stats are reset on each new source', function(assert) {
176 this.player.src({
177 src: 'manifest/playlist.m3u8',
178 type: 'application/vnd.apple.mpegurl'
179 });
180
181 this.clock.tick(1);
182
183 // make sure play() is called *after* the media source opens
184 openMediaSource(this.player, this.clock);
185 this.standardXHRResponse(this.requests.shift());
186 this.standardXHRResponse(this.requests.shift());
187
188 assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, 'stat is set');
189 this.player.src({
190 src: 'manifest/master.m3u8',
191 type: 'application/vnd.apple.mpegurl'
192 });
193
194 this.clock.tick(1);
195
196 assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 0, 'stat is reset');
197});
198
199QUnit.test('XHR requests first byte range on play', function(assert) {
200 this.player.src({
201 src: 'manifest/playlist.m3u8',
202 type: 'application/vnd.apple.mpegurl'
203 });
204
205 this.clock.tick(1);
206
207 this.player.tech_.triggerReady();
208 this.clock.tick(1);
209 this.player.tech_.trigger('play');
210 openMediaSource(this.player, this.clock);
211 this.standardXHRResponse(this.requests[0]);
212 assert.equal(this.requests[1].headers.Range, 'bytes=0-522827');
213});
214
215QUnit.test('Seeking requests correct byte range', function(assert) {
216 this.player.src({
217 src: 'manifest/playlist.m3u8',
218 type: 'application/vnd.apple.mpegurl'
219 });
220
221 this.clock.tick(1);
222
223 this.player.tech_.trigger('play');
224 openMediaSource(this.player, this.clock);
225 this.standardXHRResponse(this.requests[0]);
226 this.clock.tick(1);
227 this.player.currentTime(41);
228 this.clock.tick(2);
229 assert.equal(this.requests[2].headers.Range, 'bytes=2299992-2835603');
230});
231
232QUnit.test('autoplay seeks to the live point after playlist load', function(assert) {
233 let currentTime = 0;
234
235 this.player.autoplay(true);
236 this.player.on('seeking', () => {
237 currentTime = this.player.currentTime();
238 });
239 this.player.src({
240 src: 'liveStart30sBefore.m3u8',
241 type: 'application/vnd.apple.mpegurl'
242 });
243
244 this.clock.tick(1);
245
246 openMediaSource(this.player, this.clock);
247 this.player.tech_.trigger('play');
248 this.standardXHRResponse(this.requests.shift());
249 this.clock.tick(1);
250
251 assert.notEqual(currentTime, 0, 'seeked on autoplay');
252});
253
254QUnit.test('autoplay seeks to the live point after media source open', function(assert) {
255 let currentTime = 0;
256
257 this.player.autoplay(true);
258 this.player.on('seeking', () => {
259 currentTime = this.player.currentTime();
260 });
261 this.player.src({
262 src: 'liveStart30sBefore.m3u8',
263 type: 'application/vnd.apple.mpegurl'
264 });
265
266 this.clock.tick(1);
267
268 this.player.tech_.triggerReady();
269 this.clock.tick(1);
270 this.standardXHRResponse(this.requests.shift());
271 openMediaSource(this.player, this.clock);
272 this.player.tech_.trigger('play');
273 this.clock.tick(1);
274
275 assert.notEqual(currentTime, 0, 'seeked on autoplay');
276});
277
278QUnit.test('autoplay seeks to the live point after tech fires loadedmetadata in ie11',
279function(assert) {
280 videojs.browser.IE_VERSION = 11;
281 let currentTime = 0;
282
283 this.player.autoplay(true);
284 this.player.on('seeking', () => {
285 currentTime = this.player.currentTime();
286 });
287 this.player.src({
288 src: 'liveStart30sBefore.m3u8',
289 type: 'application/vnd.apple.mpegurl'
290 });
291
292 this.clock.tick(1);
293
294 openMediaSource(this.player, this.clock);
295 this.player.tech_.trigger('play');
296 this.standardXHRResponse(this.requests.shift());
297 this.clock.tick(1);
298
299 assert.equal(currentTime, 0, 'have not played yet');
300
301 this.player.tech_.trigger('loadedmetadata');
302 this.clock.tick(1);
303
304 assert.notEqual(currentTime, 0, 'seeked after tech is ready');
305});
306
307QUnit.test('duration is set when the source opens after the playlist is loaded',
308function(assert) {
309 this.player.src({
310 src: 'media.m3u8',
311 type: 'application/vnd.apple.mpegurl'
312 });
313
314 this.clock.tick(1);
315
316 this.player.tech_.triggerReady();
317 this.clock.tick(1);
318 this.standardXHRResponse(this.requests.shift());
319 openMediaSource(this.player, this.clock);
320
321 assert.equal(this.player.tech_.hls.mediaSource.duration,
322 40,
323 'set the duration');
324});
325
326QUnit.test('codecs are passed to the source buffer', function(assert) {
327 let codecs = [];
328
329 this.player.src({
330 src: 'custom-codecs.m3u8',
331 type: 'application/vnd.apple.mpegurl'
332 });
333
334 this.clock.tick(1);
335
336 openMediaSource(this.player, this.clock);
337 let addSourceBuffer = this.player.tech_.hls.mediaSource.addSourceBuffer;
338
339 this.player.tech_.hls.mediaSource.addSourceBuffer = function(codec) {
340 codecs.push(codec);
341 return addSourceBuffer.call(this, codec);
342 };
343
344 this.requests.shift().respond(200, null,
345 '#EXTM3U\n' +
346 '#EXT-X-STREAM-INF:CODECS="avc1.dd00dd, mp4a.40.f"\n' +
347 'media.m3u8\n');
348 this.standardXHRResponse(this.requests.shift());
349 assert.equal(codecs.length, 1, 'created a source buffer');
350 assert.equal(codecs[0], 'video/mp2t; codecs="avc1.dd00dd, mp4a.40.f"', 'specified the codecs');
351});
352
353QUnit.test('including HLS as a tech does not error', function(assert) {
354 let player = createPlayer({
355 techOrder: ['hls', 'html5']
356 });
357
358 this.clock.tick(1);
359
360 assert.ok(player, 'created the player');
361 assert.equal(this.env.log.warn.calls, 2, 'logged two warnings for deprecations');
362});
363
364QUnit.test('creates a PlaylistLoader on init', function(assert) {
365 this.player.src({
366 src: 'manifest/playlist.m3u8',
367 type: 'application/vnd.apple.mpegurl'
368 });
369
370 this.clock.tick(1);
371
372 openMediaSource(this.player, this.clock);
373 this.player.src({
374 src: 'manifest/playlist.m3u8',
375 type: 'application/vnd.apple.mpegurl'
376 });
377
378 this.clock.tick(1);
379
380 openMediaSource(this.player, this.clock);
381
382 assert.equal(this.requests[0].aborted, true, 'aborted previous src');
383 this.standardXHRResponse(this.requests[1]);
384 assert.ok(this.player.tech_.hls.playlists.master,
385 'set the master playlist');
386 assert.ok(this.player.tech_.hls.playlists.media(),
387 'set the media playlist');
388 assert.ok(this.player.tech_.hls.playlists.media().segments,
389 'the segment entries are parsed');
390 assert.strictEqual(this.player.tech_.hls.playlists.master.playlists[0],
391 this.player.tech_.hls.playlists.media(),
392 'the playlist is selected');
393});
394
395QUnit.test('sets the duration if one is available on the playlist', function(assert) {
396 let events = 0;
397
398 this.player.src({
399 src: 'manifest/media.m3u8',
400 type: 'application/vnd.apple.mpegurl'
401 });
402
403 this.clock.tick(1);
404
405 openMediaSource(this.player, this.clock);
406 this.player.tech_.on('durationchange', function() {
407 events++;
408 });
409
410 this.standardXHRResponse(this.requests[0]);
411 assert.equal(this.player.tech_.hls.mediaSource.duration,
412 40,
413 'set the duration');
414 assert.equal(events, 1, 'durationchange is fired');
415});
416
417QUnit.test('estimates individual segment durations if needed', function(assert) {
418 let changes = 0;
419
420 this.player.src({
421 src: 'http://example.com/manifest/missingExtinf.m3u8',
422 type: 'application/vnd.apple.mpegurl'
423 });
424
425 this.clock.tick(1);
426
427 openMediaSource(this.player, this.clock);
428 this.player.tech_.hls.mediaSource.duration = NaN;
429 this.player.tech_.on('durationchange', function() {
430 changes++;
431 });
432
433 this.standardXHRResponse(this.requests[0]);
434 assert.strictEqual(this.player.tech_.hls.mediaSource.duration,
435 this.player.tech_.hls.playlists.media().segments.length * 10,
436 'duration is updated');
437 assert.strictEqual(changes, 1, 'one durationchange fired');
438});
439
440QUnit.test('translates seekable by the starting time for live playlists', function(assert) {
441 let seekable;
442
443 this.player.src({
444 src: 'media.m3u8',
445 type: 'application/vnd.apple.mpegurl'
446 });
447
448 this.clock.tick(1);
449
450 openMediaSource(this.player, this.clock);
451 this.requests.shift().respond(200, null,
452 '#EXTM3U\n' +
453 '#EXT-X-MEDIA-SEQUENCE:15\n' +
454 '#EXT-X-TARGETDURATION:10\n' +
455 '#EXTINF:10,\n' +
456 '0.ts\n' +
457 '#EXTINF:10,\n' +
458 '1.ts\n' +
459 '#EXTINF:10,\n' +
460 '2.ts\n' +
461 '#EXTINF:10,\n' +
462 '3.ts\n');
463
464 seekable = this.player.seekable();
465 assert.equal(seekable.length, 1, 'one seekable range');
466 assert.equal(seekable.start(0), 0, 'the earliest possible position is at zero');
467 assert.equal(seekable.end(0), 10, 'end is relative to the start');
468});
469
470QUnit.test('starts downloading a segment on loadedmetadata', function(assert) {
471 this.player.src({
472 src: 'manifest/media.m3u8',
473 type: 'application/vnd.apple.mpegurl'
474 });
475
476 this.clock.tick(1);
477
478 this.player.buffered = function() {
479 return videojs.createTimeRange(0, 0);
480 };
481 openMediaSource(this.player, this.clock);
482
483 this.standardXHRResponse(this.requests[0]);
484 this.standardXHRResponse(this.requests[1]);
485 assert.strictEqual(this.requests[1].url,
486 absoluteUrl('manifest/media-00001.ts'),
487 'the first segment is requested');
488
489 // verify stats
490 assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
491 assert.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
492});
493
494QUnit.test('re-initializes the handler for each source', function(assert) {
495 let firstPlaylists;
496 let secondPlaylists;
497 let firstMSE;
498 let secondMSE;
499 let aborts = 0;
500 let masterPlaylistController;
501
502 this.player.src({
503 src: 'manifest/master.m3u8',
504 type: 'application/vnd.apple.mpegurl'
505 });
506
507 this.clock.tick(1);
508
509 openMediaSource(this.player, this.clock);
510 firstPlaylists = this.player.tech_.hls.playlists;
511 firstMSE = this.player.tech_.hls.mediaSource;
512 this.standardXHRResponse(this.requests.shift());
513 this.standardXHRResponse(this.requests.shift());
514 masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
515 masterPlaylistController.mainSegmentLoader_.sourceUpdater_.sourceBuffer_.abort = () => {
516 aborts++;
517 };
518
519 this.player.src({
520 src: 'manifest/master.m3u8',
521 type: 'application/vnd.apple.mpegurl'
522 });
523
524 this.clock.tick(1);
525
526 openMediaSource(this.player, this.clock);
527 secondPlaylists = this.player.tech_.hls.playlists;
528 secondMSE = this.player.tech_.hls.mediaSource;
529
530 assert.equal(1, aborts, 'aborted the old source buffer');
531 assert.ok(this.requests[0].aborted, 'aborted the old segment request');
532 assert.notStrictEqual(firstPlaylists,
533 secondPlaylists,
534 'the playlist object is not reused');
535 assert.notStrictEqual(firstMSE, secondMSE, 'the media source object is not reused');
536});
537
538QUnit.test('triggers a media source error when an initial playlist request errors',
539function(assert) {
540 this.player.src({
541 src: 'manifest/master.m3u8',
542 type: 'application/vnd.apple.mpegurl'
543 });
544
545 this.clock.tick(1);
546
547 openMediaSource(this.player, this.clock);
548 this.requests.pop().respond(500);
549
550 assert.equal(this.player.tech_.hls.mediaSource.error_,
551 'network',
552 'a network error is triggered');
553});
554
555QUnit.test(
556'triggers a player error when an initial playlist request errors and the media source ' +
557'isn\'t open',
558function(assert) {
559 const done = assert.async();
560 const origError = videojs.log.error;
561 const errLogs = [];
562 const endOfStreams = [];
563
564 videojs.log.error = (log) => errLogs.push(log);
565
566 this.player.src({
567 src: 'manifest/master.m3u8',
568 type: 'application/vnd.apple.mpegurl'
569 });
570 openMediaSource(this.player, this.clock);
571
572 this.player.tech_.hls.masterPlaylistController_.mediaSource.endOfStream = (type) => {
573 endOfStreams.push(type);
574 throw new Error();
575 };
576
577 this.player.on('error', () => {
578 const error = this.player.error();
579
580 assert.equal(endOfStreams.length, 1, 'one endOfStream called');
581 assert.equal(endOfStreams[0], 'network', 'endOfStream called with network');
582
583 assert.equal(error.code, 2, 'error has correct code');
584 assert.equal(error.message,
585 'HLS playlist request error at URL: manifest/master.m3u8',
586 'error has correct message');
587 assert.equal(errLogs.length, 1, 'logged an error');
588
589 videojs.log.error = origError;
590
591 assert.notOk(this.player.tech_.hls.mediaSource.error_, 'no media source error');
592
593 done();
594 });
595
596 this.requests.pop().respond(500);
597});
598
599QUnit.test('downloads media playlists after loading the master', function(assert) {
600 this.player.src({
601 src: 'manifest/master.m3u8',
602 type: 'application/vnd.apple.mpegurl'
603 });
604
605 this.clock.tick(1);
606
607 openMediaSource(this.player, this.clock);
608
609 this.player.tech_.hls.bandwidth = 20e10;
610 this.standardXHRResponse(this.requests[0]);
611 this.standardXHRResponse(this.requests[1]);
612 this.standardXHRResponse(this.requests[2]);
613
614 assert.strictEqual(this.requests[0].url,
615 'manifest/master.m3u8',
616 'master playlist requested');
617 assert.strictEqual(this.requests[1].url,
618 absoluteUrl('manifest/media2.m3u8'),
619 'media playlist requested');
620 assert.strictEqual(this.requests[2].url,
621 absoluteUrl('manifest/media2-00001.ts'),
622 'first segment requested');
623
624 // verify stats
625 assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
626 assert.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
627});
628
629QUnit.test('setting bandwidth resets throughput', function(assert) {
630 this.player.src({
631 src: 'manifest/master.m3u8',
632 type: 'application/vnd.apple.mpegurl'
633 });
634
635 this.clock.tick(1);
636
637 this.player.tech_.hls.throughput = 1000;
638 assert.strictEqual(this.player.tech_.hls.throughput,
639 1000,
640 'throughput is set');
641 this.player.tech_.hls.bandwidth = 20e10;
642 assert.strictEqual(this.player.tech_.hls.throughput,
643 0,
644 'throughput is reset when bandwidth is specified');
645});
646
647QUnit.test('a thoughput of zero is ignored in systemBandwidth', function(assert) {
648 this.player.src({
649 src: 'manifest/master.m3u8',
650 type: 'application/vnd.apple.mpegurl'
651 });
652
653 this.clock.tick(1);
654
655 this.player.tech_.hls.bandwidth = 20e10;
656 assert.strictEqual(this.player.tech_.hls.throughput,
657 0,
658 'throughput is reset when bandwidth is specified');
659 assert.strictEqual(this.player.tech_.hls.systemBandwidth,
660 20e10,
661 'systemBandwidth is the same as bandwidth');
662});
663
664QUnit.test('systemBandwidth is a combination of thoughput and bandwidth', function(assert) {
665 this.player.src({
666 src: 'manifest/master.m3u8',
667 type: 'application/vnd.apple.mpegurl'
668 });
669
670 this.clock.tick(1);
671
672 this.player.tech_.hls.bandwidth = 20e10;
673 this.player.tech_.hls.throughput = 20e10;
674 // 1 / ( 1 / 20e10 + 1 / 20e10) = 10e10
675 assert.strictEqual(this.player.tech_.hls.systemBandwidth,
676 10e10,
677 'systemBandwidth is the combination of bandwidth and throughput');
678});
679
680QUnit.test('upshifts if the initial bandwidth hint is high', function(assert) {
681 this.player.src({
682 src: 'manifest/master.m3u8',
683 type: 'application/vnd.apple.mpegurl'
684 });
685
686 this.clock.tick(1);
687
688 openMediaSource(this.player, this.clock);
689
690 this.player.tech_.hls.bandwidth = 10e20;
691 this.standardXHRResponse(this.requests[0]);
692 this.standardXHRResponse(this.requests[1]);
693 this.standardXHRResponse(this.requests[2]);
694
695 assert.strictEqual(
696 this.requests[0].url,
697 'manifest/master.m3u8',
698 'master playlist requested'
699 );
700 assert.strictEqual(
701 this.requests[1].url,
702 absoluteUrl('manifest/media2.m3u8'),
703 'media playlist requested'
704 );
705 assert.strictEqual(
706 this.requests[2].url,
707 absoluteUrl('manifest/media2-00001.ts'),
708 'first segment requested'
709 );
710
711 // verify stats
712 assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
713 assert.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
714});
715
716QUnit.test('downshifts if the initial bandwidth hint is low', function(assert) {
717 this.player.src({
718 src: 'manifest/master.m3u8',
719 type: 'application/vnd.apple.mpegurl'
720 });
721
722 this.clock.tick(1);
723
724 openMediaSource(this.player, this.clock);
725
726 this.player.tech_.hls.bandwidth = 100;
727 this.standardXHRResponse(this.requests[0]);
728 this.standardXHRResponse(this.requests[1]);
729 this.standardXHRResponse(this.requests[2]);
730
731 assert.strictEqual(this.requests[0].url,
732 'manifest/master.m3u8',
733 'master playlist requested');
734 assert.strictEqual(this.requests[1].url,
735 absoluteUrl('manifest/media1.m3u8'),
736 'media playlist requested');
737 assert.strictEqual(this.requests[2].url,
738 absoluteUrl('manifest/media1-00001.ts'),
739 'first segment requested');
740
741 // verify stats
742 assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
743 assert.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
744});
745
746QUnit.test('buffer checks are noops until a media playlist is ready', function(assert) {
747 this.player.src({
748 src: 'manifest/media.m3u8',
749 type: 'application/vnd.apple.mpegurl'
750 });
751
752 this.clock.tick(1);
753
754 openMediaSource(this.player, this.clock);
755 this.clock.tick(10 * 1000);
756
757 assert.strictEqual(1, this.requests.length, 'one request was made');
758 assert.strictEqual(this.requests[0].url,
759 'manifest/media.m3u8',
760 'media playlist requested');
761});
762
763QUnit.test('buffer checks are noops when only the master is ready', function(assert) {
764 this.player.src({
765 src: 'manifest/master.m3u8',
766 type: 'application/vnd.apple.mpegurl'
767 });
768
769 this.clock.tick(1);
770
771 openMediaSource(this.player, this.clock);
772 // master
773 this.standardXHRResponse(this.requests.shift());
774 // media
775 this.standardXHRResponse(this.requests.shift());
776 // ignore any outstanding segment requests
777 this.requests.length = 0;
778
779 // load in a new playlist which will cause playlists.media() to be
780 // undefined while it is being fetched
781 this.player.src({
782 src: 'manifest/master.m3u8',
783 type: 'application/vnd.apple.mpegurl'
784 });
785 openMediaSource(this.player, this.clock);
786
787 // respond with the master playlist but don't send the media playlist yet
788 // force media1 to be requested
789 this.player.tech_.hls.bandwidth = 1;
790 // master
791 this.standardXHRResponse(this.requests.shift());
792 this.clock.tick(10 * 1000);
793
794 assert.strictEqual(this.requests.length, 1, 'one request was made');
795 assert.strictEqual(this.requests[0].url,
796 absoluteUrl('manifest/media1.m3u8'),
797 'media playlist requested');
798
799 // verify stats
800 assert.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth set above');
801});
802
803QUnit.test('selects a playlist below the current bandwidth', function(assert) {
804 let playlist;
805
806 this.player.src({
807 src: 'manifest/master.m3u8',
808 type: 'application/vnd.apple.mpegurl'
809 });
810
811 this.clock.tick(1);
812
813 openMediaSource(this.player, this.clock);
814 this.standardXHRResponse(this.requests[0]);
815
816 // the default playlist has a really high bitrate
817 this.player.tech_.hls.playlists.master.playlists[0].attributes.BANDWIDTH = 9e10;
818 // playlist 1 has a very low bitrate
819 this.player.tech_.hls.playlists.master.playlists[1].attributes.BANDWIDTH = 1;
820 // but the detected client bandwidth is really low
821 this.player.tech_.hls.bandwidth = 10;
822
823 playlist = this.player.tech_.hls.selectPlaylist();
824 assert.strictEqual(playlist,
825 this.player.tech_.hls.playlists.master.playlists[1],
826 'the low bitrate stream is selected');
827
828 // verify stats
829 assert.equal(this.player.tech_.hls.stats.bandwidth, 10, 'bandwidth set above');
830});
831
832QUnit.test('selects a primary rendtion when there are multiple rendtions share same attributes', function(assert) {
833 let playlist;
834
835 this.player.src({
836 src: 'manifest/master.m3u8',
837 type: 'application/vnd.apple.mpegurl'
838 });
839 openMediaSource(this.player, this.clock);
840 standardXHRResponse(this.requests[0]);
841
842 // covers playlists with same bandwidth but different resolution and different bandwidth but same resolution
843 this.player.tech_.hls.playlists.master.playlists[0].attributes.BANDWIDTH = 528;
844 this.player.tech_.hls.playlists.master.playlists[1].attributes.BANDWIDTH = 528;
845 this.player.tech_.hls.playlists.master.playlists[2].attributes.BANDWIDTH = 728;
846 this.player.tech_.hls.playlists.master.playlists[3].attributes.BANDWIDTH = 728;
847
848 this.player.tech_.hls.bandwidth = 1000;
849
850 playlist = this.player.tech_.hls.selectPlaylist();
851 assert.strictEqual(playlist,
852 this.player.tech_.hls.playlists.master.playlists[2],
853 'select the rendition with largest bandwidth and just-larger-than video player');
854
855 // verify stats
856 assert.equal(this.player.tech_.hls.stats.bandwidth, 1000, 'bandwidth set above');
857
858 // covers playlists share same bandwidth and resolutions
859 this.player.tech_.hls.playlists.master.playlists[0].attributes.BANDWIDTH = 728;
860 this.player.tech_.hls.playlists.master.playlists[0].attributes.RESOLUTION.width = 960;
861 this.player.tech_.hls.playlists.master.playlists[0].attributes.RESOLUTION.height = 540;
862 this.player.tech_.hls.playlists.master.playlists[1].attributes.BANDWIDTH = 728;
863 this.player.tech_.hls.playlists.master.playlists[2].attributes.BANDWIDTH = 728;
864 this.player.tech_.hls.playlists.master.playlists[2].attributes.RESOLUTION.width = 960;
865 this.player.tech_.hls.playlists.master.playlists[2].attributes.RESOLUTION.height = 540;
866 this.player.tech_.hls.playlists.master.playlists[3].attributes.BANDWIDTH = 728;
867
868 this.player.tech_.hls.bandwidth = 1000;
869
870 playlist = this.player.tech_.hls.selectPlaylist();
871 assert.strictEqual(playlist,
872 this.player.tech_.hls.playlists.master.playlists[0],
873 'the primary rendition is selected');
874});
875
876QUnit.test('allows initial bandwidth to be provided', function(assert) {
877 this.player.src({
878 src: 'manifest/master.m3u8',
879 type: 'application/vnd.apple.mpegurl'
880 });
881
882 this.clock.tick(1);
883
884 openMediaSource(this.player, this.clock);
885 this.player.tech_.hls.bandwidth = 500;
886
887 this.requests[0].bandwidth = 1;
888 this.requests.shift().respond(200, null,
889 '#EXTM3U\n' +
890 '#EXT-X-PLAYLIST-TYPE:VOD\n' +
891 '#EXT-X-TARGETDURATION:10\n');
892 assert.equal(this.player.tech_.hls.bandwidth,
893 500,
894 'prefers user-specified initial bandwidth');
895
896 // verify stats
897 assert.equal(this.player.tech_.hls.stats.bandwidth, 500, 'bandwidth set above');
898});
899
900QUnit.test('raises the minimum bitrate for a stream proportionially', function(assert) {
901 let playlist;
902
903 this.player.src({
904 src: 'manifest/master.m3u8',
905 type: 'application/vnd.apple.mpegurl'
906 });
907
908 this.clock.tick(1);
909
910 openMediaSource(this.player, this.clock);
911
912 this.standardXHRResponse(this.requests[0]);
913
914 // the default playlist's bandwidth + 10% is assert.equal to the current bandwidth
915 this.player.tech_.hls.playlists.master.playlists[0].attributes.BANDWIDTH = 10;
916 this.player.tech_.hls.bandwidth = 11;
917
918 // 9.9 * 1.1 < 11
919 this.player.tech_.hls.playlists.master.playlists[1].attributes.BANDWIDTH = 9.9;
920 playlist = this.player.tech_.hls.selectPlaylist();
921
922 assert.strictEqual(playlist,
923 this.player.tech_.hls.playlists.master.playlists[1],
924 'a lower bitrate stream is selected');
925
926 // verify stats
927 assert.equal(this.player.tech_.hls.stats.bandwidth, 11, 'bandwidth set above');
928});
929
930QUnit.test('uses the lowest bitrate if no other is suitable', function(assert) {
931 let playlist;
932
933 this.player.src({
934 src: 'manifest/master.m3u8',
935 type: 'application/vnd.apple.mpegurl'
936 });
937
938 this.clock.tick(1);
939
940 openMediaSource(this.player, this.clock);
941
942 this.standardXHRResponse(this.requests[0]);
943
944 // the lowest bitrate playlist is much greater than 1b/s
945 this.player.tech_.hls.bandwidth = 1;
946 playlist = this.player.tech_.hls.selectPlaylist();
947
948 // playlist 1 has the lowest advertised bitrate
949 assert.strictEqual(playlist,
950 this.player.tech_.hls.playlists.master.playlists[1],
951 'the lowest bitrate stream is selected');
952
953 // verify stats
954 assert.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth set above');
955});
956
957QUnit.test('selects the correct rendition by tech dimensions', function(assert) {
958 let playlist;
959 let hls;
960
961 this.player.src({
962 src: 'manifest/master.m3u8',
963 type: 'application/vnd.apple.mpegurl'
964 });
965
966 this.clock.tick(1);
967
968 openMediaSource(this.player, this.clock);
969 this.standardXHRResponse(this.requests[0]);
970
971 hls = this.player.tech_.hls;
972
973 this.player.width(640);
974 this.player.height(360);
975 hls.bandwidth = 3000000;
976
977 playlist = hls.selectPlaylist();
978
979 assert.deepEqual(playlist.attributes.RESOLUTION,
980 {width: 960, height: 540},
981 'should return the correct resolution by tech dimensions');
982 assert.equal(playlist.attributes.BANDWIDTH,
983 1928000,
984 'should have the expected bandwidth in case of multiple');
985
986 this.player.width(1920);
987 this.player.height(1080);
988 hls.bandwidth = 3000000;
989
990 playlist = hls.selectPlaylist();
991
992 assert.deepEqual(playlist.attributes.RESOLUTION,
993 {width: 960, height: 540},
994 'should return the correct resolution by tech dimensions');
995 assert.equal(playlist.attributes.BANDWIDTH,
996 1928000,
997 'should have the expected bandwidth in case of multiple');
998
999 this.player.width(396);
1000 this.player.height(224);
1001 playlist = hls.selectPlaylist();
1002
1003 assert.deepEqual(playlist.attributes.RESOLUTION,
1004 {width: 396, height: 224},
1005 'should return the correct resolution by ' +
1006 'tech dimensions, if exact match');
1007 assert.equal(playlist.attributes.BANDWIDTH,
1008 440000,
1009 'should have the expected bandwidth in case of multiple, if exact match');
1010
1011 this.player.width(395);
1012 this.player.height(222);
1013 playlist = this.player.tech_.hls.selectPlaylist();
1014
1015 assert.deepEqual(playlist.attributes.RESOLUTION,
1016 {width: 396, height: 224},
1017 'should return the next larger resolution by tech dimensions, ' +
1018 'if no exact match exists');
1019 assert.equal(playlist.attributes.BANDWIDTH,
1020 440000,
1021 'should have the expected bandwidth in case of multiple, if exact match');
1022
1023 // verify stats
1024 assert.equal(this.player.tech_.hls.stats.bandwidth, 3000000, 'bandwidth set above');
1025});
1026
1027QUnit.test('selects the highest bitrate playlist when the player dimensions are ' +
1028 'larger than any of the variants', function(assert) {
1029 let playlist;
1030
1031 this.player.src({
1032 src: 'manifest/master.m3u8',
1033 type: 'application/vnd.apple.mpegurl'
1034 });
1035
1036 this.clock.tick(1);
1037
1038 openMediaSource(this.player, this.clock);
1039 // master
1040 this.requests.shift().respond(200, null,
1041 '#EXTM3U\n' +
1042 '#EXT-X-STREAM-INF:BANDWIDTH=1000,RESOLUTION=2x1\n' +
1043 'media.m3u8\n' +
1044 '#EXT-X-STREAM-INF:BANDWIDTH=1,RESOLUTION=1x1\n' +
1045 'media1.m3u8\n');
1046 // media
1047 this.standardXHRResponse(this.requests.shift());
1048 this.player.tech_.hls.bandwidth = 1e10;
1049
1050 this.player.width(1024);
1051 this.player.height(768);
1052
1053 playlist = this.player.tech_.hls.selectPlaylist();
1054
1055 assert.equal(playlist.attributes.BANDWIDTH,
1056 1000,
1057 'selected the highest bandwidth variant');
1058
1059 // verify stats
1060 assert.equal(this.player.tech_.hls.stats.bandwidth, 1e10, 'bandwidth set above');
1061});
1062
1063QUnit.test('filters playlists that are currently excluded', function(assert) {
1064 let playlist;
1065
1066 this.player.src({
1067 src: 'manifest/master.m3u8',
1068 type: 'application/vnd.apple.mpegurl'
1069 });
1070
1071 this.clock.tick(1);
1072
1073 openMediaSource(this.player, this.clock);
1074
1075 this.player.tech_.hls.bandwidth = 1e10;
1076 // master
1077 this.requests.shift().respond(200, null,
1078 '#EXTM3U\n' +
1079 '#EXT-X-STREAM-INF:BANDWIDTH=1000\n' +
1080 'media.m3u8\n' +
1081 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
1082 'media1.m3u8\n');
1083 // media
1084 this.standardXHRResponse(this.requests.shift());
1085
1086 // exclude the current playlist
1087 this.player.tech_.hls.playlists.master.playlists[0].excludeUntil = +new Date() + 1000;
1088 playlist = this.player.tech_.hls.selectPlaylist();
1089 assert.equal(playlist,
1090 this.player.tech_.hls.playlists.master.playlists[1],
1091 'respected exclusions');
1092
1093 // timeout the exclusion
1094 this.clock.tick(1000);
1095 playlist = this.player.tech_.hls.selectPlaylist();
1096 assert.equal(playlist,
1097 this.player.tech_.hls.playlists.master.playlists[0],
1098 'expired the exclusion');
1099
1100 // verify stats
1101 assert.equal(this.player.tech_.hls.stats.bandwidth, 1e10, 'bandwidth set above');
1102});
1103
1104QUnit.test('does not blacklist compatible H.264 codec strings', function(assert) {
1105 let master;
1106
1107 this.player.src({
1108 src: 'manifest/master.m3u8',
1109 type: 'application/vnd.apple.mpegurl'
1110 });
1111
1112 this.clock.tick(1);
1113
1114 openMediaSource(this.player, this.clock);
1115
1116 this.player.tech_.hls.bandwidth = 1;
1117 // master
1118 this.requests.shift()
1119 .respond(200, null,
1120 '#EXTM3U\n' +
1121 '#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="avc1.4d400d,mp4a.40.5"\n' +
1122 'media.m3u8\n' +
1123 '#EXT-X-STREAM-INF:BANDWIDTH=10,CODECS="avc1.4d400f,mp4a.40.5"\n' +
1124 'media1.m3u8\n');
1125
1126 // media
1127 this.standardXHRResponse(this.requests.shift());
1128 master = this.player.tech_.hls.playlists.master;
1129 assert.strictEqual(typeof master.playlists[0].excludeUntil,
1130 'undefined',
1131 'did not blacklist');
1132 assert.strictEqual(typeof master.playlists[1].excludeUntil,
1133 'undefined',
1134 'did not blacklist');
1135
1136 // verify stats
1137 assert.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth set above');
1138});
1139
1140QUnit.test('does not blacklist compatible AAC codec strings', function(assert) {
1141 let master;
1142
1143 this.player.src({
1144 src: 'manifest/master.m3u8',
1145 type: 'application/vnd.apple.mpegurl'
1146 });
1147
1148 this.clock.tick(1);
1149
1150 openMediaSource(this.player, this.clock);
1151
1152 this.player.tech_.hls.bandwidth = 1;
1153 // master
1154 this.requests.shift()
1155 .respond(200, null,
1156 '#EXTM3U\n' +
1157 '#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="avc1.4d400d,mp4a.40.2"\n' +
1158 'media.m3u8\n' +
1159 '#EXT-X-STREAM-INF:BANDWIDTH=10,CODECS="avc1.4d400d,not-an-audio-codec"\n' +
1160 'media1.m3u8\n');
1161
1162 // media
1163 this.standardXHRResponse(this.requests.shift());
1164 master = this.player.tech_.hls.playlists.master;
1165 assert.strictEqual(typeof master.playlists[0].excludeUntil,
1166 'undefined',
1167 'did not blacklist mp4a.40.2');
1168 assert.strictEqual(master.playlists[1].excludeUntil,
1169 Infinity,
1170 'blacklisted invalid audio codec');
1171
1172 // verify stats
1173 assert.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth set above');
1174});
1175
1176QUnit.test('cancels outstanding XHRs when seeking', function(assert) {
1177 this.player.src({
1178 src: 'manifest/media.m3u8',
1179 type: 'application/vnd.apple.mpegurl'
1180 });
1181
1182 this.clock.tick(1);
1183
1184 openMediaSource(this.player, this.clock);
1185 this.standardXHRResponse(this.requests[0]);
1186 this.player.tech_.hls.media = {
1187 segments: [{
1188 uri: '0.ts',
1189 duration: 10
1190 }, {
1191 uri: '1.ts',
1192 duration: 10
1193 }]
1194 };
1195
1196 // attempt to seek while the download is in progress
1197 this.player.currentTime(7);
1198 this.clock.tick(2);
1199
1200 assert.ok(this.requests[1].aborted, 'XHR aborted');
1201 assert.strictEqual(this.requests.length, 3, 'opened new XHR');
1202});
1203
1204QUnit.test('does not abort segment loading for in-buffer seeking', function(assert) {
1205 this.player.src({
1206 src: 'manifest/media.m3u8',
1207 type: 'application/vnd.apple.mpegurl'
1208 });
1209
1210 this.clock.tick(1);
1211
1212 openMediaSource(this.player, this.clock);
1213 this.standardXHRResponse(this.requests.shift());
1214 this.player.tech_.buffered = function() {
1215 return videojs.createTimeRange(0, 20);
1216 };
1217
1218 this.player.tech_.setCurrentTime(11);
1219 this.clock.tick(1);
1220 assert.equal(this.requests.length, 1, 'did not abort the outstanding request');
1221});
1222
1223QUnit.test('segment 404 should trigger blacklisting of media', function(assert) {
1224 let media;
1225
1226 this.player.src({
1227 src: 'manifest/master.m3u8',
1228 type: 'application/vnd.apple.mpegurl'
1229 });
1230
1231 this.clock.tick(1);
1232
1233 openMediaSource(this.player, this.clock);
1234
1235 this.player.tech_.hls.bandwidth = 20000;
1236 // master
1237 this.standardXHRResponse(this.requests[0]);
1238 // media
1239 this.standardXHRResponse(this.requests[1]);
1240
1241 media = this.player.tech_.hls.playlists.media_;
1242
1243 // segment
1244 this.requests[2].respond(400);
1245 assert.ok(media.excludeUntil > 0, 'original media blacklisted for some time');
1246 assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
1247
1248 // verify stats
1249 assert.equal(this.player.tech_.hls.stats.bandwidth, 20000, 'bandwidth set above');
1250});
1251
1252QUnit.test('playlist 404 should blacklist media', function(assert) {
1253 let media;
1254 let url;
1255 let blacklistplaylist = 0;
1256 let retryplaylist = 0;
1257 let hlsRenditionBlacklistedEvents = 0;
1258
1259 this.player.src({
1260 src: 'manifest/master.m3u8',
1261 type: 'application/vnd.apple.mpegurl'
1262 });
1263
1264 this.clock.tick(1);
1265
1266 openMediaSource(this.player, this.clock);
1267 this.player.tech_.on('blacklistplaylist', () => blacklistplaylist++);
1268 this.player.tech_.on('retryplaylist', () => retryplaylist++);
1269 this.player.tech_.on('usage', (event) => {
1270 if (event.name === 'hls-rendition-blacklisted') {
1271 hlsRenditionBlacklistedEvents++;
1272 }
1273 });
1274
1275 this.player.tech_.hls.bandwidth = 1e10;
1276 // master
1277 this.requests[0].respond(200, null,
1278 '#EXTM3U\n' +
1279 '#EXT-X-STREAM-INF:BANDWIDTH=1000\n' +
1280 'media.m3u8\n' +
1281 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
1282 'media1.m3u8\n');
1283 assert.equal(typeof this.player.tech_.hls.playlists.media_,
1284 'undefined',
1285 'no media is initially set');
1286
1287 assert.equal(blacklistplaylist, 0, 'there is no blacklisted playlist');
1288 assert.equal(hlsRenditionBlacklistedEvents, 0, 'no hls-rendition-blacklisted event was fired');
1289 // media
1290 this.requests[1].respond(404);
1291 url = this.requests[1].url.slice(this.requests[1].url.lastIndexOf('/') + 1);
1292 media = this.player.tech_.hls.playlists.master.playlists[url];
1293 assert.ok(media.excludeUntil > 0, 'original media blacklisted for some time');
1294 assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
1295 assert.equal(this.env.log.warn.args[0],
1296 'Problem encountered with the current HLS playlist. HLS playlist request error at URL: media.m3u8 Switching to another playlist.',
1297 'log generic error message');
1298 assert.equal(blacklistplaylist, 1, 'there is one blacklisted playlist');
1299 assert.equal(hlsRenditionBlacklistedEvents, 1, 'an hls-rendition-blacklisted event was fired');
1300 assert.equal(retryplaylist, 0, 'haven\'t retried any playlist');
1301
1302 // request for the final available media
1303 this.requests[2].respond(404);
1304 url = this.requests[2].url.slice(this.requests[2].url.lastIndexOf('/') + 1);
1305 media = this.player.tech_.hls.playlists.master.playlists[url];
1306
1307 // media wasn't blacklisted because it's final rendition
1308 assert.ok(!media.excludeUntil, 'media not blacklisted after playlist 404');
1309 assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
1310 assert.equal(this.env.log.warn.args[1],
1311 'Problem encountered with the current HLS playlist. Trying again since it is the final playlist.',
1312 'log specific error message for final playlist');
1313 assert.equal(retryplaylist, 1, 'retried final playlist for once');
1314 assert.equal(blacklistplaylist, 1, 'there is one blacklisted playlist');
1315 assert.equal(hlsRenditionBlacklistedEvents, 1, 'an hls-rendition-blacklisted event was fired');
1316
1317 this.clock.tick(2 * 1000);
1318 // no new request was made since it hasn't been half the segment duration
1319 assert.strictEqual(3, this.requests.length, 'no new request was made');
1320
1321 this.clock.tick(3 * 1000);
1322 // continue loading the final remaining playlist after it wasn't blacklisted
1323 // when half the segment duaration passed
1324 assert.strictEqual(4, this.requests.length, 'one more request was made');
1325
1326 assert.strictEqual(this.requests[3].url,
1327 absoluteUrl('manifest/media1.m3u8'),
1328 'media playlist requested');
1329
1330 // verify stats
1331 assert.equal(this.player.tech_.hls.stats.bandwidth, 1e10, 'bandwidth set above');
1332});
1333
1334QUnit.test('blacklists playlist if it has stopped being updated', function(assert) {
1335 let playliststuck = 0;
1336
1337 this.player.src({
1338 src: 'master.m3u8',
1339 type: 'application/vnd.apple.mpegurl'
1340 });
1341 openMediaSource(this.player, this.clock);
1342 this.player.tech_.triggerReady();
1343
1344 this.standardXHRResponse(this.requests.shift());
1345
1346 this.player.tech_.hls.masterPlaylistController_.seekable = function() {
1347 return videojs.createTimeRange(90, 130);
1348 };
1349 this.player.tech_.setCurrentTime(170);
1350 this.player.tech_.buffered = function() {
1351 return videojs.createTimeRange(0, 170);
1352 };
1353 Hls.Playlist.playlistEnd = function() {
1354 return 170;
1355 };
1356
1357 this.player.tech_.on('playliststuck', () => playliststuck++);
1358 this.requests.shift().respond(200, null,
1359 '#EXTM3U\n' +
1360 '#EXT-X-MEDIA-SEQUENCE:16\n' +
1361 '#EXTINF:10,\n' +
1362 '16.ts\n');
1363
1364 assert.ok(!this.player.tech_.hls.playlists.media().excludeUntil, 'playlist was not blacklisted');
1365 assert.equal(this.env.log.warn.calls, 0, 'no warning logged for blacklist');
1366 assert.equal(playliststuck, 0, 'there is no stuck playlist');
1367
1368 this.player.tech_.trigger('play');
1369 this.player.tech_.trigger('playing');
1370 // trigger a refresh
1371 this.clock.tick(10 * 1000);
1372
1373 this.requests.shift().respond(200, null,
1374 '#EXTM3U\n' +
1375 '#EXT-X-MEDIA-SEQUENCE:16\n' +
1376 '#EXTINF:10,\n' +
1377 '16.ts\n');
1378
1379 assert.ok(this.player.tech_.hls.playlists.media().excludeUntil > 0, 'playlist blacklisted for some time');
1380 assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
1381 assert.equal(this.env.log.warn.args[0],
1382 'Problem encountered with the current HLS playlist. Playlist no longer updating. Switching to another playlist.',
1383 'log specific error message for not updated playlist');
1384 assert.equal(playliststuck, 1, 'there is one stuck playlist');
1385});
1386
1387QUnit.test('never blacklist the playlist if it is the only playlist', function(assert) {
1388 let media;
1389
1390 this.player.src({
1391 src: 'manifest/media.m3u8',
1392 type: 'application/vnd.apple.mpegurl'
1393 });
1394 openMediaSource(this.player, this.clock);
1395
1396 this.requests.shift().respond(200, null,
1397 '#EXTM3U\n' +
1398 '#EXTINF:10,\n' +
1399 '0.ts\n');
1400
1401 this.clock.tick(10 * 1000);
1402 this.requests.shift().respond(404);
1403 media = this.player.tech_.hls.playlists.media();
1404
1405 // media wasn't blacklisted because it's final rendition
1406 assert.ok(!media.excludeUntil, 'media was not blacklisted after playlist 404');
1407 assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
1408 assert.equal(this.env.log.warn.args[0],
1409 'Problem encountered with the current HLS playlist. Trying again since it is the final playlist.',
1410 'log specific error message for final playlist');
1411});
1412
1413QUnit.test('error on the first playlist request does not trigger an error ' +
1414 'when there is master playlist with only one media playlist', function(assert) {
1415 this.player.src({
1416 src: 'manifest/master.m3u8',
1417 type: 'application/vnd.apple.mpegurl'
1418 });
1419 openMediaSource(this.player, this.clock);
1420
1421 this.requests[0]
1422 .respond(200, null,
1423 '#EXTM3U\n' +
1424 '#EXT-X-STREAM-INF:BANDWIDTH=1000\n' +
1425 'media.m3u8\n');
1426
1427 this.requests[1].respond(404);
1428
1429 let url = this.requests[1].url.slice(this.requests[1].url.lastIndexOf('/') + 1);
1430 let media = this.player.tech_.hls.playlists.master.playlists[url];
1431
1432 // media wasn't blacklisted because it's final rendition
1433 assert.ok(!media.excludeUntil, 'media was not blacklisted after playlist 404');
1434 assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
1435 assert.equal(this.env.log.warn.args[0],
1436 'Problem encountered with the current HLS playlist. Trying again since it is the final playlist.',
1437 'log specific error message for final playlist');
1438});
1439
1440QUnit.test('seeking in an empty playlist is a non-erroring noop', function(assert) {
1441 let requestsLength;
1442
1443 this.player.src({
1444 src: 'manifest/empty-live.m3u8',
1445 type: 'application/vnd.apple.mpegurl'
1446 });
1447
1448 this.clock.tick(1);
1449
1450 openMediaSource(this.player, this.clock);
1451
1452 this.requests.shift().respond(200, null, '#EXTM3U\n');
1453
1454 requestsLength = this.requests.length;
1455 this.player.tech_.setCurrentTime(183);
1456 this.clock.tick(1);
1457
1458 assert.equal(this.requests.length, requestsLength, 'made no additional requests');
1459});
1460
1461QUnit.test('fire loadedmetadata once we successfully load a playlist', function(assert) {
1462 let count = 0;
1463
1464 this.player.src({
1465 src: 'manifest/master.m3u8',
1466 type: 'application/vnd.apple.mpegurl'
1467 });
1468
1469 this.clock.tick(1);
1470
1471 openMediaSource(this.player, this.clock);
1472 let hls = this.player.tech_.hls;
1473
1474 hls.bandwidth = 20000;
1475 hls.masterPlaylistController_.masterPlaylistLoader_.on('loadedmetadata', function() {
1476 count += 1;
1477 });
1478 // master
1479 this.standardXHRResponse(this.requests.shift());
1480 assert.equal(count, 0,
1481 'loadedMedia not triggered before requesting playlist');
1482 // media
1483 this.requests.shift().respond(404);
1484 assert.equal(count, 0,
1485 'loadedMedia not triggered after playlist 404');
1486 assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
1487
1488 // media
1489 this.standardXHRResponse(this.requests.shift());
1490 assert.equal(count, 1,
1491 'loadedMedia triggered after successful recovery from 404');
1492
1493 // verify stats
1494 assert.equal(this.player.tech_.hls.stats.bandwidth, 20000, 'bandwidth set above');
1495});
1496
1497QUnit.test('sets seekable and duration for live playlists', function(assert) {
1498 this.player.src({
1499 src: 'http://example.com/manifest/missingEndlist.m3u8',
1500 type: 'application/vnd.apple.mpegurl'
1501 });
1502
1503 this.clock.tick(1);
1504
1505 openMediaSource(this.player, this.clock);
1506
1507 this.standardXHRResponse(this.requests[0]);
1508
1509 assert.equal(this.player.tech_.hls.mediaSource.seekable.length,
1510 1,
1511 'set one seekable range');
1512 assert.equal(this.player.tech_.hls.mediaSource.seekable.start(0),
1513 this.player.tech_.hls.seekable().start(0),
1514 'set seekable start');
1515 assert.equal(this.player.tech_.hls.mediaSource.seekable.end(0),
1516 this.player.tech_.hls.seekable().end(0),
1517 'set seekable end');
1518
1519 assert.strictEqual(this.player.tech_.hls.mediaSource.duration,
1520 Infinity,
1521 'duration on the mediaSource is infinity');
1522});
1523
1524QUnit.test('live playlist starts with correct currentTime value', function(assert) {
1525 this.player.src({
1526 src: 'http://example.com/manifest/liveStart30sBefore.m3u8',
1527 type: 'application/vnd.apple.mpegurl'
1528 });
1529
1530 this.clock.tick(1);
1531
1532 openMediaSource(this.player, this.clock);
1533
1534 this.standardXHRResponse(this.requests[0]);
1535
1536 this.player.tech_.hls.playlists.trigger('loadedmetadata');
1537
1538 this.player.tech_.paused = function() {
1539 return false;
1540 };
1541 this.player.tech_.trigger('play');
1542 this.clock.tick(1);
1543
1544 let media = this.player.tech_.hls.playlists.media();
1545
1546 assert.strictEqual(this.player.currentTime(),
1547 Hls.Playlist.seekable(media).end(0),
1548 'currentTime is updated at playback');
1549});
1550
1551QUnit.test('estimates seekable ranges for live streams that have been paused for a long time', function(assert) {
1552 this.player.src({
1553 src: 'http://example.com/manifest/liveStart30sBefore.m3u8',
1554 type: 'application/vnd.apple.mpegurl'
1555 });
1556
1557 this.clock.tick(1);
1558
1559 openMediaSource(this.player, this.clock);
1560
1561 this.standardXHRResponse(this.requests.shift());
1562 this.player.tech_.hls.playlists.media().mediaSequence = 172;
1563 this.player.tech_.hls.playlists.media().syncInfo = {
1564 mediaSequence: 130,
1565 time: 80
1566 };
1567 this.player.tech_.hls.masterPlaylistController_.onSyncInfoUpdate_();
1568 assert.equal(this.player.seekable().start(0),
1569 500,
1570 'offset the seekable start');
1571});
1572
1573QUnit.test('resets the time to the live point when resuming a live stream after a ' +
1574 'long break', function(assert) {
1575 let seekTarget;
1576
1577 this.player.src({
1578 src: 'live0.m3u8',
1579 type: 'application/vnd.apple.mpegurl'
1580 });
1581
1582 this.clock.tick(1);
1583
1584 openMediaSource(this.player, this.clock);
1585 this.requests.shift().respond(200, null,
1586 '#EXTM3U\n' +
1587 '#EXT-X-MEDIA-SEQUENCE:16\n' +
1588 '#EXTINF:10,\n' +
1589 '16.ts\n');
1590 // mock out the player to simulate a live stream that has been
1591 // playing for awhile
1592 this.player.tech_.hls.seekable = function() {
1593 return videojs.createTimeRange(160, 170);
1594 };
1595 this.player.tech_.setCurrentTime = function(time) {
1596 if (typeof time !== 'undefined') {
1597 seekTarget = time;
1598 }
1599 };
1600 this.player.tech_.played = function() {
1601 return videojs.createTimeRange(120, 170);
1602 };
1603 this.player.tech_.trigger('playing');
1604
1605 let seekable = this.player.seekable();
1606
1607 this.player.tech_.trigger('play');
1608 assert.equal(seekTarget, seekable.end(seekable.length - 1), 'seeked to live point');
1609 this.player.tech_.trigger('seeked');
1610});
1611
1612QUnit.test('reloads out-of-date live playlists when switching variants', function(assert) {
1613 let oldManifest = testDataManifests['variant-update'];
1614
1615 this.player.src({
1616 src: 'http://example.com/master.m3u8',
1617 type: 'application/vnd.apple.mpegurl'
1618 });
1619
1620 this.clock.tick(1);
1621
1622 openMediaSource(this.player, this.clock);
1623
1624 this.player.tech_.hls.master = {
1625 playlists: [{
1626 mediaSequence: 15,
1627 segments: [1, 1, 1]
1628 }, {
1629 uri: 'http://example.com/variant-update.m3u8',
1630 mediaSequence: 0,
1631 segments: [1, 1]
1632 }]
1633 };
1634 // playing segment 15 on playlist zero
1635 this.player.tech_.hls.media = this.player.tech_.hls.master.playlists[0];
1636 this.player.mediaIndex = 1;
1637
1638 testDataManifests['variant-update'] = '#EXTM3U\n' +
1639 '#EXT-X-MEDIA-SEQUENCE:16\n' +
1640 '#EXTINF:10,\n' +
1641 '16.ts\n' +
1642 '#EXTINF:10,\n' +
1643 '17.ts\n';
1644
1645 // switch playlists
1646 this.player.tech_.hls.selectPlaylist = function() {
1647 return this.player.tech_.hls.master.playlists[1];
1648 };
1649 // timeupdate downloads segment 16 then switches playlists
1650 this.player.trigger('timeupdate');
1651
1652 assert.strictEqual(this.player.mediaIndex, 1, 'mediaIndex points at the next segment');
1653 testDataManifests['variant-update'] = oldManifest;
1654});
1655
1656QUnit.test('if withCredentials global option is used, withCredentials is set on the XHR object', function(assert) {
1657 let hlsOptions = videojs.options.hls;
1658
1659 this.player.dispose();
1660 videojs.options.hls = {
1661 withCredentials: true
1662 };
1663 this.player = createPlayer();
1664 this.player.src({
1665 src: 'http://example.com/media.m3u8',
1666 type: 'application/vnd.apple.mpegurl'
1667 });
1668
1669 this.clock.tick(1);
1670
1671 openMediaSource(this.player, this.clock);
1672 assert.ok(this.requests[0].withCredentials,
1673 'with credentials should be set to true if that option is passed in');
1674 videojs.options.hls = hlsOptions;
1675});
1676
1677QUnit.test('the withCredentials option overrides the global default', function(assert) {
1678 let hlsOptions = videojs.options.hls;
1679
1680 this.player.dispose();
1681 videojs.options.hls = {
1682 withCredentials: true
1683 };
1684 this.player = createPlayer();
1685 this.player.src({
1686 src: 'http://example.com/media.m3u8',
1687 type: 'application/vnd.apple.mpegurl',
1688 withCredentials: false
1689 });
1690
1691 this.clock.tick(1);
1692
1693 openMediaSource(this.player, this.clock);
1694 assert.ok(!this.requests[0].withCredentials,
1695 'with credentials should be set to false if if overrode global option');
1696 videojs.options.hls = hlsOptions;
1697});
1698
1699QUnit.test('playlist blacklisting duration is set through options', function(assert) {
1700 let hlsOptions = videojs.options.hls;
1701 let url;
1702 let media;
1703
1704 this.player.dispose();
1705 videojs.options.hls = {
1706 blacklistDuration: 3 * 60
1707 };
1708 this.player = createPlayer();
1709 this.player.src({
1710 src: 'http://example.com/master.m3u8',
1711 type: 'application/vnd.apple.mpegurl'
1712 });
1713 this.player.tech_.triggerReady();
1714 openMediaSource(this.player, this.clock);
1715 this.requests[0].respond(200, null,
1716 '#EXTM3U\n' +
1717 '#EXT-X-STREAM-INF:BANDWIDTH=1000\n' +
1718 'media.m3u8\n' +
1719 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
1720 'media1.m3u8\n');
1721 this.requests[1].respond(404);
1722 // media
1723 url = this.requests[1].url.slice(this.requests[1].url.lastIndexOf('/') + 1);
1724 media = this.player.tech_.hls.playlists.master.playlists[url];
1725 assert.ok(media.excludeUntil > 0, 'original media blacklisted for some time');
1726 assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
1727 assert.equal(this.env.log.warn.args[0],
1728 'Problem encountered with the current HLS playlist. HLS playlist request error at URL: media.m3u8 Switching to another playlist.',
1729 'log generic error message');
1730
1731 this.clock.tick(2 * 60 * 1000);
1732 assert.ok(media.excludeUntil - Date.now() > 0, 'original media still be blacklisted');
1733
1734 this.clock.tick(1 * 60 * 1000);
1735 assert.equal(media.excludeUntil, Date.now(), 'media\'s exclude time reach to the current time');
1736 assert.equal(this.env.log.warn.calls, 3, 'warning logged for blacklist');
1737
1738 videojs.options.hls = hlsOptions;
1739});
1740
1741QUnit.test('if mode global option is used, mode is set to global option', function(assert) {
1742 let hlsOptions = videojs.options.hls;
1743
1744 this.player.dispose();
1745 videojs.options.hls = {
1746 mode: 'flash'
1747 };
1748 this.player = createPlayer();
1749 this.player.src({
1750 src: 'http://example.com/media.m3u8',
1751 type: 'application/vnd.apple.mpegurl'
1752 });
1753
1754 this.clock.tick(1);
1755
1756 openMediaSource(this.player, this.clock);
1757 assert.equal(this.player.tech_.hls.options_.mode, 'flash', 'mode set to flash');
1758 videojs.options.hls = hlsOptions;
1759});
1760
1761QUnit.test('respects bandwidth option of 0', function(assert) {
1762 this.player.dispose();
1763 this.player = createPlayer({ html5: { hls: { bandwidth: 0 } } });
1764
1765 this.player.src({
1766 src: 'http://example.com/media.m3u8',
1767 type: 'application/vnd.apple.mpegurl'
1768 });
1769
1770 this.clock.tick(1);
1771
1772 openMediaSource(this.player, this.clock);
1773 assert.equal(this.player.tech_.hls.bandwidth, 0, 'set bandwidth to 0');
1774});
1775
1776QUnit.test('uses default bandwidth option if non-numerical value provided', function(assert) {
1777 this.player.dispose();
1778 this.player = createPlayer({ html5: { hls: { bandwidth: 'garbage' } } });
1779
1780 this.player.src({
1781 src: 'http://example.com/media.m3u8',
1782 type: 'application/vnd.apple.mpegurl'
1783 });
1784
1785 this.clock.tick(1);
1786
1787 openMediaSource(this.player, this.clock);
1788 assert.equal(this.player.tech_.hls.bandwidth, 4194304, 'set bandwidth to default');
1789});
1790
1791QUnit.test('uses default bandwidth if browser is Android', function(assert) {
1792 this.player.dispose();
1793
1794 const origIsAndroid = videojs.browser.IS_ANDROID;
1795
1796 videojs.browser.IS_ANDROID = false;
1797
1798 this.player = createPlayer();
1799 this.player.src({
1800 src: 'http://example.com/media.m3u8',
1801 type: 'application/vnd.apple.mpegurl'
1802 });
1803 openMediaSource(this.player, this.clock);
1804
1805 assert.equal(this.player.tech_.hls.bandwidth,
1806 4194304,
1807 'set bandwidth to desktop default');
1808
1809 this.player.dispose();
1810
1811 videojs.browser.IS_ANDROID = true;
1812
1813 this.player = createPlayer();
1814 this.player.src({
1815 src: 'http://example.com/media.m3u8',
1816 type: 'application/vnd.apple.mpegurl'
1817 });
1818 openMediaSource(this.player, this.clock);
1819
1820 assert.equal(this.player.tech_.hls.bandwidth,
1821 4194304,
1822 'set bandwidth to mobile default');
1823
1824 videojs.browser.IS_ANDROID = origIsAndroid;
1825});
1826
1827QUnit.test('does not break if the playlist has no segments', function(assert) {
1828 this.player.src({
1829 src: 'manifest/master.m3u8',
1830 type: 'application/vnd.apple.mpegurl'
1831 });
1832
1833 this.clock.tick(1);
1834
1835 try {
1836 openMediaSource(this.player, this.clock);
1837 this.requests[0].respond(200, null,
1838 '#EXTM3U\n' +
1839 '#EXT-X-PLAYLIST-TYPE:VOD\n' +
1840 '#EXT-X-TARGETDURATION:10\n');
1841 } catch (e) {
1842 assert.ok(false, 'an error was thrown');
1843 throw e;
1844 }
1845 assert.ok(true, 'no error was thrown');
1846 assert.strictEqual(
1847 this.requests.length,
1848 1,
1849 'no this.requestsfor non-existent segments were queued'
1850 );
1851});
1852
1853QUnit.test('can seek before the source buffer opens', function(assert) {
1854 this.player.src({
1855 src: 'media.m3u8',
1856 type: 'application/vnd.apple.mpegurl'
1857 });
1858
1859 this.clock.tick(1);
1860
1861 this.player.tech_.triggerReady();
1862 this.clock.tick(1);
1863 this.standardXHRResponse(this.requests.shift());
1864 this.player.triggerReady();
1865
1866 this.player.currentTime(1);
1867 assert.equal(this.player.currentTime(), 1, 'seeked');
1868});
1869
1870QUnit.test('resets the switching algorithm if a request times out', function(assert) {
1871 this.player.src({
1872 src: 'master.m3u8',
1873 type: 'application/vnd.apple.mpegurl'
1874 });
1875
1876 this.clock.tick(1);
1877
1878 openMediaSource(this.player, this.clock);
1879 this.player.tech_.hls.bandwidth = 1e20;
1880
1881 // master
1882 this.standardXHRResponse(this.requests.shift());
1883 // media.m3u8
1884 this.standardXHRResponse(this.requests.shift());
1885 // simulate a segment timeout
1886 this.requests[0].timedout = true;
1887 // segment
1888 this.requests.shift().abort();
1889
1890 this.standardXHRResponse(this.requests.shift());
1891
1892 assert.strictEqual(this.player.tech_.hls.playlists.media(),
1893 this.player.tech_.hls.playlists.master.playlists[1],
1894 'reset to the lowest bitrate playlist');
1895
1896 // verify stats
1897 assert.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth is reset too');
1898});
1899
1900QUnit.test('disposes the playlist loader', function(assert) {
1901 let disposes = 0;
1902 let player;
1903 let loaderDispose;
1904
1905 player = createPlayer();
1906 player.src({
1907 src: 'manifest/master.m3u8',
1908 type: 'application/vnd.apple.mpegurl'
1909 });
1910
1911 this.clock.tick(1);
1912
1913 openMediaSource(player, this.clock);
1914 loaderDispose = player.tech_.hls.playlists.dispose;
1915 player.tech_.hls.playlists.dispose = function() {
1916 disposes++;
1917 loaderDispose.call(player.tech_.hls.playlists);
1918 };
1919
1920 player.dispose();
1921 assert.strictEqual(disposes, 1, 'disposed playlist loader');
1922});
1923
1924QUnit.test('remove event handlers on dispose', function(assert) {
1925 let player;
1926 let unscoped = 0;
1927
1928 player = createPlayer();
1929 player.on = function(owner) {
1930 if (typeof owner !== 'object') {
1931 unscoped++;
1932 }
1933 };
1934 player.off = function(owner) {
1935 if (typeof owner !== 'object') {
1936 unscoped--;
1937 }
1938 };
1939 player.src({
1940 src: 'manifest/master.m3u8',
1941 type: 'application/vnd.apple.mpegurl'
1942 });
1943
1944 this.clock.tick(1);
1945
1946 openMediaSource(player, this.clock);
1947
1948 this.standardXHRResponse(this.requests[0]);
1949 this.standardXHRResponse(this.requests[1]);
1950
1951 player.dispose();
1952
1953 assert.ok(unscoped <= 0, 'no unscoped handlers');
1954});
1955
1956QUnit.test('the source handler supports HLS mime types', function(assert) {
1957 const techs = ['html5', 'flash'];
1958
1959 techs.forEach(function(techName) {
1960 assert.ok(HlsSourceHandler(techName).canHandleSource({
1961 type: 'aPplicatiOn/x-MPegUrl'
1962 }), 'supports x-mpegurl');
1963 assert.ok(HlsSourceHandler(techName).canHandleSource({
1964 type: 'aPplicatiOn/VnD.aPPle.MpEgUrL'
1965 }), 'supports vnd.apple.mpegurl');
1966 assert.ok(HlsSourceHandler(techName).canPlayType('aPplicatiOn/VnD.aPPle.MpEgUrL'),
1967 'supports vnd.apple.mpegurl');
1968 assert.ok(HlsSourceHandler(techName).canPlayType('aPplicatiOn/x-MPegUrl'),
1969 'supports x-mpegurl');
1970
1971 assert.ok(!(HlsSourceHandler(techName).canHandleSource({
1972 type: 'video/mp4'
1973 }) instanceof HlsHandler), 'does not support mp4');
1974 assert.ok(!(HlsSourceHandler(techName).canHandleSource({
1975 type: 'video/x-flv'
1976 }) instanceof HlsHandler), 'does not support flv');
1977 assert.ok(!(HlsSourceHandler(techName).canPlayType('video/mp4')),
1978 'does not support mp4');
1979 assert.ok(!(HlsSourceHandler(techName).canPlayType('video/x-flv')),
1980 'does not support flv');
1981 });
1982});
1983
1984QUnit.test('source handler does not support sources when IE 10 or below', function(assert) {
1985 videojs.browser.IE_VERSION = 10;
1986
1987 ['html5', 'flash'].forEach(function(techName) {
1988 assert.ok(!HlsSourceHandler(techName).canHandleSource({
1989 type: 'application/x-mpegURL'
1990 }), 'does not support when browser is IE10');
1991 });
1992});
1993
1994QUnit.test('fires loadstart manually if Flash is used', function(assert) {
1995 videojs.HlsHandler.prototype.setupQualityLevels_ = () => {};
1996 let tech = new (videojs.getTech('Flash'))({});
1997 let loadstarts = 0;
1998
1999 tech.on('loadstart', function() {
2000 loadstarts++;
2001 });
2002 HlsSourceHandler('flash').handleSource({
2003 src: 'movie.m3u8',
2004 type: 'application/x-mpegURL'
2005 }, tech);
2006
2007 assert.equal(loadstarts, 0, 'loadstart is not synchronous');
2008 this.clock.tick(1);
2009 assert.equal(loadstarts, 1, 'fired loadstart');
2010 tech.dispose();
2011 videojs.HlsHandler.prototype.setupQualityLevels_ = ogHlsHandlerSetupQualityLevels;
2012});
2013
2014QUnit.test('has no effect if native HLS is available', function(assert) {
2015 const Html5 = videojs.getTech('Html5');
2016 const oldHtml5CanPlaySource = Html5.canPlaySource;
2017 let player;
2018
2019 Html5.canPlaySource = () => true;
2020 Hls.supportsNativeHls = true;
2021 player = createPlayer();
2022 player.src({
2023 src: 'http://example.com/manifest/master.m3u8',
2024 type: 'application/x-mpegURL'
2025 });
2026
2027 this.clock.tick(1);
2028
2029 assert.ok(!player.tech_.hls, 'did not load hls tech');
2030 player.dispose();
2031 Html5.canPlaySource = oldHtml5CanPlaySource;
2032});
2033
2034QUnit.test('loads if native HLS is available and override is set locally', function(assert) {
2035 let player;
2036
2037 Hls.supportsNativeHls = true;
2038 player = createPlayer({html5: {hls: {overrideNative: true}}});
2039 this.clock.tick(1);
2040 player.tech_.featuresNativeVideoTracks = true;
2041 assert.throws(function() {
2042 player.src({
2043 src: 'http://example.com/manifest/master.m3u8',
2044 type: 'application/x-mpegURL'
2045 });
2046 this.clock.tick(1);
2047 }, 'errors if native tracks are enabled');
2048 player.dispose();
2049
2050 player = createPlayer({html5: {hls: {overrideNative: true}}});
2051 this.clock.tick(1);
2052 player.tech_.featuresNativeVideoTracks = false;
2053 player.tech_.featuresNativeAudioTracks = false;
2054 player.src({
2055 src: 'http://example.com/manifest/master.m3u8',
2056 type: 'application/x-mpegURL'
2057 });
2058 this.clock.tick(1);
2059
2060 assert.ok(player.tech_.hls, 'did load hls tech');
2061 player.dispose();
2062});
2063
2064QUnit.test('loads if native HLS is available and override is set globally', function(assert) {
2065 videojs.options.hls.overrideNative = true;
2066 let player;
2067
2068 Hls.supportsNativeHls = true;
2069 player = createPlayer();
2070 player.tech_.featuresNativeVideoTracks = true;
2071 assert.throws(function() {
2072 player.src({
2073 src: 'http://example.com/manifest/master.m3u8',
2074 type: 'application/x-mpegURL'
2075 });
2076 this.clock.tick(1);
2077 }, 'errors if native tracks are enabled');
2078 player.dispose();
2079
2080 player = createPlayer();
2081 player.tech_.featuresNativeVideoTracks = false;
2082 player.tech_.featuresNativeAudioTracks = false;
2083 player.src({
2084 src: 'http://example.com/manifest/master.m3u8',
2085 type: 'application/x-mpegURL'
2086 });
2087
2088 this.clock.tick(1);
2089
2090 assert.ok(player.tech_.hls, 'did load hls tech');
2091 player.dispose();
2092});
2093
2094QUnit.test('re-emits mediachange events', function(assert) {
2095 let mediaChanges = 0;
2096
2097 this.player.on('mediachange', function() {
2098 mediaChanges++;
2099 });
2100
2101 this.player.src({
2102 src: 'http://example.com/media.m3u8',
2103 type: 'application/vnd.apple.mpegurl'
2104 });
2105
2106 this.clock.tick(1);
2107
2108 openMediaSource(this.player, this.clock);
2109 this.standardXHRResponse(this.requests.shift());
2110
2111 this.player.tech_.hls.playlists.trigger('mediachange');
2112 assert.strictEqual(mediaChanges, 1, 'fired mediachange');
2113});
2114
2115QUnit.test('can be disposed before finishing initialization', function(assert) {
2116 let readyHandlers = [];
2117
2118 this.player.ready = function(callback) {
2119 readyHandlers.push(callback);
2120 };
2121 this.player.src({
2122 src: 'http://example.com/media.m3u8',
2123 type: 'application/vnd.apple.mpegurl'
2124 });
2125
2126 this.clock.tick(1);
2127 readyHandlers.shift().call(this.player);
2128
2129 this.player.src({
2130 src: 'http://example.com/media.mp4',
2131 type: 'video/mp4'
2132 });
2133
2134 assert.ok(readyHandlers.length > 0, 'registered a ready handler');
2135 try {
2136 while (readyHandlers.length) {
2137 readyHandlers.shift().call(this.player);
2138 openMediaSource(this.player, this.clock);
2139 }
2140 assert.ok(true, 'did not throw an exception');
2141 } catch (e) {
2142 assert.ok(false, 'threw an exception');
2143 }
2144});
2145
2146QUnit.test('calling play() at the end of a video replays', function(assert) {
2147 let seekTime = -1;
2148
2149 this.player.src({
2150 src: 'http://example.com/media.m3u8',
2151 type: 'application/vnd.apple.mpegurl'
2152 });
2153
2154 this.clock.tick(1);
2155
2156 openMediaSource(this.player, this.clock);
2157 this.player.tech_.setCurrentTime = function(time) {
2158 if (typeof time !== 'undefined') {
2159 seekTime = time;
2160 }
2161 return 0;
2162 };
2163 this.requests.shift().respond(200, null,
2164 '#EXTM3U\n' +
2165 '#EXTINF:10,\n' +
2166 '0.ts\n' +
2167 '#EXT-X-ENDLIST\n');
2168 this.clock.tick(1);
2169
2170 this.standardXHRResponse(this.requests.shift());
2171 this.player.tech_.ended = function() {
2172 return true;
2173 };
2174
2175 this.player.tech_.trigger('play');
2176 this.clock.tick(1);
2177 assert.equal(seekTime, 0, 'seeked to the beginning');
2178
2179 // verify stats
2180 assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
2181 assert.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
2182});
2183
2184QUnit.test('keys are resolved relative to the master playlist', function(assert) {
2185 this.player.src({
2186 src: 'video/master-encrypted.m3u8',
2187 type: 'application/vnd.apple.mpegurl'
2188 });
2189
2190 this.clock.tick(1);
2191
2192 openMediaSource(this.player, this.clock);
2193 this.requests.shift().respond(200, null,
2194 '#EXTM3U\n' +
2195 '#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=17\n' +
2196 'playlist/playlist.m3u8\n' +
2197 '#EXT-X-ENDLIST\n');
2198 this.clock.tick(1);
2199
2200 this.requests.shift().respond(200, null,
2201 '#EXTM3U\n' +
2202 '#EXT-X-TARGETDURATION:15\n' +
2203 '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' +
2204 '#EXTINF:2.833,\n' +
2205 'http://media.example.com/fileSequence1.ts\n' +
2206 '#EXT-X-ENDLIST\n');
2207 this.clock.tick(1);
2208
2209 assert.equal(this.requests.length, 2, 'requested the key');
2210 assert.equal(this.requests[0].url,
2211 absoluteUrl('video/playlist/keys/key.php'),
2212 'resolves multiple relative paths');
2213
2214 // verify stats
2215 assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default');
2216});
2217
2218QUnit.test('keys are resolved relative to their containing playlist', function(assert) {
2219 this.player.src({
2220 src: 'video/media-encrypted.m3u8',
2221 type: 'application/vnd.apple.mpegurl'
2222 });
2223
2224 this.clock.tick(1);
2225
2226 openMediaSource(this.player, this.clock);
2227 this.requests.shift().respond(200, null,
2228 '#EXTM3U\n' +
2229 '#EXT-X-TARGETDURATION:15\n' +
2230 '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' +
2231 '#EXTINF:2.833,\n' +
2232 'http://media.example.com/fileSequence1.ts\n' +
2233 '#EXT-X-ENDLIST\n');
2234 this.clock.tick(1);
2235
2236 assert.equal(this.requests.length, 2, 'requested a key');
2237 assert.equal(this.requests[0].url,
2238 absoluteUrl('video/keys/key.php'),
2239 'resolves multiple relative paths');
2240});
2241
2242QUnit.test('seeking should abort an outstanding key request and create a new one', function(assert) {
2243 this.player.src({
2244 src: 'https://example.com/encrypted.m3u8',
2245 type: 'application/vnd.apple.mpegurl'
2246 });
2247
2248 this.clock.tick(1);
2249
2250 openMediaSource(this.player, this.clock);
2251
2252 this.requests.shift().respond(200, null,
2253 '#EXTM3U\n' +
2254 '#EXT-X-TARGETDURATION:15\n' +
2255 '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' +
2256 '#EXTINF:9,\n' +
2257 'http://media.example.com/fileSequence1.ts\n' +
2258 '#EXT-X-KEY:METHOD=AES-128,URI="keys/key2.php"\n' +
2259 '#EXTINF:9,\n' +
2260 'http://media.example.com/fileSequence2.ts\n' +
2261 '#EXT-X-ENDLIST\n');
2262 this.clock.tick(1);
2263
2264 // segment 1
2265 this.standardXHRResponse(this.requests.pop());
2266
2267 this.player.currentTime(11);
2268 this.clock.tick(2);
2269 assert.ok(this.requests[0].aborted, 'the key XHR should be aborted');
2270 // aborted key 1
2271 this.requests.shift();
2272
2273 assert.equal(this.requests.length, 2, 'requested the new key');
2274 assert.equal(this.requests[0].url,
2275 'https://example.com/' +
2276 this.player.tech_.hls.playlists.media().segments[1].key.uri,
2277 'urls should match');
2278
2279 // verify stats
2280 assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
2281 assert.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
2282});
2283
2284QUnit.test('switching playlists with an outstanding key request aborts request and ' +
2285 'loads segment', function(assert) {
2286 let keyXhr;
2287 let media = '#EXTM3U\n' +
2288 '#EXT-X-MEDIA-SEQUENCE:5\n' +
2289 '#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"\n' +
2290 '#EXTINF:2.833,\n' +
2291 'http://media.example.com/fileSequence52-A.ts\n' +
2292 '#EXTINF:15.0,\n' +
2293 'http://media.example.com/fileSequence52-B.ts\n' +
2294 '#EXT-X-ENDLIST\n';
2295
2296 this.player.src({
2297 src: 'https://example.com/master.m3u8',
2298 type: 'application/vnd.apple.mpegurl'
2299 });
2300
2301 this.clock.tick(1);
2302
2303 openMediaSource(this.player, this.clock);
2304 this.player.tech_.trigger('play');
2305 this.clock.tick(1);
2306
2307 // master playlist
2308 this.standardXHRResponse(this.requests.shift());
2309 // media playlist
2310 this.requests.shift().respond(200, null, media);
2311 this.clock.tick(1);
2312
2313 // first segment of the original media playlist
2314 this.standardXHRResponse(this.requests.pop());
2315
2316 assert.equal(this.requests.length, 1, 'key request only one outstanding');
2317 keyXhr = this.requests.shift();
2318 assert.ok(!keyXhr.aborted, 'key request outstanding');
2319
2320 this.player.tech_.hls.playlists.trigger('mediachanging');
2321 this.player.tech_.hls.playlists.trigger('mediachange');
2322 this.clock.tick(1);
2323
2324 assert.ok(keyXhr.aborted, 'key request aborted');
2325 assert.equal(this.requests.length, 2, 'loaded key and segment');
2326 assert.equal(this.requests[0].url,
2327 'https://priv.example.com/key.php?r=52',
2328 'requested the key');
2329 assert.equal(this.requests[1].url,
2330 'http://media.example.com/fileSequence52-A.ts',
2331 'requested the segment');
2332 // verify stats
2333 assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
2334 assert.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
2335});
2336
2337QUnit.test('does not download segments if preload option set to none', function(assert) {
2338 this.player.preload('none');
2339 this.player.src({
2340 src: 'master.m3u8',
2341 type: 'application/vnd.apple.mpegurl'
2342 });
2343
2344 this.clock.tick(1);
2345
2346 openMediaSource(this.player, this.clock);
2347 // master
2348 this.standardXHRResponse(this.requests.shift());
2349 // media
2350 this.standardXHRResponse(this.requests.shift());
2351 this.clock.tick(10 * 1000);
2352
2353 this.requests = this.requests.filter(function(request) {
2354 return !(/m3u8$/).test(request.uri);
2355 });
2356 assert.equal(this.requests.length, 0, 'did not download any segments');
2357
2358 // verify stats
2359 assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default');
2360});
2361
2362// workaround https://bugzilla.mozilla.org/show_bug.cgi?id=548397
2363QUnit.test('selectPlaylist does not fail if getComputedStyle returns null', function(assert) {
2364 let oldGetComputedStyle = window.getComputedStyle;
2365
2366 window.getComputedStyle = function() {
2367 return null;
2368 };
2369
2370 this.player.src({
2371 src: 'master.m3u8',
2372 type: 'application/vnd.apple.mpegurl'
2373 });
2374
2375 this.clock.tick(1);
2376
2377 openMediaSource(this.player, this.clock);
2378 // master
2379 this.standardXHRResponse(this.requests.shift());
2380 // media
2381 this.standardXHRResponse(this.requests.shift());
2382
2383 this.player.tech_.hls.selectPlaylist();
2384 assert.ok(true, 'should not throw');
2385 window.getComputedStyle = oldGetComputedStyle;
2386
2387 // verify stats
2388 assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default');
2389});
2390
2391QUnit.test('resolves relative key URLs against the playlist', function(assert) {
2392 this.player.src({
2393 src: 'https://example.com/media.m3u8',
2394 type: 'application/vnd.apple.mpegurl'
2395 });
2396
2397 this.clock.tick(1);
2398
2399 openMediaSource(this.player, this.clock);
2400 this.requests.shift()
2401 .respond(200, null,
2402 '#EXTM3U\n' +
2403 '#EXT-X-MEDIA-SEQUENCE:5\n' +
2404 '#EXT-X-KEY:METHOD=AES-128,URI="key.php?r=52"\n' +
2405 '#EXTINF:2.833,\n' +
2406 'http://media.example.com/fileSequence52-A.ts\n' +
2407 '#EXT-X-ENDLIST\n');
2408 this.clock.tick(1);
2409
2410 assert.equal(this.requests[0].url,
2411 'https://example.com/key.php?r=52',
2412 'resolves the key URL');
2413});
2414
2415QUnit.test('adds 1 default audio track if we have not parsed any and the playlist is loaded', function(assert) {
2416 this.player.src({
2417 src: 'manifest/master.m3u8',
2418 type: 'application/vnd.apple.mpegurl'
2419 });
2420
2421 this.clock.tick(1);
2422
2423 assert.equal(this.player.audioTracks().length, 0, 'zero audio tracks at load time');
2424
2425 openMediaSource(this.player, this.clock);
2426
2427 // master
2428 this.standardXHRResponse(this.requests.shift());
2429 // media
2430 this.standardXHRResponse(this.requests.shift());
2431
2432 assert.equal(this.player.audioTracks().length, 1, 'one audio track after load');
2433 assert.equal(this.player.audioTracks()[0].label, 'default', 'set the label');
2434});
2435
2436QUnit.test('adds 1 default audio track if in flash mode', function(assert) {
2437 let hlsOptions = videojs.options.hls;
2438
2439 this.player.dispose();
2440 videojs.options.hls = {
2441 mode: 'flash'
2442 };
2443
2444 this.player = createPlayer();
2445
2446 this.player.src({
2447 src: 'manifest/multipleAudioGroups.m3u8',
2448 type: 'application/vnd.apple.mpegurl'
2449 });
2450
2451 this.clock.tick(1);
2452
2453 assert.equal(this.player.audioTracks().length, 0, 'zero audio tracks at load time');
2454
2455 openMediaSource(this.player, this.clock);
2456
2457 // master
2458 this.standardXHRResponse(this.requests.shift());
2459 // media
2460 this.standardXHRResponse(this.requests.shift());
2461
2462 assert.equal(this.player.audioTracks().length, 1, 'one audio track after load');
2463 assert.equal(this.player.audioTracks()[0].label, 'default', 'set the label');
2464
2465 videojs.options.hls = hlsOptions;
2466});
2467
2468QUnit.test('adds audio tracks if we have parsed some from a playlist', function(assert) {
2469 this.player.src({
2470 src: 'manifest/multipleAudioGroups.m3u8',
2471 type: 'application/vnd.apple.mpegurl'
2472 });
2473
2474 this.clock.tick(1);
2475
2476 assert.equal(this.player.audioTracks().length, 0, 'zero audio tracks at load time');
2477
2478 openMediaSource(this.player, this.clock);
2479
2480 // master
2481 this.standardXHRResponse(this.requests.shift());
2482 // media
2483 this.standardXHRResponse(this.requests.shift());
2484 let vjsAudioTracks = this.player.audioTracks();
2485
2486 assert.equal(vjsAudioTracks.length, 3, '3 active vjs tracks');
2487
2488 assert.equal(vjsAudioTracks[0].enabled, true, 'default track is enabled');
2489
2490 vjsAudioTracks[1].enabled = true;
2491 assert.equal(vjsAudioTracks[1].enabled, true, 'new track is enabled on vjs');
2492 assert.equal(vjsAudioTracks[0].enabled, false, 'main track is disabled');
2493});
2494
2495QUnit.test('cleans up the buffer when loading live segments', function(assert) {
2496 let removes = [];
2497 let seekable = videojs.createTimeRanges([[0, 70]]);
2498
2499 this.player.src({
2500 src: 'liveStart30sBefore.m3u8',
2501 type: 'application/vnd.apple.mpegurl'
2502 });
2503
2504 this.clock.tick(1);
2505
2506 openMediaSource(this.player, this.clock);
2507
2508 this.player.tech_.hls.masterPlaylistController_.seekable = function() {
2509 return seekable;
2510 };
2511
2512 // This is so we do not track first call to remove during segment loader init
2513 this.player.tech_.hls.masterPlaylistController_.mainSegmentLoader_.resetEverything = function() {
2514 this.resetLoader();
2515 };
2516
2517 this.player.tech_.hls.mediaSource.addSourceBuffer = () => {
2518 let buffer = new (videojs.extend(videojs.EventTarget, {
2519 constructor() {},
2520 abort() {},
2521 buffered: videojs.createTimeRange(),
2522 appendBuffer() {},
2523 remove(start, end) {
2524 removes.push([start, end]);
2525 }
2526 }))();
2527
2528 this.player.tech_.hls.mediaSource.sourceBuffers = [buffer];
2529 return buffer;
2530 };
2531 this.player.tech_.hls.bandwidth = 20e10;
2532 this.player.tech_.triggerReady();
2533 this.standardXHRResponse(this.requests[0]);
2534
2535 this.player.tech_.hls.playlists.trigger('loadedmetadata');
2536 this.player.tech_.trigger('canplay');
2537 this.player.tech_.paused = function() {
2538 return false;
2539 };
2540 this.player.tech_.trigger('play');
2541 this.clock.tick(1);
2542
2543 // request first playable segment
2544 this.standardXHRResponse(this.requests[1]);
2545 this.clock.tick(1);
2546 this.player.tech_.hls.mediaSource.sourceBuffers[0].trigger('updateend');
2547
2548 this.clock.tick(1);
2549
2550 // request second playable segment
2551 this.standardXHRResponse(this.requests[2]);
2552
2553 assert.strictEqual(this.requests[0].url, 'liveStart30sBefore.m3u8',
2554 'master playlist requested');
2555 assert.equal(removes.length, 1, 'remove called');
2556 // segment-loader removes at currentTime - 30
2557 assert.deepEqual(removes[0], [0, 40],
2558 'remove called with the right range');
2559
2560 // verify stats
2561 assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 2048, '2048 bytes');
2562 assert.equal(this.player.tech_.hls.stats.mediaRequests, 2, '2 requests');
2563});
2564
2565QUnit.test('cleans up the buffer based on currentTime when loading a live segment ' +
2566 'if seekable start is after currentTime', function(assert) {
2567 let removes = [];
2568 let seekable = videojs.createTimeRanges([[0, 80]]);
2569
2570 this.player.src({
2571 src: 'liveStart30sBefore.m3u8',
2572 type: 'application/vnd.apple.mpegurl'
2573 });
2574 openMediaSource(this.player, this.clock);
2575 this.player.tech_.hls.masterPlaylistController_.seekable = function() {
2576 return seekable;
2577 };
2578
2579 // This is so we do not track first call to remove during segment loader init
2580 this.player.tech_.hls.masterPlaylistController_.mainSegmentLoader_.resetEverything = function() {
2581 this.resetLoader();
2582 };
2583
2584 this.player.tech_.hls.mediaSource.addSourceBuffer = () => {
2585 let buffer = new (videojs.extend(videojs.EventTarget, {
2586 constructor() {},
2587 abort() {},
2588 buffered: videojs.createTimeRange(),
2589 appendBuffer() {},
2590 remove(start, end) {
2591 removes.push([start, end]);
2592 }
2593 }))();
2594
2595 this.player.tech_.hls.mediaSource.sourceBuffers = [buffer];
2596 return buffer;
2597
2598 };
2599 this.player.tech_.hls.bandwidth = 20e10;
2600 this.player.tech_.triggerReady();
2601 this.standardXHRResponse(this.requests[0]);
2602 this.player.tech_.hls.playlists.trigger('loadedmetadata');
2603 this.player.tech_.trigger('canplay');
2604
2605 this.player.tech_.paused = function() {
2606 return false;
2607 };
2608
2609 this.player.tech_.trigger('play');
2610 this.clock.tick(1);
2611
2612 // request first playable segment
2613 this.standardXHRResponse(this.requests[1]);
2614
2615 this.clock.tick(1);
2616 this.player.tech_.hls.mediaSource.sourceBuffers[0].trigger('updateend');
2617
2618 // Change seekable so that it starts *after* the currentTime which was set
2619 // based on the previous seekable range (the end of 80)
2620 seekable = videojs.createTimeRanges([[100, 120]]);
2621
2622 this.clock.tick(1);
2623
2624 // request second playable segment
2625 this.standardXHRResponse(this.requests[2]);
2626
2627 assert.strictEqual(this.requests[0].url, 'liveStart30sBefore.m3u8', 'master playlist requested');
2628 assert.equal(removes.length, 1, 'remove called');
2629 assert.deepEqual(removes[0], [0, 80 - 30], 'remove called with the right range');
2630});
2631
2632QUnit.test('cleans up the buffer when loading VOD segments', function(assert) {
2633 let removes = [];
2634
2635 this.player.src({
2636 src: 'manifest/master.m3u8',
2637 type: 'application/vnd.apple.mpegurl'
2638 });
2639
2640 this.clock.tick(1);
2641
2642 openMediaSource(this.player, this.clock);
2643
2644 // This is so we do not track first call to remove during segment loader init
2645 this.player.tech_.hls.masterPlaylistController_.mainSegmentLoader_.resetEverything = function() {
2646 this.resetLoader();
2647 };
2648
2649 this.player.tech_.hls.mediaSource.addSourceBuffer = () => {
2650 let buffer = new (videojs.extend(videojs.EventTarget, {
2651 constructor() {},
2652 abort() {},
2653 buffered: videojs.createTimeRange(),
2654 appendBuffer() {},
2655 remove(start, end) {
2656 removes.push([start, end]);
2657 }
2658 }))();
2659
2660 this.player.tech_.hls.mediaSource.sourceBuffers = [buffer];
2661 return buffer;
2662
2663 };
2664 this.player.width(640);
2665 this.player.height(360);
2666 this.player.tech_.hls.bandwidth = 20e10;
2667 this.standardXHRResponse(this.requests[0]);
2668 this.standardXHRResponse(this.requests[1]);
2669 this.standardXHRResponse(this.requests[2]);
2670 this.clock.tick(1);
2671 this.player.currentTime(120);
2672 this.player.tech_.hls.mediaSource.sourceBuffers[0].trigger('updateend');
2673 // This requires 2 clock ticks because after updateend monitorBuffer_ is called
2674 // to setup fillBuffer on the next tick, but the seek also causes monitorBuffer_ to be
2675 // called, which cancels the previously set timeout and sets a new one for the following
2676 // tick.
2677 this.clock.tick(2);
2678 this.standardXHRResponse(this.requests[3]);
2679
2680 assert.strictEqual(this.requests[0].url, 'manifest/master.m3u8',
2681 'master playlist requested');
2682 assert.strictEqual(this.requests[1].url, absoluteUrl('manifest/media3.m3u8'),
2683 'media playlist requested');
2684 assert.equal(removes.length, 1, 'remove called');
2685 assert.deepEqual(removes[0], [0, 120 - 30], 'remove called with the right range');
2686});
2687
2688QUnit.test('when mediaGroup changes enabled track should not change', function(assert) {
2689 let hlsAudioChangeEvents = 0;
2690
2691 this.player.src({
2692 src: 'manifest/multipleAudioGroups.m3u8',
2693 type: 'application/vnd.apple.mpegurl'
2694 });
2695
2696 this.clock.tick(1);
2697
2698 openMediaSource(this.player, this.clock);
2699
2700 this.player.tech_.on('usage', (event) => {
2701 if (event.name === 'hls-audio-change') {
2702 hlsAudioChangeEvents++;
2703 }
2704 });
2705
2706 // master
2707 this.standardXHRResponse(this.requests.shift());
2708 // video media
2709 this.standardXHRResponse(this.requests.shift());
2710 let hls = this.player.tech_.hls;
2711 let mpc = hls.masterPlaylistController_;
2712 let audioTracks = this.player.audioTracks();
2713
2714 assert.equal(hlsAudioChangeEvents, 0, 'no hls-audio-change event was fired');
2715 assert.equal(audioTracks.length, 3, 'three audio tracks after load');
2716 assert.equal(audioTracks[0].enabled, true, 'track one enabled after load');
2717
2718 let oldMediaGroup = hls.playlists.media().attributes.AUDIO;
2719
2720 // clear out any outstanding requests
2721 this.requests.length = 0;
2722 // force mpc to select a playlist from a new media group
2723 mpc.masterPlaylistLoader_.media(mpc.master().playlists[0]);
2724 this.clock.tick(1);
2725
2726 // video media
2727 this.standardXHRResponse(this.requests.shift());
2728
2729 assert.notEqual(oldMediaGroup, hls.playlists.media().attributes.AUDIO, 'selected a new playlist');
2730 audioTracks = this.player.audioTracks();
2731 let activeGroup = mpc.mediaTypes_.AUDIO.activeGroup(audioTracks[0]);
2732
2733 assert.equal(audioTracks.length, 3, 'three audio tracks after changing mediaGroup');
2734 assert.ok(activeGroup.default, 'track one should be the default');
2735 assert.ok(audioTracks[0].enabled, 'enabled the default track');
2736 assert.notOk(audioTracks[1].enabled, 'disabled track two');
2737 assert.notOk(audioTracks[2].enabled, 'disabled track three');
2738
2739 audioTracks[1].enabled = true;
2740 assert.notOk(audioTracks[0].enabled, 'disabled track one');
2741 assert.ok(audioTracks[1].enabled, 'enabled track two');
2742 assert.notOk(audioTracks[2].enabled, 'disabled track three');
2743
2744 oldMediaGroup = hls.playlists.media().attributes.AUDIO;
2745 // clear out any outstanding requests
2746 this.requests.length = 0;
2747 // swap back to the old media group
2748 // this playlist is already loaded so no new requests are made
2749 mpc.masterPlaylistLoader_.media(mpc.master().playlists[3]);
2750 this.clock.tick(1);
2751
2752 assert.notEqual(oldMediaGroup, hls.playlists.media().attributes.AUDIO, 'selected a new playlist');
2753 audioTracks = this.player.audioTracks();
2754
2755 assert.equal(hlsAudioChangeEvents, 1, 'an hls-audio-change event was fired');
2756 assert.equal(audioTracks.length, 3, 'three audio tracks after reverting mediaGroup');
2757 assert.notOk(audioTracks[0].enabled, 'the default track is still disabled');
2758 assert.ok(audioTracks[1].enabled, 'track two is still enabled');
2759 assert.notOk(audioTracks[2].enabled, 'track three is still disabled');
2760});
2761
2762QUnit.test('Allows specifying the beforeRequest function on the player', function(assert) {
2763 let beforeRequestCalled = false;
2764
2765 this.player.src({
2766 src: 'master.m3u8',
2767 type: 'application/vnd.apple.mpegurl'
2768 });
2769
2770 this.clock.tick(1);
2771
2772 openMediaSource(this.player, this.clock);
2773
2774 this.player.tech_.hls.xhr.beforeRequest = function() {
2775 beforeRequestCalled = true;
2776 };
2777 // master
2778 this.standardXHRResponse(this.requests.shift());
2779 // media
2780 this.standardXHRResponse(this.requests.shift());
2781
2782 assert.ok(beforeRequestCalled, 'beforeRequest was called');
2783
2784 // verify stats
2785 assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default');
2786});
2787
2788QUnit.test('Allows specifying the beforeRequest function globally', function(assert) {
2789 let beforeRequestCalled = false;
2790
2791 videojs.Hls.xhr.beforeRequest = function() {
2792 beforeRequestCalled = true;
2793 };
2794
2795 this.player.src({
2796 src: 'master.m3u8',
2797 type: 'application/vnd.apple.mpegurl'
2798 });
2799
2800 this.clock.tick(1);
2801
2802 openMediaSource(this.player, this.clock);
2803 // master
2804 this.standardXHRResponse(this.requests.shift());
2805
2806 assert.ok(beforeRequestCalled, 'beforeRequest was called');
2807
2808 delete videojs.Hls.xhr.beforeRequest;
2809
2810 // verify stats
2811 assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default');
2812});
2813
2814QUnit.test('Allows overriding the global beforeRequest function', function(assert) {
2815 let beforeGlobalRequestCalled = 0;
2816 let beforeLocalRequestCalled = 0;
2817
2818 videojs.Hls.xhr.beforeRequest = function() {
2819 beforeGlobalRequestCalled++;
2820 };
2821
2822 this.player.src({
2823 src: 'master.m3u8',
2824 type: 'application/vnd.apple.mpegurl'
2825 });
2826
2827 this.clock.tick(1);
2828
2829 openMediaSource(this.player, this.clock);
2830
2831 this.player.tech_.hls.xhr.beforeRequest = function() {
2832 beforeLocalRequestCalled++;
2833 };
2834 // master
2835 this.standardXHRResponse(this.requests.shift());
2836 // media
2837 this.standardXHRResponse(this.requests.shift());
2838 // ts
2839 this.standardXHRResponse(this.requests.shift());
2840
2841 assert.equal(beforeLocalRequestCalled, 2, 'local beforeRequest was called twice ' +
2842 'for the media playlist and media');
2843 assert.equal(beforeGlobalRequestCalled, 1, 'global beforeRequest was called once ' +
2844 'for the master playlist');
2845
2846 delete videojs.Hls.xhr.beforeRequest;
2847
2848 // verify stats
2849 assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, 'seen above');
2850 assert.equal(this.player.tech_.hls.stats.mediaRequests, 1, 'one segment request');
2851});
2852
2853QUnit.test('passes useCueTags hls option to master playlist controller', function(assert) {
2854 this.player.src({
2855 src: 'master.m3u8',
2856 type: 'application/vnd.apple.mpegurl'
2857 });
2858
2859 this.clock.tick(1);
2860
2861 assert.ok(!this.player.tech_.hls.masterPlaylistController_.useCueTags_,
2862 'useCueTags is falsy by default');
2863
2864 let origHlsOptions = videojs.options.hls;
2865
2866 videojs.options.hls = {
2867 useCueTags: true
2868 };
2869
2870 this.player.dispose();
2871 this.player = createPlayer();
2872 this.player.src({
2873 src: 'http://example.com/media.m3u8',
2874 type: 'application/vnd.apple.mpegurl'
2875 });
2876
2877 this.clock.tick(1);
2878
2879 assert.ok(this.player.tech_.hls.masterPlaylistController_.useCueTags_,
2880 'useCueTags passed to master playlist controller');
2881
2882 videojs.options.hls = origHlsOptions;
2883});
2884
2885QUnit.test('populates quality levels list when available', function(assert) {
2886 this.player.src({
2887 src: 'manifest/master.m3u8',
2888 type: 'application/vnd.apple.mpegurl'
2889 });
2890
2891 this.clock.tick(1);
2892
2893 openMediaSource(this.player, this.clock);
2894
2895 assert.ok(this.player.tech_.hls.qualityLevels_, 'added quality levels');
2896
2897 let qualityLevels = this.player.qualityLevels();
2898 let addCount = 0;
2899 let changeCount = 0;
2900
2901 qualityLevels.on('addqualitylevel', () => {
2902 addCount++;
2903 });
2904
2905 qualityLevels.on('change', () => {
2906 changeCount++;
2907 });
2908
2909 // master
2910 this.standardXHRResponse(this.requests.shift());
2911 // media
2912 this.standardXHRResponse(this.requests.shift());
2913
2914 assert.equal(addCount, 4, 'four levels added from master');
2915 assert.equal(changeCount, 1, 'selected initial quality level');
2916
2917 this.player.dispose();
2918 this.player = createPlayer({}, {
2919 src: 'http://example.com/media.m3u8',
2920 type: 'application/vnd.apple.mpegurl'
2921 }, this.clock);
2922 openMediaSource(this.player, this.clock);
2923
2924 assert.ok(this.player.tech_.hls.qualityLevels_, 'added quality levels from video with source');
2925});
2926
2927QUnit.module('HLS Integration', {
2928 beforeEach(assert) {
2929 this.env = useFakeEnvironment(assert);
2930 this.requests = this.env.requests;
2931 this.mse = useFakeMediaSource();
2932 this.tech = new (videojs.getTech('Html5'))({});
2933 this.clock = this.env.clock;
2934
2935 this.standardXHRResponse = (request, data) => {
2936 standardXHRResponse(request, data);
2937
2938 // Because SegmentLoader#fillBuffer_ is now scheduled asynchronously
2939 // we have to use clock.tick to get the expected side effects of
2940 // SegmentLoader#handleUpdateEnd_
2941 this.clock.tick(1);
2942 };
2943
2944 videojs.HlsHandler.prototype.setupQualityLevels_ = () => {};
2945 },
2946 afterEach() {
2947 this.env.restore();
2948 this.mse.restore();
2949 videojs.HlsHandler.prototype.setupQualityLevels_ = ogHlsHandlerSetupQualityLevels;
2950 }
2951});
2952
2953QUnit.test('does not error when MediaSource is not defined', function(assert) {
2954 window.MediaSource = null;
2955
2956 let hls = HlsSourceHandler('html5').handleSource({
2957 src: 'manifest/alternateAudio.m3u8',
2958 type: 'application/vnd.apple.mpegurl'
2959 }, this.tech);
2960
2961 hls.mediaSource.trigger('sourceopen');
2962 // master
2963 this.standardXHRResponse(this.requests.shift());
2964 // media
2965 this.standardXHRResponse(this.requests.shift());
2966
2967 assert.ok(true, 'did not throw an exception');
2968});
2969
2970QUnit.test('aborts all in-flight work when disposed', function(assert) {
2971 let hls = HlsSourceHandler('html5').handleSource({
2972 src: 'manifest/master.m3u8',
2973 type: 'application/vnd.apple.mpegurl'
2974 }, this.tech);
2975
2976 hls.mediaSource.trigger('sourceopen');
2977 // master
2978 this.standardXHRResponse(this.requests.shift());
2979 // media
2980 this.standardXHRResponse(this.requests.shift());
2981
2982 hls.dispose();
2983 assert.ok(this.requests[0].aborted, 'aborted the old segment request');
2984 hls.mediaSource.sourceBuffers.forEach(sourceBuffer => {
2985 let lastUpdate = sourceBuffer.updates_[sourceBuffer.updates_.length - 1];
2986
2987 assert.ok(lastUpdate.abort, 'aborted the source buffer');
2988 });
2989});
2990
2991QUnit.test('stats are reset on dispose', function(assert) {
2992 let hls = HlsSourceHandler('html5').handleSource({
2993 src: 'manifest/master.m3u8',
2994 type: 'application/vnd.apple.mpegurl'
2995 }, this.tech);
2996
2997 hls.mediaSource.trigger('sourceopen');
2998 // master
2999 this.standardXHRResponse(this.requests.shift());
3000 // media
3001 this.standardXHRResponse(this.requests.shift());
3002
3003 // media
3004 this.standardXHRResponse(this.requests.shift());
3005
3006 assert.equal(hls.stats.mediaBytesTransferred, 1024, 'stat is set');
3007 hls.dispose();
3008 assert.equal(hls.stats.mediaBytesTransferred, 0, 'stat is reset');
3009});
3010
3011QUnit.test('detects fullscreen and triggers a quality change', function(assert) {
3012 let qualityChanges = 0;
3013 let hls = HlsSourceHandler('html5').handleSource({
3014 src: 'manifest/master.m3u8',
3015 type: 'application/vnd.apple.mpegurl'
3016 }, this.tech);
3017 let fullscreenElementName;
3018
3019 ['fullscreenElement', 'webkitFullscreenElement',
3020 'mozFullScreenElement', 'msFullscreenElement'
3021 ].forEach((name) => {
3022 if (!fullscreenElementName && !document.hasOwnProperty(name)) {
3023 fullscreenElementName = name;
3024 }
3025 });
3026
3027 hls.masterPlaylistController_.fastQualityChange_ = function() {
3028 qualityChanges++;
3029 };
3030
3031 // take advantage of capability detection to mock fullscreen activation
3032 document[fullscreenElementName] = this.tech.el();
3033 Events.trigger(document, 'fullscreenchange');
3034
3035 assert.equal(qualityChanges, 1, 'made a fast quality change');
3036
3037 // don't do a fast quality change when returning from fullscreen;
3038 // allow the video element to rescale the already buffered video
3039 document[fullscreenElementName] = null;
3040 Events.trigger(document, 'fullscreenchange');
3041
3042 assert.equal(qualityChanges, 1, 'did not make another quality change');
3043});
3044
3045QUnit.test('downloads additional playlists if required', function(assert) {
3046 let originalPlaylist;
3047 let hls = HlsSourceHandler('html5').handleSource({
3048 src: 'manifest/master.m3u8',
3049 type: 'application/vnd.apple.mpegurl'
3050 }, this.tech);
3051
3052 // Make segment metadata noop since most test segments dont have real data
3053 hls.masterPlaylistController_.mainSegmentLoader_.addSegmentMetadataCue_ = () => {};
3054
3055 hls.mediaSource.trigger('sourceopen');
3056 hls.bandwidth = 1;
3057 // master
3058 this.standardXHRResponse(this.requests[0]);
3059 // media
3060 this.standardXHRResponse(this.requests[1]);
3061 originalPlaylist = hls.playlists.media();
3062 hls.masterPlaylistController_.mainSegmentLoader_.mediaIndex = 0;
3063
3064 // the playlist selection is revisited after a new segment is downloaded
3065 this.requests[2].bandwidth = 3000000;
3066 // segment
3067 this.standardXHRResponse(this.requests[2]);
3068 // update the buffer to reflect the appended segment, and have enough buffer to
3069 // change playlist
3070 this.tech.buffered = () => videojs.createTimeRanges([[0, 30]]);
3071 hls.mediaSource.sourceBuffers[0].trigger('updateend');
3072
3073 // new media
3074 this.standardXHRResponse(this.requests[3]);
3075
3076 assert.ok((/manifest\/media\d+.m3u8$/).test(this.requests[3].url),
3077 'made a playlist request');
3078 assert.notEqual(originalPlaylist.resolvedUri,
3079 hls.playlists.media().resolvedUri,
3080 'a new playlists was selected');
3081 assert.ok(hls.playlists.media().segments, 'segments are now available');
3082
3083 // verify stats
3084 assert.equal(hls.stats.bandwidth, 3000000, 'default');
3085 assert.equal(hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
3086 assert.equal(hls.stats.mediaRequests, 1, '1 request');
3087});
3088
3089QUnit.test('waits to download new segments until the media playlist is stable', function(assert) {
3090 let sourceBuffer;
3091 let hls = HlsSourceHandler('html5').handleSource({
3092 src: 'manifest/master.m3u8',
3093 type: 'application/vnd.apple.mpegurl'
3094 }, this.tech);
3095
3096 hls.masterPlaylistController_.mainSegmentLoader_.addSegmentMetadataCue_ = () => {};
3097
3098 hls.mediaSource.trigger('sourceopen');
3099
3100 // make sure we stay on the lowest variant
3101 hls.bandwidth = 1;
3102 // master
3103 this.standardXHRResponse(this.requests.shift());
3104
3105 // media1
3106 this.standardXHRResponse(this.requests.shift());
3107
3108 // source buffer created after media source is open and first media playlist is selected
3109 sourceBuffer = hls.mediaSource.sourceBuffers[0];
3110 hls.masterPlaylistController_.mainSegmentLoader_.mediaIndex = 0;
3111
3112 // segment 0
3113 this.standardXHRResponse(this.requests.shift());
3114 // update the buffer to reflect the appended segment, and have enough buffer to
3115 // change playlist
3116 this.tech.buffered = () => videojs.createTimeRanges([[0, 30]]);
3117 // no time has elapsed, so bandwidth is really high and we'll switch
3118 // playlists
3119 sourceBuffer.trigger('updateend');
3120
3121 assert.equal(this.requests.length, 1, 'only the playlist request outstanding');
3122 this.clock.tick(10 * 1000);
3123 assert.equal(this.requests.length, 1, 'delays segment fetching');
3124
3125 // another media playlist
3126 this.standardXHRResponse(this.requests.shift());
3127 this.clock.tick(10 * 1000);
3128 assert.equal(this.requests.length, 1, 'resumes segment fetching');
3129
3130 // verify stats
3131 assert.equal(hls.stats.bandwidth, Infinity, 'default');
3132 assert.equal(hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
3133 assert.equal(hls.stats.mediaRequests, 1, '1 request');
3134});
3135
3136QUnit.test('live playlist starts three target durations before live', function(assert) {
3137 let hls = HlsSourceHandler('html5').handleSource({
3138 src: 'manifest/master.m3u8',
3139 type: 'application/vnd.apple.mpegurl'
3140 }, this.tech);
3141
3142 hls.mediaSource.trigger('sourceopen');
3143 this.requests.shift().respond(200, null,
3144 '#EXTM3U\n' +
3145 '#EXT-X-MEDIA-SEQUENCE:101\n' +
3146 '#EXTINF:10,\n' +
3147 '0.ts\n' +
3148 '#EXTINF:10,\n' +
3149 '1.ts\n' +
3150 '#EXTINF:10,\n' +
3151 '2.ts\n' +
3152 '#EXTINF:10,\n' +
3153 '3.ts\n' +
3154 '#EXTINF:10,\n' +
3155 '4.ts\n');
3156
3157 assert.equal(this.requests.length, 0, 'no outstanding segment request');
3158
3159 this.tech.paused = function() {
3160 return false;
3161 };
3162 this.tech.trigger('play');
3163 this.clock.tick(1);
3164 assert.equal(this.tech.currentTime(),
3165 hls.seekable().end(0),
3166 'seeked to the seekable end');
3167
3168 assert.equal(this.requests.length, 1, 'begins buffering');
3169
3170});
3171
3172QUnit.test('uses user defined selectPlaylist from HlsHandler if specified', function(assert) {
3173 let origStandardPlaylistSelector = Hls.STANDARD_PLAYLIST_SELECTOR;
3174 let defaultSelectPlaylistCount = 0;
3175
3176 Hls.STANDARD_PLAYLIST_SELECTOR = () => defaultSelectPlaylistCount++;
3177
3178 let hls = HlsSourceHandler('html5').handleSource({
3179 src: 'manifest/master.m3u8',
3180 type: 'application/vnd.apple.mpegurl'
3181 }, this.tech);
3182
3183 hls.masterPlaylistController_.selectPlaylist();
3184 assert.equal(defaultSelectPlaylistCount, 1, 'uses default playlist selector');
3185
3186 defaultSelectPlaylistCount = 0;
3187
3188 let newSelectPlaylistCount = 0;
3189 let newSelectPlaylist = () => newSelectPlaylistCount++;
3190
3191 HlsHandler.prototype.selectPlaylist = newSelectPlaylist;
3192
3193 hls = HlsSourceHandler('html5').handleSource({
3194 src: 'manifest/master.m3u8',
3195 type: 'application/vnd.apple.mpegurl'
3196 }, this.tech);
3197
3198 hls.masterPlaylistController_.selectPlaylist();
3199 assert.equal(defaultSelectPlaylistCount, 0, 'standard playlist selector not run');
3200 assert.equal(newSelectPlaylistCount, 1, 'uses overridden playlist selector');
3201
3202 newSelectPlaylistCount = 0;
3203
3204 let setSelectPlaylistCount = 0;
3205
3206 hls.selectPlaylist = () => setSelectPlaylistCount++;
3207
3208 hls.masterPlaylistController_.selectPlaylist();
3209 assert.equal(defaultSelectPlaylistCount, 0, 'standard playlist selector not run');
3210 assert.equal(newSelectPlaylistCount, 0, 'overridden playlist selector not run');
3211 assert.equal(setSelectPlaylistCount, 1, 'uses set playlist selector');
3212
3213 Hls.STANDARD_PLAYLIST_SELECTOR = origStandardPlaylistSelector;
3214 delete HlsHandler.prototype.selectPlaylist;
3215});
3216
3217QUnit.module('HLS - Encryption', {
3218 beforeEach(assert) {
3219 this.env = useFakeEnvironment(assert);
3220 this.requests = this.env.requests;
3221 this.mse = useFakeMediaSource();
3222 this.tech = new (videojs.getTech('Html5'))({});
3223 this.clock = this.env.clock;
3224
3225 this.standardXHRResponse = (request, data) => {
3226 standardXHRResponse(request, data);
3227
3228 // Because SegmentLoader#fillBuffer_ is now scheduled asynchronously
3229 // we have to use clock.tick to get the expected side effects of
3230 // SegmentLoader#handleUpdateEnd_
3231 this.clock.tick(1);
3232 };
3233
3234 videojs.HlsHandler.prototype.setupQualityLevels_ = () => {};
3235 },
3236 afterEach() {
3237 this.env.restore();
3238 this.mse.restore();
3239 videojs.HlsHandler.prototype.setupQualityLevels_ = ogHlsHandlerSetupQualityLevels;
3240 }
3241});
3242
3243QUnit.test('blacklists playlist if key requests fail', function(assert) {
3244 let hls = HlsSourceHandler('html5').handleSource({
3245 src: 'manifest/encrypted-master.m3u8',
3246 type: 'application/vnd.apple.mpegurl'
3247 }, this.tech);
3248
3249 hls.mediaSource.trigger('sourceopen');
3250 this.requests.shift()
3251 .respond(200, null,
3252 '#EXTM3U\n' +
3253 '#EXT-X-STREAM-INF:BANDWIDTH=1000\n' +
3254 'media.m3u8\n' +
3255 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
3256 'media1.m3u8\n');
3257 this.requests.shift()
3258 .respond(200, null,
3259 '#EXTM3U\n' +
3260 '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=52"\n' +
3261 '#EXTINF:2.833,\n' +
3262 'http://media.example.com/fileSequence52-A.ts\n' +
3263 '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=53"\n' +
3264 '#EXTINF:15.0,\n' +
3265 'http://media.example.com/fileSequence53-A.ts\n' +
3266 '#EXT-X-ENDLIST\n');
3267 this.clock.tick(1);
3268
3269 // segment 1
3270 if (/key\.php/i.test(this.requests[0].url)) {
3271 this.standardXHRResponse(this.requests.pop());
3272 } else {
3273 this.standardXHRResponse(this.requests.shift());
3274 }
3275 // fail key
3276 this.requests.shift().respond(404);
3277
3278 assert.ok(hls.playlists.media().excludeUntil > 0,
3279 'playlist blacklisted');
3280 assert.equal(this.env.log.warn.calls, 1, 'logged warning for blacklist');
3281});
3282
3283QUnit.test('treats invalid keys as a key request failure and blacklists playlist', function(assert) {
3284 let hls = HlsSourceHandler('html5').handleSource({
3285 src: 'manifest/encrypted-master.m3u8',
3286 type: 'application/vnd.apple.mpegurl'
3287 }, this.tech);
3288
3289 hls.mediaSource.trigger('sourceopen');
3290 this.requests.shift()
3291 .respond(200, null,
3292 '#EXTM3U\n' +
3293 '#EXT-X-STREAM-INF:BANDWIDTH=1000\n' +
3294 'media.m3u8\n' +
3295 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
3296 'media1.m3u8\n');
3297 this.requests.shift()
3298 .respond(200, null,
3299 '#EXTM3U\n' +
3300 '#EXT-X-MEDIA-SEQUENCE:5\n' +
3301 '#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"\n' +
3302 '#EXTINF:2.833,\n' +
3303 'http://media.example.com/fileSequence52-A.ts\n' +
3304 '#EXT-X-KEY:METHOD=NONE\n' +
3305 '#EXTINF:15.0,\n' +
3306 'http://media.example.com/fileSequence52-B.ts\n' +
3307 '#EXT-X-ENDLIST\n');
3308 this.clock.tick(1);
3309
3310 // segment request
3311 this.standardXHRResponse(this.requests.pop());
3312
3313 assert.equal(this.requests[0].url,
3314 'https://priv.example.com/key.php?r=52',
3315 'requested the key');
3316 // keys *should* be 16 bytes long -- this one is too small
3317 this.requests[0].response = new Uint8Array(1).buffer;
3318 this.requests.shift().respond(200, null, '');
3319 this.clock.tick(1);
3320
3321 // blacklist this playlist
3322 assert.ok(hls.playlists.media().excludeUntil > 0,
3323 'blacklisted playlist');
3324 assert.equal(this.env.log.warn.calls, 1, 'logged warning for blacklist');
3325
3326 // verify stats
3327 assert.equal(hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
3328 assert.equal(hls.stats.mediaRequests, 1, '1 request');
3329});