@clappr/hlsjs-playback 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hlsjs-playback.esm.js +3 -4
- package/dist/hlsjs-playback.esm.js.map +1 -1
- package/dist/hlsjs-playback.external.js +3 -4
- package/dist/hlsjs-playback.external.js.map +1 -1
- package/dist/hlsjs-playback.external.min.js +1 -1
- package/dist/hlsjs-playback.external.min.js.map +1 -1
- package/dist/hlsjs-playback.js +3 -4
- package/dist/hlsjs-playback.js.map +1 -1
- package/dist/hlsjs-playback.min.js +1 -1
- package/dist/hlsjs-playback.min.js.map +1 -1
- package/package.json +5 -4
- package/src/dist.smoke.test.js +27 -33
- package/src/hls.js +2 -3
- package/src/hls.test.js +780 -12
package/src/hls.test.js
CHANGED
|
@@ -1,32 +1,80 @@
|
|
|
1
|
-
import { Core, Events } from '@clappr/core'
|
|
1
|
+
import { Core, Events, Log, Playback } from '@clappr/core'
|
|
2
2
|
import HlsjsPlayback from './hls.js'
|
|
3
3
|
import HLSJS from 'hls.js'
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const DEFAULT_SRC = 'http://clappr.io/video.m3u8'
|
|
6
|
+
|
|
7
|
+
const createPlayback = (options = {}, playerError) =>
|
|
8
|
+
new HlsjsPlayback({ src: DEFAULT_SRC, mute: true, ...options }, null, playerError)
|
|
9
|
+
|
|
10
|
+
const makeLevelData = ({
|
|
11
|
+
targetduration = 6,
|
|
12
|
+
type = null,
|
|
13
|
+
totalduration = 120,
|
|
14
|
+
fragmentStart = 0,
|
|
15
|
+
rawProgramDateTime = 1556663040000,
|
|
16
|
+
fragments
|
|
17
|
+
} = {}) => ({
|
|
18
|
+
details: {
|
|
19
|
+
targetduration,
|
|
20
|
+
type,
|
|
21
|
+
totalduration,
|
|
22
|
+
fragments: fragments ?? [{ start: fragmentStart, rawProgramDateTime }]
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const stubHls = playback => {
|
|
27
|
+
playback._hls = {
|
|
28
|
+
startLoad: jest.fn(),
|
|
29
|
+
recoverMediaError: jest.fn(),
|
|
30
|
+
swapAudioCodec: jest.fn(),
|
|
31
|
+
destroy: jest.fn()
|
|
32
|
+
}
|
|
33
|
+
return playback._hls
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const freezeNow = (playback, initialMs = 1000000) => {
|
|
37
|
+
let current = initialMs
|
|
38
|
+
jest.spyOn(playback, '_now', 'get').mockImplementation(() => current)
|
|
39
|
+
return {
|
|
40
|
+
advance(ms) {
|
|
41
|
+
current += ms
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
6
45
|
|
|
7
46
|
describe('HlsjsPlayback', () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
jest.spyOn(window.HTMLMediaElement.prototype, 'play').mockImplementation(() => {})
|
|
49
|
+
jest.spyOn(window.HTMLMediaElement.prototype, 'load').mockImplementation(() => {})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
jest.restoreAllMocks()
|
|
54
|
+
})
|
|
55
|
+
|
|
8
56
|
test('have a getter called defaultOptions', () => {
|
|
57
|
+
const playback = createPlayback()
|
|
9
58
|
expect(
|
|
10
|
-
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(
|
|
11
|
-
.get
|
|
59
|
+
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(playback), 'defaultOptions').get
|
|
12
60
|
).toBeTruthy()
|
|
13
61
|
})
|
|
14
62
|
|
|
15
63
|
test('defaultOptions getter returns all the default options values into one object', () => {
|
|
16
|
-
|
|
64
|
+
const playback = createPlayback()
|
|
65
|
+
expect(playback.defaultOptions).toEqual({ preload: true })
|
|
17
66
|
})
|
|
18
67
|
|
|
19
68
|
test('have a getter called customListeners', () => {
|
|
69
|
+
const playback = createPlayback()
|
|
20
70
|
expect(
|
|
21
|
-
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(
|
|
22
|
-
.get
|
|
71
|
+
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(playback), 'customListeners').get
|
|
23
72
|
).toBeTruthy()
|
|
24
73
|
})
|
|
25
74
|
|
|
26
75
|
test('customListeners getter returns all configured custom listeners for each hls.js event', () => {
|
|
27
76
|
const cb = () => {}
|
|
28
|
-
const playback =
|
|
29
|
-
src: 'http://clappr.io/foo.m3u8',
|
|
77
|
+
const playback = createPlayback({
|
|
30
78
|
hlsPlayback: {
|
|
31
79
|
customListeners: [{ eventName: 'hlsMediaAttaching', callback: cb }]
|
|
32
80
|
}
|
|
@@ -80,7 +128,6 @@ describe('HlsjsPlayback', () => {
|
|
|
80
128
|
})
|
|
81
129
|
|
|
82
130
|
test('should trigger a playback error if source load failed', () => {
|
|
83
|
-
jest.spyOn(window.HTMLMediaElement.prototype, 'play').mockImplementation(() => {})
|
|
84
131
|
let resolveFn
|
|
85
132
|
const promise = new Promise(resolve => {
|
|
86
133
|
resolveFn = resolve
|
|
@@ -113,8 +160,7 @@ describe('HlsjsPlayback', () => {
|
|
|
113
160
|
})
|
|
114
161
|
|
|
115
162
|
test('levels supports specifying the level', () => {
|
|
116
|
-
const
|
|
117
|
-
const playback = new HlsjsPlayback(options)
|
|
163
|
+
const playback = createPlayback({ src: 'http://clappr.io/foo.m3u8' })
|
|
118
164
|
playback._setup()
|
|
119
165
|
// NOTE: rather than trying to call playback.setupHls, we'll punch a new one in place
|
|
120
166
|
playback._hls = { levels: [] }
|
|
@@ -408,4 +454,726 @@ describe('HlsjsPlayback', () => {
|
|
|
408
454
|
expect(playback.currentTimestamp).toBe(null)
|
|
409
455
|
})
|
|
410
456
|
})
|
|
457
|
+
|
|
458
|
+
describe('DVR time model', () => {
|
|
459
|
+
test('without correlation duration equals playable region duration', () => {
|
|
460
|
+
const playback = createPlayback()
|
|
461
|
+
playback._playbackType = Playback.LIVE
|
|
462
|
+
playback._playlistType = null
|
|
463
|
+
playback._playableRegionStartTime = 10
|
|
464
|
+
playback._playableRegionDuration = 100
|
|
465
|
+
playback._segmentTargetDuration = 6
|
|
466
|
+
|
|
467
|
+
expect(playback.getStartTimeOffset()).toBe(10)
|
|
468
|
+
expect(playback.getDuration()).toBe(100)
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
test('extrapolated start advances with wall clock and caps at window end', () => {
|
|
472
|
+
const playback = createPlayback()
|
|
473
|
+
playback._playbackType = Playback.LIVE
|
|
474
|
+
playback._playlistType = null
|
|
475
|
+
playback._playableRegionStartTime = 0
|
|
476
|
+
playback._playableRegionDuration = 120
|
|
477
|
+
playback._segmentTargetDuration = 6
|
|
478
|
+
playback._extrapolatedWindowNumSegments = 2
|
|
479
|
+
playback._localStartTimeCorrelation = { local: 1000000, remote: 0 }
|
|
480
|
+
playback._localEndTimeCorrelation = { local: 1000000, remote: 120000 }
|
|
481
|
+
|
|
482
|
+
const clock = freezeNow(playback, 1000000)
|
|
483
|
+
expect(playback.getStartTimeOffset()).toBe(0)
|
|
484
|
+
|
|
485
|
+
clock.advance(5000)
|
|
486
|
+
expect(playback.getStartTimeOffset()).toBe(5)
|
|
487
|
+
|
|
488
|
+
clock.advance(20000)
|
|
489
|
+
expect(playback.getStartTimeOffset()).toBe(12)
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
test('extrapolated end advances and is clamped to actual end', () => {
|
|
493
|
+
const playback = createPlayback()
|
|
494
|
+
playback._playbackType = Playback.LIVE
|
|
495
|
+
playback._playlistType = null
|
|
496
|
+
playback._playableRegionStartTime = 0
|
|
497
|
+
playback._playableRegionDuration = 100
|
|
498
|
+
playback._segmentTargetDuration = 6
|
|
499
|
+
playback._extrapolatedWindowNumSegments = 2
|
|
500
|
+
playback._localStartTimeCorrelation = null
|
|
501
|
+
playback._localEndTimeCorrelation = { local: 1000000, remote: 88000 }
|
|
502
|
+
|
|
503
|
+
const clock = freezeNow(playback, 1000000)
|
|
504
|
+
expect(playback.getDuration()).toBe(88)
|
|
505
|
+
|
|
506
|
+
clock.advance(5000)
|
|
507
|
+
expect(playback.getDuration()).toBe(93)
|
|
508
|
+
|
|
509
|
+
clock.advance(20000)
|
|
510
|
+
expect(playback.getDuration()).toBe(100)
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
test('EVENT playlist uses raw playable start instead of extrapolated start', () => {
|
|
514
|
+
const playback = createPlayback()
|
|
515
|
+
playback._playbackType = Playback.LIVE
|
|
516
|
+
playback._playlistType = 'EVENT'
|
|
517
|
+
playback._playableRegionStartTime = 40
|
|
518
|
+
playback._playableRegionDuration = 80
|
|
519
|
+
playback._segmentTargetDuration = 6
|
|
520
|
+
playback._localStartTimeCorrelation = { local: 1000000, remote: 0 }
|
|
521
|
+
freezeNow(playback, 1010000)
|
|
522
|
+
|
|
523
|
+
expect(playback.getStartTimeOffset()).toBe(40)
|
|
524
|
+
expect(playback.getDuration()).toBe(80)
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
test('VOD uses raw playable start instead of extrapolated start', () => {
|
|
528
|
+
const playback = createPlayback()
|
|
529
|
+
playback._playbackType = Playback.VOD
|
|
530
|
+
playback._playableRegionStartTime = 5
|
|
531
|
+
playback._playableRegionDuration = 60
|
|
532
|
+
playback._segmentTargetDuration = 6
|
|
533
|
+
playback._localStartTimeCorrelation = { local: 1000000, remote: 0 }
|
|
534
|
+
freezeNow(playback, 1010000)
|
|
535
|
+
|
|
536
|
+
expect(playback.getStartTimeOffset()).toBe(5)
|
|
537
|
+
expect(playback.getDuration()).toBe(60)
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
test('extrapolated window duration is zero when segment target duration is null', () => {
|
|
541
|
+
const playback = createPlayback()
|
|
542
|
+
playback._playbackType = Playback.LIVE
|
|
543
|
+
playback._playlistType = null
|
|
544
|
+
playback._playableRegionStartTime = 0
|
|
545
|
+
playback._playableRegionDuration = 100
|
|
546
|
+
playback._segmentTargetDuration = null
|
|
547
|
+
playback._localStartTimeCorrelation = { local: 1000000, remote: 0 }
|
|
548
|
+
const clock = freezeNow(playback, 1000000)
|
|
549
|
+
|
|
550
|
+
clock.advance(5000)
|
|
551
|
+
expect(playback.getStartTimeOffset()).toBe(0)
|
|
552
|
+
})
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
describe('_onLevelUpdated', () => {
|
|
556
|
+
test('returns early when fragments are empty without mutating start time', () => {
|
|
557
|
+
const playback = createPlayback()
|
|
558
|
+
playback._playableRegionStartTime = 15
|
|
559
|
+
playback._onLevelUpdated('levelUpdated', makeLevelData({ fragments: [] }))
|
|
560
|
+
|
|
561
|
+
expect(playback._playableRegionStartTime).toBe(15)
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
test('first update sets program date time, duration, and correlations', () => {
|
|
565
|
+
const playback = createPlayback()
|
|
566
|
+
playback._playbackType = Playback.LIVE
|
|
567
|
+
freezeNow(playback, 2000000)
|
|
568
|
+
|
|
569
|
+
playback._onLevelUpdated(
|
|
570
|
+
'levelUpdated',
|
|
571
|
+
makeLevelData({
|
|
572
|
+
targetduration: 6,
|
|
573
|
+
totalduration: 120,
|
|
574
|
+
fragmentStart: 10,
|
|
575
|
+
rawProgramDateTime: 1556663040000
|
|
576
|
+
})
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
expect(playback.getProgramDateTime()).toBe(1556663040000)
|
|
580
|
+
expect(playback._playableRegionDuration).toBe(102)
|
|
581
|
+
expect(playback._durationExcludesAfterLiveSyncPoint).toBe(true)
|
|
582
|
+
expect(playback._playableRegionStartTime).toBe(10)
|
|
583
|
+
expect(playback._localStartTimeCorrelation).toEqual({
|
|
584
|
+
local: 2000000,
|
|
585
|
+
remote: (10 + 6) * 1000
|
|
586
|
+
})
|
|
587
|
+
expect(playback._localEndTimeCorrelation).toEqual({
|
|
588
|
+
local: 2000000,
|
|
589
|
+
remote: 112000
|
|
590
|
+
})
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
test('does not trim duration when live sync hidden area exceeds total duration', () => {
|
|
594
|
+
const playback = createPlayback()
|
|
595
|
+
playback._playbackType = Playback.LIVE
|
|
596
|
+
freezeNow(playback)
|
|
597
|
+
|
|
598
|
+
playback._onLevelUpdated(
|
|
599
|
+
'levelUpdated',
|
|
600
|
+
makeLevelData({ targetduration: 10, totalduration: 20, fragmentStart: 0 })
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
expect(playback._playableRegionDuration).toBe(20)
|
|
604
|
+
expect(playback._durationExcludesAfterLiveSyncPoint).toBe(false)
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
test('VOD update does not apply live sync trim', () => {
|
|
608
|
+
const playback = createPlayback()
|
|
609
|
+
playback._playbackType = Playback.VOD
|
|
610
|
+
freezeNow(playback)
|
|
611
|
+
|
|
612
|
+
playback._onLevelUpdated(
|
|
613
|
+
'levelUpdated',
|
|
614
|
+
makeLevelData({ targetduration: 6, totalduration: 120, fragmentStart: 0 })
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
expect(playback._playableRegionDuration).toBe(120)
|
|
618
|
+
expect(playback._durationExcludesAfterLiveSyncPoint).toBe(false)
|
|
619
|
+
})
|
|
620
|
+
|
|
621
|
+
test('resets start correlation when extrapolated start falls before first chunk', () => {
|
|
622
|
+
const playback = createPlayback()
|
|
623
|
+
playback._playbackType = Playback.LIVE
|
|
624
|
+
playback._segmentTargetDuration = 6
|
|
625
|
+
playback._playableRegionStartTime = 0
|
|
626
|
+
playback._playableRegionDuration = 102
|
|
627
|
+
playback._localStartTimeCorrelation = { local: 1000000, remote: 0 }
|
|
628
|
+
playback._localEndTimeCorrelation = { local: 1000000, remote: 102000 }
|
|
629
|
+
freezeNow(playback, 1000000)
|
|
630
|
+
|
|
631
|
+
playback._onLevelUpdated(
|
|
632
|
+
'levelUpdated',
|
|
633
|
+
makeLevelData({
|
|
634
|
+
targetduration: 6,
|
|
635
|
+
totalduration: 120,
|
|
636
|
+
fragmentStart: 30,
|
|
637
|
+
rawProgramDateTime: null
|
|
638
|
+
})
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
expect(playback._localStartTimeCorrelation.remote).toBe(30000)
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
test('resets start correlation when extrapolated start was past the old window cap', () => {
|
|
645
|
+
const playback = createPlayback()
|
|
646
|
+
playback._playbackType = Playback.LIVE
|
|
647
|
+
playback._segmentTargetDuration = 6
|
|
648
|
+
playback._extrapolatedWindowNumSegments = 2
|
|
649
|
+
playback._playableRegionStartTime = 0
|
|
650
|
+
playback._playableRegionDuration = 102
|
|
651
|
+
playback._localStartTimeCorrelation = { local: 1000000, remote: 0 }
|
|
652
|
+
playback._localEndTimeCorrelation = { local: 1000000, remote: 102000 }
|
|
653
|
+
freezeNow(playback, 1020000)
|
|
654
|
+
|
|
655
|
+
playback._onLevelUpdated(
|
|
656
|
+
'levelUpdated',
|
|
657
|
+
makeLevelData({
|
|
658
|
+
targetduration: 6,
|
|
659
|
+
totalduration: 120,
|
|
660
|
+
fragmentStart: 5,
|
|
661
|
+
rawProgramDateTime: null
|
|
662
|
+
})
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
expect(playback._localStartTimeCorrelation.remote).toBe(12000)
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
test('resets end correlation when extrapolated end exceeds new end time', () => {
|
|
669
|
+
const playback = createPlayback()
|
|
670
|
+
playback._playbackType = Playback.LIVE
|
|
671
|
+
playback._segmentTargetDuration = 6
|
|
672
|
+
playback._playableRegionStartTime = 0
|
|
673
|
+
playback._playableRegionDuration = 200
|
|
674
|
+
playback._localStartTimeCorrelation = { local: 1000000, remote: 0 }
|
|
675
|
+
playback._localEndTimeCorrelation = { local: 1000000, remote: 200000 }
|
|
676
|
+
freezeNow(playback, 1000000)
|
|
677
|
+
|
|
678
|
+
playback._onLevelUpdated(
|
|
679
|
+
'levelUpdated',
|
|
680
|
+
makeLevelData({
|
|
681
|
+
targetduration: 6,
|
|
682
|
+
totalduration: 60,
|
|
683
|
+
fragmentStart: 0,
|
|
684
|
+
rawProgramDateTime: null
|
|
685
|
+
})
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
expect(playback._localEndTimeCorrelation.remote).toBe(42000)
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
test('resets end correlation when extrapolated end was capped past the previous end', () => {
|
|
692
|
+
const playback = createPlayback()
|
|
693
|
+
playback._playbackType = Playback.LIVE
|
|
694
|
+
playback._segmentTargetDuration = 6
|
|
695
|
+
playback._extrapolatedWindowNumSegments = 2
|
|
696
|
+
playback._playableRegionStartTime = 0
|
|
697
|
+
playback._playableRegionDuration = 100
|
|
698
|
+
playback._localStartTimeCorrelation = { local: 1000000, remote: 0 }
|
|
699
|
+
playback._localEndTimeCorrelation = { local: 1000000, remote: 100000 }
|
|
700
|
+
freezeNow(playback, 1005000)
|
|
701
|
+
|
|
702
|
+
playback._onLevelUpdated(
|
|
703
|
+
'levelUpdated',
|
|
704
|
+
makeLevelData({
|
|
705
|
+
targetduration: 6,
|
|
706
|
+
totalduration: 128,
|
|
707
|
+
fragmentStart: 0,
|
|
708
|
+
rawProgramDateTime: null
|
|
709
|
+
})
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
expect(playback._localEndTimeCorrelation.remote).toBe(100000)
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
test('resets end correlation when extrapolated end falls behind the window from the end', () => {
|
|
716
|
+
const playback = createPlayback()
|
|
717
|
+
playback._playbackType = Playback.LIVE
|
|
718
|
+
playback._segmentTargetDuration = 6
|
|
719
|
+
playback._extrapolatedWindowNumSegments = 2
|
|
720
|
+
playback._playableRegionStartTime = 0
|
|
721
|
+
playback._playableRegionDuration = 100
|
|
722
|
+
playback._localStartTimeCorrelation = { local: 1000000, remote: 0 }
|
|
723
|
+
playback._localEndTimeCorrelation = { local: 1000000, remote: 100000 }
|
|
724
|
+
freezeNow(playback, 1000000)
|
|
725
|
+
|
|
726
|
+
playback._onLevelUpdated(
|
|
727
|
+
'levelUpdated',
|
|
728
|
+
makeLevelData({
|
|
729
|
+
targetduration: 6,
|
|
730
|
+
totalduration: 200,
|
|
731
|
+
fragmentStart: 0,
|
|
732
|
+
rawProgramDateTime: null
|
|
733
|
+
})
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
expect(playback._localEndTimeCorrelation.remote).toBe((182 - 12) * 1000)
|
|
737
|
+
})
|
|
738
|
+
|
|
739
|
+
test('calls duration and progress handlers when start and duration change', () => {
|
|
740
|
+
const playback = createPlayback()
|
|
741
|
+
playback._playbackType = Playback.LIVE
|
|
742
|
+
freezeNow(playback)
|
|
743
|
+
jest.spyOn(playback, '_onDurationChange').mockImplementation(() => {})
|
|
744
|
+
jest.spyOn(playback, '_onProgress').mockImplementation(() => {})
|
|
745
|
+
|
|
746
|
+
playback._onLevelUpdated(
|
|
747
|
+
'levelUpdated',
|
|
748
|
+
makeLevelData({ targetduration: 6, totalduration: 120, fragmentStart: 10 })
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
expect(playback._onDurationChange).toHaveBeenCalled()
|
|
752
|
+
expect(playback._onProgress).toHaveBeenCalled()
|
|
753
|
+
})
|
|
754
|
+
|
|
755
|
+
test('respects custom liveSyncDurationCount from hlsjsConfig', () => {
|
|
756
|
+
const playback = createPlayback({
|
|
757
|
+
playback: { hlsjsConfig: { liveSyncDurationCount: 2 } }
|
|
758
|
+
})
|
|
759
|
+
playback._playbackType = Playback.LIVE
|
|
760
|
+
freezeNow(playback)
|
|
761
|
+
|
|
762
|
+
playback._onLevelUpdated(
|
|
763
|
+
'levelUpdated',
|
|
764
|
+
makeLevelData({ targetduration: 10, totalduration: 100, fragmentStart: 0 })
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
expect(playback._playableRegionDuration).toBe(80)
|
|
768
|
+
expect(playback._durationExcludesAfterLiveSyncPoint).toBe(true)
|
|
769
|
+
})
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
describe('time API', () => {
|
|
773
|
+
test('getCurrentTime offsets by start time and never goes below zero', () => {
|
|
774
|
+
const playback = createPlayback()
|
|
775
|
+
playback._playbackType = Playback.VOD
|
|
776
|
+
playback._playableRegionStartTime = 20
|
|
777
|
+
playback._playableRegionDuration = 100
|
|
778
|
+
playback.el.currentTime = 45
|
|
779
|
+
|
|
780
|
+
expect(playback.getCurrentTime()).toBe(25)
|
|
781
|
+
|
|
782
|
+
playback.el.currentTime = 10
|
|
783
|
+
expect(playback.getCurrentTime()).toBe(0)
|
|
784
|
+
})
|
|
785
|
+
|
|
786
|
+
test('seek writes element currentTime including start offset', () => {
|
|
787
|
+
const playback = createPlayback()
|
|
788
|
+
playback._playbackType = Playback.VOD
|
|
789
|
+
playback._playableRegionStartTime = 20
|
|
790
|
+
playback._playableRegionDuration = 100
|
|
791
|
+
|
|
792
|
+
playback.seek(30)
|
|
793
|
+
expect(playback.el.currentTime).toBe(50)
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
test('seek of negative time warns and seeks to duration', () => {
|
|
797
|
+
const playback = createPlayback()
|
|
798
|
+
playback._playbackType = Playback.VOD
|
|
799
|
+
playback._playableRegionStartTime = 0
|
|
800
|
+
playback._playableRegionDuration = 80
|
|
801
|
+
jest.spyOn(Log, 'warn').mockImplementation(() => {})
|
|
802
|
+
|
|
803
|
+
playback.seek(-1)
|
|
804
|
+
|
|
805
|
+
expect(Log.warn).toHaveBeenCalled()
|
|
806
|
+
expect(playback.el.currentTime).toBe(80)
|
|
807
|
+
})
|
|
808
|
+
|
|
809
|
+
test('seekPercentage maps percent of duration then seeks', () => {
|
|
810
|
+
const playback = createPlayback()
|
|
811
|
+
playback._playbackType = Playback.VOD
|
|
812
|
+
playback._playableRegionStartTime = 10
|
|
813
|
+
playback._playableRegionDuration = 100
|
|
814
|
+
|
|
815
|
+
playback.seekPercentage(50)
|
|
816
|
+
expect(playback.el.currentTime).toBe(60)
|
|
817
|
+
})
|
|
818
|
+
|
|
819
|
+
test('seekPercentage of zero seeks to the start', () => {
|
|
820
|
+
const playback = createPlayback()
|
|
821
|
+
playback._playbackType = Playback.VOD
|
|
822
|
+
playback._playableRegionStartTime = 10
|
|
823
|
+
playback._playableRegionDuration = 90
|
|
824
|
+
playback.el.currentTime = 50
|
|
825
|
+
|
|
826
|
+
playback.seekPercentage(0)
|
|
827
|
+
expect(playback.el.currentTime).toBe(10)
|
|
828
|
+
})
|
|
829
|
+
|
|
830
|
+
test('seekToLivePoint seeks to duration', () => {
|
|
831
|
+
const playback = createPlayback()
|
|
832
|
+
playback._playbackType = Playback.LIVE
|
|
833
|
+
playback._playlistType = 'EVENT'
|
|
834
|
+
playback._playableRegionStartTime = 5
|
|
835
|
+
playback._playableRegionDuration = 70
|
|
836
|
+
|
|
837
|
+
playback.seekToLivePoint()
|
|
838
|
+
expect(playback.el.currentTime).toBe(75)
|
|
839
|
+
})
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
describe('_onTimeUpdate throttling', () => {
|
|
843
|
+
test('first time update fires PLAYBACK_TIMEUPDATE', () => {
|
|
844
|
+
const playback = createPlayback()
|
|
845
|
+
playback._playbackType = Playback.VOD
|
|
846
|
+
playback._playableRegionDuration = 100
|
|
847
|
+
playback.el.currentTime = 1
|
|
848
|
+
freezeNow(playback, 1000000)
|
|
849
|
+
const onTimeUpdate = jest.fn()
|
|
850
|
+
playback.on(Events.PLAYBACK_TIMEUPDATE, onTimeUpdate)
|
|
851
|
+
|
|
852
|
+
playback._onTimeUpdate()
|
|
853
|
+
|
|
854
|
+
expect(onTimeUpdate).toHaveBeenCalledTimes(1)
|
|
855
|
+
expect(onTimeUpdate.mock.calls[0][0]).toMatchObject({ current: 1, total: 100 })
|
|
856
|
+
})
|
|
857
|
+
|
|
858
|
+
test('identical payload within throttle delay is suppressed', () => {
|
|
859
|
+
const playback = createPlayback()
|
|
860
|
+
playback._playbackType = Playback.VOD
|
|
861
|
+
playback._playableRegionDuration = 100
|
|
862
|
+
playback.el.currentTime = 1
|
|
863
|
+
const clock = freezeNow(playback, 1000000)
|
|
864
|
+
const onTimeUpdate = jest.fn()
|
|
865
|
+
playback.on(Events.PLAYBACK_TIMEUPDATE, onTimeUpdate)
|
|
866
|
+
|
|
867
|
+
playback._onTimeUpdate()
|
|
868
|
+
clock.advance(50)
|
|
869
|
+
playback._onTimeUpdate()
|
|
870
|
+
|
|
871
|
+
expect(onTimeUpdate).toHaveBeenCalledTimes(1)
|
|
872
|
+
})
|
|
873
|
+
|
|
874
|
+
test('fires again after throttle delay even with same payload', () => {
|
|
875
|
+
const playback = createPlayback()
|
|
876
|
+
playback._playbackType = Playback.VOD
|
|
877
|
+
playback._playableRegionDuration = 100
|
|
878
|
+
playback.el.currentTime = 1
|
|
879
|
+
const clock = freezeNow(playback, 1000000)
|
|
880
|
+
const onTimeUpdate = jest.fn()
|
|
881
|
+
playback.on(Events.PLAYBACK_TIMEUPDATE, onTimeUpdate)
|
|
882
|
+
|
|
883
|
+
playback._onTimeUpdate()
|
|
884
|
+
clock.advance(250)
|
|
885
|
+
playback._onTimeUpdate()
|
|
886
|
+
|
|
887
|
+
expect(onTimeUpdate).toHaveBeenCalledTimes(2)
|
|
888
|
+
})
|
|
889
|
+
|
|
890
|
+
test('changed current time fires despite throttle window', () => {
|
|
891
|
+
const playback = createPlayback()
|
|
892
|
+
playback._playbackType = Playback.VOD
|
|
893
|
+
playback._playableRegionDuration = 100
|
|
894
|
+
playback.el.currentTime = 1
|
|
895
|
+
const clock = freezeNow(playback, 1000000)
|
|
896
|
+
const onTimeUpdate = jest.fn()
|
|
897
|
+
playback.on(Events.PLAYBACK_TIMEUPDATE, onTimeUpdate)
|
|
898
|
+
|
|
899
|
+
playback._onTimeUpdate()
|
|
900
|
+
clock.advance(50)
|
|
901
|
+
playback.el.currentTime = 2
|
|
902
|
+
playback._onTimeUpdate()
|
|
903
|
+
|
|
904
|
+
expect(onTimeUpdate).toHaveBeenCalledTimes(2)
|
|
905
|
+
})
|
|
906
|
+
|
|
907
|
+
test('changed program date time fires despite throttle window', () => {
|
|
908
|
+
const playback = createPlayback()
|
|
909
|
+
playback._playbackType = Playback.VOD
|
|
910
|
+
playback._playableRegionDuration = 100
|
|
911
|
+
playback._programDateTime = 1000
|
|
912
|
+
playback.el.currentTime = 1
|
|
913
|
+
const clock = freezeNow(playback, 1000000)
|
|
914
|
+
const onTimeUpdate = jest.fn()
|
|
915
|
+
playback.on(Events.PLAYBACK_TIMEUPDATE, onTimeUpdate)
|
|
916
|
+
|
|
917
|
+
playback._onTimeUpdate()
|
|
918
|
+
clock.advance(50)
|
|
919
|
+
playback._programDateTime = 2000
|
|
920
|
+
playback._onTimeUpdate()
|
|
921
|
+
|
|
922
|
+
expect(onTimeUpdate).toHaveBeenCalledTimes(2)
|
|
923
|
+
})
|
|
924
|
+
})
|
|
925
|
+
|
|
926
|
+
describe('_onHLSJSError', () => {
|
|
927
|
+
const cores = []
|
|
928
|
+
|
|
929
|
+
afterEach(() => {
|
|
930
|
+
cores.splice(0).forEach(core => core.destroy())
|
|
931
|
+
})
|
|
932
|
+
|
|
933
|
+
const createErrorPlayback = (options = {}) => {
|
|
934
|
+
const core = new Core({})
|
|
935
|
+
cores.push(core)
|
|
936
|
+
const playback = createPlayback(options, core.playerError)
|
|
937
|
+
stubHls(playback)
|
|
938
|
+
jest.spyOn(playback, 'play').mockImplementation(() => {})
|
|
939
|
+
jest.spyOn(playback, 'stop').mockImplementation(() => {})
|
|
940
|
+
return playback
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
test('recoverable network fatal error calls startLoad', () => {
|
|
944
|
+
const playback = createErrorPlayback()
|
|
945
|
+
jest.spyOn(Log, 'warn').mockImplementation(() => {})
|
|
946
|
+
|
|
947
|
+
playback._onHLSJSError('hlsError', {
|
|
948
|
+
fatal: true,
|
|
949
|
+
type: HLSJS.ErrorTypes.NETWORK_ERROR,
|
|
950
|
+
details: HLSJS.ErrorDetails.FRAG_LOAD_ERROR
|
|
951
|
+
})
|
|
952
|
+
|
|
953
|
+
expect(playback._hls.startLoad).toHaveBeenCalled()
|
|
954
|
+
expect(playback.stop).not.toHaveBeenCalled()
|
|
955
|
+
expect(playback._recoverAttemptsRemaining).toBe(15)
|
|
956
|
+
})
|
|
957
|
+
|
|
958
|
+
test.each([
|
|
959
|
+
HLSJS.ErrorDetails.MANIFEST_LOAD_ERROR,
|
|
960
|
+
HLSJS.ErrorDetails.MANIFEST_LOAD_TIMEOUT,
|
|
961
|
+
HLSJS.ErrorDetails.MANIFEST_PARSING_ERROR,
|
|
962
|
+
HLSJS.ErrorDetails.LEVEL_LOAD_ERROR,
|
|
963
|
+
HLSJS.ErrorDetails.LEVEL_LOAD_TIMEOUT
|
|
964
|
+
])('unrecoverable network fatal %s triggers error and stop', details => {
|
|
965
|
+
const playback = createErrorPlayback()
|
|
966
|
+
jest.spyOn(Log, 'error').mockImplementation(() => {})
|
|
967
|
+
const onError = jest.fn()
|
|
968
|
+
playback.on(Events.PLAYBACK_ERROR, onError)
|
|
969
|
+
|
|
970
|
+
playback._onHLSJSError('hlsError', {
|
|
971
|
+
fatal: true,
|
|
972
|
+
type: HLSJS.ErrorTypes.NETWORK_ERROR,
|
|
973
|
+
details
|
|
974
|
+
})
|
|
975
|
+
|
|
976
|
+
expect(onError).toHaveBeenCalledTimes(1)
|
|
977
|
+
expect(playback.stop).toHaveBeenCalled()
|
|
978
|
+
expect(playback._hls.startLoad).not.toHaveBeenCalled()
|
|
979
|
+
})
|
|
980
|
+
|
|
981
|
+
test('media fatal error recovers via recoverMediaError then swapAudioCodec then fails', () => {
|
|
982
|
+
const playback = createErrorPlayback()
|
|
983
|
+
jest.spyOn(Log, 'warn').mockImplementation(() => {})
|
|
984
|
+
jest.spyOn(Log, 'error').mockImplementation(() => {})
|
|
985
|
+
const onError = jest.fn()
|
|
986
|
+
playback.on(Events.PLAYBACK_ERROR, onError)
|
|
987
|
+
const mediaError = {
|
|
988
|
+
fatal: true,
|
|
989
|
+
type: HLSJS.ErrorTypes.MEDIA_ERROR,
|
|
990
|
+
details: HLSJS.ErrorDetails.BUFFER_STALLED_ERROR
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
playback._onHLSJSError('hlsError', mediaError)
|
|
994
|
+
expect(playback._hls.recoverMediaError).toHaveBeenCalledTimes(1)
|
|
995
|
+
expect(playback._hls.swapAudioCodec).not.toHaveBeenCalled()
|
|
996
|
+
expect(playback.play).toHaveBeenCalledTimes(1)
|
|
997
|
+
|
|
998
|
+
playback._onHLSJSError('hlsError', mediaError)
|
|
999
|
+
expect(playback._hls.swapAudioCodec).toHaveBeenCalledTimes(1)
|
|
1000
|
+
expect(playback._hls.recoverMediaError).toHaveBeenCalledTimes(2)
|
|
1001
|
+
expect(playback.play).toHaveBeenCalledTimes(2)
|
|
1002
|
+
expect(onError).not.toHaveBeenCalled()
|
|
1003
|
+
|
|
1004
|
+
playback._onHLSJSError('hlsError', mediaError)
|
|
1005
|
+
expect(onError).toHaveBeenCalledTimes(1)
|
|
1006
|
+
expect(playback.stop).toHaveBeenCalled()
|
|
1007
|
+
})
|
|
1008
|
+
|
|
1009
|
+
test('other fatal error types trigger error and stop', () => {
|
|
1010
|
+
const playback = createErrorPlayback()
|
|
1011
|
+
jest.spyOn(Log, 'error').mockImplementation(() => {})
|
|
1012
|
+
const onError = jest.fn()
|
|
1013
|
+
playback.on(Events.PLAYBACK_ERROR, onError)
|
|
1014
|
+
|
|
1015
|
+
playback._onHLSJSError('hlsError', {
|
|
1016
|
+
fatal: true,
|
|
1017
|
+
type: HLSJS.ErrorTypes.OTHER_ERROR,
|
|
1018
|
+
details: 'whatever'
|
|
1019
|
+
})
|
|
1020
|
+
|
|
1021
|
+
expect(onError).toHaveBeenCalledTimes(1)
|
|
1022
|
+
expect(playback.stop).toHaveBeenCalled()
|
|
1023
|
+
})
|
|
1024
|
+
|
|
1025
|
+
test('exhausted recover attempts trigger error and stop', () => {
|
|
1026
|
+
const playback = createErrorPlayback({ hlsRecoverAttempts: 0 })
|
|
1027
|
+
jest.spyOn(Log, 'error').mockImplementation(() => {})
|
|
1028
|
+
const onError = jest.fn()
|
|
1029
|
+
playback.on(Events.PLAYBACK_ERROR, onError)
|
|
1030
|
+
|
|
1031
|
+
playback._onHLSJSError('hlsError', {
|
|
1032
|
+
fatal: true,
|
|
1033
|
+
type: HLSJS.ErrorTypes.MEDIA_ERROR,
|
|
1034
|
+
details: HLSJS.ErrorDetails.BUFFER_STALLED_ERROR
|
|
1035
|
+
})
|
|
1036
|
+
|
|
1037
|
+
expect(onError).toHaveBeenCalledTimes(1)
|
|
1038
|
+
expect(playback.stop).toHaveBeenCalled()
|
|
1039
|
+
expect(playback._hls.recoverMediaError).not.toHaveBeenCalled()
|
|
1040
|
+
})
|
|
1041
|
+
|
|
1042
|
+
test('non-fatal key denial with option triggers fatal error and stop', () => {
|
|
1043
|
+
const playback = createErrorPlayback({
|
|
1044
|
+
playback: { triggerFatalErrorOnResourceDenied: true }
|
|
1045
|
+
})
|
|
1046
|
+
jest.spyOn(Log, 'error').mockImplementation(() => {})
|
|
1047
|
+
const onError = jest.fn()
|
|
1048
|
+
playback.on(Events.PLAYBACK_ERROR, onError)
|
|
1049
|
+
|
|
1050
|
+
playback._onHLSJSError('hlsError', {
|
|
1051
|
+
fatal: false,
|
|
1052
|
+
type: HLSJS.ErrorTypes.NETWORK_ERROR,
|
|
1053
|
+
details: HLSJS.ErrorDetails.KEY_LOAD_ERROR,
|
|
1054
|
+
response: { code: 403 }
|
|
1055
|
+
})
|
|
1056
|
+
|
|
1057
|
+
expect(onError).toHaveBeenCalledTimes(1)
|
|
1058
|
+
expect(playback.stop).toHaveBeenCalled()
|
|
1059
|
+
})
|
|
1060
|
+
|
|
1061
|
+
test('non-fatal key denial without option only warns', () => {
|
|
1062
|
+
const playback = createErrorPlayback()
|
|
1063
|
+
jest.spyOn(Log, 'warn').mockImplementation(() => {})
|
|
1064
|
+
const onError = jest.fn()
|
|
1065
|
+
playback.on(Events.PLAYBACK_ERROR, onError)
|
|
1066
|
+
|
|
1067
|
+
playback._onHLSJSError('hlsError', {
|
|
1068
|
+
fatal: false,
|
|
1069
|
+
type: HLSJS.ErrorTypes.NETWORK_ERROR,
|
|
1070
|
+
details: HLSJS.ErrorDetails.KEY_LOAD_ERROR,
|
|
1071
|
+
response: { code: 403 }
|
|
1072
|
+
})
|
|
1073
|
+
|
|
1074
|
+
expect(onError).not.toHaveBeenCalled()
|
|
1075
|
+
expect(playback.stop).not.toHaveBeenCalled()
|
|
1076
|
+
expect(Log.warn).toHaveBeenCalled()
|
|
1077
|
+
})
|
|
1078
|
+
|
|
1079
|
+
test('non-fatal key load with response below 400 only warns', () => {
|
|
1080
|
+
const playback = createErrorPlayback({
|
|
1081
|
+
playback: { triggerFatalErrorOnResourceDenied: true }
|
|
1082
|
+
})
|
|
1083
|
+
jest.spyOn(Log, 'warn').mockImplementation(() => {})
|
|
1084
|
+
const onError = jest.fn()
|
|
1085
|
+
playback.on(Events.PLAYBACK_ERROR, onError)
|
|
1086
|
+
|
|
1087
|
+
playback._onHLSJSError('hlsError', {
|
|
1088
|
+
fatal: false,
|
|
1089
|
+
type: HLSJS.ErrorTypes.NETWORK_ERROR,
|
|
1090
|
+
details: HLSJS.ErrorDetails.KEY_LOAD_ERROR,
|
|
1091
|
+
response: { code: 200 }
|
|
1092
|
+
})
|
|
1093
|
+
|
|
1094
|
+
expect(onError).not.toHaveBeenCalled()
|
|
1095
|
+
expect(Log.warn).toHaveBeenCalled()
|
|
1096
|
+
})
|
|
1097
|
+
|
|
1098
|
+
test('includes response in error description when present', () => {
|
|
1099
|
+
const playback = createErrorPlayback()
|
|
1100
|
+
jest.spyOn(Log, 'error').mockImplementation(() => {})
|
|
1101
|
+
const onError = jest.fn()
|
|
1102
|
+
playback.on(Events.PLAYBACK_ERROR, onError)
|
|
1103
|
+
|
|
1104
|
+
playback._onHLSJSError('hlsError', {
|
|
1105
|
+
fatal: true,
|
|
1106
|
+
type: HLSJS.ErrorTypes.NETWORK_ERROR,
|
|
1107
|
+
details: HLSJS.ErrorDetails.MANIFEST_LOAD_ERROR,
|
|
1108
|
+
response: { code: 404, text: 'missing' }
|
|
1109
|
+
})
|
|
1110
|
+
|
|
1111
|
+
expect(onError.mock.calls[0][0].description).toContain('response:')
|
|
1112
|
+
})
|
|
1113
|
+
})
|
|
1114
|
+
|
|
1115
|
+
describe('dvrEnabled and settings', () => {
|
|
1116
|
+
test('dvrEnabled requires live sync trim, min duration, and LIVE type', () => {
|
|
1117
|
+
const playback = createPlayback({ hlsMinimumDvrSize: 60 })
|
|
1118
|
+
playback._playbackType = Playback.LIVE
|
|
1119
|
+
playback._playlistType = 'EVENT'
|
|
1120
|
+
playback._playableRegionStartTime = 0
|
|
1121
|
+
playback._playableRegionDuration = 120
|
|
1122
|
+
playback._durationExcludesAfterLiveSyncPoint = true
|
|
1123
|
+
|
|
1124
|
+
expect(playback.dvrEnabled).toBe(true)
|
|
1125
|
+
|
|
1126
|
+
playback._durationExcludesAfterLiveSyncPoint = false
|
|
1127
|
+
expect(playback.dvrEnabled).toBe(false)
|
|
1128
|
+
|
|
1129
|
+
playback._durationExcludesAfterLiveSyncPoint = true
|
|
1130
|
+
playback._playableRegionDuration = 30
|
|
1131
|
+
expect(playback.dvrEnabled).toBe(false)
|
|
1132
|
+
|
|
1133
|
+
playback._playableRegionDuration = 120
|
|
1134
|
+
playback._playbackType = Playback.VOD
|
|
1135
|
+
expect(playback.dvrEnabled).toBe(false)
|
|
1136
|
+
})
|
|
1137
|
+
|
|
1138
|
+
test('isSeekEnabled is true for VOD or when dvr is enabled', () => {
|
|
1139
|
+
const playback = createPlayback({ hlsMinimumDvrSize: 60 })
|
|
1140
|
+
playback._playbackType = Playback.VOD
|
|
1141
|
+
expect(playback.isSeekEnabled()).toBe(true)
|
|
1142
|
+
|
|
1143
|
+
playback._playbackType = Playback.LIVE
|
|
1144
|
+
playback._playlistType = 'EVENT'
|
|
1145
|
+
playback._playableRegionDuration = 120
|
|
1146
|
+
playback._durationExcludesAfterLiveSyncPoint = false
|
|
1147
|
+
expect(playback.isSeekEnabled()).toBe(false)
|
|
1148
|
+
|
|
1149
|
+
playback._durationExcludesAfterLiveSyncPoint = true
|
|
1150
|
+
expect(playback.isSeekEnabled()).toBe(true)
|
|
1151
|
+
})
|
|
1152
|
+
|
|
1153
|
+
test('_updateSettings chooses left controls from playback type and dvr', () => {
|
|
1154
|
+
const playback = createPlayback({ hlsMinimumDvrSize: 60 })
|
|
1155
|
+
const onSettings = jest.fn()
|
|
1156
|
+
playback.on(Events.PLAYBACK_SETTINGSUPDATE, onSettings)
|
|
1157
|
+
|
|
1158
|
+
playback._playbackType = Playback.VOD
|
|
1159
|
+
playback._updateSettings()
|
|
1160
|
+
expect(playback.settings.left).toEqual(['playpause', 'position', 'duration'])
|
|
1161
|
+
expect(playback.settings.seekEnabled).toBe(true)
|
|
1162
|
+
|
|
1163
|
+
playback._playbackType = Playback.LIVE
|
|
1164
|
+
playback._playlistType = 'EVENT'
|
|
1165
|
+
playback._playableRegionDuration = 120
|
|
1166
|
+
playback._durationExcludesAfterLiveSyncPoint = true
|
|
1167
|
+
playback._updateSettings()
|
|
1168
|
+
expect(playback.settings.left).toEqual(['playpause'])
|
|
1169
|
+
expect(playback.settings.seekEnabled).toBe(true)
|
|
1170
|
+
|
|
1171
|
+
playback._durationExcludesAfterLiveSyncPoint = false
|
|
1172
|
+
playback._updateSettings()
|
|
1173
|
+
expect(playback.settings.left).toEqual(['playstop'])
|
|
1174
|
+
expect(playback.settings.seekEnabled).toBe(false)
|
|
1175
|
+
|
|
1176
|
+
expect(onSettings).toHaveBeenCalledTimes(3)
|
|
1177
|
+
})
|
|
1178
|
+
})
|
|
411
1179
|
})
|