UNPKG

57.1 kBJavaScriptView Raw
1import QUnit from 'qunit';
2import {
3 default as PlaylistLoader,
4 updateSegments,
5 updateMaster,
6 setupMediaPlaylists,
7 resolveMediaGroupUris,
8 refreshDelay
9} from '../src/playlist-loader';
10import xhrFactory from '../src/xhr';
11import { useFakeEnvironment } from './test-helpers';
12import window from 'global/window';
13
14// Attempts to produce an absolute URL to a given relative path
15// based on window.location.href
16const urlTo = function(path) {
17 return window.location.href
18 .split('/')
19 .slice(0, -1)
20 .concat([path])
21 .join('/');
22};
23
24QUnit.module('Playlist Loader', {
25 beforeEach(assert) {
26 this.env = useFakeEnvironment(assert);
27 this.clock = this.env.clock;
28 this.requests = this.env.requests;
29 this.fakeHls = {
30 xhr: xhrFactory()
31 };
32 },
33 afterEach() {
34 this.env.restore();
35 }
36});
37
38QUnit.test('updateSegments copies over properties', function(assert) {
39 assert.deepEqual(
40 [
41 { uri: 'test-uri-0', startTime: 0, endTime: 10 },
42 {
43 uri: 'test-uri-1',
44 startTime: 10,
45 endTime: 20,
46 map: { someProp: 99, uri: '4' }
47 }
48 ],
49 updateSegments(
50 [
51 { uri: 'test-uri-0', startTime: 0, endTime: 10 },
52 { uri: 'test-uri-1', startTime: 10, endTime: 20, map: { someProp: 1 } }
53 ],
54 [
55 { uri: 'test-uri-0' },
56 { uri: 'test-uri-1', map: { someProp: 99, uri: '4' } }
57 ],
58 0),
59 'retains properties from original segment');
60
61 assert.deepEqual(
62 [
63 { uri: 'test-uri-0', map: { someProp: 100 } },
64 { uri: 'test-uri-1', map: { someProp: 99, uri: '4' } }
65 ],
66 updateSegments(
67 [
68 { uri: 'test-uri-0' },
69 { uri: 'test-uri-1', map: { someProp: 1 } }
70 ],
71 [
72 { uri: 'test-uri-0', map: { someProp: 100 } },
73 { uri: 'test-uri-1', map: { someProp: 99, uri: '4' } }
74 ],
75 0),
76 'copies over/overwrites properties without offset');
77
78 assert.deepEqual(
79 [
80 { uri: 'test-uri-1', map: { someProp: 1 } },
81 { uri: 'test-uri-2', map: { someProp: 100, uri: '2' } }
82 ],
83 updateSegments(
84 [
85 { uri: 'test-uri-0' },
86 { uri: 'test-uri-1', map: { someProp: 1 } }
87 ],
88 [
89 { uri: 'test-uri-1' },
90 { uri: 'test-uri-2', map: { someProp: 100, uri: '2' } }
91 ],
92 1),
93 'copies over/overwrites properties with offset of 1');
94
95 assert.deepEqual(
96 [
97 { uri: 'test-uri-2' },
98 { uri: 'test-uri-3', map: { someProp: 100, uri: '2' } }
99 ],
100 updateSegments(
101 [
102 { uri: 'test-uri-0' },
103 { uri: 'test-uri-1', map: { someProp: 1 } }
104 ],
105 [
106 { uri: 'test-uri-2' },
107 { uri: 'test-uri-3', map: { someProp: 100, uri: '2' } }
108 ],
109 2),
110 'copies over/overwrites properties with offset of 2');
111});
112
113QUnit.test('updateMaster returns null when no playlists', function(assert) {
114 const master = {
115 playlists: []
116 };
117 const media = {};
118
119 assert.deepEqual(updateMaster(master, media), null, 'returns null when no playlists');
120});
121
122QUnit.test('updateMaster returns null when no change', function(assert) {
123 const master = {
124 playlists: [{
125 mediaSequence: 0,
126 attributes: {
127 BANDWIDTH: 9
128 },
129 uri: 'playlist-0-uri',
130 resolvedUri: urlTo('playlist-0-uri'),
131 segments: [{
132 duration: 10,
133 uri: 'segment-0-uri',
134 resolvedUri: urlTo('segment-0-uri')
135 }]
136 }]
137 };
138 const media = {
139 mediaSequence: 0,
140 attributes: {
141 BANDWIDTH: 9
142 },
143 uri: 'playlist-0-uri',
144 segments: [{
145 duration: 10,
146 uri: 'segment-0-uri'
147 }]
148 };
149
150 assert.deepEqual(updateMaster(master, media), null, 'returns null');
151});
152
153QUnit.test('updateMaster updates master when new media sequence', function(assert) {
154 const master = {
155 playlists: [{
156 mediaSequence: 0,
157 attributes: {
158 BANDWIDTH: 9
159 },
160 uri: 'playlist-0-uri',
161 resolvedUri: urlTo('playlist-0-uri'),
162 segments: [{
163 duration: 10,
164 uri: 'segment-0-uri',
165 resolvedUri: urlTo('segment-0-uri')
166 }]
167 }]
168 };
169 const media = {
170 mediaSequence: 1,
171 attributes: {
172 BANDWIDTH: 9
173 },
174 uri: 'playlist-0-uri',
175 segments: [{
176 duration: 10,
177 uri: 'segment-0-uri'
178 }]
179 };
180
181 assert.deepEqual(
182 updateMaster(master, media),
183 {
184 playlists: [{
185 mediaSequence: 1,
186 attributes: {
187 BANDWIDTH: 9
188 },
189 uri: 'playlist-0-uri',
190 resolvedUri: urlTo('playlist-0-uri'),
191 segments: [{
192 duration: 10,
193 uri: 'segment-0-uri',
194 resolvedUri: urlTo('segment-0-uri')
195 }]
196 }]
197 },
198 'updates master when new media sequence');
199});
200
201QUnit.test('updateMaster retains top level values in master', function(assert) {
202 const master = {
203 mediaGroups: {
204 AUDIO: {
205 'GROUP-ID': {
206 default: true,
207 uri: 'audio-uri'
208 }
209 }
210 },
211 playlists: [{
212 mediaSequence: 0,
213 attributes: {
214 BANDWIDTH: 9
215 },
216 uri: 'playlist-0-uri',
217 resolvedUri: urlTo('playlist-0-uri'),
218 segments: [{
219 duration: 10,
220 uri: 'segment-0-uri',
221 resolvedUri: urlTo('segment-0-uri')
222 }]
223 }]
224 };
225 const media = {
226 mediaSequence: 1,
227 attributes: {
228 BANDWIDTH: 9
229 },
230 uri: 'playlist-0-uri',
231 segments: [{
232 duration: 10,
233 uri: 'segment-0-uri'
234 }]
235 };
236
237 assert.deepEqual(
238 updateMaster(master, media),
239 {
240 mediaGroups: {
241 AUDIO: {
242 'GROUP-ID': {
243 default: true,
244 uri: 'audio-uri'
245 }
246 }
247 },
248 playlists: [{
249 mediaSequence: 1,
250 attributes: {
251 BANDWIDTH: 9
252 },
253 uri: 'playlist-0-uri',
254 resolvedUri: urlTo('playlist-0-uri'),
255 segments: [{
256 duration: 10,
257 uri: 'segment-0-uri',
258 resolvedUri: urlTo('segment-0-uri')
259 }]
260 }]
261 },
262 'retains top level values in master');
263});
264
265QUnit.test('updateMaster adds new segments to master', function(assert) {
266 const master = {
267 mediaGroups: {
268 AUDIO: {
269 'GROUP-ID': {
270 default: true,
271 uri: 'audio-uri'
272 }
273 }
274 },
275 playlists: [{
276 mediaSequence: 0,
277 attributes: {
278 BANDWIDTH: 9
279 },
280 uri: 'playlist-0-uri',
281 resolvedUri: urlTo('playlist-0-uri'),
282 segments: [{
283 duration: 10,
284 uri: 'segment-0-uri',
285 resolvedUri: urlTo('segment-0-uri')
286 }]
287 }]
288 };
289 const media = {
290 mediaSequence: 1,
291 attributes: {
292 BANDWIDTH: 9
293 },
294 uri: 'playlist-0-uri',
295 segments: [{
296 duration: 10,
297 uri: 'segment-0-uri'
298 }, {
299 duration: 9,
300 uri: 'segment-1-uri'
301 }]
302 };
303
304 assert.deepEqual(
305 updateMaster(master, media),
306 {
307 mediaGroups: {
308 AUDIO: {
309 'GROUP-ID': {
310 default: true,
311 uri: 'audio-uri'
312 }
313 }
314 },
315 playlists: [{
316 mediaSequence: 1,
317 attributes: {
318 BANDWIDTH: 9
319 },
320 uri: 'playlist-0-uri',
321 resolvedUri: urlTo('playlist-0-uri'),
322 segments: [{
323 duration: 10,
324 uri: 'segment-0-uri',
325 resolvedUri: urlTo('segment-0-uri')
326 }, {
327 duration: 9,
328 uri: 'segment-1-uri',
329 resolvedUri: urlTo('segment-1-uri')
330 }]
331 }]
332 },
333 'adds new segment to master');
334});
335
336QUnit.test('updateMaster changes old values', function(assert) {
337 const master = {
338 mediaGroups: {
339 AUDIO: {
340 'GROUP-ID': {
341 default: true,
342 uri: 'audio-uri'
343 }
344 }
345 },
346 playlists: [{
347 mediaSequence: 0,
348 attributes: {
349 BANDWIDTH: 9
350 },
351 uri: 'playlist-0-uri',
352 resolvedUri: urlTo('playlist-0-uri'),
353 segments: [{
354 duration: 10,
355 uri: 'segment-0-uri',
356 resolvedUri: urlTo('segment-0-uri')
357 }]
358 }]
359 };
360 const media = {
361 mediaSequence: 1,
362 attributes: {
363 BANDWIDTH: 8,
364 newField: 1
365 },
366 uri: 'playlist-0-uri',
367 segments: [{
368 duration: 8,
369 uri: 'segment-0-uri'
370 }, {
371 duration: 10,
372 uri: 'segment-1-uri'
373 }]
374 };
375
376 assert.deepEqual(
377 updateMaster(master, media),
378 {
379 mediaGroups: {
380 AUDIO: {
381 'GROUP-ID': {
382 default: true,
383 uri: 'audio-uri'
384 }
385 }
386 },
387 playlists: [{
388 mediaSequence: 1,
389 attributes: {
390 BANDWIDTH: 8,
391 newField: 1
392 },
393 uri: 'playlist-0-uri',
394 resolvedUri: urlTo('playlist-0-uri'),
395 segments: [{
396 duration: 8,
397 uri: 'segment-0-uri',
398 resolvedUri: urlTo('segment-0-uri')
399 }, {
400 duration: 10,
401 uri: 'segment-1-uri',
402 resolvedUri: urlTo('segment-1-uri')
403 }]
404 }]
405 },
406 'changes old values');
407});
408
409QUnit.test('updateMaster retains saved segment values', function(assert) {
410 const master = {
411 playlists: [{
412 mediaSequence: 0,
413 uri: 'playlist-0-uri',
414 resolvedUri: urlTo('playlist-0-uri'),
415 segments: [{
416 duration: 10,
417 uri: 'segment-0-uri',
418 resolvedUri: urlTo('segment-0-uri'),
419 startTime: 0,
420 endTime: 10
421 }]
422 }]
423 };
424 const media = {
425 mediaSequence: 0,
426 uri: 'playlist-0-uri',
427 segments: [{
428 duration: 8,
429 uri: 'segment-0-uri'
430 }, {
431 duration: 10,
432 uri: 'segment-1-uri'
433 }]
434 };
435
436 assert.deepEqual(
437 updateMaster(master, media),
438 {
439 playlists: [{
440 mediaSequence: 0,
441 uri: 'playlist-0-uri',
442 resolvedUri: urlTo('playlist-0-uri'),
443 segments: [{
444 duration: 8,
445 uri: 'segment-0-uri',
446 resolvedUri: urlTo('segment-0-uri'),
447 startTime: 0,
448 endTime: 10
449 }, {
450 duration: 10,
451 uri: 'segment-1-uri',
452 resolvedUri: urlTo('segment-1-uri')
453 }]
454 }]
455 },
456 'retains saved segment values');
457});
458
459QUnit.test('updateMaster resolves key and map URIs', function(assert) {
460 const master = {
461 playlists: [{
462 mediaSequence: 0,
463 attributes: {
464 BANDWIDTH: 9
465 },
466 uri: 'playlist-0-uri',
467 resolvedUri: urlTo('playlist-0-uri'),
468 segments: [{
469 duration: 10,
470 uri: 'segment-0-uri',
471 resolvedUri: urlTo('segment-0-uri')
472 }, {
473 duration: 10,
474 uri: 'segment-1-uri',
475 resolvedUri: urlTo('segment-1-uri')
476 }]
477 }]
478 };
479 const media = {
480 mediaSequence: 3,
481 attributes: {
482 BANDWIDTH: 9
483 },
484 uri: 'playlist-0-uri',
485 segments: [{
486 duration: 9,
487 uri: 'segment-2-uri',
488 key: {
489 uri: 'key-2-uri'
490 },
491 map: {
492 uri: 'map-2-uri'
493 }
494 }, {
495 duration: 11,
496 uri: 'segment-3-uri',
497 key: {
498 uri: 'key-3-uri'
499 },
500 map: {
501 uri: 'map-3-uri'
502 }
503 }]
504 };
505
506 assert.deepEqual(
507 updateMaster(master, media),
508 {
509 playlists: [{
510 mediaSequence: 3,
511 attributes: {
512 BANDWIDTH: 9
513 },
514 uri: 'playlist-0-uri',
515 resolvedUri: urlTo('playlist-0-uri'),
516 segments: [{
517 duration: 9,
518 uri: 'segment-2-uri',
519 resolvedUri: urlTo('segment-2-uri'),
520 key: {
521 uri: 'key-2-uri',
522 resolvedUri: urlTo('key-2-uri')
523 },
524 map: {
525 uri: 'map-2-uri',
526 resolvedUri: urlTo('map-2-uri')
527 }
528 }, {
529 duration: 11,
530 uri: 'segment-3-uri',
531 resolvedUri: urlTo('segment-3-uri'),
532 key: {
533 uri: 'key-3-uri',
534 resolvedUri: urlTo('key-3-uri')
535 },
536 map: {
537 uri: 'map-3-uri',
538 resolvedUri: urlTo('map-3-uri')
539 }
540 }]
541 }]
542 },
543 'resolves key and map URIs');
544});
545
546QUnit.test('setupMediaPlaylists does nothing if no playlists', function(assert) {
547 const master = {
548 playlists: []
549 };
550
551 setupMediaPlaylists(master);
552
553 assert.deepEqual(master, {
554 playlists: []
555 }, 'master remains unchanged');
556});
557
558QUnit.test('setupMediaPlaylists adds URI keys for each playlist', function(assert) {
559 const master = {
560 uri: 'master-uri',
561 playlists: [{
562 uri: 'uri-0'
563 }, {
564 uri: 'uri-1'
565 }]
566 };
567 const expectedPlaylist0 = {
568 attributes: {},
569 resolvedUri: urlTo('uri-0'),
570 uri: 'uri-0'
571 };
572 const expectedPlaylist1 = {
573 attributes: {},
574 resolvedUri: urlTo('uri-1'),
575 uri: 'uri-1'
576 };
577
578 setupMediaPlaylists(master);
579
580 assert.deepEqual(master.playlists[0], expectedPlaylist0, 'retained playlist indices');
581 assert.deepEqual(master.playlists[1], expectedPlaylist1, 'retained playlist indices');
582 assert.deepEqual(master.playlists['uri-0'], expectedPlaylist0, 'added playlist key');
583 assert.deepEqual(master.playlists['uri-1'], expectedPlaylist1, 'added playlist key');
584
585 assert.equal(this.env.log.warn.calls, 2, 'logged two warnings');
586 assert.equal(this.env.log.warn.args[0],
587 'Invalid playlist STREAM-INF detected. Missing BANDWIDTH attribute.',
588 'logged a warning');
589 assert.equal(this.env.log.warn.args[1],
590 'Invalid playlist STREAM-INF detected. Missing BANDWIDTH attribute.',
591 'logged a warning');
592});
593
594QUnit.test('setupMediaPlaylists adds attributes objects if missing', function(assert) {
595 const master = {
596 uri: 'master-uri',
597 playlists: [{
598 uri: 'uri-0'
599 }, {
600 uri: 'uri-1'
601 }]
602 };
603
604 setupMediaPlaylists(master);
605
606 assert.ok(master.playlists[0].attributes, 'added attributes object');
607 assert.ok(master.playlists[1].attributes, 'added attributes object');
608
609 assert.equal(this.env.log.warn.calls, 2, 'logged two warnings');
610 assert.equal(this.env.log.warn.args[0],
611 'Invalid playlist STREAM-INF detected. Missing BANDWIDTH attribute.',
612 'logged a warning');
613 assert.equal(this.env.log.warn.args[1],
614 'Invalid playlist STREAM-INF detected. Missing BANDWIDTH attribute.',
615 'logged a warning');
616});
617
618QUnit.test('setupMediaPlaylists resolves playlist URIs', function(assert) {
619 const master = {
620 uri: 'master-uri',
621 playlists: [{
622 attributes: { BANDWIDTH: 10 },
623 uri: 'uri-0'
624 }, {
625 attributes: { BANDWIDTH: 100 },
626 uri: 'uri-1'
627 }]
628 };
629
630 setupMediaPlaylists(master);
631
632 assert.equal(master.playlists[0].resolvedUri, urlTo('uri-0'), 'resolves URI');
633 assert.equal(master.playlists[1].resolvedUri, urlTo('uri-1'), 'resolves URI');
634});
635
636QUnit.test('resolveMediaGroupUris does nothing when no media groups', function(assert) {
637 const master = {
638 uri: 'master-uri',
639 playlists: [],
640 mediaGroups: []
641 };
642
643 resolveMediaGroupUris(master);
644 assert.deepEqual(master, {
645 uri: 'master-uri',
646 playlists: [],
647 mediaGroups: []
648 }, 'does nothing when no media groups');
649});
650
651QUnit.test('resolveMediaGroupUris resolves media group URIs', function(assert) {
652 const master = {
653 uri: 'master-uri',
654 playlists: [{
655 attributes: { BANDWIDTH: 10 },
656 uri: 'playlist-0'
657 }],
658 mediaGroups: {
659 // CLOSED-CAPTIONS will never have a URI
660 'CLOSED-CAPTIONS': {
661 cc1: {
662 English: {}
663 }
664 },
665 'AUDIO': {
666 low: {
667 // audio doesn't need a URI if it is a label for muxed
668 main: {},
669 commentary: {
670 uri: 'audio-low-commentary-uri'
671 }
672 },
673 high: {
674 main: {},
675 commentary: {
676 uri: 'audio-high-commentary-uri'
677 }
678 }
679 },
680 'SUBTITLES': {
681 sub1: {
682 english: {
683 uri: 'subtitles-1-english-uri'
684 },
685 spanish: {
686 uri: 'subtitles-1-spanish-uri'
687 }
688 },
689 sub2: {
690 english: {
691 uri: 'subtitles-2-english-uri'
692 },
693 spanish: {
694 uri: 'subtitles-2-spanish-uri'
695 }
696 },
697 sub3: {
698 english: {
699 uri: 'subtitles-3-english-uri'
700 },
701 spanish: {
702 uri: 'subtitles-3-spanish-uri'
703 }
704 }
705 }
706 }
707 };
708
709 resolveMediaGroupUris(master);
710
711 assert.deepEqual(master, {
712 uri: 'master-uri',
713 playlists: [{
714 attributes: { BANDWIDTH: 10 },
715 uri: 'playlist-0'
716 }],
717 mediaGroups: {
718 // CLOSED-CAPTIONS will never have a URI
719 'CLOSED-CAPTIONS': {
720 cc1: {
721 English: {}
722 }
723 },
724 'AUDIO': {
725 low: {
726 // audio doesn't need a URI if it is a label for muxed
727 main: {},
728 commentary: {
729 uri: 'audio-low-commentary-uri',
730 resolvedUri: urlTo('audio-low-commentary-uri')
731 }
732 },
733 high: {
734 main: {},
735 commentary: {
736 uri: 'audio-high-commentary-uri',
737 resolvedUri: urlTo('audio-high-commentary-uri')
738 }
739 }
740 },
741 'SUBTITLES': {
742 sub1: {
743 english: {
744 uri: 'subtitles-1-english-uri',
745 resolvedUri: urlTo('subtitles-1-english-uri')
746 },
747 spanish: {
748 uri: 'subtitles-1-spanish-uri',
749 resolvedUri: urlTo('subtitles-1-spanish-uri')
750 }
751 },
752 sub2: {
753 english: {
754 uri: 'subtitles-2-english-uri',
755 resolvedUri: urlTo('subtitles-2-english-uri')
756 },
757 spanish: {
758 uri: 'subtitles-2-spanish-uri',
759 resolvedUri: urlTo('subtitles-2-spanish-uri')
760 }
761 },
762 sub3: {
763 english: {
764 uri: 'subtitles-3-english-uri',
765 resolvedUri: urlTo('subtitles-3-english-uri')
766 },
767 spanish: {
768 uri: 'subtitles-3-spanish-uri',
769 resolvedUri: urlTo('subtitles-3-spanish-uri')
770 }
771 }
772 }
773 }
774 }, 'resolved URIs of certain media groups');
775});
776
777QUnit.test('uses last segment duration for refresh delay', function(assert) {
778 const media = { targetDuration: 7, segments: [] };
779
780 assert.equal(refreshDelay(media, true), 3500,
781 'used half targetDuration when no segments');
782
783 media.segments = [ { duration: 6}, { duration: 4 }, { } ];
784 assert.equal(refreshDelay(media, true), 3500,
785 'used half targetDuration when last segment duration cannot be determined');
786
787 media.segments = [ { duration: 6}, { duration: 4}, { duration: 5 } ];
788 assert.equal(refreshDelay(media, true), 5000, 'used last segment duration for delay');
789
790 assert.equal(refreshDelay(media, false), 3500,
791 'used half targetDuration when update is false');
792});
793
794QUnit.test('throws if the playlist url is empty or undefined', function(assert) {
795 assert.throws(function() {
796 PlaylistLoader();
797 }, 'requires an argument');
798 assert.throws(function() {
799 PlaylistLoader('');
800 }, 'does not accept the empty string');
801});
802
803QUnit.test('starts without any metadata', function(assert) {
804 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
805
806 loader.load();
807
808 assert.strictEqual(loader.state, 'HAVE_NOTHING', 'no metadata has loaded yet');
809});
810
811QUnit.test('requests the initial playlist immediately', function(assert) {
812 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
813
814 loader.load();
815
816 assert.strictEqual(this.requests.length, 1, 'made a request');
817 assert.strictEqual(this.requests[0].url,
818 'master.m3u8',
819 'requested the initial playlist');
820});
821
822QUnit.test('moves to HAVE_MASTER after loading a master playlist', function(assert) {
823 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
824 let state;
825
826 loader.load();
827
828 loader.on('loadedplaylist', function() {
829 state = loader.state;
830 });
831 this.requests.pop().respond(200, null,
832 '#EXTM3U\n' +
833 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
834 'media.m3u8\n');
835 assert.ok(loader.master, 'the master playlist is available');
836 assert.strictEqual(state, 'HAVE_MASTER', 'the state at loadedplaylist correct');
837});
838
839QUnit.test('logs warning for master playlist with invalid STREAM-INF', function(assert) {
840 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
841
842 loader.load();
843
844 this.requests.pop().respond(200, null,
845 '#EXTM3U\n' +
846 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
847 'video1/media.m3u8\n' +
848 '#EXT-X-STREAM-INF:\n' +
849 'video2/media.m3u8\n');
850
851 assert.ok(loader.master, 'infers a master playlist');
852 assert.equal(loader.master.playlists[1].uri, 'video2/media.m3u8',
853 'parsed invalid stream');
854 assert.ok(loader.master.playlists[1].attributes, 'attached attributes property');
855 assert.equal(this.env.log.warn.calls, 1, 'logged a warning');
856 assert.equal(this.env.log.warn.args[0],
857 'Invalid playlist STREAM-INF detected. Missing BANDWIDTH attribute.',
858 'logged a warning');
859});
860
861QUnit.test('jumps to HAVE_METADATA when initialized with a media playlist',
862function(assert) {
863 let loadedmetadatas = 0;
864 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
865
866 loader.load();
867
868 loader.on('loadedmetadata', function() {
869 loadedmetadatas++;
870 });
871 this.requests.pop().respond(200, null,
872 '#EXTM3U\n' +
873 '#EXTINF:10,\n' +
874 '0.ts\n' +
875 '#EXT-X-ENDLIST\n');
876 assert.ok(loader.master, 'infers a master playlist');
877 assert.ok(loader.media(), 'sets the media playlist');
878 assert.ok(loader.media().uri, 'sets the media playlist URI');
879 assert.ok(loader.media().attributes, 'sets the media playlist attributes');
880 assert.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
881 assert.strictEqual(this.requests.length, 0, 'no more requests are made');
882 assert.strictEqual(loadedmetadatas, 1, 'fired one loadedmetadata');
883});
884
885QUnit.test('resolves relative media playlist URIs', function(assert) {
886 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
887
888 loader.load();
889
890 this.requests.shift().respond(200, null,
891 '#EXTM3U\n' +
892 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
893 'video/media.m3u8\n');
894 assert.equal(loader.master.playlists[0].resolvedUri, urlTo('video/media.m3u8'),
895 'resolved media URI');
896});
897
898QUnit.test('playlist loader returns the correct amount of enabled playlists',
899function(assert) {
900 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
901
902 loader.load();
903
904 this.requests.shift().respond(200, null,
905 '#EXTM3U\n' +
906 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
907 'video1/media.m3u8\n' +
908 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
909 'video2/media.m3u8\n');
910 assert.equal(loader.enabledPlaylists_(), 2, 'Returned initial amount of playlists');
911 loader.master.playlists[0].excludeUntil = Date.now() + 100000;
912 this.clock.tick(1000);
913 assert.equal(loader.enabledPlaylists_(), 1, 'Returned one less playlist');
914});
915
916QUnit.test('playlist loader detects if we are on lowest rendition', function(assert) {
917 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
918
919 loader.load();
920 this.requests.shift().respond(200, null,
921 '#EXTM3U\n' +
922 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
923 'video1/media.m3u8\n' +
924 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
925 'video2/media.m3u8\n');
926 loader.media = function() {
927 return {attributes: {BANDWIDTH: 10}};
928 };
929
930 loader.master.playlists = [{attributes: {BANDWIDTH: 10}},
931 {attributes: {BANDWIDTH: 20}}];
932 assert.ok(loader.isLowestEnabledRendition_(), 'Detected on lowest rendition');
933
934 loader.master.playlists = [{attributes: {BANDWIDTH: 10}},
935 {attributes: {BANDWIDTH: 10}},
936 {attributes: {BANDWIDTH: 10}},
937 {attributes: {BANDWIDTH: 20}}];
938 assert.ok(loader.isLowestEnabledRendition_(), 'Detected on lowest rendition');
939
940 loader.media = function() {
941 return {attributes: {BANDWIDTH: 20}};
942 };
943
944 loader.master.playlists = [{attributes: {BANDWIDTH: 10}},
945 {attributes: {BANDWIDTH: 20}}];
946 assert.ok(!loader.isLowestEnabledRendition_(), 'Detected not on lowest rendition');
947});
948
949QUnit.test('resolves media initialization segment URIs', function(assert) {
950 let loader = new PlaylistLoader('video/fmp4.m3u8', this.fakeHls);
951
952 loader.load();
953 this.requests.shift().respond(200, null,
954 '#EXTM3U\n' +
955 '#EXT-X-MAP:URI="main.mp4",BYTERANGE="720@0"\n' +
956 '#EXTINF:10,\n' +
957 '0.ts\n' +
958 '#EXT-X-ENDLIST\n');
959
960 assert.equal(loader.media().segments[0].map.resolvedUri, urlTo('video/main.mp4'),
961 'resolved init segment URI');
962});
963
964QUnit.test('recognizes absolute URIs and requests them unmodified', function(assert) {
965 let loader = new PlaylistLoader('manifest/media.m3u8', this.fakeHls);
966
967 loader.load();
968
969 this.requests.shift().respond(200, null,
970 '#EXTM3U\n' +
971 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
972 'http://example.com/video/media.m3u8\n');
973 assert.equal(loader.master.playlists[0].resolvedUri,
974 'http://example.com/video/media.m3u8', 'resolved media URI');
975
976 this.requests.shift().respond(200, null,
977 '#EXTM3U\n' +
978 '#EXTINF:10,\n' +
979 'http://example.com/00001.ts\n' +
980 '#EXT-X-ENDLIST\n');
981 assert.equal(loader.media().segments[0].resolvedUri,
982 'http://example.com/00001.ts', 'resolved segment URI');
983});
984
985QUnit.test('recognizes domain-relative URLs', function(assert) {
986 let loader = new PlaylistLoader('manifest/media.m3u8', this.fakeHls);
987
988 loader.load();
989
990 this.requests.shift().respond(200, null,
991 '#EXTM3U\n' +
992 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
993 '/media.m3u8\n');
994 assert.equal(loader.master.playlists[0].resolvedUri,
995 window.location.protocol + '//' +
996 window.location.host + '/media.m3u8',
997 'resolved media URI');
998
999 this.requests.shift().respond(200, null,
1000 '#EXTM3U\n' +
1001 '#EXTINF:10,\n' +
1002 '/00001.ts\n' +
1003 '#EXT-X-ENDLIST\n');
1004 assert.equal(loader.media().segments[0].resolvedUri,
1005 window.location.protocol + '//' +
1006 window.location.host + '/00001.ts',
1007 'resolved segment URI');
1008});
1009
1010QUnit.test('recognizes key URLs relative to master and playlist', function(assert) {
1011 let loader = new PlaylistLoader('/video/media-encrypted.m3u8', this.fakeHls);
1012
1013 loader.load();
1014
1015 this.requests.shift().respond(200, null,
1016 '#EXTM3U\n' +
1017 '#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=17\n' +
1018 'playlist/playlist.m3u8\n' +
1019 '#EXT-X-ENDLIST\n');
1020 assert.equal(loader.master.playlists[0].resolvedUri,
1021 window.location.protocol + '//' +
1022 window.location.host + '/video/playlist/playlist.m3u8',
1023 'resolved media URI');
1024
1025 this.requests.shift().respond(200, null,
1026 '#EXTM3U\n' +
1027 '#EXT-X-TARGETDURATION:15\n' +
1028 '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' +
1029 '#EXTINF:2.833,\n' +
1030 'http://example.com/000001.ts\n' +
1031 '#EXT-X-ENDLIST\n');
1032 assert.equal(loader.media().segments[0].key.resolvedUri,
1033 window.location.protocol + '//' +
1034 window.location.host + '/video/playlist/keys/key.php',
1035 'resolved multiple relative paths for key URI');
1036});
1037
1038QUnit.test('trigger an error event when a media playlist 404s', function(assert) {
1039 let count = 0;
1040 let loader = new PlaylistLoader('manifest/master.m3u8', this.fakeHls);
1041
1042 loader.load();
1043
1044 loader.on('error', function() {
1045 count += 1;
1046 });
1047
1048 // master
1049 this.requests.shift().respond(200, null,
1050 '#EXTM3U\n' +
1051 '#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=17\n' +
1052 'playlist/playlist.m3u8\n' +
1053 '#EXT-X-STREAM-INF:PROGRAM-ID=2,BANDWIDTH=170\n' +
1054 'playlist/playlist2.m3u8\n' +
1055 '#EXT-X-ENDLIST\n');
1056 assert.equal(count, 0,
1057 'error not triggered before requesting playlist');
1058
1059 // playlist
1060 this.requests.shift().respond(404);
1061
1062 assert.equal(count, 1,
1063 'error triggered after playlist 404');
1064});
1065
1066QUnit.test('recognizes absolute key URLs', function(assert) {
1067 let loader = new PlaylistLoader('/video/media-encrypted.m3u8', this.fakeHls);
1068
1069 loader.load();
1070
1071 this.requests.shift().respond(200, null,
1072 '#EXTM3U\n' +
1073 '#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=17\n' +
1074 'playlist/playlist.m3u8\n' +
1075 '#EXT-X-ENDLIST\n');
1076 assert.equal(loader.master.playlists[0].resolvedUri,
1077 window.location.protocol + '//' +
1078 window.location.host + '/video/playlist/playlist.m3u8',
1079 'resolved media URI');
1080
1081 this.requests.shift().respond(
1082 200,
1083 null,
1084 '#EXTM3U\n' +
1085 '#EXT-X-TARGETDURATION:15\n' +
1086 '#EXT-X-KEY:METHOD=AES-128,URI="http://example.com/keys/key.php"\n' +
1087 '#EXTINF:2.833,\n' +
1088 'http://example.com/000001.ts\n' +
1089 '#EXT-X-ENDLIST\n'
1090 );
1091 assert.equal(loader.media().segments[0].key.resolvedUri,
1092 'http://example.com/keys/key.php', 'resolved absolute path for key URI');
1093});
1094
1095QUnit.test('jumps to HAVE_METADATA when initialized with a live media playlist',
1096function(assert) {
1097 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
1098
1099 loader.load();
1100
1101 this.requests.pop().respond(200, null,
1102 '#EXTM3U\n' +
1103 '#EXTINF:10,\n' +
1104 '0.ts\n');
1105 assert.ok(loader.master, 'infers a master playlist');
1106 assert.ok(loader.media(), 'sets the media playlist');
1107 assert.ok(loader.media().attributes, 'sets the media playlist attributes');
1108 assert.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
1109});
1110
1111QUnit.test('moves to HAVE_METADATA after loading a media playlist', function(assert) {
1112 let loadedPlaylist = 0;
1113 let loadedMetadata = 0;
1114 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
1115
1116 loader.load();
1117
1118 loader.on('loadedplaylist', function() {
1119 loadedPlaylist++;
1120 });
1121 loader.on('loadedmetadata', function() {
1122 loadedMetadata++;
1123 });
1124 this.requests.pop().respond(200, null,
1125 '#EXTM3U\n' +
1126 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
1127 'media.m3u8\n' +
1128 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
1129 'alt.m3u8\n');
1130 assert.strictEqual(loadedPlaylist, 1, 'fired loadedplaylist once');
1131 assert.strictEqual(loadedMetadata, 0, 'did not fire loadedmetadata');
1132 assert.strictEqual(this.requests.length, 1, 'requests the media playlist');
1133 assert.strictEqual(this.requests[0].method, 'GET', 'GETs the media playlist');
1134 assert.strictEqual(this.requests[0].url,
1135 urlTo('media.m3u8'),
1136 'requests the first playlist');
1137
1138 this.requests.pop().respond(200, null,
1139 '#EXTM3U\n' +
1140 '#EXTINF:10,\n' +
1141 '0.ts\n');
1142 assert.ok(loader.master, 'sets the master playlist');
1143 assert.ok(loader.media(), 'sets the media playlist');
1144 assert.strictEqual(loadedPlaylist, 2, 'fired loadedplaylist twice');
1145 assert.strictEqual(loadedMetadata, 1, 'fired loadedmetadata once');
1146 assert.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
1147});
1148
1149QUnit.test('defaults missing media groups for a media playlist', function(assert) {
1150 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
1151
1152 loader.load();
1153 this.requests.pop().respond(200, null,
1154 '#EXTM3U\n' +
1155 '#EXTINF:10,\n' +
1156 '0.ts\n');
1157
1158 assert.ok(loader.master.mediaGroups.AUDIO, 'defaulted audio');
1159 assert.ok(loader.master.mediaGroups.VIDEO, 'defaulted video');
1160 assert.ok(loader.master.mediaGroups['CLOSED-CAPTIONS'], 'defaulted closed captions');
1161 assert.ok(loader.master.mediaGroups.SUBTITLES, 'defaulted subtitles');
1162});
1163
1164QUnit.test('moves to HAVE_CURRENT_METADATA when refreshing the playlist',
1165function(assert) {
1166 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
1167
1168 loader.load();
1169
1170 this.requests.pop().respond(200, null,
1171 '#EXTM3U\n' +
1172 '#EXTINF:10,\n' +
1173 '0.ts\n');
1174 // 10s, one target duration
1175 this.clock.tick(10 * 1000);
1176 assert.strictEqual(loader.state, 'HAVE_CURRENT_METADATA', 'the state is correct');
1177 assert.strictEqual(this.requests.length, 1, 'requested playlist');
1178 assert.strictEqual(this.requests[0].url,
1179 urlTo('live.m3u8'),
1180 'refreshes the media playlist');
1181});
1182
1183QUnit.test('returns to HAVE_METADATA after refreshing the playlist', function(assert) {
1184 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
1185
1186 loader.load();
1187
1188 this.requests.pop().respond(200, null,
1189 '#EXTM3U\n' +
1190 '#EXTINF:10,\n' +
1191 '0.ts\n');
1192 // 10s, one target duration
1193 this.clock.tick(10 * 1000);
1194 this.requests.pop().respond(200, null,
1195 '#EXTM3U\n' +
1196 '#EXTINF:10,\n' +
1197 '1.ts\n');
1198 assert.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
1199});
1200
1201QUnit.test('refreshes the playlist after last segment duration', function(assert) {
1202 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
1203 let refreshes = 0;
1204
1205 loader.on('mediaupdatetimeout', () => refreshes++);
1206
1207 loader.load();
1208
1209 this.requests.pop().respond(200, null,
1210 '#EXTM3U\n' +
1211 '#EXT-X-TARGETDURATION:10\n' +
1212 '#EXTINF:10,\n' +
1213 '0.ts\n' +
1214 '#EXTINF:4\n' +
1215 '1.ts\n');
1216 // 4s, last segment duration
1217 this.clock.tick(4 * 1000);
1218
1219 assert.equal(refreshes, 1, 'refreshed playlist after last segment duration');
1220});
1221
1222QUnit.test('emits an error when an initial playlist request fails', function(assert) {
1223 let errors = [];
1224 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
1225
1226 loader.load();
1227
1228 loader.on('error', function() {
1229 errors.push(loader.error);
1230 });
1231 this.requests.pop().respond(500);
1232
1233 assert.strictEqual(errors.length, 1, 'emitted one error');
1234 assert.strictEqual(errors[0].status, 500, 'http status is captured');
1235});
1236
1237QUnit.test('errors when an initial media playlist request fails', function(assert) {
1238 let errors = [];
1239 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
1240
1241 loader.load();
1242
1243 loader.on('error', function() {
1244 errors.push(loader.error);
1245 });
1246 this.requests.pop().respond(200, null,
1247 '#EXTM3U\n' +
1248 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
1249 'media.m3u8\n');
1250
1251 assert.strictEqual(errors.length, 0, 'emitted no errors');
1252
1253 this.requests.pop().respond(500);
1254
1255 assert.strictEqual(errors.length, 1, 'emitted one error');
1256 assert.strictEqual(errors[0].status, 500, 'http status is captured');
1257});
1258
1259// http://tools.ietf.org/html/draft-pantos-http-live-streaming-12#section-6.3.4
1260QUnit.test('halves the refresh timeout if a playlist is unchanged since the last reload',
1261function(assert) {
1262 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
1263
1264 loader.load();
1265
1266 this.requests.pop().respond(200, null,
1267 '#EXTM3U\n' +
1268 '#EXT-X-MEDIA-SEQUENCE:0\n' +
1269 '#EXTINF:10,\n' +
1270 '0.ts\n');
1271 // trigger a refresh
1272 this.clock.tick(10 * 1000);
1273 this.requests.pop().respond(200, null,
1274 '#EXTM3U\n' +
1275 '#EXT-X-MEDIA-SEQUENCE:0\n' +
1276 '#EXTINF:10,\n' +
1277 '0.ts\n');
1278 // half the default target-duration
1279 this.clock.tick(5 * 1000);
1280
1281 assert.strictEqual(this.requests.length, 1, 'sent a request');
1282 assert.strictEqual(this.requests[0].url,
1283 urlTo('live.m3u8'),
1284 'requested the media playlist');
1285});
1286
1287QUnit.test('preserves segment metadata across playlist refreshes', function(assert) {
1288 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
1289 let segment;
1290
1291 loader.load();
1292
1293 this.requests.pop().respond(200, null,
1294 '#EXTM3U\n' +
1295 '#EXT-X-MEDIA-SEQUENCE:0\n' +
1296 '#EXTINF:10,\n' +
1297 '0.ts\n' +
1298 '#EXTINF:10,\n' +
1299 '1.ts\n' +
1300 '#EXTINF:10,\n' +
1301 '2.ts\n');
1302 // add PTS info to 1.ts
1303 segment = loader.media().segments[1];
1304 segment.minVideoPts = 14;
1305 segment.maxAudioPts = 27;
1306 segment.preciseDuration = 10.045;
1307
1308 // trigger a refresh
1309 this.clock.tick(10 * 1000);
1310 this.requests.pop().respond(200, null,
1311 '#EXTM3U\n' +
1312 '#EXT-X-MEDIA-SEQUENCE:1\n' +
1313 '#EXTINF:10,\n' +
1314 '1.ts\n' +
1315 '#EXTINF:10,\n' +
1316 '2.ts\n');
1317
1318 assert.deepEqual(loader.media().segments[0], segment, 'preserved segment attributes');
1319});
1320
1321QUnit.test('clears the update timeout when switching quality', function(assert) {
1322 let loader = new PlaylistLoader('live-master.m3u8', this.fakeHls);
1323 let refreshes = 0;
1324
1325 loader.load();
1326
1327 // track the number of playlist refreshes triggered
1328 loader.on('mediaupdatetimeout', function() {
1329 refreshes++;
1330 });
1331 // deliver the master
1332 this.requests.pop().respond(200, null,
1333 '#EXTM3U\n' +
1334 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
1335 'live-low.m3u8\n' +
1336 '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
1337 'live-high.m3u8\n');
1338 // deliver the low quality playlist
1339 this.requests.pop().respond(200, null,
1340 '#EXTM3U\n' +
1341 '#EXT-X-MEDIA-SEQUENCE:0\n' +
1342 '#EXTINF:10,\n' +
1343 'low-0.ts\n');
1344 // change to a higher quality playlist
1345 loader.media('live-high.m3u8');
1346 this.requests.pop().respond(200, null,
1347 '#EXTM3U\n' +
1348 '#EXT-X-MEDIA-SEQUENCE:0\n' +
1349 '#EXTINF:10,\n' +
1350 'high-0.ts\n');
1351 // trigger a refresh
1352 this.clock.tick(10 * 1000);
1353
1354 assert.equal(1, refreshes, 'only one refresh was triggered');
1355});
1356
1357QUnit.test('media-sequence updates are considered a playlist change', function(assert) {
1358 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
1359
1360 loader.load();
1361
1362 this.requests.pop().respond(200, null,
1363 '#EXTM3U\n' +
1364 '#EXT-X-MEDIA-SEQUENCE:0\n' +
1365 '#EXTINF:10,\n' +
1366 '0.ts\n');
1367 // trigger a refresh
1368 this.clock.tick(10 * 1000);
1369 this.requests.pop().respond(200, null,
1370 '#EXTM3U\n' +
1371 '#EXT-X-MEDIA-SEQUENCE:1\n' +
1372 '#EXTINF:10,\n' +
1373 '0.ts\n');
1374 // half the default target-duration
1375 this.clock.tick(5 * 1000);
1376
1377 assert.strictEqual(this.requests.length, 0, 'no request is sent');
1378});
1379
1380QUnit.test('emits an error if a media refresh fails', function(assert) {
1381 let errors = 0;
1382 let errorResponseText = 'custom error message';
1383 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
1384
1385 loader.load();
1386
1387 loader.on('error', function() {
1388 errors++;
1389 });
1390 this.requests.pop().respond(200, null,
1391 '#EXTM3U\n' +
1392 '#EXT-X-MEDIA-SEQUENCE:0\n' +
1393 '#EXTINF:10,\n' +
1394 '0.ts\n');
1395 // trigger a refresh
1396 this.clock.tick(10 * 1000);
1397 this.requests.pop().respond(500, null, errorResponseText);
1398
1399 assert.strictEqual(errors, 1, 'emitted an error');
1400 assert.strictEqual(loader.error.status, 500, 'captured the status code');
1401 assert.strictEqual(loader.error.responseText,
1402 errorResponseText,
1403 'captured the responseText');
1404});
1405
1406QUnit.test('switches media playlists when requested', function(assert) {
1407 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
1408
1409 loader.load();
1410
1411 this.requests.pop().respond(200, null,
1412 '#EXTM3U\n' +
1413 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
1414 'low.m3u8\n' +
1415 '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
1416 'high.m3u8\n');
1417 this.requests.pop().respond(200, null,
1418 '#EXTM3U\n' +
1419 '#EXT-X-MEDIA-SEQUENCE:0\n' +
1420 '#EXTINF:10,\n' +
1421 'low-0.ts\n');
1422
1423 loader.media(loader.master.playlists[1]);
1424 assert.strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state');
1425
1426 this.requests.pop().respond(200, null,
1427 '#EXTM3U\n' +
1428 '#EXT-X-MEDIA-SEQUENCE:0\n' +
1429 '#EXTINF:10,\n' +
1430 'high-0.ts\n');
1431 assert.strictEqual(loader.state, 'HAVE_METADATA', 'switched active media');
1432 assert.strictEqual(loader.media(),
1433 loader.master.playlists[1],
1434 'updated the active media');
1435});
1436
1437QUnit.test('can switch playlists immediately after the master is downloaded',
1438function(assert) {
1439 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
1440
1441 loader.load();
1442
1443 loader.on('loadedplaylist', function() {
1444 loader.media('high.m3u8');
1445 });
1446 this.requests.pop().respond(200, null,
1447 '#EXTM3U\n' +
1448 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
1449 'low.m3u8\n' +
1450 '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
1451 'high.m3u8\n');
1452 assert.equal(this.requests[0].url, urlTo('high.m3u8'), 'switched variants immediately');
1453});
1454
1455QUnit.test('can switch media playlists based on URI', function(assert) {
1456 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
1457
1458 loader.load();
1459
1460 this.requests.pop().respond(200, null,
1461 '#EXTM3U\n' +
1462 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
1463 'low.m3u8\n' +
1464 '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
1465 'high.m3u8\n');
1466 this.requests.pop().respond(200, null,
1467 '#EXTM3U\n' +
1468 '#EXT-X-MEDIA-SEQUENCE:0\n' +
1469 '#EXTINF:10,\n' +
1470 'low-0.ts\n');
1471
1472 loader.media('high.m3u8');
1473 assert.strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state');
1474
1475 this.requests.pop().respond(200, null,
1476 '#EXTM3U\n' +
1477 '#EXT-X-MEDIA-SEQUENCE:0\n' +
1478 '#EXTINF:10,\n' +
1479 'high-0.ts\n');
1480 assert.strictEqual(loader.state, 'HAVE_METADATA', 'switched active media');
1481 assert.strictEqual(loader.media(),
1482 loader.master.playlists[1],
1483 'updated the active media');
1484});
1485
1486QUnit.test('aborts in-flight playlist refreshes when switching', function(assert) {
1487 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
1488
1489 loader.load();
1490
1491 this.requests.pop().respond(200, null,
1492 '#EXTM3U\n' +
1493 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
1494 'low.m3u8\n' +
1495 '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
1496 'high.m3u8\n');
1497 this.requests.pop().respond(200, null,
1498 '#EXTM3U\n' +
1499 '#EXT-X-MEDIA-SEQUENCE:0\n' +
1500 '#EXTINF:10,\n' +
1501 'low-0.ts\n');
1502 this.clock.tick(10 * 1000);
1503 loader.media('high.m3u8');
1504 assert.strictEqual(this.requests[0].aborted, true, 'aborted refresh request');
1505 assert.ok(!this.requests[0].onreadystatechange,
1506 'onreadystatechange handlers should be removed on abort');
1507 assert.strictEqual(loader.state,
1508 'HAVE_METADATA',
1509 'the state is set accoring to the startingState');
1510});
1511
1512QUnit.test('switching to the active playlist is a no-op', function(assert) {
1513 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
1514
1515 loader.load();
1516
1517 this.requests.pop().respond(200, null,
1518 '#EXTM3U\n' +
1519 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
1520 'low.m3u8\n' +
1521 '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
1522 'high.m3u8\n');
1523 this.requests.pop().respond(200, null,
1524 '#EXTM3U\n' +
1525 '#EXT-X-MEDIA-SEQUENCE:0\n' +
1526 '#EXTINF:10,\n' +
1527 'low-0.ts\n' +
1528 '#EXT-X-ENDLIST\n');
1529 loader.media('low.m3u8');
1530
1531 assert.strictEqual(this.requests.length, 0, 'no requests are sent');
1532});
1533
1534QUnit.test('switching to the active live playlist is a no-op', function(assert) {
1535 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
1536
1537 loader.load();
1538
1539 this.requests.pop().respond(200, null,
1540 '#EXTM3U\n' +
1541 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
1542 'low.m3u8\n' +
1543 '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
1544 'high.m3u8\n');
1545 this.requests.pop().respond(200, null,
1546 '#EXTM3U\n' +
1547 '#EXT-X-MEDIA-SEQUENCE:0\n' +
1548 '#EXTINF:10,\n' +
1549 'low-0.ts\n');
1550 loader.media('low.m3u8');
1551
1552 assert.strictEqual(this.requests.length, 0, 'no requests are sent');
1553});
1554
1555QUnit.test('switches back to loaded playlists without re-requesting them',
1556function(assert) {
1557 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
1558
1559 loader.load();
1560
1561 this.requests.pop().respond(200, null,
1562 '#EXTM3U\n' +
1563 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
1564 'low.m3u8\n' +
1565 '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
1566 'high.m3u8\n');
1567 this.requests.pop().respond(200, null,
1568 '#EXTM3U\n' +
1569 '#EXT-X-MEDIA-SEQUENCE:0\n' +
1570 '#EXTINF:10,\n' +
1571 'low-0.ts\n' +
1572 '#EXT-X-ENDLIST\n');
1573 loader.media('high.m3u8');
1574 this.requests.pop().respond(200, null,
1575 '#EXTM3U\n' +
1576 '#EXT-X-MEDIA-SEQUENCE:0\n' +
1577 '#EXTINF:10,\n' +
1578 'high-0.ts\n' +
1579 '#EXT-X-ENDLIST\n');
1580 loader.media('low.m3u8');
1581
1582 assert.strictEqual(this.requests.length, 0, 'no outstanding requests');
1583 assert.strictEqual(loader.state, 'HAVE_METADATA', 'returned to loaded playlist');
1584});
1585
1586QUnit.test('aborts outstanding requests if switching back to an already loaded playlist',
1587function(assert) {
1588 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
1589
1590 loader.load();
1591
1592 this.requests.pop().respond(200, null,
1593 '#EXTM3U\n' +
1594 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
1595 'low.m3u8\n' +
1596 '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
1597 'high.m3u8\n');
1598 this.requests.pop().respond(200, null,
1599 '#EXTM3U\n' +
1600 '#EXT-X-MEDIA-SEQUENCE:0\n' +
1601 '#EXTINF:10,\n' +
1602 'low-0.ts\n' +
1603 '#EXT-X-ENDLIST\n');
1604 loader.media('high.m3u8');
1605 loader.media('low.m3u8');
1606
1607 assert.strictEqual(this.requests.length,
1608 1,
1609 'requested high playlist');
1610 assert.ok(this.requests[0].aborted,
1611 'aborted playlist request');
1612 assert.ok(!this.requests[0].onreadystatechange,
1613 'onreadystatechange handlers should be removed on abort');
1614 assert.strictEqual(loader.state,
1615 'HAVE_METADATA',
1616 'returned to loaded playlist');
1617 assert.strictEqual(loader.media(),
1618 loader.master.playlists[0],
1619 'switched to loaded playlist');
1620});
1621
1622QUnit.test('does not abort requests when the same playlist is re-requested',
1623function(assert) {
1624 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
1625
1626 loader.load();
1627
1628 this.requests.pop().respond(200, null,
1629 '#EXTM3U\n' +
1630 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
1631 'low.m3u8\n' +
1632 '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
1633 'high.m3u8\n');
1634 this.requests.pop().respond(200, null,
1635 '#EXTM3U\n' +
1636 '#EXT-X-MEDIA-SEQUENCE:0\n' +
1637 '#EXTINF:10,\n' +
1638 'low-0.ts\n' +
1639 '#EXT-X-ENDLIST\n');
1640 loader.media('high.m3u8');
1641 loader.media('high.m3u8');
1642
1643 assert.strictEqual(this.requests.length, 1, 'made only one request');
1644 assert.ok(!this.requests[0].aborted, 'request not aborted');
1645});
1646
1647QUnit.test('throws an error if a media switch is initiated too early', function(assert) {
1648 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
1649
1650 loader.load();
1651
1652 assert.throws(function() {
1653 loader.media('high.m3u8');
1654 }, 'threw an error from HAVE_NOTHING');
1655
1656 this.requests.pop().respond(200, null,
1657 '#EXTM3U\n' +
1658 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
1659 'low.m3u8\n' +
1660 '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
1661 'high.m3u8\n');
1662});
1663
1664QUnit.test('throws an error if a switch to an unrecognized playlist is requested',
1665function(assert) {
1666 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
1667
1668 loader.load();
1669
1670 this.requests.pop().respond(200, null,
1671 '#EXTM3U\n' +
1672 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
1673 'media.m3u8\n');
1674
1675 assert.throws(function() {
1676 loader.media('unrecognized.m3u8');
1677 }, 'throws an error');
1678});
1679
1680QUnit.test('dispose cancels the refresh timeout', function(assert) {
1681 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
1682
1683 loader.load();
1684
1685 this.requests.pop().respond(200, null,
1686 '#EXTM3U\n' +
1687 '#EXT-X-MEDIA-SEQUENCE:0\n' +
1688 '#EXTINF:10,\n' +
1689 '0.ts\n');
1690 loader.dispose();
1691 // a lot of time passes...
1692 this.clock.tick(15 * 1000);
1693
1694 assert.strictEqual(this.requests.length, 0, 'no refresh request was made');
1695});
1696
1697QUnit.test('dispose aborts pending refresh requests', function(assert) {
1698 let loader = new PlaylistLoader('live.m3u8', this.fakeHls);
1699
1700 loader.load();
1701
1702 this.requests.pop().respond(200, null,
1703 '#EXTM3U\n' +
1704 '#EXT-X-MEDIA-SEQUENCE:0\n' +
1705 '#EXTINF:10,\n' +
1706 '0.ts\n');
1707 this.clock.tick(10 * 1000);
1708
1709 loader.dispose();
1710 assert.ok(this.requests[0].aborted, 'refresh request aborted');
1711 assert.ok(!this.requests[0].onreadystatechange,
1712 'onreadystatechange handler should not exist after dispose called'
1713 );
1714});
1715
1716QUnit.test('errors if requests take longer than 45s', function(assert) {
1717 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
1718 let errors = 0;
1719
1720 loader.load();
1721
1722 loader.on('error', function() {
1723 errors++;
1724 });
1725 this.clock.tick(45 * 1000);
1726
1727 assert.strictEqual(errors, 1, 'fired one error');
1728 assert.strictEqual(loader.error.code, 2, 'fired a network error');
1729});
1730
1731QUnit.test('triggers an event when the active media changes', function(assert) {
1732 let loader = new PlaylistLoader('master.m3u8', this.fakeHls);
1733 let mediaChanges = 0;
1734 let mediaChangings = 0;
1735
1736 loader.load();
1737
1738 loader.on('mediachange', function() {
1739 mediaChanges++;
1740 });
1741 loader.on('mediachanging', function() {
1742 mediaChangings++;
1743 });
1744 this.requests.pop().respond(200, null,
1745 '#EXTM3U\n' +
1746 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
1747 'low.m3u8\n' +
1748 '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
1749 'high.m3u8\n');
1750 this.requests.shift().respond(200, null,
1751 '#EXTM3U\n' +
1752 '#EXT-X-MEDIA-SEQUENCE:0\n' +
1753 '#EXTINF:10,\n' +
1754 'low-0.ts\n' +
1755 '#EXT-X-ENDLIST\n');
1756 assert.strictEqual(mediaChangings, 0, 'initial selection is not a media changing');
1757 assert.strictEqual(mediaChanges, 0, 'initial selection is not a media change');
1758
1759 loader.media('high.m3u8');
1760 assert.strictEqual(mediaChangings, 1, 'mediachanging fires immediately');
1761 assert.strictEqual(mediaChanges, 0, 'mediachange does not fire immediately');
1762
1763 this.requests.shift().respond(200, null,
1764 '#EXTM3U\n' +
1765 '#EXT-X-MEDIA-SEQUENCE:0\n' +
1766 '#EXTINF:10,\n' +
1767 'high-0.ts\n' +
1768 '#EXT-X-ENDLIST\n');
1769 assert.strictEqual(mediaChangings, 1, 'still one mediachanging');
1770 assert.strictEqual(mediaChanges, 1, 'fired a mediachange');
1771
1772 // switch back to an already loaded playlist
1773 loader.media('low.m3u8');
1774 assert.strictEqual(mediaChangings, 2, 'mediachanging fires');
1775 assert.strictEqual(mediaChanges, 2, 'fired a mediachange');
1776
1777 // trigger a no-op switch
1778 loader.media('low.m3u8');
1779 assert.strictEqual(mediaChangings, 2, 'mediachanging ignored the no-op');
1780 assert.strictEqual(mediaChanges, 2, 'ignored a no-op media change');
1781});
1782
1783QUnit.test('does not misintrepret playlists missing newlines at the end',
1784function(assert) {
1785 let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
1786
1787 loader.load();
1788
1789 // no newline
1790 this.requests.shift().respond(200, null,
1791 '#EXTM3U\n' +
1792 '#EXT-X-MEDIA-SEQUENCE:0\n' +
1793 '#EXTINF:10,\n' +
1794 'low-0.ts\n' +
1795 '#EXT-X-ENDLIST');
1796 assert.ok(loader.media().endList, 'flushed the final line of input');
1797});