1 | import Playlist from '../src/playlist';
|
2 | import PlaylistLoader from '../src/playlist-loader';
|
3 | import QUnit from 'qunit';
|
4 | import xhrFactory from '../src/xhr';
|
5 | import { useFakeEnvironment } from './test-helpers';
|
6 |
|
7 | QUnit.module('Playlist Duration');
|
8 |
|
9 | QUnit.test('total duration for live playlists is Infinity', function(assert) {
|
10 | let duration = Playlist.duration({
|
11 | segments: [{
|
12 | duration: 4,
|
13 | uri: '0.ts'
|
14 | }]
|
15 | });
|
16 |
|
17 | assert.equal(duration, Infinity, 'duration is infinity');
|
18 | });
|
19 |
|
20 | QUnit.module('Playlist Interval Duration');
|
21 |
|
22 | QUnit.test('accounts for non-zero starting VOD media sequences', function(assert) {
|
23 | let duration = Playlist.duration({
|
24 | mediaSequence: 10,
|
25 | endList: true,
|
26 | segments: [{
|
27 | duration: 10,
|
28 | uri: '0.ts'
|
29 | }, {
|
30 | duration: 10,
|
31 | uri: '1.ts'
|
32 | }, {
|
33 | duration: 10,
|
34 | uri: '2.ts'
|
35 | }, {
|
36 | duration: 10,
|
37 | uri: '3.ts'
|
38 | }]
|
39 | });
|
40 |
|
41 | assert.equal(duration, 4 * 10, 'includes only listed segments');
|
42 | });
|
43 |
|
44 | QUnit.test('uses timeline values when available', function(assert) {
|
45 | let duration = Playlist.duration({
|
46 | mediaSequence: 0,
|
47 | endList: true,
|
48 | segments: [{
|
49 | start: 0,
|
50 | uri: '0.ts'
|
51 | }, {
|
52 | duration: 10,
|
53 | end: 2 * 10 + 2,
|
54 | uri: '1.ts'
|
55 | }, {
|
56 | duration: 10,
|
57 | end: 3 * 10 + 2,
|
58 | uri: '2.ts'
|
59 | }, {
|
60 | duration: 10,
|
61 | end: 4 * 10 + 2,
|
62 | uri: '3.ts'
|
63 | }]
|
64 | }, 4);
|
65 |
|
66 | assert.equal(duration, 4 * 10 + 2, 'used timeline values');
|
67 | });
|
68 |
|
69 | QUnit.test('works when partial timeline information is available', function(assert) {
|
70 | let duration = Playlist.duration({
|
71 | mediaSequence: 0,
|
72 | endList: true,
|
73 | segments: [{
|
74 | start: 0,
|
75 | uri: '0.ts'
|
76 | }, {
|
77 | duration: 9,
|
78 | uri: '1.ts'
|
79 | }, {
|
80 | duration: 10,
|
81 | uri: '2.ts'
|
82 | }, {
|
83 | duration: 10,
|
84 | start: 30.007,
|
85 | end: 40.002,
|
86 | uri: '3.ts'
|
87 | }, {
|
88 | duration: 10,
|
89 | end: 50.0002,
|
90 | uri: '4.ts'
|
91 | }]
|
92 | }, 5);
|
93 |
|
94 | assert.equal(duration, 50.0002, 'calculated with mixed intervals');
|
95 | });
|
96 |
|
97 | QUnit.test('uses timeline values for the expired duration of live playlists',
|
98 | function(assert) {
|
99 | let playlist = {
|
100 | mediaSequence: 12,
|
101 | segments: [{
|
102 | duration: 10,
|
103 | end: 120.5,
|
104 | uri: '0.ts'
|
105 | }, {
|
106 | duration: 9,
|
107 | uri: '1.ts'
|
108 | }]
|
109 | };
|
110 | let duration;
|
111 |
|
112 | duration = Playlist.duration(playlist, playlist.mediaSequence);
|
113 | assert.equal(duration, 110.5, 'used segment end time');
|
114 | duration = Playlist.duration(playlist, playlist.mediaSequence + 1);
|
115 | assert.equal(duration, 120.5, 'used segment end time');
|
116 | duration = Playlist.duration(playlist, playlist.mediaSequence + 2);
|
117 | assert.equal(duration, 120.5 + 9, 'used segment end time');
|
118 | });
|
119 |
|
120 | QUnit.test('looks outside the queried interval for live playlist timeline values',
|
121 | function(assert) {
|
122 | let playlist = {
|
123 | mediaSequence: 12,
|
124 | segments: [{
|
125 | duration: 10,
|
126 | uri: '0.ts'
|
127 | }, {
|
128 | duration: 9,
|
129 | end: 120.5,
|
130 | uri: '1.ts'
|
131 | }]
|
132 | };
|
133 | let duration;
|
134 |
|
135 | duration = Playlist.duration(playlist, playlist.mediaSequence);
|
136 | assert.equal(duration, 120.5 - 9 - 10, 'used segment end time');
|
137 | });
|
138 |
|
139 | QUnit.test('ignores discontinuity sequences later than the end', function(assert) {
|
140 | let duration = Playlist.duration({
|
141 | mediaSequence: 0,
|
142 | discontinuityStarts: [1, 3],
|
143 | segments: [{
|
144 | duration: 10,
|
145 | uri: '0.ts'
|
146 | }, {
|
147 | discontinuity: true,
|
148 | duration: 9,
|
149 | uri: '1.ts'
|
150 | }, {
|
151 | duration: 10,
|
152 | uri: '2.ts'
|
153 | }, {
|
154 | discontinuity: true,
|
155 | duration: 10,
|
156 | uri: '3.ts'
|
157 | }]
|
158 | }, 2);
|
159 |
|
160 | assert.equal(duration, 19, 'excluded the later segments');
|
161 | });
|
162 |
|
163 | QUnit.test('handles trailing segments without timeline information', function(assert) {
|
164 | let duration;
|
165 | let playlist = {
|
166 | mediaSequence: 0,
|
167 | endList: true,
|
168 | segments: [{
|
169 | start: 0,
|
170 | end: 10.5,
|
171 | uri: '0.ts'
|
172 | }, {
|
173 | duration: 9,
|
174 | uri: '1.ts'
|
175 | }, {
|
176 | duration: 10,
|
177 | uri: '2.ts'
|
178 | }, {
|
179 | start: 29.45,
|
180 | end: 39.5,
|
181 | uri: '3.ts'
|
182 | }]
|
183 | };
|
184 |
|
185 | duration = Playlist.duration(playlist, 3);
|
186 | assert.equal(duration, 29.45, 'calculated duration');
|
187 |
|
188 | duration = Playlist.duration(playlist, 2);
|
189 | assert.equal(duration, 19.5, 'calculated duration');
|
190 | });
|
191 |
|
192 | QUnit.test('uses timeline intervals when segments have them', function(assert) {
|
193 | let duration;
|
194 | let playlist = {
|
195 | mediaSequence: 0,
|
196 | segments: [{
|
197 | start: 0,
|
198 | end: 10,
|
199 | uri: '0.ts'
|
200 | }, {
|
201 | duration: 9,
|
202 | uri: '1.ts'
|
203 | }, {
|
204 | start: 20.1,
|
205 | end: 30.1,
|
206 | duration: 10,
|
207 | uri: '2.ts'
|
208 | }]
|
209 | };
|
210 |
|
211 | duration = Playlist.duration(playlist, 2);
|
212 | assert.equal(duration, 20.1, 'used the timeline-based interval');
|
213 |
|
214 | duration = Playlist.duration(playlist, 3);
|
215 | assert.equal(duration, 30.1, 'used the timeline-based interval');
|
216 | });
|
217 |
|
218 | QUnit.test('counts the time between segments as part of the earlier segment\'s duration',
|
219 | function(assert) {
|
220 | let duration = Playlist.duration({
|
221 | mediaSequence: 0,
|
222 | endList: true,
|
223 | segments: [{
|
224 | start: 0,
|
225 | end: 10,
|
226 | uri: '0.ts'
|
227 | }, {
|
228 | start: 10.1,
|
229 | end: 20.1,
|
230 | duration: 10,
|
231 | uri: '1.ts'
|
232 | }]
|
233 | }, 1);
|
234 |
|
235 | assert.equal(duration, 10.1, 'included the segment gap');
|
236 | });
|
237 |
|
238 | QUnit.test('accounts for discontinuities', function(assert) {
|
239 | let duration = Playlist.duration({
|
240 | mediaSequence: 0,
|
241 | endList: true,
|
242 | discontinuityStarts: [1],
|
243 | segments: [{
|
244 | duration: 10,
|
245 | uri: '0.ts'
|
246 | }, {
|
247 | discontinuity: true,
|
248 | duration: 10,
|
249 | uri: '1.ts'
|
250 | }]
|
251 | }, 2);
|
252 |
|
253 | assert.equal(duration, 10 + 10, 'handles discontinuities');
|
254 | });
|
255 |
|
256 | QUnit.test('a non-positive length interval has zero duration', function(assert) {
|
257 | let playlist = {
|
258 | mediaSequence: 0,
|
259 | discontinuityStarts: [1],
|
260 | segments: [{
|
261 | duration: 10,
|
262 | uri: '0.ts'
|
263 | }, {
|
264 | discontinuity: true,
|
265 | duration: 10,
|
266 | uri: '1.ts'
|
267 | }]
|
268 | };
|
269 |
|
270 | assert.equal(Playlist.duration(playlist, 0), 0, 'zero-length duration is zero');
|
271 | assert.equal(Playlist.duration(playlist, 0, false), 0, 'zero-length duration is zero');
|
272 | assert.equal(Playlist.duration(playlist, -1), 0, 'negative length duration is zero');
|
273 | });
|
274 |
|
275 | QUnit.module('Playlist Seekable');
|
276 |
|
277 | QUnit.test('calculates seekable time ranges from available segments', function(assert) {
|
278 | let playlist = {
|
279 | mediaSequence: 0,
|
280 | segments: [{
|
281 | duration: 10,
|
282 | uri: '0.ts'
|
283 | }, {
|
284 | duration: 10,
|
285 | uri: '1.ts'
|
286 | }],
|
287 | endList: true
|
288 | };
|
289 | let seekable = Playlist.seekable(playlist);
|
290 |
|
291 | assert.equal(seekable.length, 1, 'there are seekable ranges');
|
292 | assert.equal(seekable.start(0), 0, 'starts at zero');
|
293 | assert.equal(seekable.end(0), Playlist.duration(playlist), 'ends at the duration');
|
294 | });
|
295 |
|
296 | QUnit.test('calculates playlist end time from the available segments', function(assert) {
|
297 | let playlistEnd = Playlist.playlistEnd({
|
298 | mediaSequence: 0,
|
299 | segments: [{
|
300 | duration: 10,
|
301 | uri: '0.ts'
|
302 | }, {
|
303 | duration: 10,
|
304 | uri: '1.ts'
|
305 | }],
|
306 | endList: true
|
307 | });
|
308 |
|
309 | assert.equal(playlistEnd, 20, 'paylist end at the duration');
|
310 | });
|
311 |
|
312 | QUnit.test('master playlists have empty seekable ranges and no playlist end',
|
313 | function(assert) {
|
314 | let playlist = {
|
315 | playlists: [{
|
316 | uri: 'low.m3u8'
|
317 | }, {
|
318 | uri: 'high.m3u8'
|
319 | }]
|
320 | };
|
321 | let seekable = Playlist.seekable(playlist);
|
322 | let playlistEnd = Playlist.playlistEnd(playlist);
|
323 |
|
324 | assert.equal(seekable.length, 0, 'no seekable ranges from a master playlist');
|
325 | assert.equal(playlistEnd, null, 'no playlist end from a master playlist');
|
326 | });
|
327 |
|
328 | QUnit.test('seekable end is three target durations from the actual end of live playlists',
|
329 | function(assert) {
|
330 | let seekable = Playlist.seekable({
|
331 | mediaSequence: 0,
|
332 | syncInfo: {
|
333 | time: 0,
|
334 | mediaSequence: 0
|
335 | },
|
336 | targetDuration: 10,
|
337 | segments: [{
|
338 | duration: 7,
|
339 | uri: '0.ts'
|
340 | }, {
|
341 | duration: 10,
|
342 | uri: '1.ts'
|
343 | }, {
|
344 | duration: 10,
|
345 | uri: '2.ts'
|
346 | }, {
|
347 | duration: 10,
|
348 | uri: '3.ts'
|
349 | }]
|
350 | });
|
351 |
|
352 | assert.equal(seekable.length, 1, 'there are seekable ranges');
|
353 | assert.equal(seekable.start(0), 0, 'starts at zero');
|
354 | assert.equal(seekable.end(0), 7, 'ends three target durations from the last segment');
|
355 | });
|
356 |
|
357 | QUnit.test('seekable end and playlist end account for non-standard target durations',
|
358 | function(assert) {
|
359 | const playlist = {
|
360 | targetDuration: 2,
|
361 | mediaSequence: 0,
|
362 | syncInfo: {
|
363 | time: 0,
|
364 | mediaSequence: 0
|
365 | },
|
366 | segments: [{
|
367 | duration: 2,
|
368 | uri: '0.ts'
|
369 | }, {
|
370 | duration: 2,
|
371 | uri: '1.ts'
|
372 | }, {
|
373 | duration: 1,
|
374 | uri: '2.ts'
|
375 | }, {
|
376 | duration: 2,
|
377 | uri: '3.ts'
|
378 | }, {
|
379 | duration: 2,
|
380 | uri: '4.ts'
|
381 | }]
|
382 | };
|
383 | let seekable = Playlist.seekable(playlist);
|
384 | let playlistEnd = Playlist.playlistEnd(playlist);
|
385 |
|
386 | assert.equal(seekable.start(0), 0, 'starts at the earliest available segment');
|
387 | assert.equal(seekable.end(0),
|
388 |
|
389 |
|
390 |
|
391 | 9 - (2 + 2 + 1 + 2),
|
392 | 'allows seeking no further than the start of the segment 2 target' +
|
393 | 'durations back from the beginning of the last segment');
|
394 | assert.equal(playlistEnd, 9, 'playlist end at the last segment');
|
395 | });
|
396 |
|
397 | QUnit.test('safeLiveIndex is correct for standard segment durations', function(assert) {
|
398 | const playlist = {
|
399 | targetDuration: 6,
|
400 | mediaSequence: 10,
|
401 | syncInfo: {
|
402 | time: 0,
|
403 | mediaSequence: 10
|
404 | },
|
405 | segments: [
|
406 | {
|
407 | duration: 6
|
408 | },
|
409 | {
|
410 | duration: 6
|
411 | },
|
412 | {
|
413 | duration: 6
|
414 | },
|
415 | {
|
416 | duration: 6
|
417 | },
|
418 | {
|
419 | duration: 6
|
420 | },
|
421 | {
|
422 | duration: 6
|
423 | }
|
424 | ]
|
425 | };
|
426 |
|
427 | assert.equal(Playlist.safeLiveIndex(playlist), 3,
|
428 | 'correct media index for standard durations');
|
429 | });
|
430 |
|
431 | QUnit.test('safeLiveIndex is correct for variable segment durations', function(assert) {
|
432 | const playlist = {
|
433 | targetDuration: 6,
|
434 | mediaSequence: 10,
|
435 | syncInfo: {
|
436 | time: 0,
|
437 | mediaSequence: 10
|
438 | },
|
439 | segments: [
|
440 | {
|
441 | duration: 6
|
442 | },
|
443 | {
|
444 | duration: 4
|
445 | },
|
446 | {
|
447 | duration: 5
|
448 | },
|
449 | {
|
450 |
|
451 | duration: 6
|
452 | },
|
453 | {
|
454 | duration: 3
|
455 | },
|
456 | {
|
457 | duration: 4
|
458 | },
|
459 | {
|
460 | duration: 3
|
461 | }
|
462 | ]
|
463 | };
|
464 |
|
465 |
|
466 | assert.equal(Playlist.safeLiveIndex(playlist), 3,
|
467 | 'correct media index for variable segment durations');
|
468 | });
|
469 |
|
470 | QUnit.test('safeLiveIndex is 0 when no safe live point', function(assert) {
|
471 | const playlist = {
|
472 | targetDuration: 6,
|
473 | mediaSequence: 10,
|
474 | syncInfo: {
|
475 | time: 0,
|
476 | mediaSequence: 10
|
477 | },
|
478 | segments: [
|
479 | {
|
480 | duration: 6
|
481 | },
|
482 | {
|
483 | duration: 3
|
484 | },
|
485 | {
|
486 | duration: 3
|
487 | }
|
488 | ]
|
489 | };
|
490 |
|
491 | assert.equal(Playlist.safeLiveIndex(playlist), 0,
|
492 | 'returns media index 0 when playlist has no safe live point');
|
493 | });
|
494 |
|
495 | QUnit.test(
|
496 | 'seekable end and playlist end account for non-zero starting VOD media sequence',
|
497 | function(assert) {
|
498 | let playlist = {
|
499 | targetDuration: 2,
|
500 | mediaSequence: 5,
|
501 | endList: true,
|
502 | segments: [{
|
503 | duration: 2,
|
504 | uri: '0.ts'
|
505 | }, {
|
506 | duration: 2,
|
507 | uri: '1.ts'
|
508 | }, {
|
509 | duration: 1,
|
510 | uri: '2.ts'
|
511 | }, {
|
512 | duration: 2,
|
513 | uri: '3.ts'
|
514 | }, {
|
515 | duration: 2,
|
516 | uri: '4.ts'
|
517 | }]
|
518 | };
|
519 | let seekable = Playlist.seekable(playlist);
|
520 | let playlistEnd = Playlist.playlistEnd(playlist);
|
521 |
|
522 | assert.equal(seekable.start(0), 0, 'starts at the earliest available segment');
|
523 | assert.equal(seekable.end(0), 9, 'seekable end is same as duration');
|
524 | assert.equal(playlistEnd, 9, 'playlist end at the last segment');
|
525 | });
|
526 |
|
527 | QUnit.test('playlist with no sync points has empty seekable range and empty playlist end',
|
528 | function(assert) {
|
529 | let playlist = {
|
530 | targetDuration: 10,
|
531 | mediaSequence: 0,
|
532 | segments: [{
|
533 | duration: 7,
|
534 | uri: '0.ts'
|
535 | }, {
|
536 | duration: 10,
|
537 | uri: '1.ts'
|
538 | }, {
|
539 | duration: 10,
|
540 | uri: '2.ts'
|
541 | }, {
|
542 | duration: 10,
|
543 | uri: '3.ts'
|
544 | }]
|
545 | };
|
546 |
|
547 |
|
548 |
|
549 |
|
550 | let seekable = Playlist.seekable(playlist, null);
|
551 | let playlistEnd = Playlist.playlistEnd(playlist, null);
|
552 |
|
553 | assert.equal(seekable.length, 0, 'no seekable range for playlist with no sync points');
|
554 | assert.equal(playlistEnd, null, 'no playlist end for playlist with no sync points');
|
555 | });
|
556 |
|
557 | QUnit.test('seekable and playlistEnd use available sync points for calculating',
|
558 | function(assert) {
|
559 | let playlist = {
|
560 | targetDuration: 10,
|
561 | mediaSequence: 100,
|
562 | syncInfo: {
|
563 | time: 50,
|
564 | mediaSequence: 95
|
565 | },
|
566 | segments: [
|
567 | {
|
568 | duration: 10,
|
569 | uri: '0.ts'
|
570 | },
|
571 | {
|
572 | duration: 10,
|
573 | uri: '1.ts'
|
574 | },
|
575 | {
|
576 | duration: 10,
|
577 | uri: '2.ts'
|
578 | },
|
579 | {
|
580 | duration: 10,
|
581 | uri: '3.ts'
|
582 | },
|
583 | {
|
584 | duration: 10,
|
585 | uri: '4.ts'
|
586 | }
|
587 | ]
|
588 | };
|
589 |
|
590 |
|
591 | let seekable = Playlist.seekable(playlist, 100);
|
592 | let playlistEnd = Playlist.playlistEnd(playlist, 100);
|
593 |
|
594 | assert.ok(seekable.length, 'seekable range calculated');
|
595 | assert.equal(seekable.start(0),
|
596 | 100,
|
597 | 'estimated start time based on expired sync point');
|
598 | assert.equal(seekable.end(0),
|
599 | 120,
|
600 | 'allows seeking no further than three segments from the end');
|
601 | assert.equal(playlistEnd, 150, 'playlist end at the last segment end');
|
602 |
|
603 | playlist = {
|
604 | targetDuration: 10,
|
605 | mediaSequence: 100,
|
606 | segments: [
|
607 | {
|
608 | duration: 10,
|
609 | uri: '0.ts'
|
610 | },
|
611 | {
|
612 | duration: 10,
|
613 | uri: '1.ts',
|
614 | start: 108.5,
|
615 | end: 118.4
|
616 | },
|
617 | {
|
618 | duration: 10,
|
619 | uri: '2.ts'
|
620 | },
|
621 | {
|
622 | duration: 10,
|
623 | uri: '3.ts'
|
624 | },
|
625 | {
|
626 | duration: 10,
|
627 | uri: '4.ts'
|
628 | }
|
629 | ]
|
630 | };
|
631 |
|
632 |
|
633 | seekable = Playlist.seekable(playlist, 98.5);
|
634 | playlistEnd = Playlist.playlistEnd(playlist, 98.5);
|
635 |
|
636 | assert.ok(seekable.length, 'seekable range calculated');
|
637 | assert.equal(seekable.start(0), 98.5, 'estimated start time using segmentSync');
|
638 | assert.equal(seekable.end(0),
|
639 | 118.4,
|
640 | 'allows seeking no further than three segments from the end');
|
641 | assert.equal(playlistEnd, 148.4, 'playlist end at the last segment end');
|
642 |
|
643 | playlist = {
|
644 | targetDuration: 10,
|
645 | mediaSequence: 100,
|
646 | syncInfo: {
|
647 | time: 50,
|
648 | mediaSequence: 95
|
649 | },
|
650 | segments: [
|
651 | {
|
652 | duration: 10,
|
653 | uri: '0.ts'
|
654 | },
|
655 | {
|
656 | duration: 10,
|
657 | uri: '1.ts',
|
658 | start: 108.5,
|
659 | end: 118.5
|
660 | },
|
661 | {
|
662 | duration: 10,
|
663 | uri: '2.ts'
|
664 | },
|
665 | {
|
666 | duration: 10,
|
667 | uri: '3.ts'
|
668 | },
|
669 | {
|
670 | duration: 10,
|
671 | uri: '4.ts'
|
672 | }
|
673 | ]
|
674 | };
|
675 |
|
676 |
|
677 | seekable = Playlist.seekable(playlist, 98.5);
|
678 | playlistEnd = Playlist.playlistEnd(playlist, 98.5);
|
679 |
|
680 | assert.ok(seekable.length, 'seekable range calculated');
|
681 | assert.equal(
|
682 | seekable.start(0),
|
683 | 98.5,
|
684 | 'estimated start time using nearest sync point (segmentSync in this case)');
|
685 | assert.equal(seekable.end(0),
|
686 | 118.5,
|
687 | 'allows seeking no further than three segments from the end');
|
688 | assert.equal(playlistEnd, 148.5, 'playlist end at the last segment end');
|
689 |
|
690 | playlist = {
|
691 | targetDuration: 10,
|
692 | mediaSequence: 100,
|
693 | syncInfo: {
|
694 | time: 90.8,
|
695 | mediaSequence: 99
|
696 | },
|
697 | segments: [
|
698 | {
|
699 | duration: 10,
|
700 | uri: '0.ts'
|
701 | },
|
702 | {
|
703 | duration: 10,
|
704 | uri: '1.ts'
|
705 | },
|
706 | {
|
707 | duration: 10,
|
708 | uri: '2.ts',
|
709 | start: 118.5,
|
710 | end: 128.5
|
711 | },
|
712 | {
|
713 | duration: 10,
|
714 | uri: '3.ts'
|
715 | },
|
716 | {
|
717 | duration: 10,
|
718 | uri: '4.ts'
|
719 | }
|
720 | ]
|
721 | };
|
722 |
|
723 |
|
724 | seekable = Playlist.seekable(playlist, 100.8);
|
725 | playlistEnd = Playlist.playlistEnd(playlist, 100.8);
|
726 |
|
727 | assert.ok(seekable.length, 'seekable range calculated');
|
728 | assert.equal(
|
729 | seekable.start(0),
|
730 | 100.8,
|
731 | 'estimated start time using nearest sync point (expiredSync in this case)');
|
732 | assert.equal(seekable.end(0),
|
733 | 118.5,
|
734 | 'allows seeking no further than three segments from the end');
|
735 | assert.equal(playlistEnd, 148.5, 'playlist end at the last segment end');
|
736 | });
|
737 |
|
738 | QUnit.module('Playlist hasAttribute');
|
739 |
|
740 | QUnit.test('correctly checks for existence of playlist attribute', function(assert) {
|
741 | const playlist = {};
|
742 |
|
743 | assert.notOk(Playlist.hasAttribute('BANDWIDTH', playlist),
|
744 | 'false for playlist with no attributes property');
|
745 |
|
746 | playlist.attributes = {};
|
747 |
|
748 | assert.notOk(Playlist.hasAttribute('BANDWIDTH', playlist),
|
749 | 'false for playlist with without specified attribute');
|
750 |
|
751 | playlist.attributes.BANDWIDTH = 100;
|
752 |
|
753 | assert.ok(Playlist.hasAttribute('BANDWIDTH', playlist),
|
754 | 'true for playlist with specified attribute');
|
755 | });
|
756 |
|
757 | QUnit.module('Playlist estimateSegmentRequestTime');
|
758 |
|
759 | QUnit.test('estimates segment request time based on bandwidth', function(assert) {
|
760 | let segmentDuration = 10;
|
761 | let bandwidth = 100;
|
762 | let playlist = { attributes: { } };
|
763 | let bytesReceived = 0;
|
764 |
|
765 | let estimate = Playlist.estimateSegmentRequestTime(segmentDuration,
|
766 | bandwidth,
|
767 | playlist,
|
768 | bytesReceived);
|
769 |
|
770 | assert.ok(isNaN(estimate), 'returns NaN when no BANDWIDTH information on playlist');
|
771 |
|
772 | playlist.attributes.BANDWIDTH = 100;
|
773 |
|
774 | estimate = Playlist.estimateSegmentRequestTime(segmentDuration,
|
775 | bandwidth,
|
776 | playlist,
|
777 | bytesReceived);
|
778 |
|
779 | assert.equal(estimate, 10, 'calculated estimated download time');
|
780 |
|
781 | bytesReceived = 25;
|
782 |
|
783 | estimate = Playlist.estimateSegmentRequestTime(segmentDuration,
|
784 | bandwidth,
|
785 | playlist,
|
786 | bytesReceived);
|
787 |
|
788 | assert.equal(estimate, 8, 'takes into account bytes already received from download');
|
789 | });
|
790 |
|
791 | QUnit.module('Playlist enabled states', {
|
792 | beforeEach(assert) {
|
793 | this.env = useFakeEnvironment(assert);
|
794 | this.clock = this.env.clock;
|
795 | },
|
796 | afterEach() {
|
797 | this.env.restore();
|
798 | }
|
799 | });
|
800 |
|
801 | QUnit.test('determines if a playlist is incompatible', function(assert) {
|
802 |
|
803 |
|
804 |
|
805 | assert.notOk(Playlist.isIncompatible({}),
|
806 | 'playlist not incompatible if no excludeUntil');
|
807 |
|
808 | assert.notOk(Playlist.isIncompatible({ excludeUntil: 1 }),
|
809 | 'playlist not incompatible if expired blacklist');
|
810 |
|
811 | assert.notOk(Playlist.isIncompatible({ excludeUntil: Date.now() + 9999 }),
|
812 | 'playlist not incompatible if temporarily blacklisted');
|
813 |
|
814 | assert.ok(Playlist.isIncompatible({ excludeUntil: Infinity }),
|
815 | 'playlist is incompatible if excludeUntil is Infinity');
|
816 | });
|
817 |
|
818 | QUnit.test('determines if a playlist is blacklisted', function(assert) {
|
819 | assert.notOk(Playlist.isBlacklisted({}),
|
820 | 'playlist not blacklisted if no excludeUntil');
|
821 |
|
822 | assert.notOk(Playlist.isBlacklisted({ excludeUntil: Date.now() - 1 }),
|
823 | 'playlist not blacklisted if expired excludeUntil');
|
824 |
|
825 | assert.ok(Playlist.isBlacklisted({ excludeUntil: Date.now() + 9999 }),
|
826 | 'playlist is blacklisted');
|
827 |
|
828 | assert.ok(Playlist.isBlacklisted({ excludeUntil: Infinity }),
|
829 | 'playlist is blacklisted if excludeUntil is Infinity');
|
830 | });
|
831 |
|
832 | QUnit.test('determines if a playlist is disabled', function(assert) {
|
833 | assert.notOk(Playlist.isDisabled({}), 'playlist not disabled');
|
834 |
|
835 | assert.ok(Playlist.isDisabled({ disabled: true }), 'playlist is disabled');
|
836 | });
|
837 |
|
838 | QUnit.test('playlists with no or expired blacklist are enabled', function(assert) {
|
839 |
|
840 | assert.ok(Playlist.isEnabled({}), 'playlist with no blacklist is enabled');
|
841 | assert.ok(Playlist.isEnabled({ excludeUntil: Date.now() - 1 }),
|
842 | 'playlist with expired blacklist is enabled');
|
843 | });
|
844 |
|
845 | QUnit.test('blacklisted playlists are not enabled', function(assert) {
|
846 |
|
847 | assert.notOk(Playlist.isEnabled({ excludeUntil: Date.now() + 9999 }),
|
848 | 'playlist with temporary blacklist is not enabled');
|
849 | assert.notOk(Playlist.isEnabled({ excludeUntil: Infinity }),
|
850 | 'playlist with permanent is not enabled');
|
851 | });
|
852 |
|
853 | QUnit.test('manually disabled playlists are not enabled regardless of blacklist state',
|
854 | function(assert) {
|
855 |
|
856 | assert.notOk(Playlist.isEnabled({ disabled: true }),
|
857 | 'disabled playlist with no blacklist is not enabled');
|
858 | assert.notOk(Playlist.isEnabled({ disabled: true, excludeUntil: Date.now() - 1 }),
|
859 | 'disabled playlist with expired blacklist is not enabled');
|
860 | assert.notOk(Playlist.isEnabled({ disabled: true, excludeUntil: Date.now() + 9999 }),
|
861 | 'disabled playlist with temporary blacklist is not enabled');
|
862 | assert.notOk(Playlist.isEnabled({ disabled: true, excludeUntil: Infinity }),
|
863 | 'disabled playlist with permanent blacklist is not enabled');
|
864 | });
|
865 |
|
866 | QUnit.module('Playlist isAes and isFmp4', {
|
867 | beforeEach(assert) {
|
868 | this.env = useFakeEnvironment(assert);
|
869 | this.clock = this.env.clock;
|
870 | this.requests = this.env.requests;
|
871 | this.fakeHls = {
|
872 | xhr: xhrFactory()
|
873 | };
|
874 | },
|
875 | afterEach() {
|
876 | this.env.restore();
|
877 | }
|
878 | });
|
879 |
|
880 | QUnit.test('determine if playlist is an AES encrypted HLS stream', function(assert) {
|
881 | let media;
|
882 | let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
|
883 |
|
884 | loader.load();
|
885 | this.requests.shift().respond(
|
886 | 200,
|
887 | null,
|
888 | '#EXTM3U\n' +
|
889 | '#EXT-X-TARGETDURATION:15\n' +
|
890 | '#EXT-X-KEY:METHOD=AES-128,URI="http://example.com/keys/key.php"\n' +
|
891 | '#EXTINF:2.833,\n' +
|
892 | 'http://example.com/000001.ts\n' +
|
893 | '#EXT-X-ENDLIST\n'
|
894 | );
|
895 |
|
896 | media = loader.media();
|
897 |
|
898 | assert.ok(Playlist.isAes(media), 'media is an AES encrypted HLS stream');
|
899 | });
|
900 |
|
901 | QUnit.test('determine if playlist contains an fmp4 segment', function(assert) {
|
902 | let media;
|
903 | let loader = new PlaylistLoader('video/fmp4.m3u8', this.fakeHls);
|
904 |
|
905 | loader.load();
|
906 | this.requests.shift().respond(200, null,
|
907 | '#EXTM3U\n' +
|
908 | '#EXT-X-MAP:URI="main.mp4",BYTERANGE="720@0"\n' +
|
909 | '#EXTINF:10,\n' +
|
910 | '0.mp4\n' +
|
911 | '#EXT-X-ENDLIST\n');
|
912 |
|
913 | media = loader.media();
|
914 |
|
915 | assert.ok(Playlist.isFmp4(media), 'media contains fmp4 segment');
|
916 | });
|
917 |
|
918 | QUnit.module('Playlist Media Index For Time', {
|
919 | beforeEach(assert) {
|
920 | this.env = useFakeEnvironment(assert);
|
921 | this.clock = this.env.clock;
|
922 | this.requests = this.env.requests;
|
923 | this.fakeHls = {
|
924 | xhr: xhrFactory()
|
925 | };
|
926 | },
|
927 | afterEach() {
|
928 | this.env.restore();
|
929 | }
|
930 | });
|
931 |
|
932 | QUnit.test('can get media index by playback position for non-live videos',
|
933 | function(assert) {
|
934 | let media;
|
935 | let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
|
936 |
|
937 | loader.load();
|
938 |
|
939 | this.requests.shift().respond(200, null,
|
940 | '#EXTM3U\n' +
|
941 | '#EXT-X-MEDIA-SEQUENCE:0\n' +
|
942 | '#EXTINF:4,\n' +
|
943 | '0.ts\n' +
|
944 | '#EXTINF:5,\n' +
|
945 | '1.ts\n' +
|
946 | '#EXTINF:6,\n' +
|
947 | '2.ts\n' +
|
948 | '#EXT-X-ENDLIST\n'
|
949 | );
|
950 |
|
951 | media = loader.media();
|
952 |
|
953 | assert.equal(Playlist.getMediaInfoForTime(media, -1, 0, 0).mediaIndex, 0,
|
954 | 'the index is never less than zero');
|
955 | assert.equal(Playlist.getMediaInfoForTime(media, 0, 0, 0).mediaIndex, 0,
|
956 | 'time zero is index zero');
|
957 | assert.equal(Playlist.getMediaInfoForTime(media, 3, 0, 0).mediaIndex, 0,
|
958 | 'time three is index zero');
|
959 | assert.equal(Playlist.getMediaInfoForTime(media, 10, 0, 0).mediaIndex, 2,
|
960 | 'time 10 is index 2');
|
961 | assert.equal(Playlist.getMediaInfoForTime(media, 22, 0, 0).mediaIndex, 2,
|
962 | 'time greater than the length is index 2');
|
963 | });
|
964 |
|
965 | QUnit.test('returns the lower index when calculating for a segment boundary',
|
966 | function(assert) {
|
967 | let media;
|
968 | let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
|
969 |
|
970 | loader.load();
|
971 |
|
972 | this.requests.shift().respond(200, null,
|
973 | '#EXTM3U\n' +
|
974 | '#EXT-X-MEDIA-SEQUENCE:0\n' +
|
975 | '#EXTINF:4,\n' +
|
976 | '0.ts\n' +
|
977 | '#EXTINF:5,\n' +
|
978 | '1.ts\n' +
|
979 | '#EXT-X-ENDLIST\n'
|
980 | );
|
981 |
|
982 | media = loader.media();
|
983 |
|
984 | assert.equal(Playlist.getMediaInfoForTime(media, 4, 0, 0).mediaIndex, 0,
|
985 | 'rounds down exact matches');
|
986 | assert.equal(Playlist.getMediaInfoForTime(media, 3.7, 0, 0).mediaIndex, 0,
|
987 | 'rounds down');
|
988 | assert.equal(Playlist.getMediaInfoForTime(media, 4.5, 0, 0).mediaIndex, 1,
|
989 | 'rounds up at 0.5');
|
990 | });
|
991 |
|
992 | QUnit.test(
|
993 | 'accounts for non-zero starting segment time when calculating media index',
|
994 | function(assert) {
|
995 | let media;
|
996 | let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
|
997 |
|
998 | loader.load();
|
999 |
|
1000 | this.requests.shift().respond(200, null,
|
1001 | '#EXTM3U\n' +
|
1002 | '#EXT-X-MEDIA-SEQUENCE:1001\n' +
|
1003 | '#EXTINF:4,\n' +
|
1004 | '1001.ts\n' +
|
1005 | '#EXTINF:5,\n' +
|
1006 | '1002.ts\n'
|
1007 | );
|
1008 |
|
1009 | media = loader.media();
|
1010 |
|
1011 | assert.equal(
|
1012 | Playlist.getMediaInfoForTime(media, 45, 0, 150).mediaIndex,
|
1013 | 0,
|
1014 | 'expired content returns 0 for earliest segment available'
|
1015 | );
|
1016 | assert.equal(
|
1017 | Playlist.getMediaInfoForTime(media, 75, 0, 150).mediaIndex,
|
1018 | 0,
|
1019 | 'expired content returns 0 for earliest segment available'
|
1020 | );
|
1021 | assert.equal(
|
1022 | Playlist.getMediaInfoForTime(media, 0, 0, 150).mediaIndex,
|
1023 | 0,
|
1024 | 'time of 0 with no expired time returns first segment'
|
1025 | );
|
1026 | assert.equal(
|
1027 | Playlist.getMediaInfoForTime(media, 50 + 100, 0, 150).mediaIndex,
|
1028 | 0,
|
1029 | 'calculates the earliest available position'
|
1030 | );
|
1031 | assert.equal(
|
1032 | Playlist.getMediaInfoForTime(media, 50 + 100 + 2, 0, 150).mediaIndex,
|
1033 | 0,
|
1034 | 'calculates within the first segment'
|
1035 | );
|
1036 | assert.equal(
|
1037 | Playlist.getMediaInfoForTime(media, 50 + 100 + 2, 0, 150).mediaIndex,
|
1038 | 0,
|
1039 | 'calculates within the first segment'
|
1040 | );
|
1041 | assert.equal(
|
1042 | Playlist.getMediaInfoForTime(media, 50 + 100 + 4, 0, 150).mediaIndex,
|
1043 | 0,
|
1044 | 'calculates earlier segment on exact boundary match'
|
1045 | );
|
1046 | assert.equal(
|
1047 | Playlist.getMediaInfoForTime(media, 50 + 100 + 4.5, 0, 150).mediaIndex,
|
1048 | 1,
|
1049 | 'calculates within the second segment'
|
1050 | );
|
1051 | assert.equal(
|
1052 | Playlist.getMediaInfoForTime(media, 50 + 100 + 6, 0, 150).mediaIndex,
|
1053 | 1,
|
1054 | 'calculates within the second segment'
|
1055 | );
|
1056 |
|
1057 | assert.equal(
|
1058 | Playlist.getMediaInfoForTime(media, 159, 0, 150).mediaIndex,
|
1059 | 1,
|
1060 | 'returns last segment when time is equal to end of last segment'
|
1061 | );
|
1062 | assert.equal(
|
1063 | Playlist.getMediaInfoForTime(media, 160, 0, 150).mediaIndex,
|
1064 | 1,
|
1065 | 'returns last segment when time is past end of last segment'
|
1066 | );
|
1067 | });
|