@dolby-optiview/ads-sdk-core 1.4.0 → 1.6.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/OptiViewAds.d.ts +5 -0
- package/dist/OptiViewAds.js +1 -1
- package/dist/diagnostics/dispatchDiagnostic.d.ts +4 -0
- package/dist/diagnostics/dispatchDiagnostic.js +1 -0
- package/dist/generated/diagnosticCodes.d.ts +118 -3
- 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 +11 -4
- package/dist/index.js +1 -1
- package/dist/interfaces/PlayerAdapter.d.ts +27 -0
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.js +1 -1
- package/dist/services/AdBreakSequencer.d.ts +67 -3
- package/dist/services/AdBreakSequencer.js +1 -1
- package/dist/services/AdPlayerController.d.ts +9 -4
- package/dist/services/AdPlayerController.js +1 -1
- package/dist/services/BookmarkStart.d.ts +12 -0
- package/dist/services/BookmarkStart.js +1 -0
- package/dist/services/BreakScheduler.d.ts +15 -50
- package/dist/services/BreakScheduler.js +1 -1
- package/dist/services/BreakSchedulerTypes.d.ts +11 -1
- package/dist/services/BreakWarningTracker.d.ts +6 -0
- package/dist/services/BreakWarningTracker.js +1 -0
- package/dist/services/ContentEndTracker.d.ts +17 -0
- package/dist/services/ContentEndTracker.js +1 -0
- package/dist/services/ExternalPlaybackGate.d.ts +2 -2
- package/dist/services/ManifestService.d.ts +1 -1
- package/dist/services/ManifestService.js +1 -1
- package/dist/services/PauseAdController.d.ts +9 -5
- package/dist/services/PauseAdController.js +1 -1
- package/dist/services/PlaybackCrossing.d.ts +4 -0
- package/dist/services/PlaybackCrossing.js +1 -0
- package/dist/services/RewindRearm.js +1 -1
- package/dist/services/SchedulerSnapback.d.ts +33 -0
- package/dist/services/SchedulerSnapback.js +1 -0
- package/dist/services/SlateClock.d.ts +22 -0
- package/dist/services/SlateClock.js +1 -0
- package/dist/services/SnapbackGuard.d.ts +70 -0
- package/dist/services/SnapbackGuard.js +1 -0
- package/dist/services/VastAdManager.d.ts +2 -0
- package/dist/services/VastAdManager.js +1 -1
- package/dist/services/adPlayer/AdPlayerCallbacks.d.ts +11 -1
- package/dist/services/adPlayer/AdPlayerEvents.d.ts +21 -1
- package/dist/services/adPlayer/AdPlayerEvents.js +1 -1
- package/dist/services/adPlayer/AdPlayerState.d.ts +14 -0
- package/dist/services/adPlayer/AdPlayerState.js +1 -1
- package/dist/services/adPlayer/AdPreloader.d.ts +8 -1
- package/dist/services/adPlayer/AdPreloader.js +1 -1
- package/dist/services/adPlayer/AdStage.js +1 -1
- package/dist/services/adPlayer/AssetPlayback.d.ts +1 -0
- package/dist/services/adPlayer/AssetPlayback.js +1 -1
- package/dist/services/adPlayer/BreakSequenceDriver.d.ts +7 -1
- package/dist/services/adPlayer/BreakSequenceDriver.js +1 -1
- package/dist/services/adPlayer/ContentResumeCoordinator.d.ts +6 -0
- package/dist/services/adPlayer/ContentResumeCoordinator.js +1 -1
- package/dist/services/adPlayer/PauseAdPresenter.d.ts +10 -9
- package/dist/services/adPlayer/PauseAdPresenter.js +1 -1
- package/dist/services/adPlayer/PauseAdTracking.d.ts +13 -0
- package/dist/services/adPlayer/PauseAdTracking.js +1 -0
- package/dist/services/adPlayer/SharedElementBreakPlayer.d.ts +31 -2
- package/dist/services/adPlayer/SharedElementBreakPlayer.js +1 -1
- package/dist/services/adPlayer/SlateCoordinator.d.ts +58 -0
- package/dist/services/adPlayer/SlateCoordinator.js +1 -0
- package/dist/services/adPlayer/SlatePlayback.d.ts +89 -0
- package/dist/services/adPlayer/SlatePlayback.js +1 -0
- package/dist/services/adPlayer/VariantSelector.d.ts +10 -1
- package/dist/services/adPlayer/VariantSelector.js +1 -1
- package/dist/services/adPlayer/connectPauseAd.js +1 -1
- package/dist/services/adPlayer/presentation/PresentationLayout.d.ts +5 -2
- package/dist/services/adPlayer/presentation/PresentationLayout.js +1 -1
- package/dist/services/breakTiming.d.ts +1 -0
- package/dist/services/breakTiming.js +1 -1
- package/dist/services/checkEventBreak.d.ts +5 -0
- package/dist/services/checkEventBreak.js +1 -0
- package/dist/services/parseBreakManifest.d.ts +2 -1
- package/dist/services/parseBreakManifest.js +1 -1
- package/dist/services/parseVastPause.d.ts +18 -0
- package/dist/services/parseVastPause.js +1 -0
- package/dist/services/resolveLshapeAnchor.d.ts +25 -0
- package/dist/services/resolveLshapeAnchor.js +1 -0
- package/dist/services/resolveResumePoint.d.ts +3 -1
- package/dist/services/resolveResumePoint.js +1 -1
- package/dist/services/resolveSlate.d.ts +25 -0
- package/dist/services/resolveSlate.js +1 -0
- package/dist/services/resolveSlateFill.d.ts +39 -0
- package/dist/services/resolveSlateFill.js +1 -0
- package/dist/services/ssbSwitchSignals.d.ts +3 -0
- package/dist/services/ssbSwitchSignals.js +1 -1
- package/dist/session/AdBreakStatusTracker.d.ts +22 -2
- package/dist/session/AdBreakStatusTracker.js +1 -1
- package/dist/session/BreakLifecycle.js +1 -1
- package/dist/session/ContentPlaybackStart.d.ts +23 -0
- package/dist/session/ContentPlaybackStart.js +1 -0
- package/dist/session/DiscoveredManifestUrl.d.ts +44 -0
- package/dist/session/DiscoveredManifestUrl.js +1 -0
- package/dist/session/ScheduledBreaksTracker.d.ts +22 -0
- package/dist/session/ScheduledBreaksTracker.js +1 -0
- package/dist/session/SessionStarter.d.ts +3 -9
- package/dist/session/SessionStarter.js +1 -1
- package/dist/session/SessionStarterDeps.d.ts +3 -0
- package/dist/session/SessionStarterHelpers.d.ts +22 -1
- package/dist/session/SessionStarterHelpers.js +1 -1
- package/dist/session/SessionState.d.ts +16 -0
- package/dist/session/SessionState.js +1 -1
- package/dist/session/SsbSwitch.js +1 -1
- package/dist/session/buildScheduledBreaks.d.ts +9 -0
- package/dist/session/buildScheduledBreaks.js +1 -0
- package/dist/session/contentSnapback.d.ts +5 -1
- package/dist/session/contentSnapback.js +1 -1
- package/dist/session/scheduledBreaksSession.d.ts +6 -0
- package/dist/session/scheduledBreaksSession.js +1 -0
- package/dist/types/BreakManifest.d.ts +40 -2
- package/dist/types/BreakManifest.js +1 -1
- package/dist/types/Events.d.ts +13 -4
- package/dist/types/OptiViewAdsConfig.d.ts +15 -2
- package/dist/types/ScheduledBreak.d.ts +17 -0
- package/dist/types/ScheduledBreak.js +1 -0
- package/package.json +1 -1
|
@@ -14,9 +14,11 @@ export interface ResumePointInput {
|
|
|
14
14
|
insertionType?: AdInsertionType | undefined;
|
|
15
15
|
/** The manifest's seek length from the anchor. Absent lets the ad duration decide; `0` asks for no seek. */
|
|
16
16
|
resumeOffset?: number | undefined;
|
|
17
|
+
/** Viewer seek-over target. Overrides every offset policy, including on no-fill or failure. */
|
|
18
|
+
seekPosition?: number | undefined;
|
|
17
19
|
}
|
|
18
20
|
/** Which precedence level produced the offset. */
|
|
19
|
-
export type ResumePointSource = 'client' | 'manifest' | 'ad-duration';
|
|
21
|
+
export type ResumePointSource = 'snapback' | 'client' | 'manifest' | 'ad-duration';
|
|
20
22
|
/** The resolved resume decision. */
|
|
21
23
|
export interface ResumePoint {
|
|
22
24
|
/** Whether to seek at all. A zero offset means stay where content paused. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export function resolveResumePoint(e){const{anchor:
|
|
1
|
+
export function resolveResumePoint(e){const{anchor:i,duration:t,adDuration:o,insertionType:s,resumeOffset:n}=e;if(void 0!==e.seekPosition&&Number.isFinite(e.seekPosition)){const t=e.seekPosition-i;return{seek:0!==t,target:e.seekPosition,offset:t,source:"snapback",clamped:!1}}let r,a;void 0!==s?(r="insertion"===s?0:t,a="client"):void 0!==n?(r=n,a="manifest"):(r=o,a="ad-duration"),(!Number.isFinite(r)||r<0)&&(r=0);const u=Number.isFinite(o)&&o>0?o:0,c=r>0&&u>0&&u<r;return c&&(r=u),{seek:r>0,target:i+r,offset:r,source:a,clamped:c}}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { StaticAsset } from '../types/BreakManifest';
|
|
2
|
+
/** Seconds of the break that must remain for a slate to start when the manifest sets no `minDuration`. */
|
|
3
|
+
export declare const DEFAULT_SLATE_MIN_DURATION_SEC = 5;
|
|
4
|
+
/** The slate as the player uses it: a minimum and the one asset it plays (the first). */
|
|
5
|
+
export interface ResolvedSlate {
|
|
6
|
+
minDurationSec: number;
|
|
7
|
+
/** The first slate asset, without `duration`: the slate fills the remaining break. */
|
|
8
|
+
asset: StaticAsset;
|
|
9
|
+
}
|
|
10
|
+
export type SlateIssueReason = 'not-object' | 'invalid-min-duration' | 'no-assets' | 'invalid-asset';
|
|
11
|
+
/** Why a manifest's `slate` was ignored; the breaks of that manifest are unaffected. */
|
|
12
|
+
export interface SlateIssue {
|
|
13
|
+
reason: SlateIssueReason;
|
|
14
|
+
message: string;
|
|
15
|
+
context: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Validate a raw manifest `slate` and reduce it to what the player plays. `slate: undefined`
|
|
19
|
+
* resolves to no slate without an issue; anything unusable resolves to no slate with an issue,
|
|
20
|
+
* so a malformed slate never costs a manifest its breaks.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveSlate(raw: unknown): {
|
|
23
|
+
slate: ResolvedSlate | null;
|
|
24
|
+
issue?: SlateIssue;
|
|
25
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{AssetType as t,MediaType as e}from"../types/BreakManifest";export const DEFAULT_SLATE_MIN_DURATION_SEC=5;function s(t){return"string"==typeof t&&t.length>0}function n(n){if(!n||"object"!=typeof n)return"the first asset is not an object";const r=n;return s(r.id)?r.type!==t.STATIC?"the first asset is not static":r.mediaType!==e.VIDEO&&r.mediaType!==e.IMAGE?"the first asset has an unknown mediaType":s(a=r.uri)||Array.isArray(a)&&a.length>0&&a.every(t=>!!t&&"object"==typeof t&&s(t.value))?null:"the first asset has no uri":"the first asset has no id";var a}export function resolveSlate(t){if(void 0===t)return{slate:null};const e=(t,e,s={})=>({slate:null,issue:{reason:t,message:`Ignored slate: ${e}.`,context:{reason:t,...s}}});if(!t||"object"!=typeof t||Array.isArray(t))return e("not-object","the value is not an object");const r=t,a=r.minDuration;if(void 0!==a&&("number"!=typeof a||!Number.isFinite(a)||a<0))return e("invalid-min-duration","minDuration must be a number >= 0");if(!Array.isArray(r.assets)||0===r.assets.length)return e("no-assets","assets must be a non-empty array");const i=r.assets[0],o=n(i);if(o){const t=i?.id;return e("invalid-asset",o,s(t)?{assetId:t}:{})}const u={...i};return delete u.duration,{slate:{minDurationSec:a??5,asset:u}}}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ResolvedSlate } from './resolveSlate';
|
|
2
|
+
export type SlateSkipReason = 'no-slate' | 'not-rendered' | 'no-time-left' | 'below-min-duration';
|
|
3
|
+
export interface SlateFillInput {
|
|
4
|
+
slate: ResolvedSlate | null;
|
|
5
|
+
/** The break's effective duration in seconds: the tune-in remainder, else `break.duration`. */
|
|
6
|
+
effectiveDurationSec: number;
|
|
7
|
+
/** Seconds of ad media the break has played so far. */
|
|
8
|
+
adsPlayedSec: number;
|
|
9
|
+
/**
|
|
10
|
+
* Real seconds since the break began (`adbreakbegin`), or null when unknown. Time spent on ad
|
|
11
|
+
* attempts that never played, on ad start-up and on the slate's own start-up is break time too,
|
|
12
|
+
* so the slate ends at the break's scheduled end: begin + `effectiveDurationSec`.
|
|
13
|
+
*/
|
|
14
|
+
breakElapsedSec?: number | null;
|
|
15
|
+
/**
|
|
16
|
+
* Wall seconds since content began playing behind the ad (continue-content-playout), or null
|
|
17
|
+
* when content is not playing hidden behind it. Content then reaches its resume point at
|
|
18
|
+
* `effectiveDurationSec` on this clock, so the slate may not outlast it.
|
|
19
|
+
*/
|
|
20
|
+
continuePlayoutWallElapsedSec: number | null;
|
|
21
|
+
/** False where the stitched stream fills the break: time-based breaks under ssai, or while an SSB stream plays. */
|
|
22
|
+
rendersSlate: boolean;
|
|
23
|
+
}
|
|
24
|
+
export type SlateFillDecision = {
|
|
25
|
+
fill: true;
|
|
26
|
+
durationSec: number;
|
|
27
|
+
} | {
|
|
28
|
+
fill: false;
|
|
29
|
+
reason: SlateSkipReason;
|
|
30
|
+
remainingSec: number;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Whether a slate starts now and for how long. The break's remaining time is its effective
|
|
34
|
+
* duration minus the furthest of its clocks: the ad media played, the real time since the break
|
|
35
|
+
* began, and the content clock while content plays behind the ad. The slate fills all of it and
|
|
36
|
+
* never more, so it ends at the break's scheduled end. A slate starts when at least
|
|
37
|
+
* `minDurationSec` remains (`0` starts it for any remaining time).
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveSlateFill(input: SlateFillInput): SlateFillDecision;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e=e=>"number"==typeof e&&Number.isFinite(e)&&e>0?e:0;export function resolveSlateFill(n){const a=e(n.effectiveDurationSec),i=Math.max(e(n.adsPlayedSec),e(n.breakElapsedSec),e(n.continuePlayoutWallElapsedSec)),l=(r=Math.max(0,a-i),Math.round(1e3*r)/1e3);var r;return n.slate?n.rendersSlate?l<=0?{fill:!1,reason:"no-time-left",remainingSec:l}:l<n.slate.minDurationSec?{fill:!1,reason:"below-min-duration",remainingSec:l}:{fill:!0,durationSec:l}:{fill:!1,reason:"not-rendered",remainingSec:l}:{fill:!1,reason:"no-slate",remainingSec:l}}
|
|
@@ -11,5 +11,8 @@ export type ExternalPlaybackRequestTrigger = 'connecting' | 'active';
|
|
|
11
11
|
* route was `inactive` before and no blocked request is pending (THEOplayer reports connected without
|
|
12
12
|
* connecting). Null for every other change. */
|
|
13
13
|
export declare function resolveExternalPlaybackRequestTrigger(state: string, previousState: string, pending: boolean): ExternalPlaybackRequestTrigger | null;
|
|
14
|
+
export type SsbSwitchTrigger = ExternalPlaybackRequestTrigger | 'break-ended';
|
|
15
|
+
/** The content state the switch restores to: a `break-ended` switch always treats content as playing — the break paused it, and its own resume is what is in flight. */
|
|
16
|
+
export declare function resolveSsbSwitchPreviousPaused(trigger: SsbSwitchTrigger, playerPaused: boolean): boolean;
|
|
14
17
|
/** A gate request is evaluated only while no SSB switch owns the route. */
|
|
15
18
|
export declare function shouldEvaluateExternalPlaybackRequest(ssbPhase: SsbSwitchPhase | null): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export function resolveSsbRestoreChange(e,t){return e?{previousDeliveryMode:"ssb",deliveryMode:t}:null}export function resolveStatusExternalPlaybackAllowed(e,t,n){if(void 0!==e)return"ssb"===n||e&&"active"!==t}export function resolveExternalPlaybackRequestTrigger(e,t,n){return"connecting"===e?"connecting":"active"!==e||"inactive"!==t||n?null:"active"}export function shouldEvaluateExternalPlaybackRequest(e){return null===e}
|
|
1
|
+
export function resolveSsbRestoreChange(e,t){return e?{previousDeliveryMode:"ssb",deliveryMode:t}:null}export function resolveStatusExternalPlaybackAllowed(e,t,n){if(void 0!==e)return"ssb"===n||e&&"active"!==t}export function resolveExternalPlaybackRequestTrigger(e,t,n){return"connecting"===e?"connecting":"active"!==e||"inactive"!==t||n?null:"active"}export function resolveSsbSwitchPreviousPaused(e,t){return"break-ended"!==e&&t}export function shouldEvaluateExternalPlaybackRequest(e){return null===e}
|
|
@@ -18,6 +18,8 @@ export declare class AdBreakStatusTracker {
|
|
|
18
18
|
private readonly deps;
|
|
19
19
|
adBreakStatus: AdBreakStatus;
|
|
20
20
|
activeBreak: Break | null;
|
|
21
|
+
private pendingCompletionBreakId;
|
|
22
|
+
private errorSettledPosition;
|
|
21
23
|
activeBreakRemainingAtStart: number | null;
|
|
22
24
|
activeBreakStartTimestamp: number | null;
|
|
23
25
|
activeBreakElapsedSec: number;
|
|
@@ -31,6 +33,8 @@ export declare class AdBreakStatusTracker {
|
|
|
31
33
|
currentAdDuration: number | null;
|
|
32
34
|
activeBreakWaiting: boolean;
|
|
33
35
|
activeBreakPlaying: boolean;
|
|
36
|
+
/** A slate owns the rest of the break: the countdown runs down to this wall-clock end (the break's scheduled end). */
|
|
37
|
+
slateEndsAtMs: number | null;
|
|
34
38
|
/** The `externalPlaybackAllowed` value last attached to a status; undefined before the first. */
|
|
35
39
|
private lastExternalPlaybackAllowed;
|
|
36
40
|
private lastDeliveryMode;
|
|
@@ -54,15 +58,31 @@ export declare class AdBreakStatusTracker {
|
|
|
54
58
|
/** Mark the active break visible and playing: the first-frame signal for held pre-rolls, the first `playing` otherwise. */
|
|
55
59
|
setAdBreakVisible(breakInfo: Break): void;
|
|
56
60
|
onAdStarted(breakInfo: Break, asset: Asset, index: number, total: number): void;
|
|
57
|
-
|
|
61
|
+
onAdError(breakInfo: Break, asset?: Asset, position?: {
|
|
62
|
+
adIndex: number;
|
|
63
|
+
totalAds: number;
|
|
64
|
+
}): void;
|
|
65
|
+
onAdEnded(breakInfo: Break, index: number, total: number, assetId?: string): void;
|
|
58
66
|
onAdTimeUpdated(breakInfo: Break, asset: Asset, currentTime: number, duration: number): void;
|
|
67
|
+
/**
|
|
68
|
+
* The slate plays: the countdown runs down to the break's scheduled end (`remainingSec` away on the
|
|
69
|
+
* wall clock), so it reaches 0 when the slate ends whatever the slate's start-up, stalls or loops
|
|
70
|
+
* did, and `ticking` follows whether the slate is on screen. It never rises: the ad time it
|
|
71
|
+
* replaces lags the wall clock by the time the ads lost. No ad event follows.
|
|
72
|
+
*/
|
|
73
|
+
onSlateProgress(breakInfo: Break, remainingSec: number, presenting: boolean): void;
|
|
59
74
|
onAdPlaybackWaiting(): void;
|
|
60
75
|
onAdPlaybackPlaying(): void;
|
|
61
76
|
private buildActiveAdBreakStatus;
|
|
62
77
|
/** Remaining break seconds; falls back to wall-clock for formats without per-ad `timeupdate` (e.g. backdrop). */
|
|
63
78
|
private computeBreakRemainingSec;
|
|
64
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* Ad progress is authoritative once it arrives; before then (IMA fetch, backdrop/image breaks)
|
|
81
|
+
* wall-clock keeps the countdown moving. Once a slate owns the break, the wall time to the
|
|
82
|
+
* scheduled end does, and never less than the ads already counted.
|
|
83
|
+
*/
|
|
65
84
|
currentBreakElapsedSec(): number;
|
|
85
|
+
completeAdBreakStatus(breakInfo: Break): void;
|
|
66
86
|
resetAdBreakStatus(): void;
|
|
67
87
|
private emitAdBreakStatus;
|
|
68
88
|
/** Only compares the scalar fields that affect UI rendering; `break` identity is ignored. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveStatusExternalPlaybackAllowed as t}from"../services/ssbSwitchSignals";export class AdBreakStatusTracker{constructor(t){this.deps=t,this.adBreakStatus={phase:"idle",break:null,ticking:!1},this.activeBreak=null,this.activeBreakRemainingAtStart=null,this.activeBreakStartTimestamp=null,this.activeBreakElapsedSec=0,this.currentAdStartElapsedSec=0,this.currentAdAssetId=null,this.timelineAssetId=null,this.timelineStartElapsedSec=0,this.currentAdIndex=null,this.currentAdTotal=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakWaiting=!1,this.activeBreakPlaying=!1}getAdBreakStatus(){return this.withExternalPlayback(this.liveAdBreakStatus())}refreshExternalPlayback(){const e=t(this.deps.externalPlaybackAllowed(),this.adBreakStatus.phase,this.deps.deliveryMode());if(void 0===e||e===(this.lastExternalPlaybackAllowed??!0))return;const i=this.getAdBreakStatus();this.lastExternalPlaybackAllowed=i.externalPlaybackAllowed,this.deps.emit("adbreakstatus",{type:"adbreakstatus",timestamp:Date.now(),status:i})}refreshDeliveryMode(){const t=this.deps.deliveryMode();if(void 0===t||t===(this.lastDeliveryMode??"sgai"))return;const e=this.getAdBreakStatus();this.lastDeliveryMode=e.deliveryMode,this.deps.emit("adbreakstatus",{type:"adbreakstatus",timestamp:Date.now(),status:e})}liveAdBreakStatus(){if("active"===this.adBreakStatus.phase)return{...this.adBreakStatus,breakRemainingSec:this.computeBreakRemainingSec()};if("upcoming"===this.adBreakStatus.phase&&this.activeBreak){const t=this.deps.secondsUntilBreak(this.activeBreak.id);return{...this.adBreakStatus,secondsUntilBreak:t}}return{...this.adBreakStatus}}withExternalPlayback(e){const i=this.deps.deliveryMode(),a=void 0===i?e:{...e,deliveryMode:i},s=t(this.deps.externalPlaybackAllowed(),a.phase,i);return void 0===s?a:{...a,externalPlaybackAllowed:s}}onBreakWarning(t,e,i){this.activeBreak=t,this.emitAdBreakStatus({phase:"upcoming",break:t,format:this.deps.effectiveFormatOf(t),secondsUntilBreak:i,ticking:!1,warningSec:e})}setAdBreakStatusActive(t,e){this.activeBreak=t,this.activeBreakRemainingAtStart=e,this.activeBreakStartTimestamp=Date.now(),this.activeBreakElapsedSec=0,this.currentAdStartElapsedSec=0,this.currentAdAssetId=null,this.timelineAssetId=null,this.timelineStartElapsedSec=0,this.currentAdIndex=null,this.currentAdTotal=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakWaiting=!1,this.activeBreakPlaying=!1,this.emitAdBreakStatus({phase:"active",break:t,format:this.deps.effectiveFormatOf(t),breakRemainingSec:this.activeBreakRemainingAtStart,ticking:!1})}setAdBreakVisible(t){this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),ticking:!0})}onAdStarted(t,e,i,a){this.currentAdIndex=i,this.currentAdAssetId=e.id,this.currentAdTotal=a,this.currentAdTime=null,this.currentAdDuration=null,this.currentAdStartElapsedSec=this.currentBreakElapsedSec(),this.activeBreakElapsedSec=this.currentAdStartElapsedSec,this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.deps.breakTransitionTimer()?.settle("into-break","ad-to-ad","break-to-break"),this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),adIndex:i,totalAds:a,adsRemaining:a-i,ticking:!0})}onAdEnded(t,e,i){this.timelineAssetId===this.currentAdAssetId&&null!==this.currentAdDuration&&Number.isFinite(this.currentAdDuration)?this.activeBreakElapsedSec=this.timelineStartElapsedSec+this.currentAdDuration:null!==this.currentAdTime&&(this.activeBreakElapsedSec=this.timelineStartElapsedSec+this.currentAdTime),this.currentAdIndex=null,this.currentAdAssetId=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakPlaying=!1,this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),adsRemaining:Math.max(0,i-e-1),breakRemainingSec:this.computeBreakRemainingSec(),ticking:!1})}onAdTimeUpdated(t,e,i,a){e.id!==this.timelineAssetId&&(this.timelineAssetId=e.id,this.timelineStartElapsedSec=null===this.currentAdIndex?this.currentBreakElapsedSec():this.currentAdStartElapsedSec),this.currentAdTime=i,this.currentAdDuration=a,this.activeBreakElapsedSec=this.timelineStartElapsedSec+i,this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),breakRemainingSec:this.computeBreakRemainingSec(),ticking:!0})}onAdPlaybackWaiting(){this.activeBreakWaiting=!0,this.activeBreakPlaying=!1,this.activeBreak&&this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(this.activeBreak),ticking:!1})}onAdPlaybackPlaying(){this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.activeBreak&&this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(this.activeBreak),ticking:!0})}buildActiveAdBreakStatus(t){return{phase:"active",break:t,format:this.deps.effectiveFormatOf(t),adIndex:this.currentAdIndex??void 0,totalAds:this.currentAdTotal??void 0,adsRemaining:null!==this.currentAdIndex&&null!==this.currentAdTotal?this.currentAdTotal-this.currentAdIndex:void 0,breakRemainingSec:this.computeBreakRemainingSec(),ticking:this.activeBreakPlaying&&!this.activeBreakWaiting}}computeBreakRemainingSec(){if(null!==this.activeBreakRemainingAtStart)return Math.max(0,this.activeBreakRemainingAtStart-this.currentBreakElapsedSec())}currentBreakElapsedSec(){let t=this.activeBreakElapsedSec;return 0===t&&null!==this.activeBreakStartTimestamp&&null===this.currentAdTime&&(t=(Date.now()-this.activeBreakStartTimestamp)/1e3),t}resetAdBreakStatus(){this.activeBreak=null,this.activeBreakRemainingAtStart=null,this.activeBreakStartTimestamp=null,this.activeBreakElapsedSec=0,this.currentAdStartElapsedSec=0,this.currentAdAssetId=null,this.timelineAssetId=null,this.timelineStartElapsedSec=0,this.currentAdIndex=null,this.currentAdTotal=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakWaiting=!1,this.activeBreakPlaying=!1,this.lastExternalPlaybackAllowed=void 0,this.emitAdBreakStatus({phase:"idle",break:null,ticking:!1})}emitAdBreakStatus(t){if(this.statusesEqual(this.adBreakStatus,t))return;this.adBreakStatus=t;const e=this.withExternalPlayback(t);void 0!==e.externalPlaybackAllowed&&(this.lastExternalPlaybackAllowed=e.externalPlaybackAllowed),this.lastDeliveryMode=e.deliveryMode,this.deps.emit("adbreakstatus",{type:"adbreakstatus",timestamp:Date.now(),status:e})}statusesEqual(t,e){return t.phase===e.phase&&t.format===e.format&&t.secondsUntilBreak===e.secondsUntilBreak&&t.breakRemainingSec===e.breakRemainingSec&&t.adIndex===e.adIndex&&t.totalAds===e.totalAds&&t.adsRemaining===e.adsRemaining&&t.ticking===e.ticking&&t.warningSec===e.warningSec&&t.break?.id===e.break?.id}}
|
|
1
|
+
import{resolveStatusExternalPlaybackAllowed as t}from"../services/ssbSwitchSignals";import{extractAssets as e}from"../services/AdBreakSequencer";export class AdBreakStatusTracker{constructor(t){this.deps=t,this.adBreakStatus={phase:"idle",break:null,ticking:!1},this.activeBreak=null,this.pendingCompletionBreakId=null,this.errorSettledPosition=null,this.activeBreakRemainingAtStart=null,this.activeBreakStartTimestamp=null,this.activeBreakElapsedSec=0,this.currentAdStartElapsedSec=0,this.currentAdAssetId=null,this.timelineAssetId=null,this.timelineStartElapsedSec=0,this.currentAdIndex=null,this.currentAdTotal=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakWaiting=!1,this.activeBreakPlaying=!1,this.slateEndsAtMs=null}getAdBreakStatus(){return this.withExternalPlayback(this.liveAdBreakStatus())}refreshExternalPlayback(){const e=t(this.deps.externalPlaybackAllowed(),this.adBreakStatus.phase,this.deps.deliveryMode());if(void 0===e||e===(this.lastExternalPlaybackAllowed??!0))return;const i=this.getAdBreakStatus();this.lastExternalPlaybackAllowed=i.externalPlaybackAllowed,this.deps.emit("adbreakstatus",{type:"adbreakstatus",timestamp:Date.now(),status:i})}refreshDeliveryMode(){const t=this.deps.deliveryMode();if(void 0===t||t===(this.lastDeliveryMode??"sgai"))return;const e=this.getAdBreakStatus();this.lastDeliveryMode=e.deliveryMode,this.deps.emit("adbreakstatus",{type:"adbreakstatus",timestamp:Date.now(),status:e})}liveAdBreakStatus(){if("active"===this.adBreakStatus.phase)return{...this.adBreakStatus,breakRemainingSec:this.computeBreakRemainingSec()};if("upcoming"===this.adBreakStatus.phase&&this.activeBreak){const t=this.deps.secondsUntilBreak(this.activeBreak.id);return{...this.adBreakStatus,secondsUntilBreak:t}}return{...this.adBreakStatus}}withExternalPlayback(e){const i=this.deps.deliveryMode(),a=void 0===i?e:{...e,deliveryMode:i},s=t(this.deps.externalPlaybackAllowed(),a.phase,i);return void 0===s?a:{...a,externalPlaybackAllowed:s}}onBreakWarning(t,e,i){this.activeBreak=t,this.emitAdBreakStatus({phase:"upcoming",break:t,format:this.deps.effectiveFormatOf(t),secondsUntilBreak:i,ticking:!1,warningSec:e})}setAdBreakStatusActive(t,e){this.errorSettledPosition=null,this.activeBreak=t,this.pendingCompletionBreakId=t.id,this.activeBreakRemainingAtStart=e,this.activeBreakStartTimestamp=Date.now(),this.activeBreakElapsedSec=0,this.currentAdStartElapsedSec=0,this.currentAdAssetId=null,this.timelineAssetId=null,this.timelineStartElapsedSec=0,this.currentAdIndex=null,this.currentAdTotal=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakWaiting=!1,this.activeBreakPlaying=!1,this.slateEndsAtMs=null,this.emitAdBreakStatus({phase:"active",break:t,format:this.deps.effectiveFormatOf(t),breakRemainingSec:this.activeBreakRemainingAtStart,ticking:!1})}setAdBreakVisible(t){this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),ticking:!0})}onAdStarted(t,e,i,a){this.errorSettledPosition=null,this.currentAdIndex=i,this.currentAdAssetId=e.id,this.currentAdTotal=a,this.currentAdTime=null,this.currentAdDuration=null,this.currentAdStartElapsedSec=this.currentBreakElapsedSec(),this.activeBreakElapsedSec=this.currentAdStartElapsedSec,this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.deps.breakTransitionTimer()?.settle("into-break","ad-to-ad","break-to-break"),this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),adIndex:i,totalAds:a,adsRemaining:a-i,ticking:!0})}onAdError(t,i,a){if(!i||"active"!==this.adBreakStatus.phase||this.activeBreak?.id!==t.id)return;if(this.currentAdAssetId===i.id&&null!==this.currentAdIndex&&null!==this.currentAdTotal){const e={index:this.currentAdIndex,total:this.currentAdTotal,assetId:i.id};return this.onAdEnded(t,this.currentAdIndex,this.currentAdTotal,i.id),void(this.errorSettledPosition=e)}const s=e(t),r=a?.adIndex??s.findIndex(t=>t.id===i.id),n=a?.totalAds??s.length;null!==this.currentAdIndex||r<0||r>=n||!a&&null!==this.currentAdTotal&&this.currentAdTotal!==n||this.errorSettledPosition?.index===r&&this.errorSettledPosition.total===n&&this.errorSettledPosition.assetId===i.id||(this.currentAdTotal=n,this.onAdEnded(t,r,n,i.id),this.errorSettledPosition={index:r,total:n,assetId:i.id})}onAdEnded(t,e,i,a){this.errorSettledPosition?.index!==e||this.errorSettledPosition.total!==i||void 0!==a&&this.errorSettledPosition.assetId!==a?(this.timelineAssetId===this.currentAdAssetId&&null!==this.currentAdDuration&&Number.isFinite(this.currentAdDuration)?this.activeBreakElapsedSec=this.timelineStartElapsedSec+this.currentAdDuration:null!==this.currentAdTime&&(this.activeBreakElapsedSec=this.timelineStartElapsedSec+this.currentAdTime),this.currentAdIndex=null,this.currentAdAssetId=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakPlaying=!1,this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),adsRemaining:Math.max(0,i-e-1),breakRemainingSec:this.computeBreakRemainingSec(),ticking:!1})):this.errorSettledPosition=null}onAdTimeUpdated(t,e,i,a){e.id!==this.timelineAssetId&&(this.timelineAssetId=e.id,this.timelineStartElapsedSec=null===this.currentAdIndex?this.currentBreakElapsedSec():this.currentAdStartElapsedSec),this.currentAdTime=i,this.currentAdDuration=a,this.activeBreakElapsedSec=this.timelineStartElapsedSec+i,this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),breakRemainingSec:this.computeBreakRemainingSec(),ticking:!0})}onSlateProgress(t,e,i){this.activeBreak?.id===t.id&&null!==this.activeBreakRemainingAtStart&&(this.activeBreakElapsedSec=this.currentBreakElapsedSec(),this.slateEndsAtMs=Date.now()+1e3*Math.max(0,e),this.activeBreakWaiting=!i,this.activeBreakPlaying=i,this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),adsRemaining:this.adBreakStatus.adsRemaining,ticking:i}))}onAdPlaybackWaiting(){this.activeBreakWaiting=!0,this.activeBreakPlaying=!1,this.activeBreak&&this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(this.activeBreak),ticking:!1})}onAdPlaybackPlaying(){this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.activeBreak&&this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(this.activeBreak),ticking:!0})}buildActiveAdBreakStatus(t){return{phase:"active",break:t,format:this.deps.effectiveFormatOf(t),adIndex:this.currentAdIndex??void 0,totalAds:this.currentAdTotal??void 0,adsRemaining:null!==this.currentAdIndex&&null!==this.currentAdTotal?this.currentAdTotal-this.currentAdIndex:void 0,breakRemainingSec:this.computeBreakRemainingSec(),ticking:this.activeBreakPlaying&&!this.activeBreakWaiting}}computeBreakRemainingSec(){if(null!==this.activeBreakRemainingAtStart)return Math.max(0,this.activeBreakRemainingAtStart-this.currentBreakElapsedSec())}currentBreakElapsedSec(){let t=this.activeBreakElapsedSec;if(null!==this.slateEndsAtMs&&null!==this.activeBreakRemainingAtStart){const e=Math.max(0,(this.slateEndsAtMs-Date.now())/1e3);return Math.max(t,this.activeBreakRemainingAtStart-e)}return 0===t&&null!==this.activeBreakStartTimestamp&&null===this.currentAdTime&&(t=(Date.now()-this.activeBreakStartTimestamp)/1e3),t}completeAdBreakStatus(t){this.pendingCompletionBreakId===t.id&&(this.pendingCompletionBreakId=null,this.emitAdBreakStatus({phase:"complete",break:t,format:this.deps.effectiveFormatOf(t),breakRemainingSec:0,adsRemaining:0,ticking:!1}))}resetAdBreakStatus(){this.errorSettledPosition=null,this.activeBreak=null,this.pendingCompletionBreakId=null,this.activeBreakRemainingAtStart=null,this.activeBreakStartTimestamp=null,this.activeBreakElapsedSec=0,this.currentAdStartElapsedSec=0,this.currentAdAssetId=null,this.timelineAssetId=null,this.timelineStartElapsedSec=0,this.currentAdIndex=null,this.currentAdTotal=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakWaiting=!1,this.activeBreakPlaying=!1,this.slateEndsAtMs=null,this.lastExternalPlaybackAllowed=void 0,this.emitAdBreakStatus({phase:"idle",break:null,ticking:!1})}emitAdBreakStatus(t){if(this.statusesEqual(this.adBreakStatus,t))return;this.adBreakStatus=t;const e=this.withExternalPlayback(t);void 0!==e.externalPlaybackAllowed&&(this.lastExternalPlaybackAllowed=e.externalPlaybackAllowed),this.lastDeliveryMode=e.deliveryMode,this.deps.emit("adbreakstatus",{type:"adbreakstatus",timestamp:Date.now(),status:e})}statusesEqual(t,e){return t.phase===e.phase&&t.format===e.format&&t.secondsUntilBreak===e.secondsUntilBreak&&t.breakRemainingSec===e.breakRemainingSec&&t.adIndex===e.adIndex&&t.totalAds===e.totalAds&&t.adsRemaining===e.adsRemaining&&t.ticking===e.ticking&&t.warningSec===e.warningSec&&t.break?.id===e.break?.id}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BreakFormat as e}from"../types/BreakManifest";import{playbackErrorContext as t}from"../diagnostics/playbackErrorContext";import{adErrorCode as a}from"../types/AdError";import{VendorAdError as i}from"../types/VendorAdError";export class BreakLifecycle{constructor(e,t){this.state=e,this.deps=t,this.breakBeginEmittedId=null,this.pendingAdBreakBegin=null,t.breakScheduler.setOnChainSuccessorChanged((e,a)=>{t.breakChainRecorder?.successorChanged(a?.id??null),t.adPlayerController.onChainSuccessorChanged()})}recordBreakBegan(e,t){const a=this.deps.breakScheduler.getChainedSuccessor(e.id);this.deps.breakChainRecorder?.breakBegan(e,t,a?.id??null)}emitAdBreakBegin(e,t){this.breakBeginEmittedId!==e.id&&(this.breakBeginEmittedId=e.id,this.pendingAdBreakBegin=null,this.deps.emit("adbreakbegin",{type:"adbreakbegin",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),elapsedSec:t?.elapsedSec??0,remainingSec:t?.remainingSec??e.duration??0,...t?.tuneIn?{tuneIn:{elapsedSec:t.elapsedSec,remainingSec:t.remainingSec}}:{}}))}flushPendingAdBreakBegin(){this.pendingAdBreakBegin?.()}onBreakSkipped(e,t){this.deps.emit("adbreakskipped",{type:"adbreakskipped",timestamp:Date.now(),break:e,reason:t,format:this.deps.effectiveFormatOf(e)})}async onBreakStart(r,s){const d=this.state.sessionGeneration;if(this.state.snapbackSeekInFlight=!1,this.deps.adPlayerController.isAdPlaying()){if(this.deps.diagnose("DA-BREAK-SUPPRESSED-OVERLAP",`Break suppressed (ad already playing): ${r.id}`,{breakId:r.id}),d!==this.state.sessionGeneration)return;return this.deps.breakScheduler.completeBreak(r.id),this.onBreakSkipped(r,"overlap"),void this.deps.onBreakSettled?.()}this.deps.adPlayerController.
|
|
1
|
+
import{BreakFormat as e}from"../types/BreakManifest";import{playbackErrorContext as t}from"../diagnostics/playbackErrorContext";import{adErrorCode as a}from"../types/AdError";import{VendorAdError as i}from"../types/VendorAdError";export class BreakLifecycle{constructor(e,t){this.state=e,this.deps=t,this.breakBeginEmittedId=null,this.pendingAdBreakBegin=null,t.breakScheduler.setOnChainSuccessorChanged((e,a)=>{t.breakChainRecorder?.successorChanged(a?.id??null),t.adPlayerController.onChainSuccessorChanged()})}recordBreakBegan(e,t){const a=this.deps.breakScheduler.getChainedSuccessor(e.id);this.deps.breakChainRecorder?.breakBegan(e,t,a?.id??null)}emitAdBreakBegin(e,t){this.breakBeginEmittedId!==e.id&&(this.breakBeginEmittedId=e.id,this.pendingAdBreakBegin=null,this.deps.emit("adbreakbegin",{type:"adbreakbegin",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),elapsedSec:t?.elapsedSec??0,remainingSec:t?.remainingSec??e.duration??0,...t?.tuneIn?{tuneIn:{elapsedSec:t.elapsedSec,remainingSec:t.remainingSec}}:{}}))}flushPendingAdBreakBegin(){this.pendingAdBreakBegin?.()}onBreakSkipped(e,t){this.deps.emit("adbreakskipped",{type:"adbreakskipped",timestamp:Date.now(),break:e,reason:t,format:this.deps.effectiveFormatOf(e)})}async onBreakStart(r,s){const d=this.state.sessionGeneration;if(this.state.snapbackSeekInFlight=!1,this.deps.adPlayerController.isAdPlaying()){if(this.deps.diagnose("DA-BREAK-SUPPRESSED-OVERLAP",`Break suppressed (ad already playing): ${r.id}`,{breakId:r.id}),d!==this.state.sessionGeneration)return;return this.deps.breakScheduler.completeBreak(r.id),this.onBreakSkipped(r,"overlap"),void this.deps.onBreakSettled?.()}this.deps.adPlayerController.diagnoseVariantAnomalies(r);const n=this.deps.adPlayerController.breakNothingToPlayReason(r,s.remainingSec);if(n){if(this.deps.log("Break not begun (no playable variant):",r.id,n),this.deps.diagnose("DA-BREAK-NO-PLAYABLE-ASSET",`break ${r.id} has no playable variant (${n}); it will not begin.`,{breakId:r.id,reason:n}),this.deps.emit("aderror",{type:"aderror",timestamp:Date.now(),source:"ad",break:r,format:this.deps.effectiveFormatOf(r),error:new Error(`Break ${r.id} has no playable variant (${n})`)}),d!==this.state.sessionGeneration)return;return this.deps.breakScheduler.completeBreak(r.id),this.onBreakSkipped(r,"unsupported"),void this.deps.onBreakSettled?.()}this.deps.log("Break starting:",r.id,s.tuneIn?"(tune-in)":""),this.deps.breakTransitionTimer?.mark("into-break",r.id),this.breakBeginEmittedId=null,this.pendingAdBreakBegin=null;const o=this.state.heldImmediatePreRollId===r.id,p=this.deps.effectiveFormatOf(r),k=p===e.SINGLE||p===e.LSHAPE_AD,h=o&&k;o&&(this.state.heldImmediatePreRollId=null),this.deps.adBreakStatusTracker.setAdBreakStatusActive(r,void 0!==s.remainingSec?s.remainingSec:r.duration),h||this.emitAdBreakBegin(r,s),this.deps.manifestService.setActive(!0),void 0===s.snapback?.resumePosition&&this.deps.breakScheduler.claimChain(r.id),this.recordBreakBegan(r,!1);const l={onAdStart:(e,t,a,i,r)=>{this.deps.adBreakStatusTracker.onAdStarted(e,t,a,i),this.deps.emit("adbegin",{type:"adbegin",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,adIndex:a,totalAds:i,...r?.adId?{adId:r.adId}:{},...r?.creativeId?{creativeId:r.creativeId}:{},...void 0!==r?.podPosition?{podPosition:r.podPosition}:{}})},onAdEnd:(e,t,a,i,r)=>{this.deps.adBreakStatusTracker.onAdEnded(e,a,i,t.id),a+1<i?this.deps.breakTransitionTimer?.mark("ad-to-ad",e.id):this.deps.breakTransitionTimer?.noteOutgoingEnd(),this.deps.emit("adend",{type:"adend",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,adIndex:a,totalAds:i,...r?.adId?{adId:r.adId}:{},...r?.creativeId?{creativeId:r.creativeId}:{}})},onAdError:(e,r,s,d)=>{this.flushPendingAdBreakBegin(),this.deps.adBreakStatusTracker.onAdError(e,r,d),this.deps.diagnose("DA-AD-PLAYBACK-ERROR",s.message,{breakId:e.id,assetId:r?.id,adErrorCode:a(s),...s instanceof i?{vendorCode:s.vendorCode}:{},...t(s)}),this.deps.emit("aderror",{type:"aderror",timestamp:Date.now(),source:"ad",break:e,format:this.deps.effectiveFormatOf(e),...r?{asset:r}:{},error:s})},onAdTimeUpdate:(e,t,a,i)=>{this.deps.adBreakStatusTracker.onAdTimeUpdated(e,t,a,i),this.deps.emit("adtimeupdate",{type:"adtimeupdate",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,currentTime:a,duration:i})},onSlateProgress:(e,t,a)=>this.deps.adBreakStatusTracker.onSlateProgress(e,t,a),onAdWaiting:(e,t)=>{this.deps.adBreakStatusTracker.onAdPlaybackWaiting(),this.deps.emit("waiting",{type:"waiting",timestamp:Date.now(),source:"ad",break:e,asset:t})},onAdPlaying:(e,t)=>{this.deps.adBreakStatusTracker.onAdPlaybackPlaying(),this.deps.emit("playing",{type:"playing",timestamp:Date.now(),source:"ad",break:e,asset:t})},onBreakEnd:e=>{this.onBreakEnd(e)},onBreakCut:e=>{this.deps.cmcdAdReporter.onBreakCut(e.id)},onBreakEndedWithoutContentPause:()=>{this.onBreakEndedWithoutContentPause()},onChainedBreakBegin:e=>{this.onChainedBreakBegin(e)},onSuccessorBuffered:(e,t)=>this.deps.breakTransitionTimer?.noteSuccessorBuffered(e,t),onSuccessorDropped:e=>this.deps.breakTransitionTimer?.noteSuccessorDropped(e),onChainedBreakSkipped:e=>{d===this.state.sessionGeneration&&(this.deps.log("Chained break skipped (no playable variant):",e.id),this.deps.breakScheduler.completeBreak(e.id),this.onBreakSkipped(e,"unsupported"))},onContentResumeSeek:(e,t)=>{this.deps.breakScheduler.notifyResumeSeek(e,t)},onContentReloading:e=>{this.deps.breakScheduler.setContentReloading(e)}};h&&(this.pendingAdBreakBegin=()=>this.emitAdBreakBegin(r,s),l.onBreakVisible=e=>{this.deps.adBreakStatusTracker.setAdBreakVisible(e),this.flushPendingAdBreakBegin()}),await this.deps.adPlayerController.playBreak(r,l,{effectiveDurationSec:s.remainingSec,snapbackResumePosition:s.snapback?.resumePosition,firstFrameGate:h})}onChainedBreakBegin(e){this.deps.log("Chained break starting:",e.id),this.deps.breakTransitionTimer?.mark("break-to-break",e.id),this.deps.breakScheduler.activateChainedBreak(e.id),this.recordBreakBegan(e,!0),this.deps.manifestService.setActive(!0),this.deps.adBreakStatusTracker.setAdBreakStatusActive(e,e.duration),this.emitAdBreakBegin(e)}onBreakEnd(e){this.deps.log("Break ended:",e.id),this.deps.breakTransitionTimer?.mark("out-of-break",e.id),this.deps.breakChainRecorder?.breakEnded(e.id),this.deps.breakScheduler.completeBreak(e.id),this.deps.manifestService.setActive(!1),this.flushPendingAdBreakBegin(),this.deps.adBreakStatusTracker.completeAdBreakStatus(e),this.deps.adBreakStatusTracker.resetAdBreakStatus(),this.deps.emit("adbreakend",{type:"adbreakend",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e)}),this.deps.onBreakEnded?.(),this.deps.onBreakSettled?.()}onBreakEndedWithoutContentPause(){this.deps.breakTransitionTimer?.settle("out-of-break"),this.deps.emit("playing",{type:"playing",timestamp:Date.now(),source:"content"})}onBreakAborted(e){if(this.deps.adPlayerController.isAdPlaying()){const t=this.deps.adBreakStatusTracker.currentBreakElapsedSec();this.deps.log(`Early return — ending break '${e.id}' at ${t.toFixed(2)}s`),this.deps.cmcdAdReporter.onBreakAborted(e.id),this.deps.adPlayerController.abortBreak(t)}else this.deps.breakScheduler.completeBreak(e.id),this.deps.onBreakSettled?.()}reportAbandonedBreak(e){const t=this.deps.adBreakStatusTracker.activeBreak;if(!t)return;const a=this.deps.adBreakStatusTracker.currentAdIndex;this.deps.diagnose("DA-BREAK-ABANDONED",`Session torn down (${e}) while break ${t.id} was still in flight`+(null!==a?"; an ad began and will never end.":"."),{breakId:t.id,cause:e,phase:this.deps.adBreakStatusTracker.adBreakStatus.phase,unbalancedAdBegin:null!==a,...null!==a?{adIndex:a}:{},...null!==this.deps.adBreakStatusTracker.currentAdTotal?{totalAds:this.deps.adBreakStatusTracker.currentAdTotal}:{}})}}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { BreakManifest } from '../types/BreakManifest';
|
|
2
|
+
import type { SessionStarterDeps } from './SessionStarterDeps';
|
|
3
|
+
import type { SessionState } from './SessionState';
|
|
4
|
+
export declare class ContentPlaybackStart {
|
|
5
|
+
private readonly state;
|
|
6
|
+
private readonly deps;
|
|
7
|
+
private initialized;
|
|
8
|
+
private vod;
|
|
9
|
+
private settled;
|
|
10
|
+
private playbackSeen;
|
|
11
|
+
private immediatePreRoll;
|
|
12
|
+
constructor(state: SessionState, deps: SessionStarterDeps);
|
|
13
|
+
start(manifest: BreakManifest, holdPreRoll?: boolean): void;
|
|
14
|
+
onPlaying(): void;
|
|
15
|
+
check(): boolean;
|
|
16
|
+
private contentIsReady;
|
|
17
|
+
private clearInterval;
|
|
18
|
+
private arm;
|
|
19
|
+
private waitForContent;
|
|
20
|
+
cancelOnContentError(): boolean;
|
|
21
|
+
private clearPreRoll;
|
|
22
|
+
clear(): void;
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{getPreRollDelay as e,Timebase as t}from"../types/BreakManifest";const l=1e4;export class ContentPlaybackStart{constructor(e,t){this.state=e,this.deps=t,this.initialized=!1,this.vod=!1,this.settled=!1,this.playbackSeen=!1}start(l,a=!0){this.clear(),this.initialized=!0,this.vod=l.timebase===t.MEDIATIME,this.playbackSeen=!this.deps.player.paused,this.immediatePreRoll=!a||l.adStartDelay&&l.adStartDelay>0?void 0:l.breaks.find(t=>0===e(t)),this.immediatePreRoll&&(this.state.heldImmediatePreRollId=this.immediatePreRoll.id,this.state.heldPreRollWaitStartedAt=Date.now()),this.vod?this.check():this.immediatePreRoll?(this.deps.adPlayerController.holdContentForPreRoll(this.immediatePreRoll),this.contentIsReady()?this.arm():this.state.heldPreRollArmInterval=setInterval(()=>this.waitForContent(),250)):this.playbackSeen&&this.deps.breakScheduler.markPlaybackStart()}onPlaying(){this.initialized&&(this.playbackSeen=!0,this.vod&&!this.settled?this.check():this.deps.breakScheduler.markPlaybackStart())}check(){if(!this.initialized||!this.vod||this.settled)return!0;const e=this.deps.player;return!(!1===e.playbackReady||!Number.isFinite(e.currentTime))&&(!(!0!==e.playbackReady&&!this.contentIsReady()&&!this.playbackSeen)&&(!(!this.immediatePreRoll&&!this.playbackSeen&&e.paused)&&(this.settled=!0,this.immediatePreRoll&&e.currentTime<=.5?(this.deps.adPlayerController.holdContentForPreRoll(this.immediatePreRoll),this.arm()):(this.state.heldImmediatePreRollId=null,this.state.heldPreRollWaitStartedAt=null,this.deps.breakScheduler.markPlaybackStart()),!0)))}contentIsReady(){return!Number.isNaN(this.deps.player.duration)||!this.deps.player.paused}clearInterval(){this.state.heldPreRollArmInterval&&(clearInterval(this.state.heldPreRollArmInterval),this.state.heldPreRollArmInterval=null)}arm(){this.state.heldImmediatePreRollId&&!this.state.heldPreRollArmed&&(this.clearInterval(),this.state.heldPreRollArmed=!0,this.deps.breakScheduler.markPlaybackStart())}waitForContent(){if(this.contentIsReady())return void this.arm();const e=this.state.heldPreRollWaitStartedAt;if(null===e||Date.now()-e<l)return;const t=this.state.heldImmediatePreRollId;this.arm(),this.deps.diagnose("DA-PREROLL-CONTENT-WAIT-TIMEOUT","Pending pre-roll began after waiting 10000ms for content readiness.",{breakId:t,waitMs:l})}cancelOnContentError(){const e=this.state.heldImmediatePreRollId;return!(!e||this.state.heldPreRollArmed)&&(this.clearPreRoll(),this.deps.breakScheduler?.completeBreak(e),this.deps.adPlayerController?.releaseContentHold(),this.deps.diagnose("DA-PREROLL-CANCELLED-CONTENT-ERROR","A pending pre-roll was cancelled because the content failed to load before it could begin.",{breakId:e}),!0)}clearPreRoll(){this.clearInterval(),this.immediatePreRoll=void 0,this.state.heldImmediatePreRollId=null,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=null}clear(){this.clearPreRoll(),this.initialized=!1,this.settled=!1,this.playbackSeen=!1}}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { PlayerAdapter } from '../interfaces/PlayerAdapter';
|
|
2
|
+
import type { SessionConfig } from '../types/OptiViewAdsConfig';
|
|
3
|
+
import type { Diagnose, Log } from './BreakLifecycle';
|
|
4
|
+
import type { SessionState } from './SessionState';
|
|
5
|
+
/** How long startSession() waits for a `pending` player discovery to settle. */
|
|
6
|
+
export declare const DISCOVERED_MANIFEST_URL_TIMEOUT_MS = 10000;
|
|
7
|
+
export declare const MANIFEST_URL_REQUIRED_MESSAGE = "[OptiViewAds] startSession requires a manifestUrl (the full break-manifest URL).";
|
|
8
|
+
export declare const DISCOVERY_NO_ADS_MESSAGE = "[OptiViewAds] startSession has no manifestUrl and the player discovered no ads manifest URL.";
|
|
9
|
+
export declare const DISCOVERY_TIMEOUT_MESSAGE = "[OptiViewAds] startSession has no manifestUrl and no ads manifest URL was discovered within 10 s.";
|
|
10
|
+
export type ManifestUrlResolution = {
|
|
11
|
+
kind: 'url';
|
|
12
|
+
url: string;
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'superseded';
|
|
15
|
+
};
|
|
16
|
+
interface DiscoveryDeps {
|
|
17
|
+
player: PlayerAdapter;
|
|
18
|
+
diagnose: Diagnose;
|
|
19
|
+
log: Log;
|
|
20
|
+
manifestUrlDiscovery: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Whether the adapter implements manifest-URL discovery. Both members ship together; a present
|
|
24
|
+
* state object means the adapter reports a real status rather than "capability absent".
|
|
25
|
+
*/
|
|
26
|
+
export declare function playerCanDiscoverManifestUrl(player: PlayerAdapter): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the ads manifest URL from the player's discovery state, waiting out `pending` up to
|
|
29
|
+
* `timeoutMs`. Rejects with the settled error messages; a superseding startSession/endSession/
|
|
30
|
+
* destroy (a changed `state.discoveryWaitGeneration` vs `waitToken`) resolves `{kind:'superseded'}`
|
|
31
|
+
* instead so the stale call exits quietly.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveDiscoveredManifestUrl(state: SessionState, deps: DiscoveryDeps, waitToken: number, timeoutMs?: number): Promise<ManifestUrlResolution>;
|
|
34
|
+
/**
|
|
35
|
+
* Auto-restart wiring: while a session whose URL came from discovery is active, a different
|
|
36
|
+
* `available(url)` restarts the session through the normal path (manifestUrl stays omitted) and
|
|
37
|
+
* `unavailable` ends it. Returns the unsubscribe function; a no-op cleanup for adapters without
|
|
38
|
+
* the capability.
|
|
39
|
+
*/
|
|
40
|
+
export declare function subscribeManifestUrlDiscovery(state: SessionState, deps: DiscoveryDeps & {
|
|
41
|
+
startSession: (config: SessionConfig) => Promise<void>;
|
|
42
|
+
endSession: (cause: string) => void;
|
|
43
|
+
}): () => void;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DISCOVERED_MANIFEST_URL_TIMEOUT_MS=1e4;export const MANIFEST_URL_REQUIRED_MESSAGE="[OptiViewAds] startSession requires a manifestUrl (the full break-manifest URL).";export const DISCOVERY_NO_ADS_MESSAGE="[OptiViewAds] startSession has no manifestUrl and the player discovered no ads manifest URL.";export const DISCOVERY_TIMEOUT_MESSAGE="[OptiViewAds] startSession has no manifestUrl and no ads manifest URL was discovered within 10 s.";export function playerCanDiscoverManifestUrl(e){return"function"==typeof e.onDiscoveredAdsManifestUrlChange&&void 0!==e.discoveredAdsManifestUrl}export async function resolveDiscoveredManifestUrl(e,s,r,t=1e4){const{player:i,diagnose:n}=s,a=()=>e.discoveryWaitGeneration!==r;if(!s.manifestUrlDiscovery)throw n("DA-MANIFEST-URL-UNAVAILABLE",MANIFEST_URL_REQUIRED_MESSAGE,{reason:"disabled"}),new Error(MANIFEST_URL_REQUIRED_MESSAGE);if(!playerCanDiscoverManifestUrl(i))throw n("DA-MANIFEST-URL-UNAVAILABLE",MANIFEST_URL_REQUIRED_MESSAGE,{reason:"unsupported-adapter"}),new Error(MANIFEST_URL_REQUIRED_MESSAGE);const o=e=>(n("DA-MANIFEST-URL-DISCOVERED",`Session uses discovered ads manifest ${e}`,{manifestUrl:e,source:"player-discovery"}),{kind:"url",url:e}),E=()=>{throw n("DA-MANIFEST-URL-UNAVAILABLE",DISCOVERY_NO_ADS_MESSAGE,{reason:"no-ads"}),new Error(DISCOVERY_NO_ADS_MESSAGE)},d=i.discoveredAdsManifestUrl;return"available"===d?.status?o(d.url):"unavailable"===d?.status?E():new Promise((s,r)=>{let d=!1;const l=()=>{clearTimeout(c),f(),U()},S=e=>{d||(d=!0,l(),s(e()))},A=e=>{if(!d){d=!0,l();try{e()}catch(e){r(e)}}},U=e.addDiscoveryWaiter(()=>{S(()=>({kind:"superseded"}))}),c=setTimeout(()=>{a()?S(()=>({kind:"superseded"})):A(()=>{throw n("DA-MANIFEST-URL-UNAVAILABLE",DISCOVERY_TIMEOUT_MESSAGE,{reason:"timeout"}),new Error(DISCOVERY_TIMEOUT_MESSAGE)})},t),f=i.onDiscoveredAdsManifestUrlChange(e=>{a()?S(()=>({kind:"superseded"})):"available"===e.status?S(()=>o(e.url)):"unavailable"===e.status&&A(E)})})}export function subscribeManifestUrlDiscovery(e,s){const{player:r}=s;return s.manifestUrlDiscovery&&playerCanDiscoverManifestUrl(r)?r.onDiscoveredAdsManifestUrlChange(r=>{try{if(!e.sessionActive||!e.manifestUrlFromDiscovery)return;if("pending"===r.status)return;if("unavailable"===r.status)return void s.endSession("discovery-no-ads");if(r.url===e.resolvedManifestUrl)return;const t=e.sessionConfig;if(!t)return;s.startSession(t).catch(e=>{s.log("Session restart after manifest URL discovery failed:",e)})}catch(e){s.log("Manifest URL discovery listener failed:",e)}}):()=>{}}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Break, type BreakManifest } from '../types/BreakManifest';
|
|
2
|
+
import type { ScheduledBreak } from '../types/ScheduledBreak';
|
|
3
|
+
import { type ScheduledBreakInputs } from './buildScheduledBreaks';
|
|
4
|
+
export declare class ScheduledBreaksTracker {
|
|
5
|
+
private readonly emit;
|
|
6
|
+
private readonly inputs;
|
|
7
|
+
private manifest;
|
|
8
|
+
private played;
|
|
9
|
+
private excluded;
|
|
10
|
+
private breaks;
|
|
11
|
+
private revision;
|
|
12
|
+
get generation(): number;
|
|
13
|
+
constructor(emit: (breaks: ScheduledBreak[]) => void, inputs?: ScheduledBreakInputs);
|
|
14
|
+
get(): ScheduledBreak[];
|
|
15
|
+
update(manifest: BreakManifest): void;
|
|
16
|
+
resetForChannelSwitch(manifest: BreakManifest): void;
|
|
17
|
+
refresh(): void;
|
|
18
|
+
markPlayed(id: string): void;
|
|
19
|
+
exclude(id: string): void;
|
|
20
|
+
skipBookmark(brk: Break): void;
|
|
21
|
+
reset(): void;
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{BreakEvent as e,getBreakEvent as s}from"../types/BreakManifest";import{buildScheduledBreaks as t}from"./buildScheduledBreaks";export class ScheduledBreaksTracker{get generation(){return this.revision}constructor(e,s={}){this.emit=e,this.inputs=s,this.manifest=null,this.played=new Set,this.excluded=new Set,this.breaks=[],this.revision=0}get(){return this.breaks.map(e=>({...e}))}update(e){this.manifest=e,this.refresh()}resetForChannelSwitch(e){this.revision++,this.played.clear(),this.excluded.clear(),this.update(e)}refresh(){const e=t(this.manifest,{...this.inputs,played:this.played,excluded:this.excluded});JSON.stringify(e)!==JSON.stringify(this.breaks)&&(this.breaks=e,this.emit(this.get()))}markPlayed(e){this.played.add(e),this.refresh()}exclude(e){this.excluded.add(e),this.refresh()}skipBookmark(t){s(t)===e.START&&this.exclude(t.id)}reset(){this.revision++,this.manifest=null,this.played.clear(),this.excluded.clear(),this.refresh()}}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BreakManifest } from '../types/BreakManifest';
|
|
2
|
+
import { ContentPlaybackStart } from './ContentPlaybackStart';
|
|
2
3
|
import { OptiViewAdsMode, SessionConfig, SsaiSessionConfig, StartSessionConfig } from '../types/OptiViewAdsConfig';
|
|
3
4
|
import { SessionState } from './SessionState';
|
|
4
5
|
import type { SessionStarterDeps } from './SessionStarterDeps';
|
|
@@ -6,6 +7,7 @@ import type { SessionStarterDeps } from './SessionStarterDeps';
|
|
|
6
7
|
export declare class SessionStarter {
|
|
7
8
|
private readonly state;
|
|
8
9
|
private readonly deps;
|
|
10
|
+
readonly playbackStart: ContentPlaybackStart;
|
|
9
11
|
constructor(state: SessionState, deps: SessionStarterDeps);
|
|
10
12
|
/**
|
|
11
13
|
* Start monetization for a piece of content: begins manifest polling and (if configured)
|
|
@@ -13,15 +15,6 @@ export declare class SessionStarter {
|
|
|
13
15
|
*/
|
|
14
16
|
startSession(config: StartSessionConfig): Promise<void>;
|
|
15
17
|
discardPreparedGamSessionAfterSeek(): void;
|
|
16
|
-
/**
|
|
17
|
-
* Content already playing when the session starts emits no further `playing`; the pre-roll
|
|
18
|
-
* anchor (spec `event: 'start'`) is then the session start itself.
|
|
19
|
-
*/
|
|
20
|
-
private markPlaybackStartIfPlaying;
|
|
21
|
-
private contentIsReady;
|
|
22
|
-
private clearHeldImmediatePreRollArmInterval;
|
|
23
|
-
private armHeldImmediatePreRoll;
|
|
24
|
-
private armHeldImmediatePreRollIfTimedOut;
|
|
25
18
|
/**
|
|
26
19
|
* Content errors can arrive before the held pre-roll is armed. Cancel that pending break so
|
|
27
20
|
* an erroring content load never reveals an ad that was scheduled only for the failed stream.
|
|
@@ -52,6 +45,7 @@ export declare class SessionStarter {
|
|
|
52
45
|
startManifestPolling(): void;
|
|
53
46
|
applyPendingChannelSwitch(): void;
|
|
54
47
|
private applyManifestUpdate;
|
|
48
|
+
private readonly manifestPollingActions;
|
|
55
49
|
private handleEventBoundaryTick;
|
|
56
50
|
/**
|
|
57
51
|
* A poll returned a manifest of another channel: re-align the ad setup after the current break
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{sessionTick as e}from"./sessionTick";import{getPreRollDelay as s}from"../types/BreakManifest";import{manifestHasGamAssets as t,resolveGamVendorConfiguration as a,resolveSsbStream as i}from"../services/resolveGamVendorConfiguration";import{SsaiController as r}from"../services/SsaiController";import{resolveUrlOrigin as n}from"../services/urlOrigin";import{filterSsaiClientBreaks as o,resolveStitchedStreamVendor as d,SsaiUnavailableError as l}from"../services/resolveDeliveryMode";import{detectChannelSwitch as h,gamPodIdentityChanged as c}from"../services/channelSwitch";import{prepareWebRuntime as p}from"../services/webCompatibility";import{adoptExternalVendorConfigurations as S,diagnoseExternalVendorConfigurations as m,diagnoseIgnoredDeliveryModeChange as f,emitChannelChange as u,handleEventBoundaryTick as A,isStaleSessionStart as y,isNonBlankString as g,isSsaiSessionConfig as v,rejectGamPodPreloadForBoundaryIdentity as P,resolveEventAtPosition as I,resolveSessionDeliveryMode as k}from"./SessionStarterHelpers";const C=1e4;export class SessionStarter{constructor(e,s){this.state=e,this.deps=s}async startSession(t){if("ssai"===this.deps.mode){if(!v(t)||!g(t.stitcherUrl))throw new Error('[OptiViewAds] mode "ssai" requires a stitcherUrl in startSession().')}else if(v(t)||!g(t.manifestUrl))throw new Error("[OptiViewAds] startSession requires a manifestUrl (the full break-manifest URL).");this.state.sessionActive&&(this.deps.log("Ending previous session before starting new one"),this.deps.endSessionInternal("session-restart"));const a=this.state.sessionGeneration;this.state.sessionActive=!0,this.state.adStartDelayElapsed=!1;try{const e=p();e&&await e}catch(e){if(this.isStaleStart(a))return;throw this.state.sessionActive=!1,e}if(this.isStaleStart(a))return;if(this.deps.lensReporter.startSession(),v(t))return t.adInsertionType&&this.deps.diagnose("DA-INSERTION-TYPE-IGNORED-SSAI",`adInsertionType "${t.adInsertionType}" ignored in SSAI mode; the stitcher controls ad insertion.`,{adInsertionType:t.adInsertionType}),void await this.startSsaiSession(t,a);const r=t.manifestUrl;this.state.sessionConfig=t,this.state.manifestOrigin=n(r),this.deps.manifestService.setManifestUrl(r),this.deps.log("Session starting, manifest URL:",r);const o=Date.now(),d=e=>{this.deps.diagnose("DA-SESSION-STARTING",`Session start: ${e}`,{step:e,manifestUrl:r,elapsedMs:Date.now()-o})};d("begin");const l=t.assetParameters;let h,c;this.deps.adPlayerController.setSessionAssetParameters(l),this.deps.adPlayerController.setSessionAssetParameterMacros(t.assetParameterMacros),this.state.configSummary.gam&&(this.state.configSummary.gam={...this.state.configSummary.gam,hasAssetParameters:!!l}),d("manifest-fetch");try{c=await this.deps.manifestService.fetchManifest();const e=I(c,Date.now(),this.deps.player);h=e.manifest,this.state.sessionManifest=c,this.state.effectiveSessionManifest=h,this.state.activeEventId=e.eventId}catch(e){if(this.isStaleStart(a))return;throw this.failSessionStart(e)}if(this.deps.cmcdAdReporter.onSessionStart(h),!this.isStaleStart(a))if(this.deps.externalPlayback.setManaged(null!==i(h.vendorConfiguration)),S(this.deps,h),"ssai"!==this.resolveSessionDeliveryMode(h))try{if(await this.startGamForManifest(h,l,d,a,!0),this.isStaleStart(a))return;const i=t.adInsertionType;this.deps.adPlayerController.setBreakResumePolicy(i,h.timebase,this.deps.continueContentDuringBreak),this.deps.diagnose("DA-INSERTION-TYPE-RESOLVED",void 0===i?`Ad insertion type: unset — the manifest's resumeOffset drives the resume point (timebase: ${h.timebase})`:`Ad insertion type: ${i}, client-set (timebase: ${h.timebase})`,{adInsertionType:i??null,source:void 0===i?"unset":"client",timebase:h.timebase}),d("scheduling"),this.deps.breakScheduler.updateManifest(h),this.deps.pauseAdController.updateManifest(h),this.state.pauseAdTickInterval=setInterval(e(this.deps),250);const n=(h.adStartDelay??0)>0?void 0:h.breaks.find(e=>0===s(e));n?(this.deps.adPlayerController.holdContentForPreRoll(n),this.state.heldImmediatePreRollId=n.id,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=Date.now(),this.contentIsReady()?this.armHeldImmediatePreRoll():this.state.heldPreRollArmInterval=setInterval(()=>{this.contentIsReady()?this.armHeldImmediatePreRoll():this.armHeldImmediatePreRollIfTimedOut()},250)):this.markPlaybackStartIfPlaying(),this.deps.startLivePushback(),this.startBreakScheduler(),this.startManifestPolling(),this.deps.diagnose("DA-SESSION-STARTED",`Session active for manifest ${r}`,{manifestUrl:r}),this.deps.log("Session active")}catch(e){if(this.isStaleStart(a))return;throw this.failSessionStart(e)}else await this.startSteeredSsaiSession(h,t,d)}discardPreparedGamSessionAfterSeek(){this.state.sessionActive&&(this.deps.adPlayerController?.gamPodBridge.discardPreparedGamSession(),this.state.eventSessionPreloadKeys.clear())}markPlaybackStartIfPlaying(){this.deps.player.paused||this.deps.breakScheduler.markPlaybackStart()}contentIsReady(){return!Number.isNaN(this.deps.player.duration)||!this.deps.player.paused}clearHeldImmediatePreRollArmInterval(){this.state.heldPreRollArmInterval&&(clearInterval(this.state.heldPreRollArmInterval),this.state.heldPreRollArmInterval=null)}armHeldImmediatePreRoll(){this.state.heldImmediatePreRollId&&!this.state.heldPreRollArmed&&(this.clearHeldImmediatePreRollArmInterval(),this.state.heldPreRollArmed=!0,this.deps.breakScheduler.markPlaybackStart())}armHeldImmediatePreRollIfTimedOut(){if(!this.state.heldImmediatePreRollId||this.state.heldPreRollArmed)return;const e=this.state.heldPreRollWaitStartedAt;if(null===e||Date.now()-e<C)return;const s=this.state.heldImmediatePreRollId;this.armHeldImmediatePreRoll(),this.deps.diagnose("DA-PREROLL-CONTENT-WAIT-TIMEOUT","Pending pre-roll began after waiting 10000ms for content readiness.",{breakId:s,waitMs:C})}cancelHeldImmediatePreRollOnContentError(){const e=this.state.heldImmediatePreRollId;return!(!e||this.state.heldPreRollArmed)&&(this.clearHeldImmediatePreRollArmInterval(),this.deps.breakScheduler?.completeBreak(e),this.deps.adPlayerController?.releaseContentHold(),this.state.heldImmediatePreRollId=null,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=null,this.deps.diagnose("DA-PREROLL-CANCELLED-CONTENT-ERROR","A pending pre-roll was cancelled because the content failed to load before it could begin.",{breakId:e}),!0)}clearHeldImmediatePreRollArm(){this.clearHeldImmediatePreRollArmInterval(),this.state.heldImmediatePreRollId=null,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=null}failSessionStart(e){const s=e instanceof Error?e:new Error(String(e)),t=this.deps.manifestService?.takeLastFailure();return this.deps.diagnose("DA-MANIFEST-FETCH-FAILED",s.message,t?{...t}:void 0),this.state.sessionActive=!1,this.deps.cmcdAdReporter.onSessionEnd(),this.deps.lensReporter.endSession(),s}resolveSessionDeliveryMode(e,s="manifest"){return k(this.state,this.deps,e,s)}async startSteeredSsaiSession(s,t,a){const i=d(s.vendorConfiguration);if(!i){const e="Manifest selected ssai delivery but no vendor configuration provides a stitched stream (no gam entry with an assetKey).";throw this.deps.diagnose("DA-SSAI-UNAVAILABLE",e),this.deps.manifestService.reset(),this.state.sessionActive=!1,new l(e)}try{a("ssai-live-stream"),this.state.steeredSsaiActive=!0,this.state.ssaiController=this.createSsaiController(i.networkCode),await this.state.ssaiController.startSteered(i,t.assetParameters,t.assetParameterMacros),a("scheduling");const r=o(s);this.deps.adPlayerController.setBreakResumePolicy(t.adInsertionType,s.timebase,!1),this.deps.breakScheduler.updateManifest(r),this.deps.pauseAdController.updateManifest(r),this.state.pauseAdTickInterval=setInterval(e(this.deps),250),this.markPlaybackStartIfPlaying(),this.deps.startLivePushback(),this.startBreakScheduler(),this.startManifestPolling(),this.deps.diagnose("DA-SESSION-STARTED",`SSAI session active (manifest-steered) for ${t.manifestUrl}`,{manifestUrl:t.manifestUrl}),this.deps.log("SSAI session active (manifest-steered)")}catch(e){const s=e instanceof Error?e:new Error(String(e));throw this.deps.diagnose("DA-SSAI-SESSION-FAILED",s.message),this.state.steeredSsaiActive=!1,this.deps.manifestService.reset(),this.state.sessionActive=!1,s}}createSsaiController(e){return new r(this.deps.player,{gam:this.state.steeredSsaiOptions?.streamActivityMonitorId?{streamActivityMonitorId:this.state.steeredSsaiOptions.streamActivityMonitorId}:{},ssai:{networkCode:e},maxBitrate:this.state.steeredSsaiOptions?.maxBitrate,autoplay:this.state.steeredSsaiOptions?.autoplay,debug:this.deps.debug},{emit:(e,s)=>this.deps.emit(e,s),diagnose:(e,s,t)=>this.deps.diagnose(e,s,t),log:(...e)=>this.deps.log(...e)})}startBreakScheduler(){this.deps.breakScheduler.setEventBoundaryCheck(e=>this.handleEventBoundaryTick(e)),this.deps.breakScheduler.start((e,s)=>{this.deps.breakLifecycle.onBreakStart(e,s)},e=>{if(this.deps.adPlayerController.isAdPlaying())return!1;if(this.deps.adPlayerController.gamPodBridge.shouldSkipGamPodPreload(e))return!1;const s=P(this.state,this.deps,this.deps.player,e);return"defer"!==s&&("mismatch"===s||this.deps.adPlayerController.preload(e).catch(s=>{this.deps.diagnose("DA-PRELOAD-FAILED",s instanceof Error?s.message:String(s),{breakId:e.id})}),!0)},(e,s,t)=>{this.deps.adBreakStatusTracker.onBreakWarning(e,s,t)},e=>{this.deps.breakLifecycle.onBreakAborted(e)},(e,s)=>this.deps.breakLifecycle.onBreakSkipped(e,s))}startManifestPolling(){this.deps.manifestService.startPolling(e=>{if(!this.state.sessionActive)return;const s=this.deps.notedManifestBreakIds(e);this.deps.diagnose("DA-MANIFEST-POLLED","Manifest polled",{breaks:e.breaks.length,newBreakIds:s,pollingIntervalSeconds:this.deps.manifestService.currentPollingIntervalSeconds});const t=this.state.sessionManifest,a=t?h(t,e):null;if(this.state.pendingChannelSwitch||a&&(this.deps.breakScheduler.getActiveBreak()||this.state.ssb))return this.state.pendingChannelSwitch=e,void this.deps.log("Channel switch deferred until the active break ends");if(a)return this.state.sessionManifest=e,this.state.pendingManifestUpdate=null,void this.switchChannel(e,a);const i="wallclock"===e.timebase&&!1!==this.deps.player.isLive&&Boolean(e.events?.length||t?.events?.length);this.state.sessionManifest=e,i?this.state.pendingManifestUpdate=e:(this.state.pendingManifestUpdate=null,this.state.effectiveSessionManifest=e,this.applyManifestUpdate(e))},e=>{this.state.sessionActive&&this.deps.diagnose("DA-MANIFEST-POLL-FAILED",e.message)})}applyPendingChannelSwitch(){if(!this.state.sessionActive||!this.state.pendingChannelSwitch||this.state.ssb)return;const e=this.state.pendingChannelSwitch;this.state.pendingChannelSwitch=null;const s=h(this.state.sessionManifest,e);s?this.switchChannel(e,s):this.applyManifestUpdate(e)}applyManifestUpdate(e){if(!this.state.sessionActive)return;S(this.deps,e),f(this.state,this.deps,e);const s=a(e);this.state.steeredSsaiActive?this.state.ssaiController?.setManifestAssetParameters(s.ssai?.assetParameters):this.deps.adPlayerController.setManifestAssetParameters(s.sgaiAssetParameters);const t=this.state.steeredSsaiActive?o(e):e;this.deps.breakScheduler.updateManifest(t),this.deps.pauseAdController.updateManifest(t)}handleEventBoundaryTick(e){return A(this.state,this.deps,e,{resolveDeliveryMode:e=>this.resolveSessionDeliveryMode(e,"event-boundary"),applyManifestUpdate:e=>this.applyManifestUpdate(e),switchChannel:(e,s,t,a)=>this.switchChannel(e,s,t,a)})}async switchChannel(e,s,t=!1,a=e){if(this.state.channelSwitchInFlight)return;const r=this.state.sessionConfig,n=this.state.effectiveSessionManifest??this.state.sessionManifest;if(!t){const s=this.deps.player.programDateTime?.getTime()??Date.now(),t=I(a,s,this.deps.player);e=t.manifest,this.state.activeEventId=t.eventId}const o=t?"event-boundary":s.reason,d={previousChannelId:s.previousChannelId,channelId:s.channelId,eventId:this.state.activeEventId,reason:o,manifestUrl:r.manifestUrl};this.deps.diagnose("DA-MANIFEST-CHANNEL-CHANGED",`Manifest channel changed (${o}): ${s.previousChannelId??"none"} -> ${s.channelId??"none"}`,d),this.state.sessionManifest=a,this.state.effectiveSessionManifest=e,this.state.pendingManifestUpdate=null,this.deps.externalPlayback.setManaged(null!==i(e.vendorConfiguration)),this.state.channelSwitchInFlight=!0;const l=this.state.sessionGeneration,h=this.state.sessionDeliveryMode??"sgai";try{let s=this.resolveSessionDeliveryMode(e,t?"event-boundary":"channel-switch");if(t?this.deps.cmcdAdReporter.updateEventId(this.state.activeEventId):(this.deps.cmcdAdReporter.onSessionEnd(),this.deps.cmcdAdReporter.onSessionStart(e),this.deps.lensReporter.endSession(),this.deps.lensReporter.startSession()),"ssai"===s&&"sgai"===h){const a=await this.switchToSteeredSsai(e,r,t);if(this.isStaleStart(l))return;a||(s="sgai",this.state.sessionDeliveryMode="sgai",await this.applySameModeChannelSwitch(n,e,r,l,t))}else"sgai"===s&&"ssai"===h?await this.switchToSgai(e,r,l,t):await this.applySameModeChannelSwitch(n,e,r,l,t);if(this.isStaleStart(l))return}finally{this.state.channelSwitchInFlight=!1,t&&this.deps.breakScheduler.releaseBreakStarts()}S(this.deps,e),this.deps.startLivePushback(),u(this.deps,{...d,previousDeliveryMode:h,deliveryMode:this.state.sessionDeliveryMode??h})}async applySameModeChannelSwitch(e,s,t,i,r=!1){const n=this.state.steeredSsaiActive,d=n?o(s):s;if(r?this.deps.breakScheduler.applyEventBoundary(d):this.deps.breakScheduler.resetForChannelSwitch(d),this.deps.pauseAdController.updateManifest(d),this.deps.adPlayerController.setBreakResumePolicy(t.adInsertionType,s.timebase,this.deps.continueContentDuringBreak),!n&&c(e,s)){const e=this.deps.adPlayerController,n=a(s);if(!(!r||!n.identity)&&await e.gamPodBridge.adoptPreparedGamSession(n.identity,t.assetParameters,n.sgaiAssetParameters))e.updatedAssetParameters&&e.updateAssetParameters(e.updatedAssetParameters);else{const a=e.updatedAssetParameters,r=e.sessionAssetParameterMacros;if(e.endGamSession(),e.setSessionAssetParameters(t.assetParameters),e.setSessionAssetParameterMacros(r),await this.startGamForManifest(s,t.assetParameters,e=>{this.deps.diagnose("DA-SESSION-STARTING",`Channel switch: ${e}`,{step:e,manifestUrl:t.manifestUrl})},i),this.isStaleStart(i))return;a&&e.updateAssetParameters(a)}}else if(n){const e=a(s);this.state.ssaiController?.setManifestAssetParameters(e.ssai?.assetParameters)}else{const e=a(s);this.deps.adPlayerController.setManifestAssetParameters(e.sgaiAssetParameters)}}async switchToSteeredSsai(e,s,t=!1){const a=d(e.vendorConfiguration);if(!a)return this.deps.diagnose("DA-SSAI-UNAVAILABLE","Channel switch resolved ssai delivery but no vendor configuration provides a stitched stream; the session stays in sgai.",{manifestUrl:s.manifestUrl,previousMode:"sgai"}),!1;const i=this.deps.adPlayerController,r=i.updatedAssetParameters,n=i.sessionAssetParameterMacros;i.stopActiveBreak(),i.releaseContentHold(),i.endGamSession(),i.setSessionAssetParameters(s.assetParameters),i.setSessionAssetParameterMacros(n),r&&i.updateAssetParameters(r);const l=this.createSsaiController(a.networkCode);this.state.ssaiController=l,this.state.steeredSsaiActive=!0,this.deps.diagnose("DA-SESSION-STARTING","Channel switch: ssai-live-stream",{step:"ssai-live-stream",manifestUrl:s.manifestUrl});try{await l.startSteered(a,s.assetParameters,n),r&&l.updateAssetParameters(r);const d=o(e);return t?this.deps.breakScheduler.applyEventBoundary(d):this.deps.breakScheduler.resetForChannelSwitch(d),this.deps.pauseAdController.updateManifest(d),i.setBreakResumePolicy(s.adInsertionType,e.timebase,!1),!0}catch(e){const s=e instanceof Error?e.message:String(e);return this.deps.diagnose("DA-SSAI-SESSION-FAILED",s),l.stop(),delete this.state.ssaiController,this.state.steeredSsaiActive=!1,!1}}async switchToSgai(e,s,t,a=!1){const i=this.state.ssaiController?.sessionAssetParameterMacros;this.state.ssaiController?.stop(),delete this.state.ssaiController,this.state.steeredSsaiActive=!1,a?this.deps.breakScheduler.applyEventBoundary(e):this.deps.breakScheduler.resetForChannelSwitch(e),this.deps.pauseAdController.updateManifest(e);const r=this.deps.adPlayerController;r.setBreakResumePolicy(s.adInsertionType,e.timebase,this.deps.continueContentDuringBreak);const n=r.updatedAssetParameters;r.endGamSession(),r.setSessionAssetParameters(s.assetParameters),r.setSessionAssetParameterMacros(i??s.assetParameterMacros),await this.startGamForManifest(e,s.assetParameters,e=>{this.deps.diagnose("DA-SESSION-STARTING",`Channel switch: ${e}`,{step:e,manifestUrl:s.manifestUrl})},t),this.isStaleStart(t)||n&&r.updateAssetParameters(n)}async startGamForManifest(e,s,i,r=this.state.sessionGeneration,n=!1){const o=a(e);if(this.deps.adPlayerController.setManifestAssetParameters(o.sgaiAssetParameters),o.unknownTypes.length>0&&this.deps.diagnose("DA-VENDOR-CONFIG-UNKNOWN-TYPE",`Ignored ${o.unknownTypes.length} vendorConfiguration `+(1===o.unknownTypes.length?"entry":"entries")+" with unrecognised type: "+`${o.unknownTypes.join(", ")}.`,{types:o.unknownTypes}),m(this.deps,e),!this.deps.adPlayerController.isGamEnabled())return void(t(e)&&this.deps.diagnose("DA-GAM-CONFIG-MISSING","The break manifest carries GAM pod assets but GAM is disabled for this session (gam.enabled === false); those assets will be skipped.",{reason:"gam-disabled"}));if(!o.identity)return void(t(e)&&this.deps.diagnose("DA-GAM-CONFIG-MISSING",'The break manifest carries GAM pod assets but no usable "gam" vendorConfiguration entry; those assets will be skipped.',{reason:o.reason}));this.deps.diagnose("DA-GAM-CONFIG-RESOLVED",`GAM session identity: network ${o.identity.networkCode}, custom asset ${o.identity.customAssetKey}`,{networkCode:o.identity.networkCode,customAssetKey:o.identity.customAssetKey,channelId:e.channelId,duplicateEntriesIgnored:o.duplicateGam});const d=e.adStartDelay??0;if(d>0&&!this.state.adStartDelayElapsed&&(n||this.deps.breakScheduler.isAdStartDelayActive()))return this.deps.breakScheduler.holdBreakStarts(),this.deps.diagnose("DA-GAM-START-DEFERRED",`GAM session start deferred until ${d} seconds of content have played.`,{adStartDelaySec:d}),void this.deps.breakScheduler.setOnAdStartDelayElapsed(()=>{if(this.isStaleStart(r)||!this.state.sessionActive)return;this.state.adStartDelayElapsed=!0;const t=this.state.sessionManifest??e,n=a(t).identity;n?(this.deps.adPlayerController.setManifestAssetParameters(a(t).sgaiAssetParameters),this.startResolvedGamSession(n,s,i,r,!0)):this.deps.breakScheduler.releaseBreakStarts()});this.state.adStartDelayElapsed=!0,await this.startResolvedGamSession(o.identity,s,i,r)}async startResolvedGamSession(e,s,t,a,i=!1){try{if(this.isStaleStart(a)||!this.state.sessionActive)return;t("gam-init"),await this.deps.adPlayerController.startGamSession(e,s),this.deps.log("GAM session started")}catch(e){this.deps.diagnose("DA-GAM-SESSION-FAILED",e instanceof Error?e.message:String(e))}finally{i&&!this.isStaleStart(a)&&this.state.sessionActive&&this.deps.breakScheduler.releaseBreakStarts()}}async startSsaiSession(e,s){try{if(this.state.stitcherOrigin=n(e.stitcherUrl),this.state.configSummary.gam&&(this.state.configSummary.gam={...this.state.configSummary.gam,hasAssetParameters:!!e.assetParameters}),await this.state.ssaiController.start(e),this.isStaleStart(s))return;this.deps.diagnose("DA-SESSION-STARTED",`SSAI session active for ${e.stitcherUrl}`,{stitcherUrl:e.stitcherUrl}),this.deps.startLivePushback(),this.deps.log("SSAI session active")}catch(e){if(this.isStaleStart(s))return;const t=e instanceof Error?e:new Error(String(e));throw this.deps.diagnose("DA-SSAI-SESSION-FAILED",t.message),this.state.sessionActive=!1,this.deps.cmcdAdReporter.onSessionEnd(),this.deps.lensReporter.endSession(),t}}isStaleStart(e){return y(this.state,this.deps,e)}}
|
|
1
|
+
import{sessionTick as e}from"./sessionTick";import{configureVodSnapback as s}from"./contentSnapback";import{Timebase as t}from"../types/BreakManifest";import{ContentPlaybackStart as i}from"./ContentPlaybackStart";import{manifestHasGamAssets as a,resolveGamVendorConfiguration as r,resolveSsbStream as n}from"../services/resolveGamVendorConfiguration";import{SsaiController as o}from"../services/SsaiController";import{resolveUrlOrigin as d}from"../services/urlOrigin";import{filterSsaiClientBreaks as h,resolveStitchedStreamVendor as l,SsaiUnavailableError as c}from"../services/resolveDeliveryMode";import{gamPodIdentityChanged as p}from"../services/channelSwitch";import{MANIFEST_URL_REQUIRED_MESSAGE as S,resolveDiscoveredManifestUrl as m}from"./DiscoveredManifestUrl";import{prepareWebRuntime as f}from"../services/webCompatibility";import{adoptExternalVendorConfigurations as u,applyManifestUpdate as y,applyPendingChannelSwitch as A,adoptSlate as v,diagnoseExternalVendorConfigurations as g,emitChannelChange as P,handleEventBoundaryTick as I,isStaleSessionStart as M,isNonBlankString as k,isSsaiSessionConfig as C,rejectGamPodPreloadForBoundaryIdentity as w,resolveEventAtPosition as E,resolveSessionDeliveryMode as b,startManifestPolling as D}from"./SessionStarterHelpers";export class SessionStarter{constructor(e,s){this.state=e,this.deps=s,this.manifestPollingActions={switchChannel:(e,s,t,i)=>this.switchChannel(e,s,t,i)},this.playbackStart=new i(e,s)}async startSession(s){this.state.bumpDiscoveryWaitGeneration();const t=this.state.discoveryWaitGeneration;if("ssai"===this.deps.mode){if(!C(s)||!k(s.stitcherUrl))throw new Error('[OptiViewAds] mode "ssai" requires a stitcherUrl in startSession().')}else if(C(s))throw new Error("[OptiViewAds] startSession requires a manifestUrl (the full break-manifest URL).");let i,a=!1;if(!C(s)&&"ssai"!==this.deps.mode)if(k(s.manifestUrl))i=s.manifestUrl;else{const e=await m(this.state,this.deps,t,this.deps.discoveredManifestUrlTimeoutMs);if("superseded"===e.kind||this.state.discoveryWaitGeneration!==t)return;i=e.url,a=!0}this.state.sessionActive&&(this.deps.log("Ending previous session before starting new one"),this.deps.endSessionInternal("session-restart"));const r=this.state.sessionGeneration;this.state.sessionActive=!0,this.state.adStartDelayElapsed=!1;try{const e=f();e&&await e}catch(e){if(this.isStaleStart(r))return;throw this.state.sessionActive=!1,e}if(this.isStaleStart(r))return;if(this.deps.lensReporter.startSession(),C(s))return s.adInsertionType&&this.deps.diagnose("DA-INSERTION-TYPE-IGNORED-SSAI",`adInsertionType "${s.adInsertionType}" ignored in SSAI mode; the stitcher controls ad insertion.`,{adInsertionType:s.adInsertionType}),void await this.startSsaiSession(s,r);if(void 0===i)throw new Error(S);this.state.sessionConfig=s,this.state.resolvedManifestUrl=i,this.state.manifestUrlFromDiscovery=a,this.state.manifestOrigin=d(i),this.deps.manifestService.setManifestUrl(i),this.deps.log("Session starting, manifest URL:",i);const o=Date.now(),h=e=>{this.deps.diagnose("DA-SESSION-STARTING",`Session start: ${e}`,{step:e,manifestUrl:i,elapsedMs:Date.now()-o})};h("begin");const l=s.assetParameters;let c,p;this.deps.adPlayerController.setSessionAssetParameters(l),this.deps.adPlayerController.setSessionAssetParameterMacros(s.assetParameterMacros),this.state.configSummary.gam&&(this.state.configSummary.gam={...this.state.configSummary.gam,hasAssetParameters:!!l}),h("manifest-fetch");try{p=await this.deps.manifestService.fetchManifest();const e=E(p,Date.now(),this.deps.player);c=e.manifest,this.state.sessionManifest=p,this.state.effectiveSessionManifest=c,this.state.activeEventId=e.eventId}catch(e){if(this.isStaleStart(r))return;throw this.failSessionStart(e)}if(this.deps.cmcdAdReporter.onSessionStart(c),!this.isStaleStart(r))if(this.deps.externalPlayback.setManaged(null!==n(c.vendorConfiguration)),u(this.deps,c),v(this.deps,c),"ssai"!==this.resolveSessionDeliveryMode(c))try{if(await this.startGamForManifest(c,l,h,r,!0),this.isStaleStart(r))return;const t=s.adInsertionType;this.deps.adPlayerController.setBreakResumePolicy(t,c.timebase,this.deps.continueContentDuringBreak),this.deps.diagnose("DA-INSERTION-TYPE-RESOLVED",void 0===t?`Ad insertion type: unset — the manifest's resumeOffset drives the resume point (timebase: ${c.timebase})`:`Ad insertion type: ${t}, client-set (timebase: ${c.timebase})`,{adInsertionType:t??null,source:void 0===t?"unset":"client",timebase:c.timebase}),h("scheduling"),this.deps.breakScheduler.updateManifest(c),this.deps.pauseAdController.updateManifest(c),this.state.pauseAdTickInterval=setInterval(e(this.deps),250),this.playbackStart.start(c),this.deps.startLivePushback(),this.startBreakScheduler(),this.startManifestPolling(),this.deps.diagnose("DA-SESSION-STARTED",`Session active for manifest ${i}`,{manifestUrl:i}),this.deps.log("Session active")}catch(e){if(this.isStaleStart(r))return;throw this.failSessionStart(e)}else await this.startSteeredSsaiSession(c,s,h)}discardPreparedGamSessionAfterSeek(){this.state.sessionActive&&(this.deps.adPlayerController?.gamPodBridge.discardPreparedGamSession(),this.state.eventSessionPreloadKeys.clear())}cancelHeldImmediatePreRollOnContentError(){return this.playbackStart.cancelOnContentError()}clearHeldImmediatePreRollArm(){this.playbackStart.clear()}failSessionStart(e){const s=e instanceof Error?e:new Error(String(e)),t=this.deps.manifestService?.takeLastFailure();return this.deps.diagnose("DA-MANIFEST-FETCH-FAILED",s.message,t?{...t}:void 0),this.state.sessionActive=!1,this.deps.cmcdAdReporter.onSessionEnd(),this.deps.lensReporter.endSession(),s}resolveSessionDeliveryMode(e,s="manifest"){return b(this.state,this.deps,e,s)}async startSteeredSsaiSession(s,t,i){const a=l(s.vendorConfiguration);if(!a){const e="Manifest selected ssai delivery but no vendor configuration provides a stitched stream (no gam entry with an assetKey).";throw this.deps.diagnose("DA-SSAI-UNAVAILABLE",e),this.deps.manifestService.reset(),this.state.sessionActive=!1,new c(e)}try{i("ssai-live-stream"),this.state.steeredSsaiActive=!0,this.state.ssaiController=this.createSsaiController(a.networkCode),await this.state.ssaiController.startSteered(a,t.assetParameters,t.assetParameterMacros),i("scheduling");const r=h(s);this.deps.adPlayerController.setBreakResumePolicy(t.adInsertionType,s.timebase,!1),this.deps.breakScheduler.updateManifest(r),this.deps.pauseAdController.updateManifest(r),this.state.pauseAdTickInterval=setInterval(e(this.deps),250),this.playbackStart.start(r,!1),this.deps.startLivePushback(),this.startBreakScheduler(),this.startManifestPolling(),this.deps.diagnose("DA-SESSION-STARTED",`SSAI session active (manifest-steered) for ${this.state.resolvedManifestUrl??t.manifestUrl}`,{manifestUrl:this.state.resolvedManifestUrl??t.manifestUrl}),this.deps.log("SSAI session active (manifest-steered)")}catch(e){const s=e instanceof Error?e:new Error(String(e));throw this.deps.diagnose("DA-SSAI-SESSION-FAILED",s.message),this.state.steeredSsaiActive=!1,this.deps.manifestService.reset(),this.state.sessionActive=!1,s}}createSsaiController(e){return new o(this.deps.player,{gam:this.state.steeredSsaiOptions?.streamActivityMonitorId?{streamActivityMonitorId:this.state.steeredSsaiOptions.streamActivityMonitorId}:{},ssai:{networkCode:e},maxBitrate:this.state.steeredSsaiOptions?.maxBitrate,autoplay:this.state.steeredSsaiOptions?.autoplay,debug:this.deps.debug},{emit:(e,s)=>this.deps.emit(e,s),diagnose:(e,s,t)=>this.deps.diagnose(e,s,t),log:(...e)=>this.deps.log(...e)})}startBreakScheduler(){s(this.deps.breakScheduler,this.deps.player,e=>"ssai"!==this.state.sessionDeliveryMode&&!this.state.ssb&&null===this.deps.adPlayerController.breakNothingToPlayReason(e),e=>this.deps.adPlayerController.snapbackFormatOf(e)),this.deps.breakScheduler.setEventBoundaryCheck(e=>this.playbackStart.check()&&this.handleEventBoundaryTick(e)),this.deps.breakScheduler.start((e,s)=>{this.deps.breakLifecycle.onBreakStart(e,s)},e=>{if(this.deps.adPlayerController.isAdPlaying())return!1;if(this.deps.adPlayerController.gamPodBridge.shouldSkipGamPodPreload(e))return!1;const s=w(this.state,this.deps,this.deps.player,e);return"defer"!==s&&("mismatch"===s||this.deps.adPlayerController.preload(e).catch(s=>{this.deps.diagnose("DA-PRELOAD-FAILED",s instanceof Error?s.message:String(s),{breakId:e.id})}),!0)},(e,s,t)=>{this.deps.adBreakStatusTracker.onBreakWarning(e,s,t)},e=>{this.deps.breakLifecycle.onBreakAborted(e)},(e,s)=>this.deps.breakLifecycle.onBreakSkipped(e,s))}startManifestPolling(){D(this.state,this.deps,this.manifestPollingActions)}applyPendingChannelSwitch(){A(this.state,this.deps,this.manifestPollingActions)}applyManifestUpdate(e){y(this.state,this.deps,e)}handleEventBoundaryTick(e){return I(this.state,this.deps,e,{resolveDeliveryMode:e=>this.resolveSessionDeliveryMode(e,"event-boundary"),applyManifestUpdate:e=>this.applyManifestUpdate(e),switchChannel:(e,s,t,i)=>this.switchChannel(e,s,t,i)})}async switchChannel(e,s,i=!1,a=e){if(this.state.channelSwitchInFlight)return;const r=this.state.sessionConfig,o=this.state.effectiveSessionManifest??this.state.sessionManifest;if(!i){const s=this.deps.player.programDateTime?.getTime()??Date.now(),t=E(a,s,this.deps.player);e=t.manifest,this.state.activeEventId=t.eventId}const d=i?"event-boundary":s.reason,l={previousChannelId:s.previousChannelId,channelId:s.channelId,eventId:this.state.activeEventId,reason:d,manifestUrl:this.state.resolvedManifestUrl??r.manifestUrl??""};this.deps.diagnose("DA-MANIFEST-CHANNEL-CHANGED",`Manifest channel changed (${d}): ${s.previousChannelId??"none"} -> ${s.channelId??"none"}`,l),this.state.sessionManifest=a,this.state.effectiveSessionManifest=e,this.state.pendingManifestUpdate=null,this.deps.externalPlayback.setManaged(null!==n(e.vendorConfiguration)),this.state.channelSwitchInFlight=!0;const c=this.state.sessionGeneration,p=this.state.sessionDeliveryMode??"sgai";try{let s=this.resolveSessionDeliveryMode(e,i?"event-boundary":"channel-switch");if(i?this.deps.cmcdAdReporter.updateEventId(this.state.activeEventId):(this.deps.cmcdAdReporter.onSessionEnd(),this.deps.cmcdAdReporter.onSessionStart(e),this.deps.lensReporter.endSession(),this.deps.lensReporter.startSession()),"ssai"===s&&"sgai"===p){const t=await this.switchToSteeredSsai(e,r,i);if(this.isStaleStart(c))return;t||(s="sgai",this.state.sessionDeliveryMode="sgai",await this.applySameModeChannelSwitch(o,e,r,c,i))}else"sgai"===s&&"ssai"===p?await this.switchToSgai(e,r,c,i):await this.applySameModeChannelSwitch(o,e,r,c,i);if(this.isStaleStart(c))return}finally{this.state.channelSwitchInFlight=!1,i&&this.deps.breakScheduler.releaseBreakStarts()}e.timebase===t.MEDIATIME&&this.playbackStart.start(this.state.steeredSsaiActive?h(e):e,!this.state.steeredSsaiActive),u(this.deps,e),v(this.deps,e),this.deps.startLivePushback(),P(this.deps,{...l,previousDeliveryMode:p,deliveryMode:this.state.sessionDeliveryMode??p})}async applySameModeChannelSwitch(e,s,t,i,a=!1){const n=this.state.steeredSsaiActive,o=n?h(s):s;if(a?this.deps.breakScheduler.applyEventBoundary(o):this.deps.breakScheduler.resetForChannelSwitch(o),this.deps.pauseAdController.updateManifest(o),this.deps.adPlayerController.setBreakResumePolicy(t.adInsertionType,s.timebase,this.deps.continueContentDuringBreak),!n&&p(e,s)){const e=this.deps.adPlayerController,n=r(s);if(!(!a||!n.identity)&&await e.gamPodBridge.adoptPreparedGamSession(n.identity,t.assetParameters,n.sgaiAssetParameters))e.updatedAssetParameters&&e.updateAssetParameters(e.updatedAssetParameters);else{const a=e.updatedAssetParameters,r=e.sessionAssetParameterMacros;if(e.endGamSession(),e.setSessionAssetParameters(t.assetParameters),e.setSessionAssetParameterMacros(r),await this.startGamForManifest(s,t.assetParameters,e=>{this.deps.diagnose("DA-SESSION-STARTING",`Channel switch: ${e}`,{step:e,manifestUrl:this.state.resolvedManifestUrl??t.manifestUrl})},i),this.isStaleStart(i))return;a&&e.updateAssetParameters(a)}}else if(n){const e=r(s);this.state.ssaiController?.setManifestAssetParameters(e.ssai?.assetParameters)}else{const e=r(s);this.deps.adPlayerController.setManifestAssetParameters(e.sgaiAssetParameters)}}async switchToSteeredSsai(e,s,t=!1){const i=l(e.vendorConfiguration);if(!i)return this.deps.diagnose("DA-SSAI-UNAVAILABLE","Channel switch resolved ssai delivery but no vendor configuration provides a stitched stream; the session stays in sgai.",{manifestUrl:this.state.resolvedManifestUrl??s.manifestUrl,previousMode:"sgai"}),!1;const a=this.deps.adPlayerController,r=a.updatedAssetParameters,n=a.sessionAssetParameterMacros;a.stopActiveBreak(),a.releaseContentHold(),a.endGamSession(),a.setSessionAssetParameters(s.assetParameters),a.setSessionAssetParameterMacros(n),r&&a.updateAssetParameters(r);const o=this.createSsaiController(i.networkCode);this.state.ssaiController=o,this.state.steeredSsaiActive=!0,this.deps.diagnose("DA-SESSION-STARTING","Channel switch: ssai-live-stream",{step:"ssai-live-stream",manifestUrl:this.state.resolvedManifestUrl??s.manifestUrl});try{await o.startSteered(i,s.assetParameters,n),r&&o.updateAssetParameters(r);const d=h(e);return t?this.deps.breakScheduler.applyEventBoundary(d):this.deps.breakScheduler.resetForChannelSwitch(d),this.deps.pauseAdController.updateManifest(d),a.setBreakResumePolicy(s.adInsertionType,e.timebase,!1),!0}catch(e){const s=e instanceof Error?e.message:String(e);return this.deps.diagnose("DA-SSAI-SESSION-FAILED",s),o.stop(),delete this.state.ssaiController,this.state.steeredSsaiActive=!1,!1}}async switchToSgai(e,s,t,i=!1){const a=this.state.ssaiController?.sessionAssetParameterMacros;this.state.ssaiController?.stop(),delete this.state.ssaiController,this.state.steeredSsaiActive=!1,i?this.deps.breakScheduler.applyEventBoundary(e):this.deps.breakScheduler.resetForChannelSwitch(e),this.deps.pauseAdController.updateManifest(e);const r=this.deps.adPlayerController;r.setBreakResumePolicy(s.adInsertionType,e.timebase,this.deps.continueContentDuringBreak);const n=r.updatedAssetParameters;r.endGamSession(),r.setSessionAssetParameters(s.assetParameters),r.setSessionAssetParameterMacros(a??s.assetParameterMacros),await this.startGamForManifest(e,s.assetParameters,e=>{this.deps.diagnose("DA-SESSION-STARTING",`Channel switch: ${e}`,{step:e,manifestUrl:this.state.resolvedManifestUrl??s.manifestUrl})},t),this.isStaleStart(t)||n&&r.updateAssetParameters(n)}async startGamForManifest(e,s,t,i=this.state.sessionGeneration,n=!1){const o=r(e);if(this.deps.adPlayerController.setManifestAssetParameters(o.sgaiAssetParameters),o.unknownTypes.length>0&&this.deps.diagnose("DA-VENDOR-CONFIG-UNKNOWN-TYPE",`Ignored ${o.unknownTypes.length} vendorConfiguration `+(1===o.unknownTypes.length?"entry":"entries")+" with unrecognised type: "+`${o.unknownTypes.join(", ")}.`,{types:o.unknownTypes}),g(this.deps,e),!this.deps.adPlayerController.isGamEnabled())return void(a(e)&&this.deps.diagnose("DA-GAM-CONFIG-MISSING","The break manifest carries GAM pod assets but GAM is disabled for this session (gam.enabled === false); those assets will be skipped.",{reason:"gam-disabled"}));if(!o.identity)return void(a(e)&&this.deps.diagnose("DA-GAM-CONFIG-MISSING",'The break manifest carries GAM pod assets but no usable "gam" vendorConfiguration entry; those assets will be skipped.',{reason:o.reason}));this.deps.diagnose("DA-GAM-CONFIG-RESOLVED",`GAM session identity: network ${o.identity.networkCode}, custom asset ${o.identity.customAssetKey}`,{networkCode:o.identity.networkCode,customAssetKey:o.identity.customAssetKey,channelId:e.channelId,duplicateEntriesIgnored:o.duplicateGam});const d=e.adStartDelay??0;if(d>0&&!this.state.adStartDelayElapsed&&(n||this.deps.breakScheduler.isAdStartDelayActive()))return this.deps.breakScheduler.holdBreakStarts(),this.deps.diagnose("DA-GAM-START-DEFERRED",`GAM session start deferred until ${d} seconds of content have played.`,{adStartDelaySec:d}),void this.deps.breakScheduler.setOnAdStartDelayElapsed(()=>{if(this.isStaleStart(i)||!this.state.sessionActive)return;this.state.adStartDelayElapsed=!0;const a=this.state.sessionManifest??e,n=r(a).identity;n?(this.deps.adPlayerController.setManifestAssetParameters(r(a).sgaiAssetParameters),this.startResolvedGamSession(n,s,t,i,!0)):this.deps.breakScheduler.releaseBreakStarts()});this.state.adStartDelayElapsed=!0,await this.startResolvedGamSession(o.identity,s,t,i)}async startResolvedGamSession(e,s,t,i,a=!1){try{if(this.isStaleStart(i)||!this.state.sessionActive)return;t("gam-init"),await this.deps.adPlayerController.startGamSession(e,s),this.deps.log("GAM session started")}catch(e){this.deps.diagnose("DA-GAM-SESSION-FAILED",e instanceof Error?e.message:String(e))}finally{a&&!this.isStaleStart(i)&&this.state.sessionActive&&this.deps.breakScheduler.releaseBreakStarts()}}async startSsaiSession(e,s){try{if(this.state.stitcherOrigin=d(e.stitcherUrl),this.state.configSummary.gam&&(this.state.configSummary.gam={...this.state.configSummary.gam,hasAssetParameters:!!e.assetParameters}),await this.state.ssaiController.start(e),this.isStaleStart(s))return;this.deps.diagnose("DA-SESSION-STARTED",`SSAI session active for ${e.stitcherUrl}`,{stitcherUrl:e.stitcherUrl}),this.deps.startLivePushback(),this.deps.log("SSAI session active")}catch(e){if(this.isStaleStart(s))return;const t=e instanceof Error?e:new Error(String(e));throw this.deps.diagnose("DA-SSAI-SESSION-FAILED",t.message),this.state.sessionActive=!1,this.deps.cmcdAdReporter.onSessionEnd(),this.deps.lensReporter.endSession(),t}}isStaleStart(e){return M(this.state,this.deps,e)}}
|
|
@@ -17,6 +17,7 @@ export interface SessionStarterDeps {
|
|
|
17
17
|
mode: OptiViewAdsMode;
|
|
18
18
|
hasExplicitMode: boolean;
|
|
19
19
|
continueContentDuringBreak: boolean;
|
|
20
|
+
manifestUrlDiscovery: boolean;
|
|
20
21
|
startLivePushback: () => void;
|
|
21
22
|
manifestService: ManifestService | undefined;
|
|
22
23
|
breakScheduler: BreakScheduler | undefined;
|
|
@@ -31,6 +32,8 @@ export interface SessionStarterDeps {
|
|
|
31
32
|
diagnose: Diagnose;
|
|
32
33
|
log: Log;
|
|
33
34
|
endSessionInternal: (cause: string) => void;
|
|
35
|
+
/** Test hook: overrides the player-discovery wait in startSession(); defaults to 10 s. */
|
|
36
|
+
discoveredManifestUrlTimeoutMs?: number;
|
|
34
37
|
notedManifestBreakIds: (manifest: {
|
|
35
38
|
breaks: Array<{
|
|
36
39
|
id: string;
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import type { PlayerAdapter } from '../interfaces/PlayerAdapter';
|
|
2
2
|
import type { Break, BreakManifest } from '../types/BreakManifest';
|
|
3
|
-
import type { OptiViewAdsMode, SessionDeliveryMode, SsaiSessionConfig, StartSessionConfig } from '../types/OptiViewAdsConfig';
|
|
3
|
+
import type { AssetParameterMacroUpdates, OptiViewAdsMode, SessionDeliveryMode, SsaiSessionConfig, StartSessionConfig } from '../types/OptiViewAdsConfig';
|
|
4
4
|
import { type ChannelSwitch } from '../services/channelSwitch';
|
|
5
5
|
import type { SessionState } from './SessionState';
|
|
6
6
|
import type { SessionStarterDeps } from './SessionStarterDeps';
|
|
7
7
|
export declare const EVENT_SESSION_PRELOAD_LEAD_MS = 8000;
|
|
8
|
+
/**
|
|
9
|
+
* Live-update the session's asset parameters / macros on whichever controller is
|
|
10
|
+
* active (SGAI `AdPlayerController` or the steered/explicit SSAI controller).
|
|
11
|
+
*/
|
|
12
|
+
export declare function updateSessionAssetParameters(state: SessionState, mode: OptiViewAdsMode, adPlayerController: {
|
|
13
|
+
updateAssetParameters(params: Record<string, string>): void;
|
|
14
|
+
} | undefined, params: Record<string, string>, log: (message: string) => void): void;
|
|
15
|
+
/** `updateSessionAssetParameters` for macro updates. */
|
|
16
|
+
export declare function updateSessionAssetParameterMacros(state: SessionState, mode: OptiViewAdsMode, adPlayerController: {
|
|
17
|
+
updateAssetParameterMacros(macros: AssetParameterMacroUpdates): void;
|
|
18
|
+
} | undefined, macros: AssetParameterMacroUpdates, log: (message: string) => void): void;
|
|
8
19
|
export declare function isSsaiSessionConfig(config: StartSessionConfig): config is SsaiSessionConfig;
|
|
9
20
|
export declare function isNonBlankString(value: unknown): value is string;
|
|
10
21
|
export declare function resolveEventAtPosition(manifest: BreakManifest, positionMs: number, player: PlayerAdapter): {
|
|
@@ -25,6 +36,8 @@ export declare function emitChannelChange(deps: SessionStarterDeps, context: {
|
|
|
25
36
|
previousDeliveryMode: SessionDeliveryMode;
|
|
26
37
|
deliveryMode: SessionDeliveryMode;
|
|
27
38
|
}): void;
|
|
39
|
+
/** The manifest's slate for the breaks that follow (none when it has no usable one); the controller warms a new one. */
|
|
40
|
+
export declare function adoptSlate(deps: SessionStarterDeps, manifest: BreakManifest): void;
|
|
28
41
|
export declare function adoptExternalVendorConfigurations(deps: SessionStarterDeps, manifest: BreakManifest): void;
|
|
29
42
|
export declare function diagnoseExternalVendorConfigurations(deps: SessionStarterDeps, manifest: BreakManifest): void;
|
|
30
43
|
type EventBoundaryActions = {
|
|
@@ -33,4 +46,12 @@ type EventBoundaryActions = {
|
|
|
33
46
|
switchChannel: (manifest: BreakManifest, change: ChannelSwitch, eventBoundary: boolean, rawManifest: BreakManifest) => Promise<void>;
|
|
34
47
|
};
|
|
35
48
|
export declare function handleEventBoundaryTick(state: SessionState, deps: SessionStarterDeps, positionMs: number | null, actions: EventBoundaryActions): boolean;
|
|
49
|
+
/** Channel-switch entry point shared by the manifest poll and pending-switch drain. */
|
|
50
|
+
export type ManifestPollingActions = {
|
|
51
|
+
switchChannel: (manifest: BreakManifest, change: ChannelSwitch, eventBoundary?: boolean, rawManifest?: BreakManifest) => Promise<void>;
|
|
52
|
+
};
|
|
53
|
+
/** Fold a polled manifest into the running session: vendor configs, GAM/ssai asset params, schedulers. */
|
|
54
|
+
export declare function applyManifestUpdate(state: SessionState, deps: SessionStarterDeps, manifest: BreakManifest): void;
|
|
55
|
+
export declare function startManifestPolling(state: SessionState, deps: SessionStarterDeps, actions: ManifestPollingActions): void;
|
|
56
|
+
export declare function applyPendingChannelSwitch(state: SessionState, deps: SessionStarterDeps, actions: ManifestPollingActions): void;
|
|
36
57
|
export {};
|