@dolby-optiview/ads-sdk-core 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +68 -0
  2. package/dist/OptiViewAds.d.ts +1 -0
  3. package/dist/OptiViewAds.js +1 -1
  4. package/dist/diagnostics/recordEventTimeline.d.ts +3 -0
  5. package/dist/diagnostics/recordEventTimeline.js +1 -0
  6. package/dist/generated/diagnosticCodes.d.ts +142 -2
  7. package/dist/generated/diagnosticCodes.js +1 -1
  8. package/dist/generated/version.d.ts +1 -1
  9. package/dist/generated/version.js +1 -1
  10. package/dist/index.d.ts +1 -1
  11. package/dist/interfaces/PlayerAdapter.d.ts +33 -0
  12. package/dist/internal/index.d.ts +1 -1
  13. package/dist/internal/index.js +1 -1
  14. package/dist/services/AdPlayerController.d.ts +9 -1
  15. package/dist/services/AdPlayerController.js +1 -1
  16. package/dist/services/BreakScheduler.d.ts +5 -2
  17. package/dist/services/BreakScheduler.js +1 -1
  18. package/dist/services/CmcdAdReporter.d.ts +10 -0
  19. package/dist/services/CmcdAdReporter.js +1 -1
  20. package/dist/services/GamPodAdTracker.d.ts +12 -1
  21. package/dist/services/GamPodAdTracker.js +1 -1
  22. package/dist/services/LensReporter.d.ts +3 -1
  23. package/dist/services/LensReporter.js +1 -1
  24. package/dist/services/LiveStartPushbackController.d.ts +36 -0
  25. package/dist/services/LiveStartPushbackController.js +1 -0
  26. package/dist/services/MetadataCueTracker.d.ts +42 -1
  27. package/dist/services/MetadataCueTracker.js +1 -1
  28. package/dist/services/VastAdManager.js +1 -1
  29. package/dist/services/adPlayer/AdPlayerState.d.ts +11 -0
  30. package/dist/services/adPlayer/AdPlayerState.js +1 -1
  31. package/dist/services/adPlayer/AdPreloader.d.ts +9 -1
  32. package/dist/services/adPlayer/AdPreloader.js +1 -1
  33. package/dist/services/adPlayer/AdStage.d.ts +5 -0
  34. package/dist/services/adPlayer/AdStage.js +1 -1
  35. package/dist/services/adPlayer/AssetPlayback.d.ts +15 -1
  36. package/dist/services/adPlayer/AssetPlayback.js +1 -1
  37. package/dist/services/adPlayer/ContentRecovery.js +1 -1
  38. package/dist/services/adPlayer/ContentResumeCoordinator.d.ts +81 -2
  39. package/dist/services/adPlayer/ContentResumeCoordinator.js +1 -1
  40. package/dist/services/adPlayer/GamPodBridge.d.ts +7 -0
  41. package/dist/services/adPlayer/GamPodBridge.js +1 -1
  42. package/dist/services/adPlayer/PreparedPresentation.d.ts +32 -0
  43. package/dist/services/adPlayer/PreparedPresentation.js +1 -0
  44. package/dist/services/adPlayer/SharedElementBreakPlayer.d.ts +8 -0
  45. package/dist/services/adPlayer/SharedElementBreakPlayer.js +1 -1
  46. package/dist/services/adPlayer/classifyMediaError.d.ts +9 -0
  47. package/dist/services/adPlayer/classifyMediaError.js +1 -0
  48. package/dist/services/adPlayer/liveEdge.d.ts +29 -0
  49. package/dist/services/adPlayer/liveEdge.js +1 -1
  50. package/dist/services/adPlayer/streamClockMapping.d.ts +65 -0
  51. package/dist/services/adPlayer/streamClockMapping.js +1 -0
  52. package/dist/services/adPlayer/streamClockResume.d.ts +94 -0
  53. package/dist/services/adPlayer/streamClockResume.js +1 -0
  54. package/dist/services/anvatoCueRuntime.d.ts +19 -0
  55. package/dist/services/anvatoCueRuntime.js +1 -1
  56. package/dist/services/breakTiming.d.ts +7 -0
  57. package/dist/services/breakTiming.js +1 -1
  58. package/dist/services/classifyImaAdError.d.ts +6 -0
  59. package/dist/services/classifyImaAdError.js +1 -0
  60. package/dist/services/html5ImaErrorNames.d.ts +2 -0
  61. package/dist/services/html5ImaErrorNames.js +1 -0
  62. package/dist/services/lensBackend.d.ts +1 -1
  63. package/dist/services/resolveLiveStartPushback.d.ts +1 -0
  64. package/dist/services/resolveLiveStartPushback.js +1 -0
  65. package/dist/session/AdBreakStatusTracker.js +1 -1
  66. package/dist/session/BreakLifecycle.js +1 -1
  67. package/dist/session/SessionStarter.d.ts +1 -0
  68. package/dist/session/SessionStarter.js +1 -1
  69. package/dist/types/AdError.d.ts +12 -0
  70. package/dist/types/AdError.js +1 -0
  71. package/dist/types/OptiViewAdsConfig.d.ts +17 -0
  72. package/dist/types/VendorAdError.d.ts +6 -5
  73. package/dist/types/VendorAdError.js +1 -1
  74. package/dist/vendor/lens-sdk/THIRD-PARTY-NOTICES.txt +1 -1
  75. package/dist/vendor/lens-sdk/index.d.ts +1 -1
  76. package/dist/vendor/lens-sdk/index.js +1 -1
  77. package/dist/vendor/lens-sdk/lens-core-uGZfZpQy.js +1 -0
  78. package/dist/vendor/lens-sdk/lens.d.ts +2 -0
  79. package/dist/vendor/lens-sdk/teardown.d.ts +13 -0
  80. package/dist/vendor/lens-sdk/transport.d.ts +11 -2
  81. package/dist/vendor/lens-sdk/types.d.ts +56 -5
  82. package/dist/vendor/lens-sdk/validation.d.ts +4 -1
  83. package/package.json +1 -1
  84. package/dist/vendor/lens-sdk/lens-core-Q5lbQGMC.js +0 -1
