@amazon/vinyl 3.2.0 → 3.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 +65 -45
- package/dist/index.cjs.map +2 -2
- package/dist/index.js +90 -69
- package/dist/index.js.map +3 -3
- package/dist/test/integ.es5.js +303 -287
- package/dist/test/integ.js +328 -71
- package/dist/test/integ.js.map +2 -2
- 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 +42 -20
- 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 +11 -1
- package/dist/types/current/playback/PlaybackControllerImpl.d.ts +8 -2
- package/dist/types/current/playback/PlaybackControllerImpl.d.ts.map +1 -1
- package/dist/types/current/playback/PlaybackControllerImpl.js +38 -33
- package/dist/types/current/playback/createStallMonitor.d.ts.map +1 -1
- package/dist/types/current/playback/createStallMonitor.js +19 -14
- 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 +5 -6
- 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/PlaybackControllerImpl.d.ts +8 -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/PlaybackControllerImpl.d.ts +8 -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/PlaybackControllerImpl.d.ts +8 -2
- package/dist/types/ts4.7/track/mse/MseTrack.d.ts +0 -1
- package/dist/unit/types/current/ad/AdControllerImpl.test.js +110 -4
- package/dist/unit/types/current/drm/DrmControllerImpl.test.js +19 -1
- package/dist/unit/types/current/playback/PlaybackControllerImpl.test.js +15 -0
- package/dist/unit/types/current/playback/createStallMonitor.test.js +38 -0
- package/dist/unit/types/current/streaming/MseTrack.test.js +15 -0
- package/dist/unit/types/current/tsconfig.tsbuildinfo +1 -1
- package/dist/vinylTestUtil/index.js +10 -1
- package/dist/vinylTestUtil/index.js.map +2 -2
- package/dist/vinylTestUtil/types/current/assets/vinylTestAssets.d.ts +5 -0
- package/dist/vinylTestUtil/types/current/assets/vinylTestAssets.d.ts.map +1 -1
- package/dist/vinylTestUtil/types/current/assets/vinylTestAssets.js +9 -0
- package/dist/vinylTestUtil/types/current/tsconfig.tsbuildinfo +1 -1
- package/dist/vinylTestUtil/types/ts3.7/assets/vinylTestAssets.d.ts +5 -0
- package/dist/vinylTestUtil/types/ts4.1/assets/vinylTestAssets.d.ts +5 -0
- package/dist/vinylTestUtil/types/ts4.7/assets/vinylTestAssets.d.ts +5 -0
- package/package.json +10 -10
|
@@ -7,6 +7,15 @@ import { AdsProvider } from './AdsProvider';
|
|
|
7
7
|
* The % playhead start tolerance for playback progress when emitting ad progress events.
|
|
8
8
|
*/
|
|
9
9
|
export declare const AD_START_TOLERANCE = 0.05;
|
|
10
|
+
/**
|
|
11
|
+
* When a break's X-PLAYOUT-LIMIT budget is reached, an ad whose real media end is
|
|
12
|
+
* within this many seconds is allowed to finish on its own `ended` rather than
|
|
13
|
+
* being cut. The limit is a coarse, often whole-second budget while the media's
|
|
14
|
+
* true length is fractional, so without this grace an ad effectively as long as
|
|
15
|
+
* its budget would be clipped a fraction short. The tolerance is measured against
|
|
16
|
+
* the playback-reported (media) duration, not the ad's declared duration.
|
|
17
|
+
*/
|
|
18
|
+
export declare const PLAYOUT_LIMIT_TOLERANCE = 1;
|
|
10
19
|
export interface AdControllerImplDeps {
|
|
11
20
|
readonly playbackController: ReadonlyPlaybackController;
|
|
12
21
|
}
|
|
@@ -129,8 +138,15 @@ export declare class AdControllerImpl extends EventHostImpl<AdEventMap> implemen
|
|
|
129
138
|
private emitAdProgressEvents;
|
|
130
139
|
private emitTimeUpdate;
|
|
131
140
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
141
|
+
* Enforces the break-level playout limit (X-PLAYOUT-LIMIT): a budget shared
|
|
142
|
+
* across all of the break's ads. When cumulative playout reaches it, the
|
|
143
|
+
* whole break ends, dropping any ads that have not started.
|
|
144
|
+
*
|
|
145
|
+
* This is deliberately NOT a per-ad cap. Each ad asset is its own source and
|
|
146
|
+
* ends on its media `ended` (see the 'ended' handler), which is what advances
|
|
147
|
+
* a multi-ad break. Ending an ad at its declared per-ad DURATION instead would
|
|
148
|
+
* truncate the real asset whenever a packager under-declares it — e.g. rounds
|
|
149
|
+
* a 6.732s asset down to a whole-second 6 — cutting off the fractional tail.
|
|
134
150
|
*/
|
|
135
151
|
private enforcePlayoutLimits;
|
|
136
152
|
private setPendingBreaks;
|
|
@@ -12,6 +12,12 @@ import { TrackUri } from '../track/Track';
|
|
|
12
12
|
* indicates a stalled promise result from the license provider.
|
|
13
13
|
*/
|
|
14
14
|
export declare const LICENSE_TIMEOUT = 90;
|
|
15
|
+
/**
|
|
16
|
+
* An excessive number of concurrent key sessions. Sessions are expected to be
|
|
17
|
+
* closed as tracks deactivate; a count this high signals sessions are leaking
|
|
18
|
+
* (never closing) rather than a legitimate playback need.
|
|
19
|
+
*/
|
|
20
|
+
export declare const EXCESSIVE_SESSION_COUNT = 20;
|
|
15
21
|
/**
|
|
16
22
|
* Dependencies for creating DrmController.
|
|
17
23
|
*/
|
|
@@ -63,12 +63,11 @@ export declare class PlaybackControllerImpl extends EventHostImpl<PlaybackContro
|
|
|
63
63
|
private _playing;
|
|
64
64
|
private _seeking;
|
|
65
65
|
private _waiting;
|
|
66
|
-
private
|
|
66
|
+
private readonly seekAbortSlot;
|
|
67
67
|
private stopStallDetection;
|
|
68
68
|
private userVolume;
|
|
69
69
|
private _pendingPlay;
|
|
70
70
|
private playAbort;
|
|
71
|
-
private disposeAbort;
|
|
72
71
|
readonly options: PlaybackControllerImplOptions;
|
|
73
72
|
constructor(deps: PlaybackControllerImplDeps, options?: Partial<PlaybackControllerImplOptions>);
|
|
74
73
|
/**
|
|
@@ -174,6 +173,13 @@ export declare class PlaybackControllerImpl extends EventHostImpl<PlaybackContro
|
|
|
174
173
|
get hasMetadata(): boolean;
|
|
175
174
|
get playIsPending(): boolean;
|
|
176
175
|
seekTo(time: number, tolerance?: number): Promise<void>;
|
|
176
|
+
/**
|
|
177
|
+
* The body of a single {@link seekTo} attempt. Extracted so {@link seekTo}
|
|
178
|
+
* can wrap it with per-seek abort/supersession handling. `seekAbort` is
|
|
179
|
+
* aborted when a newer seek supersedes this one (or on dispose).
|
|
180
|
+
* @private
|
|
181
|
+
*/
|
|
182
|
+
private doSeek;
|
|
177
183
|
/**
|
|
178
184
|
* Dispatches a readyStateChange event when {@link readyState} changes.
|
|
179
185
|
* @private
|
|
@@ -45,7 +45,6 @@ export declare class MseTrack extends TrackBase {
|
|
|
45
45
|
protected readonly deps: MseTrackDeps;
|
|
46
46
|
get textTrackController(): (TextTrackController | null);
|
|
47
47
|
private readonly streams;
|
|
48
|
-
private readonly disposeAbort;
|
|
49
48
|
private lastPreloadOptions;
|
|
50
49
|
private readonly allFetchedRanges;
|
|
51
50
|
private readonly _fetchedRanges;
|
|
@@ -7,6 +7,15 @@ import type { AdsProvider } from './AdsProvider';
|
|
|
7
7
|
* The % playhead start tolerance for playback progress when emitting ad progress events.
|
|
8
8
|
*/
|
|
9
9
|
export declare const AD_START_TOLERANCE = 0.05;
|
|
10
|
+
/**
|
|
11
|
+
* When a break's X-PLAYOUT-LIMIT budget is reached, an ad whose real media end is
|
|
12
|
+
* within this many seconds is allowed to finish on its own `ended` rather than
|
|
13
|
+
* being cut. The limit is a coarse, often whole-second budget while the media's
|
|
14
|
+
* true length is fractional, so without this grace an ad effectively as long as
|
|
15
|
+
* its budget would be clipped a fraction short. The tolerance is measured against
|
|
16
|
+
* the playback-reported (media) duration, not the ad's declared duration.
|
|
17
|
+
*/
|
|
18
|
+
export declare const PLAYOUT_LIMIT_TOLERANCE = 1;
|
|
10
19
|
export interface AdControllerImplDeps {
|
|
11
20
|
readonly playbackController: ReadonlyPlaybackController;
|
|
12
21
|
}
|
|
@@ -129,8 +138,15 @@ export declare class AdControllerImpl extends EventHostImpl<AdEventMap> implemen
|
|
|
129
138
|
private emitAdProgressEvents;
|
|
130
139
|
private emitTimeUpdate;
|
|
131
140
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
141
|
+
* Enforces the break-level playout limit (X-PLAYOUT-LIMIT): a budget shared
|
|
142
|
+
* across all of the break's ads. When cumulative playout reaches it, the
|
|
143
|
+
* whole break ends, dropping any ads that have not started.
|
|
144
|
+
*
|
|
145
|
+
* This is deliberately NOT a per-ad cap. Each ad asset is its own source and
|
|
146
|
+
* ends on its media `ended` (see the 'ended' handler), which is what advances
|
|
147
|
+
* a multi-ad break. Ending an ad at its declared per-ad DURATION instead would
|
|
148
|
+
* truncate the real asset whenever a packager under-declares it — e.g. rounds
|
|
149
|
+
* a 6.732s asset down to a whole-second 6 — cutting off the fractional tail.
|
|
134
150
|
*/
|
|
135
151
|
private enforcePlayoutLimits;
|
|
136
152
|
private setPendingBreaks;
|
|
@@ -13,6 +13,12 @@ import type { TrackUri } from '../track/Track';
|
|
|
13
13
|
* indicates a stalled promise result from the license provider.
|
|
14
14
|
*/
|
|
15
15
|
export declare const LICENSE_TIMEOUT = 90;
|
|
16
|
+
/**
|
|
17
|
+
* An excessive number of concurrent key sessions. Sessions are expected to be
|
|
18
|
+
* closed as tracks deactivate; a count this high signals sessions are leaking
|
|
19
|
+
* (never closing) rather than a legitimate playback need.
|
|
20
|
+
*/
|
|
21
|
+
export declare const EXCESSIVE_SESSION_COUNT = 20;
|
|
16
22
|
/**
|
|
17
23
|
* Dependencies for creating DrmController.
|
|
18
24
|
*/
|
|
@@ -65,12 +65,11 @@ export declare class PlaybackControllerImpl extends EventHostImpl<PlaybackContro
|
|
|
65
65
|
private _playing;
|
|
66
66
|
private _seeking;
|
|
67
67
|
private _waiting;
|
|
68
|
-
private
|
|
68
|
+
private readonly seekAbortSlot;
|
|
69
69
|
private stopStallDetection;
|
|
70
70
|
private userVolume;
|
|
71
71
|
private _pendingPlay;
|
|
72
72
|
private playAbort;
|
|
73
|
-
private disposeAbort;
|
|
74
73
|
readonly options: PlaybackControllerImplOptions;
|
|
75
74
|
constructor(deps: PlaybackControllerImplDeps, options?: Partial<PlaybackControllerImplOptions>);
|
|
76
75
|
/**
|
|
@@ -176,6 +175,13 @@ export declare class PlaybackControllerImpl extends EventHostImpl<PlaybackContro
|
|
|
176
175
|
get hasMetadata(): boolean;
|
|
177
176
|
get playIsPending(): boolean;
|
|
178
177
|
seekTo(time: number, tolerance?: number): Promise<void>;
|
|
178
|
+
/**
|
|
179
|
+
* The body of a single {@link seekTo} attempt. Extracted so {@link seekTo}
|
|
180
|
+
* can wrap it with per-seek abort/supersession handling. `seekAbort` is
|
|
181
|
+
* aborted when a newer seek supersedes this one (or on dispose).
|
|
182
|
+
* @private
|
|
183
|
+
*/
|
|
184
|
+
private doSeek;
|
|
179
185
|
/**
|
|
180
186
|
* Dispatches a readyStateChange event when {@link readyState} changes.
|
|
181
187
|
* @private
|
|
@@ -46,7 +46,6 @@ export declare class MseTrack extends TrackBase {
|
|
|
46
46
|
protected readonly deps: MseTrackDeps;
|
|
47
47
|
get textTrackController(): (TextTrackController | null);
|
|
48
48
|
private readonly streams;
|
|
49
|
-
private readonly disposeAbort;
|
|
50
49
|
private lastPreloadOptions;
|
|
51
50
|
private readonly allFetchedRanges;
|
|
52
51
|
private readonly _fetchedRanges;
|
|
@@ -7,6 +7,15 @@ import type { AdsProvider } from './AdsProvider';
|
|
|
7
7
|
* The % playhead start tolerance for playback progress when emitting ad progress events.
|
|
8
8
|
*/
|
|
9
9
|
export declare const AD_START_TOLERANCE = 0.05;
|
|
10
|
+
/**
|
|
11
|
+
* When a break's X-PLAYOUT-LIMIT budget is reached, an ad whose real media end is
|
|
12
|
+
* within this many seconds is allowed to finish on its own `ended` rather than
|
|
13
|
+
* being cut. The limit is a coarse, often whole-second budget while the media's
|
|
14
|
+
* true length is fractional, so without this grace an ad effectively as long as
|
|
15
|
+
* its budget would be clipped a fraction short. The tolerance is measured against
|
|
16
|
+
* the playback-reported (media) duration, not the ad's declared duration.
|
|
17
|
+
*/
|
|
18
|
+
export declare const PLAYOUT_LIMIT_TOLERANCE = 1;
|
|
10
19
|
export interface AdControllerImplDeps {
|
|
11
20
|
readonly playbackController: ReadonlyPlaybackController;
|
|
12
21
|
}
|
|
@@ -129,8 +138,15 @@ export declare class AdControllerImpl extends EventHostImpl<AdEventMap> implemen
|
|
|
129
138
|
private emitAdProgressEvents;
|
|
130
139
|
private emitTimeUpdate;
|
|
131
140
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
141
|
+
* Enforces the break-level playout limit (X-PLAYOUT-LIMIT): a budget shared
|
|
142
|
+
* across all of the break's ads. When cumulative playout reaches it, the
|
|
143
|
+
* whole break ends, dropping any ads that have not started.
|
|
144
|
+
*
|
|
145
|
+
* This is deliberately NOT a per-ad cap. Each ad asset is its own source and
|
|
146
|
+
* ends on its media `ended` (see the 'ended' handler), which is what advances
|
|
147
|
+
* a multi-ad break. Ending an ad at its declared per-ad DURATION instead would
|
|
148
|
+
* truncate the real asset whenever a packager under-declares it — e.g. rounds
|
|
149
|
+
* a 6.732s asset down to a whole-second 6 — cutting off the fractional tail.
|
|
134
150
|
*/
|
|
135
151
|
private enforcePlayoutLimits;
|
|
136
152
|
private setPendingBreaks;
|
|
@@ -12,6 +12,12 @@ import type { TrackUri } from '../track/Track';
|
|
|
12
12
|
* indicates a stalled promise result from the license provider.
|
|
13
13
|
*/
|
|
14
14
|
export declare const LICENSE_TIMEOUT = 90;
|
|
15
|
+
/**
|
|
16
|
+
* An excessive number of concurrent key sessions. Sessions are expected to be
|
|
17
|
+
* closed as tracks deactivate; a count this high signals sessions are leaking
|
|
18
|
+
* (never closing) rather than a legitimate playback need.
|
|
19
|
+
*/
|
|
20
|
+
export declare const EXCESSIVE_SESSION_COUNT = 20;
|
|
15
21
|
/**
|
|
16
22
|
* Dependencies for creating DrmController.
|
|
17
23
|
*/
|
|
@@ -63,12 +63,11 @@ export declare class PlaybackControllerImpl extends EventHostImpl<PlaybackContro
|
|
|
63
63
|
private _playing;
|
|
64
64
|
private _seeking;
|
|
65
65
|
private _waiting;
|
|
66
|
-
private
|
|
66
|
+
private readonly seekAbortSlot;
|
|
67
67
|
private stopStallDetection;
|
|
68
68
|
private userVolume;
|
|
69
69
|
private _pendingPlay;
|
|
70
70
|
private playAbort;
|
|
71
|
-
private disposeAbort;
|
|
72
71
|
readonly options: PlaybackControllerImplOptions;
|
|
73
72
|
constructor(deps: PlaybackControllerImplDeps, options?: Partial<PlaybackControllerImplOptions>);
|
|
74
73
|
/**
|
|
@@ -174,6 +173,13 @@ export declare class PlaybackControllerImpl extends EventHostImpl<PlaybackContro
|
|
|
174
173
|
get hasMetadata(): boolean;
|
|
175
174
|
get playIsPending(): boolean;
|
|
176
175
|
seekTo(time: number, tolerance?: number): Promise<void>;
|
|
176
|
+
/**
|
|
177
|
+
* The body of a single {@link seekTo} attempt. Extracted so {@link seekTo}
|
|
178
|
+
* can wrap it with per-seek abort/supersession handling. `seekAbort` is
|
|
179
|
+
* aborted when a newer seek supersedes this one (or on dispose).
|
|
180
|
+
* @private
|
|
181
|
+
*/
|
|
182
|
+
private doSeek;
|
|
177
183
|
/**
|
|
178
184
|
* Dispatches a readyStateChange event when {@link readyState} changes.
|
|
179
185
|
* @private
|
|
@@ -45,7 +45,6 @@ export declare class MseTrack extends TrackBase {
|
|
|
45
45
|
protected readonly deps: MseTrackDeps;
|
|
46
46
|
get textTrackController(): TextTrackController | null;
|
|
47
47
|
private readonly streams;
|
|
48
|
-
private readonly disposeAbort;
|
|
49
48
|
private lastPreloadOptions;
|
|
50
49
|
private readonly allFetchedRanges;
|
|
51
50
|
private readonly _fetchedRanges;
|
|
@@ -896,7 +896,6 @@ describe('AdControllerImpl', () => {
|
|
|
896
896
|
await setContent(c, trackAds(makeBreak({
|
|
897
897
|
startTime: 10,
|
|
898
898
|
duration: 20,
|
|
899
|
-
// Null per-ad duration so the per-ad cap does not end it.
|
|
900
899
|
ads: [
|
|
901
900
|
{
|
|
902
901
|
id: 'a1',
|
|
@@ -990,7 +989,11 @@ describe('AdControllerImpl', () => {
|
|
|
990
989
|
});
|
|
991
990
|
});
|
|
992
991
|
describe('ad duration and playout limits', () => {
|
|
993
|
-
it('advances to the next ad
|
|
992
|
+
it('advances to the next ad on the ad media ended, not its declared duration', async () => {
|
|
993
|
+
// Each ad asset is its own source; a non-final ad advances on its own
|
|
994
|
+
// media `ended`. The declared per-ad duration must NOT end it early —
|
|
995
|
+
// a packager may under-declare it (whole-second rounding), which would
|
|
996
|
+
// otherwise truncate the real asset.
|
|
994
997
|
const c = createController();
|
|
995
998
|
await setContent(c, trackAds(makeBreak({
|
|
996
999
|
startTime: 0,
|
|
@@ -999,7 +1002,7 @@ describe('AdControllerImpl', () => {
|
|
|
999
1002
|
{
|
|
1000
1003
|
id: 'a1',
|
|
1001
1004
|
startTime: 0,
|
|
1002
|
-
duration: 5,
|
|
1005
|
+
duration: 5, // declared, server-rounded
|
|
1003
1006
|
uri: 'ad1.m3u8',
|
|
1004
1007
|
},
|
|
1005
1008
|
{
|
|
@@ -1014,10 +1017,54 @@ describe('AdControllerImpl', () => {
|
|
|
1014
1017
|
await flush();
|
|
1015
1018
|
expect(c.currentAd?.id).toBe('a1');
|
|
1016
1019
|
playbackController.dispatch('playing', {}); // timeStart = 0
|
|
1017
|
-
|
|
1020
|
+
// Past the declared 5s, still within a1's real content — must NOT advance.
|
|
1021
|
+
updateTime(6);
|
|
1022
|
+
await flush();
|
|
1023
|
+
expect(c.currentAd?.id).toBe('a1');
|
|
1024
|
+
// a1's media ends -> advance to a2.
|
|
1025
|
+
playbackController.dispatch('ended', {
|
|
1026
|
+
previous: false,
|
|
1027
|
+
current: true,
|
|
1028
|
+
});
|
|
1018
1029
|
await flush();
|
|
1019
1030
|
expect(c.currentAd?.id).toBe('a2');
|
|
1020
1031
|
});
|
|
1032
|
+
it('does not cut the final/only ad at its declared duration; it ends on media ended', async () => {
|
|
1033
|
+
// The ad server declares DURATION in whole seconds, but the asset's
|
|
1034
|
+
// real content is longer (6.732s here). The only ad must play to its
|
|
1035
|
+
// media `ended`, not be truncated at the rounded declared duration.
|
|
1036
|
+
const c = createController();
|
|
1037
|
+
const ended = [];
|
|
1038
|
+
c.on('adEnded', (e) => ended.push(e.ad.id));
|
|
1039
|
+
await setContent(c, trackAds(makeBreak({
|
|
1040
|
+
startTime: 0,
|
|
1041
|
+
duration: 30,
|
|
1042
|
+
ads: [
|
|
1043
|
+
{
|
|
1044
|
+
id: 'a1',
|
|
1045
|
+
startTime: 0,
|
|
1046
|
+
duration: 6, // declared, server-rounded
|
|
1047
|
+
uri: 'ad1.m3u8',
|
|
1048
|
+
},
|
|
1049
|
+
],
|
|
1050
|
+
})));
|
|
1051
|
+
updateTime(0);
|
|
1052
|
+
await flush();
|
|
1053
|
+
expect(c.currentAd?.id).toBe('a1');
|
|
1054
|
+
playbackController.dispatch('playing', {}); // timeStart = 0
|
|
1055
|
+
// Past the declared 6s, into the real content — must NOT end yet.
|
|
1056
|
+
updateTime(6.5);
|
|
1057
|
+
await flush();
|
|
1058
|
+
expect(c.currentAd?.id).toBe('a1');
|
|
1059
|
+
expect(ended).toEqual([]);
|
|
1060
|
+
// Real media end at 6.732 ends the ad.
|
|
1061
|
+
playbackController.dispatch('ended', {
|
|
1062
|
+
previous: false,
|
|
1063
|
+
current: true,
|
|
1064
|
+
});
|
|
1065
|
+
expect(ended).toEqual(['a1']);
|
|
1066
|
+
expect(c.currentAdBreak).toBeNull();
|
|
1067
|
+
});
|
|
1021
1068
|
it('ends the whole break when the playout limit is reached, dropping remaining ads', async () => {
|
|
1022
1069
|
const c = createController();
|
|
1023
1070
|
const resumes = [];
|
|
@@ -1053,6 +1100,65 @@ describe('AdControllerImpl', () => {
|
|
|
1053
1100
|
// content still resumes at the cue point (0), not startTime + playout.
|
|
1054
1101
|
expect(resumes.at(-1)).toBe(0);
|
|
1055
1102
|
});
|
|
1103
|
+
it('does not cut at the playout limit when the ad is within tolerance of its media end', async () => {
|
|
1104
|
+
// The budget is reached, but the ad's real media end is <1s away, so
|
|
1105
|
+
// it should finish on its own `ended` rather than be clipped. The
|
|
1106
|
+
// tolerance keys off the playback-reported duration, not ad.duration.
|
|
1107
|
+
const c = createController();
|
|
1108
|
+
await setContent(c, trackAds(makeBreak({
|
|
1109
|
+
startTime: 0,
|
|
1110
|
+
duration: null,
|
|
1111
|
+
playoutLimit: 8,
|
|
1112
|
+
ads: [
|
|
1113
|
+
{
|
|
1114
|
+
id: 'a1',
|
|
1115
|
+
startTime: 0,
|
|
1116
|
+
duration: 20,
|
|
1117
|
+
uri: 'ad1.m3u8',
|
|
1118
|
+
},
|
|
1119
|
+
],
|
|
1120
|
+
})));
|
|
1121
|
+
updateTime(0);
|
|
1122
|
+
await flush();
|
|
1123
|
+
playbackController.dispatch('playing', {}); // timeStart = 0
|
|
1124
|
+
// Media is 8.5s long; at the 8s budget only 0.5s of media remains.
|
|
1125
|
+
playbackController.duration = 8.5;
|
|
1126
|
+
updateTime(8); // cumulative 8 >= limit 8, but within 1s of media end
|
|
1127
|
+
await flush();
|
|
1128
|
+
expect(c.currentAdBreak).not.toBeNull();
|
|
1129
|
+
expect(c.currentAd?.id).toBe('a1');
|
|
1130
|
+
// It finishes on its own media `ended`.
|
|
1131
|
+
playbackController.dispatch('ended', {
|
|
1132
|
+
previous: false,
|
|
1133
|
+
current: true,
|
|
1134
|
+
});
|
|
1135
|
+
await flush();
|
|
1136
|
+
expect(c.currentAdBreak).toBeNull();
|
|
1137
|
+
});
|
|
1138
|
+
it('cuts at the playout limit when the ad has more than the tolerance of media left', async () => {
|
|
1139
|
+
const c = createController();
|
|
1140
|
+
await setContent(c, trackAds(makeBreak({
|
|
1141
|
+
startTime: 0,
|
|
1142
|
+
duration: null,
|
|
1143
|
+
playoutLimit: 8,
|
|
1144
|
+
ads: [
|
|
1145
|
+
{
|
|
1146
|
+
id: 'a1',
|
|
1147
|
+
startTime: 0,
|
|
1148
|
+
duration: 20,
|
|
1149
|
+
uri: 'ad1.m3u8',
|
|
1150
|
+
},
|
|
1151
|
+
],
|
|
1152
|
+
})));
|
|
1153
|
+
updateTime(0);
|
|
1154
|
+
await flush();
|
|
1155
|
+
playbackController.dispatch('playing', {}); // timeStart = 0
|
|
1156
|
+
// Media is 20s long; at the 8s budget 12s of media remains → cut.
|
|
1157
|
+
playbackController.duration = 20;
|
|
1158
|
+
updateTime(8);
|
|
1159
|
+
await flush();
|
|
1160
|
+
expect(c.currentAdBreak).toBeNull();
|
|
1161
|
+
});
|
|
1056
1162
|
});
|
|
1057
1163
|
describe('CUE=ONCE and replay', () => {
|
|
1058
1164
|
it('replays a non-ONCE break after the playhead seeks back before it', async () => {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
|
-
import { createEmptyMediaQualityMetadata, DrmControllerImpl, DrmError, DrmKeySystem, DrmRobustness, LICENSE_TIMEOUT, ReportableMediaError, } from '@amazon/vinyl';
|
|
5
|
+
import { createEmptyMediaQualityMetadata, DrmControllerImpl, DrmError, DrmKeySystem, DrmRobustness, EXCESSIVE_SESSION_COUNT, LICENSE_TIMEOUT, ReportableMediaError, } from '@amazon/vinyl';
|
|
6
6
|
import { createEventSpy, useMockLogger } from '@amazon/vinyl-util/testUtil';
|
|
7
7
|
import { Abort, base64ToByteArray, bufferToBase64, Deferred, ErrorLevel, ErrorOrigin, never, nextEventAsPromise, ReportableError, setUserAgent, utf16ToUint16Array, } from '@amazon/vinyl-util';
|
|
8
8
|
import { flushPromises, implementEventFakes, MockHTMLAudioElement, MockMediaError, useMockTime, } from '@amazon/vinyl-util/browserTestUtil';
|
|
@@ -1321,6 +1321,24 @@ describe('DrmControllerImpl', () => {
|
|
|
1321
1321
|
expect(drmController.isEmeSupported()).toBeFalse();
|
|
1322
1322
|
});
|
|
1323
1323
|
});
|
|
1324
|
+
describe('excessive sessions', () => {
|
|
1325
|
+
it('warns once when the open session count exceeds the threshold', async () => {
|
|
1326
|
+
drmController.setBufferingDrmInfo(drmInfo);
|
|
1327
|
+
// Distinct init data per event so each opens its own session.
|
|
1328
|
+
for (let i = 0; i < EXCESSIVE_SESSION_COUNT; i++) {
|
|
1329
|
+
await emitEncrypted(new Uint8Array([i]));
|
|
1330
|
+
}
|
|
1331
|
+
expect(drmController.activeSessions).toBe(EXCESSIVE_SESSION_COUNT);
|
|
1332
|
+
expect(logger.value.warn).not.toHaveBeenCalled();
|
|
1333
|
+
// Crossing the threshold warns.
|
|
1334
|
+
await emitEncrypted(new Uint8Array([EXCESSIVE_SESSION_COUNT]));
|
|
1335
|
+
expect(drmController.activeSessions).toBe(EXCESSIVE_SESSION_COUNT + 1);
|
|
1336
|
+
expect(logger.value.warn).toHaveBeenCalledTimes(1);
|
|
1337
|
+
// Further sessions past the threshold don't re-warn.
|
|
1338
|
+
await emitEncrypted(new Uint8Array([EXCESSIVE_SESSION_COUNT + 1]));
|
|
1339
|
+
expect(logger.value.warn).toHaveBeenCalledTimes(1);
|
|
1340
|
+
});
|
|
1341
|
+
});
|
|
1324
1342
|
describe('closeSessions', () => {
|
|
1325
1343
|
it('clears active sessions', async () => {
|
|
1326
1344
|
const sessionCloseSpy = createEventSpy(drmController, 'sessionClose');
|
|
@@ -496,6 +496,21 @@ describe('PlaybackController', () => {
|
|
|
496
496
|
await clock.tick(controller.options.seekTimeout);
|
|
497
497
|
await seekExpectation;
|
|
498
498
|
});
|
|
499
|
+
it('resolves a superseded seek at once when its seeking event never fires', async () => {
|
|
500
|
+
// A source swap can leave the original seek's `seeking`
|
|
501
|
+
// event unfired (the default mock never emits it). A newer
|
|
502
|
+
// seek supersedes it; the stale one must resolve immediately
|
|
503
|
+
// rather than hang for the full seekTimeout.
|
|
504
|
+
setSeekable([[0, 100]]);
|
|
505
|
+
const first = controller.seekTo(50);
|
|
506
|
+
const second = controller.seekTo(60);
|
|
507
|
+
// No clock tick: the newer seek's abort resolves `first` now.
|
|
508
|
+
// (A full-timeout hang would require clock.tick(seekTimeout).)
|
|
509
|
+
await expectAsync(first).toBeResolved();
|
|
510
|
+
// `second`'s own `seeking` never fires either; park its
|
|
511
|
+
// rejection (settled on dispose) so it isn't unhandled.
|
|
512
|
+
second.catch(() => undefined);
|
|
513
|
+
});
|
|
499
514
|
it('times out pending seeks after seekTimeout', async () => {
|
|
500
515
|
const pendingSeekExpectation = expectAsync(controller.seekTo(60)).toBeRejectedWithError('seek timed out on seeking event after 30s');
|
|
501
516
|
setSeekable([[0, 100]]);
|
|
@@ -115,10 +115,48 @@ describe('createStallMonitor', () => {
|
|
|
115
115
|
expect(onStallEntered).toHaveBeenCalledTimes(2);
|
|
116
116
|
expect(onStallEnded).toHaveBeenCalledTimes(2);
|
|
117
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
|
+
});
|
|
118
155
|
it('stops reporting after disposal', async () => {
|
|
119
156
|
monitor();
|
|
120
157
|
host.dispatch('playing', {});
|
|
121
158
|
stop();
|
|
159
|
+
stop = undefined; // disposal is single-shot; don't dispose again in afterEach
|
|
122
160
|
await poll(8);
|
|
123
161
|
expect(onStallEntered).not.toHaveBeenCalled();
|
|
124
162
|
});
|
|
@@ -224,6 +224,21 @@ describe('MseTrack', () => {
|
|
|
224
224
|
abort: any(Abort),
|
|
225
225
|
});
|
|
226
226
|
});
|
|
227
|
+
it('aborts the drm session abort so key sessions close', async () => {
|
|
228
|
+
// Regression: MSE key sessions must be tied to the per-activation
|
|
229
|
+
// drmSessionAbort (fired by closeDrmSessions on deactivate), not a
|
|
230
|
+
// separate abort that never fires — otherwise sessions leak.
|
|
231
|
+
track = createTrack();
|
|
232
|
+
await awaitContentTypes();
|
|
233
|
+
getAudioStream().streamingQuality =
|
|
234
|
+
createEmptyMediaQualityMetadata();
|
|
235
|
+
track.activate({});
|
|
236
|
+
const { abort } = deps.drmController.initializeForPlayback.calls.mostRecent()
|
|
237
|
+
.args[1];
|
|
238
|
+
expect(abort.aborted()).toBeFalse();
|
|
239
|
+
track.deactivate();
|
|
240
|
+
expect(abort.aborted()).toBeTrue();
|
|
241
|
+
});
|
|
227
242
|
});
|
|
228
243
|
describe('error', () => {
|
|
229
244
|
it('returns the first emitted error', async () => {
|