@dolby-optiview/ads-sdk-core 1.3.0 → 1.5.1

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.
Files changed (43) hide show
  1. package/dist/OptiViewAds.js +1 -1
  2. package/dist/OptiViewAdsExternalVendors.d.ts +32 -0
  3. package/dist/generated/diagnosticCodes.d.ts +16 -1
  4. package/dist/generated/diagnosticCodes.js +1 -1
  5. package/dist/generated/version.d.ts +1 -1
  6. package/dist/generated/version.js +1 -1
  7. package/dist/index.d.ts +2 -2
  8. package/dist/services/AdPlayerController.d.ts +4 -12
  9. package/dist/services/AdPlayerController.js +1 -1
  10. package/dist/services/BreakScheduler.d.ts +9 -17
  11. package/dist/services/BreakScheduler.js +1 -1
  12. package/dist/services/BreakSchedulerTypes.d.ts +4 -0
  13. package/dist/services/SchedulerSnapback.d.ts +32 -0
  14. package/dist/services/SchedulerSnapback.js +1 -0
  15. package/dist/services/SnapbackGuard.d.ts +36 -0
  16. package/dist/services/SnapbackGuard.js +1 -0
  17. package/dist/services/adPlayer/AdPlayerCallbacks.d.ts +2 -0
  18. package/dist/services/adPlayer/AdPlayerEvents.d.ts +1 -7
  19. package/dist/services/adPlayer/AdPlayerEvents.js +1 -1
  20. package/dist/services/adPlayer/AdPlayerState.d.ts +1 -1
  21. package/dist/services/adPlayer/AdPlayerState.js +1 -1
  22. package/dist/services/adPlayer/AssetPlayback.d.ts +3 -0
  23. package/dist/services/adPlayer/AssetPlayback.js +1 -1
  24. package/dist/services/adPlayer/AudioController.d.ts +58 -0
  25. package/dist/services/adPlayer/AudioController.js +1 -0
  26. package/dist/services/adPlayer/ContentResumeCoordinator.d.ts +2 -0
  27. package/dist/services/adPlayer/ContentResumeCoordinator.js +1 -1
  28. package/dist/services/adPlayer/SharedElementBreakPlayer.d.ts +3 -0
  29. package/dist/services/adPlayer/SharedElementBreakPlayer.js +1 -1
  30. package/dist/services/adPlayer/SurfaceRevealReporter.d.ts +28 -0
  31. package/dist/services/adPlayer/SurfaceRevealReporter.js +1 -0
  32. package/dist/services/checkEventBreak.d.ts +5 -0
  33. package/dist/services/checkEventBreak.js +1 -0
  34. package/dist/services/resolveResumePoint.d.ts +3 -1
  35. package/dist/services/resolveResumePoint.js +1 -1
  36. package/dist/session/BreakLifecycle.js +1 -1
  37. package/dist/session/SessionStarter.js +1 -1
  38. package/dist/session/contentSnapback.d.ts +5 -1
  39. package/dist/session/contentSnapback.js +1 -1
  40. package/dist/types/BreakManifest.d.ts +26 -5
  41. package/dist/types/Events.d.ts +1 -0
  42. package/dist/types/ExternalVendorConfiguration.d.ts +3 -1
  43. package/package.json +1 -1
@@ -0,0 +1,58 @@
1
+ import type { PlayerAdapter } from '../../interfaces/PlayerAdapter';
2
+ import type { VastAdManager } from '../VastAdManager';
3
+ export type AudioSide = 'ad' | 'content';
4
+ export interface AudioControllerDeps {
5
+ contentPlayer: PlayerAdapter;
6
+ /** Outside a break the viewer cannot reach the ad player, so a change there is the engine's own. */
7
+ isAdPlaying: () => boolean;
8
+ /** IMA plays a CSAI ad through its own audio path; null outside a VAST ad. */
9
+ getVastAdManager: () => VastAdManager | null;
10
+ /** Re-emitted by the SDK facade as the public `volumechange`. */
11
+ onVolumeChange: (muted: boolean, volume: number) => void;
12
+ log: (...args: unknown[]) => void;
13
+ }
14
+ /**
15
+ * Sole owner of the SDK's audio: holds the viewer's muted/volume state and applies it to the
16
+ * content and ad players, except to a side the SDK has muted for its own reasons (double box,
17
+ * continue-content playout, resume catch-up).
18
+ *
19
+ * Decisions read state, not timing: a `volumechange` that matches what this class applied is an
20
+ * echo of its own write, whenever it arrives; anything else is the viewer, the app or an IMA
21
+ * creative, and becomes the new viewer state.
22
+ */
23
+ export declare class AudioController {
24
+ private readonly deps;
25
+ private readonly viewer;
26
+ /** The one side the SDK forces silent on top of the viewer's state; the viewer's mute is {@link viewer}. */
27
+ private mutedSide;
28
+ private adPlayer;
29
+ private readonly contentHandler;
30
+ private readonly adHandler;
31
+ constructor(deps: AudioControllerDeps);
32
+ /**
33
+ * Once per controller; the ad player is never replaced during a session. A single-surface
34
+ * device plays the ad on the content player itself: there is no second side to keep in step.
35
+ */
36
+ attachAdPlayer(adPlayer: PlayerAdapter): void;
37
+ /** Gives every muted side the viewer's state back, then stops listening. */
38
+ destroy(): void;
39
+ /** Forces `side` silent, whatever the viewer's state. Replaces a mute on the other side. */
40
+ mute(side: AudioSide): void;
41
+ /** Stops forcing `side` silent: it gets the viewer's state back, which may itself be muted. */
42
+ unmute(side: AudioSide): void;
43
+ unmuteAll(): void;
44
+ isMuted(side: AudioSide): boolean;
45
+ /** The viewer's state (`sdk.muted`), not what a muted side currently plays at. */
46
+ get muted(): boolean;
47
+ set muted(value: boolean);
48
+ /** 0-1. See {@link muted}. */
49
+ get volume(): number;
50
+ set volume(value: number);
51
+ private setViewerState;
52
+ private player;
53
+ private expectedMuted;
54
+ private apply;
55
+ private applyAll;
56
+ private onPlayerVolumeChange;
57
+ private notify;
58
+ }
@@ -0,0 +1 @@
1
+ export class AudioController{constructor(e){this.deps=e,this.mutedSide=null,this.adPlayer=null,this.contentHandler=()=>this.onPlayerVolumeChange("content"),this.adHandler=()=>this.onPlayerVolumeChange("ad"),this.viewer={muted:e.contentPlayer.muted,volume:e.contentPlayer.volume},e.contentPlayer.on("volumechange",this.contentHandler)}attachAdPlayer(e){e!==this.deps.contentPlayer&&(this.adPlayer?this.deps.log("AudioController: ad player already attached; ignoring the second one"):(this.adPlayer=e,e.on("volumechange",this.adHandler),this.apply("ad")))}destroy(){this.unmuteAll(),this.deps.contentPlayer.off("volumechange",this.contentHandler),this.adPlayer?.off("volumechange",this.adHandler),this.adPlayer=null}mute(e){this.mutedSide!==e&&(this.mutedSide&&(this.deps.log(`AudioController: muting ${e} replaces the mute on ${this.mutedSide}`),this.unmute(this.mutedSide)),this.mutedSide=e,this.apply(e))}unmute(e){this.mutedSide===e&&(this.mutedSide=null,this.apply(e))}unmuteAll(){this.mutedSide&&this.unmute(this.mutedSide)}isMuted(e){return this.mutedSide===e}get muted(){return this.viewer.muted}set muted(e){this.setViewerState(e,this.viewer.volume)}get volume(){return this.viewer.volume}set volume(e){this.setViewerState(this.viewer.muted,Math.max(0,Math.min(1,e)))}setViewerState(e,t){this.viewer.muted=e,this.viewer.volume=t,this.applyAll(),this.notify()}player(e){return"content"===e?this.deps.contentPlayer:this.adPlayer}expectedMuted(e){return this.mutedSide===e||this.viewer.muted}apply(e){const t=this.player(e);t&&(t.muted!==this.expectedMuted(e)&&(t.muted=this.expectedMuted(e)),Math.abs(t.volume-this.viewer.volume)>.001&&(t.volume=this.viewer.volume)),"ad"===e&&this.deps.getVastAdManager()?.setVolume(this.expectedMuted("ad")?0:this.viewer.volume)}applyAll(){this.apply("content"),this.apply("ad")}onPlayerVolumeChange(e){const t=this.player(e);if(!t)return;const i=t.muted!==this.expectedMuted(e),s=Math.abs(t.volume-this.viewer.volume)>.001;(i||s)&&("ad"!==e||this.deps.isAdPlaying()?(i&&(this.viewer.muted=t.muted),s&&(this.viewer.volume=t.volume),this.applyAll(),this.notify()):this.apply("ad"))}notify(){this.deps.onVolumeChange(this.viewer.muted,this.viewer.volume)}}
@@ -9,6 +9,8 @@ export interface ContentResumeCoordinatorDeps {
9
9
  getInsertionMode: () => string;
10
10
  gateContentForBreak: () => void;
11
11
  clearContentGate: (keepPlaying?: boolean) => void;
12
+ /** Silences content for the rest of the break; the break's hard end gives the viewer's audio back. */
13
+ muteContent: () => void;
12
14
  diagnose: (code: string, message: string, context?: Record<string, unknown>) => void;
13
15
  log: (...args: unknown[]) => void;
14
16
  }
