UNPKG

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