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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/dist/OptiViewAds.d.ts +5 -0
  2. package/dist/OptiViewAds.js +1 -1
  3. package/dist/diagnostics/dispatchDiagnostic.d.ts +4 -0
  4. package/dist/diagnostics/dispatchDiagnostic.js +1 -0
  5. package/dist/generated/diagnosticCodes.d.ts +104 -4
  6. package/dist/generated/diagnosticCodes.js +1 -1
  7. package/dist/generated/version.d.ts +1 -1
  8. package/dist/generated/version.js +1 -1
  9. package/dist/index.d.ts +11 -4
  10. package/dist/index.js +1 -1
  11. package/dist/interfaces/PlayerAdapter.d.ts +27 -0
  12. package/dist/server/index.d.ts +2 -2
  13. package/dist/server/index.js +1 -1
  14. package/dist/services/AdBreakSequencer.d.ts +67 -3
  15. package/dist/services/AdBreakSequencer.js +1 -1
  16. package/dist/services/AdPlayerController.d.ts +9 -4
  17. package/dist/services/AdPlayerController.js +1 -1
  18. package/dist/services/BookmarkStart.d.ts +12 -0
  19. package/dist/services/BookmarkStart.js +1 -0
  20. package/dist/services/BreakScheduler.d.ts +8 -35
  21. package/dist/services/BreakScheduler.js +1 -1
  22. package/dist/services/BreakSchedulerTypes.d.ts +7 -1
  23. package/dist/services/BreakWarningTracker.d.ts +6 -0
  24. package/dist/services/BreakWarningTracker.js +1 -0
  25. package/dist/services/ContentEndTracker.d.ts +17 -0
  26. package/dist/services/ContentEndTracker.js +1 -0
  27. package/dist/services/ExternalPlaybackGate.d.ts +2 -2
  28. package/dist/services/ManifestService.d.ts +1 -1
  29. package/dist/services/ManifestService.js +1 -1
  30. package/dist/services/PauseAdController.d.ts +9 -5
  31. package/dist/services/PauseAdController.js +1 -1
  32. package/dist/services/PlaybackCrossing.d.ts +4 -0
  33. package/dist/services/PlaybackCrossing.js +1 -0
  34. package/dist/services/RewindRearm.js +1 -1
  35. package/dist/services/SchedulerSnapback.d.ts +1 -0
  36. package/dist/services/SchedulerSnapback.js +1 -1
  37. package/dist/services/SlateClock.d.ts +22 -0
  38. package/dist/services/SlateClock.js +1 -0
  39. package/dist/services/SnapbackGuard.d.ts +36 -2
  40. package/dist/services/SnapbackGuard.js +1 -1
  41. package/dist/services/VastAdManager.d.ts +2 -0
  42. package/dist/services/VastAdManager.js +1 -1
  43. package/dist/services/adPlayer/AdPlayerCallbacks.d.ts +9 -1
  44. package/dist/services/adPlayer/AdPlayerEvents.d.ts +21 -1
  45. package/dist/services/adPlayer/AdPlayerEvents.js +1 -1
  46. package/dist/services/adPlayer/AdPlayerState.d.ts +13 -0
  47. package/dist/services/adPlayer/AdPlayerState.js +1 -1
  48. package/dist/services/adPlayer/AdPreloader.d.ts +8 -1
  49. package/dist/services/adPlayer/AdPreloader.js +1 -1
  50. package/dist/services/adPlayer/AdStage.js +1 -1
  51. package/dist/services/adPlayer/AssetPlayback.d.ts +1 -0
  52. package/dist/services/adPlayer/AssetPlayback.js +1 -1
  53. package/dist/services/adPlayer/BreakSequenceDriver.d.ts +7 -1
  54. package/dist/services/adPlayer/BreakSequenceDriver.js +1 -1
  55. package/dist/services/adPlayer/ContentResumeCoordinator.d.ts +6 -0
  56. package/dist/services/adPlayer/ContentResumeCoordinator.js +1 -1
  57. package/dist/services/adPlayer/PauseAdPresenter.d.ts +10 -9
  58. package/dist/services/adPlayer/PauseAdPresenter.js +1 -1
  59. package/dist/services/adPlayer/PauseAdTracking.d.ts +13 -0
  60. package/dist/services/adPlayer/PauseAdTracking.js +1 -0
  61. package/dist/services/adPlayer/SharedElementBreakPlayer.d.ts +31 -2
  62. package/dist/services/adPlayer/SharedElementBreakPlayer.js +1 -1
  63. package/dist/services/adPlayer/SlateCoordinator.d.ts +58 -0
  64. package/dist/services/adPlayer/SlateCoordinator.js +1 -0
  65. package/dist/services/adPlayer/SlatePlayback.d.ts +89 -0
  66. package/dist/services/adPlayer/SlatePlayback.js +1 -0
  67. package/dist/services/adPlayer/VariantSelector.d.ts +10 -1
  68. package/dist/services/adPlayer/VariantSelector.js +1 -1
  69. package/dist/services/adPlayer/connectPauseAd.js +1 -1
  70. package/dist/services/adPlayer/presentation/PresentationLayout.d.ts +5 -2
  71. package/dist/services/adPlayer/presentation/PresentationLayout.js +1 -1
  72. package/dist/services/breakTiming.d.ts +1 -0
  73. package/dist/services/breakTiming.js +1 -1
  74. package/dist/services/parseBreakManifest.d.ts +2 -1
  75. package/dist/services/parseBreakManifest.js +1 -1
  76. package/dist/services/parseVastPause.d.ts +18 -0
  77. package/dist/services/parseVastPause.js +1 -0
  78. package/dist/services/resolveLshapeAnchor.d.ts +25 -0
  79. package/dist/services/resolveLshapeAnchor.js +1 -0
  80. package/dist/services/resolveSlate.d.ts +25 -0
  81. package/dist/services/resolveSlate.js +1 -0
  82. package/dist/services/resolveSlateFill.d.ts +39 -0
  83. package/dist/services/resolveSlateFill.js +1 -0
  84. package/dist/services/ssbSwitchSignals.d.ts +3 -0
  85. package/dist/services/ssbSwitchSignals.js +1 -1
  86. package/dist/session/AdBreakStatusTracker.d.ts +22 -2
  87. package/dist/session/AdBreakStatusTracker.js +1 -1
  88. package/dist/session/BreakLifecycle.js +1 -1
  89. package/dist/session/ContentPlaybackStart.d.ts +23 -0
  90. package/dist/session/ContentPlaybackStart.js +1 -0
  91. package/dist/session/DiscoveredManifestUrl.d.ts +44 -0
  92. package/dist/session/DiscoveredManifestUrl.js +1 -0
  93. package/dist/session/ScheduledBreaksTracker.d.ts +22 -0
  94. package/dist/session/ScheduledBreaksTracker.js +1 -0
  95. package/dist/session/SessionStarter.d.ts +3 -9
  96. package/dist/session/SessionStarter.js +1 -1
  97. package/dist/session/SessionStarterDeps.d.ts +3 -0
  98. package/dist/session/SessionStarterHelpers.d.ts +22 -1
  99. package/dist/session/SessionStarterHelpers.js +1 -1
  100. package/dist/session/SessionState.d.ts +16 -0
  101. package/dist/session/SessionState.js +1 -1
  102. package/dist/session/SsbSwitch.js +1 -1
  103. package/dist/session/buildScheduledBreaks.d.ts +9 -0
  104. package/dist/session/buildScheduledBreaks.js +1 -0
  105. package/dist/session/contentSnapback.d.ts +2 -2
  106. package/dist/session/contentSnapback.js +1 -1
  107. package/dist/session/scheduledBreaksSession.d.ts +6 -0
  108. package/dist/session/scheduledBreaksSession.js +1 -0
  109. package/dist/types/BreakManifest.d.ts +39 -1
  110. package/dist/types/BreakManifest.js +1 -1
  111. package/dist/types/Events.d.ts +13 -4
  112. package/dist/types/OptiViewAdsConfig.d.ts +15 -2
  113. package/dist/types/ScheduledBreak.d.ts +17 -0
  114. package/dist/types/ScheduledBreak.js +1 -0
  115. package/package.json +1 -1
