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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/OptiViewAds.d.ts +8 -4
  2. package/dist/OptiViewAds.js +1 -1
  3. package/dist/OptiViewAdsExternalVendors.d.ts +47 -0
  4. package/dist/OptiViewAdsExternalVendors.js +1 -0
  5. package/dist/generated/diagnosticCodes.d.ts +72 -2
  6. package/dist/generated/diagnosticCodes.js +1 -1
  7. package/dist/generated/version.d.ts +1 -1
  8. package/dist/generated/version.js +1 -1
  9. package/dist/id3/index.d.ts +1 -0
  10. package/dist/id3/index.js +1 -1
  11. package/dist/id3/parseId3TextFrames.d.ts +8 -0
  12. package/dist/id3/parseId3TextFrames.js +1 -0
  13. package/dist/index.d.ts +8 -3
  14. package/dist/index.js +1 -1
  15. package/dist/interfaces/PlayerAdapter.d.ts +5 -0
  16. package/dist/services/AdBreakSequencer.js +1 -1
  17. package/dist/services/AdPlayerController.d.ts +2 -0
  18. package/dist/services/AdPlayerController.js +1 -1
  19. package/dist/services/BreakScheduler.d.ts +31 -56
  20. package/dist/services/BreakScheduler.js +1 -1
  21. package/dist/services/BreakSchedulerTypes.d.ts +55 -0
  22. package/dist/services/BreakSchedulerTypes.js +1 -0
  23. package/dist/services/ExternalVendorConfigurationTracker.d.ts +13 -0
  24. package/dist/services/ExternalVendorConfigurationTracker.js +1 -0
  25. package/dist/services/GamStreamManager.d.ts +6 -0
  26. package/dist/services/GamStreamManager.js +1 -1
  27. package/dist/services/ImaMetadataDeduplicator.d.ts +5 -0
  28. package/dist/services/ImaMetadataDeduplicator.js +1 -0
  29. package/dist/services/SsaiController.d.ts +2 -0
  30. package/dist/services/SsaiController.js +1 -1
  31. package/dist/services/adPlayer/AdPlayerEvents.js +1 -1
  32. package/dist/services/adPlayer/AdPlayerState.d.ts +13 -0
  33. package/dist/services/adPlayer/AdPlayerState.js +1 -1
  34. package/dist/services/adPlayer/AdPreloader.js +1 -1
  35. package/dist/services/adPlayer/AdStage.d.ts +7 -3
  36. package/dist/services/adPlayer/AdStage.js +1 -1
  37. package/dist/services/adPlayer/AssetParameterResolver.d.ts +2 -0
  38. package/dist/services/adPlayer/AssetParameterResolver.js +1 -1
  39. package/dist/services/adPlayer/AssetPlayback.js +1 -1
  40. package/dist/services/adPlayer/ContentResumeCoordinator.d.ts +10 -3
  41. package/dist/services/adPlayer/ContentResumeCoordinator.js +1 -1
  42. package/dist/services/adPlayer/GamPodBridge.js +1 -1
  43. package/dist/services/adPlayer/SharedElementBreakPlayer.js +1 -1
  44. package/dist/services/adPlayer/VariantSelector.js +1 -1
  45. package/dist/services/adPlayer/pdtClockResume.d.ts +57 -0
  46. package/dist/services/adPlayer/pdtClockResume.js +1 -0
  47. package/dist/services/assetParameterMacros.d.ts +0 -1
  48. package/dist/services/assetParameterMacros.js +1 -1
  49. package/dist/services/breakTiming.d.ts +2 -0
  50. package/dist/services/breakTiming.js +1 -1
  51. package/dist/services/channelSwitch.d.ts +3 -1
  52. package/dist/services/channelSwitch.js +1 -1
  53. package/dist/services/detectDeviceType.d.ts +7 -2
  54. package/dist/services/detectDeviceType.js +1 -1
  55. package/dist/services/diagnosticReport.d.ts +3 -0
  56. package/dist/services/diagnosticReport.js +1 -0
  57. package/dist/services/externalVendorConfigurationGetter.d.ts +12 -0
  58. package/dist/services/externalVendorConfigurationGetter.js +1 -0
  59. package/dist/services/matchTargeting.d.ts +19 -0
  60. package/dist/services/matchTargeting.js +1 -0
  61. package/dist/services/resolveDeliveryMode.d.ts +3 -1
  62. package/dist/services/resolveDeliveryMode.js +1 -1
  63. package/dist/services/resolveExternalVendorConfigurations.d.ts +20 -0
  64. package/dist/services/resolveExternalVendorConfigurations.js +1 -0
  65. package/dist/services/resolveGamVendorConfiguration.js +1 -1
  66. package/dist/services/resolveResumeClamp.d.ts +42 -0
  67. package/dist/services/resolveResumeClamp.js +1 -0
  68. package/dist/services/resolveTargetedUri.d.ts +2 -1
  69. package/dist/services/resolveTargetedUri.js +1 -1
  70. package/dist/services/selectVariant.d.ts +4 -0
  71. package/dist/services/selectVariant.js +1 -1
  72. package/dist/services/toPlaybackError.d.ts +1 -0
  73. package/dist/services/toPlaybackError.js +1 -0
  74. package/dist/services/wallclockBreakWindow.d.ts +41 -0
  75. package/dist/services/wallclockBreakWindow.js +1 -0
  76. package/dist/session/BreakLifecycle.d.ts +2 -0
  77. package/dist/session/BreakLifecycle.js +1 -1
  78. package/dist/session/SessionStarter.d.ts +6 -1
  79. package/dist/session/SessionStarter.js +1 -1
  80. package/dist/session/SessionState.d.ts +1 -0
  81. package/dist/session/SessionState.js +1 -1
  82. package/dist/types/BreakManifest.d.ts +39 -1
  83. package/dist/types/BreakManifest.js +1 -1
  84. package/dist/types/Events.d.ts +16 -1
  85. package/dist/types/ExternalVendorConfiguration.d.ts +5 -0
  86. package/dist/types/ExternalVendorConfiguration.js +1 -0
  87. package/dist/types/OptiViewAdsConfig.d.ts +2 -1
  88. package/package.json +1 -1
