@dolby-optiview/ads-sdk-core 1.0.0 → 1.1.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/README.md +68 -0
- package/dist/OptiViewAds.d.ts +1 -0
- package/dist/OptiViewAds.js +1 -1
- package/dist/diagnostics/recordEventTimeline.d.ts +3 -0
- package/dist/diagnostics/recordEventTimeline.js +1 -0
- package/dist/generated/diagnosticCodes.d.ts +96 -1
- 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 +1 -1
- package/dist/interfaces/PlayerAdapter.d.ts +33 -0
- package/dist/internal/index.d.ts +1 -1
- package/dist/internal/index.js +1 -1
- package/dist/services/AdPlayerController.d.ts +3 -0
- package/dist/services/AdPlayerController.js +1 -1
- package/dist/services/BreakScheduler.d.ts +5 -2
- package/dist/services/BreakScheduler.js +1 -1
- package/dist/services/CmcdAdReporter.d.ts +10 -0
- package/dist/services/CmcdAdReporter.js +1 -1
- package/dist/services/GamPodAdTracker.d.ts +12 -1
- package/dist/services/GamPodAdTracker.js +1 -1
- package/dist/services/LensReporter.d.ts +3 -1
- package/dist/services/LensReporter.js +1 -1
- package/dist/services/LiveStartPushbackController.d.ts +36 -0
- package/dist/services/LiveStartPushbackController.js +1 -0
- package/dist/services/MetadataCueTracker.d.ts +15 -0
- package/dist/services/MetadataCueTracker.js +1 -1
- package/dist/services/VastAdManager.js +1 -1
- package/dist/services/adPlayer/AdPlayerState.d.ts +11 -0
- package/dist/services/adPlayer/AdPlayerState.js +1 -1
- package/dist/services/adPlayer/AdPreloader.d.ts +9 -1
- package/dist/services/adPlayer/AdPreloader.js +1 -1
- package/dist/services/adPlayer/AdStage.d.ts +5 -0
- package/dist/services/adPlayer/AdStage.js +1 -1
- package/dist/services/adPlayer/AssetPlayback.d.ts +15 -1
- package/dist/services/adPlayer/AssetPlayback.js +1 -1
- package/dist/services/adPlayer/ContentResumeCoordinator.d.ts +39 -0
- package/dist/services/adPlayer/ContentResumeCoordinator.js +1 -1
- package/dist/services/adPlayer/GamPodBridge.d.ts +7 -0
- package/dist/services/adPlayer/GamPodBridge.js +1 -1
- package/dist/services/adPlayer/PreparedPresentation.d.ts +32 -0
- package/dist/services/adPlayer/PreparedPresentation.js +1 -0
- package/dist/services/adPlayer/SharedElementBreakPlayer.d.ts +8 -0
- package/dist/services/adPlayer/SharedElementBreakPlayer.js +1 -1
- package/dist/services/adPlayer/classifyMediaError.d.ts +9 -0
- package/dist/services/adPlayer/classifyMediaError.js +1 -0
- package/dist/services/adPlayer/liveEdge.d.ts +29 -0
- package/dist/services/adPlayer/liveEdge.js +1 -1
- package/dist/services/anvatoCueRuntime.d.ts +7 -0
- package/dist/services/anvatoCueRuntime.js +1 -1
- package/dist/services/classifyImaAdError.d.ts +6 -0
- package/dist/services/classifyImaAdError.js +1 -0
- package/dist/services/html5ImaErrorNames.d.ts +2 -0
- package/dist/services/html5ImaErrorNames.js +1 -0
- package/dist/services/lensBackend.d.ts +1 -1
- package/dist/services/resolveLiveStartPushback.d.ts +1 -0
- package/dist/services/resolveLiveStartPushback.js +1 -0
- package/dist/session/AdBreakStatusTracker.js +1 -1
- package/dist/session/BreakLifecycle.js +1 -1
- package/dist/session/SessionStarter.d.ts +1 -0
- package/dist/session/SessionStarter.js +1 -1
- package/dist/types/AdError.d.ts +12 -0
- package/dist/types/AdError.js +1 -0
- package/dist/types/OptiViewAdsConfig.d.ts +17 -0
- package/dist/types/VendorAdError.d.ts +6 -5
- package/dist/types/VendorAdError.js +1 -1
- package/dist/vendor/lens-sdk/THIRD-PARTY-NOTICES.txt +1 -1
- package/dist/vendor/lens-sdk/index.d.ts +1 -1
- package/dist/vendor/lens-sdk/index.js +1 -1
- package/dist/vendor/lens-sdk/lens-core-uGZfZpQy.js +1 -0
- package/dist/vendor/lens-sdk/lens.d.ts +2 -0
- package/dist/vendor/lens-sdk/teardown.d.ts +13 -0
- package/dist/vendor/lens-sdk/transport.d.ts +11 -2
- package/dist/vendor/lens-sdk/types.d.ts +56 -5
- package/dist/vendor/lens-sdk/validation.d.ts +4 -1
- package/package.json +1 -1
- package/dist/vendor/lens-sdk/lens-core-Q5lbQGMC.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AssetType as e,BreakFormat as t,MediaType as s,Vendor as
|
|
1
|
+
import{AssetType as e,BreakFormat as t,MediaType as s,Vendor as r,isVastAsset as a,isVendorAsset as i}from"../../types/BreakManifest";import{AUX_FETCH_TIMEOUT_MS as d,fetchWithTimeout as o}from"../fetchWithTimeout";import{resolveVastAdmission as n}from"../resolveVastAdmission";import{isImaCsaiSdkAvailable as l,VastAdManager as p}from"../VastAdManager";import{invalidateMediaPreload as h}from"./AdPlayerState";import{AdError as m}from"../../types/AdError";export const AD_LOAD_RETRY_AFTER_MS=2e3;export function adLoadShowsNoProgress(e){return!e||0===e.readyState&&0===e.buffered.length}export class AdPreloader{constructor(e){this.deps=e,this.warmedBreakIds=new Set}classifyCreative(e){const t=(e.split(/[?#]/)[0]??"").toLowerCase();return t.endsWith(".m3u8")||t.endsWith(".mpd")?"stream":/\.(mp4|m4v|m4a|webm|mov|ogg|ogv|mp3)$/.test(t)?"progressive":"unknown"}diagnosePreloadPath(e,t,s){const r=this.classifyCreative(t);this.deps.diagnose("DA-PRELOAD-PATH",`Ad preload path: ${s} (${r} creative)`,{breakId:e,adPreload:this.deps.preloadMode,path:s,creative:r})}async preload(r){if(this.deps.isPlaying())return void this.deps.log("Preload skipped: ad already playing");const i=h(this.deps.state),d=this.deps.getAdPlayer();if("none"===this.deps.preloadMode){const t=this.deps.getAssets(r)[0],s=t&&t.type===e.STATIC?this.deps.resolveUri(t.uri)??"":"";return this.diagnosePreloadPath(r.id,s,"none"),void this.deps.log('Preload skipped: adPreload "none"')}if("shared-element"===this.deps.effectiveInsertionMode()){const s=this.deps.getVariant(r);if(s&&s.format!==t.LSHAPE_CONTENT){const t=this.deps.getAssets(r)[0];if(t&&t.type===e.STATIC){const e=this.deps.resolveUri(t.uri);e&&await this.warmCache([e])}}return}const o=this.deps.getAssets(r);if(0===o.length)return void this.deps.log("No assets to preload");this.preloadCompanion(r);const n=this.deps.getVariant(r);if(n?.format===t.LSHAPE_CONTENT)return;if(o[0].mediaType===s.IMAGE){const e=await this.deps.resolveAssetUri(o[0],r.duration);if(e){(new Image).src=e,this.deps.log("Preloading overlay image:",e)}return}if(a(o[0]))return void await this.preloadVastAsset(o[0],r);const l=await this.deps.resolveAssetUri(o[0],r.duration);if(l){if(d&&this.ownsPreload(i,d))if("single-decoder"!==this.deps.preloadMode){this.deps.log("Preloading ad (parallel):",l),this.diagnosePreloadPath(r.id,l,"parallel");try{if(await d.load(l),!this.ownsPreload(i,d))return;this.deps.state.preparedMedia={breakId:r.id,assetId:o[0].id,uri:l,player:d},this.deps.log("Preload complete for break:",r.id)}catch(e){this.deps.log("Preload failed (will load on demand):",e)}}else{if(this.deps.log("Preloading ad (single-decoder, detached):",l),this.diagnosePreloadPath(r.id,l,"single-decoder"),await this.warmCache([l]),!this.ownsPreload(i,d))return;try{await(d.preload?.(l))}catch(e){this.deps.log("Detached prefetch failed (will load on demand):",e)}}}else this.deps.log("No valid URI for preload")}ownsPreload(e,t){return e===this.deps.state.preloadGeneration&&!this.deps.isPlaying()&&t===this.deps.getAdPlayer()}async preloadVastAsset(e,s){const r=this.deps.getVariant(s),a=r?.format??t.SINGLE;if(!n({format:a,ssai:!1}).admitted)return void this.deps.log("VAST preload skipped: format not supported",a);if(!l())return void this.deps.log("VAST preload skipped: IMA client-side SDK unavailable");const i=this.deps.getAdPlayer(),d=this.deps.getAdContainer();if(!i||!d)return void this.deps.log("VAST preload skipped: ad player not initialized");const o=i.videoElement;if(!o)return void this.deps.log("VAST preload skipped: no ad video element");const h=this.deps.buildVastTagUrl(e,s);if(h){this.deps.state.vastAdManager||(this.deps.state.vastAdManager=new p(d,o,this.deps.debug)),this.deps.log("Preloading VAST asset via IMA CSAI:",h);try{await this.deps.state.vastAdManager.preloadVast(h,{muted:i.muted}),this.deps.log("VAST preload complete for break:",s.id)}catch(t){const r=t instanceof Error?t:new Error(String(t));this.deps.log("VAST preload failed (will load on demand):",r.message),this.deps.diagnose("DA-PRELOAD-FAILED",r.message,{breakId:s.id,assetId:e.id})}}else this.deps.log("No valid VAST tag URL for preload")}preloadChainedSuccessor(e){const t=this.deps.resolveChainedSuccessor(e);t&&this.preloadNext(t)}clearWarmedBreaks(){this.warmedBreakIds.clear()}async preloadNext(s){if(this.warmedBreakIds.has(s.id))return;this.preloadCompanion(s);const r=this.deps.getVariant(s);if(!r||r.format===t.LSHAPE_CONTENT)return;const a=this.deps.getAssets(s)[0];if(!a||a.type!==e.STATIC)return void this.deps.log("Chained successor first asset not static; skipping media warm:",s.id);const i=this.deps.resolveUri(a.uri);if(i){this.warmedBreakIds.add(s.id),this.deps.log("Warming chained successor media:",s.id,i),await this.warmCache([i]);try{await(this.deps.getAdPlayer()?.preload?.(i))}catch(e){this.deps.log("Chained prefetch failed (will load on demand):",e)}}}revealFirstFrame(){if(!this.deps.state.firstFrameGateActive||this.deps.state.firstFrameGateShown)return;const e=this.deps.getCurrentBreak();if(!e)return;this.deps.state.firstFrameGateShown=!0,this.deps.show();const t=this.deps.getVariant(e);t&&this.deps.reapplyAdContainerLayout(t),this.deps.onBreakVisible(e)}armAdStartBudget(e){const t=1e3*((this.deps.state.effectiveBreakDurationSec??this.deps.state.currentBreak?.duration??0)+this.deps.state.adBreakCutSafetyMarginSec),s=i(e)&&e.vendor===r.GAM?this.deps.gamPodAdStartBudgetMs:this.deps.adStartBudgetMs,a=Math.max(Math.min(t-100,s),this.deps.minAdStartBudgetMs);this.deps.state.adStartBudgetMs=a,this.deps.state.adStartDeadlineMs=this.deps.clock.now()+a}remainingAdStartBudgetMs(){return Math.max(0,(this.deps.state.adStartDeadlineMs??this.deps.clock.now())-this.deps.clock.now())}loadWithinBudget(e,t){const s=this.deps.state.preloadGeneration,r=this.remainingAdStartBudgetMs(),a=this.deps.clock.now();return new Promise((i,d)=>{let o=!1,n=!1;const l=e=>{o||(o=!0,clearTimeout(p),null!==h&&clearTimeout(h),e.ok?i(!0):d(e.error instanceof Error?e.error:new Error(String(e.error))))},p=setTimeout(()=>{s===this.deps.state.preloadGeneration&&this.deps.onAdError(new m("Ad load timeout","DA-AD-START-TIMEOUT")),o||(o=!0,null!==h&&clearTimeout(h),i(!1))},r);let h=null;r>2e3&&(h=setTimeout(()=>{if(o||s!==this.deps.state.preloadGeneration)return;const r=this.deps.clock.now()-a,i=this.deps.getAdPlayer()?.videoElement;adLoadShowsNoProgress(i)?(n=!0,this.deps.log(`Ad load for ${t.assetId} stalled after ${r}ms; retrying once`),this.deps.diagnose("DA-AD-LOAD-RETRIED",`Ad load for ${t.assetId} in break ${t.breakId} had not settled after ${r}ms; issued a second load()`,{breakId:t.breakId,assetId:t.assetId,uri:t.uri,elapsedMs:r,remainingBudgetMs:this.remainingAdStartBudgetMs()}),e().then(()=>l({ok:!0}),e=>l({ok:!1,error:e}))):this.deps.log(`Ad load for ${t.assetId} still pending after ${r}ms but the element is progressing (readyState=${i?.readyState}, buffered=${i?.buffered.length}); no retry`)},2e3)),e().then(()=>l({ok:!0}),e=>{n||l({ok:!1,error:e})})})}async waitForFirstFrame(){return new Promise((e,t)=>{this.deps.state.firstFrameResolver=()=>{this.clearFirstFrameResolver(),e()},this.deps.state.firstFrameRejector=e=>{this.clearFirstFrameResolver(),t(e)},this.deps.state.firstFrameTimeout=setTimeout(()=>{this.deps.onAdError(new m("First frame timeout","DA-AD-START-TIMEOUT"))},this.remainingAdStartBudgetMs())})}clearFirstFrame(e="Break ended before first frame"){if(this.deps.state.adStartDeadlineMs=null,this.deps.state.adStartBudgetMs=0,this.deps.state.firstFrameTimeout&&(clearTimeout(this.deps.state.firstFrameTimeout),this.deps.state.firstFrameTimeout=null),this.deps.state.firstFrameRejector){const t=this.deps.state.firstFrameRejector;this.deps.state.firstFrameResolver=null,this.deps.state.firstFrameRejector=null,t(new Error(e))}}clearFirstFrameResolver(){this.deps.state.adStartDeadlineMs=null,this.deps.state.adStartBudgetMs=0,this.deps.state.firstFrameTimeout&&(clearTimeout(this.deps.state.firstFrameTimeout),this.deps.state.firstFrameTimeout=null),this.deps.state.firstFrameResolver=null,this.deps.state.firstFrameRejector=null}preloadCompanion(s){const r=this.deps.getCompanionVariant(s);if(!r)return;if(r.format===t.LSHAPE_CONTENT){const t=r.assets[0];if(!t||t.type!==e.STATIC)return;const s=this.deps.resolveUri(t.uri);if(!s)return;return(new Image).src=s,void this.deps.log("Preloading lshape_content backdrop:",s)}if(r.format!==t.DOUBLE&&r.format!==t.LSHAPE_AD)return;const a=r.assets[0]?.companion;if(!a||a.type!==e.STATIC)return;const i=this.deps.resolveUri(a.uri);if(!i)return;(new Image).src=i,this.deps.log("Preloading companion:",i)}async warmCache(e){"function"==typeof fetch&&await Promise.allSettled(e.map(e=>o(e,{mode:"no-cors"},d).catch(()=>{})))}}
|
|
@@ -40,8 +40,11 @@ export declare class AdStage {
|
|
|
40
40
|
private resizePoll;
|
|
41
41
|
private companionRemoveTimer;
|
|
42
42
|
private adFadeTimer;
|
|
43
|
+
private preparedLease;
|
|
44
|
+
private unsubscribeGeometry;
|
|
43
45
|
constructor(deps: AdStageDeps);
|
|
44
46
|
setupOverlay(): void;
|
|
47
|
+
private setupMediaLayer;
|
|
45
48
|
private refreshStageGeometry;
|
|
46
49
|
private rebindContentVideoGeometryListener;
|
|
47
50
|
private updateStageGeometry;
|
|
@@ -54,5 +57,7 @@ export declare class AdStage {
|
|
|
54
57
|
clearOverlayImage(): void;
|
|
55
58
|
show(): void;
|
|
56
59
|
hide(): void;
|
|
60
|
+
restoreContent(createdBySDK: boolean, originalStyle: string): boolean;
|
|
61
|
+
private releasePreparedPresentation;
|
|
57
62
|
destroy(): void;
|
|
58
63
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AssetType as e,BreakFormat as t}from"../../types/BreakManifest";const s=16/9;export class AdStage{constructor(e){this.deps=e,this.contentVideoGeometryHandler=null,this.contentVideoMutationObserver=null,this.resizeObserver=null,this.resizePoll=null,this.companionRemoveTimer=null,this.adFadeTimer=null}setupOverlay(){const e=window.getComputedStyle(this.deps.container);"static"!==e.position&&""!==e.position||(this.deps.container.style.position="relative"),this.deps.setStage(document.createElement("div"));const t=this.deps.getStage();if(!t)return;if(t.className="dolby-stage",t.style.cssText="position:absolute;overflow:hidden;",this.deps.container.appendChild(t),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;t.appendChild(e)}else{const e=document.createElement("div"),s=Array.from(this.deps.container.childNodes).filter(e=>e!==t);for(const t of s)e.appendChild(t);t.appendChild(e),this.deps.setPlayerContainer(e),this.deps.onPlayerContainerCreated()}const s=this.deps.getPlayerContainer();if(!s)return;this.deps.setPlayerContainerOriginalStyle(s.style.cssText),s.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,opacity 0.3s ease-in-out;",this.deps.setAdContainer(document.createElement("div"));const i=this.deps.getAdContainer();if(!i)return;i.className="dolby-ad-container",i.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 o=this.deps.getAdMediaContainer();o&&(o.style.cssText="display:block;width:100%;height:100%;",i.appendChild(o),t.appendChild(i),this.deps.setAdPlayer(this.deps.createAdAdapter(o)),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 o,n;t/s>i?(n=s,o=s*i):(o=t,n=t/i);const r=(s-n)/2,a=(t-o)/2;e.style.width=`${o}px`,e.style.height=`${n}px`,e.style.top=`${r}px`,e.style.left=`${a}px`}getContentAspectRatio(){const e=this.deps.contentPlayer.videoElement;if(!e)return s;const{videoWidth:t,videoHeight:i}=e;return!Number.isFinite(t)||!Number.isFinite(i)||t<=0||i<=0?s:t/i}renderLshapeContentBackdrop(t){const s=t.assets[0];if(!s||s.type!==e.STATIC)return;const i=this.deps.resolveUri(s.uri);if(!i)return;const o=document.createElement("img");o.className="dolby-companion",o.src=i,o.style.cssText="position:absolute;top:0;left:0;right:0;bottom:0;z-index:99;object-fit:cover;width:100%;height:100%;opacity:0;transition:opacity 0.3s ease-in-out;";const n=this.deps.getStage();n&&(n.appendChild(o),this.deps.setCompanion(o),requestAnimationFrame(()=>{const e=this.deps.getCompanion();e&&(e.style.opacity="1")}),this.deps.log("Rendered lshape_content backdrop:",i))}reapplyAdContainerLayout(e){const s=this.deps.getAdContainer();if(s){switch(e.format){case t.LSHAPE_AD:s.style.position="absolute",s.style.top="5%",s.style.left="5%",s.style.right="30%",s.style.bottom="30%",s.style.width="",s.style.height="",s.style.zIndex="100";break;case t.DOUBLE:s.style.position="absolute",s.style.top="26.04%",s.style.left="51.04%",s.style.right="1.04%",s.style.bottom="26.04%",s.style.width="",s.style.height="",s.style.zIndex="100"}this.deps.log("reapplyAdContainerLayout: re-stamped adContainer CSS for format:",e.format)}}applyLayout(e){const s=this.deps.getAdContainer(),i=this.deps.getPlayerContainer();if(!s||!i)return;const o="transition:top 0.3s ease-in-out,left 0.3s ease-in-out,right 0.3s ease-in-out,bottom 0.3s ease-in-out,opacity 0.3s ease-in-out;";switch(e.format){case t.SINGLE:s.style.cssText="display:none;opacity:0;position:absolute;top:0;left:0;right:0;bottom:0;z-index:100;background:#000;transition:opacity 0.3s ease-in-out;";break;case t.DOUBLE:i.style.cssText=`position:absolute;top:26.04%;left:1.04%;right:51.04%;bottom:26.04%;z-index:101;${o}`,s.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;transition:opacity 0.3s ease-in-out;",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;transition:opacity 0.3s ease-in-out;");break;case t.LSHAPE_AD:i.style.cssText=`position:absolute;top:5%;left:5%;right:30%;bottom:30%;z-index:101;opacity:0;${o}`,s.style.cssText="display:none;opacity:0;position:absolute;top:5%;left:5%;right:30%;bottom:30%;z-index:100;background:#000;transition:opacity 0.3s ease-in-out;",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;transition:opacity 0.3s ease-in-out;");break;case t.LSHAPE_CONTENT:i.style.top="5%",i.style.left="5%",i.style.right="30%",i.style.bottom="30%",i.style.zIndex="101";break;case t.OVERLAY:{const t=e,i=e=>void 0!==e?100*e+"%":"auto",o=i(t.position.top),n=i(t.position.bottom),r=i(t.position.left),a=i(t.position.right),d=100*t.size.width+"%",l=100*t.size.height+"%",p=t.opacity??1;s.style.cssText=`display:none;position:absolute;top:${o};bottom:${n};left:${r};right:${a};width:${d};height:${l};z-index:100;opacity:${p};`;break}default:s.style.cssText="display:none;position:absolute;top:0;left:0;right:0;bottom:0;z-index:100;background:#000;"}}restoreLayout(e){const s=e===t.DOUBLE||e===t.LSHAPE_AD||e===t.LSHAPE_CONTENT,i=this.deps.getPlayerContainer();if(!i)return;i.style.cssText="position:absolute;top:0;left:0;right:0;bottom:0;"+(s?"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,opacity 0.3s ease-in-out;",this.companionRemoveTimer&&clearTimeout(this.companionRemoveTimer);const o=this.deps.getCompanion();o&&(o.style.opacity="0"),this.companionRemoveTimer=setTimeout(()=>{const e=this.deps.getCompanion();e?.parentElement&&(e.parentElement.removeChild(e),this.deps.setCompanion(null)),this.companionRemoveTimer=null;const t=this.deps.getPlayerContainer();s&&t&&(t.style.zIndex="")},300)}renderCompanion(t,s){const i=t[0]?.companion;if(!i)return;const o=i.type===e.STATIC?this.deps.resolveUri(i.uri):null;if(!o)return;const n=document.createElement("img");n.className="dolby-companion",n.src=o,n.style.cssText=s;const r=this.deps.getStage();r&&(r.appendChild(n),this.deps.setCompanion(n),requestAnimationFrame(()=>{const e=this.deps.getCompanion();e&&(e.style.opacity="1")}))}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;this.adFadeTimer&&(clearTimeout(this.adFadeTimer),this.adFadeTimer=null);const s=this.deps.getCurrentBreak(),i=s?this.deps.getVariant(s):null;e.style.display="block",i?.format!==t.OVERLAY&&(this.deps.isChaining()?e.style.opacity="1":(e.style.opacity="0",requestAnimationFrame(()=>{const e=this.deps.getAdContainer();e&&(e.style.opacity="1")})))}hide(){const e=this.deps.getAdContainer();if(!e)return;const s=this.deps.getCurrentBreak(),i=s?this.deps.getVariant(s):null;i?.format!==t.OVERLAY?(e.style.opacity="0",this.adFadeTimer=setTimeout(()=>{const e=this.deps.getAdContainer();e&&(e.style.display="none"),this.adFadeTimer=null},300)):e.style.display="none"}destroy(){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.companionRemoveTimer&&clearTimeout(this.companionRemoveTimer),this.adFadeTimer&&clearTimeout(this.adFadeTimer)}}
|
|
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.companionRemoveTimer=null,this.adFadeTimer=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 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:top 0.3s ease-in-out,left 0.3s ease-in-out,right 0.3s ease-in-out,bottom 0.3s ease-in-out,opacity 0.3s ease-in-out;",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();s&&(s.style.cssText="display:block;width:100%;height:100%;",t.appendChild(s),e.appendChild(t),this.deps.setAdPlayer(this.deps.createAdAdapter(s)),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 o,n;t/s>i?(n=s,o=s*i):(o=t,n=t/i);const r=(s-n)/2,a=(t-o)/2;e.style.width=`${o}px`,e.style.height=`${n}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 o=document.createElement("img");o.className="dolby-companion",o.src=i,o.style.cssText="position:absolute;top:0;left:0;right:0;bottom:0;z-index:99;object-fit:cover;width:100%;height:100%;opacity:0;transition:opacity 0.3s ease-in-out;";const n=this.deps.getStage();n&&(n.appendChild(o),this.deps.setCompanion(o),requestAnimationFrame(()=>{const e=this.deps.getCompanion();e&&(e.style.opacity="1")}),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 o="transition:top 0.3s ease-in-out,left 0.3s ease-in-out,right 0.3s ease-in-out,bottom 0.3s ease-in-out,opacity 0.3s ease-in-out;";switch(e.format){case s.SINGLE:t.style.cssText="display:none;opacity:0;position:absolute;top:0;left:0;right:0;bottom:0;z-index:100;background:#000;transition:opacity 0.3s ease-in-out;";break;case s.DOUBLE:i.style.cssText=`position:absolute;top:26.04%;left:1.04%;right:51.04%;bottom:26.04%;z-index:101;${o}`,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;transition:opacity 0.3s ease-in-out;",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;transition:opacity 0.3s ease-in-out;");break;case s.LSHAPE_AD:i.style.cssText=`position:absolute;top:5%;left:5%;right:30%;bottom:30%;z-index:101;opacity:0;${o}`,t.style.cssText="display:none;opacity:0;position:absolute;top:5%;left:5%;right:30%;bottom:30%;z-index:100;background:#000;transition:opacity 0.3s ease-in-out;",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;transition:opacity 0.3s ease-in-out;");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";break;case s.OVERLAY:{const s=e,i=e=>void 0!==e?100*e+"%":"auto",o=i(s.position.top),n=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:${o};bottom:${n};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=e===s.DOUBLE||e===s.LSHAPE_AD||e===s.LSHAPE_CONTENT,i=this.deps.getPlayerContainer();if(!i)return;i.style.cssText="position:absolute;top:0;left:0;right:0;bottom:0;"+(t?"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,opacity 0.3s ease-in-out;",this.companionRemoveTimer&&clearTimeout(this.companionRemoveTimer);const o=this.deps.getCompanion();o&&(o.style.opacity="0"),this.companionRemoveTimer=setTimeout(()=>{const e=this.deps.getCompanion();e?.parentElement&&(e.parentElement.removeChild(e),this.deps.setCompanion(null)),this.companionRemoveTimer=null;const s=this.deps.getPlayerContainer();t&&s&&(s.style.zIndex="")},300)}renderCompanion(e,s){const i=e[0]?.companion;if(!i)return;const o=i.type===t.STATIC?this.deps.resolveUri(i.uri):null;if(!o)return;const n=document.createElement("img");n.className="dolby-companion",n.src=o,n.style.cssText=s;const r=this.deps.getStage();r&&(r.appendChild(n),this.deps.setCompanion(n),requestAnimationFrame(()=>{const e=this.deps.getCompanion();e&&(e.style.opacity="1")}))}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;this.adFadeTimer&&(clearTimeout(this.adFadeTimer),this.adFadeTimer=null);const t=this.deps.getCurrentBreak(),i=t?this.deps.getVariant(t):null;e.style.display="block",i?.format!==s.OVERLAY&&(this.deps.isChaining()?e.style.opacity="1":(e.style.opacity="0",requestAnimationFrame(()=>{const e=this.deps.getAdContainer();e&&(e.style.opacity="1")})))}hide(){const e=this.deps.getAdContainer();if(!e)return;const t=this.deps.getCurrentBreak(),i=t?this.deps.getVariant(t):null;i?.format!==s.OVERLAY?(e.style.opacity="0",this.adFadeTimer=setTimeout(()=>{const e=this.deps.getAdContainer();e&&(e.style.display="none"),this.adFadeTimer=null},300)):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.companionRemoveTimer&&clearTimeout(this.companionRemoveTimer),this.adFadeTimer&&clearTimeout(this.adFadeTimer)}}
|
|
@@ -25,12 +25,18 @@ export interface AssetPlaybackDeps {
|
|
|
25
25
|
revealFirstFrame: () => void;
|
|
26
26
|
transferPictureInPictureToAd: () => Promise<void>;
|
|
27
27
|
isGamPodAsset: (asset: Asset) => boolean;
|
|
28
|
+
/** Whether the GAM pod asset was skipped because its identity mismatches the stream session. */
|
|
29
|
+
hasGamIdentityMismatch: (asset: Asset) => boolean;
|
|
28
30
|
beginGamPodTracking: (asset: Asset) => void;
|
|
29
31
|
recordAdProgress: (currentTime: number) => void;
|
|
30
32
|
reportPhase: (name: string) => void;
|
|
31
33
|
armAdStartBudget: (asset: Asset) => void;
|
|
32
34
|
remainingAdStartBudgetMs: () => number;
|
|
33
|
-
loadWithinBudget: (
|
|
35
|
+
loadWithinBudget: (startLoad: () => Promise<void>, context: {
|
|
36
|
+
breakId: string;
|
|
37
|
+
assetId: string;
|
|
38
|
+
uri: string;
|
|
39
|
+
}) => Promise<boolean>;
|
|
34
40
|
waitForFirstFrame: () => Promise<void>;
|
|
35
41
|
getOverlayImage: () => HTMLImageElement | null;
|
|
36
42
|
setOverlayImage: (value: HTMLImageElement | null) => void;
|
|
@@ -47,6 +53,13 @@ export declare class AssetPlayback {
|
|
|
47
53
|
constructor(state: AdPlayerState, deps: AssetPlaybackDeps);
|
|
48
54
|
private get currentBreak();
|
|
49
55
|
playAsset(asset: Asset, index: number, total: number): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Publish the prepared ad media's own duration (ADS-619).
|
|
58
|
+
*
|
|
59
|
+
* Silent when the adapter cannot name one: a live or still-loading source reports `Infinity` or
|
|
60
|
+
* `NaN`, and a guessed number here would be read as a measurement of the break.
|
|
61
|
+
*/
|
|
62
|
+
private reportPreparedDuration;
|
|
50
63
|
/**
|
|
51
64
|
* `play()` with one retry when the browser itself pauses the element while the request is
|
|
52
65
|
* pending (Chromium: "The play() request was interrupted because the media paused", an
|
|
@@ -68,4 +81,5 @@ export declare class AssetPlayback {
|
|
|
68
81
|
revealFirstFrame(): void;
|
|
69
82
|
/** Surfaces the MediaError's code/message so consumers can diagnose ad playback failures. */
|
|
70
83
|
toAdError(mediaError?: unknown): Error;
|
|
84
|
+
private missingUriError;
|
|
71
85
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AssetType as e,BreakFormat as t,Vendor as s,MediaType as r,isVastAsset 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 p}from"../VastAdManager";import{invalidateMediaPreload as c}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,p=c(this.state),l=()=>this.state.preloadGeneration===p&&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);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);if(0===t&&h?.breakId===A&&h.assetId===e.id&&h.uri===u&&h.player===n)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 r=n.videoElement;if(r&&(r.style.cssText="display:block;width:100%;height:100%;object-fit:contain;"),this.deps.reportPhase("play-called"),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,this.deps.transferPictureInPictureToAd();const i=this.deps.getSequencer();i?(this.deps.isGamPodAsset(e)&&this.deps.beginGamPodTracking(e),await this.deps.drive(i.onOutcome({kind:"asset-started"}))):this.state.callbacks?.onAdStart(this.currentBreak,e,t,s)}catch(e){if(!l())return;if(this.state.firstFrameGateActive&&null===this.state.firstFrameResolver)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(!p())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 c=this.deps.getAdPlayer(),l=c?.videoElement,u=this.deps.getAdContainer();if(!u||!l||!c)return void this.deps.onAdError(new a("VAST playout requires an ad container and ad video element","DA-AD-URI-MISSING"));this.deps.revealFirstFrame(),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;this.state.startPlaybackCutoff?.(),this.deps.setVastPodPosition(t);const i=this.deps.getSequencer();i?this.deps.drive(i.onOutcome({kind:"asset-started"})):this.state.callbacks?.onAdStart(this.currentBreak,e,s,r,{podPosition:t})},onAdProgress:(t,s)=>{this.currentBreak&&(this.deps.recordAdProgress(t),this.state.callbacks?.onAdTimeUpdate(this.currentBreak,e,t,s))},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(c.muted),this.state.vastAdManager.startPreloaded()):this.state.vastAdManager.playVast(o,{muted:c.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){switch(t.type){case e.STATIC:return this.deps.resolveUri(t.uri);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")}}
|
|
@@ -2,6 +2,7 @@ import type { PlayerAdapter } from '../../interfaces/PlayerAdapter';
|
|
|
2
2
|
import type { Break } from '../../types/BreakManifest';
|
|
3
3
|
import type { Clock } from '../BreakScheduler';
|
|
4
4
|
import type { AdPlayerState } from './AdPlayerState';
|
|
5
|
+
export declare const START_CLAMP_MAX_WINDOW_FRACTION = 0.5;
|
|
5
6
|
export interface ContentResumeCoordinatorDeps {
|
|
6
7
|
contentPlayer: PlayerAdapter;
|
|
7
8
|
clock: Clock;
|
|
@@ -16,10 +17,48 @@ export declare class ContentResumeCoordinator {
|
|
|
16
17
|
private readonly state;
|
|
17
18
|
private readonly deps;
|
|
18
19
|
private readonly continuePlayout;
|
|
20
|
+
/**
|
|
21
|
+
* Resolves a break's frame-accurate in-stream cue position, in seconds of content; null when
|
|
22
|
+
* the SDK has no cue tracker (every timebase except `ptsSource: 'anvatoCue'`).
|
|
23
|
+
*/
|
|
24
|
+
private cueAnchorResolver;
|
|
19
25
|
constructor(state: AdPlayerState, deps: ContentResumeCoordinatorDeps);
|
|
26
|
+
/**
|
|
27
|
+
* Supply the in-stream cue position used to anchor the resume in place of the pause position.
|
|
28
|
+
*
|
|
29
|
+
* `null`, or a resolver returning `null`, keeps the pause position, so a stream without
|
|
30
|
+
* in-stream cues is unaffected.
|
|
31
|
+
*/
|
|
32
|
+
setCueAnchorResolver(resolver: ((breakInfo: Break) => number | null) | null): void;
|
|
33
|
+
/**
|
|
34
|
+
* Fix the anchor every resume offset for `breakInfo` is measured from, and reset the per-break
|
|
35
|
+
* resume state around it. Also the drift reference for `continueContentDuringBreak`.
|
|
36
|
+
*
|
|
37
|
+
* Prefers the in-stream cue over the pause position where one matched this break: the slate the
|
|
38
|
+
* ad replaces begins at the cue, and the pause lands after it, so anchoring on the pause carries
|
|
39
|
+
* that latency into the resume target and lands content late by the same amount.
|
|
40
|
+
*/
|
|
41
|
+
captureBreakAnchor(breakInfo: Break): void;
|
|
42
|
+
/**
|
|
43
|
+
* Replace the anchor part-way through a break, when content kept playing until now
|
|
44
|
+
* (picture-in-picture adoption).
|
|
45
|
+
*
|
|
46
|
+
* Clears the cue flag with it: the replacement is a pause position by construction, so the
|
|
47
|
+
* content under the ad is no longer known slate of the advertised length and the ad-played cap
|
|
48
|
+
* has to apply again. Leaving the flag set would resume at `pausedMidBreak + fullDuration`.
|
|
49
|
+
*/
|
|
50
|
+
reanchorMidBreak(position: number): void;
|
|
20
51
|
liveEdgeDistanceSec(): number | null;
|
|
21
52
|
liveSafeSeekEnd(): number | null;
|
|
53
|
+
liveSafeSeekStart(): number | null;
|
|
54
|
+
liveSeekableWindowSec(): number | null;
|
|
22
55
|
contentSeekableEnd(): number | null;
|
|
56
|
+
/**
|
|
57
|
+
* Seconds of published media the anchor sat below the engine's seekable end at break start, or
|
|
58
|
+
* null when that end was unreadable. Positive is slack the resume can spend; zero or negative
|
|
59
|
+
* means the playhead was riding the engine's limit and the resume cannot absorb anything.
|
|
60
|
+
*/
|
|
61
|
+
resumeAnchorCushionSec(): number | null;
|
|
23
62
|
isPlausibleSeekEnd(end: number): boolean;
|
|
24
63
|
plannedResumeTarget(breakInfo: Break): number | null;
|
|
25
64
|
maybePrepositionContentForResume(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Timebase as e,isPostRollBreak as t}from"../../types/BreakManifest";import{resolveResumePoint as s}from"../resolveResumePoint";import{ContinueContentPlayout as n}from"./ContinueContentPlayout";import{contentSeekableEnd as i,isPlausibleSeekEnd as a,liveEdgeDistanceSec as r,liveSafeSeekEnd as o}from"./liveEdge";const u=.04;export class ContentResumeCoordinator{constructor(e,t){this.state=e,this.deps=t,this.continuePlayout=new n(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)})}liveEdgeDistanceSec(){return r(this.deps.contentPlayer)}liveSafeSeekEnd(){return o(this.deps.contentPlayer)}contentSeekableEnd(){return i(this.deps.contentPlayer)}isPlausibleSeekEnd(e){return a(this.deps.contentPlayer,e)}plannedResumeTarget(e){if(t(e))return null;const n=s({anchor:this.state.contentPositionAtBreakStart,duration:e.duration,adDuration:e.duration,...this.state.adInsertionType?{insertionType:this.state.adInsertionType}:{},..."number"==typeof e.resumeOffset?{resumeOffset:e.resumeOffset}:{}});return n.seek?n.target: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,i=(e.duration??0)-n;let a;if(t<=s)a=t;else{if(!(i<=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 ${i}s — moving content as close to ${t}s as exists`),this.deps.contentPlayer.seek(a);const r=setTimeout(()=>{this.state.resumePrepositionSettle=null;const e=this.deps.contentPlayer.currentTime;Math.abs(e-a)<=u?(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=r}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,i=e-t;if(Math.abs(i)>u){const a=s-i,r=this.liveSafeSeekEnd();if(null!==r){const e=r>this.state.lastResumeSafeEnd+.25;e&&Number.isFinite(this.state.lastResumeSafeEnd)&&this.state.resumeSawSteps.push(Math.round(100*(r-this.state.lastResumeSafeEnd))/100),r>this.state.lastResumeSafeEnd&&(this.state.lastResumeSafeEnd=r);const s=r-a;if(s>this.state.resumeBestSawMarginSec&&(this.state.resumeBestSawMarginSec=s),a>r)return void(this.state.resumeAlignBlockedDiagnosed||(this.state.resumeAlignBlockedDiagnosed=!0,this.deps.log(`Resume align waiting: ${a}s is past the published edge ${r}s (${Math.round(1e3*(a-r))}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 ${r}s`,{target:t,requested:a,safeEnd:r,shortfallSec:Math.round(1e3*(a-r))/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*i)}ms) — content to ${a}s, ${n}s of ad left`),this.state.resumeAlignCorrectionsMs.push(Math.round(1e3*i)),this.deps.contentPlayer.seek(a)}}return}if(t-s<=u)return;const n=this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec,i=(e.duration??0)-n;if(i<=0)return;if(i>30)return;const a=this.contentSeekableEnd();if(null===a)return;const r=t-i+.3,o=Math.min(r,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 ${i}s of ad left`),this.deps.diagnose?.("DA-RESUME-CATCHUP",`Break ${e.id}: playing content from ${o}s toward ${t}s with ${i}s of ad left`,{breakId:e.id,startPos:o,target:t,remainingAdSec:Math.round(1e3*i)/1e3,publishedEnd:a,clampedStart:o<r}),Math.abs(s-o)>u&&this.deps.contentPlayer.seek(o),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){this.cancelResumeLandedProbe();let s=!1;const n=(n,i)=>{if(s)return;s=!0,this.cancelResumeLandedProbe();const a=null===n?null:Math.round(1e3*(n-t))/1e3;this.deps.log(null===n?`Resume landing unknown for break ${e} (${i})`:`Resume landed at ${n}s, ${a}s from the ${t}s asked for (break ${e})`),this.deps.diagnose?.("DA-RESUME-LANDED",null===n?`Resume landing for break ${e} could not be measured (${i})`:`Resume for break ${e} landed at ${n}s, ${a}s from the ${t}s requested`,{breakId:e,target:t,landed:n,errorSec:a,...i?{reason:i}:{}})},i=()=>n(this.deps.contentPlayer.currentTime),a=setTimeout(()=>n(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.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:(t=e-this.deps.contentPlayer.currentTime,Math.round(1e3*t)/1e3);var t}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,n=null){const i=null!==n,a=i?n:this.state.adSecondsPlayedThisBreak,r=s({anchor:this.state.contentPositionAtBreakStart,duration:t.duration,adDuration:a,...i?{}:{insertionType:this.state.adInsertionType},...i?{resumeOffset:n}:"number"==typeof t.resumeOffset?{resumeOffset:t.resumeOffset}:{}});if(r.clamped&&this.deps.diagnose?.("DA-RESUME-OFFSET-CLAMPED",`Resume offset shortened to the ${r.offset}s of ad actually played for break ${t.id}`,{breakId:t.id,offset:r.offset,source:r.source}),!r.seek)return this.deps.log(`Resume: no seek for break ${t.id} (offset 0, ${r.source})`),null;this.deps.log(`Resume-seek to ${r.target}s (offset ${r.offset}s, ${r.source}) for break ${t.id}`);const o=this.resumeAlignSummary();this.deps.diagnose?.("DA-RESUME-SEEK",`Resume-seek to ${r.target}s (offset ${r.offset}s, ${r.source}) for break ${t.id}`,{breakId:t.id,target:r.target,offset:r.offset,source:r.source,...o.catchUpRan?o:{}});(this.state.breakTimebase===e.PTS||this.state.breakTimebase===e.MEDIATIME)&&"number"==typeof t.start&&this.state.callbacks?.onContentResumeSeek?.(r.target,t.start+t.duration);const l=this.liveSafeSeekEnd();let d=r.target,h=0;if(null!==l&&d>l&&(h=Math.round(1e3*(d-l))/1e3,d=l),null!==this.state.resumePrepositionedTo&&Math.abs(this.deps.contentPlayer.currentTime-d)<=u){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)`+(h>0?` — ${h}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`+(h>0?` (${h}s short of the ${r.target}s policy target)`:""),{breakId:t.id,target:r.target,prepositionedTo:this.state.resumePrepositionedTo,landed:e,shortfallSec:h,exact:0===h,...this.resumeAlignSummary()}),null}return h>0&&(this.deps.log(`Resume-seek clamped: ${r.target}s is beyond what the stream has published; seeking ${d}s instead (${h}s short)`),this.deps.diagnose?.("DA-RESUME-TARGET-CLAMPED",`Resume target ${r.target}s for break ${t.id} is beyond the published window; clamped to ${d}s`,{breakId:t.id,requested:r.target,clamped:d,shortfallSec:h,...this.resumeAlignSummary()})),d}applyResumeSeek(e,t=null){const s=this.resolveResumeTarget(e,t);null!==s&&(this.probeResumeLanding(e.id,s),this.deps.contentPlayer.seek(s))}}
|
|
1
|
+
import{Timebase as e,isPostRollBreak as t}from"../../types/BreakManifest";import{resolveResumePoint as s}from"../resolveResumePoint";import{ContinueContentPlayout as n}from"./ContinueContentPlayout";import{contentSeekableEnd as i,isPlausibleSeekEnd as a,liveEdgeDistanceSec as r,liveSafeSeekEnd as o,liveSafeSeekStart as u,liveSeekableWindowSec as l,unreachableEdgeSec as d}from"./liveEdge";export const START_CLAMP_MAX_WINDOW_FRACTION=.5;const h=.04,c=e=>Math.round(1e3*e)/1e3;export class ContentResumeCoordinator{constructor(e,t){this.state=e,this.deps=t,this.cueAnchorResolver=null,this.continuePlayout=new n(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)})}setCueAnchorResolver(e){this.cueAnchorResolver=e}captureBreakAnchor(e){this.resetBreakResumeState();const t=this.cueAnchorResolver?.(e)??null,s=null!==t&&Number.isFinite(t);this.state.contentPositionAtBreakStart=s?t:this.deps.contentPlayer.currentTime,this.state.resumeAnchorIsCue=s,this.state.resumeAnchorSeekableEnd=this.contentSeekableEnd()}reanchorMidBreak(e){this.state.contentPositionAtBreakStart=e,this.state.resumeAnchorIsCue=!1,this.state.resumeAnchorSeekableEnd=this.contentSeekableEnd()}liveEdgeDistanceSec(){return r(this.deps.contentPlayer)}liveSafeSeekEnd(){return o(this.deps.contentPlayer)}liveSafeSeekStart(){return u(this.deps.contentPlayer)}liveSeekableWindowSec(){return l(this.deps.contentPlayer)}contentSeekableEnd(){return i(this.deps.contentPlayer)}resumeAnchorCushionSec(){const e=this.state.resumeAnchorSeekableEnd;return null!==e&&Number.isFinite(e)?c(e-this.state.contentPositionAtBreakStart):null}isPlausibleSeekEnd(e){return a(this.deps.contentPlayer,e)}plannedResumeTarget(e){if(t(e))return null;const n=s({anchor:this.state.contentPositionAtBreakStart,duration:e.duration,adDuration:e.duration,...this.state.adInsertionType?{insertionType:this.state.adInsertionType}:{},..."number"==typeof e.resumeOffset?{resumeOffset:e.resumeOffset}:{}});return n.seek?n.target: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,i=(e.duration??0)-n;let a;if(t<=s)a=t;else{if(!(i<=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 ${i}s — moving content as close to ${t}s as exists`),this.deps.contentPlayer.seek(a);const r=setTimeout(()=>{this.state.resumePrepositionSettle=null;const e=this.deps.contentPlayer.currentTime;Math.abs(e-a)<=h?(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=r}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,i=e-t;if(Math.abs(i)>h){const a=s-i,r=this.liveSafeSeekEnd();if(null!==r){const e=r>this.state.lastResumeSafeEnd+.25;e&&Number.isFinite(this.state.lastResumeSafeEnd)&&this.state.resumeSawSteps.push(Math.round(100*(r-this.state.lastResumeSafeEnd))/100),r>this.state.lastResumeSafeEnd&&(this.state.lastResumeSafeEnd=r);const s=r-a;if(s>this.state.resumeBestSawMarginSec&&(this.state.resumeBestSawMarginSec=s),a>r)return void(this.state.resumeAlignBlockedDiagnosed||(this.state.resumeAlignBlockedDiagnosed=!0,this.deps.log(`Resume align waiting: ${a}s is past the published edge ${r}s (${Math.round(1e3*(a-r))}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 ${r}s`,{target:t,requested:a,safeEnd:r,shortfallSec:Math.round(1e3*(a-r))/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*i)}ms) — content to ${a}s, ${n}s of ad left`),this.state.resumeAlignCorrectionsMs.push(Math.round(1e3*i)),this.deps.contentPlayer.seek(a)}}return}if(t-s<=h)return;const n=this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec,i=(e.duration??0)-n;if(i<=0)return;if(i>30)return;const a=this.contentSeekableEnd();if(null===a)return;const r=t-i+.3,o=Math.min(r,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 ${i}s of ad left`),this.deps.diagnose?.("DA-RESUME-CATCHUP",`Break ${e.id}: playing content from ${o}s toward ${t}s with ${i}s of ad left`,{breakId:e.id,startPos:o,target:t,remainingAdSec:Math.round(1e3*i)/1e3,publishedEnd:a,clampedStart:o<r}),Math.abs(s-o)>h&&this.deps.contentPlayer.seek(o),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){this.cancelResumeLandedProbe();let s=!1;const n=(n,i)=>{if(s)return;s=!0,this.cancelResumeLandedProbe();const a=null===n?null:Math.round(1e3*(n-t))/1e3;this.deps.log(null===n?`Resume landing unknown for break ${e} (${i})`:`Resume landed at ${n}s, ${a}s from the ${t}s asked for (break ${e})`),this.deps.diagnose?.("DA-RESUME-LANDED",null===n?`Resume landing for break ${e} could not be measured (${i})`:`Resume for break ${e} landed at ${n}s, ${a}s from the ${t}s requested`,{breakId:e,target:t,landed:n,errorSec:a,...i?{reason:i}:{}})},i=()=>n(this.deps.contentPlayer.currentTime),a=setTimeout(()=>n(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.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:c(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,n=null){const i=null!==n,a=this.state.resumeAnchorIsCue&&!i,r=i?n:a?t.duration:this.state.adSecondsPlayedThisBreak,o=s({anchor:this.state.contentPositionAtBreakStart,duration:t.duration,adDuration:r,...i?{}:{insertionType:this.state.adInsertionType},...i?{resumeOffset:n}:"number"==typeof t.resumeOffset?{resumeOffset:t.resumeOffset}:{}});if(o.clamped&&this.deps.diagnose?.("DA-RESUME-OFFSET-CLAMPED",`Resume offset shortened to the ${o.offset}s of ad actually played for break ${t.id}`,{breakId:t.id,offset:o.offset,source:o.source}),!o.seek)return this.deps.log(`Resume: no seek for break ${t.id} (offset 0, ${o.source})`),null;this.deps.log(`Resume-seek to ${o.target}s (offset ${o.offset}s, ${o.source}) for break ${t.id}`);const u=this.resumeAlignSummary(),l=this.liveSafeSeekEnd();this.deps.diagnose?.("DA-RESUME-SEEK",`Resume-seek to ${o.target}s (offset ${o.offset}s, ${o.source}) for break ${t.id}`,{breakId:t.id,target:o.target,offset:o.offset,source:o.source,anchorPosition:this.state.contentPositionAtBreakStart,anchorIsCue:this.state.resumeAnchorIsCue,anchorSeekableEnd:this.state.resumeAnchorSeekableEnd,anchorCushionSec:this.resumeAnchorCushionSec(),safeEnd:l,seekableEndNow:this.contentSeekableEnd(),liveEdge:this.deps.contentPlayer.liveEdge??null,unreachableEdgeSec:d(this.deps.contentPlayer),...u.catchUpRan?u:{}});(this.state.breakTimebase===e.PTS||this.state.breakTimebase===e.MEDIATIME)&&"number"==typeof t.start&&this.state.callbacks?.onContentResumeSeek?.(o.target,t.start+t.duration);let c=o.target,m=0;null!==l&&c>l&&(m=Math.round(1e3*(c-l))/1e3,c=l);let p=m>0?"end":null;const g=this.liveSafeSeekStart();if(null!==g&&c<g){const e=Math.round(1e3*(g-c))/1e3,s=this.liveSeekableWindowSec(),n=null===s?null:.5*s;null!==n&&e<=n?(m=e,c=g,p="start"):(this.deps.log(`Resume-seek: ${o.target}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 ${o.target}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:o.target,seekableStart:g,behindSec:e,windowSec:s,maxBehindSec:n,...this.resumeAlignSummary()}))}if(null!==this.state.resumePrepositionedTo&&Math.abs(this.deps.contentPlayer.currentTime-c)<=h){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)`+(m>0?` — ${m}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`+(m>0?` (${m}s short of the ${o.target}s policy target)`:""),{breakId:t.id,target:o.target,prepositionedTo:this.state.resumePrepositionedTo,landed:e,shortfallSec:m,exact:0===m,...this.resumeAlignSummary()}),null}return m>0&&(this.deps.log("start"===p?`Resume-seek clamped: ${o.target}s is behind the live window start (already evicted); seeking ${c}s instead (${m}s early)`:`Resume-seek clamped: ${o.target}s is beyond what the stream has published; seeking ${c}s instead (${m}s short)`),this.deps.diagnose?.("DA-RESUME-TARGET-CLAMPED","start"===p?`Resume target ${o.target}s for break ${t.id} is behind the live window start (already evicted); clamped to ${c}s`:`Resume target ${o.target}s for break ${t.id} is beyond the published window; clamped to ${c}s`,{breakId:t.id,requested:o.target,clamped:c,shortfallSec:m,bound:p,...this.resumeAlignSummary()})),c}applyResumeSeek(e,t=null){const s=this.resolveResumeTarget(e,t);null!==s&&(this.probeResumeLanding(e.id,s),this.deps.contentPlayer.seek(s))}}
|
|
@@ -57,10 +57,17 @@ export declare class GamPodBridge {
|
|
|
57
57
|
resetGamPodTracking(): void;
|
|
58
58
|
onGamPodAdProgress(currentTime: number): void;
|
|
59
59
|
onGamPodAdStarted(info?: GamPodAdInfo): void;
|
|
60
|
+
private logRestartedPodPosition;
|
|
61
|
+
private onAdBreakEndedWithPodLeft;
|
|
60
62
|
emitGamPodSteps(steps: GamPodAdStep[]): void;
|
|
61
63
|
emitGamQuartile(callback: ((breakInfo: Break, asset: Asset, adIndex?: number, totalAds?: number) => void) | undefined): void;
|
|
62
64
|
buildGamUrl(asset: VendorAsset, breakDuration: number): Promise<string | null>;
|
|
63
65
|
processTimedMetadata(cue: TimedMetadataCue): void;
|
|
64
66
|
destroy(): void;
|
|
67
|
+
/** Whether the last pod URL build for `asset` was refused for a session identity mismatch. */
|
|
68
|
+
hasIdentityMismatch(asset: Asset): boolean;
|
|
69
|
+
private identityMismatchAssetId;
|
|
70
|
+
/** Last ad position before a mid-pod `AD_BREAK_ENDED`, to spot IMA restarting positions. */
|
|
71
|
+
private midBreakEndPosition;
|
|
65
72
|
private callbacks;
|
|
66
73
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Vendor as t}from"../../types/BreakManifest";import{GamPodAdTracker as e}from"../GamPodAdTracker";import{GamStreamManager as s}from"../GamStreamManager";import{isGamVendorParameters as a}from"../GamPodUrlBuilder";import{gamPodIdentityMismatches as i}from"../resolveGamVendorConfiguration";export class GamPodBridge{constructor(t,e){this.state=t,this.deps=e,this.callbacks={}}setConfig(t){this.state.gamConfig=t}setEventCallbacks(t){this.callbacks=t}get eventCallbacks(){return this.callbacks}async startGamSession(t,e){this.state.currentGamIdentity=t,this.state.sessionAssetParameters=e,this.state.pushedAssetParameters=null,await this.initializeGam()}endGamSession(){this.state.gamStreamManager?.reset(),this.state.gamInitPromise=null,this.state.currentGamIdentity=void 0,this.state.sessionAssetParameters=void 0,this.state.manifestAssetParameters=void 0,this.state.sessionAssetParameterMacros={},this.state.updatedAssetParameters=void 0,this.state.pushedAssetParameters=null,this.state.earlyGamPodAdStart=null}async initializeGam(){if(!this.state.currentGamIdentity)return;if(this.deps.log("Initializing GAM stream manager..."),this.state.gamStreamManager)this.state.gamStreamManager.reset();else{const t=this.deps.isSharedElement()?this.deps.contentPlayer.videoElement:this.deps.getAdPlayer()?.videoElement;if(!t)return void console.warn("[OptiViewAds] GAM requires an adapter that exposes videoElement. GAM will be skipped.");const e=this.deps.isSharedElement()?void 0:this.deps.getAdContainer()||void 0;this.state.gamStreamManager=new s(t,e,this.deps.debug)}this.state.gamStreamManager.setCallbacks({onAdFirstQuartile:()=>this.emitGamQuartile(this.callbacks.onAdFirstQuartile),onAdMidpoint:()=>this.emitGamQuartile(this.callbacks.onAdMidpoint),onAdThirdQuartile:()=>this.emitGamQuartile(this.callbacks.onAdThirdQuartile),onAdStarted:t=>{this.deps.log("GAM ad started"),this.onGamPodAdStarted(t)},onAdComplete:()=>{this.deps.log("GAM ad complete"),"per-ad"===this.state.gamPodMode&&this.state.gamPodTracker&&this.emitGamPodSteps(this.state.gamPodTracker.onAdComplete())},onAdBreakEnded:()=>{this.deps.log("GAM ad break ended");const t=this.deps.getAdPlayerStreamRemainingSec();
|
|
1
|
+
import{Vendor as t}from"../../types/BreakManifest";import{GamPodAdTracker as e}from"../GamPodAdTracker";import{GamStreamManager as s}from"../GamStreamManager";import{isGamVendorParameters as a}from"../GamPodUrlBuilder";import{gamPodIdentityMismatches as i}from"../resolveGamVendorConfiguration";export class GamPodBridge{constructor(t,e){this.state=t,this.deps=e,this.identityMismatchAssetId=null,this.midBreakEndPosition=null,this.callbacks={}}setConfig(t){this.state.gamConfig=t}setEventCallbacks(t){this.callbacks=t}get eventCallbacks(){return this.callbacks}async startGamSession(t,e){this.state.currentGamIdentity=t,this.state.sessionAssetParameters=e,this.state.pushedAssetParameters=null,await this.initializeGam()}endGamSession(){this.state.gamStreamManager?.reset(),this.state.gamInitPromise=null,this.state.currentGamIdentity=void 0,this.state.sessionAssetParameters=void 0,this.state.manifestAssetParameters=void 0,this.state.sessionAssetParameterMacros={},this.state.updatedAssetParameters=void 0,this.state.pushedAssetParameters=null,this.state.earlyGamPodAdStart=null}async initializeGam(){if(!this.state.currentGamIdentity)return;if(this.deps.log("Initializing GAM stream manager..."),this.state.gamStreamManager)this.state.gamStreamManager.reset();else{const t=this.deps.isSharedElement()?this.deps.contentPlayer.videoElement:this.deps.getAdPlayer()?.videoElement;if(!t)return void console.warn("[OptiViewAds] GAM requires an adapter that exposes videoElement. GAM will be skipped.");const e=this.deps.isSharedElement()?void 0:this.deps.getAdContainer()||void 0;this.state.gamStreamManager=new s(t,e,this.deps.debug)}this.state.gamStreamManager.setCallbacks({onAdFirstQuartile:()=>this.emitGamQuartile(this.callbacks.onAdFirstQuartile),onAdMidpoint:()=>this.emitGamQuartile(this.callbacks.onAdMidpoint),onAdThirdQuartile:()=>this.emitGamQuartile(this.callbacks.onAdThirdQuartile),onAdStarted:t=>{this.deps.log("GAM ad started"),this.onGamPodAdStarted(t)},onAdComplete:()=>{this.deps.log("GAM ad complete"),"per-ad"===this.state.gamPodMode&&this.state.gamPodTracker&&this.emitGamPodSteps(this.state.gamPodTracker.onAdComplete())},onAdBreakEnded:()=>{this.deps.log("GAM ad break ended");const t=this.deps.getAdPlayerStreamRemainingSec();t>.25?this.onAdBreakEndedWithPodLeft(t):this.deps.onAdEnded()},onError:t=>this.deps.log("GAM error:",t)});const t=this.state.currentGamIdentity,e=this.deps.resolveSessionAdTagParameters(),a={networkCode:t.networkCode,customAssetKey:t.customAssetKey,...e&&{adTagParameters:e},...this.state.gamConfig?.streamActivityMonitorId&&{streamActivityMonitorId:this.state.gamConfig.streamActivityMonitorId}};this.state.gamInitPromise=this.state.gamStreamManager.initialize(a);try{await this.state.gamInitPromise,this.deps.log("GAM stream manager initialized, streamId:",this.state.gamStreamManager.getStreamId())}catch(t){throw this.state.gamInitPromise=null,t}}isGamEnabled(){return!1!==this.state.gamConfig?.enabled}isGamReady(){return!0===this.state.gamStreamManager?.isInitialized()}isGamPodAsset(e){return"vendor"===e.type&&e.vendor===t.GAM&&this.isGamReady()}isExpandingPodAsset(t){return("pending"===this.state.gamPodMode||"per-ad"===this.state.gamPodMode)&&(!!t&&t.id===this.state.gamPodAsset?.id)}beginGamPodTracking(t){this.resetGamPodTracking(),this.state.gamPodAsset=t,this.state.gamPodTracker=new e(t),this.state.gamPodMode="pending";const s=this.state.earlyGamPodAdStart;this.state.earlyGamPodAdStart=null,s&&this.onGamPodAdStarted(s)}fallBackToPodLevelReporting(){if("pending"!==this.state.gamPodMode)return;this.state.gamPodMode="legacy",this.state.gamPodTracker=null;const t=this.state.withheldPodAdBegin;this.state.withheldPodAdBegin=null,t&&this.deps.emitStep(t)}resetGamPodTracking(){this.state.gamPodMode="none",this.state.gamPodTracker=null,this.state.gamPodAsset=null,this.state.withheldPodAdBegin=null,this.midBreakEndPosition=null}onGamPodAdProgress(t){"pending"===this.state.gamPodMode&&(t<2||(this.deps.log("No IMA pod ad reported after the pod started playing; reporting the pod as a single ad"),this.fallBackToPodLevelReporting()))}onGamPodAdStarted(t){t&&("none"!==this.state.gamPodMode?this.state.gamPodTracker&&("pending"===this.state.gamPodMode&&(this.state.withheldPodAdBegin=null,this.state.gamPodMode="per-ad"),"per-ad"===this.state.gamPodMode&&(this.logRestartedPodPosition(t),this.emitGamPodSteps(this.state.gamPodTracker.onAdStarted(t)))):this.state.earlyGamPodAdStart=t)}logRestartedPodPosition(t){const e=this.midBreakEndPosition;null!==e&&(this.midBreakEndPosition=null,"number"==typeof t.adPosition&&t.adPosition<=e&&this.deps.log(`IMA restarted pod ad positions after a mid-pod ad break end (ad ${t.adPosition} after ad ${e})`))}onAdBreakEndedWithPodLeft(t){const e="per-ad"===this.state.gamPodMode?this.state.gamPodTracker:null;if(!e||e.reportedLastAd)return this.deps.log(`GAM pod has ${t.toFixed(3)}s of slate left; playing it out`),void(e&&this.emitGamPodSteps(e.finish()));const s=e.lastAdPosition,a=e.lastTotalAds,i=a>0?` of ${a}`:"";this.deps.log(`IMA ad break ended after ad ${s}${i}; ${t.toFixed(3)}s of pod left, waiting for the next ad`),this.deps.diagnose("DA-GAM-POD-MID-BREAK-END",`IMA reported the ad break ended after ad ${s}${i} with ${t.toFixed(3)}s of pod left; per-ad reporting continues until the pod ends.`,{assetId:this.state.gamPodAsset?.id,adPosition:s,totalAds:a,remainingSec:t}),this.midBreakEndPosition=s,this.emitGamPodSteps(e.onAdBreakEnded())}emitGamPodSteps(t){const e=this.deps.getCurrentBreak();if(e)for(const s of t){const t={adId:s.adId,creativeId:s.creativeId,podPosition:s.adPosition};"adbegin"===s.type?this.deps.emitAdStart(e,s.asset,s.adIndex,s.totalAds,t):this.deps.emitAdEnd(e,s.asset,s.adIndex,s.totalAds,t)}}emitGamQuartile(t){const e=this.deps.getCurrentBreak();if(!t||!e)return;const s="per-ad"===this.state.gamPodMode?this.state.gamPodTracker?.currentAd:null;if(s)return void t(e,s.asset,s.adIndex,s.totalAds);const a=this.deps.getCurrentAsset();a&&t(e,a)}async buildGamUrl(t,e){if(this.identityMismatchAssetId=null,!a(t.vendorParameters))return this.deps.log("Invalid GAM vendor parameters:",t.vendorParameters),null;if(this.state.gamInitPromise&&await this.state.gamInitPromise,!this.state.gamStreamManager?.isInitialized())throw this.deps.log("GAM stream manager not initialized"),new Error("GAM stream manager not initialized. The stream identity comes from the break manifest's vendorConfiguration — check for DA-GAM-CONFIG-MISSING.");const s=t.vendorParameters;if(this.state.currentGamIdentity){const e=i(this.state.currentGamIdentity,s);for(const s of e)this.deps.diagnose("DA-GAM-POD-IDENTITY-MISMATCH",`GAM asset ${t.id} skipped: vendorParameters.${s.field} "${s.assetValue}" does not match the stream session's "${s.sessionValue}".`,{assetId:t.id,field:s.field,assetValue:s.assetValue,sessionValue:s.sessionValue});if(e.length>0)return this.identityMismatchAssetId=t.id,null}this.state.earlyGamPodAdStart=null,this.deps.pushAssetParameters(t.assetParameters,t.id);const d=this.state.gamStreamManager.buildPodUrl(t.uri,e,s);return this.deps.log("Built GAM pod URL:",d),d}processTimedMetadata(t){this.state.gamStreamManager?.processTimedMetadata(t)}destroy(){this.resetGamPodTracking(),this.state.earlyGamPodAdStart=null,this.state.gamStreamManager?.destroy(),this.state.gamStreamManager=null,this.state.gamInitPromise=null}hasIdentityMismatch(t){return this.identityMismatchAssetId===t.id}}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
declare class PreparedPresentation {
|
|
2
|
+
readonly owner: object;
|
|
3
|
+
readonly container: HTMLElement;
|
|
4
|
+
private readonly getVideo;
|
|
5
|
+
readonly stage: HTMLDivElement;
|
|
6
|
+
readonly playerContainer: HTMLDivElement;
|
|
7
|
+
private readonly originalPosition;
|
|
8
|
+
private readonly subscribers;
|
|
9
|
+
private token;
|
|
10
|
+
private disposed;
|
|
11
|
+
private geometry;
|
|
12
|
+
private video;
|
|
13
|
+
private observer;
|
|
14
|
+
private resizeObserver;
|
|
15
|
+
private poll;
|
|
16
|
+
constructor(owner: object, container: HTMLElement, getVideo: () => HTMLVideoElement | null);
|
|
17
|
+
assertAvailable(): void;
|
|
18
|
+
acquire(): {
|
|
19
|
+
stage: HTMLDivElement;
|
|
20
|
+
playerContainer: HTMLElement;
|
|
21
|
+
subscribe: (listener: () => void) => () => void;
|
|
22
|
+
isActive: () => boolean;
|
|
23
|
+
release: () => void;
|
|
24
|
+
};
|
|
25
|
+
dispose(): void;
|
|
26
|
+
private unbindVideo;
|
|
27
|
+
private readonly refresh;
|
|
28
|
+
}
|
|
29
|
+
export declare function preparePresentation(owner: object, container: HTMLElement, getVideo: () => HTMLVideoElement | null): PreparedPresentation;
|
|
30
|
+
export declare function getPreparedPresentation(container: HTMLElement): PreparedPresentation | undefined;
|
|
31
|
+
export declare function disposePreparedPresentation(owner: object): void;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e="position:absolute;top:0;left:0;right:0;bottom:0;transition:top 0.3s ease-in-out,left 0.3s ease-in-out,right 0.3s ease-in-out,bottom 0.3s ease-in-out,opacity 0.3s ease-in-out;",t=new WeakMap,s=new WeakMap,i=new WeakSet;class PreparedPresentation{constructor(t,s,i){this.owner=t,this.container=s,this.getVideo=i,this.subscribers=new Set,this.token=null,this.disposed=!1,this.geometry="",this.video=null,this.observer=null,this.resizeObserver=null,this.poll=null,this.refresh=()=>{if(this.disposed)return;const e=this.getVideo();e!==this.video&&(this.unbindVideo(),this.video=e,e?.addEventListener("loadedmetadata",this.refresh),e?.addEventListener("resize",this.refresh));const{width:t,height:s}=this.container.getBoundingClientRect();if(!(t>0&&s>0))return;const i=e&&e.videoWidth>0&&e.videoHeight>0?e.videoWidth/e.videoHeight:16/9,r=Math.min(t,s*i),o=r/i,n=`${t}:${s}:${r}:${o}`;if(n!==this.geometry){this.geometry=n,this.stage.style.width=`${r}px`,this.stage.style.height=`${o}px`,this.stage.style.left=(t-r)/2+"px",this.stage.style.top=(s-o)/2+"px";for(const e of this.subscribers)e()}};for(const e of Array.from(s.querySelectorAll("video,audio")))if(e.currentSrc||e.getAttribute("src")||e.srcObject||e.readyState>0||!e.paused)throw new Error("Prepare the ads presentation before loading the content source");this.originalPosition=s.style.position;const r=s.ownerDocument.defaultView?.getComputedStyle(s).position;r&&"static"!==r||(s.style.position="relative"),this.stage=s.ownerDocument.createElement("div"),this.stage.className="dolby-stage",this.stage.style.cssText="position:absolute;overflow:hidden;",this.playerContainer=s.ownerDocument.createElement("div"),this.playerContainer.style.cssText=e;const o=Array.from(s.childNodes);s.appendChild(this.stage),this.stage.appendChild(this.playerContainer);for(const e of o)this.playerContainer.appendChild(e);this.refresh(),"undefined"!=typeof MutationObserver&&(this.observer=new MutationObserver(()=>{this.getVideo()!==this.video&&this.refresh()}),this.observer.observe(s,{childList:!0,subtree:!0})),"undefined"!=typeof ResizeObserver?(this.resizeObserver=new ResizeObserver(this.refresh),this.resizeObserver.observe(s)):this.poll=setInterval(this.refresh,250)}assertAvailable(){if(this.disposed)throw new Error("The content presentation has been disposed");if(this.token)throw new Error("The content player already has an ads presentation owner");if(this.stage.parentNode!==this.container||this.playerContainer.parentNode!==this.stage)throw new Error("The prepared content presentation has been moved")}acquire(){this.assertAvailable();const t={};this.token=t;const s=new Set;return{stage:this.stage,playerContainer:this.playerContainer,subscribe:e=>(this.subscribers.add(e),s.add(e),()=>{this.subscribers.delete(e),s.delete(e)}),isActive:()=>!this.disposed&&this.token===t,release:()=>{for(const e of s)this.subscribers.delete(e);s.clear(),this.disposed||this.token!==t||(this.token=null,this.playerContainer.style.cssText=e)}}}dispose(){this.disposed||(this.disposed=!0,this.observer?.disconnect(),this.resizeObserver?.disconnect(),null!==this.poll&&clearInterval(this.poll),this.poll=null,this.unbindVideo(),this.subscribers.clear(),this.stage.parentNode&&this.stage.parentNode.removeChild(this.stage),s.get(this.container)===this&&(s.delete(this.container),this.container.style.position=this.originalPosition),i.add(this.owner))}unbindVideo(){this.video?.removeEventListener("loadedmetadata",this.refresh),this.video?.removeEventListener("resize",this.refresh),this.video=null}}export function preparePresentation(e,r,o){if(i.has(e))throw new Error("Cannot prepare a disposed content player");const n=t.get(e);if(n){if(n.container!==r)throw new Error("The content player presentation container changed");return n}if(s.has(r))throw new Error("The container belongs to another content player");const a=new PreparedPresentation(e,r,o);return t.set(e,a),s.set(r,a),a}export function getPreparedPresentation(e){return s.get(e)}export function disposePreparedPresentation(e){t.get(e)?.dispose()}
|
|
@@ -60,6 +60,14 @@ export declare class SharedElementBreakPlayer {
|
|
|
60
60
|
private armSharedRestoreDeadline;
|
|
61
61
|
private ownsSharedRestoreAttempt;
|
|
62
62
|
private reloadAndResume;
|
|
63
|
+
/** Best-effort: an ad adapter that cannot let go still gets the reload attempted on top of it. */
|
|
64
|
+
private releaseAdEngineGrip;
|
|
65
|
+
/**
|
|
66
|
+
* Engines that attach their media asynchronously (THEOplayer; with `preload: 'none'` only after
|
|
67
|
+
* `play()`) have not taken the element when `play()` resolves. Releasing the ad engine then runs
|
|
68
|
+
* the load algorithm on an element without a resource, which closes Safari's PiP window.
|
|
69
|
+
*/
|
|
70
|
+
private waitForContentEngineToTakeElement;
|
|
63
71
|
private finishSharedRestore;
|
|
64
72
|
private endSharedRestoreReloading;
|
|
65
73
|
private armSharedRestoreWatchdog;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BreakFormat as e,isPostRollBreak as t}from"../../types/BreakManifest";import{armPlaybackCutoff as s,clearPlaybackCutoff as a}from"./PlaybackCutoff";export class SharedElementBreakPlayer{constructor(e,t){this.state=e,this.deps=t,this.contentElementAdPlayer=null,this.sharedAdSource=null,this.sharedAdEndedHandler=null,this.sharedAdTimeUpdateHandler=null,this.sharedAdTimedMetadataHandler=null,this.sharedAdWaitingHandler=null,this.sharedAdPlayingHandler=null,this.sharedAdFirstFrameTimer=null,this.sharedRestoreWatchdog=null,this.sharedRestore=null,this.sharedRestoreGeneration=0,this.sharedActivityGeneration=0}sharedElementAdPlayer(){const e=this.deps.contentPlayer.videoElement;if(!e)return null;if(this.contentElementAdPlayer?.videoElement===e)return this.contentElementAdPlayer;this.releaseContentElementAdPlayer();const t=this.deps.createAdAdapter(document.createElement("div"),e);return t.videoElement!==e?(this.deps.log("shared-element: createAdAdapter ignored the content element; falling back to the content player for ad playback"),t.destroy(),null):(this.contentElementAdPlayer=t,t)}async sharedAdStep(e,t,s){try{await s()}catch(s){const a=t.videoElement,n=s instanceof Error?s.message:String(s);throw new Error(`shared-element ${e} failed: ${n} (readyState=${a?.readyState??"-"} paused=${a?.paused??"-"} networkState=${a?.networkState??"-"})`,{cause:s})}}clearSharedAdFirstFrameWatchdog(){this.sharedAdFirstFrameTimer&&(clearTimeout(this.sharedAdFirstFrameTimer),this.sharedAdFirstFrameTimer=null)}armSharedAdFirstFrameWatchdog(e,t){this.clearSharedAdFirstFrameWatchdog(),this.sharedAdFirstFrameTimer=setTimeout(()=>{if(this.sharedAdFirstFrameTimer=null,!this.state.isPlaying||!this.state.currentBreak)return;const s=e.videoElement,a=this.deps.contentPlayer.videoElement,n=s?.currentSrc??"",r=`readyState=${s?.readyState??"-"} networkState=${s?.networkState??"-"} paused=${s?.paused??"-"} connected=${s?.isConnected??"-"} srcKind=${n.startsWith("blob:")?"blob":n?"url":"none"} sameElementAsContentPlayer=${String(s===a)}`;this.deps.log(`shared-element: no first frame within ${t}ms (${r})`),this.deps.diagnose("DA-AD-PLAYBACK-ERROR",`the ad never rendered a frame on the content element within ${t}ms`,{breakId:this.state.currentBreak.id,detail:r});const i=this.state.currentAsset;i&&this.state.callbacks?.onAdError(this.state.currentBreak,i,new Error(`shared-element ad produced no frames within ${t}ms (${r})`)),this.onSharedElementAdEnded()},t)}attachSharedAdListeners(e){this.sharedAdSource=e,this.sharedAdEndedHandler=()=>this.onSharedElementAdEnded(),e.on("ended",this.sharedAdEndedHandler),this.sharedAdTimeUpdateHandler=()=>{this.sharedAdFirstFrameTimer&&e.currentTime>0&&this.clearSharedAdFirstFrameWatchdog(),this.deps.reportAdProgress(e)},e.on("timeupdate",this.sharedAdTimeUpdateHandler),this.sharedAdTimedMetadataHandler=e=>{e&&this.deps.processTimedMetadata(e)},e.on("timedmetadata",this.sharedAdTimedMetadataHandler),this.sharedAdWaitingHandler=()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&this.state.callbacks?.onAdWaiting(this.state.currentBreak,this.state.currentAsset)},e.on("waiting",this.sharedAdWaitingHandler),this.sharedAdPlayingHandler=()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&!e.paused&&(this.clearSharedAdFirstFrameWatchdog(),this.state.startPlaybackCutoff?.(),this.state.callbacks?.onAdPlaying(this.state.currentBreak,this.state.currentAsset))},e.on("playing",this.sharedAdPlayingHandler)}detachSharedAdListeners(){const e=this.sharedAdSource??this.deps.contentPlayer;this.sharedAdSource=null,this.state.sharedAdClaimActive=!1,this.clearSharedAdFirstFrameWatchdog(),this.sharedAdTimedMetadataHandler&&(e.off("timedmetadata",this.sharedAdTimedMetadataHandler),this.sharedAdTimedMetadataHandler=null),this.sharedAdWaitingHandler&&(e.off("waiting",this.sharedAdWaitingHandler),this.sharedAdWaitingHandler=null),this.sharedAdPlayingHandler&&(e.off("playing",this.sharedAdPlayingHandler),this.sharedAdPlayingHandler=null),this.sharedAdTimeUpdateHandler&&(e.off("timeupdate",this.sharedAdTimeUpdateHandler),this.sharedAdTimeUpdateHandler=null),this.sharedAdEndedHandler&&(e.off("ended",this.sharedAdEndedHandler),this.sharedAdEndedHandler=null)}async releaseContentElementFromContentEngine(){if(this.deps.contentPlayer.releaseMediaElement)try{await this.deps.contentPlayer.releaseMediaElement()}catch(e){this.deps.log("shared-element: the content engine did not release the element:",e)}}releaseContentElementAdPlayer(){const e=this.contentElementAdPlayer;if(e){this.contentElementAdPlayer=null;try{e.destroy()}catch(e){this.deps.log("shared-element: releasing the ad player on the content element failed:",e)}}}async playAdThroughContentElement(){const e=this.state.currentAsset,t=this.state.currentBreak;if(!e||!t)return;const s=this.state.effectiveBreakDurationSec??t.duration,a=await this.deps.resolveAssetUri(e,s);if(!a)return void this.deps.log("shared-element: no URI for the running ad; leaving it on the ad element");const n=this.deps.getAdPlayer()?.currentTime??0;this.state.savedContentSrc=this.restorableContentSource(),this.state.savedContentTime=this.deps.contentPlayer.currentTime,this.state.savedContentLive=!Number.isFinite(this.deps.contentPlayer.duration),this.state.savedContentEdgeDistance=this.state.savedContentLive?this.deps.liveEdgeDistanceSec():null,this.state.sharedAdClaimActive=!0,await this.releaseContentElementFromContentEngine(),this.deps.rebindContentElementIfSwapped();const r=this.sharedElementAdPlayer()??this.deps.contentPlayer;try{this.deps.getAdPlayer()?.pause(),this.deps.clearContentGate(),await r.load(a),this.attachSharedAdListeners(r),n>0&&r.seek(n),await r.play(),this.deps.hide(),this.deps.log("shared-element: the ad now plays through the content element")}catch(e){this.deps.log("shared-element: could not move the ad onto the content element:",e),this.state.sharedAdClaimActive=!1,this.state.savedContentSrc=null,this.state.savedContentEdgeDistance=null}}async playSharedElementBreak(t,a){const n=this.deps.getVariant(t);if(!n)return this.deps.log("shared-element: no valid variant, skipping break:",t.id),void a.onBreakEnd(t);if(n.format===e.LSHAPE_CONTENT)return this.deps.log("shared-element: skipping lshape_content break (no fullscreen video asset):",t.id),void a.onBreakEnd(t);const r=this.deps.getAssets(t)[0];if(!r)return this.deps.log("shared-element: no asset to play, skipping break:",t.id),void a.onBreakEnd(t);n.format!==e.SINGLE&&this.deps.log(`shared-element: downgrading ${n.format} to a single fullscreen ad:`,t.id),this.state.currentBreak=t,this.state.currentAsset=r,this.state.callbacks=a,this.state.isPlaying=!0;const i=this.state.effectiveBreakDurationSec??t.duration,d=await this.deps.resolveAssetUri(r,i);if(!d)return this.deps.log("shared-element: no valid URI, skipping break:",t.id),this.state.isPlaying=!1,this.state.currentBreak=null,this.state.currentAsset=null,void a.onBreakEnd(t);this.state.savedContentSrc=this.restorableContentSource(),this.state.savedContentTime=this.deps.contentPlayer.currentTime,this.state.savedContentLive=!Number.isFinite(this.deps.contentPlayer.duration),this.state.savedContentEdgeDistance=this.state.savedContentLive?this.deps.liveEdgeDistanceSec():null,this.state.sharedAdClaimActive=!0,await this.releaseContentElementFromContentEngine(),this.deps.rebindContentElementIfSwapped();const h=this.sharedElementAdPlayer()??this.deps.contentPlayer;if(i&&i>0){const e=i+this.state.adBreakCutSafetyMarginSec;s(this.state,e,()=>{this.deps.log("shared-element: break-cut timer expired, ending break"),this.state.isPlaying&&this.state.currentBreak&&this.state.callbacks?.onBreakCut?.(this.state.currentBreak),this.onSharedElementAdEnded()},()=>{this.state.callbacks?.onAdError(t,r,new Error("Ad playback startup timed out")),this.onSharedElementAdEnded()})}try{if(this.deps.clearContentGate(),await this.sharedAdStep("load",h,()=>h.load(d)),!this.state.isPlaying||this.state.currentBreak!==t)return;if(this.attachSharedAdListeners(h),this.armSharedAdFirstFrameWatchdog(h,Math.max(Math.min(1e3*(i??0)-100,5e3),3e3)),await this.sharedAdStep("play",h,()=>h.play()),!this.state.isPlaying||this.state.currentBreak!==t)return;this.state.callbacks?.onAdStart(t,r,0,1)}catch(e){if(!this.state.isPlaying||this.state.currentBreak!==t)return;this.deps.log("shared-element: ad playback failed:",e),this.state.callbacks?.onAdError(t,r,e instanceof Error?e:new Error(String(e))),this.onSharedElementAdEnded()}}onSharedElementAdEnded(){if(!this.state.isPlaying||!this.state.currentBreak)return;const e=this.state.currentBreak,t=this.state.currentAsset,s=this.state.callbacks,n=this.sharedRestoreGeneration;this.detachSharedAdListeners(),a(this.state),this.state.isPlaying=!1,this.state.currentBreak=null,this.state.currentAsset=null,t&&s?.onAdEnd(e,t,0,1),n===this.sharedRestoreGeneration&&this.restoreSharedElementContent(e),s?.onBreakEnd(e)}restoreSharedElementContent(e){const s=this.state.savedContentSrc;if(!s){if(this.sharedRestore)return;return this.deps.log("shared-element: no saved content source to restore"),void this.releaseContentElementAdPlayer()}if(!this.beginSharedActivity())return;const a=this.state.savedContentTime,n=this.state.savedContentLive,r=this.state.savedContentEdgeDistance,i=this.state.callbacks;this.state.savedContentSrc=null,this.state.savedContentEdgeDistance=null;const d={src:s,time:a,live:n,edgeDistance:r,breakInfo:e,postRoll:!!e&&t(e),onContentReloading:i?.onContentReloading?.bind(i),adPlayer:this.contentElementAdPlayer,reloading:!1,cleanedUp:!1,cancelled:!1,finishingActivity:null,attempt:null};this.sharedRestore=d,e&&"number"==typeof e.start&&i?.onContentResumeSeek?.(n?this.deps.contentPlayer.currentTime:a,e.start+e.duration),this.sharedRestore===d&&(d.reloading=!0,d.onContentReloading?.(!0),this.sharedRestore===d&&this.runSharedRestore(d))}async runSharedRestore(e){for(let t=1;t<=2;t++){const s={phase:"load",deadline:0,timer:null,settle:null};e.attempt=s;const a=await new Promise(t=>{s.settle=e=>{s.settle&&(s.settle=null,s.timer&&clearTimeout(s.timer),s.timer=null,t(e))},this.armSharedRestoreDeadline(s,3e4),this.reloadAndResume(e,s).then(()=>s.settle?.("success"),()=>s.settle?.("rejection"))});if(this.sharedRestore!==e||e.attempt!==s)return;if("success"===a||"cancelled"===a)break;if(2===t?this.deps.diagnose("DA-SHARED-ELEMENT-RESTORE-FAILED",`Content did not resume after the shared-element break: ${s.phase} ${a}`,{breakId:e.breakInfo?.id,live:e.live,attempts:t,phase:s.phase,reason:a}):this.deps.log(`shared-element: content restore ${s.phase} ${a}, retrying once`),this.sharedRestore!==e||e.attempt!==s)return}this.finishSharedRestore(e,!0)}armSharedRestoreDeadline(e,t){e.timer&&clearTimeout(e.timer),e.deadline=performance.now()+t,e.timer=setTimeout(()=>e.settle?.("timeout"),t)}ownsSharedRestoreAttempt(e,t){return!(this.sharedRestore!==e||e.attempt!==t||!t.settle)&&(performance.now()<t.deadline||(t.settle("timeout"),!1))}async reloadAndResume(e,t){const{src:s,time:a,live:n,edgeDistance:r,postRoll:i}=e,d=()=>this.ownsSharedRestoreAttempt(e,t);if(d()&&(await this.deps.contentPlayer.load(s),d())){if(this.armSharedRestoreDeadline(t,5e3),n){if(!i&&null!==r&&r>0){t.phase="live-edge";const e=await this.deps.waitForLiveEdge(d);if(!d())return;if(null!==e){const s=Math.max(e-r,0);if(this.deps.log(`shared-element: live resume ${r.toFixed(2)}s behind the edge (${s.toFixed(2)}s)`),!d())return;t.phase="seek",this.deps.contentPlayer.seek(s)}}}else t.phase="seek",this.deps.contentPlayer.seek(a);d()&&!i&&(t.phase="play",await this.deps.contentPlayer.play(),d())}}finishSharedRestore(e,t){this.sharedRestore===e&&(null===e.finishingActivity?(e.finishingActivity=this.sharedActivityGeneration,e.cleanedUp||(this.state.contentPaused=!1,this.contentElementAdPlayer===e.adPlayer&&this.releaseContentElementAdPlayer(),this.sharedRestore===e&&this.deps.clearContentGate(),this.sharedRestore===e&&this.deps.rebindContentElementIfSwapped(),e.cleanedUp=!0),this.endSharedRestoreReloading(e),e.finishingActivity=null,this.sharedRestore===e&&(!t||e.cancelled||e.postRoll?this.sharedRestore=null:this.armSharedRestoreWatchdog(e))):e.finishingActivity!==this.sharedActivityGeneration&&(this.endSharedRestoreReloading(e),this.sharedRestore===e&&(this.sharedRestore=null)))}endSharedRestoreReloading(e){e.reloading&&(e.reloading=!1,e.onContentReloading?.(!1))}armSharedRestoreWatchdog(e){const t=this.deps.contentPlayer.currentTime;this.sharedRestoreWatchdog=setTimeout(()=>{if(this.sharedRestore!==e)return;this.sharedRestoreWatchdog=null;const s=this.deps.contentPlayer.currentTime-t;(this.deps.contentPlayer.paused||s<1)&&(this.deps.diagnose("DA-CONTENT-RESUME-STALLED",`Content did not resume within 5s of the shared-element restore (paused: ${this.deps.contentPlayer.paused}, advanced: ${s.toFixed(1)}s); nudging play()`,{breakId:e.breakInfo?.id??"?",paused:this.deps.contentPlayer.paused,advancedSec:s}),this.sharedRestore===e&&this.deps.contentPlayer.play().catch(()=>{this.sharedRestore===e&&this.deps.log("shared-element: watchdog play() nudge failed")}))},5e3)}beginSharedActivity(){return this.sharedActivityGeneration++,this.cancelSharedRestore()}cancelSharedRestore(){const e=this.sharedActivityGeneration;this.sharedRestoreGeneration++,this.sharedRestoreWatchdog&&(clearTimeout(this.sharedRestoreWatchdog),this.sharedRestoreWatchdog=null);const t=this.sharedRestore;return t&&(t.cancelled=!0,t.attempt?.settle?.("cancelled"),this.finishSharedRestore(t,!1)),e===this.sharedActivityGeneration}restorableContentSource(){const e=this.deps.contentPlayer.sourceUrl;if(e&&!e.startsWith("blob:"))return e;const t=this.deps.contentPlayer.videoElement,s=t?.currentSrc||t?.src||"";return s.length>0&&!s.startsWith("blob:")?s:null}}
|
|
1
|
+
import{BreakFormat as e,isPostRollBreak as t}from"../../types/BreakManifest";import{armPlaybackCutoff as s,clearPlaybackCutoff as a}from"./PlaybackCutoff";import{AdError as n}from"../../types/AdError";import{classifyMediaError as r,toClassifiedAdError as i}from"./classifyMediaError";function d(e){return e?{src:e.getAttribute("src"),srcObject:e.srcObject,sourceChild:null!==e.querySelector("source")}:null}export class SharedElementBreakPlayer{constructor(e,t){this.state=e,this.deps=t,this.contentElementAdPlayer=null,this.sharedAdSource=null,this.sharedAdEndedHandler=null,this.sharedAdTimeUpdateHandler=null,this.sharedAdTimedMetadataHandler=null,this.sharedAdWaitingHandler=null,this.sharedAdPlayingHandler=null,this.sharedAdFirstFrameTimer=null,this.sharedRestoreWatchdog=null,this.sharedRestore=null,this.sharedRestoreGeneration=0,this.sharedActivityGeneration=0}sharedElementAdPlayer(){const e=this.deps.contentPlayer.videoElement;if(!e)return null;if(this.contentElementAdPlayer?.videoElement===e)return this.contentElementAdPlayer;this.releaseContentElementAdPlayer();const t=this.deps.createAdAdapter(document.createElement("div"),e);return t.videoElement!==e?(this.deps.log("shared-element: createAdAdapter ignored the content element; falling back to the content player for ad playback"),t.destroy(),null):(this.contentElementAdPlayer=t,t)}async sharedAdStep(e,t,s){try{await s()}catch(s){const a=t.videoElement,i=s instanceof Error?s.message:String(s),d=new n(`shared-element ${e} failed: ${i} (readyState=${a?.readyState??"-"} paused=${a?.paused??"-"} networkState=${a?.networkState??"-"})`,r(s));throw d.cause=s,d}}clearSharedAdFirstFrameWatchdog(){this.sharedAdFirstFrameTimer&&(clearTimeout(this.sharedAdFirstFrameTimer),this.sharedAdFirstFrameTimer=null)}armSharedAdFirstFrameWatchdog(e,t){this.clearSharedAdFirstFrameWatchdog(),this.sharedAdFirstFrameTimer=setTimeout(()=>{if(this.sharedAdFirstFrameTimer=null,!this.state.isPlaying||!this.state.currentBreak)return;const s=e.videoElement,a=this.deps.contentPlayer.videoElement,r=s?.currentSrc??"",i=`readyState=${s?.readyState??"-"} networkState=${s?.networkState??"-"} paused=${s?.paused??"-"} connected=${s?.isConnected??"-"} srcKind=${r.startsWith("blob:")?"blob":r?"url":"none"} sameElementAsContentPlayer=${String(s===a)}`;this.deps.log(`shared-element: no first frame within ${t}ms (${i})`),this.deps.diagnose("DA-AD-PLAYBACK-ERROR",`the ad never rendered a frame on the content element within ${t}ms`,{breakId:this.state.currentBreak.id,detail:i});const d=this.state.currentAsset;d&&this.state.callbacks?.onAdError(this.state.currentBreak,d,new n(`shared-element ad produced no frames within ${t}ms (${i})`,"DA-AD-START-TIMEOUT")),this.onSharedElementAdEnded()},t)}attachSharedAdListeners(e){this.sharedAdSource=e,this.sharedAdEndedHandler=()=>this.onSharedElementAdEnded(),e.on("ended",this.sharedAdEndedHandler),this.sharedAdTimeUpdateHandler=()=>{this.sharedAdFirstFrameTimer&&e.currentTime>0&&this.clearSharedAdFirstFrameWatchdog(),this.deps.reportAdProgress(e)},e.on("timeupdate",this.sharedAdTimeUpdateHandler),this.sharedAdTimedMetadataHandler=e=>{e&&this.deps.processTimedMetadata(e)},e.on("timedmetadata",this.sharedAdTimedMetadataHandler),this.sharedAdWaitingHandler=()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&this.state.callbacks?.onAdWaiting(this.state.currentBreak,this.state.currentAsset)},e.on("waiting",this.sharedAdWaitingHandler),this.sharedAdPlayingHandler=()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&!e.paused&&(this.clearSharedAdFirstFrameWatchdog(),this.state.startPlaybackCutoff?.(),this.state.callbacks?.onAdPlaying(this.state.currentBreak,this.state.currentAsset))},e.on("playing",this.sharedAdPlayingHandler)}detachSharedAdListeners(){const e=this.sharedAdSource??this.deps.contentPlayer;this.sharedAdSource=null,this.state.sharedAdClaimActive=!1,this.clearSharedAdFirstFrameWatchdog(),this.sharedAdTimedMetadataHandler&&(e.off("timedmetadata",this.sharedAdTimedMetadataHandler),this.sharedAdTimedMetadataHandler=null),this.sharedAdWaitingHandler&&(e.off("waiting",this.sharedAdWaitingHandler),this.sharedAdWaitingHandler=null),this.sharedAdPlayingHandler&&(e.off("playing",this.sharedAdPlayingHandler),this.sharedAdPlayingHandler=null),this.sharedAdTimeUpdateHandler&&(e.off("timeupdate",this.sharedAdTimeUpdateHandler),this.sharedAdTimeUpdateHandler=null),this.sharedAdEndedHandler&&(e.off("ended",this.sharedAdEndedHandler),this.sharedAdEndedHandler=null)}async releaseContentElementFromContentEngine(){if(this.deps.contentPlayer.releaseMediaElement)try{await this.deps.contentPlayer.releaseMediaElement()}catch(e){this.deps.log("shared-element: the content engine did not release the element:",e)}}releaseContentElementAdPlayer(){const e=this.contentElementAdPlayer;if(e){this.contentElementAdPlayer=null;try{e.destroy()}catch(e){this.deps.log("shared-element: releasing the ad player on the content element failed:",e)}}}async playAdThroughContentElement(){const e=this.state.currentAsset,t=this.state.currentBreak;if(!e||!t)return;const s=this.state.effectiveBreakDurationSec??t.duration,a=await this.deps.resolveAssetUri(e,s);if(!a)return void this.deps.log("shared-element: no URI for the running ad; leaving it on the ad element");const n=this.deps.getAdPlayer()?.currentTime??0;this.state.savedContentSrc=this.restorableContentSource(),this.state.savedContentTime=this.deps.contentPlayer.currentTime,this.state.savedContentLive=!Number.isFinite(this.deps.contentPlayer.duration),this.state.savedContentEdgeDistance=this.state.savedContentLive?this.deps.liveEdgeDistanceSec():null,this.state.sharedAdClaimActive=!0,await this.releaseContentElementFromContentEngine(),this.deps.rebindContentElementIfSwapped();const r=this.sharedElementAdPlayer()??this.deps.contentPlayer;try{this.deps.getAdPlayer()?.pause(),this.deps.clearContentGate(),await r.load(a),this.attachSharedAdListeners(r),n>0&&r.seek(n),await r.play(),this.deps.hide(),this.deps.log("shared-element: the ad now plays through the content element")}catch(e){this.deps.log("shared-element: could not move the ad onto the content element:",e),this.state.sharedAdClaimActive=!1,this.state.savedContentSrc=null,this.state.savedContentEdgeDistance=null}}async playSharedElementBreak(t,a){const r=this.deps.getVariant(t);if(!r)return this.deps.log("shared-element: no valid variant, skipping break:",t.id),void a.onBreakEnd(t);if(r.format===e.LSHAPE_CONTENT)return this.deps.log("shared-element: skipping lshape_content break (no fullscreen video asset):",t.id),void a.onBreakEnd(t);const d=this.deps.getAssets(t)[0];if(!d)return this.deps.log("shared-element: no asset to play, skipping break:",t.id),void a.onBreakEnd(t);r.format!==e.SINGLE&&this.deps.log(`shared-element: downgrading ${r.format} to a single fullscreen ad:`,t.id),this.state.currentBreak=t,this.state.currentAsset=d,this.state.callbacks=a,this.state.isPlaying=!0;const h=this.state.effectiveBreakDurationSec??t.duration,l=await this.deps.resolveAssetUri(d,h);if(!l)return this.deps.log("shared-element: no valid URI, skipping break:",t.id),this.state.isPlaying=!1,this.state.currentBreak=null,this.state.currentAsset=null,void a.onBreakEnd(t);this.state.savedContentSrc=this.restorableContentSource(),this.state.savedContentTime=this.deps.contentPlayer.currentTime,this.state.savedContentLive=!Number.isFinite(this.deps.contentPlayer.duration),this.state.savedContentEdgeDistance=this.state.savedContentLive?this.deps.liveEdgeDistanceSec():null,this.state.sharedAdClaimActive=!0,await this.releaseContentElementFromContentEngine(),this.deps.rebindContentElementIfSwapped();const o=this.sharedElementAdPlayer()??this.deps.contentPlayer;if(h&&h>0){const e=h+this.state.adBreakCutSafetyMarginSec;s(this.state,e,()=>{this.deps.log("shared-element: break-cut timer expired, ending break"),this.state.isPlaying&&this.state.currentBreak&&this.state.callbacks?.onBreakCut?.(this.state.currentBreak),this.onSharedElementAdEnded()},()=>{this.state.callbacks?.onAdError(t,d,new n("Ad playback startup timed out","DA-AD-START-TIMEOUT")),this.onSharedElementAdEnded()})}try{if(this.deps.clearContentGate(),await this.sharedAdStep("load",o,()=>o.load(l)),!this.state.isPlaying||this.state.currentBreak!==t)return;if(this.attachSharedAdListeners(o),this.armSharedAdFirstFrameWatchdog(o,Math.max(Math.min(1e3*(h??0)-100,5e3),3e3)),await this.sharedAdStep("play",o,()=>o.play()),!this.state.isPlaying||this.state.currentBreak!==t)return;this.state.callbacks?.onAdStart(t,d,0,1)}catch(e){if(!this.state.isPlaying||this.state.currentBreak!==t)return;this.deps.log("shared-element: ad playback failed:",e),this.state.callbacks?.onAdError(t,d,i(e)),this.onSharedElementAdEnded()}}onSharedElementAdEnded(){if(!this.state.isPlaying||!this.state.currentBreak)return;const e=this.state.currentBreak,t=this.state.currentAsset,s=this.state.callbacks,n=this.sharedRestoreGeneration;this.detachSharedAdListeners(),a(this.state),this.state.isPlaying=!1,this.state.currentBreak=null,this.state.currentAsset=null,t&&s?.onAdEnd(e,t,0,1),n===this.sharedRestoreGeneration&&this.restoreSharedElementContent(e),s?.onBreakEnd(e)}restoreSharedElementContent(e){const s=this.state.savedContentSrc;if(!s){if(this.sharedRestore)return;return this.deps.log("shared-element: no saved content source to restore"),void this.releaseContentElementAdPlayer()}if(!this.beginSharedActivity())return;const a=this.state.savedContentTime,n=this.state.savedContentLive,r=this.state.savedContentEdgeDistance,i=this.state.callbacks;this.state.savedContentSrc=null,this.state.savedContentEdgeDistance=null;const d={src:s,time:a,live:n,edgeDistance:r,breakInfo:e,postRoll:!!e&&t(e),onContentReloading:i?.onContentReloading?.bind(i),adPlayer:this.contentElementAdPlayer,reloading:!1,cleanedUp:!1,cancelled:!1,finishingActivity:null,attempt:null};this.sharedRestore=d,e&&"number"==typeof e.start&&i?.onContentResumeSeek?.(n?this.deps.contentPlayer.currentTime:a,e.start+e.duration),this.sharedRestore===d&&(d.reloading=!0,d.onContentReloading?.(!0),this.sharedRestore===d&&this.runSharedRestore(d))}async runSharedRestore(e){for(let t=1;t<=2;t++){const s={phase:"load",deadline:0,timer:null,settle:null};e.attempt=s;const a=await new Promise(t=>{s.settle=e=>{s.settle&&(s.settle=null,s.timer&&clearTimeout(s.timer),s.timer=null,t(e))},this.armSharedRestoreDeadline(s,1e4),this.reloadAndResume(e,s).then(()=>s.settle?.("success"),()=>s.settle?.("rejection"))});if(this.sharedRestore!==e||e.attempt!==s)return;if("success"===a||"cancelled"===a)break;if(2===t?this.deps.diagnose("DA-SHARED-ELEMENT-RESTORE-FAILED",`Content did not resume after the shared-element break: ${s.phase} ${a}`,{breakId:e.breakInfo?.id,live:e.live,attempts:t,phase:s.phase,reason:a}):this.deps.log(`shared-element: content restore ${s.phase} ${a}, retrying once`),this.sharedRestore!==e||e.attempt!==s)return}this.finishSharedRestore(e,!0)}armSharedRestoreDeadline(e,t){e.timer&&clearTimeout(e.timer),e.deadline=performance.now()+t,e.timer=setTimeout(()=>e.settle?.("timeout"),t)}ownsSharedRestoreAttempt(e,t){return!(this.sharedRestore!==e||e.attempt!==t||!t.settle)&&(performance.now()<t.deadline||(t.settle("timeout"),!1))}async reloadAndResume(e,t){const{src:s,time:a,live:n,edgeDistance:r,postRoll:i}=e,h=()=>this.ownsSharedRestoreAttempt(e,t);if(!h())return;if(e.adPlayer?.releaseMediaElement&&(await this.releaseAdEngineGrip(e),!h()))return;const l=this.deps.contentPlayer.videoElement??null,o=d(l);if(await this.deps.contentPlayer.load(s),h()){if(this.armSharedRestoreDeadline(t,5e3),n){if(!i&&null!==r&&r>0){t.phase="live-edge";const e=await this.deps.waitForLiveEdge(h);if(!h())return;if(null!==e){const s=Math.max(e-r,0);if(this.deps.log(`shared-element: live resume ${r.toFixed(2)}s behind the edge (${s.toFixed(2)}s)`),!h())return;t.phase="seek",this.deps.contentPlayer.seek(s)}}}else t.phase="seek",this.deps.contentPlayer.seek(a);h()&&!i&&(t.phase="play",await this.deps.contentPlayer.play(),h()&&await this.waitForContentEngineToTakeElement(l,o,h))}}async releaseAdEngineGrip(e){const t=e.adPlayer;if(t?.releaseMediaElement)try{await t.releaseMediaElement()}catch(e){this.deps.log("shared-element: the ad engine did not release the element:",e)}}async waitForContentEngineToTakeElement(e,t,s){if(!(e&&t&&(a=t,a.src||a.srcObject||a.sourceChild)))return;var a;const n=()=>{if(this.deps.contentPlayer.videoElement!==e)return!0;const s=d(e);if(!s)return!1;const a=!!s.src&&s.src!==t.src,n=!!s.srcObject&&s.srcObject!==t.srcObject;return a||n},r=performance.now();for(;!n()&&s()&&performance.now()-r<1500;)await new Promise(e=>setTimeout(e,25));if(!s())return;const i=Math.round(performance.now()-r);this.deps.log(`shared-element: content engine ${n()?"took":"did not take"} the element after ${i}ms`)}finishSharedRestore(e,t){this.sharedRestore===e&&(null===e.finishingActivity?(e.finishingActivity=this.sharedActivityGeneration,e.cleanedUp||(this.state.contentPaused=!1,this.contentElementAdPlayer===e.adPlayer&&this.releaseContentElementAdPlayer(),this.sharedRestore===e&&this.deps.clearContentGate(),this.sharedRestore===e&&this.deps.rebindContentElementIfSwapped(),e.cleanedUp=!0),this.endSharedRestoreReloading(e),e.finishingActivity=null,this.sharedRestore===e&&(!t||e.cancelled||e.postRoll?this.sharedRestore=null:this.armSharedRestoreWatchdog(e))):e.finishingActivity!==this.sharedActivityGeneration&&(this.endSharedRestoreReloading(e),this.sharedRestore===e&&(this.sharedRestore=null)))}endSharedRestoreReloading(e){e.reloading&&(e.reloading=!1,e.onContentReloading?.(!1))}armSharedRestoreWatchdog(e){const t=this.deps.contentPlayer.currentTime;this.sharedRestoreWatchdog=setTimeout(()=>{if(this.sharedRestore!==e)return;this.sharedRestoreWatchdog=null;const s=this.deps.contentPlayer.currentTime-t;(this.deps.contentPlayer.paused||s<1)&&(this.deps.diagnose("DA-CONTENT-RESUME-STALLED",`Content did not resume within 5s of the shared-element restore (paused: ${this.deps.contentPlayer.paused}, advanced: ${s.toFixed(1)}s); nudging play()`,{breakId:e.breakInfo?.id??"?",paused:this.deps.contentPlayer.paused,advancedSec:s}),this.sharedRestore===e&&this.deps.contentPlayer.play().catch(()=>{this.sharedRestore===e&&this.deps.log("shared-element: watchdog play() nudge failed")}))},5e3)}beginSharedActivity(){return this.sharedActivityGeneration++,this.cancelSharedRestore()}cancelSharedRestore(){const e=this.sharedActivityGeneration;this.sharedRestoreGeneration++,this.sharedRestoreWatchdog&&(clearTimeout(this.sharedRestoreWatchdog),this.sharedRestoreWatchdog=null);const t=this.sharedRestore;return t&&(t.cancelled=!0,t.attempt?.settle?.("cancelled"),this.finishSharedRestore(t,!1)),e===this.sharedActivityGeneration}restorableContentSource(){const e=this.deps.contentPlayer.sourceUrl;if(e&&!e.startsWith("blob:"))return e;const t=this.deps.contentPlayer.videoElement,s=t?.currentSrc||t?.src||"";return s.length>0&&!s.startsWith("blob:")?s:null}}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { KnownDiagnosticCode } from '../../diagnostics/errorCodes';
|
|
2
|
+
import { AdError } from '../../types/AdError';
|
|
3
|
+
/**
|
|
4
|
+
* Canonical code for an ad player failure: an `HTMLMediaElement` `MediaError` (`code` 1–4) or a
|
|
5
|
+
* Shaka error (`category`). Anything else is the generic `DA-AD-PLAYBACK-ERROR`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function classifyMediaError(error: unknown): KnownDiagnosticCode;
|
|
8
|
+
/** `error` as a classified {@link AdError}, keeping its message and an already-assigned code. */
|
|
9
|
+
export declare function toClassifiedAdError(error: unknown): AdError;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{AdError as r}from"../../types/AdError";export function classifyMediaError(e){if(e instanceof r)return e.code;const o=e;return"number"==typeof o?.category?1===o.category?"DA-AD-MEDIA-LOAD-FAILED":"DA-AD-MEDIA-UNSUPPORTED":2===o?.code?"DA-AD-MEDIA-LOAD-FAILED":3===o?.code||4===o?.code?"DA-AD-MEDIA-UNSUPPORTED":"DA-AD-PLAYBACK-ERROR"}export function toClassifiedAdError(e){if(e instanceof r)return e;const o=e instanceof Error?e.message:String(e);return new r(o,classifyMediaError(e))}
|
|
@@ -3,4 +3,33 @@ export declare const SEEK_END_SENTINEL_HORIZON_SEC: number;
|
|
|
3
3
|
export declare function liveEdgeDistanceSec(contentPlayer: PlayerAdapter): number | null;
|
|
4
4
|
export declare function isPlausibleSeekEnd(contentPlayer: PlayerAdapter, end: number): boolean;
|
|
5
5
|
export declare function contentSeekableEnd(contentPlayer: PlayerAdapter): number | null;
|
|
6
|
+
/**
|
|
7
|
+
* The furthest position a resume seek may ask for on live; null on VOD and when nothing is
|
|
8
|
+
* readable, which means "do not clamp".
|
|
9
|
+
*
|
|
10
|
+
* The ceiling answers which frames EXIST, not which latency the engine would prefer. Those are
|
|
11
|
+
* different questions and `maxLiveSeekPosition` answers the second one: measured on hls.js, its
|
|
12
|
+
* `liveSyncPosition` sits a mean 8.0s BELOW the seekable end while seeks into that band are
|
|
13
|
+
* honoured, so taking the minimum of the two discarded 8s of published content and resumed
|
|
14
|
+
* inside the slate the ad replaced. On a replacement break that band is content the viewer is
|
|
15
|
+
* entitled to see, whereas a lower latency is only a preference.
|
|
16
|
+
*
|
|
17
|
+
* Still the fallback for an engine that reports the latency bound and nothing else.
|
|
18
|
+
* See `docs/web-live-resume-findings.md`.
|
|
19
|
+
*/
|
|
6
20
|
export declare function liveSafeSeekEnd(contentPlayer: PlayerAdapter): number | null;
|
|
21
|
+
/**
|
|
22
|
+
* How far the adapter's published live edge sits beyond the end of its seekable range: media
|
|
23
|
+
* that exists but a seek cannot reach. Reported with every resume so a short landing can be
|
|
24
|
+
* attributed without a device.
|
|
25
|
+
*
|
|
26
|
+
* Null, not zero, when the adapter cannot name an edge of its own. `PlayerAdapter.liveEdge`
|
|
27
|
+
* falls back to the seekable end by contract, so a zero would mean both "nothing is held back"
|
|
28
|
+
* and "the adapter does not know" — and on THEOplayer web, which has no playlist target duration
|
|
29
|
+
* to work from, it is always the second while the measured gap is a mean 8.7s.
|
|
30
|
+
*/
|
|
31
|
+
export declare function unreachableEdgeSec(contentPlayer: PlayerAdapter): number | null;
|
|
32
|
+
/** Earliest position the engine still holds on a live stream; null on VOD or when unreadable. */
|
|
33
|
+
export declare function liveSafeSeekStart(contentPlayer: PlayerAdapter): number | null;
|
|
34
|
+
/** Length of the live window the engine holds (seekable end minus start); null on VOD or when unreadable. */
|
|
35
|
+
export declare function liveSeekableWindowSec(contentPlayer: PlayerAdapter): number | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SEEK_END_SENTINEL_HORIZON_SEC=86400;export function liveEdgeDistanceSec(e){const n=e.videoElement,t=n?.seekable;if(!t||0===t.length)return null;const i=t.end(t.length-1);if(!Number.isFinite(i))return null;const
|
|
1
|
+
export const SEEK_END_SENTINEL_HORIZON_SEC=86400;export function liveEdgeDistanceSec(e){const n=e.videoElement,t=n?.seekable;if(!t||0===t.length)return null;const i=t.end(t.length-1);if(!Number.isFinite(i))return null;const r=i-e.currentTime;return r>=0?r:0}export function isPlausibleSeekEnd(e,n){return!!Number.isFinite(n)&&n<e.currentTime+86400}export function contentSeekableEnd(e){const n=e.seekableEnd;if("number"==typeof n&&isPlausibleSeekEnd(e,n))return n;const t=e.videoElement?.seekable;if(!t||0===t.length)return null;const i=t.end(t.length-1);return isPlausibleSeekEnd(e,i)?i:null}export function liveSafeSeekEnd(e){if(Number.isFinite(e.duration))return null;const n=contentSeekableEnd(e);if(null!==n)return n;const t=e.maxLiveSeekPosition;return"number"==typeof t&&Number.isFinite(t)?t:null}export function unreachableEdgeSec(e){const n=e.liveEdge,t=contentSeekableEnd(e);if("number"!=typeof n||!Number.isFinite(n)||null===t)return null;const i=n-t;return i>.001?Math.round(1e3*i)/1e3:null}export function liveSafeSeekStart(e){if(Number.isFinite(e.duration))return null;const n=e.videoElement?.seekable;if(!n||0===n.length)return null;const t=n.start(0);return Number.isFinite(t)?t:null}export function liveSeekableWindowSec(e){if(Number.isFinite(e.duration))return null;const n=e.videoElement?.seekable;if(!n||0===n.length)return null;const t=n.start(0),i=n.end(n.length-1);return!Number.isFinite(t)||!Number.isFinite(i)||i<=t?null:i-t}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { TimedMetadataCue } from '../interfaces/PlayerAdapter';
|
|
2
2
|
import { MetadataCueTracker } from './MetadataCueTracker';
|
|
3
3
|
import type { BreakScheduler } from './BreakScheduler';
|
|
4
|
+
import type { Break } from '../types/BreakManifest';
|
|
5
|
+
/**
|
|
6
|
+
* The media time of the in-stream cue that matched `breakInfo`, for anchoring the content resume;
|
|
7
|
+
* null when there is no cue tracker (every timebase except `ptsSource: 'anvatoCue'`), when the
|
|
8
|
+
* break carries no numeric PTS start, or when no cue has been observed for it.
|
|
9
|
+
*/
|
|
10
|
+
export declare function cueAnchorForBreak(metadataCueTracker: MetadataCueTracker | null, breakInfo: Break): number | null;
|
|
4
11
|
export declare function armBreakFromPresentedCue(cue: TimedMetadataCue, metadataCueTracker: MetadataCueTracker | null, breakScheduler: BreakScheduler | undefined): void;
|
|
5
12
|
export declare function notedManifestBreakIds(manifest: {
|
|
6
13
|
breaks: Array<{
|