@@ -1 +1 @@
1
- import{Vendor as t}from"../../types/BreakManifest";import{GamPodAdTracker as e}from"../GamPodAdTracker";import{GamStreamManager as s}from"../GamStreamManager";import{isGamVendorParameters as a}from"../GamPodUrlBuilder";import{gamPodIdentityMismatches as i}from"../resolveGamVendorConfiguration";export class GamPodBridge{constructor(t,e){this.state=t,this.deps=e,this.callbacks={}}setConfig(t){this.state.gamConfig=t}setEventCallbacks(t){this.callbacks=t}get eventCallbacks(){return this.callbacks}async startGamSession(t,e){this.state.currentGamIdentity=t,this.state.sessionAssetParameters=e,this.state.pushedAssetParameters=null,await this.initializeGam()}endGamSession(){this.state.gamStreamManager?.reset(),this.state.gamInitPromise=null,this.state.currentGamIdentity=void 0,this.state.sessionAssetParameters=void 0,this.state.manifestAssetParameters=void 0,this.state.sessionAssetParameterMacros={},this.state.updatedAssetParameters=void 0,this.state.pushedAssetParameters=null,this.state.earlyGamPodAdStart=null}async initializeGam(){if(!this.state.currentGamIdentity)return;if(this.deps.log("Initializing GAM stream manager..."),this.state.gamStreamManager)this.state.gamStreamManager.reset();else{const t=this.deps.isSharedElement()?this.deps.contentPlayer.videoElement:this.deps.getAdPlayer()?.videoElement;if(!t)return void console.warn("[OptiViewAds] GAM requires an adapter that exposes videoElement. GAM will be skipped.");const e=this.deps.isSharedElement()?void 0:this.deps.getAdContainer()||void 0;this.state.gamStreamManager=new s(t,e,this.deps.debug)}this.state.gamStreamManager.setCallbacks({onAdFirstQuartile:()=>this.emitGamQuartile(this.callbacks.onAdFirstQuartile),onAdMidpoint:()=>this.emitGamQuartile(this.callbacks.onAdMidpoint),onAdThirdQuartile:()=>this.emitGamQuartile(this.callbacks.onAdThirdQuartile),onAdStarted:t=>{this.deps.log("GAM ad started"),this.onGamPodAdStarted(t)},onAdComplete:()=>{this.deps.log("GAM ad complete"),"per-ad"===this.state.gamPodMode&&this.state.gamPodTracker&&this.emitGamPodSteps(this.state.gamPodTracker.onAdComplete())},onAdBreakEnded:()=>{this.deps.log("GAM ad break ended");const t=this.deps.getAdPlayerStreamRemainingSec();if(t>.25)return this.deps.log(`GAM pod has ${t.toFixed(3)}s of slate left; playing it out`),void("per-ad"===this.state.gamPodMode&&this.state.gamPodTracker&&this.emitGamPodSteps(this.state.gamPodTracker.finish()));this.deps.onAdEnded()},onError:t=>this.deps.log("GAM error:",t)});const t=this.state.currentGamIdentity,e=this.deps.resolveSessionAdTagParameters(),a={networkCode:t.networkCode,customAssetKey:t.customAssetKey,...e&&{adTagParameters:e},...this.state.gamConfig?.streamActivityMonitorId&&{streamActivityMonitorId:this.state.gamConfig.streamActivityMonitorId}};this.state.gamInitPromise=this.state.gamStreamManager.initialize(a);try{await this.state.gamInitPromise,this.deps.log("GAM stream manager initialized, streamId:",this.state.gamStreamManager.getStreamId())}catch(t){throw this.state.gamInitPromise=null,t}}isGamEnabled(){return!1!==this.state.gamConfig?.enabled}isGamReady(){return!0===this.state.gamStreamManager?.isInitialized()}isGamPodAsset(e){return"vendor"===e.type&&e.vendor===t.GAM&&this.isGamReady()}isExpandingPodAsset(t){return("pending"===this.state.gamPodMode||"per-ad"===this.state.gamPodMode)&&(!!t&&t.id===this.state.gamPodAsset?.id)}beginGamPodTracking(t){this.resetGamPodTracking(),this.state.gamPodAsset=t,this.state.gamPodTracker=new e(t),this.state.gamPodMode="pending";const s=this.state.earlyGamPodAdStart;this.state.earlyGamPodAdStart=null,s&&this.onGamPodAdStarted(s)}fallBackToPodLevelReporting(){if("pending"!==this.state.gamPodMode)return;this.state.gamPodMode="legacy",this.state.gamPodTracker=null;const t=this.state.withheldPodAdBegin;this.state.withheldPodAdBegin=null,t&&this.deps.emitStep(t)}resetGamPodTracking(){this.state.gamPodMode="none",this.state.gamPodTracker=null,this.state.gamPodAsset=null,this.state.withheldPodAdBegin=null}onGamPodAdProgress(t){"pending"===this.state.gamPodMode&&(t<2||(this.deps.log("No IMA pod ad reported after the pod started playing; reporting the pod as a single ad"),this.fallBackToPodLevelReporting()))}onGamPodAdStarted(t){t&&("none"!==this.state.gamPodMode?this.state.gamPodTracker&&("pending"===this.state.gamPodMode&&(this.state.withheldPodAdBegin=null,this.state.gamPodMode="per-ad"),"per-ad"===this.state.gamPodMode&&this.emitGamPodSteps(this.state.gamPodTracker.onAdStarted(t))):this.state.earlyGamPodAdStart=t)}emitGamPodSteps(t){const e=this.deps.getCurrentBreak();if(e)for(const s of t){const t={adId:s.adId,creativeId:s.creativeId,podPosition:s.adPosition};"adbegin"===s.type?this.deps.emitAdStart(e,s.asset,s.adIndex,s.totalAds,t):this.deps.emitAdEnd(e,s.asset,s.adIndex,s.totalAds,t)}}emitGamQuartile(t){const e=this.deps.getCurrentBreak();if(!t||!e)return;const s="per-ad"===this.state.gamPodMode?this.state.gamPodTracker?.currentAd:null;if(s)return void t(e,s.asset,s.adIndex,s.totalAds);const a=this.deps.getCurrentAsset();a&&t(e,a)}async buildGamUrl(t,e){if(!a(t.vendorParameters))return this.deps.log("Invalid GAM vendor parameters:",t.vendorParameters),null;if(this.state.gamInitPromise&&await this.state.gamInitPromise,!this.state.gamStreamManager?.isInitialized())throw this.deps.log("GAM stream manager not initialized"),new Error("GAM stream manager not initialized. The stream identity comes from the break manifest's vendorConfiguration — check for DA-GAM-CONFIG-MISSING.");const s=t.vendorParameters;if(this.state.currentGamIdentity){const e=i(this.state.currentGamIdentity,s);for(const s of e)this.deps.diagnose("DA-GAM-POD-IDENTITY-MISMATCH",`GAM asset ${t.id} skipped: vendorParameters.${s.field} "${s.assetValue}" does not match the stream session's "${s.sessionValue}".`,{assetId:t.id,field:s.field,assetValue:s.assetValue,sessionValue:s.sessionValue});if(e.length>0)return null}this.state.earlyGamPodAdStart=null,this.deps.pushAssetParameters(t.assetParameters,t.id);const r=this.state.gamStreamManager.buildPodUrl(t.uri,e,s);return this.deps.log("Built GAM pod URL:",r),r}processTimedMetadata(t){this.state.gamStreamManager?.processTimedMetadata(t)}destroy(){this.resetGamPodTracking(),this.state.earlyGamPodAdStart=null,this.state.gamStreamManager?.destroy(),this.state.gamStreamManager=null,this.state.gamInitPromise=null}}
1
+ import{Vendor as t}from"../../types/BreakManifest";import{GamPodAdTracker as e}from"../GamPodAdTracker";import{GamStreamManager as s}from"../GamStreamManager";import{isGamVendorParameters as a}from"../GamPodUrlBuilder";import{gamPodIdentityMismatches as i}from"../resolveGamVendorConfiguration";export class GamPodBridge{constructor(t,e){this.state=t,this.deps=e,this.identityMismatchAssetId=null,this.midBreakEndPosition=null,this.callbacks={}}setConfig(t){this.state.gamConfig=t}setEventCallbacks(t){this.callbacks=t}get eventCallbacks(){return this.callbacks}async startGamSession(t,e){this.state.currentGamIdentity=t,this.state.sessionAssetParameters=e,this.state.pushedAssetParameters=null,await this.initializeGam()}endGamSession(){this.state.gamStreamManager?.reset(),this.state.gamInitPromise=null,this.state.currentGamIdentity=void 0,this.state.sessionAssetParameters=void 0,this.state.manifestAssetParameters=void 0,this.state.sessionAssetParameterMacros={},this.state.updatedAssetParameters=void 0,this.state.pushedAssetParameters=null,this.state.earlyGamPodAdStart=null}async initializeGam(){if(!this.state.currentGamIdentity)return;if(this.deps.log("Initializing GAM stream manager..."),this.state.gamStreamManager)this.state.gamStreamManager.reset();else{const t=this.deps.isSharedElement()?this.deps.contentPlayer.videoElement:this.deps.getAdPlayer()?.videoElement;if(!t)return void console.warn("[OptiViewAds] GAM requires an adapter that exposes videoElement. GAM will be skipped.");const e=this.deps.isSharedElement()?void 0:this.deps.getAdContainer()||void 0;this.state.gamStreamManager=new s(t,e,this.deps.debug)}this.state.gamStreamManager.setCallbacks({onAdFirstQuartile:()=>this.emitGamQuartile(this.callbacks.onAdFirstQuartile),onAdMidpoint:()=>this.emitGamQuartile(this.callbacks.onAdMidpoint),onAdThirdQuartile:()=>this.emitGamQuartile(this.callbacks.onAdThirdQuartile),onAdStarted:t=>{this.deps.log("GAM ad started"),this.onGamPodAdStarted(t)},onAdComplete:()=>{this.deps.log("GAM ad complete"),"per-ad"===this.state.gamPodMode&&this.state.gamPodTracker&&this.emitGamPodSteps(this.state.gamPodTracker.onAdComplete())},onAdBreakEnded:()=>{this.deps.log("GAM ad break ended");const t=this.deps.getAdPlayerStreamRemainingSec();t>.25?this.onAdBreakEndedWithPodLeft(t):this.deps.onAdEnded()},onError:t=>this.deps.log("GAM error:",t)});const t=this.state.currentGamIdentity,e=this.deps.resolveSessionAdTagParameters(),a={networkCode:t.networkCode,customAssetKey:t.customAssetKey,...e&&{adTagParameters:e},...this.state.gamConfig?.streamActivityMonitorId&&{streamActivityMonitorId:this.state.gamConfig.streamActivityMonitorId}};this.state.gamInitPromise=this.state.gamStreamManager.initialize(a);try{await this.state.gamInitPromise,this.deps.log("GAM stream manager initialized, streamId:",this.state.gamStreamManager.getStreamId())}catch(t){throw this.state.gamInitPromise=null,t}}isGamEnabled(){return!1!==this.state.gamConfig?.enabled}isGamReady(){return!0===this.state.gamStreamManager?.isInitialized()}isGamPodAsset(e){return"vendor"===e.type&&e.vendor===t.GAM&&this.isGamReady()}isExpandingPodAsset(t){return("pending"===this.state.gamPodMode||"per-ad"===this.state.gamPodMode)&&(!!t&&t.id===this.state.gamPodAsset?.id)}beginGamPodTracking(t){this.resetGamPodTracking(),this.state.gamPodAsset=t,this.state.gamPodTracker=new e(t),this.state.gamPodMode="pending";const s=this.state.earlyGamPodAdStart;this.state.earlyGamPodAdStart=null,s&&this.onGamPodAdStarted(s)}fallBackToPodLevelReporting(){if("pending"!==this.state.gamPodMode)return;this.state.gamPodMode="legacy",this.state.gamPodTracker=null;const t=this.state.withheldPodAdBegin;this.state.withheldPodAdBegin=null,t&&this.deps.emitStep(t)}resetGamPodTracking(){this.state.gamPodMode="none",this.state.gamPodTracker=null,this.state.gamPodAsset=null,this.state.withheldPodAdBegin=null,this.midBreakEndPosition=null}onGamPodAdProgress(t){"pending"===this.state.gamPodMode&&(t<2||(this.deps.log("No IMA pod ad reported after the pod started playing; reporting the pod as a single ad"),this.fallBackToPodLevelReporting()))}onGamPodAdStarted(t){t&&("none"!==this.state.gamPodMode?this.state.gamPodTracker&&("pending"===this.state.gamPodMode&&(this.state.withheldPodAdBegin=null,this.state.gamPodMode="per-ad"),"per-ad"===this.state.gamPodMode&&(this.logRestartedPodPosition(t),this.emitGamPodSteps(this.state.gamPodTracker.onAdStarted(t)))):this.state.earlyGamPodAdStart=t)}logRestartedPodPosition(t){const e=this.midBreakEndPosition;null!==e&&(this.midBreakEndPosition=null,"number"==typeof t.adPosition&&t.adPosition<=e&&this.deps.log(`IMA restarted pod ad positions after a mid-pod ad break end (ad ${t.adPosition} after ad ${e})`))}onAdBreakEndedWithPodLeft(t){const e="per-ad"===this.state.gamPodMode?this.state.gamPodTracker:null;if(!e||e.reportedLastAd)return this.deps.log(`GAM pod has ${t.toFixed(3)}s of slate left; playing it out`),void(e&&this.emitGamPodSteps(e.finish()));const s=e.lastAdPosition,a=e.lastTotalAds,i=a>0?` of ${a}`:"";this.deps.log(`IMA ad break ended after ad ${s}${i}; ${t.toFixed(3)}s of pod left, waiting for the next ad`),this.deps.diagnose("DA-GAM-POD-MID-BREAK-END",`IMA reported the ad break ended after ad ${s}${i} with ${t.toFixed(3)}s of pod left; per-ad reporting continues until the pod ends.`,{assetId:this.state.gamPodAsset?.id,adPosition:s,totalAds:a,remainingSec:t}),this.midBreakEndPosition=s,this.emitGamPodSteps(e.onAdBreakEnded())}emitGamPodSteps(t){const e=this.deps.getCurrentBreak();if(e)for(const s of t){const t={adId:s.adId,creativeId:s.creativeId,podPosition:s.adPosition};"adbegin"===s.type?this.deps.emitAdStart(e,s.asset,s.adIndex,s.totalAds,t):this.deps.emitAdEnd(e,s.asset,s.adIndex,s.totalAds,t)}}emitGamQuartile(t){const e=this.deps.getCurrentBreak();if(!t||!e)return;const s="per-ad"===this.state.gamPodMode?this.state.gamPodTracker?.currentAd:null;if(s)return void t(e,s.asset,s.adIndex,s.totalAds);const a=this.deps.getCurrentAsset();a&&t(e,a)}async buildGamUrl(t,e){if(this.identityMismatchAssetId=null,!a(t.vendorParameters))return this.deps.log("Invalid GAM vendor parameters:",t.vendorParameters),null;if(this.state.gamInitPromise&&await this.state.gamInitPromise,!this.state.gamStreamManager?.isInitialized())throw this.deps.log("GAM stream manager not initialized"),new Error("GAM stream manager not initialized. The stream identity comes from the break manifest's vendorConfiguration — check for DA-GAM-CONFIG-MISSING.");const s=t.vendorParameters;if(this.state.currentGamIdentity){const e=i(this.state.currentGamIdentity,s);for(const s of e)this.deps.diagnose("DA-GAM-POD-IDENTITY-MISMATCH",`GAM asset ${t.id} skipped: vendorParameters.${s.field} "${s.assetValue}" does not match the stream session's "${s.sessionValue}".`,{assetId:t.id,field:s.field,assetValue:s.assetValue,sessionValue:s.sessionValue});if(e.length>0)return this.identityMismatchAssetId=t.id,null}this.state.earlyGamPodAdStart=null,this.deps.pushAssetParameters(t.assetParameters,t.id);const d=this.state.gamStreamManager.buildPodUrl(t.uri,e,s);return this.deps.log("Built GAM pod URL:",d),d}processTimedMetadata(t){this.state.gamStreamManager?.processTimedMetadata(t)}destroy(){this.resetGamPodTracking(),this.state.earlyGamPodAdStart=null,this.state.gamStreamManager?.destroy(),this.state.gamStreamManager=null,this.state.gamInitPromise=null}hasIdentityMismatch(t){return this.identityMismatchAssetId===t.id}}
@@ -0,0 +1,32 @@
1
+ declare class PreparedPresentation {
2
+ readonly owner: object;
3
+ readonly container: HTMLElement;
4
+ private readonly getVideo;
5
+ readonly stage: HTMLDivElement;
6
+ readonly playerContainer: HTMLDivElement;
7
+ private readonly originalPosition;
8
+ private readonly subscribers;
9
+ private token;
10
+ private disposed;
11
+ private geometry;
12
+ private video;
13
+ private observer;
14
+ private resizeObserver;
15
+ private poll;
16
+ constructor(owner: object, container: HTMLElement, getVideo: () => HTMLVideoElement | null);
17
+ assertAvailable(): void;
18
+ acquire(): {
19
+ stage: HTMLDivElement;
20
+ playerContainer: HTMLElement;
21
+ subscribe: (listener: () => void) => () => void;
22
+ isActive: () => boolean;
23
+ release: () => void;
24
+ };
25
+ dispose(): void;
26
+ private unbindVideo;
27
+ private readonly refresh;
28
+ }
29
+ export declare function preparePresentation(owner: object, container: HTMLElement, getVideo: () => HTMLVideoElement | null): PreparedPresentation;
30
+ export declare function getPreparedPresentation(container: HTMLElement): PreparedPresentation | undefined;
31
+ export declare function disposePreparedPresentation(owner: object): void;
32
+ export {};
@@ -0,0 +1 @@
1
+ const e="position:absolute;top:0;left:0;right:0;bottom:0;transition:top 0.3s ease-in-out,left 0.3s ease-in-out,right 0.3s ease-in-out,bottom 0.3s ease-in-out,opacity 0.3s ease-in-out;",t=new WeakMap,s=new WeakMap,i=new WeakSet;class PreparedPresentation{constructor(t,s,i){this.owner=t,this.container=s,this.getVideo=i,this.subscribers=new Set,this.token=null,this.disposed=!1,this.geometry="",this.video=null,this.observer=null,this.resizeObserver=null,this.poll=null,this.refresh=()=>{if(this.disposed)return;const e=this.getVideo();e!==this.video&&(this.unbindVideo(),this.video=e,e?.addEventListener("loadedmetadata",this.refresh),e?.addEventListener("resize",this.refresh));const{width:t,height:s}=this.container.getBoundingClientRect();if(!(t>0&&s>0))return;const i=e&&e.videoWidth>0&&e.videoHeight>0?e.videoWidth/e.videoHeight:16/9,r=Math.min(t,s*i),o=r/i,n=`${t}:${s}:${r}:${o}`;if(n!==this.geometry){this.geometry=n,this.stage.style.width=`${r}px`,this.stage.style.height=`${o}px`,this.stage.style.left=(t-r)/2+"px",this.stage.style.top=(s-o)/2+"px";for(const e of this.subscribers)e()}};for(const e of Array.from(s.querySelectorAll("video,audio")))if(e.currentSrc||e.getAttribute("src")||e.srcObject||e.readyState>0||!e.paused)throw new Error("Prepare the ads presentation before loading the content source");this.originalPosition=s.style.position;const r=s.ownerDocument.defaultView?.getComputedStyle(s).position;r&&"static"!==r||(s.style.position="relative"),this.stage=s.ownerDocument.createElement("div"),this.stage.className="dolby-stage",this.stage.style.cssText="position:absolute;overflow:hidden;",this.playerContainer=s.ownerDocument.createElement("div"),this.playerContainer.style.cssText=e;const o=Array.from(s.childNodes);s.appendChild(this.stage),this.stage.appendChild(this.playerContainer);for(const e of o)this.playerContainer.appendChild(e);this.refresh(),"undefined"!=typeof MutationObserver&&(this.observer=new MutationObserver(()=>{this.getVideo()!==this.video&&this.refresh()}),this.observer.observe(s,{childList:!0,subtree:!0})),"undefined"!=typeof ResizeObserver?(this.resizeObserver=new ResizeObserver(this.refresh),this.resizeObserver.observe(s)):this.poll=setInterval(this.refresh,250)}assertAvailable(){if(this.disposed)throw new Error("The content presentation has been disposed");if(this.token)throw new Error("The content player already has an ads presentation owner");if(this.stage.parentNode!==this.container||this.playerContainer.parentNode!==this.stage)throw new Error("The prepared content presentation has been moved")}acquire(){this.assertAvailable();const t={};this.token=t;const s=new Set;return{stage:this.stage,playerContainer:this.playerContainer,subscribe:e=>(this.subscribers.add(e),s.add(e),()=>{this.subscribers.delete(e),s.delete(e)}),isActive:()=>!this.disposed&&this.token===t,release:()=>{for(const e of s)this.subscribers.delete(e);s.clear(),this.disposed||this.token!==t||(this.token=null,this.playerContainer.style.cssText=e)}}}dispose(){this.disposed||(this.disposed=!0,this.observer?.disconnect(),this.resizeObserver?.disconnect(),null!==this.poll&&clearInterval(this.poll),this.poll=null,this.unbindVideo(),this.subscribers.clear(),this.stage.parentNode&&this.stage.parentNode.removeChild(this.stage),s.get(this.container)===this&&(s.delete(this.container),this.container.style.position=this.originalPosition),i.add(this.owner))}unbindVideo(){this.video?.removeEventListener("loadedmetadata",this.refresh),this.video?.removeEventListener("resize",this.refresh),this.video=null}}export function preparePresentation(e,r,o){if(i.has(e))throw new Error("Cannot prepare a disposed content player");const n=t.get(e);if(n){if(n.container!==r)throw new Error("The content player presentation container changed");return n}if(s.has(r))throw new Error("The container belongs to another content player");const a=new PreparedPresentation(e,r,o);return t.set(e,a),s.set(r,a),a}export function getPreparedPresentation(e){return s.get(e)}export function disposePreparedPresentation(e){t.get(e)?.dispose()}
@@ -60,6 +60,14 @@ export declare class SharedElementBreakPlayer {
60
60
  private armSharedRestoreDeadline;
61
61
  private ownsSharedRestoreAttempt;
62
62
  private reloadAndResume;
63
+ /** Best-effort: an ad adapter that cannot let go still gets the reload attempted on top of it. */
64
+ private releaseAdEngineGrip;
65
+ /**
66
+ * Engines that attach their media asynchronously (THEOplayer; with `preload: 'none'` only after
67
+ * `play()`) have not taken the element when `play()` resolves. Releasing the ad engine then runs
68
+ * the load algorithm on an element without a resource, which closes Safari's PiP window.
69
+ */
70
+ private waitForContentEngineToTakeElement;
63
71
  private finishSharedRestore;
64
72
  private endSharedRestoreReloading;
65
73
  private armSharedRestoreWatchdog;
@@ -1 +1 @@
1
- import{BreakFormat as e,isPostRollBreak as t}from"../../types/BreakManifest";import{armPlaybackCutoff as s,clearPlaybackCutoff as a}from"./PlaybackCutoff";export class SharedElementBreakPlayer{constructor(e,t){this.state=e,this.deps=t,this.contentElementAdPlayer=null,this.sharedAdSource=null,this.sharedAdEndedHandler=null,this.sharedAdTimeUpdateHandler=null,this.sharedAdTimedMetadataHandler=null,this.sharedAdWaitingHandler=null,this.sharedAdPlayingHandler=null,this.sharedAdFirstFrameTimer=null,this.sharedRestoreWatchdog=null,this.sharedRestore=null,this.sharedRestoreGeneration=0,this.sharedActivityGeneration=0}sharedElementAdPlayer(){const e=this.deps.contentPlayer.videoElement;if(!e)return null;if(this.contentElementAdPlayer?.videoElement===e)return this.contentElementAdPlayer;this.releaseContentElementAdPlayer();const t=this.deps.createAdAdapter(document.createElement("div"),e);return t.videoElement!==e?(this.deps.log("shared-element: createAdAdapter ignored the content element; falling back to the content player for ad playback"),t.destroy(),null):(this.contentElementAdPlayer=t,t)}async sharedAdStep(e,t,s){try{await s()}catch(s){const a=t.videoElement,n=s instanceof Error?s.message:String(s);throw new Error(`shared-element ${e} failed: ${n} (readyState=${a?.readyState??"-"} paused=${a?.paused??"-"} networkState=${a?.networkState??"-"})`,{cause:s})}}clearSharedAdFirstFrameWatchdog(){this.sharedAdFirstFrameTimer&&(clearTimeout(this.sharedAdFirstFrameTimer),this.sharedAdFirstFrameTimer=null)}armSharedAdFirstFrameWatchdog(e,t){this.clearSharedAdFirstFrameWatchdog(),this.sharedAdFirstFrameTimer=setTimeout(()=>{if(this.sharedAdFirstFrameTimer=null,!this.state.isPlaying||!this.state.currentBreak)return;const s=e.videoElement,a=this.deps.contentPlayer.videoElement,n=s?.currentSrc??"",r=`readyState=${s?.readyState??"-"} networkState=${s?.networkState??"-"} paused=${s?.paused??"-"} connected=${s?.isConnected??"-"} srcKind=${n.startsWith("blob:")?"blob":n?"url":"none"} sameElementAsContentPlayer=${String(s===a)}`;this.deps.log(`shared-element: no first frame within ${t}ms (${r})`),this.deps.diagnose("DA-AD-PLAYBACK-ERROR",`the ad never rendered a frame on the content element within ${t}ms`,{breakId:this.state.currentBreak.id,detail:r});const i=this.state.currentAsset;i&&this.state.callbacks?.onAdError(this.state.currentBreak,i,new Error(`shared-element ad produced no frames within ${t}ms (${r})`)),this.onSharedElementAdEnded()},t)}attachSharedAdListeners(e){this.sharedAdSource=e,this.sharedAdEndedHandler=()=>this.onSharedElementAdEnded(),e.on("ended",this.sharedAdEndedHandler),this.sharedAdTimeUpdateHandler=()=>{this.sharedAdFirstFrameTimer&&e.currentTime>0&&this.clearSharedAdFirstFrameWatchdog(),this.deps.reportAdProgress(e)},e.on("timeupdate",this.sharedAdTimeUpdateHandler),this.sharedAdTimedMetadataHandler=e=>{e&&this.deps.processTimedMetadata(e)},e.on("timedmetadata",this.sharedAdTimedMetadataHandler),this.sharedAdWaitingHandler=()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&this.state.callbacks?.onAdWaiting(this.state.currentBreak,this.state.currentAsset)},e.on("waiting",this.sharedAdWaitingHandler),this.sharedAdPlayingHandler=()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&!e.paused&&(this.clearSharedAdFirstFrameWatchdog(),this.state.startPlaybackCutoff?.(),this.state.callbacks?.onAdPlaying(this.state.currentBreak,this.state.currentAsset))},e.on("playing",this.sharedAdPlayingHandler)}detachSharedAdListeners(){const e=this.sharedAdSource??this.deps.contentPlayer;this.sharedAdSource=null,this.state.sharedAdClaimActive=!1,this.clearSharedAdFirstFrameWatchdog(),this.sharedAdTimedMetadataHandler&&(e.off("timedmetadata",this.sharedAdTimedMetadataHandler),this.sharedAdTimedMetadataHandler=null),this.sharedAdWaitingHandler&&(e.off("waiting",this.sharedAdWaitingHandler),this.sharedAdWaitingHandler=null),this.sharedAdPlayingHandler&&(e.off("playing",this.sharedAdPlayingHandler),this.sharedAdPlayingHandler=null),this.sharedAdTimeUpdateHandler&&(e.off("timeupdate",this.sharedAdTimeUpdateHandler),this.sharedAdTimeUpdateHandler=null),this.sharedAdEndedHandler&&(e.off("ended",this.sharedAdEndedHandler),this.sharedAdEndedHandler=null)}async releaseContentElementFromContentEngine(){if(this.deps.contentPlayer.releaseMediaElement)try{await this.deps.contentPlayer.releaseMediaElement()}catch(e){this.deps.log("shared-element: the content engine did not release the element:",e)}}releaseContentElementAdPlayer(){const e=this.contentElementAdPlayer;if(e){this.contentElementAdPlayer=null;try{e.destroy()}catch(e){this.deps.log("shared-element: releasing the ad player on the content element failed:",e)}}}async playAdThroughContentElement(){const e=this.state.currentAsset,t=this.state.currentBreak;if(!e||!t)return;const s=this.state.effectiveBreakDurationSec??t.duration,a=await this.deps.resolveAssetUri(e,s);if(!a)return void this.deps.log("shared-element: no URI for the running ad; leaving it on the ad element");const n=this.deps.getAdPlayer()?.currentTime??0;this.state.savedContentSrc=this.restorableContentSource(),this.state.savedContentTime=this.deps.contentPlayer.currentTime,this.state.savedContentLive=!Number.isFinite(this.deps.contentPlayer.duration),this.state.savedContentEdgeDistance=this.state.savedContentLive?this.deps.liveEdgeDistanceSec():null,this.state.sharedAdClaimActive=!0,await this.releaseContentElementFromContentEngine(),this.deps.rebindContentElementIfSwapped();const r=this.sharedElementAdPlayer()??this.deps.contentPlayer;try{this.deps.getAdPlayer()?.pause(),this.deps.clearContentGate(),await r.load(a),this.attachSharedAdListeners(r),n>0&&r.seek(n),await r.play(),this.deps.hide(),this.deps.log("shared-element: the ad now plays through the content element")}catch(e){this.deps.log("shared-element: could not move the ad onto the content element:",e),this.state.sharedAdClaimActive=!1,this.state.savedContentSrc=null,this.state.savedContentEdgeDistance=null}}async playSharedElementBreak(t,a){const n=this.deps.getVariant(t);if(!n)return this.deps.log("shared-element: no valid variant, skipping break:",t.id),void a.onBreakEnd(t);if(n.format===e.LSHAPE_CONTENT)return this.deps.log("shared-element: skipping lshape_content break (no fullscreen video asset):",t.id),void a.onBreakEnd(t);const r=this.deps.getAssets(t)[0];if(!r)return this.deps.log("shared-element: no asset to play, skipping break:",t.id),void a.onBreakEnd(t);n.format!==e.SINGLE&&this.deps.log(`shared-element: downgrading ${n.format} to a single fullscreen ad:`,t.id),this.state.currentBreak=t,this.state.currentAsset=r,this.state.callbacks=a,this.state.isPlaying=!0;const i=this.state.effectiveBreakDurationSec??t.duration,d=await this.deps.resolveAssetUri(r,i);if(!d)return this.deps.log("shared-element: no valid URI, skipping break:",t.id),this.state.isPlaying=!1,this.state.currentBreak=null,this.state.currentAsset=null,void a.onBreakEnd(t);this.state.savedContentSrc=this.restorableContentSource(),this.state.savedContentTime=this.deps.contentPlayer.currentTime,this.state.savedContentLive=!Number.isFinite(this.deps.contentPlayer.duration),this.state.savedContentEdgeDistance=this.state.savedContentLive?this.deps.liveEdgeDistanceSec():null,this.state.sharedAdClaimActive=!0,await this.releaseContentElementFromContentEngine(),this.deps.rebindContentElementIfSwapped();const h=this.sharedElementAdPlayer()??this.deps.contentPlayer;if(i&&i>0){const e=i+this.state.adBreakCutSafetyMarginSec;s(this.state,e,()=>{this.deps.log("shared-element: break-cut timer expired, ending break"),this.state.isPlaying&&this.state.currentBreak&&this.state.callbacks?.onBreakCut?.(this.state.currentBreak),this.onSharedElementAdEnded()},()=>{this.state.callbacks?.onAdError(t,r,new Error("Ad playback startup timed out")),this.onSharedElementAdEnded()})}try{if(this.deps.clearContentGate(),await this.sharedAdStep("load",h,()=>h.load(d)),!this.state.isPlaying||this.state.currentBreak!==t)return;if(this.attachSharedAdListeners(h),this.armSharedAdFirstFrameWatchdog(h,Math.max(Math.min(1e3*(i??0)-100,5e3),3e3)),await this.sharedAdStep("play",h,()=>h.play()),!this.state.isPlaying||this.state.currentBreak!==t)return;this.state.callbacks?.onAdStart(t,r,0,1)}catch(e){if(!this.state.isPlaying||this.state.currentBreak!==t)return;this.deps.log("shared-element: ad playback failed:",e),this.state.callbacks?.onAdError(t,r,e instanceof Error?e:new Error(String(e))),this.onSharedElementAdEnded()}}onSharedElementAdEnded(){if(!this.state.isPlaying||!this.state.currentBreak)return;const e=this.state.currentBreak,t=this.state.currentAsset,s=this.state.callbacks,n=this.sharedRestoreGeneration;this.detachSharedAdListeners(),a(this.state),this.state.isPlaying=!1,this.state.currentBreak=null,this.state.currentAsset=null,t&&s?.onAdEnd(e,t,0,1),n===this.sharedRestoreGeneration&&this.restoreSharedElementContent(e),s?.onBreakEnd(e)}restoreSharedElementContent(e){const s=this.state.savedContentSrc;if(!s){if(this.sharedRestore)return;return this.deps.log("shared-element: no saved content source to restore"),void this.releaseContentElementAdPlayer()}if(!this.beginSharedActivity())return;const a=this.state.savedContentTime,n=this.state.savedContentLive,r=this.state.savedContentEdgeDistance,i=this.state.callbacks;this.state.savedContentSrc=null,this.state.savedContentEdgeDistance=null;const d={src:s,time:a,live:n,edgeDistance:r,breakInfo:e,postRoll:!!e&&t(e),onContentReloading:i?.onContentReloading?.bind(i),adPlayer:this.contentElementAdPlayer,reloading:!1,cleanedUp:!1,cancelled:!1,finishingActivity:null,attempt:null};this.sharedRestore=d,e&&"number"==typeof e.start&&i?.onContentResumeSeek?.(n?this.deps.contentPlayer.currentTime:a,e.start+e.duration),this.sharedRestore===d&&(d.reloading=!0,d.onContentReloading?.(!0),this.sharedRestore===d&&this.runSharedRestore(d))}async runSharedRestore(e){for(let t=1;t<=2;t++){const s={phase:"load",deadline:0,timer:null,settle:null};e.attempt=s;const a=await new Promise(t=>{s.settle=e=>{s.settle&&(s.settle=null,s.timer&&clearTimeout(s.timer),s.timer=null,t(e))},this.armSharedRestoreDeadline(s,3e4),this.reloadAndResume(e,s).then(()=>s.settle?.("success"),()=>s.settle?.("rejection"))});if(this.sharedRestore!==e||e.attempt!==s)return;if("success"===a||"cancelled"===a)break;if(2===t?this.deps.diagnose("DA-SHARED-ELEMENT-RESTORE-FAILED",`Content did not resume after the shared-element break: ${s.phase} ${a}`,{breakId:e.breakInfo?.id,live:e.live,attempts:t,phase:s.phase,reason:a}):this.deps.log(`shared-element: content restore ${s.phase} ${a}, retrying once`),this.sharedRestore!==e||e.attempt!==s)return}this.finishSharedRestore(e,!0)}armSharedRestoreDeadline(e,t){e.timer&&clearTimeout(e.timer),e.deadline=performance.now()+t,e.timer=setTimeout(()=>e.settle?.("timeout"),t)}ownsSharedRestoreAttempt(e,t){return!(this.sharedRestore!==e||e.attempt!==t||!t.settle)&&(performance.now()<t.deadline||(t.settle("timeout"),!1))}async reloadAndResume(e,t){const{src:s,time:a,live:n,edgeDistance:r,postRoll:i}=e,d=()=>this.ownsSharedRestoreAttempt(e,t);if(d()&&(await this.deps.contentPlayer.load(s),d())){if(this.armSharedRestoreDeadline(t,5e3),n){if(!i&&null!==r&&r>0){t.phase="live-edge";const e=await this.deps.waitForLiveEdge(d);if(!d())return;if(null!==e){const s=Math.max(e-r,0);if(this.deps.log(`shared-element: live resume ${r.toFixed(2)}s behind the edge (${s.toFixed(2)}s)`),!d())return;t.phase="seek",this.deps.contentPlayer.seek(s)}}}else t.phase="seek",this.deps.contentPlayer.seek(a);d()&&!i&&(t.phase="play",await this.deps.contentPlayer.play(),d())}}finishSharedRestore(e,t){this.sharedRestore===e&&(null===e.finishingActivity?(e.finishingActivity=this.sharedActivityGeneration,e.cleanedUp||(this.state.contentPaused=!1,this.contentElementAdPlayer===e.adPlayer&&this.releaseContentElementAdPlayer(),this.sharedRestore===e&&this.deps.clearContentGate(),this.sharedRestore===e&&this.deps.rebindContentElementIfSwapped(),e.cleanedUp=!0),this.endSharedRestoreReloading(e),e.finishingActivity=null,this.sharedRestore===e&&(!t||e.cancelled||e.postRoll?this.sharedRestore=null:this.armSharedRestoreWatchdog(e))):e.finishingActivity!==this.sharedActivityGeneration&&(this.endSharedRestoreReloading(e),this.sharedRestore===e&&(this.sharedRestore=null)))}endSharedRestoreReloading(e){e.reloading&&(e.reloading=!1,e.onContentReloading?.(!1))}armSharedRestoreWatchdog(e){const t=this.deps.contentPlayer.currentTime;this.sharedRestoreWatchdog=setTimeout(()=>{if(this.sharedRestore!==e)return;this.sharedRestoreWatchdog=null;const s=this.deps.contentPlayer.currentTime-t;(this.deps.contentPlayer.paused||s<1)&&(this.deps.diagnose("DA-CONTENT-RESUME-STALLED",`Content did not resume within 5s of the shared-element restore (paused: ${this.deps.contentPlayer.paused}, advanced: ${s.toFixed(1)}s); nudging play()`,{breakId:e.breakInfo?.id??"?",paused:this.deps.contentPlayer.paused,advancedSec:s}),this.sharedRestore===e&&this.deps.contentPlayer.play().catch(()=>{this.sharedRestore===e&&this.deps.log("shared-element: watchdog play() nudge failed")}))},5e3)}beginSharedActivity(){return this.sharedActivityGeneration++,this.cancelSharedRestore()}cancelSharedRestore(){const e=this.sharedActivityGeneration;this.sharedRestoreGeneration++,this.sharedRestoreWatchdog&&(clearTimeout(this.sharedRestoreWatchdog),this.sharedRestoreWatchdog=null);const t=this.sharedRestore;return t&&(t.cancelled=!0,t.attempt?.settle?.("cancelled"),this.finishSharedRestore(t,!1)),e===this.sharedActivityGeneration}restorableContentSource(){const e=this.deps.contentPlayer.sourceUrl;if(e&&!e.startsWith("blob:"))return e;const t=this.deps.contentPlayer.videoElement,s=t?.currentSrc||t?.src||"";return s.length>0&&!s.startsWith("blob:")?s:null}}
1
+ import{BreakFormat as e,isPostRollBreak as t}from"../../types/BreakManifest";import{armPlaybackCutoff as s,clearPlaybackCutoff as a}from"./PlaybackCutoff";import{AdError as n}from"../../types/AdError";import{classifyMediaError as r,toClassifiedAdError as i}from"./classifyMediaError";function d(e){return e?{src:e.getAttribute("src"),srcObject:e.srcObject,sourceChild:null!==e.querySelector("source")}:null}export class SharedElementBreakPlayer{constructor(e,t){this.state=e,this.deps=t,this.contentElementAdPlayer=null,this.sharedAdSource=null,this.sharedAdEndedHandler=null,this.sharedAdTimeUpdateHandler=null,this.sharedAdTimedMetadataHandler=null,this.sharedAdWaitingHandler=null,this.sharedAdPlayingHandler=null,this.sharedAdFirstFrameTimer=null,this.sharedRestoreWatchdog=null,this.sharedRestore=null,this.sharedRestoreGeneration=0,this.sharedActivityGeneration=0}sharedElementAdPlayer(){const e=this.deps.contentPlayer.videoElement;if(!e)return null;if(this.contentElementAdPlayer?.videoElement===e)return this.contentElementAdPlayer;this.releaseContentElementAdPlayer();const t=this.deps.createAdAdapter(document.createElement("div"),e);return t.videoElement!==e?(this.deps.log("shared-element: createAdAdapter ignored the content element; falling back to the content player for ad playback"),t.destroy(),null):(this.contentElementAdPlayer=t,t)}async sharedAdStep(e,t,s){try{await s()}catch(s){const a=t.videoElement,i=s instanceof Error?s.message:String(s),d=new n(`shared-element ${e} failed: ${i} (readyState=${a?.readyState??"-"} paused=${a?.paused??"-"} networkState=${a?.networkState??"-"})`,r(s));throw d.cause=s,d}}clearSharedAdFirstFrameWatchdog(){this.sharedAdFirstFrameTimer&&(clearTimeout(this.sharedAdFirstFrameTimer),this.sharedAdFirstFrameTimer=null)}armSharedAdFirstFrameWatchdog(e,t){this.clearSharedAdFirstFrameWatchdog(),this.sharedAdFirstFrameTimer=setTimeout(()=>{if(this.sharedAdFirstFrameTimer=null,!this.state.isPlaying||!this.state.currentBreak)return;const s=e.videoElement,a=this.deps.contentPlayer.videoElement,r=s?.currentSrc??"",i=`readyState=${s?.readyState??"-"} networkState=${s?.networkState??"-"} paused=${s?.paused??"-"} connected=${s?.isConnected??"-"} srcKind=${r.startsWith("blob:")?"blob":r?"url":"none"} sameElementAsContentPlayer=${String(s===a)}`;this.deps.log(`shared-element: no first frame within ${t}ms (${i})`),this.deps.diagnose("DA-AD-PLAYBACK-ERROR",`the ad never rendered a frame on the content element within ${t}ms`,{breakId:this.state.currentBreak.id,detail:i});const d=this.state.currentAsset;d&&this.state.callbacks?.onAdError(this.state.currentBreak,d,new n(`shared-element ad produced no frames within ${t}ms (${i})`,"DA-AD-START-TIMEOUT")),this.onSharedElementAdEnded()},t)}attachSharedAdListeners(e){this.sharedAdSource=e,this.sharedAdEndedHandler=()=>this.onSharedElementAdEnded(),e.on("ended",this.sharedAdEndedHandler),this.sharedAdTimeUpdateHandler=()=>{this.sharedAdFirstFrameTimer&&e.currentTime>0&&this.clearSharedAdFirstFrameWatchdog(),this.deps.reportAdProgress(e)},e.on("timeupdate",this.sharedAdTimeUpdateHandler),this.sharedAdTimedMetadataHandler=e=>{e&&this.deps.processTimedMetadata(e)},e.on("timedmetadata",this.sharedAdTimedMetadataHandler),this.sharedAdWaitingHandler=()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&this.state.callbacks?.onAdWaiting(this.state.currentBreak,this.state.currentAsset)},e.on("waiting",this.sharedAdWaitingHandler),this.sharedAdPlayingHandler=()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&!e.paused&&(this.clearSharedAdFirstFrameWatchdog(),this.state.startPlaybackCutoff?.(),this.state.callbacks?.onAdPlaying(this.state.currentBreak,this.state.currentAsset))},e.on("playing",this.sharedAdPlayingHandler)}detachSharedAdListeners(){const e=this.sharedAdSource??this.deps.contentPlayer;this.sharedAdSource=null,this.state.sharedAdClaimActive=!1,this.clearSharedAdFirstFrameWatchdog(),this.sharedAdTimedMetadataHandler&&(e.off("timedmetadata",this.sharedAdTimedMetadataHandler),this.sharedAdTimedMetadataHandler=null),this.sharedAdWaitingHandler&&(e.off("waiting",this.sharedAdWaitingHandler),this.sharedAdWaitingHandler=null),this.sharedAdPlayingHandler&&(e.off("playing",this.sharedAdPlayingHandler),this.sharedAdPlayingHandler=null),this.sharedAdTimeUpdateHandler&&(e.off("timeupdate",this.sharedAdTimeUpdateHandler),this.sharedAdTimeUpdateHandler=null),this.sharedAdEndedHandler&&(e.off("ended",this.sharedAdEndedHandler),this.sharedAdEndedHandler=null)}async releaseContentElementFromContentEngine(){if(this.deps.contentPlayer.releaseMediaElement)try{await this.deps.contentPlayer.releaseMediaElement()}catch(e){this.deps.log("shared-element: the content engine did not release the element:",e)}}releaseContentElementAdPlayer(){const e=this.contentElementAdPlayer;if(e){this.contentElementAdPlayer=null;try{e.destroy()}catch(e){this.deps.log("shared-element: releasing the ad player on the content element failed:",e)}}}async playAdThroughContentElement(){const e=this.state.currentAsset,t=this.state.currentBreak;if(!e||!t)return;const s=this.state.effectiveBreakDurationSec??t.duration,a=await this.deps.resolveAssetUri(e,s);if(!a)return void this.deps.log("shared-element: no URI for the running ad; leaving it on the ad element");const n=this.deps.getAdPlayer()?.currentTime??0;this.state.savedContentSrc=this.restorableContentSource(),this.state.savedContentTime=this.deps.contentPlayer.currentTime,this.state.savedContentLive=!Number.isFinite(this.deps.contentPlayer.duration),this.state.savedContentEdgeDistance=this.state.savedContentLive?this.deps.liveEdgeDistanceSec():null,this.state.sharedAdClaimActive=!0,await this.releaseContentElementFromContentEngine(),this.deps.rebindContentElementIfSwapped();const r=this.sharedElementAdPlayer()??this.deps.contentPlayer;try{this.deps.getAdPlayer()?.pause(),this.deps.clearContentGate(),await r.load(a),this.attachSharedAdListeners(r),n>0&&r.seek(n),await r.play(),this.deps.hide(),this.deps.log("shared-element: the ad now plays through the content element")}catch(e){this.deps.log("shared-element: could not move the ad onto the content element:",e),this.state.sharedAdClaimActive=!1,this.state.savedContentSrc=null,this.state.savedContentEdgeDistance=null}}async playSharedElementBreak(t,a){const r=this.deps.getVariant(t);if(!r)return this.deps.log("shared-element: no valid variant, skipping break:",t.id),void a.onBreakEnd(t);if(r.format===e.LSHAPE_CONTENT)return this.deps.log("shared-element: skipping lshape_content break (no fullscreen video asset):",t.id),void a.onBreakEnd(t);const d=this.deps.getAssets(t)[0];if(!d)return this.deps.log("shared-element: no asset to play, skipping break:",t.id),void a.onBreakEnd(t);r.format!==e.SINGLE&&this.deps.log(`shared-element: downgrading ${r.format} to a single fullscreen ad:`,t.id),this.state.currentBreak=t,this.state.currentAsset=d,this.state.callbacks=a,this.state.isPlaying=!0;const h=this.state.effectiveBreakDurationSec??t.duration,l=await this.deps.resolveAssetUri(d,h);if(!l)return this.deps.log("shared-element: no valid URI, skipping break:",t.id),this.state.isPlaying=!1,this.state.currentBreak=null,this.state.currentAsset=null,void a.onBreakEnd(t);this.state.savedContentSrc=this.restorableContentSource(),this.state.savedContentTime=this.deps.contentPlayer.currentTime,this.state.savedContentLive=!Number.isFinite(this.deps.contentPlayer.duration),this.state.savedContentEdgeDistance=this.state.savedContentLive?this.deps.liveEdgeDistanceSec():null,this.state.sharedAdClaimActive=!0,await this.releaseContentElementFromContentEngine(),this.deps.rebindContentElementIfSwapped();const o=this.sharedElementAdPlayer()??this.deps.contentPlayer;if(h&&h>0){const e=h+this.state.adBreakCutSafetyMarginSec;s(this.state,e,()=>{this.deps.log("shared-element: break-cut timer expired, ending break"),this.state.isPlaying&&this.state.currentBreak&&this.state.callbacks?.onBreakCut?.(this.state.currentBreak),this.onSharedElementAdEnded()},()=>{this.state.callbacks?.onAdError(t,d,new n("Ad playback startup timed out","DA-AD-START-TIMEOUT")),this.onSharedElementAdEnded()})}try{if(this.deps.clearContentGate(),await this.sharedAdStep("load",o,()=>o.load(l)),!this.state.isPlaying||this.state.currentBreak!==t)return;if(this.attachSharedAdListeners(o),this.armSharedAdFirstFrameWatchdog(o,Math.max(Math.min(1e3*(h??0)-100,5e3),3e3)),await this.sharedAdStep("play",o,()=>o.play()),!this.state.isPlaying||this.state.currentBreak!==t)return;this.state.callbacks?.onAdStart(t,d,0,1)}catch(e){if(!this.state.isPlaying||this.state.currentBreak!==t)return;this.deps.log("shared-element: ad playback failed:",e),this.state.callbacks?.onAdError(t,d,i(e)),this.onSharedElementAdEnded()}}onSharedElementAdEnded(){if(!this.state.isPlaying||!this.state.currentBreak)return;const e=this.state.currentBreak,t=this.state.currentAsset,s=this.state.callbacks,n=this.sharedRestoreGeneration;this.detachSharedAdListeners(),a(this.state),this.state.isPlaying=!1,this.state.currentBreak=null,this.state.currentAsset=null,t&&s?.onAdEnd(e,t,0,1),n===this.sharedRestoreGeneration&&this.restoreSharedElementContent(e),s?.onBreakEnd(e)}restoreSharedElementContent(e){const s=this.state.savedContentSrc;if(!s){if(this.sharedRestore)return;return this.deps.log("shared-element: no saved content source to restore"),void this.releaseContentElementAdPlayer()}if(!this.beginSharedActivity())return;const a=this.state.savedContentTime,n=this.state.savedContentLive,r=this.state.savedContentEdgeDistance,i=this.state.callbacks;this.state.savedContentSrc=null,this.state.savedContentEdgeDistance=null;const d={src:s,time:a,live:n,edgeDistance:r,breakInfo:e,postRoll:!!e&&t(e),onContentReloading:i?.onContentReloading?.bind(i),adPlayer:this.contentElementAdPlayer,reloading:!1,cleanedUp:!1,cancelled:!1,finishingActivity:null,attempt:null};this.sharedRestore=d,e&&"number"==typeof e.start&&i?.onContentResumeSeek?.(n?this.deps.contentPlayer.currentTime:a,e.start+e.duration),this.sharedRestore===d&&(d.reloading=!0,d.onContentReloading?.(!0),this.sharedRestore===d&&this.runSharedRestore(d))}async runSharedRestore(e){for(let t=1;t<=2;t++){const s={phase:"load",deadline:0,timer:null,settle:null};e.attempt=s;const a=await new Promise(t=>{s.settle=e=>{s.settle&&(s.settle=null,s.timer&&clearTimeout(s.timer),s.timer=null,t(e))},this.armSharedRestoreDeadline(s,1e4),this.reloadAndResume(e,s).then(()=>s.settle?.("success"),()=>s.settle?.("rejection"))});if(this.sharedRestore!==e||e.attempt!==s)return;if("success"===a||"cancelled"===a)break;if(2===t?this.deps.diagnose("DA-SHARED-ELEMENT-RESTORE-FAILED",`Content did not resume after the shared-element break: ${s.phase} ${a}`,{breakId:e.breakInfo?.id,live:e.live,attempts:t,phase:s.phase,reason:a}):this.deps.log(`shared-element: content restore ${s.phase} ${a}, retrying once`),this.sharedRestore!==e||e.attempt!==s)return}this.finishSharedRestore(e,!0)}armSharedRestoreDeadline(e,t){e.timer&&clearTimeout(e.timer),e.deadline=performance.now()+t,e.timer=setTimeout(()=>e.settle?.("timeout"),t)}ownsSharedRestoreAttempt(e,t){return!(this.sharedRestore!==e||e.attempt!==t||!t.settle)&&(performance.now()<t.deadline||(t.settle("timeout"),!1))}async reloadAndResume(e,t){const{src:s,time:a,live:n,edgeDistance:r,postRoll:i}=e,h=()=>this.ownsSharedRestoreAttempt(e,t);if(!h())return;if(e.adPlayer?.releaseMediaElement&&(await this.releaseAdEngineGrip(e),!h()))return;const l=this.deps.contentPlayer.videoElement??null,o=d(l);if(await this.deps.contentPlayer.load(s),h()){if(this.armSharedRestoreDeadline(t,5e3),n){if(!i&&null!==r&&r>0){t.phase="live-edge";const e=await this.deps.waitForLiveEdge(h);if(!h())return;if(null!==e){const s=Math.max(e-r,0);if(this.deps.log(`shared-element: live resume ${r.toFixed(2)}s behind the edge (${s.toFixed(2)}s)`),!h())return;t.phase="seek",this.deps.contentPlayer.seek(s)}}}else t.phase="seek",this.deps.contentPlayer.seek(a);h()&&!i&&(t.phase="play",await this.deps.contentPlayer.play(),h()&&await this.waitForContentEngineToTakeElement(l,o,h))}}async releaseAdEngineGrip(e){const t=e.adPlayer;if(t?.releaseMediaElement)try{await t.releaseMediaElement()}catch(e){this.deps.log("shared-element: the ad engine did not release the element:",e)}}async waitForContentEngineToTakeElement(e,t,s){if(!(e&&t&&(a=t,a.src||a.srcObject||a.sourceChild)))return;var a;const n=()=>{if(this.deps.contentPlayer.videoElement!==e)return!0;const s=d(e);if(!s)return!1;const a=!!s.src&&s.src!==t.src,n=!!s.srcObject&&s.srcObject!==t.srcObject;return a||n},r=performance.now();for(;!n()&&s()&&performance.now()-r<1500;)await new Promise(e=>setTimeout(e,25));if(!s())return;const i=Math.round(performance.now()-r);this.deps.log(`shared-element: content engine ${n()?"took":"did not take"} the element after ${i}ms`)}finishSharedRestore(e,t){this.sharedRestore===e&&(null===e.finishingActivity?(e.finishingActivity=this.sharedActivityGeneration,e.cleanedUp||(this.state.contentPaused=!1,this.contentElementAdPlayer===e.adPlayer&&this.releaseContentElementAdPlayer(),this.sharedRestore===e&&this.deps.clearContentGate(),this.sharedRestore===e&&this.deps.rebindContentElementIfSwapped(),e.cleanedUp=!0),this.endSharedRestoreReloading(e),e.finishingActivity=null,this.sharedRestore===e&&(!t||e.cancelled||e.postRoll?this.sharedRestore=null:this.armSharedRestoreWatchdog(e))):e.finishingActivity!==this.sharedActivityGeneration&&(this.endSharedRestoreReloading(e),this.sharedRestore===e&&(this.sharedRestore=null)))}endSharedRestoreReloading(e){e.reloading&&(e.reloading=!1,e.onContentReloading?.(!1))}armSharedRestoreWatchdog(e){const t=this.deps.contentPlayer.currentTime;this.sharedRestoreWatchdog=setTimeout(()=>{if(this.sharedRestore!==e)return;this.sharedRestoreWatchdog=null;const s=this.deps.contentPlayer.currentTime-t;(this.deps.contentPlayer.paused||s<1)&&(this.deps.diagnose("DA-CONTENT-RESUME-STALLED",`Content did not resume within 5s of the shared-element restore (paused: ${this.deps.contentPlayer.paused}, advanced: ${s.toFixed(1)}s); nudging play()`,{breakId:e.breakInfo?.id??"?",paused:this.deps.contentPlayer.paused,advancedSec:s}),this.sharedRestore===e&&this.deps.contentPlayer.play().catch(()=>{this.sharedRestore===e&&this.deps.log("shared-element: watchdog play() nudge failed")}))},5e3)}beginSharedActivity(){return this.sharedActivityGeneration++,this.cancelSharedRestore()}cancelSharedRestore(){const e=this.sharedActivityGeneration;this.sharedRestoreGeneration++,this.sharedRestoreWatchdog&&(clearTimeout(this.sharedRestoreWatchdog),this.sharedRestoreWatchdog=null);const t=this.sharedRestore;return t&&(t.cancelled=!0,t.attempt?.settle?.("cancelled"),this.finishSharedRestore(t,!1)),e===this.sharedActivityGeneration}restorableContentSource(){const e=this.deps.contentPlayer.sourceUrl;if(e&&!e.startsWith("blob:"))return e;const t=this.deps.contentPlayer.videoElement,s=t?.currentSrc||t?.src||"";return s.length>0&&!s.startsWith("blob:")?s:null}}
@@ -0,0 +1,9 @@
1
+ import type { KnownDiagnosticCode } from '../../diagnostics/errorCodes';
2
+ import { AdError } from '../../types/AdError';
3
+ /**
4
+ * Canonical code for an ad player failure: an `HTMLMediaElement` `MediaError` (`code` 1–4) or a
5
+ * Shaka error (`category`). Anything else is the generic `DA-AD-PLAYBACK-ERROR`.
6
+ */
7
+ export declare function classifyMediaError(error: unknown): KnownDiagnosticCode;
8
+ /** `error` as a classified {@link AdError}, keeping its message and an already-assigned code. */
9
+ export declare function toClassifiedAdError(error: unknown): AdError;
@@ -0,0 +1 @@
1
+ import{AdError as r}from"../../types/AdError";export function classifyMediaError(e){if(e instanceof r)return e.code;const o=e;return"number"==typeof o?.category?1===o.category?"DA-AD-MEDIA-LOAD-FAILED":"DA-AD-MEDIA-UNSUPPORTED":2===o?.code?"DA-AD-MEDIA-LOAD-FAILED":3===o?.code||4===o?.code?"DA-AD-MEDIA-UNSUPPORTED":"DA-AD-PLAYBACK-ERROR"}export function toClassifiedAdError(e){if(e instanceof r)return e;const o=e instanceof Error?e.message:String(e);return new r(o,classifyMediaError(e))}
@@ -3,4 +3,33 @@ export declare const SEEK_END_SENTINEL_HORIZON_SEC: number;
3
3
  export declare function liveEdgeDistanceSec(contentPlayer: PlayerAdapter): number | null;