@@ -1 +1 @@
1
- import{Timebase as e,isPostRollBreak as t}from"../../types/BreakManifest";import{resolveResumePoint as s}from"../resolveResumePoint";import{resolveResumeClamp as n}from"../resolveResumeClamp";import{BREAK_CUT_LATE_TOLERANCE_SEC as r,resolveWallclockBreakWindow as a}from"../wallclockBreakWindow";import{ContinueContentPlayout as i}from"./ContinueContentPlayout";import{StreamClockResume as o}from"./streamClockResume";import{PdtClockResume as u,programDateTimeSample as l}from"./pdtClockResume";import{contentSeekableEnd as d,isPlausibleSeekEnd as h,liveEdgeDistanceSec as c,liveSafeSeekEnd as m,liveSafeSeekStart as p,liveSeekableWindowSec as g,unreachableEdgeSec as k}from"./liveEdge";const S=.04,f=e=>Math.round(1e3*e)/1e3;export class ContentResumeCoordinator{constructor(e,t){this.state=e,this.deps=t,this.cueAnchorResolver=null,this.resumePolicyTarget=null,this.cueEndResolver=null,this.continuePlayout=new i(this.state,{contentPlayer:this.deps.contentPlayer,clock:this.deps.clock,gateContentForBreak:()=>this.deps.gateContentForBreak(),distanceToLiveEdgeSec:()=>this.distanceToLiveEdgeSec(),plannedResumeTarget:e=>this.plannedResumeTarget(e),diagnose:(e,t,s)=>this.deps.diagnose(e,t,s),log:(...e)=>this.deps.log(...e)}),this.streamClock=new o({contentPlayer:this.deps.contentPlayer,diagnose:(e,t,s)=>this.deps.diagnose(e,t,s),activeBreak:()=>this.state.currentBreak,anchorIsCue:()=>this.state.resumeAnchorIsCue,breakEnd:e=>this.cueEndResolver?.(e)??null,computedTarget:e=>this.computedResumeTarget(e),resumesAtBreakEnd:e=>this.resumesAtBreakEnd(e),now:()=>this.deps.clock.now()}),this.pdtClock=new u({contentPlayer:this.deps.contentPlayer,diagnose:(e,t,s)=>this.deps.diagnose(e,t,s),anchorIsPdt:()=>this.state.resumeAnchorIsPdt,computedTarget:e=>this.computedResumeTarget(e),resumesAtBreakEnd:e=>this.resumesAtBreakEnd(e),now:()=>this.deps.clock.now()})}setCueEndResolver(e){this.cueEndResolver=e}ingestStreamClockCue(e){this.streamClock.ingestCue(e)}noteContentReload(){this.streamClock.noteReload()}dispose(){this.streamClock.cancelLanding(),this.pdtClock.cancelLanding()}setCueAnchorResolver(e){this.cueAnchorResolver=e}captureBreakAnchor(e){this.resetBreakResumeState();const t=this.cueAnchorResolver?.(e)??null,s=null!==t&&Number.isFinite(t),n=s?null:this.pdtBreakAnchor(e);this.state.contentPositionAtBreakStart=s?t:n??this.deps.contentPlayer.currentTime,this.state.resumeAnchorIsCue=s,this.state.resumeAnchorIsPdt=null!==n,this.state.resumeAnchorSeekableEnd=this.contentSeekableEnd(),this.streamClock.beginBreak(),this.pdtClock.beginBreak()}pdtBreakAnchor(t){if(this.state.breakTimebase!==e.WALLCLOCK||"string"!=typeof t.start)return null;const s=l(this.deps.contentPlayer);if(!s)return null;const n=a({startMs:Date.parse(t.start),durationSec:t.duration??0,sample:s});return n?(n.sampleLateSec>r&&this.deps.diagnose?.("DA-BREAK-CUT-LATE",`Break ${t.id}: content was cut ${n.sampleLateSec}s after the break start on the stream clock; the viewer saw the stream's own break until then`,{breakId:t.id,lateSec:n.sampleLateSec,start:t.start,cutPdt:new Date(s.programDateTimeMs).toISOString(),cutAt:f(s.currentTime),anchor:n.anchor}),n.anchor):null}reanchorMidBreak(e){this.state.contentPositionAtBreakStart=e,this.state.resumeAnchorIsCue=!1,this.state.resumeAnchorIsPdt=!1,this.state.resumeAnchorSeekableEnd=this.contentSeekableEnd()}liveEdgeDistanceSec(){return c(this.deps.contentPlayer)}liveSafeSeekEnd(){return m(this.deps.contentPlayer)}liveSafeSeekStart(){return p(this.deps.contentPlayer)}liveSeekableWindowSec(){return g(this.deps.contentPlayer)}contentSeekableEnd(){return d(this.deps.contentPlayer)}resumeAnchorCushionSec(){const e=this.state.resumeAnchorSeekableEnd;return null!==e&&Number.isFinite(e)?f(e-this.state.contentPositionAtBreakStart):null}isPlausibleSeekEnd(e){return h(this.deps.contentPlayer,e)}plannedResumeTarget(e){const t=this.computedResumeTarget(e);return null===t?null:this.streamClock.target(e)??this.pdtClock.target(e)??this.cueEndTarget(e)??t}computedResumeTarget(e){if(t(e))return null;const s=this.resolvePolicy(e,this.state.contentPositionAtBreakStart);return s.seek?s.target:null}resumesAtBreakEnd(e){if(t(e))return!1;const s=this.resolvePolicy(e,0);return s.seek&&Math.abs(s.offset-(e.duration??0))<.001}resolvePolicy(e,t){return s({anchor:t,duration:e.duration,adDuration:e.duration,...this.state.adInsertionType?{insertionType:this.state.adInsertionType}:{},..."number"==typeof e.resumeOffset?{resumeOffset:e.resumeOffset}:{}})}cueEndTarget(e){if(!this.state.resumeAnchorIsCue||!this.resumesAtBreakEnd(e))return null;const t=this.cueEndResolver?.(e)?.mediaTime;return"number"==typeof t&&Number.isFinite(t)?t:null}maybePrepositionContentForResume(){if("shared-element"===this.deps.getInsertionMode())return;if(!this.state.contentPaused||null!==this.state.resumePrepositionedTo)return;if(null!==this.state.resumePrepositionSettle)return;if(this.state.resumeCatchupActive)return;if(Number.isFinite(this.deps.contentPlayer.duration))return;const e=this.state.currentBreak;if(!e)return;const t=this.plannedResumeTarget(e);if(null===t)return;const s=this.contentSeekableEnd();if(null===s)return;const n=this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec,r=(e.duration??0)-n;let a;if(t<=s)a=t;else{if(!(r<=3))return;a=s}this.deps.log(a===t?`Resume pre-positioning: seeking content to ${t}s during the break (window reaches ${s}s)`:`Resume pre-positioning: window only reaches ${s}s and the ad ends in ${r}s — moving content as close to ${t}s as exists`),this.streamClock.noteSeek(a),this.streamClock.arm(),this.deps.contentPlayer.seek(a);const i=setTimeout(()=>{this.state.resumePrepositionSettle=null;const e=this.deps.contentPlayer.currentTime;Math.abs(e-a)<=S?(this.state.resumePrepositionedTo=e,this.deps.log(`Resume pre-positioned: content is at ${e}s`)):this.deps.log(`Resume pre-position rejected: asked ${a}s, landed ${e}s — window is short, will retry`)},250);this.state.resumePrepositionSettle=i}maybeCatchUpContentForResume(e){if("shared-element"===this.deps.getInsertionMode())return;if(!this.state.contentPaused||Number.isFinite(this.deps.contentPlayer.duration))return;const t=this.plannedResumeTarget(e);if(null===t)return;const s=this.deps.contentPlayer.currentTime;if(this.state.resumeCatchupActive){const n=(e.duration??0)-(this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec);if(s>t+1)return this.state.resumeCatchupActive=!1,this.deps.contentPlayer.pause(),this.state.resumePrepositionedTo=this.deps.contentPlayer.currentTime,void this.deps.log(`Resume catch-up halted: content reached ${this.deps.contentPlayer.currentTime}s, past the ${t}s target`);if(n<=30&&n>=1){this.state.resumeAlignTicks+=1;const e=s+n,r=e-t;if(Math.abs(r)>S){const a=s-r,i=this.liveSafeSeekEnd();if(null!==i){const e=i>this.state.lastResumeSafeEnd+.25;e&&Number.isFinite(this.state.lastResumeSafeEnd)&&this.state.resumeSawSteps.push(Math.round(100*(i-this.state.lastResumeSafeEnd))/100),i>this.state.lastResumeSafeEnd&&(this.state.lastResumeSafeEnd=i);const s=i-a;if(s>this.state.resumeBestSawMarginSec&&(this.state.resumeBestSawMarginSec=s),a>i)return void(this.state.resumeAlignBlockedDiagnosed||(this.state.resumeAlignBlockedDiagnosed=!0,this.deps.log(`Resume align waiting: ${a}s is past the published edge ${i}s (${Math.round(1e3*(a-i))}ms short) — rolling until a manifest step brings it in reach`),this.deps.diagnose?.("DA-RESUME-ALIGN-BLOCKED",`Resume alignment cannot reach ${t}s yet: the published edge is ${i}s`,{target:t,requested:a,safeEnd:i,shortfallSec:Math.round(1e3*(a-i))/1e3,remainingSec:n})));if(!e&&this.state.resumeSawSeekDone)return;this.state.resumeSawSeekDone=!0}if(this.state.lastResumeAlignRemainingSec-n<.5)return;this.state.lastResumeAlignRemainingSec=n,this.deps.log(`Resume align: landing ${e}s vs target ${t}s (${Math.round(1e3*r)}ms) — content to ${a}s, ${n}s of ad left`),this.state.resumeAlignCorrectionsMs.push(Math.round(1e3*r)),this.streamClock.noteSeek(a),this.deps.contentPlayer.seek(a)}}return}if(t-s<=S)return;const n=this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec,r=(e.duration??0)-n;if(r<=0)return;if(r>30)return;const a=this.contentSeekableEnd();if(null===a)return;const i=t-r+.3,o=Math.min(i,a);this.state.resumeCatchupActive=!0,this.state.resumeCatchupRan=!0,this.state.contentMutedBeforeCatchup=this.deps.contentPlayer.muted,this.deps.contentPlayer.muted=!0,this.deps.log(`Resume catch-up: content to ${o}s, playing toward ${t}s with ${r}s of ad left`),this.deps.diagnose?.("DA-RESUME-CATCHUP",`Break ${e.id}: playing content from ${o}s toward ${t}s with ${r}s of ad left`,{breakId:e.id,startPos:o,target:t,remainingAdSec:Math.round(1e3*r)/1e3,publishedEnd:a,clampedStart:o<i}),Math.abs(s-o)>S&&(this.streamClock.noteSeek(o),this.deps.contentPlayer.seek(o)),this.streamClock.arm(),this.deps.contentPlayer.play()}endResumeCatchup(e=!1){this.state.resumeCatchupActive&&(this.state.resumeCatchupActive=!1,e||this.deps.contentPlayer.pause()),null!==this.state.contentMutedBeforeCatchup&&(this.deps.contentPlayer.muted=this.state.contentMutedBeforeCatchup,this.state.contentMutedBeforeCatchup=null)}get resumeHandoverInMotion(){return this.state.resumeCatchupActive}resumeAlignSummary(){const e=this.state.resumeAlignCorrectionsMs;return{catchUpRan:this.state.resumeCatchupRan||this.state.resumeCatchupActive||e.length>0||null!==this.state.resumePrepositionedTo,alignCorrections:e.length,placedOnStep:this.state.resumeSawSeekDone,alignTicks:this.state.resumeAlignTicks,sawSteps:this.state.resumeSawSteps.slice(0,12),sawStepCount:this.state.resumeSawSteps.length,bestSawMarginSec:Number.isFinite(this.state.resumeBestSawMarginSec)?Math.round(1e3*this.state.resumeBestSawMarginSec)/1e3:null,alignCorrectionsMs:e.slice(0,12),alignLastErrorMs:e.length?e[e.length-1]:null}}cancelResumeLandedProbe(){const e=this.state.resumeLandedProbe;this.state.resumeLandedProbe=null,e?.()}probeResumeLanding(e,t,s=null){this.cancelResumeLandedProbe();let n=!1;const r=(r,a)=>{if(n)return;n=!0,this.cancelResumeLandedProbe();const i=null===r?null:Math.round(1e3*(r-t))/1e3,o=null===r||null===s?null:Math.round(1e3*(r-s))/1e3,u=null!==s&&Math.abs(s-t)>=5e-4&&null!==o?`; ${Math.abs(o)}s ${o<0?"before":"after"} the ${s}s resume point`:"";this.deps.log(null===r?`Resume landing unknown for break ${e} (${a})`:`Resume landed at ${r}s, ${i}s from the ${t}s asked for${u} (break ${e})`),this.deps.diagnose?.("DA-RESUME-LANDED",null===r?`Resume landing for break ${e} could not be measured (${a})`:`Resume for break ${e} landed at ${r}s, ${i}s from the ${t}s requested${u}`,{breakId:e,target:t,landed:r,errorSec:i,...null===s?{}:{policyTarget:s,policyErrorSec:o},...a?{reason:a}:{}})},a=()=>{this.streamClock.updateLanding(e,this.deps.contentPlayer.currentTime),r(this.deps.contentPlayer.currentTime)},i=setTimeout(()=>r(null,"no seeked event"),2e3);this.state.resumeLandedProbe=()=>{clearTimeout(i),this.deps.contentPlayer.off("seeked",a)},this.deps.contentPlayer.on("seeked",a)}async waitForLiveEdge(e=()=>!0){for(let t=0;t<20;t++){if(!e())return null;const t=this.deps.contentPlayer.videoElement,s=t?.seekable;if(s&&s.length>0){const e=s.end(s.length-1);if(Number.isFinite(e)&&e>0)return e}if(await new Promise(e=>setTimeout(e,100)),!e())return null}return this.deps.log("shared-element: no seekable range after restore; staying at the live edge"),null}resetBreakResumeState(){this.state.resumeAnchorIsCue=!1,this.state.resumeAnchorIsPdt=!1,this.state.resumeAnchorSeekableEnd=null,this.state.resumePrepositionedTo=null,this.state.resumeCatchupActive=!1,this.state.resumeCatchupRan=!1,this.state.contentMutedBeforeCatchup=null,this.state.resumeAlignCorrectionsMs=[],this.state.lastResumeAlignRemainingSec=Number.POSITIVE_INFINITY,this.state.resumeAlignBlockedDiagnosed=!1,this.state.lastResumeSafeEnd=Number.NEGATIVE_INFINITY,this.state.resumeSawSeekDone=!1,this.state.resumeBestSawMarginSec=Number.NEGATIVE_INFINITY,this.state.resumeAlignTicks=0,this.state.resumeSawSteps=[],this.state.resumePrepositionSettle&&(clearTimeout(this.state.resumePrepositionSettle),this.state.resumePrepositionSettle=null),this.state.adSecondsPlayedThisBreak=0,this.state.currentAssetElapsedSec=0}beginContinueContentPlayout(){this.continuePlayout.beginContinueContentPlayout()}distanceToLiveEdgeSec(){const e=this.contentSeekableEnd();return null===e?null:f(e-this.deps.contentPlayer.currentTime)}secondsUntilCut(e){return this.continuePlayout.secondsUntilCut(e)}restoreContinueQuality(e){this.continuePlayout.restoreContinueQuality(e)}currentContinueDriftSec(){return this.continuePlayout.currentContinueDriftSec()}abandonContinueContentPlayout(e){this.continuePlayout.abandonContinueContentPlayout(e)}endContinueContentPlayout(){this.continuePlayout.endContinueContentPlayout()}setContinueRate(e){this.continuePlayout.setContinueRate(e)}tickContinueContentPlayout(){this.continuePlayout.tickContinueContentPlayout()}resolveResumeTarget(t,r=null){this.resumePolicyTarget=null;const a=null!==r,i=(this.state.resumeAnchorIsCue||this.state.resumeAnchorIsPdt)&&!a,o=a?r:i?t.duration:this.state.adSecondsPlayedThisBreak,u=s({anchor:this.state.contentPositionAtBreakStart,duration:t.duration,adDuration:o,...a?{}:{insertionType:this.state.adInsertionType},...a?{resumeOffset:r}:"number"==typeof t.resumeOffset?{resumeOffset:t.resumeOffset}:{}});if(u.clamped&&this.deps.diagnose?.("DA-RESUME-OFFSET-CLAMPED",`Resume offset shortened to the ${u.offset}s of ad actually played for break ${t.id}`,{breakId:t.id,offset:u.offset,source:u.source}),!u.seek)return this.deps.log(`Resume: no seek for break ${t.id} (offset 0, ${u.source})`),null;const l=this.streamClock.handBackTarget(t,a)??this.pdtClock.handBackTarget(t,a),d=null!==l||a?null:this.reportCueEnd(t,u.target),h=l??d??u.target;this.resumePolicyTarget=h;const c=this.streamClock.seekBasis(t)??this.pdtClock.seekBasis()??(null!==d?"cue_end":`offset ${u.offset}s, ${u.source}`);this.deps.log(`Resume-seek to ${h}s (${c}) for break ${t.id}`);const m=this.resumeAlignSummary(),p=this.liveSafeSeekEnd();this.deps.diagnose?.("DA-RESUME-SEEK",`Resume-seek to ${h}s (${c}) for break ${t.id}`,{breakId:t.id,target:h,offset:u.offset,source:u.source,...h===u.target?{}:{computedTarget:u.target},...null===d?{}:{cueEndTarget:d},...this.streamClock.seekContext(t,h,u.target),...this.pdtClock.seekContext(t,h,u.target),anchorPosition:this.state.contentPositionAtBreakStart,anchorIsCue:this.state.resumeAnchorIsCue,anchorIsPdt:this.state.resumeAnchorIsPdt,anchorSeekableEnd:this.state.resumeAnchorSeekableEnd,anchorCushionSec:this.resumeAnchorCushionSec(),safeEnd:p,seekableEndNow:this.contentSeekableEnd(),liveEdge:this.deps.contentPlayer.liveEdge??null,unreachableEdgeSec:k(this.deps.contentPlayer),...m.catchUpRan?m:{}}),this.streamClock.reportCorrection(t,h,u.target,a),this.pdtClock.reportCorrection(t,h,u.target,a);(this.state.breakTimebase===e.PTS||this.state.breakTimebase===e.MEDIATIME)&&"number"==typeof t.start&&this.state.callbacks?.onContentResumeSeek?.(h,t.start+t.duration);const g=this.liveSafeSeekStart(),f=n({target:h,safeStart:g,safeEnd:p,windowSec:this.liveSeekableWindowSec()}),{target:C,shortfallSec:P,bound:b}=f;if(f.startClampSkipped){const{behindSec:e,windowSec:s,maxBehindSec:n}=f.startClampSkipped;this.deps.log(`Resume-seek: ${h}s is ${e}s behind the element's live window start (window ${s}s); not clamping`),this.deps.diagnose?.("DA-RESUME-START-CLAMP-SKIPPED",`Resume target ${h}s for break ${t.id} is ${e}s behind the element's seekable start but the gap exceeds half the ${s}s window; left unchanged`,{breakId:t.id,requested:h,seekableStart:g,behindSec:e,windowSec:s,maxBehindSec:n,...this.resumeAlignSummary()})}if(null!==this.state.resumePrepositionedTo&&Math.abs(this.deps.contentPlayer.currentTime-C)<=S){const e=this.deps.contentPlayer.currentTime;return this.deps.log(`Resume: content already at ${e}s for break ${t.id} (pre-positioned during the break)`+(P>0?` — ${P}s short of policy, those frames are not published yet`:"")+" — no seek"),this.deps.diagnose?.("DA-RESUME-PREPOSITIONED",`Break ${t.id} resumed without a seek: content was moved to ${e}s during the break`+(P>0?` (${P}s short of the ${h}s policy target)`:""),{breakId:t.id,target:h,prepositionedTo:this.state.resumePrepositionedTo,landed:e,shortfallSec:P,exact:0===P,...this.resumeAlignSummary()}),this.streamClock.armLanding(t,e),this.pdtClock.armLanding(t),null}return P>0&&(this.deps.log("start"===b?`Resume-seek clamped: ${h}s is behind the live window start (already evicted); seeking ${C}s instead (${P}s early)`:`Resume-seek clamped: ${h}s is beyond what the stream has published; seeking ${C}s instead (${P}s short)`),this.deps.diagnose?.("DA-RESUME-TARGET-CLAMPED","start"===b?`Resume target ${h}s for break ${t.id} is behind the live window start (already evicted); clamped to ${C}s`:`Resume target ${h}s for break ${t.id} is beyond the published window; clamped to ${C}s`,{breakId:t.id,requested:h,clamped:C,shortfallSec:P,bound:b,...this.resumeAlignSummary()})),this.streamClock.armLanding(t,C),this.pdtClock.armLanding(t),C}reportCueEnd(e,t){const s=this.state.resumeAnchorIsCue?this.cueEndResolver?.(e):null;if(!s||!Number.isFinite(s.mediaTime))return null;const n=this.resumesAtBreakEnd(e),r=f(s.mediaTime-t);return this.deps.diagnose("DA-RESUME-CUE-END",n?`Break ${e.id}: resuming on the stream's cue_end at ${s.mediaTime}s (${r}s from the computed ${t}s)`:`Break ${e.id}: cue_end at ${s.mediaTime}s not used; the resume policy does not resume at the break end`,{breakId:e.id,computedTarget:t,cueEndTarget:s.mediaTime,deltaSec:r,applied:n,stage:"hand-back",corrected:!1}),n?s.mediaTime:null}applyResumeSeek(e,t=null){const s=this.resolveResumeTarget(e,t);null!==s&&(this.probeResumeLanding(e.id,s,this.resumePolicyTarget),this.streamClock.noteSeek(s),this.deps.contentPlayer.seek(s))}}
1
+ import{Timebase as e,isPostRollBreak as t}from"../../types/BreakManifest";import{resolveResumePoint as s}from"../resolveResumePoint";import{resolveResumeClamp as n}from"../resolveResumeClamp";import{BREAK_CUT_LATE_TOLERANCE_SEC as r,resolveWallclockBreakWindow as i}from"../wallclockBreakWindow";import{ContinueContentPlayout as a}from"./ContinueContentPlayout";import{StreamClockResume as o}from"./streamClockResume";import{PdtClockResume as u,programDateTimeSample as l}from"./pdtClockResume";import{contentSeekableEnd as d,isPlausibleSeekEnd as h,liveEdgeDistanceSec as c,liveSafeSeekEnd as m,liveSafeSeekStart as p,liveSeekableWindowSec as k,unreachableEdgeSec as g}from"./liveEdge";const S=.04,f=e=>Math.round(1e3*e)/1e3;export class ContentResumeCoordinator{constructor(e,t){this.state=e,this.deps=t,this.cueAnchorResolver=null,this.resumePolicyTarget=null,this.cueEndResolver=null,this.continuePlayout=new a(this.state,{contentPlayer:this.deps.contentPlayer,clock:this.deps.clock,gateContentForBreak:()=>this.deps.gateContentForBreak(),distanceToLiveEdgeSec:()=>this.distanceToLiveEdgeSec(),plannedResumeTarget:e=>this.plannedResumeTarget(e),diagnose:(e,t,s)=>this.deps.diagnose(e,t,s),log:(...e)=>this.deps.log(...e)}),this.streamClock=new o({contentPlayer:this.deps.contentPlayer,diagnose:(e,t,s)=>this.deps.diagnose(e,t,s),activeBreak:()=>this.state.currentBreak,anchorIsCue:()=>this.state.resumeAnchorIsCue,breakEnd:e=>this.cueEndResolver?.(e)??null,computedTarget:e=>this.computedResumeTarget(e),resumesAtBreakEnd:e=>this.resumesAtBreakEnd(e),now:()=>this.deps.clock.now()}),this.pdtClock=new u({contentPlayer:this.deps.contentPlayer,diagnose:(e,t,s)=>this.deps.diagnose(e,t,s),anchorIsPdt:()=>this.state.resumeAnchorIsPdt,computedTarget:e=>this.computedResumeTarget(e),resumesAtBreakEnd:e=>this.resumesAtBreakEnd(e),now:()=>this.deps.clock.now()})}setCueEndResolver(e){this.cueEndResolver=e}ingestStreamClockCue(e){this.streamClock.ingestCue(e)}noteContentReload(){this.streamClock.noteReload()}dispose(){this.streamClock.cancelLanding(),this.pdtClock.cancelLanding()}setCueAnchorResolver(e){this.cueAnchorResolver=e}captureBreakAnchor(e){this.resetBreakResumeState();const t=this.cueAnchorResolver?.(e)??null,s=null!==t&&Number.isFinite(t),n=s?null:this.pdtBreakAnchor(e);this.state.contentPositionAtBreakStart=s?t:n??this.deps.contentPlayer.currentTime,this.state.resumeAnchorIsCue=s,this.state.resumeAnchorIsPdt=null!==n,this.state.resumeAnchorSeekableEnd=this.contentSeekableEnd(),this.streamClock.beginBreak(),this.pdtClock.beginBreak()}pdtBreakAnchor(t){if(this.state.breakTimebase!==e.WALLCLOCK||"string"!=typeof t.start)return null;const s=l(this.deps.contentPlayer);if(!s)return null;const n=i({startMs:Date.parse(t.start),durationSec:t.duration??0,sample:s});return n?(n.sampleLateSec>r&&this.deps.diagnose?.("DA-BREAK-CUT-LATE",`Break ${t.id}: content was cut ${n.sampleLateSec}s after the break start on the stream clock; the viewer saw the stream's own break until then`,{breakId:t.id,lateSec:n.sampleLateSec,start:t.start,cutPdt:new Date(s.programDateTimeMs).toISOString(),cutAt:f(s.currentTime),anchor:n.anchor}),n.anchor):null}reanchorMidBreak(e){this.state.contentPositionAtBreakStart=e,this.state.resumeAnchorIsCue=!1,this.state.resumeAnchorIsPdt=!1,this.state.resumeAnchorSeekableEnd=this.contentSeekableEnd()}liveEdgeDistanceSec(){return c(this.deps.contentPlayer)}liveSafeSeekEnd(){return m(this.deps.contentPlayer)}liveSafeSeekStart(){return p(this.deps.contentPlayer)}liveSeekableWindowSec(){return k(this.deps.contentPlayer)}contentSeekableEnd(){return d(this.deps.contentPlayer)}resumeAnchorCushionSec(){const e=this.state.resumeAnchorSeekableEnd;return null!==e&&Number.isFinite(e)?f(e-this.state.contentPositionAtBreakStart):null}isPlausibleSeekEnd(e){return h(this.deps.contentPlayer,e)}plannedResumeTarget(e){const t=this.computedResumeTarget(e);return null===t?null:this.streamClock.target(e)??this.pdtClock.target(e)??this.cueEndTarget(e)??t}computedResumeTarget(e){if(t(e))return null;const s=this.resolvePolicy(e,this.state.contentPositionAtBreakStart);return s.seek?s.target:null}resumesAtBreakEnd(e){if(t(e))return!1;const s=this.resolvePolicy(e,0);return s.seek&&Math.abs(s.offset-(e.duration??0))<.001}resolvePolicy(e,t){return s({anchor:t,duration:e.duration,adDuration:e.duration,seekPosition:this.state.snapbackResumePosition,...this.state.adInsertionType?{insertionType:this.state.adInsertionType}:{},..."number"==typeof e.resumeOffset?{resumeOffset:e.resumeOffset}:{}})}cueEndTarget(e){if(!this.state.resumeAnchorIsCue||!this.resumesAtBreakEnd(e))return null;const t=this.cueEndResolver?.(e)?.mediaTime;return"number"==typeof t&&Number.isFinite(t)?t:null}maybePrepositionContentForResume(){if("shared-element"===this.deps.getInsertionMode())return;if(!this.state.contentPaused||null!==this.state.resumePrepositionedTo)return;if(null!==this.state.resumePrepositionSettle)return;if(this.state.resumeCatchupActive)return;if(Number.isFinite(this.deps.contentPlayer.duration))return;const e=this.state.currentBreak;if(!e)return;const t=this.plannedResumeTarget(e);if(null===t)return;const s=this.contentSeekableEnd();if(null===s)return;const n=this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec,r=(e.duration??0)-n;let i;if(t<=s)i=t;else{if(!(r<=3))return;i=s}this.deps.log(i===t?`Resume pre-positioning: seeking content to ${t}s during the break (window reaches ${s}s)`:`Resume pre-positioning: window only reaches ${s}s and the ad ends in ${r}s — moving content as close to ${t}s as exists`),this.streamClock.noteSeek(i),this.streamClock.arm(),this.deps.contentPlayer.seek(i);const a=setTimeout(()=>{this.state.resumePrepositionSettle=null;const e=this.deps.contentPlayer.currentTime;Math.abs(e-i)<=S?(this.state.resumePrepositionedTo=e,this.deps.log(`Resume pre-positioned: content is at ${e}s`)):this.deps.log(`Resume pre-position rejected: asked ${i}s, landed ${e}s — window is short, will retry`)},250);this.state.resumePrepositionSettle=a}maybeCatchUpContentForResume(e){if("shared-element"===this.deps.getInsertionMode())return;if(!this.state.contentPaused||Number.isFinite(this.deps.contentPlayer.duration))return;const t=this.plannedResumeTarget(e);if(null===t)return;const s=this.deps.contentPlayer.currentTime;if(this.state.resumeCatchupActive){const n=(e.duration??0)-(this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec);if(s>t+1)return this.state.resumeCatchupActive=!1,this.deps.contentPlayer.pause(),this.state.resumePrepositionedTo=this.deps.contentPlayer.currentTime,void this.deps.log(`Resume catch-up halted: content reached ${this.deps.contentPlayer.currentTime}s, past the ${t}s target`);if(n<=30&&n>=1){this.state.resumeAlignTicks+=1;const e=s+n,r=e-t;if(Math.abs(r)>S){const i=s-r,a=this.liveSafeSeekEnd();if(null!==a){const e=a>this.state.lastResumeSafeEnd+.25;e&&Number.isFinite(this.state.lastResumeSafeEnd)&&this.state.resumeSawSteps.push(Math.round(100*(a-this.state.lastResumeSafeEnd))/100),a>this.state.lastResumeSafeEnd&&(this.state.lastResumeSafeEnd=a);const s=a-i;if(s>this.state.resumeBestSawMarginSec&&(this.state.resumeBestSawMarginSec=s),i>a)return void(this.state.resumeAlignBlockedDiagnosed||(this.state.resumeAlignBlockedDiagnosed=!0,this.deps.log(`Resume align waiting: ${i}s is past the published edge ${a}s (${Math.round(1e3*(i-a))}ms short) — rolling until a manifest step brings it in reach`),this.deps.diagnose?.("DA-RESUME-ALIGN-BLOCKED",`Resume alignment cannot reach ${t}s yet: the published edge is ${a}s`,{target:t,requested:i,safeEnd:a,shortfallSec:Math.round(1e3*(i-a))/1e3,remainingSec:n})));if(!e&&this.state.resumeSawSeekDone)return;this.state.resumeSawSeekDone=!0}if(this.state.lastResumeAlignRemainingSec-n<.5)return;this.state.lastResumeAlignRemainingSec=n,this.deps.log(`Resume align: landing ${e}s vs target ${t}s (${Math.round(1e3*r)}ms) — content to ${i}s, ${n}s of ad left`),this.state.resumeAlignCorrectionsMs.push(Math.round(1e3*r)),this.streamClock.noteSeek(i),this.deps.contentPlayer.seek(i)}}return}if(t-s<=S)return;const n=this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec,r=(e.duration??0)-n;if(r<=0)return;if(r>30)return;const i=this.contentSeekableEnd();if(null===i)return;const a=t-r+.3,o=Math.min(a,i);this.state.resumeCatchupActive=!0,this.state.resumeCatchupRan=!0,this.deps.muteContent(),this.deps.log(`Resume catch-up: content to ${o}s, playing toward ${t}s with ${r}s of ad left`),this.deps.diagnose?.("DA-RESUME-CATCHUP",`Break ${e.id}: playing content from ${o}s toward ${t}s with ${r}s of ad left`,{breakId:e.id,startPos:o,target:t,remainingAdSec:Math.round(1e3*r)/1e3,publishedEnd:i,clampedStart:o<a}),Math.abs(s-o)>S&&(this.streamClock.noteSeek(o),this.deps.contentPlayer.seek(o)),this.streamClock.arm(),this.deps.contentPlayer.play()}endResumeCatchup(e=!1){this.state.resumeCatchupActive&&(this.state.resumeCatchupActive=!1,e||this.deps.contentPlayer.pause())}get resumeHandoverInMotion(){return this.state.resumeCatchupActive}resumeAlignSummary(){const e=this.state.resumeAlignCorrectionsMs;return{catchUpRan:this.state.resumeCatchupRan||this.state.resumeCatchupActive||e.length>0||null!==this.state.resumePrepositionedTo,alignCorrections:e.length,placedOnStep:this.state.resumeSawSeekDone,alignTicks:this.state.resumeAlignTicks,sawSteps:this.state.resumeSawSteps.slice(0,12),sawStepCount:this.state.resumeSawSteps.length,bestSawMarginSec:Number.isFinite(this.state.resumeBestSawMarginSec)?Math.round(1e3*this.state.resumeBestSawMarginSec)/1e3:null,alignCorrectionsMs:e.slice(0,12),alignLastErrorMs:e.length?e[e.length-1]:null}}cancelResumeLandedProbe(){const e=this.state.resumeLandedProbe;this.state.resumeLandedProbe=null,e?.()}probeResumeLanding(e,t,s=null){this.cancelResumeLandedProbe();let n=!1;const r=(r,i)=>{if(n)return;n=!0,this.cancelResumeLandedProbe();const a=null===r?null:Math.round(1e3*(r-t))/1e3,o=null===r||null===s?null:Math.round(1e3*(r-s))/1e3,u=null!==s&&Math.abs(s-t)>=5e-4&&null!==o?`; ${Math.abs(o)}s ${o<0?"before":"after"} the ${s}s resume point`:"";this.deps.log(null===r?`Resume landing unknown for break ${e} (${i})`:`Resume landed at ${r}s, ${a}s from the ${t}s asked for${u} (break ${e})`),this.deps.diagnose?.("DA-RESUME-LANDED",null===r?`Resume landing for break ${e} could not be measured (${i})`:`Resume for break ${e} landed at ${r}s, ${a}s from the ${t}s requested${u}`,{breakId:e,target:t,landed:r,errorSec:a,...null===s?{}:{policyTarget:s,policyErrorSec:o},...i?{reason:i}:{}})},i=()=>{this.streamClock.updateLanding(e,this.deps.contentPlayer.currentTime),r(this.deps.contentPlayer.currentTime)},a=setTimeout(()=>r(null,"no seeked event"),2e3);this.state.resumeLandedProbe=()=>{clearTimeout(a),this.deps.contentPlayer.off("seeked",i)},this.deps.contentPlayer.on("seeked",i)}async waitForLiveEdge(e=()=>!0){for(let t=0;t<20;t++){if(!e())return null;const t=this.deps.contentPlayer.videoElement,s=t?.seekable;if(s&&s.length>0){const e=s.end(s.length-1);if(Number.isFinite(e)&&e>0)return e}if(await new Promise(e=>setTimeout(e,100)),!e())return null}return this.deps.log("shared-element: no seekable range after restore; staying at the live edge"),null}resetBreakResumeState(){this.state.resumeAnchorIsCue=!1,this.state.resumeAnchorIsPdt=!1,this.state.resumeAnchorSeekableEnd=null,this.state.resumePrepositionedTo=null,this.state.resumeCatchupActive=!1,this.state.resumeCatchupRan=!1,this.state.resumeAlignCorrectionsMs=[],this.state.lastResumeAlignRemainingSec=Number.POSITIVE_INFINITY,this.state.resumeAlignBlockedDiagnosed=!1,this.state.lastResumeSafeEnd=Number.NEGATIVE_INFINITY,this.state.resumeSawSeekDone=!1,this.state.resumeBestSawMarginSec=Number.NEGATIVE_INFINITY,this.state.resumeAlignTicks=0,this.state.resumeSawSteps=[],this.state.resumePrepositionSettle&&(clearTimeout(this.state.resumePrepositionSettle),this.state.resumePrepositionSettle=null),this.state.adSecondsPlayedThisBreak=0,this.state.currentAssetElapsedSec=0}beginContinueContentPlayout(){this.continuePlayout.beginContinueContentPlayout()}distanceToLiveEdgeSec(){const e=this.contentSeekableEnd();return null===e?null:f(e-this.deps.contentPlayer.currentTime)}secondsUntilCut(e){return this.continuePlayout.secondsUntilCut(e)}restoreContinueQuality(e){this.continuePlayout.restoreContinueQuality(e)}currentContinueDriftSec(){return this.continuePlayout.currentContinueDriftSec()}abandonContinueContentPlayout(e){this.continuePlayout.abandonContinueContentPlayout(e)}endContinueContentPlayout(){this.continuePlayout.endContinueContentPlayout()}setContinueRate(e){this.continuePlayout.setContinueRate(e)}tickContinueContentPlayout(){this.continuePlayout.tickContinueContentPlayout()}resolveResumeTarget(t,r=null){this.resumePolicyTarget=null;const i=null!==r,a=(this.state.resumeAnchorIsCue||this.state.resumeAnchorIsPdt)&&!i,o=i?r:a?t.duration:this.state.adSecondsPlayedThisBreak,u=s({anchor:this.state.contentPositionAtBreakStart,duration:t.duration,adDuration:o,seekPosition:this.state.snapbackResumePosition,...i?{}:{insertionType:this.state.adInsertionType},...i?{resumeOffset:r}:"number"==typeof t.resumeOffset?{resumeOffset:t.resumeOffset}:{}});if(u.clamped&&this.deps.diagnose?.("DA-RESUME-OFFSET-CLAMPED",`Resume offset shortened to the ${u.offset}s of ad actually played for break ${t.id}`,{breakId:t.id,offset:u.offset,source:u.source}),!u.seek)return this.deps.log(`Resume: no seek for break ${t.id} (offset 0, ${u.source})`),null;const l=this.streamClock.handBackTarget(t,i)??this.pdtClock.handBackTarget(t,i),d=null!==l||i?null:this.reportCueEnd(t,u.target),h=l??d??u.target;this.resumePolicyTarget=h;const c=this.streamClock.seekBasis(t)??this.pdtClock.seekBasis()??(null!==d?"cue_end":`offset ${u.offset}s, ${u.source}`);this.deps.log(`Resume-seek to ${h}s (${c}) for break ${t.id}`);const m=this.resumeAlignSummary(),p=this.liveSafeSeekEnd();this.deps.diagnose?.("DA-RESUME-SEEK",`Resume-seek to ${h}s (${c}) for break ${t.id}`,{breakId:t.id,target:h,offset:u.offset,source:u.source,...h===u.target?{}:{computedTarget:u.target},...null===d?{}:{cueEndTarget:d},...this.streamClock.seekContext(t,h,u.target),...this.pdtClock.seekContext(t,h,u.target),anchorPosition:this.state.contentPositionAtBreakStart,anchorIsCue:this.state.resumeAnchorIsCue,anchorIsPdt:this.state.resumeAnchorIsPdt,anchorSeekableEnd:this.state.resumeAnchorSeekableEnd,anchorCushionSec:this.resumeAnchorCushionSec(),safeEnd:p,seekableEndNow:this.contentSeekableEnd(),liveEdge:this.deps.contentPlayer.liveEdge??null,unreachableEdgeSec:g(this.deps.contentPlayer),...m.catchUpRan?m:{}}),this.streamClock.reportCorrection(t,h,u.target,i),this.pdtClock.reportCorrection(t,h,u.target,i);(this.state.breakTimebase===e.PTS||this.state.breakTimebase===e.MEDIATIME)&&"number"==typeof t.start&&this.state.callbacks?.onContentResumeSeek?.(h,t.start+t.duration);const k=this.liveSafeSeekStart(),f=n({target:h,safeStart:k,safeEnd:p,windowSec:this.liveSeekableWindowSec()}),{target:C,shortfallSec:P,bound:b}=f;if(f.startClampSkipped){const{behindSec:e,windowSec:s,maxBehindSec:n}=f.startClampSkipped;this.deps.log(`Resume-seek: ${h}s is ${e}s behind the element's live window start (window ${s}s); not clamping`),this.deps.diagnose?.("DA-RESUME-START-CLAMP-SKIPPED",`Resume target ${h}s for break ${t.id} is ${e}s behind the element's seekable start but the gap exceeds half the ${s}s window; left unchanged`,{breakId:t.id,requested:h,seekableStart:k,behindSec:e,windowSec:s,maxBehindSec:n,...this.resumeAlignSummary()})}if(null!==this.state.resumePrepositionedTo&&Math.abs(this.deps.contentPlayer.currentTime-C)<=S){const e=this.deps.contentPlayer.currentTime;return this.deps.log(`Resume: content already at ${e}s for break ${t.id} (pre-positioned during the break)`+(P>0?` — ${P}s short of policy, those frames are not published yet`:"")+" — no seek"),this.deps.diagnose?.("DA-RESUME-PREPOSITIONED",`Break ${t.id} resumed without a seek: content was moved to ${e}s during the break`+(P>0?` (${P}s short of the ${h}s policy target)`:""),{breakId:t.id,target:h,prepositionedTo:this.state.resumePrepositionedTo,landed:e,shortfallSec:P,exact:0===P,...this.resumeAlignSummary()}),this.streamClock.armLanding(t,e),this.pdtClock.armLanding(t),null}return P>0&&(this.deps.log("start"===b?`Resume-seek clamped: ${h}s is behind the live window start (already evicted); seeking ${C}s instead (${P}s early)`:`Resume-seek clamped: ${h}s is beyond what the stream has published; seeking ${C}s instead (${P}s short)`),this.deps.diagnose?.("DA-RESUME-TARGET-CLAMPED","start"===b?`Resume target ${h}s for break ${t.id} is behind the live window start (already evicted); clamped to ${C}s`:`Resume target ${h}s for break ${t.id} is beyond the published window; clamped to ${C}s`,{breakId:t.id,requested:h,clamped:C,shortfallSec:P,bound:b,...this.resumeAlignSummary()})),this.streamClock.armLanding(t,C),this.pdtClock.armLanding(t),C}reportCueEnd(e,t){const s=this.state.resumeAnchorIsCue?this.cueEndResolver?.(e):null;if(!s||!Number.isFinite(s.mediaTime))return null;const n=this.resumesAtBreakEnd(e),r=f(s.mediaTime-t);return this.deps.diagnose("DA-RESUME-CUE-END",n?`Break ${e.id}: resuming on the stream's cue_end at ${s.mediaTime}s (${r}s from the computed ${t}s)`:`Break ${e.id}: cue_end at ${s.mediaTime}s not used; the resume policy does not resume at the break end`,{breakId:e.id,computedTarget:t,cueEndTarget:s.mediaTime,deltaSec:r,applied:n,stage:"hand-back",corrected:!1}),n?s.mediaTime:null}applyResumeSeek(e,t=null){const s=this.resolveResumeTarget(e,t);null!==s&&(this.probeResumeLanding(e.id,s,this.resumePolicyTarget),this.streamClock.noteSeek(s),this.deps.contentPlayer.seek(s))}}
@@ -2,9 +2,12 @@ import type { PlayerAdapter, TimedMetadataCue } from '../../interfaces/PlayerAda
2
2
  import type { Asset, Break } from '../../types/BreakManifest';