@@ -1 +1 @@
1
- import{BreakFormat as e,isPostRollBreak as t}from"../../types/BreakManifest";import{armPlaybackCutoff as s,clearPlaybackCutoff as a}from"./PlaybackCutoff";import{AdError as n}from"../../types/AdError";import{classifyMediaError as r,toClassifiedAdError as i}from"./classifyMediaError";function d(e){return e?{src:e.getAttribute("src"),srcObject:e.srcObject,sourceChild:null!==e.querySelector("source")}:null}export class SharedElementBreakPlayer{constructor(e,t){this.state=e,this.deps=t,this.contentElementAdPlayer=null,this.sharedAdSource=null,this.sharedAdEndedHandler=null,this.sharedAdTimeUpdateHandler=null,this.sharedAdTimedMetadataHandler=null,this.sharedAdWaitingHandler=null,this.sharedAdPlayingHandler=null,this.sharedAdFirstFrameTimer=null,this.sharedRestoreWatchdog=null,this.sharedRestore=null,this.sharedRestoreGeneration=0,this.sharedActivityGeneration=0}sharedElementAdPlayer(){const e=this.deps.contentPlayer.videoElement;if(!e)return null;if(this.contentElementAdPlayer?.videoElement===e)return this.contentElementAdPlayer;this.releaseContentElementAdPlayer();const t=this.deps.createAdAdapter(document.createElement("div"),e);return t.videoElement!==e?(this.deps.log("shared-element: createAdAdapter ignored the content element; falling back to the content player for ad playback"),t.destroy(),null):(this.contentElementAdPlayer=t,t)}async sharedAdStep(e,t,s){try{await s()}catch(s){const a=t.videoElement,i=s instanceof Error?s.message:String(s),d=new n(`shared-element ${e} failed: ${i} (readyState=${a?.readyState??"-"} paused=${a?.paused??"-"} networkState=${a?.networkState??"-"})`,r(s));throw d.cause=s,d}}clearSharedAdFirstFrameWatchdog(){this.sharedAdFirstFrameTimer&&(clearTimeout(this.sharedAdFirstFrameTimer),this.sharedAdFirstFrameTimer=null)}armSharedAdFirstFrameWatchdog(e,t){this.clearSharedAdFirstFrameWatchdog(),this.sharedAdFirstFrameTimer=setTimeout(()=>{if(this.sharedAdFirstFrameTimer=null,!this.state.isPlaying||!this.state.currentBreak)return;const s=e.videoElement,a=this.deps.contentPlayer.videoElement,r=s?.currentSrc??"",i=`readyState=${s?.readyState??"-"} networkState=${s?.networkState??"-"} paused=${s?.paused??"-"} connected=${s?.isConnected??"-"} srcKind=${r.startsWith("blob:")?"blob":r?"url":"none"} sameElementAsContentPlayer=${String(s===a)}`;this.deps.log(`shared-element: no first frame within ${t}ms (${i})`),this.deps.diagnose("DA-AD-PLAYBACK-ERROR",`the ad never rendered a frame on the content element within ${t}ms`,{breakId:this.state.currentBreak.id,detail:i});const d=this.state.currentAsset;d&&this.state.callbacks?.onAdError(this.state.currentBreak,d,new n(`shared-element ad produced no frames within ${t}ms (${i})`,"DA-AD-START-TIMEOUT")),this.onSharedElementAdEnded()},t)}attachSharedAdListeners(e){this.sharedAdSource=e,this.sharedAdEndedHandler=()=>this.onSharedElementAdEnded(),e.on("ended",this.sharedAdEndedHandler),this.sharedAdTimeUpdateHandler=()=>{this.sharedAdFirstFrameTimer&&e.currentTime>0&&this.clearSharedAdFirstFrameWatchdog(),this.deps.reportAdProgress(e)},e.on("timeupdate",this.sharedAdTimeUpdateHandler),this.sharedAdTimedMetadataHandler=e=>{e&&this.deps.processTimedMetadata(e)},e.on("timedmetadata",this.sharedAdTimedMetadataHandler),this.sharedAdWaitingHandler=()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&this.state.callbacks?.onAdWaiting(this.state.currentBreak,this.state.currentAsset)},e.on("waiting",this.sharedAdWaitingHandler),this.sharedAdPlayingHandler=()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&!e.paused&&(this.clearSharedAdFirstFrameWatchdog(),this.state.startPlaybackCutoff?.(),this.state.callbacks?.onAdPlaying(this.state.currentBreak,this.state.currentAsset))},e.on("playing",this.sharedAdPlayingHandler)}detachSharedAdListeners(){const e=this.sharedAdSource??this.deps.contentPlayer;this.sharedAdSource=null,this.state.sharedAdClaimActive=!1,this.clearSharedAdFirstFrameWatchdog(),this.sharedAdTimedMetadataHandler&&(e.off("timedmetadata",this.sharedAdTimedMetadataHandler),this.sharedAdTimedMetadataHandler=null),this.sharedAdWaitingHandler&&(e.off("waiting",this.sharedAdWaitingHandler),this.sharedAdWaitingHandler=null),this.sharedAdPlayingHandler&&(e.off("playing",this.sharedAdPlayingHandler),this.sharedAdPlayingHandler=null),this.sharedAdTimeUpdateHandler&&(e.off("timeupdate",this.sharedAdTimeUpdateHandler),this.sharedAdTimeUpdateHandler=null),this.sharedAdEndedHandler&&(e.off("ended",this.sharedAdEndedHandler),this.sharedAdEndedHandler=null)}async releaseContentElementFromContentEngine(){if(this.deps.contentPlayer.releaseMediaElement)try{await this.deps.contentPlayer.releaseMediaElement()}catch(e){this.deps.log("shared-element: the content engine did not release the element:",e)}}releaseContentElementAdPlayer(){const e=this.contentElementAdPlayer;if(e){this.contentElementAdPlayer=null;try{e.destroy()}catch(e){this.deps.log("shared-element: releasing the ad player on the content element failed:",e)}}}async playAdThroughContentElement(){const e=this.state.currentAsset,t=this.state.currentBreak;if(!e||!t)return;const s=this.state.effectiveBreakDurationSec??t.duration,a=await this.deps.resolveAssetUri(e,s);if(!a)return void this.deps.log("shared-element: no URI for the running ad; leaving it on the ad element");const n=this.deps.getAdPlayer()?.currentTime??0;this.state.savedContentSrc=this.restorableContentSource(),this.state.savedContentTime=this.deps.contentPlayer.currentTime,this.state.savedContentLive=!Number.isFinite(this.deps.contentPlayer.duration),this.state.savedContentEdgeDistance=this.state.savedContentLive?this.deps.liveEdgeDistanceSec():null,this.state.sharedAdClaimActive=!0,await this.releaseContentElementFromContentEngine(),this.deps.rebindContentElementIfSwapped();const r=this.sharedElementAdPlayer()??this.deps.contentPlayer;try{this.deps.getAdPlayer()?.pause(),this.deps.clearContentGate(),await r.load(a),this.attachSharedAdListeners(r),n>0&&r.seek(n),await r.play(),this.deps.hide(),this.deps.log("shared-element: the ad now plays through the content element")}catch(e){this.deps.log("shared-element: could not move the ad onto the content element:",e),this.state.sharedAdClaimActive=!1,this.state.savedContentSrc=null,this.state.savedContentEdgeDistance=null}}async playSharedElementBreak(t,a){const r=this.deps.getVariant(t);if(!r)return this.deps.log("shared-element: no valid variant, skipping break:",t.id),void a.onBreakEnd(t);if(r.format===e.LSHAPE_CONTENT)return this.deps.log("shared-element: skipping lshape_content break (no fullscreen video asset):",t.id),void a.onBreakEnd(t);const d=this.deps.getAssets(t)[0];if(!d)return this.deps.log("shared-element: no asset to play, skipping break:",t.id),void a.onBreakEnd(t);r.format!==e.SINGLE&&this.deps.log(`shared-element: downgrading ${r.format} to a single fullscreen ad:`,t.id),this.state.currentBreak=t,this.state.currentAsset=d,this.state.callbacks=a,this.state.isPlaying=!0;const h=this.state.effectiveBreakDurationSec??t.duration,l=await this.deps.resolveAssetUri(d,h);if(!l)return this.deps.log("shared-element: no valid URI, skipping break:",t.id),this.state.isPlaying=!1,this.state.currentBreak=null,this.state.currentAsset=null,void a.onBreakEnd(t);this.state.savedContentSrc=this.restorableContentSource(),this.state.savedContentTime=this.deps.contentPlayer.currentTime,this.state.savedContentLive=!Number.isFinite(this.deps.contentPlayer.duration),this.state.savedContentEdgeDistance=this.state.savedContentLive?this.deps.liveEdgeDistanceSec():null,this.state.sharedAdClaimActive=!0,await this.releaseContentElementFromContentEngine(),this.deps.rebindContentElementIfSwapped();const o=this.sharedElementAdPlayer()??this.deps.contentPlayer;if(h&&h>0){const e=h+this.state.adBreakCutSafetyMarginSec;s(this.state,e,()=>{this.deps.log("shared-element: break-cut timer expired, ending break"),this.state.isPlaying&&this.state.currentBreak&&this.state.callbacks?.onBreakCut?.(this.state.currentBreak),this.onSharedElementAdEnded()},()=>{this.state.callbacks?.onAdError(t,d,new n("Ad playback startup timed out","DA-AD-START-TIMEOUT")),this.onSharedElementAdEnded()})}try{if(this.deps.clearContentGate(),await this.sharedAdStep("load",o,()=>o.load(l)),!this.state.isPlaying||this.state.currentBreak!==t)return;if(this.attachSharedAdListeners(o),this.armSharedAdFirstFrameWatchdog(o,Math.max(Math.min(1e3*(h??0)-100,5e3),3e3)),await this.sharedAdStep("play",o,()=>o.play()),!this.state.isPlaying||this.state.currentBreak!==t)return;this.state.callbacks?.onAdStart(t,d,0,1)}catch(e){if(!this.state.isPlaying||this.state.currentBreak!==t)return;this.deps.log("shared-element: ad playback failed:",e),this.state.callbacks?.onAdError(t,d,i(e)),this.onSharedElementAdEnded()}}onSharedElementAdEnded(){if(!this.state.isPlaying||!this.state.currentBreak)return;const e=this.state.currentBreak,t=this.state.currentAsset,s=this.state.callbacks,n=this.sharedRestoreGeneration;this.detachSharedAdListeners(),a(this.state),this.state.isPlaying=!1,this.state.currentBreak=null,this.state.currentAsset=null,t&&s?.onAdEnd(e,t,0,1),n===this.sharedRestoreGeneration&&this.restoreSharedElementContent(e),s?.onBreakEnd(e)}restoreSharedElementContent(e){const s=this.state.savedContentSrc;if(!s){if(this.sharedRestore)return;return this.deps.log("shared-element: no saved content source to restore"),void this.releaseContentElementAdPlayer()}if(!this.beginSharedActivity())return;const a=this.state.savedContentTime,n=this.state.savedContentLive,r=this.state.savedContentEdgeDistance,i=this.state.callbacks;this.state.savedContentSrc=null,this.state.savedContentEdgeDistance=null;const d={src:s,time:a,live:n,edgeDistance:r,breakInfo:e,postRoll:!!e&&t(e),onContentReloading:i?.onContentReloading?.bind(i),adPlayer:this.contentElementAdPlayer,reloading:!1,cleanedUp:!1,cancelled:!1,finishingActivity:null,attempt:null};this.sharedRestore=d,e&&"number"==typeof e.start&&i?.onContentResumeSeek?.(n?this.deps.contentPlayer.currentTime:a,e.start+e.duration),this.sharedRestore===d&&(d.reloading=!0,d.onContentReloading?.(!0),this.sharedRestore===d&&this.runSharedRestore(d))}async runSharedRestore(e){for(let t=1;t<=2;t++){const s={phase:"load",deadline:0,timer:null,settle:null};e.attempt=s;const a=await new Promise(t=>{s.settle=e=>{s.settle&&(s.settle=null,s.timer&&clearTimeout(s.timer),s.timer=null,t(e))},this.armSharedRestoreDeadline(s,1e4),this.reloadAndResume(e,s).then(()=>s.settle?.("success"),()=>s.settle?.("rejection"))});if(this.sharedRestore!==e||e.attempt!==s)return;if("success"===a||"cancelled"===a)break;if(2===t?this.deps.diagnose("DA-SHARED-ELEMENT-RESTORE-FAILED",`Content did not resume after the shared-element break: ${s.phase} ${a}`,{breakId:e.breakInfo?.id,live:e.live,attempts:t,phase:s.phase,reason:a}):this.deps.log(`shared-element: content restore ${s.phase} ${a}, retrying once`),this.sharedRestore!==e||e.attempt!==s)return}this.finishSharedRestore(e,!0)}armSharedRestoreDeadline(e,t){e.timer&&clearTimeout(e.timer),e.deadline=performance.now()+t,e.timer=setTimeout(()=>e.settle?.("timeout"),t)}ownsSharedRestoreAttempt(e,t){return!(this.sharedRestore!==e||e.attempt!==t||!t.settle)&&(performance.now()<t.deadline||(t.settle("timeout"),!1))}async reloadAndResume(e,t){const{src:s,time:a,live:n,edgeDistance:r,postRoll:i}=e,h=()=>this.ownsSharedRestoreAttempt(e,t);if(!h())return;if(e.adPlayer?.releaseMediaElement&&(await this.releaseAdEngineGrip(e),!h()))return;const l=this.deps.contentPlayer.videoElement??null,o=d(l);if(await this.deps.contentPlayer.load(s),h()){if(this.armSharedRestoreDeadline(t,5e3),n){if(!i&&null!==r&&r>0){t.phase="live-edge";const e=await this.deps.waitForLiveEdge(h);if(!h())return;if(null!==e){const s=Math.max(e-r,0);if(this.deps.log(`shared-element: live resume ${r.toFixed(2)}s behind the edge (${s.toFixed(2)}s)`),!h())return;t.phase="seek",this.deps.contentPlayer.seek(s)}}}else t.phase="seek",this.deps.contentPlayer.seek(a);h()&&!i&&(t.phase="play",await this.deps.contentPlayer.play(),h()&&await this.waitForContentEngineToTakeElement(l,o,h))}}async releaseAdEngineGrip(e){const t=e.adPlayer;if(t?.releaseMediaElement)try{await t.releaseMediaElement()}catch(e){this.deps.log("shared-element: the ad engine did not release the element:",e)}}async waitForContentEngineToTakeElement(e,t,s){if(!(e&&t&&(a=t,a.src||a.srcObject||a.sourceChild)))return;var a;const n=()=>{if(this.deps.contentPlayer.videoElement!==e)return!0;const s=d(e);if(!s)return!1;const a=!!s.src&&s.src!==t.src,n=!!s.srcObject&&s.srcObject!==t.srcObject;return a||n},r=performance.now();for(;!n()&&s()&&performance.now()-r<1500;)await new Promise(e=>setTimeout(e,25));if(!s())return;const i=Math.round(performance.now()-r);this.deps.log(`shared-element: content engine ${n()?"took":"did not take"} the element after ${i}ms`)}finishSharedRestore(e,t){this.sharedRestore===e&&(null===e.finishingActivity?(e.finishingActivity=this.sharedActivityGeneration,e.cleanedUp||(this.state.contentPaused=!1,this.contentElementAdPlayer===e.adPlayer&&this.releaseContentElementAdPlayer(),this.sharedRestore===e&&this.deps.clearContentGate(),this.sharedRestore===e&&this.deps.rebindContentElementIfSwapped(),e.cleanedUp=!0),this.endSharedRestoreReloading(e),e.finishingActivity=null,this.sharedRestore===e&&(!t||e.cancelled||e.postRoll?this.sharedRestore=null:this.armSharedRestoreWatchdog(e))):e.finishingActivity!==this.sharedActivityGeneration&&(this.endSharedRestoreReloading(e),this.sharedRestore===e&&(this.sharedRestore=null)))}endSharedRestoreReloading(e){e.reloading&&(e.reloading=!1,e.onContentReloading?.(!1))}armSharedRestoreWatchdog(e){const t=this.deps.contentPlayer.currentTime;this.sharedRestoreWatchdog=setTimeout(()=>{if(this.sharedRestore!==e)return;this.sharedRestoreWatchdog=null;const s=this.deps.contentPlayer.currentTime-t;(this.deps.contentPlayer.paused||s<1)&&(this.deps.diagnose("DA-CONTENT-RESUME-STALLED",`Content did not resume within 5s of the shared-element restore (paused: ${this.deps.contentPlayer.paused}, advanced: ${s.toFixed(1)}s); nudging play()`,{breakId:e.breakInfo?.id??"?",paused:this.deps.contentPlayer.paused,advancedSec:s}),this.sharedRestore===e&&this.deps.contentPlayer.play().catch(()=>{this.sharedRestore===e&&this.deps.log("shared-element: watchdog play() nudge failed")}))},5e3)}beginSharedActivity(){return this.sharedActivityGeneration++,this.cancelSharedRestore()}cancelSharedRestore(){const e=this.sharedActivityGeneration;this.sharedRestoreGeneration++,this.sharedRestoreWatchdog&&(clearTimeout(this.sharedRestoreWatchdog),this.sharedRestoreWatchdog=null);const t=this.sharedRestore;return t&&(t.cancelled=!0,t.attempt?.settle?.("cancelled"),this.finishSharedRestore(t,!1)),e===this.sharedActivityGeneration}restorableContentSource(){const e=this.deps.contentPlayer.sourceUrl;if(e&&!e.startsWith("blob:"))return e;const t=this.deps.contentPlayer.videoElement,s=t?.currentSrc||t?.src||"";return s.length>0&&!s.startsWith("blob:")?s:null}}
1
+ import{BreakFormat as e,isPostRollBreak as t}from"../../types/BreakManifest";import{armPlaybackCutoff as s,clearPlaybackCutoff as a}from"./PlaybackCutoff";import{AdError as n}from"../../types/AdError";import{classifyMediaError as r,toClassifiedAdError as i}from"./classifyMediaError";function d(e){return e?{src:e.getAttribute("src"),srcObject:e.srcObject,sourceChild:null!==e.querySelector("source")}:null}export class SharedElementBreakPlayer{constructor(e,t){this.state=e,this.deps=t,this.contentElementAdPlayer=null,this.sharedAdSource=null,this.sharedAdEndedHandler=null,this.sharedAdTimeUpdateHandler=null,this.sharedAdTimedMetadataHandler=null,this.sharedAdWaitingHandler=null,this.sharedAdPlayingHandler=null,this.sharedAdFirstFrameTimer=null,this.sharedRestoreWatchdog=null,this.sharedRestore=null,this.sharedRestoreGeneration=0,this.sharedActivityGeneration=0}sharedElementAdPlayer(){const e=this.deps.contentPlayer.videoElement;if(!e)return null;if(this.contentElementAdPlayer?.videoElement===e)return this.contentElementAdPlayer;this.releaseContentElementAdPlayer();const t=this.deps.createAdAdapter(document.createElement("div"),e);return t!==this.deps.contentPlayer&&t.prepareForAdPlayback?.(),t.videoElement!==e?(this.deps.log("shared-element: createAdAdapter ignored the content element; falling back to the content player for ad playback"),t.destroy(),null):(this.contentElementAdPlayer=t,t)}async sharedAdStep(e,t,s){try{await s()}catch(s){const a=t.videoElement,i=s instanceof Error?s.message:String(s),d=new n(`shared-element ${e} failed: ${i} (readyState=${a?.readyState??"-"} paused=${a?.paused??"-"} networkState=${a?.networkState??"-"})`,r(s));throw d.cause=s,d}}clearSharedAdFirstFrameWatchdog(){this.sharedAdFirstFrameTimer&&(clearTimeout(this.sharedAdFirstFrameTimer),this.sharedAdFirstFrameTimer=null)}armSharedAdFirstFrameWatchdog(e,t){this.clearSharedAdFirstFrameWatchdog(),this.sharedAdFirstFrameTimer=setTimeout(()=>{if(this.sharedAdFirstFrameTimer=null,!this.state.isPlaying||!this.state.currentBreak)return;const s=e.videoElement,a=this.deps.contentPlayer.videoElement,r=s?.currentSrc??"",i=`readyState=${s?.readyState??"-"} networkState=${s?.networkState??"-"} paused=${s?.paused??"-"} connected=${s?.isConnected??"-"} srcKind=${r.startsWith("blob:")?"blob":r?"url":"none"} sameElementAsContentPlayer=${String(s===a)}`;this.deps.log(`shared-element: no first frame within ${t}ms (${i})`),this.deps.diagnose("DA-AD-PLAYBACK-ERROR",`the ad never rendered a frame on the content element within ${t}ms`,{breakId:this.state.currentBreak.id,detail:i});const d=this.state.currentAsset;d&&this.state.callbacks?.onAdError(this.state.currentBreak,d,new n(`shared-element ad produced no frames within ${t}ms (${i})`,"DA-AD-START-TIMEOUT")),this.onSharedElementAdEnded()},t)}attachSharedAdListeners(e){this.sharedAdSource=e,this.sharedAdEndedHandler=()=>this.onSharedElementAdEnded(),e.on("ended",this.sharedAdEndedHandler),this.sharedAdTimeUpdateHandler=()=>{this.sharedAdFirstFrameTimer&&e.currentTime>0&&this.clearSharedAdFirstFrameWatchdog(),this.deps.reportAdProgress(e)},e.on("timeupdate",this.sharedAdTimeUpdateHandler),this.sharedAdTimedMetadataHandler=e=>{e&&this.deps.processTimedMetadata(e)},e.on("timedmetadata",this.sharedAdTimedMetadataHandler),this.sharedAdWaitingHandler=()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&this.state.callbacks?.onAdWaiting(this.state.currentBreak,this.state.currentAsset)},e.on("waiting",this.sharedAdWaitingHandler),this.sharedAdPlayingHandler=()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&!e.paused&&(this.clearSharedAdFirstFrameWatchdog(),this.state.startPlaybackCutoff?.(),this.state.callbacks?.onAdPlaying(this.state.currentBreak,this.state.currentAsset))},e.on("playing",this.sharedAdPlayingHandler)}detachSharedAdListeners(){const e=this.sharedAdSource??this.deps.contentPlayer;this.sharedAdSource=null,this.state.sharedAdClaimActive=!1,this.clearSharedAdFirstFrameWatchdog(),this.sharedAdTimedMetadataHandler&&(e.off("timedmetadata",this.sharedAdTimedMetadataHandler),this.sharedAdTimedMetadataHandler=null),this.sharedAdWaitingHandler&&(e.off("waiting",this.sharedAdWaitingHandler),this.sharedAdWaitingHandler=null),this.sharedAdPlayingHandler&&(e.off("playing",this.sharedAdPlayingHandler),this.sharedAdPlayingHandler=null),this.sharedAdTimeUpdateHandler&&(e.off("timeupdate",this.sharedAdTimeUpdateHandler),this.sharedAdTimeUpdateHandler=null),this.sharedAdEndedHandler&&(e.off("ended",this.sharedAdEndedHandler),this.sharedAdEndedHandler=null)}async releaseContentElementFromContentEngine(){if(this.deps.contentPlayer.releaseMediaElement)try{await this.deps.contentPlayer.releaseMediaElement()}catch(e){this.deps.log("shared-element: the content engine did not release the element:",e)}}releaseContentElementAdPlayer(){const e=this.contentElementAdPlayer;if(e){this.contentElementAdPlayer=null;try{e.destroy()}catch(e){this.deps.log("shared-element: releasing the ad player on the content element failed:",e)}}}async playAdThroughContentElement(){const e=this.state.currentAsset,t=this.state.currentBreak;if(!e||!t)return;const s=this.state.effectiveBreakDurationSec??t.duration,a=await this.deps.resolveAssetUri(e,s);if(!a)return void this.deps.log("shared-element: no URI for the running ad; leaving it on the ad element");const n=this.deps.getAdPlayer()?.currentTime??0;this.state.savedContentSrc=this.restorableContentSource(),this.state.savedContentTime=this.deps.contentPlayer.currentTime,this.state.savedContentLive=!Number.isFinite(this.deps.contentPlayer.duration),this.state.savedContentEdgeDistance=this.state.savedContentLive?this.deps.liveEdgeDistanceSec():null,this.state.sharedAdClaimActive=!0,await this.releaseContentElementFromContentEngine(),this.deps.rebindContentElementIfSwapped();const r=this.sharedElementAdPlayer()??this.deps.contentPlayer;try{this.deps.getAdPlayer()?.pause(),this.deps.clearContentGate(),await r.load(a),this.attachSharedAdListeners(r),n>0&&r.seek(n),await r.play(),this.deps.hide(),this.deps.log("shared-element: the ad now plays through the content element")}catch(e){this.deps.log("shared-element: could not move the ad onto the content element:",e),this.state.sharedAdClaimActive=!1,this.state.savedContentSrc=null,this.state.savedContentEdgeDistance=null}}async playSharedElementBreak(t,a){const r=this.deps.getVariant(t);if(!r)return this.deps.log("shared-element: no valid variant, skipping break:",t.id),void a.onBreakEnd(t);if(r.format===e.LSHAPE_CONTENT)return this.deps.log("shared-element: skipping lshape_content break (no fullscreen video asset):",t.id),void a.onBreakEnd(t);const d=this.deps.getAssets(t)[0];if(!d)return this.deps.log("shared-element: no asset to play, skipping break:",t.id),void a.onBreakEnd(t);r.format!==e.SINGLE&&this.deps.log(`shared-element: downgrading ${r.format} to a single fullscreen ad:`,t.id),this.state.currentBreak=t,this.state.currentAsset=d,this.state.callbacks=a,this.state.isPlaying=!0;const h=this.state.effectiveBreakDurationSec??t.duration,l=await this.deps.resolveAssetUri(d,h);if(!l)return this.deps.log("shared-element: no valid URI, skipping break:",t.id),this.state.isPlaying=!1,this.state.currentBreak=null,this.state.currentAsset=null,void a.onBreakEnd(t);this.state.savedContentSrc=this.restorableContentSource(),this.state.savedContentTime=this.deps.contentPlayer.currentTime,this.state.savedContentLive=!Number.isFinite(this.deps.contentPlayer.duration),this.state.savedContentEdgeDistance=this.state.savedContentLive?this.deps.liveEdgeDistanceSec():null,this.state.sharedAdClaimActive=!0,await this.releaseContentElementFromContentEngine(),this.deps.rebindContentElementIfSwapped();const o=this.sharedElementAdPlayer()??this.deps.contentPlayer;if(h&&h>0){const e=h+this.state.adBreakCutSafetyMarginSec;s(this.state,e,()=>{this.deps.log("shared-element: break-cut timer expired, ending break"),this.state.isPlaying&&this.state.currentBreak&&this.state.callbacks?.onBreakCut?.(this.state.currentBreak),this.onSharedElementAdEnded()},()=>{this.state.callbacks?.onAdError(t,d,new n("Ad playback startup timed out","DA-AD-START-TIMEOUT")),this.onSharedElementAdEnded()})}try{if(this.deps.clearContentGate(),await this.sharedAdStep("load",o,()=>o.load(l)),!this.state.isPlaying||this.state.currentBreak!==t)return;if(this.attachSharedAdListeners(o),this.armSharedAdFirstFrameWatchdog(o,Math.max(Math.min(1e3*(h??0)-100,5e3),3e3)),await this.sharedAdStep("play",o,()=>o.play()),!this.state.isPlaying||this.state.currentBreak!==t)return;this.state.callbacks?.onAdStart(t,d,0,1)}catch(e){if(!this.state.isPlaying||this.state.currentBreak!==t)return;this.deps.log("shared-element: ad playback failed:",e),this.state.callbacks?.onAdError(t,d,i(e)),this.onSharedElementAdEnded()}}onSharedElementAdEnded(){if(!this.state.isPlaying||!this.state.currentBreak)return;const e=this.state.currentBreak,t=this.state.currentAsset,s=this.state.callbacks,n=this.sharedRestoreGeneration;this.detachSharedAdListeners(),a(this.state),this.state.isPlaying=!1,this.state.currentBreak=null,this.state.currentAsset=null,t&&s?.onAdEnd(e,t,0,1),n===this.sharedRestoreGeneration&&this.restoreSharedElementContent(e),s?.onBreakEnd(e)}restoreSharedElementContent(e){const s=this.state.savedContentSrc;if(!s){if(this.sharedRestore)return;return this.deps.log("shared-element: no saved content source to restore"),void this.releaseContentElementAdPlayer()}if(!this.beginSharedActivity())return;const a=this.state.savedContentTime,n=this.state.savedContentLive,r=this.state.savedContentEdgeDistance,i=this.state.callbacks;this.state.savedContentSrc=null,this.state.savedContentEdgeDistance=null;const d={src:s,time:a,live:n,edgeDistance:r,breakInfo:e,postRoll:!!e&&t(e),onContentReloading:i?.onContentReloading?.bind(i),adPlayer:this.contentElementAdPlayer,reloading:!1,cleanedUp:!1,cancelled:!1,finishingActivity:null,attempt:null};this.sharedRestore=d,e&&"number"==typeof e.start&&i?.onContentResumeSeek?.(n?this.deps.contentPlayer.currentTime:a,e.start+e.duration),this.sharedRestore===d&&(d.reloading=!0,d.onContentReloading?.(!0),this.sharedRestore===d&&this.runSharedRestore(d))}async runSharedRestore(e){for(let t=1;t<=2;t++){const s={phase:"load",deadline:0,timer:null,settle:null};e.attempt=s;const a=await new Promise(t=>{s.settle=e=>{s.settle&&(s.settle=null,s.timer&&clearTimeout(s.timer),s.timer=null,t(e))},this.armSharedRestoreDeadline(s,1e4),this.reloadAndResume(e,s).then(()=>s.settle?.("success"),()=>s.settle?.("rejection"))});if(this.sharedRestore!==e||e.attempt!==s)return;if("success"===a||"cancelled"===a)break;if(2===t?this.deps.diagnose("DA-SHARED-ELEMENT-RESTORE-FAILED",`Content did not resume after the shared-element break: ${s.phase} ${a}`,{breakId:e.breakInfo?.id,live:e.live,attempts:t,phase:s.phase,reason:a}):this.deps.log(`shared-element: content restore ${s.phase} ${a}, retrying once`),this.sharedRestore!==e||e.attempt!==s)return}this.finishSharedRestore(e,!0)}armSharedRestoreDeadline(e,t){e.timer&&clearTimeout(e.timer),e.deadline=performance.now()+t,e.timer=setTimeout(()=>e.settle?.("timeout"),t)}ownsSharedRestoreAttempt(e,t){return!(this.sharedRestore!==e||e.attempt!==t||!t.settle)&&(performance.now()<t.deadline||(t.settle("timeout"),!1))}async reloadAndResume(e,t){const{src:s,time:a,live:n,edgeDistance:r,postRoll:i}=e,h=()=>this.ownsSharedRestoreAttempt(e,t);if(!h())return;if(e.adPlayer?.releaseMediaElement&&(await this.releaseAdEngineGrip(e),!h()))return;const l=this.deps.contentPlayer.videoElement??null,o=d(l);if(await this.deps.contentPlayer.load(s),h()){if(this.armSharedRestoreDeadline(t,5e3),n){if(!i&&null!==r&&r>0){t.phase="live-edge";const e=await this.deps.waitForLiveEdge(h);if(!h())return;if(null!==e){const s=Math.max(e-r,0);if(this.deps.log(`shared-element: live resume ${r.toFixed(2)}s behind the edge (${s.toFixed(2)}s)`),!h())return;t.phase="seek",this.deps.contentPlayer.seek(s)}}}else t.phase="seek",this.deps.contentPlayer.seek(a);h()&&!i&&(t.phase="play",await this.deps.contentPlayer.play(),h()&&await this.waitForContentEngineToTakeElement(l,o,h))}}async releaseAdEngineGrip(e){const t=e.adPlayer;if(t?.releaseMediaElement)try{await t.releaseMediaElement()}catch(e){this.deps.log("shared-element: the ad engine did not release the element:",e)}}async waitForContentEngineToTakeElement(e,t,s){if(!(e&&t&&(a=t,a.src||a.srcObject||a.sourceChild)))return;var a;const n=()=>{if(this.deps.contentPlayer.videoElement!==e)return!0;const s=d(e);if(!s)return!1;const a=!!s.src&&s.src!==t.src,n=!!s.srcObject&&s.srcObject!==t.srcObject;return a||n},r=performance.now();for(;!n()&&s()&&performance.now()-r<1500;)await new Promise(e=>setTimeout(e,25));if(!s())return;const i=Math.round(performance.now()-r);this.deps.log(`shared-element: content engine ${n()?"took":"did not take"} the element after ${i}ms`)}finishSharedRestore(e,t){this.sharedRestore===e&&(null===e.finishingActivity?(e.finishingActivity=this.sharedActivityGeneration,e.cleanedUp||(this.state.contentPaused=!1,this.contentElementAdPlayer===e.adPlayer&&this.releaseContentElementAdPlayer(),this.sharedRestore===e&&this.deps.clearContentGate(),this.sharedRestore===e&&this.deps.rebindContentElementIfSwapped(),e.cleanedUp=!0),this.endSharedRestoreReloading(e),e.finishingActivity=null,this.sharedRestore===e&&(!t||e.cancelled||e.postRoll?this.sharedRestore=null:this.armSharedRestoreWatchdog(e))):e.finishingActivity!==this.sharedActivityGeneration&&(this.endSharedRestoreReloading(e),this.sharedRestore===e&&(this.sharedRestore=null)))}endSharedRestoreReloading(e){e.reloading&&(e.reloading=!1,e.onContentReloading?.(!1))}armSharedRestoreWatchdog(e){const t=this.deps.contentPlayer.currentTime;this.sharedRestoreWatchdog=setTimeout(()=>{if(this.sharedRestore!==e)return;this.sharedRestoreWatchdog=null;const s=this.deps.contentPlayer.currentTime-t;(this.deps.contentPlayer.paused||s<1)&&(this.deps.diagnose("DA-CONTENT-RESUME-STALLED",`Content did not resume within 5s of the shared-element restore (paused: ${this.deps.contentPlayer.paused}, advanced: ${s.toFixed(1)}s); nudging play()`,{breakId:e.breakInfo?.id??"?",paused:this.deps.contentPlayer.paused,advancedSec:s}),this.sharedRestore===e&&this.deps.contentPlayer.play().catch(()=>{this.sharedRestore===e&&this.deps.log("shared-element: watchdog play() nudge failed")}))},5e3)}beginSharedActivity(){return this.sharedActivityGeneration++,this.cancelSharedRestore()}cancelSharedRestore(){const e=this.sharedActivityGeneration;this.sharedRestoreGeneration++,this.sharedRestoreWatchdog&&(clearTimeout(this.sharedRestoreWatchdog),this.sharedRestoreWatchdog=null);const t=this.sharedRestore;return t&&(t.cancelled=!0,t.attempt?.settle?.("cancelled"),this.finishSharedRestore(t,!1)),e===this.sharedActivityGeneration}restorableContentSource(){const e=this.deps.contentPlayer.sourceUrl;if(e&&!e.startsWith("blob:"))return e;const t=this.deps.contentPlayer.videoElement,s=t?.currentSrc||t?.src||"";return s.length>0&&!s.startsWith("blob:")?s:null}}
@@ -1 +1 @@
1
- import{BreakFormat as e,Vendor as t,isVendorAsset as s}from"../../types/BreakManifest";import{detectDeviceType as n}from"../detectDeviceType";import{assetsOfVariant as i,breakPlanEmptyReason as r,extractAssets as o}from"../AdBreakSequencer";import{playableFormatsFor as a,selectVariantDetailed as c}from"../selectVariant";export class VariantSelector{constructor(e,t){this.state=e,this.deps=t}continueContentDuringBreakReason(t){return this.state.continueContentDuringBreak?"replacement"!==this.state.adInsertionType?"not-replacement":t!==e.SINGLE?"format":"shared-element"===this.deps.getEffectiveInsertionMode()?"shared-element":this.deps.getIsContentPictureInPicture()?"picture-in-picture":"single-decoder"===this.deps.preloadMode||!1===this.deps.getSupportsParallelBuffering()?"single-decoder":"ok":"disabled"}selectionInputs(){return void 0===this.state.detectedDeviceType&&(this.state.detectedDeviceType=n()),{deviceType:this.state.detectedDeviceType,playableFormats:a({pictureInPicture:this.deps.getIsContentPictureInPicture(),singleSurfaceDevice:this.deps.singleSurfaceDevice})}}breakNothingToPlayReason(e){const t=this.deps.getEffectiveInsertionMode(),s=this.selectionInputs(),n=r(e,t,s);return null!==n?n:this.onlyGamAssetsWithoutSession(e,t,s)?"no-assets":null}onlyGamAssetsWithoutSession(e,n,i){if(this.deps.getIsGamReady()||null!==this.state.gamInitPromise)return!1;const r=o(e,i),a="shared-element"===n?r.slice(0,1):r;return a.length>0&&a.every(e=>s(e)&&e.vendor===t.GAM)}unknownVariantFormatsOf(e){return c(e,this.selectionInputs()).skippedUnknownFormats}diagnoseUnknownVariantFormats(e){for(const t of this.unknownVariantFormatsOf(e))this.deps.diagnose("DA-BREAK-VARIANT-FORMAT-UNKNOWN",`break ${e.id}: variant format ${""===t?"(absent)":`"${t}"`} is not recognised; the variant was skipped.`,{breakId:e.id,format:t})}getVariant(t){const{selected:s,deviceQualified:n}=c(t,this.selectionInputs());return s||(n?{format:e.SINGLE,assets:i(n).slice(0,1)}:null)}}
1
+ import{BreakFormat as e,Vendor as t,isVendorAsset as s}from"../../types/BreakManifest";import{detectDeviceProfile as i}from"../detectDeviceType";import{assetsOfVariant as n,breakPlanEmptyReason as r,extractAssets as o}from"../AdBreakSequencer";import{playableFormatsFor as a,selectVariantDetailed as d}from"../selectVariant";export class VariantSelector{constructor(e,t){this.state=e,this.deps=t}continueContentDuringBreakReason(t){return this.state.continueContentDuringBreak?"replacement"!==this.state.adInsertionType?"not-replacement":t!==e.SINGLE?"format":"shared-element"===this.deps.getEffectiveInsertionMode()?"shared-element":this.deps.getIsContentPictureInPicture()?"picture-in-picture":"single-decoder"===this.deps.preloadMode||!1===this.deps.getSupportsParallelBuffering()?"single-decoder":"ok":"disabled"}selectionInputs(){if(void 0===this.state.detectedDeviceProfile){const e=i();void 0!==this.state.detectedDeviceType?e.deviceType=this.state.detectedDeviceType:this.state.detectedDeviceType=e.deviceType,this.state.detectedDeviceProfile=e}return{deviceType:this.state.detectedDeviceType,deviceProfile:this.state.detectedDeviceProfile,playableFormats:a({pictureInPicture:this.deps.getIsContentPictureInPicture(),singleSurfaceDevice:this.deps.singleSurfaceDevice})}}breakNothingToPlayReason(e){const t=this.deps.getEffectiveInsertionMode(),s=this.selectionInputs(),i=r(e,t,s);return null!==i?i:this.onlyGamAssetsWithoutSession(e,t,s)?"no-assets":null}onlyGamAssetsWithoutSession(e,i,n){if(this.deps.getIsGamReady()||null!==this.state.gamInitPromise)return!1;const r=o(e,n),a="shared-element"===i?r.slice(0,1):r;return a.length>0&&a.every(e=>s(e)&&e.vendor===t.GAM)}unknownVariantFormatsOf(e){return d(e,this.selectionInputs()).skippedUnknownFormats}diagnoseUnknownVariantFormats(e){for(const t of this.unknownVariantFormatsOf(e))this.deps.diagnose("DA-BREAK-VARIANT-FORMAT-UNKNOWN",`break ${e.id}: variant format ${""===t?"(absent)":`"${t}"`} is not recognised; the variant was skipped.`,{breakId:e.id,format:t})}getVariant(t){const{selected:s,deviceQualified:i,targetingIssues:r}=d(t,this.selectionInputs());for(const e of r)this.deps.diagnose(e.code,`Targeting ${e.field} was rejected.`,{field:e.field,...void 0===e.value?{}:{value:e.value}});return s||(i?{format:e.SINGLE,assets:n(i).slice(0,1)}:null)}}
@@ -0,0 +1,57 @@
1
+ import type { PlayerAdapter } from '../../interfaces/PlayerAdapter';
2
+ import type { Break } from '../../types/BreakManifest';
3
+ import { type ProgramDateTimeSample } from '../wallclockBreakWindow';
4
+ /** The content player's current position with the stream's Program Date Time at it, or null without one. */
5
+ export declare function programDateTimeSample(player: PlayerAdapter): ProgramDateTimeSample | null;
6
+ /** The manifest `start + duration` of a wallclock break, epoch milliseconds; null off the wallclock timebase. */
7
+ export declare function wallclockBreakEndMs(breakInfo: Break): number | null;
8
+ export interface PdtClockResumeDeps {
9
+ contentPlayer: PlayerAdapter;
10
+ diagnose: (code: string, message: string, context?: Record<string, unknown>) => void;
11
+ /** Whether the break's anchor was placed through the stream's Program Date Time. */
12
+ anchorIsPdt: () => boolean;
13
+ /** The policy's resume point on the currentTime timeline, ignoring the stream. */
14
+ computedTarget: (breakInfo: Break) => number | null;
15
+ /** Replacement resumes at the break end, the only policy the manifest window can serve. */
16
+ resumesAtBreakEnd: (breakInfo: Break) => boolean;
17
+ /** Wall clock in milliseconds. */
18
+ now: () => number;
19
+ }
20
+ /**
21
+ * Live resume on the stream's Program Date Time (`timebase: 'wallclock'`): the resume point is
22
+ * the manifest `start + duration`, a wallclock instant, converted to currentTime with the
23
+ * mapping the content player reports right now, so a currentTime re-anchored by a seek during
24
+ * the break cannot move it. The web twin of the iOS runtime's `OverlayAdRenderer+PdtClock`; the
25
+ * beacon-based sibling is `StreamClockResume`. See `docs/ios-live-resume-v2.md` §17.
26
+ */
27
+ export declare class PdtClockResume {
28
+ private readonly deps;
29
+ /** The hand-back resumed on the Program Date Time target. */
30
+ applied: boolean;
31
+ private reportedTarget;
32
+ private offsetAtBreakStart;
33
+ private landingCheck;
34
+ constructor(deps: PdtClockResumeDeps);
35
+ /** Content paused for a new break: per-break state starts over, the current mapping is the reference. */
36
+ beginBreak(): void;
37
+ /** The resume point in currentTime under the mapping the player reports now, or null. */
38
+ target(breakInfo: Break): number | null;
39
+ /** The hand-back's target, recording whether it applies; null for an early return. */
40
+ handBackTarget(breakInfo: Break, earlyReturn: boolean): number | null;
41
+ /** What DA-RESUME-SEEK's target is based on, when the Program Date Time decided it. */
42
+ seekBasis(): string | null;
43
+ /** The Program Date Time fields of DA-RESUME-SEEK; empty unless the hand-back resumed on it. */
44
+ seekContext(breakInfo: Break, target: number, computed: number): Record<string, unknown>;
45
+ /**
46
+ * DA-RESUME-PDT-CORRECTED, once per break at the hand-back of a PDT-anchored break: how far the
47
+ * stream's clock moved the resume point from the anchor-based target, or why it could not.
48
+ */
49
+ reportCorrection(breakInfo: Break, target: number, computed: number, earlyReturn: boolean): void;
50
+ /**
51
+ * Arms DA-RESUME-PDT-LANDED when the hand-back resumed on the Program Date Time: the first
52
+ * position sample after the hand-back measures where content landed on the stream clock.
53
+ */
54
+ armLanding(breakInfo: Break): void;
55
+ cancelLanding(): void;
56
+ private reportTarget;
57
+ }
@@ -0,0 +1 @@
1
+ import{contentTimeForWallclock as e}from"../wallclockBreakWindow";const t=e=>Math.round(1e3*e)/1e3||0;export function programDateTimeSample(e){const t=e.programDateTime?.getTime(),r=e.currentTime;return void 0!==t&&Number.isFinite(t)&&Number.isFinite(r)?{currentTime:r,programDateTimeMs:t}:null}export function wallclockBreakEndMs(e){if("string"!=typeof e.start)return null;const t=Date.parse(e.start);return Number.isFinite(t)?t+1e3*(e.duration??0):null}export class PdtClockResume{constructor(e){this.deps=e,this.applied=!1,this.reportedTarget=null,this.offsetAtBreakStart=null,this.landingCheck=null}beginBreak(){this.cancelLanding(),this.applied=!1,this.reportedTarget=null;const e=programDateTimeSample(this.deps.contentPlayer);this.offsetAtBreakStart=e?e.programDateTimeMs/1e3-e.currentTime:null}target(t){if(!this.deps.anchorIsPdt()||!this.deps.resumesAtBreakEnd(t))return null;const r=wallclockBreakEndMs(t),a=programDateTimeSample(this.deps.contentPlayer);if(null===r||!a)return null;const n=e(a,r);return this.reportTarget(t,n,a,r),n}handBackTarget(e,t){const r=t?null:this.target(e);return this.applied=null!==r,r}seekBasis(){return this.applied?"on the stream clock, program-date-time":null}seekContext(e,r,a){const n=wallclockBreakEndMs(e),i=programDateTimeSample(this.deps.contentPlayer);return this.applied&&null!==n&&i?{pdtTarget:t(r),computedTarget:a,landingPdt:new Date(n).toISOString(),pdtOffsetSec:t(i.programDateTimeMs/1e3-i.currentTime)}:{}}reportCorrection(e,r,a,n){if(n||!this.deps.anchorIsPdt())return;const i={breakId:e.id,applied:this.applied,computedTarget:a},s=programDateTimeSample(this.deps.contentPlayer);if(!this.applied||!s)return i.reason="no program-date-time sample at the hand-back",void this.deps.diagnose("DA-RESUME-PDT-CORRECTED",`Break ${e.id}: no program-date-time sample at the hand-back; the stream clock did not place the resume`,i);const o=s.programDateTimeMs/1e3-s.currentTime,d=t(r-a),l=null===this.offsetAtBreakStart?null:t(this.offsetAtBreakStart-o);Object.assign(i,{pdtTarget:t(r),correctionSec:d,currentTimeShiftSec:l}),this.deps.diagnose("DA-RESUME-PDT-CORRECTED",`Break ${e.id}: the program-date-time placed the resume at ${t(r)}s, ${d}s from the anchor target ${a}s`+(null===l?"":`; currentTime moved ${l}s against the stream during the break`),i)}armLanding(e){const r=wallclockBreakEndMs(e);if(!this.applied||null===r)return;this.cancelLanding();const a=e.id,n=this.deps.now(),i=()=>{const e=programDateTimeSample(this.deps.contentPlayer);if(!e)return;this.cancelLanding();const i=t((e.programDateTimeMs-r)/1e3);this.deps.diagnose("DA-RESUME-PDT-LANDED",`Resume for break ${a} landed ${Math.abs(i)}s ${i<0?"before":"after"} the break end on the program-date-time`,{breakId:a,landingPdt:new Date(r).toISOString(),landedPdt:new Date(e.programDateTimeMs).toISOString(),landedAt:t(e.currentTime),errorSec:i,sinceHandBackSec:t((this.deps.now()-n)/1e3)})},s=this.deps.contentPlayer,o=setTimeout(()=>{this.cancelLanding(),this.deps.diagnose("DA-RESUME-PDT-LANDED",`Resume landing for break ${a} could not be measured on the program-date-time (no sample)`,{breakId:a,landingPdt:new Date(r).toISOString(),reason:"no program-date-time sample after the hand-back"})},2e3);this.landingCheck={breakId:a,landingMs:r,timer:o,release:()=>{s.off("seeked",i),s.off("timeupdate",i)}},s.on("seeked",i),s.on("timeupdate",i)}cancelLanding(){const e=this.landingCheck;e&&(this.landingCheck=null,clearTimeout(e.timer),e.release())}reportTarget(e,r,a,n){if(null!==this.reportedTarget&&Math.abs(r-this.reportedTarget)<.1)return;const i=null===this.reportedTarget;this.reportedTarget=r;const s=this.deps.computedTarget(e),o={breakId:e.id,target:t(r),landingPdt:new Date(n).toISOString(),pdtOffsetSec:t(a.programDateTimeMs/1e3-a.currentTime),currentTime:t(a.currentTime),remapped:!i};null!==s&&(o.computedTarget=s,o.deltaSec=t(r-s)),this.deps.diagnose("DA-RESUME-PDT-MAP",`Break ${e.id}: the break end maps to ${t(r)}s on the program-date-time`+(null===s?"":` (${t(r-s)}s from the anchor target ${s}s)`)+(i?"":"; currentTime moved against the stream"),o)}}
@@ -50,7 +50,6 @@ export type AssetParameterMacros = Record<string, AssetParameterMacro>;
50
50
  * on demand (`undefined` = no value right now).