4
4
  export declare function isPlausibleSeekEnd(contentPlayer: PlayerAdapter, end: number): boolean;
5
5
  export declare function contentSeekableEnd(contentPlayer: PlayerAdapter): number | null;
6
+ /**
7
+ * The furthest position a resume seek may ask for on live; null on VOD and when nothing is
8
+ * readable, which means "do not clamp".
9
+ *
10
+ * The ceiling answers which frames EXIST, not which latency the engine would prefer. Those are
11
+ * different questions and `maxLiveSeekPosition` answers the second one: measured on hls.js, its
12
+ * `liveSyncPosition` sits a mean 8.0s BELOW the seekable end while seeks into that band are
13
+ * honoured, so taking the minimum of the two discarded 8s of published content and resumed
14
+ * inside the slate the ad replaced. On a replacement break that band is content the viewer is
15
+ * entitled to see, whereas a lower latency is only a preference.
16
+ *
17
+ * Still the fallback for an engine that reports the latency bound and nothing else.
18
+ * See `docs/web-live-resume-findings.md`.
19
+ */
6
20
  export declare function liveSafeSeekEnd(contentPlayer: PlayerAdapter): number | null;
21
+ /**
22
+ * How far the adapter's published live edge sits beyond the end of its seekable range: media
23
+ * that exists but a seek cannot reach. Reported with every resume so a short landing can be
24
+ * attributed without a device.
25
+ *
26
+ * Null, not zero, when the adapter cannot name an edge of its own. `PlayerAdapter.liveEdge`
27
+ * falls back to the seekable end by contract, so a zero would mean both "nothing is held back"
28
+ * and "the adapter does not know" — and on THEOplayer web, which has no playlist target duration
29
+ * to work from, it is always the second while the measured gap is a mean 8.7s.
30
+ */
31
+ export declare function unreachableEdgeSec(contentPlayer: PlayerAdapter): number | null;
32
+ /** Earliest position the engine still holds on a live stream; null on VOD or when unreadable. */
33
+ export declare function liveSafeSeekStart(contentPlayer: PlayerAdapter): number | null;
34
+ /** Length of the live window the engine holds (seekable end minus start); null on VOD or when unreadable. */
35
+ export declare function liveSeekableWindowSec(contentPlayer: PlayerAdapter): number | null;
@@ -1 +1 @@
1
- export const SEEK_END_SENTINEL_HORIZON_SEC=86400;export function liveEdgeDistanceSec(e){const n=e.videoElement,t=n?.seekable;if(!t||0===t.length)return null;const i=t.end(t.length-1);if(!Number.isFinite(i))return null;const l=i-e.currentTime;return l>=0?l:0}export function isPlausibleSeekEnd(e,n){return!!Number.isFinite(n)&&n<e.currentTime+86400}export function contentSeekableEnd(e){const n=e.seekableEnd;if("number"==typeof n&&isPlausibleSeekEnd(e,n))return n;const t=e.videoElement?.seekable;if(!t||0===t.length)return null;const i=t.end(t.length-1);return isPlausibleSeekEnd(e,i)?i:null}export function liveSafeSeekEnd(e){if(Number.isFinite(e.duration))return null;const n=[],t=e.maxLiveSeekPosition;"number"==typeof t&&Number.isFinite(t)&&n.push(t);const i=contentSeekableEnd(e);return null!==i&&n.push(i),n.length?Math.min(...n):null}
1
+ export const SEEK_END_SENTINEL_HORIZON_SEC=86400;export function liveEdgeDistanceSec(e){const n=e.videoElement,t=n?.seekable;if(!t||0===t.length)return null;const i=t.end(t.length-1);if(!Number.isFinite(i))return null;const r=i-e.currentTime;return r>=0?r:0}export function isPlausibleSeekEnd(e,n){return!!Number.isFinite(n)&&n<e.currentTime+86400}export function contentSeekableEnd(e){const n=e.seekableEnd;if("number"==typeof n&&isPlausibleSeekEnd(e,n))return n;const t=e.videoElement?.seekable;if(!t||0===t.length)return null;const i=t.end(t.length-1);return isPlausibleSeekEnd(e,i)?i:null}export function liveSafeSeekEnd(e){if(Number.isFinite(e.duration))return null;const n=contentSeekableEnd(e);if(null!==n)return n;const t=e.maxLiveSeekPosition;return"number"==typeof t&&Number.isFinite(t)?t:null}export function unreachableEdgeSec(e){const n=e.liveEdge,t=contentSeekableEnd(e);if("number"!=typeof n||!Number.isFinite(n)||null===t)return null;const i=n-t;return i>.001?Math.round(1e3*i)/1e3:null}export function liveSafeSeekStart(e){if(Number.isFinite(e.duration))return null;const n=e.videoElement?.seekable;if(!n||0===n.length)return null;const t=n.start(0);return Number.isFinite(t)?t:null}export function liveSeekableWindowSec(e){if(Number.isFinite(e.duration))return null;const n=e.videoElement?.seekable;if(!n||0===n.length)return null;const t=n.start(0),i=n.end(n.length-1);return!Number.isFinite(t)||!Number.isFinite(i)||i<=t?null:i-t}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The stream's own clock against the content player's currentTime, measured from Anvato
3
+ * `type=beacon` ID3 tags (one every 3s on NFL Channel). The TS twin of the iOS runtime's
4
+ * `StreamClockMapping`.
5
+ *
6
+ * A beacon carries the stream clock in `pts` — the same clock as the break's `cue` and `cue_end` —
7
+ * and is delivered at its position in currentTime, so `pts − startTime` is the mapping in force.
8
+ * Only beacons delivered after the last seek that may have restarted the buffer count:
9
+ * {@link StreamClockMapping.markSeek} starts over. See `docs/ios-live-resume-v2.md` §16.
10
+ */
11
+ export interface StreamClockAnchor {
12
+ /** The beacon's `pts`, in the feed's own unit. */
13
+ raw: number;
14
+ /** Where the player delivered it, in seconds of currentTime. */
15
+ mediaTime: number;
16
+ }
17
+ export type StreamClockUpdate = {
18
+ kind: 'none';
19
+ } | {
20
+ kind: 'mapped';
21
+ offsetSec: number;
22
+ anchors: number;
23
+ } | {
24
+ kind: 'remapped';
25
+ fromSec: number;
26
+ toSec: number;
27
+ };
28
+ /**
29
+ * Beacons are packed into ID3 PES packets, so their delivery alternates by ~0.134s around the `pts`
30
+ * cadence; the mean over the beacons since the last seek cancels that, and parse-time deliveries
31
+ * can arrive out of order, so two delivered in a row need not be neighbours.
32
+ */
33
+ export declare const STREAM_CLOCK_ANCHORS_KEPT = 10;
34
+ /** A mapping from fewer beacons than this still carries part of the packet jitter. */
35
+ export declare const STREAM_CLOCK_SETTLED_ANCHORS = 2;
36
+ /** An offset step beyond this between consecutive beacons is a remap, not packing jitter. */
37
+ export declare const STREAM_CLOCK_REMAP_THRESHOLD_SEC = 0.2;
38
+ /**
39
+ * The feed's `pts` unit from two consecutive beacons: their `pts` advance by ~1000× or ~1× the
40
+ * currentTime between them. Anything else (a seek in between, an out-of-order delivery) says nothing.
41
+ */
42
+ export declare function streamClockUnit(rawDelta: number, mediaDelta: number): number | null;
43
+ export declare class StreamClockMapping {
44
+ /**
45
+ * Raw `pts` units per second: 1000 when the feed's `pts` is milliseconds (NFL Channel), 1 when
46
+ * seconds. Learned once from two beacons' cadence and kept for the session.
47
+ */
48
+ unitsPerSecond: number | null;
49
+ /** Bumped by {@link markSeek}; every anchor belongs to the generation it was delivered in. */
50
+ generation: number;
51
+ private lastBeacon;
52
+ private anchors;
53
+ /** Beacons behind the current mapping (0 before the first one after the last seek). */
54
+ get anchorCount(): number;
55
+ /** A seek that may have restarted the buffer: the mapping must be measured again. */
56
+ markSeek(): void;
57
+ /** Records a beacon; see {@link StreamClockUpdate} for what it did to the mapping. */
58
+ ingestBeacon(raw: number, mediaTime: number): StreamClockUpdate;
59
+ /** Mean `stream − currentTime`, in seconds, over the beacons since the last seek; null before one. */
60
+ get offsetSec(): number | null;
61
+ /** A raw `pts` (a beacon's, a cue's, a break's manifest start) in seconds of the stream clock. */
62
+ streamSeconds(raw: number): number | null;
63
+ /** Where a stream-clock instant is in currentTime under the current mapping. */
64
+ mediaTimeForStreamSeconds(seconds: number): number | null;
65
+ }
@@ -0,0 +1 @@
1
+ export const STREAM_CLOCK_ANCHORS_KEPT=10;export const STREAM_CLOCK_SETTLED_ANCHORS=2;export const STREAM_CLOCK_REMAP_THRESHOLD_SEC=.2;export function streamClockUnit(n,t){if(!(n>0&&t>.5))return null;const e=n/t;return e>800&&e<1250?1e3:e>.8&&e<1.25?1:null}export class StreamClockMapping{constructor(){this.unitsPerSecond=null,this.generation=0,this.lastBeacon=null,this.anchors=[]}get anchorCount(){return this.anchors.length}markSeek(){this.generation+=1,this.anchors=[]}ingestBeacon(n,t){if(!Number.isFinite(n)||!Number.isFinite(t))return{kind:"none"};null===this.unitsPerSecond&&this.lastBeacon&&(this.unitsPerSecond=streamClockUnit(n-this.lastBeacon.raw,t-this.lastBeacon.mediaTime)),this.lastBeacon={raw:n,mediaTime:t};const e=this.unitsPerSecond;if(null===e||this.anchors.some(t=>t.raw===n))return{kind:"none"};const s={raw:n,mediaTime:t},i=n/e-t,r=this.offsetSec;return null!==r&&Math.abs(i-r)>.2?(this.anchors=[s],{kind:"remapped",fromSec:r,toSec:i}):(this.anchors.push(s),this.anchors.length>10&&this.anchors.splice(0,this.anchors.length-10),{kind:"mapped",offsetSec:this.offsetSec??i,anchors:this.anchors.length})}get offsetSec(){const n=this.unitsPerSecond;return null===n||0===this.anchors.length?null:this.anchors.reduce((t,e)=>t+e.raw/n-e.mediaTime,0)/this.anchors.length}streamSeconds(n){return Number.isFinite(n)&&null!==this.unitsPerSecond?n/this.unitsPerSecond:null}mediaTimeForStreamSeconds(n){const t=this.offsetSec;return Number.isFinite(n)&&null!==t?n-t:null}}
@@ -0,0 +1,94 @@
1
+ import type { PlayerAdapter, TimedMetadataCue } from '../../interfaces/PlayerAdapter';
2
+ import type { Break } from '../../types/BreakManifest';
3
+ import { StreamClockMapping } from './streamClockMapping';
4
+ /** The stream's own end of a break, matched to it on the stream clock. */
5
+ export interface BreakEndOnStreamClock {
6
+ /** Where the player delivered the `cue_end`, in seconds of currentTime. */
7
+ mediaTime: number;
8
+ /** `cue_end` pts minus the break's start pts, in seconds. */
9
+ lengthSec: number;
10
+ }
11
+ /** A resume point on the stream clock, and what fixed it. */
12
+ export interface StreamClockLanding {
13
+ seconds: number;
14
+ source: 'cue_end' | 'cue+duration';
15
+ }
16
+ export interface StreamClockResumeDeps {
17
+ contentPlayer: PlayerAdapter;
18
+ diagnose: (code: string, message: string, context?: Record<string, unknown>) => void;
19
+ /** The break being resumed, while one is. */
20
+ activeBreak: () => Break | null;
21
+ /** Whether the break's anchor is its in-stream cue (the stream clock needs the cue's pts). */
22
+ anchorIsCue: () => boolean;
23
+ /** The break's matched `cue_end`, or null. */
24
+ breakEnd: (breakInfo: Break) => BreakEndOnStreamClock | null;
25
+ /** The policy's resume point on the currentTime timeline, ignoring the stream. */
26
+ computedTarget: (breakInfo: Break) => number | null;
27
+ /** Replacement resumes at the break end, the only policy the stream's end of the break can serve. */
28
+ resumesAtBreakEnd: (breakInfo: Break) => boolean;
29
+ /** Wall clock in milliseconds. */
30
+ now: () => number;
31
+ }
32
+ /**
33
+ * Live resume on the stream's own clock (`ptsSource: 'anvatoCue'`), web: the resume point is a
34
+ * stream-clock instant, converted to currentTime with the mapping that beacons parsed since the
35
+ * run-up's last seek measure, and re-mapped on every beacon until the hand-back. The TS twin of
36
+ * the iOS runtime's `OverlayAdRenderer+StreamClock`; see `docs/ios-live-resume-v2.md` §16.
37
+ */
38
+ export declare class StreamClockResume {
39
+ private readonly deps;
40
+ readonly clock: StreamClockMapping;
41
+ /** The run-up made its opening move: from here the stream clock may map the resume point. */
42
+ armed: boolean;
43
+ /** The hand-back resumed on the stream-clock target. */
44
+ applied: boolean;
45
+ private reportedTarget;
46
+ private reportedGeneration;
47
+ private offsetAtBreakStart;
48
+ private remaps;
49
+ private landingCheck;
50
+ private landingTimer;
51
+ constructor(deps: StreamClockResumeDeps);
52
+ /** The stream carries beacons, so its clock places the resume point once the run-up is mapped. */
53
+ get decides(): boolean;
54
+ /** Content paused for a new break: per-break state starts over, the current mapping is the reference. */
55
+ beginBreak(): void;
56
+ /** The run-up's opening move (catch-up start or pre-positioning). */
57
+ arm(): void;
58
+ /** Before an SDK seek: one that may restart the buffer means the mapping is measured again. */
59
+ noteSeek(position: number): void;
60
+ /** The content source was rebuilt (pipeline recreate): its timeline has to be mapped again. */
61
+ noteReload(): void;
62
+ /**
63
+ * Records an Anvato `type=beacon` cue; other cues are ignored. Presentation re-deliveries are
64
+ * skipped: they repeat a cue parsed earlier, possibly before the last seek.
65
+ */
66
+ ingestCue(cue: TimedMetadataCue): void;
67
+ /**
68
+ * The break's resume point on the stream clock: its matched `cue_end`, else the cue plus the
69
+ * break duration. Null for a break that was not cue-matched or before the feed's unit is known.
70
+ */
71
+ landing(breakInfo: Break): StreamClockLanding | null;
72
+ /** The resume point in currentTime under the mapping measured since the run-up's last seek, or null. */
73
+ target(breakInfo: Break): number | null;
74
+ /** The hand-back's stream-clock target, recording whether it applies; null for an early return. */
75
+ handBackTarget(breakInfo: Break, earlyReturn: boolean): number | null;
76
+ /** What DA-RESUME-SEEK's target is based on, when the stream clock decided it. */
77
+ seekBasis(breakInfo: Break): string | null;
78
+ /** The stream-clock fields of DA-RESUME-SEEK; empty unless the hand-back resumes on the stream clock. */
79
+ seekContext(breakInfo: Break, target: number, computed: number): Record<string, unknown>;
80
+ /**
81
+ * DA-RESUME-PTS-CORRECTED, once per break at the hand-back, whenever the stream carries beacons:
82
+ * how far the stream clock moved the resume point from the currentTime target, or why it did not.
83
+ */
84
+ reportCorrection(breakInfo: Break, target: number, computed: number, earlyReturn: boolean): void;
85
+ /** Arms the landing measurement when the hand-back resumed on the stream clock. */
86
+ armLanding(breakInfo: Break, resumedAt: number): void;
87
+ /** The landing probe measured where the hand-back seek really landed. */
88
+ updateLanding(breakId: string, landed: number): void;
89
+ cancelLanding(): void;
90
+ private reportTarget;
91
+ /** DA-RESUME-PTS-LANDED: where content resumed, on the stream clock, against the resume point. */
92
+ private finishLanding;
93
+ private bufferedState;
94
+ }
@@ -0,0 +1 @@
1
+ import{anvatoPayloadFromCue as e,parseAnvatoPayload as t}from"../MetadataCueTracker";import{STREAM_CLOCK_SETTLED_ANCHORS as n,StreamClockMapping as s}from"./streamClockMapping";const r=e=>Math.round(1e3*e)/1e3;export class StreamClockResume{constructor(e){this.deps=e,this.clock=new s,this.armed=!1,this.applied=!1,this.reportedTarget=null,this.reportedGeneration=null,this.offsetAtBreakStart=null,this.remaps=0,this.landingCheck=null,this.landingTimer=null}get decides(){return null!==this.clock.unitsPerSecond}beginBreak(){this.cancelLanding(),this.armed=!1,this.applied=!1,this.reportedTarget=null,this.reportedGeneration=null,this.remaps=0,this.offsetAtBreakStart=this.clock.offsetSec}arm(){this.armed=!0}noteSeek(e){!0!==this.bufferedState(e)&&this.clock.markSeek()}noteReload(){this.clock.markSeek()}ingestCue(n){if("presented"===n.phase||!Number.isFinite(n.startTime))return;const s=e(n);if(!s)return;const i=t(s);if("beacon"!==i.type)return;const a=Number(i.pts);if(!Number.isFinite(a)||""===i.pts)return;const c=this.clock.ingestBeacon(a,n.startTime),o=this.deps.activeBreak();"remapped"===c.kind&&(o&&(this.remaps+=1),(this.armed||this.landingCheck)&&this.deps.diagnose("DA-RESUME-PTS-REMAP",`currentTime moved ${r(c.toSec-c.fromSec)}s against the stream clock; the resume point is mapped again`,{breakId:o?.id??this.landingCheck?.breakId??"",fromOffsetSec:r(c.fromSec),toOffsetSec:r(c.toSec),deltaSec:r(c.toSec-c.fromSec),currentTime:r(this.deps.contentPlayer.currentTime),stage:this.landingCheck?"after-hand-back":"run-up"})),o&&this.armed&&this.reportTarget(o),this.finishLanding(!1)}landing(e){const t=e.start;if(!this.deps.anchorIsCue()||"number"!=typeof t)return null;const n=this.clock.streamSeconds(t);if(null===n)return null;const s=this.deps.breakEnd(e);return s&&Number.isFinite(s.lengthSec)&&s.lengthSec>0?{seconds:n+s.lengthSec,source:"cue_end"}:{seconds:n+(e.duration??0),source:"cue+duration"}}target(e){if(!this.armed||!this.deps.resumesAtBreakEnd(e))return null;const t=this.landing(e);return t?this.clock.mediaTimeForStreamSeconds(t.seconds):null}handBackTarget(e,t){const n=t?null:this.target(e);return this.applied=null!==n,n}seekBasis(e){const t=this.applied?this.landing(e):null;return t?`on the stream clock, ${t.source}`:null}seekContext(e,t,n){const s=this.applied?this.landing(e):null,i=this.clock.offsetSec;return s&&null!==i?{streamClockTarget:r(t),computedTarget:n,landingStreamSec:r(s.seconds),landingSource:s.source,streamClockOffsetSec:r(i),streamClockBeacons:this.clock.anchorCount}:{}}reportCorrection(e,t,n,s){if(s||!this.armed||!this.decides)return;const i={breakId:e.id,applied:this.applied,computedTarget:n,remaps:this.remaps},a=this.deps.breakEnd(e);a&&(i.cueEndTarget=a.mediaTime);const c=this.applied?this.landing(e):null,o=this.clock.offsetSec;if(!c||null===o)return i.reason="no beacon since the run-up's last seek",void this.deps.diagnose("DA-RESUME-PTS-CORRECTED",`Break ${e.id}: no beacon since the run-up's last seek; the stream clock did not place the resume`,i);const d=r(t-n),l=null===this.offsetAtBreakStart?null:r(this.offsetAtBreakStart-o);Object.assign(i,{streamClockTarget:r(t),correctionSec:d,currentTimeShiftSec:l,landingSource:c.source,landingStreamSec:r(c.seconds),offsetSec:r(o),beacons:this.clock.anchorCount}),this.deps.diagnose("DA-RESUME-PTS-CORRECTED",`Break ${e.id}: the stream clock placed the resume at ${r(t)}s (${c.source}), ${d}s from the currentTime target ${n}s`+(null===l?"":`; currentTime moved ${l}s against the stream during the break`),i)}armLanding(e,t){const n=this.applied?this.landing(e):null;n&&(this.cancelLanding(),this.landingCheck={breakId:e.id,landing:n,resumedAt:t,handBackAtMs:this.deps.now()},this.landingTimer=setTimeout(()=>this.finishLanding(!0),15e3))}updateLanding(e,t){this.landingCheck?.breakId===e&&Number.isFinite(t)&&(this.landingCheck.resumedAt=t)}cancelLanding(){this.landingTimer&&clearTimeout(this.landingTimer),this.landingTimer=null,this.landingCheck=null}reportTarget(e){const t=this.landing(e),n=this.target(e),s=this.clock.offsetSec;if(!t||null===n||null===s)return;const i=this.clock.generation;if(i===this.reportedGeneration&&null!==this.reportedTarget&&Math.abs(n-this.reportedTarget)<.1)return;this.reportedTarget=n,this.reportedGeneration=i;const a=this.deps.computedTarget(e),c={breakId:e.id,target:r(n),landingStreamSec:r(t.seconds),landingSource:t.source,offsetSec:r(s),beacons:this.clock.anchorCount,generation:i,currentTime:r(this.deps.contentPlayer.currentTime)};null!==a&&(c.computedTarget=a,c.deltaSec=r(n-a)),this.deps.diagnose("DA-RESUME-PTS-MAP",`Break ${e.id}: resume point (${t.source}) maps to ${r(n)}s`+(null===a?"":` (${r(n-a)}s from the computed ${a}s)`),c)}finishLanding(e){const t=this.landingCheck;if(!t)return;const s=(this.deps.now()-t.handBackAtMs)/1e3,i={breakId:t.breakId,landingStreamSec:r(t.landing.seconds),landingSource:t.landing.source,resumedAt:r(t.resumedAt),sinceHandBackSec:r(s)},a=this.clock.offsetSec;if(null===a){if(!e)return;return this.cancelLanding(),i.reason="no beacon after the hand-back",void this.deps.diagnose("DA-RESUME-PTS-LANDED",`Resume landing for break ${t.breakId} could not be measured on the stream clock (no beacon)`,i)}if(!e&&this.clock.anchorCount<n&&s<6)return;this.cancelLanding();const c=t.resumedAt+a,o=r(c-t.landing.seconds);Object.assign(i,{landedStreamSec:r(c),errorSec:o,offsetSec:r(a),beacons:this.clock.anchorCount}),this.deps.diagnose("DA-RESUME-PTS-LANDED",`Resume for break ${t.breakId} landed ${Math.abs(o)}s ${o<0?"before":"after"} the stream's resume point (${t.landing.source})`,i)}bufferedState(e){const t=this.deps.contentPlayer.videoElement?.buffered;if(!t||!Number.isFinite(e))return null;for(let n=0;n<t.length;n++)if(e>=t.start(n)&&e<t.end(n))return!0;return!1}}
@@ -1,6 +1,25 @@
1
1
  import type { TimedMetadataCue } from '../interfaces/PlayerAdapter';
