@dolby-optiview/ads-sdk-core 1.3.0 → 1.4.0
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/OptiViewAdsExternalVendors.d.ts +32 -0
- package/dist/generated/diagnosticCodes.d.ts +1 -1
- package/dist/generated/diagnosticCodes.js +1 -1
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/services/AdPlayerController.d.ts +4 -12
- package/dist/services/AdPlayerController.js +1 -1
- package/dist/services/adPlayer/AdPlayerEvents.d.ts +1 -7
- package/dist/services/adPlayer/AdPlayerEvents.js +1 -1
- package/dist/services/adPlayer/AdPlayerState.d.ts +0 -1
- package/dist/services/adPlayer/AdPlayerState.js +1 -1
- package/dist/services/adPlayer/AssetPlayback.d.ts +3 -0
- package/dist/services/adPlayer/AssetPlayback.js +1 -1
- package/dist/services/adPlayer/AudioController.d.ts +58 -0
- package/dist/services/adPlayer/AudioController.js +1 -0
- package/dist/services/adPlayer/ContentResumeCoordinator.d.ts +2 -0
- package/dist/services/adPlayer/ContentResumeCoordinator.js +1 -1
- package/dist/services/adPlayer/SharedElementBreakPlayer.d.ts +3 -0
- package/dist/services/adPlayer/SharedElementBreakPlayer.js +1 -1
- package/dist/services/adPlayer/SurfaceRevealReporter.d.ts +28 -0
- package/dist/services/adPlayer/SurfaceRevealReporter.js +1 -0
- package/dist/types/BreakManifest.d.ts +25 -4
- package/dist/types/Events.d.ts +1 -0
- package/dist/types/ExternalVendorConfiguration.d.ts +3 -1
- package/package.json +1 -1
|
@@ -3,14 +3,27 @@ import type { AssetParameterMacros } from './services/assetParameterMacros';
|
|
|
3
3
|
import type { ExternalVendorConfiguration } from './types/ExternalVendorConfiguration';
|
|
4
4
|
import type { OptiViewAdsEventHandler } from './types/Events';
|
|
5
5
|
import type { DiagnosticHandler, DiagnosticReport } from './diagnostics/DiagnosticEvent';
|
|
6
|
+
/** Configuration for reading manifest external-vendor parameters without a player. @category Configuration */
|
|
6
7
|
export interface OptiViewAdsExternalVendorsConfig {
|
|
8
|
+
/** URL of the signed manifest to fetch. */
|
|
7
9
|
manifestUrl: string;
|
|
10
|
+
/** Parameters merged beneath the entry's manifest values. */
|
|
8
11
|
assetParameters?: Record<string, string>;
|
|
12
|
+
/** Named macros that are resolved when configuration is read. */
|
|
9
13
|
assetParameterMacros?: AssetParameterMacros;
|
|
14
|
+
/** Optional request interceptor for manifest fetches. */
|
|
10
15
|
interceptManifestRequest?: ManifestRequestInterceptor;
|
|
16
|
+
/** Optional response interceptor for manifest fetches. */
|
|
11
17
|
interceptManifestResponse?: ManifestResponseInterceptor;
|
|
18
|
+
/** Enables SDK debug diagnostics. */
|
|
12
19
|
debug?: boolean;
|
|
13
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Reads the manifest's `external` vendor configuration entries without a player or an ad
|
|
23
|
+
* session, so the application can pass the resolved parameters to another ad provider.
|
|
24
|
+
* Call {@link start}, read entries with {@link getExternalVendorConfiguration}, and listen for
|
|
25
|
+
* `externalvendorconfigurationchange` to pick up template changes while the manifest is polled.
|
|
26
|
+
* @category SDK & Sessions */
|
|
14
27
|
export declare class OptiViewAdsExternalVendors {
|
|
15
28
|
private readonly manifestUrl;
|
|
16
29
|
private readonly manifestService;
|
|
@@ -26,19 +39,38 @@ export declare class OptiViewAdsExternalVendors {
|
|
|
26
39
|
private startGeneration;
|
|
27
40
|
private lastValidation;
|
|
28
41
|
private configSummary;
|
|
42
|
+
/**
|
|
43
|
+
* Create a reader for the manifest at `config.manifestUrl`; nothing is fetched until {@link start}.
|
|
44
|
+
* @throws Error when `manifestUrl` is blank.
|
|
45
|
+
* @category Lifecycle */
|
|
29
46
|
constructor(config: OptiViewAdsExternalVendorsConfig);
|
|
47
|
+
/** Fetch the initial manifest and begin polling when configured. @category Lifecycle */
|
|
30
48
|
start(): Promise<void>;
|
|
31
49
|
private startInternal;
|
|
50
|
+
/** Stop polling and clear the current entries. @category Lifecycle */
|
|
32
51
|
stop(): void;
|
|
52
|
+
/** Stop polling, clear entries, and release listeners. @category Lifecycle */
|
|
33
53
|
destroy(): void;
|
|
54
|
+
/** Return all resolved configurations in manifest order. @category Lifecycle */
|
|
34
55
|
getExternalVendorConfiguration(): ExternalVendorConfiguration[];
|
|
56
|
+
/** Return one resolved configuration using an exact provider match. @category Lifecycle */
|
|
35
57
|
getExternalVendorConfiguration(provider: string): ExternalVendorConfiguration | undefined;
|
|
58
|
+
/** Replace application parameters used to resolve each configuration. @category Lifecycle */
|
|
36
59
|
updateAssetParameters(parameters: Record<string, string>): void;
|
|
60
|
+
/**
|
|
61
|
+
* Merge per-name macro updates used to resolve each configuration; `null` marks a name empty
|
|
62
|
+
* (parameters that use it are omitted) and `undefined` removes it.
|
|
63
|
+
* @category Lifecycle */
|
|
37
64
|
updateAssetParameterMacros(macros: AssetParameterMacroUpdates): void;
|
|
65
|
+
/** Subscribe to external-vendor configuration changes. @category Events */
|
|
38
66
|
addEventListener(event: 'externalvendorconfigurationchange', handler: OptiViewAdsEventHandler<'externalvendorconfigurationchange'>): void;
|
|
67
|
+
/** Remove a configuration-change listener. @category Events */
|
|
39
68
|
removeEventListener(event: 'externalvendorconfigurationchange', handler: OptiViewAdsEventHandler<'externalvendorconfigurationchange'>): void;
|
|
69
|
+
/** Subscribe to SDK diagnostics. @category Diagnostics */
|
|
40
70
|
onDiagnostic(handler: DiagnosticHandler): void;
|
|
71
|
+
/** Remove a diagnostics listener. @category Diagnostics */
|
|
41
72
|
offDiagnostic(handler: DiagnosticHandler): void;
|
|
73
|
+
/** Export the standalone diagnostic report. @category Diagnostics */
|
|
42
74
|
exportDiagnostics(): DiagnosticReport;
|
|
43
75
|
private emit;
|
|
44
76
|
private diagnoseFetchFailure;
|
|
@@ -147,7 +147,7 @@ export declare const DIAGNOSTIC_CODES: {
|
|
|
147
147
|
readonly "DA-AD-SURFACE-REVEALED": {
|
|
148
148
|
readonly category: "break";
|
|
149
149
|
readonly level: "info";
|
|
150
|
-
readonly summary: "The ad surface became visible: how long it was held back waiting for the ad's first frame, and whether it was revealed because that frame arrived or because the wait timed out.";
|
|
150
|
+
readonly summary: "The ad surface became visible: how long it was held back waiting for the ad's first frame, and whether it was revealed because that frame arrived or because the wait timed out. On web, an ad shown before its first frame reports reason shown-before-frame with visibleGapMs, the time on screen without a picture.";
|
|
151
151
|
};
|
|
152
152
|
readonly "DA-BREAK-CUT-LATE": {
|
|
153
153
|
readonly category: "break";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const DIAGNOSTIC_CODES={"DA-SESSION-STARTED":{category:"session",level:"info",summary:"Monetization session started; manifest polling began. context: manifestUrl (stitcherUrl in SSAI mode), reported verbatim."},"DA-SESSION-ENDED":{category:"session",level:"info",summary:"Monetization session ended; polling and scheduling stopped."},"DA-ASSET-PARAMETERS-UPDATED":{category:"session",level:"info",summary:"The break manifest's session-level asset parameters changed; the merged parameters were pushed to the active GAM stream. context: source ('manifest'), keys (parameter names only, never values)."},"DA-MANIFEST-FETCH-FAILED":{category:"manifest",level:"error",summary:"Initial break-manifest fetch failed; the session could not start."},"DA-MANIFEST-POLL-FAILED":{category:"manifest",level:"warn",summary:"A manifest poll failed; the SDK keeps the last good manifest and retries."},"DA-MANIFEST-CHANNEL-CHANGED":{category:"manifest",level:"info",summary:"A polled manifest describes a different ads channel (channelId, GAM identity or timebase changed); the SDK re-aligned its ad setup and resolved delivery mode again while the session and content playback continued. context: previousChannelId, channelId, reason, manifestUrl."},"DA-MANIFEST-INTERCEPT-FAILED":{category:"manifest",level:"warn",summary:"The customer manifest-response interceptor threw; the un-modified parsed manifest was used instead."},"DA-MANIFEST-REQUEST-INTERCEPT-FAILED":{category:"manifest",level:"warn",summary:"The customer manifest-request interceptor threw; the normal network fetch of the original URL was used instead."},"DA-MANIFEST-SIGNATURE-INVALID":{category:"manifest",level:"error",summary:"The break-manifest response failed detached-JWS signature verification (missing/malformed X-Manifest-Signature, unsupported alg, unknown kid, unknown critical param, or signature mismatch); the manifest was rejected and no breaks were scheduled. context: url, reason."},"DA-MANIFEST-EVENT-INVALID":{category:"manifest",level:"warn",summary:"A channel event was ignored because its time window or configuration is invalid, or because events are unsupported for the manifest timebase. context: eventId and index when available."},"DA-PDT-MISSING":{category:"playback",level:"warn",summary:"No EXT-X-PROGRAM-DATE-TIME once content is playing (or after the startup backstop window); wallclock-timebase breaks fall back to the system clock."},"DA-ANVATO-CUE-PENDING":{category:"break",level:"info",summary:"ptsSource: 'anvatoCue' — no Anvato in-stream cue observed yet for a PTS break; the break stays pending until its cue arrives."},"DA-ANVATO-CUE-MATCHED":{category:"break",level:"info",summary:"ptsSource: 'anvatoCue' — a PTS break's start was matched to an observed Anvato cue and scheduled at the cue's media time."},"DA-ANVATO-CUE-ENTERED":{category:"break",level:"info",summary:"ptsSource: 'anvatoCue' — a break was started by the cue reaching the playhead rather than by the scheduler's 250ms tick; context carries the breakId, the cue pts and the media time it fired at, so the two start paths are distinguishable in the field."},"DA-ANVATO-CUE-OBSERVED":{category:"break",level:"info",summary:"ptsSource: 'anvatoCue' — an Anvato break-signaling (type=cue) frame was observed in the stream; context carries the cue's pts, the media time it landed at, whether the tracker recorded it, and the raw payload — so a manifest-PTS vs in-stream-PTS misalignment is observable in the field."},"DA-ANVATO-CUE-SEGMENT-ANCHORED":{category:"break",level:"warn",summary:"ptsSource: 'anvatoCue' — an Anvato emsg cue arrived without a ptsOffset from the adapter, so its break is scheduled at the cue's carrying segment start, which can be up to one segment early. Emitted once per session; context: pts, mediaTime."},"DA-ANVATO-CUE-OFFSET-REJECTED":{category:"break",level:"warn",summary:"ptsSource: 'anvatoCue' — the adapter's ptsOffset placed an Anvato cue outside its carrying segment's window (more than 0.1s before or 15s after the cue startTime), so the cue's startTime is used instead. Emitted once per cue pts; context: pts, startTime, ptsOffset, mappedTime."},"DA-ANVATO-CUE-END-MATCHED":{category:"break",level:"info",summary:"ptsSource: 'anvatoCue' — the active break's Anvato type=cue_end was observed and its length on the stream clock (cue_end pts minus the break start pts) is within 2s of the break duration. Emitted once per break; context: breakId, mediaTime (where the player reported the cue_end), lengthSec, durationSec."},"DA-ANVATO-CUE-END-REJECTED":{category:"break",level:"warn",summary:"ptsSource: 'anvatoCue' — an Anvato type=cue_end was observed for the active break but its stream-clock length differs from the break duration by more than 2s, so it is ignored and the resume keeps its computed target. Emitted once per break; context: breakId, mediaTime, lengthSec, durationSec."},"DA-PRELOAD-FAILED":{category:"preload",level:"warn",summary:"Ad preload failed; the asset will be loaded on demand at break start."},"DA-PRELOAD-MODE-RESOLVED":{category:"preload",level:"info",summary:"The configured ad preload mode was resolved for this session; context carries both the configured and the resolved mode."},"DA-PRELOAD-PATH":{category:"preload",level:"info",summary:"The preload path actually taken for a break's ad (parallel buffer / detached prefetch / first-frame warm-up on the ad player / none) and the creative container (stream vs progressive)."},"DA-BREAK-EARLY-RETURN":{category:"break",level:"info",summary:"The active break was removed from a polled manifest (early return); the break ends immediately and content resumes at break start plus the seconds already played."},"DA-BREAK-WINDOW-ESCAPED":{category:"break",level:"warn",summary:"An active wallclock break outlived its window on the stream clock (ad stall, bogus trigger position, or a re-fired completed break); the SDK ended it and returned to content. context.overrunSec says by how much."},"DA-BREAK-EVENT-UNSUPPORTED":{category:"break",level:"warn",summary:"A break is triggered by a spec event this SDK version does not schedule on this platform (web schedules `start`, `pause` and `end`; Android and iOS do not schedule `end` yet); the break was ignored. context.event names the event."},"DA-BREAK-SUPPRESSED-OVERLAP":{category:"break",level:"warn",summary:"A break was suppressed because an ad was already playing."},"DA-BREAK-TRANSITION":{category:"break",level:"info",summary:"Measured duration of a playback transition into an ad break, between two ads, between chained breaks, or back to content. A break-to-break switch runs from the outgoing ad's end to the incoming ad's first frame and adds path (queued, late-queued or cold) and, when the incoming ad was buffered, leadMs. On web an ad-to-ad switch adds path (queued or cold) and leadMs too."},"DA-BREAK-CHAIN":{category:"chaining",level:"info",summary:"Summary of one chain of back-to-back breaks, emitted once when content plays again (or the chain is flushed): chainId, per break its id, scheduledStart, plannedDurationSec, beganAtMs, endedAtMs, playedSec, announcedDuringChain and path, per switch its gapMs and path, contentResumeMs and totalMs."},"DA-AD-SURFACE-REVEALED":{category:"break",level:"info",summary:"The ad surface became visible: how long it was held back waiting for the ad's first frame, and whether it was revealed because that frame arrived or because the wait timed out."},"DA-BREAK-CUT-LATE":{category:"break",level:"warn",summary:"timebase: 'wallclock', all platforms — content was cut more than 0.5s after the break's manifest start on the stream's Program Date Time, so the viewer saw the stream's own break (slate) instead of the ad until then; typically the ad's first frame was not ready at the cut and content kept playing under the cross-fade. The replaced window is unaffected: the resume anchor is the content time of the manifest start whatever the cut landed on. Context carries breakId, lateSec (cut minus start on the PDT axis), start (the manifest start), cutPdt (the PDT at the cut), cutAt (currentTime at the cut) and anchor; iOS/Android add waitedForAdMs when the cut waited for the ad surface."},"DA-PREROLL-CONTENT-HELD":{category:"break",level:"info",summary:"Content playback was held at session start until a pending pre-roll began, so no content is shown before the ad."},"DA-PREROLL-CONTENT-WAIT-TIMEOUT":{category:"break",level:"info",summary:"A pending pre-roll began after content readiness was not reported within the wait timeout."},"DA-PREROLL-CANCELLED-CONTENT-ERROR":{category:"break",level:"warn",summary:"A pending pre-roll was cancelled because the content failed to load before it could begin."},"DA-SHARED-ELEMENT-UNSUPPORTED":{category:"break",level:"warn",summary:"Configured shared-element insertion is unsupported on this platform (non-iPhone/iPod); fell back to overlay insertion."},"DA-SHARED-ELEMENT-RESTORE-FAILED":{category:"playback",level:"error",summary:"Content did not resume after a shared-element break: the reload/seek/play restore failed twice; the viewer is left paused on the handed-back element."},"DA-IMAGE-LOAD-FAILED":{category:"break",level:"warn",summary:"An image (companion, backdrop or image ad) could not be fetched or decoded within the timeout; the break continues without it."},"DA-PIP-TRANSFER-FAILED":{category:"break",level:"warn",summary:"The picture-in-picture window could not be moved between the content and ad elements; the break continues but the viewer may not see the ad in the PiP window."},"DA-PIP-AUTODETECT-UNAVAILABLE":{category:"break",level:"warn",summary:"No Activity could be resolved from the renderer's Context or the overlay's view tree, so picture-in-picture cannot be detected automatically; the host must call OptiViewAds.setPictureInPicture() from Activity.onPictureInPictureModeChanged or breaks will be laid out for a fullscreen surface. Android only."},"DA-CONTENT-ELEMENT-SWAPPED":{category:"break",level:"info",summary:"The content player replaced its <video> element; the SDK re-bound its presentation listeners to the new one."},"DA-PIP-FORMAT-OVERRIDE":{category:"break",level:"info",summary:"The content player is in picture-in-picture, so the break's declared format was overruled and it plays as a single fullscreen ad."},"DA-PIP-ADOPTION-STATE":{category:"break",level:"info",summary:"Native PiP reconciliation deferred or reused a layout/handoff decision. Context carries reason, active, adPlaying, applied, and asset/break IDs when available; this is not evidence that a format override occurred."},"DA-PIP-HANDOFF":{category:"break",level:"info",summary:"The picture-in-picture handoff route chosen for the ad: 'layer-swap' (the presented AVPlayerLayer is re-pointed at the ad player) or 'borrow' (the ad plays through the content player). context: route, layers. iOS only."},"DA-PIP-LAYER-SWAP":{category:"break",level:"debug",summary:"Read-back check after a PiP layer-swap: whether the presented layer still points at the ad player after the re-point. context: held, layers. iOS only."},"DA-PIP-PRESENTED":{category:"break",level:"debug",summary:"Status trace of the player on the PiP-presented layer during a layer-swap: owner (ad/content/other) and time-control-status transitions, plus ad-item ends observed while presented. context: owner/status, or errored/reachedEnd/adStatus. iOS only."},"DA-CHAIN-RESOLVER-ERROR":{category:"chaining",level:"warn",summary:"The chained-successor resolver threw; chaining was skipped for this break."},"DA-GAM-NO-ASSET-KEY":{category:"gam",level:"warn",summary:"Retired: the GAM stream identity now comes from the manifest's vendorConfiguration. Kept declared so the code is never reused; see DA-GAM-CONFIG-MISSING."},"DA-GAM-SESSION-FAILED":{category:"gam",level:"error",summary:"The GAM/IMA session failed to initialize; GAM breaks will not serve."},"DA-GAM-SESSION-PRELOADED":{category:"gam",level:"info",summary:"A GAM/IMA session was preloaded for the next event; context includes the event boundary and GAM pod identity."},"DA-GAM-START-DEFERRED":{category:"gam",level:"info",summary:"The SGAI IMA stream request is deferred until the configured ad start delay has been played; context includes adStartDelaySec."},"DA-GAM-CONFIG-RESOLVED":{category:"gam",level:"info",summary:"The GAM stream identity for this session was resolved from the manifest's vendorConfiguration."},"DA-GAM-CONFIG-MISSING":{category:"gam",level:"error",summary:'The break manifest carries no usable "gam" vendorConfiguration entry; GAM assets are skipped.'},"DA-GAM-POD-IDENTITY-MISMATCH":{category:"gam",level:"error",summary:"A GAM pod asset's vendorParameters name a different network code or custom asset key than the stream session was created with; the asset is skipped because the pod request cannot serve."},"DA-GAM-POD-MID-BREAK-END":{category:"gam",level:"warn",summary:"IMA reported the ad break ended before the pod's last ad while pod stream remains; per-ad reporting continues until the pod ends."},"DA-GAM-DUPLICATE-METADATA":{category:"gam",level:"info",summary:"The stream carries the same ad marker more than once at the same time (for example in both the audio and video renditions of an fMP4 stream); the duplicate was not passed to IMA, so each ad event fires once. Reported once per stream session."},"DA-VENDOR-CONFIG-UNKNOWN-TYPE":{category:"manifest",level:"info",summary:"One or more vendorConfiguration entries declare a type this SDK version does not recognise and were ignored."},"DA-EXTERNAL-VENDOR-INVALID":{category:"manifest",level:"warn",summary:"External vendorConfiguration entries were ignored because provider was blank or assetParameters was not an object. context: count."},"DA-EXTERNAL-VENDOR-DUPLICATE":{category:"manifest",level:"warn",summary:"External vendorConfiguration entries repeated a provider; the first valid entry was used. context: providers."},"DA-CONTENT-PLAYBACK-ERROR":{category:"playback",level:"error",summary:"The content player reported a playback error."},"DA-EXTERNAL-PLAYBACK-CHANGED":{category:"playback",level:"info",summary:"The content player's external (AirPlay) playback state changed; context carries the new state and the target."},"DA-EXTERNAL-PLAYBACK-BLOCKED":{category:"playback",level:"warn",summary:"AirPlay was requested while an ad break is active or about to start; the content player stays on the device and the request is retried when the break ends."},"DA-SSB-SWITCH-FAILED":{category:"playback",level:"warn",summary:"Switching the content player to the SSB stream for AirPlay failed (context.reason: no-restore-source, load-error, load-timeout, external-not-active or receiver-paused); the SDK restores the previous content."},"DA-SSB-RESTORE-FAILED":{category:"playback",level:"error",summary:"The previous content source could not be reloaded after the SSB stream (AirPlay ended or the switch failed); the session stays in ssb delivery until the app reloads its content."},"DA-EXTERNAL-PLAYBACK-ALLOWED":{category:"playback",level:"info",summary:"An AirPlay request passed the ad-break guard; context.trigger says whether it was the request itself or the retry after a break."},"DA-CONTENT-RESUME-STALLED":{category:"playback",level:"warn",summary:"Content did not resume within the watchdog window after a break that paused it ended; the SDK re-issued play() once. iOS only for now."},"DA-CONTENT-RECOVERY-MODE-RESOLVED":{category:"playback",level:"info",summary:"The resolved post-break content recovery mode for the session (recreate or none); context carries configured and resolved."},"DA-CONTENT-PIPELINE-RECREATED":{category:"playback",level:"info",summary:"The content player's media pipeline was rebuilt at break end before content resumed (recovery mode recreate); context carries breakId, startTime and durationMs."},"DA-CONTENT-PIPELINE-RECREATE-FAILED":{category:"playback",level:"warn",summary:"Rebuilding the content media pipeline at break end failed or timed out — or the adapter does not implement recreateMediaPipeline() (reason 'unsupported-adapter', e.g. a wrapper adapter that does not forward it); the SDK fell back to resuming in place with seek + play; context carries breakId, startTime, reason."},"DA-AD-PLAYBACK-ERROR":{category:"playback",level:"error",summary:"The ad player reported a playback error during a break."},"DA-AD-LOAD-RETRIED":{category:"playback",level:"warn",summary:'The ad player\'s load() had not settled within the retry window of the ad-start budget and the ad element showed no progress (readyState 0, nothing buffered), so a second load() was issued for the same URI; a slow-but-progressing load is not retried. Context carries breakId, assetId, uri, elapsedMs, remainingBudgetMs. A still-stalled retry ends as DA-AD-PLAYBACK-ERROR "Ad load timeout".'},"DA-AD-URI-MISSING":{category:"playback",level:"error",summary:"The ad asset had no usable media URI, VAST tag URL or render surface; reported as CMCD adec."},"DA-AD-MEDIA-LOAD-FAILED":{category:"playback",level:"error",summary:"The ad player could not fetch the ad media (network or HTTP failure); reported as CMCD adec."},"DA-AD-MEDIA-UNSUPPORTED":{category:"playback",level:"error",summary:"The ad player could not decode the ad media or does not support its format; reported as CMCD adec."},"DA-AD-START-TIMEOUT":{category:"playback",level:"error",summary:"The ad did not load, render a first frame or start within its start budget; reported as CMCD adec."},"DA-AD-START-INTERRUPTED":{category:"playback",level:"warn",summary:"The ad element's play() was interrupted by a browser-initiated pause before the ad started (Chromium AbortError, seen on Samsung Tizen); the SDK re-issued play() once. context: breakId, error."},"DA-AD-DURATION-PREPARED":{category:"playback",level:"info",summary:"The ad media for a break was prepared and its real playable duration is now known, BEFORE the first frame. context: breakId, assetId, index, total, preparedDurationSec (the loaded media's own duration — on a GAM pod, the stitched pod's length), declaredDurationSec (what the manifest said about this asset, when it said anything) and breakDurationSec (the slot the break declares). Emitted per asset, so a multi-asset break reports one per asset and the break total is their sum. The three together separate a short break caused by a short POD from one caused by a short request or a late join."},"DA-DELIVERY-MODE-RESOLVED":{category:"session",level:"info",summary:"Delivery mode (sgai/ssai/ssb) resolved for the session at start and again on an ads-channel switch from the manifest's delivery rules (or the explicit client config), with the detected device profile; context.source is external-playback or external-playback-ended when AirPlay switched the session to the SSB stream and back."},"DA-TARGETING-VALUE-UNKNOWN":{category:"manifest",level:"warn",summary:"A targeting value is not recognized and the targeted entry does not match."},"DA-TARGETING-FIELD-UNKNOWN":{category:"manifest",level:"warn",summary:"A targeting field is not recognized and the targeted entry does not match."},"DA-TARGETING-INVALID":{category:"manifest",level:"warn",summary:"A targeting constraint is malformed or incomplete and the targeted entry does not match."},"DA-DELIVERY-MODE-UNKNOWN":{category:"session",level:"warn",summary:"The matching delivery rule carried an unrecognized mode value; sgai is used (forward compatibility)."},"DA-DELIVERY-MODE-OVERRIDDEN":{category:"session",level:"info",summary:"The manifest's delivery rules resolved a different mode, but the explicit client config takes precedence."},"DA-DELIVERY-MODE-CHANGE-IGNORED":{category:"session",level:"warn",summary:"An ordinary manifest poll resolves a different delivery mode; the mode pinned for the session stays in force. An ads-channel switch resolves the mode again."},"DA-SSAI-UNAVAILABLE":{category:"session",level:"error",summary:"The manifest selected ssai delivery but no vendor configuration can provide a stitched stream (no gam entry with an assetKey); at start the session fails, while on a channel switch the session stays in sgai."},"DA-SSAI-SESSION-FAILED":{category:"session",level:"error",summary:"The SSAI session failed to start (IMA stream init, stream id, or stitched-stream load)."},"DA-SSAI-IMA-ERROR":{category:"gam",level:"error",summary:"IMA reported an error during the SSAI stream session."},"DA-INSERTION-TYPE-RESOLVED":{category:"session",level:"info",summary:"Per-session ad insertion type resolved, and whether the integrator set it or left it unset (unset lets the manifest's resumeOffset drive the resume point)."},"DA-RESUME-OFFSET-CLAMPED":{category:"break",level:"info",summary:"The resume offset was shortened to the ad duration actually played, so the break could not skip further than the ads it delivered."},"DA-INSERTION-TYPE-IGNORED-SSAI":{category:"session",level:"warn",summary:"adInsertionType is ignored in SSAI mode; the stitcher controls ad insertion server-side."},"DA-VAST-UNSUPPORTED-FORMAT":{category:"break",level:"warn",summary:"A VAST asset appeared in an unsupported break format; VAST is only supported in single, double, and lshape_ad. The break was skipped."},"DA-ASSET-MACRO-UNKNOWN":{category:"break",level:"warn",summary:"An asset parameter value contained a macro the SDK has no value for (an unknown or unavailable OptiView macro, or a registered customer macro that returned no value); the parameter was still sent, with the macro kept as its literal token. Raised once per parameter key and macro. context: key, macro, breakId, assetId."},"DA-ASSET-PARAMETER-OMITTED":{category:"break",level:"info",summary:"An asset parameter value contained a customer macro that is empty (value `null`), so the parameter was not sent; for GAM `cust_params`, and for a static-asset value with a literal `&`, only the `&`-separated pair containing the macro was dropped. Raised once per parameter key and macro (per dropped pair when the value is split into pairs). context: key, macro, pair (split values only), breakId, assetId."},"DA-VAST-SSAI-UNSUPPORTED":{category:"session",level:"warn",summary:"VAST assets are not supported in SSAI mode (VAST is SGAI-only); the asset was skipped."},"DA-VAST-IMA-ERROR":{category:"gam",level:"error",summary:"Google IMA reported an error while loading or playing a VAST (client-side / CSAI) ad."},"DA-VAST-NO-FILL":{category:"gam",level:"error",summary:"The VAST ad server returned no ad (empty response, no ads after wrappers or no assets); reported as CMCD adec."},"DA-VAST-TIMEOUT":{category:"gam",level:"error",summary:"The VAST response or its media file did not load in time; reported as CMCD adec."},"DA-VAST-MALFORMED":{category:"gam",level:"error",summary:"The VAST response could not be parsed or uses an unsupported VAST version; reported as CMCD adec."},"DA-VAST-REQUEST-FAILED":{category:"gam",level:"error",summary:"The VAST ad request failed (network error, invalid tag URL, wrapper error or too many redirects); reported as CMCD adec."},"DA-VAST-CREATIVE-FAILED":{category:"gam",level:"error",summary:"The VAST creative could not be played (no compatible media file, playback error or trafficking error); reported as CMCD adec."},"DA-VAST-IMA-SDK-MISSING":{category:"gam",level:"error",summary:"The Google IMA client-side SDK (ima3.js) is unavailable; VAST (CSAI) ads cannot be played."},"DA-PAUSE-AD-SHOWN":{category:"break",level:"info",summary:"A pause ad (full-screen image or video) was shown while content is paused."},"DA-PAUSE-AD-DISMISSED":{category:"break",level:"info",summary:"A pause ad was dismissed (content resumed, or the duration cap elapsed)."},"DA-PAUSE-AD-RESUME-ALLOWED":{category:"break",level:"info",summary:"The viewer may now resume content over the shown pause ad: the break's controls.skipOffset elapsed, or (no skipOffset) the ad was watched fully: a video creative ended, an image reached its duration cap or has none. context: breakId."},"DA-PAUSE-AD-RESUME-BLOCKED":{category:"break",level:"info",summary:"Content tried to resume while a pause ad must still be watched (controls.skipOffset not reached, or no skipOffset and the video creative has not ended / the image duration cap has not elapsed); the SDK keeps content paused. context: breakId, elapsedSec, skipOffset."},"DA-PAUSE-AD-DVR-EXPIRED":{category:"break",level:"info",summary:"The viewer paused for longer than the DVR window is deep, so the position they paused at aged out and playback resumed at the live edge instead. context: pinnedPosition, liveEdge."},"DA-PAUSE-AD-NO-ASSET":{category:"break",level:"warn",summary:"A pause break had no usable image or video asset (no first asset, unsupported media type, or no resolvable VAST creative); the pause ad was skipped."},"DA-PAUSE-AD-SSAI-UNSUPPORTED":{category:"session",level:"warn",summary:"Pause ads are SGAI-only and not supported in SSAI mode; the pause break was skipped."},"DA-MANIFEST-POLLED":{category:"manifest",level:"info",summary:"A manifest poll refresh succeeded (cadence follows the manifest polling config)."},"DA-AD-SKIPPED":{category:"playback",level:"info",summary:"The viewer skipped the active ad break (controls.skipOffset honored)."},"DA-AD-SKIP-SUPPRESSED":{category:"playback",level:"info",summary:"A skip request was suppressed (no skipOffset declared, or requested before the offset)."},"DA-AD-CLICKED":{category:"playback",level:"info",summary:"The viewer clicked the ad on screen (adclick emitted)."},"DA-LIVE-START-PUSHBACK":{category:"playback",level:"info",summary:"Initial live playback requested a backward seek to the configured target-duration distance. Context includes enabledBy (always config), targetDurationSec, targetDurationSource, initialOffsetSec, initialOffsetTargetDurations, requestedOffsetSec, requestedOffsetTargetDurations, targetOffsetSec, targetOffsetTargetDurations, rewindSec, rewindTargetDurations, target and windowClamped. This is a request, not proof of the landing."},"DA-LIVE-START-PUSHBACK-SKIPPED":{category:"playback",level:"info",summary:"Initial live playback was left unchanged. The reason identifies a disabled adjustment (liveStartPushback.enabled is not true; enabledBy is always config), an already sufficient offset, missing target duration or seekable range, invalid configuration, or no safe backward target. Available target-duration and offset measurements are included."},"DA-LIVE-START-PUSHBACK-LANDED":{category:"playback",level:"info",summary:"Measured initial-live seek landing. Context includes the requested target and targetDurationSec, landed, errorSec, liveEdgeAtLanding, landedOffsetSec and landedOffsetTargetDurations. A timeout or failed seek reports landed=null, never an inferred success."},"DA-RESUME-SEEK":{category:"playback",level:"info",summary:"Content was resume-seeked to the resolved post-break position (insertion type policy)."},"DA-RESUME-UNADDRESSABLE":{category:"playback",level:"warn",summary:"A break played through the content player (picture-in-picture), so content came back on a reloaded source whose timeline has its own origin, and that source published neither a program-date-time nor a seekable range to re-address the resume point against. Content stays where the reload left it — near the live edge — rather than being seeked to a position that does not exist on the new timeline. Context carries breakId, target (the position on the pre-borrow timeline) and offset."},"DA-RESUME-LANDED":{category:"playback",level:"info",summary:"Where the post-break resume-seek actually landed, measured on the seeked event rather than inferred: context carries breakId, target (the position requested), landed and errorSec (landed - target; negative means the player settled EARLIER than asked). errorSec only says whether the SEEK did what it was asked: when the live-window clamp moved the seek, policyTarget is the resume point before the clamp and policyErrorSec (landed - policyTarget) is how far the viewer really is from it. landed is null with a reason when no seeked event arrived, which is normal for a seek to the position the playhead already holds. Report-only: the SDK does not correct the position."},"DA-RESUME-PREPARED":{category:"playback",level:"info",summary:"Android, live break that resumes at its end — the paused content was parked exactly on the resume point before the hand-back, so the hand-back only plays (DA-RESUME-SEEK then reports prepared=true and issues no seek). Context carries breakId, target, source (cue_end when the stream's matched type=cue_end was known, else cue+duration) leadMs (how long before the expected hand-back it was parked) and refresh (true when content parked long before the hand-back was seeked onto the same point again, 4.5s before it). A later cue_end parks content again."},"DA-RESUME-PREPOSITIONED":{category:"playback",level:"info",summary:"The break resumed without a seek: content was moved onto its resume position DURING the break, so the transition needed no jump. Context carries breakId, target, prepositionedTo, landed, shortfallSec and exact. exact=true means it reached the policy target. exact=false means the live window never published those frames, so content was moved as close as exists — the viewer lands short, but arrives already in place rather than via a visible seek at the cut."},"DA-RESUME-PTS-MAP":{category:"playback",level:"info",summary:"ptsSource: 'anvatoCue', iOS/tvOS and web — the live resume point was mapped onto currentTime through the stream's own clock: the Anvato type=beacon tags delivered since the run-up's last seek give the currentTime ↔ stream-clock offset, and the resume point (the matched cue_end, else the cue plus the break duration) is converted with it. Reported when a mapping is first available after a seek and whenever the mapped target moves by 0.1s or more. Context carries breakId, target, landingStreamSec, landingSource (cue_end or cue+duration), offsetSec, beacons (beacons averaged), generation (how often the mapping started over since the session began), currentTime, and computedTarget with deltaSec (target - computedTarget) when a currentTime-based target exists."},"DA-RESUME-PTS-REMAP":{category:"playback",level:"warn",summary:"ptsSource: 'anvatoCue', iOS/tvOS and web — a stream beacon disagreed with the currentTime ↔ stream-clock mapping by more than 0.2s, so the player re-anchored currentTime (a variant switch or a buffer restart); the mapping starts over from that beacon and the resume point is mapped again. Reported during the resume run-up and after the hand-back. Context carries breakId, fromOffsetSec, toOffsetSec, deltaSec, currentTime and stage (run-up or after-hand-back)."},"DA-RESUME-PTS-CORRECTED":{category:"playback",level:"info",summary:"ptsSource: 'anvatoCue', iOS/tvOS and web — once per break at the hand-back, on a stream that carries Anvato beacons: whether the stream clock placed the live resume point, and by how much it moved it. applied=true: streamClockTarget is the resume point mapped through the beacons, correctionSec is streamClockTarget - computedTarget (the currentTime target, cue plus duration), and currentTimeShiftSec is how far the same stream instant moved in currentTime between the pause and the hand-back (negative: it now sits earlier), null when no beacon mapped the stream before the pause. Context also carries breakId, landingSource (cue_end or cue+duration), landingStreamSec, offsetSec, beacons, remaps (DA-RESUME-PTS-REMAP during the break) and cueEndTarget when a cue_end was matched. applied=false with a reason means no beacon arrived after the run-up's last seek, so the cue_end or currentTime target applied."},"DA-RESUME-PTS-LANDED":{category:"playback",level:"info",summary:"ptsSource: 'anvatoCue', iOS/tvOS and web — where a stream-clock resume landed, measured on the stream's own clock from the beacons delivered after the hand-back: landedStreamSec is the position content resumed from (resumedAt, the seek landing or the playhead at a seek-free hand-back) converted with that mapping, and errorSec is landedStreamSec - landingStreamSec (negative: content resumed before the stream's resume point, so slate is shown). Context carries breakId, landingStreamSec, landingSource, resumedAt, sinceHandBackSec and, when measured, landedStreamSec, errorSec, offsetSec and beacons; reason when no beacon arrived within 15s. Report-only: the SDK does not correct the position from it."},"DA-RESUME-PDT-MAP":{category:"playback",level:"info",summary:"timebase: 'wallclock', all platforms — the live resume point (the manifest start plus duration, a wallclock instant) was mapped onto currentTime through the Program Date Time the content player reports now, so a currentTime re-anchored by a seek during the break cannot move it. Reported when the run-up first reads the target and whenever the mapped target moves by 0.1s or more (remapped=true: currentTime moved against the stream). Context carries breakId, target, landingPdt (the break end, ISO-8601), pdtOffsetSec (PDT minus currentTime, seconds), currentTime, remapped, and computedTarget with deltaSec (target - computedTarget) when an anchor-based target exists."},"DA-RESUME-PDT-CORRECTED":{category:"playback",level:"info",summary:"timebase: 'wallclock', all platforms — once per break at the hand-back of a break anchored on the Program Date Time: whether the stream clock placed the live resume point, and by how much it moved it. applied=true: pdtTarget is the break end mapped through the PDT the player reports at the hand-back, correctionSec is pdtTarget - computedTarget (the anchor plus the offset) and currentTimeShiftSec is how far the same stream instant moved in currentTime between the cut and the hand-back (negative: it now sits earlier). applied=false with a reason means no PDT sample was available at the hand-back, so the anchor-based target applied. Context also carries breakId and computedTarget."},"DA-RESUME-PDT-LANDED":{category:"playback",level:"info",summary:"timebase: 'wallclock', all platforms — where a Program-Date-Time resume landed, measured on the stream's own clock from the first position sample after the hand-back: errorSec is the PDT content resumed at minus the break end (negative: content resumed before the break end, so the stream's own break is shown; positive: post-break content was skipped). Context carries breakId, landingPdt (the break end), landedPdt, landedAt (currentTime) and sinceHandBackSec; reason when no sample carried a PDT within 2s. Report-only: the SDK does not correct the position from it."},"DA-RESUME-CUE-END":{category:"playback",level:"info",summary:"ptsSource: 'anvatoCue', iOS/tvOS, web and Android — the stream's matched type=cue_end replaced the computed resume target: the slate under the ad ends there, not at cue plus the advertised duration (150.206s for a 150s break on NFL Channel), and the player can remap its timeline while content is held, so the cue_end is reported in the current mapping. Context carries breakId, computedTarget, cueEndTarget, deltaSec (cueEndTarget - computedTarget), stage (hold, playing or hand-back; Android: always hand-back) and corrected (whether an in-buffer correction seek ran). Only a policy that resumes at the break end is replaced; otherwise applied=false and only the delta is reported."},"DA-RESUME-CUE-END-LATE":{category:"playback",level:"info",summary:"ptsSource: 'anvatoCue', iOS/tvOS and Android — the break's type=cue_end was matched only after the hand-back. When it is more than 1s ahead of the playhead and buffered, content is seeked forward to it once (corrected=true); content is never seeked backward. Context carries breakId, cueEndTarget, currentTime, deltaSec (cueEndTarget - currentTime) and corrected."},"DA-RESUME-CATCHUP":{category:"playback",level:"info",summary:"Content began playing behind the ad to reach its resume point. Context carries breakId, startPos, target, remainingAdSec, publishedEnd and clampedStart — clampedStart true means the stream had not published the ideal start position, so playback began late and the alignment loop must make up the difference (the zero-live-headroom case). Its ABSENCE on a live break that resumed short means catch-up never ran at all, which is a different fault from it running and falling short."},"DA-RESUME-TARGET-CLAMPED":{category:"playback",level:"warn",summary:"The computed resume position sat outside the held live window and was clamped to the nearest end of it; context carries breakId, requested, clamped, shortfallSec and bound — 'end' when the target is ahead of the published edge, 'start' when it is behind the live window start (already evicted) — applied only when the gap is at most half the held window, otherwise DA-RESUME-START-CLAMP-SKIPPED is emitted instead. Distinct from DA-RESUME-OFFSET-CLAMPED, which shortens the OFFSET to the ad seconds actually played."},"DA-RESUME-START-CLAMP-SKIPPED":{category:"playback",level:"info",summary:"The resume target sat behind the element's live seekable start by more than half the held window, which cannot be DVR eviction (the element's seekable range disagrees with the adapter timeline); the target was left unchanged and the engine resolved the seek itself. Context carries breakId, requested, seekableStart, behindSec, windowSec and maxBehindSec."},"DA-RESUME-ALIGN-BLOCKED":{category:"playback",level:"warn",summary:"Resume alignment wanted to move content past the live edge and stood down, because no seek can reach media the stream has not published yet; content is left riding the edge and the break-end clamp places it. Context carries target, requested, safeEnd, shortfallSec and remainingSec. Emitted once per break. A persistent shortfall here means the resume target is further ahead than the feed's published edge — typically the player's view of the edge is stale, not that the seek is wrong."},"DA-CONTINUE-PLAYOUT-BEGIN":{category:"playback",level:"info",summary:"continueContentDuringBreak engaged: content keeps PLAYING through the break, hidden and muted, so no resume seek is needed. Context carries breakId, anchor, breakDurationSec and edgeDistanceSec."},"DA-CONTINUE-PLAYOUT-END":{category:"playback",level:"info",summary:"continueContentDuringBreak reached the cut. Context carries driftSec (content position minus where the break says it should be), maxDriftSec, the corrections applied, rateHonoured (false if the engine drove playbackRate itself) and driftUnrecoverable."},"DA-CONTINUE-PLAYOUT-REFUSED":{category:"playback",level:"info",summary:"continueContentDuringBreak was not used this session, so breaks pause content and resume with a seek as before. Emitted once per session; context carries the reason (disabled, not-replacement, format, shared-element, picture-in-picture, single-decoder) and the break format. Its ABSENCE on an eligible session is the signal the mode engaged."},"DA-CONTINUE-PLAYOUT-QUALITY-UNAVAILABLE":{category:"playback",level:"warn",summary:"Content continues through the replacement break without a video-quality constraint, so the content stream may retain its normal bitrate alongside the ad. Context carries breakId and reason (unsupported when the optional Web adapter method is absent, not-applied when the adapter returned false or could not apply the request)."},"DA-CONTINUE-PLAYOUT-DRIFT":{category:"playback",level:"warn",summary:"Content drifted further from the break timeline than the remaining break can remove within the playback-rate bounds, so part of it is carried into the cut. Context carries driftSec, remainingSec, the rate applied and seekIssued (always false: a corrective seek would be clipped)."},"DA-CONTINUE-PLAYOUT-ABANDONED":{category:"playback",level:"warn",summary:"Content could not be kept playing through the break, so it reverted to the pause-and-resume path mid-break. Context carries breakId, the reason and the driftSec at the moment it converted."},"DA-AD-AV-DESYNC":{category:"playback",level:"warn",summary:"The ad player's audio sink was handed a buffer whose timestamp does not continue the audio it has already been fed, so the media clock had to be re-anchored mid-ad. Video is scheduled off that clock while the audio already queued in the output track keeps draining, so every re-anchor pushes the picture further from the sound actually being heard. Context carries breakId, assetId, expectedSec, actualSec, deltaSec (negative = audio running behind the picture) and positionSec. Seen on server-stitched pods whose per-ad audio and video durations do not match; a single event near the end of a pod is the accumulated total, not an isolated glitch."},"DA-AD-PLAYOUT-SYNC":{category:"playback",level:"info",summary:"Per-break summary of ad playout health, emitted once at break end. Context carries podUri (the resolved ad media, so a stitched pod can be fetched and inspected), desyncEvents, maxDesyncSec, droppedFrames, audioUnderruns, audioFormats and videoFormats (the distinct decoder input formats seen during the break — more than one means the stitched stream changed sample rate or codec mid-pod) and frameOffsetMeanMs (mean video frame processing offset; large negative values mean frames were released late against the audio clock)."},"DA-EVENT":{category:"event",level:"info",summary:"An SDK event, recorded into the diagnostic ring buffer so exportDiagnostics() carries one unified timeline of diagnostics and playback events. Raised to error for aderror. Buffer-only: not dispatched to onDiagnostic handlers, which would duplicate the event stream they already receive."},"DA-BREAK-CONTENT-MASK":{category:"break",level:"warn",summary:"The host's content surface cannot follow view transforms (punch-through SurfaceView, e.g. react-native-video's default), so the two-box format masks the backdrop with a transparent hole over the content box instead of reframing the content view. context: format."},"DA-BREAK-FORMAT-DEGRADED":{category:"break",level:"warn",summary:"Split break reframing degraded: the host supplied no content view or sibling container for the ad."},"DA-BREAK-NO-PLAYABLE-ASSET":{category:"break",level:"warn",summary:"A break had nothing to play, so it never began: no adbreakbegin/adbreakend was emitted, content kept playing, and a public aderror (source ad, carrying the break) reports it. context.reason says why: no-variant (none declared), no-qualifying-variant (variant selection - device targeting + format playability - matched nothing and no downgrade applied), no-assets (the selected variant was empty), or lshape-content-skipped (shared-element insertion cannot render a backdrop). This is the BREAK-level outcome; when a particular variant was passed over for an unrecognised format, DA-BREAK-VARIANT-FORMAT-UNKNOWN names that format alongside it. context: breakId, reason, declaredAssetCount, format."},"DA-BREAK-VARIANT-FORMAT-UNKNOWN":{category:"break",level:"warn",summary:"A break variant declares a format this SDK version does not recognise, so it was passed over during variant selection rather than guessed at - an unknown format is never playable, because rendering it as a fullscreen single would be the most invasive possible misreading. Raised once per skipped variant: a break that still has a playable variant plays it and reports this too, so the skip is never silent, and a break left with nothing also reports DA-BREAK-NO-PLAYABLE-ASSET with reason no-qualifying-variant. An ABSENT format is treated identically to an unrecognised one. context: breakId, format (the raw manifest string; empty when the field was absent)."},"DA-AD-STALLED":{category:"playback",level:"warn",summary:"An ad began playing and then stopped advancing: the playhead did not move for the stall threshold while the ad was meant to be playing. context: assetId, breakId, playbackState, stalledForMs, positionMs, bufferedAheadMs, reason."},"DA-AD-FOREGROUND-RESUMED":{category:"playback",level:"info",summary:"An in-flight ad resumed after the app returned to the foreground; context carries the suspended duration and ad position at resume."},"DA-AD-FOCUS-LOST":{category:"playback",level:"warn",summary:"The overlay ad lost permanent audio focus during an active break; the host player must not reclaim focus while the SDK ad is on screen. context: breakId, transient, reason."},"DA-AD-FOCUS-GRANTED":{category:"playback",level:"info",summary:"The overlay ad was granted media audio focus for the break, so system focus semantics (call, duck, permanent loss) apply to ad playback. context: breakId, cause, alreadyHeld."},"DA-AD-FOCUS-DENIED":{category:"playback",level:"warn",summary:"The platform refused the overlay ad's media audio-focus request, so the break plays with no focus of its own: another app's audio can mix over the ad, and a call can neither duck nor pause it. context: breakId, cause."},"DA-AD-FOCUS-ABANDONED":{category:"playback",level:"info",summary:"The overlay ad gave media audio focus back, so the content player's own focus request finds a clean stack. context: breakId, cause."},"DA-BREAK-ABANDONED":{category:"lifecycle",level:"warn",summary:"A break was still in flight when the session was torn down (endSession, destroy, or a startSession over an active session). context: breakId, cause, phase, unbalancedAdBegin, adIndex, totalAds, assetId."},"DA-RENDERER-UNAVAILABLE":{category:"lifecycle",level:"error",summary:"The renderer could not render a break because a required piece of it was not attached (e.g. no ad player), after the SDK had already emitted adbreakbegin — so the break began and will never end. context: breakId, missing."},"DA-SESSION-STARTING":{category:"session",level:"info",summary:"A step of startSession() was entered: begin, manifest-fetch, gam-init or scheduling. Emitted on entry, so the last one recorded names the step a hung startSession never finished. context: step, manifestUrl, elapsedMs. The URL is reported verbatim; the redacted config summary carries only its origin (config.manifestOrigin)."}};
|
|
1
|
+
export const DIAGNOSTIC_CODES={"DA-SESSION-STARTED":{category:"session",level:"info",summary:"Monetization session started; manifest polling began. context: manifestUrl (stitcherUrl in SSAI mode), reported verbatim."},"DA-SESSION-ENDED":{category:"session",level:"info",summary:"Monetization session ended; polling and scheduling stopped."},"DA-ASSET-PARAMETERS-UPDATED":{category:"session",level:"info",summary:"The break manifest's session-level asset parameters changed; the merged parameters were pushed to the active GAM stream. context: source ('manifest'), keys (parameter names only, never values)."},"DA-MANIFEST-FETCH-FAILED":{category:"manifest",level:"error",summary:"Initial break-manifest fetch failed; the session could not start."},"DA-MANIFEST-POLL-FAILED":{category:"manifest",level:"warn",summary:"A manifest poll failed; the SDK keeps the last good manifest and retries."},"DA-MANIFEST-CHANNEL-CHANGED":{category:"manifest",level:"info",summary:"A polled manifest describes a different ads channel (channelId, GAM identity or timebase changed); the SDK re-aligned its ad setup and resolved delivery mode again while the session and content playback continued. context: previousChannelId, channelId, reason, manifestUrl."},"DA-MANIFEST-INTERCEPT-FAILED":{category:"manifest",level:"warn",summary:"The customer manifest-response interceptor threw; the un-modified parsed manifest was used instead."},"DA-MANIFEST-REQUEST-INTERCEPT-FAILED":{category:"manifest",level:"warn",summary:"The customer manifest-request interceptor threw; the normal network fetch of the original URL was used instead."},"DA-MANIFEST-SIGNATURE-INVALID":{category:"manifest",level:"error",summary:"The break-manifest response failed detached-JWS signature verification (missing/malformed X-Manifest-Signature, unsupported alg, unknown kid, unknown critical param, or signature mismatch); the manifest was rejected and no breaks were scheduled. context: url, reason."},"DA-MANIFEST-EVENT-INVALID":{category:"manifest",level:"warn",summary:"A channel event was ignored because its time window or configuration is invalid, or because events are unsupported for the manifest timebase. context: eventId and index when available."},"DA-PDT-MISSING":{category:"playback",level:"warn",summary:"No EXT-X-PROGRAM-DATE-TIME once content is playing (or after the startup backstop window); wallclock-timebase breaks fall back to the system clock."},"DA-ANVATO-CUE-PENDING":{category:"break",level:"info",summary:"ptsSource: 'anvatoCue' — no Anvato in-stream cue observed yet for a PTS break; the break stays pending until its cue arrives."},"DA-ANVATO-CUE-MATCHED":{category:"break",level:"info",summary:"ptsSource: 'anvatoCue' — a PTS break's start was matched to an observed Anvato cue and scheduled at the cue's media time."},"DA-ANVATO-CUE-ENTERED":{category:"break",level:"info",summary:"ptsSource: 'anvatoCue' — a break was started by the cue reaching the playhead rather than by the scheduler's 250ms tick; context carries the breakId, the cue pts and the media time it fired at, so the two start paths are distinguishable in the field."},"DA-ANVATO-CUE-OBSERVED":{category:"break",level:"info",summary:"ptsSource: 'anvatoCue' — an Anvato break-signaling (type=cue) frame was observed in the stream; context carries the cue's pts, the media time it landed at, whether the tracker recorded it, and the raw payload — so a manifest-PTS vs in-stream-PTS misalignment is observable in the field."},"DA-ANVATO-CUE-SEGMENT-ANCHORED":{category:"break",level:"warn",summary:"ptsSource: 'anvatoCue' — an Anvato emsg cue arrived without a ptsOffset from the adapter, so its break is scheduled at the cue's carrying segment start, which can be up to one segment early. Emitted once per session; context: pts, mediaTime."},"DA-ANVATO-CUE-OFFSET-REJECTED":{category:"break",level:"warn",summary:"ptsSource: 'anvatoCue' — the adapter's ptsOffset placed an Anvato cue outside its carrying segment's window (more than 0.1s before or 15s after the cue startTime), so the cue's startTime is used instead. Emitted once per cue pts; context: pts, startTime, ptsOffset, mappedTime."},"DA-ANVATO-CUE-END-MATCHED":{category:"break",level:"info",summary:"ptsSource: 'anvatoCue' — the active break's Anvato type=cue_end was observed and its length on the stream clock (cue_end pts minus the break start pts) is within 2s of the break duration. Emitted once per break; context: breakId, mediaTime (where the player reported the cue_end), lengthSec, durationSec."},"DA-ANVATO-CUE-END-REJECTED":{category:"break",level:"warn",summary:"ptsSource: 'anvatoCue' — an Anvato type=cue_end was observed for the active break but its stream-clock length differs from the break duration by more than 2s, so it is ignored and the resume keeps its computed target. Emitted once per break; context: breakId, mediaTime, lengthSec, durationSec."},"DA-PRELOAD-FAILED":{category:"preload",level:"warn",summary:"Ad preload failed; the asset will be loaded on demand at break start."},"DA-PRELOAD-MODE-RESOLVED":{category:"preload",level:"info",summary:"The configured ad preload mode was resolved for this session; context carries both the configured and the resolved mode."},"DA-PRELOAD-PATH":{category:"preload",level:"info",summary:"The preload path actually taken for a break's ad (parallel buffer / detached prefetch / first-frame warm-up on the ad player / none) and the creative container (stream vs progressive)."},"DA-BREAK-EARLY-RETURN":{category:"break",level:"info",summary:"The active break was removed from a polled manifest (early return); the break ends immediately and content resumes at break start plus the seconds already played."},"DA-BREAK-WINDOW-ESCAPED":{category:"break",level:"warn",summary:"An active wallclock break outlived its window on the stream clock (ad stall, bogus trigger position, or a re-fired completed break); the SDK ended it and returned to content. context.overrunSec says by how much."},"DA-BREAK-EVENT-UNSUPPORTED":{category:"break",level:"warn",summary:"A break is triggered by a spec event this SDK version does not schedule on this platform (web schedules `start`, `pause` and `end`; Android and iOS do not schedule `end` yet); the break was ignored. context.event names the event."},"DA-BREAK-SUPPRESSED-OVERLAP":{category:"break",level:"warn",summary:"A break was suppressed because an ad was already playing."},"DA-BREAK-TRANSITION":{category:"break",level:"info",summary:"Measured duration of a playback transition into an ad break, between two ads, between chained breaks, or back to content. A break-to-break switch runs from the outgoing ad's end to the incoming ad's first frame and adds path (queued, late-queued or cold) and, when the incoming ad was buffered, leadMs. On web an ad-to-ad switch adds path (queued or cold) and leadMs too."},"DA-BREAK-CHAIN":{category:"chaining",level:"info",summary:"Summary of one chain of back-to-back breaks, emitted once when content plays again (or the chain is flushed): chainId, per break its id, scheduledStart, plannedDurationSec, beganAtMs, endedAtMs, playedSec, announcedDuringChain and path, per switch its gapMs and path, contentResumeMs and totalMs."},"DA-AD-SURFACE-REVEALED":{category:"break",level:"info",summary:"The ad surface became visible: how long it was held back waiting for the ad's first frame, and whether it was revealed because that frame arrived or because the wait timed out. On web, an ad shown before its first frame reports reason shown-before-frame with visibleGapMs, the time on screen without a picture."},"DA-BREAK-CUT-LATE":{category:"break",level:"warn",summary:"timebase: 'wallclock', all platforms — content was cut more than 0.5s after the break's manifest start on the stream's Program Date Time, so the viewer saw the stream's own break (slate) instead of the ad until then; typically the ad's first frame was not ready at the cut and content kept playing under the cross-fade. The replaced window is unaffected: the resume anchor is the content time of the manifest start whatever the cut landed on. Context carries breakId, lateSec (cut minus start on the PDT axis), start (the manifest start), cutPdt (the PDT at the cut), cutAt (currentTime at the cut) and anchor; iOS/Android add waitedForAdMs when the cut waited for the ad surface."},"DA-PREROLL-CONTENT-HELD":{category:"break",level:"info",summary:"Content playback was held at session start until a pending pre-roll began, so no content is shown before the ad."},"DA-PREROLL-CONTENT-WAIT-TIMEOUT":{category:"break",level:"info",summary:"A pending pre-roll began after content readiness was not reported within the wait timeout."},"DA-PREROLL-CANCELLED-CONTENT-ERROR":{category:"break",level:"warn",summary:"A pending pre-roll was cancelled because the content failed to load before it could begin."},"DA-SHARED-ELEMENT-UNSUPPORTED":{category:"break",level:"warn",summary:"Configured shared-element insertion is unsupported on this platform (non-iPhone/iPod); fell back to overlay insertion."},"DA-SHARED-ELEMENT-RESTORE-FAILED":{category:"playback",level:"error",summary:"Content did not resume after a shared-element break: the reload/seek/play restore failed twice; the viewer is left paused on the handed-back element."},"DA-IMAGE-LOAD-FAILED":{category:"break",level:"warn",summary:"An image (companion, backdrop or image ad) could not be fetched or decoded within the timeout; the break continues without it."},"DA-PIP-TRANSFER-FAILED":{category:"break",level:"warn",summary:"The picture-in-picture window could not be moved between the content and ad elements; the break continues but the viewer may not see the ad in the PiP window."},"DA-PIP-AUTODETECT-UNAVAILABLE":{category:"break",level:"warn",summary:"No Activity could be resolved from the renderer's Context or the overlay's view tree, so picture-in-picture cannot be detected automatically; the host must call OptiViewAds.setPictureInPicture() from Activity.onPictureInPictureModeChanged or breaks will be laid out for a fullscreen surface. Android only."},"DA-CONTENT-ELEMENT-SWAPPED":{category:"break",level:"info",summary:"The content player replaced its <video> element; the SDK re-bound its presentation listeners to the new one."},"DA-PIP-FORMAT-OVERRIDE":{category:"break",level:"info",summary:"The content player is in picture-in-picture, so the break's declared format was overruled and it plays as a single fullscreen ad."},"DA-PIP-ADOPTION-STATE":{category:"break",level:"info",summary:"Native PiP reconciliation deferred or reused a layout/handoff decision. Context carries reason, active, adPlaying, applied, and asset/break IDs when available; this is not evidence that a format override occurred."},"DA-PIP-HANDOFF":{category:"break",level:"info",summary:"The picture-in-picture handoff route chosen for the ad: 'layer-swap' (the presented AVPlayerLayer is re-pointed at the ad player) or 'borrow' (the ad plays through the content player). context: route, layers. iOS only."},"DA-PIP-LAYER-SWAP":{category:"break",level:"debug",summary:"Read-back check after a PiP layer-swap: whether the presented layer still points at the ad player after the re-point. context: held, layers. iOS only."},"DA-PIP-PRESENTED":{category:"break",level:"debug",summary:"Status trace of the player on the PiP-presented layer during a layer-swap: owner (ad/content/other) and time-control-status transitions, plus ad-item ends observed while presented. context: owner/status, or errored/reachedEnd/adStatus. iOS only."},"DA-CHAIN-RESOLVER-ERROR":{category:"chaining",level:"warn",summary:"The chained-successor resolver threw; chaining was skipped for this break."},"DA-GAM-NO-ASSET-KEY":{category:"gam",level:"warn",summary:"Retired: the GAM stream identity now comes from the manifest's vendorConfiguration. Kept declared so the code is never reused; see DA-GAM-CONFIG-MISSING."},"DA-GAM-SESSION-FAILED":{category:"gam",level:"error",summary:"The GAM/IMA session failed to initialize; GAM breaks will not serve."},"DA-GAM-SESSION-PRELOADED":{category:"gam",level:"info",summary:"A GAM/IMA session was preloaded for the next event; context includes the event boundary and GAM pod identity."},"DA-GAM-START-DEFERRED":{category:"gam",level:"info",summary:"The SGAI IMA stream request is deferred until the configured ad start delay has been played; context includes adStartDelaySec."},"DA-GAM-CONFIG-RESOLVED":{category:"gam",level:"info",summary:"The GAM stream identity for this session was resolved from the manifest's vendorConfiguration."},"DA-GAM-CONFIG-MISSING":{category:"gam",level:"error",summary:'The break manifest carries no usable "gam" vendorConfiguration entry; GAM assets are skipped.'},"DA-GAM-POD-IDENTITY-MISMATCH":{category:"gam",level:"error",summary:"A GAM pod asset's vendorParameters name a different network code or custom asset key than the stream session was created with; the asset is skipped because the pod request cannot serve."},"DA-GAM-POD-MID-BREAK-END":{category:"gam",level:"warn",summary:"IMA reported the ad break ended before the pod's last ad while pod stream remains; per-ad reporting continues until the pod ends."},"DA-GAM-DUPLICATE-METADATA":{category:"gam",level:"info",summary:"The stream carries the same ad marker more than once at the same time (for example in both the audio and video renditions of an fMP4 stream); the duplicate was not passed to IMA, so each ad event fires once. Reported once per stream session."},"DA-VENDOR-CONFIG-UNKNOWN-TYPE":{category:"manifest",level:"info",summary:"One or more vendorConfiguration entries declare a type this SDK version does not recognise and were ignored."},"DA-EXTERNAL-VENDOR-INVALID":{category:"manifest",level:"warn",summary:"External vendorConfiguration entries were ignored because provider was blank or assetParameters was not an object. context: count."},"DA-EXTERNAL-VENDOR-DUPLICATE":{category:"manifest",level:"warn",summary:"External vendorConfiguration entries repeated a provider; the first valid entry was used. context: providers."},"DA-CONTENT-PLAYBACK-ERROR":{category:"playback",level:"error",summary:"The content player reported a playback error."},"DA-EXTERNAL-PLAYBACK-CHANGED":{category:"playback",level:"info",summary:"The content player's external (AirPlay) playback state changed; context carries the new state and the target."},"DA-EXTERNAL-PLAYBACK-BLOCKED":{category:"playback",level:"warn",summary:"AirPlay was requested while an ad break is active or about to start; the content player stays on the device and the request is retried when the break ends."},"DA-SSB-SWITCH-FAILED":{category:"playback",level:"warn",summary:"Switching the content player to the SSB stream for AirPlay failed (context.reason: no-restore-source, load-error, load-timeout, external-not-active or receiver-paused); the SDK restores the previous content."},"DA-SSB-RESTORE-FAILED":{category:"playback",level:"error",summary:"The previous content source could not be reloaded after the SSB stream (AirPlay ended or the switch failed); the session stays in ssb delivery until the app reloads its content."},"DA-EXTERNAL-PLAYBACK-ALLOWED":{category:"playback",level:"info",summary:"An AirPlay request passed the ad-break guard; context.trigger says whether it was the request itself or the retry after a break."},"DA-CONTENT-RESUME-STALLED":{category:"playback",level:"warn",summary:"Content did not resume within the watchdog window after a break that paused it ended; the SDK re-issued play() once. iOS only for now."},"DA-CONTENT-RECOVERY-MODE-RESOLVED":{category:"playback",level:"info",summary:"The resolved post-break content recovery mode for the session (recreate or none); context carries configured and resolved."},"DA-CONTENT-PIPELINE-RECREATED":{category:"playback",level:"info",summary:"The content player's media pipeline was rebuilt at break end before content resumed (recovery mode recreate); context carries breakId, startTime and durationMs."},"DA-CONTENT-PIPELINE-RECREATE-FAILED":{category:"playback",level:"warn",summary:"Rebuilding the content media pipeline at break end failed or timed out — or the adapter does not implement recreateMediaPipeline() (reason 'unsupported-adapter', e.g. a wrapper adapter that does not forward it); the SDK fell back to resuming in place with seek + play; context carries breakId, startTime, reason."},"DA-AD-PLAYBACK-ERROR":{category:"playback",level:"error",summary:"The ad player reported a playback error during a break."},"DA-AD-LOAD-RETRIED":{category:"playback",level:"warn",summary:'The ad player\'s load() had not settled within the retry window of the ad-start budget and the ad element showed no progress (readyState 0, nothing buffered), so a second load() was issued for the same URI; a slow-but-progressing load is not retried. Context carries breakId, assetId, uri, elapsedMs, remainingBudgetMs. A still-stalled retry ends as DA-AD-PLAYBACK-ERROR "Ad load timeout".'},"DA-AD-URI-MISSING":{category:"playback",level:"error",summary:"The ad asset had no usable media URI, VAST tag URL or render surface; reported as CMCD adec."},"DA-AD-MEDIA-LOAD-FAILED":{category:"playback",level:"error",summary:"The ad player could not fetch the ad media (network or HTTP failure); reported as CMCD adec."},"DA-AD-MEDIA-UNSUPPORTED":{category:"playback",level:"error",summary:"The ad player could not decode the ad media or does not support its format; reported as CMCD adec."},"DA-AD-START-TIMEOUT":{category:"playback",level:"error",summary:"The ad did not load, render a first frame or start within its start budget; reported as CMCD adec."},"DA-AD-START-INTERRUPTED":{category:"playback",level:"warn",summary:"The ad element's play() was interrupted by a browser-initiated pause before the ad started (Chromium AbortError, seen on Samsung Tizen); the SDK re-issued play() once. context: breakId, error."},"DA-AD-DURATION-PREPARED":{category:"playback",level:"info",summary:"The ad media for a break was prepared and its real playable duration is now known, BEFORE the first frame. context: breakId, assetId, index, total, preparedDurationSec (the loaded media's own duration — on a GAM pod, the stitched pod's length), declaredDurationSec (what the manifest said about this asset, when it said anything) and breakDurationSec (the slot the break declares). Emitted per asset, so a multi-asset break reports one per asset and the break total is their sum. The three together separate a short break caused by a short POD from one caused by a short request or a late join."},"DA-DELIVERY-MODE-RESOLVED":{category:"session",level:"info",summary:"Delivery mode (sgai/ssai/ssb) resolved for the session at start and again on an ads-channel switch from the manifest's delivery rules (or the explicit client config), with the detected device profile; context.source is external-playback or external-playback-ended when AirPlay switched the session to the SSB stream and back."},"DA-TARGETING-VALUE-UNKNOWN":{category:"manifest",level:"warn",summary:"A targeting value is not recognized and the targeted entry does not match."},"DA-TARGETING-FIELD-UNKNOWN":{category:"manifest",level:"warn",summary:"A targeting field is not recognized and the targeted entry does not match."},"DA-TARGETING-INVALID":{category:"manifest",level:"warn",summary:"A targeting constraint is malformed or incomplete and the targeted entry does not match."},"DA-DELIVERY-MODE-UNKNOWN":{category:"session",level:"warn",summary:"The matching delivery rule carried an unrecognized mode value; sgai is used (forward compatibility)."},"DA-DELIVERY-MODE-OVERRIDDEN":{category:"session",level:"info",summary:"The manifest's delivery rules resolved a different mode, but the explicit client config takes precedence."},"DA-DELIVERY-MODE-CHANGE-IGNORED":{category:"session",level:"warn",summary:"An ordinary manifest poll resolves a different delivery mode; the mode pinned for the session stays in force. An ads-channel switch resolves the mode again."},"DA-SSAI-UNAVAILABLE":{category:"session",level:"error",summary:"The manifest selected ssai delivery but no vendor configuration can provide a stitched stream (no gam entry with an assetKey); at start the session fails, while on a channel switch the session stays in sgai."},"DA-SSAI-SESSION-FAILED":{category:"session",level:"error",summary:"The SSAI session failed to start (IMA stream init, stream id, or stitched-stream load)."},"DA-SSAI-IMA-ERROR":{category:"gam",level:"error",summary:"IMA reported an error during the SSAI stream session."},"DA-INSERTION-TYPE-RESOLVED":{category:"session",level:"info",summary:"Per-session ad insertion type resolved, and whether the integrator set it or left it unset (unset lets the manifest's resumeOffset drive the resume point)."},"DA-RESUME-OFFSET-CLAMPED":{category:"break",level:"info",summary:"The resume offset was shortened to the ad duration actually played, so the break could not skip further than the ads it delivered."},"DA-INSERTION-TYPE-IGNORED-SSAI":{category:"session",level:"warn",summary:"adInsertionType is ignored in SSAI mode; the stitcher controls ad insertion server-side."},"DA-VAST-UNSUPPORTED-FORMAT":{category:"break",level:"warn",summary:"A VAST asset appeared in an unsupported break format; VAST is only supported in single, double, and lshape_ad. The break was skipped."},"DA-ASSET-MACRO-UNKNOWN":{category:"break",level:"warn",summary:"An asset parameter value contained a macro the SDK has no value for (an unknown or unavailable OptiView macro, or a registered customer macro that returned no value); the parameter was still sent, with the macro kept as its literal token. Raised once per parameter key and macro. context: key, macro, breakId, assetId."},"DA-ASSET-PARAMETER-OMITTED":{category:"break",level:"info",summary:"An asset parameter value contained a customer macro that is empty (value `null`), so the parameter was not sent; for GAM `cust_params`, and for a static-asset value with a literal `&`, only the `&`-separated pair containing the macro was dropped. Raised once per parameter key and macro (per dropped pair when the value is split into pairs). context: key, macro, pair (split values only), breakId, assetId."},"DA-VAST-SSAI-UNSUPPORTED":{category:"session",level:"warn",summary:"VAST assets are not supported in SSAI mode (VAST is SGAI-only); the asset was skipped."},"DA-VAST-IMA-ERROR":{category:"gam",level:"error",summary:"Google IMA reported an error while loading or playing a VAST (client-side / CSAI) ad."},"DA-VAST-NO-FILL":{category:"gam",level:"error",summary:"The VAST ad server returned no ad (empty response, no ads after wrappers or no assets); reported as CMCD adec."},"DA-VAST-TIMEOUT":{category:"gam",level:"error",summary:"The VAST response or its media file did not load in time; reported as CMCD adec."},"DA-VAST-MALFORMED":{category:"gam",level:"error",summary:"The VAST response could not be parsed or uses an unsupported VAST version; reported as CMCD adec."},"DA-VAST-REQUEST-FAILED":{category:"gam",level:"error",summary:"The VAST ad request failed (network error, invalid tag URL, wrapper error or too many redirects); reported as CMCD adec."},"DA-VAST-CREATIVE-FAILED":{category:"gam",level:"error",summary:"The VAST creative could not be played (no compatible media file, playback error or trafficking error); reported as CMCD adec."},"DA-VAST-IMA-SDK-MISSING":{category:"gam",level:"error",summary:"The Google IMA client-side SDK (ima3.js) is unavailable; VAST (CSAI) ads cannot be played."},"DA-PAUSE-AD-SHOWN":{category:"break",level:"info",summary:"A pause ad (full-screen image or video) was shown while content is paused."},"DA-PAUSE-AD-DISMISSED":{category:"break",level:"info",summary:"A pause ad was dismissed (content resumed, or the duration cap elapsed)."},"DA-PAUSE-AD-RESUME-ALLOWED":{category:"break",level:"info",summary:"The viewer may now resume content over the shown pause ad: the break's controls.skipOffset elapsed, or (no skipOffset) the ad was watched fully: a video creative ended, an image reached its duration cap or has none. context: breakId."},"DA-PAUSE-AD-RESUME-BLOCKED":{category:"break",level:"info",summary:"Content tried to resume while a pause ad must still be watched (controls.skipOffset not reached, or no skipOffset and the video creative has not ended / the image duration cap has not elapsed); the SDK keeps content paused. context: breakId, elapsedSec, skipOffset."},"DA-PAUSE-AD-DVR-EXPIRED":{category:"break",level:"info",summary:"The viewer paused for longer than the DVR window is deep, so the position they paused at aged out and playback resumed at the live edge instead. context: pinnedPosition, liveEdge."},"DA-PAUSE-AD-NO-ASSET":{category:"break",level:"warn",summary:"A pause break had no usable image or video asset (no first asset, unsupported media type, or no resolvable VAST creative); the pause ad was skipped."},"DA-PAUSE-AD-SSAI-UNSUPPORTED":{category:"session",level:"warn",summary:"Pause ads are SGAI-only and not supported in SSAI mode; the pause break was skipped."},"DA-MANIFEST-POLLED":{category:"manifest",level:"info",summary:"A manifest poll refresh succeeded (cadence follows the manifest polling config)."},"DA-AD-SKIPPED":{category:"playback",level:"info",summary:"The viewer skipped the active ad break (controls.skipOffset honored)."},"DA-AD-SKIP-SUPPRESSED":{category:"playback",level:"info",summary:"A skip request was suppressed (no skipOffset declared, or requested before the offset)."},"DA-AD-CLICKED":{category:"playback",level:"info",summary:"The viewer clicked the ad on screen (adclick emitted)."},"DA-LIVE-START-PUSHBACK":{category:"playback",level:"info",summary:"Initial live playback requested a backward seek to the configured target-duration distance. Context includes enabledBy (always config), targetDurationSec, targetDurationSource, initialOffsetSec, initialOffsetTargetDurations, requestedOffsetSec, requestedOffsetTargetDurations, targetOffsetSec, targetOffsetTargetDurations, rewindSec, rewindTargetDurations, target and windowClamped. This is a request, not proof of the landing."},"DA-LIVE-START-PUSHBACK-SKIPPED":{category:"playback",level:"info",summary:"Initial live playback was left unchanged. The reason identifies a disabled adjustment (liveStartPushback.enabled is not true; enabledBy is always config), an already sufficient offset, missing target duration or seekable range, invalid configuration, or no safe backward target. Available target-duration and offset measurements are included."},"DA-LIVE-START-PUSHBACK-LANDED":{category:"playback",level:"info",summary:"Measured initial-live seek landing. Context includes the requested target and targetDurationSec, landed, errorSec, liveEdgeAtLanding, landedOffsetSec and landedOffsetTargetDurations. A timeout or failed seek reports landed=null, never an inferred success."},"DA-RESUME-SEEK":{category:"playback",level:"info",summary:"Content was resume-seeked to the resolved post-break position (insertion type policy)."},"DA-RESUME-UNADDRESSABLE":{category:"playback",level:"warn",summary:"A break played through the content player (picture-in-picture), so content came back on a reloaded source whose timeline has its own origin, and that source published neither a program-date-time nor a seekable range to re-address the resume point against. Content stays where the reload left it — near the live edge — rather than being seeked to a position that does not exist on the new timeline. Context carries breakId, target (the position on the pre-borrow timeline) and offset."},"DA-RESUME-LANDED":{category:"playback",level:"info",summary:"Where the post-break resume-seek actually landed, measured on the seeked event rather than inferred: context carries breakId, target (the position requested), landed and errorSec (landed - target; negative means the player settled EARLIER than asked). errorSec only says whether the SEEK did what it was asked: when the live-window clamp moved the seek, policyTarget is the resume point before the clamp and policyErrorSec (landed - policyTarget) is how far the viewer really is from it. landed is null with a reason when no seeked event arrived, which is normal for a seek to the position the playhead already holds. Report-only: the SDK does not correct the position."},"DA-RESUME-PREPARED":{category:"playback",level:"info",summary:"Android, live break that resumes at its end — the paused content was parked exactly on the resume point before the hand-back, so the hand-back only plays (DA-RESUME-SEEK then reports prepared=true and issues no seek). Context carries breakId, target, source (cue_end when the stream's matched type=cue_end was known, else cue+duration) leadMs (how long before the expected hand-back it was parked) and refresh (true when content parked long before the hand-back was seeked onto the same point again, 4.5s before it). A later cue_end parks content again."},"DA-RESUME-PREPOSITIONED":{category:"playback",level:"info",summary:"The break resumed without a seek: content was moved onto its resume position DURING the break, so the transition needed no jump. Context carries breakId, target, prepositionedTo, landed, shortfallSec and exact. exact=true means it reached the policy target. exact=false means the live window never published those frames, so content was moved as close as exists — the viewer lands short, but arrives already in place rather than via a visible seek at the cut."},"DA-RESUME-PTS-MAP":{category:"playback",level:"info",summary:"ptsSource: 'anvatoCue', iOS/tvOS and web — the live resume point was mapped onto currentTime through the stream's own clock: the Anvato type=beacon tags delivered since the run-up's last seek give the currentTime ↔ stream-clock offset, and the resume point (the matched cue_end, else the cue plus the break duration) is converted with it. Reported when a mapping is first available after a seek and whenever the mapped target moves by 0.1s or more. Context carries breakId, target, landingStreamSec, landingSource (cue_end or cue+duration), offsetSec, beacons (beacons averaged), generation (how often the mapping started over since the session began), currentTime, and computedTarget with deltaSec (target - computedTarget) when a currentTime-based target exists."},"DA-RESUME-PTS-REMAP":{category:"playback",level:"warn",summary:"ptsSource: 'anvatoCue', iOS/tvOS and web — a stream beacon disagreed with the currentTime ↔ stream-clock mapping by more than 0.2s, so the player re-anchored currentTime (a variant switch or a buffer restart); the mapping starts over from that beacon and the resume point is mapped again. Reported during the resume run-up and after the hand-back. Context carries breakId, fromOffsetSec, toOffsetSec, deltaSec, currentTime and stage (run-up or after-hand-back)."},"DA-RESUME-PTS-CORRECTED":{category:"playback",level:"info",summary:"ptsSource: 'anvatoCue', iOS/tvOS and web — once per break at the hand-back, on a stream that carries Anvato beacons: whether the stream clock placed the live resume point, and by how much it moved it. applied=true: streamClockTarget is the resume point mapped through the beacons, correctionSec is streamClockTarget - computedTarget (the currentTime target, cue plus duration), and currentTimeShiftSec is how far the same stream instant moved in currentTime between the pause and the hand-back (negative: it now sits earlier), null when no beacon mapped the stream before the pause. Context also carries breakId, landingSource (cue_end or cue+duration), landingStreamSec, offsetSec, beacons, remaps (DA-RESUME-PTS-REMAP during the break) and cueEndTarget when a cue_end was matched. applied=false with a reason means no beacon arrived after the run-up's last seek, so the cue_end or currentTime target applied."},"DA-RESUME-PTS-LANDED":{category:"playback",level:"info",summary:"ptsSource: 'anvatoCue', iOS/tvOS and web — where a stream-clock resume landed, measured on the stream's own clock from the beacons delivered after the hand-back: landedStreamSec is the position content resumed from (resumedAt, the seek landing or the playhead at a seek-free hand-back) converted with that mapping, and errorSec is landedStreamSec - landingStreamSec (negative: content resumed before the stream's resume point, so slate is shown). Context carries breakId, landingStreamSec, landingSource, resumedAt, sinceHandBackSec and, when measured, landedStreamSec, errorSec, offsetSec and beacons; reason when no beacon arrived within 15s. Report-only: the SDK does not correct the position from it."},"DA-RESUME-PDT-MAP":{category:"playback",level:"info",summary:"timebase: 'wallclock', all platforms — the live resume point (the manifest start plus duration, a wallclock instant) was mapped onto currentTime through the Program Date Time the content player reports now, so a currentTime re-anchored by a seek during the break cannot move it. Reported when the run-up first reads the target and whenever the mapped target moves by 0.1s or more (remapped=true: currentTime moved against the stream). Context carries breakId, target, landingPdt (the break end, ISO-8601), pdtOffsetSec (PDT minus currentTime, seconds), currentTime, remapped, and computedTarget with deltaSec (target - computedTarget) when an anchor-based target exists."},"DA-RESUME-PDT-CORRECTED":{category:"playback",level:"info",summary:"timebase: 'wallclock', all platforms — once per break at the hand-back of a break anchored on the Program Date Time: whether the stream clock placed the live resume point, and by how much it moved it. applied=true: pdtTarget is the break end mapped through the PDT the player reports at the hand-back, correctionSec is pdtTarget - computedTarget (the anchor plus the offset) and currentTimeShiftSec is how far the same stream instant moved in currentTime between the cut and the hand-back (negative: it now sits earlier). applied=false with a reason means no PDT sample was available at the hand-back, so the anchor-based target applied. Context also carries breakId and computedTarget."},"DA-RESUME-PDT-LANDED":{category:"playback",level:"info",summary:"timebase: 'wallclock', all platforms — where a Program-Date-Time resume landed, measured on the stream's own clock from the first position sample after the hand-back: errorSec is the PDT content resumed at minus the break end (negative: content resumed before the break end, so the stream's own break is shown; positive: post-break content was skipped). Context carries breakId, landingPdt (the break end), landedPdt, landedAt (currentTime) and sinceHandBackSec; reason when no sample carried a PDT within 2s. Report-only: the SDK does not correct the position from it."},"DA-RESUME-CUE-END":{category:"playback",level:"info",summary:"ptsSource: 'anvatoCue', iOS/tvOS, web and Android — the stream's matched type=cue_end replaced the computed resume target: the slate under the ad ends there, not at cue plus the advertised duration (150.206s for a 150s break on NFL Channel), and the player can remap its timeline while content is held, so the cue_end is reported in the current mapping. Context carries breakId, computedTarget, cueEndTarget, deltaSec (cueEndTarget - computedTarget), stage (hold, playing or hand-back; Android: always hand-back) and corrected (whether an in-buffer correction seek ran). Only a policy that resumes at the break end is replaced; otherwise applied=false and only the delta is reported."},"DA-RESUME-CUE-END-LATE":{category:"playback",level:"info",summary:"ptsSource: 'anvatoCue', iOS/tvOS and Android — the break's type=cue_end was matched only after the hand-back. When it is more than 1s ahead of the playhead and buffered, content is seeked forward to it once (corrected=true); content is never seeked backward. Context carries breakId, cueEndTarget, currentTime, deltaSec (cueEndTarget - currentTime) and corrected."},"DA-RESUME-CATCHUP":{category:"playback",level:"info",summary:"Content began playing behind the ad to reach its resume point. Context carries breakId, startPos, target, remainingAdSec, publishedEnd and clampedStart — clampedStart true means the stream had not published the ideal start position, so playback began late and the alignment loop must make up the difference (the zero-live-headroom case). Its ABSENCE on a live break that resumed short means catch-up never ran at all, which is a different fault from it running and falling short."},"DA-RESUME-TARGET-CLAMPED":{category:"playback",level:"warn",summary:"The computed resume position sat outside the held live window and was clamped to the nearest end of it; context carries breakId, requested, clamped, shortfallSec and bound — 'end' when the target is ahead of the published edge, 'start' when it is behind the live window start (already evicted) — applied only when the gap is at most half the held window, otherwise DA-RESUME-START-CLAMP-SKIPPED is emitted instead. Distinct from DA-RESUME-OFFSET-CLAMPED, which shortens the OFFSET to the ad seconds actually played."},"DA-RESUME-START-CLAMP-SKIPPED":{category:"playback",level:"info",summary:"The resume target sat behind the element's live seekable start by more than half the held window, which cannot be DVR eviction (the element's seekable range disagrees with the adapter timeline); the target was left unchanged and the engine resolved the seek itself. Context carries breakId, requested, seekableStart, behindSec, windowSec and maxBehindSec."},"DA-RESUME-ALIGN-BLOCKED":{category:"playback",level:"warn",summary:"Resume alignment wanted to move content past the live edge and stood down, because no seek can reach media the stream has not published yet; content is left riding the edge and the break-end clamp places it. Context carries target, requested, safeEnd, shortfallSec and remainingSec. Emitted once per break. A persistent shortfall here means the resume target is further ahead than the feed's published edge — typically the player's view of the edge is stale, not that the seek is wrong."},"DA-CONTINUE-PLAYOUT-BEGIN":{category:"playback",level:"info",summary:"continueContentDuringBreak engaged: content keeps PLAYING through the break, hidden and muted, so no resume seek is needed. Context carries breakId, anchor, breakDurationSec and edgeDistanceSec."},"DA-CONTINUE-PLAYOUT-END":{category:"playback",level:"info",summary:"continueContentDuringBreak reached the cut. Context carries driftSec (content position minus where the break says it should be), maxDriftSec, the corrections applied, rateHonoured (false if the engine drove playbackRate itself) and driftUnrecoverable."},"DA-CONTINUE-PLAYOUT-REFUSED":{category:"playback",level:"info",summary:"continueContentDuringBreak was not used this session, so breaks pause content and resume with a seek as before. Emitted once per session; context carries the reason (disabled, not-replacement, format, shared-element, picture-in-picture, single-decoder) and the break format. Its ABSENCE on an eligible session is the signal the mode engaged."},"DA-CONTINUE-PLAYOUT-QUALITY-UNAVAILABLE":{category:"playback",level:"warn",summary:"Content continues through the replacement break without a video-quality constraint, so the content stream may retain its normal bitrate alongside the ad. Context carries breakId and reason (unsupported when the optional Web adapter method is absent, not-applied when the adapter returned false or could not apply the request)."},"DA-CONTINUE-PLAYOUT-DRIFT":{category:"playback",level:"warn",summary:"Content drifted further from the break timeline than the remaining break can remove within the playback-rate bounds, so part of it is carried into the cut. Context carries driftSec, remainingSec, the rate applied and seekIssued (always false: a corrective seek would be clipped)."},"DA-CONTINUE-PLAYOUT-ABANDONED":{category:"playback",level:"warn",summary:"Content could not be kept playing through the break, so it reverted to the pause-and-resume path mid-break. Context carries breakId, the reason and the driftSec at the moment it converted."},"DA-AD-AV-DESYNC":{category:"playback",level:"warn",summary:"The ad player's audio sink was handed a buffer whose timestamp does not continue the audio it has already been fed, so the media clock had to be re-anchored mid-ad. Video is scheduled off that clock while the audio already queued in the output track keeps draining, so every re-anchor pushes the picture further from the sound actually being heard. Context carries breakId, assetId, expectedSec, actualSec, deltaSec (negative = audio running behind the picture) and positionSec. Seen on server-stitched pods whose per-ad audio and video durations do not match; a single event near the end of a pod is the accumulated total, not an isolated glitch."},"DA-AD-PLAYOUT-SYNC":{category:"playback",level:"info",summary:"Per-break summary of ad playout health, emitted once at break end. Context carries podUri (the resolved ad media, so a stitched pod can be fetched and inspected), desyncEvents, maxDesyncSec, droppedFrames, audioUnderruns, audioFormats and videoFormats (the distinct decoder input formats seen during the break — more than one means the stitched stream changed sample rate or codec mid-pod) and frameOffsetMeanMs (mean video frame processing offset; large negative values mean frames were released late against the audio clock)."},"DA-EVENT":{category:"event",level:"info",summary:"An SDK event, recorded into the diagnostic ring buffer so exportDiagnostics() carries one unified timeline of diagnostics and playback events. Raised to error for aderror. Buffer-only: not dispatched to onDiagnostic handlers, which would duplicate the event stream they already receive."},"DA-BREAK-CONTENT-MASK":{category:"break",level:"warn",summary:"The host's content surface cannot follow view transforms (punch-through SurfaceView, e.g. react-native-video's default), so the two-box format masks the backdrop with a transparent hole over the content box instead of reframing the content view. context: format."},"DA-BREAK-FORMAT-DEGRADED":{category:"break",level:"warn",summary:"Split break reframing degraded: the host supplied no content view or sibling container for the ad."},"DA-BREAK-NO-PLAYABLE-ASSET":{category:"break",level:"warn",summary:"A break had nothing to play, so it never began: no adbreakbegin/adbreakend was emitted, content kept playing, and a public aderror (source ad, carrying the break) reports it. context.reason says why: no-variant (none declared), no-qualifying-variant (variant selection - device targeting + format playability - matched nothing and no downgrade applied), no-assets (the selected variant was empty), or lshape-content-skipped (shared-element insertion cannot render a backdrop). This is the BREAK-level outcome; when a particular variant was passed over for an unrecognised format, DA-BREAK-VARIANT-FORMAT-UNKNOWN names that format alongside it. context: breakId, reason, declaredAssetCount, format."},"DA-BREAK-VARIANT-FORMAT-UNKNOWN":{category:"break",level:"warn",summary:"A break variant declares a format this SDK version does not recognise, so it was passed over during variant selection rather than guessed at - an unknown format is never playable, because rendering it as a fullscreen single would be the most invasive possible misreading. Raised once per skipped variant: a break that still has a playable variant plays it and reports this too, so the skip is never silent, and a break left with nothing also reports DA-BREAK-NO-PLAYABLE-ASSET with reason no-qualifying-variant. An ABSENT format is treated identically to an unrecognised one. context: breakId, format (the raw manifest string; empty when the field was absent)."},"DA-AD-STALLED":{category:"playback",level:"warn",summary:"An ad began playing and then stopped advancing: the playhead did not move for the stall threshold while the ad was meant to be playing. context: assetId, breakId, playbackState, stalledForMs, positionMs, bufferedAheadMs, reason."},"DA-AD-FOREGROUND-RESUMED":{category:"playback",level:"info",summary:"An in-flight ad resumed after the app returned to the foreground; context carries the suspended duration and ad position at resume."},"DA-AD-FOCUS-LOST":{category:"playback",level:"warn",summary:"The overlay ad lost permanent audio focus during an active break; the host player must not reclaim focus while the SDK ad is on screen. context: breakId, transient, reason."},"DA-AD-FOCUS-GRANTED":{category:"playback",level:"info",summary:"The overlay ad was granted media audio focus for the break, so system focus semantics (call, duck, permanent loss) apply to ad playback. context: breakId, cause, alreadyHeld."},"DA-AD-FOCUS-DENIED":{category:"playback",level:"warn",summary:"The platform refused the overlay ad's media audio-focus request, so the break plays with no focus of its own: another app's audio can mix over the ad, and a call can neither duck nor pause it. context: breakId, cause."},"DA-AD-FOCUS-ABANDONED":{category:"playback",level:"info",summary:"The overlay ad gave media audio focus back, so the content player's own focus request finds a clean stack. context: breakId, cause."},"DA-BREAK-ABANDONED":{category:"lifecycle",level:"warn",summary:"A break was still in flight when the session was torn down (endSession, destroy, or a startSession over an active session). context: breakId, cause, phase, unbalancedAdBegin, adIndex, totalAds, assetId."},"DA-RENDERER-UNAVAILABLE":{category:"lifecycle",level:"error",summary:"The renderer could not render a break because a required piece of it was not attached (e.g. no ad player), after the SDK had already emitted adbreakbegin — so the break began and will never end. context: breakId, missing."},"DA-SESSION-STARTING":{category:"session",level:"info",summary:"A step of startSession() was entered: begin, manifest-fetch, gam-init or scheduling. Emitted on entry, so the last one recorded names the step a hung startSession never finished. context: step, manifestUrl, elapsedMs. The URL is reported verbatim; the redacted config summary carries only its origin (config.manifestOrigin)."}};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION="1.
|
|
1
|
+
export const SDK_VERSION="1.4.0";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { OptiViewAds } from './OptiViewAds';
|
|
2
2
|
export { OptiViewAdsExternalVendors, type OptiViewAdsExternalVendorsConfig, } from './OptiViewAdsExternalVendors';
|
|
3
3
|
export type { ExternalPlaybackChange, ExternalPlaybackState, ExternalPlaybackTarget, PlayerAdapter, PlayerAdapterEvent, PlayerAdapterEventHandler, TimedMetadataCue, VideoQuality, } from './interfaces/PlayerAdapter';
|
|
4
|
-
export type { BreakManifest, Break, BreakPosition, EventTrigger, BreakVariant, SingleBreakVariant, DoubleBreakVariant, LshapeAdBreakVariant, LshapeContentBreakVariant, OverlayBreakVariant, AssetWithCompanion, OverlayPosition, OverlaySize, Asset, StaticAsset, VastAsset, VendorAsset, Polling, Controls, Targeting, OsVersionRange, Interaction, AssetUri, DeliveryRule, ChannelEvent, VendorConfiguration, VendorConfigurationBase, GamVendorConfiguration, GamSgaiConfiguration, GamSsaiConfiguration, GamSsbConfiguration, } from './types/BreakManifest';
|
|
4
|
+
export type { BreakManifest, Break, BreakPosition, EventTrigger, BreakVariant, SingleBreakVariant, DoubleBreakVariant, LshapeAdBreakVariant, LshapeContentBreakVariant, OverlayBreakVariant, AssetWithCompanion, OverlayPosition, OverlaySize, Asset, StaticAsset, VastAsset, VendorAsset, Polling, Controls, Targeting, OsVersionRange, Interaction, AssetUri, DeliveryRule, ChannelEvent, VendorConfiguration, VendorConfigurationBase, GamVendorConfiguration, GamSgaiConfiguration, GamSsaiConfiguration, GamSsbConfiguration, ExternalVendorConfigurationEntry, } from './types/BreakManifest';
|
|
5
5
|
export type { ExternalVendorConfiguration } from './types/ExternalVendorConfiguration';
|
|
6
6
|
export { BreakFormat, AssetType, MediaType, Vendor, Timebase, DeviceType, Platform, Os, DeliveryMode, BreakEvent, } from './types/BreakManifest';
|
|
7
7
|
export { isStaticAsset, isVastAsset, isVendorAsset, isGamVendorConfiguration, isEventTrigger, isPauseBreak, } from './types/BreakManifest';
|
|
@@ -52,4 +52,4 @@ export type { ChannelEventIssue } from './services/resolveChannelEvents';
|
|
|
52
52
|
export { fetchWithTimeout, AUX_FETCH_TIMEOUT_MS } from './services/fetchWithTimeout';
|
|
53
53
|
export type { GamVendorParameters } from './services/GamPodUrlBuilder';
|
|
54
54
|
export { buildGamPodUrl, isGamVendorParameters } from './services/GamPodUrlBuilder';
|
|
55
|
-
export type { OptiViewAdsEventType, OptiViewAdsEventHandler, OptiViewAdsEventMap, OptiViewAdsEvent, AdBreakBeginEvent, AdBreakEndEvent, AdBreakSkippedEvent, AdBreakSkipReason, AdBeginEvent, AdEndEvent, AdErrorEvent, AdFirstQuartileEvent, AdMidpointEvent, AdThirdQuartileEvent, AdTimeupdateEvent, WaitingEvent, PlayingEvent, VolumeChangeEvent, AdClickEvent, PlaybackSource, AdBreakPhase, AdBreakStatus, AdBreakStatusEvent, AdChannelChangeEvent, AdChannelChangeReason, } from './types/Events';
|
|
55
|
+
export type { OptiViewAdsEventType, OptiViewAdsEventHandler, OptiViewAdsEventMap, OptiViewAdsEvent, AdBreakBeginEvent, AdBreakEndEvent, AdBreakSkippedEvent, AdBreakSkipReason, AdBeginEvent, AdEndEvent, AdErrorEvent, AdFirstQuartileEvent, AdMidpointEvent, AdThirdQuartileEvent, AdTimeupdateEvent, WaitingEvent, PlayingEvent, VolumeChangeEvent, AdClickEvent, PlaybackSource, AdBreakPhase, AdBreakStatus, AdBreakStatusEvent, AdChannelChangeEvent, AdChannelChangeReason, ExternalVendorConfigurationChangeEvent, } from './types/Events';
|
|
@@ -27,6 +27,7 @@ export declare class AdPlayerController {
|
|
|
27
27
|
private createAdAdapter;
|
|
28
28
|
private adMediaContainer;
|
|
29
29
|
private adPlayer;
|
|
30
|
+
private readonly surfaceReveal;
|
|
30
31
|
private adContainer;
|
|
31
32
|
private companionElement;
|
|
32
33
|
private staticQuartilesFired;
|
|
@@ -88,9 +89,7 @@ export declare class AdPlayerController {
|
|
|
88
89
|
private readonly presentationModes;
|
|
89
90
|
private readonly sharedElementPlayer;
|
|
90
91
|
private readonly resumeCoordinator;
|
|
91
|
-
private
|
|
92
|
-
private contentVolumeHandler;
|
|
93
|
-
private adVolumeChangeHandler;
|
|
92
|
+
private readonly audio;
|
|
94
93
|
private volumeChangeCallback;
|
|
95
94
|
private stage;
|
|
96
95
|
private resizeObserver;
|
|
@@ -162,7 +161,6 @@ export declare class AdPlayerController {
|
|
|
162
161
|
* not undo it: re-boxing a rolling creative would re-fetch a skipped companion and un-pause content.
|
|
163
162
|
*/
|
|
164
163
|
private doubleBoxAudio;
|
|
165
|
-
private audioFocus;
|
|
166
164
|
constructor(contentPlayer: PlayerAdapter, container: HTMLElement, playerContainer: HTMLElement | undefined, createAdAdapter: CreateAdAdapter, debug?: boolean, gamConfig?: GamConfig, preloadMode?: ResolvedPreloadMode, insertionMode?: ResolvedInsertionMode, adBreakCutSafetyMarginSec?: number, doubleBoxAudio?: DoubleBoxAudio, singleSurfaceDevice?: boolean,
|
|
167
165
|
/** Injected so drift correction is deterministic under fake timers; reuses the scheduler's `Clock`. */
|
|
168
166
|
clock?: Clock, contentRecovery?: ResolvedContentRecovery);
|
|
@@ -435,20 +433,14 @@ export declare class AdPlayerController {
|
|
|
435
433
|
private show;
|
|
436
434
|
private hide;
|
|
437
435
|
isAdPlaying(): boolean;
|
|
438
|
-
/**
|
|
439
|
-
* Reads/writes the effective audio owner — the ad player during a break, content otherwise.
|
|
440
|
-
* The `volumechange` sync mirrors onto the other side; a CSAI ad is bridged via
|
|
441
|
-
* {@link VastAdManager.setMuted} (IMA manages its own audio path). With an active
|
|
442
|
-
* {@link audioFocus} only the focused side is targeted.
|
|
443
|
-
*/
|
|
436
|
+
/** The viewer's audio state, applied to both players; see {@link AudioController}. */
|
|
444
437
|
get muted(): boolean;
|
|
445
438
|
set muted(value: boolean);
|
|
446
|
-
/** 0-1. See {@link muted}
|
|
439
|
+
/** 0-1. See {@link muted}. */
|
|
447
440
|
get volume(): number;
|
|
448
441
|
set volume(value: number);
|
|
449
442
|
/** Invoked with the current {@link muted}/{@link volume} whenever either changes. Pass `null` to detach. */
|
|
450
443
|
setVolumeChangeCallback(callback: ((muted: boolean, volume: number) => void) | null): void;
|
|
451
|
-
private notifyVolumeChange;
|
|
452
444
|
destroy(): void;
|
|
453
445
|
private log;
|
|
454
446
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BreakFormat as e,Timebase as t,isPostRollBreak as s}from"../types/BreakManifest";import{DEFAULT_AD_BREAK_CUT_SAFETY_MARGIN_SEC as r,DEFAULT_DOUBLE_BOX_AUDIO as a}from"../types/OptiViewAdsConfig";import{pipOverridesFormat as i,pipSuppressesBackdrop as n}from"./pictureInPicture";import{AdBreakSequencer as o,assetsOfVariant as d,resolveBreakFormat as l}from"./AdBreakSequencer";import{selectVariantDetailed as h}from"./selectVariant";import{debugLog as u}from"../diagnostics/debugLog";import{systemClock as c}from"./BreakScheduler";import{createAdPlayerState as m,invalidateMediaPreload as g}from"./adPlayer/AdPlayerState";import{PauseAdPresenter as P}from"./adPlayer/PauseAdPresenter";import{AssetParameterResolver as y}from"./adPlayer/AssetParameterResolver";import{GamPodBridge as p}from"./adPlayer/GamPodBridge";import{AdStage as A}from"./adPlayer/AdStage";import{createDomPresentationHost as C,toPresentationSurfaces as v}from"./adPlayer/presentation/DomPresentationHost";import{AdPreloader as k}from"./adPlayer/AdPreloader";import{PresentationModeTracker as f}from"./adPlayer/PresentationModeTracker";import{SharedElementBreakPlayer as S}from"./adPlayer/SharedElementBreakPlayer";import{ContentResumeCoordinator as B}from"./adPlayer/ContentResumeCoordinator";import{resumeContentAfterBreak as b}from"./adPlayer/ContentRecovery";import{AssetPlayback as E}from"./adPlayer/AssetPlayback";import{BreakSequenceDriver as F}from"./adPlayer/BreakSequenceDriver";import{VariantSelector as M}from"./adPlayer/VariantSelector";import{AdPlayerEvents as T}from"./adPlayer/AdPlayerEvents";import{armPlaybackCutoff as R,clearPlaybackCutoff as I}from"./adPlayer/PlaybackCutoff";import{AdError as G}from"../types/AdError";import{SuccessorQueue as V}from"./adPlayer/SuccessorQueue";export class AdPlayerController{get contentPaused(){return this.state.contentPaused}set contentPaused(e){this.state.contentPaused=e}get firstFrameGateActive(){return this.state.firstFrameGateActive}set firstFrameGateActive(e){this.state.firstFrameGateActive=e}get firstFrameGateShown(){return this.state.firstFrameGateShown}set firstFrameGateShown(e){this.state.firstFrameGateShown=e}get firstFrameRejector(){return this.state.firstFrameRejector}set firstFrameRejector(e){this.state.firstFrameRejector=e}get gamEventCallbacks(){return this.gamPodBridge.eventCallbacks}get vastAdManager(){return this.state.vastAdManager}set vastAdManager(e){this.state.vastAdManager=e}get currentBreak(){return this.state.currentBreak}set currentBreak(e){this.state.currentBreak=e}get currentAsset(){return this.state.currentAsset}set currentAsset(e){this.state.currentAsset=e}get callbacks(){return this.state.callbacks}set callbacks(e){this.state.callbacks=e}get isPlaying(){return this.state.isPlaying}set isPlaying(e){this.state.isPlaying=e}get effectiveBreakDurationSec(){return this.state.effectiveBreakDurationSec}set effectiveBreakDurationSec(e){this.state.effectiveBreakDurationSec=e}get adBreakCutSafetyMarginSec(){return this.state.adBreakCutSafetyMarginSec}set adBreakCutSafetyMarginSec(e){this.state.adBreakCutSafetyMarginSec=e}get durationTimer(){return this.state.durationTimer}set durationTimer(e){this.state.durationTimer=e}get adInsertionType(){return this.state.adInsertionType}set adInsertionType(e){this.state.adInsertionType=e}get breakTimebase(){return this.state.breakTimebase}set breakTimebase(e){this.state.breakTimebase=e}constructor(e,t,s,i,n=!1,o,d="parallel",l="overlay",h=r,u=a,b=!1,R=c,I="none"){this.playerContainerCreatedBySDK=!1,this.adMediaContainer=null,this.adPlayer=null,this.adContainer=null,this.companionElement=null,this.staticQuartilesFired=new Set,this.sequencer=null,this.pendingAdError=null,this.preRollHoldActive=!1,this.preRollHoldHandler=null,this.breakContentGateHandler=null,this.destroyed=!1,this.abortingBreak=!1,this.overlayImageEl=null,this.imageAssetTimer=null,this.chainResolver=null,this.transitionPhaseReporter=null,this.chaining=!1,this.diagnoseHandler=null,this.playerContainerOriginalStyle="",this.companionRemoveTimer=null,this.adFadeTimer=null,this.syncing=!1,this.contentVolumeHandler=null,this.adVolumeChangeHandler=null,this.volumeChangeCallback=null,this.stage=null,this.resizeObserver=null,this.contentVideoElement=null,this.contentVideoGeometryHandler=null,this.contentVideoMutationObserver=null,this.resumeGeneration=0,this.singleSurfaceDevice=!1,this.currentSelectedFormat=void 0,this.audioFocus=null,this.state=m(),this.state.playerContainer=s??null,this.clock=R,this.singleSurfaceDevice=b,this.contentPlayer=e,this.adBreakCutSafetyMarginSec=h,this.doubleBoxAudio=u,this.container=t,s&&(this.playerContainer=s),this.createAdAdapter=i,this.debug=n,this.state.gamConfig=o,this.preloadMode=d,this.insertionMode=l,this.pauseAdPresenter=new P({contentPlayer:this.contentPlayer,getStage:()=>this.stage,resolveUri:e=>this.resolveUri(e),buildStaticAssetUrl:(e,t)=>this.buildStaticAssetUrl(e,t),diagnose:(e,t,s)=>this.diagnose(e,t,s),log:(...e)=>this.log(...e)}),this.assetParameters=new y(this.state,{contentPlayer:this.contentPlayer,container:this.container,getGamStreamManager:()=>this.state.gamStreamManager,getCurrentBreak:()=>this.currentBreak,getEffectiveBreakDuration:()=>this.effectiveBreakDurationSec,diagnose:(e,t,s)=>this.diagnose(e,t,s),log:(...e)=>this.log(...e)}),this.gamPodBridge=new p(this.state,{contentPlayer:this.contentPlayer,debug:this.debug,getAdPlayer:()=>this.adPlayer,getAdContainer:()=>this.adContainer,isSharedElement:()=>"shared-element"===this.effectiveInsertionMode(),getCurrentBreak:()=>this.currentBreak,getCurrentAsset:()=>this.currentAsset,getEffectiveBreakDuration:()=>this.effectiveBreakDurationSec,getAdPlayerStreamRemainingSec:()=>this.adPlayerStreamRemainingSec(),emitStep:e=>this.emitStep(e),emitAdStart:(e,t,s,r,a)=>this.callbacks?.onAdStart(e,t,s,r,a),emitAdEnd:(e,t,s,r,a)=>this.callbacks?.onAdEnd(e,t,s,r,a),onAdEnded:()=>this.onAdEnded(),pushAssetParameters:(e,t)=>this.pushAssetParameters(e,t),assetParameterResolver:this.assetParameters,diagnose:(e,t,s)=>this.diagnose(e,t,s),log:(...e)=>this.log(...e)}),this.adStage=new A({contentPlayer:this.contentPlayer,container:this.container,getPlayerContainer:()=>this.playerContainer,setPlayerContainer:e=>{this.playerContainer=e},setPlayerContainerOriginalStyle:e=>{this.playerContainerOriginalStyle=e},onPlayerContainerCreated:()=>{this.playerContainerCreatedBySDK=!0},getStage:()=>this.stage,setStage:e=>{this.stage=e},getContentVideo:()=>this.contentVideoElement,setContentVideo:e=>{this.contentVideoElement=e},getAdContainer:()=>this.adContainer,setAdContainer:e=>{this.adContainer=e},getAdMediaContainer:()=>this.adMediaContainer,setAdMediaContainer:e=>{this.adMediaContainer=e},setAdPlayer:e=>{g(this.state),this.adPlayer=e},createAdAdapter:e=>this.createAdAdapter(e),setupEventListeners:()=>this.setupEventListeners(),resizeVast:()=>this.vastAdManager?.resize(),getVariant:e=>this.getVariant(e),getCurrentBreak:()=>this.currentBreak,buildStaticAssetUrl:(e,t)=>this.buildStaticAssetUrl(e,t),getCompanion:()=>this.companionElement,setCompanion:e=>{this.companionElement=e},getOverlayImage:()=>this.overlayImageEl,setOverlayImage:e=>{this.overlayImageEl=e},getImageAssetTimer:()=>this.imageAssetTimer,setImageAssetTimer:e=>{this.imageAssetTimer=e},isChaining:()=>this.chaining,log:(...e)=>this.log(...e),presentationHost:C(()=>v(this.playerContainer,this.adContainer,this.stage))}),this.preloader=new k({state:this.state,clock:this.clock,adStartBudgetMs:AdPlayerController.AD_START_BUDGET_MS,gamPodAdStartBudgetMs:AdPlayerController.GAM_POD_AD_START_BUDGET_MS,minAdStartBudgetMs:AdPlayerController.MIN_AD_START_BUDGET_MS,debug:this.debug,isPlaying:()=>this.isPlaying,preloadMode:this.preloadMode,effectiveInsertionMode:()=>this.effectiveInsertionMode(),getVariant:e=>this.getVariant(e),getAssets:e=>this.getAssets(e),buildStaticAssetUrl:(e,t)=>this.buildStaticAssetUrl(e,t),resolveAssetUri:(e,t,s)=>this.resolveAssetUri(e,t,s),getCompanionVariant:e=>this.getVariant(e),getAdPlayer:()=>this.adPlayer,getAdContainer:()=>this.adContainer,buildVastTagUrl:(e,t)=>this.buildVastTagUrl(e,t),resolveChainedSuccessor:e=>this.resolveChainedSuccessor(e),show:()=>this.show(),reapplyAdContainerLayout:e=>this.reapplyAdContainerLayout(e),getCurrentBreak:()=>this.currentBreak,onBreakVisible:e=>this.callbacks?.onBreakVisible?.(e),onAdError:e=>this.onAdError(e),diagnose:(e,t,s)=>this.diagnose(e,t,s),log:(...e)=>this.log(...e)}),this.presentationModes=new f(this.state,{contentPlayer:this.contentPlayer,getAdPlayer:()=>this.adPlayer,insertionMode:this.insertionMode,getCurrentBreak:()=>this.currentBreak,getCurrentFormat:()=>this.currentSelectedFormat,isSharedElementActive:()=>this.state.sharedAdClaimActive,getVariant:e=>this.getVariant(e),isAdPlaying:()=>this.isAdPlaying(),hasCurrentAsset:()=>null!==this.currentAsset,contentSourceIsReloadable:()=>null!==this.sharedElementPlayer.restorableContentSource(),adoptPictureInPictureMidBreak:()=>this.adoptPictureInPictureMidBreak(),restoreLayout:()=>this.restoreLayout(),applyLayout:e=>this.applyLayout(e),show:()=>this.show(),playAdThroughContentElement:()=>this.sharedElementPlayer.playAdThroughContentElement(),getContentPositionAtBreakStart:()=>this.state.contentPositionAtBreakStart,setContentPositionAtBreakStart:e=>this.resumeCoordinator.reanchorMidBreak(e),gateContentForBreak:()=>this.gateContentForBreak(),diagnose:(e,t,s)=>this.diagnose(e,t,s),log:(...e)=>this.log(...e)}),this.resumeCoordinator=new B(this.state,{contentPlayer:this.contentPlayer,clock:this.clock,getInsertionMode:()=>this.effectiveInsertionMode(),gateContentForBreak:()=>this.gateContentForBreak(),clearContentGate:e=>this.clearContentGate(e),diagnose:(e,t,s)=>this.diagnose(e,t,s),log:(...e)=>this.log(...e)}),this.recoveryDeps={contentPlayer:e,getCallbacks:()=>this.callbacks,preloadMode:d,contentRecovery:I,resumeCoordinator:this.resumeCoordinator,diagnose:(e,t,s)=>this.diagnose(e,t,s),log:(...e)=>this.log(...e)},this.sharedElementPlayer=new S(this.state,{contentPlayer:this.contentPlayer,getAdPlayer:()=>this.adPlayer,resolveAssetUri:(e,t)=>this.resolveAssetUri(e,t),liveEdgeDistanceSec:()=>this.resumeCoordinator.liveEdgeDistanceSec(),rebindContentElementIfSwapped:()=>this.rebindContentElementIfSwapped(),createAdAdapter:(e,t)=>this.createAdAdapter(e,t),reportAdProgress:e=>this.reportAdProgress(e),processTimedMetadata:e=>this.gamPodBridge.processTimedMetadata(e),diagnose:(e,t,s)=>this.diagnose(e,t,s),log:(...e)=>this.log(...e),clearContentGate:()=>this.clearContentGate(),hide:()=>this.hide(),waitForLiveEdge:e=>this.resumeCoordinator.waitForLiveEdge(e),getVariant:e=>this.getVariant(e),getAssets:e=>this.getAssets(e)}),this.variantSelector=new M(this.state,{getEffectiveInsertionMode:()=>this.effectiveInsertionMode(),singleSurfaceDevice:this.singleSurfaceDevice,preloadMode:this.preloadMode,getIsContentPictureInPicture:()=>this.isContentPictureInPicture(),getIsGamReady:()=>this.isGamReady(),getSupportsParallelBuffering:()=>this.contentPlayer.supportsParallelBuffering,diagnose:(e,t,s)=>this.diagnose(e,t,s)}),this.successorQueue=new V({getAdPlayer:()=>this.adPlayer,isEnabled:()=>"parallel"===this.preloadMode&&"overlay"===this.effectiveInsertionMode()&&!this.state.sharedAdClaimActive,buildStaticAssetUrl:(e,t)=>this.buildStaticAssetUrl(e,t),getCallbacks:()=>this.state.callbacks,log:(...e)=>this.log(...e)}),this.assetPlayback=new E(this.state,{getAdPlayer:()=>this.adPlayer,getAdContainer:()=>this.adContainer,getAdMediaContainer:()=>this.adMediaContainer,clearStaticQuartiles:()=>this.staticQuartilesFired.clear(),getVariant:e=>this.getVariant(e),getGamEventCallbacks:()=>this.gamEventCallbacks,getSequencer:()=>this.sequencer,debug:this.debug,buildStaticAssetUrl:(e,t)=>this.buildStaticAssetUrl(e,t),buildGamUrl:(e,t)=>this.buildGamUrl(e,t),buildVastTagUrl:(e,t)=>this.buildVastTagUrl(e,t),onAdError:e=>this.onAdError(e),onAdEnded:()=>this.onAdEnded(),drive:e=>this.drive(e),clearOverlayImage:()=>this.clearOverlayImage(),revealFirstFrame:()=>this.preloader.revealFirstFrame(),transferPictureInPictureToAd:()=>this.presentationModes.transferPictureInPictureToAd(),isGamPodAsset:e=>this.isGamPodAsset(e),hasGamIdentityMismatch:e=>this.gamPodBridge.hasIdentityMismatch(e),beginGamPodTracking:e=>this.gamPodBridge.beginGamPodTracking(e),recordAdProgress:e=>this.adPlayerEvents.recordAdProgress(e),reportPhase:e=>this.reportPhase(e),armAdStartBudget:e=>this.armAdStartBudget(e),takeQueuedSuccessor:(e,t,s)=>this.successorQueue.take(e,t,s),queueUpcomingAd:()=>{const e=this.sequencer?.upcomingAsset();e&&this.currentBreak&&this.successorQueue.prepare(this.currentBreak,e,!1)},remainingAdStartBudgetMs:()=>this.remainingAdStartBudgetMs(),loadWithinBudget:(e,t)=>this.loadWithinBudget(e,t),waitForFirstFrame:()=>this.waitForFirstFrame(),getOverlayImage:()=>this.overlayImageEl,setOverlayImage:e=>{this.overlayImageEl=e},getImageAssetTimer:()=>this.imageAssetTimer,setImageAssetTimer:e=>{this.imageAssetTimer=e},getVastPodPosition:()=>this.vastPodPosition,setVastPodPosition:e=>{this.vastPodPosition=e},diagnose:(e,t,s)=>this.diagnose(e,t,s),log:(...e)=>this.log(...e)}),this.breakSequenceDriver=new F(this.state,{successorQueue:this.successorQueue,getAssets:e=>this.getAssets(e),getChainResolver:()=>this.chainResolver,getEffectiveInsertionMode:()=>this.effectiveInsertionMode(),getVastPodPosition:()=>this.vastPodPosition,setVastPodPosition:e=>{this.vastPodPosition=e},getPendingAdError:()=>this.pendingAdError,setPendingAdError:e=>{this.pendingAdError=e},getChaining:()=>this.chaining,setChaining:e=>{this.chaining=e},isExpandingPodAsset:e=>this.isExpandingPodAsset(e),fallBackToPodLevelReporting:()=>this.fallBackToPodLevelReporting(),resetGamPodTracking:()=>this.resetGamPodTracking(),emitGamPodSteps:e=>this.gamPodBridge.emitGamPodSteps(e),playAsset:(e,t,s)=>this.playAsset(e,t,s),endBreak:()=>this.endBreak(),playBreak:(e,t,s)=>this.playBreak(e,t,s),diagnose:(e,t,s)=>this.diagnose(e,t,s),log:(...e)=>this.log(...e)}),this.adPlayerEvents=new T(this.state,{getAdPlayer:()=>this.adPlayer,getContentPlayer:()=>this.contentPlayer,getGamEventCallbacks:()=>this.gamEventCallbacks,getSequencer:()=>this.sequencer,getAudioFocus:()=>this.audioFocus,getSyncing:()=>this.syncing,setSyncing:e=>{this.syncing=e},setAdVolumeChangeHandler:e=>{this.adVolumeChangeHandler=e},setContentVolumeHandler:e=>{this.contentVolumeHandler=e},notifyVolumeChange:()=>this.notifyVolumeChange(),onAdEnded:()=>this.onAdEnded(),processTimedMetadata:e=>this.gamPodBridge.processTimedMetadata(e),toAdError:e=>this.toAdError(e),revealFirstFrame:()=>this.revealFirstFrame(),clearFirstFrameResolver:()=>this.preloader.clearFirstFrameResolver(),drive:e=>this.drive(e),setPendingAdError:e=>{this.pendingAdError=e},dropQueuedAd:()=>this.successorQueue.drop(),log:(...e)=>this.log(...e),maybePrepositionContentForResume:()=>"single-decoder"!==this.preloadMode&&this.resumeCoordinator.maybePrepositionContentForResume(),maybeCatchUpContentForResume:e=>"single-decoder"!==this.preloadMode&&this.resumeCoordinator.maybeCatchUpContentForResume(e),getStaticQuartilesFired:()=>this.staticQuartilesFired,onGamPodAdProgress:e=>this.gamPodBridge.onGamPodAdProgress(e)})}setGamEventCallbacks(e){this.gamPodBridge.setEventCallbacks(e)}setDiagnoseHandler(e){this.diagnoseHandler=e}diagnose(e,t,s){this.diagnoseHandler?.(e,t,s)}holdContentForPreRoll(e){if(this.preRollHoldActive)return;const t=this.getVariant(e);t&&this.shouldPauseContent(t.format)&&(this.preRollHoldActive=!0,this.contentPlayer.pause(),this.preRollHoldHandler=()=>{this.preRollHoldActive&&this.contentPlayer.pause()},this.contentPlayer.on("playing",this.preRollHoldHandler),this.diagnose("DA-PREROLL-CONTENT-HELD","Content held at session start until the pending pre-roll begins.",{breakId:e.id}))}releaseContentHold(){this.preRollHoldActive&&(this.preRollHoldActive=!1,this.preRollHoldHandler&&(this.contentPlayer.off("playing",this.preRollHoldHandler),this.preRollHoldHandler=null))}gateContentForBreak(){this.contentPlayer.pause(),this.breakContentGateHandler||(this.breakContentGateHandler=()=>{this.contentPaused&&!this.state.resumeCatchupActive&&this.contentPlayer.pause()},this.contentPlayer.on("playing",this.breakContentGateHandler))}clearContentGate(e=!1){this.resumeCoordinator.endResumeCatchup(e),this.breakContentGateHandler&&(this.contentPlayer.off("playing",this.breakContentGateHandler),this.breakContentGateHandler=null)}setChainResolver(e){this.chainResolver=e}onChainSuccessorChanged(){this.sequencer&&this.currentBreak&&this.isPlaying&&this.drive(this.sequencer.onOutcome({kind:"successor-changed"}),!0)}setResumeCueAnchorResolver(e){this.resumeCoordinator.setCueAnchorResolver(e)}setResumeCueEndResolver(e){this.resumeCoordinator.setCueEndResolver(e)}ingestStreamClockCue(e){this.resumeCoordinator.ingestStreamClockCue(e)}setTransitionPhaseReporter(e){this.transitionPhaseReporter=e}reportPhase(e){this.transitionPhaseReporter?.(e)}setBreakResumePolicy(e,t,s=!1){this.adInsertionType=e,this.breakTimebase=t,this.state.continueContentDuringBreak=s,this.state.continueRefusalReported=!1}initialize(){this.adContainer||(this.setupOverlay(),this.presentationModes.setupPictureInPictureTracking(),"adaptive"===this.insertionMode&&this.presentationModes.setupFullscreenTracking())}async startGamSession(e,t){await this.gamPodBridge.startGamSession(e,t)}endGamSession(){this.gamPodBridge.endGamSession()}setSessionAssetParameters(e){this.assetParameters.setSessionAssetParameters(e)}setManifestAssetParameters(e){this.assetParameters.setManifestAssetParameters(e)}updateAssetParameters(e){this.assetParameters.updateAssetParameters(e)}resolveExternalVendorConfiguration(e){return this.assetParameters.resolveExternalVendorConfiguration(e)}updateAssetParameterMacros(e){this.assetParameters.updateAssetParameterMacros(e)}get updatedAssetParameters(){return this.state.updatedAssetParameters}setSessionAssetParameterMacros(e){this.assetParameters.setSessionAssetParameterMacros(e)}get sessionAssetParameterMacros(){return this.assetParameters.getSessionAssetParameterMacros()}buildVastTagUrl(e,t){return this.assetParameters.buildVastTagUrl(e,t)}buildStaticAssetUrl(e,t){return this.assetParameters.buildStaticAssetUrl(e,t)}pushAssetParameters(e,t){this.assetParameters.pushAssetParameters(e,t)}setupOverlay(){this.adStage.setupOverlay()}isGamPodAsset(e){return this.gamPodBridge.isGamPodAsset(e)}isExpandingPodAsset(e){return this.gamPodBridge.isExpandingPodAsset(e)}fallBackToPodLevelReporting(){this.gamPodBridge.fallBackToPodLevelReporting()}resetGamPodTracking(){this.gamPodBridge.resetGamPodTracking()}emitGamPodSteps(e){this.gamPodBridge.emitGamPodSteps(e)}isGamEnabled(){return this.gamPodBridge.isGamEnabled()}isGamReady(){return this.gamPodBridge.isGamReady()}isOverlayInsertion(){return"overlay"===this.effectiveInsertionMode()}settleAssetProgress(){this.adPlayerEvents.settleAssetProgress()}reportAdProgress(e){this.adPlayerEvents.reportAdProgress(e)}setupEventListeners(){this.adPlayerEvents.setupEventListeners()}async preload(e){return this.preloader.preload(e)}renderLshapeContentBackdrop(e){this.adStage.renderLshapeContentBackdrop(e)}currentInsertionMode(){return this.presentationModes.currentInsertionMode()}effectiveInsertionMode(){return this.presentationModes.effectiveInsertionMode()}isContentPictureInPicture(){return this.presentationModes.isPictureInPicture()}rebindContentElementIfSwapped(){this.presentationModes.rebindContentElementIfSwapped()}teardownFullscreenTracking(){this.presentationModes.teardownFullscreenTracking()}teardownPictureInPictureTracking(){this.presentationModes.teardownPictureInPictureTracking()}isPictureInPicture(){return this.presentationModes.isPictureInPicture()}pictureInPictureOwner(){return this.presentationModes.pictureInPictureOwner()}isShowingAd(){if(!this.isAdPlaying())return!1;const e=this.currentDeclaredFormat();return!n(e,this.isContentPictureInPicture())}currentDeclaredFormat(){return this.currentSelectedFormat??(this.currentBreak?l(this.currentBreak):void 0)}async playBreak(t,r,a){if(this.state.preloadGeneration++,this.resumeGeneration++,!this.sharedElementPlayer.beginSharedActivity())return;this.releaseContentHold(),this.rebindContentElementIfSwapped();const n=a?.effectiveDurationSec??null;this.effectiveBreakDurationSec=null!==n&&n>0?n:null;const d=this.effectiveBreakDurationSec??t.duration;if("shared-element"===this.effectiveInsertionMode())return g(this.state),void await this.sharedElementPlayer.playSharedElementBreak(t,r);if(!this.adPlayer)return void this.log("playBreak called before initialize()");const l=this.getVariant(t);if(!l)return void this.log("No valid variant found for break:",t.id);this.currentBreak=t,this.callbacks=r,this.isPlaying=!0,this.firstFrameGateActive=a?.firstFrameGate??!1,this.firstFrameGateShown=!1;const h=this.continueContentDuringBreakReason(l.format),u=s(t),c=this.shouldPauseContent(l.format)&&"ok"===h&&!u;c||u||this.state.continueRefusalReported||!this.shouldPauseContent(l.format)||(this.state.continueRefusalReported=!0,this.diagnose("DA-CONTINUE-PLAYOUT-REFUSED",`Content will be paused for breaks this session: ${h}`,{reason:h,format:l.format})),this.audioFocus=l.format===e.DOUBLE?this.doubleBoxAudio:c?"ad":null,this.adPlayer&&("content"===this.audioFocus?(this.adPlayer.muted=!0,this.adPlayer.volume=0):(this.adPlayer.muted=this.contentPlayer.muted,this.adPlayer.volume=this.contentPlayer.volume,"ad"===this.audioFocus&&(this.contentPlayer.muted=!0)));const m=this.declaredFormatOf(t);if(this.currentSelectedFormat=m,this.state.pipAdoptedForBreak=i(m,this.isContentPictureInPicture()),this.state.pipAdoptedForBreak&&this.diagnose("DA-PIP-FORMAT-OVERRIDE",`Picture-in-picture: ${m} break plays as single`,{declared:m,effective:e.SINGLE,reason:"picture-in-picture",midBreak:!1}),this.shouldPauseContent(l.format)?(this.resumeCoordinator.captureBreakAnchor(t),c?(this.contentPaused=!1,this.resumeCoordinator.beginContinueContentPlayout()):(this.contentPaused=!0,this.gateContentForBreak())):this.contentPaused=!1,this.applyLayout(l),l.format===e.LSHAPE_CONTENT)return this.renderLshapeContentBackdrop(l),void(d&&d>0?(this.log("Starting duration timer:",d,"seconds"),this.durationTimer=setTimeout(()=>{this.log("Duration timer expired, ending break"),this.endBreak()},1e3*d)):this.endBreak());this.firstFrameGateActive||(this.show(),this.reapplyAdContainerLayout(l));if(0===this.getAssets(t).length)return this.log("No assets in break, ending immediately"),void this.endBreak();{const e=Math.max(0,d||0)+this.adBreakCutSafetyMarginSec;R(this.state,e,()=>{this.log("Break-cut timer expired, ending break"),this.firstFrameRejector?this.onAdError(new G("Break cut before first frame rendered","DA-AD-START-TIMEOUT")):this.sequencer&&this.currentBreak&&this.isPlaying?(this.callbacks?.onBreakCut?.(this.currentBreak),this.drive(this.sequencer.onOutcome({kind:"break-elapsed"}))):this.endBreak()},()=>this.onAdError(new G("Ad playback startup timed out","DA-AD-START-TIMEOUT")))}this.sequencer=new o("overlay",()=>null,this.selectionInputs(),e=>this.resolveChainedSuccessor(e)),await this.drive(this.sequencer.beginBreak(t)),this.preloadChainedSuccessor(t.id)}get resumeHandoverInMotion(){return this.state.resumeCatchupActive}reapplyAdContainerLayout(e){this.adStage.reapplyAdContainerLayout(e)}adoptPictureInPictureMidBreak(){this.presentationModes.adoptPictureInPictureMidBreak()}async restorePictureInPictureToContent(){await this.presentationModes.restorePictureInPictureToContent()}continueContentDuringBreakReason(e){return this.variantSelector.continueContentDuringBreakReason(e)}shouldPauseContent(t){return t===e.SINGLE||t===e.LSHAPE_AD}selectionInputs(){return this.variantSelector.selectionInputs()}breakNothingToPlayReason(e){return this.variantSelector.breakNothingToPlayReason(e)}unknownVariantFormatsOf(e){return this.variantSelector.unknownVariantFormatsOf(e)}diagnoseUnknownVariantFormats(e){this.variantSelector.diagnoseUnknownVariantFormats(e)}declaredFormatOf(e){const{selected:t,deviceQualified:s}=h(e,this.selectionInputs());return(t??s)?.format??l(e)}getVariant(e){return this.variantSelector.getVariant(e)}applyLayout(e){this.adStage.applyLayout(e)}restoreLayout(){this.adStage.restoreLayout()}getAssets(e){return d(this.getVariant(e))}async playAsset(e,t,s){return this.assetPlayback.playAsset(e,t,s)}clearOverlayImage(){this.adStage.clearOverlayImage()}async showPauseImage(e){return this.pauseAdPresenter.showPauseImage(e)}hidePauseImage(){this.pauseAdPresenter.hidePauseImage()}async drive(e,t=!1){return this.breakSequenceDriver.drive(e,t)}emitStep(e){this.breakSequenceDriver.emitStep(e)}async resolveAssetUri(e,t,s){return this.assetPlayback.resolveAssetUri(e,t,s)}async buildGamUrl(e,t){return this.gamPodBridge.buildGamUrl(e,t)}resolveUri(e){return this.assetParameters.resolveUri(e)}adPlayerStreamRemainingSec(){return this.assetPlayback.adPlayerStreamRemainingSec()}onAdEnded(){this.currentBreak&&this.isPlaying&&this.sequencer&&(this.settleAssetProgress(),this.drive(this.sequencer.onOutcome({kind:"asset-ended"})))}toAdError(e){return this.assetPlayback.toAdError(e)}onAdError(e){this.adPlayerEvents.onAdError(e)}endBreak(){if(!this.currentBreak)return;this.log("Break ended:",this.currentBreak.id),I(this.state),this.state.gamPodTracker&&this.emitGamPodSteps(this.state.gamPodTracker.finish()),this.resetGamPodTracking(),this.pushAssetParameters(void 0);const e=this.currentBreak;let r=this.abortingBreak?null:this.resolveChainedSuccessor(e.id);if(r){const e=this.breakNothingToPlayReason(r);this.diagnoseUnknownVariantFormats(r),e&&(this.diagnose("DA-BREAK-NO-PLAYABLE-ASSET",`chained break ${r.id} has no playable variant (${e}); it will not begin.`,{breakId:r.id,reason:e}),this.callbacks?.onAdError(r,void 0,new G(`Break ${r.id} has no playable variant (${e})`,"DA-BREAK-NO-PLAYABLE-ASSET")),this.callbacks?.onChainedBreakSkipped?.(r),r=null)}if(r)return void this.advanceToChainedBreak(e,r);if(this.currentSelectedFormat=void 0,this.resumeCoordinator.endContinueContentPlayout(),this.restorePictureInPictureToContent(),this.isPlaying=!1,this.currentBreak=null,this.resumeGeneration++,this.effectiveBreakDurationSec=null,g(this.state),this.preloader.clearWarmedBreaks(),this.successorQueue.reset(),this.firstFrameGateActive=!1,this.firstFrameGateShown=!1,this.clearFirstFrame("Break ended before first frame"),"ad"===this.audioFocus&&this.adPlayer?(this.contentPlayer.muted=this.adPlayer.muted,this.contentPlayer.volume=this.adPlayer.volume):"content"===this.audioFocus&&this.adPlayer&&(this.adPlayer.muted=this.contentPlayer.muted,this.adPlayer.volume=this.contentPlayer.volume),this.audioFocus=null,this.clearOverlayImage(),this.vastAdManager?.reset(),this.hide(),this.restoreLayout(),this.adPlayer?.pause(),this.state.sharedAdClaimActive){const t=this.callbacks;return this.sharedElementPlayer.detachSharedAdListeners(),this.abortingBreak=!1,this.state.earlyReturnResumeOffsetSec=null,this.sharedElementPlayer.restoreSharedElementContent(e),void t?.onBreakEnd(e)}const a="recreate"===this.recoveryDeps.contentRecovery&&"function"==typeof this.contentPlayer.recreateMediaPipeline,i="single-decoder"===this.preloadMode||a?this.adPlayer?.unload?.():void 0;if(this.contentPaused&&s(e))this.contentPaused=!1,this.clearContentGate();else if(this.contentPaused){this.contentPaused=!1,this.clearContentGate(this.resumeHandoverInMotion);const s=this.resumeGeneration;b(this.recoveryDeps,{breakInfo:e,earlyReturnOffsetSec:this.state.earlyReturnResumeOffsetSec,isLiveResume:this.breakTimebase===t.WALLCLOCK,released:i,isCurrent:()=>s===this.resumeGeneration})}this.abortingBreak=!1,this.state.earlyReturnResumeOffsetSec=null,this.callbacks?.onBreakEnd(e)}currentAdAsset(){return this.isPlaying?this.currentAsset:null}skipBreak(){this.isPlaying&&(this.log("Skipping break:",this.currentBreak?.id),I(this.state),this.sequencer&&this.currentBreak?this.drive(this.sequencer.onOutcome({kind:"break-elapsed"})):this.endBreak())}abortBreak(e){this.isPlaying&&(this.log("Early return — break removed from manifest:",this.currentBreak?.id),this.state.earlyReturnResumeOffsetSec=Math.max(0,e),this.abortingBreak=!0,I(this.state),this.sequencer&&this.currentBreak?this.drive(this.sequencer.onOutcome({kind:"break-elapsed"})):this.endBreak())}stopActiveBreak(){g(this.state),this.successorQueue.reset(),this.sharedElementPlayer.cancelSharedRestore()&&this.isPlaying&&(this.log("Aborting active break on session end:",this.currentBreak?.id),I(this.state),this.sharedElementPlayer.detachSharedAdListeners(),this.sharedElementPlayer.releaseContentElementAdPlayer(),this.adPlayer?.pause(),this.clearOverlayImage(),this.vastAdManager?.reset(),this.hide(),this.restoreLayout(),this.isPlaying=!1,this.currentBreak=null,this.currentAsset=null,this.contentPaused=!1,this.clearContentGate(),this.resumeCoordinator.endContinueContentPlayout(),this.effectiveBreakDurationSec=null,this.firstFrameGateActive=!1,this.firstFrameGateShown=!1,this.clearFirstFrame("Break aborted before first frame"),this.audioFocus=null)}resolveChainedSuccessor(e){return this.breakSequenceDriver.resolveChainedSuccessor(e)}advanceToChainedBreak(e,t){this.breakSequenceDriver.advanceToChainedBreak(e,t)}preloadChainedSuccessor(e){this.preloader.preloadChainedSuccessor(e)}revealFirstFrame(){this.assetPlayback.revealFirstFrame()}armAdStartBudget(e){this.preloader.armAdStartBudget(e)}remainingAdStartBudgetMs(){return this.preloader.remainingAdStartBudgetMs()}loadWithinBudget(e,t){return this.preloader.loadWithinBudget(e,t)}async waitForFirstFrame(){return this.preloader.waitForFirstFrame()}clearFirstFrame(e="Break ended before first frame"){this.preloader.clearFirstFrame(e)}show(){this.adStage.show()}hide(){this.adStage.hide()}isAdPlaying(){return this.isPlaying}get muted(){return"content"===this.audioFocus?this.contentPlayer.muted:"ad"===this.audioFocus?this.adPlayer?this.adPlayer.muted:this.contentPlayer.muted:this.isPlaying&&this.adPlayer?this.adPlayer.muted:this.contentPlayer.muted}set muted(e){if("content"===this.audioFocus)this.contentPlayer.muted=e;else if("ad"===this.audioFocus)this.adPlayer&&(this.adPlayer.muted=e);else{(this.isPlaying&&this.adPlayer?this.adPlayer:this.contentPlayer).muted=e}this.vastAdManager?.setMuted(e),this.notifyVolumeChange()}get volume(){return"content"===this.audioFocus?this.contentPlayer.volume:"ad"===this.audioFocus?this.adPlayer?this.adPlayer.volume:this.contentPlayer.volume:this.isPlaying&&this.adPlayer?this.adPlayer.volume:this.contentPlayer.volume}set volume(e){if("content"===this.audioFocus)this.contentPlayer.volume=e;else if("ad"===this.audioFocus)this.adPlayer&&(this.adPlayer.volume=e);else{(this.isPlaying&&this.adPlayer?this.adPlayer:this.contentPlayer).volume=e}this.vastAdManager?.setVolume(e),this.notifyVolumeChange()}setVolumeChangeCallback(e){this.volumeChangeCallback=e}notifyVolumeChange(){this.volumeChangeCallback?.(this.muted,this.volume)}destroy(){if(this.destroyed)return;if(this.resumeGeneration++,g(this.state),this.successorQueue.reset(),!this.sharedElementPlayer.beginSharedActivity())return;this.destroyed=!0,this.adStage.destroy(),this.resumeCoordinator.cancelResumeLandedProbe(),this.resumeCoordinator.dispose(),this.resumeCoordinator.endResumeCatchup(),this.resumeCoordinator.endContinueContentPlayout(),this.state.resumePrepositionSettle&&(clearTimeout(this.state.resumePrepositionSettle),this.state.resumePrepositionSettle=null),this.releaseContentHold(),this.clearContentGate(),this.clearFirstFrame("Controller destroyed before first frame"),this.firstFrameGateActive=!1,this.firstFrameGateShown=!1,this.resetGamPodTracking(),I(this.state),this.companionRemoveTimer&&(clearTimeout(this.companionRemoveTimer),this.companionRemoveTimer=null),this.adFadeTimer&&(clearTimeout(this.adFadeTimer),this.adFadeTimer=null),this.adPlayer?.pause(),this.clearOverlayImage(),this.pauseAdPresenter.destroy(),this.contentVolumeHandler&&(this.contentPlayer.off("volumechange",this.contentVolumeHandler),this.contentVolumeHandler=null),this.sharedElementPlayer.detachSharedAdListeners(),this.sharedElementPlayer.releaseContentElementAdPlayer(),this.state.pipTransferredToAd=!1,this.teardownFullscreenTracking(),this.teardownPictureInPictureTracking(),this.adVolumeChangeHandler&&this.adPlayer&&this.adPlayer.off("volumechange",this.adVolumeChangeHandler),this.adVolumeChangeHandler=null,this.hide(),this.adStage.destroy(),this.resizeObserver?.disconnect(),this.resizeObserver=null,this.contentVideoMutationObserver?.disconnect(),this.contentVideoMutationObserver=null,this.contentVideoElement&&this.contentVideoGeometryHandler&&(this.contentVideoElement.removeEventListener("loadedmetadata",this.contentVideoGeometryHandler),this.contentVideoElement.removeEventListener("resize",this.contentVideoGeometryHandler)),this.contentVideoElement=null,this.contentVideoGeometryHandler=null;const e=this.adStage.restoreContent(this.playerContainerCreatedBySDK,this.playerContainerOriginalStyle);this.companionElement&&this.companionElement.parentElement&&(this.companionElement.parentElement.removeChild(this.companionElement),this.companionElement=null),this.isPlaying=!1,this.currentBreak=null,this.adContainer&&this.adContainer.parentElement&&this.adContainer.parentElement.removeChild(this.adContainer),!e&&this.stage&&this.stage.parentElement&&this.stage.parentElement.removeChild(this.stage),this.stage=null,this.adContainer=null,this.adMediaContainer=null;try{this.adPlayer?.destroy()}catch(e){this.log("Ad player adapter destroy failed (ignored):",e)}this.adPlayer=null,this.callbacks=null,this.gamPodBridge.destroy(),this.vastAdManager?.destroy(),this.vastAdManager=null,this.log("Ad player destroyed")}log(...e){this.debug&&u("[OptiViewAds:AdPlayerController]",...e)}}AdPlayerController.AD_START_BUDGET_MS=5e3,AdPlayerController.GAM_POD_AD_START_BUDGET_MS=1e4,AdPlayerController.MIN_AD_START_BUDGET_MS=3e3;
|
|
1
|
+
import{BreakFormat as e,Timebase as t,isPostRollBreak as r}from"../types/BreakManifest";import{DEFAULT_AD_BREAK_CUT_SAFETY_MARGIN_SEC as s,DEFAULT_DOUBLE_BOX_AUDIO as a}from"../types/OptiViewAdsConfig";import{pipOverridesFormat as i,pipSuppressesBackdrop as n}from"./pictureInPicture";import{AdBreakSequencer as o,assetsOfVariant as d,resolveBreakFormat as h}from"./AdBreakSequencer";import{selectVariantDetailed as l}from"./selectVariant";import{debugLog as u}from"../diagnostics/debugLog";import{systemClock as c}from"./BreakScheduler";import{createAdPlayerState as m,invalidateMediaPreload as g}from"./adPlayer/AdPlayerState";import{PauseAdPresenter as P}from"./adPlayer/PauseAdPresenter";import{AssetParameterResolver as p}from"./adPlayer/AssetParameterResolver";import{GamPodBridge as y}from"./adPlayer/GamPodBridge";import{AdStage as A}from"./adPlayer/AdStage";import{createDomPresentationHost as C,toPresentationSurfaces as v}from"./adPlayer/presentation/DomPresentationHost";import{AdPreloader as k}from"./adPlayer/AdPreloader";import{PresentationModeTracker as f}from"./adPlayer/PresentationModeTracker";import{SharedElementBreakPlayer as S}from"./adPlayer/SharedElementBreakPlayer";import{ContentResumeCoordinator as B}from"./adPlayer/ContentResumeCoordinator";import{resumeContentAfterBreak as b}from"./adPlayer/ContentRecovery";import{AssetPlayback as E}from"./adPlayer/AssetPlayback";import{SurfaceRevealReporter as T}from"./adPlayer/SurfaceRevealReporter";import{BreakSequenceDriver as M}from"./adPlayer/BreakSequenceDriver";import{VariantSelector as R}from"./adPlayer/VariantSelector";import{AdPlayerEvents as F}from"./adPlayer/AdPlayerEvents";import{AudioController as I}from"./adPlayer/AudioController";import{armPlaybackCutoff as G,clearPlaybackCutoff as w}from"./adPlayer/PlaybackCutoff";import{AdError as D}from"../types/AdError";import{SuccessorQueue as V}from"./adPlayer/SuccessorQueue";export class AdPlayerController{get contentPaused(){return this.state.contentPaused}set contentPaused(e){this.state.contentPaused=e}get firstFrameGateActive(){return this.state.firstFrameGateActive}set firstFrameGateActive(e){this.state.firstFrameGateActive=e}get firstFrameGateShown(){return this.state.firstFrameGateShown}set firstFrameGateShown(e){this.state.firstFrameGateShown=e}get firstFrameRejector(){return this.state.firstFrameRejector}set firstFrameRejector(e){this.state.firstFrameRejector=e}get gamEventCallbacks(){return this.gamPodBridge.eventCallbacks}get vastAdManager(){return this.state.vastAdManager}set vastAdManager(e){this.state.vastAdManager=e}get currentBreak(){return this.state.currentBreak}set currentBreak(e){this.state.currentBreak=e}get currentAsset(){return this.state.currentAsset}set currentAsset(e){this.state.currentAsset=e}get callbacks(){return this.state.callbacks}set callbacks(e){this.state.callbacks=e}get isPlaying(){return this.state.isPlaying}set isPlaying(e){this.state.isPlaying=e}get effectiveBreakDurationSec(){return this.state.effectiveBreakDurationSec}set effectiveBreakDurationSec(e){this.state.effectiveBreakDurationSec=e}get adBreakCutSafetyMarginSec(){return this.state.adBreakCutSafetyMarginSec}set adBreakCutSafetyMarginSec(e){this.state.adBreakCutSafetyMarginSec=e}get durationTimer(){return this.state.durationTimer}set durationTimer(e){this.state.durationTimer=e}get adInsertionType(){return this.state.adInsertionType}set adInsertionType(e){this.state.adInsertionType=e}get breakTimebase(){return this.state.breakTimebase}set breakTimebase(e){this.state.breakTimebase=e}constructor(e,t,r,i,n=!1,o,d="parallel",h="overlay",l=s,u=a,b=!1,G=c,w="none"){this.playerContainerCreatedBySDK=!1,this.adMediaContainer=null,this.adPlayer=null,this.surfaceReveal=new T((e,t,r)=>this.diagnose(e,t,r)),this.adContainer=null,this.companionElement=null,this.staticQuartilesFired=new Set,this.sequencer=null,this.pendingAdError=null,this.preRollHoldActive=!1,this.preRollHoldHandler=null,this.breakContentGateHandler=null,this.destroyed=!1,this.abortingBreak=!1,this.overlayImageEl=null,this.imageAssetTimer=null,this.chainResolver=null,this.transitionPhaseReporter=null,this.chaining=!1,this.diagnoseHandler=null,this.playerContainerOriginalStyle="",this.companionRemoveTimer=null,this.adFadeTimer=null,this.volumeChangeCallback=null,this.stage=null,this.resizeObserver=null,this.contentVideoElement=null,this.contentVideoGeometryHandler=null,this.contentVideoMutationObserver=null,this.resumeGeneration=0,this.singleSurfaceDevice=!1,this.currentSelectedFormat=void 0,this.state=m(),this.state.playerContainer=r??null,this.clock=G,this.singleSurfaceDevice=b,this.contentPlayer=e,this.audio=new I({contentPlayer:e,isAdPlaying:()=>this.isPlaying,getVastAdManager:()=>this.vastAdManager,onVolumeChange:(e,t)=>this.volumeChangeCallback?.(e,t),log:(...e)=>this.log(...e)}),this.adBreakCutSafetyMarginSec=l,this.doubleBoxAudio=u,this.container=t,r&&(this.playerContainer=r),this.createAdAdapter=i,this.debug=n,this.state.gamConfig=o,this.preloadMode=d,this.insertionMode=h,this.pauseAdPresenter=new P({contentPlayer:this.contentPlayer,getStage:()=>this.stage,resolveUri:e=>this.resolveUri(e),buildStaticAssetUrl:(e,t)=>this.buildStaticAssetUrl(e,t),diagnose:(e,t,r)=>this.diagnose(e,t,r),log:(...e)=>this.log(...e)}),this.assetParameters=new p(this.state,{contentPlayer:this.contentPlayer,container:this.container,getGamStreamManager:()=>this.state.gamStreamManager,getCurrentBreak:()=>this.currentBreak,getEffectiveBreakDuration:()=>this.effectiveBreakDurationSec,diagnose:(e,t,r)=>this.diagnose(e,t,r),log:(...e)=>this.log(...e)}),this.gamPodBridge=new y(this.state,{contentPlayer:this.contentPlayer,debug:this.debug,getAdPlayer:()=>this.adPlayer,getAdContainer:()=>this.adContainer,isSharedElement:()=>"shared-element"===this.effectiveInsertionMode(),getCurrentBreak:()=>this.currentBreak,getCurrentAsset:()=>this.currentAsset,getEffectiveBreakDuration:()=>this.effectiveBreakDurationSec,getAdPlayerStreamRemainingSec:()=>this.adPlayerStreamRemainingSec(),emitStep:e=>this.emitStep(e),emitAdStart:(e,t,r,s,a)=>this.callbacks?.onAdStart(e,t,r,s,a),emitAdEnd:(e,t,r,s,a)=>this.callbacks?.onAdEnd(e,t,r,s,a),onAdEnded:()=>this.onAdEnded(),pushAssetParameters:(e,t)=>this.pushAssetParameters(e,t),assetParameterResolver:this.assetParameters,diagnose:(e,t,r)=>this.diagnose(e,t,r),log:(...e)=>this.log(...e)}),this.adStage=new A({contentPlayer:this.contentPlayer,container:this.container,getPlayerContainer:()=>this.playerContainer,setPlayerContainer:e=>{this.playerContainer=e},setPlayerContainerOriginalStyle:e=>{this.playerContainerOriginalStyle=e},onPlayerContainerCreated:()=>{this.playerContainerCreatedBySDK=!0},getStage:()=>this.stage,setStage:e=>{this.stage=e},getContentVideo:()=>this.contentVideoElement,setContentVideo:e=>{this.contentVideoElement=e},getAdContainer:()=>this.adContainer,setAdContainer:e=>{this.adContainer=e},getAdMediaContainer:()=>this.adMediaContainer,setAdMediaContainer:e=>{this.adMediaContainer=e},setAdPlayer:e=>{g(this.state),this.adPlayer=e,e&&this.audio.attachAdPlayer(e)},createAdAdapter:e=>this.createAdAdapter(e),setupEventListeners:()=>this.setupEventListeners(),resizeVast:()=>this.vastAdManager?.resize(),getVariant:e=>this.getVariant(e),getCurrentBreak:()=>this.currentBreak,buildStaticAssetUrl:(e,t)=>this.buildStaticAssetUrl(e,t),getCompanion:()=>this.companionElement,setCompanion:e=>{this.companionElement=e},getOverlayImage:()=>this.overlayImageEl,setOverlayImage:e=>{this.overlayImageEl=e},getImageAssetTimer:()=>this.imageAssetTimer,setImageAssetTimer:e=>{this.imageAssetTimer=e},isChaining:()=>this.chaining,log:(...e)=>this.log(...e),presentationHost:C(()=>v(this.playerContainer,this.adContainer,this.stage))}),this.preloader=new k({state:this.state,clock:this.clock,adStartBudgetMs:AdPlayerController.AD_START_BUDGET_MS,gamPodAdStartBudgetMs:AdPlayerController.GAM_POD_AD_START_BUDGET_MS,minAdStartBudgetMs:AdPlayerController.MIN_AD_START_BUDGET_MS,debug:this.debug,isPlaying:()=>this.isPlaying,preloadMode:this.preloadMode,effectiveInsertionMode:()=>this.effectiveInsertionMode(),getVariant:e=>this.getVariant(e),getAssets:e=>this.getAssets(e),buildStaticAssetUrl:(e,t)=>this.buildStaticAssetUrl(e,t),resolveAssetUri:(e,t,r)=>this.resolveAssetUri(e,t,r),getCompanionVariant:e=>this.getVariant(e),getAdPlayer:()=>this.adPlayer,getAdContainer:()=>this.adContainer,buildVastTagUrl:(e,t)=>this.buildVastTagUrl(e,t),resolveChainedSuccessor:e=>this.resolveChainedSuccessor(e),show:()=>this.show(),reapplyAdContainerLayout:e=>this.reapplyAdContainerLayout(e),getCurrentBreak:()=>this.currentBreak,onBreakVisible:e=>this.callbacks?.onBreakVisible?.(e),onAdError:e=>this.onAdError(e),diagnose:(e,t,r)=>this.diagnose(e,t,r),log:(...e)=>this.log(...e)}),this.presentationModes=new f(this.state,{contentPlayer:this.contentPlayer,getAdPlayer:()=>this.adPlayer,insertionMode:this.insertionMode,getCurrentBreak:()=>this.currentBreak,getCurrentFormat:()=>this.currentSelectedFormat,isSharedElementActive:()=>this.state.sharedAdClaimActive,getVariant:e=>this.getVariant(e),isAdPlaying:()=>this.isAdPlaying(),hasCurrentAsset:()=>null!==this.currentAsset,contentSourceIsReloadable:()=>null!==this.sharedElementPlayer.restorableContentSource(),adoptPictureInPictureMidBreak:()=>this.adoptPictureInPictureMidBreak(),restoreLayout:()=>this.restoreLayout(),applyLayout:e=>this.applyLayout(e),show:()=>this.show(),playAdThroughContentElement:()=>this.sharedElementPlayer.playAdThroughContentElement(),getContentPositionAtBreakStart:()=>this.state.contentPositionAtBreakStart,setContentPositionAtBreakStart:e=>this.resumeCoordinator.reanchorMidBreak(e),gateContentForBreak:()=>this.gateContentForBreak(),diagnose:(e,t,r)=>this.diagnose(e,t,r),log:(...e)=>this.log(...e)}),this.resumeCoordinator=new B(this.state,{contentPlayer:this.contentPlayer,clock:this.clock,getInsertionMode:()=>this.effectiveInsertionMode(),gateContentForBreak:()=>this.gateContentForBreak(),clearContentGate:e=>this.clearContentGate(e),muteContent:()=>this.audio.mute("content"),diagnose:(e,t,r)=>this.diagnose(e,t,r),log:(...e)=>this.log(...e)}),this.recoveryDeps={contentPlayer:e,getCallbacks:()=>this.callbacks,preloadMode:d,contentRecovery:w,resumeCoordinator:this.resumeCoordinator,diagnose:(e,t,r)=>this.diagnose(e,t,r),log:(...e)=>this.log(...e)},this.sharedElementPlayer=new S(this.state,{contentPlayer:this.contentPlayer,getAdPlayer:()=>this.adPlayer,surfaceReveal:this.surfaceReveal,resolveAssetUri:(e,t)=>this.resolveAssetUri(e,t),liveEdgeDistanceSec:()=>this.resumeCoordinator.liveEdgeDistanceSec(),rebindContentElementIfSwapped:()=>this.rebindContentElementIfSwapped(),createAdAdapter:(e,t)=>this.createAdAdapter(e,t),reportAdProgress:e=>this.reportAdProgress(e),processTimedMetadata:e=>this.gamPodBridge.processTimedMetadata(e),diagnose:(e,t,r)=>this.diagnose(e,t,r),log:(...e)=>this.log(...e),clearContentGate:()=>this.clearContentGate(),hide:()=>this.hide(),waitForLiveEdge:e=>this.resumeCoordinator.waitForLiveEdge(e),getVariant:e=>this.getVariant(e),getAssets:e=>this.getAssets(e)}),this.variantSelector=new R(this.state,{getEffectiveInsertionMode:()=>this.effectiveInsertionMode(),singleSurfaceDevice:this.singleSurfaceDevice,preloadMode:this.preloadMode,getIsContentPictureInPicture:()=>this.isContentPictureInPicture(),getIsGamReady:()=>this.isGamReady(),getSupportsParallelBuffering:()=>this.contentPlayer.supportsParallelBuffering,diagnose:(e,t,r)=>this.diagnose(e,t,r)}),this.successorQueue=new V({getAdPlayer:()=>this.adPlayer,isEnabled:()=>"parallel"===this.preloadMode&&"overlay"===this.effectiveInsertionMode()&&!this.state.sharedAdClaimActive,buildStaticAssetUrl:(e,t)=>this.buildStaticAssetUrl(e,t),getCallbacks:()=>this.state.callbacks,log:(...e)=>this.log(...e)}),this.assetPlayback=new E(this.state,{getAdPlayer:()=>this.adPlayer,getAdContainer:()=>this.adContainer,getAdMediaContainer:()=>this.adMediaContainer,clearStaticQuartiles:()=>this.staticQuartilesFired.clear(),getVariant:e=>this.getVariant(e),getGamEventCallbacks:()=>this.gamEventCallbacks,getSequencer:()=>this.sequencer,debug:this.debug,buildStaticAssetUrl:(e,t)=>this.buildStaticAssetUrl(e,t),buildGamUrl:(e,t)=>this.buildGamUrl(e,t),buildVastTagUrl:(e,t)=>this.buildVastTagUrl(e,t),onAdError:e=>this.onAdError(e),onAdEnded:()=>this.onAdEnded(),drive:e=>this.drive(e),clearOverlayImage:()=>this.clearOverlayImage(),revealFirstFrame:()=>this.preloader.revealFirstFrame(),surfaceReveal:this.surfaceReveal,transferPictureInPictureToAd:()=>this.presentationModes.transferPictureInPictureToAd(),isGamPodAsset:e=>this.isGamPodAsset(e),hasGamIdentityMismatch:e=>this.gamPodBridge.hasIdentityMismatch(e),beginGamPodTracking:e=>this.gamPodBridge.beginGamPodTracking(e),recordAdProgress:e=>this.adPlayerEvents.recordAdProgress(e),reportPhase:e=>this.reportPhase(e),armAdStartBudget:e=>this.armAdStartBudget(e),takeQueuedSuccessor:(e,t,r)=>this.successorQueue.take(e,t,r),queueUpcomingAd:()=>{const e=this.sequencer?.upcomingAsset();e&&this.currentBreak&&this.successorQueue.prepare(this.currentBreak,e,!1)},remainingAdStartBudgetMs:()=>this.remainingAdStartBudgetMs(),loadWithinBudget:(e,t)=>this.loadWithinBudget(e,t),waitForFirstFrame:()=>this.waitForFirstFrame(),getOverlayImage:()=>this.overlayImageEl,setOverlayImage:e=>{this.overlayImageEl=e},getImageAssetTimer:()=>this.imageAssetTimer,setImageAssetTimer:e=>{this.imageAssetTimer=e},getVastPodPosition:()=>this.vastPodPosition,setVastPodPosition:e=>{this.vastPodPosition=e},diagnose:(e,t,r)=>this.diagnose(e,t,r),log:(...e)=>this.log(...e)}),this.breakSequenceDriver=new M(this.state,{successorQueue:this.successorQueue,getAssets:e=>this.getAssets(e),getChainResolver:()=>this.chainResolver,getEffectiveInsertionMode:()=>this.effectiveInsertionMode(),getVastPodPosition:()=>this.vastPodPosition,setVastPodPosition:e=>{this.vastPodPosition=e},getPendingAdError:()=>this.pendingAdError,setPendingAdError:e=>{this.pendingAdError=e},getChaining:()=>this.chaining,setChaining:e=>{this.chaining=e},isExpandingPodAsset:e=>this.isExpandingPodAsset(e),fallBackToPodLevelReporting:()=>this.fallBackToPodLevelReporting(),resetGamPodTracking:()=>this.resetGamPodTracking(),emitGamPodSteps:e=>this.gamPodBridge.emitGamPodSteps(e),playAsset:(e,t,r)=>this.playAsset(e,t,r),endBreak:()=>this.endBreak(),playBreak:(e,t,r)=>this.playBreak(e,t,r),diagnose:(e,t,r)=>this.diagnose(e,t,r),log:(...e)=>this.log(...e)}),this.adPlayerEvents=new F(this.state,{getAdPlayer:()=>this.adPlayer,getContentPlayer:()=>this.contentPlayer,getGamEventCallbacks:()=>this.gamEventCallbacks,getSequencer:()=>this.sequencer,onAdEnded:()=>this.onAdEnded(),processTimedMetadata:e=>this.gamPodBridge.processTimedMetadata(e),toAdError:e=>this.toAdError(e),revealFirstFrame:()=>this.revealFirstFrame(),clearFirstFrameResolver:()=>this.preloader.clearFirstFrameResolver(),drive:e=>this.drive(e),setPendingAdError:e=>{this.pendingAdError=e},dropQueuedAd:()=>this.successorQueue.drop(),log:(...e)=>this.log(...e),maybePrepositionContentForResume:()=>"single-decoder"!==this.preloadMode&&this.resumeCoordinator.maybePrepositionContentForResume(),maybeCatchUpContentForResume:e=>"single-decoder"!==this.preloadMode&&this.resumeCoordinator.maybeCatchUpContentForResume(e),getStaticQuartilesFired:()=>this.staticQuartilesFired,onGamPodAdProgress:e=>this.gamPodBridge.onGamPodAdProgress(e)})}setGamEventCallbacks(e){this.gamPodBridge.setEventCallbacks(e)}setDiagnoseHandler(e){this.diagnoseHandler=e}diagnose(e,t,r){this.diagnoseHandler?.(e,t,r)}holdContentForPreRoll(e){if(this.preRollHoldActive)return;const t=this.getVariant(e);t&&this.shouldPauseContent(t.format)&&(this.preRollHoldActive=!0,this.contentPlayer.pause(),this.preRollHoldHandler=()=>{this.preRollHoldActive&&this.contentPlayer.pause()},this.contentPlayer.on("playing",this.preRollHoldHandler),this.diagnose("DA-PREROLL-CONTENT-HELD","Content held at session start until the pending pre-roll begins.",{breakId:e.id}))}releaseContentHold(){this.preRollHoldActive&&(this.preRollHoldActive=!1,this.preRollHoldHandler&&(this.contentPlayer.off("playing",this.preRollHoldHandler),this.preRollHoldHandler=null))}gateContentForBreak(){this.contentPlayer.pause(),this.breakContentGateHandler||(this.breakContentGateHandler=()=>{this.contentPaused&&!this.state.resumeCatchupActive&&this.contentPlayer.pause()},this.contentPlayer.on("playing",this.breakContentGateHandler))}clearContentGate(e=!1){this.resumeCoordinator.endResumeCatchup(e),this.breakContentGateHandler&&(this.contentPlayer.off("playing",this.breakContentGateHandler),this.breakContentGateHandler=null)}setChainResolver(e){this.chainResolver=e}onChainSuccessorChanged(){this.sequencer&&this.currentBreak&&this.isPlaying&&this.drive(this.sequencer.onOutcome({kind:"successor-changed"}),!0)}setResumeCueAnchorResolver(e){this.resumeCoordinator.setCueAnchorResolver(e)}setResumeCueEndResolver(e){this.resumeCoordinator.setCueEndResolver(e)}ingestStreamClockCue(e){this.resumeCoordinator.ingestStreamClockCue(e)}setTransitionPhaseReporter(e){this.transitionPhaseReporter=e}reportPhase(e){this.transitionPhaseReporter?.(e)}setBreakResumePolicy(e,t,r=!1){this.adInsertionType=e,this.breakTimebase=t,this.state.continueContentDuringBreak=r,this.state.continueRefusalReported=!1}initialize(){this.adContainer||(this.setupOverlay(),this.presentationModes.setupPictureInPictureTracking(),"adaptive"===this.insertionMode&&this.presentationModes.setupFullscreenTracking())}async startGamSession(e,t){await this.gamPodBridge.startGamSession(e,t)}endGamSession(){this.gamPodBridge.endGamSession()}setSessionAssetParameters(e){this.assetParameters.setSessionAssetParameters(e)}setManifestAssetParameters(e){this.assetParameters.setManifestAssetParameters(e)}updateAssetParameters(e){this.assetParameters.updateAssetParameters(e)}resolveExternalVendorConfiguration(e){return this.assetParameters.resolveExternalVendorConfiguration(e)}updateAssetParameterMacros(e){this.assetParameters.updateAssetParameterMacros(e)}get updatedAssetParameters(){return this.state.updatedAssetParameters}setSessionAssetParameterMacros(e){this.assetParameters.setSessionAssetParameterMacros(e)}get sessionAssetParameterMacros(){return this.assetParameters.getSessionAssetParameterMacros()}buildVastTagUrl(e,t){return this.assetParameters.buildVastTagUrl(e,t)}buildStaticAssetUrl(e,t){return this.assetParameters.buildStaticAssetUrl(e,t)}pushAssetParameters(e,t){this.assetParameters.pushAssetParameters(e,t)}setupOverlay(){this.adStage.setupOverlay()}isGamPodAsset(e){return this.gamPodBridge.isGamPodAsset(e)}isExpandingPodAsset(e){return this.gamPodBridge.isExpandingPodAsset(e)}fallBackToPodLevelReporting(){this.gamPodBridge.fallBackToPodLevelReporting()}resetGamPodTracking(){this.gamPodBridge.resetGamPodTracking()}emitGamPodSteps(e){this.gamPodBridge.emitGamPodSteps(e)}isGamEnabled(){return this.gamPodBridge.isGamEnabled()}isGamReady(){return this.gamPodBridge.isGamReady()}isOverlayInsertion(){return"overlay"===this.effectiveInsertionMode()}settleAssetProgress(){this.adPlayerEvents.settleAssetProgress()}reportAdProgress(e){this.adPlayerEvents.reportAdProgress(e)}setupEventListeners(){this.adPlayerEvents.setupEventListeners()}async preload(e){return this.preloader.preload(e)}renderLshapeContentBackdrop(e){this.adStage.renderLshapeContentBackdrop(e)}currentInsertionMode(){return this.presentationModes.currentInsertionMode()}effectiveInsertionMode(){return this.presentationModes.effectiveInsertionMode()}isContentPictureInPicture(){return this.presentationModes.isPictureInPicture()}rebindContentElementIfSwapped(){this.presentationModes.rebindContentElementIfSwapped()}teardownFullscreenTracking(){this.presentationModes.teardownFullscreenTracking()}teardownPictureInPictureTracking(){this.presentationModes.teardownPictureInPictureTracking()}isPictureInPicture(){return this.presentationModes.isPictureInPicture()}pictureInPictureOwner(){return this.presentationModes.pictureInPictureOwner()}isShowingAd(){if(!this.isAdPlaying())return!1;const e=this.currentDeclaredFormat();return!n(e,this.isContentPictureInPicture())}currentDeclaredFormat(){return this.currentSelectedFormat??(this.currentBreak?h(this.currentBreak):void 0)}async playBreak(t,s,a){if(this.state.preloadGeneration++,this.resumeGeneration++,!this.sharedElementPlayer.beginSharedActivity())return;this.releaseContentHold(),this.rebindContentElementIfSwapped();const n=a?.effectiveDurationSec??null;this.effectiveBreakDurationSec=null!==n&&n>0?n:null;const d=this.effectiveBreakDurationSec??t.duration;if("shared-element"===this.effectiveInsertionMode())return g(this.state),void await this.sharedElementPlayer.playSharedElementBreak(t,s);if(!this.adPlayer)return void this.log("playBreak called before initialize()");const h=this.getVariant(t);if(!h)return void this.log("No valid variant found for break:",t.id);this.currentBreak=t,this.callbacks=s,this.isPlaying=!0,this.firstFrameGateActive=a?.firstFrameGate??!1,this.firstFrameGateShown=!1;const l=this.continueContentDuringBreakReason(h.format),u=r(t),c=this.shouldPauseContent(h.format)&&"ok"===l&&!u;c||u||this.state.continueRefusalReported||!this.shouldPauseContent(h.format)||(this.state.continueRefusalReported=!0,this.diagnose("DA-CONTINUE-PLAYOUT-REFUSED",`Content will be paused for breaks this session: ${l}`,{reason:l,format:h.format})),h.format===e.DOUBLE?this.audio.mute("ad"===this.doubleBoxAudio?"content":"ad"):c?this.audio.mute("content"):this.shouldPauseContent(h.format)&&this.audio.unmute("ad");const m=this.declaredFormatOf(t);if(this.currentSelectedFormat=m,this.state.pipAdoptedForBreak=i(m,this.isContentPictureInPicture()),this.state.pipAdoptedForBreak&&this.diagnose("DA-PIP-FORMAT-OVERRIDE",`Picture-in-picture: ${m} break plays as single`,{declared:m,effective:e.SINGLE,reason:"picture-in-picture",midBreak:!1}),this.shouldPauseContent(h.format)?(this.resumeCoordinator.captureBreakAnchor(t),c?(this.contentPaused=!1,this.resumeCoordinator.beginContinueContentPlayout()):(this.contentPaused=!0,this.gateContentForBreak())):this.contentPaused=!1,this.applyLayout(h),h.format===e.LSHAPE_CONTENT)return this.renderLshapeContentBackdrop(h),void(d&&d>0?(this.log("Starting duration timer:",d,"seconds"),this.durationTimer=setTimeout(()=>{this.log("Duration timer expired, ending break"),this.endBreak()},1e3*d)):this.endBreak());this.firstFrameGateActive||(this.show(),this.reapplyAdContainerLayout(h));if(0===this.getAssets(t).length)return this.log("No assets in break, ending immediately"),void this.endBreak();{const e=Math.max(0,d||0)+this.adBreakCutSafetyMarginSec;G(this.state,e,()=>{this.log("Break-cut timer expired, ending break"),this.firstFrameRejector?this.onAdError(new D("Break cut before first frame rendered","DA-AD-START-TIMEOUT")):this.sequencer&&this.currentBreak&&this.isPlaying?(this.callbacks?.onBreakCut?.(this.currentBreak),this.drive(this.sequencer.onOutcome({kind:"break-elapsed"}))):this.endBreak()},()=>this.onAdError(new D("Ad playback startup timed out","DA-AD-START-TIMEOUT")))}this.sequencer=new o("overlay",()=>null,this.selectionInputs(),e=>this.resolveChainedSuccessor(e)),await this.drive(this.sequencer.beginBreak(t)),this.preloadChainedSuccessor(t.id)}get resumeHandoverInMotion(){return this.state.resumeCatchupActive}reapplyAdContainerLayout(e){this.adStage.reapplyAdContainerLayout(e)}adoptPictureInPictureMidBreak(){this.presentationModes.adoptPictureInPictureMidBreak()}async restorePictureInPictureToContent(){await this.presentationModes.restorePictureInPictureToContent()}continueContentDuringBreakReason(e){return this.variantSelector.continueContentDuringBreakReason(e)}shouldPauseContent(t){return t===e.SINGLE||t===e.LSHAPE_AD}selectionInputs(){return this.variantSelector.selectionInputs()}breakNothingToPlayReason(e){return this.variantSelector.breakNothingToPlayReason(e)}unknownVariantFormatsOf(e){return this.variantSelector.unknownVariantFormatsOf(e)}diagnoseUnknownVariantFormats(e){this.variantSelector.diagnoseUnknownVariantFormats(e)}declaredFormatOf(e){const{selected:t,deviceQualified:r}=l(e,this.selectionInputs());return(t??r)?.format??h(e)}getVariant(e){return this.variantSelector.getVariant(e)}applyLayout(e){this.adStage.applyLayout(e)}restoreLayout(){this.adStage.restoreLayout()}getAssets(e){return d(this.getVariant(e))}async playAsset(e,t,r){return this.assetPlayback.playAsset(e,t,r)}clearOverlayImage(){this.adStage.clearOverlayImage()}async showPauseImage(e){return this.pauseAdPresenter.showPauseImage(e)}hidePauseImage(){this.pauseAdPresenter.hidePauseImage()}async drive(e,t=!1){return this.breakSequenceDriver.drive(e,t)}emitStep(e){this.breakSequenceDriver.emitStep(e)}async resolveAssetUri(e,t,r){return this.assetPlayback.resolveAssetUri(e,t,r)}async buildGamUrl(e,t){return this.gamPodBridge.buildGamUrl(e,t)}resolveUri(e){return this.assetParameters.resolveUri(e)}adPlayerStreamRemainingSec(){return this.assetPlayback.adPlayerStreamRemainingSec()}onAdEnded(){this.currentBreak&&this.isPlaying&&this.sequencer&&(this.settleAssetProgress(),this.drive(this.sequencer.onOutcome({kind:"asset-ended"})))}toAdError(e){return this.assetPlayback.toAdError(e)}onAdError(e){this.adPlayerEvents.onAdError(e)}endBreak(){if(!this.currentBreak)return;this.log("Break ended:",this.currentBreak.id),w(this.state),this.state.gamPodTracker&&this.emitGamPodSteps(this.state.gamPodTracker.finish()),this.resetGamPodTracking(),this.pushAssetParameters(void 0);const e=this.currentBreak;let s=this.abortingBreak?null:this.resolveChainedSuccessor(e.id);if(s){const e=this.breakNothingToPlayReason(s);this.diagnoseUnknownVariantFormats(s),e&&(this.diagnose("DA-BREAK-NO-PLAYABLE-ASSET",`chained break ${s.id} has no playable variant (${e}); it will not begin.`,{breakId:s.id,reason:e}),this.callbacks?.onAdError(s,void 0,new D(`Break ${s.id} has no playable variant (${e})`,"DA-BREAK-NO-PLAYABLE-ASSET")),this.callbacks?.onChainedBreakSkipped?.(s),s=null)}if(s)return void this.advanceToChainedBreak(e,s);if(this.currentSelectedFormat=void 0,this.resumeCoordinator.endContinueContentPlayout(),this.restorePictureInPictureToContent(),this.isPlaying=!1,this.currentBreak=null,this.resumeGeneration++,this.effectiveBreakDurationSec=null,g(this.state),this.preloader.clearWarmedBreaks(),this.successorQueue.reset(),this.firstFrameGateActive=!1,this.firstFrameGateShown=!1,this.clearFirstFrame("Break ended before first frame"),this.audio.unmuteAll(),this.clearOverlayImage(),this.vastAdManager?.reset(),this.hide(),this.restoreLayout(),this.adPlayer?.pause(),this.state.sharedAdClaimActive){const t=this.callbacks;return this.sharedElementPlayer.detachSharedAdListeners(),this.abortingBreak=!1,this.state.earlyReturnResumeOffsetSec=null,this.sharedElementPlayer.restoreSharedElementContent(e),void t?.onBreakEnd(e)}const a="recreate"===this.recoveryDeps.contentRecovery&&"function"==typeof this.contentPlayer.recreateMediaPipeline,i="single-decoder"===this.preloadMode||a?this.adPlayer?.unload?.():void 0;if(this.contentPaused&&r(e))this.contentPaused=!1,this.clearContentGate();else if(this.contentPaused){this.contentPaused=!1,this.clearContentGate(this.resumeHandoverInMotion);const r=this.resumeGeneration;b(this.recoveryDeps,{breakInfo:e,earlyReturnOffsetSec:this.state.earlyReturnResumeOffsetSec,isLiveResume:this.breakTimebase===t.WALLCLOCK,released:i,isCurrent:()=>r===this.resumeGeneration})}this.abortingBreak=!1,this.state.earlyReturnResumeOffsetSec=null,this.callbacks?.onBreakEnd(e)}currentAdAsset(){return this.isPlaying?this.currentAsset:null}skipBreak(){this.isPlaying&&(this.log("Skipping break:",this.currentBreak?.id),w(this.state),this.sequencer&&this.currentBreak?this.drive(this.sequencer.onOutcome({kind:"break-elapsed"})):this.endBreak())}abortBreak(e){this.isPlaying&&(this.log("Early return — break removed from manifest:",this.currentBreak?.id),this.state.earlyReturnResumeOffsetSec=Math.max(0,e),this.abortingBreak=!0,w(this.state),this.sequencer&&this.currentBreak?this.drive(this.sequencer.onOutcome({kind:"break-elapsed"})):this.endBreak())}stopActiveBreak(){g(this.state),this.successorQueue.reset(),this.sharedElementPlayer.cancelSharedRestore()&&this.isPlaying&&(this.log("Aborting active break on session end:",this.currentBreak?.id),w(this.state),this.sharedElementPlayer.detachSharedAdListeners(),this.sharedElementPlayer.releaseContentElementAdPlayer(),this.adPlayer?.pause(),this.clearOverlayImage(),this.vastAdManager?.reset(),this.hide(),this.restoreLayout(),this.isPlaying=!1,this.currentBreak=null,this.currentAsset=null,this.contentPaused=!1,this.clearContentGate(),this.resumeCoordinator.endContinueContentPlayout(),this.effectiveBreakDurationSec=null,this.firstFrameGateActive=!1,this.firstFrameGateShown=!1,this.clearFirstFrame("Break aborted before first frame"),this.audio.unmuteAll())}resolveChainedSuccessor(e){return this.breakSequenceDriver.resolveChainedSuccessor(e)}advanceToChainedBreak(e,t){this.breakSequenceDriver.advanceToChainedBreak(e,t)}preloadChainedSuccessor(e){this.preloader.preloadChainedSuccessor(e)}revealFirstFrame(){this.assetPlayback.revealFirstFrame()}armAdStartBudget(e){this.preloader.armAdStartBudget(e)}remainingAdStartBudgetMs(){return this.preloader.remainingAdStartBudgetMs()}loadWithinBudget(e,t){return this.preloader.loadWithinBudget(e,t)}async waitForFirstFrame(){return this.preloader.waitForFirstFrame()}clearFirstFrame(e="Break ended before first frame"){this.preloader.clearFirstFrame(e)}show(){this.adStage.show(),this.surfaceReveal.surfaceShown()}hide(){this.adStage.hide(),this.surfaceReveal.surfaceHidden()}isAdPlaying(){return this.isPlaying}get muted(){return this.audio.muted}set muted(e){this.audio.muted=e}get volume(){return this.audio.volume}set volume(e){this.audio.volume=e}setVolumeChangeCallback(e){this.volumeChangeCallback=e}destroy(){if(this.destroyed)return;if(this.resumeGeneration++,g(this.state),this.successorQueue.reset(),!this.sharedElementPlayer.beginSharedActivity())return;this.destroyed=!0,this.adStage.destroy(),this.resumeCoordinator.cancelResumeLandedProbe(),this.resumeCoordinator.dispose(),this.resumeCoordinator.endResumeCatchup(),this.resumeCoordinator.endContinueContentPlayout(),this.state.resumePrepositionSettle&&(clearTimeout(this.state.resumePrepositionSettle),this.state.resumePrepositionSettle=null),this.releaseContentHold(),this.clearContentGate(),this.clearFirstFrame("Controller destroyed before first frame"),this.firstFrameGateActive=!1,this.firstFrameGateShown=!1,this.resetGamPodTracking(),w(this.state),this.companionRemoveTimer&&(clearTimeout(this.companionRemoveTimer),this.companionRemoveTimer=null),this.adFadeTimer&&(clearTimeout(this.adFadeTimer),this.adFadeTimer=null),this.adPlayer?.pause(),this.clearOverlayImage(),this.pauseAdPresenter.destroy(),this.audio.destroy(),this.sharedElementPlayer.detachSharedAdListeners(),this.sharedElementPlayer.releaseContentElementAdPlayer(),this.state.pipTransferredToAd=!1,this.teardownFullscreenTracking(),this.teardownPictureInPictureTracking(),this.hide(),this.adStage.destroy(),this.resizeObserver?.disconnect(),this.resizeObserver=null,this.contentVideoMutationObserver?.disconnect(),this.contentVideoMutationObserver=null,this.contentVideoElement&&this.contentVideoGeometryHandler&&(this.contentVideoElement.removeEventListener("loadedmetadata",this.contentVideoGeometryHandler),this.contentVideoElement.removeEventListener("resize",this.contentVideoGeometryHandler)),this.contentVideoElement=null,this.contentVideoGeometryHandler=null;const e=this.adStage.restoreContent(this.playerContainerCreatedBySDK,this.playerContainerOriginalStyle);this.companionElement&&this.companionElement.parentElement&&(this.companionElement.parentElement.removeChild(this.companionElement),this.companionElement=null),this.isPlaying=!1,this.currentBreak=null,this.adContainer&&this.adContainer.parentElement&&this.adContainer.parentElement.removeChild(this.adContainer),!e&&this.stage&&this.stage.parentElement&&this.stage.parentElement.removeChild(this.stage),this.stage=null,this.adContainer=null,this.adMediaContainer=null;try{this.adPlayer?.destroy()}catch(e){this.log("Ad player adapter destroy failed (ignored):",e)}this.adPlayer=null,this.callbacks=null,this.gamPodBridge.destroy(),this.vastAdManager?.destroy(),this.vastAdManager=null,this.log("Ad player destroyed")}log(...e){this.debug&&u("[OptiViewAds:AdPlayerController]",...e)}}AdPlayerController.AD_START_BUDGET_MS=5e3,AdPlayerController.GAM_POD_AD_START_BUDGET_MS=1e4,AdPlayerController.MIN_AD_START_BUDGET_MS=3e3;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PlayerAdapter,
|
|
1
|
+
import type { PlayerAdapter, TimedMetadataCue } from '../../interfaces/PlayerAdapter';
|
|
2
2
|
import { type Break } from '../../types/BreakManifest';
|
|
3
3
|
import type { GamEventCallbacks } from '../AdPlayerController';
|
|
4
4
|
import type { AdBreakSequencer, AdBreakTransition } from '../AdBreakSequencer';
|
|
@@ -8,12 +8,6 @@ export interface AdPlayerEventsDeps {
|
|
|
8
8
|
getContentPlayer: () => PlayerAdapter;
|
|
9
9
|
getGamEventCallbacks: () => GamEventCallbacks;
|
|
10
10
|
getSequencer: () => AdBreakSequencer | null;
|
|
11
|
-
getAudioFocus: () => 'content' | 'ad' | null;
|
|
12
|
-
getSyncing: () => boolean;
|
|
13
|
-
setSyncing: (value: boolean) => void;
|
|
14
|
-
setAdVolumeChangeHandler: (handler: PlayerAdapterEventHandler | null) => void;
|
|
15
|
-
setContentVolumeHandler: (handler: (() => void) | null) => void;
|
|
16
|
-
notifyVolumeChange: () => void;
|
|
17
11
|
onAdEnded: () => void;
|
|
18
12
|
processTimedMetadata: (cue: TimedMetadataCue) => void;
|
|
19
13
|
toAdError: (mediaError?: unknown) => Error;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AssetType as t}from"../../types/BreakManifest";export class AdPlayerEvents{constructor(t,e){this.state=t,this.deps=e}recordAdProgress(t){!Number.isFinite(t)||t<=0||(this.deps.onGamPodAdProgress(t),t>this.state.currentAssetElapsedSec&&(this.state.currentAssetElapsedSec=t))}settleAssetProgress(){let t=this.state.currentAssetElapsedSec;if(t<=0){const e=this.state.currentAsset?.duration;"number"==typeof e&&Number.isFinite(e)&&e>0&&(t=e)}this.state.adSecondsPlayedThisBreak+=t,this.state.currentAssetElapsedSec=0}reportAdProgress(e){if(this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&(this.recordAdProgress(e.currentTime),this.deps.maybePrepositionContentForResume(),this.state.currentBreak&&this.deps.maybeCatchUpContentForResume(this.state.currentBreak),this.state.callbacks?.onAdTimeUpdate(this.state.currentBreak,this.state.currentAsset,e.currentTime,e.duration),this.state.currentAsset.type===t.STATIC&&e.duration>0)){const t=e.currentTime/e.duration,s=this.deps.getStaticQuartilesFired(),r=this.deps.getGamEventCallbacks();t>=.25&&!s.has("first")&&(s.add("first"),r.onAdFirstQuartile?.(this.state.currentBreak,this.state.currentAsset)),t>=.5&&!s.has("mid")&&(s.add("mid"),r.onAdMidpoint?.(this.state.currentBreak,this.state.currentAsset)),t>=.75&&!s.has("third")&&(s.add("third"),r.onAdThirdQuartile?.(this.state.currentBreak,this.state.currentAsset))}}setupEventListeners(){const t=this.deps.getAdPlayer();
|
|
1
|
+
import{AssetType as t}from"../../types/BreakManifest";export class AdPlayerEvents{constructor(t,e){this.state=t,this.deps=e}recordAdProgress(t){!Number.isFinite(t)||t<=0||(this.deps.onGamPodAdProgress(t),t>this.state.currentAssetElapsedSec&&(this.state.currentAssetElapsedSec=t))}settleAssetProgress(){let t=this.state.currentAssetElapsedSec;if(t<=0){const e=this.state.currentAsset?.duration;"number"==typeof e&&Number.isFinite(e)&&e>0&&(t=e)}this.state.adSecondsPlayedThisBreak+=t,this.state.currentAssetElapsedSec=0}reportAdProgress(e){if(this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&(this.recordAdProgress(e.currentTime),this.deps.maybePrepositionContentForResume(),this.state.currentBreak&&this.deps.maybeCatchUpContentForResume(this.state.currentBreak),this.state.callbacks?.onAdTimeUpdate(this.state.currentBreak,this.state.currentAsset,e.currentTime,e.duration),this.state.currentAsset.type===t.STATIC&&e.duration>0)){const t=e.currentTime/e.duration,s=this.deps.getStaticQuartilesFired(),r=this.deps.getGamEventCallbacks();t>=.25&&!s.has("first")&&(s.add("first"),r.onAdFirstQuartile?.(this.state.currentBreak,this.state.currentAsset)),t>=.5&&!s.has("mid")&&(s.add("mid"),r.onAdMidpoint?.(this.state.currentBreak,this.state.currentAsset)),t>=.75&&!s.has("third")&&(s.add("third"),r.onAdThirdQuartile?.(this.state.currentBreak,this.state.currentAsset))}}setupEventListeners(){const t=this.deps.getAdPlayer();t&&(t.on("ended",()=>{this.deps.onAdEnded()}),t.on("timedmetadata",t=>{t&&this.deps.processTimedMetadata(t)}),t.on("error",t=>{this.onAdError(this.deps.toAdError(t))}),t.on("timeupdate",()=>this.reportAdProgress(t)),t.on("waiting",()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&this.state.callbacks?.onAdWaiting(this.state.currentBreak,this.state.currentAsset)}),t.on("playing",()=>{if(this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&!t.paused){if(this.state.startPlaybackCutoff?.(),this.state.firstFrameResolver)return this.deps.revealFirstFrame(),this.state.firstFrameResolver(),void this.state.callbacks?.onAdPlaying(this.state.currentBreak,this.state.currentAsset);this.state.callbacks?.onAdPlaying(this.state.currentBreak,this.state.currentAsset)}}))}onAdError(t){const e=this.deps.getSequencer(),s=this.state.currentBreak;if(!s||!e)return;const r=this.state.preloadGeneration,a=this.state.erroredAsset;if(a?.breakInfo!==s||a.generation!==r){if(this.state.erroredAsset={breakInfo:s,generation:r},this.state.firstFrameRejector){const e=this.state.firstFrameRejector;this.deps.clearFirstFrameResolver(),e(t)}this.deps.log("Ad error:",t.message),this.deps.dropQueuedAd(),this.deps.setPendingAdError(t),this.deps.drive(e.onOutcome({kind:"asset-errored"}))}else this.deps.log("Ignoring further ad error for the same asset:",t.message)}}
|
|
@@ -102,7 +102,6 @@ export interface AdPlayerState {
|
|
|
102
102
|
continueCorrections: number[];
|
|
103
103
|
continueDriftUnrecoverable: boolean;
|
|
104
104
|
continueRefusalReported: boolean;
|
|
105
|
-
contentMutedBeforeCatchup: boolean | null;
|
|
106
105
|
resumeAlignCorrectionsMs: number[];
|
|
107
106
|
resumeLandedProbe: (() => void) | null;
|
|
108
107
|
adSecondsPlayedThisBreak: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export function invalidateMediaPreload(e){return e.preparedMedia=null,++e.preloadGeneration}export function createAdPlayerState(){return{currentBreak:null,currentAsset:null,isPlaying:!1,contentPaused:!1,callbacks:null,durationTimer:null,startPlaybackCutoff:null,effectiveBreakDurationSec:null,adInsertionType:void 0,breakTimebase:null,adBreakCutSafetyMarginSec:0,detectedDeviceType:void 0,sessionAssetParameters:void 0,updatedAssetParameters:void 0,manifestAssetParameters:void 0,sessionAssetParameterMacros:{},pushedAssetParameters:null,gamStreamManager:null,gamPodMode:"none",gamPodTracker:null,gamPodAsset:null,earlyGamPodAdStart:null,withheldPodAdBegin:null,gamConfig:void 0,gamInitPromise:null,currentGamIdentity:void 0,playerContainer:null,stage:null,contentVideoElement:null,adContainer:null,adMediaContainer:null,adPlayer:null,companionElement:null,overlayImageEl:null,isContentFullscreen:!1,trackedContentVideo:null,fullscreenEnterHandler:null,fullscreenExitHandler:null,contentPictureInPicture:!1,pipTransferredToAd:!1,pipAdoptedForBreak:!1,sharedAdClaimActive:!1,savedContentSrc:null,savedContentTime:0,savedContentLive:!1,savedContentEdgeDistance:null,earlyReturnResumeOffsetSec:null,contentPositionAtBreakStart:0,resumeAnchorIsCue:!1,resumeAnchorIsPdt:!1,resumeAnchorSeekableEnd:null,resumePrepositionedTo:null,resumePrepositionSettle:null,resumeCatchupActive:!1,resumeCatchupRan:!1,lastResumeAlignRemainingSec:Number.POSITIVE_INFINITY,resumeAlignBlockedDiagnosed:!1,lastResumeSafeEnd:Number.NEGATIVE_INFINITY,resumeSawSeekDone:!1,resumeBestSawMarginSec:Number.NEGATIVE_INFINITY,resumeAlignTicks:0,resumeSawSteps:[],continueContentDuringBreak:!1,continueQualityOverrideApplied:!1,continueContentDuringBreakActive:!1,keepContentPlayingHandler:null,continuePlayoutStartWallMs:0,continuePlayoutTicker:null,continueRateWritten:null,continueRateWrittenAtMs:0,continueRateHonoured:!0,continueMaxAbsDriftSec:0,continueCorrections:[],continueDriftUnrecoverable:!1,continueRefusalReported:!1,
|
|
1
|
+
export function invalidateMediaPreload(e){return e.preparedMedia=null,++e.preloadGeneration}export function createAdPlayerState(){return{currentBreak:null,currentAsset:null,isPlaying:!1,contentPaused:!1,callbacks:null,durationTimer:null,startPlaybackCutoff:null,effectiveBreakDurationSec:null,adInsertionType:void 0,breakTimebase:null,adBreakCutSafetyMarginSec:0,detectedDeviceType:void 0,sessionAssetParameters:void 0,updatedAssetParameters:void 0,manifestAssetParameters:void 0,sessionAssetParameterMacros:{},pushedAssetParameters:null,gamStreamManager:null,gamPodMode:"none",gamPodTracker:null,gamPodAsset:null,earlyGamPodAdStart:null,withheldPodAdBegin:null,gamConfig:void 0,gamInitPromise:null,currentGamIdentity:void 0,playerContainer:null,stage:null,contentVideoElement:null,adContainer:null,adMediaContainer:null,adPlayer:null,companionElement:null,overlayImageEl:null,isContentFullscreen:!1,trackedContentVideo:null,fullscreenEnterHandler:null,fullscreenExitHandler:null,contentPictureInPicture:!1,pipTransferredToAd:!1,pipAdoptedForBreak:!1,sharedAdClaimActive:!1,savedContentSrc:null,savedContentTime:0,savedContentLive:!1,savedContentEdgeDistance:null,earlyReturnResumeOffsetSec:null,contentPositionAtBreakStart:0,resumeAnchorIsCue:!1,resumeAnchorIsPdt:!1,resumeAnchorSeekableEnd:null,resumePrepositionedTo:null,resumePrepositionSettle:null,resumeCatchupActive:!1,resumeCatchupRan:!1,lastResumeAlignRemainingSec:Number.POSITIVE_INFINITY,resumeAlignBlockedDiagnosed:!1,lastResumeSafeEnd:Number.NEGATIVE_INFINITY,resumeSawSeekDone:!1,resumeBestSawMarginSec:Number.NEGATIVE_INFINITY,resumeAlignTicks:0,resumeSawSteps:[],continueContentDuringBreak:!1,continueQualityOverrideApplied:!1,continueContentDuringBreakActive:!1,keepContentPlayingHandler:null,continuePlayoutStartWallMs:0,continuePlayoutTicker:null,continueRateWritten:null,continueRateWrittenAtMs:0,continueRateHonoured:!0,continueMaxAbsDriftSec:0,continueCorrections:[],continueDriftUnrecoverable:!1,continueRefusalReported:!1,resumeAlignCorrectionsMs:[],resumeLandedProbe:null,adSecondsPlayedThisBreak:0,currentAssetElapsedSec:0,preparedMedia:null,preloadGeneration:0,erroredAsset:null,firstFrameGateActive:!1,firstFrameGateShown:!1,firstFrameResolver:null,firstFrameRejector:null,firstFrameTimeout:null,adStartDeadlineMs:null,adStartBudgetMs:0,vastAdManager:null}}
|
|
@@ -2,6 +2,7 @@ import type { PlayerAdapter } from '../../interfaces/PlayerAdapter';
|
|
|
2
2
|
import { Asset, Break, BreakVariant, StaticAsset, VastAsset, VendorAsset } from '../../types/BreakManifest';
|
|
3
3
|
import type { AdBreakSequencer, AdBreakTransition } from '../AdBreakSequencer';
|
|
4
4
|
import { type AdPlayerState } from './AdPlayerState';
|
|
5
|
+
import type { SurfaceRevealReporter } from './SurfaceRevealReporter';
|
|
5
6
|
export interface AssetPlaybackDeps {
|
|
6
7
|
getAdPlayer: () => PlayerAdapter | null;
|
|
7
8
|
getAdContainer: () => HTMLDivElement | null;
|
|
@@ -23,6 +24,8 @@ export interface AssetPlaybackDeps {
|
|
|
23
24
|
drive: (transition: AdBreakTransition) => Promise<void>;
|
|
24
25
|
clearOverlayImage: () => void;
|
|
25
26
|
revealFirstFrame: () => void;
|
|
27
|
+
/** Watches for the ad's first frame on screen (`DA-AD-SURFACE-REVEALED`). */
|
|
28
|
+
surfaceReveal: SurfaceRevealReporter;
|
|
26
29
|
transferPictureInPictureToAd: () => Promise<void>;
|
|
27
30
|
isGamPodAsset: (asset: Asset) => boolean;
|
|
28
31
|
/** Whether the GAM pod asset was skipped because its identity mismatches the stream session. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AssetType as e,BreakFormat as t,Vendor as s,MediaType as r,isVastAsset as i}from"../../types/BreakManifest";import{AdError as a}from"../../types/AdError";import{resolveVastAdmission as d}from"../resolveVastAdmission";import{classifyMediaError as n,toClassifiedAdError as o}from"./classifyMediaError";import{VastAdManager as h,isImaCsaiSdkAvailable as
|
|
1
|
+
import{AssetType as e,BreakFormat as t,Vendor as s,MediaType as r,isVastAsset as i}from"../../types/BreakManifest";import{AdError as a}from"../../types/AdError";import{resolveVastAdmission as d}from"../resolveVastAdmission";import{classifyMediaError as n,toClassifiedAdError as o}from"./classifyMediaError";import{VastAdManager as h,isImaCsaiSdkAvailable as c}from"../VastAdManager";import{invalidateMediaPreload as p}from"./AdPlayerState";export class AssetPlayback{constructor(e,t){this.state=e,this.deps=t}get currentBreak(){return this.state.currentBreak}async playAsset(e,t,s){const d=this.currentBreak,n=this.deps.getAdPlayer();if(!d||!n)return;const h=this.state.preparedMedia,c=p(this.state),l=()=>this.state.preloadGeneration===c&&this.currentBreak===d&&this.deps.getAdPlayer()===n;if(this.state.currentAsset=e,this.state.currentAssetElapsedSec=0,this.deps.clearStaticQuartiles(),this.deps.clearOverlayImage(),this.deps.log("Playing asset:",e.id,"type:",e.type,"index:",t),i(e))return void this.playVastAsset(e,t,s);if(e.mediaType===r.IMAGE)return void await this.playImageAsset(e,t,s);this.deps.surfaceReveal.watch(e.id,d.id);const u=await this.resolveAssetUri(e,this.state.effectiveBreakDurationSec??d.duration);if(!l())return;if(!u)return void this.deps.onAdError(this.missingUriError(e));this.deps.log("Resolved URI:",u),this.deps.reportPhase("uri-resolved");const A=d.id;try{this.deps.armAdStartBudget(e);const r=0===t&&h?.breakId===A&&h.assetId===e.id&&h.uri===u&&h.player===n,i=!r&&this.deps.takeQueuedSuccessor(A,e.id,u);if(i)this.deps.reportPhase("load-skipped-queued");else if(r)this.deps.reportPhase("load-skipped-preloaded");else{if(this.deps.reportPhase("load-start"),!await this.deps.loadWithinBudget(()=>n.load(u),{breakId:A,assetId:e.id,uri:u})||!l())return;this.deps.reportPhase("load-resolved")}this.reportPreparedDuration(n,d,e,t,s);const o=n.videoElement;if(o&&(o.style.cssText="display:block;width:100%;height:100%;object-fit:contain;"),this.deps.reportPhase("play-called"),o&&this.deps.surfaceReveal.watchVideo(o),this.state.firstFrameGateActive&&!this.state.firstFrameGateShown){const e=this.deps.waitForFirstFrame();await this.playRetryingInterruption(n,A),await e}else{let e=null;const t=await Promise.race([this.playRetryingInterruption(n,A).then(()=>!0),new Promise(t=>{e=setTimeout(()=>t(!1),this.deps.remainingAdStartBudgetMs())})]);if(e&&clearTimeout(e),!l())return;if(!t)return void this.deps.onAdError(new a(`Ad did not start within ${this.state.adStartBudgetMs}ms ad-start budget`,"DA-AD-START-TIMEOUT"))}if(!l())return;this.state.adStartDeadlineMs=null,this.state.adStartBudgetMs=0,i&&this.state.startPlaybackCutoff?.(),this.deps.transferPictureInPictureToAd();const c=this.deps.getSequencer();if(c){const t=this.deps.isGamPodAsset(e);t&&this.deps.beginGamPodTracking(e),await this.deps.drive(c.onOutcome({kind:"asset-started"})),!t&&l()&&this.deps.queueUpcomingAd()}else this.state.callbacks?.onAdStart(this.currentBreak,e,t,s)}catch(e){if(!l())return;this.deps.onAdError(o(e))}}reportPreparedDuration(e,t,s,r,i){const a=e.duration;if(!Number.isFinite(a)||a<=0)return;const d=s.duration;this.deps.diagnose("DA-AD-DURATION-PREPARED",`Ad media for break ${t.id} prepared: ${a}s of playable ad`,{breakId:t.id,assetId:s.id,index:r,total:i,preparedDurationSec:a,..."number"==typeof d?{declaredDurationSec:d}:{},breakDurationSec:this.state.effectiveBreakDurationSec??t.duration})}async playRetryingInterruption(e,t){const s=this.state.preloadGeneration;try{await e.play()}catch(r){if(s!==this.state.preloadGeneration||!function(e){return"AbortError"===e?.name}(r)||!this.isStartingBreak(t))throw r;this.deps.diagnose("DA-AD-START-INTERRUPTED","Ad play() was interrupted by a browser-initiated pause before the ad started; play() re-issued once.",{breakId:t,error:r instanceof Error?r.message:String(r)}),this.deps.reportPhase("play-retried"),await e.play()}}isStartingBreak(e){return this.state.isPlaying&&this.state.currentBreak?.id===e}playVastAsset(e,s,r){if(!this.currentBreak||!this.deps.getAdPlayer())return;const i=this.deps.getVariant(this.currentBreak)?.format??t.SINGLE,n=d({format:i,ssai:!1});if(!n.admitted)return this.deps.diagnose(n.diagnostic??"DA-VAST-UNSUPPORTED-FORMAT",`VAST asset '${e.id}' is not supported in '${i}' breaks (single/double/lshape_ad only); skipping.`,{breakId:this.currentBreak.id,assetId:e.id,format:i}),void this.deps.onAdError(new a(`VAST not supported in ${i} format`,n.diagnostic??"DA-VAST-UNSUPPORTED-FORMAT"));if(!c())return this.deps.diagnose("DA-VAST-IMA-SDK-MISSING","Google IMA client-side SDK (ima3.js) is unavailable; cannot play VAST ad.",{breakId:this.currentBreak.id,assetId:e.id}),void this.deps.onAdError(new a("IMA client-side SDK (ima3.js) not available","DA-VAST-IMA-SDK-MISSING"));const o=this.deps.buildVastTagUrl(e,this.currentBreak);if(!o)return void this.deps.onAdError(new a("No valid VAST tag URL for asset","DA-AD-URI-MISSING"));const p=this.deps.getAdPlayer(),l=p?.videoElement,u=this.deps.getAdContainer();if(!u||!l||!p)return void this.deps.onAdError(new a("VAST playout requires an ad container and ad video element","DA-AD-URI-MISSING"));this.deps.revealFirstFrame(),this.deps.surfaceReveal.watch(e.id,this.currentBreak.id),l.style.cssText="display:block;width:100%;height:100%;object-fit:contain;",this.state.vastAdManager||(this.state.vastAdManager=new h(u,l,this.deps.debug)),this.state.vastAdManager.setCallbacks({onAdStarted:t=>{if(!this.currentBreak)return;const i=Array.from(u.querySelectorAll("video")).find(e=>!e.paused);this.deps.surfaceReveal.watchVideo(i??l),this.state.startPlaybackCutoff?.(),this.deps.setVastPodPosition(t);const a=this.deps.getSequencer();a?this.deps.drive(a.onOutcome({kind:"asset-started"})):this.state.callbacks?.onAdStart(this.currentBreak,e,s,r,{podPosition:t})},onAdProgress:(t,s)=>{this.currentBreak&&(this.deps.recordAdProgress(t),this.state.callbacks?.onAdTimeUpdate(this.currentBreak,e,t,s))},onAdFirstQuartile:()=>{this.currentBreak&&this.deps.getGamEventCallbacks().onAdFirstQuartile?.(this.currentBreak,e)},onAdMidpoint:()=>{this.currentBreak&&this.deps.getGamEventCallbacks().onAdMidpoint?.(this.currentBreak,e)},onAdThirdQuartile:()=>{this.currentBreak&&this.deps.getGamEventCallbacks().onAdThirdQuartile?.(this.currentBreak,e)}}),this.deps.log("Playing VAST asset via IMA CSAI:",o);(this.state.vastAdManager.hasPreloadedManager()?(this.state.vastAdManager.setMuted(p.muted),this.state.vastAdManager.startPreloaded()):this.state.vastAdManager.playVast(o,{muted:p.muted})).then(()=>{this.deps.onAdEnded()},t=>{const s=t instanceof Error?t.message:String(t),r=/ima3\.js|client-side SDK/i.test(s)?"DA-VAST-IMA-SDK-MISSING":"DA-VAST-IMA-ERROR",i=t instanceof a?t:new a(s,r);this.deps.diagnose(r,s,{...this.currentBreak?{breakId:this.currentBreak.id}:{},assetId:e.id}),this.deps.onAdError(i)})}async playImageAsset(e,t,s){const r=this.currentBreak,i=this.deps.getAdMediaContainer();if(!r||!i)return;const d=this.state.effectiveBreakDurationSec??r.duration,n=await this.resolveAssetUri(e,d);if(!n)return void this.deps.onAdError(this.missingUriError(e));const o=document.createElement("img");o.className="dolby-overlay-image",o.style.cssText="display:block;width:100%;height:100%;object-fit:contain;",this.deps.setOverlayImage(o);const h=e.duration??d;o.onload=()=>{if(this.deps.getOverlayImage()!==o||!this.currentBreak)return;this.state.startPlaybackCutoff?.();const r=this.deps.getSequencer();r?this.deps.drive(r.onOutcome({kind:"asset-started"})):this.state.callbacks?.onAdStart(this.currentBreak,e,t,s),h>0?this.deps.setImageAssetTimer(setTimeout(()=>this.onImageAssetEnded(),1e3*h)):this.onImageAssetEnded()},o.onerror=()=>{this.deps.getOverlayImage()===o&&this.deps.onAdError(new a(`Overlay image failed to load: ${n}`,"DA-IMAGE-LOAD-FAILED"))},o.src=n,i.appendChild(o)}onImageAssetEnded(){const e=this.deps.getSequencer();if(!this.currentBreak||!this.state.isPlaying||!e)return;const t=this.deps.getImageAssetTimer();t&&(clearTimeout(t),this.deps.setImageAssetTimer(null)),this.deps.drive(e.onOutcome({kind:"asset-ended"}))}async resolveAssetUri(t,r,i=this.currentBreak){switch(t.type){case e.STATIC:return this.deps.buildStaticAssetUrl(t,i);case e.VENDOR:{const e=t;return e.vendor===s.GAM?await this.deps.buildGamUrl(e,r):(this.deps.log("Unsupported vendor:",e.vendor),null)}default:return this.deps.log("Unsupported asset type:",t.type),null}}adPlayerStreamRemainingSec(){const e=this.deps.getAdPlayer();if(!e)return 0;const{duration:t,currentTime:s}=e;if(!Number.isFinite(t)||t<=0)return 0;if(!Number.isFinite(s)||s<0)return 0;const r=t-s;return r>0?r:0}revealFirstFrame(){this.deps.revealFirstFrame()}toAdError(e){const t=e,s=n(e);if(t&&("number"==typeof t.code||t.message)){const e="number"==typeof t.code?` (code ${t.code})`:"",r=t.message?`: ${t.message}`:"";return new a(`Ad playback error${e}${r}`,s)}return new a("Ad playback error",s)}missingUriError(e){return this.deps.hasGamIdentityMismatch(e)?new a("GAM pod asset identity does not match the stream session","DA-GAM-POD-IDENTITY-MISMATCH"):new a("No valid URI for asset","DA-AD-URI-MISSING")}}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { PlayerAdapter } from '../../interfaces/PlayerAdapter';
|
|
2
|
+
import type { VastAdManager } from '../VastAdManager';
|
|
3
|
+
export type AudioSide = 'ad' | 'content';
|
|
4
|
+
export interface AudioControllerDeps {
|
|
5
|
+
contentPlayer: PlayerAdapter;
|
|
6
|
+
/** Outside a break the viewer cannot reach the ad player, so a change there is the engine's own. */
|
|
7
|
+
isAdPlaying: () => boolean;
|
|
8
|
+
/** IMA plays a CSAI ad through its own audio path; null outside a VAST ad. */
|
|
9
|
+
getVastAdManager: () => VastAdManager | null;
|
|
10
|
+
/** Re-emitted by the SDK facade as the public `volumechange`. */
|
|
11
|
+
onVolumeChange: (muted: boolean, volume: number) => void;
|
|
12
|
+
log: (...args: unknown[]) => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Sole owner of the SDK's audio: holds the viewer's muted/volume state and applies it to the
|
|
16
|
+
* content and ad players, except to a side the SDK has muted for its own reasons (double box,
|
|
17
|
+
* continue-content playout, resume catch-up).
|
|
18
|
+
*
|
|
19
|
+
* Decisions read state, not timing: a `volumechange` that matches what this class applied is an
|
|
20
|
+
* echo of its own write, whenever it arrives; anything else is the viewer, the app or an IMA
|
|
21
|
+
* creative, and becomes the new viewer state.
|
|
22
|
+
*/
|
|
23
|
+
export declare class AudioController {
|
|
24
|
+
private readonly deps;
|
|
25
|
+
private readonly viewer;
|
|
26
|
+
/** The one side the SDK forces silent on top of the viewer's state; the viewer's mute is {@link viewer}. */
|
|
27
|
+
private mutedSide;
|
|
28
|
+
private adPlayer;
|
|
29
|
+
private readonly contentHandler;
|
|
30
|
+
private readonly adHandler;
|
|
31
|
+
constructor(deps: AudioControllerDeps);
|
|
32
|
+
/**
|
|
33
|
+
* Once per controller; the ad player is never replaced during a session. A single-surface
|
|
34
|
+
* device plays the ad on the content player itself: there is no second side to keep in step.
|
|
35
|
+
*/
|
|
36
|
+
attachAdPlayer(adPlayer: PlayerAdapter): void;
|
|
37
|
+
/** Gives every muted side the viewer's state back, then stops listening. */
|
|
38
|
+
destroy(): void;
|
|
39
|
+
/** Forces `side` silent, whatever the viewer's state. Replaces a mute on the other side. */
|
|
40
|
+
mute(side: AudioSide): void;
|
|
41
|
+
/** Stops forcing `side` silent: it gets the viewer's state back, which may itself be muted. */
|
|
42
|
+
unmute(side: AudioSide): void;
|
|
43
|
+
unmuteAll(): void;
|
|
44
|
+
isMuted(side: AudioSide): boolean;
|
|
45
|
+
/** The viewer's state (`sdk.muted`), not what a muted side currently plays at. */
|
|
46
|
+
get muted(): boolean;
|
|
47
|
+
set muted(value: boolean);
|
|
48
|
+
/** 0-1. See {@link muted}. */
|
|
49
|
+
get volume(): number;
|
|
50
|
+
set volume(value: number);
|
|
51
|
+
private setViewerState;
|
|
52
|
+
private player;
|
|
53
|
+
private expectedMuted;
|
|
54
|
+
private apply;
|
|
55
|
+
private applyAll;
|
|
56
|
+
private onPlayerVolumeChange;
|
|
57
|
+
private notify;
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class AudioController{constructor(e){this.deps=e,this.mutedSide=null,this.adPlayer=null,this.contentHandler=()=>this.onPlayerVolumeChange("content"),this.adHandler=()=>this.onPlayerVolumeChange("ad"),this.viewer={muted:e.contentPlayer.muted,volume:e.contentPlayer.volume},e.contentPlayer.on("volumechange",this.contentHandler)}attachAdPlayer(e){e!==this.deps.contentPlayer&&(this.adPlayer?this.deps.log("AudioController: ad player already attached; ignoring the second one"):(this.adPlayer=e,e.on("volumechange",this.adHandler),this.apply("ad")))}destroy(){this.unmuteAll(),this.deps.contentPlayer.off("volumechange",this.contentHandler),this.adPlayer?.off("volumechange",this.adHandler),this.adPlayer=null}mute(e){this.mutedSide!==e&&(this.mutedSide&&(this.deps.log(`AudioController: muting ${e} replaces the mute on ${this.mutedSide}`),this.unmute(this.mutedSide)),this.mutedSide=e,this.apply(e))}unmute(e){this.mutedSide===e&&(this.mutedSide=null,this.apply(e))}unmuteAll(){this.mutedSide&&this.unmute(this.mutedSide)}isMuted(e){return this.mutedSide===e}get muted(){return this.viewer.muted}set muted(e){this.setViewerState(e,this.viewer.volume)}get volume(){return this.viewer.volume}set volume(e){this.setViewerState(this.viewer.muted,Math.max(0,Math.min(1,e)))}setViewerState(e,t){this.viewer.muted=e,this.viewer.volume=t,this.applyAll(),this.notify()}player(e){return"content"===e?this.deps.contentPlayer:this.adPlayer}expectedMuted(e){return this.mutedSide===e||this.viewer.muted}apply(e){const t=this.player(e);t&&(t.muted!==this.expectedMuted(e)&&(t.muted=this.expectedMuted(e)),Math.abs(t.volume-this.viewer.volume)>.001&&(t.volume=this.viewer.volume)),"ad"===e&&this.deps.getVastAdManager()?.setVolume(this.expectedMuted("ad")?0:this.viewer.volume)}applyAll(){this.apply("content"),this.apply("ad")}onPlayerVolumeChange(e){const t=this.player(e);if(!t)return;const i=t.muted!==this.expectedMuted(e),s=Math.abs(t.volume-this.viewer.volume)>.001;(i||s)&&("ad"!==e||this.deps.isAdPlaying()?(i&&(this.viewer.muted=t.muted),s&&(this.viewer.volume=t.volume),this.applyAll(),this.notify()):this.apply("ad"))}notify(){this.deps.onVolumeChange(this.viewer.muted,this.viewer.volume)}}
|
|
@@ -9,6 +9,8 @@ export interface ContentResumeCoordinatorDeps {
|
|
|
9
9
|
getInsertionMode: () => string;
|
|
10
10
|
gateContentForBreak: () => void;
|
|
11
11
|
clearContentGate: (keepPlaying?: boolean) => void;
|
|
12
|
+
/** Silences content for the rest of the break; the break's hard end gives the viewer's audio back. */
|
|
13
|
+
muteContent: () => void;
|
|
12
14
|
diagnose: (code: string, message: string, context?: Record<string, unknown>) => void;
|
|
13
15
|
log: (...args: unknown[]) => void;
|
|
14
16
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Timebase as e,isPostRollBreak as t}from"../../types/BreakManifest";import{resolveResumePoint as s}from"../resolveResumePoint";import{resolveResumeClamp as n}from"../resolveResumeClamp";import{BREAK_CUT_LATE_TOLERANCE_SEC as r,resolveWallclockBreakWindow as a}from"../wallclockBreakWindow";import{ContinueContentPlayout as i}from"./ContinueContentPlayout";import{StreamClockResume as o}from"./streamClockResume";import{PdtClockResume as u,programDateTimeSample as l}from"./pdtClockResume";import{contentSeekableEnd as d,isPlausibleSeekEnd as h,liveEdgeDistanceSec as c,liveSafeSeekEnd as m,liveSafeSeekStart as p,liveSeekableWindowSec as g,unreachableEdgeSec as k}from"./liveEdge";const S=.04,f=e=>Math.round(1e3*e)/1e3;export class ContentResumeCoordinator{constructor(e,t){this.state=e,this.deps=t,this.cueAnchorResolver=null,this.resumePolicyTarget=null,this.cueEndResolver=null,this.continuePlayout=new i(this.state,{contentPlayer:this.deps.contentPlayer,clock:this.deps.clock,gateContentForBreak:()=>this.deps.gateContentForBreak(),distanceToLiveEdgeSec:()=>this.distanceToLiveEdgeSec(),plannedResumeTarget:e=>this.plannedResumeTarget(e),diagnose:(e,t,s)=>this.deps.diagnose(e,t,s),log:(...e)=>this.deps.log(...e)}),this.streamClock=new o({contentPlayer:this.deps.contentPlayer,diagnose:(e,t,s)=>this.deps.diagnose(e,t,s),activeBreak:()=>this.state.currentBreak,anchorIsCue:()=>this.state.resumeAnchorIsCue,breakEnd:e=>this.cueEndResolver?.(e)??null,computedTarget:e=>this.computedResumeTarget(e),resumesAtBreakEnd:e=>this.resumesAtBreakEnd(e),now:()=>this.deps.clock.now()}),this.pdtClock=new u({contentPlayer:this.deps.contentPlayer,diagnose:(e,t,s)=>this.deps.diagnose(e,t,s),anchorIsPdt:()=>this.state.resumeAnchorIsPdt,computedTarget:e=>this.computedResumeTarget(e),resumesAtBreakEnd:e=>this.resumesAtBreakEnd(e),now:()=>this.deps.clock.now()})}setCueEndResolver(e){this.cueEndResolver=e}ingestStreamClockCue(e){this.streamClock.ingestCue(e)}noteContentReload(){this.streamClock.noteReload()}dispose(){this.streamClock.cancelLanding(),this.pdtClock.cancelLanding()}setCueAnchorResolver(e){this.cueAnchorResolver=e}captureBreakAnchor(e){this.resetBreakResumeState();const t=this.cueAnchorResolver?.(e)??null,s=null!==t&&Number.isFinite(t),n=s?null:this.pdtBreakAnchor(e);this.state.contentPositionAtBreakStart=s?t:n??this.deps.contentPlayer.currentTime,this.state.resumeAnchorIsCue=s,this.state.resumeAnchorIsPdt=null!==n,this.state.resumeAnchorSeekableEnd=this.contentSeekableEnd(),this.streamClock.beginBreak(),this.pdtClock.beginBreak()}pdtBreakAnchor(t){if(this.state.breakTimebase!==e.WALLCLOCK||"string"!=typeof t.start)return null;const s=l(this.deps.contentPlayer);if(!s)return null;const n=a({startMs:Date.parse(t.start),durationSec:t.duration??0,sample:s});return n?(n.sampleLateSec>r&&this.deps.diagnose?.("DA-BREAK-CUT-LATE",`Break ${t.id}: content was cut ${n.sampleLateSec}s after the break start on the stream clock; the viewer saw the stream's own break until then`,{breakId:t.id,lateSec:n.sampleLateSec,start:t.start,cutPdt:new Date(s.programDateTimeMs).toISOString(),cutAt:f(s.currentTime),anchor:n.anchor}),n.anchor):null}reanchorMidBreak(e){this.state.contentPositionAtBreakStart=e,this.state.resumeAnchorIsCue=!1,this.state.resumeAnchorIsPdt=!1,this.state.resumeAnchorSeekableEnd=this.contentSeekableEnd()}liveEdgeDistanceSec(){return c(this.deps.contentPlayer)}liveSafeSeekEnd(){return m(this.deps.contentPlayer)}liveSafeSeekStart(){return p(this.deps.contentPlayer)}liveSeekableWindowSec(){return g(this.deps.contentPlayer)}contentSeekableEnd(){return d(this.deps.contentPlayer)}resumeAnchorCushionSec(){const e=this.state.resumeAnchorSeekableEnd;return null!==e&&Number.isFinite(e)?f(e-this.state.contentPositionAtBreakStart):null}isPlausibleSeekEnd(e){return h(this.deps.contentPlayer,e)}plannedResumeTarget(e){const t=this.computedResumeTarget(e);return null===t?null:this.streamClock.target(e)??this.pdtClock.target(e)??this.cueEndTarget(e)??t}computedResumeTarget(e){if(t(e))return null;const s=this.resolvePolicy(e,this.state.contentPositionAtBreakStart);return s.seek?s.target:null}resumesAtBreakEnd(e){if(t(e))return!1;const s=this.resolvePolicy(e,0);return s.seek&&Math.abs(s.offset-(e.duration??0))<.001}resolvePolicy(e,t){return s({anchor:t,duration:e.duration,adDuration:e.duration,...this.state.adInsertionType?{insertionType:this.state.adInsertionType}:{},..."number"==typeof e.resumeOffset?{resumeOffset:e.resumeOffset}:{}})}cueEndTarget(e){if(!this.state.resumeAnchorIsCue||!this.resumesAtBreakEnd(e))return null;const t=this.cueEndResolver?.(e)?.mediaTime;return"number"==typeof t&&Number.isFinite(t)?t:null}maybePrepositionContentForResume(){if("shared-element"===this.deps.getInsertionMode())return;if(!this.state.contentPaused||null!==this.state.resumePrepositionedTo)return;if(null!==this.state.resumePrepositionSettle)return;if(this.state.resumeCatchupActive)return;if(Number.isFinite(this.deps.contentPlayer.duration))return;const e=this.state.currentBreak;if(!e)return;const t=this.plannedResumeTarget(e);if(null===t)return;const s=this.contentSeekableEnd();if(null===s)return;const n=this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec,r=(e.duration??0)-n;let a;if(t<=s)a=t;else{if(!(r<=3))return;a=s}this.deps.log(a===t?`Resume pre-positioning: seeking content to ${t}s during the break (window reaches ${s}s)`:`Resume pre-positioning: window only reaches ${s}s and the ad ends in ${r}s — moving content as close to ${t}s as exists`),this.streamClock.noteSeek(a),this.streamClock.arm(),this.deps.contentPlayer.seek(a);const i=setTimeout(()=>{this.state.resumePrepositionSettle=null;const e=this.deps.contentPlayer.currentTime;Math.abs(e-a)<=S?(this.state.resumePrepositionedTo=e,this.deps.log(`Resume pre-positioned: content is at ${e}s`)):this.deps.log(`Resume pre-position rejected: asked ${a}s, landed ${e}s — window is short, will retry`)},250);this.state.resumePrepositionSettle=i}maybeCatchUpContentForResume(e){if("shared-element"===this.deps.getInsertionMode())return;if(!this.state.contentPaused||Number.isFinite(this.deps.contentPlayer.duration))return;const t=this.plannedResumeTarget(e);if(null===t)return;const s=this.deps.contentPlayer.currentTime;if(this.state.resumeCatchupActive){const n=(e.duration??0)-(this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec);if(s>t+1)return this.state.resumeCatchupActive=!1,this.deps.contentPlayer.pause(),this.state.resumePrepositionedTo=this.deps.contentPlayer.currentTime,void this.deps.log(`Resume catch-up halted: content reached ${this.deps.contentPlayer.currentTime}s, past the ${t}s target`);if(n<=30&&n>=1){this.state.resumeAlignTicks+=1;const e=s+n,r=e-t;if(Math.abs(r)>S){const a=s-r,i=this.liveSafeSeekEnd();if(null!==i){const e=i>this.state.lastResumeSafeEnd+.25;e&&Number.isFinite(this.state.lastResumeSafeEnd)&&this.state.resumeSawSteps.push(Math.round(100*(i-this.state.lastResumeSafeEnd))/100),i>this.state.lastResumeSafeEnd&&(this.state.lastResumeSafeEnd=i);const s=i-a;if(s>this.state.resumeBestSawMarginSec&&(this.state.resumeBestSawMarginSec=s),a>i)return void(this.state.resumeAlignBlockedDiagnosed||(this.state.resumeAlignBlockedDiagnosed=!0,this.deps.log(`Resume align waiting: ${a}s is past the published edge ${i}s (${Math.round(1e3*(a-i))}ms short) — rolling until a manifest step brings it in reach`),this.deps.diagnose?.("DA-RESUME-ALIGN-BLOCKED",`Resume alignment cannot reach ${t}s yet: the published edge is ${i}s`,{target:t,requested:a,safeEnd:i,shortfallSec:Math.round(1e3*(a-i))/1e3,remainingSec:n})));if(!e&&this.state.resumeSawSeekDone)return;this.state.resumeSawSeekDone=!0}if(this.state.lastResumeAlignRemainingSec-n<.5)return;this.state.lastResumeAlignRemainingSec=n,this.deps.log(`Resume align: landing ${e}s vs target ${t}s (${Math.round(1e3*r)}ms) — content to ${a}s, ${n}s of ad left`),this.state.resumeAlignCorrectionsMs.push(Math.round(1e3*r)),this.streamClock.noteSeek(a),this.deps.contentPlayer.seek(a)}}return}if(t-s<=S)return;const n=this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec,r=(e.duration??0)-n;if(r<=0)return;if(r>30)return;const a=this.contentSeekableEnd();if(null===a)return;const i=t-r+.3,o=Math.min(i,a);this.state.resumeCatchupActive=!0,this.state.resumeCatchupRan=!0,this.state.contentMutedBeforeCatchup=this.deps.contentPlayer.muted,this.deps.contentPlayer.muted=!0,this.deps.log(`Resume catch-up: content to ${o}s, playing toward ${t}s with ${r}s of ad left`),this.deps.diagnose?.("DA-RESUME-CATCHUP",`Break ${e.id}: playing content from ${o}s toward ${t}s with ${r}s of ad left`,{breakId:e.id,startPos:o,target:t,remainingAdSec:Math.round(1e3*r)/1e3,publishedEnd:a,clampedStart:o<i}),Math.abs(s-o)>S&&(this.streamClock.noteSeek(o),this.deps.contentPlayer.seek(o)),this.streamClock.arm(),this.deps.contentPlayer.play()}endResumeCatchup(e=!1){this.state.resumeCatchupActive&&(this.state.resumeCatchupActive=!1,e||this.deps.contentPlayer.pause()),null!==this.state.contentMutedBeforeCatchup&&(this.deps.contentPlayer.muted=this.state.contentMutedBeforeCatchup,this.state.contentMutedBeforeCatchup=null)}get resumeHandoverInMotion(){return this.state.resumeCatchupActive}resumeAlignSummary(){const e=this.state.resumeAlignCorrectionsMs;return{catchUpRan:this.state.resumeCatchupRan||this.state.resumeCatchupActive||e.length>0||null!==this.state.resumePrepositionedTo,alignCorrections:e.length,placedOnStep:this.state.resumeSawSeekDone,alignTicks:this.state.resumeAlignTicks,sawSteps:this.state.resumeSawSteps.slice(0,12),sawStepCount:this.state.resumeSawSteps.length,bestSawMarginSec:Number.isFinite(this.state.resumeBestSawMarginSec)?Math.round(1e3*this.state.resumeBestSawMarginSec)/1e3:null,alignCorrectionsMs:e.slice(0,12),alignLastErrorMs:e.length?e[e.length-1]:null}}cancelResumeLandedProbe(){const e=this.state.resumeLandedProbe;this.state.resumeLandedProbe=null,e?.()}probeResumeLanding(e,t,s=null){this.cancelResumeLandedProbe();let n=!1;const r=(r,a)=>{if(n)return;n=!0,this.cancelResumeLandedProbe();const i=null===r?null:Math.round(1e3*(r-t))/1e3,o=null===r||null===s?null:Math.round(1e3*(r-s))/1e3,u=null!==s&&Math.abs(s-t)>=5e-4&&null!==o?`; ${Math.abs(o)}s ${o<0?"before":"after"} the ${s}s resume point`:"";this.deps.log(null===r?`Resume landing unknown for break ${e} (${a})`:`Resume landed at ${r}s, ${i}s from the ${t}s asked for${u} (break ${e})`),this.deps.diagnose?.("DA-RESUME-LANDED",null===r?`Resume landing for break ${e} could not be measured (${a})`:`Resume for break ${e} landed at ${r}s, ${i}s from the ${t}s requested${u}`,{breakId:e,target:t,landed:r,errorSec:i,...null===s?{}:{policyTarget:s,policyErrorSec:o},...a?{reason:a}:{}})},a=()=>{this.streamClock.updateLanding(e,this.deps.contentPlayer.currentTime),r(this.deps.contentPlayer.currentTime)},i=setTimeout(()=>r(null,"no seeked event"),2e3);this.state.resumeLandedProbe=()=>{clearTimeout(i),this.deps.contentPlayer.off("seeked",a)},this.deps.contentPlayer.on("seeked",a)}async waitForLiveEdge(e=()=>!0){for(let t=0;t<20;t++){if(!e())return null;const t=this.deps.contentPlayer.videoElement,s=t?.seekable;if(s&&s.length>0){const e=s.end(s.length-1);if(Number.isFinite(e)&&e>0)return e}if(await new Promise(e=>setTimeout(e,100)),!e())return null}return this.deps.log("shared-element: no seekable range after restore; staying at the live edge"),null}resetBreakResumeState(){this.state.resumeAnchorIsCue=!1,this.state.resumeAnchorIsPdt=!1,this.state.resumeAnchorSeekableEnd=null,this.state.resumePrepositionedTo=null,this.state.resumeCatchupActive=!1,this.state.resumeCatchupRan=!1,this.state.contentMutedBeforeCatchup=null,this.state.resumeAlignCorrectionsMs=[],this.state.lastResumeAlignRemainingSec=Number.POSITIVE_INFINITY,this.state.resumeAlignBlockedDiagnosed=!1,this.state.lastResumeSafeEnd=Number.NEGATIVE_INFINITY,this.state.resumeSawSeekDone=!1,this.state.resumeBestSawMarginSec=Number.NEGATIVE_INFINITY,this.state.resumeAlignTicks=0,this.state.resumeSawSteps=[],this.state.resumePrepositionSettle&&(clearTimeout(this.state.resumePrepositionSettle),this.state.resumePrepositionSettle=null),this.state.adSecondsPlayedThisBreak=0,this.state.currentAssetElapsedSec=0}beginContinueContentPlayout(){this.continuePlayout.beginContinueContentPlayout()}distanceToLiveEdgeSec(){const e=this.contentSeekableEnd();return null===e?null:f(e-this.deps.contentPlayer.currentTime)}secondsUntilCut(e){return this.continuePlayout.secondsUntilCut(e)}restoreContinueQuality(e){this.continuePlayout.restoreContinueQuality(e)}currentContinueDriftSec(){return this.continuePlayout.currentContinueDriftSec()}abandonContinueContentPlayout(e){this.continuePlayout.abandonContinueContentPlayout(e)}endContinueContentPlayout(){this.continuePlayout.endContinueContentPlayout()}setContinueRate(e){this.continuePlayout.setContinueRate(e)}tickContinueContentPlayout(){this.continuePlayout.tickContinueContentPlayout()}resolveResumeTarget(t,r=null){this.resumePolicyTarget=null;const a=null!==r,i=(this.state.resumeAnchorIsCue||this.state.resumeAnchorIsPdt)&&!a,o=a?r:i?t.duration:this.state.adSecondsPlayedThisBreak,u=s({anchor:this.state.contentPositionAtBreakStart,duration:t.duration,adDuration:o,...a?{}:{insertionType:this.state.adInsertionType},...a?{resumeOffset:r}:"number"==typeof t.resumeOffset?{resumeOffset:t.resumeOffset}:{}});if(u.clamped&&this.deps.diagnose?.("DA-RESUME-OFFSET-CLAMPED",`Resume offset shortened to the ${u.offset}s of ad actually played for break ${t.id}`,{breakId:t.id,offset:u.offset,source:u.source}),!u.seek)return this.deps.log(`Resume: no seek for break ${t.id} (offset 0, ${u.source})`),null;const l=this.streamClock.handBackTarget(t,a)??this.pdtClock.handBackTarget(t,a),d=null!==l||a?null:this.reportCueEnd(t,u.target),h=l??d??u.target;this.resumePolicyTarget=h;const c=this.streamClock.seekBasis(t)??this.pdtClock.seekBasis()??(null!==d?"cue_end":`offset ${u.offset}s, ${u.source}`);this.deps.log(`Resume-seek to ${h}s (${c}) for break ${t.id}`);const m=this.resumeAlignSummary(),p=this.liveSafeSeekEnd();this.deps.diagnose?.("DA-RESUME-SEEK",`Resume-seek to ${h}s (${c}) for break ${t.id}`,{breakId:t.id,target:h,offset:u.offset,source:u.source,...h===u.target?{}:{computedTarget:u.target},...null===d?{}:{cueEndTarget:d},...this.streamClock.seekContext(t,h,u.target),...this.pdtClock.seekContext(t,h,u.target),anchorPosition:this.state.contentPositionAtBreakStart,anchorIsCue:this.state.resumeAnchorIsCue,anchorIsPdt:this.state.resumeAnchorIsPdt,anchorSeekableEnd:this.state.resumeAnchorSeekableEnd,anchorCushionSec:this.resumeAnchorCushionSec(),safeEnd:p,seekableEndNow:this.contentSeekableEnd(),liveEdge:this.deps.contentPlayer.liveEdge??null,unreachableEdgeSec:k(this.deps.contentPlayer),...m.catchUpRan?m:{}}),this.streamClock.reportCorrection(t,h,u.target,a),this.pdtClock.reportCorrection(t,h,u.target,a);(this.state.breakTimebase===e.PTS||this.state.breakTimebase===e.MEDIATIME)&&"number"==typeof t.start&&this.state.callbacks?.onContentResumeSeek?.(h,t.start+t.duration);const g=this.liveSafeSeekStart(),f=n({target:h,safeStart:g,safeEnd:p,windowSec:this.liveSeekableWindowSec()}),{target:C,shortfallSec:P,bound:b}=f;if(f.startClampSkipped){const{behindSec:e,windowSec:s,maxBehindSec:n}=f.startClampSkipped;this.deps.log(`Resume-seek: ${h}s is ${e}s behind the element's live window start (window ${s}s); not clamping`),this.deps.diagnose?.("DA-RESUME-START-CLAMP-SKIPPED",`Resume target ${h}s for break ${t.id} is ${e}s behind the element's seekable start but the gap exceeds half the ${s}s window; left unchanged`,{breakId:t.id,requested:h,seekableStart:g,behindSec:e,windowSec:s,maxBehindSec:n,...this.resumeAlignSummary()})}if(null!==this.state.resumePrepositionedTo&&Math.abs(this.deps.contentPlayer.currentTime-C)<=S){const e=this.deps.contentPlayer.currentTime;return this.deps.log(`Resume: content already at ${e}s for break ${t.id} (pre-positioned during the break)`+(P>0?` — ${P}s short of policy, those frames are not published yet`:"")+" — no seek"),this.deps.diagnose?.("DA-RESUME-PREPOSITIONED",`Break ${t.id} resumed without a seek: content was moved to ${e}s during the break`+(P>0?` (${P}s short of the ${h}s policy target)`:""),{breakId:t.id,target:h,prepositionedTo:this.state.resumePrepositionedTo,landed:e,shortfallSec:P,exact:0===P,...this.resumeAlignSummary()}),this.streamClock.armLanding(t,e),this.pdtClock.armLanding(t),null}return P>0&&(this.deps.log("start"===b?`Resume-seek clamped: ${h}s is behind the live window start (already evicted); seeking ${C}s instead (${P}s early)`:`Resume-seek clamped: ${h}s is beyond what the stream has published; seeking ${C}s instead (${P}s short)`),this.deps.diagnose?.("DA-RESUME-TARGET-CLAMPED","start"===b?`Resume target ${h}s for break ${t.id} is behind the live window start (already evicted); clamped to ${C}s`:`Resume target ${h}s for break ${t.id} is beyond the published window; clamped to ${C}s`,{breakId:t.id,requested:h,clamped:C,shortfallSec:P,bound:b,...this.resumeAlignSummary()})),this.streamClock.armLanding(t,C),this.pdtClock.armLanding(t),C}reportCueEnd(e,t){const s=this.state.resumeAnchorIsCue?this.cueEndResolver?.(e):null;if(!s||!Number.isFinite(s.mediaTime))return null;const n=this.resumesAtBreakEnd(e),r=f(s.mediaTime-t);return this.deps.diagnose("DA-RESUME-CUE-END",n?`Break ${e.id}: resuming on the stream's cue_end at ${s.mediaTime}s (${r}s from the computed ${t}s)`:`Break ${e.id}: cue_end at ${s.mediaTime}s not used; the resume policy does not resume at the break end`,{breakId:e.id,computedTarget:t,cueEndTarget:s.mediaTime,deltaSec:r,applied:n,stage:"hand-back",corrected:!1}),n?s.mediaTime:null}applyResumeSeek(e,t=null){const s=this.resolveResumeTarget(e,t);null!==s&&(this.probeResumeLanding(e.id,s,this.resumePolicyTarget),this.streamClock.noteSeek(s),this.deps.contentPlayer.seek(s))}}
|
|
1
|
+
import{Timebase as e,isPostRollBreak as t}from"../../types/BreakManifest";import{resolveResumePoint as s}from"../resolveResumePoint";import{resolveResumeClamp as n}from"../resolveResumeClamp";import{BREAK_CUT_LATE_TOLERANCE_SEC as r,resolveWallclockBreakWindow as i}from"../wallclockBreakWindow";import{ContinueContentPlayout as a}from"./ContinueContentPlayout";import{StreamClockResume as o}from"./streamClockResume";import{PdtClockResume as u,programDateTimeSample as l}from"./pdtClockResume";import{contentSeekableEnd as d,isPlausibleSeekEnd as h,liveEdgeDistanceSec as c,liveSafeSeekEnd as m,liveSafeSeekStart as p,liveSeekableWindowSec as g,unreachableEdgeSec as k}from"./liveEdge";const S=.04,f=e=>Math.round(1e3*e)/1e3;export class ContentResumeCoordinator{constructor(e,t){this.state=e,this.deps=t,this.cueAnchorResolver=null,this.resumePolicyTarget=null,this.cueEndResolver=null,this.continuePlayout=new a(this.state,{contentPlayer:this.deps.contentPlayer,clock:this.deps.clock,gateContentForBreak:()=>this.deps.gateContentForBreak(),distanceToLiveEdgeSec:()=>this.distanceToLiveEdgeSec(),plannedResumeTarget:e=>this.plannedResumeTarget(e),diagnose:(e,t,s)=>this.deps.diagnose(e,t,s),log:(...e)=>this.deps.log(...e)}),this.streamClock=new o({contentPlayer:this.deps.contentPlayer,diagnose:(e,t,s)=>this.deps.diagnose(e,t,s),activeBreak:()=>this.state.currentBreak,anchorIsCue:()=>this.state.resumeAnchorIsCue,breakEnd:e=>this.cueEndResolver?.(e)??null,computedTarget:e=>this.computedResumeTarget(e),resumesAtBreakEnd:e=>this.resumesAtBreakEnd(e),now:()=>this.deps.clock.now()}),this.pdtClock=new u({contentPlayer:this.deps.contentPlayer,diagnose:(e,t,s)=>this.deps.diagnose(e,t,s),anchorIsPdt:()=>this.state.resumeAnchorIsPdt,computedTarget:e=>this.computedResumeTarget(e),resumesAtBreakEnd:e=>this.resumesAtBreakEnd(e),now:()=>this.deps.clock.now()})}setCueEndResolver(e){this.cueEndResolver=e}ingestStreamClockCue(e){this.streamClock.ingestCue(e)}noteContentReload(){this.streamClock.noteReload()}dispose(){this.streamClock.cancelLanding(),this.pdtClock.cancelLanding()}setCueAnchorResolver(e){this.cueAnchorResolver=e}captureBreakAnchor(e){this.resetBreakResumeState();const t=this.cueAnchorResolver?.(e)??null,s=null!==t&&Number.isFinite(t),n=s?null:this.pdtBreakAnchor(e);this.state.contentPositionAtBreakStart=s?t:n??this.deps.contentPlayer.currentTime,this.state.resumeAnchorIsCue=s,this.state.resumeAnchorIsPdt=null!==n,this.state.resumeAnchorSeekableEnd=this.contentSeekableEnd(),this.streamClock.beginBreak(),this.pdtClock.beginBreak()}pdtBreakAnchor(t){if(this.state.breakTimebase!==e.WALLCLOCK||"string"!=typeof t.start)return null;const s=l(this.deps.contentPlayer);if(!s)return null;const n=i({startMs:Date.parse(t.start),durationSec:t.duration??0,sample:s});return n?(n.sampleLateSec>r&&this.deps.diagnose?.("DA-BREAK-CUT-LATE",`Break ${t.id}: content was cut ${n.sampleLateSec}s after the break start on the stream clock; the viewer saw the stream's own break until then`,{breakId:t.id,lateSec:n.sampleLateSec,start:t.start,cutPdt:new Date(s.programDateTimeMs).toISOString(),cutAt:f(s.currentTime),anchor:n.anchor}),n.anchor):null}reanchorMidBreak(e){this.state.contentPositionAtBreakStart=e,this.state.resumeAnchorIsCue=!1,this.state.resumeAnchorIsPdt=!1,this.state.resumeAnchorSeekableEnd=this.contentSeekableEnd()}liveEdgeDistanceSec(){return c(this.deps.contentPlayer)}liveSafeSeekEnd(){return m(this.deps.contentPlayer)}liveSafeSeekStart(){return p(this.deps.contentPlayer)}liveSeekableWindowSec(){return g(this.deps.contentPlayer)}contentSeekableEnd(){return d(this.deps.contentPlayer)}resumeAnchorCushionSec(){const e=this.state.resumeAnchorSeekableEnd;return null!==e&&Number.isFinite(e)?f(e-this.state.contentPositionAtBreakStart):null}isPlausibleSeekEnd(e){return h(this.deps.contentPlayer,e)}plannedResumeTarget(e){const t=this.computedResumeTarget(e);return null===t?null:this.streamClock.target(e)??this.pdtClock.target(e)??this.cueEndTarget(e)??t}computedResumeTarget(e){if(t(e))return null;const s=this.resolvePolicy(e,this.state.contentPositionAtBreakStart);return s.seek?s.target:null}resumesAtBreakEnd(e){if(t(e))return!1;const s=this.resolvePolicy(e,0);return s.seek&&Math.abs(s.offset-(e.duration??0))<.001}resolvePolicy(e,t){return s({anchor:t,duration:e.duration,adDuration:e.duration,...this.state.adInsertionType?{insertionType:this.state.adInsertionType}:{},..."number"==typeof e.resumeOffset?{resumeOffset:e.resumeOffset}:{}})}cueEndTarget(e){if(!this.state.resumeAnchorIsCue||!this.resumesAtBreakEnd(e))return null;const t=this.cueEndResolver?.(e)?.mediaTime;return"number"==typeof t&&Number.isFinite(t)?t:null}maybePrepositionContentForResume(){if("shared-element"===this.deps.getInsertionMode())return;if(!this.state.contentPaused||null!==this.state.resumePrepositionedTo)return;if(null!==this.state.resumePrepositionSettle)return;if(this.state.resumeCatchupActive)return;if(Number.isFinite(this.deps.contentPlayer.duration))return;const e=this.state.currentBreak;if(!e)return;const t=this.plannedResumeTarget(e);if(null===t)return;const s=this.contentSeekableEnd();if(null===s)return;const n=this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec,r=(e.duration??0)-n;let i;if(t<=s)i=t;else{if(!(r<=3))return;i=s}this.deps.log(i===t?`Resume pre-positioning: seeking content to ${t}s during the break (window reaches ${s}s)`:`Resume pre-positioning: window only reaches ${s}s and the ad ends in ${r}s — moving content as close to ${t}s as exists`),this.streamClock.noteSeek(i),this.streamClock.arm(),this.deps.contentPlayer.seek(i);const a=setTimeout(()=>{this.state.resumePrepositionSettle=null;const e=this.deps.contentPlayer.currentTime;Math.abs(e-i)<=S?(this.state.resumePrepositionedTo=e,this.deps.log(`Resume pre-positioned: content is at ${e}s`)):this.deps.log(`Resume pre-position rejected: asked ${i}s, landed ${e}s — window is short, will retry`)},250);this.state.resumePrepositionSettle=a}maybeCatchUpContentForResume(e){if("shared-element"===this.deps.getInsertionMode())return;if(!this.state.contentPaused||Number.isFinite(this.deps.contentPlayer.duration))return;const t=this.plannedResumeTarget(e);if(null===t)return;const s=this.deps.contentPlayer.currentTime;if(this.state.resumeCatchupActive){const n=(e.duration??0)-(this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec);if(s>t+1)return this.state.resumeCatchupActive=!1,this.deps.contentPlayer.pause(),this.state.resumePrepositionedTo=this.deps.contentPlayer.currentTime,void this.deps.log(`Resume catch-up halted: content reached ${this.deps.contentPlayer.currentTime}s, past the ${t}s target`);if(n<=30&&n>=1){this.state.resumeAlignTicks+=1;const e=s+n,r=e-t;if(Math.abs(r)>S){const i=s-r,a=this.liveSafeSeekEnd();if(null!==a){const e=a>this.state.lastResumeSafeEnd+.25;e&&Number.isFinite(this.state.lastResumeSafeEnd)&&this.state.resumeSawSteps.push(Math.round(100*(a-this.state.lastResumeSafeEnd))/100),a>this.state.lastResumeSafeEnd&&(this.state.lastResumeSafeEnd=a);const s=a-i;if(s>this.state.resumeBestSawMarginSec&&(this.state.resumeBestSawMarginSec=s),i>a)return void(this.state.resumeAlignBlockedDiagnosed||(this.state.resumeAlignBlockedDiagnosed=!0,this.deps.log(`Resume align waiting: ${i}s is past the published edge ${a}s (${Math.round(1e3*(i-a))}ms short) — rolling until a manifest step brings it in reach`),this.deps.diagnose?.("DA-RESUME-ALIGN-BLOCKED",`Resume alignment cannot reach ${t}s yet: the published edge is ${a}s`,{target:t,requested:i,safeEnd:a,shortfallSec:Math.round(1e3*(i-a))/1e3,remainingSec:n})));if(!e&&this.state.resumeSawSeekDone)return;this.state.resumeSawSeekDone=!0}if(this.state.lastResumeAlignRemainingSec-n<.5)return;this.state.lastResumeAlignRemainingSec=n,this.deps.log(`Resume align: landing ${e}s vs target ${t}s (${Math.round(1e3*r)}ms) — content to ${i}s, ${n}s of ad left`),this.state.resumeAlignCorrectionsMs.push(Math.round(1e3*r)),this.streamClock.noteSeek(i),this.deps.contentPlayer.seek(i)}}return}if(t-s<=S)return;const n=this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec,r=(e.duration??0)-n;if(r<=0)return;if(r>30)return;const i=this.contentSeekableEnd();if(null===i)return;const a=t-r+.3,o=Math.min(a,i);this.state.resumeCatchupActive=!0,this.state.resumeCatchupRan=!0,this.deps.muteContent(),this.deps.log(`Resume catch-up: content to ${o}s, playing toward ${t}s with ${r}s of ad left`),this.deps.diagnose?.("DA-RESUME-CATCHUP",`Break ${e.id}: playing content from ${o}s toward ${t}s with ${r}s of ad left`,{breakId:e.id,startPos:o,target:t,remainingAdSec:Math.round(1e3*r)/1e3,publishedEnd:i,clampedStart:o<a}),Math.abs(s-o)>S&&(this.streamClock.noteSeek(o),this.deps.contentPlayer.seek(o)),this.streamClock.arm(),this.deps.contentPlayer.play()}endResumeCatchup(e=!1){this.state.resumeCatchupActive&&(this.state.resumeCatchupActive=!1,e||this.deps.contentPlayer.pause())}get resumeHandoverInMotion(){return this.state.resumeCatchupActive}resumeAlignSummary(){const e=this.state.resumeAlignCorrectionsMs;return{catchUpRan:this.state.resumeCatchupRan||this.state.resumeCatchupActive||e.length>0||null!==this.state.resumePrepositionedTo,alignCorrections:e.length,placedOnStep:this.state.resumeSawSeekDone,alignTicks:this.state.resumeAlignTicks,sawSteps:this.state.resumeSawSteps.slice(0,12),sawStepCount:this.state.resumeSawSteps.length,bestSawMarginSec:Number.isFinite(this.state.resumeBestSawMarginSec)?Math.round(1e3*this.state.resumeBestSawMarginSec)/1e3:null,alignCorrectionsMs:e.slice(0,12),alignLastErrorMs:e.length?e[e.length-1]:null}}cancelResumeLandedProbe(){const e=this.state.resumeLandedProbe;this.state.resumeLandedProbe=null,e?.()}probeResumeLanding(e,t,s=null){this.cancelResumeLandedProbe();let n=!1;const r=(r,i)=>{if(n)return;n=!0,this.cancelResumeLandedProbe();const a=null===r?null:Math.round(1e3*(r-t))/1e3,o=null===r||null===s?null:Math.round(1e3*(r-s))/1e3,u=null!==s&&Math.abs(s-t)>=5e-4&&null!==o?`; ${Math.abs(o)}s ${o<0?"before":"after"} the ${s}s resume point`:"";this.deps.log(null===r?`Resume landing unknown for break ${e} (${i})`:`Resume landed at ${r}s, ${a}s from the ${t}s asked for${u} (break ${e})`),this.deps.diagnose?.("DA-RESUME-LANDED",null===r?`Resume landing for break ${e} could not be measured (${i})`:`Resume for break ${e} landed at ${r}s, ${a}s from the ${t}s requested${u}`,{breakId:e,target:t,landed:r,errorSec:a,...null===s?{}:{policyTarget:s,policyErrorSec:o},...i?{reason:i}:{}})},i=()=>{this.streamClock.updateLanding(e,this.deps.contentPlayer.currentTime),r(this.deps.contentPlayer.currentTime)},a=setTimeout(()=>r(null,"no seeked event"),2e3);this.state.resumeLandedProbe=()=>{clearTimeout(a),this.deps.contentPlayer.off("seeked",i)},this.deps.contentPlayer.on("seeked",i)}async waitForLiveEdge(e=()=>!0){for(let t=0;t<20;t++){if(!e())return null;const t=this.deps.contentPlayer.videoElement,s=t?.seekable;if(s&&s.length>0){const e=s.end(s.length-1);if(Number.isFinite(e)&&e>0)return e}if(await new Promise(e=>setTimeout(e,100)),!e())return null}return this.deps.log("shared-element: no seekable range after restore; staying at the live edge"),null}resetBreakResumeState(){this.state.resumeAnchorIsCue=!1,this.state.resumeAnchorIsPdt=!1,this.state.resumeAnchorSeekableEnd=null,this.state.resumePrepositionedTo=null,this.state.resumeCatchupActive=!1,this.state.resumeCatchupRan=!1,this.state.resumeAlignCorrectionsMs=[],this.state.lastResumeAlignRemainingSec=Number.POSITIVE_INFINITY,this.state.resumeAlignBlockedDiagnosed=!1,this.state.lastResumeSafeEnd=Number.NEGATIVE_INFINITY,this.state.resumeSawSeekDone=!1,this.state.resumeBestSawMarginSec=Number.NEGATIVE_INFINITY,this.state.resumeAlignTicks=0,this.state.resumeSawSteps=[],this.state.resumePrepositionSettle&&(clearTimeout(this.state.resumePrepositionSettle),this.state.resumePrepositionSettle=null),this.state.adSecondsPlayedThisBreak=0,this.state.currentAssetElapsedSec=0}beginContinueContentPlayout(){this.continuePlayout.beginContinueContentPlayout()}distanceToLiveEdgeSec(){const e=this.contentSeekableEnd();return null===e?null:f(e-this.deps.contentPlayer.currentTime)}secondsUntilCut(e){return this.continuePlayout.secondsUntilCut(e)}restoreContinueQuality(e){this.continuePlayout.restoreContinueQuality(e)}currentContinueDriftSec(){return this.continuePlayout.currentContinueDriftSec()}abandonContinueContentPlayout(e){this.continuePlayout.abandonContinueContentPlayout(e)}endContinueContentPlayout(){this.continuePlayout.endContinueContentPlayout()}setContinueRate(e){this.continuePlayout.setContinueRate(e)}tickContinueContentPlayout(){this.continuePlayout.tickContinueContentPlayout()}resolveResumeTarget(t,r=null){this.resumePolicyTarget=null;const i=null!==r,a=(this.state.resumeAnchorIsCue||this.state.resumeAnchorIsPdt)&&!i,o=i?r:a?t.duration:this.state.adSecondsPlayedThisBreak,u=s({anchor:this.state.contentPositionAtBreakStart,duration:t.duration,adDuration:o,...i?{}:{insertionType:this.state.adInsertionType},...i?{resumeOffset:r}:"number"==typeof t.resumeOffset?{resumeOffset:t.resumeOffset}:{}});if(u.clamped&&this.deps.diagnose?.("DA-RESUME-OFFSET-CLAMPED",`Resume offset shortened to the ${u.offset}s of ad actually played for break ${t.id}`,{breakId:t.id,offset:u.offset,source:u.source}),!u.seek)return this.deps.log(`Resume: no seek for break ${t.id} (offset 0, ${u.source})`),null;const l=this.streamClock.handBackTarget(t,i)??this.pdtClock.handBackTarget(t,i),d=null!==l||i?null:this.reportCueEnd(t,u.target),h=l??d??u.target;this.resumePolicyTarget=h;const c=this.streamClock.seekBasis(t)??this.pdtClock.seekBasis()??(null!==d?"cue_end":`offset ${u.offset}s, ${u.source}`);this.deps.log(`Resume-seek to ${h}s (${c}) for break ${t.id}`);const m=this.resumeAlignSummary(),p=this.liveSafeSeekEnd();this.deps.diagnose?.("DA-RESUME-SEEK",`Resume-seek to ${h}s (${c}) for break ${t.id}`,{breakId:t.id,target:h,offset:u.offset,source:u.source,...h===u.target?{}:{computedTarget:u.target},...null===d?{}:{cueEndTarget:d},...this.streamClock.seekContext(t,h,u.target),...this.pdtClock.seekContext(t,h,u.target),anchorPosition:this.state.contentPositionAtBreakStart,anchorIsCue:this.state.resumeAnchorIsCue,anchorIsPdt:this.state.resumeAnchorIsPdt,anchorSeekableEnd:this.state.resumeAnchorSeekableEnd,anchorCushionSec:this.resumeAnchorCushionSec(),safeEnd:p,seekableEndNow:this.contentSeekableEnd(),liveEdge:this.deps.contentPlayer.liveEdge??null,unreachableEdgeSec:k(this.deps.contentPlayer),...m.catchUpRan?m:{}}),this.streamClock.reportCorrection(t,h,u.target,i),this.pdtClock.reportCorrection(t,h,u.target,i);(this.state.breakTimebase===e.PTS||this.state.breakTimebase===e.MEDIATIME)&&"number"==typeof t.start&&this.state.callbacks?.onContentResumeSeek?.(h,t.start+t.duration);const g=this.liveSafeSeekStart(),f=n({target:h,safeStart:g,safeEnd:p,windowSec:this.liveSeekableWindowSec()}),{target:C,shortfallSec:P,bound:b}=f;if(f.startClampSkipped){const{behindSec:e,windowSec:s,maxBehindSec:n}=f.startClampSkipped;this.deps.log(`Resume-seek: ${h}s is ${e}s behind the element's live window start (window ${s}s); not clamping`),this.deps.diagnose?.("DA-RESUME-START-CLAMP-SKIPPED",`Resume target ${h}s for break ${t.id} is ${e}s behind the element's seekable start but the gap exceeds half the ${s}s window; left unchanged`,{breakId:t.id,requested:h,seekableStart:g,behindSec:e,windowSec:s,maxBehindSec:n,...this.resumeAlignSummary()})}if(null!==this.state.resumePrepositionedTo&&Math.abs(this.deps.contentPlayer.currentTime-C)<=S){const e=this.deps.contentPlayer.currentTime;return this.deps.log(`Resume: content already at ${e}s for break ${t.id} (pre-positioned during the break)`+(P>0?` — ${P}s short of policy, those frames are not published yet`:"")+" — no seek"),this.deps.diagnose?.("DA-RESUME-PREPOSITIONED",`Break ${t.id} resumed without a seek: content was moved to ${e}s during the break`+(P>0?` (${P}s short of the ${h}s policy target)`:""),{breakId:t.id,target:h,prepositionedTo:this.state.resumePrepositionedTo,landed:e,shortfallSec:P,exact:0===P,...this.resumeAlignSummary()}),this.streamClock.armLanding(t,e),this.pdtClock.armLanding(t),null}return P>0&&(this.deps.log("start"===b?`Resume-seek clamped: ${h}s is behind the live window start (already evicted); seeking ${C}s instead (${P}s early)`:`Resume-seek clamped: ${h}s is beyond what the stream has published; seeking ${C}s instead (${P}s short)`),this.deps.diagnose?.("DA-RESUME-TARGET-CLAMPED","start"===b?`Resume target ${h}s for break ${t.id} is behind the live window start (already evicted); clamped to ${C}s`:`Resume target ${h}s for break ${t.id} is beyond the published window; clamped to ${C}s`,{breakId:t.id,requested:h,clamped:C,shortfallSec:P,bound:b,...this.resumeAlignSummary()})),this.streamClock.armLanding(t,C),this.pdtClock.armLanding(t),C}reportCueEnd(e,t){const s=this.state.resumeAnchorIsCue?this.cueEndResolver?.(e):null;if(!s||!Number.isFinite(s.mediaTime))return null;const n=this.resumesAtBreakEnd(e),r=f(s.mediaTime-t);return this.deps.diagnose("DA-RESUME-CUE-END",n?`Break ${e.id}: resuming on the stream's cue_end at ${s.mediaTime}s (${r}s from the computed ${t}s)`:`Break ${e.id}: cue_end at ${s.mediaTime}s not used; the resume policy does not resume at the break end`,{breakId:e.id,computedTarget:t,cueEndTarget:s.mediaTime,deltaSec:r,applied:n,stage:"hand-back",corrected:!1}),n?s.mediaTime:null}applyResumeSeek(e,t=null){const s=this.resolveResumeTarget(e,t);null!==s&&(this.probeResumeLanding(e.id,s,this.resumePolicyTarget),this.streamClock.noteSeek(s),this.deps.contentPlayer.seek(s))}}
|
|
@@ -2,9 +2,12 @@ import type { PlayerAdapter, TimedMetadataCue } from '../../interfaces/PlayerAda
|
|
|
2
2
|
import type { Asset, Break } from '../../types/BreakManifest';
|
|
3
3
|
import type { AdPlayerCallbacks } from '../AdPlayerController';
|
|
4
4
|
import type { AdPlayerState } from './AdPlayerState';
|
|
5
|
+
import type { SurfaceRevealReporter } from './SurfaceRevealReporter';
|
|
5
6
|
export interface SharedElementBreakPlayerDeps {
|
|
6
7
|
contentPlayer: PlayerAdapter;
|
|
7
8
|
getAdPlayer: () => PlayerAdapter | null;
|
|
9
|
+
/** Watches for the ad's first frame on the content element (`DA-AD-SURFACE-REVEALED`). */
|
|
10
|
+
surfaceReveal?: SurfaceRevealReporter;
|
|
8
11
|
resolveAssetUri: (asset: Asset, duration: number) => Promise<string | null>;
|
|
9
12
|
liveEdgeDistanceSec: () => number | null;
|
|
10
13
|
rebindContentElementIfSwapped: () => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BreakFormat as e,isPostRollBreak as t}from"../../types/BreakManifest";import{armPlaybackCutoff as s,clearPlaybackCutoff as a}from"./PlaybackCutoff";import{AdError as n}from"../../types/AdError";import{classifyMediaError as r,toClassifiedAdError as i}from"./classifyMediaError";function d(e){return e?{src:e.getAttribute("src"),srcObject:e.srcObject,sourceChild:null!==e.querySelector("source")}:null}export class SharedElementBreakPlayer{constructor(e,t){this.state=e,this.deps=t,this.contentElementAdPlayer=null,this.sharedAdSource=null,this.sharedAdEndedHandler=null,this.sharedAdTimeUpdateHandler=null,this.sharedAdTimedMetadataHandler=null,this.sharedAdWaitingHandler=null,this.sharedAdPlayingHandler=null,this.sharedAdFirstFrameTimer=null,this.sharedRestoreWatchdog=null,this.sharedRestore=null,this.sharedRestoreGeneration=0,this.sharedActivityGeneration=0}sharedElementAdPlayer(){const e=this.deps.contentPlayer.videoElement;if(!e)return null;if(this.contentElementAdPlayer?.videoElement===e)return this.contentElementAdPlayer;this.releaseContentElementAdPlayer();const t=this.deps.createAdAdapter(document.createElement("div"),e);return t!==this.deps.contentPlayer&&t.prepareForAdPlayback?.(),t.videoElement!==e?(this.deps.log("shared-element: createAdAdapter ignored the content element; falling back to the content player for ad playback"),t.destroy(),null):(this.contentElementAdPlayer=t,t)}async sharedAdStep(e,t,s){try{await s()}catch(s){const a=t.videoElement,i=s instanceof Error?s.message:String(s),d=new n(`shared-element ${e} failed: ${i} (readyState=${a?.readyState??"-"} paused=${a?.paused??"-"} networkState=${a?.networkState??"-"})`,r(s));throw d.cause=s,d}}clearSharedAdFirstFrameWatchdog(){this.sharedAdFirstFrameTimer&&(clearTimeout(this.sharedAdFirstFrameTimer),this.sharedAdFirstFrameTimer=null)}armSharedAdFirstFrameWatchdog(e,t){this.clearSharedAdFirstFrameWatchdog(),this.sharedAdFirstFrameTimer=setTimeout(()=>{if(this.sharedAdFirstFrameTimer=null,!this.state.isPlaying||!this.state.currentBreak)return;const s=e.videoElement,a=this.deps.contentPlayer.videoElement,r=s?.currentSrc??"",i=`readyState=${s?.readyState??"-"} networkState=${s?.networkState??"-"} paused=${s?.paused??"-"} connected=${s?.isConnected??"-"} srcKind=${r.startsWith("blob:")?"blob":r?"url":"none"} sameElementAsContentPlayer=${String(s===a)}`;this.deps.log(`shared-element: no first frame within ${t}ms (${i})`),this.deps.diagnose("DA-AD-PLAYBACK-ERROR",`the ad never rendered a frame on the content element within ${t}ms`,{breakId:this.state.currentBreak.id,detail:i});const d=this.state.currentAsset;d&&this.state.callbacks?.onAdError(this.state.currentBreak,d,new n(`shared-element ad produced no frames within ${t}ms (${i})`,"DA-AD-START-TIMEOUT")),this.onSharedElementAdEnded()},t)}attachSharedAdListeners(e){this.sharedAdSource=e,this.sharedAdEndedHandler=()=>this.onSharedElementAdEnded(),e.on("ended",this.sharedAdEndedHandler),this.sharedAdTimeUpdateHandler=()=>{this.sharedAdFirstFrameTimer&&e.currentTime>0&&this.clearSharedAdFirstFrameWatchdog(),this.deps.reportAdProgress(e)},e.on("timeupdate",this.sharedAdTimeUpdateHandler),this.sharedAdTimedMetadataHandler=e=>{e&&this.deps.processTimedMetadata(e)},e.on("timedmetadata",this.sharedAdTimedMetadataHandler),this.sharedAdWaitingHandler=()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&this.state.callbacks?.onAdWaiting(this.state.currentBreak,this.state.currentAsset)},e.on("waiting",this.sharedAdWaitingHandler),this.sharedAdPlayingHandler=()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&!e.paused&&(this.clearSharedAdFirstFrameWatchdog(),this.state.startPlaybackCutoff?.(),this.state.callbacks?.onAdPlaying(this.state.currentBreak,this.state.currentAsset))},e.on("playing",this.sharedAdPlayingHandler)}detachSharedAdListeners(){const e=this.sharedAdSource??this.deps.contentPlayer;this.sharedAdSource=null,this.state.sharedAdClaimActive=!1,this.clearSharedAdFirstFrameWatchdog(),this.sharedAdTimedMetadataHandler&&(e.off("timedmetadata",this.sharedAdTimedMetadataHandler),this.sharedAdTimedMetadataHandler=null),this.sharedAdWaitingHandler&&(e.off("waiting",this.sharedAdWaitingHandler),this.sharedAdWaitingHandler=null),this.sharedAdPlayingHandler&&(e.off("playing",this.sharedAdPlayingHandler),this.sharedAdPlayingHandler=null),this.sharedAdTimeUpdateHandler&&(e.off("timeupdate",this.sharedAdTimeUpdateHandler),this.sharedAdTimeUpdateHandler=null),this.sharedAdEndedHandler&&(e.off("ended",this.sharedAdEndedHandler),this.sharedAdEndedHandler=null)}async releaseContentElementFromContentEngine(){if(this.deps.contentPlayer.releaseMediaElement)try{await this.deps.contentPlayer.releaseMediaElement()}catch(e){this.deps.log("shared-element: the content engine did not release the element:",e)}}releaseContentElementAdPlayer(){const e=this.contentElementAdPlayer;if(e){this.contentElementAdPlayer=null;try{e.destroy()}catch(e){this.deps.log("shared-element: releasing the ad player on the content element failed:",e)}}}async playAdThroughContentElement(){const e=this.state.currentAsset,t=this.state.currentBreak;if(!e||!t)return;const s=this.state.effectiveBreakDurationSec??t.duration,a=await this.deps.resolveAssetUri(e,s);if(!a)return void this.deps.log("shared-element: no URI for the running ad; leaving it on the ad element");const n=this.deps.getAdPlayer()?.currentTime??0;this.state.savedContentSrc=this.restorableContentSource(),this.state.savedContentTime=this.deps.contentPlayer.currentTime,this.state.savedContentLive=!Number.isFinite(this.deps.contentPlayer.duration),this.state.savedContentEdgeDistance=this.state.savedContentLive?this.deps.liveEdgeDistanceSec():null,this.state.sharedAdClaimActive=!0,await this.releaseContentElementFromContentEngine(),this.deps.rebindContentElementIfSwapped();const r=this.sharedElementAdPlayer()??this.deps.contentPlayer;try{this.deps.getAdPlayer()?.pause(),this.deps.clearContentGate(),await r.load(a),this.attachSharedAdListeners(r),n>0&&r.seek(n),await r.play(),this.deps.hide(),this.deps.log("shared-element: the ad now plays through the content element")}catch(e){this.deps.log("shared-element: could not move the ad onto the content element:",e),this.state.sharedAdClaimActive=!1,this.state.savedContentSrc=null,this.state.savedContentEdgeDistance=null}}async playSharedElementBreak(t,a){const r=this.deps.getVariant(t);if(!r)return this.deps.log("shared-element: no valid variant, skipping break:",t.id),void a.onBreakEnd(t);if(r.format===e.LSHAPE_CONTENT)return this.deps.log("shared-element: skipping lshape_content break (no fullscreen video asset):",t.id),void a.onBreakEnd(t);const d=this.deps.getAssets(t)[0];if(!d)return this.deps.log("shared-element: no asset to play, skipping break:",t.id),void a.onBreakEnd(t);r.format!==e.SINGLE&&this.deps.log(`shared-element: downgrading ${r.format} to a single fullscreen ad:`,t.id),this.state.currentBreak=t,this.state.currentAsset=d,this.state.callbacks=a,this.state.isPlaying=!0;const h=this.state.effectiveBreakDurationSec??t.duration,l=await this.deps.resolveAssetUri(d,h);if(!l)return this.deps.log("shared-element: no valid URI, skipping break:",t.id),this.state.isPlaying=!1,this.state.currentBreak=null,this.state.currentAsset=null,void a.onBreakEnd(t);this.state.savedContentSrc=this.restorableContentSource(),this.state.savedContentTime=this.deps.contentPlayer.currentTime,this.state.savedContentLive=!Number.isFinite(this.deps.contentPlayer.duration),this.state.savedContentEdgeDistance=this.state.savedContentLive?this.deps.liveEdgeDistanceSec():null,this.state.sharedAdClaimActive=!0,await this.releaseContentElementFromContentEngine(),this.deps.rebindContentElementIfSwapped();const o=this.sharedElementAdPlayer()??this.deps.contentPlayer;if(h&&h>0){const e=h+this.state.adBreakCutSafetyMarginSec;s(this.state,e,()=>{this.deps.log("shared-element: break-cut timer expired, ending break"),this.state.isPlaying&&this.state.currentBreak&&this.state.callbacks?.onBreakCut?.(this.state.currentBreak),this.onSharedElementAdEnded()},()=>{this.state.callbacks?.onAdError(t,d,new n("Ad playback startup timed out","DA-AD-START-TIMEOUT")),this.onSharedElementAdEnded()})}try{if(this.deps.clearContentGate(),await this.sharedAdStep("load",o,()=>o.load(l)),!this.state.isPlaying||this.state.currentBreak!==t)return;if(this.attachSharedAdListeners(o),this.armSharedAdFirstFrameWatchdog(o,Math.max(Math.min(1e3*(h??0)-100,5e3),3e3)),await this.sharedAdStep("play",o,()=>o.play()),!this.state.isPlaying||this.state.currentBreak!==t)return;this.state.callbacks?.onAdStart(t,d,0,1)}catch(e){if(!this.state.isPlaying||this.state.currentBreak!==t)return;this.deps.log("shared-element: ad playback failed:",e),this.state.callbacks?.onAdError(t,d,i(e)),this.onSharedElementAdEnded()}}onSharedElementAdEnded(){if(!this.state.isPlaying||!this.state.currentBreak)return;const e=this.state.currentBreak,t=this.state.currentAsset,s=this.state.callbacks,n=this.sharedRestoreGeneration;this.detachSharedAdListeners(),a(this.state),this.state.isPlaying=!1,this.state.currentBreak=null,this.state.currentAsset=null,t&&s?.onAdEnd(e,t,0,1),n===this.sharedRestoreGeneration&&this.restoreSharedElementContent(e),s?.onBreakEnd(e)}restoreSharedElementContent(e){const s=this.state.savedContentSrc;if(!s){if(this.sharedRestore)return;return this.deps.log("shared-element: no saved content source to restore"),void this.releaseContentElementAdPlayer()}if(!this.beginSharedActivity())return;const a=this.state.savedContentTime,n=this.state.savedContentLive,r=this.state.savedContentEdgeDistance,i=this.state.callbacks;this.state.savedContentSrc=null,this.state.savedContentEdgeDistance=null;const d={src:s,time:a,live:n,edgeDistance:r,breakInfo:e,postRoll:!!e&&t(e),onContentReloading:i?.onContentReloading?.bind(i),adPlayer:this.contentElementAdPlayer,reloading:!1,cleanedUp:!1,cancelled:!1,finishingActivity:null,attempt:null};this.sharedRestore=d,e&&"number"==typeof e.start&&i?.onContentResumeSeek?.(n?this.deps.contentPlayer.currentTime:a,e.start+e.duration),this.sharedRestore===d&&(d.reloading=!0,d.onContentReloading?.(!0),this.sharedRestore===d&&this.runSharedRestore(d))}async runSharedRestore(e){for(let t=1;t<=2;t++){const s={phase:"load",deadline:0,timer:null,settle:null};e.attempt=s;const a=await new Promise(t=>{s.settle=e=>{s.settle&&(s.settle=null,s.timer&&clearTimeout(s.timer),s.timer=null,t(e))},this.armSharedRestoreDeadline(s,1e4),this.reloadAndResume(e,s).then(()=>s.settle?.("success"),()=>s.settle?.("rejection"))});if(this.sharedRestore!==e||e.attempt!==s)return;if("success"===a||"cancelled"===a)break;if(2===t?this.deps.diagnose("DA-SHARED-ELEMENT-RESTORE-FAILED",`Content did not resume after the shared-element break: ${s.phase} ${a}`,{breakId:e.breakInfo?.id,live:e.live,attempts:t,phase:s.phase,reason:a}):this.deps.log(`shared-element: content restore ${s.phase} ${a}, retrying once`),this.sharedRestore!==e||e.attempt!==s)return}this.finishSharedRestore(e,!0)}armSharedRestoreDeadline(e,t){e.timer&&clearTimeout(e.timer),e.deadline=performance.now()+t,e.timer=setTimeout(()=>e.settle?.("timeout"),t)}ownsSharedRestoreAttempt(e,t){return!(this.sharedRestore!==e||e.attempt!==t||!t.settle)&&(performance.now()<t.deadline||(t.settle("timeout"),!1))}async reloadAndResume(e,t){const{src:s,time:a,live:n,edgeDistance:r,postRoll:i}=e,h=()=>this.ownsSharedRestoreAttempt(e,t);if(!h())return;if(e.adPlayer?.releaseMediaElement&&(await this.releaseAdEngineGrip(e),!h()))return;const l=this.deps.contentPlayer.videoElement??null,o=d(l);if(await this.deps.contentPlayer.load(s),h()){if(this.armSharedRestoreDeadline(t,5e3),n){if(!i&&null!==r&&r>0){t.phase="live-edge";const e=await this.deps.waitForLiveEdge(h);if(!h())return;if(null!==e){const s=Math.max(e-r,0);if(this.deps.log(`shared-element: live resume ${r.toFixed(2)}s behind the edge (${s.toFixed(2)}s)`),!h())return;t.phase="seek",this.deps.contentPlayer.seek(s)}}}else t.phase="seek",this.deps.contentPlayer.seek(a);h()&&!i&&(t.phase="play",await this.deps.contentPlayer.play(),h()&&await this.waitForContentEngineToTakeElement(l,o,h))}}async releaseAdEngineGrip(e){const t=e.adPlayer;if(t?.releaseMediaElement)try{await t.releaseMediaElement()}catch(e){this.deps.log("shared-element: the ad engine did not release the element:",e)}}async waitForContentEngineToTakeElement(e,t,s){if(!(e&&t&&(a=t,a.src||a.srcObject||a.sourceChild)))return;var a;const n=()=>{if(this.deps.contentPlayer.videoElement!==e)return!0;const s=d(e);if(!s)return!1;const a=!!s.src&&s.src!==t.src,n=!!s.srcObject&&s.srcObject!==t.srcObject;return a||n},r=performance.now();for(;!n()&&s()&&performance.now()-r<1500;)await new Promise(e=>setTimeout(e,25));if(!s())return;const i=Math.round(performance.now()-r);this.deps.log(`shared-element: content engine ${n()?"took":"did not take"} the element after ${i}ms`)}finishSharedRestore(e,t){this.sharedRestore===e&&(null===e.finishingActivity?(e.finishingActivity=this.sharedActivityGeneration,e.cleanedUp||(this.state.contentPaused=!1,this.contentElementAdPlayer===e.adPlayer&&this.releaseContentElementAdPlayer(),this.sharedRestore===e&&this.deps.clearContentGate(),this.sharedRestore===e&&this.deps.rebindContentElementIfSwapped(),e.cleanedUp=!0),this.endSharedRestoreReloading(e),e.finishingActivity=null,this.sharedRestore===e&&(!t||e.cancelled||e.postRoll?this.sharedRestore=null:this.armSharedRestoreWatchdog(e))):e.finishingActivity!==this.sharedActivityGeneration&&(this.endSharedRestoreReloading(e),this.sharedRestore===e&&(this.sharedRestore=null)))}endSharedRestoreReloading(e){e.reloading&&(e.reloading=!1,e.onContentReloading?.(!1))}armSharedRestoreWatchdog(e){const t=this.deps.contentPlayer.currentTime;this.sharedRestoreWatchdog=setTimeout(()=>{if(this.sharedRestore!==e)return;this.sharedRestoreWatchdog=null;const s=this.deps.contentPlayer.currentTime-t;(this.deps.contentPlayer.paused||s<1)&&(this.deps.diagnose("DA-CONTENT-RESUME-STALLED",`Content did not resume within 5s of the shared-element restore (paused: ${this.deps.contentPlayer.paused}, advanced: ${s.toFixed(1)}s); nudging play()`,{breakId:e.breakInfo?.id??"?",paused:this.deps.contentPlayer.paused,advancedSec:s}),this.sharedRestore===e&&this.deps.contentPlayer.play().catch(()=>{this.sharedRestore===e&&this.deps.log("shared-element: watchdog play() nudge failed")}))},5e3)}beginSharedActivity(){return this.sharedActivityGeneration++,this.cancelSharedRestore()}cancelSharedRestore(){const e=this.sharedActivityGeneration;this.sharedRestoreGeneration++,this.sharedRestoreWatchdog&&(clearTimeout(this.sharedRestoreWatchdog),this.sharedRestoreWatchdog=null);const t=this.sharedRestore;return t&&(t.cancelled=!0,t.attempt?.settle?.("cancelled"),this.finishSharedRestore(t,!1)),e===this.sharedActivityGeneration}restorableContentSource(){const e=this.deps.contentPlayer.sourceUrl;if(e&&!e.startsWith("blob:"))return e;const t=this.deps.contentPlayer.videoElement,s=t?.currentSrc||t?.src||"";return s.length>0&&!s.startsWith("blob:")?s:null}}
|
|
1
|
+
import{BreakFormat as e,isPostRollBreak as t}from"../../types/BreakManifest";import{armPlaybackCutoff as s,clearPlaybackCutoff as a}from"./PlaybackCutoff";import{AdError as n}from"../../types/AdError";import{classifyMediaError as r,toClassifiedAdError as i}from"./classifyMediaError";function d(e){return e?{src:e.getAttribute("src"),srcObject:e.srcObject,sourceChild:null!==e.querySelector("source")}:null}export class SharedElementBreakPlayer{constructor(e,t){this.state=e,this.deps=t,this.contentElementAdPlayer=null,this.sharedAdSource=null,this.sharedAdEndedHandler=null,this.sharedAdTimeUpdateHandler=null,this.sharedAdTimedMetadataHandler=null,this.sharedAdWaitingHandler=null,this.sharedAdPlayingHandler=null,this.sharedAdFirstFrameTimer=null,this.sharedRestoreWatchdog=null,this.sharedRestore=null,this.sharedRestoreGeneration=0,this.sharedActivityGeneration=0}sharedElementAdPlayer(){const e=this.deps.contentPlayer.videoElement;if(!e)return null;if(this.contentElementAdPlayer?.videoElement===e)return this.contentElementAdPlayer;this.releaseContentElementAdPlayer();const t=this.deps.createAdAdapter(document.createElement("div"),e);return t!==this.deps.contentPlayer&&t.prepareForAdPlayback?.(),t.videoElement!==e?(this.deps.log("shared-element: createAdAdapter ignored the content element; falling back to the content player for ad playback"),t.destroy(),null):(this.contentElementAdPlayer=t,t)}async sharedAdStep(e,t,s){try{await s()}catch(s){const a=t.videoElement,i=s instanceof Error?s.message:String(s),d=new n(`shared-element ${e} failed: ${i} (readyState=${a?.readyState??"-"} paused=${a?.paused??"-"} networkState=${a?.networkState??"-"})`,r(s));throw d.cause=s,d}}clearSharedAdFirstFrameWatchdog(){this.sharedAdFirstFrameTimer&&(clearTimeout(this.sharedAdFirstFrameTimer),this.sharedAdFirstFrameTimer=null)}armSharedAdFirstFrameWatchdog(e,t){this.clearSharedAdFirstFrameWatchdog(),this.sharedAdFirstFrameTimer=setTimeout(()=>{if(this.sharedAdFirstFrameTimer=null,!this.state.isPlaying||!this.state.currentBreak)return;const s=e.videoElement,a=this.deps.contentPlayer.videoElement,r=s?.currentSrc??"",i=`readyState=${s?.readyState??"-"} networkState=${s?.networkState??"-"} paused=${s?.paused??"-"} connected=${s?.isConnected??"-"} srcKind=${r.startsWith("blob:")?"blob":r?"url":"none"} sameElementAsContentPlayer=${String(s===a)}`;this.deps.log(`shared-element: no first frame within ${t}ms (${i})`),this.deps.diagnose("DA-AD-PLAYBACK-ERROR",`the ad never rendered a frame on the content element within ${t}ms`,{breakId:this.state.currentBreak.id,detail:i});const d=this.state.currentAsset;d&&this.state.callbacks?.onAdError(this.state.currentBreak,d,new n(`shared-element ad produced no frames within ${t}ms (${i})`,"DA-AD-START-TIMEOUT")),this.onSharedElementAdEnded()},t)}attachSharedAdListeners(e){this.sharedAdSource=e,this.sharedAdEndedHandler=()=>this.onSharedElementAdEnded(),e.on("ended",this.sharedAdEndedHandler),this.sharedAdTimeUpdateHandler=()=>{this.sharedAdFirstFrameTimer&&e.currentTime>0&&this.clearSharedAdFirstFrameWatchdog(),this.deps.reportAdProgress(e)},e.on("timeupdate",this.sharedAdTimeUpdateHandler),this.sharedAdTimedMetadataHandler=e=>{e&&this.deps.processTimedMetadata(e)},e.on("timedmetadata",this.sharedAdTimedMetadataHandler),this.sharedAdWaitingHandler=()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&this.state.callbacks?.onAdWaiting(this.state.currentBreak,this.state.currentAsset)},e.on("waiting",this.sharedAdWaitingHandler),this.sharedAdPlayingHandler=()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&!e.paused&&(this.clearSharedAdFirstFrameWatchdog(),this.state.startPlaybackCutoff?.(),this.state.callbacks?.onAdPlaying(this.state.currentBreak,this.state.currentAsset))},e.on("playing",this.sharedAdPlayingHandler)}detachSharedAdListeners(){const e=this.sharedAdSource??this.deps.contentPlayer;this.sharedAdSource=null,this.state.sharedAdClaimActive=!1,this.clearSharedAdFirstFrameWatchdog(),this.sharedAdTimedMetadataHandler&&(e.off("timedmetadata",this.sharedAdTimedMetadataHandler),this.sharedAdTimedMetadataHandler=null),this.sharedAdWaitingHandler&&(e.off("waiting",this.sharedAdWaitingHandler),this.sharedAdWaitingHandler=null),this.sharedAdPlayingHandler&&(e.off("playing",this.sharedAdPlayingHandler),this.sharedAdPlayingHandler=null),this.sharedAdTimeUpdateHandler&&(e.off("timeupdate",this.sharedAdTimeUpdateHandler),this.sharedAdTimeUpdateHandler=null),this.sharedAdEndedHandler&&(e.off("ended",this.sharedAdEndedHandler),this.sharedAdEndedHandler=null)}async releaseContentElementFromContentEngine(){if(this.deps.contentPlayer.releaseMediaElement)try{await this.deps.contentPlayer.releaseMediaElement()}catch(e){this.deps.log("shared-element: the content engine did not release the element:",e)}}releaseContentElementAdPlayer(){const e=this.contentElementAdPlayer;if(e){this.contentElementAdPlayer=null;try{e.destroy()}catch(e){this.deps.log("shared-element: releasing the ad player on the content element failed:",e)}}}async playAdThroughContentElement(){const e=this.state.currentAsset,t=this.state.currentBreak;if(!e||!t)return;const s=this.state.effectiveBreakDurationSec??t.duration,a=await this.deps.resolveAssetUri(e,s);if(!a)return void this.deps.log("shared-element: no URI for the running ad; leaving it on the ad element");const n=this.deps.getAdPlayer()?.currentTime??0;this.state.savedContentSrc=this.restorableContentSource(),this.state.savedContentTime=this.deps.contentPlayer.currentTime,this.state.savedContentLive=!Number.isFinite(this.deps.contentPlayer.duration),this.state.savedContentEdgeDistance=this.state.savedContentLive?this.deps.liveEdgeDistanceSec():null,this.state.sharedAdClaimActive=!0,await this.releaseContentElementFromContentEngine(),this.deps.rebindContentElementIfSwapped();const r=this.sharedElementAdPlayer()??this.deps.contentPlayer;try{this.deps.getAdPlayer()?.pause(),this.deps.clearContentGate(),await r.load(a),this.attachSharedAdListeners(r),n>0&&r.seek(n),await r.play(),this.deps.hide(),this.deps.log("shared-element: the ad now plays through the content element")}catch(e){this.deps.log("shared-element: could not move the ad onto the content element:",e),this.state.sharedAdClaimActive=!1,this.state.savedContentSrc=null,this.state.savedContentEdgeDistance=null}}async playSharedElementBreak(t,a){const r=this.deps.getVariant(t);if(!r)return this.deps.log("shared-element: no valid variant, skipping break:",t.id),void a.onBreakEnd(t);if(r.format===e.LSHAPE_CONTENT)return this.deps.log("shared-element: skipping lshape_content break (no fullscreen video asset):",t.id),void a.onBreakEnd(t);const d=this.deps.getAssets(t)[0];if(!d)return this.deps.log("shared-element: no asset to play, skipping break:",t.id),void a.onBreakEnd(t);r.format!==e.SINGLE&&this.deps.log(`shared-element: downgrading ${r.format} to a single fullscreen ad:`,t.id),this.state.currentBreak=t,this.state.currentAsset=d,this.state.callbacks=a,this.state.isPlaying=!0;const h=this.state.effectiveBreakDurationSec??t.duration,l=await this.deps.resolveAssetUri(d,h);if(!l)return this.deps.log("shared-element: no valid URI, skipping break:",t.id),this.state.isPlaying=!1,this.state.currentBreak=null,this.state.currentAsset=null,void a.onBreakEnd(t);this.state.savedContentSrc=this.restorableContentSource(),this.state.savedContentTime=this.deps.contentPlayer.currentTime,this.state.savedContentLive=!Number.isFinite(this.deps.contentPlayer.duration),this.state.savedContentEdgeDistance=this.state.savedContentLive?this.deps.liveEdgeDistanceSec():null,this.state.sharedAdClaimActive=!0,await this.releaseContentElementFromContentEngine(),this.deps.rebindContentElementIfSwapped();const o=this.sharedElementAdPlayer()??this.deps.contentPlayer;if(h&&h>0){const e=h+this.state.adBreakCutSafetyMarginSec;s(this.state,e,()=>{this.deps.log("shared-element: break-cut timer expired, ending break"),this.state.isPlaying&&this.state.currentBreak&&this.state.callbacks?.onBreakCut?.(this.state.currentBreak),this.onSharedElementAdEnded()},()=>{this.state.callbacks?.onAdError(t,d,new n("Ad playback startup timed out","DA-AD-START-TIMEOUT")),this.onSharedElementAdEnded()})}try{if(this.deps.clearContentGate(),await this.sharedAdStep("load",o,()=>o.load(l)),!this.state.isPlaying||this.state.currentBreak!==t)return;if(this.attachSharedAdListeners(o),this.armSharedAdFirstFrameWatchdog(o,Math.max(Math.min(1e3*(h??0)-100,5e3),3e3)),this.deps.surfaceReveal?.surfaceShown(),this.deps.surfaceReveal?.watch(d.id,t.id,o.videoElement),await this.sharedAdStep("play",o,()=>o.play()),!this.state.isPlaying||this.state.currentBreak!==t)return;this.state.callbacks?.onAdStart(t,d,0,1)}catch(e){if(!this.state.isPlaying||this.state.currentBreak!==t)return;this.deps.log("shared-element: ad playback failed:",e),this.state.callbacks?.onAdError(t,d,i(e)),this.onSharedElementAdEnded()}}onSharedElementAdEnded(){if(!this.state.isPlaying||!this.state.currentBreak)return;const e=this.state.currentBreak,t=this.state.currentAsset,s=this.state.callbacks,n=this.sharedRestoreGeneration;this.detachSharedAdListeners(),a(this.state),this.state.isPlaying=!1,this.state.currentBreak=null,this.state.currentAsset=null,t&&s?.onAdEnd(e,t,0,1),n===this.sharedRestoreGeneration&&this.restoreSharedElementContent(e),this.deps.surfaceReveal?.surfaceHidden(),s?.onBreakEnd(e)}restoreSharedElementContent(e){const s=this.state.savedContentSrc;if(!s){if(this.sharedRestore)return;return this.deps.log("shared-element: no saved content source to restore"),void this.releaseContentElementAdPlayer()}if(!this.beginSharedActivity())return;const a=this.state.savedContentTime,n=this.state.savedContentLive,r=this.state.savedContentEdgeDistance,i=this.state.callbacks;this.state.savedContentSrc=null,this.state.savedContentEdgeDistance=null;const d={src:s,time:a,live:n,edgeDistance:r,breakInfo:e,postRoll:!!e&&t(e),onContentReloading:i?.onContentReloading?.bind(i),adPlayer:this.contentElementAdPlayer,reloading:!1,cleanedUp:!1,cancelled:!1,finishingActivity:null,attempt:null};this.sharedRestore=d,e&&"number"==typeof e.start&&i?.onContentResumeSeek?.(n?this.deps.contentPlayer.currentTime:a,e.start+e.duration),this.sharedRestore===d&&(d.reloading=!0,d.onContentReloading?.(!0),this.sharedRestore===d&&this.runSharedRestore(d))}async runSharedRestore(e){for(let t=1;t<=2;t++){const s={phase:"load",deadline:0,timer:null,settle:null};e.attempt=s;const a=await new Promise(t=>{s.settle=e=>{s.settle&&(s.settle=null,s.timer&&clearTimeout(s.timer),s.timer=null,t(e))},this.armSharedRestoreDeadline(s,1e4),this.reloadAndResume(e,s).then(()=>s.settle?.("success"),()=>s.settle?.("rejection"))});if(this.sharedRestore!==e||e.attempt!==s)return;if("success"===a||"cancelled"===a)break;if(2===t?this.deps.diagnose("DA-SHARED-ELEMENT-RESTORE-FAILED",`Content did not resume after the shared-element break: ${s.phase} ${a}`,{breakId:e.breakInfo?.id,live:e.live,attempts:t,phase:s.phase,reason:a}):this.deps.log(`shared-element: content restore ${s.phase} ${a}, retrying once`),this.sharedRestore!==e||e.attempt!==s)return}this.finishSharedRestore(e,!0)}armSharedRestoreDeadline(e,t){e.timer&&clearTimeout(e.timer),e.deadline=performance.now()+t,e.timer=setTimeout(()=>e.settle?.("timeout"),t)}ownsSharedRestoreAttempt(e,t){return!(this.sharedRestore!==e||e.attempt!==t||!t.settle)&&(performance.now()<t.deadline||(t.settle("timeout"),!1))}async reloadAndResume(e,t){const{src:s,time:a,live:n,edgeDistance:r,postRoll:i}=e,h=()=>this.ownsSharedRestoreAttempt(e,t);if(!h())return;if(e.adPlayer?.releaseMediaElement&&(await this.releaseAdEngineGrip(e),!h()))return;const l=this.deps.contentPlayer.videoElement??null,o=d(l);if(await this.deps.contentPlayer.load(s),h()){if(this.armSharedRestoreDeadline(t,5e3),n){if(!i&&null!==r&&r>0){t.phase="live-edge";const e=await this.deps.waitForLiveEdge(h);if(!h())return;if(null!==e){const s=Math.max(e-r,0);if(this.deps.log(`shared-element: live resume ${r.toFixed(2)}s behind the edge (${s.toFixed(2)}s)`),!h())return;t.phase="seek",this.deps.contentPlayer.seek(s)}}}else t.phase="seek",this.deps.contentPlayer.seek(a);h()&&!i&&(t.phase="play",await this.deps.contentPlayer.play(),h()&&await this.waitForContentEngineToTakeElement(l,o,h))}}async releaseAdEngineGrip(e){const t=e.adPlayer;if(t?.releaseMediaElement)try{await t.releaseMediaElement()}catch(e){this.deps.log("shared-element: the ad engine did not release the element:",e)}}async waitForContentEngineToTakeElement(e,t,s){if(!(e&&t&&(a=t,a.src||a.srcObject||a.sourceChild)))return;var a;const n=()=>{if(this.deps.contentPlayer.videoElement!==e)return!0;const s=d(e);if(!s)return!1;const a=!!s.src&&s.src!==t.src,n=!!s.srcObject&&s.srcObject!==t.srcObject;return a||n},r=performance.now();for(;!n()&&s()&&performance.now()-r<1500;)await new Promise(e=>setTimeout(e,25));if(!s())return;const i=Math.round(performance.now()-r);this.deps.log(`shared-element: content engine ${n()?"took":"did not take"} the element after ${i}ms`)}finishSharedRestore(e,t){this.sharedRestore===e&&(null===e.finishingActivity?(e.finishingActivity=this.sharedActivityGeneration,e.cleanedUp||(this.state.contentPaused=!1,this.contentElementAdPlayer===e.adPlayer&&this.releaseContentElementAdPlayer(),this.sharedRestore===e&&this.deps.clearContentGate(),this.sharedRestore===e&&this.deps.rebindContentElementIfSwapped(),e.cleanedUp=!0),this.endSharedRestoreReloading(e),e.finishingActivity=null,this.sharedRestore===e&&(!t||e.cancelled||e.postRoll?this.sharedRestore=null:this.armSharedRestoreWatchdog(e))):e.finishingActivity!==this.sharedActivityGeneration&&(this.endSharedRestoreReloading(e),this.sharedRestore===e&&(this.sharedRestore=null)))}endSharedRestoreReloading(e){e.reloading&&(e.reloading=!1,e.onContentReloading?.(!1))}armSharedRestoreWatchdog(e){const t=this.deps.contentPlayer.currentTime;this.sharedRestoreWatchdog=setTimeout(()=>{if(this.sharedRestore!==e)return;this.sharedRestoreWatchdog=null;const s=this.deps.contentPlayer.currentTime-t;(this.deps.contentPlayer.paused||s<1)&&(this.deps.diagnose("DA-CONTENT-RESUME-STALLED",`Content did not resume within 5s of the shared-element restore (paused: ${this.deps.contentPlayer.paused}, advanced: ${s.toFixed(1)}s); nudging play()`,{breakId:e.breakInfo?.id??"?",paused:this.deps.contentPlayer.paused,advancedSec:s}),this.sharedRestore===e&&this.deps.contentPlayer.play().catch(()=>{this.sharedRestore===e&&this.deps.log("shared-element: watchdog play() nudge failed")}))},5e3)}beginSharedActivity(){return this.sharedActivityGeneration++,this.cancelSharedRestore()}cancelSharedRestore(){const e=this.sharedActivityGeneration;this.sharedRestoreGeneration++,this.sharedRestoreWatchdog&&(clearTimeout(this.sharedRestoreWatchdog),this.sharedRestoreWatchdog=null);const t=this.sharedRestore;return t&&(t.cancelled=!0,t.attempt?.settle?.("cancelled"),this.finishSharedRestore(t,!1)),e===this.sharedActivityGeneration}restorableContentSource(){const e=this.deps.contentPlayer.sourceUrl;if(e&&!e.startsWith("blob:"))return e;const t=this.deps.contentPlayer.videoElement,s=t?.currentSrc||t?.src||"";return s.length>0&&!s.startsWith("blob:")?s:null}}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Native's ad-start deadline: an ad with no picture by then is reported as `reveal-timeout`. */
|
|
2
|
+
export declare const SURFACE_REVEAL_DEADLINE_MS = 10000;
|
|
3
|
+
type Diagnose = (code: string, message: string, context: Record<string, unknown>) => void;
|
|
4
|
+
/**
|
|
5
|
+
* Reports `DA-AD-SURFACE-REVEALED` once per ad when its first frame is on screen, with the same
|
|
6
|
+
* fields as Android and iOS. Web shows the surface before the picture unless a pre-roll is held,
|
|
7
|
+
* so that gap is a distinct reason (`shown-before-frame`, `visibleGapMs`) and never `waitedMs`.
|
|
8
|
+
*/
|
|
9
|
+
export declare class SurfaceRevealReporter {
|
|
10
|
+
private readonly diagnose;
|
|
11
|
+
private readonly deadlineMs;
|
|
12
|
+
private readonly now;
|
|
13
|
+
private visibleSince;
|
|
14
|
+
private pending;
|
|
15
|
+
constructor(diagnose: Diagnose, deadlineMs?: number, now?: () => number);
|
|
16
|
+
/** The surface carrying the ad is on screen from now on. */
|
|
17
|
+
surfaceShown(): void;
|
|
18
|
+
surfaceHidden(): void;
|
|
19
|
+
/** An ad starts; `video` draws it, or is passed later through `watchVideo` (IMA's own element). */
|
|
20
|
+
watch(assetId: string, breakId: string, video?: HTMLVideoElement | null): void;
|
|
21
|
+
/** Chrome, Edge and Firefox stop frame callbacks in a hidden tab, so the deadline only runs while it is visible. */
|
|
22
|
+
private armDeadline;
|
|
23
|
+
watchVideo(video: HTMLVideoElement): void;
|
|
24
|
+
cancel(): void;
|
|
25
|
+
/** The surface may be shown after the ad started (a held pre-roll), so hidden and visible time split here. */
|
|
26
|
+
private report;
|
|
27
|
+
}
|
|
28
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const SURFACE_REVEAL_DEADLINE_MS=1e4;export class SurfaceRevealReporter{constructor(e,t=1e4,i=()=>performance.now()){this.diagnose=e,this.deadlineMs=t,this.now=i,this.visibleSince=null,this.pending=null}surfaceShown(){this.visibleSince??(this.visibleSince=this.now())}surfaceHidden(){this.visibleSince=null,this.cancel()}watch(e,t,i){this.cancel();const n={assetId:e,breakId:t,armedAt:this.now(),shownAtStart:null!==this.visibleSince,stops:[]};this.pending=n,n.stops.push(this.armDeadline(n)),i&&this.watchVideo(i)}armDeadline(e){let t=this.deadlineMs,i=0,n=null;const s=()=>{i=this.now(),n=setTimeout(()=>this.report(e,!0),t)},a=()=>{null!==n&&(clearTimeout(n),n=null,t=Math.max(0,t-(this.now()-i)))},r=()=>{"hidden"===document.visibilityState?a():null===n&&s()};return"hidden"!==document.visibilityState&&s(),document.addEventListener("visibilitychange",r),()=>{a(),document.removeEventListener("visibilitychange",r)}}watchVideo(e){const t=this.pending;t&&t.stops.push(function(e,t){if("function"==typeof e.requestVideoFrameCallback){const i=e.requestVideoFrameCallback(()=>t());return()=>e.cancelVideoFrameCallback?.(i)}const i=()=>{e.currentTime<=0||(e.removeEventListener("timeupdate",i),t())};return e.addEventListener("timeupdate",i),()=>e.removeEventListener("timeupdate",i)}(e,()=>this.report(t,!1)))}cancel(){this.pending?.stops.forEach(e=>e()),this.pending=null}report(e,t){if(this.pending!==e)return;this.cancel();const i=this.now(),n=this.visibleSince,s=t?"reveal-timeout":e.shownAtStart?"shown-before-frame":"first-frame",a=Math.round(Math.max(0,(n??i)-e.armedAt)),r={reason:s,waitedMs:a,transitionMs:0,cut:!0,assetId:e.assetId,breakId:e.breakId};let o=`Ad surface revealed after ${a}ms (${s})`;if(null!==n){const t=Math.round(Math.max(0,i-Math.max(e.armedAt,n)));r.visibleGapMs=t,o+=`, ${t}ms on screen without a picture`}this.diagnose("DA-AD-SURFACE-REVEALED",o,r)}}
|
|
@@ -25,7 +25,9 @@ export interface VendorConfigurationBase {
|
|
|
25
25
|
}
|
|
26
26
|
/** Session-level GAM configuration for pod-serving SGAI. @category Break Manifests & Ad Formats */
|
|
27
27
|
export interface GamSgaiConfiguration {
|
|
28
|
+
/** The custom asset key of the channel's pod-serving stream. */
|
|
28
29
|
customAssetKey: string;
|
|
30
|
+
/** Session-level ad tag parameters for pod requests. */
|
|
29
31
|
assetParameters?: Record<string, string>;
|
|
30
32
|
}
|
|
31
33
|
/**
|
|
@@ -44,7 +46,9 @@ export interface GamSsbConfiguration {
|
|
|
44
46
|
}
|
|
45
47
|
/** Session-level GAM configuration for full-service DAI SSAI. @category Break Manifests & Ad Formats */
|
|
46
48
|
export interface GamSsaiConfiguration {
|
|
49
|
+
/** The asset key of the full-service DAI livestream. */
|
|
47
50
|
assetKey: string;
|
|
51
|
+
/** Session-level ad tag parameters for the stitched stream request. */
|
|
48
52
|
assetParameters?: Record<string, string>;
|
|
49
53
|
/** The channel's server-side beaconing stream, when it has one. */
|
|
50
54
|
ssb?: GamSsbConfiguration;
|
|
@@ -70,9 +74,15 @@ export interface GamVendorConfiguration extends VendorConfigurationBase {
|
|
|
70
74
|
sgai?: GamSgaiConfiguration[];
|
|
71
75
|
ssai?: GamSsaiConfiguration[];
|
|
72
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Ad parameters for a provider outside the SDK, read by the application with
|
|
79
|
+
* `getExternalVendorConfiguration()` or {@link OptiViewAdsExternalVendors}.
|
|
80
|
+
* @category Break Manifests & Ad Formats */
|
|
73
81
|
export interface ExternalVendorConfigurationEntry extends VendorConfigurationBase {
|
|
74
82
|
type: 'external';
|
|
83
|
+
/** The provider the parameters are for; the first valid entry per provider is used. */
|
|
75
84
|
provider: string;
|
|
85
|
+
/** The provider's parameter template; values may contain `$NAME$` macros. */
|
|
76
86
|
assetParameters: Record<string, string>;
|
|
77
87
|
}
|
|
78
88
|
/** Known vendor configuration entries, kept open to unrecognized vendors from newer manifests. @category Break Manifests & Ad Formats */
|
|
@@ -104,13 +114,22 @@ export interface BreakManifest {
|
|
|
104
114
|
/** The available breaks */
|
|
105
115
|
breaks: Break[];
|
|
106
116
|
}
|
|
107
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* An event window on a wallclock live stream and the session configuration in effect while it
|
|
119
|
+
* runs. Crossing its start or end emits `adchannelchange` with reason `event-boundary`.
|
|
120
|
+
* @category Break Manifests & Ad Formats */
|
|
108
121
|
export interface ChannelEvent {
|
|
122
|
+
/** Identifier of the event, reported as the manifest's `eventId` while it runs. */
|
|
109
123
|
id: string;
|
|
124
|
+
/** Start of the event window (ISO-8601 wallclock). */
|
|
110
125
|
start: string;
|
|
126
|
+
/** End of the event window (ISO-8601 wallclock). */
|
|
111
127
|
end: string;
|
|
128
|
+
/** When set, replaces the manifest's {@link BreakManifest.adStartDelay} during the event. */
|
|
112
129
|
adStartDelay?: number;
|
|
130
|
+
/** When set, replaces the manifest's {@link BreakManifest.delivery} rules during the event. */
|
|
113
131
|
delivery?: DeliveryRule[];
|
|
132
|
+
/** When set, replaces the manifest's {@link BreakManifest.vendorConfiguration} during the event. */
|
|
114
133
|
vendorConfiguration?: VendorConfiguration[];
|
|
115
134
|
}
|
|
116
135
|
/** Delivery architecture selected by a {@link DeliveryRule}. @category Break Manifests & Ad Formats */
|
|
@@ -205,7 +224,7 @@ export declare enum DeviceType {
|
|
|
205
224
|
TABLET = "tablet",
|
|
206
225
|
TV = "tv"
|
|
207
226
|
}
|
|
208
|
-
/** Platforms supported by manifest targeting (manifest spec v1.7.0). */
|
|
227
|
+
/** Platforms supported by manifest targeting (manifest spec v1.7.0). @category Break Manifests & Ad Formats */
|
|
209
228
|
export declare enum Platform {
|
|
210
229
|
WEB = "web",
|
|
211
230
|
ANDROID = "android",
|
|
@@ -213,7 +232,7 @@ export declare enum Platform {
|
|
|
213
232
|
TVOS = "tvos",
|
|
214
233
|
ROKU = "roku"
|
|
215
234
|
}
|
|
216
|
-
/** Operating systems supported by manifest targeting (manifest spec v1.8.0). */
|
|
235
|
+
/** Operating systems supported by manifest targeting (manifest spec v1.8.0). @category Break Manifests & Ad Formats */
|
|
217
236
|
export declare enum Os {
|
|
218
237
|
ANDROID = "android",
|
|
219
238
|
CHROMEOS = "chromeos",
|
|
@@ -229,9 +248,11 @@ export declare enum Os {
|
|
|
229
248
|
WINDOWS = "windows",
|
|
230
249
|
XBOX = "xbox"
|
|
231
250
|
}
|
|
232
|
-
/** Inclusive operating-system version bounds; each bound is dot-separated decimal integers. */
|
|
251
|
+
/** Inclusive operating-system version bounds; each bound is dot-separated decimal integers. @category Break Manifests & Ad Formats */
|
|
233
252
|
export interface OsVersionRange {
|
|
253
|
+
/** Lowest matching version, e.g. `"14"` or `"17.2"`. */
|
|
234
254
|
min?: string;
|
|
255
|
+
/** Highest matching version, e.g. `"18.1"`. */
|
|
235
256
|
max?: string;
|
|
236
257
|
}
|
|
237
258
|
/** Targeting criteria for break variants. @category Break Manifests & Ad Formats */
|
package/dist/types/Events.d.ts
CHANGED
|
@@ -244,6 +244,7 @@ export interface AdChannelChangeEvent extends OptiViewAdsEvent {
|
|
|
244
244
|
/** An external provider's manifest parameter template was added, changed, or removed. @category Events */
|
|
245
245
|
export interface ExternalVendorConfigurationChangeEvent extends OptiViewAdsEvent {
|
|
246
246
|
type: 'externalvendorconfigurationchange';
|
|
247
|
+
/** The provider whose template changed; read its new configuration with `getExternalVendorConfiguration(provider)`. */
|
|
247
248
|
provider: string;
|
|
248
249
|
}
|
|
249
250
|
/** Union type of all SDK events. @category Events */
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
/** Resolved parameters for an external provider declared in the break manifest. */
|
|
1
|
+
/** Resolved parameters for an external provider declared in the break manifest. @category Manifest Customization */
|
|
2
2
|
export interface ExternalVendorConfiguration {
|
|
3
|
+
/** The provider, as declared by the manifest's `external` vendor configuration entry. */
|
|
3
4
|
provider: string;
|
|
5
|
+
/** The provider's parameters after merging application parameters and resolving macros. */
|
|
4
6
|
assetParameters: Record<string, string>;
|
|
5
7
|
}
|