51
51
  */
52
52
  export type AssetParameterMacroTable = Record<string, () => AssetParameterMacroValue>;
53
- export declare function omitBreakScopedParameters(params: Record<string, string> | undefined): Record<string, string> | undefined;
54
53
  export declare function resolveAssetParameterMacro(macro: AssetParameterMacro): AssetParameterMacroValue;
55
54
  export interface UnresolvedAssetParameterMacro {
56
55
  key: string;
@@ -1 +1 @@
1
- export const OPTIVIEW_MACRO_PREFIX="$OPTIVIEW_";export const OPTIVIEW_ASSET_PARAMETER_MACROS={USER_AGENT:"$OPTIVIEW_USER_AGENT$",PLAYER_WIDTH:"$OPTIVIEW_PLAYER_WIDTH$",PLAYER_HEIGHT:"$OPTIVIEW_PLAYER_HEIGHT$",BREAK_DURATION_MS:"$OPTIVIEW_BREAK_DURATION_MS$",BREAK_DURATION_SECONDS:"$OPTIVIEW_BREAK_DURATION_SECONDS$"};export const OPTIVIEW_ASSET_PARAMETER_MACRO_SCOPES={[OPTIVIEW_ASSET_PARAMETER_MACROS.USER_AGENT]:"session",[OPTIVIEW_ASSET_PARAMETER_MACROS.PLAYER_WIDTH]:"session",[OPTIVIEW_ASSET_PARAMETER_MACROS.PLAYER_HEIGHT]:"session",[OPTIVIEW_ASSET_PARAMETER_MACROS.BREAK_DURATION_MS]:"break",[OPTIVIEW_ASSET_PARAMETER_MACROS.BREAK_DURATION_SECONDS]:"break"};export function omitBreakScopedParameters(e){if(!e)return;const t=Object.fromEntries(Object.entries(e).filter(([,e])=>!Object.entries(OPTIVIEW_ASSET_PARAMETER_MACRO_SCOPES).some(([t,o])=>"break"===o&&e.includes(t))));return Object.keys(t).length>0?t:void 0}export function resolveAssetParameterMacro(e){return"function"==typeof e?e():e}export function applyAssetParameterMacros(o,n,r){const s=o??{},E={},i=[],_=[];for(const o of Object.keys(s).sort()){if("gam"===r&&"cust_params"===o){const e=t(s[o],n);void 0!==e.value&&(E[o]=e.value),_.push(...e.omitted.map(({macro:e,pair:t})=>({key:o,macro:e,pair:t}))),i.push(...e.unresolved.map(e=>({key:o,macro:e})));continue}const A=e(s[o],n);void 0===A.omittedBy?(E[o]=A.value,i.push(...A.unresolved.map(e=>({key:o,macro:e})))):_.push({key:o,macro:A.omittedBy})}return{params:Object.keys(E).length>0?E:void 0,unresolved:i,omitted:_}}function e(e,t){let o="",r=0;const s=[],E=new Set;for(;r<e.length;){const i=e.indexOf("$",r);if(-1===i){o+=e.slice(r);break}o+=e.slice(r,i);const _=n(e,i);if(void 0===_){o+="$",r=i+1;continue}const A=t[_]?.();if(null===A)return{value:"",unresolved:[],omittedBy:_};void 0!==A?o+=A:(o+=_,!_.startsWith("$OPTIVIEW_")&&!(_ in t)||E.has(_)||(s.push(_),E.add(_))),r=i+_.length}return{value:o,unresolved:s}}function t(t,o){const n=[],r=[],s=[];for(const E of t.split("&")){const t=e(E,o);void 0===t.omittedBy?(n.push(t.value),r.push(...t.unresolved)):s.push({macro:t.omittedBy,pair:E})}return{value:n.length>0?n.join("&"):void 0,unresolved:r,omitted:s}}function o(e){return e>="A"&&e<="Z"||e>="a"&&e<="z"||e>="0"&&e<="9"||"_"===e}function n(e,t){let n=t+1;for(;n<e.length&&o(e[n]);)n++;return n>t+1&&"$"===e[n]?e.slice(t,n+1):void 0}export function appendQueryParameters(e,t){const o=t??{},n=Object.keys(o).sort();if(0===n.length)return e;if(/^data:/i.test(e))return e;const r=n.map(e=>`${encodeURIComponent(e)}=${encodeURIComponent(o[e])}`).join("&"),s=e.indexOf("#"),E=-1===s?e:e.slice(0,s),i=-1===s?"":e.slice(s),_=E.includes("?")?E.endsWith("?")||E.endsWith("&")?"":"&":"?";return`${E}${_}${r}${i}`}
1
+ export const OPTIVIEW_MACRO_PREFIX="$OPTIVIEW_";export const OPTIVIEW_ASSET_PARAMETER_MACROS={USER_AGENT:"$OPTIVIEW_USER_AGENT$",PLAYER_WIDTH:"$OPTIVIEW_PLAYER_WIDTH$",PLAYER_HEIGHT:"$OPTIVIEW_PLAYER_HEIGHT$",BREAK_DURATION_MS:"$OPTIVIEW_BREAK_DURATION_MS$",BREAK_DURATION_SECONDS:"$OPTIVIEW_BREAK_DURATION_SECONDS$"};export const OPTIVIEW_ASSET_PARAMETER_MACRO_SCOPES={[OPTIVIEW_ASSET_PARAMETER_MACROS.USER_AGENT]:"session",[OPTIVIEW_ASSET_PARAMETER_MACROS.PLAYER_WIDTH]:"session",[OPTIVIEW_ASSET_PARAMETER_MACROS.PLAYER_HEIGHT]:"session",[OPTIVIEW_ASSET_PARAMETER_MACROS.BREAK_DURATION_MS]:"break",[OPTIVIEW_ASSET_PARAMETER_MACROS.BREAK_DURATION_SECONDS]:"break"};export function resolveAssetParameterMacro(e){return"function"==typeof e?e():e}export function applyAssetParameterMacros(o,n,s){const r=o??{},E={},i=[],_=[];for(const o of Object.keys(r).sort()){if("gam"===s&&"cust_params"===o){const e=t(r[o],n);void 0!==e.value&&(E[o]=e.value),_.push(...e.omitted.map(({macro:e,pair:t})=>({key:o,macro:e,pair:t}))),i.push(...e.unresolved.map(e=>({key:o,macro:e})));continue}const A=e(r[o],n);void 0===A.omittedBy?(E[o]=A.value,i.push(...A.unresolved.map(e=>({key:o,macro:e})))):_.push({key:o,macro:A.omittedBy})}return{params:Object.keys(E).length>0?E:void 0,unresolved:i,omitted:_}}function e(e,t){let o="",s=0;const r=[],E=new Set;for(;s<e.length;){const i=e.indexOf("$",s);if(-1===i){o+=e.slice(s);break}o+=e.slice(s,i);const _=n(e,i);if(void 0===_){o+="$",s=i+1;continue}const A=t[_]?.();if(null===A)return{value:"",unresolved:[],omittedBy:_};void 0!==A?o+=A:(o+=_,!_.startsWith("$OPTIVIEW_")&&!(_ in t)||E.has(_)||(r.push(_),E.add(_))),s=i+_.length}return{value:o,unresolved:r}}function t(t,o){const n=[],s=[],r=[];for(const E of t.split("&")){const t=e(E,o);void 0===t.omittedBy?(n.push(t.value),s.push(...t.unresolved)):r.push({macro:t.omittedBy,pair:E})}return{value:n.length>0?n.join("&"):void 0,unresolved:s,omitted:r}}function o(e){return e>="A"&&e<="Z"||e>="a"&&e<="z"||e>="0"&&e<="9"||"_"===e}function n(e,t){let n=t+1;for(;n<e.length&&o(e[n]);)n++;return n>t+1&&"$"===e[n]?e.slice(t,n+1):void 0}export function appendQueryParameters(e,t){const o=t??{},n=Object.keys(o).sort();if(0===n.length)return e;if(/^data:/i.test(e))return e;const s=n.map(e=>`${encodeURIComponent(e)}=${encodeURIComponent(o[e])}`).join("&"),r=e.indexOf("#"),E=-1===r?e:e.slice(0,r),i=-1===r?"":e.slice(r),_=E.includes("?")?E.endsWith("?")||E.endsWith("&")?"":"&":"?";return`${E}${_}${s}${i}`}
@@ -65,3 +65,5 @@ export declare function resolveAnvatoStartTime(state: BreakTimingState, deps: Br
65
65
  */
66
66
  export declare function diagnoseAnvatoBreakEnd(state: BreakTimingState, deps: BreakTimingDeps, breakInfo: Break): void;
67
67
  export declare function secondsUntilBreak(breakStartTime: number, currentTime: number): number;
68
+ /** Human-readable start of a break for scheduler logs. */
69
+ export declare function describeBreakStart(breakInfo: Break): string;
@@ -1 +1 @@
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}
1
+ import{Timebase as e,getPostRollDelay as t,getPreRollDelay as a}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}export function describeBreakStart(e){const n=a(e);if(null!==n)return n>0?`playback start + ${n}s`:"playback start";const r=t(e);return null!==r?r>0?`playback end + ${r}s`:"playback end":"string"==typeof e.start?new Date(e.start).toLocaleTimeString():String(e.start)}
@@ -1,5 +1,6 @@
1
1
  import type { BreakManifest } from '../types/BreakManifest';
