UNPKG

79.4 kBJavaScriptView Raw
1import QUnit from 'qunit';
2import videojs from 'video.js';
3import {
4 useFakeEnvironment,
5 useFakeMediaSource,
6 createPlayer,
7 standardXHRResponse,
8 openMediaSource
9} from './test-helpers.js';
10import manifests from './test-manifests.js';
11import {
12 MasterPlaylistController,
13 mimeTypesForPlaylist_,
14 mapLegacyAvcCodecs_
15} from '../src/master-playlist-controller';
16/* eslint-disable no-unused-vars */
17// we need this so that it can register hls with videojs
18import { Hls } from '../src/videojs-contrib-hls';
19/* eslint-enable no-unused-vars */
20import Playlist from '../src/playlist';
21import Config from '../src/config';
22
23const generateMedia = function(isMaat, isMuxed, hasVideoCodec, hasAudioCodec, isFMP4) {
24 const codec = (hasVideoCodec ? 'avc1.deadbeef' : '') +
25 (hasVideoCodec && hasAudioCodec ? ',' : '') +
26 (hasAudioCodec ? 'mp4a.40.E' : '');
27 const master = {
28 mediaGroups: {},
29 playlists: []
30 };
31 const media = {
32 attributes: {}
33 };
34
35 if (isMaat) {
36 master.mediaGroups.AUDIO = {
37 test: {
38 demuxed: {
39 uri: 'foo.bar'
40 }
41 }
42 };
43
44 if (isMuxed) {
45 master.mediaGroups.AUDIO.test.muxed = {};
46 }
47 media.attributes.AUDIO = 'test';
48 }
49
50 if (isFMP4) {
51 // This is not a great way to signal that the playlist is fmp4 but
52 // this is how we currently detect it in HLS so let's emulate it here
53 media.segments = [
54 {
55 map: 'test'
56 }
57 ];
58 }
59
60 if (hasVideoCodec || hasAudioCodec) {
61 media.attributes.CODECS = codec;
62 }
63
64 return [master, media];
65};
66
67QUnit.module('MasterPlaylistController', {
68 beforeEach(assert) {
69 this.env = useFakeEnvironment(assert);
70 this.clock = this.env.clock;
71 this.requests = this.env.requests;
72 this.mse = useFakeMediaSource();
73
74 // force the HLS tech to run
75 this.origSupportsNativeHls = videojs.Hls.supportsNativeHls;
76 videojs.Hls.supportsNativeHls = false;
77 this.oldBrowser = videojs.browser;
78 videojs.browser = videojs.mergeOptions({}, videojs.browser);
79 this.player = createPlayer();
80 this.player.src({
81 src: 'manifest/master.m3u8',
82 type: 'application/vnd.apple.mpegurl'
83 });
84
85 this.clock.tick(1);
86
87 this.standardXHRResponse = (request, data) => {
88 standardXHRResponse(request, data);
89
90 // Because SegmentLoader#fillBuffer_ is now scheduled asynchronously
91 // we have to use clock.tick to get the expected side effects of
92 // SegmentLoader#handleUpdateEnd_
93 this.clock.tick(1);
94 };
95
96 this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
97
98 // Make segment metadata noop since most test segments dont have real data
99 this.masterPlaylistController.mainSegmentLoader_.addSegmentMetadataCue_ = () => {};
100 },
101 afterEach() {
102 this.env.restore();
103 this.mse.restore();
104 videojs.Hls.supportsNativeHls = this.origSupportsNativeHls;
105 videojs.browser = this.oldBrowser;
106 this.player.dispose();
107 }
108});
109
110QUnit.test('throws error when given an empty URL', function(assert) {
111 let options = {
112 url: 'test',
113 tech: this.player.tech_
114 };
115
116 assert.ok(new MasterPlaylistController(options), 'can create with options');
117
118 options.url = '';
119 assert.throws(() => {
120 new MasterPlaylistController(options); // eslint-disable-line no-new
121 }, /A non-empty playlist URL is required/, 'requires a non empty url');
122});
123
124QUnit.test('obeys none preload option', function(assert) {
125 this.player.preload('none');
126 // master
127 this.standardXHRResponse(this.requests.shift());
128 // playlist
129 this.standardXHRResponse(this.requests.shift());
130
131 openMediaSource(this.player, this.clock);
132
133 assert.equal(this.requests.length, 0, 'no segment requests');
134
135 // verify stats
136 assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth');
137});
138
139QUnit.test('obeys auto preload option', function(assert) {
140 this.player.preload('auto');
141 // master
142 this.standardXHRResponse(this.requests.shift());
143 // playlist
144 this.standardXHRResponse(this.requests.shift());
145
146 openMediaSource(this.player, this.clock);
147
148 assert.equal(this.requests.length, 1, '1 segment request');
149
150 // verify stats
151 assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth');
152});
153
154QUnit.test('obeys metadata preload option', function(assert) {
155 this.player.preload('metadata');
156 // master
157 this.standardXHRResponse(this.requests.shift());
158 // playlist
159 this.standardXHRResponse(this.requests.shift());
160
161 openMediaSource(this.player, this.clock);
162
163 assert.equal(this.requests.length, 1, '1 segment request');
164
165 // verify stats
166 assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth');
167});
168
169QUnit.test('resets SegmentLoader when seeking in flash for both in and out of buffer',
170 function(assert) {
171 let resets = 0;
172
173 // master
174 this.standardXHRResponse(this.requests.shift());
175 // media
176 this.standardXHRResponse(this.requests.shift());
177 this.masterPlaylistController.mediaSource.trigger('sourceopen');
178
179 let mpc = this.masterPlaylistController;
180 let segmentLoader = mpc.mainSegmentLoader_;
181
182 segmentLoader.resetEverything = function() {
183 resets++;
184 };
185
186 let buffered;
187
188 mpc.tech_.buffered = function() {
189 return buffered;
190 };
191
192 buffered = videojs.createTimeRanges([[0, 20]]);
193 mpc.mode_ = 'html5';
194
195 mpc.setCurrentTime(10);
196 assert.equal(resets, 0,
197 'does not reset loader when seeking into a buffered region in html5');
198
199 mpc.setCurrentTime(21);
200 assert.equal(resets, 1,
201 'does reset loader when seeking outside of the buffered region in html5');
202
203 mpc.mode_ = 'flash';
204
205 mpc.setCurrentTime(10);
206 assert.equal(resets, 2,
207 'does reset loader when seeking into a buffered region in flash');
208
209 mpc.setCurrentTime(21);
210 assert.equal(resets, 3,
211 'does reset loader when seeking outside of the buffered region in flash');
212
213 });
214
215QUnit.test('resyncs SegmentLoader for a fast quality change', function(assert) {
216 let resyncs = 0;
217
218 // master
219 this.standardXHRResponse(this.requests.shift());
220 // media
221 this.standardXHRResponse(this.requests.shift());
222 this.masterPlaylistController.mediaSource.trigger('sourceopen');
223
224 let segmentLoader = this.masterPlaylistController.mainSegmentLoader_;
225
226 segmentLoader.resyncLoader = function() {
227 resyncs++;
228 };
229
230 this.masterPlaylistController.selectPlaylist = () => {
231 return this.masterPlaylistController.master().playlists[0];
232 };
233
234 this.masterPlaylistController.fastQualityChange_();
235
236 assert.equal(resyncs, 1, 'resynced the segmentLoader');
237
238 // verify stats
239 assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth');
240});
241
242QUnit.test('does not resync the segmentLoader when no fast quality change occurs',
243 function(assert) {
244 let resyncs = 0;
245
246 // master
247 this.standardXHRResponse(this.requests.shift());
248 // media
249 this.standardXHRResponse(this.requests.shift());
250 this.masterPlaylistController.mediaSource.trigger('sourceopen');
251
252 let segmentLoader = this.masterPlaylistController.mainSegmentLoader_;
253
254 segmentLoader.resyncLoader = function() {
255 resyncs++;
256 };
257
258 this.masterPlaylistController.fastQualityChange_();
259
260 assert.equal(resyncs, 0, 'did not resync the segmentLoader');
261 // verify stats
262 assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth');
263 });
264
265QUnit.test('fast quality change resyncs audio segment loader', function(assert) {
266 this.requests.length = 0;
267 this.player = createPlayer();
268 this.player.src({
269 src: 'alternate-audio-multiple-groups.m3u8',
270 type: 'application/vnd.apple.mpegurl'
271 });
272
273 this.clock.tick(1);
274
275 const masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
276
277 masterPlaylistController.selectPlaylist = () => {
278 return masterPlaylistController.master().playlists[0];
279 };
280
281 // master
282 this.standardXHRResponse(this.requests.shift());
283 // media
284 this.standardXHRResponse(this.requests.shift());
285
286 masterPlaylistController.mediaSource.trigger('sourceopen');
287
288 this.player.audioTracks()[0].enabled = true;
289
290 let resyncs = 0;
291 let resets = 0;
292 let realReset = masterPlaylistController.audioSegmentLoader_.resetLoader;
293
294 masterPlaylistController.audioSegmentLoader_.resetLoader = function() {
295 resets++;
296 realReset.call(this);
297 };
298
299 masterPlaylistController.audioSegmentLoader_.resyncLoader = () => resyncs++;
300 masterPlaylistController.fastQualityChange_();
301 assert.equal(resyncs, 0, 'does not resync the audio segment loader when media same');
302
303 // force different media
304 masterPlaylistController.selectPlaylist = () => {
305 return masterPlaylistController.master().playlists[1];
306 };
307
308 assert.equal(this.requests.length, 1, 'one request');
309 masterPlaylistController.fastQualityChange_();
310 assert.equal(this.requests.length, 2, 'added a request for new media');
311 assert.equal(resyncs, 0, 'does not resync the audio segment loader yet');
312 // new media request
313 this.standardXHRResponse(this.requests[1]);
314 assert.equal(resyncs, 1, 'resyncs the audio segment loader when media changes');
315 assert.equal(resets, 0, 'does not reset the audio segment loader when media changes');
316});
317
318QUnit.test('audio segment loader is reset on audio track change', function(assert) {
319 this.requests.length = 0;
320 this.player = createPlayer();
321 this.player.src({
322 src: 'alternate-audio-multiple-groups.m3u8',
323 type: 'application/vnd.apple.mpegurl'
324 });
325
326 this.clock.tick(1);
327
328 const masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
329
330 masterPlaylistController.selectPlaylist = () => {
331 return masterPlaylistController.master().playlists[0];
332 };
333
334 // master
335 this.standardXHRResponse(this.requests.shift());
336 // media
337 this.standardXHRResponse(this.requests.shift());
338
339 masterPlaylistController.mediaSource.trigger('sourceopen');
340
341 let resyncs = 0;
342 let resets = 0;
343 let realReset = masterPlaylistController.audioSegmentLoader_.resetLoader;
344
345 masterPlaylistController.audioSegmentLoader_.resetLoader = function() {
346 resets++;
347 realReset.call(this);
348 };
349 masterPlaylistController.audioSegmentLoader_.resyncLoader = () => resyncs++;
350
351 assert.equal(this.requests.length, 1, 'one request');
352 assert.equal(resyncs, 0, 'does not resync the audio segment loader yet');
353
354 this.player.audioTracks()[1].enabled = true;
355
356 assert.equal(this.requests.length, 2, 'two requests');
357 assert.equal(resyncs, 1, 'resyncs the audio segment loader when audio track changes');
358 assert.equal(resets, 1, 'resets the audio segment loader when audio track changes');
359});
360
361QUnit.test('if buffered, will request second segment byte range', function(assert) {
362 this.requests.length = 0;
363 this.player.src({
364 src: 'manifest/playlist.m3u8',
365 type: 'application/vnd.apple.mpegurl'
366 });
367
368 this.clock.tick(1);
369
370 this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
371 // Make segment metadata noop since most test segments dont have real data
372 this.masterPlaylistController.mainSegmentLoader_.addSegmentMetadataCue_ = () => {};
373
374 // mock that the user has played the video before
375 this.player.tech_.triggerReady();
376 this.clock.tick(1);
377 this.player.tech_.trigger('play');
378 this.player.tech_.paused_ = false;
379 this.player.tech_.played = () => videojs.createTimeRanges([[0, 20]]);
380
381 openMediaSource(this.player, this.clock);
382 // playlist
383 this.standardXHRResponse(this.requests[0]);
384
385 this.masterPlaylistController.mainSegmentLoader_.sourceUpdater_.buffered = () => {
386 return videojs.createTimeRanges([[0, 20]]);
387 };
388 // 1ms has passed to upload 1kb
389 // that gives us a bandwidth of 1024 / 1 * 8 * 1000 = 8192000
390 this.clock.tick(1);
391 // segment
392 this.standardXHRResponse(this.requests[1]);
393 this.masterPlaylistController.mainSegmentLoader_.fetchAtBuffer_ = true;
394 this.masterPlaylistController.mediaSource.sourceBuffers[0].trigger('updateend');
395 this.clock.tick(10 * 1000);
396 this.clock.tick(1);
397 assert.equal(this.requests[2].headers.Range, 'bytes=522828-1110327');
398
399 // verify stats
400 assert.equal(this.player.tech_.hls.stats.bandwidth, 8192000, 'Live stream');
401 assert.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 segment request');
402 assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred,
403 1024,
404 '1024 bytes downloaded');
405});
406
407QUnit.test('re-initializes the combined playlist loader when switching sources',
408function(assert) {
409 openMediaSource(this.player, this.clock);
410 // master
411 this.standardXHRResponse(this.requests.shift());
412 // playlist
413 this.standardXHRResponse(this.requests.shift());
414 // segment
415 this.standardXHRResponse(this.requests.shift());
416 // change the source
417 this.player.src({
418 src: 'manifest/master.m3u8',
419 type: 'application/vnd.apple.mpegurl'
420 });
421
422 this.clock.tick(1);
423
424 this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
425 // Make segment metadata noop since most test segments dont have real data
426 this.masterPlaylistController.mainSegmentLoader_.addSegmentMetadataCue_ = () => {};
427
428 // maybe not needed if https://github.com/videojs/video.js/issues/2326 gets fixed
429 this.clock.tick(1);
430 assert.ok(!this.masterPlaylistController.masterPlaylistLoader_.media(),
431 'no media playlist');
432 assert.equal(this.masterPlaylistController.masterPlaylistLoader_.state,
433 'HAVE_NOTHING',
434 'reset the playlist loader state');
435 assert.equal(this.requests.length, 1, 'requested the new src');
436
437 // buffer check
438 this.clock.tick(10 * 1000);
439 assert.equal(this.requests.length, 1, 'did not request a stale segment');
440
441 // sourceopen
442 openMediaSource(this.player, this.clock);
443
444 assert.equal(this.requests.length, 1, 'made one request');
445 assert.ok(
446 this.requests[0].url.indexOf('master.m3u8') >= 0,
447 'requested only the new playlist'
448 );
449});
450
451QUnit.test('updates the combined segment loader on live playlist refreshes',
452function(assert) {
453 let updates = [];
454
455 openMediaSource(this.player, this.clock);
456 // master
457 this.standardXHRResponse(this.requests.shift());
458 // media
459 this.standardXHRResponse(this.requests.shift());
460
461 this.masterPlaylistController.mainSegmentLoader_.playlist = function(update) {
462 updates.push(update);
463 };
464
465 this.masterPlaylistController.masterPlaylistLoader_.trigger('loadedplaylist');
466 assert.equal(updates.length, 1, 'updated the segment list');
467 // verify stats
468 assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth');
469});
470
471QUnit.test(
472'fires a progress event after downloading a segment from combined segment loader',
473function(assert) {
474 let progressCount = 0;
475
476 openMediaSource(this.player, this.clock);
477
478 // master
479 this.standardXHRResponse(this.requests.shift());
480 // media
481 this.standardXHRResponse(this.requests.shift());
482
483 this.player.tech_.on('progress', function() {
484 progressCount++;
485 });
486 // 1ms has passed to upload 1kb
487 // that gives us a bandwidth of 1024 / 1 * 8 * 1000 = 8192000
488 this.clock.tick(1);
489 // segment
490 this.standardXHRResponse(this.requests.shift());
491 this.masterPlaylistController.mainSegmentLoader_.trigger('progress');
492 assert.equal(progressCount, 1, 'fired a progress event');
493
494 // verify stats
495 assert.equal(this.player.tech_.hls.stats.bandwidth, 8192000, 'Live stream');
496 assert.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 segment request');
497 assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred,
498 1024,
499 '1024 bytes downloaded');
500});
501
502QUnit.test('updates the enabled track when switching audio groups', function(assert) {
503 openMediaSource(this.player, this.clock);
504 // master
505 this.requests.shift().respond(200, null,
506 manifests.multipleAudioGroupsCombinedMain);
507 // media
508 this.standardXHRResponse(this.requests.shift());
509 // init segment
510 this.standardXHRResponse(this.requests.shift());
511 // video segment
512 this.standardXHRResponse(this.requests.shift());
513 // audio media
514 this.standardXHRResponse(this.requests.shift());
515 // ignore audio segment requests
516 this.requests.length = 0;
517
518 let mpc = this.masterPlaylistController;
519 let combinedPlaylist = mpc.master().playlists[0];
520
521 mpc.masterPlaylistLoader_.media(combinedPlaylist);
522 // updated media
523 this.requests.shift().respond(200, null,
524 '#EXTM3U\n' +
525 '#EXTINF:5.0\n' +
526 '0.ts\n' +
527 '#EXT-X-ENDLIST\n');
528
529 assert.ok(mpc.activeAudioGroup().filter((track) => track.enabled)[0],
530 'enabled a track in the new audio group');
531});
532
533QUnit.test('waits for both main and audio loaders to finish before calling endOfStream',
534function(assert) {
535 openMediaSource(this.player, this.clock);
536
537 const videoMedia = '#EXTM3U\n' +
538 '#EXT-X-VERSION:3\n' +
539 '#EXT-X-PLAYLIST-TYPE:VOD\n' +
540 '#EXT-X-MEDIA-SEQUENCE:0\n' +
541 '#EXT-X-TARGETDURATION:10\n' +
542 '#EXTINF:10,\n' +
543 'video-0.ts\n' +
544 '#EXT-X-ENDLIST\n';
545
546 const audioMedia = '#EXTM3U\n' +
547 '#EXT-X-VERSION:3\n' +
548 '#EXT-X-PLAYLIST-TYPE:VOD\n' +
549 '#EXT-X-MEDIA-SEQUENCE:0\n' +
550 '#EXT-X-TARGETDURATION:10\n' +
551 '#EXTINF:10,\n' +
552 'audio-0.ts\n' +
553 '#EXT-X-ENDLIST\n';
554
555 let videoEnded = 0;
556 let audioEnded = 0;
557
558 const MPC = this.masterPlaylistController;
559
560 MPC.mainSegmentLoader_.on('ended', () => videoEnded++);
561 MPC.audioSegmentLoader_.on('ended', () => audioEnded++);
562
563 // master
564 this.standardXHRResponse(this.requests.shift(), manifests.demuxed);
565
566 // video media
567 this.standardXHRResponse(this.requests.shift(), videoMedia);
568
569 // audio media
570 this.standardXHRResponse(this.requests.shift(), audioMedia);
571
572 // video segment
573 this.standardXHRResponse(this.requests.shift());
574
575 MPC.mediaSource.sourceBuffers[0].trigger('updateend');
576
577 assert.equal(videoEnded, 1, 'main segment loader triggered endded');
578 assert.equal(audioEnded, 0, 'audio segment loader did not trigger ended');
579 assert.equal(MPC.mediaSource.readyState, 'open', 'Media Source not yet ended');
580
581 // audio segment
582 this.standardXHRResponse(this.requests.shift());
583
584 MPC.mediaSource.sourceBuffers[1].trigger('updateend');
585
586 assert.equal(videoEnded, 1, 'main segment loader did not trigger ended again');
587 assert.equal(audioEnded, 1, 'audio segment loader triggered ended');
588 assert.equal(MPC.mediaSource.readyState, 'ended', 'Media Source ended');
589});
590
591QUnit.test('detects if the player is stuck at the playlist end', function(assert) {
592 let playlistCopy = Hls.Playlist.playlistEnd;
593
594 this.masterPlaylistController.mediaSource.trigger('sourceopen');
595 this.standardXHRResponse(this.requests.shift());
596 let playlist = this.player.tech_.hls.selectPlaylist();
597
598 // not stuck at playlist end when no seekable, even if empty buffer
599 // and positive currentTime
600 this.masterPlaylistController.seekable = () => videojs.createTimeRange();
601 this.player.tech_.buffered = () => videojs.createTimeRange();
602 this.player.tech_.setCurrentTime(170);
603 assert.ok(!this.masterPlaylistController.stuckAtPlaylistEnd_(playlist),
604 'not stuck at playlist end');
605
606 // not stuck at playlist end when no seekable, even if empty buffer
607 // and currentTime 0
608 this.player.tech_.setCurrentTime(0);
609 assert.ok(!this.masterPlaylistController.stuckAtPlaylistEnd_(playlist),
610 'not stuck at playlist end');
611
612 // not stuck at playlist end when no seekable but current time is at
613 // the end of the buffered range
614 this.player.tech_.buffered = () => videojs.createTimeRange(0, 170);
615 assert.ok(!this.masterPlaylistController.stuckAtPlaylistEnd_(playlist),
616 'not stuck at playlist end');
617
618 // not stuck at playlist end when currentTime not at seekable end
619 // even if the buffer is empty
620 this.masterPlaylistController.seekable = () => videojs.createTimeRange(0, 130);
621 this.masterPlaylistController.syncController_.getExpiredTime = () => 0;
622 this.player.tech_.setCurrentTime(50);
623 this.player.tech_.buffered = () => videojs.createTimeRange();
624 Hls.Playlist.playlistEnd = () => 130;
625 assert.ok(!this.masterPlaylistController.stuckAtPlaylistEnd_(playlist),
626 'not stuck at playlist end');
627
628 // not stuck at playlist end when buffer reached the absolute end of the playlist
629 // and current time is in the buffered range
630 this.player.tech_.setCurrentTime(159);
631 this.player.tech_.buffered = () => videojs.createTimeRange(0, 160);
632 Hls.Playlist.playlistEnd = () => 160;
633 assert.ok(!this.masterPlaylistController.stuckAtPlaylistEnd_(playlist),
634 'not stuck at playlist end');
635
636 // stuck at playlist end when there is no buffer and playhead
637 // reached absolute end of playlist
638 this.player.tech_.setCurrentTime(160);
639 assert.ok(this.masterPlaylistController.stuckAtPlaylistEnd_(playlist),
640 'stuck at playlist end');
641
642 // stuck at playlist end when current time reached the buffer end
643 // and buffer has reached absolute end of playlist
644 this.masterPlaylistController.seekable = () => videojs.createTimeRange(90, 130);
645 this.player.tech_.buffered = () => videojs.createTimeRange(0, 170);
646 this.player.tech_.setCurrentTime(170);
647 Hls.Playlist.playlistEnd = () => 170;
648 assert.ok(this.masterPlaylistController.stuckAtPlaylistEnd_(playlist),
649 'stuck at playlist end');
650
651 Hls.Playlist.playlistEnd = playlistCopy;
652});
653
654QUnit.test('blacklists switching from video+audio playlists to audio only',
655function(assert) {
656 let audioPlaylist;
657
658 openMediaSource(this.player, this.clock);
659
660 this.player.tech_.hls.bandwidth = 1e10;
661
662 // master
663 this.requests.shift().respond(200, null,
664 '#EXTM3U\n' +
665 '#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="mp4a.40.2"\n' +
666 'media.m3u8\n' +
667 '#EXT-X-STREAM-INF:BANDWIDTH=10,RESOLUTION=1x1\n' +
668 'media1.m3u8\n');
669 // media1
670 this.standardXHRResponse(this.requests.shift());
671
672 assert.equal(this.masterPlaylistController.masterPlaylistLoader_.media(),
673 this.masterPlaylistController.masterPlaylistLoader_.master.playlists[1],
674 'selected video+audio');
675 audioPlaylist = this.masterPlaylistController.masterPlaylistLoader_.master.playlists[0];
676 assert.equal(audioPlaylist.excludeUntil, Infinity, 'excluded incompatible playlist');
677
678 // verify stats
679 assert.equal(this.player.tech_.hls.stats.bandwidth, 1e10, 'bandwidth we set above');
680});
681
682QUnit.test('blacklists switching from audio-only playlists to video+audio',
683function(assert) {
684 let videoAudioPlaylist;
685
686 openMediaSource(this.player, this.clock);
687
688 this.player.tech_.hls.bandwidth = 1;
689 // master
690 this.requests.shift().respond(200, null,
691 '#EXTM3U\n' +
692 '#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="mp4a.40.2"\n' +
693 'media.m3u8\n' +
694 '#EXT-X-STREAM-INF:BANDWIDTH=10,RESOLUTION=1x1\n' +
695 'media1.m3u8\n');
696
697 // media1
698 this.standardXHRResponse(this.requests.shift());
699 assert.equal(this.masterPlaylistController.masterPlaylistLoader_.media(),
700 this.masterPlaylistController.masterPlaylistLoader_.master.playlists[0],
701 'selected audio only');
702 videoAudioPlaylist =
703 this.masterPlaylistController.masterPlaylistLoader_.master.playlists[1];
704 assert.equal(videoAudioPlaylist.excludeUntil,
705 Infinity,
706 'excluded incompatible playlist');
707
708 // verify stats
709 assert.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth we set above');
710});
711
712QUnit.test('blacklists switching from video-only playlists to video+audio',
713function(assert) {
714 let videoAudioPlaylist;
715
716 openMediaSource(this.player, this.clock);
717
718 this.player.tech_.hls.bandwidth = 1;
719 // master
720 this.requests.shift()
721 .respond(200, null,
722 '#EXTM3U\n' +
723 '#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="avc1.4d400d"\n' +
724 'media.m3u8\n' +
725 '#EXT-X-STREAM-INF:BANDWIDTH=10,CODECS="avc1.4d400d,mp4a.40.2"\n' +
726 'media1.m3u8\n');
727
728 // media
729 this.standardXHRResponse(this.requests.shift());
730 assert.equal(this.masterPlaylistController.masterPlaylistLoader_.media(),
731 this.masterPlaylistController.masterPlaylistLoader_.master.playlists[0],
732 'selected video only');
733 videoAudioPlaylist =
734 this.masterPlaylistController.masterPlaylistLoader_.master.playlists[1];
735 assert.equal(videoAudioPlaylist.excludeUntil,
736 Infinity,
737 'excluded incompatible playlist');
738
739 // verify stats
740 assert.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth we set above');
741});
742
743QUnit.test('blacklists switching between playlists with incompatible audio codecs',
744function(assert) {
745 let alternatePlaylist;
746
747 openMediaSource(this.player, this.clock);
748
749 this.player.tech_.hls.bandwidth = 1;
750 // master
751 this.requests.shift()
752 .respond(200, null,
753 '#EXTM3U\n' +
754 '#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="avc1.4d400d,mp4a.40.5"\n' +
755 'media.m3u8\n' +
756 '#EXT-X-STREAM-INF:BANDWIDTH=10,CODECS="avc1.4d400d,mp4a.40.2"\n' +
757 'media1.m3u8\n');
758
759 // media
760 this.standardXHRResponse(this.requests.shift());
761 assert.equal(this.masterPlaylistController.masterPlaylistLoader_.media(),
762 this.masterPlaylistController.masterPlaylistLoader_.master.playlists[0],
763 'selected HE-AAC stream');
764 alternatePlaylist =
765 this.masterPlaylistController.masterPlaylistLoader_.master.playlists[1];
766 assert.equal(alternatePlaylist.excludeUntil,
767 undefined,
768 'not excluded incompatible playlist');
769 // verify stats
770 assert.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth we set above');
771});
772
773QUnit.test('blacklists the current playlist when audio changes in Firefox 48 & below',
774 function(assert) {
775 videojs.browser.IS_FIREFOX = true;
776
777 let origSupportsAudioInfoChange_ = videojs.Hls.supportsAudioInfoChange_;
778
779 videojs.Hls.supportsAudioInfoChange_ = () => false;
780
781 // master
782 this.standardXHRResponse(this.requests.shift());
783 // media
784 this.standardXHRResponse(this.requests.shift());
785
786 let media = this.masterPlaylistController.media();
787
788 // initial audio config
789 this.masterPlaylistController.mediaSource.trigger({
790 type: 'audioinfo',
791 info: {}
792 });
793 // updated audio config
794
795 this.masterPlaylistController.mediaSource.trigger({
796 type: 'audioinfo',
797 info: {
798 different: true
799 }
800 });
801 assert.ok(media.excludeUntil > 0, 'blacklisted the old playlist');
802 assert.equal(this.env.log.warn.callCount, 2, 'logged two warnings');
803 this.env.log.warn.callCount = 0;
804 videojs.Hls.supportsAudioInfoChange_ = origSupportsAudioInfoChange_;
805 });
806
807QUnit.test('updates the combined segment loader on media changes', function(assert) {
808 let updates = [];
809
810 this.masterPlaylistController.mediaSource.trigger('sourceopen');
811
812 this.masterPlaylistController.mainSegmentLoader_.bandwidth = 1;
813
814 // master
815 this.standardXHRResponse(this.requests.shift());
816 // media
817 this.standardXHRResponse(this.requests.shift());
818
819 this.masterPlaylistController.mainSegmentLoader_.playlist = function(update) {
820 updates.push(update);
821 };
822 // 1ms has passed to upload 1kb
823 // that gives us a bandwidth of 1024 / 1 * 8 * 1000 = 8192000
824 this.clock.tick(1);
825
826 this.masterPlaylistController.mainSegmentLoader_.mediaIndex = 0;
827 // downloading the new segment will update bandwidth and cause a
828 // playlist change
829 // segment 0
830 this.standardXHRResponse(this.requests.shift());
831 // update the buffer to reflect the appended segment, and have enough buffer to
832 // change playlist
833 this.masterPlaylistController.tech_.buffered = () => {
834 return videojs.createTimeRanges([[0, 30]]);
835 };
836 this.masterPlaylistController.mediaSource.sourceBuffers[0].trigger('updateend');
837 // media
838 this.standardXHRResponse(this.requests.shift());
839 assert.ok(updates.length > 0, 'updated the segment list');
840
841 // verify stats
842 assert.equal(this.player.tech_.hls.stats.bandwidth, 8192000, 'Live stream');
843 assert.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 segment request');
844 assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred,
845 1024,
846 '1024 bytes downloaded');
847});
848
849QUnit.test('selects a playlist after main/combined segment downloads', function(assert) {
850 let calls = 0;
851
852 this.masterPlaylistController.selectPlaylist = () => {
853 calls++;
854 return this.masterPlaylistController.masterPlaylistLoader_.master.playlists[0];
855 };
856 this.masterPlaylistController.mediaSource.trigger('sourceopen');
857
858 // master
859 this.standardXHRResponse(this.requests.shift());
860 // media
861 this.standardXHRResponse(this.requests.shift());
862
863 // "downloaded" a segment
864 this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
865 assert.strictEqual(calls, 2, 'selects after the initial segment');
866
867 // and another
868 this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
869 assert.strictEqual(calls, 3, 'selects after additional segments');
870 // verify stats
871 assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default bandwidth');
872});
873
874QUnit.test('re-triggers bandwidthupdate events on the tech', function(assert) {
875 this.masterPlaylistController.mediaSource.trigger('sourceopen');
876 // master
877 this.standardXHRResponse(this.requests.shift());
878 // media
879 this.standardXHRResponse(this.requests.shift());
880
881 let bandwidthupdateEvents = 0;
882
883 this.player.tech_.on('bandwidthupdate', () => bandwidthupdateEvents++);
884
885 this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
886
887 assert.equal(bandwidthupdateEvents, 1, 'triggered bandwidthupdate');
888
889 this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
890
891 assert.equal(bandwidthupdateEvents, 2, 'triggered bandwidthupdate');
892});
893
894QUnit.test('switches to lower renditions immediately, higher dependent on buffer',
895function(assert) {
896 this.masterPlaylistController.mediaSource.trigger('sourceopen');
897 // master
898 this.standardXHRResponse(this.requests.shift());
899 // media
900 this.standardXHRResponse(this.requests.shift());
901
902 let buffered = [];
903 let currentPlaylistBandwidth = 0;
904 let nextPlaylistBandwidth = 0;
905 let mediaChanges = [];
906 let currentTime = 0;
907 let endList = true;
908 let duration = 100;
909
910 this.masterPlaylistController.tech_.currentTime = () => currentTime;
911 this.masterPlaylistController.tech_.buffered = () => videojs.createTimeRanges(buffered);
912 this.masterPlaylistController.duration = () => duration;
913 this.masterPlaylistController.selectPlaylist = () => {
914 return {
915 attributes: {
916 BANDWIDTH: nextPlaylistBandwidth
917 },
918 endList
919 };
920 };
921 this.masterPlaylistController.masterPlaylistLoader_.media = (media) => {
922 if (!media) {
923 return {
924 attributes: {
925 BANDWIDTH: currentPlaylistBandwidth
926 },
927 endList
928 };
929 }
930 mediaChanges.push(media);
931 };
932
933 currentTime = 0;
934 currentPlaylistBandwidth = 1000;
935 nextPlaylistBandwidth = 1000;
936 buffered = [];
937 this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
938 assert.equal(mediaChanges.length,
939 1,
940 'changes media when no buffer and equal bandwidth playlist');
941 buffered = [[0, 9]];
942 this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
943 assert.equal(mediaChanges.length,
944 2,
945 'changes media when sufficient forward buffer and equal ' +
946 'bandwidth playlist');
947 buffered = [[0, 30]];
948 this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
949 assert.equal(mediaChanges.length,
950 3,
951 'changes media when sufficient forward buffer and equal ' +
952 'bandwidth playlist');
953
954 mediaChanges.length = 0;
955
956 currentTime = 10;
957 currentPlaylistBandwidth = 1000;
958 nextPlaylistBandwidth = 1001;
959 buffered = [];
960 this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
961 assert.equal(mediaChanges.length,
962 0,
963 'did not change media when no buffer and and higher bandwidth playlist');
964 buffered = [[0, 19]];
965 this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
966 assert.equal(mediaChanges.length,
967 0,
968 'did not change media when insufficient forward buffer and higher ' +
969 'bandwidth playlist');
970 buffered = [[0, 20]];
971 this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
972 assert.equal(mediaChanges.length,
973 1,
974 'changes media when sufficient forward buffer and higher ' +
975 'bandwidth playlist');
976 buffered = [[0, 21]];
977 this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
978 assert.equal(mediaChanges.length,
979 2,
980 'changes media when sufficient forward buffer and higher ' +
981 'bandwidth playlist');
982
983 mediaChanges.length = 0;
984
985 currentTime = 100;
986 currentPlaylistBandwidth = 1000;
987 nextPlaylistBandwidth = 1001;
988 buffered = [];
989 this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
990 assert.equal(mediaChanges.length,
991 0,
992 'did not change media when no buffer and higher bandwidth playlist');
993 buffered = [[0, 100], [100, 109]];
994 this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
995 assert.equal(mediaChanges.length,
996 0,
997 'did not change media when insufficient forward buffer and higher ' +
998 'bandwidth playlist');
999 buffered = [[0, 100], [100, 130]];
1000 this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
1001 assert.equal(mediaChanges.length,
1002 1,
1003 'changes media when sufficient forward buffer and higher ' +
1004 'bandwidth playlist');
1005
1006 mediaChanges.length = 0;
1007
1008 buffered = [];
1009 currentPlaylistBandwidth = 1000;
1010 nextPlaylistBandwidth = 999;
1011 this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
1012 assert.equal(mediaChanges.length,
1013 1,
1014 'changes media when no buffer but lower bandwidth playlist');
1015 buffered = [[100, 109]];
1016 this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
1017 assert.equal(mediaChanges.length,
1018 2,
1019 'changes media when insufficient forward buffer but lower ' +
1020 'bandwidth playlist');
1021 buffered = [[100, 110]];
1022 this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
1023 assert.equal(mediaChanges.length,
1024 3,
1025 'changes media when sufficient forward buffer and lower ' +
1026 'bandwidth playlist');
1027
1028 mediaChanges.length = 0;
1029
1030 endList = false;
1031 currentTime = 100;
1032 currentPlaylistBandwidth = 1000;
1033 nextPlaylistBandwidth = 1001;
1034 buffered = [];
1035 this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
1036 assert.equal(mediaChanges.length,
1037 1,
1038 'changes live media when no buffer and higher bandwidth playlist');
1039 buffered = [[0, 100], [100, 109]];
1040 this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
1041 assert.equal(mediaChanges.length,
1042 2,
1043 'changes live media when insufficient forward buffer and higher ' +
1044 'bandwidth playlist');
1045 buffered = [[0, 100], [100, 130]];
1046 this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
1047 assert.equal(mediaChanges.length,
1048 3,
1049 'changes live media when sufficient forward buffer and higher ' +
1050 'bandwidth playlist');
1051
1052 mediaChanges.length = 0;
1053
1054 endList = true;
1055 currentTime = 9;
1056 duration = 18;
1057 buffered = [];
1058 this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
1059 assert.equal(mediaChanges.length,
1060 1,
1061 'changes media when no buffer and duration less than low water line');
1062 buffered = [[0, 10]];
1063 this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate');
1064 assert.equal(mediaChanges.length,
1065 2,
1066 'changes media when insufficient forward buffer and duration ' +
1067 'less than low water line');
1068});
1069
1070QUnit.test('updates the duration after switching playlists', function(assert) {
1071 let selectedPlaylist = false;
1072
1073 this.masterPlaylistController.mediaSource.trigger('sourceopen');
1074
1075 this.masterPlaylistController.bandwidth = 1e20;
1076
1077 // master
1078 this.standardXHRResponse(this.requests[0]);
1079 // media
1080 this.standardXHRResponse(this.requests[1]);
1081
1082 this.masterPlaylistController.selectPlaylist = () => {
1083 selectedPlaylist = true;
1084
1085 // this duration should be overwritten by the playlist change
1086 this.masterPlaylistController.mediaSource.duration = 0;
1087 this.masterPlaylistController.mediaSource.readyState = 'open';
1088
1089 return this.masterPlaylistController.masterPlaylistLoader_.master.playlists[1];
1090 };
1091 // 1ms has passed to upload 1kb
1092 // that gives us a bandwidth of 1024 / 1 * 8 * 1000 = 8192000
1093 this.clock.tick(1);
1094 this.masterPlaylistController.mainSegmentLoader_.mediaIndex = 0;
1095 // segment 0
1096 this.standardXHRResponse(this.requests[2]);
1097 this.masterPlaylistController.mediaSource.sourceBuffers[0].trigger('updateend');
1098 // media1
1099 this.standardXHRResponse(this.requests[3]);
1100 assert.ok(selectedPlaylist, 'selected playlist');
1101 assert.ok(this.masterPlaylistController.mediaSource.duration !== 0,
1102 'updates the duration');
1103
1104 // verify stats
1105 assert.equal(this.player.tech_.hls.stats.bandwidth, 8192000, 'Live stream');
1106 assert.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 segment request');
1107 assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred,
1108 1024,
1109 '1024 bytes downloaded');
1110});
1111
1112QUnit.test('playlist selection uses systemBandwidth', function(assert) {
1113 this.masterPlaylistController.mediaSource.trigger('sourceopen');
1114 this.player.width(1000);
1115 this.player.height(900);
1116
1117 // master
1118 this.standardXHRResponse(this.requests[0]);
1119 // media
1120 this.standardXHRResponse(this.requests[1]);
1121 assert.ok(/media3\.m3u8/i.test(this.requests[1].url), 'Selected the highest rendition');
1122
1123 // 1ms has passed to upload 1kb
1124 // that gives us a bandwidth of 1024 / 1 * 8 * 1000 = 8192000
1125 this.clock.tick(1);
1126 this.masterPlaylistController.mainSegmentLoader_.mediaIndex = 0;
1127 // segment 0
1128 this.standardXHRResponse(this.requests[2]);
1129 // 20ms have passed to upload 1kb
1130 // that gives us a throughput of 1024 / 20 * 8 * 1000 = 409600
1131 this.clock.tick(20);
1132 this.masterPlaylistController.mediaSource.sourceBuffers[0].trigger('updateend');
1133 // systemBandwidth is 1 / (1 / 8192000 + 1 / 409600) = ~390095
1134
1135 // media1
1136 this.standardXHRResponse(this.requests[3]);
1137 assert.ok(/media\.m3u8/i.test(this.requests[3].url), 'Selected the rendition < 390095');
1138
1139 assert.ok(this.masterPlaylistController.mediaSource.duration !== 0,
1140 'updates the duration');
1141
1142 // verify stats
1143 assert.equal(this.player.tech_.hls.stats.bandwidth, 8192000, 'Live stream');
1144 assert.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 segment request');
1145 assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred,
1146 1024,
1147 '1024 bytes downloaded');
1148});
1149
1150QUnit.test('removes request timeout when segment timesout on lowest rendition',
1151function(assert) {
1152 this.masterPlaylistController.mediaSource.trigger('sourceopen');
1153
1154 // master
1155 this.standardXHRResponse(this.requests[0]);
1156 // media
1157 this.standardXHRResponse(this.requests[1]);
1158
1159 assert.equal(this.masterPlaylistController.requestOptions_.timeout,
1160 this.masterPlaylistController.masterPlaylistLoader_.targetDuration * 1.5 *
1161 1000,
1162 'default request timeout');
1163
1164 assert.ok(!this.masterPlaylistController
1165 .masterPlaylistLoader_
1166 .isLowestEnabledRendition_(), 'Not lowest rendition');
1167
1168 // Cause segment to timeout to force player into lowest rendition
1169 this.requests[2].timedout = true;
1170
1171 // Downloading segment should cause media change and timeout removal
1172 // segment 0
1173 this.standardXHRResponse(this.requests[2]);
1174 // Download new segment after media change
1175 this.standardXHRResponse(this.requests[3]);
1176
1177 assert.ok(this.masterPlaylistController
1178 .masterPlaylistLoader_.isLowestEnabledRendition_(), 'On lowest rendition');
1179
1180 assert.equal(this.masterPlaylistController.requestOptions_.timeout, 0,
1181 'request timeout 0');
1182});
1183
1184QUnit.test('removes request timeout when the source is a media playlist and not master',
1185 function(assert) {
1186 this.requests.length = 0;
1187
1188 this.player.src({
1189 src: 'manifest/media.m3u8',
1190 type: 'application/vnd.apple.mpegurl'
1191 });
1192
1193 this.clock.tick(1);
1194
1195 this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
1196
1197 // media
1198 this.standardXHRResponse(this.requests.shift());
1199
1200 assert.equal(this.masterPlaylistController.requestOptions_.timeout, 0,
1201 'request timeout set to 0 when loading a non master playlist');
1202 });
1203
1204QUnit.test('seekable uses the intersection of alternate audio and combined tracks',
1205function(assert) {
1206 let origSeekable = Playlist.seekable;
1207 let mpc = this.masterPlaylistController;
1208 let mainMedia = {};
1209 let audioMedia = {};
1210 let mainTimeRanges = [];
1211 let audioTimeRanges = [];
1212 let assertTimeRangesEqual = (left, right, message) => {
1213 if (left.length === 0 && right.length === 0) {
1214 return;
1215 }
1216
1217 assert.equal(left.length, 1, message);
1218 assert.equal(right.length, 1, message);
1219
1220 assert.equal(left.start(0), right.start(0), message);
1221 assert.equal(left.end(0), right.end(0), message);
1222 };
1223
1224 this.masterPlaylistController.masterPlaylistLoader_.media = () => mainMedia;
1225 this.masterPlaylistController.syncController_.getExpiredTime = () => 0;
1226
1227 Playlist.seekable = (media) => {
1228 if (media === mainMedia) {
1229 return videojs.createTimeRanges(mainTimeRanges);
1230 }
1231 return videojs.createTimeRanges(audioTimeRanges);
1232 };
1233
1234 assertTimeRangesEqual(mpc.seekable(),
1235 videojs.createTimeRanges(),
1236 'empty when main empty');
1237 mainTimeRanges = [[0, 10]];
1238 mpc.seekable_ = videojs.createTimeRanges();
1239 mpc.onSyncInfoUpdate_();
1240 assertTimeRangesEqual(mpc.seekable(),
1241 videojs.createTimeRanges([[0, 10]]),
1242 'main when no audio');
1243
1244 mpc.audioPlaylistLoader_ = {
1245 media: () => audioMedia,
1246 dispose() {},
1247 expired_: 0
1248 };
1249 mainTimeRanges = [];
1250 mpc.seekable_ = videojs.createTimeRanges();
1251 mpc.onSyncInfoUpdate_();
1252
1253 assertTimeRangesEqual(mpc.seekable(),
1254 videojs.createTimeRanges(),
1255 'empty when both empty');
1256 mainTimeRanges = [[0, 10]];
1257 mpc.seekable_ = videojs.createTimeRanges();
1258 mpc.onSyncInfoUpdate_();
1259 assertTimeRangesEqual(mpc.seekable(),
1260 videojs.createTimeRanges(),
1261 'empty when audio empty');
1262 mainTimeRanges = [];
1263 audioTimeRanges = [[0, 10]];
1264 mpc.seekable_ = videojs.createTimeRanges();
1265 mpc.onSyncInfoUpdate_();
1266 assertTimeRangesEqual(mpc.seekable(),
1267 videojs.createTimeRanges(),
1268 'empty when main empty');
1269 mainTimeRanges = [[0, 10]];
1270 audioTimeRanges = [[0, 10]];
1271 mpc.seekable_ = videojs.createTimeRanges();
1272 mpc.onSyncInfoUpdate_();
1273 assertTimeRangesEqual(mpc.seekable(),
1274 videojs.createTimeRanges([[0, 10]]),
1275 'ranges equal');
1276 mainTimeRanges = [[5, 10]];
1277 mpc.seekable_ = videojs.createTimeRanges();
1278 mpc.onSyncInfoUpdate_();
1279 assertTimeRangesEqual(mpc.seekable(),
1280 videojs.createTimeRanges([[5, 10]]),
1281 'main later start');
1282 mainTimeRanges = [[0, 10]];
1283 audioTimeRanges = [[5, 10]];
1284 mpc.seekable_ = videojs.createTimeRanges();
1285 mpc.onSyncInfoUpdate_();
1286 assertTimeRangesEqual(mpc.seekable(),
1287 videojs.createTimeRanges([[5, 10]]),
1288 'audio later start');
1289 mainTimeRanges = [[0, 9]];
1290 audioTimeRanges = [[0, 10]];
1291 mpc.seekable_ = videojs.createTimeRanges();
1292 mpc.onSyncInfoUpdate_();
1293 assertTimeRangesEqual(mpc.seekable(),
1294 videojs.createTimeRanges([[0, 9]]),
1295 'main earlier end');
1296 mainTimeRanges = [[0, 10]];
1297 audioTimeRanges = [[0, 9]];
1298 mpc.seekable_ = videojs.createTimeRanges();
1299 mpc.onSyncInfoUpdate_();
1300 assertTimeRangesEqual(mpc.seekable(),
1301 videojs.createTimeRanges([[0, 9]]),
1302 'audio earlier end');
1303 mainTimeRanges = [[1, 10]];
1304 audioTimeRanges = [[0, 9]];
1305 mpc.seekable_ = videojs.createTimeRanges();
1306 mpc.onSyncInfoUpdate_();
1307 assertTimeRangesEqual(mpc.seekable(),
1308 videojs.createTimeRanges([[1, 9]]),
1309 'main later start, audio earlier end');
1310 mainTimeRanges = [[0, 9]];
1311 audioTimeRanges = [[1, 10]];
1312 mpc.seekable_ = videojs.createTimeRanges();
1313 mpc.onSyncInfoUpdate_();
1314 assertTimeRangesEqual(mpc.seekable(),
1315 videojs.createTimeRanges([[1, 9]]),
1316 'audio later start, main earlier end');
1317 mainTimeRanges = [[2, 9]];
1318 mpc.seekable_ = videojs.createTimeRanges();
1319 mpc.onSyncInfoUpdate_();
1320 assertTimeRangesEqual(mpc.seekable(),
1321 videojs.createTimeRanges([[2, 9]]),
1322 'main later start, main earlier end');
1323 mainTimeRanges = [[1, 10]];
1324 audioTimeRanges = [[2, 9]];
1325 mpc.seekable_ = videojs.createTimeRanges();
1326 mpc.onSyncInfoUpdate_();
1327 assertTimeRangesEqual(mpc.seekable(),
1328 videojs.createTimeRanges([[2, 9]]),
1329 'audio later start, audio earlier end');
1330 mainTimeRanges = [[1, 10]];
1331 audioTimeRanges = [[11, 20]];
1332 mpc.seekable_ = videojs.createTimeRanges();
1333 mpc.onSyncInfoUpdate_();
1334 assertTimeRangesEqual(mpc.seekable(),
1335 videojs.createTimeRanges([[1, 10]]),
1336 'no intersection, audio later');
1337 mainTimeRanges = [[11, 20]];
1338 audioTimeRanges = [[1, 10]];
1339 mpc.seekable_ = videojs.createTimeRanges();
1340 mpc.onSyncInfoUpdate_();
1341 assertTimeRangesEqual(mpc.seekable(),
1342 videojs.createTimeRanges([[11, 20]]),
1343 'no intersection, main later');
1344
1345 Playlist.seekable = origSeekable;
1346});
1347
1348QUnit.test('syncInfoUpdate triggers seekablechanged when seekable is updated',
1349function(assert) {
1350 let origSeekable = Playlist.seekable;
1351 let mpc = this.masterPlaylistController;
1352 let tech = this.player.tech_;
1353 let mainTimeRanges = [];
1354 let media = {};
1355 let seekablechanged = 0;
1356
1357 tech.on('seekablechanged', () => seekablechanged++);
1358
1359 Playlist.seekable = () => {
1360 return videojs.createTimeRanges(mainTimeRanges);
1361 };
1362 this.masterPlaylistController.masterPlaylistLoader_.media = () => media;
1363 this.masterPlaylistController.syncController_.getExpiredTime = () => 0;
1364
1365 mainTimeRanges = [[0, 10]];
1366 mpc.seekable_ = videojs.createTimeRanges();
1367 mpc.onSyncInfoUpdate_();
1368 assert.equal(seekablechanged, 1, 'seekablechanged triggered');
1369
1370 Playlist.seekable = origSeekable;
1371});
1372
1373QUnit.test('calls to update cues on new media', function(assert) {
1374 let origHlsOptions = videojs.options.hls;
1375
1376 videojs.options.hls = {
1377 useCueTags: true
1378 };
1379
1380 this.player = createPlayer();
1381 this.player.src({
1382 src: 'manifest/media.m3u8',
1383 type: 'application/vnd.apple.mpegurl'
1384 });
1385
1386 this.clock.tick(1);
1387
1388 this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
1389
1390 let callCount = 0;
1391
1392 this.masterPlaylistController.updateAdCues_ = (media) => callCount++;
1393
1394 // master
1395 this.standardXHRResponse(this.requests.shift());
1396
1397 assert.equal(callCount, 0, 'no call to update cues on master');
1398
1399 // media
1400 this.standardXHRResponse(this.requests.shift());
1401
1402 assert.equal(callCount, 1, 'calls to update cues on first media');
1403
1404 this.masterPlaylistController.masterPlaylistLoader_.trigger('loadedplaylist');
1405
1406 assert.equal(callCount, 2, 'calls to update cues on subsequent media');
1407
1408 videojs.options.hls = origHlsOptions;
1409});
1410
1411QUnit.test('calls to update cues on media when no master', function(assert) {
1412 this.requests.length = 0;
1413
1414 this.player.src({
1415 src: 'manifest/media.m3u8',
1416 type: 'application/vnd.apple.mpegurl'
1417 });
1418
1419 this.clock.tick(1);
1420
1421 this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
1422 this.masterPlaylistController.useCueTags_ = true;
1423
1424 let callCount = 0;
1425
1426 this.masterPlaylistController.updateAdCues_ = (media) => callCount++;
1427
1428 // media
1429 this.standardXHRResponse(this.requests.shift());
1430
1431 assert.equal(callCount, 1, 'calls to update cues on first media');
1432
1433 this.masterPlaylistController.masterPlaylistLoader_.trigger('loadedplaylist');
1434
1435 assert.equal(callCount, 2, 'calls to update cues on subsequent media');
1436});
1437
1438QUnit.test('respects useCueTags option', function(assert) {
1439 let origHlsOptions = videojs.options.hls;
1440 let hlsPlaylistCueTagsEvents = 0;
1441
1442 videojs.options.hls = {
1443 useCueTags: true
1444 };
1445
1446 this.player = createPlayer();
1447 this.player.tech_.on('usage', (event) => {
1448 if (event.name === 'hls-playlist-cue-tags') {
1449 hlsPlaylistCueTagsEvents++;
1450 }
1451 });
1452 this.player.src({
1453 src: 'manifest/media.m3u8',
1454 type: 'application/vnd.apple.mpegurl'
1455 });
1456
1457 this.clock.tick(1);
1458
1459 this.masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
1460 this.standardXHRResponse(this.requests.shift());
1461 this.standardXHRResponse(this.requests.shift());
1462
1463 assert.equal(hlsPlaylistCueTagsEvents, 1, 'cue tags event has been triggered once');
1464 assert.ok(this.masterPlaylistController.cueTagsTrack_,
1465 'creates cueTagsTrack_ if useCueTags is truthy');
1466 assert.equal(this.masterPlaylistController.cueTagsTrack_.label,
1467 'ad-cues',
1468 'cueTagsTrack_ has label of ad-cues');
1469 assert.equal(this.player.textTracks()[0], this.masterPlaylistController.cueTagsTrack_,
1470 'adds cueTagsTrack as a text track if useCueTags is truthy');
1471
1472 videojs.options.hls = origHlsOptions;
1473});
1474
1475QUnit.test('correctly sets alternate audio track kinds', function(assert) {
1476 this.requests.length = 0;
1477 this.player = createPlayer();
1478 this.player.src({
1479 src: 'manifest/alternate-audio-accessibility.m3u8',
1480 type: 'application/vnd.apple.mpegurl'
1481 });
1482
1483 this.clock.tick(1);
1484
1485 // master
1486 this.standardXHRResponse(this.requests.shift());
1487 // media - required for loadedmetadata
1488 this.standardXHRResponse(this.requests.shift());
1489
1490 const audioTracks = this.player.tech_.audioTracks();
1491
1492 assert.equal(audioTracks.length, 4, 'added 4 audio tracks');
1493 assert.equal(audioTracks[0].id, 'English', 'contains english track');
1494 assert.equal(audioTracks[0].kind, 'main', 'english track\'s kind is "main"');
1495 assert.equal(audioTracks[1].id,
1496 'English Descriptions',
1497 'contains english descriptions track');
1498 assert.equal(audioTracks[1].kind,
1499 'main-desc',
1500 'english descriptions track\'s kind is "main-desc"');
1501 assert.equal(audioTracks[2].id, 'Français', 'contains french track');
1502 assert.equal(audioTracks[2].kind,
1503 'alternative',
1504 'french track\'s kind is "alternative"');
1505 assert.equal(audioTracks[3].id, 'Espanol', 'contains spanish track');
1506 assert.equal(audioTracks[3].kind,
1507 'alternative',
1508 'spanish track\'s kind is "alternative"');
1509});
1510
1511QUnit.test('trigger events when video and audio is demuxed by default', function(assert) {
1512 let hlsDemuxedEvents = 0;
1513
1514 this.requests.length = 0;
1515 this.player = createPlayer();
1516 this.player.src({
1517 src: 'manifest/multipleAudioGroups.m3u8',
1518 type: 'application/vnd.apple.mpegurl'
1519 });
1520
1521 this.player.tech_.on('usage', (event) => {
1522 if (event.name === 'hls-demuxed') {
1523 hlsDemuxedEvents++;
1524 }
1525 });
1526
1527 openMediaSource(this.player, this.clock);
1528 // master
1529 this.standardXHRResponse(this.requests.shift());
1530 // media
1531 this.standardXHRResponse(this.requests.shift());
1532
1533 assert.equal(hlsDemuxedEvents, 1, 'video and audio is demuxed by default');
1534});
1535
1536QUnit.test('trigger events when an AES is detected', function(assert) {
1537 let hlsAesEvents = 0;
1538 let isAesCopy = Hls.Playlist.isAes;
1539
1540 Hls.Playlist.isAes = (media) => {
1541 return true;
1542 };
1543
1544 this.player.tech_.on('usage', (event) => {
1545 if (event.name === 'hls-aes') {
1546 hlsAesEvents++;
1547 }
1548 });
1549
1550 // master
1551 this.standardXHRResponse(this.requests.shift());
1552 // media
1553 this.standardXHRResponse(this.requests.shift());
1554 this.masterPlaylistController.mediaSource.trigger('sourceopen');
1555
1556 assert.equal(hlsAesEvents, 1, 'an AES HLS stream is detected');
1557 Hls.Playlist.isAes = isAesCopy;
1558});
1559
1560QUnit.test('trigger events when an fMP4 stream is detected', function(assert) {
1561 let hlsFmp4Events = 0;
1562 let isFmp4Copy = Hls.Playlist.isFmp4;
1563
1564 Hls.Playlist.isFmp4 = (media) => {
1565 return true;
1566 };
1567
1568 this.player.tech_.on('usage', (event) => {
1569 if (event.name === 'hls-fmp4') {
1570 hlsFmp4Events++;
1571 }
1572 });
1573
1574 // master
1575 this.standardXHRResponse(this.requests.shift());
1576 // media
1577 this.standardXHRResponse(this.requests.shift());
1578 this.masterPlaylistController.mediaSource.trigger('sourceopen');
1579
1580 assert.equal(hlsFmp4Events, 1, 'an fMP4 stream is detected');
1581 Hls.Playlist.isFmp4 = isFmp4Copy;
1582});
1583
1584QUnit.test('adds subtitle tracks when a media playlist is loaded', function(assert) {
1585 let hlsWebvttEvents = 0;
1586
1587 this.requests.length = 0;
1588 this.player = createPlayer();
1589 this.player.src({
1590 src: 'manifest/master-subtitles.m3u8',
1591 type: 'application/vnd.apple.mpegurl'
1592 });
1593
1594 this.clock.tick(1);
1595
1596 this.player.tech_.on('usage', (event) => {
1597 if (event.name === 'hls-webvtt') {
1598 hlsWebvttEvents++;
1599 }
1600 });
1601
1602 const masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
1603
1604 assert.equal(hlsWebvttEvents, 0, 'there is no webvtt detected');
1605 assert.equal(this.player.textTracks().length, 1, 'one text track to start');
1606 assert.equal(this.player.textTracks()[0].label,
1607 'segment-metadata',
1608 'only segment-metadata text track');
1609
1610 // master, contains media groups for subtitles
1611 this.standardXHRResponse(this.requests.shift());
1612
1613 // we wait for loadedmetadata before setting subtitle tracks, so we need to wait for a
1614 // media playlist
1615 assert.equal(this.player.textTracks().length, 1, 'only one text track after master');
1616
1617 // media
1618 this.standardXHRResponse(this.requests.shift());
1619
1620 const master = masterPlaylistController.masterPlaylistLoader_.master;
1621 const subs = master.mediaGroups.SUBTITLES.subs;
1622 const subsArr = Object.keys(subs).map(key => subs[key]);
1623
1624 assert.equal(subsArr.length, 4, 'got 4 subtitles');
1625 assert.equal(subsArr.filter(sub => sub.forced === false).length, 2, '2 forced');
1626 assert.equal(subsArr.filter(sub => sub.forced === true).length, 2, '2 non-forced');
1627
1628 const textTracks = this.player.textTracks();
1629
1630 assert.equal(textTracks.length, 3, 'non-forced text tracks were added');
1631 assert.equal(textTracks[1].mode, 'disabled', 'track starts disabled');
1632 assert.equal(textTracks[2].mode, 'disabled', 'track starts disabled');
1633 assert.equal(hlsWebvttEvents, 1, 'there is webvtt detected in the rendition');
1634});
1635
1636QUnit.test('switches off subtitles on subtitle errors', function(assert) {
1637 this.requests.length = 0;
1638 this.player = createPlayer();
1639 this.player.src({
1640 src: 'manifest/master-subtitles.m3u8',
1641 type: 'application/vnd.apple.mpegurl'
1642 });
1643
1644 this.clock.tick(1);
1645
1646 const masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
1647
1648 // sets up listener for text track changes
1649 masterPlaylistController.trigger('sourceopen');
1650
1651 // master, contains media groups for subtitles
1652 this.standardXHRResponse(this.requests.shift());
1653 // media
1654 this.standardXHRResponse(this.requests.shift());
1655
1656 const textTracks = this.player.textTracks();
1657
1658 assert.equal(this.requests.length, 0, 'no outstanding requests');
1659
1660 // enable first subtitle text track
1661 assert.notEqual(textTracks[0].kind, 'subtitles', 'kind is not subtitles');
1662 assert.equal(textTracks[1].kind, 'subtitles', 'kind is subtitles');
1663 textTracks[1].mode = 'showing';
1664
1665 assert.equal(this.requests.length, 1, 'made a request');
1666 assert.equal(textTracks[1].mode, 'showing', 'text track still showing');
1667
1668 // request failed
1669 this.requests.shift().respond(404, null, '');
1670
1671 assert.equal(textTracks[1].mode, 'disabled', 'disabled text track');
1672
1673 assert.equal(this.env.log.warn.callCount, 1, 'logged a warning');
1674 this.env.log.warn.callCount = 0;
1675
1676 assert.equal(this.requests.length, 0, 'no outstanding requests');
1677
1678 // re-enable first text track
1679 textTracks[1].mode = 'showing';
1680
1681 assert.equal(this.requests.length, 1, 'made a request');
1682 assert.equal(textTracks[1].mode, 'showing', 'text track still showing');
1683
1684 this.requests.shift().respond(200, null, `
1685 #EXTM3U
1686 #EXT-X-TARGETDURATION:10
1687 #EXT-X-MEDIA-SEQUENCE:0
1688 #EXTINF:10
1689 0.webvtt
1690 #EXT-X-ENDLIST
1691 `);
1692
1693 const syncController = masterPlaylistController.subtitleSegmentLoader_.syncController_;
1694
1695 // required for the vtt request to be made
1696 syncController.timestampOffsetForTimeline = () => 0;
1697
1698 this.clock.tick(1);
1699
1700 assert.equal(this.requests.length, 1, 'made a request');
1701 assert.ok(this.requests[0].url.endsWith('0.webvtt'), 'made a webvtt request');
1702 assert.equal(textTracks[1].mode, 'showing', 'text track still showing');
1703
1704 this.requests.shift().respond(404, null, '');
1705
1706 assert.equal(textTracks[1].mode, 'disabled', 'disabled text track');
1707
1708 assert.equal(this.env.log.warn.callCount, 1, 'logged a warning');
1709 this.env.log.warn.callCount = 0;
1710});
1711
1712QUnit.test('pauses subtitle segment loader on tech errors', function(assert) {
1713 this.requests.length = 0;
1714 this.player = createPlayer();
1715 this.player.src({
1716 src: 'manifest/master-subtitles.m3u8',
1717 type: 'application/vnd.apple.mpegurl'
1718 });
1719
1720 this.clock.tick(1);
1721
1722 const masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
1723
1724 // sets up listener for text track changes
1725 masterPlaylistController.trigger('sourceopen');
1726
1727 // master, contains media groups for subtitles
1728 this.standardXHRResponse(this.requests.shift());
1729 // media
1730 this.standardXHRResponse(this.requests.shift());
1731
1732 const textTracks = this.player.textTracks();
1733
1734 // enable first subtitle text track
1735 assert.notEqual(textTracks[0].kind, 'subtitles', 'kind is not subtitles');
1736 assert.equal(textTracks[1].kind, 'subtitles', 'kind is subtitles');
1737 textTracks[1].mode = 'showing';
1738
1739 let pauseCount = 0;
1740
1741 masterPlaylistController.subtitleSegmentLoader_.pause = () => pauseCount++;
1742
1743 this.player.tech_.trigger('error');
1744
1745 assert.equal(pauseCount, 1, 'paused subtitle segment loader');
1746});
1747
1748QUnit.test('disposes subtitle loaders on dispose', function(assert) {
1749 this.requests.length = 0;
1750 this.player = createPlayer();
1751 this.player.src({
1752 src: 'manifest/master-subtitles.m3u8',
1753 type: 'application/vnd.apple.mpegurl'
1754 });
1755
1756 this.clock.tick(1);
1757
1758 let masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
1759
1760 assert.notOk(masterPlaylistController.subtitlePlaylistLoader_,
1761 'does not start with a subtitle playlist loader');
1762 assert.ok(masterPlaylistController.subtitleSegmentLoader_,
1763 'starts with a subtitle segment loader');
1764
1765 let segmentLoaderDisposeCount = 0;
1766
1767 masterPlaylistController.subtitleSegmentLoader_.dispose =
1768 () => segmentLoaderDisposeCount++;
1769
1770 masterPlaylistController.dispose();
1771
1772 assert.equal(segmentLoaderDisposeCount, 1, 'disposed the subtitle segment loader');
1773
1774 this.requests.length = 0;
1775 this.player = createPlayer();
1776 this.player.src({
1777 src: 'manifest/master-subtitles.m3u8',
1778 type: 'application/vnd.apple.mpegurl'
1779 });
1780
1781 this.clock.tick(1);
1782
1783 masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
1784
1785 // sets up listener for text track changes
1786 masterPlaylistController.trigger('sourceopen');
1787
1788 // master, contains media groups for subtitles
1789 this.standardXHRResponse(this.requests.shift());
1790 // media
1791 this.standardXHRResponse(this.requests.shift());
1792
1793 const textTracks = this.player.textTracks();
1794
1795 // enable first subtitle text track
1796 assert.notEqual(textTracks[0].kind, 'subtitles', 'kind is not subtitles');
1797 assert.equal(textTracks[1].kind, 'subtitles', 'kind is subtitles');
1798 textTracks[1].mode = 'showing';
1799
1800 assert.ok(masterPlaylistController.subtitlePlaylistLoader_,
1801 'has a subtitle playlist loader');
1802 assert.ok(masterPlaylistController.subtitleSegmentLoader_,
1803 'has a subtitle segment loader');
1804
1805 let playlistLoaderDisposeCount = 0;
1806
1807 segmentLoaderDisposeCount = 0;
1808
1809 masterPlaylistController.subtitlePlaylistLoader_.dispose =
1810 () => playlistLoaderDisposeCount++;
1811 masterPlaylistController.subtitleSegmentLoader_.dispose =
1812 () => segmentLoaderDisposeCount++;
1813
1814 masterPlaylistController.dispose();
1815
1816 assert.equal(playlistLoaderDisposeCount, 1, 'disposed the subtitle playlist loader');
1817 assert.equal(segmentLoaderDisposeCount, 1, 'disposed the subtitle segment loader');
1818});
1819
1820QUnit.test('subtitle segment loader resets on seeks', function(assert) {
1821 this.requests.length = 0;
1822 this.player = createPlayer();
1823 this.player.src({
1824 src: 'manifest/master-subtitles.m3u8',
1825 type: 'application/vnd.apple.mpegurl'
1826 });
1827
1828 this.clock.tick(1);
1829
1830 const masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
1831
1832 // sets up listener for text track changes
1833 masterPlaylistController.trigger('sourceopen');
1834
1835 // master, contains media groups for subtitles
1836 this.standardXHRResponse(this.requests.shift());
1837 // media
1838 this.standardXHRResponse(this.requests.shift());
1839
1840 const textTracks = this.player.textTracks();
1841
1842 // enable first subtitle text track
1843 assert.notEqual(textTracks[0].kind, 'subtitles', 'kind is not subtitles');
1844 assert.equal(textTracks[1].kind, 'subtitles', 'kind is subtitles');
1845 textTracks[1].mode = 'showing';
1846
1847 let resetCount = 0;
1848 let abortCount = 0;
1849 let loadCount = 0;
1850
1851 masterPlaylistController.subtitleSegmentLoader_.resetEverything = () => resetCount++;
1852 masterPlaylistController.subtitleSegmentLoader_.abort = () => abortCount++;
1853 masterPlaylistController.subtitleSegmentLoader_.load = () => loadCount++;
1854
1855 this.player.pause();
1856 masterPlaylistController.setCurrentTime(5);
1857
1858 assert.equal(resetCount, 1, 'reset subtitle segment loader');
1859 assert.equal(abortCount, 1, 'aborted subtitle segment loader');
1860 assert.equal(loadCount, 0, 'did not call load on subtitle segment loader');
1861
1862 this.player.play();
1863 resetCount = 0;
1864 abortCount = 0;
1865 loadCount = 0;
1866 masterPlaylistController.setCurrentTime(10);
1867
1868 assert.equal(resetCount, 1, 'reset subtitle segment loader');
1869 assert.equal(abortCount, 1, 'aborted subtitle segment loader');
1870 assert.equal(loadCount, 1, 'called load on subtitle segment loader');
1871});
1872
1873QUnit.test('can get active subtitle group', function(assert) {
1874 this.requests.length = 0;
1875 this.player = createPlayer();
1876 this.player.src({
1877 src: 'manifest/master-subtitles.m3u8',
1878 type: 'application/vnd.apple.mpegurl'
1879 });
1880
1881 this.clock.tick(1);
1882
1883 const masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
1884
1885 assert.notOk(masterPlaylistController.activeSubtitleGroup_(),
1886 'no active subtitle group');
1887
1888 // master, contains media groups for subtitles
1889 this.standardXHRResponse(this.requests.shift());
1890
1891 assert.notOk(masterPlaylistController.activeSubtitleGroup_(),
1892 'no active subtitle group');
1893
1894 // media
1895 this.standardXHRResponse(this.requests.shift());
1896
1897 assert.ok(masterPlaylistController.activeSubtitleGroup_(), 'active subtitle group');
1898});
1899
1900QUnit.test('can get active subtitle track', function(assert) {
1901 this.requests.length = 0;
1902 this.player = createPlayer();
1903 this.player.src({
1904 src: 'manifest/master-subtitles.m3u8',
1905 type: 'application/vnd.apple.mpegurl'
1906 });
1907
1908 this.clock.tick(1);
1909
1910 // master, contains media groups for subtitles
1911 this.standardXHRResponse(this.requests.shift());
1912 // media
1913 this.standardXHRResponse(this.requests.shift());
1914
1915 const masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
1916
1917 assert.notOk(masterPlaylistController.activeSubtitleTrack_(),
1918 'no active subtitle track');
1919
1920 const textTracks = this.player.textTracks();
1921
1922 // enable first subtitle text track
1923 assert.notEqual(textTracks[0].kind, 'subtitles', 'kind is not subtitles');
1924 assert.equal(textTracks[1].kind, 'subtitles', 'kind is subtitles');
1925 textTracks[1].mode = 'showing';
1926
1927 assert.ok(masterPlaylistController.activeSubtitleTrack_(), 'active subtitle track');
1928});
1929
1930QUnit.test('handles subtitle errors appropriately', function(assert) {
1931 this.requests.length = 0;
1932 this.player = createPlayer();
1933 this.player.src({
1934 src: 'manifest/master-subtitles.m3u8',
1935 type: 'application/vnd.apple.mpegurl'
1936 });
1937
1938 this.clock.tick(1);
1939
1940 // master, contains media groups for subtitles
1941 this.standardXHRResponse(this.requests.shift());
1942 // media
1943 this.standardXHRResponse(this.requests.shift());
1944
1945 const textTracks = this.player.textTracks();
1946
1947 // enable first subtitle text track
1948 assert.notEqual(textTracks[0].kind, 'subtitles', 'kind is not subtitles');
1949 assert.equal(textTracks[1].kind, 'subtitles', 'kind is subtitles');
1950 textTracks[1].mode = 'showing';
1951
1952 const masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
1953 let abortCalls = 0;
1954 let setupSubtitlesCalls = 0;
1955
1956 masterPlaylistController.subtitleSegmentLoader_.abort = () => abortCalls++;
1957 masterPlaylistController.setupSubtitles = () => setupSubtitlesCalls++;
1958
1959 masterPlaylistController.handleSubtitleError_();
1960
1961 assert.equal(textTracks[1].mode, 'disabled', 'set text track to disabled');
1962 assert.equal(abortCalls, 1, 'aborted subtitle segment loader');
1963 assert.equal(setupSubtitlesCalls, 1, 'setup subtitles');
1964 assert.equal(this.env.log.warn.callCount, 1, 'logged a warning');
1965
1966 this.env.log.warn.callCount = 0;
1967});
1968
1969QUnit.test('sets up subtitles', function(assert) {
1970 this.requests.length = 0;
1971 this.player = createPlayer();
1972 this.player.src({
1973 src: 'manifest/master-subtitles.m3u8',
1974 type: 'application/vnd.apple.mpegurl'
1975 });
1976
1977 this.clock.tick(1);
1978
1979 // master, contains media groups for subtitles
1980 this.standardXHRResponse(this.requests.shift());
1981 // media
1982 this.standardXHRResponse(this.requests.shift());
1983
1984 const masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
1985
1986 // sets up listener for text track changes
1987 masterPlaylistController.trigger('sourceopen');
1988
1989 const segmentLoader = masterPlaylistController.subtitleSegmentLoader_;
1990
1991 let segmentDisposeCalls = 0;
1992 let segmentLoadCalls = 0;
1993 let segmentPauseCalls = 0;
1994 let segmentResetCalls = 0;
1995
1996 segmentLoader.load = () => segmentLoadCalls++;
1997 segmentLoader.dispose = () => segmentDisposeCalls++;
1998 segmentLoader.pause = () => segmentPauseCalls++;
1999 segmentLoader.resetEverything = () => segmentResetCalls++;
2000
2001 assert.notOk(masterPlaylistController.subtitlePlaylistLoader_,
2002 'no subtitle playlist loader');
2003
2004 // no active text track
2005 masterPlaylistController.setupSubtitles();
2006
2007 assert.equal(segmentDisposeCalls, 0, 'did not dispose subtitles segment loader');
2008 assert.equal(segmentLoadCalls, 0, 'did not load subtitles segment loader');
2009 assert.equal(segmentPauseCalls, 1, 'paused subtitles segment loader');
2010 assert.equal(segmentResetCalls, 0, 'did not reset subtitle segment loader');
2011 assert.notOk(masterPlaylistController.subtitlePlaylistLoader_,
2012 'no subtitle playlist loader');
2013 assert.ok(masterPlaylistController.subtitleSegmentLoader_,
2014 'did not remove subtitle segment loader');
2015
2016 const textTracks = this.player.textTracks();
2017
2018 // enable first subtitle text track
2019 assert.notEqual(textTracks[0].kind, 'subtitles', 'kind is not subtitles');
2020 assert.equal(textTracks[1].kind, 'subtitles', 'kind is subtitles');
2021 textTracks[1].mode = 'showing';
2022
2023 assert.ok(masterPlaylistController.subtitlePlaylistLoader_,
2024 'added a new subtitle playlist loader');
2025 assert.equal(segmentLoader,
2026 masterPlaylistController.subtitleSegmentLoader_,
2027 'did not change subtitle segment loader');
2028 assert.equal(segmentLoadCalls, 0, 'did not load subtitles segment loader');
2029 assert.equal(segmentResetCalls, 1, 'reset subtitle segment loader');
2030
2031 let playlistLoader = masterPlaylistController.subtitlePlaylistLoader_;
2032 let playlistLoadCalls = 0;
2033
2034 playlistLoader.load = () => playlistLoadCalls++;
2035
2036 // same active text track, haven't yet gotten a response from webvtt
2037 masterPlaylistController.setupSubtitles();
2038
2039 assert.equal(this.requests.length, 2, 'total of two requests');
2040
2041 let oldRequest = this.requests.shift();
2042
2043 // tracking playlist loader dispose calls by checking request aborted status
2044 assert.ok(oldRequest.aborted, 'aborted the old request');
2045 assert.notEqual(playlistLoader,
2046 masterPlaylistController.subtitlePlaylistLoader_,
2047 'changed subtitle playlist loader');
2048
2049 let playlistDisposeCalls = 0;
2050
2051 playlistLoader = masterPlaylistController.subtitlePlaylistLoader_;
2052 playlistLoadCalls = 0;
2053
2054 playlistLoader.load = () => playlistLoadCalls++;
2055 playlistLoader.dispose = () => playlistDisposeCalls++;
2056
2057 this.requests.shift().respond(200, null, `
2058 #EXTM3U
2059 #EXT-X-TARGETDURATION:10
2060 #EXT-X-MEDIA-SEQUENCE:0
2061 #EXTINF:10
2062 0.webvtt
2063 #EXT-X-ENDLIST
2064 `);
2065
2066 segmentLoadCalls = 0;
2067
2068 // same active text track, got a response from webvtt playlist
2069 masterPlaylistController.setupSubtitles();
2070
2071 assert.equal(playlistLoader,
2072 masterPlaylistController.subtitlePlaylistLoader_,
2073 'did not change subtitle playlist loader');
2074 assert.equal(segmentLoader,
2075 masterPlaylistController.subtitleSegmentLoader_,
2076 'did not change subtitle segment loader');
2077 assert.equal(playlistDisposeCalls, 0, 'did not dispose subtitles playlist loader');
2078 assert.equal(playlistLoadCalls, 0, 'did not load subtitles playlist loader');
2079 assert.equal(segmentLoadCalls, 1, 'loaded subtitles segment loader');
2080
2081 playlistDisposeCalls = 0;
2082 segmentDisposeCalls = 0;
2083 playlistLoadCalls = 0;
2084 segmentLoadCalls = 0;
2085 segmentPauseCalls = 0;
2086 segmentResetCalls = 0;
2087
2088 // turn off active subtitle text track
2089 textTracks[1].mode = 'disabled';
2090
2091 assert.equal(playlistDisposeCalls, 1, 'disposed subtitles playlist loader');
2092 assert.equal(segmentDisposeCalls, 0, 'did not dispose subtitles segment loader');
2093 assert.equal(playlistLoadCalls, 0, 'did not load subtitles playlist loader');
2094 assert.equal(segmentLoadCalls, 0, 'did not load subtitles segment loader');
2095 assert.equal(segmentPauseCalls, 1, 'paused subtitles segment loader');
2096 assert.equal(segmentResetCalls, 0, 'did not reset subtitle segment loader');
2097 assert.notOk(masterPlaylistController.subtitlePlaylistLoader_,
2098 'removed subtitle playlist loader');
2099 assert.ok(masterPlaylistController.subtitleSegmentLoader_,
2100 'did not remove subtitle segment loader');
2101});
2102
2103QUnit.test('calculates dynamic GOAL_BUFFER_LENGTH', function(assert) {
2104 const configOld = {
2105 GOAL_BUFFER_LENGTH: Config.GOAL_BUFFER_LENGTH,
2106 MAX_GOAL_BUFFER_LENGTH: Config.MAX_GOAL_BUFFER_LENGTH,
2107 GOAL_BUFFER_LENGTH_RATE: Config.GOAL_BUFFER_LENGTH_RATE
2108 };
2109 const mpc = this.masterPlaylistController;
2110
2111 let currentTime = 0;
2112
2113 Config.GOAL_BUFFER_LENGTH = 30;
2114 Config.MAX_GOAL_BUFFER_LENGTH = 60;
2115 Config.GOAL_BUFFER_LENGTH_RATE = 0.5;
2116
2117 mpc.tech_.currentTime = () => currentTime;
2118
2119 assert.equal(mpc.goalBufferLength(), 30, 'dynamic GBL uses starting value at time 0');
2120
2121 currentTime = 10;
2122
2123 assert.equal(mpc.goalBufferLength(), 35, 'dynamic GBL increases by currentTime * rate');
2124
2125 currentTime = 60;
2126
2127 assert.equal(mpc.goalBufferLength(), 60, 'dynamic GBL uses max value');
2128
2129 currentTime = 70;
2130
2131 assert.equal(mpc.goalBufferLength(), 60, 'dynamic GBL continues to use max value');
2132
2133 // restore config
2134 Object.keys(configOld).forEach((key) => Config[key] = configOld[key]);
2135});
2136
2137QUnit.test('calculates dynamic BUFFER_LOW_WATER_LINE', function(assert) {
2138 const configOld = {
2139 BUFFER_LOW_WATER_LINE: Config.BUFFER_LOW_WATER_LINE,
2140 MAX_BUFFER_LOW_WATER_LINE: Config.MAX_BUFFER_LOW_WATER_LINE,
2141 BUFFER_LOW_WATER_LINE_RATE: Config.BUFFER_LOW_WATER_LINE_RATE
2142 };
2143 const mpc = this.masterPlaylistController;
2144
2145 let currentTime = 0;
2146
2147 Config.BUFFER_LOW_WATER_LINE = 0;
2148 Config.MAX_BUFFER_LOW_WATER_LINE = 30;
2149 Config.BUFFER_LOW_WATER_LINE_RATE = 0.5;
2150
2151 mpc.tech_.currentTime = () => currentTime;
2152
2153 assert.equal(mpc.bufferLowWaterLine(), 0, 'dynamic BLWL uses starting value at time 0');
2154
2155 currentTime = 10;
2156
2157 assert.equal(mpc.bufferLowWaterLine(), 5,
2158 'dynamic BLWL increases by currentTime * rate');
2159
2160 currentTime = 60;
2161
2162 assert.equal(mpc.bufferLowWaterLine(), 30, 'dynamic BLWL uses max value');
2163
2164 currentTime = 70;
2165
2166 assert.equal(mpc.bufferLowWaterLine(), 30, 'dynamic BLWL continues to use max value');
2167
2168 // restore config
2169 Object.keys(configOld).forEach((key) => Config[key] = configOld[key]);
2170});
2171
2172QUnit.module('Codec to MIME Type Conversion');
2173
2174const testMimeTypes = function(assert, isFMP4) {
2175 let container = isFMP4 ? 'mp4' : 'mp2t';
2176
2177 let videoMime = `video/${container}`;
2178 let audioMime = `audio/${container}`;
2179
2180 // no MAAT
2181 assert.deepEqual(mimeTypesForPlaylist_.apply(null,
2182 generateMedia(false, true, false, false, isFMP4)),
2183 [`${videoMime}; codecs="avc1.4d400d, mp4a.40.2"`],
2184 `no MAAT, container: ${container}, codecs: none`);
2185
2186 assert.deepEqual(mimeTypesForPlaylist_.apply(null,
2187 generateMedia(false, true, true, false, isFMP4)),
2188 [`${videoMime}; codecs="avc1.deadbeef"`],
2189 `no MAAT, container: ${container}, codecs: video`);
2190
2191 assert.deepEqual(mimeTypesForPlaylist_.apply(null,
2192 generateMedia(false, true, false, true, isFMP4)),
2193 [`${audioMime}; codecs="mp4a.40.E"`],
2194 `no MAAT, container: ${container}, codecs: audio`);
2195
2196 assert.deepEqual(mimeTypesForPlaylist_.apply(null,
2197 generateMedia(false, true, true, true, isFMP4)),
2198 [`${videoMime}; codecs="avc1.deadbeef, mp4a.40.E"`],
2199 `no MAAT, container: ${container}, codecs: video, audio`);
2200
2201 // MAAT, not muxed
2202 assert.deepEqual(mimeTypesForPlaylist_.apply(null,
2203 generateMedia(true, false, false, false, isFMP4)),
2204 [`${videoMime}; codecs="avc1.4d400d"`,
2205 `${audioMime}; codecs="mp4a.40.2"`],
2206 `MAAT, demuxed, container: ${container}, codecs: none`);
2207
2208 assert.deepEqual(mimeTypesForPlaylist_.apply(null,
2209 generateMedia(true, false, true, false, isFMP4)),
2210 [`${videoMime}; codecs="avc1.deadbeef"`,
2211 `${audioMime}; codecs="mp4a.40.2"`],
2212 `MAAT, demuxed, container: ${container}, codecs: video`);
2213
2214 assert.deepEqual(mimeTypesForPlaylist_.apply(null,
2215 generateMedia(true, false, false, true, isFMP4)),
2216 [`${videoMime}; codecs="mp4a.40.E"`,
2217 `${audioMime}; codecs="mp4a.40.E"`],
2218 `MAAT, demuxed, container: ${container}, codecs: audio`);
2219
2220 assert.deepEqual(mimeTypesForPlaylist_.apply(null,
2221 generateMedia(true, false, true, true, isFMP4)),
2222 [`${videoMime}; codecs="avc1.deadbeef"`,
2223 `${audioMime}; codecs="mp4a.40.E"`],
2224 `MAAT, demuxed, container: ${container}, codecs: video, audio`);
2225
2226 // MAAT, muxed
2227 assert.deepEqual(mimeTypesForPlaylist_.apply(null,
2228 generateMedia(true, true, false, false, isFMP4)),
2229 [`${videoMime}; codecs="avc1.4d400d, mp4a.40.2"`,
2230 `${audioMime}; codecs="mp4a.40.2"`],
2231 `MAAT, muxed, container: ${container}, codecs: none`);
2232
2233 assert.deepEqual(mimeTypesForPlaylist_.apply(null,
2234 generateMedia(true, true, true, false, isFMP4)),
2235 [`${videoMime}; codecs="avc1.deadbeef, mp4a.40.2"`,
2236 `${audioMime}; codecs="mp4a.40.2"`],
2237 `MAAT, muxed, container: ${container}, codecs: video`);
2238
2239 assert.deepEqual(mimeTypesForPlaylist_.apply(null,
2240 generateMedia(true, true, false, true, isFMP4)),
2241 [`${videoMime}; codecs="mp4a.40.E"`,
2242 `${audioMime}; codecs="mp4a.40.E"`],
2243 `MAAT, muxed, container: ${container}, codecs: audio`);
2244
2245 assert.deepEqual(mimeTypesForPlaylist_.apply(null,
2246 generateMedia(true, true, true, true, isFMP4)),
2247 [`${videoMime}; codecs="avc1.deadbeef, mp4a.40.E"`,
2248 `${audioMime}; codecs="mp4a.40.E"`],
2249 `MAAT, muxed, container: ${container}, codecs: video, audio`);
2250};
2251
2252QUnit.test('recognizes muxed codec configurations', function(assert) {
2253 testMimeTypes(assert, false);
2254 testMimeTypes(assert, true);
2255});
2256
2257QUnit.module('Map Legacy AVC Codec');
2258
2259QUnit.test('maps legacy AVC codecs', function(assert) {
2260 assert.equal(mapLegacyAvcCodecs_('avc1.deadbeef'),
2261 'avc1.deadbeef',
2262 'does nothing for non legacy pattern');
2263 assert.equal(mapLegacyAvcCodecs_('avc1.dead.beef, mp4a.something'),
2264 'avc1.dead.beef, mp4a.something',
2265 'does nothing for non legacy pattern');
2266 assert.equal(mapLegacyAvcCodecs_('avc1.dead.beef,mp4a.something'),
2267 'avc1.dead.beef,mp4a.something',
2268 'does nothing for non legacy pattern');
2269 assert.equal(mapLegacyAvcCodecs_('mp4a.something,avc1.dead.beef'),
2270 'mp4a.something,avc1.dead.beef',
2271 'does nothing for non legacy pattern');
2272 assert.equal(mapLegacyAvcCodecs_('mp4a.something, avc1.dead.beef'),
2273 'mp4a.something, avc1.dead.beef',
2274 'does nothing for non legacy pattern');
2275 assert.equal(mapLegacyAvcCodecs_('avc1.42001e'),
2276 'avc1.42001e',
2277 'does nothing for non legacy pattern');
2278 assert.equal(mapLegacyAvcCodecs_('avc1.4d0020,mp4a.40.2'),
2279 'avc1.4d0020,mp4a.40.2',
2280 'does nothing for non legacy pattern');
2281 assert.equal(mapLegacyAvcCodecs_('mp4a.40.2,avc1.4d0020'),
2282 'mp4a.40.2,avc1.4d0020',
2283 'does nothing for non legacy pattern');
2284 assert.equal(mapLegacyAvcCodecs_('mp4a.40.40'),
2285 'mp4a.40.40',
2286 'does nothing for non video codecs');
2287
2288 assert.equal(mapLegacyAvcCodecs_('avc1.66.30'),
2289 'avc1.42001e',
2290 'translates legacy video codec alone');
2291 assert.equal(mapLegacyAvcCodecs_('avc1.66.30, mp4a.40.2'),
2292 'avc1.42001e, mp4a.40.2',
2293 'translates legacy video codec when paired with audio');
2294 assert.equal(mapLegacyAvcCodecs_('mp4a.40.2, avc1.66.30'),
2295 'mp4a.40.2, avc1.42001e',
2296 'translates video codec when specified second');
2297});