@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,45 @@
|
|
|
1
|
+
import { type Timestamp } from '@amazon/vinyl-util';
|
|
2
|
+
import type { ReadonlyPlaybackController } from './ReadonlyPlaybackController';
|
|
3
|
+
import { StallEndedReason } from './ReadonlyPlaybackController';
|
|
4
|
+
export interface StallMonitorOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Seconds the play head must be frozen (no `timeUpdate`) while playing before a stall is
|
|
7
|
+
* reported. Default: 1
|
|
8
|
+
*/
|
|
9
|
+
readonly stallThreshold?: number;
|
|
10
|
+
/**
|
|
11
|
+
* How often (seconds) the play head freeze is checked. Default: 0.25
|
|
12
|
+
*/
|
|
13
|
+
readonly pollInterval?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface StallEndedInfo {
|
|
16
|
+
/** Time of the last `timeUpdate` observed before the play head froze. */
|
|
17
|
+
readonly started: Timestamp;
|
|
18
|
+
/** Time the stall ended. */
|
|
19
|
+
readonly ended: Timestamp;
|
|
20
|
+
/** Total seconds the play head was frozen. */
|
|
21
|
+
readonly duration: number;
|
|
22
|
+
/** Why the stall ended. */
|
|
23
|
+
readonly reason: StallEndedReason;
|
|
24
|
+
}
|
|
25
|
+
export interface StallMonitorCallbacks {
|
|
26
|
+
/** Called when a stall is first detected. */
|
|
27
|
+
readonly onStallEntered: () => void;
|
|
28
|
+
/** Called when a detected stall ends. */
|
|
29
|
+
readonly onStallEnded: (info: StallEndedInfo) => void;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Watches a {@link ReadonlyPlaybackController} for stalls while playing: the play head freezing
|
|
33
|
+
* (no `timeUpdate`) for longer than {@link StallMonitorOptions.stallThreshold} seconds.
|
|
34
|
+
*
|
|
35
|
+
* Detection only runs once playback has actually been observed (a `playing` event) and is
|
|
36
|
+
* suspended on `pause`/`seeking`/`emptied` until playback is observed again — so a stall is never
|
|
37
|
+
* reported during initial loading or while awaiting a seek. A detected stall ends when the play
|
|
38
|
+
* head advances again (`playing`) or playback stops for another reason (`pause`/`seeking`/
|
|
39
|
+
* `emptied`); {@link StallEndedInfo.duration} is measured from the last `timeUpdate` before the
|
|
40
|
+
* freeze, so it reflects the full frozen time rather than only the time past the threshold.
|
|
41
|
+
*
|
|
42
|
+
* Returns a function that stops the monitor.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createStallMonitor(controller: ReadonlyPlaybackController, callbacks: StallMonitorCallbacks, options?: StallMonitorOptions): () => void;
|
|
45
|
+
//# sourceMappingURL=createStallMonitor.d.ts.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ObjectSchema } from '@amazon/vinyl-validation';
|
|
2
|
+
import type { Maybe } from '@amazon/vinyl-util';
|
|
2
3
|
/**
|
|
3
4
|
* Criteria used to pick which audio rendition to play.
|
|
4
5
|
*/
|
|
@@ -33,6 +34,32 @@ export interface AudioOptions {
|
|
|
33
34
|
* The audio selection criteria (language / descriptive).
|
|
34
35
|
*/
|
|
35
36
|
readonly selection?: AudioSelection;
|
|
37
|
+
/**
|
|
38
|
+
* When `true`, disables the audio sampling-rate support filter, so audio
|
|
39
|
+
* renditions whose sampling rate exceeds what the platform reports (via the
|
|
40
|
+
* `AudioContext` output rate) are kept and offered to playback anyway.
|
|
41
|
+
* `false` (the default) applies the filter. Changing this re-filters the
|
|
42
|
+
* timeline immediately.
|
|
43
|
+
*/
|
|
44
|
+
readonly disableSampleRateFilter?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* The maximum audio sampling rate (Hz) the sampling-rate filter treats as
|
|
47
|
+
* supported. When set, it takes precedence over the platform's
|
|
48
|
+
* `AudioContext`-reported output rate. Ignored when
|
|
49
|
+
* {@link disableSampleRateFilter} is `true`. Changing this re-filters the
|
|
50
|
+
* timeline immediately. `null`/`undefined` falls back to the reported rate.
|
|
51
|
+
*/
|
|
52
|
+
readonly maxSampleRate?: Maybe<number>;
|
|
53
|
+
/**
|
|
54
|
+
* The minimum audio sampling rate (Hz) the sampling-rate filter treats as
|
|
55
|
+
* supported. When set, renditions below it are filtered out as a soft floor
|
|
56
|
+
* (if every audio rendition is below it, the highest is kept so playback is
|
|
57
|
+
* never stranded). Applied alongside the max cap whenever the filter runs,
|
|
58
|
+
* regardless of how the max was determined; ignored when
|
|
59
|
+
* {@link disableSampleRateFilter} is `true`. Changing this re-filters the
|
|
60
|
+
* timeline immediately.
|
|
61
|
+
*/
|
|
62
|
+
readonly minSampleRate?: Maybe<number>;
|
|
36
63
|
}
|
|
37
64
|
export declare const defaultAudioOptions: AudioOptions;
|
|
38
65
|
export declare const audioOptionsValidator: ObjectSchema<AudioOptions>;
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import type { Timestamp } from '@amazon/vinyl-util';
|
|
2
2
|
import type { TrackUri } from '../track/Track';
|
|
3
|
+
import type { ContentType } from './MediaQualityMetadata';
|
|
3
4
|
/**
|
|
4
5
|
* The stage of the track-load journey a {@link LoadSpanMeasurement} describes.
|
|
5
6
|
*
|
|
6
7
|
* - `manifest`: fetching and parsing the DASH/HLS manifest.
|
|
8
|
+
* - `keySetup`: the EME/CDM setup, from session creation to the CDM's first
|
|
9
|
+
* license message. Precedes and is contiguous with `license`.
|
|
7
10
|
* - `license`: the DRM license request/response exchange.
|
|
8
11
|
* - `initSegment`: fetching the initialization segment.
|
|
9
12
|
* - `firstSegment`: fetching the first media segment.
|
|
10
13
|
*/
|
|
11
|
-
export type LoadSpanKind = 'manifest' | 'license' | 'initSegment' | 'firstSegment';
|
|
14
|
+
export type LoadSpanKind = 'manifest' | 'keySetup' | 'license' | 'initSegment' | 'firstSegment';
|
|
12
15
|
/**
|
|
13
16
|
* A timed span for one stage of a track load.
|
|
14
17
|
*
|
|
@@ -35,6 +38,17 @@ export interface LoadSpanMeasurement {
|
|
|
35
38
|
* buffering DRM info is set).
|
|
36
39
|
*/
|
|
37
40
|
readonly trackUri?: TrackUri;
|
|
41
|
+
/**
|
|
42
|
+
* The MIME type of the content this span was measured for, when known. Set
|
|
43
|
+
* for DRM `license` spans so a certificate exchange and the per-content-type
|
|
44
|
+
* license exchanges are distinguishable.
|
|
45
|
+
*/
|
|
46
|
+
readonly mimeType?: string;
|
|
47
|
+
/**
|
|
48
|
+
* The content type this span was measured for, when known. Set alongside
|
|
49
|
+
* {@link mimeType} for DRM `license` spans.
|
|
50
|
+
*/
|
|
51
|
+
readonly contentType?: ContentType;
|
|
38
52
|
}
|
|
39
53
|
/**
|
|
40
54
|
* A {@link LoadSpanMeasurement} attributed to the track it was measured for.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ContentTypesValue } from './ContentTypesValue';
|
|
2
|
+
import type { MediaTimeline } from './MediaTimeline';
|
|
3
|
+
import type { ObservableValue } from '@amazon/vinyl-observable';
|
|
4
|
+
export interface TimelineContentTypesValueDeps {
|
|
5
|
+
/**
|
|
6
|
+
* The filtered, playable timeline. Content types are derived from the
|
|
7
|
+
* qualities that survive filtering, so a content type whose renditions are
|
|
8
|
+
* all filtered out (e.g. every audio rendition dropped by the sample-rate
|
|
9
|
+
* filter) is not reported — it never gates readiness and no source buffer
|
|
10
|
+
* is expected for it, so the remaining content types still play.
|
|
11
|
+
*/
|
|
12
|
+
readonly mediaTimelineTransformed: ObservableValue<Promise<MediaTimeline>>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Provides the MSE content types to create source buffers for, derived from the
|
|
16
|
+
* qualities that survive timeline filtering rather than the raw manifest. Only
|
|
17
|
+
* `'audio'`/`'video'` are reported; text is a sidecar pipeline, never an MSE
|
|
18
|
+
* source buffer.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createTimelineContentTypesValue(deps: TimelineContentTypesValueDeps): ContentTypesValue;
|
|
21
|
+
//# sourceMappingURL=createTimelineContentTypesValue.d.ts.map
|
|
@@ -5,8 +5,8 @@ import type { TextTrackInfo } from './TextTrack';
|
|
|
5
5
|
*
|
|
6
6
|
* Considers an `<AdaptationSet>` to carry text when one of:
|
|
7
7
|
* - `contentType="text"`
|
|
8
|
-
* - `mimeType
|
|
9
|
-
*
|
|
8
|
+
* - its `mimeType`, or that of one of its `<Representation>`s, starts with
|
|
9
|
+
* `text/` (e.g. `text/vtt`)
|
|
10
10
|
*
|
|
11
11
|
* Within a text adaptation set, each `<Representation>` becomes one
|
|
12
12
|
* {@link TextTrackInfo}. The `BaseURL` chain (MPD → Period → AdaptationSet →
|
|
@@ -26,7 +26,6 @@ export declare class MseTrack extends TrackBase {
|
|
|
26
26
|
get [Symbol.toStringTag](): string;
|
|
27
27
|
protected readonly deps: MseTrackDeps;
|
|
28
28
|
private readonly streams;
|
|
29
|
-
private readonly disposeAbort;
|
|
30
29
|
private lastPreloadOptions;
|
|
31
30
|
private activateOptions;
|
|
32
31
|
private readonly allFetchedRanges;
|
|
@@ -6,11 +6,22 @@ import { AdControllerImpl } from '@amazon/vinyl';
|
|
|
6
6
|
import { MockPlaybackController, MockTrack } from '@amazon/vinyl/vinylTestUtil';
|
|
7
7
|
describe('AdControllerImpl', () => {
|
|
8
8
|
let playbackController;
|
|
9
|
+
let controllers;
|
|
9
10
|
beforeEach(() => {
|
|
10
11
|
playbackController = new MockPlaybackController();
|
|
12
|
+
controllers = [];
|
|
13
|
+
});
|
|
14
|
+
// Dispose every controller a spec created. An activated-but-unstarted ad
|
|
15
|
+
// arms a `sleep(adLoadTimeout)` timer.
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
for (const c of controllers)
|
|
18
|
+
if (!c.disposed)
|
|
19
|
+
c.dispose();
|
|
11
20
|
});
|
|
12
21
|
function createController() {
|
|
13
|
-
|
|
22
|
+
const c = new AdControllerImpl({ playbackController });
|
|
23
|
+
controllers.push(c);
|
|
24
|
+
return c;
|
|
14
25
|
}
|
|
15
26
|
function updateTime(time) {
|
|
16
27
|
playbackController.currentTime = time;
|
|
@@ -301,6 +312,21 @@ describe('AdControllerImpl', () => {
|
|
|
301
312
|
await flush();
|
|
302
313
|
expect(c.currentAdBreak).toBeNull();
|
|
303
314
|
});
|
|
315
|
+
it('ignores a late provider ad-discovery resolution after dispose', async () => {
|
|
316
|
+
const c = createController();
|
|
317
|
+
let resolveAds = () => { };
|
|
318
|
+
const track = new MockTrack();
|
|
319
|
+
track.uri = 't1';
|
|
320
|
+
track.active = true;
|
|
321
|
+
track.getAds = () => new Promise((res) => {
|
|
322
|
+
resolveAds = res;
|
|
323
|
+
});
|
|
324
|
+
c.setAdsProvider(track); // refreshAds() runs; getAds() stays pending
|
|
325
|
+
c.dispose();
|
|
326
|
+
resolveAds(trackAds(makeBreak())); // refreshAds .then: interrupted (disposed)
|
|
327
|
+
await flush();
|
|
328
|
+
expect(c.currentTrackAds).toBeNull();
|
|
329
|
+
});
|
|
304
330
|
});
|
|
305
331
|
it('emits currentTrackAdsChange with the TrackAds when a parent track is set', async () => {
|
|
306
332
|
const c = createController();
|
|
@@ -500,6 +526,44 @@ describe('AdControllerImpl', () => {
|
|
|
500
526
|
await flush();
|
|
501
527
|
expect(c.currentAdBreak).toBeNull();
|
|
502
528
|
});
|
|
529
|
+
it('still enters a pending break when skipped while its ads resolve', async () => {
|
|
530
|
+
const c = createController();
|
|
531
|
+
let resolveAds = () => undefined;
|
|
532
|
+
const entered = [];
|
|
533
|
+
await setContent(c, trackAds(makeBreak({
|
|
534
|
+
id: 'b1',
|
|
535
|
+
startTime: 0,
|
|
536
|
+
duration: 30,
|
|
537
|
+
ads: () => new Promise((resolve) => {
|
|
538
|
+
resolveAds = resolve;
|
|
539
|
+
}),
|
|
540
|
+
}), makeBreak({
|
|
541
|
+
id: 'b2',
|
|
542
|
+
startTime: 1,
|
|
543
|
+
duration: 30,
|
|
544
|
+
ads: [
|
|
545
|
+
{
|
|
546
|
+
id: 'a2',
|
|
547
|
+
startTime: 1,
|
|
548
|
+
duration: 10,
|
|
549
|
+
uri: 'ad2.m3u8',
|
|
550
|
+
},
|
|
551
|
+
],
|
|
552
|
+
})));
|
|
553
|
+
c.on('adBreakEntered', (e) => entered.push(e.adBreak.id));
|
|
554
|
+
// Both breaks contain the playhead, so both become pending and the
|
|
555
|
+
// first is entered while its ad list is still in flight.
|
|
556
|
+
updateTime(2);
|
|
557
|
+
await flush();
|
|
558
|
+
expect(entered).toEqual(['b1']);
|
|
559
|
+
c.skipAdBreak();
|
|
560
|
+
resolveAds([]);
|
|
561
|
+
await flush();
|
|
562
|
+
// Nothing else drains the pending list, and a non-empty list blocks
|
|
563
|
+
// further midroll ingress, so b2 must be entered here.
|
|
564
|
+
expect(entered).toEqual(['b1', 'b2']);
|
|
565
|
+
expect(c.currentAdBreak?.id).toBe('b2');
|
|
566
|
+
});
|
|
503
567
|
});
|
|
504
568
|
describe('a break with no ads', () => {
|
|
505
569
|
it('completes immediately with a resume position and plays no ad', async () => {
|
|
@@ -727,6 +791,34 @@ describe('AdControllerImpl', () => {
|
|
|
727
791
|
await flush();
|
|
728
792
|
expect(thirds).toEqual(['a1']);
|
|
729
793
|
});
|
|
794
|
+
it('dispatches each quartile at most once per ad', async () => {
|
|
795
|
+
const c = createController();
|
|
796
|
+
const events = [];
|
|
797
|
+
c.on('adFirstQuartile', () => events.push('first'));
|
|
798
|
+
c.on('adMidpoint', () => events.push('mid'));
|
|
799
|
+
c.on('adThirdQuartile', () => events.push('third'));
|
|
800
|
+
await setContent(c, trackAds(makeBreak({ startTime: 0, duration: 10 })));
|
|
801
|
+
updateTime(0);
|
|
802
|
+
await flush();
|
|
803
|
+
playbackController.dispatch('playing', {});
|
|
804
|
+
playbackController.duration = 10;
|
|
805
|
+
playbackController.playbackRate = 1;
|
|
806
|
+
// Every subsequent timeUpdate is still past each threshold, so an
|
|
807
|
+
// unlatched check would report the quartile again.
|
|
808
|
+
for (const [percent, time] of [
|
|
809
|
+
[0.3, 3],
|
|
810
|
+
[0.4, 4],
|
|
811
|
+
[0.6, 6],
|
|
812
|
+
[0.7, 7],
|
|
813
|
+
[0.8, 8],
|
|
814
|
+
[0.9, 9],
|
|
815
|
+
]) {
|
|
816
|
+
playbackController.currentTimePercent = percent;
|
|
817
|
+
updateTime(time);
|
|
818
|
+
await flush();
|
|
819
|
+
}
|
|
820
|
+
expect(events).toEqual(['first', 'mid', 'third']);
|
|
821
|
+
});
|
|
730
822
|
it('does not emit quartiles or throw when the ad duration is zero', async () => {
|
|
731
823
|
// Regression: a zero reported duration must not divide-by-zero into
|
|
732
824
|
// NaN progress and must not emit bogus quartile events.
|
|
@@ -870,7 +962,6 @@ describe('AdControllerImpl', () => {
|
|
|
870
962
|
await setContent(c, trackAds(makeBreak({
|
|
871
963
|
startTime: 10,
|
|
872
964
|
duration: 20,
|
|
873
|
-
// Null per-ad duration so the per-ad cap does not end it.
|
|
874
965
|
ads: [
|
|
875
966
|
{
|
|
876
967
|
id: 'a1',
|
|
@@ -964,7 +1055,11 @@ describe('AdControllerImpl', () => {
|
|
|
964
1055
|
});
|
|
965
1056
|
});
|
|
966
1057
|
describe('ad duration and playout limits', () => {
|
|
967
|
-
it('advances to the next ad
|
|
1058
|
+
it('advances to the next ad on the ad media ended, not its declared duration', async () => {
|
|
1059
|
+
// Each ad asset is its own source; a non-final ad advances on its own
|
|
1060
|
+
// media `ended`. The declared per-ad duration must NOT end it early —
|
|
1061
|
+
// a packager may under-declare it (whole-second rounding), which would
|
|
1062
|
+
// otherwise truncate the real asset.
|
|
968
1063
|
const c = createController();
|
|
969
1064
|
await setContent(c, trackAds(makeBreak({
|
|
970
1065
|
startTime: 0,
|
|
@@ -973,7 +1068,7 @@ describe('AdControllerImpl', () => {
|
|
|
973
1068
|
{
|
|
974
1069
|
id: 'a1',
|
|
975
1070
|
startTime: 0,
|
|
976
|
-
duration: 5,
|
|
1071
|
+
duration: 5, // declared, server-rounded
|
|
977
1072
|
uri: 'ad1.m3u8',
|
|
978
1073
|
},
|
|
979
1074
|
{
|
|
@@ -988,10 +1083,54 @@ describe('AdControllerImpl', () => {
|
|
|
988
1083
|
await flush();
|
|
989
1084
|
expect(c.currentAd?.id).toBe('a1');
|
|
990
1085
|
playbackController.dispatch('playing', {}); // timeStart = 0
|
|
991
|
-
|
|
1086
|
+
// Past the declared 5s, still within a1's real content — must NOT advance.
|
|
1087
|
+
updateTime(6);
|
|
1088
|
+
await flush();
|
|
1089
|
+
expect(c.currentAd?.id).toBe('a1');
|
|
1090
|
+
// a1's media ends -> advance to a2.
|
|
1091
|
+
playbackController.dispatch('ended', {
|
|
1092
|
+
previous: false,
|
|
1093
|
+
current: true,
|
|
1094
|
+
});
|
|
992
1095
|
await flush();
|
|
993
1096
|
expect(c.currentAd?.id).toBe('a2');
|
|
994
1097
|
});
|
|
1098
|
+
it('does not cut the final/only ad at its declared duration; it ends on media ended', async () => {
|
|
1099
|
+
// The ad server declares DURATION in whole seconds, but the asset's
|
|
1100
|
+
// real content is longer (6.732s here). The only ad must play to its
|
|
1101
|
+
// media `ended`, not be truncated at the rounded declared duration.
|
|
1102
|
+
const c = createController();
|
|
1103
|
+
const ended = [];
|
|
1104
|
+
c.on('adEnded', (e) => ended.push(e.ad.id));
|
|
1105
|
+
await setContent(c, trackAds(makeBreak({
|
|
1106
|
+
startTime: 0,
|
|
1107
|
+
duration: 30,
|
|
1108
|
+
ads: [
|
|
1109
|
+
{
|
|
1110
|
+
id: 'a1',
|
|
1111
|
+
startTime: 0,
|
|
1112
|
+
duration: 6, // declared, server-rounded
|
|
1113
|
+
uri: 'ad1.m3u8',
|
|
1114
|
+
},
|
|
1115
|
+
],
|
|
1116
|
+
})));
|
|
1117
|
+
updateTime(0);
|
|
1118
|
+
await flush();
|
|
1119
|
+
expect(c.currentAd?.id).toBe('a1');
|
|
1120
|
+
playbackController.dispatch('playing', {}); // timeStart = 0
|
|
1121
|
+
// Past the declared 6s, into the real content — must NOT end yet.
|
|
1122
|
+
updateTime(6.5);
|
|
1123
|
+
await flush();
|
|
1124
|
+
expect(c.currentAd?.id).toBe('a1');
|
|
1125
|
+
expect(ended).toEqual([]);
|
|
1126
|
+
// Real media end at 6.732 ends the ad.
|
|
1127
|
+
playbackController.dispatch('ended', {
|
|
1128
|
+
previous: false,
|
|
1129
|
+
current: true,
|
|
1130
|
+
});
|
|
1131
|
+
expect(ended).toEqual(['a1']);
|
|
1132
|
+
expect(c.currentAdBreak).toBeNull();
|
|
1133
|
+
});
|
|
995
1134
|
it('ends the whole break when the playout limit is reached, dropping remaining ads', async () => {
|
|
996
1135
|
const c = createController();
|
|
997
1136
|
const resumes = [];
|
|
@@ -1027,6 +1166,65 @@ describe('AdControllerImpl', () => {
|
|
|
1027
1166
|
// content still resumes at the cue point (0), not startTime + playout.
|
|
1028
1167
|
expect(resumes.at(-1)).toBe(0);
|
|
1029
1168
|
});
|
|
1169
|
+
it('does not cut at the playout limit when the ad is within tolerance of its media end', async () => {
|
|
1170
|
+
// The budget is reached, but the ad's real media end is <1s away, so
|
|
1171
|
+
// it should finish on its own `ended` rather than be clipped. The
|
|
1172
|
+
// tolerance keys off the playback-reported duration, not ad.duration.
|
|
1173
|
+
const c = createController();
|
|
1174
|
+
await setContent(c, trackAds(makeBreak({
|
|
1175
|
+
startTime: 0,
|
|
1176
|
+
duration: null,
|
|
1177
|
+
playoutLimit: 8,
|
|
1178
|
+
ads: [
|
|
1179
|
+
{
|
|
1180
|
+
id: 'a1',
|
|
1181
|
+
startTime: 0,
|
|
1182
|
+
duration: 20,
|
|
1183
|
+
uri: 'ad1.m3u8',
|
|
1184
|
+
},
|
|
1185
|
+
],
|
|
1186
|
+
})));
|
|
1187
|
+
updateTime(0);
|
|
1188
|
+
await flush();
|
|
1189
|
+
playbackController.dispatch('playing', {}); // timeStart = 0
|
|
1190
|
+
// Media is 8.5s long; at the 8s budget only 0.5s of media remains.
|
|
1191
|
+
playbackController.duration = 8.5;
|
|
1192
|
+
updateTime(8); // cumulative 8 >= limit 8, but within 1s of media end
|
|
1193
|
+
await flush();
|
|
1194
|
+
expect(c.currentAdBreak).not.toBeNull();
|
|
1195
|
+
expect(c.currentAd?.id).toBe('a1');
|
|
1196
|
+
// It finishes on its own media `ended`.
|
|
1197
|
+
playbackController.dispatch('ended', {
|
|
1198
|
+
previous: false,
|
|
1199
|
+
current: true,
|
|
1200
|
+
});
|
|
1201
|
+
await flush();
|
|
1202
|
+
expect(c.currentAdBreak).toBeNull();
|
|
1203
|
+
});
|
|
1204
|
+
it('cuts at the playout limit when the ad has more than the tolerance of media left', async () => {
|
|
1205
|
+
const c = createController();
|
|
1206
|
+
await setContent(c, trackAds(makeBreak({
|
|
1207
|
+
startTime: 0,
|
|
1208
|
+
duration: null,
|
|
1209
|
+
playoutLimit: 8,
|
|
1210
|
+
ads: [
|
|
1211
|
+
{
|
|
1212
|
+
id: 'a1',
|
|
1213
|
+
startTime: 0,
|
|
1214
|
+
duration: 20,
|
|
1215
|
+
uri: 'ad1.m3u8',
|
|
1216
|
+
},
|
|
1217
|
+
],
|
|
1218
|
+
})));
|
|
1219
|
+
updateTime(0);
|
|
1220
|
+
await flush();
|
|
1221
|
+
playbackController.dispatch('playing', {}); // timeStart = 0
|
|
1222
|
+
// Media is 20s long; at the 8s budget 12s of media remains → cut.
|
|
1223
|
+
playbackController.duration = 20;
|
|
1224
|
+
updateTime(8);
|
|
1225
|
+
await flush();
|
|
1226
|
+
expect(c.currentAdBreak).toBeNull();
|
|
1227
|
+
});
|
|
1030
1228
|
});
|
|
1031
1229
|
describe('CUE=ONCE and replay', () => {
|
|
1032
1230
|
it('replays a non-ONCE break after the playhead seeks back before it', async () => {
|
|
@@ -199,7 +199,8 @@ describe('discoverHlsInterstitials', () => {
|
|
|
199
199
|
const ads = await resolveValueProvider(breaks[0].ads);
|
|
200
200
|
expect(fetchSpy.calls.argsFor(0)[0]).toBe('https://example.com/ads.json');
|
|
201
201
|
expect(ads.length).toBe(2);
|
|
202
|
-
|
|
202
|
+
// Relative to the asset list, not to the media playlist.
|
|
203
|
+
expect(ads[0].uri).toBe('https://example.com/mid1.m3u8');
|
|
203
204
|
expect(ads[0].duration).toBe(10);
|
|
204
205
|
expect(ads[1].uri).toBe('https://cdn.example.com/mid2.m3u8');
|
|
205
206
|
expect(ads[1].duration).toBeNull();
|
|
@@ -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';
|
|
@@ -331,6 +331,17 @@ describe('DrmControllerImpl', () => {
|
|
|
331
331
|
expect(sessions.map((session) => session.dispose.calls.count())).toEqual([1, 1, 1, 1, 1, 1]); // Expect all sessions to have been disposed
|
|
332
332
|
expect(drmController.activeSessions).toEqual(0);
|
|
333
333
|
});
|
|
334
|
+
it('does not close sessions again after dispose', async () => {
|
|
335
|
+
const abort = new Abort();
|
|
336
|
+
drmController.setBufferingDrmInfo(drmInfo, abort);
|
|
337
|
+
await emitEncrypted();
|
|
338
|
+
const session = mediaKeys.createSession.calls.mostRecent()
|
|
339
|
+
.returnValue;
|
|
340
|
+
drmController.dispose();
|
|
341
|
+
const disposeCount = session.dispose.calls.count();
|
|
342
|
+
abort.abort();
|
|
343
|
+
expect(session.dispose.calls.count()).toBe(disposeCount);
|
|
344
|
+
});
|
|
334
345
|
});
|
|
335
346
|
});
|
|
336
347
|
describe('when an encrypted event is observed', () => {
|
|
@@ -1048,6 +1059,24 @@ describe('DrmControllerImpl', () => {
|
|
|
1048
1059
|
expect(drmController.isEmeSupported()).toBeFalse();
|
|
1049
1060
|
});
|
|
1050
1061
|
});
|
|
1062
|
+
describe('excessive sessions', () => {
|
|
1063
|
+
it('warns once when the open session count exceeds the threshold', async () => {
|
|
1064
|
+
drmController.setBufferingDrmInfo(drmInfo);
|
|
1065
|
+
// Distinct init data per event so each opens its own session.
|
|
1066
|
+
for (let i = 0; i < EXCESSIVE_SESSION_COUNT; i++) {
|
|
1067
|
+
await emitEncrypted(new Uint8Array([i]));
|
|
1068
|
+
}
|
|
1069
|
+
expect(drmController.activeSessions).toBe(EXCESSIVE_SESSION_COUNT);
|
|
1070
|
+
expect(logger.value.warn).not.toHaveBeenCalled();
|
|
1071
|
+
// Crossing the threshold warns.
|
|
1072
|
+
await emitEncrypted(new Uint8Array([EXCESSIVE_SESSION_COUNT]));
|
|
1073
|
+
expect(drmController.activeSessions).toBe(EXCESSIVE_SESSION_COUNT + 1);
|
|
1074
|
+
expect(logger.value.warn).toHaveBeenCalledTimes(1);
|
|
1075
|
+
// Further sessions past the threshold don't re-warn.
|
|
1076
|
+
await emitEncrypted(new Uint8Array([EXCESSIVE_SESSION_COUNT + 1]));
|
|
1077
|
+
expect(logger.value.warn).toHaveBeenCalledTimes(1);
|
|
1078
|
+
});
|
|
1079
|
+
});
|
|
1051
1080
|
describe('closeSessions', () => {
|
|
1052
1081
|
it('clears active sessions', async () => {
|
|
1053
1082
|
const sessionCloseSpy = createEventSpy(drmController, 'sessionClose');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createStallDetector.test.d.ts","sourceRoot":"","sources":["../../../../../test/unit/playback/createStallDetector.test.ts"],"names":[],"mappings":""}
|