2
2
  import { DeviceType } from '../types/BreakManifest';
3
+ import type { DeviceProfile, TargetingIssue } from './matchTargeting';
3
4
  import type { AdChannelChangeReason } from '../types/Events';
4
5
  import type { OptiViewAdsMode } from '../types/OptiViewAdsConfig';
5
6
  export interface ChannelSwitch {
@@ -16,9 +17,10 @@ export interface DeliveryModeDecision {
16
17
  mode: OptiViewAdsMode;
17
18
  ruleIndex: number | null;
18
19
  unknownMode: string | null;
20
+ targetingIssues: TargetingIssue[];
19
21
  source: DeliveryModeSource;
20
22
  }
21
- export declare function decideDeliveryMode(manifest: BreakManifest, deviceType: DeviceType, explicitMode: OptiViewAdsMode | null, source: 'manifest' | 'channel-switch'): DeliveryModeDecision;
23
+ export declare function decideDeliveryMode(manifest: BreakManifest, deviceProfile: DeviceType | DeviceProfile, explicitMode: OptiViewAdsMode | null, source: 'manifest' | 'channel-switch'): DeliveryModeDecision;
22
24
  /** Blank ids count as absent: the backend omits the field, a proxy may emit `""`. */
23
25
  export declare function normalizeChannelId(channelId: string | undefined): string | null;
24
26
  /** True when the new manifest needs a new IMA session (different network or custom asset key). */
@@ -1 +1 @@
1
- import{resolveGamVendorConfiguration as e}from"./resolveGamVendorConfiguration";import{resolveDeliveryMode as n,resolveStitchedStreamVendor as o}from"./resolveDeliveryMode";export function decideDeliveryMode(e,o,r,t){const d=n(e.delivery,o);return{manifestMode:d.mode,mode:r??d.mode,ruleIndex:d.ruleIndex,unknownMode:d.unknownMode,source:r?"config":t}}export function normalizeChannelId(e){return"string"==typeof e&&""!==e.trim()?e:null}function r(n){const o=e(n).identity;return o?`${o.networkCode}|${o.customAssetKey}`:""}export function gamPodIdentityChanged(e,n){return r(e)!==r(n)}function t(e){const n=o(e.vendorConfiguration);return n?`${n.networkCode}|${n.assetKey}`:""}export function detectChannelSwitch(e,n){const o=normalizeChannelId(e.channelId),r=normalizeChannelId(n.channelId),d={previousChannelId:o,channelId:r};return o!==r?null===o?{...d,reason:"channel-id-added"}:null===r?{...d,reason:"channel-id-removed"}:{...d,reason:"channel-id-changed"}:gamPodIdentityChanged(e,n)||t(e)!==t(n)?{...d,reason:"vendor-configuration-changed"}:e.timebase!==n.timebase?{...d,reason:"timebase-changed"}:null}
1
+ import{resolveGamVendorConfiguration as e}from"./resolveGamVendorConfiguration";import{resolveDeliveryMode as n,resolveStitchedStreamVendor as o}from"./resolveDeliveryMode";export function decideDeliveryMode(e,o,r,t){const d=n(e.delivery,o);return{manifestMode:d.mode,mode:r??d.mode,ruleIndex:d.ruleIndex,unknownMode:d.unknownMode,targetingIssues:d.targetingIssues,source:r?"config":t}}export function normalizeChannelId(e){return"string"==typeof e&&""!==e.trim()?e:null}function r(n){const o=e(n).identity;return o?`${o.networkCode}|${o.customAssetKey}`:""}export function gamPodIdentityChanged(e,n){return r(e)!==r(n)}function t(e){const n=o(e.vendorConfiguration);return n?`${n.networkCode}|${n.assetKey}`:""}export function detectChannelSwitch(e,n){const o=normalizeChannelId(e.channelId),r=normalizeChannelId(n.channelId),d={previousChannelId:o,channelId:r};return o!==r?null===o?{...d,reason:"channel-id-added"}:null===r?{...d,reason:"channel-id-removed"}:{...d,reason:"channel-id-changed"}:gamPodIdentityChanged(e,n)||t(e)!==t(n)?{...d,reason:"vendor-configuration-changed"}:e.timebase!==n.timebase?{...d,reason:"timebase-changed"}:null}
@@ -1,4 +1,5 @@
1
1
  import { DeviceType } from '../types/BreakManifest';
2
+ import type { DeviceProfile } from './matchTargeting';
2
3
  /**
3
4
  * Heuristic: classify a User-Agent string into a {@link DeviceType} for
4
5
  * per-URI asset targeting (see {@link resolveTargetedUri}).
@@ -9,7 +10,11 @@ import { DeviceType } from '../types/BreakManifest';
9
10
  * conformance fixture (mirrors `isSharedElementUserAgent` /
10
11
  * `isSingleDecoderUserAgent`, which are likewise UA-only web glue).
11
12
  *
12
- * Checked in order TV → tablet → mobile → desktop (default), since a tablet
13
- * or TV UA can otherwise coincidentally match a broader mobile pattern.
13
+ * Checked in order console → TV → tablet → mobile → desktop (default), since a tablet
14
+ * or TV UA can otherwise coincidentally match a broader mobile pattern. Console and TV
15
+ * devices are recognised by their User-Agent brand tokens;
16
+ * a bare `CrKey` is not enough, as it also marks Chromecast-style desktops.
14
17
  */
15
18
  export declare function detectDeviceType(ua?: string): DeviceType;
19
+ /** Derives the browser's targeting profile synchronously from its User-Agent. */
20
+ export declare function detectDeviceProfile(ua?: string): DeviceProfile;
@@ -1 +1 @@
1
- import{DeviceType as e}from"../types/BreakManifest";export function detectDeviceType(t=("undefined"!=typeof navigator?navigator.userAgent:"")){return t?/SMART-TV|SmartTV|Tizen|Web0S|WebOS|GoogleTV|AppleTV|HbbTV|NetCast|VIDAA|BRAVIA/i.test(t)?e.TV:/iPad/i.test(t)||/Android/i.test(t)&&!/Mobile/i.test(t)?e.TABLET:/iPhone|iPod|Mobi|Android.*Mobile/i.test(t)?e.MOBILE:e.DESKTOP:e.DESKTOP}
1
+ import{DeviceType as e}from"../types/BreakManifest";import{Os as t,Platform as i}from"../types/BreakManifest";export function detectDeviceType(t=("undefined"!=typeof navigator?navigator.userAgent:"")){return t?/Xbox/i.test(t)?e.CONSOLE:/SMART-TV|SmartTV|Tizen|Web0S|WebOS|GoogleTV|AppleTV|HbbTV|NetCast|VIDAA|BRAVIA|VIZIO|SmartCast/i.test(t)?e.TV:/iPad/i.test(t)||/Android/i.test(t)&&!/Mobile/i.test(t)?e.TABLET:/iPhone|iPod|Mobi|Android.*Mobile/i.test(t)?e.MOBILE:e.DESKTOP:e.DESKTOP}const o={38:"3",53:"4",68:"5",79:"6",87:"22",94:"23",108:"24",120:"25"},s=Object.keys(o).reduce((e,t)=>Number(t)>Number(e)?t:e),r=o[s];export function detectDeviceProfile(e=("undefined"!=typeof navigator?navigator.userAgent:"")){const n={deviceType:detectDeviceType(e),platform:i.WEB};let a;if(/Xbox/i.test(e))return{...n,os:t.XBOX};if(/VIZIO/i.test(e))return{...n,os:t.VIZIO};if(/Tizen/i.test(e)){const i=e.match(/Tizen\s+([\d.]+)/i)?.[1];return{...n,os:t.TIZEN,...i?{osVersion:i}:{}}}if(/Web0S|WebOS/i.test(e)){const i=e.match(/Chrome\/(\d+)/)?.[1],a=i?o[i]??(Number(i)>Number(s)?r:void 0):void 0;return{...n,os:t.WEBOS,...a?{osVersion:a}:{}}}return(a=e.match(/CrOS\s+\S+\s+([\d.]+)/i))?{...n,os:t.CHROMEOS,osVersion:a[1]}:(a=e.match(/Android\s+([\d.]+)/i))?{...n,os:t.ANDROID,osVersion:a[1]}:(a=e.match(/(?:iPhone|iPad|iPod).*?OS\s+([\d_]+)/i))?{...n,os:/iPad/i.test(e)?t.IPADOS:t.IOS,osVersion:a[1].replace(/_/g,".")}:/Macintosh|Mac OS X/i.test(e)?{...n,os:t.MACOS}:/Windows/i.test(e)?{...n,os:t.WINDOWS}:/Linux/i.test(e)?{...n,os:t.LINUX}:n}
@@ -0,0 +1,3 @@
1
+ import type { DiagnosticEvent, DiagnosticReport } from '../diagnostics/DiagnosticEvent';
2
+ import type { SessionState } from '../session/SessionState';
3
+ export declare function buildDiagnosticReport(state: Pick<SessionState, 'configSummary' | 'manifestOrigin' | 'stitcherOrigin'>, diagnostics: DiagnosticEvent[]): DiagnosticReport;
@@ -0,0 +1 @@
1
+ import{SDK_VERSION as i}from"../generated/version";export function buildDiagnosticReport(n,r){const t={...n.configSummary,...n.manifestOrigin?{manifestOrigin:n.manifestOrigin}:{},...n.configSummary.ssai&&n.stitcherOrigin?{ssai:{...n.configSummary.ssai,stitcherOrigin:n.stitcherOrigin}}:{}};return{sdkVersion:i,generatedAt:Date.now(),userAgent:"undefined"!=typeof navigator?navigator.userAgent:null,config:t,diagnostics:r}}
@@ -0,0 +1,12 @@
1
+ import type { ExternalVendorConfiguration } from '../types/ExternalVendorConfiguration';
2
+ import type { ExternalVendorConfiguration as ExternalVendorConfigurationTemplate } from './resolveExternalVendorConfigurations';
3
+ interface ExternalVendorConfigurationController {
4
+ resolveExternalVendorConfiguration(entry: ExternalVendorConfigurationTemplate): ExternalVendorConfiguration | undefined;
5
+ }
6
+ interface ExternalVendorConfigurationState {
7
+ sessionActive: boolean;
8
+ steeredSsaiActive: boolean;
9
+ ssaiController?: ExternalVendorConfigurationController | null;
10
+ }
11
+ export declare function readExternalVendorConfiguration(state: ExternalVendorConfigurationState, entries: ExternalVendorConfigurationTemplate[], mode: string, adPlayerController: ExternalVendorConfigurationController | null | undefined, provider?: string): ExternalVendorConfiguration[] | ExternalVendorConfiguration | undefined;
12
+ export {};
@@ -0,0 +1 @@
1
+ export function readExternalVendorConfiguration(r,e,o,i,t){if(!r.sessionActive)return void 0===t?[]:void 0;const s="ssai"===o||r.steeredSsaiActive?r.ssaiController:i,n=r=>{const e=s?.resolveExternalVendorConfiguration(r)??{provider:r.provider,assetParameters:{}};return{provider:e.provider,assetParameters:{...e.assetParameters}}};if(void 0!==t){const r=e.find(r=>r.provider===t);return r?n(r):void 0}return e.map(n)}
@@ -0,0 +1,19 @@
1
+ import { DeviceType, Os, Platform } from '../types/BreakManifest';
2
+ import type { Targeting } from '../types/BreakManifest';
3
+ export interface DeviceProfile {
4
+ deviceType?: DeviceType | string;
5
+ platform?: Platform | string;
6
+ os?: Os | string;
7
+ osVersion?: string;
8
+ }
9
+ export interface TargetingIssue {
10
+ code: 'DA-TARGETING-VALUE-UNKNOWN' | 'DA-TARGETING-FIELD-UNKNOWN' | 'DA-TARGETING-INVALID';
11
+ field: string;
12
+ value?: string;
13
+ }
14
+ export interface TargetingMatch {
15
+ matches: boolean;
16
+ issues: TargetingIssue[];
17
+ }
18
+ /** Matches every specified targeting field; empty targeting remains a default. */
19
+ export declare function matchTargeting(targeting: Targeting | undefined, profile: DeviceProfile): TargetingMatch;
@@ -0,0 +1 @@
1
+ import{DeviceType as e,Os as s,Platform as i}from"../types/BreakManifest";const o=["deviceType","platform","os","osVersion"],n=Object.keys(e).map(s=>e[s]),t=Object.keys(i).map(e=>i[e]),r=Object.keys(s).map(e=>s[e]),f=/^\d+$/;function u(e){const s=e.split(".");return s.every(e=>f.test(e))?s:null}function c(e,s){const i=e.replace(/^0+(?=\d)/,""),o=s.replace(/^0+(?=\d)/,"");return i.length!==o.length?i.length<o.length?-1:1:i===o?0:i<o?-1:1}function m(e,s){for(let i=0;i<s.length;i++){const o=c(e[i]??"0",s[i]);if(0!==o)return o}return 0}function a(e,s,i){if("object"!=typeof(o=e)||null===o||Array.isArray(o))return i.push({code:"DA-TARGETING-INVALID",field:"osVersion"}),!1;var o;for(const s of Object.keys(e))if("min"!==s&&"max"!==s)return i.push({code:"DA-TARGETING-FIELD-UNKNOWN",field:`osVersion.${s}`}),!1;const n=e;if(void 0===n.min&&void 0===n.max)return i.push({code:"DA-TARGETING-INVALID",field:"osVersion"}),!1;if("string"==typeof n.min&&!u(n.min))return i.push({code:"DA-TARGETING-INVALID",field:"osVersion.min",value:n.min}),!1;if("string"==typeof n.max&&!u(n.max))return i.push({code:"DA-TARGETING-INVALID",field:"osVersion.max",value:n.max}),!1;if(void 0!==n.min&&"string"!=typeof n.min||void 0!==n.max&&"string"!=typeof n.max)return i.push({code:"DA-TARGETING-INVALID",field:"osVersion"}),!1;if(!s.os||!s.osVersion)return!1;const t=u(s.osVersion);return!!t&&(!(void 0!==n.min&&m(t,u(n.min))<0)&&!(void 0!==n.max&&m(t,u(n.max))>0))}export function matchTargeting(e,s){if(void 0===e)return{matches:!0,issues:[]};const i=e,f=[];for(const e of Object.keys(i))o.includes(e)||f.push({code:"DA-TARGETING-FIELD-UNKNOWN",field:e});if(f.length>0)return{matches:!1,issues:f};for(const[e,o,u]of[["deviceType",n,s.deviceType],["platform",t,s.platform],["os",r,s.os]]){const s=i[e];if(void 0===s)continue;if("string"!=typeof s||!o.includes(s))return f.push({code:"DA-TARGETING-VALUE-UNKNOWN",field:e,value:String(s)}),{matches:!1,issues:f};if(s!==u)return{matches:!1,issues:f}}if(void 0!==i.osVersion){if(!i.os)return f.push({code:"DA-TARGETING-INVALID",field:"osVersion"}),{matches:!1,issues:f};if(!a(i.osVersion,s,f)||f.length>0)return{matches:!1,issues:f}}return{matches:!0,issues:f}}
@@ -1,4 +1,5 @@
1
1
  import { BreakManifest, DeliveryMode, DeliveryRule, DeviceType, VendorConfiguration } from '../types/BreakManifest';
2
+ import type { DeviceProfile, TargetingIssue } from './matchTargeting';
2
3
  import { type StitchedStreamVendor } from './resolveGamVendorConfiguration';
3
4
  export type { StitchedStreamVendor } from './resolveGamVendorConfiguration';
4
5
  /**
@@ -22,6 +23,7 @@ export interface DeliveryModeDecision {
22
23
  * value, which the resolver replaced with sgai (forward compatibility).
23
24
  */
24
25
  unknownMode: string | null;
26
+ targetingIssues: TargetingIssue[];
25
27
  }
