@dolby-optiview/ads-sdk-core 1.5.1 → 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 +104 -4
- 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 +8 -35
- package/dist/services/BreakScheduler.js +1 -1
- package/dist/services/BreakSchedulerTypes.d.ts +7 -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 +1 -0
- package/dist/services/SchedulerSnapback.js +1 -1
- package/dist/services/SlateClock.d.ts +22 -0
- package/dist/services/SlateClock.js +1 -0
- package/dist/services/SnapbackGuard.d.ts +36 -2
- package/dist/services/SnapbackGuard.js +1 -1
- package/dist/services/VastAdManager.d.ts +2 -0
- package/dist/services/VastAdManager.js +1 -1
- package/dist/services/adPlayer/AdPlayerCallbacks.d.ts +9 -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 +13 -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/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/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 +2 -2
- 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 +39 -1
- 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
|
@@ -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{configureVodSnapback as s}from"./contentSnapback";import{getPreRollDelay as t}from"../types/BreakManifest";import{manifestHasGamAssets as a,resolveGamVendorConfiguration as i,resolveSsbStream as r}from"../services/resolveGamVendorConfiguration";import{SsaiController as n}from"../services/SsaiController";import{resolveUrlOrigin as o}from"../services/urlOrigin";import{filterSsaiClientBreaks as d,resolveStitchedStreamVendor as l,SsaiUnavailableError as h}from"../services/resolveDeliveryMode";import{detectChannelSwitch as c,gamPodIdentityChanged as p}from"../services/channelSwitch";import{prepareWebRuntime as S}from"../services/webCompatibility";import{adoptExternalVendorConfigurations as m,diagnoseExternalVendorConfigurations as f,diagnoseIgnoredDeliveryModeChange as u,emitChannelChange as A,handleEventBoundaryTick as y,isStaleSessionStart as g,isNonBlankString as v,isSsaiSessionConfig as P,rejectGamPodPreloadForBoundaryIdentity as I,resolveEventAtPosition as k,resolveSessionDeliveryMode as C}from"./SessionStarterHelpers";const M=1e4;export class SessionStarter{constructor(e,s){this.state=e,this.deps=s}async startSession(s){if("ssai"===this.deps.mode){if(!P(s)||!v(s.stitcherUrl))throw new Error('[OptiViewAds] mode "ssai" requires a stitcherUrl in startSession().')}else if(P(s)||!v(s.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=S();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(),P(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,a);const i=s.manifestUrl;this.state.sessionConfig=s,this.state.manifestOrigin=o(i),this.deps.manifestService.setManifestUrl(i),this.deps.log("Session starting, manifest URL:",i);const n=Date.now(),d=e=>{this.deps.diagnose("DA-SESSION-STARTING",`Session start: ${e}`,{step:e,manifestUrl:i,elapsedMs:Date.now()-n})};d("begin");const l=s.assetParameters;let h,c;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}),d("manifest-fetch");try{c=await this.deps.manifestService.fetchManifest();const e=k(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!==r(h.vendorConfiguration)),m(this.deps,h),"ssai"!==this.resolveSessionDeliveryMode(h))try{if(await this.startGamForManifest(h,l,d,a,!0),this.isStaleStart(a))return;const r=s.adInsertionType;this.deps.adPlayerController.setBreakResumePolicy(r,h.timebase,this.deps.continueContentDuringBreak),this.deps.diagnose("DA-INSERTION-TYPE-RESOLVED",void 0===r?`Ad insertion type: unset — the manifest's resumeOffset drives the resume point (timebase: ${h.timebase})`:`Ad insertion type: ${r}, client-set (timebase: ${h.timebase})`,{adInsertionType:r??null,source:void 0===r?"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===t(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 ${i}`,{manifestUrl:i}),this.deps.log("Session active")}catch(e){if(this.isStaleStart(a))return;throw this.failSessionStart(e)}else await this.startSteeredSsaiSession(h,s,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<M)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:M})}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 C(this.state,this.deps,e,s)}async startSteeredSsaiSession(s,t,a){const i=l(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 h(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=d(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 n(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)),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=I(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?c(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=c(this.state.sessionManifest,e);s?this.switchChannel(e,s):this.applyManifestUpdate(e)}applyManifestUpdate(e){if(!this.state.sessionActive)return;m(this.deps,e),u(this.state,this.deps,e);const s=i(e);this.state.steeredSsaiActive?this.state.ssaiController?.setManifestAssetParameters(s.ssai?.assetParameters):this.deps.adPlayerController.setManifestAssetParameters(s.sgaiAssetParameters);const t=this.state.steeredSsaiActive?d(e):e;this.deps.breakScheduler.updateManifest(t),this.deps.pauseAdController.updateManifest(t)}handleEventBoundaryTick(e){return y(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 i=this.state.sessionConfig,n=this.state.effectiveSessionManifest??this.state.sessionManifest;if(!t){const s=this.deps.player.programDateTime?.getTime()??Date.now(),t=k(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:i.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!==r(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,i,t);if(this.isStaleStart(l))return;a||(s="sgai",this.state.sessionDeliveryMode="sgai",await this.applySameModeChannelSwitch(n,e,i,l,t))}else"sgai"===s&&"ssai"===h?await this.switchToSgai(e,i,l,t):await this.applySameModeChannelSwitch(n,e,i,l,t);if(this.isStaleStart(l))return}finally{this.state.channelSwitchInFlight=!1,t&&this.deps.breakScheduler.releaseBreakStarts()}m(this.deps,e),this.deps.startLivePushback(),A(this.deps,{...d,previousDeliveryMode:h,deliveryMode:this.state.sessionDeliveryMode??h})}async applySameModeChannelSwitch(e,s,t,a,r=!1){const n=this.state.steeredSsaiActive,o=n?d(s):s;if(r?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=i(s);if(!(!r||!n.identity)&&await e.gamPodBridge.adoptPreparedGamSession(n.identity,t.assetParameters,n.sgaiAssetParameters))e.updatedAssetParameters&&e.updateAssetParameters(e.updatedAssetParameters);else{const i=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})},a),this.isStaleStart(a))return;i&&e.updateAssetParameters(i)}}else if(n){const e=i(s);this.state.ssaiController?.setManifestAssetParameters(e.ssai?.assetParameters)}else{const e=i(s);this.deps.adPlayerController.setManifestAssetParameters(e.sgaiAssetParameters)}}async switchToSteeredSsai(e,s,t=!1){const a=l(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 o=this.createSsaiController(a.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:s.manifestUrl});try{await o.startSteered(a,s.assetParameters,n),r&&o.updateAssetParameters(r);const l=d(e);return t?this.deps.breakScheduler.applyEventBoundary(l):this.deps.breakScheduler.resetForChannelSwitch(l),this.deps.pauseAdController.updateManifest(l),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),o.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,t,r=this.state.sessionGeneration,n=!1){const o=i(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}),f(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(r)||!this.state.sessionActive)return;this.state.adStartDelayElapsed=!0;const a=this.state.sessionManifest??e,n=i(a).identity;n?(this.deps.adPlayerController.setManifestAssetParameters(i(a).sgaiAssetParameters),this.startResolvedGamSession(n,s,t,r,!0)):this.deps.breakScheduler.releaseBreakStarts()});this.state.adStartDelayElapsed=!0,await this.startResolvedGamSession(o.identity,s,t,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=o(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 g(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 {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Timebase as e}from"../types/BreakManifest";import{resolveExternalVendorConfigurations as n}from"../services/resolveExternalVendorConfigurations";import{
|
|
1
|
+
import{Timebase as e}from"../types/BreakManifest";import{resolveExternalVendorConfigurations as n}from"../services/resolveExternalVendorConfigurations";import{resolveSlate as t}from"../services/resolveSlate";import{detectDeviceProfile as s}from"../services/detectDeviceType";import{isGamVendorParameters as i}from"../services/GamPodUrlBuilder";import{decideDeliveryMode as r,detectChannelSwitch as o,gamPodIdentityChanged as a}from"../services/channelSwitch";import{nextEventBoundaryMs as d,resolveEventManifest as l}from"../services/resolveChannelEvents";import{filterSsaiClientBreaks as c}from"../services/resolveDeliveryMode";import{gamPodIdentityMismatches as u,resolveGamVendorConfiguration as f}from"../services/resolveGamVendorConfiguration";export const EVENT_SESSION_PRELOAD_LEAD_MS=8e3;export function updateSessionAssetParameters(e,n,t,s,i){if(!e.sessionActive)return void console.warn("[OptiViewAds] updateAssetParameters called but no active session.");const r="ssai"===n||e.steeredSsaiActive;(r?e.ssaiController:t).updateAssetParameters(s),i("Asset parameters updated"+(r?" (SSAI)":""))}export function updateSessionAssetParameterMacros(e,n,t,s,i){if(!e.sessionActive)return void console.warn("[OptiViewAds] updateAssetParameterMacros called but no active session.");const r="ssai"===n||e.steeredSsaiActive;(r?e.ssaiController:t).updateAssetParameterMacros(s),i("Asset parameter macros updated"+(r?" (SSAI)":""))}export function isSsaiSessionConfig(e){return"stitcherUrl"in e}export function isNonBlankString(e){return"string"==typeof e&&""!==e.trim()}export function resolveEventAtPosition(n,t,s){return n.timebase!==e.WALLCLOCK||!1===s.isLive?{manifest:n,eventId:n.eventId??null}:l(n,t)}export function rejectGamPodPreloadForBoundaryIdentity(n,t,s,r){const o=n.pendingManifestUpdate??n.sessionManifest;if(!o||o.timebase!==e.WALLCLOCK||!1===s.isLive||"string"!=typeof r.start)return"none";const a=Date.parse(r.start);if(!Number.isFinite(a))return"none";const d=l(o,a).manifest,c=f(d).identity;if(!c)return"none";const v=n.effectiveSessionManifest??o,p=f(v).identity;if(p&&c.networkCode===p.networkCode&&c.customAssetKey===p.customAssetKey)return"none";const m=(Array.isArray(r.variant)?r.variant:[r.variant]).flatMap(e=>e.assets).filter(e=>"vendor"===e.type&&"gam"===e.vendor&&i(e.vendorParameters));if(0===m.length)return"none";const g=m.find(e=>0===u(c,e.vendorParameters).length);if(g)return"defer";const M=m[0];if(!M)return"none";for(const e of u(c,M.vendorParameters))t.diagnose("DA-GAM-POD-IDENTITY-MISMATCH",`GAM asset ${M.id} skipped: vendorParameters.${e.field} "${e.assetValue}" does not match the stream session's "${e.sessionValue}".`,{assetId:M.id,field:e.field,assetValue:e.assetValue,sessionValue:e.sessionValue});return"mismatch"}export function diagnoseIgnoredDeliveryModeChange(e,n,t){if(!e.sessionDeliveryMode)return;const i="ssb"===e.sessionDeliveryMode?e.ssb?.previousMode:e.sessionDeliveryMode;if(!i)return;const o=r(t,s(),null,"manifest");o.manifestMode!==i&&n.diagnose("DA-DELIVERY-MODE-CHANGE-IGNORED",`Polled manifest resolves delivery mode "${o.manifestMode}" but "${i}" is pinned for this session.`,{polledMode:o.manifestMode,pinnedMode:i})}export function resolveSessionDeliveryMode(e,n,t,i="manifest"){const o=e.sessionDeliveryMode,a=r(t,s(),n.hasExplicitMode?n.mode:null,i);null!==a.unknownMode&&n.diagnose("DA-DELIVERY-MODE-UNKNOWN",`Delivery rule ${a.ruleIndex} carries unrecognized mode "${a.unknownMode}"; using sgai.`,{unknownMode:a.unknownMode,ruleIndex:a.ruleIndex});for(const e of a.targetingIssues)n.diagnose(e.code,`Targeting ${e.field} was rejected.`,{field:e.field,...void 0===e.value?{}:{value:e.value}});return n.hasExplicitMode&&a.manifestMode!==a.mode&&n.diagnose("DA-DELIVERY-MODE-OVERRIDDEN",`Manifest delivery rules resolve "${a.manifestMode}" but the explicit config mode "${a.mode}" takes precedence.`,{manifestMode:a.manifestMode,configuredMode:a.mode}),e.sessionDeliveryMode=a.mode,n.diagnose("DA-DELIVERY-MODE-RESOLVED",`Delivery mode: ${a.mode}`,{mode:a.mode,ruleIndex:a.ruleIndex,source:a.source,deviceProfile:s(),..."manifest"!==i?{previousMode:o}:{}}),a.mode}export function isStaleSessionStart(e,n,t){return t!==e.sessionGeneration&&(n.log("Session start aborted — the session ended or was replaced while starting"),!0)}export function emitChannelChange(e,n){e.emit("adchannelchange",{type:"adchannelchange",timestamp:Date.now(),...n})}export function adoptSlate(e,n){e.adPlayerController?.setSlate(t(n.slate).slate)}export function adoptExternalVendorConfigurations(e,n){e.externalVendorConfigurationTracker.adopt(n)}export function diagnoseExternalVendorConfigurations(e,t){const s=n(t);s.invalidCount>0&&e.diagnose("DA-EXTERNAL-VENDOR-INVALID",`Ignored ${s.invalidCount} invalid external vendorConfiguration `+(1===s.invalidCount?"entry":"entries")+".",{count:s.invalidCount}),s.duplicateProviders.length>0&&e.diagnose("DA-EXTERNAL-VENDOR-DUPLICATE",`Ignored duplicate external vendorConfiguration entries for providers: ${s.duplicateProviders.join(", ")}.`,{providers:s.duplicateProviders})}export function handleEventBoundaryTick(n,t,i,l){const u=n.sessionManifest;if(!n.sessionActive||!u||u.timebase!==e.WALLCLOCK||!1===t.player.isLive||null===i||!Number.isFinite(i))return!0;const v=n.pendingManifestUpdate,p=v??u,m=resolveEventAtPosition(p,i,t.player),g=m.eventId!==n.activeEventId;if(g){if(n.channelSwitchInFlight)return!1;if(t.breakScheduler.getActiveBreak())return!0}if(!g)return v&&(n.pendingManifestUpdate=null,n.effectiveSessionManifest=m.manifest,l.applyManifestUpdate(m.manifest)),function(e,n,t,i){const o=d(t,i),l=null===o?null:o-i;if(null===o||null===l||l<0||l>8e3||e.steeredSsaiActive)return void n.adPlayerController.gamPodBridge.discardPreparedGamSession();const c=resolveEventAtPosition(t,i,n.player).manifest,u=resolveEventAtPosition(t,o,n.player).manifest,v=n.hasExplicitMode?n.mode:null,p=r(c,s(),v,"event-boundary").mode,m=r(u,s(),v,"event-boundary").mode,g=f(c),M=f(u);if("sgai"!==p||"sgai"!==m||!M.identity||g.identity?.customAssetKey===M.identity.customAssetKey||!a(c,u)||!function(e,n){return e.breaks.some(e=>{const t="string"==typeof e.start?Date.parse(e.start):Number.NaN,s=Array.isArray(e.variant)?e.variant:[e.variant];return Number.isFinite(t)&&t>=n&&t<=n+8e3&&s.some(e=>e.assets.some(e=>"vendor"===e.type&&"gam"===e.vendor))})}(u,o))return void n.adPlayerController.gamPodBridge.discardPreparedGamSession();const A=M.identity,h=`${o}:${A.networkCode}:${A.customAssetKey}`;if(e.eventSessionPreloadKeys.has(h))return;e.eventSessionPreloadKeys.add(h),n.adPlayerController.gamPodBridge.prepareNextGamSession(A,M.sgaiAssetParameters).then(e=>{e&&n.diagnose("DA-GAM-SESSION-PRELOADED","GAM session preloaded for the next event.",{boundaryMs:o,networkCode:A.networkCode,customAssetKey:A.customAssetKey})}).catch(e=>{n.log("Next-event GAM session preload failed",e)})}(n,t,p,i),!0;const M=n.effectiveSessionManifest??u,A=r(m.manifest,s(),t.hasExplicitMode?t.mode:null,"event-boundary").mode,h=o(M,m.manifest)??(A!==n.sessionDeliveryMode?{previousChannelId:M.channelId??null,channelId:m.manifest.channelId??M.channelId??null,reason:"event-boundary"}:null);if(v&&(n.pendingManifestUpdate=null),t.breakScheduler.holdBreakStarts(),n.activeEventId=m.eventId,h)return l.switchChannel(m.manifest,{...h,reason:"event-boundary"},!0,p),!1;n.effectiveSessionManifest=m.manifest;try{!function(e,n,t,s,i,r){const o=e.sessionDeliveryMode??"sgai",a=r.resolveDeliveryMode(s);adoptExternalVendorConfigurations(n,s),adoptSlate(n,s);const d=f(s);e.steeredSsaiActive?e.ssaiController?.setManifestAssetParameters(d.ssai?.assetParameters):n.adPlayerController.setManifestAssetParameters(d.sgaiAssetParameters);const l=e.steeredSsaiActive?c(s):s;n.breakScheduler.applyEventBoundary(l),n.pauseAdController.updateManifest(l),n.cmcdAdReporter.updateEventId(i);const u=s.channelId??t.channelId??null;emitChannelChange(n,{previousChannelId:t.channelId??u,channelId:u,eventId:i,reason:"event-boundary",manifestUrl:e.resolvedManifestUrl??e.sessionConfig.manifestUrl??"",previousDeliveryMode:o,deliveryMode:a})}(n,t,M,m.manifest,m.eventId,l)}finally{t.breakScheduler.releaseBreakStarts()}return!0}export function applyManifestUpdate(e,n,t){if(!e.sessionActive)return;adoptExternalVendorConfigurations(n,t),adoptSlate(n,t),diagnoseIgnoredDeliveryModeChange(e,n,t);const s=f(t);e.steeredSsaiActive?e.ssaiController?.setManifestAssetParameters(s.ssai?.assetParameters):n.adPlayerController.setManifestAssetParameters(s.sgaiAssetParameters);const i=e.steeredSsaiActive?c(t):t;n.breakScheduler.updateManifest(i),n.pauseAdController.updateManifest(i)}export function startManifestPolling(e,n,t){n.manifestService.startPolling(s=>{if(!e.sessionActive)return;const i=n.notedManifestBreakIds(s);n.diagnose("DA-MANIFEST-POLLED","Manifest polled",{breaks:s.breaks.length,newBreakIds:i,pollingIntervalSeconds:n.manifestService.currentPollingIntervalSeconds});const r=e.sessionManifest,a=r?o(r,s):null;if(e.pendingChannelSwitch||a&&(n.breakScheduler.getActiveBreak()||e.ssb))return e.pendingChannelSwitch=s,void n.log("Channel switch deferred until the active break ends");if(a)return e.sessionManifest=s,e.pendingManifestUpdate=null,void t.switchChannel(s,a);const d="wallclock"===s.timebase&&!1!==n.player.isLive&&Boolean(s.events?.length||r?.events?.length);e.sessionManifest=s,d?e.pendingManifestUpdate=s:(e.pendingManifestUpdate=null,e.effectiveSessionManifest=s,applyManifestUpdate(e,n,s))},t=>{e.sessionActive&&n.diagnose("DA-MANIFEST-POLL-FAILED",t.message)})}export function applyPendingChannelSwitch(e,n,t){if(!e.sessionActive||!e.pendingChannelSwitch||e.ssb)return;const s=e.pendingChannelSwitch;e.pendingChannelSwitch=null;const i=o(e.sessionManifest,s);i?t.switchChannel(s,i):applyManifestUpdate(e,n,s)}
|
|
@@ -50,6 +50,22 @@ export declare class SessionState {
|
|
|
50
50
|
heldPreRollWaitStartedAt: number | null;
|
|
51
51
|
snapbackSeekInFlight: boolean;
|
|
52
52
|
sessionConfig: SessionConfig | null;
|
|
53
|
+
/**
|
|
54
|
+
* The manifest URL the session actually polls — the explicit `sessionConfig.manifestUrl` or the
|
|
55
|
+
* URL resolved from player discovery. Null between sessions.
|
|
56
|
+
*/
|
|
57
|
+
resolvedManifestUrl: string | null;
|
|
58
|
+
/** True when the active session's URL came from player discovery (enables auto-restart). */
|
|
59
|
+
manifestUrlFromDiscovery: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Bumped by every startSession()/endSession()/destroy() call so a pending discovery wait can
|
|
62
|
+
* tell it was superseded — unlike `sessionGeneration`, it moves even with no active session.
|
|
63
|
+
*/
|
|
64
|
+
discoveryWaitGeneration: number;
|
|
65
|
+
/** Wake callbacks for in-flight discovery waits; run on every bump so a superseded wait exits. */
|
|
66
|
+
private discoveryWaiters;
|
|
67
|
+
bumpDiscoveryWaitGeneration(): void;
|
|
68
|
+
addDiscoveryWaiter(wake: () => void): () => void;
|
|
53
69
|
sessionManifest: BreakManifest | null;
|
|
54
70
|
effectiveSessionManifest: BreakManifest | null;
|
|
55
71
|
activeEventId: string | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export class SessionState{constructor(){this.sessionActive=!1,this.sessionGeneration=0,this.adStartDelayElapsed=!1,this.sessionDeliveryMode=null,this.ssb=null,this.steeredSsaiActive=!1,this.steeredSsaiOptions=null,this.pauseAdTickInterval=null,this.heldPreRollArmInterval=null,this.manifestOrigin=null,this.stitcherOrigin=null,this.heldImmediatePreRollId=null,this.heldPreRollArmed=!1,this.heldPreRollWaitStartedAt=null,this.snapbackSeekInFlight=!1,this.sessionConfig=null,this.sessionManifest=null,this.effectiveSessionManifest=null,this.activeEventId=null,this.eventSessionPreloadKeys=new Set,this.pendingManifestUpdate=null,this.channelSwitchInFlight=!1,this.pendingChannelSwitch=null}clearSessionIdentity(){this.sessionDeliveryMode=null,this.sessionManifest=null,this.effectiveSessionManifest=null,this.activeEventId=null,this.eventSessionPreloadKeys.clear(),this.pendingManifestUpdate=null,this.channelSwitchInFlight=!1,this.pendingChannelSwitch=null}}
|
|
1
|
+
export class SessionState{constructor(){this.sessionActive=!1,this.sessionGeneration=0,this.adStartDelayElapsed=!1,this.sessionDeliveryMode=null,this.ssb=null,this.steeredSsaiActive=!1,this.steeredSsaiOptions=null,this.pauseAdTickInterval=null,this.heldPreRollArmInterval=null,this.manifestOrigin=null,this.stitcherOrigin=null,this.heldImmediatePreRollId=null,this.heldPreRollArmed=!1,this.heldPreRollWaitStartedAt=null,this.snapbackSeekInFlight=!1,this.sessionConfig=null,this.resolvedManifestUrl=null,this.manifestUrlFromDiscovery=!1,this.discoveryWaitGeneration=0,this.discoveryWaiters=new Set,this.sessionManifest=null,this.effectiveSessionManifest=null,this.activeEventId=null,this.eventSessionPreloadKeys=new Set,this.pendingManifestUpdate=null,this.channelSwitchInFlight=!1,this.pendingChannelSwitch=null}bumpDiscoveryWaitGeneration(){this.discoveryWaitGeneration++;for(const e of[...this.discoveryWaiters])e()}addDiscoveryWaiter(e){return this.discoveryWaiters.add(e),()=>{this.discoveryWaiters.delete(e)}}clearSessionIdentity(){this.sessionDeliveryMode=null,this.resolvedManifestUrl=null,this.manifestUrlFromDiscovery=!1,this.sessionManifest=null,this.effectiveSessionManifest=null,this.activeEventId=null,this.eventSessionPreloadKeys.clear(),this.pendingManifestUpdate=null,this.channelSwitchInFlight=!1,this.pendingChannelSwitch=null}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{buildAssetParameterMacroTable as e}from"../services/assetParameterMacroTable";import{resolveSsbStream as s}from"../services/resolveGamVendorConfiguration";import{resolveSsbRestoreChange as t}from"../services/ssbSwitchSignals";import{buildSsbStreamUrl as
|
|
1
|
+
import{buildAssetParameterMacroTable as e}from"../services/assetParameterMacroTable";import{resolveSsbStream as s}from"../services/resolveGamVendorConfiguration";import{resolveSsbRestoreChange as t,resolveSsbSwitchPreviousPaused as i}from"../services/ssbSwitchSignals";import{buildSsbStreamUrl as a,resolveSsbRestorePosition as r}from"../services/ssbStream";export const SSB_ACTIVE_TIMEOUT_MS=1500;export const SSB_LOAD_TIMEOUT_MS=15e3;export const SSB_RECEIVER_PAUSE_WINDOW_MS=3e3;export const SSB_RESTORE_PLAYING_BACKSTOP_MS=1e4;export const SSB_RESTORE_ATTEMPTS=2;export class SsbSwitchController{constructor(t,r,o){this.state=t,this.starter=r,this.deps=o,this.timers=new Set,this.previousPaused=!1,this.streamReady=!1,this.onAllowed=t=>{const{state:r}=this,{player:o}=this.deps,n=r.effectiveSessionManifest??r.sessionManifest,l=n?s(n.vendorConfiguration):null,d=r.sessionConfig;if(!r.sessionActive||r.ssb||!n||!l||!d)return;const c=o.sourceUrl;if("string"!=typeof c||""===c.trim())return void this.deps.diagnose("DA-SSB-SWITCH-FAILED","The content adapter reports no source to restore; AirPlay stays on the device.",{reason:"no-restore-source",trigger:t});const h=this.deps.adPlayerController;this.runtimeParams=h?.updatedAssetParameters,this.sessionMacros=h?.sessionAssetParameterMacros,this.previousPaused=i(t,o.paused);const p=r.sessionDeliveryMode??"sgai";r.ssb={previousMode:p,previousSource:c,previousPosition:o.currentTime,previousLive:o.isLive??!Number.isFinite(o.duration),switchedAt:Date.now(),trigger:t,phase:"switching",activeAt:null,announced:!1},h?.stopActiveBreak(),h?.abandonContentResume(),h?.releaseContentHold(),h?.endGamSession(),r.steeredSsaiActive&&(r.ssaiController?.stop(),delete r.ssaiController,r.steeredSsaiActive=!1),this.deps.breakScheduler?.suspend(),this.deps.pauseAdController?.reset(),r.sessionDeliveryMode="ssb",this.deps.diagnose("DA-DELIVERY-MODE-RESOLVED","AirPlay started: playing the SSB stream",{mode:"ssb",source:"external-playback",previousMode:p,trigger:t}),this.rollSession(n);const u=a(l,{manifest:l.assetParameters,session:d.assetParameters,runtime:this.runtimeParams},e({customerMacros:h?.sessionAssetParameterMacros??{},element:null,breakDurationSec:void 0})),m=r.ssb;this.streamReady=!1;const v=()=>r.ssb===m&&"switching"===m.phase,S=()=>{this.streamReady=!0,v()&&("active"!==o.externalPlaybackState?this.later(()=>{v()&&("active"===o.externalPlaybackState?this.markActive(m):this.fail("external-not-active",t,u))},1500):this.markActive(m))};o.load(u).then(()=>(S(),this.previousPaused||r.ssb!==m||"restoring"===m.phase?void 0:o.play())).catch(e=>this.fail("load-error",t,u,e)),o.setExternalPlaybackAllowed?.(!0),this.later(()=>{v()&&!this.streamReady&&this.fail("load-timeout",t,u)},15e3)},this.onExternalPlaybackChange=e=>{const{state:s}=this,t=s.ssb,i=e;t&&("active"===i?.state&&"switching"===t.phase&&this.streamReady?this.markActive(t):"inactive"===i?.state&&"restoring"!==t.phase&&this.restoreFromSsb("external-playback-ended"))},this.onPause=()=>{const e=this.state.ssb;"active"===e?.phase&&null!==e.activeAt&&Date.now()-e.activeAt<=3e3&&this.fail("receiver-paused",e.trigger)},o.player.on("externalplaybackchange",this.onExternalPlaybackChange),o.player.on("pause",this.onPause)}get isActive(){return null!==this.state.ssb}dispose(){this.clearTimers(),this.state.ssb=null,this.deps.player.off("externalplaybackchange",this.onExternalPlaybackChange),this.deps.player.off("pause",this.onPause)}reset(){this.clearTimers(),this.streamReady=!1,this.state.ssb=null}markActive(e){this.clearTimers(),e.phase="active",e.activeAt=Date.now(),this.later(()=>{this.state.ssb===e&&"active"===e.phase&&(e.announced=!0,this.emitChange(e.previousMode,"ssb"),this.deps.adBreakStatusTracker.refreshDeliveryMode())},3e3)}fail(e,s,t,i){this.state.ssb&&"restoring"!==this.state.ssb.phase&&(this.deps.diagnose("DA-SSB-SWITCH-FAILED",`The SSB stream switch failed: ${e}`,{reason:e,uri:t,trigger:s,...void 0===i?{}:{message:i instanceof Error?i.message:String(i)}}),this.restoreFromSsb("switch-failed"))}async restoreFromSsb(e){const{state:s}=this,{player:i}=this.deps,a=s.ssb,r=s.effectiveSessionManifest??s.sessionManifest,o=s.sessionConfig,{starter:n}=this;if(!(a&&"restoring"!==a.phase&&r&&o&&n))return;a.phase="restoring",this.clearTimers(),i.setExternalPlaybackAllowed?.(!1);const l=s.sessionGeneration,d=this.deps.breakScheduler;d?.setContentReloading(!0);let c=!1;if("ssai"===a.previousMode&&(c=await n.switchToSteeredSsai(r,o,!0)),c||(c=await this.restoreSgai(a,r,o,n,l)),s.ssb!==a)return;if(!c)return a.phase="failed",void d?.setContentReloading(!1);if(await this.waitForPlaying(),s.ssb!==a)return;d?.setContentReloading(!1),d?.skipPendingBreaksDueBy("played-by-ssb"),d?.resume();const h="ssai"===a.previousMode&&s.steeredSsaiActive?"ssai":"sgai";s.sessionDeliveryMode=h,s.ssb=null,this.deps.diagnose("DA-DELIVERY-MODE-RESOLVED","AirPlay ended: content restored",{mode:h,source:"external-playback-ended",cause:e}),this.rollSession(r);const p=t(a.announced,h);p&&this.emitChange(p.previousDeliveryMode,p.deliveryMode),this.deps.adBreakStatusTracker.refreshDeliveryMode(),n.applyPendingChannelSwitch()}async restoreSgai(e,s,t,i,a){const{player:o}=this.deps,{state:n}=this;if(!await this.reloadPrevious(e.previousSource))return!1;if(n.ssb!==e)return!0;const l=Number.isFinite(o.duration)?o.duration:null,d=r({live:e.previousLive,recordedPosition:e.previousPosition,duration:l});null!==d&&o.seek(d),this.previousPaused||await o.play().catch(()=>{}),await i.switchToSgai(s,t,a,!0);const c=this.deps.adPlayerController;return this.sessionMacros&&c?.setSessionAssetParameterMacros(this.sessionMacros),this.runtimeParams&&c?.updateAssetParameters(this.runtimeParams),!0}async reloadPrevious(e){let s;for(let t=1;t<=2;t++)try{return await this.deps.player.load(e),!0}catch(e){s=e}return this.deps.diagnose("DA-SSB-RESTORE-FAILED","The previous content source could not be reloaded.",{attempt:2,reason:"load-error",source:e,message:s instanceof Error?s.message:String(s)}),!1}waitForPlaying(){return new Promise(e=>{const s=()=>{this.deps.player.off("playing",s),e()};this.deps.player.on("playing",s),this.later(s,1e4)})}rollSession(e){this.deps.cmcdAdReporter.onSessionEnd(),this.deps.cmcdAdReporter.onSessionStart(e),this.deps.lensReporter.endSession(),this.deps.lensReporter.startSession()}emitChange(e,s){const{state:t}=this,i=t.effectiveSessionManifest??t.sessionManifest,a=i?.channelId??null;this.deps.emit("adchannelchange",{type:"adchannelchange",timestamp:Date.now(),previousChannelId:a,channelId:a,eventId:t.activeEventId,reason:"external-playback",manifestUrl:t.resolvedManifestUrl??t.sessionConfig?.manifestUrl??"",previousDeliveryMode:e,deliveryMode:s})}later(e,s){const t=setTimeout(()=>{this.timers.delete(t),e()},s);this.timers.add(t)}clearTimers(){for(const e of this.timers)clearTimeout(e);this.timers.clear()}}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Break, type BreakManifest } from '../types/BreakManifest';
|
|
2
|
+
import type { ScheduledBreak } from '../types/ScheduledBreak';
|
|
3
|
+
export interface ScheduledBreakInputs {
|
|
4
|
+
canPlay?: (brk: Break) => boolean;
|
|
5
|
+
mapPts?: (pts: number) => number | null;
|
|
6
|
+
played?: ReadonlySet<string>;
|
|
7
|
+
excluded?: ReadonlySet<string>;
|
|
8
|
+
}
|
|
9
|
+
export declare function buildScheduledBreaks(manifest: BreakManifest | null, inputs?: ScheduledBreakInputs): ScheduledBreak[];
|