2
2
  import { MetadataCueTracker } from './MetadataCueTracker';
3
3
  import type { BreakScheduler } from './BreakScheduler';
4
+ import type { Break } from '../types/BreakManifest';
5
+ import type { BreakEndOnStreamClock } from './adPlayer/streamClockResume';
6
+ import type { AdPlayerController } from './AdPlayerController';
7
+ /**
8
+ * The media time of the in-stream cue that matched `breakInfo`, for anchoring the content resume;
9
+ * null when there is no cue tracker (every timebase except `ptsSource: 'anvatoCue'`), when the
10
+ * break carries no numeric PTS start, or when no cue has been observed for it.
11
+ */
12
+ export declare function cueAnchorForBreak(metadataCueTracker: MetadataCueTracker | null, breakInfo: Break): number | null;
13
+ /**
14
+ * The break's `cue_end` when one was matched to it on the stream clock and its length is accepted
15
+ * (within tolerance of the duration); null otherwise.
16
+ */
17
+ export declare function cueEndForBreak(metadataCueTracker: MetadataCueTracker | null, breakInfo: Break): BreakEndOnStreamClock | null;
18
+ /**
19
+ * Anchor the content resume on the break's in-stream cue and end it on the stream's `cue_end`,
20
+ * both read from the session's tracker (null when `ptsSource` is not `'anvatoCue'`).
21
+ */
22
+ export declare function wireCueResume(controller: AdPlayerController, tracker: () => MetadataCueTracker | null): void;
4
23
  export declare function armBreakFromPresentedCue(cue: TimedMetadataCue, metadataCueTracker: MetadataCueTracker | null, breakScheduler: BreakScheduler | undefined): void;