26
28
  /**
27
29
  * Resolve the delivery mode from a manifest's ordered `delivery` rules for the
@@ -38,7 +40,7 @@ export interface DeliveryModeDecision {
38
40
  * 3. A matching rule with an unrecognized mode value → sgai, with the raw value
39
41
  * reported via `unknownMode` so the caller can log it (forward compatibility).
40
42
  */
41
- export declare function resolveDeliveryMode(delivery: DeliveryRule[] | undefined, deviceType: DeviceType): DeliveryModeDecision;
43
+ export declare function resolveDeliveryMode(delivery: DeliveryRule[] | undefined, deviceProfile: DeviceType | DeviceProfile): DeliveryModeDecision;
42
44
  /**
43
45
  * Find the first vendor configuration entry able to provide a server-stitched
44
46
  * stream — currently a `gam` entry carrying a full-service `assetKey`. Returns
@@ -1 +1 @@
1
- import{DeliveryMode as e,getBreakEvent as r}from"../types/BreakManifest";import{resolveGamVendorConfiguration as n}from"./resolveGamVendorConfiguration";export class SsaiUnavailableError extends Error{constructor(e){super(e),this.name="SsaiUnavailableError"}}export function resolveDeliveryMode(r,n){if(!Array.isArray(r))return{mode:e.SGAI,ruleIndex:null,unknownMode:null};for(let o=0;o<r.length;o++){const t=r[o],i=t?.targeting?.deviceType;if(void 0===i||i===n)return t.mode===e.SGAI||t.mode===e.SSAI?{mode:t.mode,ruleIndex:o,unknownMode:null}:{mode:e.SGAI,ruleIndex:o,unknownMode:String(t.mode)}}return{mode:e.SGAI,ruleIndex:null,unknownMode:null}}export function resolveStitchedStreamVendor(e){return n(void 0===e?{}:{vendorConfiguration:e}).ssai}export function filterSsaiClientBreaks(e){return{...e,breaks:e.breaks.filter(e=>null!==r(e))}}
1
+ import{DeliveryMode as e,getBreakEvent as r}from"../types/BreakManifest";import{matchTargeting as n}from"./matchTargeting";import{resolveGamVendorConfiguration as o}from"./resolveGamVendorConfiguration";export class SsaiUnavailableError extends Error{constructor(e){super(e),this.name="SsaiUnavailableError"}}export function resolveDeliveryMode(r,o){const t="string"==typeof o?{deviceType:o}:o;if(!Array.isArray(r))return{mode:e.SGAI,ruleIndex:null,unknownMode:null,targetingIssues:[]};const s=[];for(let o=0;o<r.length;o++){const i=r[o],u=n(i?.targeting,t);if(s.push(...u.issues),u.matches)return i.mode===e.SGAI||i.mode===e.SSAI?{mode:i.mode,ruleIndex:o,unknownMode:null,targetingIssues:s}:{mode:e.SGAI,ruleIndex:o,unknownMode:String(i.mode),targetingIssues:s}}return{mode:e.SGAI,ruleIndex:null,unknownMode:null,targetingIssues:s}}export function resolveStitchedStreamVendor(e){return o(void 0===e?{}:{vendorConfiguration:e}).ssai}export function filterSsaiClientBreaks(e){return{...e,breaks:e.breaks.filter(e=>null!==r(e))}}
@@ -0,0 +1,20 @@
1
+ import type { BreakManifest } from '../types/BreakManifest';
2
+ import { type AssetParameterMacroTable, type OmittedAssetParameter, type UnresolvedAssetParameterMacro } from './assetParameterMacros';
3
+ export interface ExternalVendorConfiguration {
4
+ provider: string;
5
+ assetParameters: Record<string, string>;
6
+ }
7
+ export interface ExternalVendorConfigurationsResult {
8
+ entries: ExternalVendorConfiguration[];
9
+ duplicateProviders: string[];
10
+ invalidCount: number;
11
+ }
12
+ export interface ResolvedExternalVendorParameters {
13
+ configuration: ExternalVendorConfiguration;
14
+ unresolved: UnresolvedAssetParameterMacro[];
15
+ omitted: OmittedAssetParameter[];
16
+ }
17
+ /** Return providers whose manifest templates were added, changed, or removed. */
18
+ export declare function changedExternalVendorProviders(previous: ExternalVendorConfiguration[], next: ExternalVendorConfiguration[]): string[];
19
+ export declare function resolveExternalVendorConfigurations(manifest: Pick<BreakManifest, 'vendorConfiguration'>): ExternalVendorConfigurationsResult;
20
+ export declare function resolveExternalVendorParameters(entry: ExternalVendorConfiguration, applicationParameters: Record<string, string> | undefined, macros: AssetParameterMacroTable): ResolvedExternalVendorParameters;
@@ -0,0 +1 @@
1
+ import{applyAssetParameterMacros as r}from"./assetParameterMacros";import{objectEntries as e,objectFromEntries as t}from"./runtimeObject";export function changedExternalVendorProviders(r,e){const t=new Map(r.map(r=>[r.provider,r])),n=new Set(e.map(r=>r.provider)),o=e.filter(r=>{const e=t.get(r.provider);return void 0===e||!function(r,e){const t=Object.keys(r);return t.length===Object.keys(e).length&&t.every(t=>Object.prototype.hasOwnProperty.call(e,t)&&r[t]===e[t])}(e.assetParameters,r.assetParameters)}).map(r=>r.provider);for(const e of r)n.has(e.provider)||o.push(e.provider);return o}function n(r){if("object"!=typeof r||null===r||Array.isArray(r))return!1;const e=Object.getPrototypeOf(r);return e===Object.prototype||null===e}function o(r){return"string"==typeof r&&""!==r.trim()}function s(r){return n(r)?t(e(r).filter(r=>"string"==typeof r[1])):null}export function resolveExternalVendorConfigurations(r){const e=r.vendorConfiguration,t=[],i=[],a=new Set;let u=0;if(!Array.isArray(e))return{entries:t,duplicateProviders:i,invalidCount:u};for(const r of e){if(!n(r)||"external"!==r.type)continue;const e=s(r.assetParameters);if(!o(r.provider)||null===e){u+=1;continue}const p=r.provider;a.has(p)?i.includes(p)||i.push(p):(a.add(p),t.push({provider:p,assetParameters:e}))}return{entries:t,duplicateProviders:i,invalidCount:u}}export function resolveExternalVendorParameters(e,t,n){const o={...t??{},...e.assetParameters},s=r(o,n,"gam");return{configuration:{provider:e.provider,assetParameters:s.params??{}},unresolved:s.unresolved,omitted:s.omitted}}
@@ -1 +1 @@
1
- import{objectEntries as e,objectFromEntries as s}from"./runtimeObject";function t(e){return"string"==typeof e&&""!==e.trim()?e:null}export function resolveGamVendorConfiguration(e){const s=e.vendorConfiguration,o={identity:null,unknownTypes:[],duplicateGam:!1,sgaiAssetParameters:void 0,ssai:null};if(!Array.isArray(s))return{...o,reason:"absent"};const r=[];let a=null,i=0;for(const e of s){if("object"!=typeof e||null===e)continue;const s=e,n=t(s.type);null!==n&&("gam"!==n?r.includes(n)||r.push(n):(i+=1,null===a&&(a=s)))}const u=i>1;if(null===a)return{...o,reason:"absent",unknownTypes:r,duplicateGam:u};const l=t(a.networkCode);if(null===l)return{...o,reason:"missing-network-code",unknownTypes:r,duplicateGam:u};const c=Array.isArray(a.ssai)?a.ssai[0]:void 0,y=("object"==typeof c&&null!==c?t(c.assetKey):null)??t(a.assetKey),f=y?{networkCode:l,assetKey:y,assetParameters:n("object"==typeof c&&null!==c?c.assetParameters:void 0)}:null,m=Array.isArray(a.sgai)?a.sgai[0]:void 0,d=("object"==typeof m&&null!==m?t(m.customAssetKey):null)??t(a.customAssetKey);if(null===d)return{...o,reason:"missing-custom-asset-key",unknownTypes:r,duplicateGam:u,ssai:f};return{identity:{networkCode:l,customAssetKey:d},reason:"ok",unknownTypes:r,duplicateGam:u,sgaiAssetParameters:n("object"==typeof m&&null!==m?m.assetParameters:void 0),ssai:f}}function n(t){if("object"!=typeof t||null===t||Array.isArray(t))return;const n=s(e(t).filter(e=>"string"==typeof e[1]));return Object.keys(n).length>0?n:void 0}export function gamPodIdentityMismatches(e,s){const t=[];return s.networkCode!==e.networkCode&&t.push({field:"networkCode",assetValue:s.networkCode,sessionValue:e.networkCode}),s.customAssetKey!==e.customAssetKey&&t.push({field:"customAssetKey",assetValue:s.customAssetKey,sessionValue:e.customAssetKey}),t}export function manifestHasGamAssets(e){for(const s of e.breaks??[]){const e=[...s.variants??[],...s.variant?[s.variant]:[]];for(const s of e)for(const e of s?.assets??[])if("gam"===e?.vendor)return!0}return!1}
1
+ import{objectEntries as e,objectFromEntries as s}from"./runtimeObject";function t(e){return"string"==typeof e&&""!==e.trim()?e:null}export function resolveGamVendorConfiguration(e){const s=e.vendorConfiguration,o={identity:null,unknownTypes:[],duplicateGam:!1,sgaiAssetParameters:void 0,ssai:null};if(!Array.isArray(s))return{...o,reason:"absent"};const r=[];let a=null,i=0;for(const e of s){if("object"!=typeof e||null===e)continue;const s=e,n=t(s.type);null!==n&&("gam"!==n?"external"!==n&&(r.includes(n)||r.push(n)):(i+=1,null===a&&(a=s)))}const u=i>1;if(null===a)return{...o,reason:"absent",unknownTypes:r,duplicateGam:u};const l=t(a.networkCode);if(null===l)return{...o,reason:"missing-network-code",unknownTypes:r,duplicateGam:u};const c=Array.isArray(a.ssai)?a.ssai[0]:void 0,y=("object"==typeof c&&null!==c?t(c.assetKey):null)??t(a.assetKey),f=y?{networkCode:l,assetKey:y,assetParameters:n("object"==typeof c&&null!==c?c.assetParameters:void 0)}:null,m=Array.isArray(a.sgai)?a.sgai[0]:void 0,d=("object"==typeof m&&null!==m?t(m.customAssetKey):null)??t(a.customAssetKey);if(null===d)return{...o,reason:"missing-custom-asset-key",unknownTypes:r,duplicateGam:u,ssai:f};return{identity:{networkCode:l,customAssetKey:d},reason:"ok",unknownTypes:r,duplicateGam:u,sgaiAssetParameters:n("object"==typeof m&&null!==m?m.assetParameters:void 0),ssai:f}}function n(t){if("object"!=typeof t||null===t||Array.isArray(t))return;const n=s(e(t).filter(e=>"string"==typeof e[1]));return Object.keys(n).length>0?n:void 0}export function gamPodIdentityMismatches(e,s){const t=[];return s.networkCode!==e.networkCode&&t.push({field:"networkCode",assetValue:s.networkCode,sessionValue:e.networkCode}),s.customAssetKey!==e.customAssetKey&&t.push({field:"customAssetKey",assetValue:s.customAssetKey,sessionValue:e.customAssetKey}),t}export function manifestHasGamAssets(e){for(const s of e.breaks??[]){const e=[...s.variants??[],...s.variant?[s.variant]:[]];for(const s of e)for(const e of s?.assets??[])if("gam"===e?.vendor)return!0}return!1}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The live-window clamp on a resume target: a delivery constraint applied AFTER the
3
+ * conformance-locked resume policy, pulling the target back inside what the engine holds.
4
+ * Portable brain rule, locked by the conformance harness (`resume-clamp`).
5
+ *
6
+ * The end clamp answers which frames exist: a target beyond the published end is pulled to it.
7
+ * The start clamp answers which frames are still held: the live window slides during a break,
8
+ * and a target behind `seekable.start` is already evicted, so asking for it lands on the window
9
+ * start anyway and measures as an error. A gap larger than half the window cannot be eviction
10
+ * (the engine's seekable range does not match the adapter's timeline) and is left to the engine.
11
+ */
12
+ export declare const START_CLAMP_MAX_WINDOW_FRACTION = 0.5;
13
+ export interface ResumeClampInput {
14
+ /** The policy's resume target, seconds of currentTime. */
15
+ target: number;
16
+ /** Earliest position the engine still holds; null on VOD or when unreadable (no start clamp). */
17
+ safeStart: number | null;
18
+ /** Latest position a seek will stick; null on VOD or when unreadable (no end clamp). */
19
+ safeEnd: number | null;
20
+ /** Length of the held window; null when unreadable (the start clamp is then skipped). */
21
+ windowSec: number | null;
22
+ /**
23
+ * Seconds kept clear of the window start on a start clamp, so the aim is not evicted while the
24
+ * seek resolves (AVFoundation cancels a seek outside its seekable range outright). Default 0.
25
+ */
26
+ startMarginSec?: number;
27
+ }
28
+ export interface ResumeClamp {
29
+ /** The target to seek, after the clamp. */
30
+ target: number;
31
+ /** Which bound pulled the target, or null when it was left unchanged. */
32
+ bound: 'start' | 'end' | null;
33
+ /** How far the clamp moved the target, seconds (0 when unchanged). */
34
+ shortfallSec: number;
35
+ /** Set when the target was behind the window start by more than the clamp allows. */
36
+ startClampSkipped: {
37
+ behindSec: number;
38
+ windowSec: number | null;
39
+ maxBehindSec: number | null;
40
+ } | null;
41
+ }
42
+ export declare function resolveResumeClamp(input: ResumeClampInput): ResumeClamp;
@@ -0,0 +1 @@
1
+ export const START_CLAMP_MAX_WINDOW_FRACTION=.5;const e=e=>Math.round(1e3*e)/1e3;export function resolveResumeClamp(t){const{safeStart:n,safeEnd:l,windowSec:a}=t,o=Math.max(0,t.startMarginSec??0);let r=t.target,s=0,u=null;null!==l&&r>l&&(s=e(r-l),r=l,u="end");let c=null;if(null!==n&&r<n){const t=e(n-r),i=null===a?null:.5*a;if(null!==i&&t<=i){const t=null===l?n+o:Math.min(n+o,l);s=e(t-r),r=t,u="start"}else c={behindSec:t,windowSec:a,maxBehindSec:i}}return{target:r,bound:u,shortfallSec:s,startClampSkipped:c}}
@@ -1,4 +1,5 @@
1
1
  import { AssetUri, DeviceType } from '../types/BreakManifest';
