1 | import QUnit from 'qunit';
|
2 | import {
|
3 | default as PlaylistLoader,
|
4 | updateSegments,
|
5 | updateMaster,
|
6 | setupMediaPlaylists,
|
7 | resolveMediaGroupUris,
|
8 | refreshDelay
|
9 | } from '../src/playlist-loader';
|
10 | import xhrFactory from '../src/xhr';
|
11 | import { useFakeEnvironment } from './test-helpers';
|
12 | import window from 'global/window';
|
13 |
|
14 |
|
15 |
|
16 | const urlTo = function(path) {
|
17 | return window.location.href
|
18 | .split('/')
|
19 | .slice(0, -1)
|
20 | .concat([path])
|
21 | .join('/');
|
22 | };
|
23 |
|
24 | QUnit.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 |
|
38 | QUnit.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 |
|
113 | QUnit.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 |
|
122 | QUnit.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 |
|
153 | QUnit.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 |
|
201 | QUnit.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 |
|
265 | QUnit.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 |
|
336 | QUnit.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 |
|
409 | QUnit.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 |
|
459 | QUnit.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 |
|
546 | QUnit.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 |
|
558 | QUnit.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 |
|
594 | QUnit.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 |
|
618 | QUnit.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 |
|
636 | QUnit.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 |
|
651 | QUnit.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 |
|
660 | 'CLOSED-CAPTIONS': {
|
661 | cc1: {
|
662 | English: {}
|
663 | }
|
664 | },
|
665 | 'AUDIO': {
|
666 | low: {
|
667 |
|
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 |
|
719 | 'CLOSED-CAPTIONS': {
|
720 | cc1: {
|
721 | English: {}
|
722 | }
|
723 | },
|
724 | 'AUDIO': {
|
725 | low: {
|
726 |
|
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 |
|
777 | QUnit.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 |
|
794 | QUnit.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 |
|
803 | QUnit.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 |
|
811 | QUnit.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 |
|
822 | QUnit.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 |
|
839 | QUnit.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 |
|
861 | QUnit.test('jumps to HAVE_METADATA when initialized with a media playlist',
|
862 | function(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 |
|
885 | QUnit.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 |
|
898 | QUnit.test('playlist loader returns the correct amount of enabled playlists',
|
899 | function(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 |
|
916 | QUnit.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 |
|
949 | QUnit.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 |
|
964 | QUnit.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 |
|
985 | QUnit.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 |
|
1010 | QUnit.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 |
|
1038 | QUnit.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 |
|
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 |
|
1060 | this.requests.shift().respond(404);
|
1061 |
|
1062 | assert.equal(count, 1,
|
1063 | 'error triggered after playlist 404');
|
1064 | });
|
1065 |
|
1066 | QUnit.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 |
|
1095 | QUnit.test('jumps to HAVE_METADATA when initialized with a live media playlist',
|
1096 | function(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 |
|
1111 | QUnit.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 |
|
1149 | QUnit.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 |
|
1164 | QUnit.test('moves to HAVE_CURRENT_METADATA when refreshing the playlist',
|
1165 | function(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 |
|
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 |
|
1183 | QUnit.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 |
|
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 |
|
1201 | QUnit.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 |
|
1217 | this.clock.tick(4 * 1000);
|
1218 |
|
1219 | assert.equal(refreshes, 1, 'refreshed playlist after last segment duration');
|
1220 | });
|
1221 |
|
1222 | QUnit.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 |
|
1237 | QUnit.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 |
|
1260 | QUnit.test('halves the refresh timeout if a playlist is unchanged since the last reload',
|
1261 | function(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 |
|
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 |
|
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 |
|
1287 | QUnit.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 |
|
1303 | segment = loader.media().segments[1];
|
1304 | segment.minVideoPts = 14;
|
1305 | segment.maxAudioPts = 27;
|
1306 | segment.preciseDuration = 10.045;
|
1307 |
|
1308 |
|
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 |
|
1321 | QUnit.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 |
|
1328 | loader.on('mediaupdatetimeout', function() {
|
1329 | refreshes++;
|
1330 | });
|
1331 |
|
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 |
|
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 |
|
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 |
|
1352 | this.clock.tick(10 * 1000);
|
1353 |
|
1354 | assert.equal(1, refreshes, 'only one refresh was triggered');
|
1355 | });
|
1356 |
|
1357 | QUnit.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 |
|
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 |
|
1375 | this.clock.tick(5 * 1000);
|
1376 |
|
1377 | assert.strictEqual(this.requests.length, 0, 'no request is sent');
|
1378 | });
|
1379 |
|
1380 | QUnit.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 |
|
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 |
|
1406 | QUnit.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 |
|
1437 | QUnit.test('can switch playlists immediately after the master is downloaded',
|
1438 | function(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 |
|
1455 | QUnit.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 |
|
1486 | QUnit.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 |
|
1512 | QUnit.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 |
|
1534 | QUnit.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 |
|
1555 | QUnit.test('switches back to loaded playlists without re-requesting them',
|
1556 | function(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 |
|
1586 | QUnit.test('aborts outstanding requests if switching back to an already loaded playlist',
|
1587 | function(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 |
|
1622 | QUnit.test('does not abort requests when the same playlist is re-requested',
|
1623 | function(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 |
|
1647 | QUnit.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 |
|
1664 | QUnit.test('throws an error if a switch to an unrecognized playlist is requested',
|
1665 | function(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 |
|
1680 | QUnit.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 |
|
1692 | this.clock.tick(15 * 1000);
|
1693 |
|
1694 | assert.strictEqual(this.requests.length, 0, 'no refresh request was made');
|
1695 | });
|
1696 |
|
1697 | QUnit.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 |
|
1716 | QUnit.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 |
|
1731 | QUnit.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 |
|
1773 | loader.media('low.m3u8');
|
1774 | assert.strictEqual(mediaChangings, 2, 'mediachanging fires');
|
1775 | assert.strictEqual(mediaChanges, 2, 'fired a mediachange');
|
1776 |
|
1777 |
|
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 |
|
1783 | QUnit.test('does not misintrepret playlists missing newlines at the end',
|
1784 | function(assert) {
|
1785 | let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
|
1786 |
|
1787 | loader.load();
|
1788 |
|
1789 |
|
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 | });
|