5
24
  export declare function notedManifestBreakIds(manifest: {
6
25
  breaks: Array<{
@@ -1 +1 @@
1
- import{isAnvatoCue as e,parseAnvatoPayload as t}from"./MetadataCueTracker";export function armBreakFromPresentedCue(r,s,a){if(!s||!a||!e(r))return;const i=t(r.messageData??"");if("cue"!==i.type)return;const n=Number(i.pts);Number.isFinite(n)&&a.notifyCuePresented(n)}export function notedManifestBreakIds(e,t){const r=[];for(const s of e.breaks)t.has(s.id)||(t.add(s.id),r.push(s.id));return r}export function diagnoseAnvatoCueObserved(r,s,a,i,n,o){if(!a||!e(r))return;const d=r.messageData??"",u=t(d);if("cue"!==u.type)return;const c=`${r.startTime}|${d}`;if(i.has(c))return;i.add(c);const m=Number(u.pts);o("DA-ANVATO-CUE-OBSERVED",`Anvato cue observed: pts=${u.pts} at media time ${r.startTime}s`,{pts:Number.isFinite(m)?m:u.pts,mediaTime:r.startTime,playheadSec:n,recorded:s,payload:d.slice(0,300)})}
1
+ import{isAnvatoCue as e,parseAnvatoPayload as t}from"./MetadataCueTracker";export function cueAnchorForBreak(e,t){const r=t.start;return e&&"number"==typeof r&&Number.isFinite(r)?e.mediaTimeForPts(r):null}export function cueEndForBreak(e,t){const r=t.start;if(!e||"number"!=typeof r||!Number.isFinite(r))return null;const n=e.breakEnd(r,t.duration);return n?.accepted?{mediaTime:n.mediaTime,lengthSec:n.lengthSec}:null}export function wireCueResume(e,t){e.setResumeCueAnchorResolver(e=>cueAnchorForBreak(t(),e)),e.setResumeCueEndResolver(e=>cueEndForBreak(t(),e))}export function armBreakFromPresentedCue(r,n,s){if(!n||!s||!e(r))return;const o=t(r.messageData??"");if("cue"!==o.type)return;const i=Number(o.pts);Number.isFinite(i)&&s.notifyCuePresented(i)}export function notedManifestBreakIds(e,t){const r=[];for(const n of e.breaks)t.has(n.id)||(t.add(n.id),r.push(n.id));return r}export function diagnoseAnvatoCueObserved(r,n,s,o,i,u){if(!s||!e(r))return;const a=r.messageData??"",c=t(a);if("cue"!==c.type)return;const d=`${r.startTime}|${a}`;if(o.has(d))return;o.add(d);const m=Number(c.pts);u("DA-ANVATO-CUE-OBSERVED",`Anvato cue observed: pts=${c.pts} at media time ${r.startTime}s`,{pts:Number.isFinite(m)?m:c.pts,mediaTime:r.startTime,playheadSec:i,recorded:n,payload:a.slice(0,300)})}
@@ -25,6 +25,8 @@ export interface BreakTimingState {
25
25
  anvatoPendingDiagnosed: Set<string>;
26
26
  /** Break ids for which DA-ANVATO-CUE-MATCHED has been emitted (one-shot). */
27
27
  anvatoMatchedDiagnosed: Set<string>;
28
+ /** Break ids whose `cue_end` was judged (DA-ANVATO-CUE-END-MATCHED or -REJECTED, one-shot). */
29
+ anvatoEndDiagnosed: Set<string>;
28
30
  }
29
31
  export declare function createBreakTimingState(): BreakTimingState;
30
32
  export interface BreakTimingDeps {
@@ -57,4 +59,9 @@ export declare function getBreakStartTime(state: BreakTimingState, deps: BreakTi
57
59
  * Null while no matching cue was observed; pending and first match are each diagnosed once.
58
60
  */
59
61
  export declare function resolveAnvatoStartTime(state: BreakTimingState, deps: BreakTimingDeps, breakInfo: Break, startPts: number): number | null;
62
+ /**
63
+ * Judge the active PTS break's `type=cue_end` once: matched when its stream-clock length is
64
+ * within tolerance of the duration, rejected otherwise. Nothing until a candidate is observed.
65
+ */
66
+ export declare function diagnoseAnvatoBreakEnd(state: BreakTimingState, deps: BreakTimingDeps, breakInfo: Break): void;
60
67
  export declare function secondsUntilBreak(breakStartTime: number, currentTime: number): number;
@@ -1 +1 @@
1
- import{Timebase as t}from"../types/BreakManifest";export const PDT_PLAYBACK_SETTLE_MS=1e3;export function createBreakTimingState(){return{timebase:t.WALLCLOCK,pdtMissingWarned:!1,pdtSeen:!1,firstPdtAttemptMs:null,playbackStartedAtMs:null,anvatoPendingDiagnosed:new Set,anvatoMatchedDiagnosed:new Set}}export function getCurrentTime(e,a){if(e.timebase!==t.WALLCLOCK)return 1e3*a.player.currentTime;const n=a.player.programDateTime;if(n){const t=n.getTime();if(!isNaN(t))return e.pdtSeen=!0,t}if(e.pdtSeen)return null;const r=a.clock.now();null===e.firstPdtAttemptMs&&(e.firstPdtAttemptMs=r),null===e.playbackStartedAtMs&&a.player.currentTime>0&&(e.playbackStartedAtMs=r);const s=null!==e.playbackStartedAtMs&&r-e.playbackStartedAtMs>=1e3,i=r-e.firstPdtAttemptMs>=a.pdtGraceMs;return s||i?(warnPdtMissing(e,a,s),r):null}export function warnPdtMissing(t,e,a){if(t.pdtMissingWarned)return;t.pdtMissingWarned=!0;const n=a?"content is playing and exposes none":`none appeared within ${e.pdtGraceSeconds}s`;e.log(`No Program Date Time on wallclock stream (${n}); falling back to the system clock`),e.diagnose?.("DA-PDT-MISSING",`No EXT-X-PROGRAM-DATE-TIME available (${n}); wallclock-timebase breaks are matched against the system clock instead of the stream position.`,{graceSeconds:e.pdtGraceSeconds,playbackStarted:a})}export function getBreakStartTime(e,a,n){if(e.timebase===t.WALLCLOCK){if("string"!=typeof n.start)return a.log("Invalid break start for wallclock timebase:",n.start),null;const t=new Date(n.start);return isNaN(t.getTime())?(a.log("Invalid break start date:",n.start),null):t.getTime()}return"number"!=typeof n.start?(a.log(`Invalid break start for ${e.timebase} timebase:`,n.start),null):a.cueTracker&&e.timebase===t.PTS?resolveAnvatoStartTime(e,a,n,n.start):1e3*n.start}export function resolveAnvatoStartTime(t,e,a,n){const r=e.cueTracker.mediaTimeForPts(n);return null===r?(t.anvatoPendingDiagnosed.has(a.id)||(t.anvatoPendingDiagnosed.add(a.id),e.log(`⌛ Break "${a.id}" awaiting Anvato cue for PTS ${n}`),e.diagnose?.("DA-ANVATO-CUE-PENDING",`No Anvato cue observed for break "${a.id}" (pts ${n}); break stays pending.`,{breakId:a.id,pts:n})),null):(t.anvatoMatchedDiagnosed.has(a.id)||(t.anvatoMatchedDiagnosed.add(a.id),e.log(`🎯 Break "${a.id}" matched Anvato cue: pts ${n} → ${r}s`),e.diagnose?.("DA-ANVATO-CUE-MATCHED",`Anvato cue matched for break "${a.id}": pts ${n} plays at media time ${r}s.`,{breakId:a.id,pts:n,mediaTime:r})),1e3*r)}export function secondsUntilBreak(t,e){const a=Math.round((t-e)/1e3);return a>0?a:0}
1
+ import{Timebase as e}from"../types/BreakManifest";export const PDT_PLAYBACK_SETTLE_MS=1e3;export function createBreakTimingState(){return{timebase:e.WALLCLOCK,pdtMissingWarned:!1,pdtSeen:!1,firstPdtAttemptMs:null,playbackStartedAtMs:null,anvatoPendingDiagnosed:new Set,anvatoMatchedDiagnosed:new Set,anvatoEndDiagnosed:new Set}}export function getCurrentTime(t,a){if(t.timebase!==e.WALLCLOCK)return 1e3*a.player.currentTime;const n=a.player.programDateTime;if(n){const e=n.getTime();if(!isNaN(e))return t.pdtSeen=!0,e}if(t.pdtSeen)return null;const r=a.clock.now();null===t.firstPdtAttemptMs&&(t.firstPdtAttemptMs=r),null===t.playbackStartedAtMs&&a.player.currentTime>0&&(t.playbackStartedAtMs=r);const i=null!==t.playbackStartedAtMs&&r-t.playbackStartedAtMs>=1e3,s=r-t.firstPdtAttemptMs>=a.pdtGraceMs;return i||s?(warnPdtMissing(t,a,i),r):null}export function warnPdtMissing(e,t,a){if(e.pdtMissingWarned)return;e.pdtMissingWarned=!0;const n=a?"content is playing and exposes none":`none appeared within ${t.pdtGraceSeconds}s`;t.log(`No Program Date Time on wallclock stream (${n}); falling back to the system clock`),t.diagnose?.("DA-PDT-MISSING",`No EXT-X-PROGRAM-DATE-TIME available (${n}); wallclock-timebase breaks are matched against the system clock instead of the stream position.`,{graceSeconds:t.pdtGraceSeconds,playbackStarted:a})}export function getBreakStartTime(t,a,n){if(t.timebase===e.WALLCLOCK){if("string"!=typeof n.start)return a.log("Invalid break start for wallclock timebase:",n.start),null;const e=new Date(n.start);return isNaN(e.getTime())?(a.log("Invalid break start date:",n.start),null):e.getTime()}return"number"!=typeof n.start?(a.log(`Invalid break start for ${t.timebase} timebase:`,n.start),null):a.cueTracker&&t.timebase===e.PTS?resolveAnvatoStartTime(t,a,n,n.start):1e3*n.start}export function resolveAnvatoStartTime(e,t,a,n){const r=t.cueTracker.mediaTimeForPts(n);return null===r?(e.anvatoPendingDiagnosed.has(a.id)||(e.anvatoPendingDiagnosed.add(a.id),t.log(`⌛ Break "${a.id}" awaiting Anvato cue for PTS ${n}`),t.diagnose?.("DA-ANVATO-CUE-PENDING",`No Anvato cue observed for break "${a.id}" (pts ${n}); break stays pending.`,{breakId:a.id,pts:n})),null):(e.anvatoMatchedDiagnosed.has(a.id)||(e.anvatoMatchedDiagnosed.add(a.id),t.log(`🎯 Break "${a.id}" matched Anvato cue: pts ${n} → ${r}s`),t.diagnose?.("DA-ANVATO-CUE-MATCHED",`Anvato cue matched for break "${a.id}": pts ${n} plays at media time ${r}s.`,{breakId:a.id,pts:n,mediaTime:r})),1e3*r)}export function diagnoseAnvatoBreakEnd(e,t,a){if(null===t.cueTracker||"number"!=typeof a.start)return;if(e.anvatoEndDiagnosed.has(a.id))return;const n=t.cueTracker.breakEnd(a.start,a.duration);if(null===n)return;e.anvatoEndDiagnosed.add(a.id);const r={breakId:a.id,mediaTime:n.mediaTime,lengthSec:n.lengthSec,durationSec:a.duration};n.accepted?(t.log(`Break "${a.id}" end signalled at media time ${n.mediaTime}s`),t.diagnose?.("DA-ANVATO-CUE-END-MATCHED",`Anvato cue_end matched for break "${a.id}": the stream ends it at media time ${n.mediaTime}s (${n.lengthSec}s on the stream clock).`,r)):t.diagnose?.("DA-ANVATO-CUE-END-REJECTED",`Anvato cue_end for break "${a.id}" signals ${n.lengthSec}s, not the ${a.duration}s duration; it is ignored.`,r)}export function secondsUntilBreak(e,t){const a=Math.round((e-t)/1e3);return a>0?a:0}
@@ -0,0 +1,6 @@
1
+ import type { KnownDiagnosticCode } from '../diagnostics/errorCodes';
2
+ /**
3
+ * Canonical CMCD `adec` code for an IMA client-side (VAST) error, by IMA `AdErrorCode` name.
4
+ * Unknown or absent names map to `DA-VAST-IMA-ERROR`; the raw IMA number travels in `adevc`.
5
+ */
6
+ export declare function classifyImaAdError(name: string | null | undefined): KnownDiagnosticCode;
@@ -0,0 +1 @@
1
+ const A=new Map([["DA-VAST-NO-FILL",["VAST_EMPTY_RESPONSE","VAST_NO_ADS_AFTER_WRAPPER","VAST_ASSET_NOT_FOUND"]],["DA-VAST-TIMEOUT",["VAST_LOAD_TIMEOUT","VAST_MEDIA_LOAD_TIMEOUT"]],["DA-VAST-MALFORMED",["VAST_MALFORMED_RESPONSE","VAST_SCHEMA_VALIDATION_ERROR","VAST_UNSUPPORTED_VERSION","UNKNOWN_AD_RESPONSE","PLAYLIST_MALFORMED_RESPONSE"]],["DA-VAST-REQUEST-FAILED",["FAILED_TO_REQUEST_ADS","ADS_REQUEST_NETWORK_ERROR","VAST_WRAPPER_ERROR","VAST_TOO_MANY_REDIRECTS","VAST_INVALID_URL","INVALID_AD_TAG","UNSUPPORTED_URL"]],["DA-VAST-CREATIVE-FAILED",["VIDEO_PLAY_ERROR","VAST_LINEAR_ASSET_MISMATCH","VAST_PROBLEM_DISPLAYING_MEDIA_FILE","VAST_TRAFFICKING_ERROR","VAST_UNEXPECTED_LINEARITY","VAST_UNEXPECTED_DURATION","VPAID_ERROR"]]].flatMap(([A,E])=>E.map(E=>[E,A])));export function classifyImaAdError(E){return(E?A.get(E):void 0)??"DA-VAST-IMA-ERROR"}
@@ -0,0 +1,2 @@
1
+ /** Name of an HTML5 IMA error number; undefined when the number is not mapped. */
2
+ export declare function html5ImaErrorName(code: number): string | undefined;
@@ -0,0 +1 @@
1
+ const _=new Map([[100,"VAST_MALFORMED_RESPONSE"],[101,"VAST_SCHEMA_VALIDATION_ERROR"],[102,"VAST_UNSUPPORTED_VERSION"],[200,"VAST_TRAFFICKING_ERROR"],[201,"VAST_UNEXPECTED_LINEARITY"],[202,"VAST_UNEXPECTED_DURATION"],[300,"VAST_WRAPPER_ERROR"],[301,"VAST_LOAD_TIMEOUT"],[302,"VAST_TOO_MANY_REDIRECTS"],[303,"VAST_NO_ADS_AFTER_WRAPPER"],[400,"VIDEO_PLAY_ERROR"],[402,"VAST_MEDIA_LOAD_TIMEOUT"],[403,"VAST_LINEAR_ASSET_MISMATCH"],[405,"VAST_PROBLEM_DISPLAYING_MEDIA_FILE"],[900,"UNKNOWN_ERROR"],[901,"VPAID_ERROR"],[1005,"FAILED_TO_REQUEST_ADS"],[1007,"VAST_ASSET_NOT_FOUND"],[1009,"VAST_EMPTY_RESPONSE"],[1010,"UNKNOWN_AD_RESPONSE"],[1012,"ADS_REQUEST_NETWORK_ERROR"],[1013,"INVALID_AD_TAG"],[1022,"UNSUPPORTED_URL"]]);export function html5ImaErrorName(E){return _.get(E)}
@@ -4,7 +4,7 @@
4
4
  * builds (never committed, like the version stamp):
5
5
  *
6
6
  * main branch → production (committed default, no stamp)
7
- * any other branch → development (rp2 ingest)
7
+ * any other branch → development (staging ingest)
8
8
  *
9
9
  * The committed default is production on purpose: a build that somehow skips the
10
10
  * stamp must degrade to "reports to production", never to "release artefact
@@ -0,0 +1 @@
1
+ export declare function resolveLiveStartPushback(position: number, seekableStart: number, seekableEnd: number, targetDuration: number, minimumDistanceTargetDurations?: number): number | null;
@@ -0,0 +1 @@
1
+ export function resolveLiveStartPushback(e,t,n,r,u=5){const i=r*u;if(![e,t,n,r,u,i].every(Number.isFinite)||r<=0||u<=0||n<=t||n-e>=86400||n-e>=i)return null;const l=Math.max(t,n-i);return l<e?l:null}
@@ -1 +1 @@
1
- export class AdBreakStatusTracker{constructor(t){this.deps=t,this.adBreakStatus={phase:"idle",break:null,ticking:!1},this.activeBreak=null,this.activeBreakRemainingAtStart=null,this.activeBreakStartTimestamp=null,this.activeBreakElapsedSec=0,this.currentAdStartElapsedSec=0,this.currentAdAssetId=null,this.timelineAssetId=null,this.timelineStartElapsedSec=0,this.currentAdIndex=null,this.currentAdTotal=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakWaiting=!1,this.activeBreakPlaying=!1}getAdBreakStatus(){if("active"===this.adBreakStatus.phase)return{...this.adBreakStatus,breakRemainingSec:this.computeBreakRemainingSec()};if("upcoming"===this.adBreakStatus.phase&&this.activeBreak){const t=this.deps.secondsUntilBreak(this.activeBreak.id);return{...this.adBreakStatus,secondsUntilBreak:t}}return{...this.adBreakStatus}}onBreakWarning(t,e,i){this.activeBreak=t,this.emitAdBreakStatus({phase:"upcoming",break:t,format:this.deps.effectiveFormatOf(t),secondsUntilBreak:i,ticking:!1,warningSec:e})}setAdBreakStatusActive(t,e){this.activeBreak=t,this.activeBreakRemainingAtStart=e,this.activeBreakStartTimestamp=Date.now(),this.activeBreakElapsedSec=0,this.currentAdStartElapsedSec=0,this.currentAdAssetId=null,this.timelineAssetId=null,this.timelineStartElapsedSec=0,this.currentAdIndex=null,this.currentAdTotal=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakWaiting=!1,this.activeBreakPlaying=!1,this.emitAdBreakStatus({phase:"active",break:t,format:this.deps.effectiveFormatOf(t),breakRemainingSec:this.activeBreakRemainingAtStart,ticking:!1})}setAdBreakVisible(t){this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),ticking:!0})}onAdStarted(t,e,i,a){this.currentAdIndex=i,this.currentAdAssetId=e.id,this.currentAdTotal=a,this.currentAdTime=null,this.currentAdDuration=null,this.currentAdStartElapsedSec=this.currentBreakElapsedSec(),this.activeBreakElapsedSec=this.currentAdStartElapsedSec,this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.deps.breakTransitionTimer()?.settle("into-break","ad-to-ad","break-to-break"),this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),adIndex:i,totalAds:a,adsRemaining:a-i,ticking:!0})}onAdEnded(t,e,i){this.timelineAssetId===this.currentAdAssetId&&null!==this.currentAdDuration&&Number.isFinite(this.currentAdDuration)?this.activeBreakElapsedSec=this.timelineStartElapsedSec+this.currentAdDuration:null!==this.currentAdTime&&(this.activeBreakElapsedSec=this.timelineStartElapsedSec+this.currentAdTime),this.currentAdIndex=null,this.currentAdAssetId=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakPlaying=!1,this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),adsRemaining:Math.max(0,i-e-1),breakRemainingSec:this.computeBreakRemainingSec(),ticking:!1})}onAdTimeUpdated(t,e,i,a){e.id!==this.timelineAssetId&&(this.timelineAssetId=e.id,this.timelineStartElapsedSec=this.currentAdStartElapsedSec),this.currentAdTime=i,this.currentAdDuration=a,this.activeBreakElapsedSec=this.timelineStartElapsedSec+i,this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),breakRemainingSec:this.computeBreakRemainingSec(),ticking:!0})}onAdPlaybackWaiting(){this.activeBreakWaiting=!0,this.activeBreakPlaying=!1,this.activeBreak&&this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(this.activeBreak),ticking:!1})}onAdPlaybackPlaying(){this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.activeBreak&&this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(this.activeBreak),ticking:!0})}buildActiveAdBreakStatus(t){return{phase:"active",break:t,format:this.deps.effectiveFormatOf(t),adIndex:this.currentAdIndex??void 0,totalAds:this.currentAdTotal??void 0,adsRemaining:null!==this.currentAdIndex&&null!==this.currentAdTotal?this.currentAdTotal-this.currentAdIndex:void 0,breakRemainingSec:this.computeBreakRemainingSec(),ticking:this.activeBreakPlaying&&!this.activeBreakWaiting}}computeBreakRemainingSec(){if(null!==this.activeBreakRemainingAtStart)return Math.max(0,this.activeBreakRemainingAtStart-this.currentBreakElapsedSec())}currentBreakElapsedSec(){let t=this.activeBreakElapsedSec;return 0===t&&null!==this.activeBreakStartTimestamp&&null===this.currentAdTime&&(t=(Date.now()-this.activeBreakStartTimestamp)/1e3),t}resetAdBreakStatus(){this.activeBreak=null,this.activeBreakRemainingAtStart=null,this.activeBreakStartTimestamp=null,this.activeBreakElapsedSec=0,this.currentAdStartElapsedSec=0,this.currentAdAssetId=null,this.timelineAssetId=null,this.timelineStartElapsedSec=0,this.currentAdIndex=null,this.currentAdTotal=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakWaiting=!1,this.activeBreakPlaying=!1,this.emitAdBreakStatus({phase:"idle",break:null,ticking:!1})}emitAdBreakStatus(t){this.statusesEqual(this.adBreakStatus,t)||(this.adBreakStatus=t,this.deps.emit("adbreakstatus",{type:"adbreakstatus",timestamp:Date.now(),status:t}))}statusesEqual(t,e){return t.phase===e.phase&&t.format===e.format&&t.secondsUntilBreak===e.secondsUntilBreak&&t.breakRemainingSec===e.breakRemainingSec&&t.adIndex===e.adIndex&&t.totalAds===e.totalAds&&t.adsRemaining===e.adsRemaining&&t.ticking===e.ticking&&t.warningSec===e.warningSec&&t.break?.id===e.break?.id}}
1
+ export class AdBreakStatusTracker{constructor(t){this.deps=t,this.adBreakStatus={phase:"idle",break:null,ticking:!1},this.activeBreak=null,this.activeBreakRemainingAtStart=null,this.activeBreakStartTimestamp=null,this.activeBreakElapsedSec=0,this.currentAdStartElapsedSec=0,this.currentAdAssetId=null,this.timelineAssetId=null,this.timelineStartElapsedSec=0,this.currentAdIndex=null,this.currentAdTotal=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakWaiting=!1,this.activeBreakPlaying=!1}getAdBreakStatus(){if("active"===this.adBreakStatus.phase)return{...this.adBreakStatus,breakRemainingSec:this.computeBreakRemainingSec()};if("upcoming"===this.adBreakStatus.phase&&this.activeBreak){const t=this.deps.secondsUntilBreak(this.activeBreak.id);return{...this.adBreakStatus,secondsUntilBreak:t}}return{...this.adBreakStatus}}onBreakWarning(t,e,i){this.activeBreak=t,this.emitAdBreakStatus({phase:"upcoming",break:t,format:this.deps.effectiveFormatOf(t),secondsUntilBreak:i,ticking:!1,warningSec:e})}setAdBreakStatusActive(t,e){this.activeBreak=t,this.activeBreakRemainingAtStart=e,this.activeBreakStartTimestamp=Date.now(),this.activeBreakElapsedSec=0,this.currentAdStartElapsedSec=0,this.currentAdAssetId=null,this.timelineAssetId=null,this.timelineStartElapsedSec=0,this.currentAdIndex=null,this.currentAdTotal=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakWaiting=!1,this.activeBreakPlaying=!1,this.emitAdBreakStatus({phase:"active",break:t,format:this.deps.effectiveFormatOf(t),breakRemainingSec:this.activeBreakRemainingAtStart,ticking:!1})}setAdBreakVisible(t){this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),ticking:!0})}onAdStarted(t,e,i,a){this.currentAdIndex=i,this.currentAdAssetId=e.id,this.currentAdTotal=a,this.currentAdTime=null,this.currentAdDuration=null,this.currentAdStartElapsedSec=this.currentBreakElapsedSec(),this.activeBreakElapsedSec=this.currentAdStartElapsedSec,this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.deps.breakTransitionTimer()?.settle("into-break","ad-to-ad","break-to-break"),this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),adIndex:i,totalAds:a,adsRemaining:a-i,ticking:!0})}onAdEnded(t,e,i){this.timelineAssetId===this.currentAdAssetId&&null!==this.currentAdDuration&&Number.isFinite(this.currentAdDuration)?this.activeBreakElapsedSec=this.timelineStartElapsedSec+this.currentAdDuration:null!==this.currentAdTime&&(this.activeBreakElapsedSec=this.timelineStartElapsedSec+this.currentAdTime),this.currentAdIndex=null,this.currentAdAssetId=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakPlaying=!1,this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),adsRemaining:Math.max(0,i-e-1),breakRemainingSec:this.computeBreakRemainingSec(),ticking:!1})}onAdTimeUpdated(t,e,i,a){e.id!==this.timelineAssetId&&(this.timelineAssetId=e.id,this.timelineStartElapsedSec=null===this.currentAdIndex?this.currentBreakElapsedSec():this.currentAdStartElapsedSec),this.currentAdTime=i,this.currentAdDuration=a,this.activeBreakElapsedSec=this.timelineStartElapsedSec+i,this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),breakRemainingSec:this.computeBreakRemainingSec(),ticking:!0})}onAdPlaybackWaiting(){this.activeBreakWaiting=!0,this.activeBreakPlaying=!1,this.activeBreak&&this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(this.activeBreak),ticking:!1})}onAdPlaybackPlaying(){this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.activeBreak&&this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(this.activeBreak),ticking:!0})}buildActiveAdBreakStatus(t){return{phase:"active",break:t,format:this.deps.effectiveFormatOf(t),adIndex:this.currentAdIndex??void 0,totalAds:this.currentAdTotal??void 0,adsRemaining:null!==this.currentAdIndex&&null!==this.currentAdTotal?this.currentAdTotal-this.currentAdIndex:void 0,breakRemainingSec:this.computeBreakRemainingSec(),ticking:this.activeBreakPlaying&&!this.activeBreakWaiting}}computeBreakRemainingSec(){if(null!==this.activeBreakRemainingAtStart)return Math.max(0,this.activeBreakRemainingAtStart-this.currentBreakElapsedSec())}currentBreakElapsedSec(){let t=this.activeBreakElapsedSec;return 0===t&&null!==this.activeBreakStartTimestamp&&null===this.currentAdTime&&(t=(Date.now()-this.activeBreakStartTimestamp)/1e3),t}resetAdBreakStatus(){this.activeBreak=null,this.activeBreakRemainingAtStart=null,this.activeBreakStartTimestamp=null,this.activeBreakElapsedSec=0,this.currentAdStartElapsedSec=0,this.currentAdAssetId=null,this.timelineAssetId=null,this.timelineStartElapsedSec=0,this.currentAdIndex=null,this.currentAdTotal=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakWaiting=!1,this.activeBreakPlaying=!1,this.emitAdBreakStatus({phase:"idle",break:null,ticking:!1})}emitAdBreakStatus(t){this.statusesEqual(this.adBreakStatus,t)||(this.adBreakStatus=t,this.deps.emit("adbreakstatus",{type:"adbreakstatus",timestamp:Date.now(),status:t}))}statusesEqual(t,e){return t.phase===e.phase&&t.format===e.format&&t.secondsUntilBreak===e.secondsUntilBreak&&t.breakRemainingSec===e.breakRemainingSec&&t.adIndex===e.adIndex&&t.totalAds===e.totalAds&&t.adsRemaining===e.adsRemaining&&t.ticking===e.ticking&&t.warningSec===e.warningSec&&t.break?.id===e.break?.id}}
@@ -1 +1 @@
1
- import{BreakFormat as e}from"../types/BreakManifest";import{playbackErrorContext as t}from"../diagnostics/playbackErrorContext";export class BreakLifecycle{constructor(e,t){this.state=e,this.deps=t,this.breakBeginEmittedId=null,this.pendingAdBreakBegin=null}emitAdBreakBegin(e,t){this.breakBeginEmittedId!==e.id&&(this.breakBeginEmittedId=e.id,this.pendingAdBreakBegin=null,this.deps.emit("adbreakbegin",{type:"adbreakbegin",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),elapsedSec:t?.elapsedSec??0,remainingSec:t?.remainingSec??e.duration??0,...t?.tuneIn?{tuneIn:{elapsedSec:t.elapsedSec,remainingSec:t.remainingSec}}:{}}))}flushPendingAdBreakBegin(){this.pendingAdBreakBegin?.()}async onBreakStart(a,i){if(this.state.snapbackSeekInFlight=!1,this.deps.adPlayerController.isAdPlaying())return this.deps.diagnose("DA-BREAK-SUPPRESSED-OVERLAP",`Break suppressed (ad already playing): ${a.id}`,{breakId:a.id}),this.deps.breakScheduler.completeBreak(a.id),void this.deps.onBreakSettled?.();this.deps.adPlayerController.diagnoseUnknownVariantFormats(a);const d=this.deps.adPlayerController.breakNothingToPlayReason(a);if(d)return this.deps.log("Break not begun (no playable variant):",a.id,d),this.deps.diagnose("DA-BREAK-NO-PLAYABLE-ASSET",`break ${a.id} has no playable variant (${d}); it will not begin.`,{breakId:a.id,reason:d}),this.deps.emit("aderror",{type:"aderror",timestamp:Date.now(),source:"ad",break:a,format:this.deps.effectiveFormatOf(a),error:new Error(`Break ${a.id} has no playable variant (${d})`)}),this.deps.breakScheduler.completeBreak(a.id),void this.deps.onBreakSettled?.();this.deps.log("Break starting:",a.id,i.tuneIn?"(tune-in)":""),this.deps.breakTransitionTimer?.mark("into-break",a.id),this.breakBeginEmittedId=null,this.pendingAdBreakBegin=null;const r=this.state.heldImmediatePreRollId===a.id,s=this.deps.effectiveFormatOf(a),n=s===e.SINGLE||s===e.LSHAPE_AD,o=r&&n;r&&(this.state.heldImmediatePreRollId=null),this.deps.adBreakStatusTracker.setAdBreakStatusActive(a,void 0!==i.remainingSec?i.remainingSec:a.duration),o||this.emitAdBreakBegin(a,i),this.deps.manifestService.setActive(!0),this.deps.breakScheduler.claimChain(a.id);const k={onAdStart:(e,t,a,i,d)=>{this.deps.adBreakStatusTracker.onAdStarted(e,t,a,i),this.deps.emit("adbegin",{type:"adbegin",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,adIndex:a,totalAds:i,...d?.adId?{adId:d.adId}:{},...d?.creativeId?{creativeId:d.creativeId}:{},...void 0!==d?.podPosition?{podPosition:d.podPosition}:{}})},onAdEnd:(e,t,a,i,d)=>{this.deps.adBreakStatusTracker.onAdEnded(e,a,i),a+1<i&&this.deps.breakTransitionTimer?.mark("ad-to-ad",e.id),this.deps.emit("adend",{type:"adend",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,adIndex:a,totalAds:i,...d?.adId?{adId:d.adId}:{},...d?.creativeId?{creativeId:d.creativeId}:{}})},onAdError:(e,a,i)=>{this.flushPendingAdBreakBegin(),this.deps.diagnose("DA-AD-PLAYBACK-ERROR",i.message,{breakId:e.id,assetId:a?.id,...t(i)}),this.deps.emit("aderror",{type:"aderror",timestamp:Date.now(),source:"ad",break:e,format:this.deps.effectiveFormatOf(e),...a?{asset:a}:{},error:i})},onAdTimeUpdate:(e,t,a,i)=>{this.deps.adBreakStatusTracker.onAdTimeUpdated(e,t,a,i),this.deps.emit("adtimeupdate",{type:"adtimeupdate",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,currentTime:a,duration:i})},onAdWaiting:(e,t)=>{this.deps.adBreakStatusTracker.onAdPlaybackWaiting(),this.deps.emit("waiting",{type:"waiting",timestamp:Date.now(),source:"ad",break:e,asset:t})},onAdPlaying:(e,t)=>{this.deps.adBreakStatusTracker.onAdPlaybackPlaying(),this.deps.emit("playing",{type:"playing",timestamp:Date.now(),source:"ad",break:e,asset:t})},onBreakEnd:e=>{this.onBreakEnd(e)},onBreakCut:e=>{this.deps.cmcdAdReporter.onBreakCut(e.id)},onBreakEndedWithoutContentPause:()=>{this.onBreakEndedWithoutContentPause()},onChainedBreakBegin:e=>{this.onChainedBreakBegin(e)},onChainedBreakSkipped:e=>{this.deps.log("Chained break skipped (no playable variant):",e.id),this.deps.breakScheduler.completeBreak(e.id)},onContentResumeSeek:(e,t)=>{this.deps.breakScheduler.notifyResumeSeek(e,t)},onContentReloading:e=>{this.deps.breakScheduler.setContentReloading(e)}};o&&(this.pendingAdBreakBegin=()=>this.emitAdBreakBegin(a,i),k.onBreakVisible=e=>{this.deps.adBreakStatusTracker.setAdBreakVisible(e),this.flushPendingAdBreakBegin()}),await this.deps.adPlayerController.playBreak(a,k,{effectiveDurationSec:i.remainingSec,firstFrameGate:o})}onChainedBreakBegin(e){this.deps.log("Chained break starting:",e.id),this.deps.breakTransitionTimer?.mark("break-to-break",e.id),this.deps.breakScheduler.activateChainedBreak(e.id),this.deps.manifestService.setActive(!0),this.deps.adBreakStatusTracker.setAdBreakStatusActive(e,e.duration),this.emitAdBreakBegin(e)}onBreakEnd(e){this.deps.log("Break ended:",e.id),this.deps.breakTransitionTimer?.mark("out-of-break",e.id),this.deps.breakScheduler.completeBreak(e.id),this.deps.manifestService.setActive(!1),this.deps.adBreakStatusTracker.resetAdBreakStatus(),this.flushPendingAdBreakBegin(),this.deps.emit("adbreakend",{type:"adbreakend",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e)}),this.deps.onBreakSettled?.()}onBreakEndedWithoutContentPause(){this.deps.breakTransitionTimer?.settle("out-of-break"),this.deps.emit("playing",{type:"playing",timestamp:Date.now(),source:"content"})}onBreakAborted(e){if(this.deps.adPlayerController.isAdPlaying()){const t=this.deps.adBreakStatusTracker.currentBreakElapsedSec();this.deps.log(`Early return — ending break '${e.id}' at ${t.toFixed(2)}s`),this.deps.cmcdAdReporter.onBreakAborted(e.id),this.deps.adPlayerController.abortBreak(t)}else this.deps.breakScheduler.completeBreak(e.id),this.deps.onBreakSettled?.()}reportAbandonedBreak(e){const t=this.deps.adBreakStatusTracker.activeBreak;if(!t)return;const a=this.deps.adBreakStatusTracker.currentAdIndex;this.deps.diagnose("DA-BREAK-ABANDONED",`Session torn down (${e}) while break ${t.id} was still in flight`+(null!==a?"; an ad began and will never end.":"."),{breakId:t.id,cause:e,phase:this.deps.adBreakStatusTracker.adBreakStatus.phase,unbalancedAdBegin:null!==a,...null!==a?{adIndex:a}:{},...null!==this.deps.adBreakStatusTracker.currentAdTotal?{totalAds:this.deps.adBreakStatusTracker.currentAdTotal}:{}})}}
1
+ import{BreakFormat as e}from"../types/BreakManifest";import{playbackErrorContext as t}from"../diagnostics/playbackErrorContext";import{adErrorCode as a}from"../types/AdError";import{VendorAdError as i}from"../types/VendorAdError";export class BreakLifecycle{constructor(e,t){this.state=e,this.deps=t,this.breakBeginEmittedId=null,this.pendingAdBreakBegin=null}emitAdBreakBegin(e,t){this.breakBeginEmittedId!==e.id&&(this.breakBeginEmittedId=e.id,this.pendingAdBreakBegin=null,this.deps.emit("adbreakbegin",{type:"adbreakbegin",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),elapsedSec:t?.elapsedSec??0,remainingSec:t?.remainingSec??e.duration??0,...t?.tuneIn?{tuneIn:{elapsedSec:t.elapsedSec,remainingSec:t.remainingSec}}:{}}))}flushPendingAdBreakBegin(){this.pendingAdBreakBegin?.()}async onBreakStart(d,r){if(this.state.snapbackSeekInFlight=!1,this.deps.adPlayerController.isAdPlaying())return this.deps.diagnose("DA-BREAK-SUPPRESSED-OVERLAP",`Break suppressed (ad already playing): ${d.id}`,{breakId:d.id}),this.deps.breakScheduler.completeBreak(d.id),void this.deps.onBreakSettled?.();this.deps.adPlayerController.diagnoseUnknownVariantFormats(d);const s=this.deps.adPlayerController.breakNothingToPlayReason(d);if(s)return this.deps.log("Break not begun (no playable variant):",d.id,s),this.deps.diagnose("DA-BREAK-NO-PLAYABLE-ASSET",`break ${d.id} has no playable variant (${s}); it will not begin.`,{breakId:d.id,reason:s}),this.deps.emit("aderror",{type:"aderror",timestamp:Date.now(),source:"ad",break:d,format:this.deps.effectiveFormatOf(d),error:new Error(`Break ${d.id} has no playable variant (${s})`)}),this.deps.breakScheduler.completeBreak(d.id),void this.deps.onBreakSettled?.();this.deps.log("Break starting:",d.id,r.tuneIn?"(tune-in)":""),this.deps.breakTransitionTimer?.mark("into-break",d.id),this.breakBeginEmittedId=null,this.pendingAdBreakBegin=null;const n=this.state.heldImmediatePreRollId===d.id,o=this.deps.effectiveFormatOf(d),k=o===e.SINGLE||o===e.LSHAPE_AD,p=n&&k;n&&(this.state.heldImmediatePreRollId=null),this.deps.adBreakStatusTracker.setAdBreakStatusActive(d,void 0!==r.remainingSec?r.remainingSec:d.duration),p||this.emitAdBreakBegin(d,r),this.deps.manifestService.setActive(!0),this.deps.breakScheduler.claimChain(d.id);const h={onAdStart:(e,t,a,i,d)=>{this.deps.adBreakStatusTracker.onAdStarted(e,t,a,i),this.deps.emit("adbegin",{type:"adbegin",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,adIndex:a,totalAds:i,...d?.adId?{adId:d.adId}:{},...d?.creativeId?{creativeId:d.creativeId}:{},...void 0!==d?.podPosition?{podPosition:d.podPosition}:{}})},onAdEnd:(e,t,a,i,d)=>{this.deps.adBreakStatusTracker.onAdEnded(e,a,i),a+1<i&&this.deps.breakTransitionTimer?.mark("ad-to-ad",e.id),this.deps.emit("adend",{type:"adend",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,adIndex:a,totalAds:i,...d?.adId?{adId:d.adId}:{},...d?.creativeId?{creativeId:d.creativeId}:{}})},onAdError:(e,d,r)=>{this.flushPendingAdBreakBegin(),this.deps.diagnose("DA-AD-PLAYBACK-ERROR",r.message,{breakId:e.id,assetId:d?.id,adErrorCode:a(r),...r instanceof i?{vendorCode:r.vendorCode}:{},...t(r)}),this.deps.emit("aderror",{type:"aderror",timestamp:Date.now(),source:"ad",break:e,format:this.deps.effectiveFormatOf(e),...d?{asset:d}:{},error:r})},onAdTimeUpdate:(e,t,a,i)=>{this.deps.adBreakStatusTracker.onAdTimeUpdated(e,t,a,i),this.deps.emit("adtimeupdate",{type:"adtimeupdate",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,currentTime:a,duration:i})},onAdWaiting:(e,t)=>{this.deps.adBreakStatusTracker.onAdPlaybackWaiting(),this.deps.emit("waiting",{type:"waiting",timestamp:Date.now(),source:"ad",break:e,asset:t})},onAdPlaying:(e,t)=>{this.deps.adBreakStatusTracker.onAdPlaybackPlaying(),this.deps.emit("playing",{type:"playing",timestamp:Date.now(),source:"ad",break:e,asset:t})},onBreakEnd:e=>{this.onBreakEnd(e)},onBreakCut:e=>{this.deps.cmcdAdReporter.onBreakCut(e.id)},onBreakEndedWithoutContentPause:()=>{this.onBreakEndedWithoutContentPause()},onChainedBreakBegin:e=>{this.onChainedBreakBegin(e)},onChainedBreakSkipped:e=>{this.deps.log("Chained break skipped (no playable variant):",e.id),this.deps.breakScheduler.completeBreak(e.id)},onContentResumeSeek:(e,t)=>{this.deps.breakScheduler.notifyResumeSeek(e,t)},onContentReloading:e=>{this.deps.breakScheduler.setContentReloading(e)}};p&&(this.pendingAdBreakBegin=()=>this.emitAdBreakBegin(d,r),h.onBreakVisible=e=>{this.deps.adBreakStatusTracker.setAdBreakVisible(e),this.flushPendingAdBreakBegin()}),await this.deps.adPlayerController.playBreak(d,h,{effectiveDurationSec:r.remainingSec,firstFrameGate:p})}onChainedBreakBegin(e){this.deps.log("Chained break starting:",e.id),this.deps.breakTransitionTimer?.mark("break-to-break",e.id),this.deps.breakScheduler.activateChainedBreak(e.id),this.deps.manifestService.setActive(!0),this.deps.adBreakStatusTracker.setAdBreakStatusActive(e,e.duration),this.emitAdBreakBegin(e)}onBreakEnd(e){this.deps.log("Break ended:",e.id),this.deps.breakTransitionTimer?.mark("out-of-break",e.id),this.deps.breakScheduler.completeBreak(e.id),this.deps.manifestService.setActive(!1),this.deps.adBreakStatusTracker.resetAdBreakStatus(),this.flushPendingAdBreakBegin(),this.deps.emit("adbreakend",{type:"adbreakend",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e)}),this.deps.onBreakSettled?.()}onBreakEndedWithoutContentPause(){this.deps.breakTransitionTimer?.settle("out-of-break"),this.deps.emit("playing",{type:"playing",timestamp:Date.now(),source:"content"})}onBreakAborted(e){if(this.deps.adPlayerController.isAdPlaying()){const t=this.deps.adBreakStatusTracker.currentBreakElapsedSec();this.deps.log(`Early return — ending break '${e.id}' at ${t.toFixed(2)}s`),this.deps.cmcdAdReporter.onBreakAborted(e.id),this.deps.adPlayerController.abortBreak(t)}else this.deps.breakScheduler.completeBreak(e.id),this.deps.onBreakSettled?.()}reportAbandonedBreak(e){const t=this.deps.adBreakStatusTracker.activeBreak;if(!t)return;const a=this.deps.adBreakStatusTracker.currentAdIndex;this.deps.diagnose("DA-BREAK-ABANDONED",`Session torn down (${e}) while break ${t.id} was still in flight`+(null!==a?"; an ad began and will never end.":"."),{breakId:t.id,cause:e,phase:this.deps.adBreakStatusTracker.adBreakStatus.phase,unbalancedAdBegin:null!==a,...null!==a?{adIndex:a}:{},...null!==this.deps.adBreakStatusTracker.currentAdTotal?{totalAds:this.deps.adBreakStatusTracker.currentAdTotal}:{}})}}
@@ -16,6 +16,7 @@ export interface SessionStarterDeps {
16
16
  mode: OptiViewAdsMode;
17
17
  hasExplicitMode: boolean;
18
18
  continueContentDuringBreak: boolean;
19
+ startLivePushback: () => void;
19
20
  manifestService: ManifestService | undefined;
20
21
  breakScheduler: BreakScheduler | undefined;
21
22
  adPlayerController: AdPlayerController | undefined;