3
3
  import type { AdPlayerCallbacks } from '../AdPlayerController';
4
4
  import type { AdPlayerState } from './AdPlayerState';
5
+ import type { SurfaceRevealReporter } from './SurfaceRevealReporter';
5
6
  export interface SharedElementBreakPlayerDeps {
6
7
  contentPlayer: PlayerAdapter;
7
8
  getAdPlayer: () => PlayerAdapter | null;
9
+ /** Watches for the ad's first frame on the content element (`DA-AD-SURFACE-REVEALED`). */
10
+ surfaceReveal?: SurfaceRevealReporter;
8
11
  resolveAssetUri: (asset: Asset, duration: number) => Promise<string | null>;
9
12
  liveEdgeDistanceSec: () => number | null;
10
13
  rebindContentElementIfSwapped: () => void;
@@ -1 +1 @@
1
- import{BreakFormat as e,isPostRollBreak as t}from"../../types/BreakManifest";import{armPlaybackCutoff as s,clearPlaybackCutoff as a}from"./PlaybackCutoff";import{AdError as n}from"../../types/AdError";import{classifyMediaError as r,toClassifiedAdError as i}from"./classifyMediaError";function d(e){return e?{src:e.getAttribute("src"),srcObject:e.srcObject,sourceChild:null!==e.querySelector("source")}:null}export class SharedElementBreakPlayer{constructor(e,t){this.state=e,this.deps=t,this.contentElementAdPlayer=null,this.sharedAdSource=null,this.sharedAdEndedHandler=null,this.sharedAdTimeUpdateHandler=null,this.sharedAdTimedMetadataHandler=null,this.sharedAdWaitingHandler=null,this.sharedAdPlayingHandler=null,this.sharedAdFirstFrameTimer=null,this.sharedRestoreWatchdog=null,this.sharedRestore=null,this.sharedRestoreGeneration=0,this.sharedActivityGeneration=0}sharedElementAdPlayer(){const e=this.deps.contentPlayer.videoElement;if(!e)return null;if(this.contentElementAdPlayer?.videoElement===e)return this.contentElementAdPlayer;this.releaseContentElementAdPlayer();const t=this.deps.createAdAdapter(document.createElement("div"),e);return t!==this.deps.contentPlayer&&t.prepareForAdPlayback?.(),t.videoElement!==e?(this.deps.log("shared-element: createAdAdapter ignored the content element; falling back to the content player for ad playback"),t.destroy(),null):(this.contentElementAdPlayer=t,t)}async sharedAdStep(e,t,s){try{await s()}catch(s){const a=t.videoElement,i=s instanceof Error?s.message:String(s),d=new n(`shared-element ${e} failed: ${i} (readyState=${a?.readyState??"-"} paused=${a?.paused??"-"} networkState=${a?.networkState??"-"})`,r(s));throw d.cause=s,d}}clearSharedAdFirstFrameWatchdog(){this.sharedAdFirstFrameTimer&&(clearTimeout(this.sharedAdFirstFrameTimer),this.sharedAdFirstFrameTimer=null)}armSharedAdFirstFrameWatchdog(e,t){this.clearSharedAdFirstFrameWatchdog(),this.sharedAdFirstFrameTimer=setTimeout(()=>{if(this.sharedAdFirstFrameTimer=null,!this.state.isPlaying||!this.state.currentBreak)return;const s=e.videoElement,a=this.deps.contentPlayer.videoElement,r=s?.currentSrc??"",i=`readyState=${s?.readyState??"-"} networkState=${s?.networkState??"-"} paused=${s?.paused??"-"} connected=${s?.isConnected??"-"} srcKind=${r.startsWith("blob:")?"blob":r?"url":"none"} sameElementAsContentPlayer=${String(s===a)}`;this.deps.log(`shared-element: no first frame within ${t}ms (${i})`),this.deps.diagnose("DA-AD-PLAYBACK-ERROR",`the ad never rendered a frame on the content element within ${t}ms`,{breakId:this.state.currentBreak.id,detail:i});const d=this.state.currentAsset;d&&this.state.callbacks?.onAdError(this.state.currentBreak,d,new n(`shared-element ad produced no frames within ${t}ms (${i})`,"DA-AD-START-TIMEOUT")),this.onSharedElementAdEnded()},t)}attachSharedAdListeners(e){this.sharedAdSource=e,this.sharedAdEndedHandler=()=>this.onSharedElementAdEnded(),e.on("ended",this.sharedAdEndedHandler),this.sharedAdTimeUpdateHandler=()=>{this.sharedAdFirstFrameTimer&&e.currentTime>0&&this.clearSharedAdFirstFrameWatchdog(),this.deps.reportAdProgress(e)},e.on("timeupdate",this.sharedAdTimeUpdateHandler),this.sharedAdTimedMetadataHandler=e=>{e&&this.deps.processTimedMetadata(e)},e.on("timedmetadata",this.sharedAdTimedMetadataHandler),this.sharedAdWaitingHandler=()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&this.state.callbacks?.onAdWaiting(this.state.currentBreak,this.state.currentAsset)},e.on("waiting",this.sharedAdWaitingHandler),this.sharedAdPlayingHandler=()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&!e.paused&&(this.clearSharedAdFirstFrameWatchdog(),this.state.startPlaybackCutoff?.(),this.state.callbacks?.onAdPlaying(this.state.currentBreak,this.state.currentAsset))},e.on("playing",this.sharedAdPlayingHandler)}detachSharedAdListeners(){const e=this.sharedAdSource??this.deps.contentPlayer;this.sharedAdSource=null,this.state.sharedAdClaimActive=!1,this.clearSharedAdFirstFrameWatchdog(),this.sharedAdTimedMetadataHandler&&(e.off("timedmetadata",this.sharedAdTimedMetadataHandler),this.sharedAdTimedMetadataHandler=null),this.sharedAdWaitingHandler&&(e.off("waiting",this.sharedAdWaitingHandler),this.sharedAdWaitingHandler=null),this.sharedAdPlayingHandler&&(e.off("playing",this.sharedAdPlayingHandler),this.sharedAdPlayingHandler=null),this.sharedAdTimeUpdateHandler&&(e.off("timeupdate",this.sharedAdTimeUpdateHandler),this.sharedAdTimeUpdateHandler=null),this.sharedAdEndedHandler&&(e.off("ended",this.sharedAdEndedHandler),this.sharedAdEndedHandler=null)}async releaseContentElementFromContentEngine(){if(this.deps.contentPlayer.releaseMediaElement)try{await this.deps.contentPlayer.releaseMediaElement()}catch(e){this.deps.log("shared-element: the content engine did not release the element:",e)}}releaseContentElementAdPlayer(){const e=this.contentElementAdPlayer;if(e){this.contentElementAdPlayer=null;try{e.destroy()}catch(e){this.deps.log("shared-element: releasing the ad player on the content element failed:",e)}}}async playAdThroughContentElement(){const e=this.state.currentAsset,t=this.state.currentBreak;if(!e||!t)return;const s=this.state.effectiveBreakDurationSec??t.duration,a=await this.deps.resolveAssetUri(e,s);if(!a)return void this.deps.log("shared-element: no URI for the running ad; leaving it on the ad element");const n=this.deps.getAdPlayer()?.currentTime??0;this.state.savedContentSrc=this.restorableContentSource(),this.state.savedContentTime=this.deps.contentPlayer.currentTime,this.state.savedContentLive=!Number.isFinite(this.deps.contentPlayer.duration),this.state.savedContentEdgeDistance=this.state.savedContentLive?this.deps.liveEdgeDistanceSec():null,this.state.sharedAdClaimActive=!0,await this.releaseContentElementFromContentEngine(),this.deps.rebindContentElementIfSwapped();const r=this.sharedElementAdPlayer()??this.deps.contentPlayer;try{this.deps.getAdPlayer()?.pause(),this.deps.clearContentGate(),await r.load(a),this.attachSharedAdListeners(r),n>0&&r.seek(n),await r.play(),this.deps.hide(),this.deps.log("shared-element: the ad now plays through the content element")}catch(e){this.deps.log("shared-element: could not move the ad onto the content element:",e),this.state.sharedAdClaimActive=!1,this.state.savedContentSrc=null,this.state.savedContentEdgeDistance=null}}async playSharedElementBreak(t,a){const r=this.deps.getVariant(t);if(!r)return this.deps.log("shared-element: no valid variant, skipping break:",t.id),void a.onBreakEnd(t);if(r.format===e.LSHAPE_CONTENT)return this.deps.log("shared-element: skipping lshape_content break (no fullscreen video asset):",t.id),void a.onBreakEnd(t);const d=this.deps.getAssets(t)[0];if(!d)return this.deps.log("shared-element: no asset to play, skipping break:",t.id),void a.onBreakEnd(t);r.format!==e.SINGLE&&this.deps.log(`shared-element: downgrading ${r.format} to a single fullscreen ad:`,t.id),this.state.currentBreak=t,this.state.currentAsset=d,this.state.callbacks=a,this.state.isPlaying=!0;const h=this.state.effectiveBreakDurationSec??t.duration,l=await this.deps.resolveAssetUri(d,h);if(!l)return this.deps.log("shared-element: no valid URI, skipping break:",t.id),this.state.isPlaying=!1,this.state.currentBreak=null,this.state.currentAsset=null,void a.onBreakEnd(t);this.state.savedContentSrc=this.restorableContentSource(),this.state.savedContentTime=this.deps.contentPlayer.currentTime,this.state.savedContentLive=!Number.isFinite(this.deps.contentPlayer.duration),this.state.savedContentEdgeDistance=this.state.savedContentLive?this.deps.liveEdgeDistanceSec():null,this.state.sharedAdClaimActive=!0,await this.releaseContentElementFromContentEngine(),this.deps.rebindContentElementIfSwapped();const o=this.sharedElementAdPlayer()??this.deps.contentPlayer;if(h&&h>0){const e=h+this.state.adBreakCutSafetyMarginSec;s(this.state,e,()=>{this.deps.log("shared-element: break-cut timer expired, ending break"),this.state.isPlaying&&this.state.currentBreak&&this.state.callbacks?.onBreakCut?.(this.state.currentBreak),this.onSharedElementAdEnded()},()=>{this.state.callbacks?.onAdError(t,d,new n("Ad playback startup timed out","DA-AD-START-TIMEOUT")),this.onSharedElementAdEnded()})}try{if(this.deps.clearContentGate(),await this.sharedAdStep("load",o,()=>o.load(l)),!this.state.isPlaying||this.state.currentBreak!==t)return;if(this.attachSharedAdListeners(o),this.armSharedAdFirstFrameWatchdog(o,Math.max(Math.min(1e3*(h??0)-100,5e3),3e3)),await this.sharedAdStep("play",o,()=>o.play()),!this.state.isPlaying||this.state.currentBreak!==t)return;this.state.callbacks?.onAdStart(t,d,0,1)}catch(e){if(!this.state.isPlaying||this.state.currentBreak!==t)return;this.deps.log("shared-element: ad playback failed:",e),this.state.callbacks?.onAdError(t,d,i(e)),this.onSharedElementAdEnded()}}onSharedElementAdEnded(){if(!this.state.isPlaying||!this.state.currentBreak)return;const e=this.state.currentBreak,t=this.state.currentAsset,s=this.state.callbacks,n=this.sharedRestoreGeneration;this.detachSharedAdListeners(),a(this.state),this.state.isPlaying=!1,this.state.currentBreak=null,this.state.currentAsset=null,t&&s?.onAdEnd(e,t,0,1),n===this.sharedRestoreGeneration&&this.restoreSharedElementContent(e),s?.onBreakEnd(e)}restoreSharedElementContent(e){const s=this.state.savedContentSrc;if(!s){if(this.sharedRestore)return;return this.deps.log("shared-element: no saved content source to restore"),void this.releaseContentElementAdPlayer()}if(!this.beginSharedActivity())return;const a=this.state.savedContentTime,n=this.state.savedContentLive,r=this.state.savedContentEdgeDistance,i=this.state.callbacks;this.state.savedContentSrc=null,this.state.savedContentEdgeDistance=null;const d={src:s,time:a,live:n,edgeDistance:r,breakInfo:e,postRoll:!!e&&t(e),onContentReloading:i?.onContentReloading?.bind(i),adPlayer:this.contentElementAdPlayer,reloading:!1,cleanedUp:!1,cancelled:!1,finishingActivity:null,attempt:null};this.sharedRestore=d,e&&"number"==typeof e.start&&i?.onContentResumeSeek?.(n?this.deps.contentPlayer.currentTime:a,e.start+e.duration),this.sharedRestore===d&&(d.reloading=!0,d.onContentReloading?.(!0),this.sharedRestore===d&&this.runSharedRestore(d))}async runSharedRestore(e){for(let t=1;t<=2;t++){const s={phase:"load",deadline:0,timer:null,settle:null};e.attempt=s;const a=await new Promise(t=>{s.settle=e=>{s.settle&&(s.settle=null,s.timer&&clearTimeout(s.timer),s.timer=null,t(e))},this.armSharedRestoreDeadline(s,1e4),this.reloadAndResume(e,s).then(()=>s.settle?.("success"),()=>s.settle?.("rejection"))});if(this.sharedRestore!==e||e.attempt!==s)return;if("success"===a||"cancelled"===a)break;if(2===t?this.deps.diagnose("DA-SHARED-ELEMENT-RESTORE-FAILED",`Content did not resume after the shared-element break: ${s.phase} ${a}`,{breakId:e.breakInfo?.id,live:e.live,attempts:t,phase:s.phase,reason:a}):this.deps.log(`shared-element: content restore ${s.phase} ${a}, retrying once`),this.sharedRestore!==e||e.attempt!==s)return}this.finishSharedRestore(e,!0)}armSharedRestoreDeadline(e,t){e.timer&&clearTimeout(e.timer),e.deadline=performance.now()+t,e.timer=setTimeout(()=>e.settle?.("timeout"),t)}ownsSharedRestoreAttempt(e,t){return!(this.sharedRestore!==e||e.attempt!==t||!t.settle)&&(performance.now()<t.deadline||(t.settle("timeout"),!1))}async reloadAndResume(e,t){const{src:s,time:a,live:n,edgeDistance:r,postRoll:i}=e,h=()=>this.ownsSharedRestoreAttempt(e,t);if(!h())return;if(e.adPlayer?.releaseMediaElement&&(await this.releaseAdEngineGrip(e),!h()))return;const l=this.deps.contentPlayer.videoElement??null,o=d(l);if(await this.deps.contentPlayer.load(s),h()){if(this.armSharedRestoreDeadline(t,5e3),n){if(!i&&null!==r&&r>0){t.phase="live-edge";const e=await this.deps.waitForLiveEdge(h);if(!h())return;if(null!==e){const s=Math.max(e-r,0);if(this.deps.log(`shared-element: live resume ${r.toFixed(2)}s behind the edge (${s.toFixed(2)}s)`),!h())return;t.phase="seek",this.deps.contentPlayer.seek(s)}}}else t.phase="seek",this.deps.contentPlayer.seek(a);h()&&!i&&(t.phase="play",await this.deps.contentPlayer.play(),h()&&await this.waitForContentEngineToTakeElement(l,o,h))}}async releaseAdEngineGrip(e){const t=e.adPlayer;if(t?.releaseMediaElement)try{await t.releaseMediaElement()}catch(e){this.deps.log("shared-element: the ad engine did not release the element:",e)}}async waitForContentEngineToTakeElement(e,t,s){if(!(e&&t&&(a=t,a.src||a.srcObject||a.sourceChild)))return;var a;const n=()=>{if(this.deps.contentPlayer.videoElement!==e)return!0;const s=d(e);if(!s)return!1;const a=!!s.src&&s.src!==t.src,n=!!s.srcObject&&s.srcObject!==t.srcObject;return a||n},r=performance.now();for(;!n()&&s()&&performance.now()-r<1500;)await new Promise(e=>setTimeout(e,25));if(!s())return;const i=Math.round(performance.now()-r);this.deps.log(`shared-element: content engine ${n()?"took":"did not take"} the element after ${i}ms`)}finishSharedRestore(e,t){this.sharedRestore===e&&(null===e.finishingActivity?(e.finishingActivity=this.sharedActivityGeneration,e.cleanedUp||(this.state.contentPaused=!1,this.contentElementAdPlayer===e.adPlayer&&this.releaseContentElementAdPlayer(),this.sharedRestore===e&&this.deps.clearContentGate(),this.sharedRestore===e&&this.deps.rebindContentElementIfSwapped(),e.cleanedUp=!0),this.endSharedRestoreReloading(e),e.finishingActivity=null,this.sharedRestore===e&&(!t||e.cancelled||e.postRoll?this.sharedRestore=null:this.armSharedRestoreWatchdog(e))):e.finishingActivity!==this.sharedActivityGeneration&&(this.endSharedRestoreReloading(e),this.sharedRestore===e&&(this.sharedRestore=null)))}endSharedRestoreReloading(e){e.reloading&&(e.reloading=!1,e.onContentReloading?.(!1))}armSharedRestoreWatchdog(e){const t=this.deps.contentPlayer.currentTime;this.sharedRestoreWatchdog=setTimeout(()=>{if(this.sharedRestore!==e)return;this.sharedRestoreWatchdog=null;const s=this.deps.contentPlayer.currentTime-t;(this.deps.contentPlayer.paused||s<1)&&(this.deps.diagnose("DA-CONTENT-RESUME-STALLED",`Content did not resume within 5s of the shared-element restore (paused: ${this.deps.contentPlayer.paused}, advanced: ${s.toFixed(1)}s); nudging play()`,{breakId:e.breakInfo?.id??"?",paused:this.deps.contentPlayer.paused,advancedSec:s}),this.sharedRestore===e&&this.deps.contentPlayer.play().catch(()=>{this.sharedRestore===e&&this.deps.log("shared-element: watchdog play() nudge failed")}))},5e3)}beginSharedActivity(){return this.sharedActivityGeneration++,this.cancelSharedRestore()}cancelSharedRestore(){const e=this.sharedActivityGeneration;this.sharedRestoreGeneration++,this.sharedRestoreWatchdog&&(clearTimeout(this.sharedRestoreWatchdog),this.sharedRestoreWatchdog=null);const t=this.sharedRestore;return t&&(t.cancelled=!0,t.attempt?.settle?.("cancelled"),this.finishSharedRestore(t,!1)),e===this.sharedActivityGeneration}restorableContentSource(){const e=this.deps.contentPlayer.sourceUrl;if(e&&!e.startsWith("blob:"))return e;const t=this.deps.contentPlayer.videoElement,s=t?.currentSrc||t?.src||"";return s.length>0&&!s.startsWith("blob:")?s:null}}
1
+ import{BreakFormat as e,isPostRollBreak as t}from"../../types/BreakManifest";import{armPlaybackCutoff as s,clearPlaybackCutoff as a}from"./PlaybackCutoff";import{AdError as n}from"../../types/AdError";import{classifyMediaError as r,toClassifiedAdError as i}from"./classifyMediaError";function d(e){return e?{src:e.getAttribute("src"),srcObject:e.srcObject,sourceChild:null!==e.querySelector("source")}:null}export class SharedElementBreakPlayer{constructor(e,t){this.state=e,this.deps=t,this.contentElementAdPlayer=null,this.sharedAdSource=null,this.sharedAdEndedHandler=null,this.sharedAdTimeUpdateHandler=null,this.sharedAdTimedMetadataHandler=null,this.sharedAdWaitingHandler=null,this.sharedAdPlayingHandler=null,this.sharedAdFirstFrameTimer=null,this.sharedRestoreWatchdog=null,this.sharedRestore=null,this.sharedRestoreGeneration=0,this.sharedActivityGeneration=0}sharedElementAdPlayer(){const e=this.deps.contentPlayer.videoElement;if(!e)return null;if(this.contentElementAdPlayer?.videoElement===e)return this.contentElementAdPlayer;this.releaseContentElementAdPlayer();const t=this.deps.createAdAdapter(document.createElement("div"),e);return t!==this.deps.contentPlayer&&t.prepareForAdPlayback?.(),t.videoElement!==e?(this.deps.log("shared-element: createAdAdapter ignored the content element; falling back to the content player for ad playback"),t.destroy(),null):(this.contentElementAdPlayer=t,t)}async sharedAdStep(e,t,s){try{await s()}catch(s){const a=t.videoElement,i=s instanceof Error?s.message:String(s),d=new n(`shared-element ${e} failed: ${i} (readyState=${a?.readyState??"-"} paused=${a?.paused??"-"} networkState=${a?.networkState??"-"})`,r(s));throw d.cause=s,d}}clearSharedAdFirstFrameWatchdog(){this.sharedAdFirstFrameTimer&&(clearTimeout(this.sharedAdFirstFrameTimer),this.sharedAdFirstFrameTimer=null)}armSharedAdFirstFrameWatchdog(e,t){this.clearSharedAdFirstFrameWatchdog(),this.sharedAdFirstFrameTimer=setTimeout(()=>{if(this.sharedAdFirstFrameTimer=null,!this.state.isPlaying||!this.state.currentBreak)return;const s=e.videoElement,a=this.deps.contentPlayer.videoElement,r=s?.currentSrc??"",i=`readyState=${s?.readyState??"-"} networkState=${s?.networkState??"-"} paused=${s?.paused??"-"} connected=${s?.isConnected??"-"} srcKind=${r.startsWith("blob:")?"blob":r?"url":"none"} sameElementAsContentPlayer=${String(s===a)}`;this.deps.log(`shared-element: no first frame within ${t}ms (${i})`),this.deps.diagnose("DA-AD-PLAYBACK-ERROR",`the ad never rendered a frame on the content element within ${t}ms`,{breakId:this.state.currentBreak.id,detail:i});const d=this.state.currentAsset;d&&this.state.callbacks?.onAdError(this.state.currentBreak,d,new n(`shared-element ad produced no frames within ${t}ms (${i})`,"DA-AD-START-TIMEOUT")),this.onSharedElementAdEnded()},t)}attachSharedAdListeners(e){this.sharedAdSource=e,this.sharedAdEndedHandler=()=>this.onSharedElementAdEnded(),e.on("ended",this.sharedAdEndedHandler),this.sharedAdTimeUpdateHandler=()=>{this.sharedAdFirstFrameTimer&&e.currentTime>0&&this.clearSharedAdFirstFrameWatchdog(),this.deps.reportAdProgress(e)},e.on("timeupdate",this.sharedAdTimeUpdateHandler),this.sharedAdTimedMetadataHandler=e=>{e&&this.deps.processTimedMetadata(e)},e.on("timedmetadata",this.sharedAdTimedMetadataHandler),this.sharedAdWaitingHandler=()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&this.state.callbacks?.onAdWaiting(this.state.currentBreak,this.state.currentAsset)},e.on("waiting",this.sharedAdWaitingHandler),this.sharedAdPlayingHandler=()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&!e.paused&&(this.clearSharedAdFirstFrameWatchdog(),this.state.startPlaybackCutoff?.(),this.state.callbacks?.onAdPlaying(this.state.currentBreak,this.state.currentAsset))},e.on("playing",this.sharedAdPlayingHandler)}detachSharedAdListeners(){const e=this.sharedAdSource??this.deps.contentPlayer;this.sharedAdSource=null,this.state.sharedAdClaimActive=!1,this.clearSharedAdFirstFrameWatchdog(),this.sharedAdTimedMetadataHandler&&(e.off("timedmetadata",this.sharedAdTimedMetadataHandler),this.sharedAdTimedMetadataHandler=null),this.sharedAdWaitingHandler&&(e.off("waiting",this.sharedAdWaitingHandler),this.sharedAdWaitingHandler=null),this.sharedAdPlayingHandler&&(e.off("playing",this.sharedAdPlayingHandler),this.sharedAdPlayingHandler=null),this.sharedAdTimeUpdateHandler&&(e.off("timeupdate",this.sharedAdTimeUpdateHandler),this.sharedAdTimeUpdateHandler=null),this.sharedAdEndedHandler&&(e.off("ended",this.sharedAdEndedHandler),this.sharedAdEndedHandler=null)}async releaseContentElementFromContentEngine(){if(this.deps.contentPlayer.releaseMediaElement)try{await this.deps.contentPlayer.releaseMediaElement()}catch(e){this.deps.log("shared-element: the content engine did not release the element:",e)}}releaseContentElementAdPlayer(){const e=this.contentElementAdPlayer;if(e){this.contentElementAdPlayer=null;try{e.destroy()}catch(e){this.deps.log("shared-element: releasing the ad player on the content element failed:",e)}}}async playAdThroughContentElement(){const e=this.state.currentAsset,t=this.state.currentBreak;if(!e||!t)return;const s=this.state.effectiveBreakDurationSec??t.duration,a=await this.deps.resolveAssetUri(e,s);if(!a)return void this.deps.log("shared-element: no URI for the running ad; leaving it on the ad element");const n=this.deps.getAdPlayer()?.currentTime??0;this.state.savedContentSrc=this.restorableContentSource(),this.state.savedContentTime=this.deps.contentPlayer.currentTime,this.state.savedContentLive=!Number.isFinite(this.deps.contentPlayer.duration),this.state.savedContentEdgeDistance=this.state.savedContentLive?this.deps.liveEdgeDistanceSec():null,this.state.sharedAdClaimActive=!0,await this.releaseContentElementFromContentEngine(),this.deps.rebindContentElementIfSwapped();const r=this.sharedElementAdPlayer()??this.deps.contentPlayer;try{this.deps.getAdPlayer()?.pause(),this.deps.clearContentGate(),await r.load(a),this.attachSharedAdListeners(r),n>0&&r.seek(n),await r.play(),this.deps.hide(),this.deps.log("shared-element: the ad now plays through the content element")}catch(e){this.deps.log("shared-element: could not move the ad onto the content element:",e),this.state.sharedAdClaimActive=!1,this.state.savedContentSrc=null,this.state.savedContentEdgeDistance=null}}async playSharedElementBreak(t,a){const r=this.deps.getVariant(t);if(!r)return this.deps.log("shared-element: no valid variant, skipping break:",t.id),void a.onBreakEnd(t);if(r.format===e.LSHAPE_CONTENT)return this.deps.log("shared-element: skipping lshape_content break (no fullscreen video asset):",t.id),void a.onBreakEnd(t);const d=this.deps.getAssets(t)[0];if(!d)return this.deps.log("shared-element: no asset to play, skipping break:",t.id),void a.onBreakEnd(t);r.format!==e.SINGLE&&this.deps.log(`shared-element: downgrading ${r.format} to a single fullscreen ad:`,t.id),this.state.currentBreak=t,this.state.currentAsset=d,this.state.callbacks=a,this.state.isPlaying=!0;const h=this.state.effectiveBreakDurationSec??t.duration,l=await this.deps.resolveAssetUri(d,h);if(!l)return this.deps.log("shared-element: no valid URI, skipping break:",t.id),this.state.isPlaying=!1,this.state.currentBreak=null,this.state.currentAsset=null,void a.onBreakEnd(t);this.state.savedContentSrc=this.restorableContentSource(),this.state.savedContentTime=this.deps.contentPlayer.currentTime,this.state.savedContentLive=!Number.isFinite(this.deps.contentPlayer.duration),this.state.savedContentEdgeDistance=this.state.savedContentLive?this.deps.liveEdgeDistanceSec():null,this.state.sharedAdClaimActive=!0,await this.releaseContentElementFromContentEngine(),this.deps.rebindContentElementIfSwapped();const o=this.sharedElementAdPlayer()??this.deps.contentPlayer;if(h&&h>0){const e=h+this.state.adBreakCutSafetyMarginSec;s(this.state,e,()=>{this.deps.log("shared-element: break-cut timer expired, ending break"),this.state.isPlaying&&this.state.currentBreak&&this.state.callbacks?.onBreakCut?.(this.state.currentBreak),this.onSharedElementAdEnded()},()=>{this.state.callbacks?.onAdError(t,d,new n("Ad playback startup timed out","DA-AD-START-TIMEOUT")),this.onSharedElementAdEnded()})}try{if(this.deps.clearContentGate(),await this.sharedAdStep("load",o,()=>o.load(l)),!this.state.isPlaying||this.state.currentBreak!==t)return;if(this.attachSharedAdListeners(o),this.armSharedAdFirstFrameWatchdog(o,Math.max(Math.min(1e3*(h??0)-100,5e3),3e3)),this.deps.surfaceReveal?.surfaceShown(),this.deps.surfaceReveal?.watch(d.id,t.id,o.videoElement),await this.sharedAdStep("play",o,()=>o.play()),!this.state.isPlaying||this.state.currentBreak!==t)return;this.state.callbacks?.onAdStart(t,d,0,1)}catch(e){if(!this.state.isPlaying||this.state.currentBreak!==t)return;this.deps.log("shared-element: ad playback failed:",e),this.state.callbacks?.onAdError(t,d,i(e)),this.onSharedElementAdEnded()}}onSharedElementAdEnded(){if(!this.state.isPlaying||!this.state.currentBreak)return;const e=this.state.currentBreak,t=this.state.currentAsset,s=this.state.callbacks,n=this.sharedRestoreGeneration;this.detachSharedAdListeners(),a(this.state),this.state.isPlaying=!1,this.state.currentBreak=null,this.state.currentAsset=null,t&&s?.onAdEnd(e,t,0,1),n===this.sharedRestoreGeneration&&this.restoreSharedElementContent(e),this.deps.surfaceReveal?.surfaceHidden(),s?.onBreakEnd(e)}restoreSharedElementContent(e){const s=this.state.savedContentSrc;if(!s){if(this.sharedRestore)return;return this.deps.log("shared-element: no saved content source to restore"),void this.releaseContentElementAdPlayer()}if(!this.beginSharedActivity())return;const a=this.state.savedContentLive,n=a||void 0===this.state.snapbackResumePosition?this.state.savedContentTime:this.state.snapbackResumePosition,r=this.state.savedContentEdgeDistance,i=this.state.callbacks;this.state.savedContentSrc=null,this.state.savedContentEdgeDistance=null;const d={src:s,time:n,live:a,edgeDistance:r,breakInfo:e,postRoll:!!e&&t(e),onContentReloading:i?.onContentReloading?.bind(i),adPlayer:this.contentElementAdPlayer,reloading:!1,cleanedUp:!1,cancelled:!1,finishingActivity:null,attempt:null};this.sharedRestore=d,e&&"number"==typeof e.start&&i?.onContentResumeSeek?.(a?this.deps.contentPlayer.currentTime:n,e.start+e.duration),this.sharedRestore===d&&(d.reloading=!0,d.onContentReloading?.(!0),this.sharedRestore===d&&this.runSharedRestore(d))}async runSharedRestore(e){for(let t=1;t<=2;t++){const s={phase:"load",deadline:0,timer:null,settle:null};e.attempt=s;const a=await new Promise(t=>{s.settle=e=>{s.settle&&(s.settle=null,s.timer&&clearTimeout(s.timer),s.timer=null,t(e))},this.armSharedRestoreDeadline(s,1e4),this.reloadAndResume(e,s).then(()=>s.settle?.("success"),()=>s.settle?.("rejection"))});if(this.sharedRestore!==e||e.attempt!==s)return;if("success"===a||"cancelled"===a)break;if(2===t?this.deps.diagnose("DA-SHARED-ELEMENT-RESTORE-FAILED",`Content did not resume after the shared-element break: ${s.phase} ${a}`,{breakId:e.breakInfo?.id,live:e.live,attempts:t,phase:s.phase,reason:a}):this.deps.log(`shared-element: content restore ${s.phase} ${a}, retrying once`),this.sharedRestore!==e||e.attempt!==s)return}this.finishSharedRestore(e,!0)}armSharedRestoreDeadline(e,t){e.timer&&clearTimeout(e.timer),e.deadline=performance.now()+t,e.timer=setTimeout(()=>e.settle?.("timeout"),t)}ownsSharedRestoreAttempt(e,t){return!(this.sharedRestore!==e||e.attempt!==t||!t.settle)&&(performance.now()<t.deadline||(t.settle("timeout"),!1))}async reloadAndResume(e,t){const{src:s,time:a,live:n,edgeDistance:r,postRoll:i}=e,h=()=>this.ownsSharedRestoreAttempt(e,t);if(!h())return;if(e.adPlayer?.releaseMediaElement&&(await this.releaseAdEngineGrip(e),!h()))return;const l=this.deps.contentPlayer.videoElement??null,o=d(l);if(await this.deps.contentPlayer.load(s),h()){if(this.armSharedRestoreDeadline(t,5e3),n){if(!i&&null!==r&&r>0){t.phase="live-edge";const e=await this.deps.waitForLiveEdge(h);if(!h())return;if(null!==e){const s=Math.max(e-r,0);if(this.deps.log(`shared-element: live resume ${r.toFixed(2)}s behind the edge (${s.toFixed(2)}s)`),!h())return;t.phase="seek",this.deps.contentPlayer.seek(s)}}}else t.phase="seek",this.deps.contentPlayer.seek(a);h()&&!i&&(t.phase="play",await this.deps.contentPlayer.play(),h()&&await this.waitForContentEngineToTakeElement(l,o,h))}}async releaseAdEngineGrip(e){const t=e.adPlayer;if(t?.releaseMediaElement)try{await t.releaseMediaElement()}catch(e){this.deps.log("shared-element: the ad engine did not release the element:",e)}}async waitForContentEngineToTakeElement(e,t,s){if(!(e&&t&&(a=t,a.src||a.srcObject||a.sourceChild)))return;var a;const n=()=>{if(this.deps.contentPlayer.videoElement!==e)return!0;const s=d(e);if(!s)return!1;const a=!!s.src&&s.src!==t.src,n=!!s.srcObject&&s.srcObject!==t.srcObject;return a||n},r=performance.now();for(;!n()&&s()&&performance.now()-r<1500;)await new Promise(e=>setTimeout(e,25));if(!s())return;const i=Math.round(performance.now()-r);this.deps.log(`shared-element: content engine ${n()?"took":"did not take"} the element after ${i}ms`)}finishSharedRestore(e,t){this.sharedRestore===e&&(null===e.finishingActivity?(e.finishingActivity=this.sharedActivityGeneration,e.cleanedUp||(this.state.contentPaused=!1,this.contentElementAdPlayer===e.adPlayer&&this.releaseContentElementAdPlayer(),this.sharedRestore===e&&this.deps.clearContentGate(),this.sharedRestore===e&&this.deps.rebindContentElementIfSwapped(),e.cleanedUp=!0),this.endSharedRestoreReloading(e),e.finishingActivity=null,this.sharedRestore===e&&(!t||e.cancelled||e.postRoll?this.sharedRestore=null:this.armSharedRestoreWatchdog(e))):e.finishingActivity!==this.sharedActivityGeneration&&(this.endSharedRestoreReloading(e),this.sharedRestore===e&&(this.sharedRestore=null)))}endSharedRestoreReloading(e){e.reloading&&(e.reloading=!1,e.onContentReloading?.(!1))}armSharedRestoreWatchdog(e){const t=this.deps.contentPlayer.currentTime;this.sharedRestoreWatchdog=setTimeout(()=>{if(this.sharedRestore!==e)return;this.sharedRestoreWatchdog=null;const s=this.deps.contentPlayer.currentTime-t;(this.deps.contentPlayer.paused||s<1)&&(this.deps.diagnose("DA-CONTENT-RESUME-STALLED",`Content did not resume within 5s of the shared-element restore (paused: ${this.deps.contentPlayer.paused}, advanced: ${s.toFixed(1)}s); nudging play()`,{breakId:e.breakInfo?.id??"?",paused:this.deps.contentPlayer.paused,advancedSec:s}),this.sharedRestore===e&&this.deps.contentPlayer.play().catch(()=>{this.sharedRestore===e&&this.deps.log("shared-element: watchdog play() nudge failed")}))},5e3)}beginSharedActivity(){return this.sharedActivityGeneration++,this.cancelSharedRestore()}cancelSharedRestore(){const e=this.sharedActivityGeneration;this.sharedRestoreGeneration++,this.sharedRestoreWatchdog&&(clearTimeout(this.sharedRestoreWatchdog),this.sharedRestoreWatchdog=null);const t=this.sharedRestore;return t&&(t.cancelled=!0,t.attempt?.settle?.("cancelled"),this.finishSharedRestore(t,!1)),e===this.sharedActivityGeneration}restorableContentSource(){const e=this.deps.contentPlayer.sourceUrl;if(e&&!e.startsWith("blob:"))return e;const t=this.deps.contentPlayer.videoElement,s=t?.currentSrc||t?.src||"";return s.length>0&&!s.startsWith("blob:")?s:null}}
@@ -0,0 +1,28 @@
1
+ /** Native's ad-start deadline: an ad with no picture by then is reported as `reveal-timeout`. */
2
+ export declare const SURFACE_REVEAL_DEADLINE_MS = 10000;
3
+ type Diagnose = (code: string, message: string, context: Record<string, unknown>) => void;
4
+ /**
5
+ * Reports `DA-AD-SURFACE-REVEALED` once per ad when its first frame is on screen, with the same
6
+ * fields as Android and iOS. Web shows the surface before the picture unless a pre-roll is held,
7
+ * so that gap is a distinct reason (`shown-before-frame`, `visibleGapMs`) and never `waitedMs`.
8
+ */
9
+ export declare class SurfaceRevealReporter {
10
+ private readonly diagnose;
11
+ private readonly deadlineMs;
12
+ private readonly now;
13
+ private visibleSince;
14
+ private pending;
15
+ constructor(diagnose: Diagnose, deadlineMs?: number, now?: () => number);
16
+ /** The surface carrying the ad is on screen from now on. */
17
+ surfaceShown(): void;
18
+ surfaceHidden(): void;
19
+ /** An ad starts; `video` draws it, or is passed later through `watchVideo` (IMA's own element). */
20
+ watch(assetId: string, breakId: string, video?: HTMLVideoElement | null): void;
21
+ /** Chrome, Edge and Firefox stop frame callbacks in a hidden tab, so the deadline only runs while it is visible. */
22
+ private armDeadline;
23
+ watchVideo(video: HTMLVideoElement): void;
24
+ cancel(): void;
25
+ /** The surface may be shown after the ad started (a held pre-roll), so hidden and visible time split here. */
26
+ private report;
27
+ }
28
+ export {};
@@ -0,0 +1 @@
1
+ export const SURFACE_REVEAL_DEADLINE_MS=1e4;export class SurfaceRevealReporter{constructor(e,t=1e4,i=()=>performance.now()){this.diagnose=e,this.deadlineMs=t,this.now=i,this.visibleSince=null,this.pending=null}surfaceShown(){this.visibleSince??(this.visibleSince=this.now())}surfaceHidden(){this.visibleSince=null,this.cancel()}watch(e,t,i){this.cancel();const n={assetId:e,breakId:t,armedAt:this.now(),shownAtStart:null!==this.visibleSince,stops:[]};this.pending=n,n.stops.push(this.armDeadline(n)),i&&this.watchVideo(i)}armDeadline(e){let t=this.deadlineMs,i=0,n=null;const s=()=>{i=this.now(),n=setTimeout(()=>this.report(e,!0),t)},a=()=>{null!==n&&(clearTimeout(n),n=null,t=Math.max(0,t-(this.now()-i)))},r=()=>{"hidden"===document.visibilityState?a():null===n&&s()};return"hidden"!==document.visibilityState&&s(),document.addEventListener("visibilitychange",r),()=>{a(),document.removeEventListener("visibilitychange",r)}}watchVideo(e){const t=this.pending;t&&t.stops.push(function(e,t){if("function"==typeof e.requestVideoFrameCallback){const i=e.requestVideoFrameCallback(()=>t());return()=>e.cancelVideoFrameCallback?.(i)}const i=()=>{e.currentTime<=0||(e.removeEventListener("timeupdate",i),t())};return e.addEventListener("timeupdate",i),()=>e.removeEventListener("timeupdate",i)}(e,()=>this.report(t,!1)))}cancel(){this.pending?.stops.forEach(e=>e()),this.pending=null}report(e,t){if(this.pending!==e)return;this.cancel();const i=this.now(),n=this.visibleSince,s=t?"reveal-timeout":e.shownAtStart?"shown-before-frame":"first-frame",a=Math.round(Math.max(0,(n??i)-e.armedAt)),r={reason:s,waitedMs:a,transitionMs:0,cut:!0,assetId:e.assetId,breakId:e.breakId};let o=`Ad surface revealed after ${a}ms (${s})`;if(null!==n){const t=Math.round(Math.max(0,i-Math.max(e.armedAt,n)));r.visibleGapMs=t,o+=`, ${t}ms on screen without a picture`}this.diagnose("DA-AD-SURFACE-REVEALED",o,r)}}
@@ -0,0 +1,5 @@
1
+ import type { Break } from '../types/BreakManifest';
2
+ import type { TrackedBreak } from './RewindRearm';
3
+ import type { BreakStartCallback } from './BreakSchedulerTypes';
4
+ /** Event delays are playback-relative for pre-rolls and wallclock-relative for post-rolls. */
5
+ export declare function checkEventBreak(tracked: TrackedBreak, delaySec: number, elapsedMs: number | null, adStartDelayMs: number, held: boolean, preload: (brk: Break, label: string) => void, skip: (tracked: TrackedBreak) => void, start: BreakStartCallback): void;
@@ -0,0 +1 @@
1
+ import{BreakState as e}from"./BreakSchedulerTypes";export function checkEventBreak(r,n,t,a,o,c,i,u){if(null===t)return;const d=1e3*n;if(a>0&&d<a)return void i(r);const k=Math.round((d-t)/1e3);k>0&&k<=8&&c(r.break,`event, ${k}s ahead`),t>=d&&!o&&(r.state=e.ACTIVE,u(r.break,{tuneIn:!1,elapsedSec:0,remainingSec:r.break.duration??0}))}
@@ -14,9 +14,11 @@ export interface ResumePointInput {
14
14
  insertionType?: AdInsertionType | undefined;
15
15
  /** The manifest's seek length from the anchor. Absent lets the ad duration decide; `0` asks for no seek. */
16
16
  resumeOffset?: number | undefined;
17
+ /** Viewer seek-over target. Overrides every offset policy, including on no-fill or failure. */
18
+ seekPosition?: number | undefined;
17
19
  }