@@ -1 +1 @@
1
- import{debugLog as e}from"../diagnostics/debugLog";import{AdError as t}from"../types/AdError";import{VendorAdError as s}from"../types/VendorAdError";import{classifyImaAdError as a}from"./classifyImaAdError";import{html5ImaErrorName as r}from"./html5ImaErrorNames";export function isImaCsaiSdkAvailable(){return"undefined"!=typeof google&&void 0!==google.ima&&void 0!==google.ima.AdsLoader&&void 0!==google.ima.AdDisplayContainer}export class VastAdManager{constructor(e,t,s=!1){this.adContainer=e,this.adVideoElement=t,this.adDisplayContainer=null,this.adsLoader=null,this.adsManager=null,this.callbacks={},this.preloadResolve=null,this.preloadReject=null,this.startResolve=null,this.startReject=null,this.pendingMuted=!1,this.settled=!1,this.preloaded=!1,this.started=!1,this.currentAdDurationSec=null,this.debug=s}setCallbacks(e){this.callbacks=e}resize(){this.adsManager&&(this.adsManager.resize(this.slotWidth(),this.slotHeight(),google.ima.ViewMode.NORMAL),this.log("VAST AdsManager resized to",this.slotWidth(),"x",this.slotHeight()))}preloadVast(e,s){return this.reset(),isImaCsaiSdkAvailable()?new Promise((t,a)=>{this.preloadResolve=t,this.preloadReject=a;try{this.setupContainerAndLoader();const t=new google.ima.AdsRequest;t.adTagUrl=e,t.linearAdSlotWidth=this.slotWidth(),t.linearAdSlotHeight=this.slotHeight(),t.setAdWillAutoPlay(!0),this.pendingMuted=s?.muted??!1,t.setAdWillPlayMuted(this.pendingMuted),this.log("Preloading VAST ads:",e),this.adsLoader.requestAds(t)}catch(e){this.rejectPreload(e instanceof Error?e:new Error(String(e)))}}):Promise.reject(new t('Google IMA client-side SDK not found. Include <script src="https://imasdk.googleapis.com/js/sdkloader/ima3.js"><\/script> in your page.',"DA-VAST-IMA-SDK-MISSING"))}startPreloaded(){return new Promise((e,t)=>{if(this.adsManager&&this.preloaded&&!this.started){this.started=!0,this.settled=!1,this.startResolve=e,this.startReject=t;try{this.resize(),this.adsManager.start(),this.log("VAST AdsManager started")}catch(e){this.failStart(e instanceof Error?e:new Error(String(e)))}}else t(new Error("No preloaded VAST AdsManager; call preloadVast() first"))})}hasPreloadedManager(){return null!==this.adsManager&&this.preloaded&&!this.started&&!this.settled}setMuted(e){this.pendingMuted=e,this.adsManager?.setVolume(e?0:1)}setVolume(e){this.pendingMuted=e<=0,this.adsManager?.setVolume(e)}playVast(e,t){return this.preloadVast(e,t).then(()=>this.startPreloaded())}setupContainerAndLoader(){this.adDisplayContainer=new google.ima.AdDisplayContainer(this.adContainer,this.adVideoElement),this.adDisplayContainer.initialize(),this.adsLoader=new google.ima.AdsLoader(this.adDisplayContainer),this.adsLoader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,e=>this.onAdsManagerLoaded(e)),this.adsLoader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR,e=>this.onAdError(e))}onAdsManagerLoaded(e){try{const t=new google.ima.AdsRenderingSettings;t.restoreCustomPlaybackStateOnAdBreakComplete=!0,t.enablePreloading=!0;const s=e.getAdsManager(this.adVideoElement,t);this.adsManager=s,this.wireManagerEvents(s),this.pendingMuted&&s.setVolume(0),s.init(this.slotWidth(),this.slotHeight(),google.ima.ViewMode.NORMAL),this.preloaded=!0,this.resolvePreload(),this.log("VAST AdsManager initialized and ready")}catch(e){this.rejectPreload(e instanceof Error?e:new Error(String(e)))}}wireManagerEvents(e){const t=google.ima.AdEvent.Type;e.addEventListener(t.STARTED,e=>{this.resize();const t=e.getAd?.(),s=t?.getDuration?.();this.currentAdDurationSec="number"==typeof s&&Number.isFinite(s)&&s>0?s:null;const a=t?.getAdPodInfo?.()?.getAdPosition?.();this.callbacks.onAdStarted?.("number"==typeof a&&Number.isFinite(a)&&a>=1?Math.floor(a):void 0)}),e.addEventListener(t.AD_PROGRESS,()=>this.emitAdProgress()),e.addEventListener(t.FIRST_QUARTILE,()=>this.callbacks.onAdFirstQuartile?.()),e.addEventListener(t.MIDPOINT,()=>this.callbacks.onAdMidpoint?.()),e.addEventListener(t.THIRD_QUARTILE,()=>this.callbacks.onAdThirdQuartile?.()),e.addEventListener(t.CONTENT_PAUSE_REQUESTED,()=>this.callbacks.onContentPauseRequested?.()),e.addEventListener(t.CONTENT_RESUME_REQUESTED,()=>this.callbacks.onContentResumeRequested?.()),e.addEventListener(t.ALL_ADS_COMPLETED,()=>this.succeedStart()),e.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR,e=>this.onAdError(e))}emitAdProgress(){if(!this.callbacks.onAdProgress||!this.adsManager)return;const e=this.currentAdDurationSec;if(null==e)return;const t=this.adsManager.getRemainingTime();if(!Number.isFinite(t)||t<0)return;const s=Math.max(0,Math.min(e,e-t));this.callbacks.onAdProgress(s,e)}onAdError(e){const i=e?.getError?.(),o=i?.getMessage?.()??"Unknown IMA CSAI error",d=i?.getErrorCode?.();this.log("VAST IMA error:",o);const n="number"==typeof d?new s(o,String(d),a(r(d))):new t(o,"DA-VAST-IMA-ERROR");this.startReject?this.failStart(n):this.preloadReject&&this.rejectPreload(n)}succeedStart(){this.settled||(this.settled=!0,this.log("VAST playout complete"),this.startResolve?.(),this.clearStartPromises(),this.teardownManager())}failStart(e){this.settled||(this.settled=!0,this.startReject?.(e),this.clearStartPromises(),this.teardownManager())}resolvePreload(){this.preloadResolve?.(),this.clearPreloadPromises()}rejectPreload(e){this.preloadReject?.(e),this.clearPreloadPromises(),this.teardownManager()}clearPreloadPromises(){this.preloadResolve=null,this.preloadReject=null}clearStartPromises(){this.startResolve=null,this.startReject=null}slotWidth(){return this.adVideoElement.clientWidth||640}slotHeight(){return this.adVideoElement.clientHeight||360}teardownManager(){this.adsManager&&(this.adsManager.destroy(),this.adsManager=null),this.adsLoader&&(this.adsLoader.destroy(),this.adsLoader=null),this.adDisplayContainer&&(this.adDisplayContainer.destroy(),this.adDisplayContainer=null)}reset(){this.teardownManager(),this.settled=!1,this.preloaded=!1,this.started=!1,this.pendingMuted=!1,this.currentAdDurationSec=null,this.clearPreloadPromises(),this.clearStartPromises()}destroy(){this.reset(),this.callbacks={}}log(...t){this.debug&&e("[VastAdManager]",...t)}}
1
+ import{debugLog as e}from"../diagnostics/debugLog";import{AdError as t}from"../types/AdError";import{VendorAdError as s}from"../types/VendorAdError";import{classifyImaAdError as a}from"./classifyImaAdError";import{html5ImaErrorName as r}from"./html5ImaErrorNames";export function isImaCsaiSdkAvailable(){return"undefined"!=typeof google&&void 0!==google.ima&&void 0!==google.ima.AdsLoader&&void 0!==google.ima.AdDisplayContainer}export class VastAdManager{constructor(e,t,s=!1){this.adContainer=e,this.adVideoElement=t,this.adDisplayContainer=null,this.adsLoader=null,this.adsManager=null,this.callbacks={},this.preloadResolve=null,this.preloadReject=null,this.startResolve=null,this.startReject=null,this.pendingMuted=!1,this.settled=!1,this.preloaded=!1,this.started=!1,this.currentAdDurationSec=null,this.debug=s}setCallbacks(e){this.callbacks=e}resize(){this.adsManager&&(this.adsManager.resize(this.slotWidth(),this.slotHeight(),google.ima.ViewMode.NORMAL),this.log("VAST AdsManager resized to",this.slotWidth(),"x",this.slotHeight()))}preloadVast(e,s){return this.reset(),isImaCsaiSdkAvailable()?new Promise((t,a)=>{this.preloadResolve=t,this.preloadReject=a;try{this.setupContainerAndLoader();const t=new google.ima.AdsRequest;t.adTagUrl=e,t.linearAdSlotWidth=this.slotWidth(),t.linearAdSlotHeight=this.slotHeight(),t.setAdWillAutoPlay(!0),this.pendingMuted=s?.muted??!1,t.setAdWillPlayMuted(this.pendingMuted),this.log("Preloading VAST ads:",e),this.adsLoader.requestAds(t)}catch(e){this.rejectPreload(e instanceof Error?e:new Error(String(e)))}}):Promise.reject(new t('Google IMA client-side SDK not found. Include <script src="https://imasdk.googleapis.com/js/sdkloader/ima3.js"><\/script> in your page.',"DA-VAST-IMA-SDK-MISSING"))}startPreloaded(){return new Promise((e,t)=>{if(this.adsManager&&this.preloaded&&!this.started){this.started=!0,this.settled=!1,this.startResolve=e,this.startReject=t;try{this.resize(),this.adsManager.start(),this.log("VAST AdsManager started")}catch(e){this.failStart(e instanceof Error?e:new Error(String(e)))}}else t(new Error("No preloaded VAST AdsManager; call preloadVast() first"))})}hasPreloadedManager(){return null!==this.adsManager&&this.preloaded&&!this.started&&!this.settled}setMuted(e){this.pendingMuted=e,this.adsManager?.setVolume(e?0:1)}setVolume(e){this.pendingMuted=e<=0,this.adsManager?.setVolume(e)}playVast(e,t){return this.preloadVast(e,t).then(()=>this.startPreloaded())}setupContainerAndLoader(){this.adDisplayContainer=new google.ima.AdDisplayContainer(this.adContainer,this.adVideoElement),this.adDisplayContainer.initialize(),this.adsLoader=new google.ima.AdsLoader(this.adDisplayContainer),this.adsLoader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,e=>this.onAdsManagerLoaded(e)),this.adsLoader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR,e=>this.onAdError(e))}onAdsManagerLoaded(e){try{const t=new google.ima.AdsRenderingSettings;t.restoreCustomPlaybackStateOnAdBreakComplete=!0,t.enablePreloading=!0;const s=e.getAdsManager(this.adVideoElement,t);this.adsManager=s,this.wireManagerEvents(s),this.pendingMuted&&s.setVolume(0),s.init(this.slotWidth(),this.slotHeight(),google.ima.ViewMode.NORMAL),this.preloaded=!0,this.resolvePreload(),this.log("VAST AdsManager initialized and ready")}catch(e){this.rejectPreload(e instanceof Error?e:new Error(String(e)))}}wireManagerEvents(e){const t=google.ima.AdEvent.Type;e.addEventListener(t.STARTED,e=>{this.resize();const t=e.getAd?.(),s=t?.getDuration?.();this.currentAdDurationSec="number"==typeof s&&Number.isFinite(s)&&s>0?s:null;const a=t?.getAdPodInfo?.()?.getAdPosition?.();this.callbacks.onAdStarted?.("number"==typeof a&&Number.isFinite(a)&&a>=1?Math.floor(a):void 0)}),e.addEventListener(t.AD_PROGRESS,()=>this.emitAdProgress()),e.addEventListener(t.COMPLETE,()=>{null!=this.currentAdDurationSec&&this.callbacks.onAdCompleted?.(this.currentAdDurationSec)}),e.addEventListener(t.FIRST_QUARTILE,()=>this.callbacks.onAdFirstQuartile?.()),e.addEventListener(t.MIDPOINT,()=>this.callbacks.onAdMidpoint?.()),e.addEventListener(t.THIRD_QUARTILE,()=>this.callbacks.onAdThirdQuartile?.()),e.addEventListener(t.CONTENT_PAUSE_REQUESTED,()=>this.callbacks.onContentPauseRequested?.()),e.addEventListener(t.CONTENT_RESUME_REQUESTED,()=>this.callbacks.onContentResumeRequested?.()),e.addEventListener(t.ALL_ADS_COMPLETED,()=>this.succeedStart()),e.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR,e=>this.onAdError(e))}emitAdProgress(){if(!this.callbacks.onAdProgress||!this.adsManager)return;const e=this.currentAdDurationSec;if(null==e)return;const t=this.adsManager.getRemainingTime();if(!Number.isFinite(t)||t<0)return;const s=Math.max(0,Math.min(e,e-t));this.callbacks.onAdProgress(s,e)}onAdError(e){const i=e?.getError?.(),o=i?.getMessage?.()??"Unknown IMA CSAI error",d=i?.getErrorCode?.();this.log("VAST IMA error:",o);const n="number"==typeof d?new s(o,String(d),a(r(d))):new t(o,"DA-VAST-IMA-ERROR");this.startReject?this.failStart(n):this.preloadReject&&this.rejectPreload(n)}succeedStart(){this.settled||(this.settled=!0,this.log("VAST playout complete"),this.startResolve?.(),this.clearStartPromises(),this.teardownManager())}failStart(e){this.settled||(this.settled=!0,this.startReject?.(e),this.clearStartPromises(),this.teardownManager())}resolvePreload(){this.preloadResolve?.(),this.clearPreloadPromises()}rejectPreload(e){this.preloadReject?.(e),this.clearPreloadPromises(),this.teardownManager()}clearPreloadPromises(){this.preloadResolve=null,this.preloadReject=null}clearStartPromises(){this.startResolve=null,this.startReject=null}slotWidth(){return this.adVideoElement.clientWidth||640}slotHeight(){return this.adVideoElement.clientHeight||360}teardownManager(){this.adsManager&&(this.adsManager.destroy(),this.adsManager=null),this.adsLoader&&(this.adsLoader.destroy(),this.adsLoader=null),this.adDisplayContainer&&(this.adDisplayContainer.destroy(),this.adDisplayContainer=null)}reset(){this.teardownManager(),this.settled=!1,this.preloaded=!1,this.started=!1,this.pendingMuted=!1,this.currentAdDurationSec=null,this.clearPreloadPromises(),this.clearStartPromises()}destroy(){this.reset(),this.callbacks={}}log(...t){this.debug&&e("[VastAdManager]",...t)}}
@@ -10,8 +10,16 @@ export interface AdPlayerCallbacks {
10
10
  onAdStart: (breakInfo: Break, asset: Asset, index: number, total: number, identity?: AdCreativeIdentity) => void;
11
11
  onAdEnd: (breakInfo: Break, asset: Asset, index: number, total: number, identity?: AdCreativeIdentity) => void;
12
12
  /** `asset` is undefined for a break-level error (no playable variant, the break never began). */
13
- onAdError: (breakInfo: Break, asset: Asset | undefined, error: Error) => void;
13
+ onAdError: (breakInfo: Break, asset: Asset | undefined, error: Error, position?: {
14
+ adIndex: number;
15
+ totalAds: number;
16
+ }) => void;
14
17
  onAdTimeUpdate: (breakInfo: Break, asset: Asset, currentTime: number, duration: number) => void;
18
+ /**
19
+ * The slate plays: `remainingSec` is the wall time left to the break's scheduled end, `presenting`
20
+ * whether the slate is on screen. Feeds the break countdown, never an ad event.
21
+ */
22
+ onSlateProgress?: (breakInfo: Break, remainingSec: number, presenting: boolean) => void;
15
23
  onAdWaiting: (breakInfo: Break, asset: Asset) => void;
16
24
  onAdPlaying: (breakInfo: Break, asset: Asset) => void;
17
25
  onBreakEnd: (breakInfo: Break) => void;
@@ -3,6 +3,7 @@ import { type Break } from '../../types/BreakManifest';
3
3
  import type { GamEventCallbacks } from '../AdPlayerController';
4
4
  import type { AdBreakSequencer, AdBreakTransition } from '../AdBreakSequencer';
5
5
  import type { AdPlayerState } from './AdPlayerState';
6
+ import type { SlateProgress } from './SlatePlayback';
6
7
  export interface AdPlayerEventsDeps {
7
8
  getAdPlayer: () => PlayerAdapter | null;
8
9
  getContentPlayer: () => PlayerAdapter;
@@ -22,6 +23,8 @@ export interface AdPlayerEventsDeps {
22
23
  maybeCatchUpContentForResume: (breakInfo: Break) => void;
23
24
  getStaticQuartilesFired: () => Set<'first' | 'mid' | 'third'>;
24
25
  onGamPodAdProgress: (currentTime: number) => void;
26
+ /** While a slate owns the ad player its media events are the slate's, not an ad's: true when consumed. */
27
+ onSlateMediaEvent: (type: 'playing' | 'waiting' | 'ended' | 'error' | 'timeupdate', detail?: unknown) => boolean;
25
28
  }
26
29
  export declare class AdPlayerEvents {
27
30
  private readonly state;
@@ -29,7 +32,24 @@ export declare class AdPlayerEvents {
29
32
  constructor(state: AdPlayerState, deps: AdPlayerEventsDeps);
30
33
  /** Summed into `adSecondsPlayedThisBreak`, which caps the resume offset to the ads actually delivered. */
31
34
  recordAdProgress(currentTime: number): void;
32
- settleAssetProgress(): void;
35
+ /** A VAST pod creative played to its end: bank its whole length so the next creative adds to it. */
36
+ completeCreative(durationSec: number): void;
37
+ /**
38
+ * The asset ended (or, with no media timeline, was shown). `mediaDurationSec` is set when the media
39
+ * played to its natural end: the last progress tick lags that end by up to a tick, so the whole
40
+ * length counts, as long as it is only a tick beyond what was seen.
41
+ */
42
+ settleAssetProgress(mediaDurationSec?: number): void;
43
+ /** An asset ended: a static video reports its media length too; an image, VAST or pod asset has no such timeline. */
44
+ settleEndedAsset(): void;
45
+ /** A failed ad still showed what it showed: those seconds count, a declared duration it never reached does not. */
46
+ settleErroredAssetProgress(): void;
47
+ /**
48
+ * Ad (or slate) progress is the only clock that ticks reliably while content is paused: move
49
+ * content onto its resume point, and if the window still has not published it, let content play
50
+ * the last stretch behind the ad.
51
+ */
52
+ reportSlateProgress(slate?: SlateProgress): void;
33
53
  reportAdProgress(player: PlayerAdapter): void;
34
54
  setupEventListeners(): void;
35
55
  onAdError(error: Error): void;
@@ -1 +1 @@
1
- import{AssetType as t}from"../../types/BreakManifest";export class AdPlayerEvents{constructor(t,e){this.state=t,this.deps=e}recordAdProgress(t){!Number.isFinite(t)||t<=0||(this.deps.onGamPodAdProgress(t),t>this.state.currentAssetElapsedSec&&(this.state.currentAssetElapsedSec=t))}settleAssetProgress(){let t=this.state.currentAssetElapsedSec;if(t<=0){const e=this.state.currentAsset?.duration;"number"==typeof e&&Number.isFinite(e)&&e>0&&(t=e)}this.state.adSecondsPlayedThisBreak+=t,this.state.currentAssetElapsedSec=0}reportAdProgress(e){if(this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&(this.recordAdProgress(e.currentTime),this.deps.maybePrepositionContentForResume(),this.state.currentBreak&&this.deps.maybeCatchUpContentForResume(this.state.currentBreak),this.state.callbacks?.onAdTimeUpdate(this.state.currentBreak,this.state.currentAsset,e.currentTime,e.duration),this.state.currentAsset.type===t.STATIC&&e.duration>0)){const t=e.currentTime/e.duration,s=this.deps.getStaticQuartilesFired(),r=this.deps.getGamEventCallbacks();t>=.25&&!s.has("first")&&(s.add("first"),r.onAdFirstQuartile?.(this.state.currentBreak,this.state.currentAsset)),t>=.5&&!s.has("mid")&&(s.add("mid"),r.onAdMidpoint?.(this.state.currentBreak,this.state.currentAsset)),t>=.75&&!s.has("third")&&(s.add("third"),r.onAdThirdQuartile?.(this.state.currentBreak,this.state.currentAsset))}}setupEventListeners(){const t=this.deps.getAdPlayer();t&&(t.on("ended",()=>{this.deps.onAdEnded()}),t.on("timedmetadata",t=>{t&&this.deps.processTimedMetadata(t)}),t.on("error",t=>{this.onAdError(this.deps.toAdError(t))}),t.on("timeupdate",()=>this.reportAdProgress(t)),t.on("waiting",()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&this.state.callbacks?.onAdWaiting(this.state.currentBreak,this.state.currentAsset)}),t.on("playing",()=>{if(this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&!t.paused){if(this.state.startPlaybackCutoff?.(),this.state.firstFrameResolver)return this.deps.revealFirstFrame(),this.state.firstFrameResolver(),void this.state.callbacks?.onAdPlaying(this.state.currentBreak,this.state.currentAsset);this.state.callbacks?.onAdPlaying(this.state.currentBreak,this.state.currentAsset)}}))}onAdError(t){const e=this.deps.getSequencer(),s=this.state.currentBreak;if(!s||!e)return;const r=this.state.preloadGeneration,a=this.state.erroredAsset;if(a?.breakInfo!==s||a.generation!==r){if(this.state.erroredAsset={breakInfo:s,generation:r},this.state.firstFrameRejector){const e=this.state.firstFrameRejector;this.deps.clearFirstFrameResolver(),e(t)}this.deps.log("Ad error:",t.message),this.deps.dropQueuedAd(),this.deps.setPendingAdError(t),this.deps.drive(e.onOutcome({kind:"asset-errored"}))}else this.deps.log("Ignoring further ad error for the same asset:",t.message)}}
1
+ import{AssetType as t,MediaType as e}from"../../types/BreakManifest";export class AdPlayerEvents{constructor(t,e){this.state=t,this.deps=e}recordAdProgress(t){if(!Number.isFinite(t)||t<=0)return;this.deps.onGamPodAdProgress(t);const e=this.state.currentAssetBaseSec+t;e>this.state.currentAssetElapsedSec&&(this.state.currentAssetElapsedSec=e)}completeCreative(t){!Number.isFinite(t)||t<=0||(this.state.currentAssetBaseSec+=t,this.state.currentAssetBaseSec>this.state.currentAssetElapsedSec&&(this.state.currentAssetElapsedSec=this.state.currentAssetBaseSec))}settleAssetProgress(t){let e=this.state.currentAssetElapsedSec;if("number"==typeof t&&Number.isFinite(t)&&t>e&&t-e<=1&&(e=t),e<=0){const t=this.state.currentAsset?.duration;"number"==typeof t&&Number.isFinite(t)&&t>0&&(e=t)}this.state.adSecondsPlayedThisBreak+=e,this.state.currentAssetElapsedSec=0,this.state.currentAssetBaseSec=0}settleEndedAsset(){const s=this.state.currentAsset,r=s?.type===t.STATIC&&s.mediaType!==e.IMAGE;this.settleAssetProgress(r?this.deps.getAdPlayer()?.duration:void 0)}settleErroredAssetProgress(){this.state.adSecondsPlayedThisBreak+=Math.max(0,this.state.currentAssetElapsedSec),this.state.currentAssetElapsedSec=0,this.state.currentAssetBaseSec=0}reportSlateProgress(t){this.deps.maybePrepositionContentForResume(),this.state.currentBreak&&this.deps.maybeCatchUpContentForResume(this.state.currentBreak),t&&this.state.callbacks?.onSlateProgress?.(t.breakInfo,t.remainingSec,t.presenting)}reportAdProgress(e){if(this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&(this.recordAdProgress(e.currentTime),this.reportSlateProgress(),this.state.callbacks?.onAdTimeUpdate(this.state.currentBreak,this.state.currentAsset,e.currentTime,e.duration),this.state.currentAsset.type===t.STATIC&&e.duration>0)){const t=e.currentTime/e.duration,s=this.deps.getStaticQuartilesFired(),r=this.deps.getGamEventCallbacks();t>=.25&&!s.has("first")&&(s.add("first"),r.onAdFirstQuartile?.(this.state.currentBreak,this.state.currentAsset)),t>=.5&&!s.has("mid")&&(s.add("mid"),r.onAdMidpoint?.(this.state.currentBreak,this.state.currentAsset)),t>=.75&&!s.has("third")&&(s.add("third"),r.onAdThirdQuartile?.(this.state.currentBreak,this.state.currentAsset))}}setupEventListeners(){const t=this.deps.getAdPlayer();t&&(t.on("ended",()=>{this.deps.onSlateMediaEvent("ended")||this.deps.onAdEnded()}),t.on("timedmetadata",t=>{t&&this.deps.processTimedMetadata(t)}),t.on("error",t=>{this.deps.onSlateMediaEvent("error",t)||this.onAdError(this.deps.toAdError(t))}),t.on("timeupdate",()=>{this.deps.onSlateMediaEvent("timeupdate")||this.reportAdProgress(t)}),t.on("waiting",()=>{this.deps.onSlateMediaEvent("waiting")||this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&this.state.callbacks?.onAdWaiting(this.state.currentBreak,this.state.currentAsset)}),t.on("playing",()=>{if(!this.deps.onSlateMediaEvent("playing")&&this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&!t.paused){if(this.state.startPlaybackCutoff?.(),this.state.firstFrameResolver)return this.deps.revealFirstFrame(),this.state.firstFrameResolver(),void this.state.callbacks?.onAdPlaying(this.state.currentBreak,this.state.currentAsset);this.state.callbacks?.onAdPlaying(this.state.currentBreak,this.state.currentAsset)}}))}onAdError(t){const e=this.deps.getSequencer(),s=this.state.currentBreak;if(!s||!e)return;const r=this.state.preloadGeneration,a=this.state.erroredAsset;if(a?.breakInfo!==s||a.generation!==r){if(this.state.erroredAsset={breakInfo:s,generation:r},this.state.firstFrameRejector){const e=this.state.firstFrameRejector;this.deps.clearFirstFrameResolver(),e(t)}this.deps.log("Ad error:",t.message),this.deps.dropQueuedAd(),this.settleErroredAssetProgress(),this.deps.setPendingAdError(t),this.deps.drive(e.onOutcome({kind:"asset-errored"}))}else this.deps.log("Ignoring further ad error for the same asset:",t.message)}}
@@ -107,6 +107,10 @@ export interface AdPlayerState {
107
107
  resumeLandedProbe: (() => void) | null;
108
108
  adSecondsPlayedThisBreak: number;
109
109
  currentAssetElapsedSec: number;
110
+ /** Seconds of the current asset's VAST pod creatives that already played to their end. */
111
+ currentAssetBaseSec: number;
112
+ /** When the break being played began, on the controller's clock: its scheduled end is this plus its effective duration. */
113
+ breakBeganAtMs: number;
110
114
  preparedMedia: {
111
115
  breakId: string;
112
116
  assetId: string;
@@ -129,4 +133,13 @@ export interface AdPlayerState {
129
133
  vastAdManager: VastAdManager | null;
130
134
  }
131
135
  export declare function invalidateMediaPreload(state: AdPlayerState): number;
136
+ /** A break begins now: its ad tally restarts (a slate is sized from what THIS break's ads left) and its clock is anchored. */
137
+ export declare function beginBreakTally(state: AdPlayerState, nowMs: number): void;
138
+ /**
139
+ * A first-frame-gated break begins publicly (`adbreakbegin`) when its overlay is revealed or an ad
140
+ * fails first, so its clock, and with it a slate's scheduled end, is anchored then.
141
+ */
142
+ export declare function gateBreakClock(state: AdPlayerState, callbacks: AdPlayerCallbacks, clock: {
143
+ now: () => number;
144
+ }): AdPlayerCallbacks;
132
145
  export declare function createAdPlayerState(): AdPlayerState;
@@ -1 +1 @@
1
- export function invalidateMediaPreload(e){return e.preparedMedia=null,++e.preloadGeneration}export function createAdPlayerState(){return{currentBreak:null,currentAsset:null,isPlaying:!1,contentPaused:!1,callbacks:null,durationTimer:null,startPlaybackCutoff:null,effectiveBreakDurationSec:null,adInsertionType:void 0,breakTimebase:null,adBreakCutSafetyMarginSec:0,detectedDeviceType:void 0,sessionAssetParameters:void 0,updatedAssetParameters:void 0,manifestAssetParameters:void 0,sessionAssetParameterMacros:{},pushedAssetParameters:null,gamStreamManager:null,gamPodMode:"none",gamPodTracker:null,gamPodAsset:null,earlyGamPodAdStart:null,withheldPodAdBegin:null,gamConfig:void 0,gamInitPromise:null,currentGamIdentity:void 0,playerContainer:null,stage:null,contentVideoElement:null,adContainer:null,adMediaContainer:null,adPlayer:null,companionElement:null,overlayImageEl:null,isContentFullscreen:!1,trackedContentVideo:null,fullscreenEnterHandler:null,fullscreenExitHandler:null,contentPictureInPicture:!1,pipTransferredToAd:!1,pipAdoptedForBreak:!1,sharedAdClaimActive:!1,savedContentSrc:null,savedContentTime:0,savedContentLive:!1,savedContentEdgeDistance:null,earlyReturnResumeOffsetSec:null,contentPositionAtBreakStart:0,snapbackResumePosition:void 0,resumeAnchorIsCue:!1,resumeAnchorIsPdt:!1,resumeAnchorSeekableEnd:null,resumePrepositionedTo:null,resumePrepositionSettle:null,resumeCatchupActive:!1,resumeCatchupRan:!1,lastResumeAlignRemainingSec:Number.POSITIVE_INFINITY,resumeAlignBlockedDiagnosed:!1,lastResumeSafeEnd:Number.NEGATIVE_INFINITY,resumeSawSeekDone:!1,resumeBestSawMarginSec:Number.NEGATIVE_INFINITY,resumeAlignTicks:0,resumeSawSteps:[],continueContentDuringBreak:!1,continueQualityOverrideApplied:!1,continueContentDuringBreakActive:!1,keepContentPlayingHandler:null,continuePlayoutStartWallMs:0,continuePlayoutTicker:null,continueRateWritten:null,continueRateWrittenAtMs:0,continueRateHonoured:!0,continueMaxAbsDriftSec:0,continueCorrections:[],continueDriftUnrecoverable:!1,continueRefusalReported:!1,resumeAlignCorrectionsMs:[],resumeLandedProbe:null,adSecondsPlayedThisBreak:0,currentAssetElapsedSec:0,preparedMedia:null,preloadGeneration:0,erroredAsset:null,firstFrameGateActive:!1,firstFrameGateShown:!1,firstFrameResolver:null,firstFrameRejector:null,firstFrameTimeout:null,adStartDeadlineMs:null,adStartBudgetMs:0,vastAdManager:null}}
1
+ export function invalidateMediaPreload(e){return e.preparedMedia=null,++e.preloadGeneration}export function beginBreakTally(e,n){e.adSecondsPlayedThisBreak=0,e.currentAssetElapsedSec=0,e.currentAssetBaseSec=0,e.breakBeganAtMs=n}export function gateBreakClock(e,n,t){let r=!1;const a=()=>{r||(r=!0,e.breakBeganAtMs=t.now())},l={onBreakVisible:e=>{a(),n.onBreakVisible?.(e)},onAdError:(e,t,r,l)=>{a(),n.onAdError(e,t,r,l)}};return Object.assign(Object.create(n),l)}export function createAdPlayerState(){return{currentBreak:null,currentAsset:null,isPlaying:!1,contentPaused:!1,callbacks:null,durationTimer:null,startPlaybackCutoff:null,effectiveBreakDurationSec:null,adInsertionType:void 0,breakTimebase:null,adBreakCutSafetyMarginSec:0,detectedDeviceType:void 0,sessionAssetParameters:void 0,updatedAssetParameters:void 0,manifestAssetParameters:void 0,sessionAssetParameterMacros:{},pushedAssetParameters:null,gamStreamManager:null,gamPodMode:"none",gamPodTracker:null,gamPodAsset:null,earlyGamPodAdStart:null,withheldPodAdBegin:null,gamConfig:void 0,gamInitPromise:null,currentGamIdentity:void 0,playerContainer:null,stage:null,contentVideoElement:null,adContainer:null,adMediaContainer:null,adPlayer:null,companionElement:null,overlayImageEl:null,isContentFullscreen:!1,trackedContentVideo:null,fullscreenEnterHandler:null,fullscreenExitHandler:null,contentPictureInPicture:!1,pipTransferredToAd:!1,pipAdoptedForBreak:!1,sharedAdClaimActive:!1,savedContentSrc:null,savedContentTime:0,savedContentLive:!1,savedContentEdgeDistance:null,earlyReturnResumeOffsetSec:null,contentPositionAtBreakStart:0,snapbackResumePosition:void 0,resumeAnchorIsCue:!1,resumeAnchorIsPdt:!1,resumeAnchorSeekableEnd:null,resumePrepositionedTo:null,resumePrepositionSettle:null,resumeCatchupActive:!1,resumeCatchupRan:!1,lastResumeAlignRemainingSec:Number.POSITIVE_INFINITY,resumeAlignBlockedDiagnosed:!1,lastResumeSafeEnd:Number.NEGATIVE_INFINITY,resumeSawSeekDone:!1,resumeBestSawMarginSec:Number.NEGATIVE_INFINITY,resumeAlignTicks:0,resumeSawSteps:[],continueContentDuringBreak:!1,continueQualityOverrideApplied:!1,continueContentDuringBreakActive:!1,keepContentPlayingHandler:null,continuePlayoutStartWallMs:0,continuePlayoutTicker:null,continueRateWritten:null,continueRateWrittenAtMs:0,continueRateHonoured:!0,continueMaxAbsDriftSec:0,continueCorrections:[],continueDriftUnrecoverable:!1,continueRefusalReported:!1,resumeAlignCorrectionsMs:[],resumeLandedProbe:null,adSecondsPlayedThisBreak:0,currentAssetElapsedSec:0,currentAssetBaseSec:0,breakBeganAtMs:0,preparedMedia:null,preloadGeneration:0,erroredAsset:null,firstFrameGateActive:!1,firstFrameGateShown:!1,firstFrameResolver:null,firstFrameRejector:null,firstFrameTimeout:null,adStartDeadlineMs:null,adStartBudgetMs:0,vastAdManager:null}}
@@ -3,6 +3,7 @@ import { Asset, Break, BreakVariant, StaticAsset, VastAsset } from '../../types/
3
3
  import type { ResolvedPreloadMode } from '../detectPreloadMode';
4
4
  import { type AdPlayerState } from './AdPlayerState';
5
5
  import type { Clock } from '../BreakScheduler';
6
+ import type { ResolvedSlate } from '../resolveSlate';
6
7
  /** A stalled ad load gets one second load() this far into the ad-start budget. */
7
8
  export declare const AD_LOAD_RETRY_AFTER_MS = 2000;
8
9
  /** True when the ad element has produced nothing since load() — no metadata and no buffered media. */
@@ -19,7 +20,7 @@ export interface AdPreloaderDeps {
19
20
  effectiveInsertionMode: () => string;
20
21
  getVariant: (breakInfo: Break) => BreakVariant | null;
21
22
  getAssets: (breakInfo: Break) => Asset[];
22
- buildStaticAssetUrl: (asset: StaticAsset, breakInfo: Break) => string | null;
23
+ buildStaticAssetUrl: (asset: StaticAsset, breakInfo: Break | null) => string | null;
23
24
  resolveAssetUri: (asset: Asset, duration: number, breakInfo: Break) => Promise<string | null>;
24
25
  getCompanionVariant: (breakInfo: Break) => BreakVariant | null;
25
26
  getAdPlayer: () => PlayerAdapter | null;
@@ -42,6 +43,12 @@ export declare class AdPreloader {
42
43
  private classifyCreative;
43
44
  private diagnosePreloadPath;
44
45
  preload(breakInfo: Break): Promise<void>;
46
+ /**
47
+ * Warms the manifest's slate on its first appearance, or when its URL changes: every break that
48
+ * needs it reuses the same asset, so it is fetched once, ahead of need. Best-effort and detached
49
+ * from the ad player, so it never disturbs the ad being prepared there.
50
+ */
51
+ sideloadSlate(slate: ResolvedSlate): Promise<void>;
45
52
  private ownsPreload;
46
53
  preloadVastAsset(asset: VastAsset, breakInfo: Break): Promise<void>;
47
54
  preloadChainedSuccessor(currentBreakId: string): void;
@@ -1 +1 @@
1
- import{AssetType as e,BreakFormat as t,MediaType as s,Vendor as a,isVastAsset as r,isVendorAsset as i}from"../../types/BreakManifest";import{AUX_FETCH_TIMEOUT_MS as d,fetchWithTimeout as o}from"../fetchWithTimeout";import{resolveVastAdmission as n}from"../resolveVastAdmission";import{isImaCsaiSdkAvailable as l,VastAdManager as p}from"../VastAdManager";import{invalidateMediaPreload as h}from"./AdPlayerState";import{AdError as m}from"../../types/AdError";export const AD_LOAD_RETRY_AFTER_MS=2e3;export function adLoadShowsNoProgress(e){return!e||0===e.readyState&&0===e.buffered.length}export class AdPreloader{constructor(e){this.deps=e,this.warmedBreakIds=new Set}classifyCreative(e){const t=(e.split(/[?#]/)[0]??"").toLowerCase();return t.endsWith(".m3u8")||t.endsWith(".mpd")?"stream":/\.(mp4|m4v|m4a|webm|mov|ogg|ogv|mp3)$/.test(t)?"progressive":"unknown"}diagnosePreloadPath(e,t,s){const a=this.classifyCreative(t);this.deps.diagnose("DA-PRELOAD-PATH",`Ad preload path: ${s} (${a} creative)`,{breakId:e,adPreload:this.deps.preloadMode,path:s,creative:a})}async preload(a){if(this.deps.isPlaying())return void this.deps.log("Preload skipped: ad already playing");const i=h(this.deps.state),d=this.deps.getAdPlayer();if("none"===this.deps.preloadMode){const t=this.deps.getAssets(a)[0],s=t&&t.type===e.STATIC?this.deps.buildStaticAssetUrl(t,a)??"":"";return this.diagnosePreloadPath(a.id,s,"none"),void this.deps.log('Preload skipped: adPreload "none"')}if("shared-element"===this.deps.effectiveInsertionMode()){const s=this.deps.getVariant(a);if(s&&s.format!==t.LSHAPE_CONTENT){const t=this.deps.getAssets(a)[0];if(t&&t.type===e.STATIC){const e=this.deps.buildStaticAssetUrl(t,a);e&&await this.warmCache([e])}}return}const o=this.deps.getAssets(a);if(0===o.length)return void this.deps.log("No assets to preload");this.preloadCompanion(a);const n=this.deps.getVariant(a);if(n?.format===t.LSHAPE_CONTENT)return;if(o[0].mediaType===s.IMAGE){const e=await this.deps.resolveAssetUri(o[0],a.duration,a);if(e){(new Image).src=e,this.deps.log("Preloading overlay image:",e)}return}if(r(o[0]))return void await this.preloadVastAsset(o[0],a);const l=await this.deps.resolveAssetUri(o[0],a.duration,a);if(l){if(d&&this.ownsPreload(i,d))if("single-decoder"!==this.deps.preloadMode){this.deps.log("Preloading ad (parallel):",l),this.diagnosePreloadPath(a.id,l,"parallel");try{if(await d.load(l),!this.ownsPreload(i,d))return;this.deps.state.preparedMedia={breakId:a.id,assetId:o[0].id,uri:l,player:d},this.deps.log("Preload complete for break:",a.id)}catch(e){this.deps.log("Preload failed (will load on demand):",e)}}else{if(this.deps.log("Preloading ad (single-decoder, detached):",l),this.diagnosePreloadPath(a.id,l,"single-decoder"),await this.warmCache([l]),!this.ownsPreload(i,d))return;try{await(d.preload?.(l))}catch(e){this.deps.log("Detached prefetch failed (will load on demand):",e)}}}else this.deps.log("No valid URI for preload")}ownsPreload(e,t){return e===this.deps.state.preloadGeneration&&!this.deps.isPlaying()&&t===this.deps.getAdPlayer()}async preloadVastAsset(e,s){const a=this.deps.getVariant(s),r=a?.format??t.SINGLE;if(!n({format:r,ssai:!1}).admitted)return void this.deps.log("VAST preload skipped: format not supported",r);if(!l())return void this.deps.log("VAST preload skipped: IMA client-side SDK unavailable");const i=this.deps.getAdPlayer(),d=this.deps.getAdContainer();if(!i||!d)return void this.deps.log("VAST preload skipped: ad player not initialized");const o=i.videoElement;if(!o)return void this.deps.log("VAST preload skipped: no ad video element");const h=this.deps.buildVastTagUrl(e,s);if(h){this.deps.state.vastAdManager||(this.deps.state.vastAdManager=new p(d,o,this.deps.debug)),this.deps.log("Preloading VAST asset via IMA CSAI:",h);try{await this.deps.state.vastAdManager.preloadVast(h,{muted:i.muted}),this.deps.log("VAST preload complete for break:",s.id)}catch(t){const a=t instanceof Error?t:new Error(String(t));this.deps.log("VAST preload failed (will load on demand):",a.message),this.deps.diagnose("DA-PRELOAD-FAILED",a.message,{breakId:s.id,assetId:e.id})}}else this.deps.log("No valid VAST tag URL for preload")}preloadChainedSuccessor(e){const t=this.deps.resolveChainedSuccessor(e);t&&this.preloadNext(t)}clearWarmedBreaks(){this.warmedBreakIds.clear()}async preloadNext(s){if(this.warmedBreakIds.has(s.id))return;this.preloadCompanion(s);const a=this.deps.getVariant(s);if(!a||a.format===t.LSHAPE_CONTENT)return;const r=this.deps.getAssets(s)[0];if(!r||r.type!==e.STATIC)return void this.deps.log("Chained successor first asset not static; skipping media warm:",s.id);const i=this.deps.buildStaticAssetUrl(r,s);if(i){this.warmedBreakIds.add(s.id),this.deps.log("Warming chained successor media:",s.id,i),await this.warmCache([i]);try{await(this.deps.getAdPlayer()?.preload?.(i))}catch(e){this.deps.log("Chained prefetch failed (will load on demand):",e)}}}revealFirstFrame(){if(!this.deps.state.firstFrameGateActive||this.deps.state.firstFrameGateShown)return;const e=this.deps.getCurrentBreak();if(!e)return;this.deps.state.firstFrameGateShown=!0,this.deps.show();const t=this.deps.getVariant(e);t&&this.deps.reapplyAdContainerLayout(t),this.deps.onBreakVisible(e)}armAdStartBudget(e){const t=1e3*((this.deps.state.effectiveBreakDurationSec??this.deps.state.currentBreak?.duration??0)+this.deps.state.adBreakCutSafetyMarginSec),s=i(e)&&e.vendor===a.GAM?this.deps.gamPodAdStartBudgetMs:this.deps.adStartBudgetMs,r=Math.max(Math.min(t-100,s),this.deps.minAdStartBudgetMs);this.deps.state.adStartBudgetMs=r,this.deps.state.adStartDeadlineMs=this.deps.clock.now()+r}remainingAdStartBudgetMs(){return Math.max(0,(this.deps.state.adStartDeadlineMs??this.deps.clock.now())-this.deps.clock.now())}loadWithinBudget(e,t){const s=this.deps.state.preloadGeneration,a=this.remainingAdStartBudgetMs(),r=this.deps.clock.now();return new Promise((i,d)=>{let o=!1,n=!1;const l=e=>{o||(o=!0,clearTimeout(p),null!==h&&clearTimeout(h),e.ok?i(!0):d(e.error))},p=setTimeout(()=>{s===this.deps.state.preloadGeneration&&this.deps.onAdError(new m("Ad load timeout","DA-AD-START-TIMEOUT")),o||(o=!0,null!==h&&clearTimeout(h),i(!1))},a);let h=null;a>2e3&&(h=setTimeout(()=>{if(o||s!==this.deps.state.preloadGeneration)return;const a=this.deps.clock.now()-r,i=this.deps.getAdPlayer()?.videoElement;adLoadShowsNoProgress(i)?(n=!0,this.deps.log(`Ad load for ${t.assetId} stalled after ${a}ms; retrying once`),this.deps.diagnose("DA-AD-LOAD-RETRIED",`Ad load for ${t.assetId} in break ${t.breakId} had not settled after ${a}ms; issued a second load()`,{breakId:t.breakId,assetId:t.assetId,uri:t.uri,elapsedMs:a,remainingBudgetMs:this.remainingAdStartBudgetMs()}),e().then(()=>l({ok:!0}),e=>l({ok:!1,error:e}))):this.deps.log(`Ad load for ${t.assetId} still pending after ${a}ms but the element is progressing (readyState=${i?.readyState}, buffered=${i?.buffered.length}); no retry`)},2e3)),e().then(()=>l({ok:!0}),e=>{n||l({ok:!1,error:e})})})}async waitForFirstFrame(){return new Promise((e,t)=>{this.deps.state.firstFrameResolver=()=>{this.clearFirstFrameResolver(),e()},this.deps.state.firstFrameRejector=e=>{this.clearFirstFrameResolver(),t(e)},this.deps.state.firstFrameTimeout=setTimeout(()=>{this.deps.onAdError(new m("First frame timeout","DA-AD-START-TIMEOUT"))},this.remainingAdStartBudgetMs())})}clearFirstFrame(e="Break ended before first frame"){if(this.deps.state.adStartDeadlineMs=null,this.deps.state.adStartBudgetMs=0,this.deps.state.firstFrameTimeout&&(clearTimeout(this.deps.state.firstFrameTimeout),this.deps.state.firstFrameTimeout=null),this.deps.state.firstFrameRejector){const t=this.deps.state.firstFrameRejector;this.deps.state.firstFrameResolver=null,this.deps.state.firstFrameRejector=null,t(new Error(e))}}clearFirstFrameResolver(){this.deps.state.adStartDeadlineMs=null,this.deps.state.adStartBudgetMs=0,this.deps.state.firstFrameTimeout&&(clearTimeout(this.deps.state.firstFrameTimeout),this.deps.state.firstFrameTimeout=null),this.deps.state.firstFrameResolver=null,this.deps.state.firstFrameRejector=null}preloadCompanion(s){const a=this.deps.getCompanionVariant(s);if(!a)return;if(a.format===t.LSHAPE_CONTENT){const t=a.assets[0];if(!t||t.type!==e.STATIC)return;const r=this.deps.buildStaticAssetUrl(t,s);if(!r)return;return(new Image).src=r,void this.deps.log("Preloading lshape_content backdrop:",r)}if(a.format!==t.DOUBLE&&a.format!==t.LSHAPE_AD)return;const r=a.assets[0]?.companion;if(!r||r.type!==e.STATIC)return;const i=this.deps.buildStaticAssetUrl(r,s);if(!i)return;(new Image).src=i,this.deps.log("Preloading companion:",i)}async warmCache(e){"function"==typeof fetch&&await Promise.allSettled(e.map(e=>o(e,{mode:"no-cors"},d).catch(()=>{})))}}
1
+ import{AssetType as e,BreakFormat as t,MediaType as s,Vendor as a,isVastAsset as r,isVendorAsset as i}from"../../types/BreakManifest";import{AUX_FETCH_TIMEOUT_MS as d,fetchWithTimeout as o}from"../fetchWithTimeout";import{resolveVastAdmission as n}from"../resolveVastAdmission";import{isImaCsaiSdkAvailable as l,VastAdManager as p}from"../VastAdManager";import{invalidateMediaPreload as h}from"./AdPlayerState";import{AdError as m}from"../../types/AdError";import{OPTIVIEW_ASSET_PARAMETER_MACRO_SCOPES as c}from"../assetParameterMacros";export const AD_LOAD_RETRY_AFTER_MS=2e3;export function adLoadShowsNoProgress(e){return!e||0===e.readyState&&0===e.buffered.length}export class AdPreloader{constructor(e){this.deps=e,this.warmedBreakIds=new Set}classifyCreative(e){const t=(e.split(/[?#]/)[0]??"").toLowerCase();return t.endsWith(".m3u8")||t.endsWith(".mpd")?"stream":/\.(mp4|m4v|m4a|webm|mov|ogg|ogv|mp3)$/.test(t)?"progressive":"unknown"}diagnosePreloadPath(e,t,s){const a=this.classifyCreative(t);this.deps.diagnose("DA-PRELOAD-PATH",`Ad preload path: ${s} (${a} creative)`,{breakId:e,adPreload:this.deps.preloadMode,path:s,creative:a})}async preload(a){if(this.deps.isPlaying())return void this.deps.log("Preload skipped: ad already playing");const i=h(this.deps.state),d=this.deps.getAdPlayer();if("none"===this.deps.preloadMode){const t=this.deps.getAssets(a)[0],s=t&&t.type===e.STATIC?this.deps.buildStaticAssetUrl(t,a)??"":"";return this.diagnosePreloadPath(a.id,s,"none"),void this.deps.log('Preload skipped: adPreload "none"')}if("shared-element"===this.deps.effectiveInsertionMode()){const s=this.deps.getVariant(a);if(s&&s.format!==t.LSHAPE_CONTENT){const t=this.deps.getAssets(a)[0];if(t&&t.type===e.STATIC){const e=this.deps.buildStaticAssetUrl(t,a);e&&await this.warmCache([e])}}return}const o=this.deps.getAssets(a);if(0===o.length)return void this.deps.log("No assets to preload");this.preloadCompanion(a);const n=this.deps.getVariant(a);if(n?.format===t.LSHAPE_CONTENT)return;if(o[0].mediaType===s.IMAGE){const e=await this.deps.resolveAssetUri(o[0],a.duration,a);if(e){(new Image).src=e,this.deps.log("Preloading overlay image:",e)}return}if(r(o[0]))return void await this.preloadVastAsset(o[0],a);const l=await this.deps.resolveAssetUri(o[0],a.duration,a);if(l){if(d&&this.ownsPreload(i,d))if("single-decoder"!==this.deps.preloadMode){this.deps.log("Preloading ad (parallel):",l),this.diagnosePreloadPath(a.id,l,"parallel");try{if(await d.load(l),!this.ownsPreload(i,d))return;this.deps.state.preparedMedia={breakId:a.id,assetId:o[0].id,uri:l,player:d},this.deps.log("Preload complete for break:",a.id)}catch(e){this.deps.log("Preload failed (will load on demand):",e)}}else{if(this.deps.log("Preloading ad (single-decoder, detached):",l),this.diagnosePreloadPath(a.id,l,"single-decoder"),await this.warmCache([l]),!this.ownsPreload(i,d))return;try{await(d.preload?.(l))}catch(e){this.deps.log("Detached prefetch failed (will load on demand):",e)}}}else this.deps.log("No valid URI for preload")}async sideloadSlate(e){if("none"===this.deps.preloadMode)return;const{asset:t}=e;if(function(e){const t="string"==typeof e.uri?[e.uri]:e.uri.map(e=>e.value),s=[...t,...Object.values(e.assetParameters??{})];return Object.entries(c).some(([e,t])=>"break"===t&&s.some(t=>t.includes(e)))}(t))return;const a=this.deps.buildStaticAssetUrl(t,null);if(!a)return;const r=t.mediaType===s.IMAGE;r?(new Image).src=a:await this.warmCache([a]),this.deps.diagnose("DA-SLATE-PRELOADED",`Slate ${t.id} preloaded`,{slateAssetId:t.id,mediaType:t.mediaType,path:r?"image":"http-cache"})}ownsPreload(e,t){return e===this.deps.state.preloadGeneration&&!this.deps.isPlaying()&&t===this.deps.getAdPlayer()}async preloadVastAsset(e,s){const a=this.deps.getVariant(s),r=a?.format??t.SINGLE;if(!n({format:r,ssai:!1}).admitted)return void this.deps.log("VAST preload skipped: format not supported",r);if(!l())return void this.deps.log("VAST preload skipped: IMA client-side SDK unavailable");const i=this.deps.getAdPlayer(),d=this.deps.getAdContainer();if(!i||!d)return void this.deps.log("VAST preload skipped: ad player not initialized");const o=i.videoElement;if(!o)return void this.deps.log("VAST preload skipped: no ad video element");const h=this.deps.buildVastTagUrl(e,s);if(h){this.deps.state.vastAdManager||(this.deps.state.vastAdManager=new p(d,o,this.deps.debug)),this.deps.log("Preloading VAST asset via IMA CSAI:",h);try{await this.deps.state.vastAdManager.preloadVast(h,{muted:i.muted}),this.deps.log("VAST preload complete for break:",s.id)}catch(t){const a=t instanceof Error?t:new Error(String(t));this.deps.log("VAST preload failed (will load on demand):",a.message),this.deps.diagnose("DA-PRELOAD-FAILED",a.message,{breakId:s.id,assetId:e.id})}}else this.deps.log("No valid VAST tag URL for preload")}preloadChainedSuccessor(e){const t=this.deps.resolveChainedSuccessor(e);t&&this.preloadNext(t)}clearWarmedBreaks(){this.warmedBreakIds.clear()}async preloadNext(s){if(this.warmedBreakIds.has(s.id))return;this.preloadCompanion(s);const a=this.deps.getVariant(s);if(!a||a.format===t.LSHAPE_CONTENT)return;const r=this.deps.getAssets(s)[0];if(!r||r.type!==e.STATIC)return void this.deps.log("Chained successor first asset not static; skipping media warm:",s.id);const i=this.deps.buildStaticAssetUrl(r,s);if(i){this.warmedBreakIds.add(s.id),this.deps.log("Warming chained successor media:",s.id,i),await this.warmCache([i]);try{await(this.deps.getAdPlayer()?.preload?.(i))}catch(e){this.deps.log("Chained prefetch failed (will load on demand):",e)}}}revealFirstFrame(){if(!this.deps.state.firstFrameGateActive||this.deps.state.firstFrameGateShown)return;const e=this.deps.getCurrentBreak();if(!e)return;this.deps.state.firstFrameGateShown=!0,this.deps.show();const t=this.deps.getVariant(e);t&&this.deps.reapplyAdContainerLayout(t),this.deps.onBreakVisible(e)}armAdStartBudget(e){const t=1e3*((this.deps.state.effectiveBreakDurationSec??this.deps.state.currentBreak?.duration??0)+this.deps.state.adBreakCutSafetyMarginSec),s=i(e)&&e.vendor===a.GAM?this.deps.gamPodAdStartBudgetMs:this.deps.adStartBudgetMs,r=Math.max(Math.min(t-100,s),this.deps.minAdStartBudgetMs);this.deps.state.adStartBudgetMs=r,this.deps.state.adStartDeadlineMs=this.deps.clock.now()+r}remainingAdStartBudgetMs(){return Math.max(0,(this.deps.state.adStartDeadlineMs??this.deps.clock.now())-this.deps.clock.now())}loadWithinBudget(e,t){const s=this.deps.state.preloadGeneration,a=this.remainingAdStartBudgetMs(),r=this.deps.clock.now();return new Promise((i,d)=>{let o=!1,n=!1;const l=e=>{o||(o=!0,clearTimeout(p),null!==h&&clearTimeout(h),e.ok?i(!0):d(e.error))},p=setTimeout(()=>{s===this.deps.state.preloadGeneration&&this.deps.onAdError(new m("Ad load timeout","DA-AD-START-TIMEOUT")),o||(o=!0,null!==h&&clearTimeout(h),i(!1))},a);let h=null;a>2e3&&(h=setTimeout(()=>{if(o||s!==this.deps.state.preloadGeneration)return;const a=this.deps.clock.now()-r,i=this.deps.getAdPlayer()?.videoElement;adLoadShowsNoProgress(i)?(n=!0,this.deps.log(`Ad load for ${t.assetId} stalled after ${a}ms; retrying once`),this.deps.diagnose("DA-AD-LOAD-RETRIED",`Ad load for ${t.assetId} in break ${t.breakId} had not settled after ${a}ms; issued a second load()`,{breakId:t.breakId,assetId:t.assetId,uri:t.uri,elapsedMs:a,remainingBudgetMs:this.remainingAdStartBudgetMs()}),e().then(()=>l({ok:!0}),e=>l({ok:!1,error:e}))):this.deps.log(`Ad load for ${t.assetId} still pending after ${a}ms but the element is progressing (readyState=${i?.readyState}, buffered=${i?.buffered.length}); no retry`)},2e3)),e().then(()=>l({ok:!0}),e=>{n||l({ok:!1,error:e})})})}async waitForFirstFrame(){return new Promise((e,t)=>{this.deps.state.firstFrameResolver=()=>{this.clearFirstFrameResolver(),e()},this.deps.state.firstFrameRejector=e=>{this.clearFirstFrameResolver(),t(e)},this.deps.state.firstFrameTimeout=setTimeout(()=>{this.deps.onAdError(new m("First frame timeout","DA-AD-START-TIMEOUT"))},this.remainingAdStartBudgetMs())})}clearFirstFrame(e="Break ended before first frame"){if(this.deps.state.adStartDeadlineMs=null,this.deps.state.adStartBudgetMs=0,this.deps.state.firstFrameTimeout&&(clearTimeout(this.deps.state.firstFrameTimeout),this.deps.state.firstFrameTimeout=null),this.deps.state.firstFrameRejector){const t=this.deps.state.firstFrameRejector;this.deps.state.firstFrameResolver=null,this.deps.state.firstFrameRejector=null,t(new Error(e))}}clearFirstFrameResolver(){this.deps.state.adStartDeadlineMs=null,this.deps.state.adStartBudgetMs=0,this.deps.state.firstFrameTimeout&&(clearTimeout(this.deps.state.firstFrameTimeout),this.deps.state.firstFrameTimeout=null),this.deps.state.firstFrameResolver=null,this.deps.state.firstFrameRejector=null}preloadCompanion(s){const a=this.deps.getCompanionVariant(s);if(!a)return;if(a.format===t.LSHAPE_CONTENT){const t=a.assets[0];if(!t||t.type!==e.STATIC)return;const r=this.deps.buildStaticAssetUrl(t,s);if(!r)return;return(new Image).src=r,void this.deps.log("Preloading lshape_content backdrop:",r)}if(a.format!==t.DOUBLE&&a.format!==t.LSHAPE_AD)return;const r=a.assets[0]?.companion;if(!r||r.type!==e.STATIC)return;const i=this.deps.buildStaticAssetUrl(r,s);if(!i)return;(new Image).src=i,this.deps.log("Preloading companion:",i)}async warmCache(e){"function"==typeof fetch&&await Promise.allSettled(e.map(e=>o(e,{mode:"no-cors"},d).catch(()=>{})))}}
@@ -1 +1 @@
1
- import{getPreparedPresentation as e}from"./PreparedPresentation";import{LAYOUT_TRANSITION as t}from"./presentation/LayoutTransform";import{presentationLayoutFor as s,RESTORED_CONTENT_PLACEMENT as i}from"./presentation/PresentationLayout";import{AssetType as n,BreakFormat as o}from"../../types/BreakManifest";const r=16/9;export class AdStage{constructor(e){this.deps=e,this.contentVideoGeometryHandler=null,this.contentVideoMutationObserver=null,this.resizeObserver=null,this.resizePoll=null,this.contentLayout=null,this.preparedLease=null,this.unsubscribeGeometry=null}setupOverlay(){const s=e(this.deps.container);if(s){const e=this.deps.getPlayerContainer();if(e&&e!==s.playerContainer)throw new Error("The prepared content container cannot be replaced");this.preparedLease=s.acquire(),this.deps.setStage(this.preparedLease.stage),this.deps.setPlayerContainer(this.preparedLease.playerContainer),this.unsubscribeGeometry=this.preparedLease.subscribe(()=>this.deps.resizeVast());try{this.setupMediaLayer()}catch(e){const t=this.deps.getAdContainer();throw t?.parentNode?.removeChild(t),this.deps.setAdContainer(null),this.deps.setAdMediaContainer(null),this.unsubscribeGeometry?.(),this.preparedLease.release(),e}return}const i=window.getComputedStyle(this.deps.container);"static"!==i.position&&""!==i.position||(this.deps.container.style.position="relative"),this.deps.setStage(document.createElement("div"));const n=this.deps.getStage();if(!n)return;if(n.className="dolby-stage",n.style.cssText="position:absolute;overflow:hidden;",this.deps.container.appendChild(n),this.contentVideoGeometryHandler=()=>this.refreshStageGeometry(),"undefined"!=typeof MutationObserver&&(this.contentVideoMutationObserver=new MutationObserver(()=>{(this.deps.contentPlayer.videoElement??null)!==this.deps.getContentVideo()&&this.refreshStageGeometry()}),this.contentVideoMutationObserver.observe(this.deps.container,{childList:!0,subtree:!0})),"undefined"!=typeof ResizeObserver)this.resizeObserver=new ResizeObserver(()=>this.refreshStageGeometry()),this.resizeObserver.observe(this.deps.container);else{let e=this.deps.container.getBoundingClientRect();this.resizePoll=setInterval(()=>{const t=this.deps.container.getBoundingClientRect();t.width===e.width&&t.height===e.height||(e=t,this.refreshStageGeometry())},250)}if(this.refreshStageGeometry(),this.deps.getPlayerContainer()){const e=this.deps.getPlayerContainer();if(!e)return;n.appendChild(e)}else{const e=document.createElement("div"),t=Array.from(this.deps.container.childNodes).filter(e=>e!==n);for(const s of t)e.appendChild(s);n.appendChild(e),this.deps.setPlayerContainer(e),this.deps.onPlayerContainerCreated()}const o=this.deps.getPlayerContainer();o&&(this.deps.setPlayerContainerOriginalStyle(o.style.cssText),o.style.cssText=`position:absolute;top:0;left:0;right:0;bottom:0;transition:${t};`,this.setupMediaLayer())}setupMediaLayer(){const e=this.deps.getStage();if(!e)throw new Error("An ads presentation stage is required");this.deps.setAdContainer(document.createElement("div"));const s=this.deps.getAdContainer();if(!s)return;s.className="dolby-ad-container",s.style.cssText=`display:none;position:absolute;top:0;left:0;right:0;bottom:0;z-index:100;background:#000;transition:${t};`,this.deps.setAdMediaContainer(document.createElement("div"));const i=this.deps.getAdMediaContainer();if(!i)return;i.style.cssText="display:block;width:100%;height:100%;",s.appendChild(i),e.appendChild(s);const n=this.deps.createAdAdapter(i);n!==this.deps.contentPlayer&&(n.prepareForAdPlayback?.(),n.setExternalPlaybackAllowed?.(!1)),this.deps.setAdPlayer(n),this.deps.setupEventListeners(),this.deps.log("Ad player overlay initialized")}refreshStageGeometry(){this.rebindContentVideoGeometryListener(),this.updateStageGeometry(),this.deps.resizeVast()}rebindContentVideoGeometryListener(){const e=this.deps.contentPlayer.videoElement??null,t=e&&"function"==typeof e.addEventListener&&"function"==typeof e.removeEventListener?e:null;t!==this.deps.getContentVideo()&&(this.deps.getContentVideo()&&this.contentVideoGeometryHandler&&(this.deps.getContentVideo()?.removeEventListener("loadedmetadata",this.contentVideoGeometryHandler),this.deps.getContentVideo()?.removeEventListener("resize",this.contentVideoGeometryHandler)),this.deps.setContentVideo(t),t&&this.contentVideoGeometryHandler&&(t.addEventListener("loadedmetadata",this.contentVideoGeometryHandler),t.addEventListener("resize",this.contentVideoGeometryHandler)))}updateStageGeometry(){const e=this.deps.getStage();if(!e)return;const{width:t,height:s}=this.deps.container.getBoundingClientRect();if(0===t||0===s)return;const i=this.getContentAspectRatio();let n,o;t/s>i?(o=s,n=s*i):(n=t,o=t/i);const r=(s-o)/2,a=(t-n)/2;e.style.width=`${n}px`,e.style.height=`${o}px`,e.style.top=`${r}px`,e.style.left=`${a}px`}getContentAspectRatio(){const e=this.deps.contentPlayer.videoElement;if(!e)return r;const{videoWidth:t,videoHeight:s}=e;return!Number.isFinite(t)||!Number.isFinite(s)||t<=0||s<=0?r:t/s}renderLshapeContentBackdrop(e){const t=e.assets[0];if(!t||t.type!==n.STATIC)return;const s=this.deps.buildStaticAssetUrl(t,this.deps.getCurrentBreak());if(!s)return;const i=document.createElement("img");i.className="dolby-companion",i.src=s,i.style.cssText="position:absolute;top:0;left:0;right:0;bottom:0;z-index:99;object-fit:cover;width:100%;height:100%;opacity:0;";const o=this.deps.presentationHost.getSurfaces()?.backdrop;o&&(this.installCompanion(i,o),this.deps.log("Rendered lshape_content backdrop:",s))}reapplyAdContainerLayout(e){const t=this.deps.getAdContainer();if(!t)return;const i=s(e.format);i&&(e.format!==o.LSHAPE_AD&&e.format!==o.DOUBLE||(t.style.zIndex="100"),this.deps.presentationHost.placeAd(i.ad),this.deps.log("reapplyAdContainerLayout: re-stamped adContainer CSS for format:",e.format))}applyLayout(e){const i=this.deps.getAdContainer(),n=this.deps.getPlayerContainer();if(!i||!n)return;const r=s(e.format);if(r){if(e.format!==o.LSHAPE_CONTENT){const e=i.style;e.display="none",e.opacity="0",e.position="absolute",e.top="0",e.left="0",e.right="0",e.bottom="0",e.width="",e.height="",e.zIndex="100",e.background="#000",e.transition=t}return((a=e.format)===o.DOUBLE||a===o.LSHAPE_AD||a===o.LSHAPE_CONTENT)&&(n.style.zIndex="101",this.contentLayout=e.format),e.format===o.LSHAPE_AD?n.style.opacity="0":e.format===o.DOUBLE&&(n.style.opacity=""),e.format===o.SINGLE?this.contentLayout&&(n.style.opacity="0"):this.deps.presentationHost.placeContent(r.content),e.format!==o.LSHAPE_CONTENT&&this.deps.presentationHost.placeAd(r.ad),void((e.format===o.DOUBLE||e.format===o.LSHAPE_AD)&&this.renderCompanion(e.assets,"position:absolute;top:0;left:0;right:0;bottom:0;z-index:99;object-fit:cover;width:100%;height:100%;opacity:0;"))}var a;switch(e.format){case o.OVERLAY:{const t=e,s=e=>void 0!==e?100*e+"%":"auto",n=s(t.position.top),o=s(t.position.bottom),r=s(t.position.left),a=s(t.position.right),d=100*t.size.width+"%",p=100*t.size.height+"%",l=t.opacity??1;i.style.cssText=`display:none;position:absolute;top:${n};bottom:${o};left:${r};right:${a};width:${d};height:${p};z-index:100;opacity:${l};`;break}default:i.style.cssText="display:none;position:absolute;top:0;left:0;right:0;bottom:0;z-index:100;background:#000;"}}restoreLayout(){if(this.preparedLease&&!this.preparedLease.isActive())return;const e=this.deps.getPlayerContainer();if(!e)return;this.contentLayout=null,e.style.zIndex="",e.style.opacity="",this.deps.presentationHost.placeContent(i);const t=this.deps.getCompanion();t&&(this.deps.setCompanion(null),this.retireCompanion(t))}renderCompanion(e,t){const s=e[0]?.companion;if(!s)return;const i=s.type===n.STATIC?this.deps.buildStaticAssetUrl(s,this.deps.getCurrentBreak()):null;if(!i)return;const o=document.createElement("img");o.className="dolby-companion",o.src=i,o.style.cssText=t;const r=this.deps.presentationHost.getSurfaces()?.backdrop;r&&this.installCompanion(o,r)}installCompanion(e,t){const s=this.deps.getCompanion();t.appendChild(e),this.deps.setCompanion(e),e.style.opacity="1",s&&s!==e&&this.retireCompanion(s)}retireCompanion(e){e.parentElement?.removeChild(e)}clearOverlayImage(){const e=this.deps.getImageAssetTimer();e&&(clearTimeout(e),this.deps.setImageAssetTimer(null));const t=this.deps.getOverlayImage();t?.parentElement&&t.parentElement.removeChild(t),this.deps.setOverlayImage(null)}show(){const e=this.deps.getAdContainer();if(!e)return;const t=this.deps.getCurrentBreak(),s=t?this.deps.getVariant(t):null;e.style.display="block",s?.format!==o.OVERLAY&&(e.style.opacity="1")}hide(){const e=this.deps.getAdContainer();e&&(e.style.opacity="0",e.style.display="none")}restoreContent(e,t){if(this.deps.presentationHost.cancelPendingTransitions(),this.releasePreparedPresentation())return!0;const s=this.deps.getPlayerContainer();if(!s)return!1;if(e)for(;s.firstChild;)this.deps.container.appendChild(s.firstChild);else this.deps.container.appendChild(s),s.style.cssText=t;return!1}releasePreparedPresentation(){return!!this.preparedLease&&(this.unsubscribeGeometry?.(),this.unsubscribeGeometry=null,this.preparedLease.release(),!0)}destroy(){this.deps.presentationHost.cancelPendingTransitions(),this.unsubscribeGeometry?.(),this.unsubscribeGeometry=null;const e=this.deps.getContentVideo();e&&this.contentVideoGeometryHandler&&(e.removeEventListener("loadedmetadata",this.contentVideoGeometryHandler),e.removeEventListener("resize",this.contentVideoGeometryHandler)),this.contentVideoMutationObserver?.disconnect(),this.resizeObserver?.disconnect(),null!==this.resizePoll&&clearInterval(this.resizePoll),this.resizePoll=null,this.contentVideoMutationObserver=null,this.resizeObserver=null,this.contentVideoGeometryHandler=null,this.contentLayout=null}}
1
+ import{getPreparedPresentation as e}from"./PreparedPresentation";import{LAYOUT_TRANSITION as t}from"./presentation/LayoutTransform";import{presentationLayoutFor as s,RESTORED_CONTENT_PLACEMENT as n}from"./presentation/PresentationLayout";import{AssetType as i,BreakFormat as o}from"../../types/BreakManifest";import{resolveLshapeAnchor as r}from"../resolveLshapeAnchor";const a=16/9;export class AdStage{constructor(e){this.deps=e,this.contentVideoGeometryHandler=null,this.contentVideoMutationObserver=null,this.resizeObserver=null,this.resizePoll=null,this.contentLayout=null,this.preparedLease=null,this.unsubscribeGeometry=null}setupOverlay(){const s=e(this.deps.container);if(s){const e=this.deps.getPlayerContainer();if(e&&e!==s.playerContainer)throw new Error("The prepared content container cannot be replaced");this.preparedLease=s.acquire(),this.deps.setStage(this.preparedLease.stage),this.deps.setPlayerContainer(this.preparedLease.playerContainer),this.unsubscribeGeometry=this.preparedLease.subscribe(()=>this.deps.resizeVast());try{this.setupMediaLayer()}catch(e){const t=this.deps.getAdContainer();throw t?.parentNode?.removeChild(t),this.deps.setAdContainer(null),this.deps.setAdMediaContainer(null),this.unsubscribeGeometry?.(),this.preparedLease.release(),e}return}const n=window.getComputedStyle(this.deps.container);"static"!==n.position&&""!==n.position||(this.deps.container.style.position="relative"),this.deps.setStage(document.createElement("div"));const i=this.deps.getStage();if(!i)return;if(i.className="dolby-stage",i.style.cssText="position:absolute;overflow:hidden;",this.deps.container.appendChild(i),this.contentVideoGeometryHandler=()=>this.refreshStageGeometry(),"undefined"!=typeof MutationObserver&&(this.contentVideoMutationObserver=new MutationObserver(()=>{(this.deps.contentPlayer.videoElement??null)!==this.deps.getContentVideo()&&this.refreshStageGeometry()}),this.contentVideoMutationObserver.observe(this.deps.container,{childList:!0,subtree:!0})),"undefined"!=typeof ResizeObserver)this.resizeObserver=new ResizeObserver(()=>this.refreshStageGeometry()),this.resizeObserver.observe(this.deps.container);else{let e=this.deps.container.getBoundingClientRect();this.resizePoll=setInterval(()=>{const t=this.deps.container.getBoundingClientRect();t.width===e.width&&t.height===e.height||(e=t,this.refreshStageGeometry())},250)}if(this.refreshStageGeometry(),this.deps.getPlayerContainer()){const e=this.deps.getPlayerContainer();if(!e)return;i.appendChild(e)}else{const e=document.createElement("div"),t=Array.from(this.deps.container.childNodes).filter(e=>e!==i);for(const s of t)e.appendChild(s);i.appendChild(e),this.deps.setPlayerContainer(e),this.deps.onPlayerContainerCreated()}const o=this.deps.getPlayerContainer();o&&(this.deps.setPlayerContainerOriginalStyle(o.style.cssText),o.style.cssText=`position:absolute;top:0;left:0;right:0;bottom:0;transition:${t};`,this.setupMediaLayer())}setupMediaLayer(){const e=this.deps.getStage();if(!e)throw new Error("An ads presentation stage is required");this.deps.setAdContainer(document.createElement("div"));const s=this.deps.getAdContainer();if(!s)return;s.className="dolby-ad-container",s.style.cssText=`display:none;position:absolute;top:0;left:0;right:0;bottom:0;z-index:100;background:#000;transition:${t};`,this.deps.setAdMediaContainer(document.createElement("div"));const n=this.deps.getAdMediaContainer();if(!n)return;n.style.cssText="display:block;width:100%;height:100%;",s.appendChild(n),e.appendChild(s);const i=this.deps.createAdAdapter(n);i!==this.deps.contentPlayer&&(i.prepareForAdPlayback?.(),i.setExternalPlaybackAllowed?.(!1)),this.deps.setAdPlayer(i),this.deps.setupEventListeners(),this.deps.log("Ad player overlay initialized")}refreshStageGeometry(){this.rebindContentVideoGeometryListener(),this.updateStageGeometry(),this.deps.resizeVast()}rebindContentVideoGeometryListener(){const e=this.deps.contentPlayer.videoElement??null,t=e&&"function"==typeof e.addEventListener&&"function"==typeof e.removeEventListener?e:null;t!==this.deps.getContentVideo()&&(this.deps.getContentVideo()&&this.contentVideoGeometryHandler&&(this.deps.getContentVideo()?.removeEventListener("loadedmetadata",this.contentVideoGeometryHandler),this.deps.getContentVideo()?.removeEventListener("resize",this.contentVideoGeometryHandler)),this.deps.setContentVideo(t),t&&this.contentVideoGeometryHandler&&(t.addEventListener("loadedmetadata",this.contentVideoGeometryHandler),t.addEventListener("resize",this.contentVideoGeometryHandler)))}updateStageGeometry(){const e=this.deps.getStage();if(!e)return;const{width:t,height:s}=this.deps.container.getBoundingClientRect();if(0===t||0===s)return;const n=this.getContentAspectRatio();let i,o;t/s>n?(o=s,i=s*n):(i=t,o=t/n);const r=(s-o)/2,a=(t-i)/2;e.style.width=`${i}px`,e.style.height=`${o}px`,e.style.top=`${r}px`,e.style.left=`${a}px`}getContentAspectRatio(){const e=this.deps.contentPlayer.videoElement;if(!e)return a;const{videoWidth:t,videoHeight:s}=e;return!Number.isFinite(t)||!Number.isFinite(s)||t<=0||s<=0?a:t/s}renderLshapeContentBackdrop(e){const t=e.assets[0];if(!t||t.type!==i.STATIC)return;const s=this.deps.buildStaticAssetUrl(t,this.deps.getCurrentBreak());if(!s)return;const n=document.createElement("img");n.className="dolby-companion",n.src=s,n.style.cssText="position:absolute;top:0;left:0;right:0;bottom:0;z-index:99;object-fit:cover;width:100%;height:100%;opacity:0;";const o=this.deps.presentationHost.getSurfaces()?.backdrop;o&&(this.installCompanion(n,o),this.deps.log("Rendered lshape_content backdrop:",s))}reapplyAdContainerLayout(e){const t=this.deps.getAdContainer();if(!t)return;const n=s(e.format,r(e)?.anchor);n&&(e.format!==o.LSHAPE_AD&&e.format!==o.DOUBLE||(t.style.zIndex="100"),this.deps.presentationHost.placeAd(n.ad),this.deps.log("reapplyAdContainerLayout: re-stamped adContainer CSS for format:",e.format))}applyLayout(e){const n=this.deps.getAdContainer(),i=this.deps.getPlayerContainer();if(!n||!i)return;const a=s(e.format,r(e)?.anchor);if(a){if(e.format!==o.LSHAPE_CONTENT){const e=n.style;e.display="none",e.opacity="0",e.position="absolute",e.top="0",e.left="0",e.right="0",e.bottom="0",e.width="",e.height="",e.zIndex="100",e.background="#000",e.transition=t}return((d=e.format)===o.DOUBLE||d===o.LSHAPE_AD||d===o.LSHAPE_CONTENT)&&(i.style.zIndex="101",this.contentLayout=e.format),e.format===o.LSHAPE_AD?i.style.opacity="0":e.format===o.DOUBLE&&(i.style.opacity=""),e.format===o.SINGLE?this.contentLayout&&(i.style.opacity="0"):this.deps.presentationHost.placeContent(a.content),e.format!==o.LSHAPE_CONTENT&&this.deps.presentationHost.placeAd(a.ad),void((e.format===o.DOUBLE||e.format===o.LSHAPE_AD)&&this.renderCompanion(e.assets,"position:absolute;top:0;left:0;right:0;bottom:0;z-index:99;object-fit:cover;width:100%;height:100%;opacity:0;"))}var d;switch(e.format){case o.OVERLAY:{const t=e,s=e=>void 0!==e?100*e+"%":"auto",i=s(t.position.top),o=s(t.position.bottom),r=s(t.position.left),a=s(t.position.right),d=100*t.size.width+"%",p=100*t.size.height+"%",l=t.opacity??1;n.style.cssText=`display:none;position:absolute;top:${i};bottom:${o};left:${r};right:${a};width:${d};height:${p};z-index:100;opacity:${l};`;break}default:n.style.cssText="display:none;position:absolute;top:0;left:0;right:0;bottom:0;z-index:100;background:#000;"}}restoreLayout(){if(this.preparedLease&&!this.preparedLease.isActive())return;const e=this.deps.getPlayerContainer();if(!e)return;this.contentLayout=null,e.style.zIndex="",e.style.opacity="",this.deps.presentationHost.placeContent(n);const t=this.deps.getCompanion();t&&(this.deps.setCompanion(null),this.retireCompanion(t))}renderCompanion(e,t){const s=e[0]?.companion;if(!s)return;const n=s.type===i.STATIC?this.deps.buildStaticAssetUrl(s,this.deps.getCurrentBreak()):null;if(!n)return;const o=document.createElement("img");o.className="dolby-companion",o.src=n,o.style.cssText=t;const r=this.deps.presentationHost.getSurfaces()?.backdrop;r&&this.installCompanion(o,r)}installCompanion(e,t){const s=this.deps.getCompanion();t.appendChild(e),this.deps.setCompanion(e),e.style.opacity="1",s&&s!==e&&this.retireCompanion(s)}retireCompanion(e){e.parentElement?.removeChild(e)}clearOverlayImage(){const e=this.deps.getImageAssetTimer();e&&(clearTimeout(e),this.deps.setImageAssetTimer(null));const t=this.deps.getOverlayImage();t?.parentElement&&t.parentElement.removeChild(t),this.deps.setOverlayImage(null)}show(){const e=this.deps.getAdContainer();if(!e)return;const t=this.deps.getCurrentBreak(),s=t?this.deps.getVariant(t):null;e.style.display="block",s?.format!==o.OVERLAY&&(e.style.opacity="1")}hide(){const e=this.deps.getAdContainer();e&&(e.style.opacity="0",e.style.display="none")}restoreContent(e,t){if(this.deps.presentationHost.cancelPendingTransitions(),this.releasePreparedPresentation())return!0;const s=this.deps.getPlayerContainer();if(!s)return!1;if(e)for(;s.firstChild;)this.deps.container.appendChild(s.firstChild);else this.deps.container.appendChild(s),s.style.cssText=t;return!1}releasePreparedPresentation(){return!!this.preparedLease&&(this.unsubscribeGeometry?.(),this.unsubscribeGeometry=null,this.preparedLease.release(),!0)}destroy(){this.deps.presentationHost.cancelPendingTransitions(),this.unsubscribeGeometry?.(),this.unsubscribeGeometry=null;const e=this.deps.getContentVideo();e&&this.contentVideoGeometryHandler&&(e.removeEventListener("loadedmetadata",this.contentVideoGeometryHandler),e.removeEventListener("resize",this.contentVideoGeometryHandler)),this.contentVideoMutationObserver?.disconnect(),this.resizeObserver?.disconnect(),null!==this.resizePoll&&clearInterval(this.resizePoll),this.resizePoll=null,this.contentVideoMutationObserver=null,this.resizeObserver=null,this.contentVideoGeometryHandler=null,this.contentLayout=null}}
@@ -32,6 +32,7 @@ export interface AssetPlaybackDeps {
32
32
  hasGamIdentityMismatch: (asset: Asset) => boolean;
33
33
  beginGamPodTracking: (asset: Asset) => void;
34
34
  recordAdProgress: (currentTime: number) => void;
35
+ completeCreative: (durationSec: number) => void;
35
36
  reportPhase: (name: string) => void;
36
37
  armAdStartBudget: (asset: Asset) => void;
37
38
  /** The ad queued behind the previous ad: true when it became the current source. */
@@ -1 +1 @@
1
- import{AssetType as e,BreakFormat as t,Vendor as s,MediaType as r,isVastAsset as i}from"../../types/BreakManifest";import{AdError as a}from"../../types/AdError";import{resolveVastAdmission as d}from"../resolveVastAdmission";import{classifyMediaError as n,toClassifiedAdError as o}from"./classifyMediaError";import{VastAdManager as h,isImaCsaiSdkAvailable as c}from"../VastAdManager";import{invalidateMediaPreload as p}from"./AdPlayerState";export class AssetPlayback{constructor(e,t){this.state=e,this.deps=t}get currentBreak(){return this.state.currentBreak}async playAsset(e,t,s){const d=this.currentBreak,n=this.deps.getAdPlayer();if(!d||!n)return;const h=this.state.preparedMedia,c=p(this.state),l=()=>this.state.preloadGeneration===c&&this.currentBreak===d&&this.deps.getAdPlayer()===n;if(this.state.currentAsset=e,this.state.currentAssetElapsedSec=0,this.deps.clearStaticQuartiles(),this.deps.clearOverlayImage(),this.deps.log("Playing asset:",e.id,"type:",e.type,"index:",t),i(e))return void this.playVastAsset(e,t,s);if(e.mediaType===r.IMAGE)return void await this.playImageAsset(e,t,s);this.deps.surfaceReveal.watch(e.id,d.id);const u=await this.resolveAssetUri(e,this.state.effectiveBreakDurationSec??d.duration);if(!l())return;if(!u)return void this.deps.onAdError(this.missingUriError(e));this.deps.log("Resolved URI:",u),this.deps.reportPhase("uri-resolved");const A=d.id;try{this.deps.armAdStartBudget(e);const r=0===t&&h?.breakId===A&&h.assetId===e.id&&h.uri===u&&h.player===n,i=!r&&this.deps.takeQueuedSuccessor(A,e.id,u);if(i)this.deps.reportPhase("load-skipped-queued");else if(r)this.deps.reportPhase("load-skipped-preloaded");else{if(this.deps.reportPhase("load-start"),!await this.deps.loadWithinBudget(()=>n.load(u),{breakId:A,assetId:e.id,uri:u})||!l())return;this.deps.reportPhase("load-resolved")}this.reportPreparedDuration(n,d,e,t,s);const o=n.videoElement;if(o&&(o.style.cssText="display:block;width:100%;height:100%;object-fit:contain;"),this.deps.reportPhase("play-called"),o&&this.deps.surfaceReveal.watchVideo(o),this.state.firstFrameGateActive&&!this.state.firstFrameGateShown){const e=this.deps.waitForFirstFrame();await this.playRetryingInterruption(n,A),await e}else{let e=null;const t=await Promise.race([this.playRetryingInterruption(n,A).then(()=>!0),new Promise(t=>{e=setTimeout(()=>t(!1),this.deps.remainingAdStartBudgetMs())})]);if(e&&clearTimeout(e),!l())return;if(!t)return void this.deps.onAdError(new a(`Ad did not start within ${this.state.adStartBudgetMs}ms ad-start budget`,"DA-AD-START-TIMEOUT"))}if(!l())return;this.state.adStartDeadlineMs=null,this.state.adStartBudgetMs=0,i&&this.state.startPlaybackCutoff?.(),this.deps.transferPictureInPictureToAd();const c=this.deps.getSequencer();if(c){const t=this.deps.isGamPodAsset(e);t&&this.deps.beginGamPodTracking(e),await this.deps.drive(c.onOutcome({kind:"asset-started"})),!t&&l()&&this.deps.queueUpcomingAd()}else this.state.callbacks?.onAdStart(this.currentBreak,e,t,s)}catch(e){if(!l())return;this.deps.onAdError(o(e))}}reportPreparedDuration(e,t,s,r,i){const a=e.duration;if(!Number.isFinite(a)||a<=0)return;const d=s.duration;this.deps.diagnose("DA-AD-DURATION-PREPARED",`Ad media for break ${t.id} prepared: ${a}s of playable ad`,{breakId:t.id,assetId:s.id,index:r,total:i,preparedDurationSec:a,..."number"==typeof d?{declaredDurationSec:d}:{},breakDurationSec:this.state.effectiveBreakDurationSec??t.duration})}async playRetryingInterruption(e,t){const s=this.state.preloadGeneration;try{await e.play()}catch(r){if(s!==this.state.preloadGeneration||!function(e){return"AbortError"===e?.name}(r)||!this.isStartingBreak(t))throw r;this.deps.diagnose("DA-AD-START-INTERRUPTED","Ad play() was interrupted by a browser-initiated pause before the ad started; play() re-issued once.",{breakId:t,error:r instanceof Error?r.message:String(r)}),this.deps.reportPhase("play-retried"),await e.play()}}isStartingBreak(e){return this.state.isPlaying&&this.state.currentBreak?.id===e}playVastAsset(e,s,r){if(!this.currentBreak||!this.deps.getAdPlayer())return;const i=this.deps.getVariant(this.currentBreak)?.format??t.SINGLE,n=d({format:i,ssai:!1});if(!n.admitted)return this.deps.diagnose(n.diagnostic??"DA-VAST-UNSUPPORTED-FORMAT",`VAST asset '${e.id}' is not supported in '${i}' breaks (single/double/lshape_ad only); skipping.`,{breakId:this.currentBreak.id,assetId:e.id,format:i}),void this.deps.onAdError(new a(`VAST not supported in ${i} format`,n.diagnostic??"DA-VAST-UNSUPPORTED-FORMAT"));if(!c())return this.deps.diagnose("DA-VAST-IMA-SDK-MISSING","Google IMA client-side SDK (ima3.js) is unavailable; cannot play VAST ad.",{breakId:this.currentBreak.id,assetId:e.id}),void this.deps.onAdError(new a("IMA client-side SDK (ima3.js) not available","DA-VAST-IMA-SDK-MISSING"));const o=this.deps.buildVastTagUrl(e,this.currentBreak);if(!o)return void this.deps.onAdError(new a("No valid VAST tag URL for asset","DA-AD-URI-MISSING"));const p=this.deps.getAdPlayer(),l=p?.videoElement,u=this.deps.getAdContainer();if(!u||!l||!p)return void this.deps.onAdError(new a("VAST playout requires an ad container and ad video element","DA-AD-URI-MISSING"));this.deps.revealFirstFrame(),this.deps.surfaceReveal.watch(e.id,this.currentBreak.id),l.style.cssText="display:block;width:100%;height:100%;object-fit:contain;",this.state.vastAdManager||(this.state.vastAdManager=new h(u,l,this.deps.debug)),this.state.vastAdManager.setCallbacks({onAdStarted:t=>{if(!this.currentBreak)return;const i=Array.from(u.querySelectorAll("video")).find(e=>!e.paused);this.deps.surfaceReveal.watchVideo(i??l),this.state.startPlaybackCutoff?.(),this.deps.setVastPodPosition(t);const a=this.deps.getSequencer();a?this.deps.drive(a.onOutcome({kind:"asset-started"})):this.state.callbacks?.onAdStart(this.currentBreak,e,s,r,{podPosition:t})},onAdProgress:(t,s)=>{this.currentBreak&&(this.deps.recordAdProgress(t),this.state.callbacks?.onAdTimeUpdate(this.currentBreak,e,t,s))},onAdFirstQuartile:()=>{this.currentBreak&&this.deps.getGamEventCallbacks().onAdFirstQuartile?.(this.currentBreak,e)},onAdMidpoint:()=>{this.currentBreak&&this.deps.getGamEventCallbacks().onAdMidpoint?.(this.currentBreak,e)},onAdThirdQuartile:()=>{this.currentBreak&&this.deps.getGamEventCallbacks().onAdThirdQuartile?.(this.currentBreak,e)}}),this.deps.log("Playing VAST asset via IMA CSAI:",o);(this.state.vastAdManager.hasPreloadedManager()?(this.state.vastAdManager.setMuted(p.muted),this.state.vastAdManager.startPreloaded()):this.state.vastAdManager.playVast(o,{muted:p.muted})).then(()=>{this.deps.onAdEnded()},t=>{const s=t instanceof Error?t.message:String(t),r=/ima3\.js|client-side SDK/i.test(s)?"DA-VAST-IMA-SDK-MISSING":"DA-VAST-IMA-ERROR",i=t instanceof a?t:new a(s,r);this.deps.diagnose(r,s,{...this.currentBreak?{breakId:this.currentBreak.id}:{},assetId:e.id}),this.deps.onAdError(i)})}async playImageAsset(e,t,s){const r=this.currentBreak,i=this.deps.getAdMediaContainer();if(!r||!i)return;const d=this.state.effectiveBreakDurationSec??r.duration,n=await this.resolveAssetUri(e,d);if(!n)return void this.deps.onAdError(this.missingUriError(e));const o=document.createElement("img");o.className="dolby-overlay-image",o.style.cssText="display:block;width:100%;height:100%;object-fit:contain;",this.deps.setOverlayImage(o);const h=e.duration??d;o.onload=()=>{if(this.deps.getOverlayImage()!==o||!this.currentBreak)return;this.state.startPlaybackCutoff?.();const r=this.deps.getSequencer();r?this.deps.drive(r.onOutcome({kind:"asset-started"})):this.state.callbacks?.onAdStart(this.currentBreak,e,t,s),h>0?this.deps.setImageAssetTimer(setTimeout(()=>this.onImageAssetEnded(),1e3*h)):this.onImageAssetEnded()},o.onerror=()=>{this.deps.getOverlayImage()===o&&this.deps.onAdError(new a(`Overlay image failed to load: ${n}`,"DA-IMAGE-LOAD-FAILED"))},o.src=n,i.appendChild(o)}onImageAssetEnded(){const e=this.deps.getSequencer();if(!this.currentBreak||!this.state.isPlaying||!e)return;const t=this.deps.getImageAssetTimer();t&&(clearTimeout(t),this.deps.setImageAssetTimer(null)),this.deps.drive(e.onOutcome({kind:"asset-ended"}))}async resolveAssetUri(t,r,i=this.currentBreak){switch(t.type){case e.STATIC:return this.deps.buildStaticAssetUrl(t,i);case e.VENDOR:{const e=t;return e.vendor===s.GAM?await this.deps.buildGamUrl(e,r):(this.deps.log("Unsupported vendor:",e.vendor),null)}default:return this.deps.log("Unsupported asset type:",t.type),null}}adPlayerStreamRemainingSec(){const e=this.deps.getAdPlayer();if(!e)return 0;const{duration:t,currentTime:s}=e;if(!Number.isFinite(t)||t<=0)return 0;if(!Number.isFinite(s)||s<0)return 0;const r=t-s;return r>0?r:0}revealFirstFrame(){this.deps.revealFirstFrame()}toAdError(e){const t=e,s=n(e);if(t&&("number"==typeof t.code||t.message)){const e="number"==typeof t.code?` (code ${t.code})`:"",r=t.message?`: ${t.message}`:"";return new a(`Ad playback error${e}${r}`,s)}return new a("Ad playback error",s)}missingUriError(e){return this.deps.hasGamIdentityMismatch(e)?new a("GAM pod asset identity does not match the stream session","DA-GAM-POD-IDENTITY-MISMATCH"):new a("No valid URI for asset","DA-AD-URI-MISSING")}}
1
+ import{AssetType as e,BreakFormat as t,Vendor as s,MediaType as r,isVastAsset as a}from"../../types/BreakManifest";import{AdError as i}from"../../types/AdError";import{resolveVastAdmission as d}from"../resolveVastAdmission";import{classifyMediaError as n,toClassifiedAdError as o}from"./classifyMediaError";import{VastAdManager as h,isImaCsaiSdkAvailable as c}from"../VastAdManager";import{invalidateMediaPreload as p}from"./AdPlayerState";export class AssetPlayback{constructor(e,t){this.state=e,this.deps=t}get currentBreak(){return this.state.currentBreak}async playAsset(e,t,s){const d=this.currentBreak,n=this.deps.getAdPlayer();if(!d||!n)return;const h=this.state.preparedMedia,c=p(this.state),l=()=>this.state.preloadGeneration===c&&this.currentBreak===d&&this.deps.getAdPlayer()===n;if(this.state.currentAsset=e,this.state.currentAssetElapsedSec=0,this.state.currentAssetBaseSec=0,this.deps.clearStaticQuartiles(),this.deps.clearOverlayImage(),this.deps.log("Playing asset:",e.id,"type:",e.type,"index:",t),a(e))return void this.playVastAsset(e,t,s);if(e.mediaType===r.IMAGE)return void await this.playImageAsset(e,t,s);this.deps.surfaceReveal.watch(e.id,d.id);const u=await this.resolveAssetUri(e,this.state.effectiveBreakDurationSec??d.duration);if(!l())return;if(!u)return void this.deps.onAdError(this.missingUriError(e));this.deps.log("Resolved URI:",u),this.deps.reportPhase("uri-resolved");const A=d.id;try{this.deps.armAdStartBudget(e);const r=0===t&&h?.breakId===A&&h.assetId===e.id&&h.uri===u&&h.player===n,a=!r&&this.deps.takeQueuedSuccessor(A,e.id,u);if(a)this.deps.reportPhase("load-skipped-queued");else if(r)this.deps.reportPhase("load-skipped-preloaded");else{if(this.deps.reportPhase("load-start"),!await this.deps.loadWithinBudget(()=>n.load(u),{breakId:A,assetId:e.id,uri:u})||!l())return;this.deps.reportPhase("load-resolved")}this.reportPreparedDuration(n,d,e,t,s);const o=n.videoElement;if(o&&(o.style.cssText="display:block;width:100%;height:100%;object-fit:contain;"),this.deps.reportPhase("play-called"),o&&this.deps.surfaceReveal.watchVideo(o),this.state.firstFrameGateActive&&!this.state.firstFrameGateShown){const e=this.deps.waitForFirstFrame();await this.playRetryingInterruption(n,A),await e}else{let e=null;const t=await Promise.race([this.playRetryingInterruption(n,A).then(()=>!0),new Promise(t=>{e=setTimeout(()=>t(!1),this.deps.remainingAdStartBudgetMs())})]);if(e&&clearTimeout(e),!l())return;if(!t)return void this.deps.onAdError(new i(`Ad did not start within ${this.state.adStartBudgetMs}ms ad-start budget`,"DA-AD-START-TIMEOUT"))}if(!l())return;this.state.adStartDeadlineMs=null,this.state.adStartBudgetMs=0,a&&this.state.startPlaybackCutoff?.(),this.deps.transferPictureInPictureToAd();const c=this.deps.getSequencer();if(c){const t=this.deps.isGamPodAsset(e);t&&this.deps.beginGamPodTracking(e),await this.deps.drive(c.onOutcome({kind:"asset-started"})),!t&&l()&&this.deps.queueUpcomingAd()}else this.state.callbacks?.onAdStart(this.currentBreak,e,t,s)}catch(e){if(!l())return;this.deps.onAdError(o(e))}}reportPreparedDuration(e,t,s,r,a){const i=e.duration;if(!Number.isFinite(i)||i<=0)return;const d=s.duration;this.deps.diagnose("DA-AD-DURATION-PREPARED",`Ad media for break ${t.id} prepared: ${i}s of playable ad`,{breakId:t.id,assetId:s.id,index:r,total:a,preparedDurationSec:i,..."number"==typeof d?{declaredDurationSec:d}:{},breakDurationSec:this.state.effectiveBreakDurationSec??t.duration})}async playRetryingInterruption(e,t){const s=this.state.preloadGeneration;try{await e.play()}catch(r){if(s!==this.state.preloadGeneration||!function(e){return"AbortError"===e?.name}(r)||!this.isStartingBreak(t))throw r;this.deps.diagnose("DA-AD-START-INTERRUPTED","Ad play() was interrupted by a browser-initiated pause before the ad started; play() re-issued once.",{breakId:t,error:r instanceof Error?r.message:String(r)}),this.deps.reportPhase("play-retried"),await e.play()}}isStartingBreak(e){return this.state.isPlaying&&this.state.currentBreak?.id===e}playVastAsset(e,s,r){if(!this.currentBreak||!this.deps.getAdPlayer())return;const a=this.deps.getVariant(this.currentBreak)?.format??t.SINGLE,n=d({format:a,ssai:!1});if(!n.admitted)return this.deps.diagnose(n.diagnostic??"DA-VAST-UNSUPPORTED-FORMAT",`VAST asset '${e.id}' is not supported in '${a}' breaks (single/double/lshape_ad only); skipping.`,{breakId:this.currentBreak.id,assetId:e.id,format:a}),void this.deps.onAdError(new i(`VAST not supported in ${a} format`,n.diagnostic??"DA-VAST-UNSUPPORTED-FORMAT"));if(!c())return this.deps.diagnose("DA-VAST-IMA-SDK-MISSING","Google IMA client-side SDK (ima3.js) is unavailable; cannot play VAST ad.",{breakId:this.currentBreak.id,assetId:e.id}),void this.deps.onAdError(new i("IMA client-side SDK (ima3.js) not available","DA-VAST-IMA-SDK-MISSING"));const o=this.deps.buildVastTagUrl(e,this.currentBreak);if(!o)return void this.deps.onAdError(new i("No valid VAST tag URL for asset","DA-AD-URI-MISSING"));const p=this.deps.getAdPlayer(),l=p?.videoElement,u=this.deps.getAdContainer();if(!u||!l||!p)return void this.deps.onAdError(new i("VAST playout requires an ad container and ad video element","DA-AD-URI-MISSING"));this.deps.revealFirstFrame(),this.deps.surfaceReveal.watch(e.id,this.currentBreak.id),l.style.cssText="display:block;width:100%;height:100%;object-fit:contain;",this.state.vastAdManager||(this.state.vastAdManager=new h(u,l,this.deps.debug)),this.state.vastAdManager.setCallbacks({onAdStarted:t=>{if(!this.currentBreak)return;const a=Array.from(u.querySelectorAll("video")).find(e=>!e.paused);this.deps.surfaceReveal.watchVideo(a??l),this.state.startPlaybackCutoff?.(),this.deps.setVastPodPosition(t);const i=this.deps.getSequencer();i?this.deps.drive(i.onOutcome({kind:"asset-started"})):this.state.callbacks?.onAdStart(this.currentBreak,e,s,r,{podPosition:t})},onAdProgress:(t,s)=>{this.currentBreak&&(this.deps.recordAdProgress(t),this.state.callbacks?.onAdTimeUpdate(this.currentBreak,e,t,s))},onAdCompleted:e=>this.deps.completeCreative(e),onAdFirstQuartile:()=>{this.currentBreak&&this.deps.getGamEventCallbacks().onAdFirstQuartile?.(this.currentBreak,e)},onAdMidpoint:()=>{this.currentBreak&&this.deps.getGamEventCallbacks().onAdMidpoint?.(this.currentBreak,e)},onAdThirdQuartile:()=>{this.currentBreak&&this.deps.getGamEventCallbacks().onAdThirdQuartile?.(this.currentBreak,e)}}),this.deps.log("Playing VAST asset via IMA CSAI:",o);(this.state.vastAdManager.hasPreloadedManager()?(this.state.vastAdManager.setMuted(p.muted),this.state.vastAdManager.startPreloaded()):this.state.vastAdManager.playVast(o,{muted:p.muted})).then(()=>{this.deps.onAdEnded()},t=>{const s=t instanceof Error?t.message:String(t),r=/ima3\.js|client-side SDK/i.test(s)?"DA-VAST-IMA-SDK-MISSING":"DA-VAST-IMA-ERROR",a=t instanceof i?t:new i(s,r);this.deps.diagnose(r,s,{...this.currentBreak?{breakId:this.currentBreak.id}:{},assetId:e.id}),this.deps.onAdError(a)})}async playImageAsset(e,t,s){const r=this.currentBreak,a=this.deps.getAdMediaContainer();if(!r||!a)return;const d=this.state.effectiveBreakDurationSec??r.duration,n=await this.resolveAssetUri(e,d);if(!n)return void this.deps.onAdError(this.missingUriError(e));const o=document.createElement("img");o.className="dolby-overlay-image",o.style.cssText="display:block;width:100%;height:100%;object-fit:contain;",this.deps.setOverlayImage(o);const h=e.duration??d;o.onload=()=>{if(this.deps.getOverlayImage()!==o||!this.currentBreak)return;this.state.startPlaybackCutoff?.();const r=this.deps.getSequencer();r?this.deps.drive(r.onOutcome({kind:"asset-started"})):this.state.callbacks?.onAdStart(this.currentBreak,e,t,s),h>0?this.deps.setImageAssetTimer(setTimeout(()=>this.onImageAssetEnded(),1e3*h)):this.onImageAssetEnded()},o.onerror=()=>{this.deps.getOverlayImage()===o&&this.deps.onAdError(new i(`Overlay image failed to load: ${n}`,"DA-IMAGE-LOAD-FAILED"))},o.src=n,a.appendChild(o)}onImageAssetEnded(){if(!this.currentBreak||!this.state.isPlaying||!this.deps.getSequencer())return;const e=this.deps.getImageAssetTimer();e&&(clearTimeout(e),this.deps.setImageAssetTimer(null)),this.deps.onAdEnded()}async resolveAssetUri(t,r,a=this.currentBreak){switch(t.type){case e.STATIC:return this.deps.buildStaticAssetUrl(t,a);case e.VENDOR:{const e=t;return e.vendor===s.GAM?await this.deps.buildGamUrl(e,r):(this.deps.log("Unsupported vendor:",e.vendor),null)}default:return this.deps.log("Unsupported asset type:",t.type),null}}adPlayerStreamRemainingSec(){const e=this.deps.getAdPlayer();if(!e)return 0;const{duration:t,currentTime:s}=e;if(!Number.isFinite(t)||t<=0)return 0;if(!Number.isFinite(s)||s<0)return 0;const r=t-s;return r>0?r:0}revealFirstFrame(){this.deps.revealFirstFrame()}toAdError(e){const t=e,s=n(e);if(t&&("number"==typeof t.code||t.message)){const e="number"==typeof t.code?` (code ${t.code})`:"",r=t.message?`: ${t.message}`:"";return new i(`Ad playback error${e}${r}`,s)}return new i("Ad playback error",s)}missingUriError(e){return this.deps.hasGamIdentityMismatch(e)?new i("GAM pod asset identity does not match the stream session","DA-GAM-POD-IDENTITY-MISMATCH"):new i("No valid URI for asset","DA-AD-URI-MISSING")}}
@@ -1,6 +1,6 @@
1
1
  import type { Asset, Break } from '../../types/BreakManifest';
2
2
  import type { AdPlayerCallbacks, PlayBreakOptions } from '../AdPlayerController';
3
- import type { AdBreakAction, AdBreakStep, AdBreakTransition, EffectiveInsertionMode } from '../AdBreakSequencer';
3
+ import type { AdBreakAction, AdBreakStep, AdBreakTransition, EffectiveInsertionMode, SlateNotStartedNotice, SlateStartReason } from '../AdBreakSequencer';
4
4
  import type { GamPodAdStep } from '../GamPodAdTracker';
5
5
  import { type AdPlayerState } from './AdPlayerState';
6
6
  import type { SuccessorQueue } from './SuccessorQueue';
@@ -20,6 +20,12 @@ export interface BreakSequenceDriverDeps {
20
20
  resetGamPodTracking: () => void;
21
21
  emitGamPodSteps: (steps: GamPodAdStep[]) => void;
22
22
  playAsset: (asset: Asset, index: number, total: number) => Promise<void>;
23
+ /** Plays the manifest slate for the part of the break no ad covers. */
24
+ playSlate: (breakInfo: Break, plan: {
25
+ durationSec: number;
26
+ reason: SlateStartReason;
27
+ }) => void;
28
+ diagnoseSlateNotStarted: (notices: SlateNotStartedNotice[]) => void;
23
29
  endBreak: () => void;
24
30
  playBreak: (breakInfo: Break, callbacks: AdPlayerCallbacks, options?: PlayBreakOptions) => Promise<void>;
25
31
  diagnose: (code: string, message: string, context?: Record<string, unknown>) => void;
@@ -1 +1 @@
1
- import{invalidateMediaPreload as e}from"./AdPlayerState";export class BreakSequenceDriver{constructor(e,s){this.state=e,this.deps=s}async drive(e,s=!1){for(const s of e.noPlayableAsset??[])this.deps.diagnose("DA-BREAK-NO-PLAYABLE-ASSET",`break ${s.breakId} had no playable ad (${s.reason}); it began and ended without an ad.`,{breakId:s.breakId,reason:s.reason,declaredAssetCount:s.declaredAssetCount,...void 0!==s.format?{format:s.format}:{}});const{action:t}=e;"play-asset"===t.kind&&t.index>0&&this.state.currentBreak&&this.deps.successorQueue.confirm(this.state.currentBreak,t.asset);for(const s of e.steps)this.emitStep(s);await this.performAction(t,s)}emitStep(e){if(this.state.currentBreak)switch(e.type){case"adbreakbegin":case"adbreakend":return;case"adbegin":return this.deps.isExpandingPodAsset(e.asset)?void("pending"===this.state.gamPodMode&&(this.state.withheldPodAdBegin=e)):void(void 0!==this.deps.getVastPodPosition()?(this.state.callbacks?.onAdStart(this.state.currentBreak,e.asset,e.adIndex,e.totalAds,{podPosition:this.deps.getVastPodPosition()}),this.deps.setVastPodPosition(void 0)):this.state.callbacks?.onAdStart(this.state.currentBreak,e.asset,e.adIndex,e.totalAds));case"adend":if(this.settlePodAdEnd(e))return;return this.state.callbacks?.onAdEnd(this.state.currentBreak,e.asset,e.adIndex,e.totalAds),void this.deps.resetGamPodTracking();case"aderror":{this.deps.isExpandingPodAsset(e.asset)&&(this.state.withheldPodAdBegin=null,this.state.gamPodTracker?.sawAnyAd&&this.emitGamPodSteps(this.state.gamPodTracker.finish()));const s=this.deps.getPendingAdError()??new Error("Ad playback error");return this.deps.setPendingAdError(null),void this.state.callbacks?.onAdError(this.state.currentBreak,e.asset,s)}}}settlePodAdEnd(e){return!!this.deps.isExpandingPodAsset(e.asset)&&(this.state.gamPodTracker?.sawAnyAd?(this.emitGamPodSteps(this.state.gamPodTracker.finish()),this.deps.resetGamPodTracking(),!0):(this.deps.fallBackToPodLevelReporting(),!1))}async performAction(e,s=!1){switch(e.kind){case"play-asset":return void await this.deps.playAsset(e.asset,e.index,e.total);case"end-break":return void this.deps.endBreak();case"prepare-successor":return void this.deps.successorQueue.prepare(e.break,e.asset,s);case"drop-successor":return void this.deps.successorQueue.drop(e.breakId);default:return}}emitGamPodSteps(e){this.deps.emitGamPodSteps(e)}resolveChainedSuccessor(e){const s=this.deps.getChainResolver();if(!s)return null;if("shared-element"===this.deps.getEffectiveInsertionMode())return null;try{return s(e)}catch(e){return this.deps.log("Chain resolver error:",e),null}}advanceToChainedBreak(s,t){this.deps.log(`Chaining: "${s.id}" -> "${t.id}" (overlay held, content stays paused)`),this.deps.successorQueue.confirm(t,this.deps.getAssets(t)[0]),this.state.currentBreak=null,this.state.effectiveBreakDurationSec=null,e(this.state);const a=this.state.callbacks;a?.onBreakEnd(s),a?.onChainedBreakBegin?.(t),a&&(this.deps.setChaining(!0),this.deps.playBreak(t,a).finally(()=>{this.deps.setChaining(!1)}))}}
1
+ import{AdError as e}from"../../types/AdError";import{invalidateMediaPreload as s}from"./AdPlayerState";export class BreakSequenceDriver{constructor(e,s){this.state=e,this.deps=s}async drive(s,t=!1){const{action:a}=s,d="play-slate"===a.kind;for(const e of s.noPlayableAsset??[])this.deps.diagnose("DA-BREAK-NO-PLAYABLE-ASSET",`break ${e.breakId} had no playable ad (${e.reason}); `+(d?"the slate covers the break.":"it began and ended without an ad."),{breakId:e.breakId,reason:e.reason,declaredAssetCount:e.declaredAssetCount,...void 0!==e.format?{format:e.format}:{}});if(this.deps.diagnoseSlateNotStarted(s.slateNotStarted??[]),"play-slate"===a.kind&&"no-ad"===a.reason&&!this.deps.getPendingAdError()){const t=s.noPlayableAsset?.[0]?.reason??"no-assets";this.deps.setPendingAdError(new e(`Break has no playable ad (${t})`,"DA-BREAK-NO-PLAYABLE-ASSET"))}"play-asset"===a.kind&&a.index>0&&this.state.currentBreak&&this.deps.successorQueue.confirm(this.state.currentBreak,a.asset);for(const e of s.steps)this.emitStep(e);await this.performAction(a,t)}emitStep(e){if(this.state.currentBreak)switch(e.type){case"adbreakbegin":case"adbreakend":return;case"adbegin":return this.deps.isExpandingPodAsset(e.asset)?void("pending"===this.state.gamPodMode&&(this.state.withheldPodAdBegin=e)):void(void 0!==this.deps.getVastPodPosition()?(this.state.callbacks?.onAdStart(this.state.currentBreak,e.asset,e.adIndex,e.totalAds,{podPosition:this.deps.getVastPodPosition()}),this.deps.setVastPodPosition(void 0)):this.state.callbacks?.onAdStart(this.state.currentBreak,e.asset,e.adIndex,e.totalAds));case"adend":if(this.settlePodAdEnd(e))return;return this.state.callbacks?.onAdEnd(this.state.currentBreak,e.asset,e.adIndex,e.totalAds),void this.deps.resetGamPodTracking();case"aderror":{this.deps.isExpandingPodAsset(e.asset)&&(this.state.withheldPodAdBegin=null);const s=this.deps.getPendingAdError()??new Error("Ad playback error");return this.deps.setPendingAdError(null),this.state.callbacks?.onAdError(this.state.currentBreak,e.asset,s,void 0!==e.adIndex&&void 0!==e.totalAds?{adIndex:e.adIndex,totalAds:e.totalAds}:void 0),void(this.deps.isExpandingPodAsset(e.asset)&&this.state.gamPodTracker?.sawAnyAd&&this.emitGamPodSteps(this.state.gamPodTracker.finish()))}}}settlePodAdEnd(e){return!!this.deps.isExpandingPodAsset(e.asset)&&(this.state.withheldPodAdBegin||this.state.gamPodTracker?.sawAnyAd||"pending"!==this.state.gamPodMode?this.state.gamPodTracker?.sawAnyAd?(this.emitGamPodSteps(this.state.gamPodTracker.finish()),this.deps.resetGamPodTracking(),!0):(this.deps.fallBackToPodLevelReporting(),!1):(this.deps.resetGamPodTracking(),!0))}async performAction(e,s=!1){switch(e.kind){case"play-asset":return void await this.deps.playAsset(e.asset,e.index,e.total);case"end-break":return void this.deps.endBreak();case"prepare-successor":return void this.deps.successorQueue.prepare(e.break,e.asset,s);case"drop-successor":return void this.deps.successorQueue.drop(e.breakId);case"play-slate":return void 0!==e.dropSuccessorId&&this.deps.successorQueue.drop(e.dropSuccessorId),void(this.state.currentBreak&&this.deps.playSlate(this.state.currentBreak,e));default:return}}emitGamPodSteps(e){this.deps.emitGamPodSteps(e)}resolveChainedSuccessor(e){const s=this.deps.getChainResolver();if(!s)return null;if("shared-element"===this.deps.getEffectiveInsertionMode())return null;try{return s(e)}catch(e){return this.deps.log("Chain resolver error:",e),null}}advanceToChainedBreak(e,t){this.deps.log(`Chaining: "${e.id}" -> "${t.id}" (overlay held, content stays paused)`),this.deps.successorQueue.confirm(t,this.deps.getAssets(t)[0]),this.state.currentBreak=null,this.state.effectiveBreakDurationSec=null,s(this.state);const a=this.state.callbacks;a?.onBreakEnd(e),a?.onChainedBreakBegin?.(t),a&&(this.deps.setChaining(!0),this.deps.playBreak(t,a).finally(()=>{this.deps.setChaining(!1)}))}}
@@ -116,6 +116,12 @@ export declare class ContentResumeCoordinator {
116
116
  resetBreakResumeState(): void;
117
117
  beginContinueContentPlayout(): void;
118
118
  distanceToLiveEdgeSec(): number | null;
119
+ /**
120
+ * The break's playable length: the tune-in remainder when the viewer joined part-way, else its
121
+ * declared duration. The one number every "time left" below reads, so content reaches its resume
122
+ * point at the instant the ad (or slate) is cut.
123
+ */
124
+ private breakLengthSec;
119
125
  secondsUntilCut(elapsedWallSec: number): number;
120
126
  restoreContinueQuality(stage: 'staged' | 'break-end'): void;
121
127
  currentContinueDriftSec(): number;
@@ -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 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))}}
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 h,isPlausibleSeekEnd as d,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 h(this.deps.contentPlayer)}resumeAnchorCushionSec(){const e=this.state.resumeAnchorSeekableEnd;return null!==e&&Number.isFinite(e)?f(e-this.state.contentPositionAtBreakStart):null}isPlausibleSeekEnd(e){return d(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=this.breakLengthSec(e)-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=this.breakLengthSec(e)-(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=this.breakLengthSec(e)-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)}breakLengthSec(e){return this.state.effectiveBreakDurationSec??e.duration??0}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),h=null!==l||i?null:this.reportCueEnd(t,u.target),d=l??h??u.target;this.resumePolicyTarget=d;const c=this.streamClock.seekBasis(t)??this.pdtClock.seekBasis()??(null!==h?"cue_end":`offset ${u.offset}s, ${u.source}`);this.deps.log(`Resume-seek to ${d}s (${c}) for break ${t.id}`);const m=this.resumeAlignSummary(),p=this.liveSafeSeekEnd();this.deps.diagnose?.("DA-RESUME-SEEK",`Resume-seek to ${d}s (${c}) for break ${t.id}`,{breakId:t.id,target:d,offset:u.offset,source:u.source,...d===u.target?{}:{computedTarget:u.target},...null===h?{}:{cueEndTarget:h},...this.streamClock.seekContext(t,d,u.target),...this.pdtClock.seekContext(t,d,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,d,u.target,i),this.pdtClock.reportCorrection(t,d,u.target,i);(this.state.breakTimebase===e.PTS||this.state.breakTimebase===e.MEDIATIME)&&"number"==typeof t.start&&this.state.callbacks?.onContentResumeSeek?.(d,t.start+t.duration);const k=this.liveSafeSeekStart(),f=n({target:d,safeStart:k,safeEnd:p,windowSec:this.liveSeekableWindowSec()}),{target:b,shortfallSec:C,bound:P}=f;if(f.startClampSkipped){const{behindSec:e,windowSec:s,maxBehindSec:n}=f.startClampSkipped;this.deps.log(`Resume-seek: ${d}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 ${d}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:d,seekableStart:k,behindSec:e,windowSec:s,maxBehindSec:n,...this.resumeAlignSummary()})}if(null!==this.state.resumePrepositionedTo&&Math.abs(this.deps.contentPlayer.currentTime-b)<=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)`+(C>0?` — ${C}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`+(C>0?` (${C}s short of the ${d}s policy target)`:""),{breakId:t.id,target:d,prepositionedTo:this.state.resumePrepositionedTo,landed:e,shortfallSec:C,exact:0===C,...this.resumeAlignSummary()}),this.streamClock.armLanding(t,e),this.pdtClock.armLanding(t),null}return C>0&&(this.deps.log("start"===P?`Resume-seek clamped: ${d}s is behind the live window start (already evicted); seeking ${b}s instead (${C}s early)`:`Resume-seek clamped: ${d}s is beyond what the stream has published; seeking ${b}s instead (${C}s short)`),this.deps.diagnose?.("DA-RESUME-TARGET-CLAMPED","start"===P?`Resume target ${d}s for break ${t.id} is behind the live window start (already evicted); clamped to ${b}s`:`Resume target ${d}s for break ${t.id} is beyond the published window; clamped to ${b}s`,{breakId:t.id,requested:d,clamped:b,shortfallSec:C,bound:P,...this.resumeAlignSummary()})),this.streamClock.armLanding(t,b),this.pdtClock.armLanding(t),b}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,5 +1,5 @@
1
1
  import type { PlayerAdapter } from '../../interfaces/PlayerAdapter';
2
- import { StaticAsset } from '../../types/BreakManifest';
2
+ import { MediaType, StaticAsset } from '../../types/BreakManifest';
3
3
  import type { AssetUri, Break } from '../../types/BreakManifest';
4
4
  import type { PauseAdResolution } from '../PauseAdController';
5
5
  export interface PauseAdPresenterDeps {
@@ -17,21 +17,26 @@ export declare class PauseAdPresenter {
17
17
  private pauseAdVideoEl;
18
18
  private pauseAdFadeTimer;
19
19
  private pauseAdPinnedPosition;
20
+ private pauseAdGeneration;
21
+ private pauseTracking;
20
22
  /** Resume/close controls of the mounted pause ad, hidden until the controller allows resuming. */
21
23
  private pauseAdResumeControls;
24
+ private pauseAdResumeRevealed;
25
+ private pauseAdCreativeResolvedCallback;
22
26
  private pauseAdCreativeEndedCallback;
23
27
  constructor(deps: PauseAdPresenterDeps);
24
28
  /**
25
29
  * Full-screen pause ad over the paused content: an `<img>` or a muted, play-once `<video>`
26
30
  * as click-through target, plus SDK-rendered resume and close buttons because the player's
27
- * own controls are covered. For `vast` the tag is fetched and its CompanionAds StaticResource
28
- * (image) or Linear MediaFile (video) is parsed.
31
+ * own controls are covered. VAST wrappers resolve to the selected image or video creative.
29
32
  */
30
33
  showPauseImage(resolution: PauseAdResolution): Promise<void>;
31
34
  /** A video creative is stopped immediately so it does not keep playing under the fade. */
32
35
  hidePauseImage(): void;
33
36
  /** Show the resume and close controls of the mounted pause ad (the viewer may resume content). */
34
37
  revealPauseAdResume(): void;
38
+ /** Report the resolved kind, or null when the creative cannot play. */
39
+ setPauseAdCreativeResolvedCallback(callback: ((breakId: string, kind: MediaType | null) => void) | null): void;
35
40
  /** Invoked when a video pause-ad creative ends (or cannot play). Pass `null` to detach. */
36
41
  setPauseAdCreativeEndedCallback(callback: ((breakId: string) => void) | null): void;
37
42
  destroy(): void;
@@ -52,11 +57,7 @@ export declare class PauseAdPresenter {
52
57
  private buildPauseResumeButton;
53
58
  /** A top-right close button that also resumes content on click. */
54
59
  private buildPauseCloseButton;
55
- /**
56
- * For `static` the asset uri is the creative URL. For `vast` the tag is parsed by media type:
57
- * video reads the Linear MediaFile, image the CompanionAds StaticResource. Null when none is usable.
58
- */
59
60
  private resolvePauseAsset;
60
- /** Fire companion creativeView impression beacons (fire-and-forget). */
61
- private firePauseImpressions;
61
+ private fetchPauseVast;
62
+ private firePauseBeacons;
62
63
  }
@@ -1 +1 @@
1
- import{MediaType as e}from"../../types/BreakManifest";import{AUX_FETCH_TIMEOUT_MS as t,fetchWithTimeout as s}from"../fetchWithTimeout";import{parseVastCompanionImage as i}from"../parseVastCompanion";import{parseVastLinearMediaFile as a}from"../parseVastLinear";import{resolvePauseResumePoint as n}from"../resolvePauseResumePoint";export class PauseAdPresenter{constructor(e){this.deps=e,this.pauseAdContainer=null,this.pauseAdVideoEl=null,this.pauseAdFadeTimer=null,this.pauseAdPinnedPosition=0,this.pauseAdResumeControls=[],this.pauseAdCreativeEndedCallback=null}async showPauseImage(e){const t=this.deps.getStage();if(!t)return;const s=await this.resolvePauseAsset(e);if(!s)return void this.deps.diagnose("DA-PAUSE-AD-NO-ASSET",`Pause ad '${e.break.id}' asset could not be resolved; nothing shown.`,{breakId:e.break.id,source:e.source,mediaType:e.mediaType});this.removePauseAdNow(),this.pauseAdPinnedPosition=this.deps.contentPlayer.currentTime;const i=document.createElement("div");if(i.className="dolby-pause-ad",i.style.cssText="position:absolute;top:0;left:0;right:0;bottom:0;z-index:150;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.35);opacity:0;transition:opacity 300ms ease-in-out;"+(s.clickThrough?"cursor:pointer;":""),"video"===s.kind){const e=document.createElement("video");e.className="dolby-pause-ad-video",e.style.cssText="max-width:100%;max-height:100%;object-fit:contain;",e.muted=!0,e.loop=!1,e.autoplay=!0,e.playsInline=!0,e.setAttribute("playsinline",""),e.src=s.url,this.pauseAdVideoEl=e,i.appendChild(e)}else{const e=document.createElement("img");e.className="dolby-pause-ad-image",e.style.cssText="max-width:100%;max-height:100%;object-fit:contain;",e.src=s.url,i.appendChild(e)}if(s.clickThrough){const e=s.clickThrough;i.addEventListener("click",()=>{window.open(e,"_blank","noopener")})}const a=this.buildPauseResumeButton(),n=this.buildPauseCloseButton();if(a.style.display="none",n.style.display="none",this.pauseAdResumeControls=[a,n],i.appendChild(a),i.appendChild(n),this.pauseAdVideoEl){const t=this.pauseAdVideoEl,s=e.break.id,i=()=>{this.pauseAdVideoEl===t&&this.pauseAdCreativeEndedCallback?.(s)};t.addEventListener("ended",i,{once:!0}),t.addEventListener("error",i,{once:!0}),t.play().catch(i)}t.appendChild(i),this.pauseAdContainer=i,requestAnimationFrame(()=>{this.pauseAdContainer===i&&(i.style.opacity="1")}),this.firePauseImpressions(s.impressions),this.deps.log("Pause ad shown:",e.break.id,e.source,e.mediaType)}hidePauseImage(){const e=this.pauseAdContainer;e&&(this.stopPauseAdVideo(),this.pauseAdContainer=null,e.style.opacity="0",this.pauseAdFadeTimer&&clearTimeout(this.pauseAdFadeTimer),this.pauseAdFadeTimer=setTimeout(()=>{e.parentElement&&e.parentElement.removeChild(e),this.pauseAdFadeTimer=null},300))}revealPauseAdResume(){for(const e of this.pauseAdResumeControls)e.style.display=""}setPauseAdCreativeEndedCallback(e){this.pauseAdCreativeEndedCallback=e}destroy(){this.removePauseAdNow()}removePauseAdNow(){this.pauseAdFadeTimer&&(clearTimeout(this.pauseAdFadeTimer),this.pauseAdFadeTimer=null),this.stopPauseAdVideo(),this.pauseAdContainer?.parentElement&&this.pauseAdContainer.parentElement.removeChild(this.pauseAdContainer),this.pauseAdContainer=null}stopPauseAdVideo(){this.pauseAdResumeControls=[];const e=this.pauseAdVideoEl;if(e){try{e.pause(),e.removeAttribute("src")}catch{}this.pauseAdVideoEl=null}}resumeFromPauseAd(){const e=n(this.pauseAdPinnedPosition,this.seekableStart(),this.seekableEnd());null!==e&&(this.deps.diagnose("DA-PAUSE-AD-DVR-EXPIRED",`Pause outlived the DVR window; resuming at the live edge (${e.toFixed(2)}s).`,{pinnedPosition:this.pauseAdPinnedPosition,liveEdge:e}),this.deps.contentPlayer.seek(e)),this.deps.contentPlayer.play()}seekableStart(){const e=this.deps.contentPlayer.videoElement?.seekable;return e&&0!==e.length?e.start(0):Number.NaN}seekableEnd(){const e=this.deps.contentPlayer.videoElement?.seekable;return e&&0!==e.length?e.end(e.length-1):Number.NaN}buildPauseResumeButton(){const e=document.createElement("button");return e.className="dolby-pause-ad-resume",e.setAttribute("aria-label","Resume playback"),e.style.cssText="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:72px;height:72px;border:none;border-radius:50%;cursor:pointer;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;",e.innerHTML='<svg width="32" height="32" viewBox="0 0 24 24" fill="#fff" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>',e.addEventListener("click",e=>{e.stopPropagation(),this.resumeFromPauseAd()}),e}buildPauseCloseButton(){const e=document.createElement("button");return e.className="dolby-pause-ad-close",e.setAttribute("aria-label","Close pause ad and resume"),e.style.cssText="position:absolute;top:8px;right:8px;width:32px;height:32px;border:none;border-radius:50%;cursor:pointer;background:rgba(0,0,0,0.6);color:#fff;font-size:18px;line-height:32px;text-align:center;padding:0;",e.textContent="✕",e.addEventListener("click",e=>{e.stopPropagation(),this.resumeFromPauseAd()}),e}async resolvePauseAsset(n){const o=n.asset,r=n.mediaType===e.VIDEO?"video":"image";if("static"===n.source){const e=this.deps.buildStaticAssetUrl(o,n.break);if(!e)return null;const t=o.interaction?.clickThrough;return{kind:r,url:e,...t?{clickThrough:t}:{},impressions:[]}}const d=this.deps.resolveUri(o.uri);if(!d)return null;try{const e=await s(d,{},t),n=await e.text();if("video"===r){const e=a(n);if(!e)return null;const t=e.clickThrough??o.interaction?.clickThrough;return{kind:r,url:e.mediaFileUrl,...t?{clickThrough:t}:{},impressions:e.impressions}}const l=i(n);if(!l)return null;const u=l.clickThrough??o.interaction?.clickThrough;return{kind:r,url:l.imageUrl,...u?{clickThrough:u}:{},impressions:l.impressions}}catch(e){return this.deps.log("Pause VAST fetch/parse failed:",e),null}}firePauseImpressions(e){for(const t of e)try{(new Image).src=t}catch{}}}
1
+ import{MediaType as e}from"../../types/BreakManifest";import{AUX_FETCH_TIMEOUT_MS as t,fetchWithTimeout as s}from"../fetchWithTimeout";import{resolveVastPauseCreative as a}from"../parseVastPause";import{PauseAdTracking as i}from"./PauseAdTracking";import{resolvePauseResumePoint as n}from"../resolvePauseResumePoint";export class PauseAdPresenter{constructor(e){this.deps=e,this.pauseAdContainer=null,this.pauseAdVideoEl=null,this.pauseAdFadeTimer=null,this.pauseAdPinnedPosition=0,this.pauseAdGeneration=0,this.pauseTracking=null,this.pauseAdResumeControls=[],this.pauseAdResumeRevealed=!1,this.pauseAdCreativeResolvedCallback=null,this.pauseAdCreativeEndedCallback=null}async showPauseImage(t){const s=this.deps.getStage();if(!s)return;const a=++this.pauseAdGeneration;this.removePauseAdNow(),this.pauseAdResumeRevealed=!1;const n=await this.resolvePauseAsset(t);if(a!==this.pauseAdGeneration)return;const r=n.creative;if(!r)return this.deps.diagnose("DA-PAUSE-AD-NO-ASSET",`Pause ad '${t.break.id}' asset could not be resolved; nothing shown.`,{breakId:t.break.id,source:t.source,mediaType:t.mediaType,reason:n.reason}),this.pauseAdCreativeResolvedCallback?.(t.break.id,null),void this.pauseAdCreativeEndedCallback?.(t.break.id);this.pauseAdCreativeResolvedCallback?.(t.break.id,"video"===r.kind?e.VIDEO:e.IMAGE);const o=new i(r,e=>this.firePauseBeacons(e));this.pauseTracking=o,this.pauseAdPinnedPosition=this.deps.contentPlayer.currentTime;const d=document.createElement("div");if(d.className="dolby-pause-ad",d.style.cssText="position:absolute;top:0;left:0;right:0;bottom:0;z-index:150;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.35);opacity:0;transition:opacity 300ms ease-in-out;"+(r.clickThrough?"cursor:pointer;":""),"video"===r.kind){const e=document.createElement("video");e.className="dolby-pause-ad-video",e.style.cssText="max-width:100%;max-height:100%;object-fit:contain;",e.muted=!0,e.loop=!1,e.autoplay=!0,e.playsInline=!0,e.setAttribute("playsinline",""),e.src=r.url,this.pauseAdVideoEl=e,d.appendChild(e)}else{const e=document.createElement("img");e.className="dolby-pause-ad-image",e.style.cssText="max-width:100%;max-height:100%;object-fit:contain;",e.addEventListener("error",()=>{this.pauseAdContainer===d&&(this.deps.diagnose("DA-PAUSE-AD-NO-ASSET","Pause ad image failed to load.",{breakId:t.break.id,source:t.source,reason:"image-load-failed"}),this.pauseAdCreativeResolvedCallback?.(t.break.id,null),this.pauseAdCreativeEndedCallback?.(t.break.id))},{once:!0}),e.src=r.url,d.appendChild(e)}if(r.clickThrough){const e=r.clickThrough;d.addEventListener("click",()=>{this.pauseTracking===o&&(o.click(),window.open(e,"_blank","noopener"))})}const l=this.buildPauseResumeButton(),u=this.buildPauseCloseButton();if(l.style.display=this.pauseAdResumeRevealed?"":"none",u.style.display=this.pauseAdResumeRevealed?"":"none",this.pauseAdResumeControls=[l,u],d.appendChild(l),d.appendChild(u),this.pauseAdVideoEl){const e=this.pauseAdVideoEl,s=t.break.id,a=()=>{this.pauseAdVideoEl===e&&this.pauseAdCreativeEndedCallback?.(s)};e.addEventListener("ended",()=>o.complete(e.duration)),e.addEventListener("ended",a,{once:!0}),e.addEventListener("playing",()=>o.progress(e.currentTime,e.duration,!0)),e.addEventListener("timeupdate",()=>o.progress(e.currentTime,e.duration));const i=()=>{this.pauseAdVideoEl===e&&(this.pauseAdCreativeResolvedCallback?.(s,null),a())};e.addEventListener("error",i,{once:!0}),e.play().catch(i)}s.appendChild(d),this.pauseAdContainer=d,requestAnimationFrame(()=>{this.pauseAdContainer===d&&(d.style.opacity="1")}),this.firePauseBeacons(r.impressions),this.deps.log("Pause ad shown:",t.break.id,t.source,t.mediaType)}hidePauseImage(){++this.pauseAdGeneration,this.pauseTracking?.dismiss(),this.pauseTracking=null;const e=this.pauseAdContainer;e&&(this.stopPauseAdVideo(),this.pauseAdContainer=null,e.style.opacity="0",this.pauseAdFadeTimer&&clearTimeout(this.pauseAdFadeTimer),this.pauseAdFadeTimer=setTimeout(()=>{e.parentElement&&e.parentElement.removeChild(e),this.pauseAdFadeTimer=null},300))}revealPauseAdResume(){this.pauseAdResumeRevealed=!0;for(const e of this.pauseAdResumeControls)e.style.display=""}setPauseAdCreativeResolvedCallback(e){this.pauseAdCreativeResolvedCallback=e}setPauseAdCreativeEndedCallback(e){this.pauseAdCreativeEndedCallback=e}destroy(){++this.pauseAdGeneration,this.removePauseAdNow()}removePauseAdNow(){this.pauseTracking?.dismiss(),this.pauseTracking=null,this.pauseAdFadeTimer&&(clearTimeout(this.pauseAdFadeTimer),this.pauseAdFadeTimer=null),this.stopPauseAdVideo(),this.pauseAdContainer?.parentElement&&this.pauseAdContainer.parentElement.removeChild(this.pauseAdContainer),this.pauseAdContainer=null}stopPauseAdVideo(){this.pauseAdResumeControls=[];const e=this.pauseAdVideoEl;if(e){try{e.pause(),e.removeAttribute("src")}catch{}this.pauseAdVideoEl=null}}resumeFromPauseAd(){const e=n(this.pauseAdPinnedPosition,this.seekableStart(),this.seekableEnd());null!==e&&(this.deps.diagnose("DA-PAUSE-AD-DVR-EXPIRED",`Pause outlived the DVR window; resuming at the live edge (${e.toFixed(2)}s).`,{pinnedPosition:this.pauseAdPinnedPosition,liveEdge:e}),this.deps.contentPlayer.seek(e)),this.deps.contentPlayer.play()}seekableStart(){const e=this.deps.contentPlayer.videoElement?.seekable;return e&&0!==e.length?e.start(0):Number.NaN}seekableEnd(){const e=this.deps.contentPlayer.videoElement?.seekable;return e&&0!==e.length?e.end(e.length-1):Number.NaN}buildPauseResumeButton(){const e=document.createElement("button");return e.className="dolby-pause-ad-resume",e.setAttribute("aria-label","Resume playback"),e.style.cssText="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:72px;height:72px;border:none;border-radius:50%;cursor:pointer;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;",e.innerHTML='<svg width="32" height="32" viewBox="0 0 24 24" fill="#fff" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>',e.addEventListener("click",e=>{e.stopPropagation(),this.resumeFromPauseAd()}),e}buildPauseCloseButton(){const e=document.createElement("button");return e.className="dolby-pause-ad-close",e.setAttribute("aria-label","Close pause ad and resume"),e.style.cssText="position:absolute;top:8px;right:8px;width:32px;height:32px;border:none;border-radius:50%;cursor:pointer;background:rgba(0,0,0,0.6);color:#fff;font-size:18px;line-height:32px;text-align:center;padding:0;",e.textContent="✕",e.addEventListener("click",e=>{e.stopPropagation(),this.resumeFromPauseAd()}),e}async resolvePauseAsset(t){const s=t.asset,i=t.mediaType===e.VIDEO?"video":"image";if("static"===t.source){const e=this.deps.buildStaticAssetUrl(s,t.break);if(!e)return{creative:null,reason:"no-creative"};const a=s.interaction?.clickThrough;return{reason:null,creative:{kind:i,url:e,...a?{clickThrough:a}:{},impressions:[],clickTracking:[],closeTracking:[],videoTracking:{start:[],firstQuartile:[],midpoint:[],thirdQuartile:[],complete:[]}}}}const n=this.deps.resolveUri(s.uri);if(!n)return{creative:null,reason:"no-creative"};try{const e=await this.fetchPauseVast(n),t=await a(e,e=>this.fetchPauseVast(e));return t.creative&&!t.creative.clickThrough&&s.interaction?.clickThrough&&(t.creative.clickThrough=s.interaction.clickThrough),t}catch(e){return this.deps.log("Pause VAST fetch/parse failed:",e),{creative:null,reason:"fetch-failed"}}}async fetchPauseVast(e){const a="undefined"==typeof AbortController?null:new AbortController;let i;try{return await Promise.race([s(e,a?{signal:a.signal}:{},t).then(e=>{if(!1===e.ok)throw new Error("Pause VAST HTTP failure");return e.text()}),new Promise((e,s)=>{i=setTimeout(()=>{a?.abort(),s(new Error("Pause VAST timeout"))},t)})])}finally{clearTimeout(i)}}firePauseBeacons(e){for(const t of e)try{(new Image).src=t}catch{}}}