@dolby-optiview/ads-sdk-core 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/OptiViewAds.d.ts +5 -4
- package/dist/OptiViewAds.js +1 -1
- package/dist/OptiViewAdsExternalVendors.d.ts +32 -0
- package/dist/external-playback/index.d.ts +4 -0
- package/dist/external-playback/index.js +1 -0
- package/dist/external-playback/observeExternalPlayback.d.ts +19 -0
- package/dist/external-playback/observeExternalPlayback.js +1 -0
- package/dist/external-playback/reconnectHold.d.ts +19 -0
- package/dist/external-playback/reconnectHold.js +1 -0
- package/dist/generated/diagnosticCodes.d.ts +46 -6
- package/dist/generated/diagnosticCodes.js +1 -1
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.js +1 -1
- package/dist/interfaces/AdSourceQueue.d.ts +19 -0
- package/dist/interfaces/AdSourceQueue.js +1 -0
- package/dist/interfaces/PlayerAdapter.d.ts +31 -1
- package/dist/internal/index.d.ts +2 -0
- package/dist/internal/index.js +1 -1
- package/dist/services/AdBreakSequencer.d.ts +33 -4
- package/dist/services/AdBreakSequencer.js +1 -1
- package/dist/services/AdPlayerController.d.ts +12 -73
- package/dist/services/AdPlayerController.js +1 -1
- package/dist/services/BreakChain.d.ts +43 -0
- package/dist/services/BreakChain.js +1 -0
- package/dist/services/BreakChainRecorder.d.ts +63 -0
- package/dist/services/BreakChainRecorder.js +1 -0
- package/dist/services/BreakPreloadLedger.d.ts +13 -0
- package/dist/services/BreakPreloadLedger.js +1 -0
- package/dist/services/BreakScheduler.d.ts +20 -6
- package/dist/services/BreakScheduler.js +1 -1
- package/dist/services/BreakTransitionTimer.d.ts +27 -2
- package/dist/services/BreakTransitionTimer.js +1 -1
- package/dist/services/CmcdAdReporter.d.ts +1 -0
- package/dist/services/CmcdAdReporter.js +1 -1
- package/dist/services/ExternalPlaybackGate.d.ts +40 -0
- package/dist/services/ExternalPlaybackGate.js +1 -0
- package/dist/services/LiveStartPushbackController.d.ts +5 -10
- package/dist/services/LiveStartPushbackController.js +1 -1
- package/dist/services/ManifestService.d.ts +2 -1
- package/dist/services/ManifestService.js +1 -1
- package/dist/services/ReloadGuard.d.ts +11 -0
- package/dist/services/ReloadGuard.js +1 -0
- package/dist/services/RewindRearm.d.ts +3 -1
- package/dist/services/RewindRearm.js +1 -1
- package/dist/services/SsaiController.js +1 -1
- package/dist/services/adPlayer/AdPlayerCallbacks.d.ts +63 -0
- package/dist/services/adPlayer/AdPlayerCallbacks.js +1 -0
- package/dist/services/adPlayer/AdPlayerEvents.d.ts +3 -7
- package/dist/services/adPlayer/AdPlayerEvents.js +1 -1
- package/dist/services/adPlayer/AdPlayerState.d.ts +0 -1
- package/dist/services/adPlayer/AdPlayerState.js +1 -1
- package/dist/services/adPlayer/AdPreloader.d.ts +2 -2
- package/dist/services/adPlayer/AdPreloader.js +1 -1
- package/dist/services/adPlayer/AdStage.d.ts +7 -4
- package/dist/services/adPlayer/AdStage.js +1 -1
- package/dist/services/adPlayer/AssetParameterResolver.d.ts +10 -2
- package/dist/services/adPlayer/AssetParameterResolver.js +1 -1
- package/dist/services/adPlayer/AssetPlayback.d.ts +9 -2
- package/dist/services/adPlayer/AssetPlayback.js +1 -1
- package/dist/services/adPlayer/AudioController.d.ts +58 -0
- package/dist/services/adPlayer/AudioController.js +1 -0
- package/dist/services/adPlayer/BreakSequenceDriver.d.ts +9 -3
- package/dist/services/adPlayer/BreakSequenceDriver.js +1 -1
- package/dist/services/adPlayer/ContentResumeCoordinator.d.ts +2 -0
- package/dist/services/adPlayer/ContentResumeCoordinator.js +1 -1
- package/dist/services/adPlayer/GamPodBridge.d.ts +11 -1
- package/dist/services/adPlayer/GamPodBridge.js +1 -1
- package/dist/services/adPlayer/PauseAdPresenter.d.ts +3 -1
- package/dist/services/adPlayer/PauseAdPresenter.js +1 -1
- package/dist/services/adPlayer/PreparedPresentation.js +1 -1
- package/dist/services/adPlayer/PresentationModeTracker.d.ts +1 -1
- package/dist/services/adPlayer/PresentationModeTracker.js +1 -1
- package/dist/services/adPlayer/SharedElementBreakPlayer.d.ts +3 -0
- package/dist/services/adPlayer/SharedElementBreakPlayer.js +1 -1
- package/dist/services/adPlayer/SuccessorQueue.d.ts +36 -0
- package/dist/services/adPlayer/SuccessorQueue.js +1 -0
- package/dist/services/adPlayer/SurfaceRevealReporter.d.ts +28 -0
- package/dist/services/adPlayer/SurfaceRevealReporter.js +1 -0
- package/dist/services/adPlayer/presentation/DomPresentationHost.d.ts +22 -0
- package/dist/services/adPlayer/presentation/DomPresentationHost.js +1 -0
- package/dist/services/adPlayer/presentation/LayoutTransform.d.ts +28 -0
- package/dist/services/adPlayer/presentation/LayoutTransform.js +1 -0
- package/dist/services/adPlayer/presentation/PresentationLayout.d.ts +19 -0
- package/dist/services/adPlayer/presentation/PresentationLayout.js +1 -0
- package/dist/services/assetParameterMacroTable.js +1 -1
- package/dist/services/assetParameterMacros.d.ts +9 -9
- package/dist/services/assetParameterMacros.js +1 -1
- package/dist/services/channelSwitch.d.ts +2 -2
- package/dist/services/externalPlaybackDiagnostic.d.ts +3 -0
- package/dist/services/externalPlaybackDiagnostic.js +1 -0
- package/dist/services/externalPlaybackGuard.d.ts +24 -0
- package/dist/services/externalPlaybackGuard.js +1 -0
- package/dist/services/parseBreakManifest.d.ts +2 -1
- package/dist/services/parseBreakManifest.js +1 -1
- package/dist/services/resolveChannelEvents.d.ts +17 -0
- package/dist/services/resolveChannelEvents.js +1 -0
- package/dist/services/resolveExternalVendorConfigurations.js +1 -1
- package/dist/services/resolveGamVendorConfiguration.d.ts +6 -1
- package/dist/services/resolveGamVendorConfiguration.js +1 -1
- package/dist/services/ssbStream.d.ts +39 -0
- package/dist/services/ssbStream.js +1 -0
- package/dist/services/ssbSwitchSignals.d.ts +15 -0
- package/dist/services/ssbSwitchSignals.js +1 -0
- package/dist/services/staticAssetUrl.d.ts +28 -0
- package/dist/services/staticAssetUrl.js +1 -0
- package/dist/session/AdBreakStatusTracker.d.ts +18 -0
- package/dist/session/AdBreakStatusTracker.js +1 -1
- package/dist/session/BreakLifecycle.d.ts +6 -0
- package/dist/session/BreakLifecycle.js +1 -1
- package/dist/session/SessionStarter.d.ts +6 -43
- package/dist/session/SessionStarter.js +1 -1
- package/dist/session/SessionStarterDeps.d.ts +39 -0
- package/dist/session/SessionStarterDeps.js +1 -0
- package/dist/session/SessionStarterHelpers.d.ts +36 -0
- package/dist/session/SessionStarterHelpers.js +1 -0
- package/dist/session/SessionState.d.ts +22 -2
- package/dist/session/SessionState.js +1 -1
- package/dist/session/SsbSwitch.d.ts +55 -0
- package/dist/session/SsbSwitch.js +1 -0
- package/dist/session/breakMeasurement.d.ts +16 -0
- package/dist/session/breakMeasurement.js +1 -0
- package/dist/session/contentSnapback.d.ts +13 -0
- package/dist/session/contentSnapback.js +1 -0
- package/dist/session/createExternalPlaybackGate.d.ts +19 -0
- package/dist/session/createExternalPlaybackGate.js +1 -0
- package/dist/session/sessionTick.d.ts +7 -0
- package/dist/session/sessionTick.js +1 -0
- package/dist/types/BreakManifest.d.ts +58 -5
- package/dist/types/Events.d.ts +21 -10
- package/dist/types/ExternalVendorConfiguration.d.ts +3 -1
- package/dist/types/OptiViewAdsConfig.d.ts +24 -8
- package/package.json +5 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PlayerAdapter } from '../../interfaces/PlayerAdapter';
|
|
2
|
-
import {
|
|
2
|
+
import { type PresentationHost } from './presentation/DomPresentationHost';
|
|
3
|
+
import { AssetWithCompanion, Break, BreakVariant, LshapeContentBreakVariant, StaticAsset } from '../../types/BreakManifest';
|
|
3
4
|
export interface AdStageDeps {
|
|
4
5
|
contentPlayer: PlayerAdapter;
|
|
5
6
|
container: HTMLElement;
|
|
@@ -21,7 +22,7 @@ export interface AdStageDeps {
|
|
|
21
22
|
resizeVast: () => void;
|
|
22
23
|
getVariant: (breakInfo: Break) => BreakVariant | null;
|
|
23
24
|
getCurrentBreak: () => Break | null;
|
|
24
|
-
|
|
25
|
+
buildStaticAssetUrl: (asset: StaticAsset, breakInfo: Break | null) => string | null;
|
|
25
26
|
getCompanion: () => HTMLImageElement | HTMLVideoElement | null;
|
|
26
27
|
setCompanion: (element: HTMLImageElement | HTMLVideoElement | null) => void;
|
|
27
28
|
getOverlayImage: () => HTMLImageElement | null;
|
|
@@ -30,6 +31,8 @@ export interface AdStageDeps {
|
|
|
30
31
|
setImageAssetTimer: (timer: ReturnType<typeof setTimeout> | null) => void;
|
|
31
32
|
isChaining: () => boolean;
|
|
32
33
|
log: (...args: unknown[]) => void;
|
|
34
|
+
/** Presentation host seam: places the stage surfaces with normalized transforms. */
|
|
35
|
+
presentationHost: PresentationHost;
|
|
33
36
|
}
|
|
34
37
|
/** Owns the overlay stage geometry, break layout and companions. The content/ad switch is a cut; only the layout reflow glides. */
|
|
35
38
|
export declare class AdStage {
|
|
@@ -52,9 +55,9 @@ export declare class AdStage {
|
|
|
52
55
|
renderLshapeContentBackdrop(variant: LshapeContentBreakVariant): void;
|
|
53
56
|
reapplyAdContainerLayout(variant: BreakVariant): void;
|
|
54
57
|
applyLayout(variant: BreakVariant): void;
|
|
55
|
-
restoreLayout(
|
|
58
|
+
restoreLayout(): void;
|
|
56
59
|
renderCompanion(assets: AssetWithCompanion[], cssText: string): void;
|
|
57
|
-
/** Shows `img` as the stage companion and
|
|
60
|
+
/** Shows `img` as the stage companion and removes the previous one. */
|
|
58
61
|
private installCompanion;
|
|
59
62
|
/** Removes that specific companion element at once (a cut, like the ad surface). */
|
|
60
63
|
private retireCompanion;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{getPreparedPresentation as e}from"./PreparedPresentation";import{AssetType as t,BreakFormat as s}from"../../types/BreakManifest";const i=16/9;export class AdStage{constructor(e){this.deps=e,this.contentVideoGeometryHandler=null,this.contentVideoMutationObserver=null,this.resizeObserver=null,this.resizePoll=null,this.contentLayout=null,this.preparedLease=null,this.unsubscribeGeometry=null}setupOverlay(){const t=e(this.deps.container);if(t){const e=this.deps.getPlayerContainer();if(e&&e!==t.playerContainer)throw new Error("The prepared content container cannot be replaced");this.preparedLease=t.acquire(),this.deps.setStage(this.preparedLease.stage),this.deps.setPlayerContainer(this.preparedLease.playerContainer),this.unsubscribeGeometry=this.preparedLease.subscribe(()=>this.deps.resizeVast());try{this.setupMediaLayer()}catch(e){const t=this.deps.getAdContainer();throw t?.parentNode?.removeChild(t),this.deps.setAdContainer(null),this.deps.setAdMediaContainer(null),this.unsubscribeGeometry?.(),this.preparedLease.release(),e}return}const s=window.getComputedStyle(this.deps.container);"static"!==s.position&&""!==s.position||(this.deps.container.style.position="relative"),this.deps.setStage(document.createElement("div"));const i=this.deps.getStage();if(!i)return;if(i.className="dolby-stage",i.style.cssText="position:absolute;overflow:hidden;",this.deps.container.appendChild(i),this.contentVideoGeometryHandler=()=>this.refreshStageGeometry(),"undefined"!=typeof MutationObserver&&(this.contentVideoMutationObserver=new MutationObserver(()=>{(this.deps.contentPlayer.videoElement??null)!==this.deps.getContentVideo()&&this.refreshStageGeometry()}),this.contentVideoMutationObserver.observe(this.deps.container,{childList:!0,subtree:!0})),"undefined"!=typeof ResizeObserver)this.resizeObserver=new ResizeObserver(()=>this.refreshStageGeometry()),this.resizeObserver.observe(this.deps.container);else{let e=this.deps.container.getBoundingClientRect();this.resizePoll=setInterval(()=>{const t=this.deps.container.getBoundingClientRect();t.width===e.width&&t.height===e.height||(e=t,this.refreshStageGeometry())},250)}if(this.refreshStageGeometry(),this.deps.getPlayerContainer()){const e=this.deps.getPlayerContainer();if(!e)return;i.appendChild(e)}else{const e=document.createElement("div"),t=Array.from(this.deps.container.childNodes).filter(e=>e!==i);for(const s of t)e.appendChild(s);i.appendChild(e),this.deps.setPlayerContainer(e),this.deps.onPlayerContainerCreated()}const n=this.deps.getPlayerContainer();n&&(this.deps.setPlayerContainerOriginalStyle(n.style.cssText),n.style.cssText="position:absolute;top:0;left:0;right:0;bottom:0;transition:top 0.3s ease-in-out,left 0.3s ease-in-out,right 0.3s ease-in-out,bottom 0.3s ease-in-out;",this.setupMediaLayer())}setupMediaLayer(){const e=this.deps.getStage();if(!e)throw new Error("An ads presentation stage is required");this.deps.setAdContainer(document.createElement("div"));const t=this.deps.getAdContainer();if(!t)return;t.className="dolby-ad-container",t.style.cssText="display:none;position:absolute;top:0;left:0;right:0;bottom:0;z-index:100;background:#000;",this.deps.setAdMediaContainer(document.createElement("div"));const s=this.deps.getAdMediaContainer();if(!s)return;s.style.cssText="display:block;width:100%;height:100%;",t.appendChild(s),e.appendChild(t);const i=this.deps.createAdAdapter(s);i!==this.deps.contentPlayer&&i.prepareForAdPlayback?.(),this.deps.setAdPlayer(i),this.deps.setupEventListeners(),this.deps.log("Ad player overlay initialized")}refreshStageGeometry(){this.rebindContentVideoGeometryListener(),this.updateStageGeometry(),this.deps.resizeVast()}rebindContentVideoGeometryListener(){const e=this.deps.contentPlayer.videoElement??null,t=e&&"function"==typeof e.addEventListener&&"function"==typeof e.removeEventListener?e:null;t!==this.deps.getContentVideo()&&(this.deps.getContentVideo()&&this.contentVideoGeometryHandler&&(this.deps.getContentVideo()?.removeEventListener("loadedmetadata",this.contentVideoGeometryHandler),this.deps.getContentVideo()?.removeEventListener("resize",this.contentVideoGeometryHandler)),this.deps.setContentVideo(t),t&&this.contentVideoGeometryHandler&&(t.addEventListener("loadedmetadata",this.contentVideoGeometryHandler),t.addEventListener("resize",this.contentVideoGeometryHandler)))}updateStageGeometry(){const e=this.deps.getStage();if(!e)return;const{width:t,height:s}=this.deps.container.getBoundingClientRect();if(0===t||0===s)return;const i=this.getContentAspectRatio();let n,o;t/s>i?(o=s,n=s*i):(n=t,o=t/i);const r=(s-o)/2,a=(t-n)/2;e.style.width=`${n}px`,e.style.height=`${o}px`,e.style.top=`${r}px`,e.style.left=`${a}px`}getContentAspectRatio(){const e=this.deps.contentPlayer.videoElement;if(!e)return i;const{videoWidth:t,videoHeight:s}=e;return!Number.isFinite(t)||!Number.isFinite(s)||t<=0||s<=0?i:t/s}renderLshapeContentBackdrop(e){const s=e.assets[0];if(!s||s.type!==t.STATIC)return;const i=this.deps.resolveUri(s.uri);if(!i)return;const n=document.createElement("img");n.className="dolby-companion",n.src=i,n.style.cssText="position:absolute;top:0;left:0;right:0;bottom:0;z-index:99;object-fit:cover;width:100%;height:100%;opacity:0;";const o=this.deps.getStage();o&&(this.installCompanion(n,o),this.deps.log("Rendered lshape_content backdrop:",i))}reapplyAdContainerLayout(e){const t=this.deps.getAdContainer();if(t){switch(e.format){case s.LSHAPE_AD:t.style.position="absolute",t.style.top="5%",t.style.left="5%",t.style.right="30%",t.style.bottom="30%",t.style.width="",t.style.height="",t.style.zIndex="100";break;case s.DOUBLE:t.style.position="absolute",t.style.top="26.04%",t.style.left="51.04%",t.style.right="1.04%",t.style.bottom="26.04%",t.style.width="",t.style.height="",t.style.zIndex="100"}this.deps.log("reapplyAdContainerLayout: re-stamped adContainer CSS for format:",e.format)}}applyLayout(e){const t=this.deps.getAdContainer(),i=this.deps.getPlayerContainer();if(!t||!i)return;const n="transition:top 0.3s ease-in-out,left 0.3s ease-in-out,right 0.3s ease-in-out,bottom 0.3s ease-in-out;";switch(e.format){case s.SINGLE:this.contentLayout&&(i.style.opacity="0"),t.style.cssText="display:none;opacity:0;position:absolute;top:0;left:0;right:0;bottom:0;z-index:100;background:#000;";break;case s.DOUBLE:i.style.cssText=`position:absolute;top:26.04%;left:1.04%;right:51.04%;bottom:26.04%;z-index:101;${n}`,t.style.cssText="display:none;opacity:0;position:absolute;top:26.04%;left:51.04%;right:1.04%;bottom:26.04%;z-index:100;background:#000;",this.renderCompanion(e.assets,"position:absolute;top:0;left:0;right:0;bottom:0;z-index:99;object-fit:cover;width:100%;height:100%;opacity:0;"),this.contentLayout=e.format;break;case s.LSHAPE_AD:i.style.cssText=`position:absolute;top:5%;left:5%;right:30%;bottom:30%;z-index:101;opacity:0;${n}`,t.style.cssText="display:none;opacity:0;position:absolute;top:5%;left:5%;right:30%;bottom:30%;z-index:100;background:#000;",this.renderCompanion(e.assets,"position:absolute;top:0;left:0;right:0;bottom:0;z-index:99;object-fit:cover;width:100%;height:100%;opacity:0;"),this.contentLayout=e.format;break;case s.LSHAPE_CONTENT:i.style.top="5%",i.style.left="5%",i.style.right="30%",i.style.bottom="30%",i.style.zIndex="101",this.contentLayout=e.format;break;case s.OVERLAY:{const s=e,i=e=>void 0!==e?100*e+"%":"auto",n=i(s.position.top),o=i(s.position.bottom),r=i(s.position.left),a=i(s.position.right),d=100*s.size.width+"%",l=100*s.size.height+"%",p=s.opacity??1;t.style.cssText=`display:none;position:absolute;top:${n};bottom:${o};left:${r};right:${a};width:${d};height:${l};z-index:100;opacity:${p};`;break}default:t.style.cssText="display:none;position:absolute;top:0;left:0;right:0;bottom:0;z-index:100;background:#000;"}}restoreLayout(e){if(this.preparedLease&&!this.preparedLease.isActive())return;const t=this.deps.getPlayerContainer();if(!t)return;const i=function(e){return e===s.DOUBLE||e===s.LSHAPE_AD||e===s.LSHAPE_CONTENT}(e)||null!==this.contentLayout;this.contentLayout=null,t.style.cssText="position:absolute;top:0;left:0;right:0;bottom:0;"+(i?"z-index:101;":"")+"transition:top 0.3s ease-in-out,left 0.3s ease-in-out,right 0.3s ease-in-out,bottom 0.3s ease-in-out;";const n=this.deps.getCompanion();n&&(this.deps.setCompanion(null),this.retireCompanion(n)),i&&(t.style.zIndex="")}renderCompanion(e,s){const i=e[0]?.companion;if(!i)return;const n=i.type===t.STATIC?this.deps.resolveUri(i.uri):null;if(!n)return;const o=document.createElement("img");o.className="dolby-companion",o.src=n,o.style.cssText=s;const r=this.deps.getStage();r&&this.installCompanion(o,r)}installCompanion(e,t){const s=this.deps.getCompanion();t.appendChild(e),this.deps.setCompanion(e),e.style.opacity="1",s&&s!==e&&this.retireCompanion(s)}retireCompanion(e){e.parentElement?.removeChild(e)}clearOverlayImage(){const e=this.deps.getImageAssetTimer();e&&(clearTimeout(e),this.deps.setImageAssetTimer(null));const t=this.deps.getOverlayImage();t?.parentElement&&t.parentElement.removeChild(t),this.deps.setOverlayImage(null)}show(){const e=this.deps.getAdContainer();if(!e)return;const t=this.deps.getCurrentBreak(),i=t?this.deps.getVariant(t):null;e.style.display="block",i?.format!==s.OVERLAY&&(e.style.opacity="1")}hide(){const e=this.deps.getAdContainer();e&&(e.style.opacity="0",e.style.display="none")}restoreContent(e,t){if(this.releasePreparedPresentation())return!0;const s=this.deps.getPlayerContainer();if(!s)return!1;if(e)for(;s.firstChild;)this.deps.container.appendChild(s.firstChild);else this.deps.container.appendChild(s),s.style.cssText=t;return!1}releasePreparedPresentation(){return!!this.preparedLease&&(this.unsubscribeGeometry?.(),this.unsubscribeGeometry=null,this.preparedLease.release(),!0)}destroy(){this.unsubscribeGeometry?.(),this.unsubscribeGeometry=null;const e=this.deps.getContentVideo();e&&this.contentVideoGeometryHandler&&(e.removeEventListener("loadedmetadata",this.contentVideoGeometryHandler),e.removeEventListener("resize",this.contentVideoGeometryHandler)),this.contentVideoMutationObserver?.disconnect(),this.resizeObserver?.disconnect(),null!==this.resizePoll&&clearInterval(this.resizePoll),this.resizePoll=null,this.contentVideoMutationObserver=null,this.resizeObserver=null,this.contentVideoGeometryHandler=null,this.contentLayout=null}}
|
|
1
|
+
import{getPreparedPresentation as e}from"./PreparedPresentation";import{LAYOUT_TRANSITION as t}from"./presentation/LayoutTransform";import{presentationLayoutFor as s,RESTORED_CONTENT_PLACEMENT as i}from"./presentation/PresentationLayout";import{AssetType as n,BreakFormat as o}from"../../types/BreakManifest";const r=16/9;export class AdStage{constructor(e){this.deps=e,this.contentVideoGeometryHandler=null,this.contentVideoMutationObserver=null,this.resizeObserver=null,this.resizePoll=null,this.contentLayout=null,this.preparedLease=null,this.unsubscribeGeometry=null}setupOverlay(){const s=e(this.deps.container);if(s){const e=this.deps.getPlayerContainer();if(e&&e!==s.playerContainer)throw new Error("The prepared content container cannot be replaced");this.preparedLease=s.acquire(),this.deps.setStage(this.preparedLease.stage),this.deps.setPlayerContainer(this.preparedLease.playerContainer),this.unsubscribeGeometry=this.preparedLease.subscribe(()=>this.deps.resizeVast());try{this.setupMediaLayer()}catch(e){const t=this.deps.getAdContainer();throw t?.parentNode?.removeChild(t),this.deps.setAdContainer(null),this.deps.setAdMediaContainer(null),this.unsubscribeGeometry?.(),this.preparedLease.release(),e}return}const i=window.getComputedStyle(this.deps.container);"static"!==i.position&&""!==i.position||(this.deps.container.style.position="relative"),this.deps.setStage(document.createElement("div"));const n=this.deps.getStage();if(!n)return;if(n.className="dolby-stage",n.style.cssText="position:absolute;overflow:hidden;",this.deps.container.appendChild(n),this.contentVideoGeometryHandler=()=>this.refreshStageGeometry(),"undefined"!=typeof MutationObserver&&(this.contentVideoMutationObserver=new MutationObserver(()=>{(this.deps.contentPlayer.videoElement??null)!==this.deps.getContentVideo()&&this.refreshStageGeometry()}),this.contentVideoMutationObserver.observe(this.deps.container,{childList:!0,subtree:!0})),"undefined"!=typeof ResizeObserver)this.resizeObserver=new ResizeObserver(()=>this.refreshStageGeometry()),this.resizeObserver.observe(this.deps.container);else{let e=this.deps.container.getBoundingClientRect();this.resizePoll=setInterval(()=>{const t=this.deps.container.getBoundingClientRect();t.width===e.width&&t.height===e.height||(e=t,this.refreshStageGeometry())},250)}if(this.refreshStageGeometry(),this.deps.getPlayerContainer()){const e=this.deps.getPlayerContainer();if(!e)return;n.appendChild(e)}else{const e=document.createElement("div"),t=Array.from(this.deps.container.childNodes).filter(e=>e!==n);for(const s of t)e.appendChild(s);n.appendChild(e),this.deps.setPlayerContainer(e),this.deps.onPlayerContainerCreated()}const o=this.deps.getPlayerContainer();o&&(this.deps.setPlayerContainerOriginalStyle(o.style.cssText),o.style.cssText=`position:absolute;top:0;left:0;right:0;bottom:0;transition:${t};`,this.setupMediaLayer())}setupMediaLayer(){const e=this.deps.getStage();if(!e)throw new Error("An ads presentation stage is required");this.deps.setAdContainer(document.createElement("div"));const s=this.deps.getAdContainer();if(!s)return;s.className="dolby-ad-container",s.style.cssText=`display:none;position:absolute;top:0;left:0;right:0;bottom:0;z-index:100;background:#000;transition:${t};`,this.deps.setAdMediaContainer(document.createElement("div"));const i=this.deps.getAdMediaContainer();if(!i)return;i.style.cssText="display:block;width:100%;height:100%;",s.appendChild(i),e.appendChild(s);const n=this.deps.createAdAdapter(i);n!==this.deps.contentPlayer&&(n.prepareForAdPlayback?.(),n.setExternalPlaybackAllowed?.(!1)),this.deps.setAdPlayer(n),this.deps.setupEventListeners(),this.deps.log("Ad player overlay initialized")}refreshStageGeometry(){this.rebindContentVideoGeometryListener(),this.updateStageGeometry(),this.deps.resizeVast()}rebindContentVideoGeometryListener(){const e=this.deps.contentPlayer.videoElement??null,t=e&&"function"==typeof e.addEventListener&&"function"==typeof e.removeEventListener?e:null;t!==this.deps.getContentVideo()&&(this.deps.getContentVideo()&&this.contentVideoGeometryHandler&&(this.deps.getContentVideo()?.removeEventListener("loadedmetadata",this.contentVideoGeometryHandler),this.deps.getContentVideo()?.removeEventListener("resize",this.contentVideoGeometryHandler)),this.deps.setContentVideo(t),t&&this.contentVideoGeometryHandler&&(t.addEventListener("loadedmetadata",this.contentVideoGeometryHandler),t.addEventListener("resize",this.contentVideoGeometryHandler)))}updateStageGeometry(){const e=this.deps.getStage();if(!e)return;const{width:t,height:s}=this.deps.container.getBoundingClientRect();if(0===t||0===s)return;const i=this.getContentAspectRatio();let n,o;t/s>i?(o=s,n=s*i):(n=t,o=t/i);const r=(s-o)/2,a=(t-n)/2;e.style.width=`${n}px`,e.style.height=`${o}px`,e.style.top=`${r}px`,e.style.left=`${a}px`}getContentAspectRatio(){const e=this.deps.contentPlayer.videoElement;if(!e)return r;const{videoWidth:t,videoHeight:s}=e;return!Number.isFinite(t)||!Number.isFinite(s)||t<=0||s<=0?r:t/s}renderLshapeContentBackdrop(e){const t=e.assets[0];if(!t||t.type!==n.STATIC)return;const s=this.deps.buildStaticAssetUrl(t,this.deps.getCurrentBreak());if(!s)return;const i=document.createElement("img");i.className="dolby-companion",i.src=s,i.style.cssText="position:absolute;top:0;left:0;right:0;bottom:0;z-index:99;object-fit:cover;width:100%;height:100%;opacity:0;";const o=this.deps.presentationHost.getSurfaces()?.backdrop;o&&(this.installCompanion(i,o),this.deps.log("Rendered lshape_content backdrop:",s))}reapplyAdContainerLayout(e){const t=this.deps.getAdContainer();if(!t)return;const i=s(e.format);i&&(e.format!==o.LSHAPE_AD&&e.format!==o.DOUBLE||(t.style.zIndex="100"),this.deps.presentationHost.placeAd(i.ad),this.deps.log("reapplyAdContainerLayout: re-stamped adContainer CSS for format:",e.format))}applyLayout(e){const i=this.deps.getAdContainer(),n=this.deps.getPlayerContainer();if(!i||!n)return;const r=s(e.format);if(r){if(e.format!==o.LSHAPE_CONTENT){const e=i.style;e.display="none",e.opacity="0",e.position="absolute",e.top="0",e.left="0",e.right="0",e.bottom="0",e.width="",e.height="",e.zIndex="100",e.background="#000",e.transition=t}return((a=e.format)===o.DOUBLE||a===o.LSHAPE_AD||a===o.LSHAPE_CONTENT)&&(n.style.zIndex="101",this.contentLayout=e.format),e.format===o.LSHAPE_AD?n.style.opacity="0":e.format===o.DOUBLE&&(n.style.opacity=""),e.format===o.SINGLE?this.contentLayout&&(n.style.opacity="0"):this.deps.presentationHost.placeContent(r.content),e.format!==o.LSHAPE_CONTENT&&this.deps.presentationHost.placeAd(r.ad),void((e.format===o.DOUBLE||e.format===o.LSHAPE_AD)&&this.renderCompanion(e.assets,"position:absolute;top:0;left:0;right:0;bottom:0;z-index:99;object-fit:cover;width:100%;height:100%;opacity:0;"))}var a;switch(e.format){case o.OVERLAY:{const t=e,s=e=>void 0!==e?100*e+"%":"auto",n=s(t.position.top),o=s(t.position.bottom),r=s(t.position.left),a=s(t.position.right),d=100*t.size.width+"%",p=100*t.size.height+"%",l=t.opacity??1;i.style.cssText=`display:none;position:absolute;top:${n};bottom:${o};left:${r};right:${a};width:${d};height:${p};z-index:100;opacity:${l};`;break}default:i.style.cssText="display:none;position:absolute;top:0;left:0;right:0;bottom:0;z-index:100;background:#000;"}}restoreLayout(){if(this.preparedLease&&!this.preparedLease.isActive())return;const e=this.deps.getPlayerContainer();if(!e)return;this.contentLayout=null,e.style.zIndex="",e.style.opacity="",this.deps.presentationHost.placeContent(i);const t=this.deps.getCompanion();t&&(this.deps.setCompanion(null),this.retireCompanion(t))}renderCompanion(e,t){const s=e[0]?.companion;if(!s)return;const i=s.type===n.STATIC?this.deps.buildStaticAssetUrl(s,this.deps.getCurrentBreak()):null;if(!i)return;const o=document.createElement("img");o.className="dolby-companion",o.src=i,o.style.cssText=t;const r=this.deps.presentationHost.getSurfaces()?.backdrop;r&&this.installCompanion(o,r)}installCompanion(e,t){const s=this.deps.getCompanion();t.appendChild(e),this.deps.setCompanion(e),e.style.opacity="1",s&&s!==e&&this.retireCompanion(s)}retireCompanion(e){e.parentElement?.removeChild(e)}clearOverlayImage(){const e=this.deps.getImageAssetTimer();e&&(clearTimeout(e),this.deps.setImageAssetTimer(null));const t=this.deps.getOverlayImage();t?.parentElement&&t.parentElement.removeChild(t),this.deps.setOverlayImage(null)}show(){const e=this.deps.getAdContainer();if(!e)return;const t=this.deps.getCurrentBreak(),s=t?this.deps.getVariant(t):null;e.style.display="block",s?.format!==o.OVERLAY&&(e.style.opacity="1")}hide(){const e=this.deps.getAdContainer();e&&(e.style.opacity="0",e.style.display="none")}restoreContent(e,t){if(this.deps.presentationHost.cancelPendingTransitions(),this.releasePreparedPresentation())return!0;const s=this.deps.getPlayerContainer();if(!s)return!1;if(e)for(;s.firstChild;)this.deps.container.appendChild(s.firstChild);else this.deps.container.appendChild(s),s.style.cssText=t;return!1}releasePreparedPresentation(){return!!this.preparedLease&&(this.unsubscribeGeometry?.(),this.unsubscribeGeometry=null,this.preparedLease.release(),!0)}destroy(){this.deps.presentationHost.cancelPendingTransitions(),this.unsubscribeGeometry?.(),this.unsubscribeGeometry=null;const e=this.deps.getContentVideo();e&&this.contentVideoGeometryHandler&&(e.removeEventListener("loadedmetadata",this.contentVideoGeometryHandler),e.removeEventListener("resize",this.contentVideoGeometryHandler)),this.contentVideoMutationObserver?.disconnect(),this.resizeObserver?.disconnect(),null!==this.resizePoll&&clearInterval(this.resizePoll),this.resizePoll=null,this.contentVideoMutationObserver=null,this.resizeObserver=null,this.contentVideoGeometryHandler=null,this.contentLayout=null}}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PlayerAdapter } from '../../interfaces/PlayerAdapter';
|
|
2
2
|
import type { AssetUri, Break, VastAsset } from '../../types/BreakManifest';
|
|
3
|
+
import type { StaticAsset } from '../../types/BreakManifest';
|
|
3
4
|
import { AssetParameterMacros } from '../assetParameterMacros';
|
|
4
5
|
import type { GamStreamManager } from '../GamStreamManager';
|
|
5
6
|
import type { AdPlayerState } from './AdPlayerState';
|
|
@@ -29,18 +30,25 @@ export declare class AssetParameterResolver {
|
|
|
29
30
|
/**
|
|
30
31
|
* Substitute the `$` macros in an effective (already layered) parameter map.
|
|
31
32
|
* Precedence per macro name: session macros, then the built-ins; a customer
|
|
32
|
-
* macro returning
|
|
33
|
-
* marks the parameter empty.
|
|
33
|
+
* macro returning an empty value omits the parameter.
|
|
34
34
|
* Every unresolved macro is sent literally and reported as
|
|
35
35
|
* `DA-ASSET-MACRO-UNKNOWN`.
|
|
36
36
|
*/
|
|
37
37
|
private resolveAssetParameterMacros;
|
|
38
|
+
private diagnoseApplied;
|
|
38
39
|
resolveSessionAdTagParameters(): Record<string, string> | undefined;
|
|
40
|
+
resolveSessionAdTagParametersForManifest: (manifestAssetParameters: Record<string, string> | undefined) => Record<string, string> | undefined;
|
|
39
41
|
/**
|
|
40
42
|
* The VAST tag URL with the effective asset parameters appended. Preload and
|
|
41
43
|
* playback both go through here so the two URLs are byte-identical.
|
|
42
44
|
*/
|
|
43
45
|
buildVastTagUrl(asset: VastAsset, breakInfo: Break): string | null;
|
|
46
|
+
/**
|
|
47
|
+
* The request URL of a static asset (media, image, companion, backdrop or pause ad):
|
|
48
|
+
* the application layers and the asset's own parameters, without the manifest
|
|
49
|
+
* session layer. Preload and playback both go through here so the URLs match.
|
|
50
|
+
*/
|
|
51
|
+
buildStaticAssetUrl(asset: StaticAsset, breakInfo: Break | null): string | null;
|
|
44
52
|
/**
|
|
45
53
|
* IMA's `replaceAdTagParameters` replaces the whole map, so every push carries the full
|
|
46
54
|
* effective map. `undefined` falls back to the session baseline (the restore at break end).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{isVendorAsset as e}from"../../types/BreakManifest";import{appendQueryParameters as s,applyAssetParameterMacros as t}from"../assetParameterMacros";import{buildAssetParameterMacroTable as a}from"../assetParameterMacroTable";import{objectEntries as r}from"../runtimeObject";import{resolveAssetParameters as i}from"../resolveAssetParameters";import{detectDeviceProfile as o}from"../detectDeviceType";import{resolveTargetedUri as n}from"../resolveTargetedUri";import{
|
|
1
|
+
import{isVendorAsset as e}from"../../types/BreakManifest";import{appendQueryParameters as s,applyAssetParameterMacros as t}from"../assetParameterMacros";import{buildAssetParameterMacroTable as a}from"../assetParameterMacroTable";import{objectEntries as r}from"../runtimeObject";import{resolveAssetParameters as i}from"../resolveAssetParameters";import{detectDeviceProfile as o}from"../detectDeviceType";import{resolveTargetedUri as n}from"../resolveTargetedUri";import{buildStaticAssetUrl as d}from"../staticAssetUrl";import{resolveExternalVendorParameters as m}from"../resolveExternalVendorConfigurations";function c(e,s){if(e&&0===Object.keys(e).length&&(e=void 0),s&&0===Object.keys(s).length&&(s=void 0),e===s)return!0;if(!e||!s)return!1;const t=Object.keys(e);return t.length===Object.keys(s).length&&t.every(t=>e[t]===s[t])}export class AssetParameterResolver{constructor(e,s){this.state=e,this.deps=s,this.resolveSessionAdTagParametersForManifest=e=>{const s=i(this.state.sessionAssetParameters,this.state.updatedAssetParameters,e,void 0);return this.resolveAssetParameterMacros(s,void 0,"session",{breakId:void 0,assetId:void 0})}}setSessionAssetParameters(e){this.state.sessionAssetParameters=e}setManifestAssetParameters(s){if(c(this.state.manifestAssetParameters,s))return;if(this.state.manifestAssetParameters=s,!this.deps.getGamStreamManager()?.isInitialized())return void this.deps.log("Manifest asset parameters stored before GAM initialization");const t=this.state.gamPodAsset;this.pushAssetParameters(t&&e(t)?t.assetParameters:void 0,t?.id),this.deps.diagnose("DA-ASSET-PARAMETERS-UPDATED","Manifest asset parameters changed; IMA targeting updated.",{source:"manifest",keys:Object.keys(s??{}).sort()})}updateAssetParameters(e){this.state.updatedAssetParameters=e,this.deps.getGamStreamManager()?.isInitialized()?this.pushAssetParameters(void 0):this.deps.log("updateAssetParameters: GAM not initialized, stored for VAST only")}updateAssetParameterMacros(e){const s={...this.state.sessionAssetParameterMacros};for(const[t,a]of r(e))void 0===a?delete s[t]:s[t]=a;this.state.sessionAssetParameterMacros=s,this.deps.diagnose("DA-ASSET-PARAMETERS-UPDATED","Asset parameter macros updated.",{source:"macros",keys:Object.keys(e).sort()}),this.deps.getGamStreamManager()?.isInitialized()?this.pushAssetParameters(void 0):this.deps.log("updateAssetParameterMacros: GAM not initialized, stored for VAST only")}setSessionAssetParameterMacros(e){this.state.sessionAssetParameterMacros={...e}}getSessionAssetParameterMacros(){return{...this.state.sessionAssetParameterMacros}}resolveExternalVendorConfiguration(e){const s=m(e,i(this.state.sessionAssetParameters,this.state.updatedAssetParameters,void 0,void 0),a({customerMacros:this.state.sessionAssetParameterMacros,element:this.deps.container,breakDurationSec:void 0}));for(const{key:e,macro:t}of s.unresolved)this.deps.diagnose("DA-ASSET-MACRO-UNKNOWN",`Asset parameter '${e}' sent with literal macro '${t}': the macro has no value.`,{key:e,macro:t});for(const{key:e,macro:t}of s.omitted)this.deps.diagnose("DA-ASSET-PARAMETER-OMITTED",`Asset parameter '${e}' not sent: macro '${t}' is empty.`,{key:e,macro:t});return s.configuration}resolveAssetParameterMacros(e,s,r,i){const o=t(e,a({customerMacros:this.state.sessionAssetParameterMacros,element:this.deps.container,breakDurationSec:"session"===r?void 0:s}));return this.diagnoseApplied(o,i),o.params}diagnoseApplied(e,s){for(const{key:t,macro:a}of e.unresolved)this.deps.diagnose("DA-ASSET-MACRO-UNKNOWN",`Asset parameter '${t}' sent with literal macro '${a}': the macro has no value.`,{...s,key:t,macro:a});for(const{key:t,macro:a,pair:r}of e.omitted)this.deps.diagnose("DA-ASSET-PARAMETER-OMITTED",`Asset parameter '${t}' not sent: macro '${a}' is empty.`,{...s,key:t,macro:a,...void 0!==r?{pair:r}:{}})}resolveSessionAdTagParameters(){return this.resolveSessionAdTagParametersForManifest(this.state.manifestAssetParameters)}buildVastTagUrl(e,t){const a=this.resolveUri(e.uri);if(!a)return null;const r=i(this.state.sessionAssetParameters,this.state.updatedAssetParameters,this.state.manifestAssetParameters,e.assetParameters),o=e.duration??this.deps.getEffectiveBreakDuration()??t.duration,n=this.resolveAssetParameterMacros(r,o,"break",{breakId:t.id,assetId:e.id});return s(a,n)}buildStaticAssetUrl(e,s){const t=this.resolveUri(e.uri);if(!t)return null;const r=e.duration??this.deps.getEffectiveBreakDuration()??s?.duration,i=d(t,{sessionConfigParameters:this.state.sessionAssetParameters,updateParameters:this.state.updatedAssetParameters,assetParameters:e.assetParameters},a({customerMacros:this.state.sessionAssetParameterMacros,element:this.deps.container,breakDurationSec:r}));return this.diagnoseApplied(i.applied,{breakId:s?.id,assetId:e.id}),i.url}pushAssetParameters(e,s){const t=this.deps.getGamStreamManager();if(!t?.isInitialized())return;const a=this.deps.getCurrentBreak(),r=i(this.state.sessionAssetParameters,this.state.updatedAssetParameters,this.state.manifestAssetParameters,e),o=this.resolveAssetParameterMacros(r,this.deps.getEffectiveBreakDuration()??a?.duration,"break",{breakId:a?.id,assetId:s});null!==this.state.pushedAssetParameters&&c(this.state.pushedAssetParameters,o)||(this.state.pushedAssetParameters=o,t.replaceAdTagParameters(o??{}))}resolveUri(e){if(void 0===this.state.detectedDeviceProfile){const e=o();void 0!==this.state.detectedDeviceType?e.deviceType=this.state.detectedDeviceType:this.state.detectedDeviceType=e.deviceType,this.state.detectedDeviceProfile=e}return n(e,this.state.detectedDeviceProfile??this.state.detectedDeviceType,e=>this.deps.diagnose(e.code,`Targeting ${e.field} was rejected.`,{field:e.field,...void 0===e.value?{}:{value:e.value}}))}}
|
|
@@ -2,6 +2,7 @@ import type { PlayerAdapter } from '../../interfaces/PlayerAdapter';
|
|
|
2
2
|
import { Asset, Break, BreakVariant, StaticAsset, VastAsset, VendorAsset } from '../../types/BreakManifest';
|
|
3
3
|
import type { AdBreakSequencer, AdBreakTransition } from '../AdBreakSequencer';
|
|
4
4
|
import { type AdPlayerState } from './AdPlayerState';
|
|
5
|
+
import type { SurfaceRevealReporter } from './SurfaceRevealReporter';
|
|
5
6
|
export interface AssetPlaybackDeps {
|
|
6
7
|
getAdPlayer: () => PlayerAdapter | null;
|
|
7
8
|
getAdContainer: () => HTMLDivElement | null;
|
|
@@ -15,7 +16,7 @@ export interface AssetPlaybackDeps {
|
|
|
15
16
|
};
|
|
16
17
|
getSequencer: () => AdBreakSequencer | null;
|
|
17
18
|
debug: boolean;
|
|
18
|
-
|
|
19
|
+
buildStaticAssetUrl: (asset: StaticAsset, breakInfo: Break | null) => string | null;
|
|
19
20
|
buildGamUrl: (asset: VendorAsset, breakDuration: number) => Promise<string | null>;
|
|
20
21
|
buildVastTagUrl: (asset: VastAsset, breakInfo: Break) => string | null;
|
|
21
22
|
onAdError: (error: Error) => void;
|
|
@@ -23,6 +24,8 @@ export interface AssetPlaybackDeps {
|
|
|
23
24
|
drive: (transition: AdBreakTransition) => Promise<void>;
|
|
24
25
|
clearOverlayImage: () => void;
|
|
25
26
|
revealFirstFrame: () => void;
|
|
27
|
+
/** Watches for the ad's first frame on screen (`DA-AD-SURFACE-REVEALED`). */
|
|
28
|
+
surfaceReveal: SurfaceRevealReporter;
|
|
26
29
|
transferPictureInPictureToAd: () => Promise<void>;
|
|
27
30
|
isGamPodAsset: (asset: Asset) => boolean;
|
|
28
31
|
/** Whether the GAM pod asset was skipped because its identity mismatches the stream session. */
|
|
@@ -31,6 +34,10 @@ export interface AssetPlaybackDeps {
|
|
|
31
34
|
recordAdProgress: (currentTime: number) => void;
|
|
32
35
|
reportPhase: (name: string) => void;
|
|
33
36
|
armAdStartBudget: (asset: Asset) => void;
|
|
37
|
+
/** The ad queued behind the previous ad: true when it became the current source. */
|
|
38
|
+
takeQueuedSuccessor: (breakId: string, assetId: string, uri: string) => boolean;
|
|
39
|
+
/** Queue the playing break's next ad behind the ad that just started. */
|
|
40
|
+
queueUpcomingAd: () => void;
|
|
34
41
|
remainingAdStartBudgetMs: () => number;
|
|
35
42
|
loadWithinBudget: (startLoad: () => Promise<void>, context: {
|
|
36
43
|
breakId: string;
|
|
@@ -71,7 +78,7 @@ export declare class AssetPlayback {
|
|
|
71
78
|
playVastAsset(asset: VastAsset, index: number, total: number): void;
|
|
72
79
|
playImageAsset(asset: Asset, index: number, total: number): Promise<void>;
|
|
73
80
|
onImageAssetEnded(): void;
|
|
74
|
-
resolveAssetUri(asset: Asset, breakDuration: number): Promise<string | null>;
|
|
81
|
+
resolveAssetUri(asset: Asset, breakDuration: number, breakInfo?: Break | null): Promise<string | null>;
|
|
75
82
|
/**
|
|
76
83
|
* `duration` is Infinity for live and NaN before metadata; both read as zero so the caller
|
|
77
84
|
* does not hold a break open on a guess.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AssetType as e,BreakFormat as t,Vendor as s,MediaType as r,isVastAsset as i}from"../../types/BreakManifest";import{AdError as a}from"../../types/AdError";import{resolveVastAdmission as d}from"../resolveVastAdmission";import{classifyMediaError as n,toClassifiedAdError as o}from"./classifyMediaError";import{VastAdManager as h,isImaCsaiSdkAvailable as
|
|
1
|
+
import{AssetType as e,BreakFormat as t,Vendor as s,MediaType as r,isVastAsset as i}from"../../types/BreakManifest";import{AdError as a}from"../../types/AdError";import{resolveVastAdmission as d}from"../resolveVastAdmission";import{classifyMediaError as n,toClassifiedAdError as o}from"./classifyMediaError";import{VastAdManager as h,isImaCsaiSdkAvailable as c}from"../VastAdManager";import{invalidateMediaPreload as p}from"./AdPlayerState";export class AssetPlayback{constructor(e,t){this.state=e,this.deps=t}get currentBreak(){return this.state.currentBreak}async playAsset(e,t,s){const d=this.currentBreak,n=this.deps.getAdPlayer();if(!d||!n)return;const h=this.state.preparedMedia,c=p(this.state),l=()=>this.state.preloadGeneration===c&&this.currentBreak===d&&this.deps.getAdPlayer()===n;if(this.state.currentAsset=e,this.state.currentAssetElapsedSec=0,this.deps.clearStaticQuartiles(),this.deps.clearOverlayImage(),this.deps.log("Playing asset:",e.id,"type:",e.type,"index:",t),i(e))return void this.playVastAsset(e,t,s);if(e.mediaType===r.IMAGE)return void await this.playImageAsset(e,t,s);this.deps.surfaceReveal.watch(e.id,d.id);const u=await this.resolveAssetUri(e,this.state.effectiveBreakDurationSec??d.duration);if(!l())return;if(!u)return void this.deps.onAdError(this.missingUriError(e));this.deps.log("Resolved URI:",u),this.deps.reportPhase("uri-resolved");const A=d.id;try{this.deps.armAdStartBudget(e);const r=0===t&&h?.breakId===A&&h.assetId===e.id&&h.uri===u&&h.player===n,i=!r&&this.deps.takeQueuedSuccessor(A,e.id,u);if(i)this.deps.reportPhase("load-skipped-queued");else if(r)this.deps.reportPhase("load-skipped-preloaded");else{if(this.deps.reportPhase("load-start"),!await this.deps.loadWithinBudget(()=>n.load(u),{breakId:A,assetId:e.id,uri:u})||!l())return;this.deps.reportPhase("load-resolved")}this.reportPreparedDuration(n,d,e,t,s);const o=n.videoElement;if(o&&(o.style.cssText="display:block;width:100%;height:100%;object-fit:contain;"),this.deps.reportPhase("play-called"),o&&this.deps.surfaceReveal.watchVideo(o),this.state.firstFrameGateActive&&!this.state.firstFrameGateShown){const e=this.deps.waitForFirstFrame();await this.playRetryingInterruption(n,A),await e}else{let e=null;const t=await Promise.race([this.playRetryingInterruption(n,A).then(()=>!0),new Promise(t=>{e=setTimeout(()=>t(!1),this.deps.remainingAdStartBudgetMs())})]);if(e&&clearTimeout(e),!l())return;if(!t)return void this.deps.onAdError(new a(`Ad did not start within ${this.state.adStartBudgetMs}ms ad-start budget`,"DA-AD-START-TIMEOUT"))}if(!l())return;this.state.adStartDeadlineMs=null,this.state.adStartBudgetMs=0,i&&this.state.startPlaybackCutoff?.(),this.deps.transferPictureInPictureToAd();const c=this.deps.getSequencer();if(c){const t=this.deps.isGamPodAsset(e);t&&this.deps.beginGamPodTracking(e),await this.deps.drive(c.onOutcome({kind:"asset-started"})),!t&&l()&&this.deps.queueUpcomingAd()}else this.state.callbacks?.onAdStart(this.currentBreak,e,t,s)}catch(e){if(!l())return;this.deps.onAdError(o(e))}}reportPreparedDuration(e,t,s,r,i){const a=e.duration;if(!Number.isFinite(a)||a<=0)return;const d=s.duration;this.deps.diagnose("DA-AD-DURATION-PREPARED",`Ad media for break ${t.id} prepared: ${a}s of playable ad`,{breakId:t.id,assetId:s.id,index:r,total:i,preparedDurationSec:a,..."number"==typeof d?{declaredDurationSec:d}:{},breakDurationSec:this.state.effectiveBreakDurationSec??t.duration})}async playRetryingInterruption(e,t){const s=this.state.preloadGeneration;try{await e.play()}catch(r){if(s!==this.state.preloadGeneration||!function(e){return"AbortError"===e?.name}(r)||!this.isStartingBreak(t))throw r;this.deps.diagnose("DA-AD-START-INTERRUPTED","Ad play() was interrupted by a browser-initiated pause before the ad started; play() re-issued once.",{breakId:t,error:r instanceof Error?r.message:String(r)}),this.deps.reportPhase("play-retried"),await e.play()}}isStartingBreak(e){return this.state.isPlaying&&this.state.currentBreak?.id===e}playVastAsset(e,s,r){if(!this.currentBreak||!this.deps.getAdPlayer())return;const i=this.deps.getVariant(this.currentBreak)?.format??t.SINGLE,n=d({format:i,ssai:!1});if(!n.admitted)return this.deps.diagnose(n.diagnostic??"DA-VAST-UNSUPPORTED-FORMAT",`VAST asset '${e.id}' is not supported in '${i}' breaks (single/double/lshape_ad only); skipping.`,{breakId:this.currentBreak.id,assetId:e.id,format:i}),void this.deps.onAdError(new a(`VAST not supported in ${i} format`,n.diagnostic??"DA-VAST-UNSUPPORTED-FORMAT"));if(!c())return this.deps.diagnose("DA-VAST-IMA-SDK-MISSING","Google IMA client-side SDK (ima3.js) is unavailable; cannot play VAST ad.",{breakId:this.currentBreak.id,assetId:e.id}),void this.deps.onAdError(new a("IMA client-side SDK (ima3.js) not available","DA-VAST-IMA-SDK-MISSING"));const o=this.deps.buildVastTagUrl(e,this.currentBreak);if(!o)return void this.deps.onAdError(new a("No valid VAST tag URL for asset","DA-AD-URI-MISSING"));const p=this.deps.getAdPlayer(),l=p?.videoElement,u=this.deps.getAdContainer();if(!u||!l||!p)return void this.deps.onAdError(new a("VAST playout requires an ad container and ad video element","DA-AD-URI-MISSING"));this.deps.revealFirstFrame(),this.deps.surfaceReveal.watch(e.id,this.currentBreak.id),l.style.cssText="display:block;width:100%;height:100%;object-fit:contain;",this.state.vastAdManager||(this.state.vastAdManager=new h(u,l,this.deps.debug)),this.state.vastAdManager.setCallbacks({onAdStarted:t=>{if(!this.currentBreak)return;const i=Array.from(u.querySelectorAll("video")).find(e=>!e.paused);this.deps.surfaceReveal.watchVideo(i??l),this.state.startPlaybackCutoff?.(),this.deps.setVastPodPosition(t);const a=this.deps.getSequencer();a?this.deps.drive(a.onOutcome({kind:"asset-started"})):this.state.callbacks?.onAdStart(this.currentBreak,e,s,r,{podPosition:t})},onAdProgress:(t,s)=>{this.currentBreak&&(this.deps.recordAdProgress(t),this.state.callbacks?.onAdTimeUpdate(this.currentBreak,e,t,s))},onAdFirstQuartile:()=>{this.currentBreak&&this.deps.getGamEventCallbacks().onAdFirstQuartile?.(this.currentBreak,e)},onAdMidpoint:()=>{this.currentBreak&&this.deps.getGamEventCallbacks().onAdMidpoint?.(this.currentBreak,e)},onAdThirdQuartile:()=>{this.currentBreak&&this.deps.getGamEventCallbacks().onAdThirdQuartile?.(this.currentBreak,e)}}),this.deps.log("Playing VAST asset via IMA CSAI:",o);(this.state.vastAdManager.hasPreloadedManager()?(this.state.vastAdManager.setMuted(p.muted),this.state.vastAdManager.startPreloaded()):this.state.vastAdManager.playVast(o,{muted:p.muted})).then(()=>{this.deps.onAdEnded()},t=>{const s=t instanceof Error?t.message:String(t),r=/ima3\.js|client-side SDK/i.test(s)?"DA-VAST-IMA-SDK-MISSING":"DA-VAST-IMA-ERROR",i=t instanceof a?t:new a(s,r);this.deps.diagnose(r,s,{...this.currentBreak?{breakId:this.currentBreak.id}:{},assetId:e.id}),this.deps.onAdError(i)})}async playImageAsset(e,t,s){const r=this.currentBreak,i=this.deps.getAdMediaContainer();if(!r||!i)return;const d=this.state.effectiveBreakDurationSec??r.duration,n=await this.resolveAssetUri(e,d);if(!n)return void this.deps.onAdError(this.missingUriError(e));const o=document.createElement("img");o.className="dolby-overlay-image",o.style.cssText="display:block;width:100%;height:100%;object-fit:contain;",this.deps.setOverlayImage(o);const h=e.duration??d;o.onload=()=>{if(this.deps.getOverlayImage()!==o||!this.currentBreak)return;this.state.startPlaybackCutoff?.();const r=this.deps.getSequencer();r?this.deps.drive(r.onOutcome({kind:"asset-started"})):this.state.callbacks?.onAdStart(this.currentBreak,e,t,s),h>0?this.deps.setImageAssetTimer(setTimeout(()=>this.onImageAssetEnded(),1e3*h)):this.onImageAssetEnded()},o.onerror=()=>{this.deps.getOverlayImage()===o&&this.deps.onAdError(new a(`Overlay image failed to load: ${n}`,"DA-IMAGE-LOAD-FAILED"))},o.src=n,i.appendChild(o)}onImageAssetEnded(){const e=this.deps.getSequencer();if(!this.currentBreak||!this.state.isPlaying||!e)return;const t=this.deps.getImageAssetTimer();t&&(clearTimeout(t),this.deps.setImageAssetTimer(null)),this.deps.drive(e.onOutcome({kind:"asset-ended"}))}async resolveAssetUri(t,r,i=this.currentBreak){switch(t.type){case e.STATIC:return this.deps.buildStaticAssetUrl(t,i);case e.VENDOR:{const e=t;return e.vendor===s.GAM?await this.deps.buildGamUrl(e,r):(this.deps.log("Unsupported vendor:",e.vendor),null)}default:return this.deps.log("Unsupported asset type:",t.type),null}}adPlayerStreamRemainingSec(){const e=this.deps.getAdPlayer();if(!e)return 0;const{duration:t,currentTime:s}=e;if(!Number.isFinite(t)||t<=0)return 0;if(!Number.isFinite(s)||s<0)return 0;const r=t-s;return r>0?r:0}revealFirstFrame(){this.deps.revealFirstFrame()}toAdError(e){const t=e,s=n(e);if(t&&("number"==typeof t.code||t.message)){const e="number"==typeof t.code?` (code ${t.code})`:"",r=t.message?`: ${t.message}`:"";return new a(`Ad playback error${e}${r}`,s)}return new a("Ad playback error",s)}missingUriError(e){return this.deps.hasGamIdentityMismatch(e)?new a("GAM pod asset identity does not match the stream session","DA-GAM-POD-IDENTITY-MISMATCH"):new a("No valid URI for asset","DA-AD-URI-MISSING")}}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { PlayerAdapter } from '../../interfaces/PlayerAdapter';
|
|
2
|
+
import type { VastAdManager } from '../VastAdManager';
|
|
3
|
+
export type AudioSide = 'ad' | 'content';
|
|
4
|
+
export interface AudioControllerDeps {
|
|
5
|
+
contentPlayer: PlayerAdapter;
|
|
6
|
+
/** Outside a break the viewer cannot reach the ad player, so a change there is the engine's own. */
|
|
7
|
+
isAdPlaying: () => boolean;
|
|
8
|
+
/** IMA plays a CSAI ad through its own audio path; null outside a VAST ad. */
|
|
9
|
+
getVastAdManager: () => VastAdManager | null;
|
|
10
|
+
/** Re-emitted by the SDK facade as the public `volumechange`. */
|
|
11
|
+
onVolumeChange: (muted: boolean, volume: number) => void;
|
|
12
|
+
log: (...args: unknown[]) => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Sole owner of the SDK's audio: holds the viewer's muted/volume state and applies it to the
|
|
16
|
+
* content and ad players, except to a side the SDK has muted for its own reasons (double box,
|
|
17
|
+
* continue-content playout, resume catch-up).
|
|
18
|
+
*
|
|
19
|
+
* Decisions read state, not timing: a `volumechange` that matches what this class applied is an
|
|
20
|
+
* echo of its own write, whenever it arrives; anything else is the viewer, the app or an IMA
|
|
21
|
+
* creative, and becomes the new viewer state.
|
|
22
|
+
*/
|
|
23
|
+
export declare class AudioController {
|
|
24
|
+
private readonly deps;
|
|
25
|
+
private readonly viewer;
|
|
26
|
+
/** The one side the SDK forces silent on top of the viewer's state; the viewer's mute is {@link viewer}. */
|
|
27
|
+
private mutedSide;
|
|
28
|
+
private adPlayer;
|
|
29
|
+
private readonly contentHandler;
|
|
30
|
+
private readonly adHandler;
|
|
31
|
+
constructor(deps: AudioControllerDeps);
|
|
32
|
+
/**
|
|
33
|
+
* Once per controller; the ad player is never replaced during a session. A single-surface
|
|
34
|
+
* device plays the ad on the content player itself: there is no second side to keep in step.
|
|
35
|
+
*/
|
|
36
|
+
attachAdPlayer(adPlayer: PlayerAdapter): void;
|
|
37
|
+
/** Gives every muted side the viewer's state back, then stops listening. */
|
|
38
|
+
destroy(): void;
|
|
39
|
+
/** Forces `side` silent, whatever the viewer's state. Replaces a mute on the other side. */
|
|
40
|
+
mute(side: AudioSide): void;
|
|
41
|
+
/** Stops forcing `side` silent: it gets the viewer's state back, which may itself be muted. */
|
|
42
|
+
unmute(side: AudioSide): void;
|
|
43
|
+
unmuteAll(): void;
|
|
44
|
+
isMuted(side: AudioSide): boolean;
|
|
45
|
+
/** The viewer's state (`sdk.muted`), not what a muted side currently plays at. */
|
|
46
|
+
get muted(): boolean;
|
|
47
|
+
set muted(value: boolean);
|
|
48
|
+
/** 0-1. See {@link muted}. */
|
|
49
|
+
get volume(): number;
|
|
50
|
+
set volume(value: number);
|
|
51
|
+
private setViewerState;
|
|
52
|
+
private player;
|
|
53
|
+
private expectedMuted;
|
|
54
|
+
private apply;
|
|
55
|
+
private applyAll;
|
|
56
|
+
private onPlayerVolumeChange;
|
|
57
|
+
private notify;
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class AudioController{constructor(e){this.deps=e,this.mutedSide=null,this.adPlayer=null,this.contentHandler=()=>this.onPlayerVolumeChange("content"),this.adHandler=()=>this.onPlayerVolumeChange("ad"),this.viewer={muted:e.contentPlayer.muted,volume:e.contentPlayer.volume},e.contentPlayer.on("volumechange",this.contentHandler)}attachAdPlayer(e){e!==this.deps.contentPlayer&&(this.adPlayer?this.deps.log("AudioController: ad player already attached; ignoring the second one"):(this.adPlayer=e,e.on("volumechange",this.adHandler),this.apply("ad")))}destroy(){this.unmuteAll(),this.deps.contentPlayer.off("volumechange",this.contentHandler),this.adPlayer?.off("volumechange",this.adHandler),this.adPlayer=null}mute(e){this.mutedSide!==e&&(this.mutedSide&&(this.deps.log(`AudioController: muting ${e} replaces the mute on ${this.mutedSide}`),this.unmute(this.mutedSide)),this.mutedSide=e,this.apply(e))}unmute(e){this.mutedSide===e&&(this.mutedSide=null,this.apply(e))}unmuteAll(){this.mutedSide&&this.unmute(this.mutedSide)}isMuted(e){return this.mutedSide===e}get muted(){return this.viewer.muted}set muted(e){this.setViewerState(e,this.viewer.volume)}get volume(){return this.viewer.volume}set volume(e){this.setViewerState(this.viewer.muted,Math.max(0,Math.min(1,e)))}setViewerState(e,t){this.viewer.muted=e,this.viewer.volume=t,this.applyAll(),this.notify()}player(e){return"content"===e?this.deps.contentPlayer:this.adPlayer}expectedMuted(e){return this.mutedSide===e||this.viewer.muted}apply(e){const t=this.player(e);t&&(t.muted!==this.expectedMuted(e)&&(t.muted=this.expectedMuted(e)),Math.abs(t.volume-this.viewer.volume)>.001&&(t.volume=this.viewer.volume)),"ad"===e&&this.deps.getVastAdManager()?.setVolume(this.expectedMuted("ad")?0:this.viewer.volume)}applyAll(){this.apply("content"),this.apply("ad")}onPlayerVolumeChange(e){const t=this.player(e);if(!t)return;const i=t.muted!==this.expectedMuted(e),s=Math.abs(t.volume-this.viewer.volume)>.001;(i||s)&&("ad"!==e||this.deps.isAdPlaying()?(i&&(this.viewer.muted=t.muted),s&&(this.viewer.volume=t.volume),this.applyAll(),this.notify()):this.apply("ad"))}notify(){this.deps.onVolumeChange(this.viewer.muted,this.viewer.volume)}}
|
|
@@ -3,7 +3,10 @@ import type { AdPlayerCallbacks, PlayBreakOptions } from '../AdPlayerController'
|
|
|
3
3
|
import type { AdBreakAction, AdBreakStep, AdBreakTransition, EffectiveInsertionMode } from '../AdBreakSequencer';
|
|
4
4
|
import type { GamPodAdStep } from '../GamPodAdTracker';
|
|
5
5
|
import { type AdPlayerState } from './AdPlayerState';
|
|
6
|
+
import type { SuccessorQueue } from './SuccessorQueue';
|
|
6
7
|
export interface BreakSequenceDriverDeps {
|
|
8
|
+
successorQueue: SuccessorQueue;
|
|
9
|
+
getAssets: (breakInfo: Break) => Asset[];
|
|
7
10
|
getChainResolver: () => ((currentBreakId: string) => Break | null) | null;
|
|
8
11
|
getEffectiveInsertionMode: () => EffectiveInsertionMode;
|
|
9
12
|
getVastPodPosition: () => number | undefined;
|
|
@@ -26,8 +29,11 @@ export declare class BreakSequenceDriver {
|
|
|
26
29
|
private readonly state;
|
|
27
30
|
private readonly deps;
|
|
28
31
|
constructor(state: AdPlayerState, deps: BreakSequenceDriverDeps);
|
|
29
|
-
/**
|
|
30
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Emit the transition's ad events (break-level steps are owned elsewhere), then perform its side
|
|
34
|
+
* effect. `late`: the transition answers a successor change while the last ad plays.
|
|
35
|
+
*/
|
|
36
|
+
drive(transition: AdBreakTransition, late?: boolean): Promise<void>;
|
|
31
37
|
/** Map a sequencer step to the matching SDK callback. */
|
|
32
38
|
emitStep(step: AdBreakStep): void;
|
|
33
39
|
/**
|
|
@@ -37,7 +43,7 @@ export declare class BreakSequenceDriver {
|
|
|
37
43
|
*/
|
|
38
44
|
settlePodAdEnd(step: AdBreakStep): boolean;
|
|
39
45
|
/** Perform the side effect a transition asks for (overlay intra-break only). */
|
|
40
|
-
performAction(action: AdBreakAction): Promise<void>;
|
|
46
|
+
performAction(action: AdBreakAction, late?: boolean): Promise<void>;
|
|
41
47
|
emitGamPodSteps(steps: GamPodAdStep[]): void;
|
|
42
48
|
/** Null when no resolver is injected or in shared-element mode (those breaks always hard-end). */
|
|
43
49
|
resolveChainedSuccessor(currentBreakId: string): Break | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{invalidateMediaPreload as e}from"./AdPlayerState";export class BreakSequenceDriver{constructor(e,
|
|
1
|
+
import{invalidateMediaPreload as e}from"./AdPlayerState";export class BreakSequenceDriver{constructor(e,s){this.state=e,this.deps=s}async drive(e,s=!1){for(const s of e.noPlayableAsset??[])this.deps.diagnose("DA-BREAK-NO-PLAYABLE-ASSET",`break ${s.breakId} had no playable ad (${s.reason}); it began and ended without an ad.`,{breakId:s.breakId,reason:s.reason,declaredAssetCount:s.declaredAssetCount,...void 0!==s.format?{format:s.format}:{}});const{action:t}=e;"play-asset"===t.kind&&t.index>0&&this.state.currentBreak&&this.deps.successorQueue.confirm(this.state.currentBreak,t.asset);for(const s of e.steps)this.emitStep(s);await this.performAction(t,s)}emitStep(e){if(this.state.currentBreak)switch(e.type){case"adbreakbegin":case"adbreakend":return;case"adbegin":return this.deps.isExpandingPodAsset(e.asset)?void("pending"===this.state.gamPodMode&&(this.state.withheldPodAdBegin=e)):void(void 0!==this.deps.getVastPodPosition()?(this.state.callbacks?.onAdStart(this.state.currentBreak,e.asset,e.adIndex,e.totalAds,{podPosition:this.deps.getVastPodPosition()}),this.deps.setVastPodPosition(void 0)):this.state.callbacks?.onAdStart(this.state.currentBreak,e.asset,e.adIndex,e.totalAds));case"adend":if(this.settlePodAdEnd(e))return;return this.state.callbacks?.onAdEnd(this.state.currentBreak,e.asset,e.adIndex,e.totalAds),void this.deps.resetGamPodTracking();case"aderror":{this.deps.isExpandingPodAsset(e.asset)&&(this.state.withheldPodAdBegin=null,this.state.gamPodTracker?.sawAnyAd&&this.emitGamPodSteps(this.state.gamPodTracker.finish()));const s=this.deps.getPendingAdError()??new Error("Ad playback error");return this.deps.setPendingAdError(null),void this.state.callbacks?.onAdError(this.state.currentBreak,e.asset,s)}}}settlePodAdEnd(e){return!!this.deps.isExpandingPodAsset(e.asset)&&(this.state.gamPodTracker?.sawAnyAd?(this.emitGamPodSteps(this.state.gamPodTracker.finish()),this.deps.resetGamPodTracking(),!0):(this.deps.fallBackToPodLevelReporting(),!1))}async performAction(e,s=!1){switch(e.kind){case"play-asset":return void await this.deps.playAsset(e.asset,e.index,e.total);case"end-break":return void this.deps.endBreak();case"prepare-successor":return void this.deps.successorQueue.prepare(e.break,e.asset,s);case"drop-successor":return void this.deps.successorQueue.drop(e.breakId);default:return}}emitGamPodSteps(e){this.deps.emitGamPodSteps(e)}resolveChainedSuccessor(e){const s=this.deps.getChainResolver();if(!s)return null;if("shared-element"===this.deps.getEffectiveInsertionMode())return null;try{return s(e)}catch(e){return this.deps.log("Chain resolver error:",e),null}}advanceToChainedBreak(s,t){this.deps.log(`Chaining: "${s.id}" -> "${t.id}" (overlay held, content stays paused)`),this.deps.successorQueue.confirm(t,this.deps.getAssets(t)[0]),this.state.currentBreak=null,this.state.effectiveBreakDurationSec=null,e(this.state);const a=this.state.callbacks;a?.onBreakEnd(s),a?.onChainedBreakBegin?.(t),a&&(this.deps.setChaining(!0),this.deps.playBreak(t,a).finally(()=>{this.deps.setChaining(!1)}))}}
|
|
@@ -9,6 +9,8 @@ export interface ContentResumeCoordinatorDeps {
|
|
|
9
9
|
getInsertionMode: () => string;
|
|
10
10
|
gateContentForBreak: () => void;
|
|
11
11
|
clearContentGate: (keepPlaying?: boolean) => void;
|
|
12
|
+
/** Silences content for the rest of the break; the break's hard end gives the viewer's audio back. */
|
|
13
|
+
muteContent: () => void;
|
|
12
14
|
diagnose: (code: string, message: string, context?: Record<string, unknown>) => void;
|
|
13
15
|
log: (...args: unknown[]) => void;
|
|
14
16
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Timebase as e,isPostRollBreak as t}from"../../types/BreakManifest";import{resolveResumePoint as s}from"../resolveResumePoint";import{resolveResumeClamp as n}from"../resolveResumeClamp";import{BREAK_CUT_LATE_TOLERANCE_SEC as r,resolveWallclockBreakWindow as a}from"../wallclockBreakWindow";import{ContinueContentPlayout as i}from"./ContinueContentPlayout";import{StreamClockResume as o}from"./streamClockResume";import{PdtClockResume as u,programDateTimeSample as l}from"./pdtClockResume";import{contentSeekableEnd as d,isPlausibleSeekEnd as h,liveEdgeDistanceSec as c,liveSafeSeekEnd as m,liveSafeSeekStart as p,liveSeekableWindowSec as g,unreachableEdgeSec as k}from"./liveEdge";const S=.04,f=e=>Math.round(1e3*e)/1e3;export class ContentResumeCoordinator{constructor(e,t){this.state=e,this.deps=t,this.cueAnchorResolver=null,this.resumePolicyTarget=null,this.cueEndResolver=null,this.continuePlayout=new i(this.state,{contentPlayer:this.deps.contentPlayer,clock:this.deps.clock,gateContentForBreak:()=>this.deps.gateContentForBreak(),distanceToLiveEdgeSec:()=>this.distanceToLiveEdgeSec(),plannedResumeTarget:e=>this.plannedResumeTarget(e),diagnose:(e,t,s)=>this.deps.diagnose(e,t,s),log:(...e)=>this.deps.log(...e)}),this.streamClock=new o({contentPlayer:this.deps.contentPlayer,diagnose:(e,t,s)=>this.deps.diagnose(e,t,s),activeBreak:()=>this.state.currentBreak,anchorIsCue:()=>this.state.resumeAnchorIsCue,breakEnd:e=>this.cueEndResolver?.(e)??null,computedTarget:e=>this.computedResumeTarget(e),resumesAtBreakEnd:e=>this.resumesAtBreakEnd(e),now:()=>this.deps.clock.now()}),this.pdtClock=new u({contentPlayer:this.deps.contentPlayer,diagnose:(e,t,s)=>this.deps.diagnose(e,t,s),anchorIsPdt:()=>this.state.resumeAnchorIsPdt,computedTarget:e=>this.computedResumeTarget(e),resumesAtBreakEnd:e=>this.resumesAtBreakEnd(e),now:()=>this.deps.clock.now()})}setCueEndResolver(e){this.cueEndResolver=e}ingestStreamClockCue(e){this.streamClock.ingestCue(e)}noteContentReload(){this.streamClock.noteReload()}dispose(){this.streamClock.cancelLanding(),this.pdtClock.cancelLanding()}setCueAnchorResolver(e){this.cueAnchorResolver=e}captureBreakAnchor(e){this.resetBreakResumeState();const t=this.cueAnchorResolver?.(e)??null,s=null!==t&&Number.isFinite(t),n=s?null:this.pdtBreakAnchor(e);this.state.contentPositionAtBreakStart=s?t:n??this.deps.contentPlayer.currentTime,this.state.resumeAnchorIsCue=s,this.state.resumeAnchorIsPdt=null!==n,this.state.resumeAnchorSeekableEnd=this.contentSeekableEnd(),this.streamClock.beginBreak(),this.pdtClock.beginBreak()}pdtBreakAnchor(t){if(this.state.breakTimebase!==e.WALLCLOCK||"string"!=typeof t.start)return null;const s=l(this.deps.contentPlayer);if(!s)return null;const n=a({startMs:Date.parse(t.start),durationSec:t.duration??0,sample:s});return n?(n.sampleLateSec>r&&this.deps.diagnose?.("DA-BREAK-CUT-LATE",`Break ${t.id}: content was cut ${n.sampleLateSec}s after the break start on the stream clock; the viewer saw the stream's own break until then`,{breakId:t.id,lateSec:n.sampleLateSec,start:t.start,cutPdt:new Date(s.programDateTimeMs).toISOString(),cutAt:f(s.currentTime),anchor:n.anchor}),n.anchor):null}reanchorMidBreak(e){this.state.contentPositionAtBreakStart=e,this.state.resumeAnchorIsCue=!1,this.state.resumeAnchorIsPdt=!1,this.state.resumeAnchorSeekableEnd=this.contentSeekableEnd()}liveEdgeDistanceSec(){return c(this.deps.contentPlayer)}liveSafeSeekEnd(){return m(this.deps.contentPlayer)}liveSafeSeekStart(){return p(this.deps.contentPlayer)}liveSeekableWindowSec(){return g(this.deps.contentPlayer)}contentSeekableEnd(){return d(this.deps.contentPlayer)}resumeAnchorCushionSec(){const e=this.state.resumeAnchorSeekableEnd;return null!==e&&Number.isFinite(e)?f(e-this.state.contentPositionAtBreakStart):null}isPlausibleSeekEnd(e){return h(this.deps.contentPlayer,e)}plannedResumeTarget(e){const t=this.computedResumeTarget(e);return null===t?null:this.streamClock.target(e)??this.pdtClock.target(e)??this.cueEndTarget(e)??t}computedResumeTarget(e){if(t(e))return null;const s=this.resolvePolicy(e,this.state.contentPositionAtBreakStart);return s.seek?s.target:null}resumesAtBreakEnd(e){if(t(e))return!1;const s=this.resolvePolicy(e,0);return s.seek&&Math.abs(s.offset-(e.duration??0))<.001}resolvePolicy(e,t){return s({anchor:t,duration:e.duration,adDuration:e.duration,...this.state.adInsertionType?{insertionType:this.state.adInsertionType}:{},..."number"==typeof e.resumeOffset?{resumeOffset:e.resumeOffset}:{}})}cueEndTarget(e){if(!this.state.resumeAnchorIsCue||!this.resumesAtBreakEnd(e))return null;const t=this.cueEndResolver?.(e)?.mediaTime;return"number"==typeof t&&Number.isFinite(t)?t:null}maybePrepositionContentForResume(){if("shared-element"===this.deps.getInsertionMode())return;if(!this.state.contentPaused||null!==this.state.resumePrepositionedTo)return;if(null!==this.state.resumePrepositionSettle)return;if(this.state.resumeCatchupActive)return;if(Number.isFinite(this.deps.contentPlayer.duration))return;const e=this.state.currentBreak;if(!e)return;const t=this.plannedResumeTarget(e);if(null===t)return;const s=this.contentSeekableEnd();if(null===s)return;const n=this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec,r=(e.duration??0)-n;let a;if(t<=s)a=t;else{if(!(r<=3))return;a=s}this.deps.log(a===t?`Resume pre-positioning: seeking content to ${t}s during the break (window reaches ${s}s)`:`Resume pre-positioning: window only reaches ${s}s and the ad ends in ${r}s — moving content as close to ${t}s as exists`),this.streamClock.noteSeek(a),this.streamClock.arm(),this.deps.contentPlayer.seek(a);const i=setTimeout(()=>{this.state.resumePrepositionSettle=null;const e=this.deps.contentPlayer.currentTime;Math.abs(e-a)<=S?(this.state.resumePrepositionedTo=e,this.deps.log(`Resume pre-positioned: content is at ${e}s`)):this.deps.log(`Resume pre-position rejected: asked ${a}s, landed ${e}s — window is short, will retry`)},250);this.state.resumePrepositionSettle=i}maybeCatchUpContentForResume(e){if("shared-element"===this.deps.getInsertionMode())return;if(!this.state.contentPaused||Number.isFinite(this.deps.contentPlayer.duration))return;const t=this.plannedResumeTarget(e);if(null===t)return;const s=this.deps.contentPlayer.currentTime;if(this.state.resumeCatchupActive){const n=(e.duration??0)-(this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec);if(s>t+1)return this.state.resumeCatchupActive=!1,this.deps.contentPlayer.pause(),this.state.resumePrepositionedTo=this.deps.contentPlayer.currentTime,void this.deps.log(`Resume catch-up halted: content reached ${this.deps.contentPlayer.currentTime}s, past the ${t}s target`);if(n<=30&&n>=1){this.state.resumeAlignTicks+=1;const e=s+n,r=e-t;if(Math.abs(r)>S){const a=s-r,i=this.liveSafeSeekEnd();if(null!==i){const e=i>this.state.lastResumeSafeEnd+.25;e&&Number.isFinite(this.state.lastResumeSafeEnd)&&this.state.resumeSawSteps.push(Math.round(100*(i-this.state.lastResumeSafeEnd))/100),i>this.state.lastResumeSafeEnd&&(this.state.lastResumeSafeEnd=i);const s=i-a;if(s>this.state.resumeBestSawMarginSec&&(this.state.resumeBestSawMarginSec=s),a>i)return void(this.state.resumeAlignBlockedDiagnosed||(this.state.resumeAlignBlockedDiagnosed=!0,this.deps.log(`Resume align waiting: ${a}s is past the published edge ${i}s (${Math.round(1e3*(a-i))}ms short) — rolling until a manifest step brings it in reach`),this.deps.diagnose?.("DA-RESUME-ALIGN-BLOCKED",`Resume alignment cannot reach ${t}s yet: the published edge is ${i}s`,{target:t,requested:a,safeEnd:i,shortfallSec:Math.round(1e3*(a-i))/1e3,remainingSec:n})));if(!e&&this.state.resumeSawSeekDone)return;this.state.resumeSawSeekDone=!0}if(this.state.lastResumeAlignRemainingSec-n<.5)return;this.state.lastResumeAlignRemainingSec=n,this.deps.log(`Resume align: landing ${e}s vs target ${t}s (${Math.round(1e3*r)}ms) — content to ${a}s, ${n}s of ad left`),this.state.resumeAlignCorrectionsMs.push(Math.round(1e3*r)),this.streamClock.noteSeek(a),this.deps.contentPlayer.seek(a)}}return}if(t-s<=S)return;const n=this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec,r=(e.duration??0)-n;if(r<=0)return;if(r>30)return;const a=this.contentSeekableEnd();if(null===a)return;const i=t-r+.3,o=Math.min(i,a);this.state.resumeCatchupActive=!0,this.state.resumeCatchupRan=!0,this.state.contentMutedBeforeCatchup=this.deps.contentPlayer.muted,this.deps.contentPlayer.muted=!0,this.deps.log(`Resume catch-up: content to ${o}s, playing toward ${t}s with ${r}s of ad left`),this.deps.diagnose?.("DA-RESUME-CATCHUP",`Break ${e.id}: playing content from ${o}s toward ${t}s with ${r}s of ad left`,{breakId:e.id,startPos:o,target:t,remainingAdSec:Math.round(1e3*r)/1e3,publishedEnd:a,clampedStart:o<i}),Math.abs(s-o)>S&&(this.streamClock.noteSeek(o),this.deps.contentPlayer.seek(o)),this.streamClock.arm(),this.deps.contentPlayer.play()}endResumeCatchup(e=!1){this.state.resumeCatchupActive&&(this.state.resumeCatchupActive=!1,e||this.deps.contentPlayer.pause()),null!==this.state.contentMutedBeforeCatchup&&(this.deps.contentPlayer.muted=this.state.contentMutedBeforeCatchup,this.state.contentMutedBeforeCatchup=null)}get resumeHandoverInMotion(){return this.state.resumeCatchupActive}resumeAlignSummary(){const e=this.state.resumeAlignCorrectionsMs;return{catchUpRan:this.state.resumeCatchupRan||this.state.resumeCatchupActive||e.length>0||null!==this.state.resumePrepositionedTo,alignCorrections:e.length,placedOnStep:this.state.resumeSawSeekDone,alignTicks:this.state.resumeAlignTicks,sawSteps:this.state.resumeSawSteps.slice(0,12),sawStepCount:this.state.resumeSawSteps.length,bestSawMarginSec:Number.isFinite(this.state.resumeBestSawMarginSec)?Math.round(1e3*this.state.resumeBestSawMarginSec)/1e3:null,alignCorrectionsMs:e.slice(0,12),alignLastErrorMs:e.length?e[e.length-1]:null}}cancelResumeLandedProbe(){const e=this.state.resumeLandedProbe;this.state.resumeLandedProbe=null,e?.()}probeResumeLanding(e,t,s=null){this.cancelResumeLandedProbe();let n=!1;const r=(r,a)=>{if(n)return;n=!0,this.cancelResumeLandedProbe();const i=null===r?null:Math.round(1e3*(r-t))/1e3,o=null===r||null===s?null:Math.round(1e3*(r-s))/1e3,u=null!==s&&Math.abs(s-t)>=5e-4&&null!==o?`; ${Math.abs(o)}s ${o<0?"before":"after"} the ${s}s resume point`:"";this.deps.log(null===r?`Resume landing unknown for break ${e} (${a})`:`Resume landed at ${r}s, ${i}s from the ${t}s asked for${u} (break ${e})`),this.deps.diagnose?.("DA-RESUME-LANDED",null===r?`Resume landing for break ${e} could not be measured (${a})`:`Resume for break ${e} landed at ${r}s, ${i}s from the ${t}s requested${u}`,{breakId:e,target:t,landed:r,errorSec:i,...null===s?{}:{policyTarget:s,policyErrorSec:o},...a?{reason:a}:{}})},a=()=>{this.streamClock.updateLanding(e,this.deps.contentPlayer.currentTime),r(this.deps.contentPlayer.currentTime)},i=setTimeout(()=>r(null,"no seeked event"),2e3);this.state.resumeLandedProbe=()=>{clearTimeout(i),this.deps.contentPlayer.off("seeked",a)},this.deps.contentPlayer.on("seeked",a)}async waitForLiveEdge(e=()=>!0){for(let t=0;t<20;t++){if(!e())return null;const t=this.deps.contentPlayer.videoElement,s=t?.seekable;if(s&&s.length>0){const e=s.end(s.length-1);if(Number.isFinite(e)&&e>0)return e}if(await new Promise(e=>setTimeout(e,100)),!e())return null}return this.deps.log("shared-element: no seekable range after restore; staying at the live edge"),null}resetBreakResumeState(){this.state.resumeAnchorIsCue=!1,this.state.resumeAnchorIsPdt=!1,this.state.resumeAnchorSeekableEnd=null,this.state.resumePrepositionedTo=null,this.state.resumeCatchupActive=!1,this.state.resumeCatchupRan=!1,this.state.contentMutedBeforeCatchup=null,this.state.resumeAlignCorrectionsMs=[],this.state.lastResumeAlignRemainingSec=Number.POSITIVE_INFINITY,this.state.resumeAlignBlockedDiagnosed=!1,this.state.lastResumeSafeEnd=Number.NEGATIVE_INFINITY,this.state.resumeSawSeekDone=!1,this.state.resumeBestSawMarginSec=Number.NEGATIVE_INFINITY,this.state.resumeAlignTicks=0,this.state.resumeSawSteps=[],this.state.resumePrepositionSettle&&(clearTimeout(this.state.resumePrepositionSettle),this.state.resumePrepositionSettle=null),this.state.adSecondsPlayedThisBreak=0,this.state.currentAssetElapsedSec=0}beginContinueContentPlayout(){this.continuePlayout.beginContinueContentPlayout()}distanceToLiveEdgeSec(){const e=this.contentSeekableEnd();return null===e?null:f(e-this.deps.contentPlayer.currentTime)}secondsUntilCut(e){return this.continuePlayout.secondsUntilCut(e)}restoreContinueQuality(e){this.continuePlayout.restoreContinueQuality(e)}currentContinueDriftSec(){return this.continuePlayout.currentContinueDriftSec()}abandonContinueContentPlayout(e){this.continuePlayout.abandonContinueContentPlayout(e)}endContinueContentPlayout(){this.continuePlayout.endContinueContentPlayout()}setContinueRate(e){this.continuePlayout.setContinueRate(e)}tickContinueContentPlayout(){this.continuePlayout.tickContinueContentPlayout()}resolveResumeTarget(t,r=null){this.resumePolicyTarget=null;const a=null!==r,i=(this.state.resumeAnchorIsCue||this.state.resumeAnchorIsPdt)&&!a,o=a?r:i?t.duration:this.state.adSecondsPlayedThisBreak,u=s({anchor:this.state.contentPositionAtBreakStart,duration:t.duration,adDuration:o,...a?{}:{insertionType:this.state.adInsertionType},...a?{resumeOffset:r}:"number"==typeof t.resumeOffset?{resumeOffset:t.resumeOffset}:{}});if(u.clamped&&this.deps.diagnose?.("DA-RESUME-OFFSET-CLAMPED",`Resume offset shortened to the ${u.offset}s of ad actually played for break ${t.id}`,{breakId:t.id,offset:u.offset,source:u.source}),!u.seek)return this.deps.log(`Resume: no seek for break ${t.id} (offset 0, ${u.source})`),null;const l=this.streamClock.handBackTarget(t,a)??this.pdtClock.handBackTarget(t,a),d=null!==l||a?null:this.reportCueEnd(t,u.target),h=l??d??u.target;this.resumePolicyTarget=h;const c=this.streamClock.seekBasis(t)??this.pdtClock.seekBasis()??(null!==d?"cue_end":`offset ${u.offset}s, ${u.source}`);this.deps.log(`Resume-seek to ${h}s (${c}) for break ${t.id}`);const m=this.resumeAlignSummary(),p=this.liveSafeSeekEnd();this.deps.diagnose?.("DA-RESUME-SEEK",`Resume-seek to ${h}s (${c}) for break ${t.id}`,{breakId:t.id,target:h,offset:u.offset,source:u.source,...h===u.target?{}:{computedTarget:u.target},...null===d?{}:{cueEndTarget:d},...this.streamClock.seekContext(t,h,u.target),...this.pdtClock.seekContext(t,h,u.target),anchorPosition:this.state.contentPositionAtBreakStart,anchorIsCue:this.state.resumeAnchorIsCue,anchorIsPdt:this.state.resumeAnchorIsPdt,anchorSeekableEnd:this.state.resumeAnchorSeekableEnd,anchorCushionSec:this.resumeAnchorCushionSec(),safeEnd:p,seekableEndNow:this.contentSeekableEnd(),liveEdge:this.deps.contentPlayer.liveEdge??null,unreachableEdgeSec:k(this.deps.contentPlayer),...m.catchUpRan?m:{}}),this.streamClock.reportCorrection(t,h,u.target,a),this.pdtClock.reportCorrection(t,h,u.target,a);(this.state.breakTimebase===e.PTS||this.state.breakTimebase===e.MEDIATIME)&&"number"==typeof t.start&&this.state.callbacks?.onContentResumeSeek?.(h,t.start+t.duration);const g=this.liveSafeSeekStart(),f=n({target:h,safeStart:g,safeEnd:p,windowSec:this.liveSeekableWindowSec()}),{target:C,shortfallSec:P,bound:b}=f;if(f.startClampSkipped){const{behindSec:e,windowSec:s,maxBehindSec:n}=f.startClampSkipped;this.deps.log(`Resume-seek: ${h}s is ${e}s behind the element's live window start (window ${s}s); not clamping`),this.deps.diagnose?.("DA-RESUME-START-CLAMP-SKIPPED",`Resume target ${h}s for break ${t.id} is ${e}s behind the element's seekable start but the gap exceeds half the ${s}s window; left unchanged`,{breakId:t.id,requested:h,seekableStart:g,behindSec:e,windowSec:s,maxBehindSec:n,...this.resumeAlignSummary()})}if(null!==this.state.resumePrepositionedTo&&Math.abs(this.deps.contentPlayer.currentTime-C)<=S){const e=this.deps.contentPlayer.currentTime;return this.deps.log(`Resume: content already at ${e}s for break ${t.id} (pre-positioned during the break)`+(P>0?` — ${P}s short of policy, those frames are not published yet`:"")+" — no seek"),this.deps.diagnose?.("DA-RESUME-PREPOSITIONED",`Break ${t.id} resumed without a seek: content was moved to ${e}s during the break`+(P>0?` (${P}s short of the ${h}s policy target)`:""),{breakId:t.id,target:h,prepositionedTo:this.state.resumePrepositionedTo,landed:e,shortfallSec:P,exact:0===P,...this.resumeAlignSummary()}),this.streamClock.armLanding(t,e),this.pdtClock.armLanding(t),null}return P>0&&(this.deps.log("start"===b?`Resume-seek clamped: ${h}s is behind the live window start (already evicted); seeking ${C}s instead (${P}s early)`:`Resume-seek clamped: ${h}s is beyond what the stream has published; seeking ${C}s instead (${P}s short)`),this.deps.diagnose?.("DA-RESUME-TARGET-CLAMPED","start"===b?`Resume target ${h}s for break ${t.id} is behind the live window start (already evicted); clamped to ${C}s`:`Resume target ${h}s for break ${t.id} is beyond the published window; clamped to ${C}s`,{breakId:t.id,requested:h,clamped:C,shortfallSec:P,bound:b,...this.resumeAlignSummary()})),this.streamClock.armLanding(t,C),this.pdtClock.armLanding(t),C}reportCueEnd(e,t){const s=this.state.resumeAnchorIsCue?this.cueEndResolver?.(e):null;if(!s||!Number.isFinite(s.mediaTime))return null;const n=this.resumesAtBreakEnd(e),r=f(s.mediaTime-t);return this.deps.diagnose("DA-RESUME-CUE-END",n?`Break ${e.id}: resuming on the stream's cue_end at ${s.mediaTime}s (${r}s from the computed ${t}s)`:`Break ${e.id}: cue_end at ${s.mediaTime}s not used; the resume policy does not resume at the break end`,{breakId:e.id,computedTarget:t,cueEndTarget:s.mediaTime,deltaSec:r,applied:n,stage:"hand-back",corrected:!1}),n?s.mediaTime:null}applyResumeSeek(e,t=null){const s=this.resolveResumeTarget(e,t);null!==s&&(this.probeResumeLanding(e.id,s,this.resumePolicyTarget),this.streamClock.noteSeek(s),this.deps.contentPlayer.seek(s))}}
|
|
1
|
+
import{Timebase as e,isPostRollBreak as t}from"../../types/BreakManifest";import{resolveResumePoint as s}from"../resolveResumePoint";import{resolveResumeClamp as n}from"../resolveResumeClamp";import{BREAK_CUT_LATE_TOLERANCE_SEC as r,resolveWallclockBreakWindow as i}from"../wallclockBreakWindow";import{ContinueContentPlayout as a}from"./ContinueContentPlayout";import{StreamClockResume as o}from"./streamClockResume";import{PdtClockResume as u,programDateTimeSample as l}from"./pdtClockResume";import{contentSeekableEnd as d,isPlausibleSeekEnd as h,liveEdgeDistanceSec as c,liveSafeSeekEnd as m,liveSafeSeekStart as p,liveSeekableWindowSec as g,unreachableEdgeSec as k}from"./liveEdge";const S=.04,f=e=>Math.round(1e3*e)/1e3;export class ContentResumeCoordinator{constructor(e,t){this.state=e,this.deps=t,this.cueAnchorResolver=null,this.resumePolicyTarget=null,this.cueEndResolver=null,this.continuePlayout=new a(this.state,{contentPlayer:this.deps.contentPlayer,clock:this.deps.clock,gateContentForBreak:()=>this.deps.gateContentForBreak(),distanceToLiveEdgeSec:()=>this.distanceToLiveEdgeSec(),plannedResumeTarget:e=>this.plannedResumeTarget(e),diagnose:(e,t,s)=>this.deps.diagnose(e,t,s),log:(...e)=>this.deps.log(...e)}),this.streamClock=new o({contentPlayer:this.deps.contentPlayer,diagnose:(e,t,s)=>this.deps.diagnose(e,t,s),activeBreak:()=>this.state.currentBreak,anchorIsCue:()=>this.state.resumeAnchorIsCue,breakEnd:e=>this.cueEndResolver?.(e)??null,computedTarget:e=>this.computedResumeTarget(e),resumesAtBreakEnd:e=>this.resumesAtBreakEnd(e),now:()=>this.deps.clock.now()}),this.pdtClock=new u({contentPlayer:this.deps.contentPlayer,diagnose:(e,t,s)=>this.deps.diagnose(e,t,s),anchorIsPdt:()=>this.state.resumeAnchorIsPdt,computedTarget:e=>this.computedResumeTarget(e),resumesAtBreakEnd:e=>this.resumesAtBreakEnd(e),now:()=>this.deps.clock.now()})}setCueEndResolver(e){this.cueEndResolver=e}ingestStreamClockCue(e){this.streamClock.ingestCue(e)}noteContentReload(){this.streamClock.noteReload()}dispose(){this.streamClock.cancelLanding(),this.pdtClock.cancelLanding()}setCueAnchorResolver(e){this.cueAnchorResolver=e}captureBreakAnchor(e){this.resetBreakResumeState();const t=this.cueAnchorResolver?.(e)??null,s=null!==t&&Number.isFinite(t),n=s?null:this.pdtBreakAnchor(e);this.state.contentPositionAtBreakStart=s?t:n??this.deps.contentPlayer.currentTime,this.state.resumeAnchorIsCue=s,this.state.resumeAnchorIsPdt=null!==n,this.state.resumeAnchorSeekableEnd=this.contentSeekableEnd(),this.streamClock.beginBreak(),this.pdtClock.beginBreak()}pdtBreakAnchor(t){if(this.state.breakTimebase!==e.WALLCLOCK||"string"!=typeof t.start)return null;const s=l(this.deps.contentPlayer);if(!s)return null;const n=i({startMs:Date.parse(t.start),durationSec:t.duration??0,sample:s});return n?(n.sampleLateSec>r&&this.deps.diagnose?.("DA-BREAK-CUT-LATE",`Break ${t.id}: content was cut ${n.sampleLateSec}s after the break start on the stream clock; the viewer saw the stream's own break until then`,{breakId:t.id,lateSec:n.sampleLateSec,start:t.start,cutPdt:new Date(s.programDateTimeMs).toISOString(),cutAt:f(s.currentTime),anchor:n.anchor}),n.anchor):null}reanchorMidBreak(e){this.state.contentPositionAtBreakStart=e,this.state.resumeAnchorIsCue=!1,this.state.resumeAnchorIsPdt=!1,this.state.resumeAnchorSeekableEnd=this.contentSeekableEnd()}liveEdgeDistanceSec(){return c(this.deps.contentPlayer)}liveSafeSeekEnd(){return m(this.deps.contentPlayer)}liveSafeSeekStart(){return p(this.deps.contentPlayer)}liveSeekableWindowSec(){return g(this.deps.contentPlayer)}contentSeekableEnd(){return d(this.deps.contentPlayer)}resumeAnchorCushionSec(){const e=this.state.resumeAnchorSeekableEnd;return null!==e&&Number.isFinite(e)?f(e-this.state.contentPositionAtBreakStart):null}isPlausibleSeekEnd(e){return h(this.deps.contentPlayer,e)}plannedResumeTarget(e){const t=this.computedResumeTarget(e);return null===t?null:this.streamClock.target(e)??this.pdtClock.target(e)??this.cueEndTarget(e)??t}computedResumeTarget(e){if(t(e))return null;const s=this.resolvePolicy(e,this.state.contentPositionAtBreakStart);return s.seek?s.target:null}resumesAtBreakEnd(e){if(t(e))return!1;const s=this.resolvePolicy(e,0);return s.seek&&Math.abs(s.offset-(e.duration??0))<.001}resolvePolicy(e,t){return s({anchor:t,duration:e.duration,adDuration:e.duration,...this.state.adInsertionType?{insertionType:this.state.adInsertionType}:{},..."number"==typeof e.resumeOffset?{resumeOffset:e.resumeOffset}:{}})}cueEndTarget(e){if(!this.state.resumeAnchorIsCue||!this.resumesAtBreakEnd(e))return null;const t=this.cueEndResolver?.(e)?.mediaTime;return"number"==typeof t&&Number.isFinite(t)?t:null}maybePrepositionContentForResume(){if("shared-element"===this.deps.getInsertionMode())return;if(!this.state.contentPaused||null!==this.state.resumePrepositionedTo)return;if(null!==this.state.resumePrepositionSettle)return;if(this.state.resumeCatchupActive)return;if(Number.isFinite(this.deps.contentPlayer.duration))return;const e=this.state.currentBreak;if(!e)return;const t=this.plannedResumeTarget(e);if(null===t)return;const s=this.contentSeekableEnd();if(null===s)return;const n=this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec,r=(e.duration??0)-n;let i;if(t<=s)i=t;else{if(!(r<=3))return;i=s}this.deps.log(i===t?`Resume pre-positioning: seeking content to ${t}s during the break (window reaches ${s}s)`:`Resume pre-positioning: window only reaches ${s}s and the ad ends in ${r}s — moving content as close to ${t}s as exists`),this.streamClock.noteSeek(i),this.streamClock.arm(),this.deps.contentPlayer.seek(i);const a=setTimeout(()=>{this.state.resumePrepositionSettle=null;const e=this.deps.contentPlayer.currentTime;Math.abs(e-i)<=S?(this.state.resumePrepositionedTo=e,this.deps.log(`Resume pre-positioned: content is at ${e}s`)):this.deps.log(`Resume pre-position rejected: asked ${i}s, landed ${e}s — window is short, will retry`)},250);this.state.resumePrepositionSettle=a}maybeCatchUpContentForResume(e){if("shared-element"===this.deps.getInsertionMode())return;if(!this.state.contentPaused||Number.isFinite(this.deps.contentPlayer.duration))return;const t=this.plannedResumeTarget(e);if(null===t)return;const s=this.deps.contentPlayer.currentTime;if(this.state.resumeCatchupActive){const n=(e.duration??0)-(this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec);if(s>t+1)return this.state.resumeCatchupActive=!1,this.deps.contentPlayer.pause(),this.state.resumePrepositionedTo=this.deps.contentPlayer.currentTime,void this.deps.log(`Resume catch-up halted: content reached ${this.deps.contentPlayer.currentTime}s, past the ${t}s target`);if(n<=30&&n>=1){this.state.resumeAlignTicks+=1;const e=s+n,r=e-t;if(Math.abs(r)>S){const i=s-r,a=this.liveSafeSeekEnd();if(null!==a){const e=a>this.state.lastResumeSafeEnd+.25;e&&Number.isFinite(this.state.lastResumeSafeEnd)&&this.state.resumeSawSteps.push(Math.round(100*(a-this.state.lastResumeSafeEnd))/100),a>this.state.lastResumeSafeEnd&&(this.state.lastResumeSafeEnd=a);const s=a-i;if(s>this.state.resumeBestSawMarginSec&&(this.state.resumeBestSawMarginSec=s),i>a)return void(this.state.resumeAlignBlockedDiagnosed||(this.state.resumeAlignBlockedDiagnosed=!0,this.deps.log(`Resume align waiting: ${i}s is past the published edge ${a}s (${Math.round(1e3*(i-a))}ms short) — rolling until a manifest step brings it in reach`),this.deps.diagnose?.("DA-RESUME-ALIGN-BLOCKED",`Resume alignment cannot reach ${t}s yet: the published edge is ${a}s`,{target:t,requested:i,safeEnd:a,shortfallSec:Math.round(1e3*(i-a))/1e3,remainingSec:n})));if(!e&&this.state.resumeSawSeekDone)return;this.state.resumeSawSeekDone=!0}if(this.state.lastResumeAlignRemainingSec-n<.5)return;this.state.lastResumeAlignRemainingSec=n,this.deps.log(`Resume align: landing ${e}s vs target ${t}s (${Math.round(1e3*r)}ms) — content to ${i}s, ${n}s of ad left`),this.state.resumeAlignCorrectionsMs.push(Math.round(1e3*r)),this.streamClock.noteSeek(i),this.deps.contentPlayer.seek(i)}}return}if(t-s<=S)return;const n=this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec,r=(e.duration??0)-n;if(r<=0)return;if(r>30)return;const i=this.contentSeekableEnd();if(null===i)return;const a=t-r+.3,o=Math.min(a,i);this.state.resumeCatchupActive=!0,this.state.resumeCatchupRan=!0,this.deps.muteContent(),this.deps.log(`Resume catch-up: content to ${o}s, playing toward ${t}s with ${r}s of ad left`),this.deps.diagnose?.("DA-RESUME-CATCHUP",`Break ${e.id}: playing content from ${o}s toward ${t}s with ${r}s of ad left`,{breakId:e.id,startPos:o,target:t,remainingAdSec:Math.round(1e3*r)/1e3,publishedEnd:i,clampedStart:o<a}),Math.abs(s-o)>S&&(this.streamClock.noteSeek(o),this.deps.contentPlayer.seek(o)),this.streamClock.arm(),this.deps.contentPlayer.play()}endResumeCatchup(e=!1){this.state.resumeCatchupActive&&(this.state.resumeCatchupActive=!1,e||this.deps.contentPlayer.pause())}get resumeHandoverInMotion(){return this.state.resumeCatchupActive}resumeAlignSummary(){const e=this.state.resumeAlignCorrectionsMs;return{catchUpRan:this.state.resumeCatchupRan||this.state.resumeCatchupActive||e.length>0||null!==this.state.resumePrepositionedTo,alignCorrections:e.length,placedOnStep:this.state.resumeSawSeekDone,alignTicks:this.state.resumeAlignTicks,sawSteps:this.state.resumeSawSteps.slice(0,12),sawStepCount:this.state.resumeSawSteps.length,bestSawMarginSec:Number.isFinite(this.state.resumeBestSawMarginSec)?Math.round(1e3*this.state.resumeBestSawMarginSec)/1e3:null,alignCorrectionsMs:e.slice(0,12),alignLastErrorMs:e.length?e[e.length-1]:null}}cancelResumeLandedProbe(){const e=this.state.resumeLandedProbe;this.state.resumeLandedProbe=null,e?.()}probeResumeLanding(e,t,s=null){this.cancelResumeLandedProbe();let n=!1;const r=(r,i)=>{if(n)return;n=!0,this.cancelResumeLandedProbe();const a=null===r?null:Math.round(1e3*(r-t))/1e3,o=null===r||null===s?null:Math.round(1e3*(r-s))/1e3,u=null!==s&&Math.abs(s-t)>=5e-4&&null!==o?`; ${Math.abs(o)}s ${o<0?"before":"after"} the ${s}s resume point`:"";this.deps.log(null===r?`Resume landing unknown for break ${e} (${i})`:`Resume landed at ${r}s, ${a}s from the ${t}s asked for${u} (break ${e})`),this.deps.diagnose?.("DA-RESUME-LANDED",null===r?`Resume landing for break ${e} could not be measured (${i})`:`Resume for break ${e} landed at ${r}s, ${a}s from the ${t}s requested${u}`,{breakId:e,target:t,landed:r,errorSec:a,...null===s?{}:{policyTarget:s,policyErrorSec:o},...i?{reason:i}:{}})},i=()=>{this.streamClock.updateLanding(e,this.deps.contentPlayer.currentTime),r(this.deps.contentPlayer.currentTime)},a=setTimeout(()=>r(null,"no seeked event"),2e3);this.state.resumeLandedProbe=()=>{clearTimeout(a),this.deps.contentPlayer.off("seeked",i)},this.deps.contentPlayer.on("seeked",i)}async waitForLiveEdge(e=()=>!0){for(let t=0;t<20;t++){if(!e())return null;const t=this.deps.contentPlayer.videoElement,s=t?.seekable;if(s&&s.length>0){const e=s.end(s.length-1);if(Number.isFinite(e)&&e>0)return e}if(await new Promise(e=>setTimeout(e,100)),!e())return null}return this.deps.log("shared-element: no seekable range after restore; staying at the live edge"),null}resetBreakResumeState(){this.state.resumeAnchorIsCue=!1,this.state.resumeAnchorIsPdt=!1,this.state.resumeAnchorSeekableEnd=null,this.state.resumePrepositionedTo=null,this.state.resumeCatchupActive=!1,this.state.resumeCatchupRan=!1,this.state.resumeAlignCorrectionsMs=[],this.state.lastResumeAlignRemainingSec=Number.POSITIVE_INFINITY,this.state.resumeAlignBlockedDiagnosed=!1,this.state.lastResumeSafeEnd=Number.NEGATIVE_INFINITY,this.state.resumeSawSeekDone=!1,this.state.resumeBestSawMarginSec=Number.NEGATIVE_INFINITY,this.state.resumeAlignTicks=0,this.state.resumeSawSteps=[],this.state.resumePrepositionSettle&&(clearTimeout(this.state.resumePrepositionSettle),this.state.resumePrepositionSettle=null),this.state.adSecondsPlayedThisBreak=0,this.state.currentAssetElapsedSec=0}beginContinueContentPlayout(){this.continuePlayout.beginContinueContentPlayout()}distanceToLiveEdgeSec(){const e=this.contentSeekableEnd();return null===e?null:f(e-this.deps.contentPlayer.currentTime)}secondsUntilCut(e){return this.continuePlayout.secondsUntilCut(e)}restoreContinueQuality(e){this.continuePlayout.restoreContinueQuality(e)}currentContinueDriftSec(){return this.continuePlayout.currentContinueDriftSec()}abandonContinueContentPlayout(e){this.continuePlayout.abandonContinueContentPlayout(e)}endContinueContentPlayout(){this.continuePlayout.endContinueContentPlayout()}setContinueRate(e){this.continuePlayout.setContinueRate(e)}tickContinueContentPlayout(){this.continuePlayout.tickContinueContentPlayout()}resolveResumeTarget(t,r=null){this.resumePolicyTarget=null;const i=null!==r,a=(this.state.resumeAnchorIsCue||this.state.resumeAnchorIsPdt)&&!i,o=i?r:a?t.duration:this.state.adSecondsPlayedThisBreak,u=s({anchor:this.state.contentPositionAtBreakStart,duration:t.duration,adDuration:o,...i?{}:{insertionType:this.state.adInsertionType},...i?{resumeOffset:r}:"number"==typeof t.resumeOffset?{resumeOffset:t.resumeOffset}:{}});if(u.clamped&&this.deps.diagnose?.("DA-RESUME-OFFSET-CLAMPED",`Resume offset shortened to the ${u.offset}s of ad actually played for break ${t.id}`,{breakId:t.id,offset:u.offset,source:u.source}),!u.seek)return this.deps.log(`Resume: no seek for break ${t.id} (offset 0, ${u.source})`),null;const l=this.streamClock.handBackTarget(t,i)??this.pdtClock.handBackTarget(t,i),d=null!==l||i?null:this.reportCueEnd(t,u.target),h=l??d??u.target;this.resumePolicyTarget=h;const c=this.streamClock.seekBasis(t)??this.pdtClock.seekBasis()??(null!==d?"cue_end":`offset ${u.offset}s, ${u.source}`);this.deps.log(`Resume-seek to ${h}s (${c}) for break ${t.id}`);const m=this.resumeAlignSummary(),p=this.liveSafeSeekEnd();this.deps.diagnose?.("DA-RESUME-SEEK",`Resume-seek to ${h}s (${c}) for break ${t.id}`,{breakId:t.id,target:h,offset:u.offset,source:u.source,...h===u.target?{}:{computedTarget:u.target},...null===d?{}:{cueEndTarget:d},...this.streamClock.seekContext(t,h,u.target),...this.pdtClock.seekContext(t,h,u.target),anchorPosition:this.state.contentPositionAtBreakStart,anchorIsCue:this.state.resumeAnchorIsCue,anchorIsPdt:this.state.resumeAnchorIsPdt,anchorSeekableEnd:this.state.resumeAnchorSeekableEnd,anchorCushionSec:this.resumeAnchorCushionSec(),safeEnd:p,seekableEndNow:this.contentSeekableEnd(),liveEdge:this.deps.contentPlayer.liveEdge??null,unreachableEdgeSec:k(this.deps.contentPlayer),...m.catchUpRan?m:{}}),this.streamClock.reportCorrection(t,h,u.target,i),this.pdtClock.reportCorrection(t,h,u.target,i);(this.state.breakTimebase===e.PTS||this.state.breakTimebase===e.MEDIATIME)&&"number"==typeof t.start&&this.state.callbacks?.onContentResumeSeek?.(h,t.start+t.duration);const g=this.liveSafeSeekStart(),f=n({target:h,safeStart:g,safeEnd:p,windowSec:this.liveSeekableWindowSec()}),{target:C,shortfallSec:P,bound:b}=f;if(f.startClampSkipped){const{behindSec:e,windowSec:s,maxBehindSec:n}=f.startClampSkipped;this.deps.log(`Resume-seek: ${h}s is ${e}s behind the element's live window start (window ${s}s); not clamping`),this.deps.diagnose?.("DA-RESUME-START-CLAMP-SKIPPED",`Resume target ${h}s for break ${t.id} is ${e}s behind the element's seekable start but the gap exceeds half the ${s}s window; left unchanged`,{breakId:t.id,requested:h,seekableStart:g,behindSec:e,windowSec:s,maxBehindSec:n,...this.resumeAlignSummary()})}if(null!==this.state.resumePrepositionedTo&&Math.abs(this.deps.contentPlayer.currentTime-C)<=S){const e=this.deps.contentPlayer.currentTime;return this.deps.log(`Resume: content already at ${e}s for break ${t.id} (pre-positioned during the break)`+(P>0?` — ${P}s short of policy, those frames are not published yet`:"")+" — no seek"),this.deps.diagnose?.("DA-RESUME-PREPOSITIONED",`Break ${t.id} resumed without a seek: content was moved to ${e}s during the break`+(P>0?` (${P}s short of the ${h}s policy target)`:""),{breakId:t.id,target:h,prepositionedTo:this.state.resumePrepositionedTo,landed:e,shortfallSec:P,exact:0===P,...this.resumeAlignSummary()}),this.streamClock.armLanding(t,e),this.pdtClock.armLanding(t),null}return P>0&&(this.deps.log("start"===b?`Resume-seek clamped: ${h}s is behind the live window start (already evicted); seeking ${C}s instead (${P}s early)`:`Resume-seek clamped: ${h}s is beyond what the stream has published; seeking ${C}s instead (${P}s short)`),this.deps.diagnose?.("DA-RESUME-TARGET-CLAMPED","start"===b?`Resume target ${h}s for break ${t.id} is behind the live window start (already evicted); clamped to ${C}s`:`Resume target ${h}s for break ${t.id} is beyond the published window; clamped to ${C}s`,{breakId:t.id,requested:h,clamped:C,shortfallSec:P,bound:b,...this.resumeAlignSummary()})),this.streamClock.armLanding(t,C),this.pdtClock.armLanding(t),C}reportCueEnd(e,t){const s=this.state.resumeAnchorIsCue?this.cueEndResolver?.(e):null;if(!s||!Number.isFinite(s.mediaTime))return null;const n=this.resumesAtBreakEnd(e),r=f(s.mediaTime-t);return this.deps.diagnose("DA-RESUME-CUE-END",n?`Break ${e.id}: resuming on the stream's cue_end at ${s.mediaTime}s (${r}s from the computed ${t}s)`:`Break ${e.id}: cue_end at ${s.mediaTime}s not used; the resume policy does not resume at the break end`,{breakId:e.id,computedTarget:t,cueEndTarget:s.mediaTime,deltaSec:r,applied:n,stage:"hand-back",corrected:!1}),n?s.mediaTime:null}applyResumeSeek(e,t=null){const s=this.resolveResumeTarget(e,t);null!==s&&(this.probeResumeLanding(e.id,s,this.resumePolicyTarget),this.streamClock.noteSeek(s),this.deps.contentPlayer.seek(s))}}
|
|
@@ -5,6 +5,7 @@ import { type GamPodAdInfo, type GamPodAdStep } from '../GamPodAdTracker';
|
|
|
5
5
|
import { type GamSessionIdentity } from '../resolveGamVendorConfiguration';
|
|
6
6
|
import type { AdBreakStep } from '../AdBreakSequencer';
|
|
7
7
|
import type { AdPlayerState } from './AdPlayerState';
|
|
8
|
+
import type { AssetParameterResolver } from './AssetParameterResolver';
|
|
8
9
|
export interface GamEventCallbacks {
|
|
9
10
|
onAdFirstQuartile?: (breakInfo: Break, asset: Asset, adIndex?: number, totalAds?: number) => void;
|
|
10
11
|
onAdMidpoint?: (breakInfo: Break, asset: Asset, adIndex?: number, totalAds?: number) => void;
|
|
@@ -33,7 +34,7 @@ export interface GamPodBridgeDeps {
|
|
|
33
34
|
}) => void;
|
|
34
35
|
onAdEnded: () => void;
|
|
35
36
|
pushAssetParameters: (assetParameters: Record<string, string> | undefined, assetId?: string) => void;
|
|
36
|
-
|
|
37
|
+
assetParameterResolver: Pick<AssetParameterResolver, 'resolveSessionAdTagParameters' | 'resolveSessionAdTagParametersForManifest'>;
|
|
37
38
|
diagnose: (code: string, message: string, context?: Record<string, unknown>) => void;
|
|
38
39
|
log: (...args: unknown[]) => void;
|
|
39
40
|
}
|
|
@@ -41,13 +42,22 @@ export interface GamPodBridgeDeps {
|
|
|
41
42
|
export declare class GamPodBridge {
|
|
42
43
|
private readonly state;
|
|
43
44
|
private readonly deps;
|
|
45
|
+
private preparedGamSession;
|
|
46
|
+
private nextGamIdentity;
|
|
44
47
|
constructor(state: AdPlayerState, deps: GamPodBridgeDeps);
|
|
45
48
|
setConfig(config: GamConfig | undefined): void;
|
|
46
49
|
setEventCallbacks(callbacks: GamEventCallbacks): void;
|
|
47
50
|
get eventCallbacks(): GamEventCallbacks;
|
|
48
51
|
startGamSession(identity: GamSessionIdentity, assetParameters?: Record<string, string>): Promise<void>;
|
|
49
52
|
endGamSession(): void;
|
|
53
|
+
prepareNextGamSession(identity: GamSessionIdentity, manifestAssetParameters: Record<string, string> | undefined): Promise<boolean>;
|
|
54
|
+
adoptPreparedGamSession(identity: GamSessionIdentity, sessionAssetParameters: Record<string, string> | undefined, manifestAssetParameters: Record<string, string> | undefined): Promise<boolean>;
|
|
55
|
+
discardPreparedGamSession(): void;
|
|
56
|
+
shouldSkipGamPodPreload(breakInfo: Break): boolean;
|
|
50
57
|
initializeGam(): Promise<void>;
|
|
58
|
+
private createGamStreamManager;
|
|
59
|
+
private createGamStreamConfig;
|
|
60
|
+
private attachGamCallbacks;
|
|
51
61
|
isGamEnabled(): boolean;
|
|
52
62
|
isGamReady(): boolean;
|
|
53
63
|
isGamPodAsset(asset: Asset): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Vendor as t}from"../../types/BreakManifest";import{GamPodAdTracker as e}from"../GamPodAdTracker";import{GAM_DUPLICATE_METADATA_DIAGNOSTIC_MESSAGE as s,GamStreamManager as a}from"../GamStreamManager";import{isGamVendorParameters as i}from"../GamPodUrlBuilder";import{gamPodIdentityMismatches as
|
|
1
|
+
import{Vendor as t}from"../../types/BreakManifest";import{GamPodAdTracker as e}from"../GamPodAdTracker";import{GAM_DUPLICATE_METADATA_DIAGNOSTIC_MESSAGE as s,GamStreamManager as a}from"../GamStreamManager";import{isGamVendorParameters as i}from"../GamPodUrlBuilder";import{gamPodIdentityMismatches as r}from"../resolveGamVendorConfiguration";export class GamPodBridge{constructor(t,e){this.state=t,this.deps=e,this.preparedGamSession=null,this.nextGamIdentity=null,this.identityMismatchAssetId=null,this.midBreakEndPosition=null,this.callbacks={}}setConfig(t){this.state.gamConfig=t}setEventCallbacks(t){this.callbacks=t}get eventCallbacks(){return this.callbacks}async startGamSession(t,e){this.discardPreparedGamSession(),this.state.currentGamIdentity=t,this.state.sessionAssetParameters=e,this.state.pushedAssetParameters=null,await this.initializeGam()}endGamSession(){this.discardPreparedGamSession(),this.state.gamStreamManager?.reset(),this.state.gamInitPromise=null,this.state.currentGamIdentity=void 0,this.state.sessionAssetParameters=void 0,this.state.manifestAssetParameters=void 0,this.state.sessionAssetParameterMacros={},this.state.updatedAssetParameters=void 0,this.state.pushedAssetParameters=null,this.state.earlyGamPodAdStart=null}async prepareNextGamSession(t,e){if(this.preparedGamSession&&this.preparedGamSession.identity.networkCode===t.networkCode&&this.preparedGamSession.identity.customAssetKey===t.customAssetKey)return await this.preparedGamSession.initPromise,this.preparedGamSession?.identity.networkCode===t.networkCode&&this.preparedGamSession.identity.customAssetKey===t.customAssetKey;this.discardPreparedGamSession();const s=this.createGamStreamManager();if(!s)throw new Error("GAM requires an adapter that exposes videoElement.");this.nextGamIdentity=t;const a=s.initialize(this.createGamStreamConfig(t,this.deps.assetParameterResolver.resolveSessionAdTagParametersForManifest(e)));this.preparedGamSession={identity:t,manager:s,initPromise:a};try{return await a,this.preparedGamSession?.manager===s}catch(t){throw this.preparedGamSession?.manager===s&&(s.reset(),this.preparedGamSession=null),t}}async adoptPreparedGamSession(t,e,s){const a=this.preparedGamSession;if(!a||a.identity.networkCode!==t.networkCode||a.identity.customAssetKey!==t.customAssetKey)return this.discardPreparedGamSession(),!1;try{await a.initPromise}catch{return this.discardPreparedGamSession(),!1}if(this.preparedGamSession!==a)return!1;const i=this.state.gamStreamManager;return this.state.gamStreamManager=a.manager,this.state.gamInitPromise=a.initPromise,this.state.currentGamIdentity=t,this.state.sessionAssetParameters=e,this.state.manifestAssetParameters=s,this.state.pushedAssetParameters=null,this.attachGamCallbacks(a.manager),this.preparedGamSession=null,this.nextGamIdentity=null,i&&i!==a.manager&&i.reset(),!0}discardPreparedGamSession(){this.preparedGamSession?.manager.reset(),this.preparedGamSession=null,this.nextGamIdentity=null}shouldSkipGamPodPreload(e){const s=this.nextGamIdentity;if(!s)return!1;return(Array.isArray(e.variant)?e.variant:[e.variant]).some(e=>e.assets.some(e=>"vendor"===e.type&&e.vendor===t.GAM&&i(e.vendorParameters)&&0===r(s,e.vendorParameters).length))}async initializeGam(){if(!this.state.currentGamIdentity)return;if(this.deps.log("Initializing GAM stream manager..."),this.state.gamStreamManager)this.state.gamStreamManager.reset();else if(this.state.gamStreamManager=this.createGamStreamManager(),!this.state.gamStreamManager)return;this.attachGamCallbacks(this.state.gamStreamManager);const t=this.state.currentGamIdentity,e=this.deps.assetParameterResolver.resolveSessionAdTagParameters();this.state.gamInitPromise=this.state.gamStreamManager.initialize(this.createGamStreamConfig(t,e));try{await this.state.gamInitPromise,this.deps.log("GAM stream manager initialized, streamId:",this.state.gamStreamManager.getStreamId())}catch(t){throw this.state.gamInitPromise=null,t}}createGamStreamManager(){const t=this.deps.isSharedElement()?this.deps.contentPlayer.videoElement:this.deps.getAdPlayer()?.videoElement;if(!t)return console.warn("[OptiViewAds] GAM requires an adapter that exposes videoElement. GAM will be skipped."),null;const e=this.deps.isSharedElement()?void 0:this.deps.getAdContainer()||void 0;return new a(t,e,this.deps.debug)}createGamStreamConfig(t,e){return{networkCode:t.networkCode,customAssetKey:t.customAssetKey,...e&&{adTagParameters:e},...this.state.gamConfig?.streamActivityMonitorId&&{streamActivityMonitorId:this.state.gamConfig.streamActivityMonitorId}}}attachGamCallbacks(t){t.setCallbacks({onAdFirstQuartile:()=>this.emitGamQuartile(this.callbacks.onAdFirstQuartile),onAdMidpoint:()=>this.emitGamQuartile(this.callbacks.onAdMidpoint),onAdThirdQuartile:()=>this.emitGamQuartile(this.callbacks.onAdThirdQuartile),onAdStarted:t=>{this.deps.log("GAM ad started"),this.onGamPodAdStarted(t)},onAdComplete:()=>{this.deps.log("GAM ad complete"),"per-ad"===this.state.gamPodMode&&this.state.gamPodTracker&&this.emitGamPodSteps(this.state.gamPodTracker.onAdComplete())},onAdBreakEnded:()=>{this.deps.log("GAM ad break ended");const t=this.deps.getAdPlayerStreamRemainingSec();t>.25?this.onAdBreakEndedWithPodLeft(t):this.deps.onAdEnded()},onError:t=>this.deps.log("GAM error:",t),onDuplicateMetadata:t=>{this.deps.diagnose("DA-GAM-DUPLICATE-METADATA",s,{assetId:this.state.gamPodAsset?.id,startTime:t})}})}isGamEnabled(){return!1!==this.state.gamConfig?.enabled}isGamReady(){return!0===this.state.gamStreamManager?.isInitialized()}isGamPodAsset(e){return"vendor"===e.type&&e.vendor===t.GAM&&this.isGamReady()}isExpandingPodAsset(t){return("pending"===this.state.gamPodMode||"per-ad"===this.state.gamPodMode)&&(!!t&&t.id===this.state.gamPodAsset?.id)}beginGamPodTracking(t){this.resetGamPodTracking(),this.state.gamPodAsset=t,this.state.gamPodTracker=new e(t),this.state.gamPodMode="pending";const s=this.state.earlyGamPodAdStart;this.state.earlyGamPodAdStart=null,s&&this.onGamPodAdStarted(s)}fallBackToPodLevelReporting(){if("pending"!==this.state.gamPodMode)return;this.state.gamPodMode="legacy",this.state.gamPodTracker=null;const t=this.state.withheldPodAdBegin;this.state.withheldPodAdBegin=null,t&&this.deps.emitStep(t)}resetGamPodTracking(){this.state.gamPodMode="none",this.state.gamPodTracker=null,this.state.gamPodAsset=null,this.state.withheldPodAdBegin=null,this.midBreakEndPosition=null}onGamPodAdProgress(t){"pending"===this.state.gamPodMode&&(t<2||(this.deps.log("No IMA pod ad reported after the pod started playing; reporting the pod as a single ad"),this.fallBackToPodLevelReporting()))}onGamPodAdStarted(t){t&&("none"!==this.state.gamPodMode?this.state.gamPodTracker&&("pending"===this.state.gamPodMode&&(this.state.withheldPodAdBegin=null,this.state.gamPodMode="per-ad"),"per-ad"===this.state.gamPodMode&&(this.logRestartedPodPosition(t),this.emitGamPodSteps(this.state.gamPodTracker.onAdStarted(t)))):this.state.earlyGamPodAdStart=t)}logRestartedPodPosition(t){const e=this.midBreakEndPosition;null!==e&&(this.midBreakEndPosition=null,"number"==typeof t.adPosition&&t.adPosition<=e&&this.deps.log(`IMA restarted pod ad positions after a mid-pod ad break end (ad ${t.adPosition} after ad ${e})`))}onAdBreakEndedWithPodLeft(t){const e="per-ad"===this.state.gamPodMode?this.state.gamPodTracker:null;if(!e||e.reportedLastAd)return this.deps.log(`GAM pod has ${t.toFixed(3)}s of slate left; playing it out`),void(e&&this.emitGamPodSteps(e.finish()));const s=e.lastAdPosition,a=e.lastTotalAds,i=a>0?` of ${a}`:"";this.deps.log(`IMA ad break ended after ad ${s}${i}; ${t.toFixed(3)}s of pod left, waiting for the next ad`),this.deps.diagnose("DA-GAM-POD-MID-BREAK-END",`IMA reported the ad break ended after ad ${s}${i} with ${t.toFixed(3)}s of pod left; per-ad reporting continues until the pod ends.`,{assetId:this.state.gamPodAsset?.id,adPosition:s,totalAds:a,remainingSec:t}),this.midBreakEndPosition=s,this.emitGamPodSteps(e.onAdBreakEnded())}emitGamPodSteps(t){const e=this.deps.getCurrentBreak();if(e)for(const s of t){const t={adId:s.adId,creativeId:s.creativeId,podPosition:s.adPosition};"adbegin"===s.type?this.deps.emitAdStart(e,s.asset,s.adIndex,s.totalAds,t):this.deps.emitAdEnd(e,s.asset,s.adIndex,s.totalAds,t)}}emitGamQuartile(t){const e=this.deps.getCurrentBreak();if(!t||!e)return;const s="per-ad"===this.state.gamPodMode?this.state.gamPodTracker?.currentAd:null;if(s)return void t(e,s.asset,s.adIndex,s.totalAds);const a=this.deps.getCurrentAsset();a&&t(e,a)}async buildGamUrl(t,e){if(this.identityMismatchAssetId=null,!i(t.vendorParameters))return this.deps.log("Invalid GAM vendor parameters:",t.vendorParameters),null;if(this.state.gamInitPromise&&await this.state.gamInitPromise,!this.state.gamStreamManager?.isInitialized())throw this.deps.log("GAM stream manager not initialized"),new Error("GAM stream manager not initialized. The stream identity comes from the break manifest's vendorConfiguration — check for DA-GAM-CONFIG-MISSING.");const s=t.vendorParameters;if(this.state.currentGamIdentity){const e=r(this.state.currentGamIdentity,s);for(const s of e)this.deps.diagnose("DA-GAM-POD-IDENTITY-MISMATCH",`GAM asset ${t.id} skipped: vendorParameters.${s.field} "${s.assetValue}" does not match the stream session's "${s.sessionValue}".`,{assetId:t.id,field:s.field,assetValue:s.assetValue,sessionValue:s.sessionValue});if(e.length>0)return this.identityMismatchAssetId=t.id,null}this.state.earlyGamPodAdStart=null,this.deps.pushAssetParameters(t.assetParameters,t.id);const a=this.state.gamStreamManager.buildPodUrl(t.uri,e,s);return this.deps.log("Built GAM pod URL:",a),a}processTimedMetadata(t){this.state.gamStreamManager?.processTimedMetadata(t)}destroy(){this.resetGamPodTracking(),this.state.earlyGamPodAdStart=null,this.state.gamStreamManager?.destroy(),this.state.gamStreamManager=null,this.state.gamInitPromise=null}hasIdentityMismatch(t){return this.identityMismatchAssetId===t.id}}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { PlayerAdapter } from '../../interfaces/PlayerAdapter';
|
|
2
|
-
import
|
|
2
|
+
import { StaticAsset } from '../../types/BreakManifest';
|
|
3
|
+
import type { AssetUri, Break } from '../../types/BreakManifest';
|
|
3
4
|
import type { PauseAdResolution } from '../PauseAdController';
|
|
4
5
|
export interface PauseAdPresenterDeps {
|
|
5
6
|
contentPlayer: PlayerAdapter;
|
|
6
7
|
getStage: () => HTMLElement | null;
|
|
7
8
|
resolveUri: (uri: string | AssetUri[]) => string | null;
|
|
9
|
+
buildStaticAssetUrl: (asset: StaticAsset, breakInfo: Break) => string | null;
|
|
8
10
|
diagnose: (code: string, message: string, context?: Record<string, unknown>) => void;
|
|
9
11
|
log: (...args: unknown[]) => void;
|
|
10
12
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{MediaType as e}from"../../types/BreakManifest";import{AUX_FETCH_TIMEOUT_MS as t,fetchWithTimeout as s}from"../fetchWithTimeout";import{parseVastCompanionImage as i}from"../parseVastCompanion";import{parseVastLinearMediaFile as a}from"../parseVastLinear";import{resolvePauseResumePoint as n}from"../resolvePauseResumePoint";export class PauseAdPresenter{constructor(e){this.deps=e,this.pauseAdContainer=null,this.pauseAdVideoEl=null,this.pauseAdFadeTimer=null,this.pauseAdPinnedPosition=0,this.pauseAdResumeControls=[],this.pauseAdCreativeEndedCallback=null}async showPauseImage(e){const t=this.deps.getStage();if(!t)return;const s=await this.resolvePauseAsset(e);if(!s)return void this.deps.diagnose("DA-PAUSE-AD-NO-ASSET",`Pause ad '${e.break.id}' asset could not be resolved; nothing shown.`,{breakId:e.break.id,source:e.source,mediaType:e.mediaType});this.removePauseAdNow(),this.pauseAdPinnedPosition=this.deps.contentPlayer.currentTime;const i=document.createElement("div");if(i.className="dolby-pause-ad",i.style.cssText="position:absolute;top:0;left:0;right:0;bottom:0;z-index:150;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.35);opacity:0;transition:opacity 300ms ease-in-out;"+(s.clickThrough?"cursor:pointer;":""),"video"===s.kind){const e=document.createElement("video");e.className="dolby-pause-ad-video",e.style.cssText="max-width:100%;max-height:100%;object-fit:contain;",e.muted=!0,e.loop=!1,e.autoplay=!0,e.playsInline=!0,e.setAttribute("playsinline",""),e.src=s.url,this.pauseAdVideoEl=e,i.appendChild(e)}else{const e=document.createElement("img");e.className="dolby-pause-ad-image",e.style.cssText="max-width:100%;max-height:100%;object-fit:contain;",e.src=s.url,i.appendChild(e)}if(s.clickThrough){const e=s.clickThrough;i.addEventListener("click",()=>{window.open(e,"_blank","noopener")})}const a=this.buildPauseResumeButton(),n=this.buildPauseCloseButton();if(a.style.display="none",n.style.display="none",this.pauseAdResumeControls=[a,n],i.appendChild(a),i.appendChild(n),this.pauseAdVideoEl){const t=this.pauseAdVideoEl,s=e.break.id,i=()=>{this.pauseAdVideoEl===t&&this.pauseAdCreativeEndedCallback?.(s)};t.addEventListener("ended",i,{once:!0}),t.addEventListener("error",i,{once:!0}),t.play().catch(i)}t.appendChild(i),this.pauseAdContainer=i,requestAnimationFrame(()=>{this.pauseAdContainer===i&&(i.style.opacity="1")}),this.firePauseImpressions(s.impressions),this.deps.log("Pause ad shown:",e.break.id,e.source,e.mediaType)}hidePauseImage(){const e=this.pauseAdContainer;e&&(this.stopPauseAdVideo(),this.pauseAdContainer=null,e.style.opacity="0",this.pauseAdFadeTimer&&clearTimeout(this.pauseAdFadeTimer),this.pauseAdFadeTimer=setTimeout(()=>{e.parentElement&&e.parentElement.removeChild(e),this.pauseAdFadeTimer=null},300))}revealPauseAdResume(){for(const e of this.pauseAdResumeControls)e.style.display=""}setPauseAdCreativeEndedCallback(e){this.pauseAdCreativeEndedCallback=e}destroy(){this.removePauseAdNow()}removePauseAdNow(){this.pauseAdFadeTimer&&(clearTimeout(this.pauseAdFadeTimer),this.pauseAdFadeTimer=null),this.stopPauseAdVideo(),this.pauseAdContainer?.parentElement&&this.pauseAdContainer.parentElement.removeChild(this.pauseAdContainer),this.pauseAdContainer=null}stopPauseAdVideo(){this.pauseAdResumeControls=[];const e=this.pauseAdVideoEl;if(e){try{e.pause(),e.removeAttribute("src")}catch{}this.pauseAdVideoEl=null}}resumeFromPauseAd(){const e=n(this.pauseAdPinnedPosition,this.seekableStart(),this.seekableEnd());null!==e&&(this.deps.diagnose("DA-PAUSE-AD-DVR-EXPIRED",`Pause outlived the DVR window; resuming at the live edge (${e.toFixed(2)}s).`,{pinnedPosition:this.pauseAdPinnedPosition,liveEdge:e}),this.deps.contentPlayer.seek(e)),this.deps.contentPlayer.play()}seekableStart(){const e=this.deps.contentPlayer.videoElement?.seekable;return e&&0!==e.length?e.start(0):Number.NaN}seekableEnd(){const e=this.deps.contentPlayer.videoElement?.seekable;return e&&0!==e.length?e.end(e.length-1):Number.NaN}buildPauseResumeButton(){const e=document.createElement("button");return e.className="dolby-pause-ad-resume",e.setAttribute("aria-label","Resume playback"),e.style.cssText="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:72px;height:72px;border:none;border-radius:50%;cursor:pointer;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;",e.innerHTML='<svg width="32" height="32" viewBox="0 0 24 24" fill="#fff" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>',e.addEventListener("click",e=>{e.stopPropagation(),this.resumeFromPauseAd()}),e}buildPauseCloseButton(){const e=document.createElement("button");return e.className="dolby-pause-ad-close",e.setAttribute("aria-label","Close pause ad and resume"),e.style.cssText="position:absolute;top:8px;right:8px;width:32px;height:32px;border:none;border-radius:50%;cursor:pointer;background:rgba(0,0,0,0.6);color:#fff;font-size:18px;line-height:32px;text-align:center;padding:0;",e.textContent="✕",e.addEventListener("click",e=>{e.stopPropagation(),this.resumeFromPauseAd()}),e}async resolvePauseAsset(n){const o=n.asset,r=n.mediaType===e.VIDEO?"video":"image";if("static"===n.source){const e=this.deps.
|
|
1
|
+
import{MediaType as e}from"../../types/BreakManifest";import{AUX_FETCH_TIMEOUT_MS as t,fetchWithTimeout as s}from"../fetchWithTimeout";import{parseVastCompanionImage as i}from"../parseVastCompanion";import{parseVastLinearMediaFile as a}from"../parseVastLinear";import{resolvePauseResumePoint as n}from"../resolvePauseResumePoint";export class PauseAdPresenter{constructor(e){this.deps=e,this.pauseAdContainer=null,this.pauseAdVideoEl=null,this.pauseAdFadeTimer=null,this.pauseAdPinnedPosition=0,this.pauseAdResumeControls=[],this.pauseAdCreativeEndedCallback=null}async showPauseImage(e){const t=this.deps.getStage();if(!t)return;const s=await this.resolvePauseAsset(e);if(!s)return void this.deps.diagnose("DA-PAUSE-AD-NO-ASSET",`Pause ad '${e.break.id}' asset could not be resolved; nothing shown.`,{breakId:e.break.id,source:e.source,mediaType:e.mediaType});this.removePauseAdNow(),this.pauseAdPinnedPosition=this.deps.contentPlayer.currentTime;const i=document.createElement("div");if(i.className="dolby-pause-ad",i.style.cssText="position:absolute;top:0;left:0;right:0;bottom:0;z-index:150;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.35);opacity:0;transition:opacity 300ms ease-in-out;"+(s.clickThrough?"cursor:pointer;":""),"video"===s.kind){const e=document.createElement("video");e.className="dolby-pause-ad-video",e.style.cssText="max-width:100%;max-height:100%;object-fit:contain;",e.muted=!0,e.loop=!1,e.autoplay=!0,e.playsInline=!0,e.setAttribute("playsinline",""),e.src=s.url,this.pauseAdVideoEl=e,i.appendChild(e)}else{const e=document.createElement("img");e.className="dolby-pause-ad-image",e.style.cssText="max-width:100%;max-height:100%;object-fit:contain;",e.src=s.url,i.appendChild(e)}if(s.clickThrough){const e=s.clickThrough;i.addEventListener("click",()=>{window.open(e,"_blank","noopener")})}const a=this.buildPauseResumeButton(),n=this.buildPauseCloseButton();if(a.style.display="none",n.style.display="none",this.pauseAdResumeControls=[a,n],i.appendChild(a),i.appendChild(n),this.pauseAdVideoEl){const t=this.pauseAdVideoEl,s=e.break.id,i=()=>{this.pauseAdVideoEl===t&&this.pauseAdCreativeEndedCallback?.(s)};t.addEventListener("ended",i,{once:!0}),t.addEventListener("error",i,{once:!0}),t.play().catch(i)}t.appendChild(i),this.pauseAdContainer=i,requestAnimationFrame(()=>{this.pauseAdContainer===i&&(i.style.opacity="1")}),this.firePauseImpressions(s.impressions),this.deps.log("Pause ad shown:",e.break.id,e.source,e.mediaType)}hidePauseImage(){const e=this.pauseAdContainer;e&&(this.stopPauseAdVideo(),this.pauseAdContainer=null,e.style.opacity="0",this.pauseAdFadeTimer&&clearTimeout(this.pauseAdFadeTimer),this.pauseAdFadeTimer=setTimeout(()=>{e.parentElement&&e.parentElement.removeChild(e),this.pauseAdFadeTimer=null},300))}revealPauseAdResume(){for(const e of this.pauseAdResumeControls)e.style.display=""}setPauseAdCreativeEndedCallback(e){this.pauseAdCreativeEndedCallback=e}destroy(){this.removePauseAdNow()}removePauseAdNow(){this.pauseAdFadeTimer&&(clearTimeout(this.pauseAdFadeTimer),this.pauseAdFadeTimer=null),this.stopPauseAdVideo(),this.pauseAdContainer?.parentElement&&this.pauseAdContainer.parentElement.removeChild(this.pauseAdContainer),this.pauseAdContainer=null}stopPauseAdVideo(){this.pauseAdResumeControls=[];const e=this.pauseAdVideoEl;if(e){try{e.pause(),e.removeAttribute("src")}catch{}this.pauseAdVideoEl=null}}resumeFromPauseAd(){const e=n(this.pauseAdPinnedPosition,this.seekableStart(),this.seekableEnd());null!==e&&(this.deps.diagnose("DA-PAUSE-AD-DVR-EXPIRED",`Pause outlived the DVR window; resuming at the live edge (${e.toFixed(2)}s).`,{pinnedPosition:this.pauseAdPinnedPosition,liveEdge:e}),this.deps.contentPlayer.seek(e)),this.deps.contentPlayer.play()}seekableStart(){const e=this.deps.contentPlayer.videoElement?.seekable;return e&&0!==e.length?e.start(0):Number.NaN}seekableEnd(){const e=this.deps.contentPlayer.videoElement?.seekable;return e&&0!==e.length?e.end(e.length-1):Number.NaN}buildPauseResumeButton(){const e=document.createElement("button");return e.className="dolby-pause-ad-resume",e.setAttribute("aria-label","Resume playback"),e.style.cssText="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:72px;height:72px;border:none;border-radius:50%;cursor:pointer;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;",e.innerHTML='<svg width="32" height="32" viewBox="0 0 24 24" fill="#fff" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>',e.addEventListener("click",e=>{e.stopPropagation(),this.resumeFromPauseAd()}),e}buildPauseCloseButton(){const e=document.createElement("button");return e.className="dolby-pause-ad-close",e.setAttribute("aria-label","Close pause ad and resume"),e.style.cssText="position:absolute;top:8px;right:8px;width:32px;height:32px;border:none;border-radius:50%;cursor:pointer;background:rgba(0,0,0,0.6);color:#fff;font-size:18px;line-height:32px;text-align:center;padding:0;",e.textContent="✕",e.addEventListener("click",e=>{e.stopPropagation(),this.resumeFromPauseAd()}),e}async resolvePauseAsset(n){const o=n.asset,r=n.mediaType===e.VIDEO?"video":"image";if("static"===n.source){const e=this.deps.buildStaticAssetUrl(o,n.break);if(!e)return null;const t=o.interaction?.clickThrough;return{kind:r,url:e,...t?{clickThrough:t}:{},impressions:[]}}const d=this.deps.resolveUri(o.uri);if(!d)return null;try{const e=await s(d,{},t),n=await e.text();if("video"===r){const e=a(n);if(!e)return null;const t=e.clickThrough??o.interaction?.clickThrough;return{kind:r,url:e.mediaFileUrl,...t?{clickThrough:t}:{},impressions:e.impressions}}const l=i(n);if(!l)return null;const u=l.clickThrough??o.interaction?.clickThrough;return{kind:r,url:l.imageUrl,...u?{clickThrough:u}:{},impressions:l.impressions}}catch(e){return this.deps.log("Pause VAST fetch/parse failed:",e),null}}firePauseImpressions(e){for(const t of e)try{(new Image).src=t}catch{}}}
|