2
+ import type { DeviceProfile, TargetingIssue } from './matchTargeting';
2
3
  /**
3
4
  * Resolve a `static`/`vast` asset's `uri` field (a plain string, or an
4
5
  * `AssetUri[]` where each entry may carry `targeting.deviceType`) to the single
@@ -19,4 +20,4 @@ import { AssetUri, DeviceType } from '../types/BreakManifest';
19
20
  * This is the single source of truth for `AdPlayerController.resolveUri` and
20
21
  * the native asset-URI resolvers.
21
22
  */
22
- export declare function resolveTargetedUri(uri: string | AssetUri[], deviceType?: DeviceType): string | null;
23
+ export declare function resolveTargetedUri(uri: string | AssetUri[], deviceType?: DeviceType | DeviceProfile, onTargetingIssue?: (issue: TargetingIssue) => void): string | null;
@@ -1 +1 @@
1
- export function resolveTargetedUri(e,r){if("string"==typeof e)return e;if(!Array.isArray(e)||0===e.length)return null;if(r){const t=e.find(e=>e.targeting?.deviceType===r);if(t)return t.value}const t=e.find(e=>void 0===e.targeting?.deviceType);return t?t.value:e[0].value}
1
+ import{matchTargeting as t}from"./matchTargeting";export function resolveTargetedUri(e,r,n){if("string"==typeof e)return e;if(!Array.isArray(e)||0===e.length)return null;const i="string"==typeof r?{deviceType:r}:r??{};for(const r of e){if(!r.targeting||0===Object.keys(r.targeting).length)continue;const e=t(r.targeting,i);if(e.issues.forEach(t=>n?.(t)),e.matches)return r.value}const g=e.find(t=>void 0===t.targeting||0===Object.keys(t.targeting).length);return g?g.value:e[0].value}
@@ -1,5 +1,6 @@
1
1
  import { BreakFormat } from '../types/BreakManifest';