18
20
  /** Which precedence level produced the offset. */
19
- export type ResumePointSource = 'client' | 'manifest' | 'ad-duration';
21
+ export type ResumePointSource = 'snapback' | 'client' | 'manifest' | 'ad-duration';
20
22
  /** The resolved resume decision. */
21
23
  export interface ResumePoint {
22
24
  /** Whether to seek at all. A zero offset means stay where content paused. */
@@ -1 +1 @@
1
- export function resolveResumePoint(e){const{anchor:t,duration:i,adDuration:n,insertionType:o,resumeOffset:r}=e;let s,u;void 0!==o?(s="insertion"===o?0:i,u="client"):void 0!==r?(s=r,u="manifest"):(s=n,u="ad-duration"),(!Number.isFinite(s)||s<0)&&(s=0);const a=Number.isFinite(n)&&n>0?n:0,c=s>0&&a>0&&a<s;return c&&(s=a),{seek:s>0,target:t+s,offset:s,source:u,clamped:c}}
1
+ export function resolveResumePoint(e){const{anchor:i,duration:t,adDuration:o,insertionType:s,resumeOffset:n}=e;if(void 0!==e.seekPosition&&Number.isFinite(e.seekPosition)){const t=e.seekPosition-i;return{seek:0!==t,target:e.seekPosition,offset:t,source:"snapback",clamped:!1}}let r,a;void 0!==s?(r="insertion"===s?0:t,a="client"):void 0!==n?(r=n,a="manifest"):(r=o,a="ad-duration"),(!Number.isFinite(r)||r<0)&&(r=0);const u=Number.isFinite(o)&&o>0?o:0,c=r>0&&u>0&&u<r;return c&&(r=u),{seek:r>0,target:i+r,offset:r,source:a,clamped:c}}
@@ -1 +1 @@
1
- import{BreakFormat as e}from"../types/BreakManifest";import{playbackErrorContext as t}from"../diagnostics/playbackErrorContext";import{adErrorCode as a}from"../types/AdError";import{VendorAdError as i}from"../types/VendorAdError";export class BreakLifecycle{constructor(e,t){this.state=e,this.deps=t,this.breakBeginEmittedId=null,this.pendingAdBreakBegin=null,t.breakScheduler.setOnChainSuccessorChanged((e,a)=>{t.breakChainRecorder?.successorChanged(a?.id??null),t.adPlayerController.onChainSuccessorChanged()})}recordBreakBegan(e,t){const a=this.deps.breakScheduler.getChainedSuccessor(e.id);this.deps.breakChainRecorder?.breakBegan(e,t,a?.id??null)}emitAdBreakBegin(e,t){this.breakBeginEmittedId!==e.id&&(this.breakBeginEmittedId=e.id,this.pendingAdBreakBegin=null,this.deps.emit("adbreakbegin",{type:"adbreakbegin",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),elapsedSec:t?.elapsedSec??0,remainingSec:t?.remainingSec??e.duration??0,...t?.tuneIn?{tuneIn:{elapsedSec:t.elapsedSec,remainingSec:t.remainingSec}}:{}}))}flushPendingAdBreakBegin(){this.pendingAdBreakBegin?.()}onBreakSkipped(e,t){this.deps.emit("adbreakskipped",{type:"adbreakskipped",timestamp:Date.now(),break:e,reason:t,format:this.deps.effectiveFormatOf(e)})}async onBreakStart(r,s){const d=this.state.sessionGeneration;if(this.state.snapbackSeekInFlight=!1,this.deps.adPlayerController.isAdPlaying()){if(this.deps.diagnose("DA-BREAK-SUPPRESSED-OVERLAP",`Break suppressed (ad already playing): ${r.id}`,{breakId:r.id}),d!==this.state.sessionGeneration)return;return this.deps.breakScheduler.completeBreak(r.id),this.onBreakSkipped(r,"overlap"),void this.deps.onBreakSettled?.()}this.deps.adPlayerController.diagnoseUnknownVariantFormats(r);const n=this.deps.adPlayerController.breakNothingToPlayReason(r);if(n){if(this.deps.log("Break not begun (no playable variant):",r.id,n),this.deps.diagnose("DA-BREAK-NO-PLAYABLE-ASSET",`break ${r.id} has no playable variant (${n}); it will not begin.`,{breakId:r.id,reason:n}),this.deps.emit("aderror",{type:"aderror",timestamp:Date.now(),source:"ad",break:r,format:this.deps.effectiveFormatOf(r),error:new Error(`Break ${r.id} has no playable variant (${n})`)}),d!==this.state.sessionGeneration)return;return this.deps.breakScheduler.completeBreak(r.id),this.onBreakSkipped(r,"unsupported"),void this.deps.onBreakSettled?.()}this.deps.log("Break starting:",r.id,s.tuneIn?"(tune-in)":""),this.deps.breakTransitionTimer?.mark("into-break",r.id),this.breakBeginEmittedId=null,this.pendingAdBreakBegin=null;const o=this.state.heldImmediatePreRollId===r.id,p=this.deps.effectiveFormatOf(r),k=p===e.SINGLE||p===e.LSHAPE_AD,h=o&&k;o&&(this.state.heldImmediatePreRollId=null),this.deps.adBreakStatusTracker.setAdBreakStatusActive(r,void 0!==s.remainingSec?s.remainingSec:r.duration),h||this.emitAdBreakBegin(r,s),this.deps.manifestService.setActive(!0),this.deps.breakScheduler.claimChain(r.id),this.recordBreakBegan(r,!1);const l={onAdStart:(e,t,a,i,r)=>{this.deps.adBreakStatusTracker.onAdStarted(e,t,a,i),this.deps.emit("adbegin",{type:"adbegin",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,adIndex:a,totalAds:i,...r?.adId?{adId:r.adId}:{},...r?.creativeId?{creativeId:r.creativeId}:{},...void 0!==r?.podPosition?{podPosition:r.podPosition}:{}})},onAdEnd:(e,t,a,i,r)=>{this.deps.adBreakStatusTracker.onAdEnded(e,a,i),a+1<i?this.deps.breakTransitionTimer?.mark("ad-to-ad",e.id):this.deps.breakTransitionTimer?.noteOutgoingEnd(),this.deps.emit("adend",{type:"adend",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,adIndex:a,totalAds:i,...r?.adId?{adId:r.adId}:{},...r?.creativeId?{creativeId:r.creativeId}:{}})},onAdError:(e,r,s)=>{this.flushPendingAdBreakBegin(),this.deps.diagnose("DA-AD-PLAYBACK-ERROR",s.message,{breakId:e.id,assetId:r?.id,adErrorCode:a(s),...s instanceof i?{vendorCode:s.vendorCode}:{},...t(s)}),this.deps.emit("aderror",{type:"aderror",timestamp:Date.now(),source:"ad",break:e,format:this.deps.effectiveFormatOf(e),...r?{asset:r}:{},error:s})},onAdTimeUpdate:(e,t,a,i)=>{this.deps.adBreakStatusTracker.onAdTimeUpdated(e,t,a,i),this.deps.emit("adtimeupdate",{type:"adtimeupdate",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,currentTime:a,duration:i})},onAdWaiting:(e,t)=>{this.deps.adBreakStatusTracker.onAdPlaybackWaiting(),this.deps.emit("waiting",{type:"waiting",timestamp:Date.now(),source:"ad",break:e,asset:t})},onAdPlaying:(e,t)=>{this.deps.adBreakStatusTracker.onAdPlaybackPlaying(),this.deps.emit("playing",{type:"playing",timestamp:Date.now(),source:"ad",break:e,asset:t})},onBreakEnd:e=>{this.onBreakEnd(e)},onBreakCut:e=>{this.deps.cmcdAdReporter.onBreakCut(e.id)},onBreakEndedWithoutContentPause:()=>{this.onBreakEndedWithoutContentPause()},onChainedBreakBegin:e=>{this.onChainedBreakBegin(e)},onSuccessorBuffered:(e,t)=>this.deps.breakTransitionTimer?.noteSuccessorBuffered(e,t),onSuccessorDropped:e=>this.deps.breakTransitionTimer?.noteSuccessorDropped(e),onChainedBreakSkipped:e=>{d===this.state.sessionGeneration&&(this.deps.log("Chained break skipped (no playable variant):",e.id),this.deps.breakScheduler.completeBreak(e.id),this.onBreakSkipped(e,"unsupported"))},onContentResumeSeek:(e,t)=>{this.deps.breakScheduler.notifyResumeSeek(e,t)},onContentReloading:e=>{this.deps.breakScheduler.setContentReloading(e)}};h&&(this.pendingAdBreakBegin=()=>this.emitAdBreakBegin(r,s),l.onBreakVisible=e=>{this.deps.adBreakStatusTracker.setAdBreakVisible(e),this.flushPendingAdBreakBegin()}),await this.deps.adPlayerController.playBreak(r,l,{effectiveDurationSec:s.remainingSec,firstFrameGate:h})}onChainedBreakBegin(e){this.deps.log("Chained break starting:",e.id),this.deps.breakTransitionTimer?.mark("break-to-break",e.id),this.deps.breakScheduler.activateChainedBreak(e.id),this.recordBreakBegan(e,!0),this.deps.manifestService.setActive(!0),this.deps.adBreakStatusTracker.setAdBreakStatusActive(e,e.duration),this.emitAdBreakBegin(e)}onBreakEnd(e){this.deps.log("Break ended:",e.id),this.deps.breakTransitionTimer?.mark("out-of-break",e.id),this.deps.breakChainRecorder?.breakEnded(e.id),this.deps.breakScheduler.completeBreak(e.id),this.deps.manifestService.setActive(!1),this.deps.adBreakStatusTracker.resetAdBreakStatus(),this.flushPendingAdBreakBegin(),this.deps.emit("adbreakend",{type:"adbreakend",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e)}),this.deps.onBreakEnded?.(),this.deps.onBreakSettled?.()}onBreakEndedWithoutContentPause(){this.deps.breakTransitionTimer?.settle("out-of-break"),this.deps.emit("playing",{type:"playing",timestamp:Date.now(),source:"content"})}onBreakAborted(e){if(this.deps.adPlayerController.isAdPlaying()){const t=this.deps.adBreakStatusTracker.currentBreakElapsedSec();this.deps.log(`Early return — ending break '${e.id}' at ${t.toFixed(2)}s`),this.deps.cmcdAdReporter.onBreakAborted(e.id),this.deps.adPlayerController.abortBreak(t)}else this.deps.breakScheduler.completeBreak(e.id),this.deps.onBreakSettled?.()}reportAbandonedBreak(e){const t=this.deps.adBreakStatusTracker.activeBreak;if(!t)return;const a=this.deps.adBreakStatusTracker.currentAdIndex;this.deps.diagnose("DA-BREAK-ABANDONED",`Session torn down (${e}) while break ${t.id} was still in flight`+(null!==a?"; an ad began and will never end.":"."),{breakId:t.id,cause:e,phase:this.deps.adBreakStatusTracker.adBreakStatus.phase,unbalancedAdBegin:null!==a,...null!==a?{adIndex:a}:{},...null!==this.deps.adBreakStatusTracker.currentAdTotal?{totalAds:this.deps.adBreakStatusTracker.currentAdTotal}:{}})}}
1
+ import{BreakFormat as e}from"../types/BreakManifest";import{playbackErrorContext as t}from"../diagnostics/playbackErrorContext";import{adErrorCode as a}from"../types/AdError";import{VendorAdError as i}from"../types/VendorAdError";export class BreakLifecycle{constructor(e,t){this.state=e,this.deps=t,this.breakBeginEmittedId=null,this.pendingAdBreakBegin=null,t.breakScheduler.setOnChainSuccessorChanged((e,a)=>{t.breakChainRecorder?.successorChanged(a?.id??null),t.adPlayerController.onChainSuccessorChanged()})}recordBreakBegan(e,t){const a=this.deps.breakScheduler.getChainedSuccessor(e.id);this.deps.breakChainRecorder?.breakBegan(e,t,a?.id??null)}emitAdBreakBegin(e,t){this.breakBeginEmittedId!==e.id&&(this.breakBeginEmittedId=e.id,this.pendingAdBreakBegin=null,this.deps.emit("adbreakbegin",{type:"adbreakbegin",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),elapsedSec:t?.elapsedSec??0,remainingSec:t?.remainingSec??e.duration??0,...t?.tuneIn?{tuneIn:{elapsedSec:t.elapsedSec,remainingSec:t.remainingSec}}:{}}))}flushPendingAdBreakBegin(){this.pendingAdBreakBegin?.()}onBreakSkipped(e,t){this.deps.emit("adbreakskipped",{type:"adbreakskipped",timestamp:Date.now(),break:e,reason:t,format:this.deps.effectiveFormatOf(e)})}async onBreakStart(r,s){const d=this.state.sessionGeneration;if(this.state.snapbackSeekInFlight=!1,this.deps.adPlayerController.isAdPlaying()){if(this.deps.diagnose("DA-BREAK-SUPPRESSED-OVERLAP",`Break suppressed (ad already playing): ${r.id}`,{breakId:r.id}),d!==this.state.sessionGeneration)return;return this.deps.breakScheduler.completeBreak(r.id),this.onBreakSkipped(r,"overlap"),void this.deps.onBreakSettled?.()}this.deps.adPlayerController.diagnoseUnknownVariantFormats(r);const n=this.deps.adPlayerController.breakNothingToPlayReason(r);if(n){if(this.deps.log("Break not begun (no playable variant):",r.id,n),this.deps.diagnose("DA-BREAK-NO-PLAYABLE-ASSET",`break ${r.id} has no playable variant (${n}); it will not begin.`,{breakId:r.id,reason:n}),this.deps.emit("aderror",{type:"aderror",timestamp:Date.now(),source:"ad",break:r,format:this.deps.effectiveFormatOf(r),error:new Error(`Break ${r.id} has no playable variant (${n})`)}),d!==this.state.sessionGeneration)return;return this.deps.breakScheduler.completeBreak(r.id),this.onBreakSkipped(r,"unsupported"),void this.deps.onBreakSettled?.()}this.deps.log("Break starting:",r.id,s.tuneIn?"(tune-in)":""),this.deps.breakTransitionTimer?.mark("into-break",r.id),this.breakBeginEmittedId=null,this.pendingAdBreakBegin=null;const o=this.state.heldImmediatePreRollId===r.id,p=this.deps.effectiveFormatOf(r),k=p===e.SINGLE||p===e.LSHAPE_AD,h=o&&k;o&&(this.state.heldImmediatePreRollId=null),this.deps.adBreakStatusTracker.setAdBreakStatusActive(r,void 0!==s.remainingSec?s.remainingSec:r.duration),h||this.emitAdBreakBegin(r,s),this.deps.manifestService.setActive(!0),void 0===s.snapback?.resumePosition&&this.deps.breakScheduler.claimChain(r.id),this.recordBreakBegan(r,!1);const l={onAdStart:(e,t,a,i,r)=>{this.deps.adBreakStatusTracker.onAdStarted(e,t,a,i),this.deps.emit("adbegin",{type:"adbegin",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,adIndex:a,totalAds:i,...r?.adId?{adId:r.adId}:{},...r?.creativeId?{creativeId:r.creativeId}:{},...void 0!==r?.podPosition?{podPosition:r.podPosition}:{}})},onAdEnd:(e,t,a,i,r)=>{this.deps.adBreakStatusTracker.onAdEnded(e,a,i),a+1<i?this.deps.breakTransitionTimer?.mark("ad-to-ad",e.id):this.deps.breakTransitionTimer?.noteOutgoingEnd(),this.deps.emit("adend",{type:"adend",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,adIndex:a,totalAds:i,...r?.adId?{adId:r.adId}:{},...r?.creativeId?{creativeId:r.creativeId}:{}})},onAdError:(e,r,s)=>{this.flushPendingAdBreakBegin(),this.deps.diagnose("DA-AD-PLAYBACK-ERROR",s.message,{breakId:e.id,assetId:r?.id,adErrorCode:a(s),...s instanceof i?{vendorCode:s.vendorCode}:{},...t(s)}),this.deps.emit("aderror",{type:"aderror",timestamp:Date.now(),source:"ad",break:e,format:this.deps.effectiveFormatOf(e),...r?{asset:r}:{},error:s})},onAdTimeUpdate:(e,t,a,i)=>{this.deps.adBreakStatusTracker.onAdTimeUpdated(e,t,a,i),this.deps.emit("adtimeupdate",{type:"adtimeupdate",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,currentTime:a,duration:i})},onAdWaiting:(e,t)=>{this.deps.adBreakStatusTracker.onAdPlaybackWaiting(),this.deps.emit("waiting",{type:"waiting",timestamp:Date.now(),source:"ad",break:e,asset:t})},onAdPlaying:(e,t)=>{this.deps.adBreakStatusTracker.onAdPlaybackPlaying(),this.deps.emit("playing",{type:"playing",timestamp:Date.now(),source:"ad",break:e,asset:t})},onBreakEnd:e=>{this.onBreakEnd(e)},onBreakCut:e=>{this.deps.cmcdAdReporter.onBreakCut(e.id)},onBreakEndedWithoutContentPause:()=>{this.onBreakEndedWithoutContentPause()},onChainedBreakBegin:e=>{this.onChainedBreakBegin(e)},onSuccessorBuffered:(e,t)=>this.deps.breakTransitionTimer?.noteSuccessorBuffered(e,t),onSuccessorDropped:e=>this.deps.breakTransitionTimer?.noteSuccessorDropped(e),onChainedBreakSkipped:e=>{d===this.state.sessionGeneration&&(this.deps.log("Chained break skipped (no playable variant):",e.id),this.deps.breakScheduler.completeBreak(e.id),this.onBreakSkipped(e,"unsupported"))},onContentResumeSeek:(e,t)=>{this.deps.breakScheduler.notifyResumeSeek(e,t)},onContentReloading:e=>{this.deps.breakScheduler.setContentReloading(e)}};h&&(this.pendingAdBreakBegin=()=>this.emitAdBreakBegin(r,s),l.onBreakVisible=e=>{this.deps.adBreakStatusTracker.setAdBreakVisible(e),this.flushPendingAdBreakBegin()}),await this.deps.adPlayerController.playBreak(r,l,{effectiveDurationSec:s.remainingSec,snapbackResumePosition:s.snapback?.resumePosition,firstFrameGate:h})}onChainedBreakBegin(e){this.deps.log("Chained break starting:",e.id),this.deps.breakTransitionTimer?.mark("break-to-break",e.id),this.deps.breakScheduler.activateChainedBreak(e.id),this.recordBreakBegan(e,!0),this.deps.manifestService.setActive(!0),this.deps.adBreakStatusTracker.setAdBreakStatusActive(e,e.duration),this.emitAdBreakBegin(e)}onBreakEnd(e){this.deps.log("Break ended:",e.id),this.deps.breakTransitionTimer?.mark("out-of-break",e.id),this.deps.breakChainRecorder?.breakEnded(e.id),this.deps.breakScheduler.completeBreak(e.id),this.deps.manifestService.setActive(!1),this.deps.adBreakStatusTracker.resetAdBreakStatus(),this.flushPendingAdBreakBegin(),this.deps.emit("adbreakend",{type:"adbreakend",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e)}),this.deps.onBreakEnded?.(),this.deps.onBreakSettled?.()}onBreakEndedWithoutContentPause(){this.deps.breakTransitionTimer?.settle("out-of-break"),this.deps.emit("playing",{type:"playing",timestamp:Date.now(),source:"content"})}onBreakAborted(e){if(this.deps.adPlayerController.isAdPlaying()){const t=this.deps.adBreakStatusTracker.currentBreakElapsedSec();this.deps.log(`Early return — ending break '${e.id}' at ${t.toFixed(2)}s`),this.deps.cmcdAdReporter.onBreakAborted(e.id),this.deps.adPlayerController.abortBreak(t)}else this.deps.breakScheduler.completeBreak(e.id),this.deps.onBreakSettled?.()}reportAbandonedBreak(e){const t=this.deps.adBreakStatusTracker.activeBreak;if(!t)return;const a=this.deps.adBreakStatusTracker.currentAdIndex;this.deps.diagnose("DA-BREAK-ABANDONED",`Session torn down (${e}) while break ${t.id} was still in flight`+(null!==a?"; an ad began and will never end.":"."),{breakId:t.id,cause:e,phase:this.deps.adBreakStatusTracker.adBreakStatus.phase,unbalancedAdBegin:null!==a,...null!==a?{adIndex:a}:{},...null!==this.deps.adBreakStatusTracker.currentAdTotal?{totalAds:this.deps.adBreakStatusTracker.currentAdTotal}:{}})}}
@@ -1 +1 @@
1
- import{sessionTick as e}from"./sessionTick";import{getPreRollDelay as s}from"../types/BreakManifest";import{manifestHasGamAssets as t,resolveGamVendorConfiguration as a,resolveSsbStream as i}from"../services/resolveGamVendorConfiguration";import{SsaiController as r}from"../services/SsaiController";import{resolveUrlOrigin as n}from"../services/urlOrigin";import{filterSsaiClientBreaks as o,resolveStitchedStreamVendor as d,SsaiUnavailableError as l}from"../services/resolveDeliveryMode";import{detectChannelSwitch as h,gamPodIdentityChanged as c}from"../services/channelSwitch";import{prepareWebRuntime as p}from"../services/webCompatibility";import{adoptExternalVendorConfigurations as S,diagnoseExternalVendorConfigurations as m,diagnoseIgnoredDeliveryModeChange as f,emitChannelChange as u,handleEventBoundaryTick as A,isStaleSessionStart as y,isNonBlankString as g,isSsaiSessionConfig as v,rejectGamPodPreloadForBoundaryIdentity as P,resolveEventAtPosition as I,resolveSessionDeliveryMode as k}from"./SessionStarterHelpers";const C=1e4;export class SessionStarter{constructor(e,s){this.state=e,this.deps=s}async startSession(t){if("ssai"===this.deps.mode){if(!v(t)||!g(t.stitcherUrl))throw new Error('[OptiViewAds] mode "ssai" requires a stitcherUrl in startSession().')}else if(v(t)||!g(t.manifestUrl))throw new Error("[OptiViewAds] startSession requires a manifestUrl (the full break-manifest URL).");this.state.sessionActive&&(this.deps.log("Ending previous session before starting new one"),this.deps.endSessionInternal("session-restart"));const a=this.state.sessionGeneration;this.state.sessionActive=!0,this.state.adStartDelayElapsed=!1;try{const e=p();e&&await e}catch(e){if(this.isStaleStart(a))return;throw this.state.sessionActive=!1,e}if(this.isStaleStart(a))return;if(this.deps.lensReporter.startSession(),v(t))return t.adInsertionType&&this.deps.diagnose("DA-INSERTION-TYPE-IGNORED-SSAI",`adInsertionType "${t.adInsertionType}" ignored in SSAI mode; the stitcher controls ad insertion.`,{adInsertionType:t.adInsertionType}),void await this.startSsaiSession(t,a);const r=t.manifestUrl;this.state.sessionConfig=t,this.state.manifestOrigin=n(r),this.deps.manifestService.setManifestUrl(r),this.deps.log("Session starting, manifest URL:",r);const o=Date.now(),d=e=>{this.deps.diagnose("DA-SESSION-STARTING",`Session start: ${e}`,{step:e,manifestUrl:r,elapsedMs:Date.now()-o})};d("begin");const l=t.assetParameters;let h,c;this.deps.adPlayerController.setSessionAssetParameters(l),this.deps.adPlayerController.setSessionAssetParameterMacros(t.assetParameterMacros),this.state.configSummary.gam&&(this.state.configSummary.gam={...this.state.configSummary.gam,hasAssetParameters:!!l}),d("manifest-fetch");try{c=await this.deps.manifestService.fetchManifest();const e=I(c,Date.now(),this.deps.player);h=e.manifest,this.state.sessionManifest=c,this.state.effectiveSessionManifest=h,this.state.activeEventId=e.eventId}catch(e){if(this.isStaleStart(a))return;throw this.failSessionStart(e)}if(this.deps.cmcdAdReporter.onSessionStart(h),!this.isStaleStart(a))if(this.deps.externalPlayback.setManaged(null!==i(h.vendorConfiguration)),S(this.deps,h),"ssai"!==this.resolveSessionDeliveryMode(h))try{if(await this.startGamForManifest(h,l,d,a,!0),this.isStaleStart(a))return;const i=t.adInsertionType;this.deps.adPlayerController.setBreakResumePolicy(i,h.timebase,this.deps.continueContentDuringBreak),this.deps.diagnose("DA-INSERTION-TYPE-RESOLVED",void 0===i?`Ad insertion type: unset — the manifest's resumeOffset drives the resume point (timebase: ${h.timebase})`:`Ad insertion type: ${i}, client-set (timebase: ${h.timebase})`,{adInsertionType:i??null,source:void 0===i?"unset":"client",timebase:h.timebase}),d("scheduling"),this.deps.breakScheduler.updateManifest(h),this.deps.pauseAdController.updateManifest(h),this.state.pauseAdTickInterval=setInterval(e(this.deps),250);const n=(h.adStartDelay??0)>0?void 0:h.breaks.find(e=>0===s(e));n?(this.deps.adPlayerController.holdContentForPreRoll(n),this.state.heldImmediatePreRollId=n.id,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=Date.now(),this.contentIsReady()?this.armHeldImmediatePreRoll():this.state.heldPreRollArmInterval=setInterval(()=>{this.contentIsReady()?this.armHeldImmediatePreRoll():this.armHeldImmediatePreRollIfTimedOut()},250)):this.markPlaybackStartIfPlaying(),this.deps.startLivePushback(),this.startBreakScheduler(),this.startManifestPolling(),this.deps.diagnose("DA-SESSION-STARTED",`Session active for manifest ${r}`,{manifestUrl:r}),this.deps.log("Session active")}catch(e){if(this.isStaleStart(a))return;throw this.failSessionStart(e)}else await this.startSteeredSsaiSession(h,t,d)}discardPreparedGamSessionAfterSeek(){this.state.sessionActive&&(this.deps.adPlayerController?.gamPodBridge.discardPreparedGamSession(),this.state.eventSessionPreloadKeys.clear())}markPlaybackStartIfPlaying(){this.deps.player.paused||this.deps.breakScheduler.markPlaybackStart()}contentIsReady(){return!Number.isNaN(this.deps.player.duration)||!this.deps.player.paused}clearHeldImmediatePreRollArmInterval(){this.state.heldPreRollArmInterval&&(clearInterval(this.state.heldPreRollArmInterval),this.state.heldPreRollArmInterval=null)}armHeldImmediatePreRoll(){this.state.heldImmediatePreRollId&&!this.state.heldPreRollArmed&&(this.clearHeldImmediatePreRollArmInterval(),this.state.heldPreRollArmed=!0,this.deps.breakScheduler.markPlaybackStart())}armHeldImmediatePreRollIfTimedOut(){if(!this.state.heldImmediatePreRollId||this.state.heldPreRollArmed)return;const e=this.state.heldPreRollWaitStartedAt;if(null===e||Date.now()-e<C)return;const s=this.state.heldImmediatePreRollId;this.armHeldImmediatePreRoll(),this.deps.diagnose("DA-PREROLL-CONTENT-WAIT-TIMEOUT","Pending pre-roll began after waiting 10000ms for content readiness.",{breakId:s,waitMs:C})}cancelHeldImmediatePreRollOnContentError(){const e=this.state.heldImmediatePreRollId;return!(!e||this.state.heldPreRollArmed)&&(this.clearHeldImmediatePreRollArmInterval(),this.deps.breakScheduler?.completeBreak(e),this.deps.adPlayerController?.releaseContentHold(),this.state.heldImmediatePreRollId=null,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=null,this.deps.diagnose("DA-PREROLL-CANCELLED-CONTENT-ERROR","A pending pre-roll was cancelled because the content failed to load before it could begin.",{breakId:e}),!0)}clearHeldImmediatePreRollArm(){this.clearHeldImmediatePreRollArmInterval(),this.state.heldImmediatePreRollId=null,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=null}failSessionStart(e){const s=e instanceof Error?e:new Error(String(e)),t=this.deps.manifestService?.takeLastFailure();return this.deps.diagnose("DA-MANIFEST-FETCH-FAILED",s.message,t?{...t}:void 0),this.state.sessionActive=!1,this.deps.cmcdAdReporter.onSessionEnd(),this.deps.lensReporter.endSession(),s}resolveSessionDeliveryMode(e,s="manifest"){return k(this.state,this.deps,e,s)}async startSteeredSsaiSession(s,t,a){const i=d(s.vendorConfiguration);if(!i){const e="Manifest selected ssai delivery but no vendor configuration provides a stitched stream (no gam entry with an assetKey).";throw this.deps.diagnose("DA-SSAI-UNAVAILABLE",e),this.deps.manifestService.reset(),this.state.sessionActive=!1,new l(e)}try{a("ssai-live-stream"),this.state.steeredSsaiActive=!0,this.state.ssaiController=this.createSsaiController(i.networkCode),await this.state.ssaiController.startSteered(i,t.assetParameters,t.assetParameterMacros),a("scheduling");const r=o(s);this.deps.adPlayerController.setBreakResumePolicy(t.adInsertionType,s.timebase,!1),this.deps.breakScheduler.updateManifest(r),this.deps.pauseAdController.updateManifest(r),this.state.pauseAdTickInterval=setInterval(e(this.deps),250),this.markPlaybackStartIfPlaying(),this.deps.startLivePushback(),this.startBreakScheduler(),this.startManifestPolling(),this.deps.diagnose("DA-SESSION-STARTED",`SSAI session active (manifest-steered) for ${t.manifestUrl}`,{manifestUrl:t.manifestUrl}),this.deps.log("SSAI session active (manifest-steered)")}catch(e){const s=e instanceof Error?e:new Error(String(e));throw this.deps.diagnose("DA-SSAI-SESSION-FAILED",s.message),this.state.steeredSsaiActive=!1,this.deps.manifestService.reset(),this.state.sessionActive=!1,s}}createSsaiController(e){return new r(this.deps.player,{gam:this.state.steeredSsaiOptions?.streamActivityMonitorId?{streamActivityMonitorId:this.state.steeredSsaiOptions.streamActivityMonitorId}:{},ssai:{networkCode:e},maxBitrate:this.state.steeredSsaiOptions?.maxBitrate,autoplay:this.state.steeredSsaiOptions?.autoplay,debug:this.deps.debug},{emit:(e,s)=>this.deps.emit(e,s),diagnose:(e,s,t)=>this.deps.diagnose(e,s,t),log:(...e)=>this.deps.log(...e)})}startBreakScheduler(){this.deps.breakScheduler.setEventBoundaryCheck(e=>this.handleEventBoundaryTick(e)),this.deps.breakScheduler.start((e,s)=>{this.deps.breakLifecycle.onBreakStart(e,s)},e=>{if(this.deps.adPlayerController.isAdPlaying())return!1;if(this.deps.adPlayerController.gamPodBridge.shouldSkipGamPodPreload(e))return!1;const s=P(this.state,this.deps,this.deps.player,e);return"defer"!==s&&("mismatch"===s||this.deps.adPlayerController.preload(e).catch(s=>{this.deps.diagnose("DA-PRELOAD-FAILED",s instanceof Error?s.message:String(s),{breakId:e.id})}),!0)},(e,s,t)=>{this.deps.adBreakStatusTracker.onBreakWarning(e,s,t)},e=>{this.deps.breakLifecycle.onBreakAborted(e)},(e,s)=>this.deps.breakLifecycle.onBreakSkipped(e,s))}startManifestPolling(){this.deps.manifestService.startPolling(e=>{if(!this.state.sessionActive)return;const s=this.deps.notedManifestBreakIds(e);this.deps.diagnose("DA-MANIFEST-POLLED","Manifest polled",{breaks:e.breaks.length,newBreakIds:s,pollingIntervalSeconds:this.deps.manifestService.currentPollingIntervalSeconds});const t=this.state.sessionManifest,a=t?h(t,e):null;if(this.state.pendingChannelSwitch||a&&(this.deps.breakScheduler.getActiveBreak()||this.state.ssb))return this.state.pendingChannelSwitch=e,void this.deps.log("Channel switch deferred until the active break ends");if(a)return this.state.sessionManifest=e,this.state.pendingManifestUpdate=null,void this.switchChannel(e,a);const i="wallclock"===e.timebase&&!1!==this.deps.player.isLive&&Boolean(e.events?.length||t?.events?.length);this.state.sessionManifest=e,i?this.state.pendingManifestUpdate=e:(this.state.pendingManifestUpdate=null,this.state.effectiveSessionManifest=e,this.applyManifestUpdate(e))},e=>{this.state.sessionActive&&this.deps.diagnose("DA-MANIFEST-POLL-FAILED",e.message)})}applyPendingChannelSwitch(){if(!this.state.sessionActive||!this.state.pendingChannelSwitch||this.state.ssb)return;const e=this.state.pendingChannelSwitch;this.state.pendingChannelSwitch=null;const s=h(this.state.sessionManifest,e);s?this.switchChannel(e,s):this.applyManifestUpdate(e)}applyManifestUpdate(e){if(!this.state.sessionActive)return;S(this.deps,e),f(this.state,this.deps,e);const s=a(e);this.state.steeredSsaiActive?this.state.ssaiController?.setManifestAssetParameters(s.ssai?.assetParameters):this.deps.adPlayerController.setManifestAssetParameters(s.sgaiAssetParameters);const t=this.state.steeredSsaiActive?o(e):e;this.deps.breakScheduler.updateManifest(t),this.deps.pauseAdController.updateManifest(t)}handleEventBoundaryTick(e){return A(this.state,this.deps,e,{resolveDeliveryMode:e=>this.resolveSessionDeliveryMode(e,"event-boundary"),applyManifestUpdate:e=>this.applyManifestUpdate(e),switchChannel:(e,s,t,a)=>this.switchChannel(e,s,t,a)})}async switchChannel(e,s,t=!1,a=e){if(this.state.channelSwitchInFlight)return;const r=this.state.sessionConfig,n=this.state.effectiveSessionManifest??this.state.sessionManifest;if(!t){const s=this.deps.player.programDateTime?.getTime()??Date.now(),t=I(a,s,this.deps.player);e=t.manifest,this.state.activeEventId=t.eventId}const o=t?"event-boundary":s.reason,d={previousChannelId:s.previousChannelId,channelId:s.channelId,eventId:this.state.activeEventId,reason:o,manifestUrl:r.manifestUrl};this.deps.diagnose("DA-MANIFEST-CHANNEL-CHANGED",`Manifest channel changed (${o}): ${s.previousChannelId??"none"} -> ${s.channelId??"none"}`,d),this.state.sessionManifest=a,this.state.effectiveSessionManifest=e,this.state.pendingManifestUpdate=null,this.deps.externalPlayback.setManaged(null!==i(e.vendorConfiguration)),this.state.channelSwitchInFlight=!0;const l=this.state.sessionGeneration,h=this.state.sessionDeliveryMode??"sgai";try{let s=this.resolveSessionDeliveryMode(e,t?"event-boundary":"channel-switch");if(t?this.deps.cmcdAdReporter.updateEventId(this.state.activeEventId):(this.deps.cmcdAdReporter.onSessionEnd(),this.deps.cmcdAdReporter.onSessionStart(e),this.deps.lensReporter.endSession(),this.deps.lensReporter.startSession()),"ssai"===s&&"sgai"===h){const a=await this.switchToSteeredSsai(e,r,t);if(this.isStaleStart(l))return;a||(s="sgai",this.state.sessionDeliveryMode="sgai",await this.applySameModeChannelSwitch(n,e,r,l,t))}else"sgai"===s&&"ssai"===h?await this.switchToSgai(e,r,l,t):await this.applySameModeChannelSwitch(n,e,r,l,t);if(this.isStaleStart(l))return}finally{this.state.channelSwitchInFlight=!1,t&&this.deps.breakScheduler.releaseBreakStarts()}S(this.deps,e),this.deps.startLivePushback(),u(this.deps,{...d,previousDeliveryMode:h,deliveryMode:this.state.sessionDeliveryMode??h})}async applySameModeChannelSwitch(e,s,t,i,r=!1){const n=this.state.steeredSsaiActive,d=n?o(s):s;if(r?this.deps.breakScheduler.applyEventBoundary(d):this.deps.breakScheduler.resetForChannelSwitch(d),this.deps.pauseAdController.updateManifest(d),this.deps.adPlayerController.setBreakResumePolicy(t.adInsertionType,s.timebase,this.deps.continueContentDuringBreak),!n&&c(e,s)){const e=this.deps.adPlayerController,n=a(s);if(!(!r||!n.identity)&&await e.gamPodBridge.adoptPreparedGamSession(n.identity,t.assetParameters,n.sgaiAssetParameters))e.updatedAssetParameters&&e.updateAssetParameters(e.updatedAssetParameters);else{const a=e.updatedAssetParameters,r=e.sessionAssetParameterMacros;if(e.endGamSession(),e.setSessionAssetParameters(t.assetParameters),e.setSessionAssetParameterMacros(r),await this.startGamForManifest(s,t.assetParameters,e=>{this.deps.diagnose("DA-SESSION-STARTING",`Channel switch: ${e}`,{step:e,manifestUrl:t.manifestUrl})},i),this.isStaleStart(i))return;a&&e.updateAssetParameters(a)}}else if(n){const e=a(s);this.state.ssaiController?.setManifestAssetParameters(e.ssai?.assetParameters)}else{const e=a(s);this.deps.adPlayerController.setManifestAssetParameters(e.sgaiAssetParameters)}}async switchToSteeredSsai(e,s,t=!1){const a=d(e.vendorConfiguration);if(!a)return this.deps.diagnose("DA-SSAI-UNAVAILABLE","Channel switch resolved ssai delivery but no vendor configuration provides a stitched stream; the session stays in sgai.",{manifestUrl:s.manifestUrl,previousMode:"sgai"}),!1;const i=this.deps.adPlayerController,r=i.updatedAssetParameters,n=i.sessionAssetParameterMacros;i.stopActiveBreak(),i.releaseContentHold(),i.endGamSession(),i.setSessionAssetParameters(s.assetParameters),i.setSessionAssetParameterMacros(n),r&&i.updateAssetParameters(r);const l=this.createSsaiController(a.networkCode);this.state.ssaiController=l,this.state.steeredSsaiActive=!0,this.deps.diagnose("DA-SESSION-STARTING","Channel switch: ssai-live-stream",{step:"ssai-live-stream",manifestUrl:s.manifestUrl});try{await l.startSteered(a,s.assetParameters,n),r&&l.updateAssetParameters(r);const d=o(e);return t?this.deps.breakScheduler.applyEventBoundary(d):this.deps.breakScheduler.resetForChannelSwitch(d),this.deps.pauseAdController.updateManifest(d),i.setBreakResumePolicy(s.adInsertionType,e.timebase,!1),!0}catch(e){const s=e instanceof Error?e.message:String(e);return this.deps.diagnose("DA-SSAI-SESSION-FAILED",s),l.stop(),delete this.state.ssaiController,this.state.steeredSsaiActive=!1,!1}}async switchToSgai(e,s,t,a=!1){const i=this.state.ssaiController?.sessionAssetParameterMacros;this.state.ssaiController?.stop(),delete this.state.ssaiController,this.state.steeredSsaiActive=!1,a?this.deps.breakScheduler.applyEventBoundary(e):this.deps.breakScheduler.resetForChannelSwitch(e),this.deps.pauseAdController.updateManifest(e);const r=this.deps.adPlayerController;r.setBreakResumePolicy(s.adInsertionType,e.timebase,this.deps.continueContentDuringBreak);const n=r.updatedAssetParameters;r.endGamSession(),r.setSessionAssetParameters(s.assetParameters),r.setSessionAssetParameterMacros(i??s.assetParameterMacros),await this.startGamForManifest(e,s.assetParameters,e=>{this.deps.diagnose("DA-SESSION-STARTING",`Channel switch: ${e}`,{step:e,manifestUrl:s.manifestUrl})},t),this.isStaleStart(t)||n&&r.updateAssetParameters(n)}async startGamForManifest(e,s,i,r=this.state.sessionGeneration,n=!1){const o=a(e);if(this.deps.adPlayerController.setManifestAssetParameters(o.sgaiAssetParameters),o.unknownTypes.length>0&&this.deps.diagnose("DA-VENDOR-CONFIG-UNKNOWN-TYPE",`Ignored ${o.unknownTypes.length} vendorConfiguration `+(1===o.unknownTypes.length?"entry":"entries")+" with unrecognised type: "+`${o.unknownTypes.join(", ")}.`,{types:o.unknownTypes}),m(this.deps,e),!this.deps.adPlayerController.isGamEnabled())return void(t(e)&&this.deps.diagnose("DA-GAM-CONFIG-MISSING","The break manifest carries GAM pod assets but GAM is disabled for this session (gam.enabled === false); those assets will be skipped.",{reason:"gam-disabled"}));if(!o.identity)return void(t(e)&&this.deps.diagnose("DA-GAM-CONFIG-MISSING",'The break manifest carries GAM pod assets but no usable "gam" vendorConfiguration entry; those assets will be skipped.',{reason:o.reason}));this.deps.diagnose("DA-GAM-CONFIG-RESOLVED",`GAM session identity: network ${o.identity.networkCode}, custom asset ${o.identity.customAssetKey}`,{networkCode:o.identity.networkCode,customAssetKey:o.identity.customAssetKey,channelId:e.channelId,duplicateEntriesIgnored:o.duplicateGam});const d=e.adStartDelay??0;if(d>0&&!this.state.adStartDelayElapsed&&(n||this.deps.breakScheduler.isAdStartDelayActive()))return this.deps.breakScheduler.holdBreakStarts(),this.deps.diagnose("DA-GAM-START-DEFERRED",`GAM session start deferred until ${d} seconds of content have played.`,{adStartDelaySec:d}),void this.deps.breakScheduler.setOnAdStartDelayElapsed(()=>{if(this.isStaleStart(r)||!this.state.sessionActive)return;this.state.adStartDelayElapsed=!0;const t=this.state.sessionManifest??e,n=a(t).identity;n?(this.deps.adPlayerController.setManifestAssetParameters(a(t).sgaiAssetParameters),this.startResolvedGamSession(n,s,i,r,!0)):this.deps.breakScheduler.releaseBreakStarts()});this.state.adStartDelayElapsed=!0,await this.startResolvedGamSession(o.identity,s,i,r)}async startResolvedGamSession(e,s,t,a,i=!1){try{if(this.isStaleStart(a)||!this.state.sessionActive)return;t("gam-init"),await this.deps.adPlayerController.startGamSession(e,s),this.deps.log("GAM session started")}catch(e){this.deps.diagnose("DA-GAM-SESSION-FAILED",e instanceof Error?e.message:String(e))}finally{i&&!this.isStaleStart(a)&&this.state.sessionActive&&this.deps.breakScheduler.releaseBreakStarts()}}async startSsaiSession(e,s){try{if(this.state.stitcherOrigin=n(e.stitcherUrl),this.state.configSummary.gam&&(this.state.configSummary.gam={...this.state.configSummary.gam,hasAssetParameters:!!e.assetParameters}),await this.state.ssaiController.start(e),this.isStaleStart(s))return;this.deps.diagnose("DA-SESSION-STARTED",`SSAI session active for ${e.stitcherUrl}`,{stitcherUrl:e.stitcherUrl}),this.deps.startLivePushback(),this.deps.log("SSAI session active")}catch(e){if(this.isStaleStart(s))return;const t=e instanceof Error?e:new Error(String(e));throw this.deps.diagnose("DA-SSAI-SESSION-FAILED",t.message),this.state.sessionActive=!1,this.deps.cmcdAdReporter.onSessionEnd(),this.deps.lensReporter.endSession(),t}}isStaleStart(e){return y(this.state,this.deps,e)}}
1
+ import{sessionTick as e}from"./sessionTick";import{configureVodSnapback as s}from"./contentSnapback";import{getPreRollDelay as t}from"../types/BreakManifest";import{manifestHasGamAssets as a,resolveGamVendorConfiguration as i,resolveSsbStream as r}from"../services/resolveGamVendorConfiguration";import{SsaiController as n}from"../services/SsaiController";import{resolveUrlOrigin as o}from"../services/urlOrigin";import{filterSsaiClientBreaks as d,resolveStitchedStreamVendor as l,SsaiUnavailableError as h}from"../services/resolveDeliveryMode";import{detectChannelSwitch as c,gamPodIdentityChanged as p}from"../services/channelSwitch";import{prepareWebRuntime as S}from"../services/webCompatibility";import{adoptExternalVendorConfigurations as m,diagnoseExternalVendorConfigurations as f,diagnoseIgnoredDeliveryModeChange as u,emitChannelChange as A,handleEventBoundaryTick as y,isStaleSessionStart as g,isNonBlankString as v,isSsaiSessionConfig as P,rejectGamPodPreloadForBoundaryIdentity as I,resolveEventAtPosition as k,resolveSessionDeliveryMode as C}from"./SessionStarterHelpers";const M=1e4;export class SessionStarter{constructor(e,s){this.state=e,this.deps=s}async startSession(s){if("ssai"===this.deps.mode){if(!P(s)||!v(s.stitcherUrl))throw new Error('[OptiViewAds] mode "ssai" requires a stitcherUrl in startSession().')}else if(P(s)||!v(s.manifestUrl))throw new Error("[OptiViewAds] startSession requires a manifestUrl (the full break-manifest URL).");this.state.sessionActive&&(this.deps.log("Ending previous session before starting new one"),this.deps.endSessionInternal("session-restart"));const a=this.state.sessionGeneration;this.state.sessionActive=!0,this.state.adStartDelayElapsed=!1;try{const e=S();e&&await e}catch(e){if(this.isStaleStart(a))return;throw this.state.sessionActive=!1,e}if(this.isStaleStart(a))return;if(this.deps.lensReporter.startSession(),P(s))return s.adInsertionType&&this.deps.diagnose("DA-INSERTION-TYPE-IGNORED-SSAI",`adInsertionType "${s.adInsertionType}" ignored in SSAI mode; the stitcher controls ad insertion.`,{adInsertionType:s.adInsertionType}),void await this.startSsaiSession(s,a);const i=s.manifestUrl;this.state.sessionConfig=s,this.state.manifestOrigin=o(i),this.deps.manifestService.setManifestUrl(i),this.deps.log("Session starting, manifest URL:",i);const n=Date.now(),d=e=>{this.deps.diagnose("DA-SESSION-STARTING",`Session start: ${e}`,{step:e,manifestUrl:i,elapsedMs:Date.now()-n})};d("begin");const l=s.assetParameters;let h,c;this.deps.adPlayerController.setSessionAssetParameters(l),this.deps.adPlayerController.setSessionAssetParameterMacros(s.assetParameterMacros),this.state.configSummary.gam&&(this.state.configSummary.gam={...this.state.configSummary.gam,hasAssetParameters:!!l}),d("manifest-fetch");try{c=await this.deps.manifestService.fetchManifest();const e=k(c,Date.now(),this.deps.player);h=e.manifest,this.state.sessionManifest=c,this.state.effectiveSessionManifest=h,this.state.activeEventId=e.eventId}catch(e){if(this.isStaleStart(a))return;throw this.failSessionStart(e)}if(this.deps.cmcdAdReporter.onSessionStart(h),!this.isStaleStart(a))if(this.deps.externalPlayback.setManaged(null!==r(h.vendorConfiguration)),m(this.deps,h),"ssai"!==this.resolveSessionDeliveryMode(h))try{if(await this.startGamForManifest(h,l,d,a,!0),this.isStaleStart(a))return;const r=s.adInsertionType;this.deps.adPlayerController.setBreakResumePolicy(r,h.timebase,this.deps.continueContentDuringBreak),this.deps.diagnose("DA-INSERTION-TYPE-RESOLVED",void 0===r?`Ad insertion type: unset — the manifest's resumeOffset drives the resume point (timebase: ${h.timebase})`:`Ad insertion type: ${r}, client-set (timebase: ${h.timebase})`,{adInsertionType:r??null,source:void 0===r?"unset":"client",timebase:h.timebase}),d("scheduling"),this.deps.breakScheduler.updateManifest(h),this.deps.pauseAdController.updateManifest(h),this.state.pauseAdTickInterval=setInterval(e(this.deps),250);const n=(h.adStartDelay??0)>0?void 0:h.breaks.find(e=>0===t(e));n?(this.deps.adPlayerController.holdContentForPreRoll(n),this.state.heldImmediatePreRollId=n.id,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=Date.now(),this.contentIsReady()?this.armHeldImmediatePreRoll():this.state.heldPreRollArmInterval=setInterval(()=>{this.contentIsReady()?this.armHeldImmediatePreRoll():this.armHeldImmediatePreRollIfTimedOut()},250)):this.markPlaybackStartIfPlaying(),this.deps.startLivePushback(),this.startBreakScheduler(),this.startManifestPolling(),this.deps.diagnose("DA-SESSION-STARTED",`Session active for manifest ${i}`,{manifestUrl:i}),this.deps.log("Session active")}catch(e){if(this.isStaleStart(a))return;throw this.failSessionStart(e)}else await this.startSteeredSsaiSession(h,s,d)}discardPreparedGamSessionAfterSeek(){this.state.sessionActive&&(this.deps.adPlayerController?.gamPodBridge.discardPreparedGamSession(),this.state.eventSessionPreloadKeys.clear())}markPlaybackStartIfPlaying(){this.deps.player.paused||this.deps.breakScheduler.markPlaybackStart()}contentIsReady(){return!Number.isNaN(this.deps.player.duration)||!this.deps.player.paused}clearHeldImmediatePreRollArmInterval(){this.state.heldPreRollArmInterval&&(clearInterval(this.state.heldPreRollArmInterval),this.state.heldPreRollArmInterval=null)}armHeldImmediatePreRoll(){this.state.heldImmediatePreRollId&&!this.state.heldPreRollArmed&&(this.clearHeldImmediatePreRollArmInterval(),this.state.heldPreRollArmed=!0,this.deps.breakScheduler.markPlaybackStart())}armHeldImmediatePreRollIfTimedOut(){if(!this.state.heldImmediatePreRollId||this.state.heldPreRollArmed)return;const e=this.state.heldPreRollWaitStartedAt;if(null===e||Date.now()-e<M)return;const s=this.state.heldImmediatePreRollId;this.armHeldImmediatePreRoll(),this.deps.diagnose("DA-PREROLL-CONTENT-WAIT-TIMEOUT","Pending pre-roll began after waiting 10000ms for content readiness.",{breakId:s,waitMs:M})}cancelHeldImmediatePreRollOnContentError(){const e=this.state.heldImmediatePreRollId;return!(!e||this.state.heldPreRollArmed)&&(this.clearHeldImmediatePreRollArmInterval(),this.deps.breakScheduler?.completeBreak(e),this.deps.adPlayerController?.releaseContentHold(),this.state.heldImmediatePreRollId=null,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=null,this.deps.diagnose("DA-PREROLL-CANCELLED-CONTENT-ERROR","A pending pre-roll was cancelled because the content failed to load before it could begin.",{breakId:e}),!0)}clearHeldImmediatePreRollArm(){this.clearHeldImmediatePreRollArmInterval(),this.state.heldImmediatePreRollId=null,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=null}failSessionStart(e){const s=e instanceof Error?e:new Error(String(e)),t=this.deps.manifestService?.takeLastFailure();return this.deps.diagnose("DA-MANIFEST-FETCH-FAILED",s.message,t?{...t}:void 0),this.state.sessionActive=!1,this.deps.cmcdAdReporter.onSessionEnd(),this.deps.lensReporter.endSession(),s}resolveSessionDeliveryMode(e,s="manifest"){return C(this.state,this.deps,e,s)}async startSteeredSsaiSession(s,t,a){const i=l(s.vendorConfiguration);if(!i){const e="Manifest selected ssai delivery but no vendor configuration provides a stitched stream (no gam entry with an assetKey).";throw this.deps.diagnose("DA-SSAI-UNAVAILABLE",e),this.deps.manifestService.reset(),this.state.sessionActive=!1,new h(e)}try{a("ssai-live-stream"),this.state.steeredSsaiActive=!0,this.state.ssaiController=this.createSsaiController(i.networkCode),await this.state.ssaiController.startSteered(i,t.assetParameters,t.assetParameterMacros),a("scheduling");const r=d(s);this.deps.adPlayerController.setBreakResumePolicy(t.adInsertionType,s.timebase,!1),this.deps.breakScheduler.updateManifest(r),this.deps.pauseAdController.updateManifest(r),this.state.pauseAdTickInterval=setInterval(e(this.deps),250),this.markPlaybackStartIfPlaying(),this.deps.startLivePushback(),this.startBreakScheduler(),this.startManifestPolling(),this.deps.diagnose("DA-SESSION-STARTED",`SSAI session active (manifest-steered) for ${t.manifestUrl}`,{manifestUrl:t.manifestUrl}),this.deps.log("SSAI session active (manifest-steered)")}catch(e){const s=e instanceof Error?e:new Error(String(e));throw this.deps.diagnose("DA-SSAI-SESSION-FAILED",s.message),this.state.steeredSsaiActive=!1,this.deps.manifestService.reset(),this.state.sessionActive=!1,s}}createSsaiController(e){return new n(this.deps.player,{gam:this.state.steeredSsaiOptions?.streamActivityMonitorId?{streamActivityMonitorId:this.state.steeredSsaiOptions.streamActivityMonitorId}:{},ssai:{networkCode:e},maxBitrate:this.state.steeredSsaiOptions?.maxBitrate,autoplay:this.state.steeredSsaiOptions?.autoplay,debug:this.deps.debug},{emit:(e,s)=>this.deps.emit(e,s),diagnose:(e,s,t)=>this.deps.diagnose(e,s,t),log:(...e)=>this.deps.log(...e)})}startBreakScheduler(){s(this.deps.breakScheduler,this.deps.player,e=>"ssai"!==this.state.sessionDeliveryMode&&!this.state.ssb&&null===this.deps.adPlayerController.breakNothingToPlayReason(e)),this.deps.breakScheduler.setEventBoundaryCheck(e=>this.handleEventBoundaryTick(e)),this.deps.breakScheduler.start((e,s)=>{this.deps.breakLifecycle.onBreakStart(e,s)},e=>{if(this.deps.adPlayerController.isAdPlaying())return!1;if(this.deps.adPlayerController.gamPodBridge.shouldSkipGamPodPreload(e))return!1;const s=I(this.state,this.deps,this.deps.player,e);return"defer"!==s&&("mismatch"===s||this.deps.adPlayerController.preload(e).catch(s=>{this.deps.diagnose("DA-PRELOAD-FAILED",s instanceof Error?s.message:String(s),{breakId:e.id})}),!0)},(e,s,t)=>{this.deps.adBreakStatusTracker.onBreakWarning(e,s,t)},e=>{this.deps.breakLifecycle.onBreakAborted(e)},(e,s)=>this.deps.breakLifecycle.onBreakSkipped(e,s))}startManifestPolling(){this.deps.manifestService.startPolling(e=>{if(!this.state.sessionActive)return;const s=this.deps.notedManifestBreakIds(e);this.deps.diagnose("DA-MANIFEST-POLLED","Manifest polled",{breaks:e.breaks.length,newBreakIds:s,pollingIntervalSeconds:this.deps.manifestService.currentPollingIntervalSeconds});const t=this.state.sessionManifest,a=t?c(t,e):null;if(this.state.pendingChannelSwitch||a&&(this.deps.breakScheduler.getActiveBreak()||this.state.ssb))return this.state.pendingChannelSwitch=e,void this.deps.log("Channel switch deferred until the active break ends");if(a)return this.state.sessionManifest=e,this.state.pendingManifestUpdate=null,void this.switchChannel(e,a);const i="wallclock"===e.timebase&&!1!==this.deps.player.isLive&&Boolean(e.events?.length||t?.events?.length);this.state.sessionManifest=e,i?this.state.pendingManifestUpdate=e:(this.state.pendingManifestUpdate=null,this.state.effectiveSessionManifest=e,this.applyManifestUpdate(e))},e=>{this.state.sessionActive&&this.deps.diagnose("DA-MANIFEST-POLL-FAILED",e.message)})}applyPendingChannelSwitch(){if(!this.state.sessionActive||!this.state.pendingChannelSwitch||this.state.ssb)return;const e=this.state.pendingChannelSwitch;this.state.pendingChannelSwitch=null;const s=c(this.state.sessionManifest,e);s?this.switchChannel(e,s):this.applyManifestUpdate(e)}applyManifestUpdate(e){if(!this.state.sessionActive)return;m(this.deps,e),u(this.state,this.deps,e);const s=i(e);this.state.steeredSsaiActive?this.state.ssaiController?.setManifestAssetParameters(s.ssai?.assetParameters):this.deps.adPlayerController.setManifestAssetParameters(s.sgaiAssetParameters);const t=this.state.steeredSsaiActive?d(e):e;this.deps.breakScheduler.updateManifest(t),this.deps.pauseAdController.updateManifest(t)}handleEventBoundaryTick(e){return y(this.state,this.deps,e,{resolveDeliveryMode:e=>this.resolveSessionDeliveryMode(e,"event-boundary"),applyManifestUpdate:e=>this.applyManifestUpdate(e),switchChannel:(e,s,t,a)=>this.switchChannel(e,s,t,a)})}async switchChannel(e,s,t=!1,a=e){if(this.state.channelSwitchInFlight)return;const i=this.state.sessionConfig,n=this.state.effectiveSessionManifest??this.state.sessionManifest;if(!t){const s=this.deps.player.programDateTime?.getTime()??Date.now(),t=k(a,s,this.deps.player);e=t.manifest,this.state.activeEventId=t.eventId}const o=t?"event-boundary":s.reason,d={previousChannelId:s.previousChannelId,channelId:s.channelId,eventId:this.state.activeEventId,reason:o,manifestUrl:i.manifestUrl};this.deps.diagnose("DA-MANIFEST-CHANNEL-CHANGED",`Manifest channel changed (${o}): ${s.previousChannelId??"none"} -> ${s.channelId??"none"}`,d),this.state.sessionManifest=a,this.state.effectiveSessionManifest=e,this.state.pendingManifestUpdate=null,this.deps.externalPlayback.setManaged(null!==r(e.vendorConfiguration)),this.state.channelSwitchInFlight=!0;const l=this.state.sessionGeneration,h=this.state.sessionDeliveryMode??"sgai";try{let s=this.resolveSessionDeliveryMode(e,t?"event-boundary":"channel-switch");if(t?this.deps.cmcdAdReporter.updateEventId(this.state.activeEventId):(this.deps.cmcdAdReporter.onSessionEnd(),this.deps.cmcdAdReporter.onSessionStart(e),this.deps.lensReporter.endSession(),this.deps.lensReporter.startSession()),"ssai"===s&&"sgai"===h){const a=await this.switchToSteeredSsai(e,i,t);if(this.isStaleStart(l))return;a||(s="sgai",this.state.sessionDeliveryMode="sgai",await this.applySameModeChannelSwitch(n,e,i,l,t))}else"sgai"===s&&"ssai"===h?await this.switchToSgai(e,i,l,t):await this.applySameModeChannelSwitch(n,e,i,l,t);if(this.isStaleStart(l))return}finally{this.state.channelSwitchInFlight=!1,t&&this.deps.breakScheduler.releaseBreakStarts()}m(this.deps,e),this.deps.startLivePushback(),A(this.deps,{...d,previousDeliveryMode:h,deliveryMode:this.state.sessionDeliveryMode??h})}async applySameModeChannelSwitch(e,s,t,a,r=!1){const n=this.state.steeredSsaiActive,o=n?d(s):s;if(r?this.deps.breakScheduler.applyEventBoundary(o):this.deps.breakScheduler.resetForChannelSwitch(o),this.deps.pauseAdController.updateManifest(o),this.deps.adPlayerController.setBreakResumePolicy(t.adInsertionType,s.timebase,this.deps.continueContentDuringBreak),!n&&p(e,s)){const e=this.deps.adPlayerController,n=i(s);if(!(!r||!n.identity)&&await e.gamPodBridge.adoptPreparedGamSession(n.identity,t.assetParameters,n.sgaiAssetParameters))e.updatedAssetParameters&&e.updateAssetParameters(e.updatedAssetParameters);else{const i=e.updatedAssetParameters,r=e.sessionAssetParameterMacros;if(e.endGamSession(),e.setSessionAssetParameters(t.assetParameters),e.setSessionAssetParameterMacros(r),await this.startGamForManifest(s,t.assetParameters,e=>{this.deps.diagnose("DA-SESSION-STARTING",`Channel switch: ${e}`,{step:e,manifestUrl:t.manifestUrl})},a),this.isStaleStart(a))return;i&&e.updateAssetParameters(i)}}else if(n){const e=i(s);this.state.ssaiController?.setManifestAssetParameters(e.ssai?.assetParameters)}else{const e=i(s);this.deps.adPlayerController.setManifestAssetParameters(e.sgaiAssetParameters)}}async switchToSteeredSsai(e,s,t=!1){const a=l(e.vendorConfiguration);if(!a)return this.deps.diagnose("DA-SSAI-UNAVAILABLE","Channel switch resolved ssai delivery but no vendor configuration provides a stitched stream; the session stays in sgai.",{manifestUrl:s.manifestUrl,previousMode:"sgai"}),!1;const i=this.deps.adPlayerController,r=i.updatedAssetParameters,n=i.sessionAssetParameterMacros;i.stopActiveBreak(),i.releaseContentHold(),i.endGamSession(),i.setSessionAssetParameters(s.assetParameters),i.setSessionAssetParameterMacros(n),r&&i.updateAssetParameters(r);const o=this.createSsaiController(a.networkCode);this.state.ssaiController=o,this.state.steeredSsaiActive=!0,this.deps.diagnose("DA-SESSION-STARTING","Channel switch: ssai-live-stream",{step:"ssai-live-stream",manifestUrl:s.manifestUrl});try{await o.startSteered(a,s.assetParameters,n),r&&o.updateAssetParameters(r);const l=d(e);return t?this.deps.breakScheduler.applyEventBoundary(l):this.deps.breakScheduler.resetForChannelSwitch(l),this.deps.pauseAdController.updateManifest(l),i.setBreakResumePolicy(s.adInsertionType,e.timebase,!1),!0}catch(e){const s=e instanceof Error?e.message:String(e);return this.deps.diagnose("DA-SSAI-SESSION-FAILED",s),o.stop(),delete this.state.ssaiController,this.state.steeredSsaiActive=!1,!1}}async switchToSgai(e,s,t,a=!1){const i=this.state.ssaiController?.sessionAssetParameterMacros;this.state.ssaiController?.stop(),delete this.state.ssaiController,this.state.steeredSsaiActive=!1,a?this.deps.breakScheduler.applyEventBoundary(e):this.deps.breakScheduler.resetForChannelSwitch(e),this.deps.pauseAdController.updateManifest(e);const r=this.deps.adPlayerController;r.setBreakResumePolicy(s.adInsertionType,e.timebase,this.deps.continueContentDuringBreak);const n=r.updatedAssetParameters;r.endGamSession(),r.setSessionAssetParameters(s.assetParameters),r.setSessionAssetParameterMacros(i??s.assetParameterMacros),await this.startGamForManifest(e,s.assetParameters,e=>{this.deps.diagnose("DA-SESSION-STARTING",`Channel switch: ${e}`,{step:e,manifestUrl:s.manifestUrl})},t),this.isStaleStart(t)||n&&r.updateAssetParameters(n)}async startGamForManifest(e,s,t,r=this.state.sessionGeneration,n=!1){const o=i(e);if(this.deps.adPlayerController.setManifestAssetParameters(o.sgaiAssetParameters),o.unknownTypes.length>0&&this.deps.diagnose("DA-VENDOR-CONFIG-UNKNOWN-TYPE",`Ignored ${o.unknownTypes.length} vendorConfiguration `+(1===o.unknownTypes.length?"entry":"entries")+" with unrecognised type: "+`${o.unknownTypes.join(", ")}.`,{types:o.unknownTypes}),f(this.deps,e),!this.deps.adPlayerController.isGamEnabled())return void(a(e)&&this.deps.diagnose("DA-GAM-CONFIG-MISSING","The break manifest carries GAM pod assets but GAM is disabled for this session (gam.enabled === false); those assets will be skipped.",{reason:"gam-disabled"}));if(!o.identity)return void(a(e)&&this.deps.diagnose("DA-GAM-CONFIG-MISSING",'The break manifest carries GAM pod assets but no usable "gam" vendorConfiguration entry; those assets will be skipped.',{reason:o.reason}));this.deps.diagnose("DA-GAM-CONFIG-RESOLVED",`GAM session identity: network ${o.identity.networkCode}, custom asset ${o.identity.customAssetKey}`,{networkCode:o.identity.networkCode,customAssetKey:o.identity.customAssetKey,channelId:e.channelId,duplicateEntriesIgnored:o.duplicateGam});const d=e.adStartDelay??0;if(d>0&&!this.state.adStartDelayElapsed&&(n||this.deps.breakScheduler.isAdStartDelayActive()))return this.deps.breakScheduler.holdBreakStarts(),this.deps.diagnose("DA-GAM-START-DEFERRED",`GAM session start deferred until ${d} seconds of content have played.`,{adStartDelaySec:d}),void this.deps.breakScheduler.setOnAdStartDelayElapsed(()=>{if(this.isStaleStart(r)||!this.state.sessionActive)return;this.state.adStartDelayElapsed=!0;const a=this.state.sessionManifest??e,n=i(a).identity;n?(this.deps.adPlayerController.setManifestAssetParameters(i(a).sgaiAssetParameters),this.startResolvedGamSession(n,s,t,r,!0)):this.deps.breakScheduler.releaseBreakStarts()});this.state.adStartDelayElapsed=!0,await this.startResolvedGamSession(o.identity,s,t,r)}async startResolvedGamSession(e,s,t,a,i=!1){try{if(this.isStaleStart(a)||!this.state.sessionActive)return;t("gam-init"),await this.deps.adPlayerController.startGamSession(e,s),this.deps.log("GAM session started")}catch(e){this.deps.diagnose("DA-GAM-SESSION-FAILED",e instanceof Error?e.message:String(e))}finally{i&&!this.isStaleStart(a)&&this.state.sessionActive&&this.deps.breakScheduler.releaseBreakStarts()}}async startSsaiSession(e,s){try{if(this.state.stitcherOrigin=o(e.stitcherUrl),this.state.configSummary.gam&&(this.state.configSummary.gam={...this.state.configSummary.gam,hasAssetParameters:!!e.assetParameters}),await this.state.ssaiController.start(e),this.isStaleStart(s))return;this.deps.diagnose("DA-SESSION-STARTED",`SSAI session active for ${e.stitcherUrl}`,{stitcherUrl:e.stitcherUrl}),this.deps.startLivePushback(),this.deps.log("SSAI session active")}catch(e){if(this.isStaleStart(s))return;const t=e instanceof Error?e:new Error(String(e));throw this.deps.diagnose("DA-SSAI-SESSION-FAILED",t.message),this.state.sessionActive=!1,this.deps.cmcdAdReporter.onSessionEnd(),this.deps.lensReporter.endSession(),t}}isStaleStart(e){return g(this.state,this.deps,e)}}
@@ -1,12 +1,16 @@
1
1
  import type { PlayerAdapter } from '../interfaces/PlayerAdapter';
