@dolby-optiview/ads-sdk-core 1.2.0 â 1.3.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 -4
- package/dist/OptiViewAds.js +1 -1
- package/dist/external-playback/index.d.ts +4 -0
- package/dist/external-playback/index.js +1 -0
- package/dist/external-playback/observeExternalPlayback.d.ts +19 -0
- package/dist/external-playback/observeExternalPlayback.js +1 -0
- package/dist/external-playback/reconnectHold.d.ts +19 -0
- package/dist/external-playback/reconnectHold.js +1 -0
- package/dist/generated/diagnosticCodes.d.ts +45 -5
- 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 +6 -4
- package/dist/index.js +1 -1
- package/dist/interfaces/AdSourceQueue.d.ts +19 -0
- package/dist/interfaces/AdSourceQueue.js +1 -0
- package/dist/interfaces/PlayerAdapter.d.ts +31 -1
- package/dist/internal/index.d.ts +2 -0
- package/dist/internal/index.js +1 -1
- package/dist/services/AdBreakSequencer.d.ts +33 -4
- package/dist/services/AdBreakSequencer.js +1 -1
- package/dist/services/AdPlayerController.d.ts +8 -61
- package/dist/services/AdPlayerController.js +1 -1
- package/dist/services/BreakChain.d.ts +43 -0
- package/dist/services/BreakChain.js +1 -0
- package/dist/services/BreakChainRecorder.d.ts +63 -0
- package/dist/services/BreakChainRecorder.js +1 -0
- package/dist/services/BreakPreloadLedger.d.ts +13 -0
- package/dist/services/BreakPreloadLedger.js +1 -0
- package/dist/services/BreakScheduler.d.ts +20 -6
- package/dist/services/BreakScheduler.js +1 -1
- package/dist/services/BreakTransitionTimer.d.ts +27 -2
- package/dist/services/BreakTransitionTimer.js +1 -1
- package/dist/services/CmcdAdReporter.d.ts +1 -0
- package/dist/services/CmcdAdReporter.js +1 -1
- package/dist/services/ExternalPlaybackGate.d.ts +40 -0
- package/dist/services/ExternalPlaybackGate.js +1 -0
- package/dist/services/LiveStartPushbackController.d.ts +5 -10
- package/dist/services/LiveStartPushbackController.js +1 -1
- package/dist/services/ManifestService.d.ts +2 -1
- package/dist/services/ManifestService.js +1 -1
- package/dist/services/ReloadGuard.d.ts +11 -0
- package/dist/services/ReloadGuard.js +1 -0
- package/dist/services/RewindRearm.d.ts +3 -1
- package/dist/services/RewindRearm.js +1 -1
- package/dist/services/SsaiController.js +1 -1
- package/dist/services/adPlayer/AdPlayerCallbacks.d.ts +63 -0
- package/dist/services/adPlayer/AdPlayerCallbacks.js +1 -0
- package/dist/services/adPlayer/AdPlayerEvents.d.ts +2 -0
- package/dist/services/adPlayer/AdPlayerEvents.js +1 -1
- package/dist/services/adPlayer/AdPreloader.d.ts +2 -2
- package/dist/services/adPlayer/AdPreloader.js +1 -1
- package/dist/services/adPlayer/AdStage.d.ts +7 -4
- package/dist/services/adPlayer/AdStage.js +1 -1
- package/dist/services/adPlayer/AssetParameterResolver.d.ts +10 -2
- package/dist/services/adPlayer/AssetParameterResolver.js +1 -1
- package/dist/services/adPlayer/AssetPlayback.d.ts +6 -2
- package/dist/services/adPlayer/AssetPlayback.js +1 -1
- package/dist/services/adPlayer/BreakSequenceDriver.d.ts +9 -3
- package/dist/services/adPlayer/BreakSequenceDriver.js +1 -1
- package/dist/services/adPlayer/GamPodBridge.d.ts +11 -1
- package/dist/services/adPlayer/GamPodBridge.js +1 -1
- package/dist/services/adPlayer/PauseAdPresenter.d.ts +3 -1
- package/dist/services/adPlayer/PauseAdPresenter.js +1 -1
- package/dist/services/adPlayer/PreparedPresentation.js +1 -1
- package/dist/services/adPlayer/PresentationModeTracker.d.ts +1 -1
- package/dist/services/adPlayer/PresentationModeTracker.js +1 -1
- package/dist/services/adPlayer/SuccessorQueue.d.ts +36 -0
- package/dist/services/adPlayer/SuccessorQueue.js +1 -0
- package/dist/services/adPlayer/presentation/DomPresentationHost.d.ts +22 -0
- package/dist/services/adPlayer/presentation/DomPresentationHost.js +1 -0
- package/dist/services/adPlayer/presentation/LayoutTransform.d.ts +28 -0
- package/dist/services/adPlayer/presentation/LayoutTransform.js +1 -0
- package/dist/services/adPlayer/presentation/PresentationLayout.d.ts +19 -0
- package/dist/services/adPlayer/presentation/PresentationLayout.js +1 -0
- package/dist/services/assetParameterMacroTable.js +1 -1
- package/dist/services/assetParameterMacros.d.ts +9 -9
- package/dist/services/assetParameterMacros.js +1 -1
- package/dist/services/channelSwitch.d.ts +2 -2
- package/dist/services/externalPlaybackDiagnostic.d.ts +3 -0
- package/dist/services/externalPlaybackDiagnostic.js +1 -0
- package/dist/services/externalPlaybackGuard.d.ts +24 -0
- package/dist/services/externalPlaybackGuard.js +1 -0
- package/dist/services/parseBreakManifest.d.ts +2 -1
- package/dist/services/parseBreakManifest.js +1 -1
- package/dist/services/resolveChannelEvents.d.ts +17 -0
- package/dist/services/resolveChannelEvents.js +1 -0
- package/dist/services/resolveExternalVendorConfigurations.js +1 -1
- package/dist/services/resolveGamVendorConfiguration.d.ts +6 -1
- package/dist/services/resolveGamVendorConfiguration.js +1 -1
- package/dist/services/ssbStream.d.ts +39 -0
- package/dist/services/ssbStream.js +1 -0
- package/dist/services/ssbSwitchSignals.d.ts +15 -0
- package/dist/services/ssbSwitchSignals.js +1 -0
- package/dist/services/staticAssetUrl.d.ts +28 -0
- package/dist/services/staticAssetUrl.js +1 -0
- package/dist/session/AdBreakStatusTracker.d.ts +18 -0
- package/dist/session/AdBreakStatusTracker.js +1 -1
- package/dist/session/BreakLifecycle.d.ts +6 -0
- package/dist/session/BreakLifecycle.js +1 -1
- package/dist/session/SessionStarter.d.ts +6 -43
- package/dist/session/SessionStarter.js +1 -1
- package/dist/session/SessionStarterDeps.d.ts +39 -0
- package/dist/session/SessionStarterDeps.js +1 -0
- package/dist/session/SessionStarterHelpers.d.ts +36 -0
- package/dist/session/SessionStarterHelpers.js +1 -0
- package/dist/session/SessionState.d.ts +22 -2
- package/dist/session/SessionState.js +1 -1
- package/dist/session/SsbSwitch.d.ts +55 -0
- package/dist/session/SsbSwitch.js +1 -0
- package/dist/session/breakMeasurement.d.ts +16 -0
- package/dist/session/breakMeasurement.js +1 -0
- package/dist/session/contentSnapback.d.ts +13 -0
- package/dist/session/contentSnapback.js +1 -0
- package/dist/session/createExternalPlaybackGate.d.ts +19 -0
- package/dist/session/createExternalPlaybackGate.js +1 -0
- package/dist/session/sessionTick.d.ts +7 -0
- package/dist/session/sessionTick.js +1 -0
- package/dist/types/BreakManifest.d.ts +34 -2
- package/dist/types/Events.d.ts +20 -10
- package/dist/types/OptiViewAdsConfig.d.ts +24 -8
- package/package.json +5 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare class BreakPreloadLedger {
|
|
2
|
+
private readonly preloadedBreakIds;
|
|
3
|
+
private readonly rejectedBreakIds;
|
|
4
|
+
private retryRejectedPreloadsAtStart;
|
|
5
|
+
wasPreloaded(breakId: string): boolean;
|
|
6
|
+
wasRejected(breakId: string): boolean;
|
|
7
|
+
shouldRetryRejectedAtStart(breakId: string): boolean;
|
|
8
|
+
checkAtBreakStart(wasHeld: boolean, check: () => void): void;
|
|
9
|
+
recordAttempt(breakId: string, accepted: boolean): void;
|
|
10
|
+
deletePreloaded(breakId: string): void;
|
|
11
|
+
delete(breakId: string): void;
|
|
12
|
+
clear(): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class BreakPreloadLedger{constructor(){this.preloadedBreakIds=new Set,this.rejectedBreakIds=new Set,this.retryRejectedPreloadsAtStart=!1}wasPreloaded(e){return this.preloadedBreakIds.has(e)}wasRejected(e){return this.rejectedBreakIds.has(e)}shouldRetryRejectedAtStart(e){return this.retryRejectedPreloadsAtStart&&this.wasRejected(e)}checkAtBreakStart(e,t){this.retryRejectedPreloadsAtStart=e;try{t()}finally{this.retryRejectedPreloadsAtStart=!1}}recordAttempt(e,t){t?(this.rejectedBreakIds.delete(e),this.preloadedBreakIds.add(e)):this.rejectedBreakIds.add(e)}deletePreloaded(e){this.preloadedBreakIds.delete(e)}delete(e){this.deletePreloaded(e),this.rejectedBreakIds.delete(e)}clear(){this.preloadedBreakIds.clear(),this.rejectedBreakIds.clear()}}
|
|
@@ -3,8 +3,10 @@ import type { AdBreakSkipReason } from '../types/Events';
|
|
|
3
3
|
import { Break, BreakManifest } from '../types/BreakManifest';
|
|
4
4
|
import { type ResolvedBreakWarnings } from '../types/OptiViewAdsConfig';
|
|
5
5
|
import type { MetadataCueTracker } from './MetadataCueTracker';
|
|
6
|
+
import { type ChainSuccessorChangedCallback } from './BreakChain';
|
|
6
7
|
import { type BreakAbortedCallback, type BreakApproachingCallback, type BreakStartCallback, type BreakWarningCallback, type Clock, type SchedulerDiagnose, type TuneInOptions } from './BreakSchedulerTypes';
|
|
7
8
|
export { BreakState, systemClock, type BreakAbortedCallback, type BreakApproachingCallback, type BreakStartCallback, type BreakStartContext, type BreakWarningCallback, type Clock, type SchedulerDiagnose, type TuneInOptions, } from './BreakSchedulerTypes';
|
|
9
|
+
export type { ChainSuccessorChangedCallback } from './BreakChain';
|
|
8
10
|
/** Scheduling cadence; a break is discovered up to one interval late on the tick alone. */
|
|
9
11
|
export declare const TICK_INTERVAL_MS = 250;
|
|
10
12
|
export declare class BreakScheduler {
|
|
@@ -27,12 +29,13 @@ export declare class BreakScheduler {
|
|
|
27
29
|
private onBreakWarning;
|
|
28
30
|
private onBreakAborted;
|
|
29
31
|
private onBreakSkipped;
|
|
32
|
+
private beforeBreakCheck;
|
|
30
33
|
private breakStartsHeld;
|
|
34
|
+
private readonly preloadLedger;
|
|
31
35
|
private onAdStartDelayElapsed;
|
|
32
36
|
private adStartDelayElapsedNotified;
|
|
33
37
|
/** Break ids whose early-return abort was already notified; polls keep arriving during the teardown. */
|
|
34
38
|
private abortNotifiedBreakIds;
|
|
35
|
-
private preloadedBreakIds;
|
|
36
39
|
private breakWarnings;
|
|
37
40
|
/** Warning thresholds already fired per break, so each `breakWarnings.seconds` value emits once. */
|
|
38
41
|
private breakWarningFired;
|
|
@@ -46,8 +49,7 @@ export declare class BreakScheduler {
|
|
|
46
49
|
/** Wallclock instant content playback ended (spec `event: 'end'`), or null while content plays. */
|
|
47
50
|
private playbackEndedAtMs;
|
|
48
51
|
private debug;
|
|
49
|
-
private
|
|
50
|
-
private maxChainGapMs;
|
|
52
|
+
private readonly chain;
|
|
51
53
|
private tuneInEnabled;
|
|
52
54
|
private minBreakDurationMs;
|
|
53
55
|
private clock;
|
|
@@ -97,9 +99,11 @@ export declare class BreakScheduler {
|
|
|
97
99
|
getChainedSuccessor(breakId: string): Break | null;
|
|
98
100
|
/** Promote a CHAINED (or PENDING) break to ACTIVE as the chain advances, so completeBreak() cleans it up. */
|
|
99
101
|
activateChainedBreak(breakId: string): void;
|
|
100
|
-
/**
|
|
101
|
-
|
|
102
|
+
/** Notified when a manifest update adds, changes or removes the active break's chained successor. */
|
|
103
|
+
setOnChainSuccessorChanged(callback: ChainSuccessorChangedCallback | null): void;
|
|
102
104
|
updateManifest(manifest: BreakManifest): void;
|
|
105
|
+
applyEventBoundary(manifest: BreakManifest): void;
|
|
106
|
+
setEventBoundaryCheck(check: ((positionMs: number | null) => boolean) | null): void;
|
|
103
107
|
/**
|
|
104
108
|
* Re-align the schedule to a manifest from a different channel (see `detectChannelSwitch`).
|
|
105
109
|
* Every ledger of the previous channel is dropped so a break id reused by the new channel
|
|
@@ -139,6 +143,11 @@ export declare class BreakScheduler {
|
|
|
139
143
|
*/
|
|
140
144
|
private armStartDeadline;
|
|
141
145
|
private clearStartDeadline;
|
|
146
|
+
/** Halt the tick, keeping tracked breaks and callbacks; {@link resume} restarts it. */
|
|
147
|
+
suspend(): void;
|
|
148
|
+
resume(): void;
|
|
149
|
+
/** Retire the PENDING breaks that started while another stream played; returns their ids. */
|
|
150
|
+
skipPendingBreaksDueBy(reason: AdBreakSkipReason): string[];
|
|
142
151
|
stop(): void;
|
|
143
152
|
completeBreak(breakId: string): void;
|
|
144
153
|
/** Bracket a content RELOAD so the new stream's start is not read as a backward seek. `false` also discards the stale previous position. */
|
|
@@ -159,6 +168,11 @@ export declare class BreakScheduler {
|
|
|
159
168
|
getActiveBreak(): Break | null;
|
|
160
169
|
/** Rounded seconds until a PENDING break starts, or `null`; refreshes the `upcoming` countdown without polling events. */
|
|
161
170
|
getSecondsUntilBreak(breakId: string): number | null;
|
|
171
|
+
/** The PENDING break starting soonest (ties: smaller id) with the seconds until it; null when none has a known start. */
|
|
172
|
+
getNextPendingBreak(): {
|
|
173
|
+
id: string;
|
|
174
|
+
secondsUntil: number;
|
|
175
|
+
} | null;
|
|
162
176
|
/**
|
|
163
177
|
* Advance the schedule NOW because a cue reached the playhead: the 250ms tick discovers a start
|
|
164
178
|
* ~125ms late on average, an adapter reporting PRESENTATION knows the instant. Runs
|
|
@@ -178,7 +192,7 @@ export declare class BreakScheduler {
|
|
|
178
192
|
/** While true no break may start and consumers must not apply break-driven restrictions (e.g. pause ads). */
|
|
179
193
|
isAdStartDelayActive(): boolean;
|
|
180
194
|
private getCurrentTime;
|
|
181
|
-
/**
|
|
195
|
+
/** Retry rejected callbacks; an accepted preload fires at most once per break. */
|
|
182
196
|
private maybePreload;
|
|
183
197
|
private getBreakStartTime;
|
|
184
198
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BreakEvent as e,Timebase as t,getBreakEvent as i,getPostRollDelay as a,getPreRollDelay as s,isEventTrigger as r,isPauseBreak as n}from"../types/BreakManifest";import{DEFAULT_BREAK_WARNINGS as d,DEFAULT_PDT_GRACE_SECONDS as l}from"../types/OptiViewAdsConfig";import{PTS_MATCH_TOLERANCE_SECONDS as h}from"./MetadataCueTracker";import{debugLog as o}from"../diagnostics/debugLog";import{createBreakTimingState as k,describeBreakStart as c,diagnoseAnvatoBreakEnd as u,getBreakStartTime as b,getCurrentTime as g,secondsUntilBreak as S}from"./breakTiming";import{RewindRearm as m}from"./RewindRearm";import{BreakState as p,systemClock as B}from"./BreakSchedulerTypes";export{BreakState,systemClock}from"./BreakSchedulerTypes";export const TICK_INTERVAL_MS=250;export class BreakScheduler{constructor(e,t=!1,i={enabled:!1,maxGapSeconds:0},a=B,s={enabled:!1,minBreakDurationSeconds:0},r=null,n=d,h=null,o=l){this.breaks=new Map,this.timing=k(),this.checkInterval=null,this.startDeadline=null,this.nextStartDeadlineMs=null,this.onBreakStart=null,this.onBreakApproaching=null,this.onBreakWarning=null,this.onBreakAborted=null,this.onBreakSkipped=null,this.breakStartsHeld=!1,this.onAdStartDelayElapsed=null,this.adStartDelayElapsedNotified=!1,this.abortNotifiedBreakIds=new Set,this.preloadedBreakIds=new Set,this.breakWarnings=d,this.breakWarningFired=new Map,this.completedBreakIds=new Set,this.unsupportedEventBreakIds=new Set,this.playbackEndedAtMs=null,this.anvatoEnteredDiagnosed=new Set,this.retiredBreaks=new Map,this.minSeenTime=null,this.JOIN_DISCRIMINATOR_SLACK_MS=1e3,this.WINDOW_ESCAPE_GRACE_MS=12e3,this.TIMING_TOLERANCE_MS=5e3,this.PRELOAD_AHEAD_SECONDS=8,this.contentReloading=!1,this.startupSeeking=!1,this.player=e,this.debug=t,this.chainingEnabled=i.enabled,this.maxChainGapMs=1e3*Math.max(0,i.maxGapSeconds),this.tuneInEnabled=s.enabled,this.minBreakDurationMs=1e3*Math.max(0,s.minBreakDurationSeconds),this.clock=a,this.diagnose=r,this.breakWarnings=n,this.cueTracker=h;const c=Math.max(0,o);this.timingDeps={player:this.player,clock:this.clock,cueTracker:this.cueTracker,diagnose:this.diagnose,log:(...e)=>this.log(...e),pdtGraceSeconds:c,pdtGraceMs:1e3*c},this.rewind=new m({breaks:this.breaks,retiredBreaks:this.retiredBreaks,completedBreakIds:this.completedBreakIds,preloadedBreakIds:this.preloadedBreakIds,timing:this.timing,clock:this.clock,log:(...e)=>this.log(...e),getBreakStartTime:e=>this.getBreakStartTime(e)})}claimChain(e){if(!this.chainingEnabled)return;let t=this.breaks.get(e)?.break??null;for(;t;){const e=this.findSuccessor(t);if(!e)break;const i=this.breaks.get(e.id);if(!i||i.state!==p.PENDING)break;i.state=p.CHAINED,this.log(`đ Break "${e.id}" claimed as chained after "${t.id}"`),t=e}}getChainedSuccessor(e){if(!this.chainingEnabled)return null;const t=this.breaks.get(e)?.break;return t?this.findSuccessor(t,[p.PENDING,p.CHAINED]):null}activateChainedBreak(e){const t=this.breaks.get(e);t&&(t.state=p.ACTIVE)}findSuccessor(e,t=[p.PENDING]){const i=this.getBreakStartTime(e);if(null===i)return null;const a=i+1e3*(e.duration??0);let s=null;for(const r of this.breaks.values()){if(r.break.id===e.id)continue;if(!t.includes(r.state))continue;const n=this.getBreakStartTime(r.break);if(null===n)continue;if(n<=i)continue;const d=n-a;d>this.maxChainGapMs||(d<-this.TIMING_TOLERANCE_MS||(!s||n<s.start)&&(s={break:r.break,start:n}))}return s?.break??null}updateManifest(e){this.timing.timebase=e.timebase;const t=this.rewind.adStartDelayMs;this.rewind.setAdStartDelayMs(1e3*Math.max(0,e.adStartDelay??0)),t>0&&0===this.rewind.adStartDelayMs&&this.notifyAdStartDelayElapsed();for(const t of e.breaks){if(n(t))continue;if(!this.isSchedulableEvent(t))continue;if(this.completedBreakIds.has(t.id))continue;const e=this.breaks.get(t.id);e?e.state===p.PENDING&&(e.break=t):(this.breaks.set(t.id,{break:t,state:p.PENDING}),this.log(`đ
Break scheduled: "${t.id}" at ${c(t)}`))}const i=new Set(e.breaks.map(e=>e.id));for(const[e,t]of this.breaks)i.has(e)||t.state!==p.ACTIVE||this.abortNotifiedBreakIds.has(e)||(this.abortNotifiedBreakIds.add(e),this.log("Break removed from manifest while active (early return):",e),this.diagnose?.("DA-BREAK-EARLY-RETURN","Active break removed from manifest; returning to content early",{breakId:e}),this.onBreakAborted?.(t.break));this.cleanupBreaks(i)}resetForChannelSwitch(e){const t=this.getActiveBreak();for(const e of[...this.breaks.keys()])e!==t?.id&&this.breaks.delete(e);for(const e of[...this.abortNotifiedBreakIds])e!==t?.id&&this.abortNotifiedBreakIds.delete(e);this.completedBreakIds.clear(),this.retiredBreaks.clear(),this.preloadedBreakIds.clear(),this.breakWarningFired.clear(),this.unsupportedEventBreakIds.clear(),this.anvatoEnteredDiagnosed.clear(),this.timing.anvatoPendingDiagnosed.clear(),this.timing.anvatoMatchedDiagnosed.clear(),this.timing.anvatoEndDiagnosed.clear(),this.playbackEndedAtMs=null,this.rewind.beginChannel();const i=t?{...e,breaks:e.breaks.filter(e=>e.id!==t.id)}:e;this.updateManifest(i)}isSchedulableEvent(t){return!r(t.start)||t.start.event===e.START||t.start.event===e.END||(this.unsupportedEventBreakIds.has(t.id)||(this.unsupportedEventBreakIds.add(t.id),this.log(`â ī¸ Break ignored: "${t.id}" (unsupported event trigger "${t.start.event}")`),this.diagnose?.("DA-BREAK-EVENT-UNSUPPORTED","Break triggered by an event this SDK version does not schedule; the break was ignored",{breakId:t.id,event:t.start.event})),!1)}markPlaybackStart(){null!==this.playbackEndedAtMs&&(this.playbackEndedAtMs=null,this.log("Content replayed; pending post-rolls cancelled")),this.rewind.playbackStarted||(this.rewind.markPlaybackStart(this.peekPosition()),this.log("Content playback started at",new Date(this.clock.now()).toISOString()))}setOnAdStartDelayElapsed(e){this.onAdStartDelayElapsed=e}holdBreakStarts(){this.breakStartsHeld=!0}releaseBreakStarts(){this.breakStartsHeld=!1,this.checkBreaks()}areBreakStartsHeld(){return this.breakStartsHeld||this.rewind.isAdStartDelayActive()}notifyAdStartDelayElapsed(){this.adStartDelayElapsedNotified||(this.adStartDelayElapsedNotified=!0,this.onAdStartDelayElapsed?.())}markPlaybackEnd(){null===this.playbackEndedAtMs&&(this.playbackEndedAtMs=this.clock.now(),this.log("Content playback ended at",new Date(this.playbackEndedAtMs).toISOString()))}start(e,t,i,a,s){this.onBreakStart=e,this.onBreakApproaching=t??null,this.onBreakWarning=i??null,this.onBreakAborted=a??null,this.onBreakSkipped=s??null,this.checkInterval&&clearInterval(this.checkInterval),this.checkInterval=setInterval(()=>{this.tick()},250),this.log("Scheduler started")}tick(){this.checkBreaks(),null!==this.nextStartDeadlineMs&&this.armStartDeadline(this.nextStartDeadlineMs)}armStartDeadline(e){null!==this.checkInterval&&null===this.startDeadline&&(this.startDeadline=setTimeout(()=>{this.startDeadline=null,this.tick()},Math.max(1,Math.ceil(e))))}clearStartDeadline(){this.startDeadline&&clearTimeout(this.startDeadline),this.startDeadline=null}stop(){this.checkInterval&&(clearInterval(this.checkInterval),this.checkInterval=null),this.clearStartDeadline(),this.onBreakStart=null,this.onBreakSkipped=null,this.log("Scheduler stopped")}completeBreak(e){this.completedBreakIds.add(e),this.breakWarningFired.delete(e),this.abortNotifiedBreakIds.delete(e);const t=this.breaks.get(e);t&&(this.log("Break completed and removed:",e),null===i(t.break)&&null===this.cueTracker&&this.retiredBreaks.set(e,t.break),this.breaks.delete(e),this.preloadedBreakIds.delete(e))}setContentReloading(e){this.contentReloading=e,this.rewind.setContentReloading(e||this.startupSeeking)}setStartupSeeking(e){this.startupSeeking=e,this.rewind.setContentReloading(e||this.contentReloading)}notifyResumeSeek(e,t){this.rewind.notifyResumeSeek(e,t)}notifyViewerSeek(){this.rewind.notifyViewerSeek()}skipBreak(e,t){e.state=p.SKIPPED,this.completedBreakIds.add(e.break.id),this.onBreakSkipped?.(e.break,t)}cleanupBreaks(e){const t=[];for(const[i,a]of this.breaks)a.state!==p.COMPLETED&&a.state!==p.SKIPPED?e.has(i)||a.state===p.ACTIVE||(this.skipBreak(a,"removed"),t.push(i)):t.push(i);for(const e of t)this.breaks.delete(e),this.preloadedBreakIds.delete(e),this.breakWarningFired.delete(e)}getActiveBreak(){for(const e of this.breaks.values())if(e.state===p.ACTIVE)return e.break;return null}getSecondsUntilBreak(e){const t=this.breaks.get(e);if(!t||t.state!==p.PENDING)return null;const i=this.getCurrentTime();if(null===i)return null;const a=this.getBreakStartTime(t.break);return null===a?null:S(a,i)}notifyCuePresented(e){if(null===this.cueTracker||this.timing.timebase!==t.PTS)return null;if(!Number.isFinite(e))return null;let i=null;for(const t of this.breaks.values())if(t.state===p.PENDING&&"number"==typeof t.break.start&&!(Math.abs(t.break.start-e)>h)){i=t.break.id;break}if(null===i)return null;const a=this.player.currentTime;this.checkBreaks();const s=this.breaks.get(i);return s&&s.state===p.ACTIVE?this.anvatoEnteredDiagnosed.has(i)?null:(this.anvatoEnteredDiagnosed.add(i),this.log(`đ¯ Break "${i}" armed by cue presentation (pts ${e})`),this.diagnose?.("DA-ANVATO-CUE-ENTERED",`Break "${i}" started from the presented cue (pts ${e}) rather than the scheduler tick.`,{breakId:i,pts:e,mediaTimeSec:a}),i):null}checkBreaks(){if(this.nextStartDeadlineMs=null,this.startupSeeking)return;const e=this.getCurrentTime();this.rewind.trackPlayedTime(e),this.rewind.adStartDelayMs>0&&!this.rewind.isAdStartDelayActive()&&this.notifyAdStartDelayElapsed();const i=this.areBreakStartsHeld();null!==e&&(this.minSeenTime=null===this.minSeenTime?e:Math.min(this.minSeenTime,e),this.rewind.rearmOnRewind(e));for(const r of this.breaks.values()){if(r.state===p.ACTIVE&&this.timing.timebase===t.WALLCLOCK&&null!==e&&!this.abortNotifiedBreakIds.has(r.break.id)){const t=this.getBreakStartTime(r.break),i=1e3*(r.break.duration??0);null!==t&&i>0&&e>t+i+this.WINDOW_ESCAPE_GRACE_MS&&(this.abortNotifiedBreakIds.add(r.break.id),this.log(`â ī¸ Break window escaped: "${r.break.id}" (ended ${Math.round((e-t-i)/1e3)}s ago on the stream clock)`),this.diagnose?.("DA-BREAK-WINDOW-ESCAPED","Active break outlived its wallclock window; returning to content",{breakId:r.break.id,overrunSec:Math.round((e-t-i)/100)/10}),this.onBreakAborted?.(r.break));continue}if(r.state===p.ACTIVE&&this.timing.timebase===t.PTS&&u(this.timing,this.timingDeps,r.break),r.state!==p.PENDING)continue;const n=s(r.break);if(null!==n){this.checkPreRollBreak(r,n,i);continue}const d=a(r.break);if(null!==d){this.checkPostRollBreak(r,d,i);continue}if(null===e)continue;if(i)continue;null!==this.cueTracker&&this.timing.timebase===t.PTS&&this.maybePreload(r.break,"manifest arrival â Anvato flow");const l=this.getBreakStartTime(r.break);if(null===l)continue;const h=e-l,o=Math.round(-h/1e3);if(this.timing.timebase===t.WALLCLOCK&&h<0&&-h<=250&&(this.nextStartDeadlineMs=Math.min(this.nextStartDeadlineMs??1/0,-h)),this.checkWarnings(r.break,o),o>0&&o<=15&&o%2==0&&this.log(`âąī¸ Break "${r.break.id}" starts in ${o}s`),o>0&&o<=this.PRELOAD_AHEAD_SECONDS&&this.maybePreload(r.break,`${o}s ahead`),h>=0&&h<=this.TIMING_TOLERANCE_MS){const e=Math.max(0,h),t=1e3*(r.break.duration??0)-e;if(t<=0){this.log(`â ī¸ Break missed: "${r.break.id}" (ended ${Math.round(-t/1e3)}s ago, inside the start tolerance)`),this.skipBreak(r,t>0&&this.tuneInEnabled?"insufficient-remaining":"late"),null===this.cueTracker&&this.breaks.get(r.break.id)===r&&this.retiredBreaks.set(r.break.id,r.break);break}if(null!==this.minSeenTime&&l<this.minSeenTime-this.JOIN_DISCRIMINATOR_SLACK_MS&&(!this.tuneInEnabled||t<this.minBreakDurationMs)){this.log(`â ī¸ Break missed: "${r.break.id}" (joined in progress inside the start tolerance, ${Math.round(t/1e3)}s remaining < tune-in rules)`),this.skipBreak(r,t>0&&this.tuneInEnabled?"insufficient-remaining":"late"),null===this.cueTracker&&this.breaks.get(r.break.id)===r&&this.retiredBreaks.set(r.break.id,r.break);break}this.log(`đŦ BREAK TRIGGERED: "${r.break.id}"`),r.state=p.ACTIVE,this.onBreakStart&&this.onBreakStart(r.break,{tuneIn:!1,elapsedSec:e/1e3,remainingSec:t/1e3});break}if(h>this.TIMING_TOLERANCE_MS){const t=l+1e3*(r.break.duration??0)-e;if(this.tuneInEnabled&&t>=this.minBreakDurationMs&&t>0){this.log(`đŦ TUNE-IN BREAK TRIGGERED: "${r.break.id}" (${Math.round(t/1e3)}s remaining)`),r.state=p.ACTIVE,this.onBreakStart&&this.onBreakStart(r.break,{tuneIn:!0,elapsedSec:h/1e3,remainingSec:t/1e3});break}this.log(`â ī¸ Break missed: "${r.break.id}" (was ${Math.round(h/1e3)}s ago)`),this.skipBreak(r,t>0&&this.tuneInEnabled?"insufficient-remaining":"late"),null===this.cueTracker&&this.breaks.get(r.break.id)===r&&this.retiredBreaks.set(r.break.id,r.break)}}}checkPreRollBreak(e,t,i){if(!this.rewind.playbackStarted)return;const a=1e3*t;if(this.rewind.adStartDelayMs>0&&a<this.rewind.adStartDelayMs)return this.log(`â ī¸ Pre-roll skipped: "${e.break.id}" (delay ${t}s < adStartDelay ${this.rewind.adStartDelayMs/1e3}s)`),void this.skipBreak(e,"start-delay");const s=this.rewind.channelPlayedMs,r=Math.round((a-s)/1e3);r>0&&r<=15&&r%2==0&&this.log(`âąī¸ Pre-roll "${e.break.id}" starts in ${r}s`),r>0&&r<=this.PRELOAD_AHEAD_SECONDS&&this.maybePreload(e.break,`pre-roll, ${r}s ahead`),s>=a&&!i&&(this.log(`đŦ PRE-ROLL TRIGGERED: "${e.break.id}"${t>0?` (after ${t}s delay)`:""}`),e.state=p.ACTIVE,this.onBreakStart&&this.onBreakStart(e.break,{tuneIn:!1,elapsedSec:0,remainingSec:e.break.duration??0}))}checkPostRollBreak(e,t,i){if(null===this.playbackEndedAtMs)return;const a=1e3*t,s=this.clock.now()-this.playbackEndedAtMs,r=Math.round((a-s)/1e3);r>0&&r<=this.PRELOAD_AHEAD_SECONDS&&this.maybePreload(e.break,`post-roll, ${r}s ahead`),s>=a&&!i&&(this.log(`đŦ POST-ROLL TRIGGERED: "${e.break.id}"${t>0?` (after ${t}s delay)`:""}`),e.state=p.ACTIVE,this.onBreakStart&&this.onBreakStart(e.break,{tuneIn:!1,elapsedSec:0,remainingSec:e.break.duration??0}))}isAdStartDelayActive(){return this.areBreakStartsHeld()}getCurrentTime(){return g(this.timing,this.timingDeps)}maybePreload(e,t){if(this.preloadedBreakIds.has(e.id)||!this.onBreakApproaching)return;this.log(`âŗ Preloading break "${e.id}" (${t})`);!1!==this.onBreakApproaching(e)&&this.preloadedBreakIds.add(e.id)}getBreakStartTime(e){return r(e.start)?null:b(this.timing,this.timingDeps,e)}peekPosition(){if(this.timing.timebase!==t.WALLCLOCK)return 1e3*this.player.currentTime;const e=this.player.programDateTime?.getTime();return void 0===e||isNaN(e)?null:e}destroy(){this.stop(),this.breaks.clear(),this.preloadedBreakIds.clear(),this.completedBreakIds.clear(),this.retiredBreaks.clear(),this.minSeenTime=null,this.breakStartsHeld=!1,this.onAdStartDelayElapsed=null,this.adStartDelayElapsedNotified=!1,this.unsupportedEventBreakIds.clear(),this.playbackEndedAtMs=null,this.rewind.reset(),this.breakWarningFired.clear(),this.abortNotifiedBreakIds.clear(),this.timing.pdtMissingWarned=!1,this.timing.pdtSeen=!1,this.timing.firstPdtAttemptMs=null,this.timing.playbackStartedAtMs=null,this.timing.anvatoPendingDiagnosed.clear(),this.timing.anvatoMatchedDiagnosed.clear(),this.timing.anvatoEndDiagnosed.clear(),this.cueTracker?.clear()}checkWarnings(e,t){if(t<=0||0===this.breakWarnings.seconds.length)return;let i=this.breakWarningFired.get(e.id);i||(i=new Set,this.breakWarningFired.set(e.id,i));for(const a of this.breakWarnings.seconds)i.has(a)||t<=a&&(i.add(a),this.log(`Break warning for "${e.id}" (${t}s <= ${a}s)`),this.onBreakWarning?.(e,a,t))}log(...e){this.debug&&o("[OptiViewAds:BreakScheduler]",...e)}}
|
|
1
|
+
import{BreakEvent as e,Timebase as t,getBreakEvent as a,getPostRollDelay as i,getPreRollDelay as r,isEventTrigger as s,isPauseBreak as n}from"../types/BreakManifest";import{DEFAULT_BREAK_WARNINGS as d,DEFAULT_PDT_GRACE_SECONDS as h}from"../types/OptiViewAdsConfig";import{PTS_MATCH_TOLERANCE_SECONDS as l}from"./MetadataCueTracker";import{debugLog as o}from"../diagnostics/debugLog";import{createBreakTimingState as k,describeBreakStart as c,diagnoseAnvatoBreakEnd as u,getBreakStartTime as b,getCurrentTime as g,secondsUntilBreak as S}from"./breakTiming";import{BreakPreloadLedger as m}from"./BreakPreloadLedger";import{skipBreaksStartedBy as p,soonestBreak as B}from"./ssbStream";import{RewindRearm as E}from"./RewindRearm";import{BreakChain as f}from"./BreakChain";import{BreakState as D,systemClock as I}from"./BreakSchedulerTypes";export{BreakState,systemClock}from"./BreakSchedulerTypes";export const TICK_INTERVAL_MS=250;export class BreakScheduler{constructor(e,t=!1,a={enabled:!1,maxGapSeconds:0},i=I,r={enabled:!1,minBreakDurationSeconds:0},s=null,n=d,l=null,o=h){this.breaks=new Map,this.timing=k(),this.checkInterval=null,this.startDeadline=null,this.nextStartDeadlineMs=null,this.onBreakStart=null,this.onBreakApproaching=null,this.onBreakWarning=null,this.onBreakAborted=null,this.onBreakSkipped=null,this.beforeBreakCheck=null,this.breakStartsHeld=!1,this.preloadLedger=new m,this.onAdStartDelayElapsed=null,this.adStartDelayElapsedNotified=!1,this.abortNotifiedBreakIds=new Set,this.breakWarnings=d,this.breakWarningFired=new Map,this.completedBreakIds=new Set,this.unsupportedEventBreakIds=new Set,this.playbackEndedAtMs=null,this.anvatoEnteredDiagnosed=new Set,this.retiredBreaks=new Map,this.minSeenTime=null,this.JOIN_DISCRIMINATOR_SLACK_MS=1e3,this.WINDOW_ESCAPE_GRACE_MS=12e3,this.TIMING_TOLERANCE_MS=5e3,this.PRELOAD_AHEAD_SECONDS=8,this.contentReloading=!1,this.startupSeeking=!1,this.player=e,this.debug=t,this.chain=new f({breaks:this.breaks,enabled:a.enabled,maxGapMs:1e3*Math.max(0,a.maxGapSeconds),toleranceMs:this.TIMING_TOLERANCE_MS,getBreakStartTime:e=>this.getBreakStartTime(e),log:(...e)=>this.log(...e)}),this.tuneInEnabled=r.enabled,this.minBreakDurationMs=1e3*Math.max(0,r.minBreakDurationSeconds),this.clock=i,this.diagnose=s,this.breakWarnings=n,this.cueTracker=l;const c=Math.max(0,o);this.timingDeps={player:this.player,clock:this.clock,cueTracker:this.cueTracker,diagnose:this.diagnose,log:(...e)=>this.log(...e),pdtGraceSeconds:c,pdtGraceMs:1e3*c},this.rewind=new E({breaks:this.breaks,retiredBreaks:this.retiredBreaks,completedBreakIds:this.completedBreakIds,preloadLedger:this.preloadLedger,timing:this.timing,clock:this.clock,log:(...e)=>this.log(...e),getBreakStartTime:e=>this.getBreakStartTime(e)})}claimChain(e){this.chain.claim(e)}getChainedSuccessor(e){return this.chain.successorOf(e)}activateChainedBreak(e){this.chain.activate(e)}setOnChainSuccessorChanged(e){this.chain.setOnSuccessorChanged(e)}updateManifest(e){this.timing.timebase=e.timebase;const t=this.rewind.adStartDelayMs;this.rewind.setAdStartDelayMs(1e3*Math.max(0,e.adStartDelay??0)),t>0&&0===this.rewind.adStartDelayMs&&this.notifyAdStartDelayElapsed();for(const t of e.breaks){if(n(t))continue;if(!this.isSchedulableEvent(t))continue;if(this.completedBreakIds.has(t.id))continue;const e=this.breaks.get(t.id);e?e.state!==D.PENDING&&e.state!==D.CHAINED||(e.break=t):(this.breaks.set(t.id,{break:t,state:D.PENDING}),this.log(`đ
Break scheduled: "${t.id}" at ${c(t)}`))}const a=new Set(e.breaks.map(e=>e.id));for(const[e,t]of this.breaks)a.has(e)||t.state!==D.ACTIVE||this.abortNotifiedBreakIds.has(e)||(this.abortNotifiedBreakIds.add(e),this.log("Break removed from manifest while active (early return):",e),this.diagnose?.("DA-BREAK-EARLY-RETURN","Active break removed from manifest; returning to content early",{breakId:e}),this.onBreakAborted?.(t.break));this.cleanupBreaks(a);const i=this.getActiveBreak();i&&!this.abortNotifiedBreakIds.has(i.id)&&this.chain.refresh(i)}applyEventBoundary(e){const t=this.rewind.playbackStarted;this.updateManifest(t?{...e,adStartDelay:0}:e),t&&this.rewind.ignoreAdStartDelay()}setEventBoundaryCheck(e){this.beforeBreakCheck=e}resetForChannelSwitch(e){const t=this.getActiveBreak();for(const e of[...this.breaks.keys()])e!==t?.id&&this.breaks.delete(e);for(const e of[...this.abortNotifiedBreakIds])e!==t?.id&&this.abortNotifiedBreakIds.delete(e);this.completedBreakIds.clear(),this.retiredBreaks.clear(),this.preloadLedger.clear(),this.breakWarningFired.clear(),this.unsupportedEventBreakIds.clear(),this.anvatoEnteredDiagnosed.clear(),this.timing.anvatoPendingDiagnosed.clear(),this.timing.anvatoMatchedDiagnosed.clear(),this.timing.anvatoEndDiagnosed.clear(),this.playbackEndedAtMs=null,this.rewind.beginChannel();const a=t?{...e,breaks:e.breaks.filter(e=>e.id!==t.id)}:e;this.updateManifest(a)}isSchedulableEvent(t){return!s(t.start)||t.start.event===e.START||t.start.event===e.END||(this.unsupportedEventBreakIds.has(t.id)||(this.unsupportedEventBreakIds.add(t.id),this.log(`â ī¸ Break ignored: "${t.id}" (unsupported event trigger "${t.start.event}")`),this.diagnose?.("DA-BREAK-EVENT-UNSUPPORTED","Break triggered by an event this SDK version does not schedule; the break was ignored",{breakId:t.id,event:t.start.event})),!1)}markPlaybackStart(){null!==this.playbackEndedAtMs&&(this.playbackEndedAtMs=null,this.log("Content replayed; pending post-rolls cancelled")),this.rewind.playbackStarted||(this.rewind.markPlaybackStart(this.peekPosition()),this.log("Content playback started at",new Date(this.clock.now()).toISOString()))}setOnAdStartDelayElapsed(e){this.onAdStartDelayElapsed=e}holdBreakStarts(){this.breakStartsHeld=!0}releaseBreakStarts(){const e=this.breakStartsHeld;this.breakStartsHeld=!1,this.preloadLedger.checkAtBreakStart(e,()=>this.checkBreaks())}areBreakStartsHeld(){return this.breakStartsHeld||this.rewind.isAdStartDelayActive()}notifyAdStartDelayElapsed(){this.adStartDelayElapsedNotified||(this.adStartDelayElapsedNotified=!0,this.onAdStartDelayElapsed?.())}markPlaybackEnd(){null===this.playbackEndedAtMs&&(this.playbackEndedAtMs=this.clock.now(),this.log("Content playback ended at",new Date(this.playbackEndedAtMs).toISOString()))}start(e,t,a,i,r){this.onBreakStart=e,this.onBreakApproaching=t??null,this.onBreakWarning=a??null,this.onBreakAborted=i??null,this.onBreakSkipped=r??null,this.checkInterval&&clearInterval(this.checkInterval),this.checkInterval=setInterval(()=>{this.tick()},250),this.log("Scheduler started")}tick(){!1!==this.beforeBreakCheck?.(this.getCurrentTime())&&(this.checkBreaks(),null!==this.nextStartDeadlineMs&&this.armStartDeadline(this.nextStartDeadlineMs))}armStartDeadline(e){null!==this.checkInterval&&null===this.startDeadline&&(this.startDeadline=setTimeout(()=>{this.startDeadline=null,this.tick()},Math.max(1,Math.ceil(e))))}clearStartDeadline(){this.startDeadline&&clearTimeout(this.startDeadline),this.startDeadline=null}suspend(){this.checkInterval&&clearInterval(this.checkInterval),this.checkInterval=null,this.clearStartDeadline()}resume(){!this.checkInterval&&this.onBreakStart&&(this.checkInterval=setInterval(()=>this.tick(),250))}skipPendingBreaksDueBy(e){return p(this.getCurrentTime(),[...this.breaks.values()],e=>e.state===D.PENDING?this.getBreakStartTime(e.break):null,t=>this.skipBreak(t,e))}stop(){this.checkInterval&&(clearInterval(this.checkInterval),this.checkInterval=null),this.clearStartDeadline(),this.onBreakStart=null,this.onBreakSkipped=null,this.log("Scheduler stopped")}completeBreak(e){this.completedBreakIds.add(e),this.breakWarningFired.delete(e),this.abortNotifiedBreakIds.delete(e);const t=this.breaks.get(e);t&&(this.log("Break completed and removed:",e),null===a(t.break)&&null===this.cueTracker&&this.retiredBreaks.set(e,t.break),this.breaks.delete(e),this.preloadLedger.delete(e))}setContentReloading(e){this.contentReloading=e,this.rewind.setContentReloading(e||this.startupSeeking)}setStartupSeeking(e){this.startupSeeking=e,this.rewind.setContentReloading(e||this.contentReloading)}notifyResumeSeek(e,t){this.rewind.notifyResumeSeek(e,t)}notifyViewerSeek(){this.rewind.notifyViewerSeek()}skipBreak(e,t){e.state=D.SKIPPED,this.completedBreakIds.add(e.break.id),this.onBreakSkipped?.(e.break,t)}cleanupBreaks(e){const t=[];for(const[a,i]of this.breaks)i.state!==D.COMPLETED&&i.state!==D.SKIPPED?e.has(a)||i.state===D.ACTIVE||(this.skipBreak(i,"removed"),t.push(a)):t.push(a);for(const e of t)this.breaks.delete(e),this.preloadLedger.delete(e),this.breakWarningFired.delete(e)}getActiveBreak(){for(const e of this.breaks.values())if(e.state===D.ACTIVE)return e.break;return null}getSecondsUntilBreak(e){const t=this.breaks.get(e);if(!t||t.state!==D.PENDING)return null;const a=this.getCurrentTime();if(null===a)return null;const i=this.getBreakStartTime(t.break);return null===i?null:S(i,a)}getNextPendingBreak(){return B([...this.breaks.keys()],e=>this.getSecondsUntilBreak(e))}notifyCuePresented(e){if(null===this.cueTracker||this.timing.timebase!==t.PTS)return null;if(!Number.isFinite(e))return null;let a=null;for(const t of this.breaks.values())if(t.state===D.PENDING&&"number"==typeof t.break.start&&!(Math.abs(t.break.start-e)>l)){a=t.break.id;break}if(null===a)return null;const i=this.player.currentTime;this.checkBreaks();const r=this.breaks.get(a);return r&&r.state===D.ACTIVE?this.anvatoEnteredDiagnosed.has(a)?null:(this.anvatoEnteredDiagnosed.add(a),this.log(`đ¯ Break "${a}" armed by cue presentation (pts ${e})`),this.diagnose?.("DA-ANVATO-CUE-ENTERED",`Break "${a}" started from the presented cue (pts ${e}) rather than the scheduler tick.`,{breakId:a,pts:e,mediaTimeSec:i}),a):null}checkBreaks(){if(this.nextStartDeadlineMs=null,this.startupSeeking)return;const e=this.getCurrentTime();this.rewind.trackPlayedTime(e),this.rewind.adStartDelayMs>0&&!this.rewind.isAdStartDelayActive()&&this.notifyAdStartDelayElapsed();const a=this.areBreakStartsHeld();null!==e&&(this.minSeenTime=null===this.minSeenTime?e:Math.min(this.minSeenTime,e),this.rewind.rearmOnRewind(e));for(const s of this.breaks.values()){if(s.state===D.ACTIVE&&this.timing.timebase===t.WALLCLOCK&&null!==e&&!this.abortNotifiedBreakIds.has(s.break.id)){const t=this.getBreakStartTime(s.break),a=1e3*(s.break.duration??0);null!==t&&a>0&&e>t+a+this.WINDOW_ESCAPE_GRACE_MS&&(this.abortNotifiedBreakIds.add(s.break.id),this.log(`â ī¸ Break window escaped: "${s.break.id}" (ended ${Math.round((e-t-a)/1e3)}s ago on the stream clock)`),this.diagnose?.("DA-BREAK-WINDOW-ESCAPED","Active break outlived its wallclock window; returning to content",{breakId:s.break.id,overrunSec:Math.round((e-t-a)/100)/10}),this.onBreakAborted?.(s.break));continue}if(s.state===D.ACTIVE&&this.timing.timebase===t.PTS&&u(this.timing,this.timingDeps,s.break),s.state!==D.PENDING)continue;const n=r(s.break);if(null!==n){this.checkPreRollBreak(s,n,a);continue}const d=i(s.break);if(null!==d){this.checkPostRollBreak(s,d,a);continue}if(null===e)continue;if(a)continue;null!==this.cueTracker&&this.timing.timebase===t.PTS&&this.maybePreload(s.break,"manifest arrival â Anvato flow");const h=this.getBreakStartTime(s.break);if(null===h)continue;const l=e-h,o=Math.round(-l/1e3);if(this.timing.timebase===t.WALLCLOCK&&l<0&&-l<=250&&(this.nextStartDeadlineMs=Math.min(this.nextStartDeadlineMs??1/0,-l)),this.checkWarnings(s.break,o),o>0&&o<=15&&o%2==0&&this.log(`âąī¸ Break "${s.break.id}" starts in ${o}s`),o>0&&o<=this.PRELOAD_AHEAD_SECONDS&&this.maybePreload(s.break,`${o}s ahead`),l>=0&&l<=this.TIMING_TOLERANCE_MS){const e=Math.max(0,l),t=1e3*(s.break.duration??0)-e;if(t<=0){this.log(`â ī¸ Break missed: "${s.break.id}" (ended ${Math.round(-t/1e3)}s ago, inside the start tolerance)`),this.skipBreak(s,t>0&&this.tuneInEnabled?"insufficient-remaining":"late"),null===this.cueTracker&&this.breaks.get(s.break.id)===s&&this.retiredBreaks.set(s.break.id,s.break);break}if(null!==this.minSeenTime&&h<this.minSeenTime-this.JOIN_DISCRIMINATOR_SLACK_MS&&(!this.tuneInEnabled||t<this.minBreakDurationMs)){this.log(`â ī¸ Break missed: "${s.break.id}" (joined in progress inside the start tolerance, ${Math.round(t/1e3)}s remaining < tune-in rules)`),this.skipBreak(s,t>0&&this.tuneInEnabled?"insufficient-remaining":"late"),null===this.cueTracker&&this.breaks.get(s.break.id)===s&&this.retiredBreaks.set(s.break.id,s.break);break}this.preloadLedger.shouldRetryRejectedAtStart(s.break.id)&&this.maybePreload(s.break,"retrying a rejected preload at break start"),this.log(`đŦ BREAK TRIGGERED: "${s.break.id}"`),s.state=D.ACTIVE,this.onBreakStart&&this.onBreakStart(s.break,{tuneIn:!1,elapsedSec:e/1e3,remainingSec:t/1e3});break}if(l>this.TIMING_TOLERANCE_MS){const t=h+1e3*(s.break.duration??0)-e;if(this.tuneInEnabled&&t>=this.minBreakDurationMs&&t>0){this.log(`đŦ TUNE-IN BREAK TRIGGERED: "${s.break.id}" (${Math.round(t/1e3)}s remaining)`),s.state=D.ACTIVE,this.onBreakStart&&this.onBreakStart(s.break,{tuneIn:!0,elapsedSec:l/1e3,remainingSec:t/1e3});break}this.log(`â ī¸ Break missed: "${s.break.id}" (was ${Math.round(l/1e3)}s ago)`),this.skipBreak(s,t>0&&this.tuneInEnabled?"insufficient-remaining":"late"),null===this.cueTracker&&this.breaks.get(s.break.id)===s&&this.retiredBreaks.set(s.break.id,s.break)}}}checkPreRollBreak(e,t,a){if(!this.rewind.playbackStarted)return;const i=1e3*t;if(this.rewind.adStartDelayMs>0&&i<this.rewind.adStartDelayMs)return this.log(`â ī¸ Pre-roll skipped: "${e.break.id}" (delay ${t}s < adStartDelay ${this.rewind.adStartDelayMs/1e3}s)`),void this.skipBreak(e,"start-delay");const r=this.rewind.channelPlayedMs,s=Math.round((i-r)/1e3);s>0&&s<=15&&s%2==0&&this.log(`âąī¸ Pre-roll "${e.break.id}" starts in ${s}s`),s>0&&s<=this.PRELOAD_AHEAD_SECONDS&&this.maybePreload(e.break,`pre-roll, ${s}s ahead`),r>=i&&!a&&(this.log(`đŦ PRE-ROLL TRIGGERED: "${e.break.id}"${t>0?` (after ${t}s delay)`:""}`),e.state=D.ACTIVE,this.onBreakStart&&this.onBreakStart(e.break,{tuneIn:!1,elapsedSec:0,remainingSec:e.break.duration??0}))}checkPostRollBreak(e,t,a){if(null===this.playbackEndedAtMs)return;const i=1e3*t,r=this.clock.now()-this.playbackEndedAtMs,s=Math.round((i-r)/1e3);s>0&&s<=this.PRELOAD_AHEAD_SECONDS&&this.maybePreload(e.break,`post-roll, ${s}s ahead`),r>=i&&!a&&(this.log(`đŦ POST-ROLL TRIGGERED: "${e.break.id}"${t>0?` (after ${t}s delay)`:""}`),e.state=D.ACTIVE,this.onBreakStart&&this.onBreakStart(e.break,{tuneIn:!1,elapsedSec:0,remainingSec:e.break.duration??0}))}isAdStartDelayActive(){return this.areBreakStartsHeld()}getCurrentTime(){return g(this.timing,this.timingDeps)}maybePreload(e,t){if(this.preloadLedger.wasPreloaded(e.id)||!this.onBreakApproaching)return;this.log(`âŗ Preloading break "${e.id}" (${t})`);const a=!1!==this.onBreakApproaching(e);this.preloadLedger.recordAttempt(e.id,a)}getBreakStartTime(e){return s(e.start)?null:b(this.timing,this.timingDeps,e)}peekPosition(){if(this.timing.timebase!==t.WALLCLOCK)return 1e3*this.player.currentTime;const e=this.player.programDateTime?.getTime();return void 0===e||isNaN(e)?null:e}destroy(){this.stop(),this.breaks.clear(),this.preloadLedger.clear(),this.completedBreakIds.clear(),this.retiredBreaks.clear(),this.minSeenTime=null,this.breakStartsHeld=!1,this.onAdStartDelayElapsed=null,this.adStartDelayElapsedNotified=!1,this.unsupportedEventBreakIds.clear(),this.playbackEndedAtMs=null,this.rewind.reset(),this.breakWarningFired.clear(),this.abortNotifiedBreakIds.clear(),this.timing.pdtMissingWarned=!1,this.timing.pdtSeen=!1,this.timing.firstPdtAttemptMs=null,this.timing.playbackStartedAtMs=null,this.timing.anvatoPendingDiagnosed.clear(),this.timing.anvatoMatchedDiagnosed.clear(),this.timing.anvatoEndDiagnosed.clear(),this.cueTracker?.clear()}checkWarnings(e,t){if(t<=0||0===this.breakWarnings.seconds.length)return;let a=this.breakWarningFired.get(e.id);a||(a=new Set,this.breakWarningFired.set(e.id,a));for(const i of this.breakWarnings.seconds)a.has(i)||t<=i&&(a.add(i),this.log(`Break warning for "${e.id}" (${t}s <= ${i}s)`),this.onBreakWarning?.(e,i,t))}log(...e){this.debug&&o("[OptiViewAds:BreakScheduler]",...e)}}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { Clock } from './BreakScheduler';
|
|
2
2
|
/**
|
|
3
3
|
* `into-break`: break triggered â first ad renders. `ad-to-ad`: ad ended â next ad renders.
|
|
4
|
-
* `break-to-break`:
|
|
4
|
+
* `break-to-break`: outgoing break's last ad ended â the chained break's first ad renders.
|
|
5
|
+
* `out-of-break`: break ended â content playing.
|
|
5
6
|
*/
|
|
6
7
|
export type BreakTransitionKind = 'into-break' | 'ad-to-ad' | 'break-to-break' | 'out-of-break';
|
|
8
|
+
/**
|
|
9
|
+
* How the incoming break's first ad reached the ad player at a `break-to-break` switch:
|
|
10
|
+
* buffered ahead (`queued`), buffered after the successor changed during the last ad
|
|
11
|
+
* (`late-queued`), or loaded at the switch (`cold`).
|
|
12
|
+
*/
|
|
13
|
+
export type BreakSwitchPath = 'queued' | 'late-queued' | 'cold';
|
|
7
14
|
/** A named checkpoint inside a transition, so a slow transition can be attributed to a phase. */
|
|
8
15
|
export interface TransitionPhase {
|
|
9
16
|
/** Checkpoint name, e.g. `uri-resolved`, `load-resolved`, `play-called`. */
|
|
@@ -22,6 +29,10 @@ export interface BreakTransitionMeasurement {
|
|
|
22
29
|
breakId: string;
|
|
23
30
|
/** Ordered checkpoints from {@link BreakTransitionTimer.phase}; omitted when none, keeping the default report shape. */
|
|
24
31
|
phases?: TransitionPhase[];
|
|
32
|
+
/** `break-to-break`, and `ad-to-ad` in the TS core. */
|
|
33
|
+
path?: BreakSwitchPath;
|
|
34
|
+
/** With `path`, when the incoming ad was buffered: milliseconds it was buffered before the switch. */
|
|
35
|
+
leadMs?: number;
|
|
25
36
|
}
|
|
26
37
|
/**
|
|
27
38
|
* Measures the gap where the viewer sees neither outgoing nor incoming media; each mark/settle
|
|
@@ -33,9 +44,23 @@ export declare class BreakTransitionTimer {
|
|
|
33
44
|
private readonly onMeasurement;
|
|
34
45
|
private readonly clock;
|
|
35
46
|
private pending;
|
|
47
|
+
/** When the outgoing break's last ad ended: where a following `break-to-break` switch starts. */
|
|
48
|
+
private outgoingEndAtMs;
|
|
49
|
+
/** The successor the renderer buffered behind the playing ad, and when. */
|
|
50
|
+
private buffered;
|
|
36
51
|
constructor(onMeasurement: (measurement: BreakTransitionMeasurement) => void, clock?: Clock);
|
|
37
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Record the start of a transition. Supersedes any still-pending one. A `break-to-break`
|
|
54
|
+
* switch starts at the outgoing ad's end when one was noted, and takes the buffered path.
|
|
55
|
+
*/
|
|
38
56
|
mark(kind: BreakTransitionKind, breakId: string): void;
|
|
57
|
+
/** The outgoing break's last ad ended (`atMs`, default now): the start of a chained switch. */
|
|
58
|
+
noteOutgoingEnd(atMs?: number): void;
|
|
59
|
+
/** The renderer buffered `breakId`'s first ad behind the playing ad; `late` after a change during the last ad. */
|
|
60
|
+
noteSuccessorBuffered(breakId: string, late: boolean): void;
|
|
61
|
+
/** The renderer discarded the buffered `breakId`. */
|
|
62
|
+
noteSuccessorDropped(breakId: string): void;
|
|
63
|
+
private clearSwitchAnchors;
|
|
39
64
|
/** No-op when nothing is pending; a superseded transition drops its phases with it. */
|
|
40
65
|
phase(name: string): void;
|
|
41
66
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{systemClock as
|
|
1
|
+
import{systemClock as t}from"./BreakScheduler";export class BreakTransitionTimer{constructor(e,s=t){this.onMeasurement=e,this.clock=s,this.pending=null,this.outgoingEndAtMs=null,this.buffered=null}mark(t,e){const s=this.clock.now();if("ad-to-ad"===t){const n=this.buffered?.breakId===e?this.buffered:null;return n&&(this.buffered=null),void(this.pending={kind:t,breakId:e,startedAtMs:s,phases:[],path:n?"queued":"cold",...n?{leadMs:Math.max(0,s-n.atMs)}:{}})}if("break-to-break"!==t)return"into-break"===t&&this.clearSwitchAnchors(),void(this.pending={kind:t,breakId:e,startedAtMs:s,phases:[]});const n=this.outgoingEndAtMs??s,a=this.buffered?.breakId===e?this.buffered:null;this.pending={kind:t,breakId:e,startedAtMs:n,phases:[],path:a?a.late?"late-queued":"queued":"cold",...a?{leadMs:Math.max(0,n-a.atMs)}:{}},this.clearSwitchAnchors()}noteOutgoingEnd(t=this.clock.now()){this.outgoingEndAtMs=t}noteSuccessorBuffered(t,e){this.buffered={breakId:t,atMs:this.clock.now(),late:e}}noteSuccessorDropped(t){this.buffered?.breakId===t&&(this.buffered=null)}clearSwitchAnchors(){this.outgoingEndAtMs=null,this.buffered=null}phase(t){const e=this.pending;if(!e)return;const s=Math.max(0,this.clock.now()-e.startedAtMs),n=e.phases[e.phases.length-1]?.atMs??0;e.phases.push({name:t,atMs:s,sinceLastMs:Math.max(0,s-n)})}settle(...t){const e=this.pending;e&&t.includes(e.kind)&&(this.pending=null,"out-of-break"===e.kind&&this.clearSwitchAnchors(),this.onMeasurement({kind:e.kind,breakId:e.breakId,durationMs:Math.max(0,this.clock.now()-e.startedAtMs),...e.phases.length>0?{phases:e.phases}:{},...e.path?{path:e.path}:{},...void 0!==e.leadMs?{leadMs:e.leadMs}:{}}))}reset(){this.pending=null,this.clearSwitchAnchors()}}
|
|
@@ -60,6 +60,7 @@ export declare class CmcdAdReporter {
|
|
|
60
60
|
* manifest version, and the optional identity ids (org/channel/event).
|
|
61
61
|
*/
|
|
62
62
|
onSessionStart(manifest: BreakManifest): void;
|
|
63
|
+
updateEventId(eventId: string | null): void;
|
|
63
64
|
/** Clear all telemetry state and the heartbeat payload. */
|
|
64
65
|
onSessionEnd(): void;
|
|
65
66
|
private finalizeActiveBreak;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{LensEventType as e,LensToken as t}from"../vendor/lens-sdk/index.js";import{AssetType as i,BreakFormat as s,getBreakEvent as a}from"../types/BreakManifest";import{adErrorCode as d}from"../types/AdError";import{VendorAdError as r}from"../types/VendorAdError";import{SDK_VERSION as n}from"../generated/version";const h={[s.SINGLE]:"s",[s.DOUBLE]:"d",[s.LSHAPE_AD]:"la",[s.LSHAPE_CONTENT]:"lc",[s.OVERLAY]:"o"},o={[i.STATIC]:"s",[i.VAST]:"va",[i.VENDOR]:"ve"};function l(e){const t=a(e);if(null!==t)return t;const{start:i}=e;return"number"==typeof i?String(Math.round(1e3*i)):"string"==typeof i?i:i.event}function c(e){return o[e]??e}export class CmcdAdReporter{constructor(e,t){this.getHandle=e,this.log=t,this.sessionKeys={},this.activeBreakId=null,this.breakBeginTimestamp=null,this.breakAvailableMs=null,this.breakRequestedMs=null,this.firstAdReported=!1,this.breakFilledMs=0,this.breakWatchedMs=0,this.activeAdKeys=null,this.activeAdCompanion=null,this.activeAdAssetId=null,this.activeAdIsVendor=!1,this.activeAdBaseSec=0,this.breakLastTimeSec=0,this.pendingAdStart=null,this.activeAdWatchedSec=0,this.activeAdDeclaredSec=null,this.activeAdIsVast=!1,this.activeAdResolvedSec=null,this.skipRequestedBreakId=null,this.abortedBreakId=null,this.cutBreakId=null,this.erroredAssetId=null,this.finalizedBreakId=null}onSessionStart(e){this.sessionKeys={adv:n,admv:e.version,...e.organizationId?{adoid:e.organizationId}:{},...e.channelId?{adcid:e.channelId}:{},...e.eventId?{adeid:e.eventId}:{}},this.updateHeartbeat()}onSessionEnd(){this.finalizeActiveBreak(),this.sessionKeys={},this.resetBreakState(),this.guard("setHeartbeatPayload",e=>e.setHeartbeatPayload())}finalizeActiveBreak(){if(null===this.activeBreakId)return;const e=this.activeBreakId;this.emitPendingAdStart(null),null!==this.activeAdKeys&&this.emitAdEnd(this.activeAdKeys,this.activeAdCompanion,"a"),this.emitBreakEnd(e),this.finalizedBreakId=e}onSkipRequested(e){this.skipRequestedBreakId=e}onBreakAborted(e){this.abortedBreakId=e}onBreakCut(e){this.cutBreakId=e}onEvent(e){switch(e.type){case"adbreakbegin":this.onBreakBegin(e);break;case"adbegin":this.onAdBegin(e);break;case"adtimeupdate":{const t=e;this.breakLastTimeSec=Math.max(this.breakLastTimeSec,t.currentTime);null!==this.pendingAdStart&&(t.asset.id===this.pendingAdStart.assetId||this.pendingAdStart.isVendor&&t.break.id===this.activeBreakId)&&t.currentTime>(this.pendingAdStart?.isVendor?this.pendingAdStart.baseSec:0)&&null!==this.breakBeginTimestamp&&this.emitPendingAdStart(t.timestamp-this.breakBeginTimestamp);if(t.asset.id===this.activeAdAssetId||this.activeAdIsVendor&&t.break.id===this.activeBreakId){const e=t.currentTime-this.activeAdBaseSec;this.activeAdWatchedSec=Math.max(this.activeAdWatchedSec,e),this.activeAdIsVast&&Number.isFinite(t.duration)&&t.duration>0&&(this.activeAdResolvedSec=t.duration)}break}case"adend":this.onAdEnd(e);break;case"adbreakend":this.onBreakEnd(e);break;case"adfirstquartile":case"admidpoint":case"adthirdquartile":this.onMilestone(e);break;case"adclick":this.onClick(e);break;case"aderror":this.onAdError(e)}}onBreakBegin(i){this.resetBreakState(),this.finalizedBreakId=null,this.activeBreakId=i.break.id,this.breakBeginTimestamp=i.timestamp;const s=Math.round(1e3*i.remainingSec),a=this.requestedPlayableMs(i.break,i.format,s)??s;this.breakAvailableMs=s,this.breakRequestedMs=a,this.guard("abs",d=>{return d.emit(e.AD_BREAK_START,{custom:{...this.sessionKeys,brid:i.break.id,brst:l(i.break),brmd:Math.round(1e3*i.break.duration),brad:s,brrd:a,...i.format?{brf:new t((r=i.format,h[r]??r))}:{}}});var r}),this.updateHeartbeat()}onAdBegin(e){this.emitPendingAdStart(null);const s=e.asset,a=s.interaction?.clickThrough,d=!this.firstAdReported&&null!==this.breakBeginTimestamp;this.firstAdReported=!0;const r={brid:e.break.id,adasid:s.id,adid:e.adId??s.id,...void 0!==e.podPosition?{adpos:String(e.podPosition)}:{},...void 0!==s.duration?{adasd:Math.round(1e3*s.duration)}:{},adast:new t(c(s.type)),...a?{adcl:!0}:{},...e.creativeId?{adgcrid:e.creativeId}:{},...e.adId&&"string"==typeof s.uri&&"vendor"===s.type?{adgpid:s.uri}:{}};this.activeAdKeys=r,this.activeAdCompanion=s.companion?{id:s.companion.id,parentAssetId:s.id}:null,this.activeAdAssetId=s.id,this.activeAdIsVendor=s.type===i.VENDOR,this.activeAdBaseSec=this.activeAdIsVendor?this.breakLastTimeSec:0,this.activeAdWatchedSec=0,this.activeAdDeclaredSec=s.duration??null,this.activeAdIsVast=s.type===i.VAST,this.activeAdResolvedSec=null;const n={...this.sessionKeys,...r},h=s.companion?{...this.sessionKeys,brid:e.break.id,adasid:s.companion.id,adid:s.companion.id,adast:new t(c(s.companion.type)),adcomp:!0,adpasid:s.id}:null;d?this.pendingAdStart={assetId:s.id,custom:n,companionCustom:h,isVendor:this.activeAdIsVendor,baseSec:this.activeAdBaseSec}:this.emitAdStart(n,h),this.updateHeartbeat()}emitAdStart(t,i){this.guard("as",i=>i.emit(e.AD_START,{custom:t})),i&&this.guard("as",t=>t.emit(e.AD_START,{custom:i}))}emitPendingAdStart(e){const t=this.pendingAdStart;if(!t)return;this.pendingAdStart=null;const i=null!==e?{...t.custom,brttf:e}:t.custom;this.emitAdStart(i,t.companionCustom)}onAdEnd(e){if(e.break.id===this.finalizedBreakId)return;this.emitPendingAdStart(null);const t=e.asset,i=this.adEndReason(e),s=this.activeAdKeys??{brid:e.break.id,adasid:t.id,adid:e.adId??t.id},a=t.companion?{id:t.companion.id,parentAssetId:t.id}:null;this.emitAdEnd(s,a,i)}emitAdEnd(i,s,a){const d=this.activeAdFilledSec(),r=null!==d?Math.round(1e3*d):null,n=Math.round(1e3*this.activeAdWatchedSec),h=null!==r?Math.min(n,r):n;"s"===a&&this.guard("sk",t=>t.emit(e.AD_SKIP,{custom:{...this.sessionKeys,...i}})),this.guard("ae",s=>s.emit(e.AD_END,{custom:{...this.sessionKeys,...i,ader:new t(a),adwd:h}})),s&&this.guard("ae",d=>d.emit(e.AD_END,{custom:{...this.sessionKeys,brid:i.brid,adasid:s.id,adid:s.id,adcomp:!0,adpasid:s.parentAssetId,ader:new t(a)}})),this.breakWatchedMs+=h,this.breakFilledMs+=null!==r?r:h,this.activeAdKeys=null,this.activeAdCompanion=null,this.activeAdAssetId=null,this.activeAdIsVendor=!1,this.activeAdBaseSec=0,this.activeAdWatchedSec=0,this.activeAdDeclaredSec=null,this.activeAdIsVast=!1,this.activeAdResolvedSec=null,this.erroredAssetId===i.adasid&&(this.erroredAssetId=null),this.updateHeartbeat()}onBreakEnd(e){if(e.break.id!==this.finalizedBreakId){if(this.emitPendingAdStart(null),null!==this.breakAvailableMs){const t=this.requestedPlayableMs(e.break,e.format,this.breakAvailableMs);null!==t&&(this.breakRequestedMs=t)}this.emitBreakEnd(e.break.id)}}emitBreakEnd(t){const i=this.breakAvailableMs,s=null!==this.breakRequestedMs&&null!==i?Math.min(this.breakRequestedMs,i):this.breakRequestedMs,a=null!==s?Math.min(this.breakFilledMs,s):this.breakFilledMs,d=Math.min(this.breakWatchedMs,a);this.guard("abe",r=>r.emit(e.AD_BREAK_END,{custom:{...this.sessionKeys,brid:t,...null!==i?{brad:i}:{},...null!==s?{brrd:s}:{},brfd:a,brwd:d}})),this.resetBreakState(),this.updateHeartbeat()}onMilestone(e){this.emitPendingAdStart(null),this.emitCustomWithAdContext(e.type,e.break,e.asset)}onClick(e){this.emitPendingAdStart(null),this.emitCustomWithAdContext("adclick",e.break,e.asset)}onAdError(e){"ad"===e.source&&(this.emitPendingAdStart(null),e.asset&&(this.erroredAssetId=e.asset.id),this.guard("aderror",t=>t.emitCustom("aderror",{custom:{...this.sessionKeys,adec:e.asset?d(e.error):"DA-BREAK-NO-PLAYABLE-ASSET",...e.error instanceof r?{adevc:e.error.vendorCode}:{},...e.break&&e.asset?this.adContextKeys(e.break,e.asset):{...e.break?{brid:e.break.id}:{},...e.asset?{adasid:e.asset.id,adid:e.asset.id}:{}}}})))}emitCustomWithAdContext(e,t,i){this.guard(e,s=>s.emitCustom(e,{custom:{...this.sessionKeys,...this.adContextKeys(t,i)}}))}activeAdFilledSec(){const e=this.activeAdDeclaredSec,t=this.activeAdResolvedSec;return null===t?e:null===e?t:Math.min(t,e)}adContextKeys(e,t){return(t.id===this.activeAdAssetId||this.activeAdIsVendor&&e.id===this.activeBreakId)&&null!==this.activeAdKeys?{...this.activeAdKeys}:{brid:e.id,adasid:t.id,...void 0!==this.activeAdKeys?.adid?{adid:this.activeAdKeys.adid}:{}}}adEndReason(e){return this.skipRequestedBreakId===e.break.id?(this.skipRequestedBreakId=null,"s"):this.erroredAssetId===e.asset.id?"e":this.abortedBreakId===e.break.id?"a":this.cutBreakId===e.break.id?(this.cutBreakId=null,"a"):"c"}requestedPlayableMs(e,t,i){const s=Array.isArray(e.variant)?e.variant:[e.variant],a=s.find(e=>e.format===t)??s[0];if(!a)return null;if(0===a.assets.length)return null;const d=a.assets.map(e=>e.duration);if(d.some(e=>void 0===e||!Number.isFinite(e)||e<=0))return i;const r=d.reduce((e,t)=>e+(t??0),0);return Math.min(Math.round(1e3*r),i)}updateHeartbeat(){this.guard("setHeartbeatPayload",e=>e.setHeartbeatPayload({custom:{...this.sessionKeys,...this.activeBreakId?{brid:this.activeBreakId}:{},...void 0!==this.activeAdKeys?.adid?{adid:this.activeAdKeys.adid}:{}}}))}resetBreakState(){this.emitPendingAdStart(null),this.activeBreakId=null,this.breakBeginTimestamp=null,this.breakAvailableMs=null,this.breakRequestedMs=null,this.firstAdReported=!1,this.breakFilledMs=0,this.breakWatchedMs=0,this.activeAdKeys=null,this.activeAdCompanion=null,this.activeAdAssetId=null,this.activeAdIsVendor=!1,this.activeAdBaseSec=0,this.breakLastTimeSec=0,this.activeAdWatchedSec=0,this.activeAdDeclaredSec=null,this.activeAdIsVast=!1,this.activeAdResolvedSec=null,this.skipRequestedBreakId=null,this.abortedBreakId=null,this.cutBreakId=null,this.erroredAssetId=null}guard(e,t){const i=this.getHandle();if(i)try{t(i)}catch(t){this.log(`Lens CMCD ${e} failed:`,t)}}}
|
|
1
|
+
import{LensEventType as e,LensToken as t}from"../vendor/lens-sdk/index.js";import{AssetType as i,BreakFormat as s,getBreakEvent as a}from"../types/BreakManifest";import{adErrorCode as d}from"../types/AdError";import{VendorAdError as r}from"../types/VendorAdError";import{SDK_VERSION as n}from"../generated/version";const h={[s.SINGLE]:"s",[s.DOUBLE]:"d",[s.LSHAPE_AD]:"la",[s.LSHAPE_CONTENT]:"lc",[s.OVERLAY]:"o"},o={[i.STATIC]:"s",[i.VAST]:"va",[i.VENDOR]:"ve"};function l(e){const t=a(e);if(null!==t)return t;const{start:i}=e;return"number"==typeof i?String(Math.round(1e3*i)):"string"==typeof i?i:i.event}function c(e){return o[e]??e}export class CmcdAdReporter{constructor(e,t){this.getHandle=e,this.log=t,this.sessionKeys={},this.activeBreakId=null,this.breakBeginTimestamp=null,this.breakAvailableMs=null,this.breakRequestedMs=null,this.firstAdReported=!1,this.breakFilledMs=0,this.breakWatchedMs=0,this.activeAdKeys=null,this.activeAdCompanion=null,this.activeAdAssetId=null,this.activeAdIsVendor=!1,this.activeAdBaseSec=0,this.breakLastTimeSec=0,this.pendingAdStart=null,this.activeAdWatchedSec=0,this.activeAdDeclaredSec=null,this.activeAdIsVast=!1,this.activeAdResolvedSec=null,this.skipRequestedBreakId=null,this.abortedBreakId=null,this.cutBreakId=null,this.erroredAssetId=null,this.finalizedBreakId=null}onSessionStart(e){this.sessionKeys={adv:n,admv:e.version,...e.organizationId?{adoid:e.organizationId}:{},...e.channelId?{adcid:e.channelId}:{},...e.eventId?{adeid:e.eventId}:{}},this.updateHeartbeat()}updateEventId(e){null===e?delete this.sessionKeys.adeid:this.sessionKeys.adeid=e,this.updateHeartbeat()}onSessionEnd(){this.finalizeActiveBreak(),this.sessionKeys={},this.resetBreakState(),this.guard("setHeartbeatPayload",e=>e.setHeartbeatPayload())}finalizeActiveBreak(){if(null===this.activeBreakId)return;const e=this.activeBreakId;this.emitPendingAdStart(null),null!==this.activeAdKeys&&this.emitAdEnd(this.activeAdKeys,this.activeAdCompanion,"a"),this.emitBreakEnd(e),this.finalizedBreakId=e}onSkipRequested(e){this.skipRequestedBreakId=e}onBreakAborted(e){this.abortedBreakId=e}onBreakCut(e){this.cutBreakId=e}onEvent(e){switch(e.type){case"adbreakbegin":this.onBreakBegin(e);break;case"adbegin":this.onAdBegin(e);break;case"adtimeupdate":{const t=e;this.breakLastTimeSec=Math.max(this.breakLastTimeSec,t.currentTime);null!==this.pendingAdStart&&(t.asset.id===this.pendingAdStart.assetId||this.pendingAdStart.isVendor&&t.break.id===this.activeBreakId)&&t.currentTime>(this.pendingAdStart?.isVendor?this.pendingAdStart.baseSec:0)&&null!==this.breakBeginTimestamp&&this.emitPendingAdStart(t.timestamp-this.breakBeginTimestamp);if(t.asset.id===this.activeAdAssetId||this.activeAdIsVendor&&t.break.id===this.activeBreakId){const e=t.currentTime-this.activeAdBaseSec;this.activeAdWatchedSec=Math.max(this.activeAdWatchedSec,e),this.activeAdIsVast&&Number.isFinite(t.duration)&&t.duration>0&&(this.activeAdResolvedSec=t.duration)}break}case"adend":this.onAdEnd(e);break;case"adbreakend":this.onBreakEnd(e);break;case"adfirstquartile":case"admidpoint":case"adthirdquartile":this.onMilestone(e);break;case"adclick":this.onClick(e);break;case"aderror":this.onAdError(e)}}onBreakBegin(i){this.resetBreakState(),this.finalizedBreakId=null,this.activeBreakId=i.break.id,this.breakBeginTimestamp=i.timestamp;const s=Math.round(1e3*i.remainingSec),a=this.requestedPlayableMs(i.break,i.format,s)??s;this.breakAvailableMs=s,this.breakRequestedMs=a,this.guard("abs",d=>{return d.emit(e.AD_BREAK_START,{custom:{...this.sessionKeys,brid:i.break.id,brst:l(i.break),brmd:Math.round(1e3*i.break.duration),brad:s,brrd:a,...i.format?{brf:new t((r=i.format,h[r]??r))}:{}}});var r}),this.updateHeartbeat()}onAdBegin(e){this.emitPendingAdStart(null);const s=e.asset,a=s.interaction?.clickThrough,d=!this.firstAdReported&&null!==this.breakBeginTimestamp;this.firstAdReported=!0;const r={brid:e.break.id,adasid:s.id,adid:e.adId??s.id,...void 0!==e.podPosition?{adpos:String(e.podPosition)}:{},...void 0!==s.duration?{adasd:Math.round(1e3*s.duration)}:{},adast:new t(c(s.type)),...a?{adcl:!0}:{},...e.creativeId?{adgcrid:e.creativeId}:{},...e.adId&&"string"==typeof s.uri&&"vendor"===s.type?{adgpid:s.uri}:{}};this.activeAdKeys=r,this.activeAdCompanion=s.companion?{id:s.companion.id,parentAssetId:s.id}:null,this.activeAdAssetId=s.id,this.activeAdIsVendor=s.type===i.VENDOR,this.activeAdBaseSec=this.activeAdIsVendor?this.breakLastTimeSec:0,this.activeAdWatchedSec=0,this.activeAdDeclaredSec=s.duration??null,this.activeAdIsVast=s.type===i.VAST,this.activeAdResolvedSec=null;const n={...this.sessionKeys,...r},h=s.companion?{...this.sessionKeys,brid:e.break.id,adasid:s.companion.id,adid:s.companion.id,adast:new t(c(s.companion.type)),adcomp:!0,adpasid:s.id}:null;d?this.pendingAdStart={assetId:s.id,custom:n,companionCustom:h,isVendor:this.activeAdIsVendor,baseSec:this.activeAdBaseSec}:this.emitAdStart(n,h),this.updateHeartbeat()}emitAdStart(t,i){this.guard("as",i=>i.emit(e.AD_START,{custom:t})),i&&this.guard("as",t=>t.emit(e.AD_START,{custom:i}))}emitPendingAdStart(e){const t=this.pendingAdStart;if(!t)return;this.pendingAdStart=null;const i=null!==e?{...t.custom,brttf:e}:t.custom;this.emitAdStart(i,t.companionCustom)}onAdEnd(e){if(e.break.id===this.finalizedBreakId)return;this.emitPendingAdStart(null);const t=e.asset,i=this.adEndReason(e),s=this.activeAdKeys??{brid:e.break.id,adasid:t.id,adid:e.adId??t.id},a=t.companion?{id:t.companion.id,parentAssetId:t.id}:null;this.emitAdEnd(s,a,i)}emitAdEnd(i,s,a){const d=this.activeAdFilledSec(),r=null!==d?Math.round(1e3*d):null,n=Math.round(1e3*this.activeAdWatchedSec),h=null!==r?Math.min(n,r):n;"s"===a&&this.guard("sk",t=>t.emit(e.AD_SKIP,{custom:{...this.sessionKeys,...i}})),this.guard("ae",s=>s.emit(e.AD_END,{custom:{...this.sessionKeys,...i,ader:new t(a),adwd:h}})),s&&this.guard("ae",d=>d.emit(e.AD_END,{custom:{...this.sessionKeys,brid:i.brid,adasid:s.id,adid:s.id,adcomp:!0,adpasid:s.parentAssetId,ader:new t(a)}})),this.breakWatchedMs+=h,this.breakFilledMs+=null!==r?r:h,this.activeAdKeys=null,this.activeAdCompanion=null,this.activeAdAssetId=null,this.activeAdIsVendor=!1,this.activeAdBaseSec=0,this.activeAdWatchedSec=0,this.activeAdDeclaredSec=null,this.activeAdIsVast=!1,this.activeAdResolvedSec=null,this.erroredAssetId===i.adasid&&(this.erroredAssetId=null),this.updateHeartbeat()}onBreakEnd(e){if(e.break.id!==this.finalizedBreakId){if(this.emitPendingAdStart(null),null!==this.breakAvailableMs){const t=this.requestedPlayableMs(e.break,e.format,this.breakAvailableMs);null!==t&&(this.breakRequestedMs=t)}this.emitBreakEnd(e.break.id)}}emitBreakEnd(t){const i=this.breakAvailableMs,s=null!==this.breakRequestedMs&&null!==i?Math.min(this.breakRequestedMs,i):this.breakRequestedMs,a=null!==s?Math.min(this.breakFilledMs,s):this.breakFilledMs,d=Math.min(this.breakWatchedMs,a);this.guard("abe",r=>r.emit(e.AD_BREAK_END,{custom:{...this.sessionKeys,brid:t,...null!==i?{brad:i}:{},...null!==s?{brrd:s}:{},brfd:a,brwd:d}})),this.resetBreakState(),this.updateHeartbeat()}onMilestone(e){this.emitPendingAdStart(null),this.emitCustomWithAdContext(e.type,e.break,e.asset)}onClick(e){this.emitPendingAdStart(null),this.emitCustomWithAdContext("adclick",e.break,e.asset)}onAdError(e){"ad"===e.source&&(this.emitPendingAdStart(null),e.asset&&(this.erroredAssetId=e.asset.id),this.guard("aderror",t=>t.emitCustom("aderror",{custom:{...this.sessionKeys,adec:e.asset?d(e.error):"DA-BREAK-NO-PLAYABLE-ASSET",...e.error instanceof r?{adevc:e.error.vendorCode}:{},...e.break&&e.asset?this.adContextKeys(e.break,e.asset):{...e.break?{brid:e.break.id}:{},...e.asset?{adasid:e.asset.id,adid:e.asset.id}:{}}}})))}emitCustomWithAdContext(e,t,i){this.guard(e,s=>s.emitCustom(e,{custom:{...this.sessionKeys,...this.adContextKeys(t,i)}}))}activeAdFilledSec(){const e=this.activeAdDeclaredSec,t=this.activeAdResolvedSec;return null===t?e:null===e?t:Math.min(t,e)}adContextKeys(e,t){return(t.id===this.activeAdAssetId||this.activeAdIsVendor&&e.id===this.activeBreakId)&&null!==this.activeAdKeys?{...this.activeAdKeys}:{brid:e.id,adasid:t.id,...void 0!==this.activeAdKeys?.adid?{adid:this.activeAdKeys.adid}:{}}}adEndReason(e){return this.skipRequestedBreakId===e.break.id?(this.skipRequestedBreakId=null,"s"):this.erroredAssetId===e.asset.id?"e":this.abortedBreakId===e.break.id?"a":this.cutBreakId===e.break.id?(this.cutBreakId=null,"a"):"c"}requestedPlayableMs(e,t,i){const s=Array.isArray(e.variant)?e.variant:[e.variant],a=s.find(e=>e.format===t)??s[0];if(!a)return null;if(0===a.assets.length)return null;const d=a.assets.map(e=>e.duration);if(d.some(e=>void 0===e||!Number.isFinite(e)||e<=0))return i;const r=d.reduce((e,t)=>e+(t??0),0);return Math.min(Math.round(1e3*r),i)}updateHeartbeat(){this.guard("setHeartbeatPayload",e=>e.setHeartbeatPayload({custom:{...this.sessionKeys,...this.activeBreakId?{brid:this.activeBreakId}:{},...void 0!==this.activeAdKeys?.adid?{adid:this.activeAdKeys.adid}:{}}}))}resetBreakState(){this.emitPendingAdStart(null),this.activeBreakId=null,this.breakBeginTimestamp=null,this.breakAvailableMs=null,this.breakRequestedMs=null,this.firstAdReported=!1,this.breakFilledMs=0,this.breakWatchedMs=0,this.activeAdKeys=null,this.activeAdCompanion=null,this.activeAdAssetId=null,this.activeAdIsVendor=!1,this.activeAdBaseSec=0,this.breakLastTimeSec=0,this.activeAdWatchedSec=0,this.activeAdDeclaredSec=null,this.activeAdIsVast=!1,this.activeAdResolvedSec=null,this.skipRequestedBreakId=null,this.abortedBreakId=null,this.cutBreakId=null,this.erroredAssetId=null}guard(e,t){const i=this.getHandle();if(i)try{t(i)}catch(t){this.log(`Lens CMCD ${e} failed:`,t)}}}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { KnownDiagnosticCode } from '../diagnostics/errorCodes';
|
|
2
|
+
import type { PlayerAdapter } from '../interfaces/PlayerAdapter';
|
|
3
|
+
import { type ExternalPlaybackGuardInput } from './externalPlaybackGuard';
|
|
4
|
+
import { type SsbSwitchPhase } from './ssbSwitchSignals';
|
|
5
|
+
export type ExternalPlaybackAllowedTrigger = 'connecting' | 'active' | 'break-ended';
|
|
6
|
+
export interface ExternalPlaybackGateDeps {
|
|
7
|
+
player: PlayerAdapter;
|
|
8
|
+
blockBeforeBreakSec: number;
|
|
9
|
+
/** The break timeline right now; all nulls when there is no scheduler (SSAI mode). */
|
|
10
|
+
guardInput(): Omit<ExternalPlaybackGuardInput, 'blockBeforeBreakSec'>;
|
|
11
|
+
diagnose(code: KnownDiagnosticCode, message: string, context?: Record<string, unknown>): void;
|
|
12
|
+
/** An AirPlay request passed the guard; the hook for whoever takes over the receiver. */
|
|
13
|
+
onAllowed(trigger: ExternalPlaybackAllowedTrigger): void;
|
|
14
|
+
/** The phase of the SSB switch that owns the route, or null when none is in progress. */
|
|
15
|
+
ssbSwitchPhase(): SsbSwitchPhase | null;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Keeps the content player off AirPlay while the channel offers an SSB stream: the gate is closed
|
|
19
|
+
* for the session, a request during or just before a break is reported as blocked, and a blocked
|
|
20
|
+
* request is evaluated again when the break ends if the route is still on AirPlay.
|
|
21
|
+
*/
|
|
22
|
+
export declare class ExternalPlaybackGate {
|
|
23
|
+
private readonly deps;
|
|
24
|
+
private managedByUs;
|
|
25
|
+
private closedByUs;
|
|
26
|
+
private pending;
|
|
27
|
+
private lastState;
|
|
28
|
+
constructor(deps: ExternalPlaybackGateDeps);
|
|
29
|
+
get managed(): boolean;
|
|
30
|
+
/** Called on session start and channel switch: true when the manifest carries an SSB stream. */
|
|
31
|
+
setManaged(managed: boolean): void;
|
|
32
|
+
/** The `externalplaybackchange` listener: reports the change, then guards a new AirPlay request. */
|
|
33
|
+
readonly onPlayerChange: (payload?: unknown) => void;
|
|
34
|
+
handleBreakEnded(): void;
|
|
35
|
+
isAllowedNow(): boolean;
|
|
36
|
+
dispose(): void;
|
|
37
|
+
private release;
|
|
38
|
+
private decide;
|
|
39
|
+
private evaluate;
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{diagnoseExternalPlaybackChange as e}from"./externalPlaybackDiagnostic";import{decideExternalPlaybackGuard as s}from"./externalPlaybackGuard";import{resolveExternalPlaybackRequestTrigger as t,shouldEvaluateExternalPlaybackRequest as a}from"./ssbSwitchSignals";export class ExternalPlaybackGate{constructor(s){this.deps=s,this.managedByUs=!1,this.closedByUs=!1,this.pending=!1,this.lastState="inactive",this.onPlayerChange=s=>{e(s,this.deps.diagnose);const a=s?.state;if(void 0===a)return;const i=t(a,this.lastState,this.pending);this.lastState=a,i?this.evaluate(i):"inactive"===a&&(this.pending=!1)}}get managed(){return this.managedByUs}setManaged(e){const{player:s}=this.deps;this.managedByUs=e&&"function"==typeof s.setExternalPlaybackAllowed,this.managedByUs&&!this.closedByUs?(s.setExternalPlaybackAllowed?.(!1),this.closedByUs=!0):this.managedByUs||this.release()}handleBreakEnded(){if(!this.pending)return;const e=this.deps.player.externalPlaybackState;"connecting"===e||"active"===e?this.evaluate("break-ended"):this.pending=!1}isAllowedNow(){return this.decide().allowed}dispose(){this.managedByUs=!1,this.pending=!1,this.release()}release(){this.pending=!1,this.closedByUs&&(this.closedByUs=!1,this.deps.player.setExternalPlaybackAllowed?.(!0))}decide(){return s({...this.deps.guardInput(),blockBeforeBreakSec:this.deps.blockBeforeBreakSec})}evaluate(e){if(!this.managedByUs||!a(this.deps.ssbSwitchPhase()))return;const t=this.deps.guardInput(),i=s({...t,blockBeforeBreakSec:this.deps.blockBeforeBreakSec});if(i.allowed)return this.pending=!1,void this.deps.onAllowed(e);this.pending=!0,this.deps.diagnose("DA-EXTERNAL-PLAYBACK-BLOCKED",`AirPlay blocked: ${i.reason}`,{reason:i.reason,breakId:i.breakId,secondsUntilBreak:t.secondsUntilNextBreak,trigger:e})}}
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import type { PlayerAdapter } from '../interfaces/PlayerAdapter';
|
|
2
|
-
import type { LiveStartPushbackConfig
|
|
3
|
-
/**
|
|
4
|
-
export
|
|
5
|
-
enabled: boolean;
|
|
6
|
-
enabledBy: 'config' | 'ptsSource';
|
|
7
|
-
}
|
|
8
|
-
/** An explicit `enabled` wins; otherwise the adjustment follows Anvato cue signaling. */
|
|
9
|
-
export declare function resolveLiveStartEnablement(config: LiveStartPushbackConfig | undefined, ptsSource: PtsSource | undefined): LiveStartEnablement;
|
|
2
|
+
import type { LiveStartPushbackConfig } from '../types/OptiViewAdsConfig';
|
|
3
|
+
/** The startup adjustment is opt-in: only an explicit `enabled: true` turns it on. */
|
|
4
|
+
export declare function isLiveStartPushbackEnabled(config: LiveStartPushbackConfig | undefined): boolean;
|
|
10
5
|
interface LiveStartPushbackDeps {
|
|
11
6
|
blocked: () => boolean;
|
|
12
7
|
setSeeking: (active: boolean) => void;
|
|
@@ -21,8 +16,8 @@ export declare class LiveStartPushbackController {
|
|
|
21
16
|
private startOffsetRequested;
|
|
22
17
|
private generation;
|
|
23
18
|
private cleanupLanding;
|
|
24
|
-
private readonly
|
|
25
|
-
constructor(player: PlayerAdapter, config: LiveStartPushbackConfig | undefined, deps: LiveStartPushbackDeps
|
|
19
|
+
private readonly enabled;
|
|
20
|
+
constructor(player: PlayerAdapter, config: LiveStartPushbackConfig | undefined, deps: LiveStartPushbackDeps);
|
|
26
21
|
start(): void;
|
|
27
22
|
stop(): void;
|
|
28
23
|
private disarm;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{contentSeekableEnd as e}from"./adPlayer/liveEdge";import{resolveLiveStartPushback as t}from"./resolveLiveStartPushback";export function
|
|
1
|
+
import{contentSeekableEnd as e}from"./adPlayer/liveEdge";import{resolveLiveStartPushback as t}from"./resolveLiveStartPushback";export function isLiveStartPushbackEnabled(e){return!0===e?.enabled}const i=e=>"number"==typeof e&&Number.isFinite(e)&&e>0,s=e=>Math.round(1e3*e)/1e3;export class LiveStartPushbackController{constructor(a,n,r){this.player=a,this.config=n,this.deps=r,this.armed=!1,this.playbackSeen=!1,this.startOffsetRequested=!1,this.generation=0,this.cleanupLanding=null,this.onPlaying=()=>{this.playbackSeen=!0,this.attempt()},this.attempt=()=>{if(!this.playbackSeen)return;if(!this.armed||this.deps.blocked()||this.player.paused||!1===this.player.playbackReady)return;const a=this.player.isLive??(this.player.duration===1/0||!Number.isFinite(this.player.duration)&&null);if(null===a)return;const n=this.startOffsetRequested;if(this.disarm(),this.startOffsetRequested=!1,!a)return;const r=this.player.targetDuration,l=i(r)?r:this.config?.targetDurationSeconds,o=this.config?.minimumDistanceTargetDurations??5,d=this.player.currentTime,u=e(this.player),h=this.publishedEdge(u),f=this.player.videoElement?.seekable,g=this.player.seekableStart??(f?.length?f.start(f.length-1):null),p={enabledBy:"config",position:d,seekableStart:g,seekableEnd:u,liveEdge:h,liveEdgeSource:i(this.player.liveEdge)?"adapter":"seekable",targetDurationSec:i(l)?l:null,targetDurationSource:i(r)?"adapter":i(l)?"config":"unavailable",requestedOffsetTargetDurations:o,startOffsetRequested:n};let c=null;if(i(l)?i(o)&&Number.isFinite(l*o)?(null===h||null===g||!Number.isFinite(g)||h<=g||!Number.isFinite(d))&&(c="no-range"):c="invalid-config":c="no-target-duration",!c&&i(l)&&null!==h&&Object.assign(p,{initialOffsetSec:s(h-d),initialOffsetTargetDurations:s((h-d)/l),requestedOffsetSec:s(o*l)}),!this.enabled)return void this.skip("disabled",p);if(c||!i(l)||null===h||null===g)return void this.skip(c??"no-range",p);if(n&&h-d>=o*l-.25)return void this.reportStartOffset(d,h,l,o,p);const y=t(d,g,h,l,o);if(null===y)return void this.skip(h-d>=o*l?"already-behind":"no-backward-target",p);Object.assign(p,{target:y,rewindSec:s(d-y),rewindTargetDurations:s((d-y)/l),targetOffsetSec:s(h-y),targetOffsetTargetDurations:s((h-y)/l),windowClamped:y>h-o*l,method:"seek"});const D=this.generation;this.deps.setSeeking(!0);const S=t=>{if(D!==this.generation||!this.cleanupLanding)return;this.cleanupLanding();const i="seeked"===t?this.player.currentTime:null,a=this.publishedEdge(e(this.player)),n=null!==i&&Number.isFinite(i)&&null!==a?a-i:null;this.deps.diagnose("DA-LIVE-START-PUSHBACK-LANDED",`Live start landing: TD=${l}s, offset=${null===n?"unknown":`${s(n)}s (${s(n/l)}xTD)`}, ${t}`,{...p,reason:t,landed:i,liveEdgeAtLanding:a,landedOffsetSec:null===n?null:s(n),landedOffsetTargetDurations:null===n?null:s(n/l),errorSec:null===i?null:s(i-y)})},m=()=>S("seeked"),b=setTimeout(()=>S("timeout"),1e4);if(this.cleanupLanding=()=>{this.cleanupLanding=null,clearTimeout(b),this.player.off("seeked",m),this.deps.setSeeking(!1)},this.player.on("seeked",m),this.deps.diagnose("DA-LIVE-START-PUSHBACK",`Live start: TD=${l}s, offset=${p.initialOffsetSec}s (${p.initialOffsetTargetDurations}xTD), requested=${o*l}s (${o}xTD), rewind=${p.rewindSec}s (${p.rewindTargetDurations}xTD), target=${y}s`,p),D===this.generation)try{this.player.seek(y)}catch{S("seek-failed")}},this.enabled=isLiveStartPushbackEnabled(n)}start(){this.stop(),this.armed=!0,this.playbackSeen=!this.player.paused&&!1!==this.player.playbackReady;const e=this.config?.minimumDistanceTargetDurations??5,t=i(this.player.targetDuration)?this.player.targetDuration:i(this.config?.targetDurationSeconds)?this.config.targetDurationSeconds:null;this.startOffsetRequested=this.enabled&&i(e)&&!0===this.player.requestLiveStartOffset?.(e,t),this.player.on("playing",this.onPlaying),this.player.on("timeupdate",this.attempt),this.attempt()}stop(){this.generation++,this.disarm(),this.cleanupLanding?.()}disarm(){this.armed=!1,this.player.off("playing",this.onPlaying),this.player.off("timeupdate",this.attempt),this.startOffsetRequested&&this.player.requestLiveStartOffset?.(null,null)}reportStartOffset(e,t,i,a,n){const r=t-a*i,l=t-e,o={...n,target:s(r),rewindSec:0,rewindTargetDurations:0,targetOffsetSec:s(a*i),targetOffsetTargetDurations:s(a),windowClamped:!1,method:"start-offset"};this.deps.diagnose("DA-LIVE-START-PUSHBACK",`Live start: TD=${i}s, began ${s(l)}s (${s(l/i)}xTD) behind live via the engine start offset, requested=${s(a*i)}s (${a}xTD), no seek`,o),this.deps.diagnose("DA-LIVE-START-PUSHBACK-LANDED",`Live start landing: TD=${i}s, offset=${s(l)}s (${s(l/i)}xTD), start-offset`,{...o,reason:"start-offset",landed:e,liveEdgeAtLanding:t,landedOffsetSec:s(l),landedOffsetTargetDurations:s(l/i),errorSec:s(e-r)})}publishedEdge(e){const t=this.player.liveEdge;return!i(t)||t-this.player.currentTime>=86400?e:null===e?t:Math.max(t,e)}skip(e,t){this.deps.diagnose("DA-LIVE-START-PUSHBACK-SKIPPED",`Live start unchanged: ${e}, TD=${t.targetDurationSec??"unknown"}s, offset=${t.initialOffsetSec??"unknown"}s (${t.initialOffsetTargetDurations??"unknown"}xTD)`,{...t,reason:e})}}
|
|
@@ -2,7 +2,7 @@ import { BreakManifest } from '../types/BreakManifest';
|
|
|
2
2
|
import { ManifestRequestInterceptor, ManifestResponseInterceptor } from '../types/OptiViewAdsConfig';
|
|
3
3
|
import { ManifestSignatureKey } from './manifestSignature';
|
|
4
4
|
/** Narrow diagnostic sink so the service does not depend on the full diagnostics module. */
|
|
5
|
-
export type ManifestDiagnose = (code: 'DA-MANIFEST-INTERCEPT-FAILED' | 'DA-MANIFEST-REQUEST-INTERCEPT-FAILED' | 'DA-MANIFEST-SIGNATURE-INVALID', message: string, context?: Record<string, unknown>) => void;
|
|
5
|
+
export type ManifestDiagnose = (code: 'DA-MANIFEST-INTERCEPT-FAILED' | 'DA-MANIFEST-REQUEST-INTERCEPT-FAILED' | 'DA-MANIFEST-SIGNATURE-INVALID' | 'DA-MANIFEST-EVENT-INVALID', message: string, context?: Record<string, unknown>) => void;
|
|
6
6
|
/** Fetches, verifies, parses and polls break manifests. */
|
|
7
7
|
/**
|
|
8
8
|
* Whole-request budget for one manifest fetch. Matches the Android OkHttp `callTimeout` and
|
|
@@ -47,6 +47,7 @@ export declare class ManifestService {
|
|
|
47
47
|
private trustedKeys;
|
|
48
48
|
/** Per-host tally of manifest reads this session, and the last failure. */
|
|
49
49
|
private readonly hostTally;
|
|
50
|
+
private readonly reportedEventIssues;
|
|
50
51
|
private lastFailure;
|
|
51
52
|
constructor(debug?: boolean, interceptor?: ManifestResponseInterceptor, diagnose?: ManifestDiagnose, requestInterceptor?: ManifestRequestInterceptor, trustedKeys?: readonly ManifestSignatureKey[]);
|
|
52
53
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fetchWithTimeout as t}from"./fetchWithTimeout";import{verifyManifestSignature as e,MANIFEST_SIGNATURE_HEADER as
|
|
1
|
+
import{fetchWithTimeout as t}from"./fetchWithTimeout";import{verifyManifestSignature as e,MANIFEST_SIGNATURE_HEADER as s}from"./manifestSignature";import{embeddedManifestKeys as i}from"./manifestTrustedKeys";import{parseBreakManifest as r}from"./parseBreakManifest";import{resolvePollingDecision as n}from"./resolvePollingDecision";import{debugLog as o}from"../diagnostics/debugLog";export const MANIFEST_FETCH_TIMEOUT_MS=15e3;export class ManifestService{get currentPollingIntervalSeconds(){return this.pollingIntervalSeconds}constructor(t=!1,e,s,r,n){this.manifestUrl=null,this.pollingInterval=null,this.pollingIntervalSeconds=null,this.pollCallbacks=null,this.currentManifest=null,this.isActive=!1,this.hostTally=new Map,this.reportedEventIssues=new Set,this.lastFailure=null,this.debug=t,this.interceptor=e,this.diagnose=s,this.requestInterceptor=r,this.trustedKeys=n??i()}takeLastFailure(){const t=this.lastFailure;return this.lastFailure=null,t}record(t,e,s,i){const r=function(t){try{return new URL(t).host}catch{return t}}(t),n=this.hostTally.get(r)??{ok:0,fail:0};if("ok"===e)return n.ok++,void this.hostTally.set(r,n);this.lastFailure={url:t,host:r,outcome:e,ms:s,hostOk:n.ok,hostFail:n.fail,...void 0===i?{}:{status:i}},n.fail++,this.hostTally.set(r,n)}setManifestUrl(t){this.manifestUrl=t,this.reportedEventIssues.clear()}async fetchManifest(){if(!this.manifestUrl)throw new Error("Manifest URL not set. Call setManifestUrl() before fetching.");this.log("Fetching manifest from:",this.manifestUrl);const t=await this.fetchManifestData(this.manifestUrl),e=this.parseManifest(t),s=await this.applyInterceptor(e);return this.currentManifest=s,this.log("Manifest loaded:",s.breaks.length,"breaks"),s}async fetchManifestData(i){const r={url:i};let n;if(this.requestInterceptor)try{n=await this.requestInterceptor({...r},{url:i})??void 0}catch(t){const e=t instanceof Error?t.message:String(t);this.diagnose?.("DA-MANIFEST-REQUEST-INTERCEPT-FAILED",e,{url:i}),this.log("Manifest request interceptor threw; using network fetch:",e),n=void 0}if(n&&"body"in n)return this.log("Manifest request interceptor returned a mock response (no network)"),"string"==typeof n.body?JSON.parse(n.body):n.body;const o=n??r,l=Date.now();let a;try{a=await t(o.url,o.headers?{headers:o.headers}:{},15e3,"Failed to fetch manifest: no response within 15000ms")}catch(t){const e=t instanceof Error&&t.message.includes("no response within");throw this.record(o.url,e?"timeout":"network",Date.now()-l),t}if(!a.ok)throw this.record(o.url,"http",Date.now()-l,a.status),new Error(`Failed to fetch manifest: ${a.status} ${a.statusText}`);this.record(o.url,"ok",Date.now()-l);const h=new Uint8Array(await a.arrayBuffer()),c=a.headers.get(s),u=e(c,h,this.trustedKeys);if(!u.valid){const t=`Manifest signature verification failed: ${u.reason}`;throw this.diagnose?.("DA-MANIFEST-SIGNATURE-INVALID",t,{url:o.url,reason:u.reason}),this.log("Rejecting manifest:",t),new Error(t)}return JSON.parse((new TextDecoder).decode(h))}async applyInterceptor(t){if(!this.interceptor)return t;const e=this.manifestUrl??"";try{return await this.interceptor(t,{url:e})}catch(s){const i=s instanceof Error?s.message:String(s);return this.diagnose?.("DA-MANIFEST-INTERCEPT-FAILED",i,{url:e}),this.log("Manifest interceptor threw; using un-modified manifest:",i),t}}parseManifest(t){return r(t,t=>{this.reportedEventIssues.has(t.key)||(this.reportedEventIssues.add(t.key),this.diagnose?.("DA-MANIFEST-EVENT-INVALID",t.message,t.context))})}startPolling(t,e){this.pollCallbacks={onUpdate:t,onError:e},this.applyPollingDecision()}stopPolling(){this.pollCallbacks=null,this.clearPollingTimer()}setActive(t){this.isActive!==t&&(this.isActive=t,this.log("Active state changed:",t),this.applyPollingDecision())}applyPollingDecision(){if(!this.pollCallbacks)return;const t=n(this.currentManifest?.polling,this.isActive);if("once"===t.mode)return this.pollingInterval&&this.log("Manifest has no polling object â fetch-once, stopping timer"),void this.clearPollingTimer();this.pollingInterval&&this.pollingIntervalSeconds===t.intervalSeconds||(this.clearPollingTimer(),this.log("Starting polling with interval:",t.intervalSeconds,"seconds"),this.pollingIntervalSeconds=t.intervalSeconds,this.pollingInterval=setInterval(async()=>{const t=this.pollCallbacks;if(t){try{const e=await this.fetchManifest();e.breaks.length>0&&this.log(`đĄ Manifest: ${e.breaks.length} break(s)`),t.onUpdate(e)}catch(e){t.onError(e instanceof Error?e:new Error(String(e)))}this.applyPollingDecision()}},1e3*t.intervalSeconds))}clearPollingTimer(){this.pollingInterval&&(clearInterval(this.pollingInterval),this.pollingInterval=null,this.pollingIntervalSeconds=null,this.log("Polling stopped"))}getManifest(){return this.currentManifest}reset(){this.stopPolling(),this.currentManifest=null,this.manifestUrl=null,this.isActive=!1,this.reportedEventIssues.clear()}destroy(){this.stopPolling(),this.currentManifest=null,this.manifestUrl=null,this.reportedEventIssues.clear()}log(...t){this.debug&&o("[OptiViewAds:ManifestService]",...t)}}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Marks a window as reload noise: on while armed, released by a timer, re-armable, cancellable. */
|
|
2
|
+
export declare class ReloadGuard {
|
|
3
|
+
private readonly setReloading;
|
|
4
|
+
private readonly windowMs;
|
|
5
|
+
private timer;
|
|
6
|
+
constructor(setReloading: (reloading: boolean) => void, windowMs: number);
|
|
7
|
+
/** Starts (or restarts) the window. */
|
|
8
|
+
arm(): void;
|
|
9
|
+
/** Cancels a pending release without signalling. */
|
|
10
|
+
dispose(): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class ReloadGuard{constructor(t,i){this.setReloading=t,this.windowMs=i,this.timer=null}arm(){this.setReloading(!0),this.timer&&clearTimeout(this.timer),this.timer=setTimeout(()=>{this.timer=null,this.setReloading(!1)},this.windowMs)}dispose(){this.timer&&clearTimeout(this.timer),this.timer=null}}
|
|
@@ -2,6 +2,7 @@ import { BreakState } from './BreakScheduler';
|
|
|
2
2
|
import type { Clock } from './BreakScheduler';
|
|
3
3
|
import { Break } from '../types/BreakManifest';
|
|
4
4
|
import type { BreakTimingState } from './breakTiming';
|
|
5
|
+
import type { BreakPreloadLedger } from './BreakPreloadLedger';
|
|
5
6
|
export interface TrackedBreak {
|
|
6
7
|
break: Break;
|
|
7
8
|
state: BreakState;
|
|
@@ -10,7 +11,7 @@ export interface RewindRearmDeps {
|
|
|
10
11
|
breaks: Map<string, TrackedBreak>;
|
|
11
12
|
retiredBreaks: Map<string, Break>;
|
|
12
13
|
completedBreakIds: Set<string>;
|
|
13
|
-
|
|
14
|
+
preloadLedger: BreakPreloadLedger;
|
|
14
15
|
timing: BreakTimingState;
|
|
15
16
|
clock: Clock;
|
|
16
17
|
log: (...args: unknown[]) => void;
|
|
@@ -74,6 +75,7 @@ export declare class RewindRearm {
|
|
|
74
75
|
/** Anchor played-time tracking at the position where content playback began. */
|
|
75
76
|
markPlaybackStart(positionMs: number | null): void;
|
|
76
77
|
setAdStartDelayMs(ms: number): void;
|
|
78
|
+
ignoreAdStartDelay(): void;
|
|
77
79
|
/**
|
|
78
80
|
* The schedule now follows another channel: pre-roll `delay` counts from here. Once playback
|
|
79
81
|
* has started, `adStartDelay` is frozen (also for later polls); before that the next manifest's
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BreakState as e}from"./BreakScheduler";import{Timebase as s}from"../types/BreakManifest";export class RewindRearm{constructor(e){this.deps=e,this._adStartDelayMs=0,this.adStartDelayElapsedLogged=!1,this.adStartDelayIgnored=!1,this.channelPlayedBaselineMs=0,this._playbackStarted=!1,this._playedMs=0,this.lastPlayedPositionMs=null,this.lastPlayedClockMs=null,this.contentReloading=!1,this.resumeSeekBaselineMs=null,this.resumeSeekBreakEndMs=null,this.resumeSeekInFlight=!1,this.pendingRewindFromMs=null,this.lastCurrentTime=null,this.REWIND_JITTER_TOLERANCE_MS=2e3}isAdStartDelayActive(){return this._adStartDelayMs>0&&this._playedMs<this._adStartDelayMs}get playbackStarted(){return this._playbackStarted}get playedMs(){return this._playedMs}markPlaybackStart(e){this._playbackStarted=!0,this.lastPlayedPositionMs=e,this.lastPlayedClockMs=this.deps.clock.now()}setAdStartDelayMs(e){this.adStartDelayIgnored||(this._adStartDelayMs=e)}
|
|
1
|
+
import{BreakState as e}from"./BreakScheduler";import{Timebase as s}from"../types/BreakManifest";export class RewindRearm{constructor(e){this.deps=e,this._adStartDelayMs=0,this.adStartDelayElapsedLogged=!1,this.adStartDelayIgnored=!1,this.channelPlayedBaselineMs=0,this._playbackStarted=!1,this._playedMs=0,this.lastPlayedPositionMs=null,this.lastPlayedClockMs=null,this.contentReloading=!1,this.resumeSeekBaselineMs=null,this.resumeSeekBreakEndMs=null,this.resumeSeekInFlight=!1,this.pendingRewindFromMs=null,this.lastCurrentTime=null,this.REWIND_JITTER_TOLERANCE_MS=2e3}isAdStartDelayActive(){return this._adStartDelayMs>0&&this._playedMs<this._adStartDelayMs}get playbackStarted(){return this._playbackStarted}get playedMs(){return this._playedMs}markPlaybackStart(e){this._playbackStarted=!0,this.lastPlayedPositionMs=e,this.lastPlayedClockMs=this.deps.clock.now()}setAdStartDelayMs(e){this.adStartDelayIgnored||(this._adStartDelayMs=e)}ignoreAdStartDelay(){this._playbackStarted&&(this._adStartDelayMs=0,this.adStartDelayIgnored=!0)}beginChannel(){this.channelPlayedBaselineMs=this._playedMs,this.ignoreAdStartDelay()}get channelPlayedMs(){return this._playedMs-this.channelPlayedBaselineMs}get adStartDelayMs(){return this._adStartDelayMs}setContentReloading(e){this.contentReloading=e,e||(this.lastCurrentTime=null),this.pendingRewindFromMs=null}notifyResumeSeek(e,s){Number.isFinite(e)&&Number.isFinite(s)&&(this.resumeSeekBaselineMs=1e3*e,this.resumeSeekBreakEndMs=1e3*s,this.resumeSeekInFlight=!0,this.lastCurrentTime=this.resumeSeekBaselineMs,this.pendingRewindFromMs=null)}notifyViewerSeek(){this.resumeSeekBaselineMs=null,this.resumeSeekBreakEndMs=null,this.resumeSeekInFlight=!1}trackPlayedTime(e){if(!this._playbackStarted)return;const s=this.deps.clock.now();if(null===e)return this.lastPlayedPositionMs=null,void(this.lastPlayedClockMs=s);if(null!==this.lastPlayedPositionMs&&null!==this.lastPlayedClockMs){const t=e-this.lastPlayedPositionMs,i=s-this.lastPlayedClockMs;this._playedMs+=Math.min(Math.max(0,t),Math.max(0,i)),this._adStartDelayMs>0&&this._playedMs>=this._adStartDelayMs&&!this.adStartDelayElapsedLogged&&(this.adStartDelayElapsedLogged=!0,this.deps.log(`â° Ad start delay elapsed (${this._adStartDelayMs/1e3}s played)`))}this.lastPlayedPositionMs=e,this.lastPlayedClockMs=s}rearmOnRewind(e){const s=this.lastCurrentTime;if(this.lastCurrentTime=e,null!==this.resumeSeekBreakEndMs&&e>this.resumeSeekBreakEndMs&&(this.resumeSeekBaselineMs=null,this.resumeSeekBreakEndMs=null,this.resumeSeekInFlight=!1),this.contentReloading)this.pendingRewindFromMs=null;else{if(null!==this.pendingRewindFromMs){const s=this.pendingRewindFromMs;if(this.pendingRewindFromMs=null,this.isRewindBeyondJitter(s,e))return void this.rearmRetiredBreaks(e)}null===s||e>=s?this.resumeSeekInFlight&&null!==s&&e>s&&(this.resumeSeekInFlight=!1,this.resumeSeekBaselineMs=null,this.resumeSeekBreakEndMs=null):this.isRewindBeyondJitter(s,e)&&(this.resumeSeekInFlight&&null!==this.resumeSeekBreakEndMs&&e>=0&&e<=this.resumeSeekBreakEndMs||(this.pendingRewindFromMs=s))}}reset(){this.lastCurrentTime=null,this._adStartDelayMs=0,this.adStartDelayElapsedLogged=!1,this._playbackStarted=!1,this._playedMs=0,this.adStartDelayIgnored=!1,this.channelPlayedBaselineMs=0,this.lastPlayedPositionMs=null,this.lastPlayedClockMs=null,this.resumeSeekBaselineMs=null,this.resumeSeekBreakEndMs=null,this.resumeSeekInFlight=!1,this.pendingRewindFromMs=null}isRewindBeyondJitter(e,t){return!(t>=e)&&!(this.deps.timing.timebase===s.WALLCLOCK&&e-t<=this.REWIND_JITTER_TOLERANCE_MS)}rearmRetiredBreaks(s){if(this.resumeSeekBaselineMs=null,this.resumeSeekBreakEndMs=null,this.resumeSeekInFlight=!1,0!==this.deps.retiredBreaks.size)for(const[t,i]of[...this.deps.retiredBreaks]){const a=this.deps.breaks.get(t);if(a&&(a.state===e.PENDING||a.state===e.ACTIVE||a.state===e.CHAINED))continue;const l=this.deps.getBreakStartTime(i);if(null===l)continue;s<l+1e3*(i.duration??0)&&(this.deps.retiredBreaks.delete(t),this.deps.completedBreakIds.delete(t),this.deps.preloadLedger.deletePreloaded(t),this.deps.breaks.set(t,{break:i,state:e.PENDING}),this.deps.log(`âŠī¸ Re-armed break "${t}" after backward seek`))}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{objectEntries as e}from"./runtimeObject";import{applyAssetParameterMacros as t}from"./assetParameterMacros";import{buildAssetParameterMacroTable as a}from"./assetParameterMacroTable";import{resolveAssetParameters as s}from"./resolveAssetParameters";import{AssetType as r,MediaType as i,BreakFormat as o}from"../types/BreakManifest";import{GAM_DUPLICATE_METADATA_DIAGNOSTIC_MESSAGE as n,GamStreamManager as m}from"./GamStreamManager";import{resolveBreakFormat as d}from"./AdBreakSequencer";import{buildStitcherMasterUrl as c}from"./StitcherUrlBuilder";import{resolveExternalVendorParameters as h}from"./resolveExternalVendorConfigurations";export class SsaiController{constructor(e,t,a){this.player=e,this.options=t,this.callbacks=a,this.gamStreamManager=null,this.breakCounter=0,this.adIndexInBreak=0,this.currentBreak=null,this.currentAsset=null,this.customerMacros={},this.onTimedMetadata=e=>{if(!e)return;const t=e;this.gamStreamManager?.processTimedMetadata(t)}}async start(e){const t=this.player.videoElement;if(!t)throw new Error("SSAI mode requires a content PlayerAdapter that exposes a videoElement (for IMA DAI).");if(!e.customAssetKey)throw new Error("SSAI mode requires a customAssetKey in startSession().");this.sessionConfigParameters=e.assetParameters,this.updatedParameters=void 0,this.manifestParameters=void 0,this.customerMacros={...e.assetParameterMacros};const a=new m(t,void 0,this.options.debug);a.setCallbacks(this.buildGamCallbacks()),this.gamStreamManager=a,await a.initialize({networkCode:this.options.ssai.networkCode,customAssetKey:e.customAssetKey,adTagParameters:this.resolveAdTagParameters(),streamActivityMonitorId:this.options.gam.streamActivityMonitorId});const s=a.getStreamId();if(!s)throw new Error("SSAI mode: IMA DAI session did not return a stream id.");const r=c({stitcherUrl:e.stitcherUrl,streamId:s,maxBitrate:this.options.maxBitrate});this.callbacks.log("SSAI stitched master URL:",r),this.player.on("timedmetadata",this.onTimedMetadata),await this.player.load(r),this.options.autoplay&&await this.player.play()}async startSteered(e,t,a){const s=this.player.videoElement;if(!s)throw new Error("SSAI mode requires a content PlayerAdapter that exposes a videoElement (for IMA DAI).");this.sessionConfigParameters=t,this.updatedParameters=void 0,this.manifestParameters=e.assetParameters,this.customerMacros={...a};const r=new m(s,void 0,this.options.debug);r.setCallbacks(this.buildGamCallbacks()),this.gamStreamManager=r;const i=await r.initializeLiveStream({assetKey:e.assetKey,networkCode:e.networkCode,adTagParameters:this.resolveAdTagParameters(),streamActivityMonitorId:this.options.gam.streamActivityMonitorId});this.callbacks.log("SSAI full-service DAI stream URL:",i),this.player.on("timedmetadata",this.onTimedMetadata),await this.player.load(i),this.options.autoplay&&await this.player.play()}resolveAdTagParameters(){const e=s(this.sessionConfigParameters,this.updatedParameters,this.manifestParameters,void 0),r=t(e,a({customerMacros:this.customerMacros,element:this.player.videoElement??null,breakDurationSec:void 0})
|
|
1
|
+
import{objectEntries as e}from"./runtimeObject";import{applyAssetParameterMacros as t}from"./assetParameterMacros";import{buildAssetParameterMacroTable as a}from"./assetParameterMacroTable";import{resolveAssetParameters as s}from"./resolveAssetParameters";import{AssetType as r,MediaType as i,BreakFormat as o}from"../types/BreakManifest";import{GAM_DUPLICATE_METADATA_DIAGNOSTIC_MESSAGE as n,GamStreamManager as m}from"./GamStreamManager";import{resolveBreakFormat as d}from"./AdBreakSequencer";import{buildStitcherMasterUrl as c}from"./StitcherUrlBuilder";import{resolveExternalVendorParameters as h}from"./resolveExternalVendorConfigurations";export class SsaiController{constructor(e,t,a){this.player=e,this.options=t,this.callbacks=a,this.gamStreamManager=null,this.breakCounter=0,this.adIndexInBreak=0,this.currentBreak=null,this.currentAsset=null,this.customerMacros={},this.onTimedMetadata=e=>{if(!e)return;const t=e;this.gamStreamManager?.processTimedMetadata(t)}}async start(e){const t=this.player.videoElement;if(!t)throw new Error("SSAI mode requires a content PlayerAdapter that exposes a videoElement (for IMA DAI).");if(!e.customAssetKey)throw new Error("SSAI mode requires a customAssetKey in startSession().");this.sessionConfigParameters=e.assetParameters,this.updatedParameters=void 0,this.manifestParameters=void 0,this.customerMacros={...e.assetParameterMacros};const a=new m(t,void 0,this.options.debug);a.setCallbacks(this.buildGamCallbacks()),this.gamStreamManager=a,await a.initialize({networkCode:this.options.ssai.networkCode,customAssetKey:e.customAssetKey,adTagParameters:this.resolveAdTagParameters(),streamActivityMonitorId:this.options.gam.streamActivityMonitorId});const s=a.getStreamId();if(!s)throw new Error("SSAI mode: IMA DAI session did not return a stream id.");const r=c({stitcherUrl:e.stitcherUrl,streamId:s,maxBitrate:this.options.maxBitrate});this.callbacks.log("SSAI stitched master URL:",r),this.player.on("timedmetadata",this.onTimedMetadata),await this.player.load(r),this.options.autoplay&&await this.player.play()}async startSteered(e,t,a){const s=this.player.videoElement;if(!s)throw new Error("SSAI mode requires a content PlayerAdapter that exposes a videoElement (for IMA DAI).");this.sessionConfigParameters=t,this.updatedParameters=void 0,this.manifestParameters=e.assetParameters,this.customerMacros={...a};const r=new m(s,void 0,this.options.debug);r.setCallbacks(this.buildGamCallbacks()),this.gamStreamManager=r;const i=await r.initializeLiveStream({assetKey:e.assetKey,networkCode:e.networkCode,adTagParameters:this.resolveAdTagParameters(),streamActivityMonitorId:this.options.gam.streamActivityMonitorId});this.callbacks.log("SSAI full-service DAI stream URL:",i),this.player.on("timedmetadata",this.onTimedMetadata),await this.player.load(i),this.options.autoplay&&await this.player.play()}resolveAdTagParameters(){const e=s(this.sessionConfigParameters,this.updatedParameters,this.manifestParameters,void 0),r=t(e,a({customerMacros:this.customerMacros,element:this.player.videoElement??null,breakDurationSec:void 0}));for(const{key:e,macro:t}of r.unresolved)this.callbacks.diagnose("DA-ASSET-MACRO-UNKNOWN",`Asset parameter '${e}' sent with literal macro '${t}': the macro has no value.`,{key:e,macro:t});for(const{key:e,macro:t,pair:a}of r.omitted)this.callbacks.diagnose("DA-ASSET-PARAMETER-OMITTED",`Asset parameter '${e}' not sent: macro '${t}' is empty.`,{key:e,macro:t,...void 0!==a?{pair:a}:{}});return r.params}updateAssetParameters(e){this.updatedParameters=e,this.gamStreamManager?.replaceAdTagParameters(this.resolveAdTagParameters()??{})}resolveExternalVendorConfiguration(e){const t=h(e,s(this.sessionConfigParameters,this.updatedParameters,void 0,void 0),a({customerMacros:this.customerMacros,element:this.player.videoElement??null,breakDurationSec:void 0}));for(const{key:e,macro:a}of t.unresolved)this.callbacks.diagnose("DA-ASSET-MACRO-UNKNOWN",`Asset parameter '${e}' sent with literal macro '${a}': the macro has no value.`,{key:e,macro:a});for(const{key:e,macro:a}of t.omitted)this.callbacks.diagnose("DA-ASSET-PARAMETER-OMITTED",`Asset parameter '${e}' not sent: macro '${a}' is empty.`,{key:e,macro:a});return t.configuration}updateAssetParameterMacros(t){const a={...this.customerMacros};for(const[s,r]of e(t))void 0===r?delete a[s]:a[s]=r;this.customerMacros=a,this.callbacks.diagnose("DA-ASSET-PARAMETERS-UPDATED","Asset parameter macros updated.",{source:"macros",keys:Object.keys(t).sort()}),this.gamStreamManager?.replaceAdTagParameters(this.resolveAdTagParameters()??{})}get sessionAssetParameterMacros(){return{...this.customerMacros}}setManifestAssetParameters(e){(function(e,t){e&&0===Object.keys(e).length&&(e=void 0);t&&0===Object.keys(t).length&&(t=void 0);if(e===t)return!0;if(!e||!t)return!1;const a=Object.keys(e);return a.length===Object.keys(t).length&&a.every(a=>e[a]===t[a])})(this.manifestParameters,e)||(this.manifestParameters=e,this.gamStreamManager&&(this.gamStreamManager.replaceAdTagParameters(this.resolveAdTagParameters()??{}),this.callbacks.diagnose("DA-ASSET-PARAMETERS-UPDATED","Manifest asset parameters changed; IMA targeting updated.",{source:"manifest",keys:Object.keys(e??{}).sort()})))}stop(){this.player.off("timedmetadata",this.onTimedMetadata),this.gamStreamManager?.destroy(),this.gamStreamManager=null,this.sessionConfigParameters=void 0,this.updatedParameters=void 0,this.manifestParameters=void 0,this.customerMacros={},this.currentBreak=null,this.currentAsset=null,this.breakCounter=0,this.adIndexInBreak=0}buildGamCallbacks(){return{onAdBreakStarted:()=>{this.breakCounter+=1,this.adIndexInBreak=0,this.currentBreak=this.synthBreak(`ssai-break-${this.breakCounter}`),this.callbacks.emit("adbreakbegin",{type:"adbreakbegin",timestamp:Date.now(),break:this.currentBreak,format:d(this.currentBreak),elapsedSec:0,remainingSec:this.currentBreak.duration})},onAdStarted:e=>{const t=this.ensureBreak(),a=this.adIndexInBreak;this.adIndexInBreak+=1,this.currentAsset=this.synthAsset(`${t.id}-ad-${this.adIndexInBreak}`),this.callbacks.emit("adbegin",{type:"adbegin",timestamp:Date.now(),break:t,format:d(t),asset:this.currentAsset,adIndex:a,totalAds:0,...void 0!==e?.adPosition&&Number.isInteger(e.adPosition)&&e.adPosition>=1?{podPosition:e.adPosition}:{}})},onAdFirstQuartile:()=>this.emitQuartile("adfirstquartile"),onAdMidpoint:()=>this.emitQuartile("admidpoint"),onAdThirdQuartile:()=>this.emitQuartile("adthirdquartile"),onDuplicateMetadata:e=>{this.callbacks.diagnose("DA-GAM-DUPLICATE-METADATA",n,{...this.currentAsset?{assetId:this.currentAsset.id}:{},startTime:e})},onAdComplete:()=>{const e=this.ensureBreak(),t=this.currentAsset??this.synthAsset(`${e.id}-ad`);this.callbacks.emit("adend",{type:"adend",timestamp:Date.now(),break:e,format:d(e),asset:t,adIndex:Math.max(0,this.adIndexInBreak-1),totalAds:0}),this.currentAsset=null},onAdBreakEnded:()=>{const e=this.ensureBreak();this.callbacks.emit("adbreakend",{type:"adbreakend",timestamp:Date.now(),break:e,format:d(e)}),this.currentBreak=null,this.currentAsset=null},onError:e=>{this.callbacks.diagnose("DA-SSAI-IMA-ERROR",e),this.callbacks.emit("aderror",{type:"aderror",timestamp:Date.now(),source:"ad",...this.currentBreak?{break:this.currentBreak,format:d(this.currentBreak)}:{},...this.currentAsset?{asset:this.currentAsset}:{},error:new Error(e)})}}}emitQuartile(e){const t=this.ensureBreak(),a=this.currentAsset??this.synthAsset(`${t.id}-ad`);this.callbacks.emit(e,{type:e,timestamp:Date.now(),break:t,format:d(t),asset:a})}ensureBreak(){return this.currentBreak||(this.breakCounter+=1,this.currentBreak=this.synthBreak(`ssai-break-${this.breakCounter}`)),this.currentBreak}synthBreak(e){return{id:e,start:this.player.currentTime,duration:0,variant:{format:o.SINGLE,assets:[]}}}synthAsset(e){return{id:e,type:r.STATIC,mediaType:i.VIDEO,uri:""}}}
|