2
2
  import type { Break, BreakVariant } from '../types/BreakManifest';
3
+ import type { DeviceProfile, TargetingIssue } from './matchTargeting';
3
4
  /**
4
5
  * Break-variant selection per the manifest spec's `targeting`, locked across the three cores by
5
6
  * the `variant-selection` conformance fixture. One ordered walk with two filters:
@@ -20,6 +21,8 @@ export declare const KNOWN_BREAK_FORMATS: readonly string[];
20
21
  export interface VariantSelectionInputs {
21
22
  /** Detected device class. Undefined = unknown: targeted variants never match, defaults still qualify. */
22
23
  deviceType?: string | undefined;
24
+ /** Full platform/OS profile; when omitted, selection remains device-type-only. */
25
+ deviceProfile?: DeviceProfile | undefined;
23
26
  /** Formats renderable in the CURRENT context (PiP narrows it, see {@link playableFormatsFor}). Omitted = all renderable. */
24
27
  playableFormats?: readonly string[] | undefined;
25
28
  }
@@ -35,6 +38,7 @@ export interface VariantSelection {
35
38
  * Carried on the result so the selector stays pure; each core diagnoses from its own layer.
36
39
  */
37
40
  skippedUnknownFormats: string[];
41
+ targetingIssues: TargetingIssue[];
38
42
  }
