@amazon/vinyl 1.2.1 → 1.2.2
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/index.cjs +13 -4
- package/dist/index.cjs.map +2 -2
- package/dist/index.js +18 -9
- package/dist/index.js.map +3 -3
- package/dist/test/integ.es5.js +60 -60
- package/dist/test/integ.js +49 -4
- package/dist/test/integ.js.map +2 -2
- package/dist/types/current/ad/AdBreakInfo.d.ts.map +1 -1
- package/dist/types/current/ad/AdControllerImpl.d.ts +18 -2
- package/dist/types/current/ad/AdControllerImpl.d.ts.map +1 -1
- package/dist/types/current/ad/AdControllerImpl.js +58 -22
- package/dist/types/current/ad/discoverHlsInterstitials.d.ts.map +1 -1
- package/dist/types/current/ad/discoverHlsInterstitials.js +5 -3
- package/dist/types/current/drm/DrmControllerImpl.d.ts +6 -0
- package/dist/types/current/drm/DrmControllerImpl.d.ts.map +1 -1
- package/dist/types/current/drm/DrmControllerImpl.js +13 -1
- package/dist/types/current/playback/createStallDetector.d.ts +43 -0
- package/dist/types/current/playback/createStallDetector.d.ts.map +1 -0
- package/dist/types/current/playback/createStallDetector.js +101 -0
- package/dist/types/current/playback/createStallMonitor.d.ts +45 -0
- package/dist/types/current/playback/createStallMonitor.d.ts.map +1 -0
- package/dist/types/current/playback/createStallMonitor.js +99 -0
- package/dist/types/current/streaming/AudioOptions.d.ts +27 -0
- package/dist/types/current/streaming/AudioOptions.d.ts.map +1 -1
- package/dist/types/current/streaming/AudioOptions.js +5 -1
- package/dist/types/current/streaming/LoadMetric.d.ts +15 -1
- package/dist/types/current/streaming/LoadMetric.d.ts.map +1 -1
- package/dist/types/current/streaming/createTimelineContentTypesValue.d.ts +21 -0
- package/dist/types/current/streaming/createTimelineContentTypesValue.d.ts.map +1 -0
- package/dist/types/current/streaming/createTimelineContentTypesValue.js +25 -0
- package/dist/types/current/text/SidecarTextTrackLoader.d.ts.map +1 -1
- package/dist/types/current/text/SidecarTextTrackLoader.js +4 -4
- package/dist/types/current/text/TextTrack.d.ts.map +1 -1
- package/dist/types/current/text/discoverDashTextTracks.d.ts +2 -2
- package/dist/types/current/text/discoverDashTextTracks.js +4 -3
- package/dist/types/current/text/parseWebVtt.d.ts.map +1 -1
- package/dist/types/current/text/parseWebVtt.js +3 -2
- package/dist/types/current/track/mse/MseTrack.d.ts +0 -1
- package/dist/types/current/track/mse/MseTrack.d.ts.map +1 -1
- package/dist/types/current/track/mse/MseTrack.js +4 -5
- package/dist/types/current/tsconfig.tsbuildinfo +1 -1
- package/dist/types/ts3.7/ad/AdControllerImpl.d.ts +18 -2
- package/dist/types/ts3.7/drm/DrmControllerImpl.d.ts +6 -0
- package/dist/types/ts3.7/playback/createStallDetector.d.ts +43 -0
- package/dist/types/ts3.7/playback/createStallMonitor.d.ts +45 -0
- package/dist/types/ts3.7/streaming/AudioOptions.d.ts +27 -0
- package/dist/types/ts3.7/streaming/LoadMetric.d.ts +15 -1
- package/dist/types/ts3.7/streaming/createTimelineContentTypesValue.d.ts +21 -0
- package/dist/types/ts3.7/text/discoverDashTextTracks.d.ts +2 -2
- package/dist/types/ts3.7/track/mse/MseTrack.d.ts +0 -1
- package/dist/types/ts4.1/ad/AdControllerImpl.d.ts +18 -2
- package/dist/types/ts4.1/drm/DrmControllerImpl.d.ts +6 -0
- package/dist/types/ts4.1/playback/createStallDetector.d.ts +43 -0
- package/dist/types/ts4.1/playback/createStallMonitor.d.ts +45 -0
- package/dist/types/ts4.1/streaming/AudioOptions.d.ts +27 -0
- package/dist/types/ts4.1/streaming/LoadMetric.d.ts +15 -1
- package/dist/types/ts4.1/streaming/createTimelineContentTypesValue.d.ts +21 -0
- package/dist/types/ts4.1/text/discoverDashTextTracks.d.ts +2 -2
- package/dist/types/ts4.1/track/mse/MseTrack.d.ts +0 -1
- package/dist/types/ts4.7/ad/AdControllerImpl.d.ts +18 -2
- package/dist/types/ts4.7/drm/DrmControllerImpl.d.ts +6 -0
- package/dist/types/ts4.7/playback/createStallDetector.d.ts +43 -0
- package/dist/types/ts4.7/playback/createStallMonitor.d.ts +45 -0
- package/dist/types/ts4.7/streaming/AudioOptions.d.ts +27 -0
- package/dist/types/ts4.7/streaming/LoadMetric.d.ts +15 -1
- package/dist/types/ts4.7/streaming/createTimelineContentTypesValue.d.ts +21 -0
- package/dist/types/ts4.7/text/discoverDashTextTracks.d.ts +2 -2
- package/dist/types/ts4.7/track/mse/MseTrack.d.ts +0 -1
- package/dist/unit/types/current/ad/AdControllerImpl.test.js +203 -5
- package/dist/unit/types/current/ad/discoverHlsInterstitials.test.js +2 -1
- package/dist/unit/types/current/drm/DrmControllerImpl.test.js +30 -1
- package/dist/unit/types/current/playback/createStallDetector.test.d.ts +2 -0
- package/dist/unit/types/current/playback/createStallDetector.test.d.ts.map +1 -0
- package/dist/unit/types/current/playback/createStallDetector.test.js +163 -0
- package/dist/unit/types/current/playback/createStallMonitor.test.d.ts +2 -0
- package/dist/unit/types/current/playback/createStallMonitor.test.d.ts.map +1 -0
- package/dist/unit/types/current/playback/createStallMonitor.test.js +163 -0
- package/dist/unit/types/current/streaming/MseTrack.test.js +15 -0
- package/dist/unit/types/current/streaming/createTimelineContentTypesValue.test.d.ts +2 -0
- package/dist/unit/types/current/streaming/createTimelineContentTypesValue.test.d.ts.map +1 -0
- package/dist/unit/types/current/streaming/createTimelineContentTypesValue.test.js +57 -0
- package/dist/unit/types/current/text/TextTrack.test.d.ts +2 -0
- package/dist/unit/types/current/text/TextTrack.test.d.ts.map +1 -0
- package/dist/unit/types/current/text/TextTrack.test.js +12 -0
- package/dist/unit/types/current/text/discoverDashTextTracks.test.js +17 -0
- package/dist/unit/types/current/text/parseWebVtt.test.js +26 -0
- package/dist/unit/types/current/tsconfig.tsbuildinfo +1 -1
- package/dist/vinylTestUtil/types/current/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -9
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { createStallDetector } from '@amazon/vinyl';
|
|
6
|
+
import { implementEventFakes, MockHTMLAudioElement, mockEvent, MockTimeRanges, useMockTime, } from '@amazon/vinyl-util/browserTestUtil';
|
|
7
|
+
describe('createStallDetector', () => {
|
|
8
|
+
const clock = useMockTime();
|
|
9
|
+
let media;
|
|
10
|
+
let fabricatePlaying;
|
|
11
|
+
let stop;
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
media = new MockHTMLAudioElement();
|
|
14
|
+
implementEventFakes(media);
|
|
15
|
+
media.play.and.returnValue(Promise.resolve());
|
|
16
|
+
media.paused = false;
|
|
17
|
+
media.ended = false;
|
|
18
|
+
media.readyState = media.HAVE_ENOUGH_DATA;
|
|
19
|
+
media.currentTime = 10;
|
|
20
|
+
media.buffered = new MockTimeRanges([[0, 100]]);
|
|
21
|
+
fabricatePlaying = jasmine.createSpy('fabricatePlaying');
|
|
22
|
+
});
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
stop?.();
|
|
25
|
+
});
|
|
26
|
+
function detect(overrides = {}) {
|
|
27
|
+
stop = createStallDetector(media, { fabricatePlaying, ...overrides });
|
|
28
|
+
}
|
|
29
|
+
/** Advances the mock clock by `n` default poll intervals. */
|
|
30
|
+
async function poll(n) {
|
|
31
|
+
for (let i = 0; i < n; i++) {
|
|
32
|
+
await clock.tick(0.25);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
describe('a stall inside ready buffered data', () => {
|
|
36
|
+
it('nudges the play head once frozen past the stall threshold', async () => {
|
|
37
|
+
detect();
|
|
38
|
+
// Threshold is 0.75s = three polls; the third nudges by stallSkip.
|
|
39
|
+
await poll(3);
|
|
40
|
+
expect(media.currentTime).toBeCloseTo(10.1);
|
|
41
|
+
});
|
|
42
|
+
it('does not recover before the stall threshold', async () => {
|
|
43
|
+
detect();
|
|
44
|
+
await poll(2);
|
|
45
|
+
expect(media.currentTime).toBe(10);
|
|
46
|
+
expect(media.play).not.toHaveBeenCalled();
|
|
47
|
+
});
|
|
48
|
+
it('nudges even when the ready state is low (iOS reports a stale readyState)', async () => {
|
|
49
|
+
media.readyState = media.HAVE_METADATA;
|
|
50
|
+
detect();
|
|
51
|
+
await poll(3);
|
|
52
|
+
expect(media.currentTime).toBeCloseTo(10.1);
|
|
53
|
+
expect(media.play).not.toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
it('keeps nudging, then gives up after maxAttempts', async () => {
|
|
56
|
+
detect();
|
|
57
|
+
// Default maxAttempts is 3, so at most three nudges (one every 3 polls).
|
|
58
|
+
await poll(12);
|
|
59
|
+
expect(media.currentTime).toBeCloseTo(10.3);
|
|
60
|
+
// Further polls do nothing; the detector has stopped.
|
|
61
|
+
await poll(6);
|
|
62
|
+
expect(media.currentTime).toBeCloseTo(10.3);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe('a stall with no data at the playhead', () => {
|
|
66
|
+
it('does not recover when the playhead is outside all buffered ranges', async () => {
|
|
67
|
+
media.buffered = new MockTimeRanges([[50, 100]]);
|
|
68
|
+
detect();
|
|
69
|
+
await poll(6);
|
|
70
|
+
expect(media.currentTime).toBe(10);
|
|
71
|
+
expect(fabricatePlaying).not.toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
it('does not recover at the end of buffered data', async () => {
|
|
74
|
+
// Within the default end margin (0.5s) of the buffer end.
|
|
75
|
+
media.buffered = new MockTimeRanges([[0, 10.3]]);
|
|
76
|
+
detect();
|
|
77
|
+
await poll(6);
|
|
78
|
+
expect(media.currentTime).toBe(10);
|
|
79
|
+
});
|
|
80
|
+
it('nudges at the end of the media (stuck end-of-track on src)', async () => {
|
|
81
|
+
// Buffer ends at the media duration: no more data is coming, so a
|
|
82
|
+
// freeze here is a stuck end-of-track and a nudge drives it to end.
|
|
83
|
+
media.buffered = new MockTimeRanges([[0, 10.3]]);
|
|
84
|
+
media.duration = 10.3;
|
|
85
|
+
detect();
|
|
86
|
+
await poll(3);
|
|
87
|
+
expect(media.currentTime).toBeCloseTo(10.1);
|
|
88
|
+
});
|
|
89
|
+
it('nudges outside buffered ranges when nudgeUnbuffered is set', async () => {
|
|
90
|
+
// WebKit path: re-issue the seek even though buffered looks empty.
|
|
91
|
+
media.buffered = new MockTimeRanges([[50, 100]]);
|
|
92
|
+
detect({ nudgeUnbuffered: true });
|
|
93
|
+
await poll(3);
|
|
94
|
+
expect(media.currentTime).toBeCloseTo(10.1);
|
|
95
|
+
});
|
|
96
|
+
it('stops when the source is emptied', async () => {
|
|
97
|
+
detect();
|
|
98
|
+
media.dispatchEvent(mockEvent('emptied'));
|
|
99
|
+
// Stopped: a later freeze in buffered data is not nudged.
|
|
100
|
+
await poll(6);
|
|
101
|
+
expect(media.currentTime).toBe(10);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('fabricating a playing event', () => {
|
|
105
|
+
it('fabricates when the head progresses without a playing event', async () => {
|
|
106
|
+
detect();
|
|
107
|
+
media.currentTime = 20;
|
|
108
|
+
await poll(1);
|
|
109
|
+
expect(fabricatePlaying).toHaveBeenCalledTimes(1);
|
|
110
|
+
// Detector stopped; a later freeze is not recovered.
|
|
111
|
+
await poll(6);
|
|
112
|
+
expect(media.play).not.toHaveBeenCalled();
|
|
113
|
+
});
|
|
114
|
+
it('does not fabricate when a real playing event fired and it keeps progressing', async () => {
|
|
115
|
+
detect();
|
|
116
|
+
media.dispatchEvent(mockEvent('playing'));
|
|
117
|
+
media.currentTime = 20;
|
|
118
|
+
await poll(6);
|
|
119
|
+
expect(fabricatePlaying).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
it('fabricates after reviving a stall even if a playing event fired earlier', async () => {
|
|
122
|
+
// iOS pattern: a playing event fires, then playback stalls.
|
|
123
|
+
media.readyState = media.HAVE_METADATA;
|
|
124
|
+
detect();
|
|
125
|
+
media.dispatchEvent(mockEvent('playing'));
|
|
126
|
+
await poll(3);
|
|
127
|
+
expect(media.currentTime).toBeCloseTo(10.1); // nudged (recovered)
|
|
128
|
+
// The recovery revives playback.
|
|
129
|
+
media.currentTime = 11;
|
|
130
|
+
await poll(1);
|
|
131
|
+
expect(fabricatePlaying).toHaveBeenCalledTimes(1);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
describe('when the element is not advancing by design', () => {
|
|
135
|
+
it('does not recover while paused', async () => {
|
|
136
|
+
media.paused = true;
|
|
137
|
+
detect();
|
|
138
|
+
await poll(6);
|
|
139
|
+
expect(media.play).not.toHaveBeenCalled();
|
|
140
|
+
expect(media.currentTime).toBe(10);
|
|
141
|
+
});
|
|
142
|
+
it('does not recover once ended', async () => {
|
|
143
|
+
media.ended = true;
|
|
144
|
+
detect();
|
|
145
|
+
await poll(6);
|
|
146
|
+
expect(media.play).not.toHaveBeenCalled();
|
|
147
|
+
expect(media.currentTime).toBe(10);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
it('honors provided options', async () => {
|
|
151
|
+
detect({
|
|
152
|
+
pollInterval: 0.5,
|
|
153
|
+
stallThreshold: 1,
|
|
154
|
+
bufferEndMargin: 0.2,
|
|
155
|
+
stallSkip: 0.25,
|
|
156
|
+
maxAttempts: 1,
|
|
157
|
+
});
|
|
158
|
+
// Threshold 1s = two 0.5s polls; one nudge of 0.25, then it gives up.
|
|
159
|
+
for (let i = 0; i < 6; i++)
|
|
160
|
+
await clock.tick(0.5);
|
|
161
|
+
expect(media.currentTime).toBeCloseTo(10.25);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createStallMonitor.test.d.ts","sourceRoot":"","sources":["../../../../../test/unit/playback/createStallMonitor.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { createStallMonitor, StallEndedReason, } from '@amazon/vinyl';
|
|
6
|
+
import { EventHostImpl } from '@amazon/vinyl-util';
|
|
7
|
+
import { useMockTime } from '@amazon/vinyl-util/browserTestUtil';
|
|
8
|
+
describe('createStallMonitor', () => {
|
|
9
|
+
const clock = useMockTime();
|
|
10
|
+
let host;
|
|
11
|
+
let onStallEntered;
|
|
12
|
+
let onStallEnded;
|
|
13
|
+
let stop;
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
host = new EventHostImpl();
|
|
16
|
+
onStallEntered = jasmine.createSpy('onStallEntered');
|
|
17
|
+
onStallEnded = jasmine.createSpy('onStallEnded');
|
|
18
|
+
});
|
|
19
|
+
afterEach(() => stop?.());
|
|
20
|
+
function monitor(overrides = {}) {
|
|
21
|
+
stop = createStallMonitor(host, { onStallEntered, onStallEnded }, overrides);
|
|
22
|
+
}
|
|
23
|
+
/** Advances the mock clock by `n` default (0.25s) poll intervals. */
|
|
24
|
+
async function poll(n) {
|
|
25
|
+
for (let i = 0; i < n; i++)
|
|
26
|
+
await clock.tick(0.25);
|
|
27
|
+
}
|
|
28
|
+
it('does not report a stall before playback is observed', async () => {
|
|
29
|
+
monitor();
|
|
30
|
+
// No 'playing' has been observed (e.g. initial load / awaiting a seek).
|
|
31
|
+
await poll(8);
|
|
32
|
+
expect(onStallEntered).not.toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
it('reports a stall once the play head is frozen past the threshold', async () => {
|
|
35
|
+
monitor();
|
|
36
|
+
host.dispatch('playing', {});
|
|
37
|
+
await poll(3); // 0.75s < 1s threshold
|
|
38
|
+
expect(onStallEntered).not.toHaveBeenCalled();
|
|
39
|
+
await poll(1); // 1s
|
|
40
|
+
expect(onStallEntered).toHaveBeenCalledTimes(1);
|
|
41
|
+
});
|
|
42
|
+
it('honors a configurable threshold', async () => {
|
|
43
|
+
monitor({ stallThreshold: 2 });
|
|
44
|
+
host.dispatch('playing', {});
|
|
45
|
+
await poll(4); // 1s < 2s
|
|
46
|
+
expect(onStallEntered).not.toHaveBeenCalled();
|
|
47
|
+
await poll(4); // 2s
|
|
48
|
+
expect(onStallEntered).toHaveBeenCalledTimes(1);
|
|
49
|
+
});
|
|
50
|
+
it('does not report a stall while timeUpdates keep arriving', async () => {
|
|
51
|
+
monitor();
|
|
52
|
+
host.dispatch('playing', {});
|
|
53
|
+
for (let i = 0; i < 10; i++) {
|
|
54
|
+
await poll(2); // 0.5s, under the threshold
|
|
55
|
+
host.dispatch('timeUpdate', { previous: i, current: i + 1 });
|
|
56
|
+
}
|
|
57
|
+
expect(onStallEntered).not.toHaveBeenCalled();
|
|
58
|
+
});
|
|
59
|
+
it('ends a stall with reason "playing" when the play head advances, measuring from the last timeUpdate', async () => {
|
|
60
|
+
monitor();
|
|
61
|
+
host.dispatch('playing', {});
|
|
62
|
+
host.dispatch('timeUpdate', { previous: 0, current: 1 });
|
|
63
|
+
await poll(4); // freeze 1s -> stallEntered
|
|
64
|
+
expect(onStallEntered).toHaveBeenCalledTimes(1);
|
|
65
|
+
await poll(4); // frozen another 1s (total 2s)
|
|
66
|
+
host.dispatch('timeUpdate', { previous: 1, current: 2 }); // resume
|
|
67
|
+
expect(onStallEnded).toHaveBeenCalledTimes(1);
|
|
68
|
+
const info = onStallEnded.calls.mostRecent().args[0];
|
|
69
|
+
expect(info.reason).toBe(StallEndedReason.PLAYING);
|
|
70
|
+
expect(info.duration).toBeCloseTo(2, 1);
|
|
71
|
+
});
|
|
72
|
+
for (const [event, reason] of [
|
|
73
|
+
['pause', StallEndedReason.PAUSE],
|
|
74
|
+
['seeking', StallEndedReason.SEEKING],
|
|
75
|
+
['emptied', StallEndedReason.EMPTIED],
|
|
76
|
+
]) {
|
|
77
|
+
it(`ends a stall with reason "${reason}" on ${event}`, async () => {
|
|
78
|
+
monitor();
|
|
79
|
+
host.dispatch('playing', {});
|
|
80
|
+
await poll(4); // stallEntered
|
|
81
|
+
expect(onStallEntered).toHaveBeenCalledTimes(1);
|
|
82
|
+
host.dispatch(event, {});
|
|
83
|
+
expect(onStallEnded).toHaveBeenCalledTimes(1);
|
|
84
|
+
expect(onStallEnded.calls.mostRecent().args[0].reason).toBe(reason);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
it('ends a stall with reason "playing" when a playing event fires (resume without a timeUpdate)', async () => {
|
|
88
|
+
monitor();
|
|
89
|
+
host.dispatch('playing', {});
|
|
90
|
+
await poll(4); // stallEntered
|
|
91
|
+
expect(onStallEntered).toHaveBeenCalledTimes(1);
|
|
92
|
+
// A 'playing' event (rather than a timeUpdate) signals the resume.
|
|
93
|
+
host.dispatch('playing', {});
|
|
94
|
+
expect(onStallEnded).toHaveBeenCalledTimes(1);
|
|
95
|
+
expect(onStallEnded.calls.mostRecent().args[0].reason).toBe(StallEndedReason.PLAYING);
|
|
96
|
+
});
|
|
97
|
+
it('suspends detection after a seek until playback is observed again', async () => {
|
|
98
|
+
monitor();
|
|
99
|
+
host.dispatch('playing', {});
|
|
100
|
+
host.dispatch('seeking', {}); // suspends; observedPlaying = false
|
|
101
|
+
await poll(8);
|
|
102
|
+
expect(onStallEntered).not.toHaveBeenCalled();
|
|
103
|
+
// Once playback is observed again, detection resumes.
|
|
104
|
+
host.dispatch('playing', {});
|
|
105
|
+
await poll(4);
|
|
106
|
+
expect(onStallEntered).toHaveBeenCalledTimes(1);
|
|
107
|
+
});
|
|
108
|
+
it('reports each stall/recovery cycle', async () => {
|
|
109
|
+
monitor();
|
|
110
|
+
host.dispatch('playing', {});
|
|
111
|
+
await poll(4);
|
|
112
|
+
host.dispatch('timeUpdate', { previous: 0, current: 1 }); // resume 1
|
|
113
|
+
await poll(4);
|
|
114
|
+
host.dispatch('timeUpdate', { previous: 1, current: 2 }); // resume 2
|
|
115
|
+
expect(onStallEntered).toHaveBeenCalledTimes(2);
|
|
116
|
+
expect(onStallEnded).toHaveBeenCalledTimes(2);
|
|
117
|
+
});
|
|
118
|
+
it('does not end and re-detect a stall on a non-advancing timeUpdate', async () => {
|
|
119
|
+
// Browsers emit `timeUpdate` even while the play head is frozen, with an unchanged (or
|
|
120
|
+
// briefly backward) currentTime. These must not be treated as progress, or the stall is
|
|
121
|
+
// ended and immediately re-detected, doubling the reported events.
|
|
122
|
+
monitor();
|
|
123
|
+
host.dispatch('playing', {});
|
|
124
|
+
host.dispatch('timeUpdate', { previous: 4, current: 5 });
|
|
125
|
+
await poll(4); // freeze 1s -> stallEntered
|
|
126
|
+
expect(onStallEntered).toHaveBeenCalledTimes(1);
|
|
127
|
+
// Still frozen: currentTime hasn't advanced (unchanged, backward, or sub-epsilon jitter).
|
|
128
|
+
host.dispatch('timeUpdate', { previous: 5, current: 5 });
|
|
129
|
+
host.dispatch('timeUpdate', { previous: 5, current: 4.99 });
|
|
130
|
+
host.dispatch('timeUpdate', { previous: 5, current: 5 + 1e-9 });
|
|
131
|
+
await poll(4);
|
|
132
|
+
expect(onStallEnded).not.toHaveBeenCalled();
|
|
133
|
+
expect(onStallEntered).toHaveBeenCalledTimes(1);
|
|
134
|
+
// A real forward advance ends the stall exactly once.
|
|
135
|
+
host.dispatch('timeUpdate', { previous: 5, current: 6 });
|
|
136
|
+
expect(onStallEnded).toHaveBeenCalledTimes(1);
|
|
137
|
+
expect(onStallEntered).toHaveBeenCalledTimes(1);
|
|
138
|
+
});
|
|
139
|
+
it('does not re-detect a stall immediately after resuming via a playing event', async () => {
|
|
140
|
+
// Resuming via `playing` must restart the freeze clock. Otherwise the poll, still seeing
|
|
141
|
+
// the stale pre-freeze progress time, immediately re-enters a stall before the first
|
|
142
|
+
// post-resume timeUpdate arrives — an extra, spurious stall.
|
|
143
|
+
monitor();
|
|
144
|
+
host.dispatch('playing', {});
|
|
145
|
+
host.dispatch('timeUpdate', { previous: 0, current: 1 });
|
|
146
|
+
await poll(4); // freeze 1s -> stallEntered
|
|
147
|
+
expect(onStallEntered).toHaveBeenCalledTimes(1);
|
|
148
|
+
// Resume with a 'playing' event and no timeUpdate yet.
|
|
149
|
+
host.dispatch('playing', {});
|
|
150
|
+
expect(onStallEnded).toHaveBeenCalledTimes(1);
|
|
151
|
+
// The freeze clock reset on resume, so a sub-threshold gap is not a new stall.
|
|
152
|
+
await poll(3); // 0.75s < 1s threshold
|
|
153
|
+
expect(onStallEntered).toHaveBeenCalledTimes(1);
|
|
154
|
+
});
|
|
155
|
+
it('stops reporting after disposal', async () => {
|
|
156
|
+
monitor();
|
|
157
|
+
host.dispatch('playing', {});
|
|
158
|
+
stop();
|
|
159
|
+
stop = undefined; // disposal is single-shot; don't dispose again in afterEach
|
|
160
|
+
await poll(8);
|
|
161
|
+
expect(onStallEntered).not.toHaveBeenCalled();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -207,6 +207,21 @@ describe('MseTrack', () => {
|
|
|
207
207
|
expect(deps.playbackSource.load).toHaveBeenCalledOnceWith();
|
|
208
208
|
expect(deps.playbackController.pause).toHaveBeenCalledOnceWith();
|
|
209
209
|
});
|
|
210
|
+
it('aborts the drm session abort so key sessions close', async () => {
|
|
211
|
+
// Regression: MSE key sessions must be tied to the per-activation
|
|
212
|
+
// drmSessionAbort (fired by closeDrmSessions on deactivate), not a
|
|
213
|
+
// separate abort that never fires — otherwise sessions leak.
|
|
214
|
+
track = createTrack();
|
|
215
|
+
await awaitContentTypes();
|
|
216
|
+
getAudioStream().streamingQuality =
|
|
217
|
+
createEmptyMediaQualityMetadata();
|
|
218
|
+
track.activate({});
|
|
219
|
+
const abort = deps.drmController.initializeForPlayback.calls.mostRecent()
|
|
220
|
+
.args[1];
|
|
221
|
+
expect(abort.aborted()).toBeFalse();
|
|
222
|
+
track.deactivate();
|
|
223
|
+
expect(abort.aborted()).toBeTrue();
|
|
224
|
+
});
|
|
210
225
|
});
|
|
211
226
|
describe('error', () => {
|
|
212
227
|
it('returns the first emitted error', async () => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createTimelineContentTypesValue.test.d.ts","sourceRoot":"","sources":["../../../../../test/unit/streaming/createTimelineContentTypesValue.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { createEmptyMediaQualityMetadata, createTimelineContentTypesValue, } from '@amazon/vinyl';
|
|
6
|
+
import { data } from '@amazon/vinyl-observable';
|
|
7
|
+
function createQuality(contentType) {
|
|
8
|
+
return {
|
|
9
|
+
metadata: {
|
|
10
|
+
...createEmptyMediaQualityMetadata(),
|
|
11
|
+
contentType,
|
|
12
|
+
},
|
|
13
|
+
getSegment: () => Promise.resolve(null),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function timelineOf(...contentTypes) {
|
|
17
|
+
return {
|
|
18
|
+
periods: contentTypes.map((types, index) => ({
|
|
19
|
+
startTime: index * 10,
|
|
20
|
+
endTime: index * 10 + 10,
|
|
21
|
+
qualities: types.map(createQuality),
|
|
22
|
+
})),
|
|
23
|
+
minBufferTime: 2,
|
|
24
|
+
getAdBreaks: () => Promise.resolve([]),
|
|
25
|
+
getDuration: () => Promise.resolve(Infinity),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
describe('createTimelineContentTypesValue', () => {
|
|
29
|
+
function contentTypesOf(timeline) {
|
|
30
|
+
return createTimelineContentTypesValue({
|
|
31
|
+
mediaTimelineTransformed: data(Promise.resolve(timeline)),
|
|
32
|
+
}).value;
|
|
33
|
+
}
|
|
34
|
+
it('reports every media content type present in the filtered timeline', async () => {
|
|
35
|
+
const result = await contentTypesOf(timelineOf(['audio', 'audio', 'video']));
|
|
36
|
+
expect(result).toEqual(new Set(['audio', 'video']));
|
|
37
|
+
});
|
|
38
|
+
it('omits a content type whose renditions were all filtered out', async () => {
|
|
39
|
+
// Every audio rendition dropped (e.g. by the sample-rate filter): video
|
|
40
|
+
// must still be reported so the load is not stranded waiting for an
|
|
41
|
+
// audio source buffer that will never be created.
|
|
42
|
+
const result = await contentTypesOf(timelineOf(['video']));
|
|
43
|
+
expect(result).toEqual(new Set(['video']));
|
|
44
|
+
});
|
|
45
|
+
it('reports only audio when every video rendition was filtered out', async () => {
|
|
46
|
+
const result = await contentTypesOf(timelineOf(['audio']));
|
|
47
|
+
expect(result).toEqual(new Set(['audio']));
|
|
48
|
+
});
|
|
49
|
+
it('excludes text — captions are a sidecar pipeline, not an MSE source buffer', async () => {
|
|
50
|
+
const result = await contentTypesOf(timelineOf(['audio', 'video', 'text']));
|
|
51
|
+
expect(result).toEqual(new Set(['audio', 'video']));
|
|
52
|
+
});
|
|
53
|
+
it('unions content types across periods', async () => {
|
|
54
|
+
const result = await contentTypesOf(timelineOf(['video'], ['audio', 'video']));
|
|
55
|
+
expect(result).toEqual(new Set(['audio', 'video']));
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TextTrack.test.d.ts","sourceRoot":"","sources":["../../../../../test/unit/text/TextTrack.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { expectTypeStrictlyEquals } from '@amazon/vinyl-util/browserTestUtil';
|
|
6
|
+
describe('ALL_TEXT_TRACK_EVENTS', () => {
|
|
7
|
+
it('provides a comprehensive list of all text track events', () => {
|
|
8
|
+
// The player redispatches exactly these; an omission silently stops an
|
|
9
|
+
// event reaching consumers, and `satisfies` alone does not catch it.
|
|
10
|
+
expectTypeStrictlyEquals(true);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -42,6 +42,19 @@ describe('discoverDashTextTracks', () => {
|
|
|
42
42
|
expect(result.length).toBe(1);
|
|
43
43
|
expect(result[0].language).toBe('es');
|
|
44
44
|
});
|
|
45
|
+
it('discovers a text AdaptationSet by its Representation mimeType', () => {
|
|
46
|
+
// DASH allows @mimeType on the Representation instead of the set.
|
|
47
|
+
const m = manifest(`
|
|
48
|
+
<AdaptationSet lang="fr">
|
|
49
|
+
<Representation id="fr-sub" mimeType="text/vtt" bandwidth="100">
|
|
50
|
+
<BaseURL>fr.vtt</BaseURL>
|
|
51
|
+
</Representation>
|
|
52
|
+
</AdaptationSet>`);
|
|
53
|
+
const result = discoverDashTextTracks(m, baseUrl);
|
|
54
|
+
expect(result.length).toBe(1);
|
|
55
|
+
expect(result[0].language).toBe('fr');
|
|
56
|
+
expect(result[0].uri).toBe('https://example.com/dash/fr.vtt');
|
|
57
|
+
});
|
|
45
58
|
it('classifies kind=captions when role=caption is set', () => {
|
|
46
59
|
const m = manifest(`
|
|
47
60
|
<AdaptationSet contentType="text" lang="en" mimeType="text/vtt">
|
|
@@ -96,6 +109,10 @@ describe('discoverDashTextTracks', () => {
|
|
|
96
109
|
</AdaptationSet>`);
|
|
97
110
|
expect(discoverDashTextTracks(m, baseUrl)).toEqual([]);
|
|
98
111
|
});
|
|
112
|
+
it('skips a non-text AdaptationSet with no Representation', () => {
|
|
113
|
+
const m = manifest(`<AdaptationSet contentType="audio"/>`);
|
|
114
|
+
expect(discoverDashTextTracks(m, baseUrl)).toEqual([]);
|
|
115
|
+
});
|
|
99
116
|
it('skips text AdaptationSets carrying segmented codecs (stpp/wvtt)', () => {
|
|
100
117
|
const m = manifest(`
|
|
101
118
|
<AdaptationSet contentType="text" mimeType="application/mp4" codecs="stpp">
|
|
@@ -382,6 +382,32 @@ hi`;
|
|
|
382
382
|
expect(cues.length).toBe(1);
|
|
383
383
|
expect(cues[0].text).toBe('hi');
|
|
384
384
|
});
|
|
385
|
+
it('parses a cue that follows the signature with no blank line', () => {
|
|
386
|
+
// The header block ends at a timing line as well as at a blank line
|
|
387
|
+
// (WebVTT file parsing breaks out of the header loop on `-->`), so a
|
|
388
|
+
// document omitting the blank line still yields its cues.
|
|
389
|
+
const text = `WEBVTT
|
|
390
|
+
00:00:01.000 --> 00:00:02.000
|
|
391
|
+
hi
|
|
392
|
+
|
|
393
|
+
00:00:03.000 --> 00:00:04.000
|
|
394
|
+
there`;
|
|
395
|
+
const cues = parseWebVtt(text).cues;
|
|
396
|
+
expect(cues.length).toBe(2);
|
|
397
|
+
expect(cues[0].startTime).toBe(1);
|
|
398
|
+
expect(cues[0].text).toBe('hi');
|
|
399
|
+
expect(cues[1].text).toBe('there');
|
|
400
|
+
});
|
|
401
|
+
it('keeps header lines as header when a cue follows with no blank line', () => {
|
|
402
|
+
const text = `WEBVTT
|
|
403
|
+
Kind: captions
|
|
404
|
+
00:00:01.000 --> 00:00:02.000
|
|
405
|
+
hi`;
|
|
406
|
+
const cues = parseWebVtt(text).cues;
|
|
407
|
+
expect(cues.length).toBe(1);
|
|
408
|
+
expect(cues[0].id).toBeNull();
|
|
409
|
+
expect(cues[0].text).toBe('hi');
|
|
410
|
+
});
|
|
385
411
|
it('handles trailing identifier without timing line', () => {
|
|
386
412
|
// Identifier with no following timing line - cue is dropped, parser
|
|
387
413
|
// breaks out cleanly without throwing.
|