2
- import type { Break } from '../types/BreakManifest';
2
+ import { type Break } from '../types/BreakManifest';
3
3
  import type { SessionState } from './SessionState';
4
+ import type { BreakScheduler } from '../services/BreakScheduler';
5
+ /** VOD snapback is unavailable on TV browsers, including React Native web TV shells. */
6
+ export declare function configureVodSnapback(scheduler: BreakScheduler, player: PlayerAdapter, canPlay: (brk: Break) => boolean): void;
4
7
  export interface SnapbackDeps {
5
8
  player: PlayerAdapter;
6
9
  state: Pick<SessionState, 'snapbackSeekInFlight'>;
7
10
  activeBreak(): Break | null | undefined;
8
11
  /** A viewer seek (not our own corrective one) landed. */
9
12
  onViewerSeek(): void;
13
+ onSeeked?(): void;
10
14
  log(...args: unknown[]): void;
11
15
  }
12
16
  /** The content `seeked` handler: snaps a viewer seek back to the break start while the break locks it. */
@@ -1 +1 @@
1
- export function createSnapbackHandler(e){return()=>{if(e.state.snapbackSeekInFlight)return void(e.state.snapbackSeekInFlight=!1);e.onViewerSeek();const t=e.activeBreak();if(!t?.controls?.snapback)return;const a="number"==typeof t.start?t.start:e.player.currentTime;Math.abs(e.player.currentTime-a)<=.5||(e.log("Snapback: seek detected during locked break, snapping back to break start"),e.state.snapbackSeekInFlight=!0,e.player.seek(a))}}
1
+ import{DeviceType as e}from"../types/BreakManifest";import{detectDeviceProfile as t}from"../services/detectDeviceType";export function configureVodSnapback(a,n,r){a.setSnapbackOnSeek(t().deviceType!==e.TV,r),a.setOnSnapback(e=>n.seek(e.target))}export function createSnapbackHandler(e){return()=>{if(e.state.snapbackSeekInFlight)return void(e.state.snapbackSeekInFlight=!1);e.onViewerSeek(),e.onSeeked?.();const t=e.activeBreak();if(!t?.controls?.snapback)return;const a="number"==typeof t.start?t.start:e.player.currentTime;Math.abs(e.player.currentTime-a)<=.5||(e.log("Snapback: seek detected during locked break, snapping back to break start"),e.state.snapbackSeekInFlight=!0,e.player.seek(a))}}
@@ -25,7 +25,9 @@ export interface VendorConfigurationBase {
25
25
  }