39
43
  /** The playable-format set for the web renderer's current context; native renderers mirror this table. */
40
44
  export declare function playableFormatsFor(context: {
@@ -1 +1 @@
1
- import{BreakFormat as e}from"../types/BreakManifest";export const RENDERABLE_BREAK_FORMATS=[e.SINGLE,e.DOUBLE,e.LSHAPE_AD,e.LSHAPE_CONTENT,e.OVERLAY];export const KNOWN_BREAK_FORMATS=[...RENDERABLE_BREAK_FORMATS];export function playableFormatsFor(t){return t.pictureInPicture?[e.SINGLE,e.LSHAPE_CONTENT]:t.singleSurfaceDevice?RENDERABLE_BREAK_FORMATS.filter(t=>t!==e.DOUBLE):RENDERABLE_BREAK_FORMATS}export function variantsOf(e){const t=e.variant;return Array.isArray(t)?t:t?[t]:[]}function t(e,t){const n=e.targeting?.deviceType;return void 0===n||void 0!==t&&n===t}export function selectVariantDetailed(e,n={}){const r=n.playableFormats??RENDERABLE_BREAK_FORMATS;let i=null,o=null;const E=[];for(const A of variantsOf(e)){if(!t(A,n.deviceType))continue;const e=A.format,a=void 0!==e&&KNOWN_BREAK_FORMATS.includes(e);if(a||E.push(e??""),null===o&&a&&(o=A),null===i&&void 0!==e&&r.includes(e)&&(i=A),null!==i)break}return{selected:i,deviceQualified:o,skippedUnknownFormats:E}}
1
+ import{BreakFormat as e}from"../types/BreakManifest";import{matchTargeting as t}from"./matchTargeting";export const RENDERABLE_BREAK_FORMATS=[e.SINGLE,e.DOUBLE,e.LSHAPE_AD,e.LSHAPE_CONTENT,e.OVERLAY];export const KNOWN_BREAK_FORMATS=[...RENDERABLE_BREAK_FORMATS];export function playableFormatsFor(t){return t.pictureInPicture?[e.SINGLE,e.LSHAPE_CONTENT]:t.singleSurfaceDevice?RENDERABLE_BREAK_FORMATS.filter(t=>t!==e.DOUBLE):RENDERABLE_BREAK_FORMATS}export function variantsOf(e){const t=e.variant;return Array.isArray(t)?t:t?[t]:[]}export function selectVariantDetailed(e,n={}){const r=n.playableFormats??RENDERABLE_BREAK_FORMATS;let i=null,o=null;const E=[],s=[],a=n.deviceProfile??(void 0===n.deviceType?{}:{deviceType:n.deviceType});for(const n of variantsOf(e)){const e=t(n.targeting,a);if(s.push(...e.issues),!e.matches)continue;const c=n.format,A=void 0!==c&&KNOWN_BREAK_FORMATS.includes(c);if(A||E.push(c??""),null===o&&A&&(o=n),null===i&&void 0!==c&&r.includes(c)&&(i=n),null!==i)break}return{selected:i,deviceQualified:o,skippedUnknownFormats:E,targetingIssues:s}}
@@ -0,0 +1 @@
1
+ export declare function toPlaybackError(mediaError: unknown, fallback: string): Error;
@@ -0,0 +1 @@
1
+ export function toPlaybackError(e,r){const o=e;if(o&&("number"==typeof o.code||o.message)){const e="number"==typeof o.code?` (code ${o.code})`:"",n=o.message?`: ${o.message}`:"";return new Error(`${r}${e}${n}`)}return new Error(r)}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * A `wallclock` break's window on the content timeline, placed through the stream's own
3
+ * Program Date Time. Portable brain rule, locked by the conformance harness (`wallclock-break-window`).
4
+ *
5
+ * The manifest names the break in wallclock (`start`, `duration`); the player names positions in
6
+ * currentTime. One sample of both (`currentTime`, `programDateTime`) is the mapping in force, and
7
+ * under it the break starts at `currentTime - (programDateTime - start)` whenever the sample was
8
+ * taken — at the cut, or seconds later when content was only paused once the ad was ready. The
9
+ * resume point follows from the same mapping, so neither depends on when content stopped.
10
+ */
11
+ /** A content-player sample: a position and the stream's Program Date Time at that position. */
12
+ export interface ProgramDateTimeSample {
13
+ /** Content position, seconds of currentTime. */
14
+ currentTime: number;
15
+ /** The stream's Program Date Time at that position, epoch milliseconds. */
16
+ programDateTimeMs: number;
17
+ }
18
+ export interface WallclockBreakWindowInput {
19
+ /** The manifest `start`, epoch milliseconds. */
20
+ startMs: number;
21
+ /** The manifest `duration`, seconds. */
22
+ durationSec: number;
23
+ sample: ProgramDateTimeSample;
24
+ }
25
+ export interface WallclockBreakWindow {
26
+ /** Content time of the manifest start: the resume anchor. */
27
+ anchor: number;
28
+ /** Content time of `start + duration`: where content resumes. */
29
+ resumeTarget: number;
30
+ /** How far past the start the sample sits on the stream clock, seconds; negative before it. */
31
+ sampleLateSec: number;
32
+ }
33
+ /**
34
+ * Content cut this long after the manifest start on the stream clock is reported
35
+ * (DA-BREAK-CUT-LATE): the viewer saw the stream's own break instead of the ad for that long.
36
+ */
37
+ export declare const BREAK_CUT_LATE_TOLERANCE_SEC = 0.5;
38
+ /** Where a wallclock instant sits in currentTime under `sample`'s mapping, seconds (unrounded). */
39
+ export declare function contentTimeForWallclock(sample: ProgramDateTimeSample, wallclockMs: number): number;
40
+ /** Null when any input is not a finite number; otherwise every field at millisecond precision. */
41
+ export declare function resolveWallclockBreakWindow(input: WallclockBreakWindowInput): WallclockBreakWindow | null;
@@ -0,0 +1 @@
1
+ export const BREAK_CUT_LATE_TOLERANCE_SEC=.5;const e=e=>Math.round(1e3*e)/1e3;export function contentTimeForWallclock(e,r){return e.currentTime+(r-e.programDateTimeMs)/1e3}export function resolveWallclockBreakWindow(r){const{startMs:t,durationSec:o,sample:n}=r;if(![t,o,n.currentTime,n.programDateTimeMs].every(e=>Number.isFinite(e)))return null;const c=contentTimeForWallclock(n,t);return{anchor:e(c),resumeTarget:e(c+o),sampleLateSec:e((n.programDateTimeMs-t)/1e3)}}
@@ -1,4 +1,5 @@
1
1
  import { Break, BreakFormat } from '../types/BreakManifest';
2
+ import type { AdBreakSkipReason } from '../types/Events';
2
3
  import { BreakScheduler, BreakStartContext } from '../services/BreakScheduler';
3
4
  import { ManifestService } from '../services/ManifestService';
4
5
  import { AdPlayerController } from '../services/AdPlayerController';
@@ -36,6 +37,7 @@ export declare class BreakLifecycle {
36
37
  emitAdBreakBegin(brk: Break, context?: BreakStartContext): void;
37
38
  /** Flush a deferred (first-frame-gated) `adbreakbegin`, if one is pending. */
38
39
  flushPendingAdBreakBegin(): void;
40
+ onBreakSkipped(brk: Break, reason: AdBreakSkipReason): void;
39
41
  onBreakStart(breakInfo: Break, context: BreakStartContext): Promise<void>;
40
42
  /** A chained break begins (overlay held, content not resumed): emit `adbreakbegin`, promote it to ACTIVE, keep polling. */
41
43
  onChainedBreakBegin(next: Break): void;