26
26
  /** Session-level GAM configuration for pod-serving SGAI. @category Break Manifests & Ad Formats */
27
27
  export interface GamSgaiConfiguration {
28
+ /** The custom asset key of the channel's pod-serving stream. */
28
29
  customAssetKey: string;
30
+ /** Session-level ad tag parameters for pod requests. */
29
31
  assetParameters?: Record<string, string>;
30
32
  }
31
33
  /**
@@ -44,7 +46,9 @@ export interface GamSsbConfiguration {
44
46
  }
45
47
  /** Session-level GAM configuration for full-service DAI SSAI. @category Break Manifests & Ad Formats */
46
48
  export interface GamSsaiConfiguration {
49
+ /** The asset key of the full-service DAI livestream. */
47
50
  assetKey: string;
51
+ /** Session-level ad tag parameters for the stitched stream request. */
48
52
  assetParameters?: Record<string, string>;
49
53
  /** The channel's server-side beaconing stream, when it has one. */
50
54
  ssb?: GamSsbConfiguration;
@@ -70,9 +74,15 @@ export interface GamVendorConfiguration extends VendorConfigurationBase {
70
74
  sgai?: GamSgaiConfiguration[];
71
75
  ssai?: GamSsaiConfiguration[];
72
76
  }
77
+ /**
78
+ * Ad parameters for a provider outside the SDK, read by the application with
79
+ * `getExternalVendorConfiguration()` or {@link OptiViewAdsExternalVendors}.
80
+ * @category Break Manifests & Ad Formats */
73
81
  export interface ExternalVendorConfigurationEntry extends VendorConfigurationBase {
74
82
  type: 'external';
83
+ /** The provider the parameters are for; the first valid entry per provider is used. */
75
84
  provider: string;
85
+ /** The provider's parameter template; values may contain `$NAME$` macros. */
76
86
  assetParameters: Record<string, string>;
77
87
  }
78
88
  /** Known vendor configuration entries, kept open to unrecognized vendors from newer manifests. @category Break Manifests & Ad Formats */
@@ -104,13 +114,22 @@ export interface BreakManifest {
104
114
  /** The available breaks */
105
115
  breaks: Break[];
106
116
  }
107
- /** An event window and the session configuration in effect while it runs. */
117
+ /**
118
+ * An event window on a wallclock live stream and the session configuration in effect while it
119
+ * runs. Crossing its start or end emits `adchannelchange` with reason `event-boundary`.
120
+ * @category Break Manifests & Ad Formats */
108
121
  export interface ChannelEvent {
122
+ /** Identifier of the event, reported as the manifest's `eventId` while it runs. */
109
123
  id: string;
124
+ /** Start of the event window (ISO-8601 wallclock). */
110
125
  start: string;
126
+ /** End of the event window (ISO-8601 wallclock). */
111
127
  end: string;
128
+ /** When set, replaces the manifest's {@link BreakManifest.adStartDelay} during the event. */
112
129
  adStartDelay?: number;
130
+ /** When set, replaces the manifest's {@link BreakManifest.delivery} rules during the event. */
113
131
  delivery?: DeliveryRule[];
132
+ /** When set, replaces the manifest's {@link BreakManifest.vendorConfiguration} during the event. */
114
133
  vendorConfiguration?: VendorConfiguration[];
115
134
  }
116
135
  /** Delivery architecture selected by a {@link DeliveryRule}. @category Break Manifests & Ad Formats */
@@ -205,7 +224,7 @@ export declare enum DeviceType {
205
224
  TABLET = "tablet",
206
225
  TV = "tv"
207
226
  }
208
- /** Platforms supported by manifest targeting (manifest spec v1.7.0). */
227
+ /** Platforms supported by manifest targeting (manifest spec v1.7.0). @category Break Manifests & Ad Formats */
209
228
  export declare enum Platform {
210
229
  WEB = "web",
211
230
  ANDROID = "android",
@@ -213,7 +232,7 @@ export declare enum Platform {
213
232
  TVOS = "tvos",
214
233
  ROKU = "roku"
215
234
  }
216
- /** Operating systems supported by manifest targeting (manifest spec v1.8.0). */
235
+ /** Operating systems supported by manifest targeting (manifest spec v1.8.0). @category Break Manifests & Ad Formats */
217
236
  export declare enum Os {
218
237
  ANDROID = "android",
219
238
  CHROMEOS = "chromeos",
@@ -229,9 +248,11 @@ export declare enum Os {
229
248
  WINDOWS = "windows",
230
249
  XBOX = "xbox"
231
250
  }
232
- /** Inclusive operating-system version bounds; each bound is dot-separated decimal integers. */
251
+ /** Inclusive operating-system version bounds; each bound is dot-separated decimal integers. @category Break Manifests & Ad Formats */
233
252
  export interface OsVersionRange {
253
+ /** Lowest matching version, e.g. `"14"` or `"17.2"`. */
234
254
  min?: string;
255
+ /** Highest matching version, e.g. `"18.1"`. */
235
256
  max?: string;
236
257
  }
237
258
  /** Targeting criteria for break variants. @category Break Manifests & Ad Formats */
@@ -270,7 +291,7 @@ export interface OverlaySize {
270
291
  width: number;
271
292
  height: number;
272
293
  }
273
- /** Double-box variant — ad on one side, companion on the other. Content pauses. @category Break Manifests & Ad Formats */
294
+ /** Double-box variant — ad alongside continuing content, with a companion backdrop. @category Break Manifests & Ad Formats */
274
295
  export interface DoubleBreakVariant extends BaseBreakVariant {
275
296
  format: BreakFormat.DOUBLE;
276
297
  /** Paired ad + companion assets. */
@@ -244,6 +244,7 @@ export interface AdChannelChangeEvent extends OptiViewAdsEvent {
244
244
  /** An external provider's manifest parameter template was added, changed, or removed. @category Events */
245
245
  export interface ExternalVendorConfigurationChangeEvent extends OptiViewAdsEvent {
246
246
  type: 'externalvendorconfigurationchange';
247
+ /** The provider whose template changed; read its new configuration with `getExternalVendorConfiguration(provider)`. */
247
248
  provider: string;
248
249
  }
249
250
  /** Union type of all SDK events. @category Events */