@dolby-optiview/ads-sdk-core 1.0.0-beta.6 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/OptiViewAds.js +1 -1
- package/dist/diagnostics/DiagnosticEvent.d.ts +2 -0
- package/dist/diagnostics/playbackErrorContext.d.ts +0 -17
- package/dist/diagnostics/playbackErrorContext.js +1 -1
- package/dist/generated/diagnosticCodes.d.ts +15 -0
- package/dist/generated/diagnosticCodes.js +1 -1
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/id3/parseId3Geob.d.ts +26 -5
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -1
- package/dist/interfaces/PlayerAdapter.d.ts +12 -1
- package/dist/server/index.d.ts +3 -1
- package/dist/server/index.js +1 -1
- package/dist/services/AdPlayerController.d.ts +8 -9
- package/dist/services/AdPlayerController.js +1 -1
- package/dist/services/CmcdAdReporter.d.ts +23 -3
- package/dist/services/CmcdAdReporter.js +1 -1
- package/dist/services/GamPodAdTracker.d.ts +3 -1
- package/dist/services/GamPodAdTracker.js +1 -1
- package/dist/services/GamStreamManager.js +1 -1
- package/dist/services/SsaiController.js +1 -1
- package/dist/services/StitcherUrlBuilder.js +1 -1
- package/dist/services/adPlayer/AdPlayerEvents.js +1 -1
- package/dist/services/adPlayer/AdPlayerState.d.ts +3 -0
- package/dist/services/adPlayer/AdPlayerState.js +1 -1
- package/dist/services/adPlayer/AdStage.d.ts +1 -0
- package/dist/services/adPlayer/AdStage.js +1 -1
- package/dist/services/adPlayer/AssetParameterResolver.js +1 -1
- package/dist/services/adPlayer/AssetPlayback.js +1 -1
- package/dist/services/adPlayer/ContentRecovery.d.ts +32 -0
- package/dist/services/adPlayer/ContentRecovery.js +1 -0
- package/dist/services/adPlayer/ContentResumeCoordinator.d.ts +7 -0
- package/dist/services/adPlayer/ContentResumeCoordinator.js +1 -1
- package/dist/services/adPlayer/PlaybackCutoff.d.ts +3 -0
- package/dist/services/adPlayer/PlaybackCutoff.js +1 -0
- package/dist/services/adPlayer/SharedElementBreakPlayer.d.ts +7 -1
- package/dist/services/adPlayer/SharedElementBreakPlayer.js +1 -1
- package/dist/services/detectContentRecovery.d.ts +12 -0
- package/dist/services/detectContentRecovery.js +1 -0
- package/dist/services/detectInsertionMode.js +1 -1
- package/dist/services/fetchWithTimeout.js +1 -1
- package/dist/services/resolveGamVendorConfiguration.js +1 -1
- package/dist/services/resolvedModeDiagnostics.d.ts +18 -0
- package/dist/services/resolvedModeDiagnostics.js +1 -0
- package/dist/services/runtimeObject.d.ts +2 -0
- package/dist/services/runtimeObject.js +1 -0
- package/dist/services/webCompatibility.d.ts +4 -0
- package/dist/services/webCompatibility.js +1 -0
- package/dist/services/webRuntime.d.ts +8 -0
- package/dist/services/webRuntime.js +1 -0
- package/dist/services/xhrWithTimeout.d.ts +1 -0
- package/dist/services/xhrWithTimeout.js +1 -0
- package/dist/session/AdBreakStatusTracker.d.ts +6 -3
- package/dist/session/AdBreakStatusTracker.js +1 -1
- package/dist/session/BreakLifecycle.js +1 -1
- package/dist/session/SessionStarter.js +1 -1
- package/dist/types/Events.d.ts +1 -1
- package/dist/types/OptiViewAdsConfig.d.ts +25 -5
- package/dist/types/OptiViewAdsConfig.js +1 -1
- package/dist/types/compatibility-modules.d.ts +4 -0
- package/dist/types/compatibility-modules.js +1 -0
- package/package.json +6 -2
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { PlayerAdapter } from '../../interfaces/PlayerAdapter';
|
|
2
|
+
import type { Break } from '../../types/BreakManifest';
|
|
3
|
+
import type { ResolvedPreloadMode } from '../detectPreloadMode';
|
|
4
|
+
import type { ResolvedContentRecovery } from '../detectContentRecovery';
|
|
5
|
+
import type { ContentResumeCoordinator } from './ContentResumeCoordinator';
|
|
6
|
+
/** Everything resumeContentAfterBreak needs from the controller, injected once at construction. */
|
|
7
|
+
export interface ContentRecoveryDeps {
|
|
8
|
+
contentPlayer: PlayerAdapter;
|
|
9
|
+
/** Live read — callbacks are per break. */
|
|
10
|
+
getCallbacks: () => {
|
|
11
|
+
onContentReloading?: (active: boolean) => void;
|
|
12
|
+
} | null;
|
|
13
|
+
preloadMode: ResolvedPreloadMode;
|
|
14
|
+
contentRecovery: ResolvedContentRecovery;
|
|
15
|
+
resumeCoordinator: ContentResumeCoordinator;
|
|
16
|
+
diagnose: (code: string, message: string, context?: Record<string, unknown>) => void;
|
|
17
|
+
log: (...args: unknown[]) => void;
|
|
18
|
+
}
|
|
19
|
+
export interface ResumeAfterBreakArgs {
|
|
20
|
+
breakInfo: Break;
|
|
21
|
+
earlyReturnOffsetSec: number | null;
|
|
22
|
+
isLiveResume: boolean;
|
|
23
|
+
/** Return of the ad player's `unload()` already issued at break end, if any. */
|
|
24
|
+
released: void | Promise<void> | undefined;
|
|
25
|
+
/** False once a newer break or destroy() superseded this resume; checked after every await. */
|
|
26
|
+
isCurrent: () => boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Post-break content resume, run async so the ad release and (in `recreate` recovery) the
|
|
30
|
+
* content pipeline rebuild can be awaited.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resumeContentAfterBreak(deps: ContentRecoveryDeps, args: ResumeAfterBreakArgs): Promise<void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export async function resumeContentAfterBreak(e,t){const{breakInfo:r,earlyReturnOffsetSec:n,isLiveResume:i,released:a,isCurrent:o}=t,{contentPlayer:s,preloadMode:d,contentRecovery:l,resumeCoordinator:c,log:u}=e;if(a&&(await Promise.resolve(a).catch(e=>{u("Ad media release failed:",e)}),!o()))return;const m=()=>{i&&setTimeout(()=>e.getCallbacks()?.onContentReloading?.(!1),2e3)},p=async()=>{await s.play().catch(e=>{u("Content resume failed:",e)}).finally(m)};if("recreate"===l){if("function"==typeof s.recreateMediaPipeline)return void await async function(e,t,r){const{breakInfo:n,earlyReturnOffsetSec:i,isLiveResume:a,isCurrent:o}=t,{contentPlayer:s,resumeCoordinator:d,diagnose:l,preloadMode:c}=e,u=s.recreateMediaPipeline;if(!u)return;if("single-decoder"===c&&(await new Promise(e=>setTimeout(e,300)),!o()))return;a&&e.getCallbacks()?.onContentReloading?.(!0);const m=d.resolveResumeTarget(n,i)??s.currentTime,p=()=>"undefined"!=typeof performance?performance.now():Date.now(),f=p(),C=u.call(s,{startTime:m});let E,T=null;try{await Promise.race([C,new Promise((e,t)=>{E=setTimeout(()=>t(new Error("recreateMediaPipeline timed out")),1e4)})])}catch(e){T=e}finally{void 0!==E&&clearTimeout(E)}if(!o())return;if(null===T){const e=Math.round(p()-f);l("DA-CONTENT-PIPELINE-RECREATED",`Content media pipeline rebuilt at ${m}s for break ${n.id} in ${e}ms`,{breakId:n.id,startTime:m,durationMs:e}),d.probeResumeLanding(n.id,m)}else{const e=T instanceof Error?T.message:String(T);l("DA-CONTENT-PIPELINE-RECREATE-FAILED",`Content media pipeline rebuild for break ${n.id} failed (${e}); resuming in place`,{breakId:n.id,startTime:m,reason:e}),d.probeResumeLanding(n.id,m),s.seek(m)}await r()}(e,t,p);u('Content recovery "recreate" requested but the content adapter does not implement recreateMediaPipeline(); resuming in place with seek + play'),e.diagnose("DA-CONTENT-PIPELINE-RECREATE-FAILED",'Content recovery "recreate" requested but the content adapter does not implement recreateMediaPipeline(); resuming in place',{breakId:r.id,startTime:s.currentTime,reason:"unsupported-adapter"})}const f=()=>{o()&&(i&&e.getCallbacks()?.onContentReloading?.(!0),c.applyResumeSeek(r,n),p())};"single-decoder"===d?setTimeout(f,300):f()}
|
|
@@ -40,5 +40,12 @@ export declare class ContentResumeCoordinator {
|
|
|
40
40
|
endContinueContentPlayout(): void;
|
|
41
41
|
setContinueRate(rate: number): void;
|
|
42
42
|
tickContinueContentPlayout(): void;
|
|
43
|
+
/**
|
|
44
|
+
* Resolve the position content should resume at after `breakInfo`, applying the insertion-type
|
|
45
|
+
* policy, the manifest/client offset precedence and the live-window clamp, and emitting the
|
|
46
|
+
* DA-RESUME-* diagnostics for each decision. Returns the clamped target, or `null` when no
|
|
47
|
+
* seek is needed (policy says resume in place, or content was already pre-positioned there).
|
|
48
|
+
*/
|
|
49
|
+
resolveResumeTarget(breakInfo: Break, earlyReturnOffsetSec?: number | null): number | null;
|
|
43
50
|
applyResumeSeek(breakInfo: Break, earlyReturnOffsetSec?: number | null): void;
|
|
44
51
|
}
|
|
@@ -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.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.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.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()}applyResumeSeek(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 void this.deps.log(`Resume: no seek for break ${t.id} (offset 0, ${r.source})`);this.deps.log(`Resume-seek to ${r.target}s (offset ${r.offset}s, ${r.source}) for break ${t.id}`),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});(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 o=this.liveSafeSeekEnd();let d=r.target,l=0;if(null!==o&&d>o&&(l=Math.round(1e3*(d-o))/1e3,d=o),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)`+(l>0?` — ${l}s short of policy, those frames are not published yet`:"")+" — no seek"),void this.deps.diagnose?.("DA-RESUME-PREPOSITIONED",`Break ${t.id} resumed without a seek: content was moved to ${e}s during the break`+(l>0?` (${l}s short of the ${r.target}s policy target)`:""),{breakId:t.id,target:r.target,prepositionedTo:this.state.resumePrepositionedTo,landed:e,shortfallSec:l,exact:0===l,...this.resumeAlignSummary()})}l>0&&(this.deps.log(`Resume-seek clamped: ${r.target}s is beyond what the stream has published; seeking ${d}s instead (${l}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:l,...this.resumeAlignSummary()})),this.probeResumeLanding(t.id,d),this.deps.contentPlayer.seek(d)}}
|
|
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))}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function clearPlaybackCutoff(a){null!==a.durationTimer&&clearTimeout(a.durationTimer),a.durationTimer=null,a.startPlaybackCutoff=null}export function armPlaybackCutoff(a,t,r,u){clearPlaybackCutoff(a);const e=a.currentBreak,o=()=>a.isPlaying&&a.currentBreak===e,i=setTimeout(()=>{a.durationTimer===i&&(clearPlaybackCutoff(a),o()&&u())},15e3);a.durationTimer=i;const n=()=>{if(!o()||a.startPlaybackCutoff!==n)return;clearPlaybackCutoff(a);const u=setTimeout(()=>{a.durationTimer===u&&(a.durationTimer=null,o()&&r())},1e3*Math.max(0,t));a.durationTimer=u};a.startPlaybackCutoff=n}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PlayerAdapter } from '../../interfaces/PlayerAdapter';
|
|
1
|
+
import type { PlayerAdapter, TimedMetadataCue } from '../../interfaces/PlayerAdapter';
|
|
2
2
|
import type { Asset, Break } from '../../types/BreakManifest';
|
|
3
3
|
import type { AdPlayerCallbacks } from '../AdPlayerController';
|
|
4
4
|
import type { AdPlayerState } from './AdPlayerState';
|
|
@@ -10,6 +10,8 @@ export interface SharedElementBreakPlayerDeps {
|
|
|
10
10
|
rebindContentElementIfSwapped: () => void;
|
|
11
11
|
createAdAdapter: (container: HTMLElement, video?: HTMLVideoElement) => PlayerAdapter;
|
|
12
12
|
reportAdProgress: (player: PlayerAdapter) => void;
|
|
13
|
+
/** ID3 from the element the ad plays on → IMA; a GAM pod's ad markers ride the pod stream. */
|
|
14
|
+
processTimedMetadata: (cue: TimedMetadataCue) => void;
|
|
13
15
|
diagnose: (code: string, message: string, context?: Record<string, unknown>) => void;
|
|
14
16
|
log: (...args: unknown[]) => void;
|
|
15
17
|
clearContentGate: () => void;
|
|
@@ -28,6 +30,9 @@ export declare class SharedElementBreakPlayer {
|
|
|
28
30
|
private sharedAdSource;
|
|
29
31
|
private sharedAdEndedHandler;
|
|
30
32
|
private sharedAdTimeUpdateHandler;
|
|
33
|
+
private sharedAdTimedMetadataHandler;
|
|
34
|
+
private sharedAdWaitingHandler;
|
|
35
|
+
private sharedAdPlayingHandler;
|
|
31
36
|
private sharedAdFirstFrameTimer;
|
|
32
37
|
private sharedRestoreWatchdog;
|
|
33
38
|
private sharedRestore;
|
|
@@ -52,6 +57,7 @@ export declare class SharedElementBreakPlayer {
|
|
|
52
57
|
onSharedElementAdEnded(): void;
|
|
53
58
|
restoreSharedElementContent(breakInfo?: Break | null): void;
|
|
54
59
|
private runSharedRestore;
|
|
60
|
+
private armSharedRestoreDeadline;
|
|
55
61
|
private ownsSharedRestoreAttempt;
|
|
56
62
|
private reloadAndResume;
|
|
57
63
|
private finishSharedRestore;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BreakFormat as e,isPostRollBreak as t}from"../../types/BreakManifest";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.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)}detachSharedAdListeners(){const e=this.sharedAdSource??this.deps.contentPlayer;this.sharedAdSource=null,this.state.sharedAdClaimActive=!1,this.clearSharedAdFirstFrameWatchdog(),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,s){const a=this.deps.getVariant(t);if(!a)return this.deps.log("shared-element: no valid variant, skipping break:",t.id),void s.onBreakEnd(t);if(a.format===e.LSHAPE_CONTENT)return this.deps.log("shared-element: skipping lshape_content break (no fullscreen video asset):",t.id),void s.onBreakEnd(t);const n=this.deps.getAssets(t)[0];if(!n)return this.deps.log("shared-element: no asset to play, skipping break:",t.id),void s.onBreakEnd(t);a.format!==e.SINGLE&&this.deps.log(`shared-element: downgrading ${a.format} to a single fullscreen ad:`,t.id),this.state.currentBreak=t,this.state.currentAsset=n,this.state.callbacks=s,this.state.isPlaying=!0;const r=this.state.effectiveBreakDurationSec??t.duration,i=await this.deps.resolveAssetUri(n,r);if(!i)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 s.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 d=this.sharedElementAdPlayer()??this.deps.contentPlayer;if(r&&r>0){const e=r+this.state.adBreakCutSafetyMarginSec;this.state.durationTimer=setTimeout(()=>{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()},1e3*e)}try{this.deps.clearContentGate(),await this.sharedAdStep("load",d,()=>d.load(i)),this.attachSharedAdListeners(d),this.armSharedAdFirstFrameWatchdog(d,Math.max(Math.min(1e3*(r??0)-100,5e3),3e3)),await this.sharedAdStep("play",d,()=>d.play()),this.state.callbacks?.onAdStart(t,n,0,1)}catch(e){this.deps.log("shared-element: ad playback failed:",e),this.state.callbacks?.onAdError(t,n,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,a=this.sharedRestoreGeneration;this.detachSharedAdListeners(),this.state.durationTimer&&(clearTimeout(this.state.durationTimer),this.state.durationTimer=null),this.state.isPlaying=!1,this.state.currentBreak=null,this.state.currentAsset=null,t&&s?.onAdEnd(e,t,0,1),a===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:performance.now()+5e3,settle:null};e.attempt=s;const a=await new Promise(t=>{const a=setTimeout(()=>s.settle?.("timeout"),5e3);s.settle=e=>{s.settle&&(s.settle=null,clearTimeout(a),t(e))},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)}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(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";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}}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AdBreakContentRecovery } from '../types/OptiViewAdsConfig';
|
|
2
|
+
/** Concrete recovery strategy (`auto` is always resolved before the controller). */
|
|
3
|
+
export type ResolvedContentRecovery = 'recreate' | 'none';
|
|
4
|
+
/**
|
|
5
|
+
* Panels that cannot resume content in the MediaSource an overlay ad interrupted: the ad's
|
|
6
|
+
* pipeline displaces the content one and a plain seek+play never recovers (VIZIO SmartCast;
|
|
7
|
+
* THEOplayer applies the same workaround on that platform). SmartCast UAs carry "VIZIO"
|
|
8
|
+
* and/or "SmartCast"; either token alone identifies the TV.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isRecreateAfterBreakUserAgent(ua: string): boolean;
|
|
11
|
+
/** Resolve a configured AdBreakContentRecovery to the concrete strategy the controller runs. */
|
|
12
|
+
export declare function resolveContentRecovery(mode: AdBreakContentRecovery, ua?: string): ResolvedContentRecovery;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function isRecreateAfterBreakUserAgent(e){return!!e&&(!!/VIZIO/i.test(e)||!!/SmartCast/i.test(e))}export function resolveContentRecovery(e,t=("undefined"!=typeof navigator?navigator.userAgent:"")){return"recreate"===e?"recreate":"none"===e?"none":isRecreateAfterBreakUserAgent(t)?"recreate":"none"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export function isSharedElementUserAgent(e){return!!e&&/iPhone|iPod/i.test(e)}export function isSingleSurfaceWebDevice(e,
|
|
1
|
+
export function isSharedElementUserAgent(e){return!!e&&/iPhone|iPod/i.test(e)}export function isSingleSurfaceWebDevice(e,i=0){return!!e&&(!/\b(TV|SMART-TV|CrKey|AFT[A-Z]?)\b/i.test(e)&&(!(!/iPad|iPhone|iPod/i.test(e)&&!/Android/i.test(e))||/Macintosh/i.test(e)&&i>1))}export function detectMediaSourceCapabilities(){return{hasNativeMse:"undefined"!=typeof MediaSource,hasManagedMediaSource:"undefined"!=typeof window&&void 0!==window.ManagedMediaSource}}export function resolveInsertionMode(e,i=("undefined"!=typeof navigator?navigator.userAgent:""),t=detectMediaSourceCapabilities()){return"overlay"===e?"overlay":"shared-element"===e?isSharedElementUserAgent(i)?"shared-element":(console.warn('[OptiViewAds] adInsertion "shared-element" is only supported on iPhone/iPod (native HLS); falling back to "overlay" insertion on this platform.'),"overlay"):"adaptive"===e?"adaptive":isSharedElementUserAgent(i)?t.hasNativeMse?"overlay":t.hasManagedMediaSource?"adaptive":"shared-element":"overlay"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const AUX_FETCH_TIMEOUT_MS=5e3;export async function fetchWithTimeout(t,r={},
|
|
1
|
+
import{xhrWithTimeout as e}from"./xhrWithTimeout";export const AUX_FETCH_TIMEOUT_MS=5e3;export async function fetchWithTimeout(t,r={},o=5e3,n=`no response within ${o}ms`){const i=r.signal??void 0;if(i?.aborted){const e=new Error("The operation was aborted.");throw e.name="AbortError",e}if("undefined"==typeof AbortController||"undefined"!=typeof Request&&!("signal"in Request.prototype))return e(t,r,o,n);const a=new AbortController;if("undefined"!=typeof Request)try{new Request("https://example.invalid/",{signal:a.signal})}catch{return e(t,r,o,n)}const s=setTimeout(()=>a.abort(),o),l=()=>a.abort();i?.addEventListener("abort",l);try{return await fetch(t,{...r,signal:a.signal})}catch(e){if(a.signal.aborted&&!i?.aborted)throw new Error(n,{cause:e});throw e}finally{clearTimeout(s),i?.removeEventListener("abort",l)}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
function
|
|
1
|
+
import{objectEntries as e,objectFromEntries as s}from"./runtimeObject";function t(e){return"string"==typeof e&&""!==e.trim()?e:null}export function resolveGamVendorConfiguration(e){const s=e.vendorConfiguration,o={identity:null,unknownTypes:[],duplicateGam:!1,sgaiAssetParameters:void 0,ssai:null};if(!Array.isArray(s))return{...o,reason:"absent"};const r=[];let a=null,i=0;for(const e of s){if("object"!=typeof e||null===e)continue;const s=e,n=t(s.type);null!==n&&("gam"!==n?r.includes(n)||r.push(n):(i+=1,null===a&&(a=s)))}const u=i>1;if(null===a)return{...o,reason:"absent",unknownTypes:r,duplicateGam:u};const l=t(a.networkCode);if(null===l)return{...o,reason:"missing-network-code",unknownTypes:r,duplicateGam:u};const c=Array.isArray(a.ssai)?a.ssai[0]:void 0,y=("object"==typeof c&&null!==c?t(c.assetKey):null)??t(a.assetKey),f=y?{networkCode:l,assetKey:y,assetParameters:n("object"==typeof c&&null!==c?c.assetParameters:void 0)}:null,m=Array.isArray(a.sgai)?a.sgai[0]:void 0,d=("object"==typeof m&&null!==m?t(m.customAssetKey):null)??t(a.customAssetKey);if(null===d)return{...o,reason:"missing-custom-asset-key",unknownTypes:r,duplicateGam:u,ssai:f};return{identity:{networkCode:l,customAssetKey:d},reason:"ok",unknownTypes:r,duplicateGam:u,sgaiAssetParameters:n("object"==typeof m&&null!==m?m.assetParameters:void 0),ssai:f}}function n(t){if("object"!=typeof t||null===t||Array.isArray(t))return;const n=s(e(t).filter(e=>"string"==typeof e[1]));return Object.keys(n).length>0?n:void 0}export function gamPodIdentityMismatches(e,s){const t=[];return s.networkCode!==e.networkCode&&t.push({field:"networkCode",assetValue:s.networkCode,sessionValue:e.networkCode}),s.customAssetKey!==e.customAssetKey&&t.push({field:"customAssetKey",assetValue:s.customAssetKey,sessionValue:e.customAssetKey}),t}export function manifestHasGamAssets(e){for(const s of e.breaks??[]){const e=[...s.variants??[],...s.variant?[s.variant]:[]];for(const s of e)for(const e of s?.assets??[])if("gam"===e?.vendor)return!0}return!1}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AdBreakContentRecovery, AdInsertionMode, AdPreloadMode } from '../types/OptiViewAdsConfig';
|
|
2
|
+
import type { ResolvedContentRecovery } from './detectContentRecovery';
|
|
3
|
+
import type { ResolvedInsertionMode } from './detectInsertionMode';
|
|
4
|
+
import type { ResolvedPreloadMode } from './detectPreloadMode';
|
|
5
|
+
export type DiagnosticEmit = (code: string, message: string, context?: Record<string, unknown>) => void;
|
|
6
|
+
/**
|
|
7
|
+
* Emit the resolved-mode diagnostics so a report states them without inferring from timings:
|
|
8
|
+
* the preload mode, the post-break content recovery, and the shared-element downgrade.
|
|
9
|
+
*/
|
|
10
|
+
export declare function emitResolvedModeDiagnostics(configured: {
|
|
11
|
+
adPreload: AdPreloadMode;
|
|
12
|
+
adInsertion: AdInsertionMode;
|
|
13
|
+
adBreakContentRecovery: AdBreakContentRecovery;
|
|
14
|
+
}, resolved: {
|
|
15
|
+
preloadMode: ResolvedPreloadMode;
|
|
16
|
+
insertionMode: ResolvedInsertionMode;
|
|
17
|
+
contentRecovery: ResolvedContentRecovery;
|
|
18
|
+
}, emit: DiagnosticEmit): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function emitResolvedModeDiagnostics(e,o,r){r("DA-PRELOAD-MODE-RESOLVED",`Ad preload mode: ${o.preloadMode}`+(e.adPreload!==o.preloadMode?` (configured "${e.adPreload}")`:""),{configured:e.adPreload,resolved:o.preloadMode}),r("DA-CONTENT-RECOVERY-MODE-RESOLVED",`Post-break content recovery: ${o.contentRecovery}`+(e.adBreakContentRecovery!==o.contentRecovery?` (configured "${e.adBreakContentRecovery}")`:""),{configured:e.adBreakContentRecovery,resolved:o.contentRecovery}),"shared-element"===e.adInsertion&&"overlay"===o.insertionMode&&r("DA-SHARED-ELEMENT-UNSUPPORTED",'adInsertion "shared-element" is unsupported on this platform (non-iPhone/iPod); fell back to "overlay".',{configured:"shared-element",resolved:o.insertionMode})}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function objectEntries(e){if("function"==typeof Object.entries)return Object.entries(e);const t=[];for(const r of Object.getOwnPropertyNames(e))Object.prototype.propertyIsEnumerable.call(e,r)&&t.push([r,e[r]]);return t}export function objectFromEntries(e){if("function"==typeof Object.fromEntries)return Object.fromEntries(e);const t={};for(const[r,n]of e)Object.defineProperty(t,r,{configurable:!0,enumerable:!0,writable:!0,value:n});return t}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"../types/compatibility-modules";import{createRuntimePreparation as e,installMissingFunction as t}from"./webRuntime";function r(e,r,o,n){return{id:e,load:n,present:()=>"function"==typeof r[o],install:e=>t(r,o,e)}}export function webRuntimeFeatures(){const e=function(){if("undefined"!=typeof globalThis)return globalThis;const e="undefined"!=typeof self?self:window;return Object.defineProperty(e,"globalThis",{configurable:!0,writable:!0,value:e}),e}();return[r("Object.entries",Object,"entries",()=>import("core-js-pure/actual/object/entries.js").then(e=>e.default)),r("Object.values",Object,"values",()=>import("core-js-pure/actual/object/values.js").then(e=>e.default)),r("Object.fromEntries",Object,"fromEntries",()=>import("core-js-pure/actual/object/from-entries.js").then(e=>e.default)),r("Promise.allSettled",Promise,"allSettled",()=>import("core-js-pure/actual/promise/all-settled.js").then(e=>e.default)),r("Promise.finally",Promise.prototype,"finally",()=>import("core-js-pure/actual/promise/finally.js").then(e=>{const t=e.default;return function(e){return t(this,e)}})),r("URLSearchParams",e,"URLSearchParams",()=>import("core-js-pure/actual/url-search-params/index.js").then(e=>e.default)),r("URL",e,"URL",()=>import("core-js-pure/actual/url/index.js").then(e=>e.default)),{id:"AbortController",present:()=>"function"==typeof e.AbortController&&"function"==typeof e.AbortSignal,load:()=>"function"==typeof e.AbortController?Promise.resolve({AbortSignal:Object.getPrototypeOf((new e.AbortController).signal).constructor}):import("abort-controller/dist/abort-controller.js"),install:r=>{const o=r;t(e,"AbortController",o.AbortController),t(e,"AbortSignal",o.AbortSignal)}}]}let o;export function prepareWebRuntime(){const t=webRuntimeFeatures();return t.every(e=>e.present())?null:(o||(o=e(t)),o())}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface RuntimeFeature {
|
|
2
|
+
id: string;
|
|
3
|
+
present: () => boolean;
|
|
4
|
+
load: () => Promise<unknown>;
|
|
5
|
+
install: (implementation: unknown) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function createRuntimePreparation(features: readonly RuntimeFeature[]): () => Promise<void>;
|
|
8
|
+
export declare function installMissingFunction(target: object, key: string, implementation: unknown): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function createRuntimePreparation(e){let t;return()=>{if(!t){let i,r=!0;t=new Promise((t,n)=>{i=setTimeout(()=>{r=!1,n(new Error("SDK compatibility initialization timed out after 15000ms"))},15e3),Promise.resolve().then(()=>Promise.all(e.map(async e=>{if(e.present())return;const t=await e.load();if(r&&(e.present()||e.install(t),!e.present()))throw new Error(`SDK compatibility unavailable: ${e.id}`)}))).then(()=>t(),n)}).then(()=>{r=!1,clearTimeout(i)},e=>{throw r=!1,clearTimeout(i),t=void 0,e})}return t}}export function installMissingFunction(e,t,i){if("function"!=typeof e[t]){if("function"!=typeof i)throw new Error(`Invalid SDK compatibility module: ${t}`);Object.defineProperty(e,t,{configurable:!0,writable:!0,value:i})}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function xhrWithTimeout(url: string, init: RequestInit, timeoutMs: number, timeoutMessage: string): Promise<Response>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function xhrWithTimeout(e,r,t,o){return new Promise((n,s)=>{const a=new XMLHttpRequest,i=r.signal;let d,c=!1;const l=()=>{void 0!==d&&clearTimeout(d),i?.removeEventListener("abort",p),a.onload=a.onerror=a.onabort=null},u=e=>{c||(c=!0,l(),a.abort(),s(e))},p=()=>{const e=new Error("The operation was aborted.");e.name="AbortError",u(e)};try{if(i?.aborted)return void p();if("no-cors"===r.mode)throw new TypeError("Opaque requests require abortable fetch");if("omit"===r.credentials||"same-origin"===r.mode||r.integrity||r.redirect&&"follow"!==r.redirect)throw new TypeError("Request policy requires native abortable fetch");a.open(r.method??"GET",e,!0),a.responseType="arraybuffer",a.withCredentials="include"===r.credentials,new Headers(r.headers??{}).forEach((e,r)=>a.setRequestHeader(r,e)),a.onerror=()=>u(new TypeError("Network request failed")),a.onabort=p,a.onload=()=>{if(!c)try{const e=new Headers;for(const r of a.getAllResponseHeaders().trim().split(/[\r\n]+/)){const t=r.indexOf(":");t>0&&e.append(r.slice(0,t).trim(),r.slice(t+1).trim())}const t="HEAD"===r.method?.toUpperCase()||[204,205,304].includes(a.status),o=new Response(t?null:a.response,{status:a.status,statusText:a.statusText,headers:e});c=!0,l(),n(o)}catch(e){u(e instanceof Error?e:new Error(String(e)))}},i?.addEventListener("abort",p),d=setTimeout(()=>u(new Error(o)),t),a.send(r.body)}catch(e){u(e instanceof Error?e:new Error(String(e)))}})}
|
|
@@ -17,6 +17,9 @@ export declare class AdBreakStatusTracker {
|
|
|
17
17
|
activeBreakStartTimestamp: number | null;
|
|
18
18
|
activeBreakElapsedSec: number;
|
|
19
19
|
currentAdStartElapsedSec: number;
|
|
20
|
+
currentAdAssetId: string | null;
|
|
21
|
+
timelineAssetId: string | null;
|
|
22
|
+
timelineStartElapsedSec: number;
|
|
20
23
|
currentAdIndex: number | null;
|
|
21
24
|
currentAdTotal: number | null;
|
|
22
25
|
currentAdTime: number | null;
|
|
@@ -32,9 +35,9 @@ export declare class AdBreakStatusTracker {
|
|
|
32
35
|
setAdBreakStatusActive(breakInfo: Break, breakRemainingSec: number): void;
|
|
33
36
|
/** Mark the active break visible and playing: the first-frame signal for held pre-rolls, the first `playing` otherwise. */
|
|
34
37
|
setAdBreakVisible(breakInfo: Break): void;
|
|
35
|
-
onAdStarted(breakInfo: Break,
|
|
36
|
-
onAdEnded(breakInfo: Break): void;
|
|
37
|
-
onAdTimeUpdated(breakInfo: Break, currentTime: number, duration: number): void;
|
|
38
|
+
onAdStarted(breakInfo: Break, asset: Asset, index: number, total: number): void;
|
|
39
|
+
onAdEnded(breakInfo: Break, index: number, total: number): void;
|
|
40
|
+
onAdTimeUpdated(breakInfo: Break, asset: Asset, currentTime: number, duration: number): void;
|
|
38
41
|
onAdPlaybackWaiting(): void;
|
|
39
42
|
onAdPlaybackPlaying(): void;
|
|
40
43
|
private buildActiveAdBreakStatus;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export class AdBreakStatusTracker{constructor(t){this.deps=t,this.adBreakStatus={phase:"idle",break:null,ticking:!1},this.activeBreak=null,this.activeBreakRemainingAtStart=null,this.activeBreakStartTimestamp=null,this.activeBreakElapsedSec=0,this.currentAdStartElapsedSec=0,this.currentAdIndex=null,this.currentAdTotal=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakWaiting=!1,this.activeBreakPlaying=!1}getAdBreakStatus(){if("active"===this.adBreakStatus.phase)return{...this.adBreakStatus,breakRemainingSec:this.computeBreakRemainingSec()};if("upcoming"===this.adBreakStatus.phase&&this.activeBreak){const t=this.deps.secondsUntilBreak(this.activeBreak.id);return{...this.adBreakStatus,secondsUntilBreak:t}}return{...this.adBreakStatus}}onBreakWarning(t,e,i){this.activeBreak=t,this.emitAdBreakStatus({phase:"upcoming",break:t,format:this.deps.effectiveFormatOf(t),secondsUntilBreak:i,ticking:!1,warningSec:e})}setAdBreakStatusActive(t,e){this.activeBreak=t,this.activeBreakRemainingAtStart=e,this.activeBreakStartTimestamp=Date.now(),this.activeBreakElapsedSec=0,this.currentAdStartElapsedSec=0,this.currentAdIndex=null,this.currentAdTotal=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakWaiting=!1,this.activeBreakPlaying=!1,this.emitAdBreakStatus({phase:"active",break:t,format:this.deps.effectiveFormatOf(t),breakRemainingSec:this.activeBreakRemainingAtStart,ticking:!1})}setAdBreakVisible(t){this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),ticking:!0})}onAdStarted(t,e,i,a){this.currentAdIndex=i,this.currentAdTotal=a,this.currentAdTime=null,this.currentAdDuration=null,this.currentAdStartElapsedSec=this.currentBreakElapsedSec(),this.activeBreakElapsedSec=this.currentAdStartElapsedSec,this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.deps.breakTransitionTimer()?.settle("into-break","ad-to-ad","break-to-break"),this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),adIndex:i,totalAds:a,adsRemaining:a-i,ticking:!0})}onAdEnded(t){null!==this.currentAdDuration&&Number.isFinite(this.currentAdDuration)?this.activeBreakElapsedSec=this.
|
|
1
|
+
export class AdBreakStatusTracker{constructor(t){this.deps=t,this.adBreakStatus={phase:"idle",break:null,ticking:!1},this.activeBreak=null,this.activeBreakRemainingAtStart=null,this.activeBreakStartTimestamp=null,this.activeBreakElapsedSec=0,this.currentAdStartElapsedSec=0,this.currentAdAssetId=null,this.timelineAssetId=null,this.timelineStartElapsedSec=0,this.currentAdIndex=null,this.currentAdTotal=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakWaiting=!1,this.activeBreakPlaying=!1}getAdBreakStatus(){if("active"===this.adBreakStatus.phase)return{...this.adBreakStatus,breakRemainingSec:this.computeBreakRemainingSec()};if("upcoming"===this.adBreakStatus.phase&&this.activeBreak){const t=this.deps.secondsUntilBreak(this.activeBreak.id);return{...this.adBreakStatus,secondsUntilBreak:t}}return{...this.adBreakStatus}}onBreakWarning(t,e,i){this.activeBreak=t,this.emitAdBreakStatus({phase:"upcoming",break:t,format:this.deps.effectiveFormatOf(t),secondsUntilBreak:i,ticking:!1,warningSec:e})}setAdBreakStatusActive(t,e){this.activeBreak=t,this.activeBreakRemainingAtStart=e,this.activeBreakStartTimestamp=Date.now(),this.activeBreakElapsedSec=0,this.currentAdStartElapsedSec=0,this.currentAdAssetId=null,this.timelineAssetId=null,this.timelineStartElapsedSec=0,this.currentAdIndex=null,this.currentAdTotal=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakWaiting=!1,this.activeBreakPlaying=!1,this.emitAdBreakStatus({phase:"active",break:t,format:this.deps.effectiveFormatOf(t),breakRemainingSec:this.activeBreakRemainingAtStart,ticking:!1})}setAdBreakVisible(t){this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),ticking:!0})}onAdStarted(t,e,i,a){this.currentAdIndex=i,this.currentAdAssetId=e.id,this.currentAdTotal=a,this.currentAdTime=null,this.currentAdDuration=null,this.currentAdStartElapsedSec=this.currentBreakElapsedSec(),this.activeBreakElapsedSec=this.currentAdStartElapsedSec,this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.deps.breakTransitionTimer()?.settle("into-break","ad-to-ad","break-to-break"),this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),adIndex:i,totalAds:a,adsRemaining:a-i,ticking:!0})}onAdEnded(t,e,i){this.timelineAssetId===this.currentAdAssetId&&null!==this.currentAdDuration&&Number.isFinite(this.currentAdDuration)?this.activeBreakElapsedSec=this.timelineStartElapsedSec+this.currentAdDuration:null!==this.currentAdTime&&(this.activeBreakElapsedSec=this.timelineStartElapsedSec+this.currentAdTime),this.currentAdIndex=null,this.currentAdAssetId=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakPlaying=!1,this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),adsRemaining:Math.max(0,i-e-1),breakRemainingSec:this.computeBreakRemainingSec(),ticking:!1})}onAdTimeUpdated(t,e,i,a){e.id!==this.timelineAssetId&&(this.timelineAssetId=e.id,this.timelineStartElapsedSec=this.currentAdStartElapsedSec),this.currentAdTime=i,this.currentAdDuration=a,this.activeBreakElapsedSec=this.timelineStartElapsedSec+i,this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),breakRemainingSec:this.computeBreakRemainingSec(),ticking:!0})}onAdPlaybackWaiting(){this.activeBreakWaiting=!0,this.activeBreakPlaying=!1,this.activeBreak&&this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(this.activeBreak),ticking:!1})}onAdPlaybackPlaying(){this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.activeBreak&&this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(this.activeBreak),ticking:!0})}buildActiveAdBreakStatus(t){return{phase:"active",break:t,format:this.deps.effectiveFormatOf(t),adIndex:this.currentAdIndex??void 0,totalAds:this.currentAdTotal??void 0,adsRemaining:null!==this.currentAdIndex&&null!==this.currentAdTotal?this.currentAdTotal-this.currentAdIndex:void 0,breakRemainingSec:this.computeBreakRemainingSec(),ticking:this.activeBreakPlaying&&!this.activeBreakWaiting}}computeBreakRemainingSec(){if(null!==this.activeBreakRemainingAtStart)return Math.max(0,this.activeBreakRemainingAtStart-this.currentBreakElapsedSec())}currentBreakElapsedSec(){let t=this.activeBreakElapsedSec;return 0===t&&null!==this.activeBreakStartTimestamp&&null===this.currentAdTime&&(t=(Date.now()-this.activeBreakStartTimestamp)/1e3),t}resetAdBreakStatus(){this.activeBreak=null,this.activeBreakRemainingAtStart=null,this.activeBreakStartTimestamp=null,this.activeBreakElapsedSec=0,this.currentAdStartElapsedSec=0,this.currentAdAssetId=null,this.timelineAssetId=null,this.timelineStartElapsedSec=0,this.currentAdIndex=null,this.currentAdTotal=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakWaiting=!1,this.activeBreakPlaying=!1,this.emitAdBreakStatus({phase:"idle",break:null,ticking:!1})}emitAdBreakStatus(t){this.statusesEqual(this.adBreakStatus,t)||(this.adBreakStatus=t,this.deps.emit("adbreakstatus",{type:"adbreakstatus",timestamp:Date.now(),status:t}))}statusesEqual(t,e){return t.phase===e.phase&&t.format===e.format&&t.secondsUntilBreak===e.secondsUntilBreak&&t.breakRemainingSec===e.breakRemainingSec&&t.adIndex===e.adIndex&&t.totalAds===e.totalAds&&t.adsRemaining===e.adsRemaining&&t.ticking===e.ticking&&t.warningSec===e.warningSec&&t.break?.id===e.break?.id}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BreakFormat as e}from"../types/BreakManifest";import{playbackErrorContext as t}from"../diagnostics/playbackErrorContext";export class BreakLifecycle{constructor(e,t){this.state=e,this.deps=t,this.breakBeginEmittedId=null,this.pendingAdBreakBegin=null}emitAdBreakBegin(e,t){this.breakBeginEmittedId!==e.id&&(this.breakBeginEmittedId=e.id,this.pendingAdBreakBegin=null,this.deps.emit("adbreakbegin",{type:"adbreakbegin",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),elapsedSec:t?.elapsedSec??0,remainingSec:t?.remainingSec??e.duration??0,...t?.tuneIn?{tuneIn:{elapsedSec:t.elapsedSec,remainingSec:t.remainingSec}}:{}}))}flushPendingAdBreakBegin(){this.pendingAdBreakBegin?.()}async onBreakStart(a,i){if(this.state.snapbackSeekInFlight=!1,this.deps.adPlayerController.isAdPlaying())return this.deps.diagnose("DA-BREAK-SUPPRESSED-OVERLAP",`Break suppressed (ad already playing): ${a.id}`,{breakId:a.id}),this.deps.breakScheduler.completeBreak(a.id),void this.deps.onBreakSettled?.();this.deps.adPlayerController.diagnoseUnknownVariantFormats(a);const d=this.deps.adPlayerController.breakNothingToPlayReason(a);if(d)return this.deps.log("Break not begun (no playable variant):",a.id,d),this.deps.diagnose("DA-BREAK-NO-PLAYABLE-ASSET",`break ${a.id} has no playable variant (${d}); it will not begin.`,{breakId:a.id,reason:d}),this.deps.emit("aderror",{type:"aderror",timestamp:Date.now(),source:"ad",break:a,format:this.deps.effectiveFormatOf(a),error:new Error(`Break ${a.id} has no playable variant (${d})`)}),this.deps.breakScheduler.completeBreak(a.id),void this.deps.onBreakSettled?.();this.deps.log("Break starting:",a.id,i.tuneIn?"(tune-in)":""),this.deps.breakTransitionTimer?.mark("into-break",a.id),this.breakBeginEmittedId=null,this.pendingAdBreakBegin=null;const r=this.state.heldImmediatePreRollId===a.id,s=this.deps.effectiveFormatOf(a),n=s===e.SINGLE||s===e.LSHAPE_AD,o=r&&n;r&&(this.state.heldImmediatePreRollId=null),this.deps.adBreakStatusTracker.setAdBreakStatusActive(a,void 0!==i.remainingSec?i.remainingSec:a.duration),o||this.emitAdBreakBegin(a,i),this.deps.manifestService.setActive(!0),this.deps.breakScheduler.claimChain(a.id);const k={onAdStart:(e,t,a,i,d)=>{this.deps.adBreakStatusTracker.onAdStarted(e,t,a,i),this.deps.emit("adbegin",{type:"adbegin",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,adIndex:a,totalAds:i,...d?.adId?{adId:d.adId}:{},...d?.creativeId?{creativeId:d.creativeId}:{},...void 0!==d?.podPosition?{podPosition:d.podPosition}:{}})},onAdEnd:(e,t,a,i,d)=>{this.deps.adBreakStatusTracker.onAdEnded(e),a+1<i&&this.deps.breakTransitionTimer?.mark("ad-to-ad",e.id),this.deps.emit("adend",{type:"adend",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,adIndex:a,totalAds:i,...d?.adId?{adId:d.adId}:{},...d?.creativeId?{creativeId:d.creativeId}:{}})},onAdError:(e,a,i)=>{this.flushPendingAdBreakBegin(),this.deps.diagnose("DA-AD-PLAYBACK-ERROR",i.message,{breakId:e.id,assetId:a?.id,...t(i)}),this.deps.emit("aderror",{type:"aderror",timestamp:Date.now(),source:"ad",break:e,format:this.deps.effectiveFormatOf(e),...a?{asset:a}:{},error:i})},onAdTimeUpdate:(e,t,a,i)=>{this.deps.adBreakStatusTracker.onAdTimeUpdated(e,a,i),this.deps.emit("adtimeupdate",{type:"adtimeupdate",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,currentTime:a,duration:i})},onAdWaiting:(e,t)=>{this.deps.adBreakStatusTracker.onAdPlaybackWaiting(),this.deps.emit("waiting",{type:"waiting",timestamp:Date.now(),source:"ad",break:e,asset:t})},onAdPlaying:(e,t)=>{this.deps.adBreakStatusTracker.onAdPlaybackPlaying(),this.deps.emit("playing",{type:"playing",timestamp:Date.now(),source:"ad",break:e,asset:t})},onBreakEnd:e=>{this.onBreakEnd(e)},onBreakCut:e=>{this.deps.cmcdAdReporter.onBreakCut(e.id)},onBreakEndedWithoutContentPause:()=>{this.onBreakEndedWithoutContentPause()},onChainedBreakBegin:e=>{this.onChainedBreakBegin(e)},onChainedBreakSkipped:e=>{this.deps.log("Chained break skipped (no playable variant):",e.id),this.deps.breakScheduler.completeBreak(e.id)},onContentResumeSeek:(e,t)=>{this.deps.breakScheduler.notifyResumeSeek(e,t)},onContentReloading:e=>{this.deps.breakScheduler.setContentReloading(e)}};o&&(this.pendingAdBreakBegin=()=>this.emitAdBreakBegin(a,i),k.onBreakVisible=e=>{this.deps.adBreakStatusTracker.setAdBreakVisible(e),this.flushPendingAdBreakBegin()}),await this.deps.adPlayerController.playBreak(a,k,{effectiveDurationSec:i.remainingSec,firstFrameGate:o})}onChainedBreakBegin(e){this.deps.log("Chained break starting:",e.id),this.deps.breakTransitionTimer?.mark("break-to-break",e.id),this.deps.breakScheduler.activateChainedBreak(e.id),this.deps.manifestService.setActive(!0),this.deps.adBreakStatusTracker.setAdBreakStatusActive(e,e.duration),this.emitAdBreakBegin(e)}onBreakEnd(e){this.deps.log("Break ended:",e.id),this.deps.breakTransitionTimer?.mark("out-of-break",e.id),this.deps.breakScheduler.completeBreak(e.id),this.deps.manifestService.setActive(!1),this.deps.adBreakStatusTracker.resetAdBreakStatus(),this.flushPendingAdBreakBegin(),this.deps.emit("adbreakend",{type:"adbreakend",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e)}),this.deps.onBreakSettled?.()}onBreakEndedWithoutContentPause(){this.deps.breakTransitionTimer?.settle("out-of-break"),this.deps.emit("playing",{type:"playing",timestamp:Date.now(),source:"content"})}onBreakAborted(e){if(this.deps.adPlayerController.isAdPlaying()){const t=this.deps.adBreakStatusTracker.currentBreakElapsedSec();this.deps.log(`Early return — ending break '${e.id}' at ${t.toFixed(2)}s`),this.deps.cmcdAdReporter.onBreakAborted(e.id),this.deps.adPlayerController.abortBreak(t)}else this.deps.breakScheduler.completeBreak(e.id),this.deps.onBreakSettled?.()}reportAbandonedBreak(e){const t=this.deps.adBreakStatusTracker.activeBreak;if(!t)return;const a=this.deps.adBreakStatusTracker.currentAdIndex;this.deps.diagnose("DA-BREAK-ABANDONED",`Session torn down (${e}) while break ${t.id} was still in flight`+(null!==a?"; an ad began and will never end.":"."),{breakId:t.id,cause:e,phase:this.deps.adBreakStatusTracker.adBreakStatus.phase,unbalancedAdBegin:null!==a,...null!==a?{adIndex:a}:{},...null!==this.deps.adBreakStatusTracker.currentAdTotal?{totalAds:this.deps.adBreakStatusTracker.currentAdTotal}:{}})}}
|
|
1
|
+
import{BreakFormat as e}from"../types/BreakManifest";import{playbackErrorContext as t}from"../diagnostics/playbackErrorContext";export class BreakLifecycle{constructor(e,t){this.state=e,this.deps=t,this.breakBeginEmittedId=null,this.pendingAdBreakBegin=null}emitAdBreakBegin(e,t){this.breakBeginEmittedId!==e.id&&(this.breakBeginEmittedId=e.id,this.pendingAdBreakBegin=null,this.deps.emit("adbreakbegin",{type:"adbreakbegin",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),elapsedSec:t?.elapsedSec??0,remainingSec:t?.remainingSec??e.duration??0,...t?.tuneIn?{tuneIn:{elapsedSec:t.elapsedSec,remainingSec:t.remainingSec}}:{}}))}flushPendingAdBreakBegin(){this.pendingAdBreakBegin?.()}async onBreakStart(a,i){if(this.state.snapbackSeekInFlight=!1,this.deps.adPlayerController.isAdPlaying())return this.deps.diagnose("DA-BREAK-SUPPRESSED-OVERLAP",`Break suppressed (ad already playing): ${a.id}`,{breakId:a.id}),this.deps.breakScheduler.completeBreak(a.id),void this.deps.onBreakSettled?.();this.deps.adPlayerController.diagnoseUnknownVariantFormats(a);const d=this.deps.adPlayerController.breakNothingToPlayReason(a);if(d)return this.deps.log("Break not begun (no playable variant):",a.id,d),this.deps.diagnose("DA-BREAK-NO-PLAYABLE-ASSET",`break ${a.id} has no playable variant (${d}); it will not begin.`,{breakId:a.id,reason:d}),this.deps.emit("aderror",{type:"aderror",timestamp:Date.now(),source:"ad",break:a,format:this.deps.effectiveFormatOf(a),error:new Error(`Break ${a.id} has no playable variant (${d})`)}),this.deps.breakScheduler.completeBreak(a.id),void this.deps.onBreakSettled?.();this.deps.log("Break starting:",a.id,i.tuneIn?"(tune-in)":""),this.deps.breakTransitionTimer?.mark("into-break",a.id),this.breakBeginEmittedId=null,this.pendingAdBreakBegin=null;const r=this.state.heldImmediatePreRollId===a.id,s=this.deps.effectiveFormatOf(a),n=s===e.SINGLE||s===e.LSHAPE_AD,o=r&&n;r&&(this.state.heldImmediatePreRollId=null),this.deps.adBreakStatusTracker.setAdBreakStatusActive(a,void 0!==i.remainingSec?i.remainingSec:a.duration),o||this.emitAdBreakBegin(a,i),this.deps.manifestService.setActive(!0),this.deps.breakScheduler.claimChain(a.id);const k={onAdStart:(e,t,a,i,d)=>{this.deps.adBreakStatusTracker.onAdStarted(e,t,a,i),this.deps.emit("adbegin",{type:"adbegin",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,adIndex:a,totalAds:i,...d?.adId?{adId:d.adId}:{},...d?.creativeId?{creativeId:d.creativeId}:{},...void 0!==d?.podPosition?{podPosition:d.podPosition}:{}})},onAdEnd:(e,t,a,i,d)=>{this.deps.adBreakStatusTracker.onAdEnded(e,a,i),a+1<i&&this.deps.breakTransitionTimer?.mark("ad-to-ad",e.id),this.deps.emit("adend",{type:"adend",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,adIndex:a,totalAds:i,...d?.adId?{adId:d.adId}:{},...d?.creativeId?{creativeId:d.creativeId}:{}})},onAdError:(e,a,i)=>{this.flushPendingAdBreakBegin(),this.deps.diagnose("DA-AD-PLAYBACK-ERROR",i.message,{breakId:e.id,assetId:a?.id,...t(i)}),this.deps.emit("aderror",{type:"aderror",timestamp:Date.now(),source:"ad",break:e,format:this.deps.effectiveFormatOf(e),...a?{asset:a}:{},error:i})},onAdTimeUpdate:(e,t,a,i)=>{this.deps.adBreakStatusTracker.onAdTimeUpdated(e,t,a,i),this.deps.emit("adtimeupdate",{type:"adtimeupdate",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,currentTime:a,duration:i})},onAdWaiting:(e,t)=>{this.deps.adBreakStatusTracker.onAdPlaybackWaiting(),this.deps.emit("waiting",{type:"waiting",timestamp:Date.now(),source:"ad",break:e,asset:t})},onAdPlaying:(e,t)=>{this.deps.adBreakStatusTracker.onAdPlaybackPlaying(),this.deps.emit("playing",{type:"playing",timestamp:Date.now(),source:"ad",break:e,asset:t})},onBreakEnd:e=>{this.onBreakEnd(e)},onBreakCut:e=>{this.deps.cmcdAdReporter.onBreakCut(e.id)},onBreakEndedWithoutContentPause:()=>{this.onBreakEndedWithoutContentPause()},onChainedBreakBegin:e=>{this.onChainedBreakBegin(e)},onChainedBreakSkipped:e=>{this.deps.log("Chained break skipped (no playable variant):",e.id),this.deps.breakScheduler.completeBreak(e.id)},onContentResumeSeek:(e,t)=>{this.deps.breakScheduler.notifyResumeSeek(e,t)},onContentReloading:e=>{this.deps.breakScheduler.setContentReloading(e)}};o&&(this.pendingAdBreakBegin=()=>this.emitAdBreakBegin(a,i),k.onBreakVisible=e=>{this.deps.adBreakStatusTracker.setAdBreakVisible(e),this.flushPendingAdBreakBegin()}),await this.deps.adPlayerController.playBreak(a,k,{effectiveDurationSec:i.remainingSec,firstFrameGate:o})}onChainedBreakBegin(e){this.deps.log("Chained break starting:",e.id),this.deps.breakTransitionTimer?.mark("break-to-break",e.id),this.deps.breakScheduler.activateChainedBreak(e.id),this.deps.manifestService.setActive(!0),this.deps.adBreakStatusTracker.setAdBreakStatusActive(e,e.duration),this.emitAdBreakBegin(e)}onBreakEnd(e){this.deps.log("Break ended:",e.id),this.deps.breakTransitionTimer?.mark("out-of-break",e.id),this.deps.breakScheduler.completeBreak(e.id),this.deps.manifestService.setActive(!1),this.deps.adBreakStatusTracker.resetAdBreakStatus(),this.flushPendingAdBreakBegin(),this.deps.emit("adbreakend",{type:"adbreakend",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e)}),this.deps.onBreakSettled?.()}onBreakEndedWithoutContentPause(){this.deps.breakTransitionTimer?.settle("out-of-break"),this.deps.emit("playing",{type:"playing",timestamp:Date.now(),source:"content"})}onBreakAborted(e){if(this.deps.adPlayerController.isAdPlaying()){const t=this.deps.adBreakStatusTracker.currentBreakElapsedSec();this.deps.log(`Early return — ending break '${e.id}' at ${t.toFixed(2)}s`),this.deps.cmcdAdReporter.onBreakAborted(e.id),this.deps.adPlayerController.abortBreak(t)}else this.deps.breakScheduler.completeBreak(e.id),this.deps.onBreakSettled?.()}reportAbandonedBreak(e){const t=this.deps.adBreakStatusTracker.activeBreak;if(!t)return;const a=this.deps.adBreakStatusTracker.currentAdIndex;this.deps.diagnose("DA-BREAK-ABANDONED",`Session torn down (${e}) while break ${t.id} was still in flight`+(null!==a?"; an ad began and will never end.":"."),{breakId:t.id,cause:e,phase:this.deps.adBreakStatusTracker.adBreakStatus.phase,unbalancedAdBegin:null!==a,...null!==a?{adIndex:a}:{},...null!==this.deps.adBreakStatusTracker.currentAdTotal?{totalAds:this.deps.adBreakStatusTracker.currentAdTotal}:{}})}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{getPreRollDelay as e}from"../types/BreakManifest";import{manifestHasGamAssets as s,resolveGamVendorConfiguration as t}from"../services/resolveGamVendorConfiguration";import{SsaiController as i}from"../services/SsaiController";import{resolveUrlOrigin as a}from"../services/urlOrigin";import{filterSsaiClientBreaks as r,resolveStitchedStreamVendor as n,SsaiUnavailableError as o}from"../services/resolveDeliveryMode";import{detectDeviceType as d}from"../services/detectDeviceType";import{decideDeliveryMode as l,detectChannelSwitch as h,gamPodIdentityChanged as c}from"../services/channelSwitch";const m=1e4;function p(e){return"stitcherUrl"in e}function S(e){return"string"==typeof e&&""!==e.trim()}export class SessionStarter{constructor(e,s){this.state=e,this.deps=s}async startSession(s){if("ssai"===this.deps.mode){if(!p(s)||!S(s.stitcherUrl))throw new Error('[OptiViewAds] mode "ssai" requires a stitcherUrl in startSession().')}else if(p(s)||!S(s.manifestUrl))throw new Error("[OptiViewAds] startSession requires a manifestUrl (the full break-manifest URL).");this.state.sessionActive&&(this.deps.log("Ending previous session before starting new one"),this.deps.endSessionInternal("session-restart"));const t=this.state.sessionGeneration;if(this.state.sessionActive=!0,this.deps.lensReporter.startSession(),p(s))return s.adInsertionType&&this.deps.diagnose("DA-INSERTION-TYPE-IGNORED-SSAI",`adInsertionType "${s.adInsertionType}" ignored in SSAI mode; the stitcher controls ad insertion.`,{adInsertionType:s.adInsertionType}),void await this.startSsaiSession(s,t);const i=s.manifestUrl;this.state.sessionConfig=s,this.state.manifestOrigin=a(i),this.deps.manifestService.setManifestUrl(i),this.deps.log("Session starting, manifest URL:",i);const r=Date.now(),n=e=>{this.deps.diagnose("DA-SESSION-STARTING",`Session start: ${e}`,{step:e,manifestUrl:i,elapsedMs:Date.now()-r})};n("begin");const o=s.assetParameters;let d;this.deps.adPlayerController.setSessionAssetParameters(o),this.deps.adPlayerController.setSessionAssetParameterMacros(s.assetParameterMacros),this.state.configSummary.gam&&(this.state.configSummary.gam={...this.state.configSummary.gam,hasAssetParameters:!!o}),n("manifest-fetch");try{d=await this.deps.manifestService.fetchManifest()}catch(e){if(this.isStaleStart(t))return;throw this.failSessionStart(e)}if(this.deps.cmcdAdReporter.onSessionStart(d),!this.isStaleStart(t))if(this.state.sessionManifest=d,"ssai"!==this.resolveSessionDeliveryMode(d))try{if(await this.startGamForManifest(d,o,n),this.isStaleStart(t))return;const a=s.adInsertionType;this.deps.adPlayerController.setBreakResumePolicy(a,d.timebase,this.deps.continueContentDuringBreak),this.deps.diagnose("DA-INSERTION-TYPE-RESOLVED",void 0===a?`Ad insertion type: unset — the manifest's resumeOffset drives the resume point (timebase: ${d.timebase})`:`Ad insertion type: ${a}, client-set (timebase: ${d.timebase})`,{adInsertionType:a??null,source:void 0===a?"unset":"client",timebase:d.timebase}),n("scheduling"),this.deps.breakScheduler.updateManifest(d),this.deps.pauseAdController.updateManifest(d),this.state.pauseAdTickInterval=setInterval(()=>this.deps.pauseAdController?.tick(),250);const r=(d.adStartDelay??0)>0?void 0:d.breaks.find(s=>0===e(s));r?(this.deps.adPlayerController.holdContentForPreRoll(r),this.state.heldImmediatePreRollId=r.id,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=Date.now(),this.contentIsReady()?this.armHeldImmediatePreRoll():this.state.heldPreRollArmInterval=setInterval(()=>{this.contentIsReady()?this.armHeldImmediatePreRoll():this.armHeldImmediatePreRollIfTimedOut()},250)):this.markPlaybackStartIfPlaying(),this.startBreakScheduler(),this.startManifestPolling(),this.deps.diagnose("DA-SESSION-STARTED",`Session active for manifest ${i}`,{manifestUrl:i}),this.deps.log("Session active")}catch(e){if(this.isStaleStart(t))return;throw this.failSessionStart(e)}else await this.startSteeredSsaiSession(d,s,n)}markPlaybackStartIfPlaying(){this.deps.player.paused||this.deps.breakScheduler.markPlaybackStart()}contentIsReady(){return!Number.isNaN(this.deps.player.duration)||!this.deps.player.paused}clearHeldImmediatePreRollArmInterval(){this.state.heldPreRollArmInterval&&(clearInterval(this.state.heldPreRollArmInterval),this.state.heldPreRollArmInterval=null)}armHeldImmediatePreRoll(){this.state.heldImmediatePreRollId&&!this.state.heldPreRollArmed&&(this.clearHeldImmediatePreRollArmInterval(),this.state.heldPreRollArmed=!0,this.deps.breakScheduler.markPlaybackStart())}armHeldImmediatePreRollIfTimedOut(){if(!this.state.heldImmediatePreRollId||this.state.heldPreRollArmed)return;const e=this.state.heldPreRollWaitStartedAt;if(null===e||Date.now()-e<m)return;const s=this.state.heldImmediatePreRollId;this.armHeldImmediatePreRoll(),this.deps.diagnose("DA-PREROLL-CONTENT-WAIT-TIMEOUT","Pending pre-roll began after waiting 10000ms for content readiness.",{breakId:s,waitMs:m})}cancelHeldImmediatePreRollOnContentError(){const e=this.state.heldImmediatePreRollId;return!(!e||this.state.heldPreRollArmed)&&(this.clearHeldImmediatePreRollArmInterval(),this.deps.breakScheduler?.completeBreak(e),this.deps.adPlayerController?.releaseContentHold(),this.state.heldImmediatePreRollId=null,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=null,this.deps.diagnose("DA-PREROLL-CANCELLED-CONTENT-ERROR","A pending pre-roll was cancelled because the content failed to load before it could begin.",{breakId:e}),!0)}clearHeldImmediatePreRollArm(){this.clearHeldImmediatePreRollArmInterval(),this.state.heldImmediatePreRollId=null,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=null}failSessionStart(e){const s=e instanceof Error?e:new Error(String(e)),t=this.deps.manifestService?.takeLastFailure();return this.deps.diagnose("DA-MANIFEST-FETCH-FAILED",s.message,t?{...t}:void 0),this.state.sessionActive=!1,this.deps.cmcdAdReporter.onSessionEnd(),this.deps.lensReporter.endSession(),s}resolveSessionDeliveryMode(e,s="manifest"){const t=this.state.sessionDeliveryMode,i=l(e,d(),this.deps.hasExplicitMode?this.deps.mode:null,s);return null!==i.unknownMode&&this.deps.diagnose("DA-DELIVERY-MODE-UNKNOWN",`Delivery rule ${i.ruleIndex} carries unrecognized mode "${i.unknownMode}"; using sgai.`,{unknownMode:i.unknownMode,ruleIndex:i.ruleIndex}),this.deps.hasExplicitMode&&i.manifestMode!==i.mode&&this.deps.diagnose("DA-DELIVERY-MODE-OVERRIDDEN",`Manifest delivery rules resolve "${i.manifestMode}" but the explicit config mode "${i.mode}" takes precedence.`,{manifestMode:i.manifestMode,configuredMode:i.mode}),this.state.sessionDeliveryMode=i.mode,this.deps.diagnose("DA-DELIVERY-MODE-RESOLVED",`Delivery mode: ${i.mode}`,{mode:i.mode,ruleIndex:i.ruleIndex,source:i.source,..."channel-switch"===s?{previousMode:t}:{}}),i.mode}async startSteeredSsaiSession(e,s,t){const i=n(e.vendorConfiguration);if(!i){const e="Manifest selected ssai delivery but no vendor configuration provides a stitched stream (no gam entry with an assetKey).";throw this.deps.diagnose("DA-SSAI-UNAVAILABLE",e),this.deps.manifestService.reset(),this.state.sessionActive=!1,new o(e)}try{t("ssai-live-stream"),this.state.steeredSsaiActive=!0,this.state.ssaiController=this.createSsaiController(i.networkCode),await this.state.ssaiController.startSteered(i,s.assetParameters,s.assetParameterMacros),t("scheduling");const a=r(e);this.deps.adPlayerController.setBreakResumePolicy(s.adInsertionType,e.timebase,!1),this.deps.breakScheduler.updateManifest(a),this.deps.pauseAdController.updateManifest(a),this.state.pauseAdTickInterval=setInterval(()=>this.deps.pauseAdController?.tick(),250),this.markPlaybackStartIfPlaying(),this.startBreakScheduler(),this.startManifestPolling(),this.deps.diagnose("DA-SESSION-STARTED",`SSAI session active (manifest-steered) for ${s.manifestUrl}`,{manifestUrl:s.manifestUrl}),this.deps.log("SSAI session active (manifest-steered)")}catch(e){const s=e instanceof Error?e:new Error(String(e));throw this.deps.diagnose("DA-SSAI-SESSION-FAILED",s.message),this.state.steeredSsaiActive=!1,this.deps.manifestService.reset(),this.state.sessionActive=!1,s}}createSsaiController(e){return new i(this.deps.player,{gam:this.state.steeredSsaiOptions?.streamActivityMonitorId?{streamActivityMonitorId:this.state.steeredSsaiOptions.streamActivityMonitorId}:{},ssai:{networkCode:e},maxBitrate:this.state.steeredSsaiOptions?.maxBitrate,autoplay:this.state.steeredSsaiOptions?.autoplay,debug:this.deps.debug},{emit:(e,s)=>this.deps.emit(e,s),diagnose:(e,s,t)=>this.deps.diagnose(e,s,t),log:(...e)=>this.deps.log(...e)})}startBreakScheduler(){this.deps.breakScheduler.start((e,s)=>{this.deps.breakLifecycle.onBreakStart(e,s)},e=>!this.deps.adPlayerController.isAdPlaying()&&(this.deps.adPlayerController.preload(e).catch(s=>{this.deps.diagnose("DA-PRELOAD-FAILED",s instanceof Error?s.message:String(s),{breakId:e.id})}),!0),(e,s,t)=>{this.deps.adBreakStatusTracker.onBreakWarning(e,s,t)},e=>{this.deps.breakLifecycle.onBreakAborted(e)})}startManifestPolling(){this.deps.manifestService.startPolling(e=>{if(!this.state.sessionActive)return;const s=this.deps.notedManifestBreakIds(e);this.deps.diagnose("DA-MANIFEST-POLLED","Manifest polled",{breaks:e.breaks.length,newBreakIds:s,pollingIntervalSeconds:this.deps.manifestService.currentPollingIntervalSeconds});const t=this.state.sessionManifest,i=t?h(t,e):null;if(this.state.pendingChannelSwitch||i&&this.deps.breakScheduler.getActiveBreak())return this.state.pendingChannelSwitch=e,void this.deps.log("Channel switch deferred until the active break ends");i?this.switchChannel(e,i):this.applyManifestUpdate(e)},e=>{this.state.sessionActive&&this.deps.diagnose("DA-MANIFEST-POLL-FAILED",e.message)})}applyPendingChannelSwitch(){if(!this.state.sessionActive||!this.state.pendingChannelSwitch)return;const e=this.state.pendingChannelSwitch;this.state.pendingChannelSwitch=null;const s=h(this.state.sessionManifest,e);s?this.switchChannel(e,s):this.applyManifestUpdate(e)}applyManifestUpdate(e){this.diagnoseIgnoredDeliveryModeChange(e);const s=t(e);this.state.steeredSsaiActive?this.state.ssaiController?.setManifestAssetParameters(s.ssai?.assetParameters):this.deps.adPlayerController.setManifestAssetParameters(s.sgaiAssetParameters);const i=this.state.steeredSsaiActive?r(e):e;this.deps.breakScheduler.updateManifest(i),this.deps.pauseAdController.updateManifest(i)}diagnoseIgnoredDeliveryModeChange(e){if(!this.state.sessionDeliveryMode)return;const s=l(e,d(),null,"manifest");s.manifestMode!==this.state.sessionDeliveryMode&&this.deps.diagnose("DA-DELIVERY-MODE-CHANGE-IGNORED",`Polled manifest resolves delivery mode "${s.manifestMode}" but "${this.state.sessionDeliveryMode}" is pinned for this session.`,{polledMode:s.manifestMode,pinnedMode:this.state.sessionDeliveryMode})}async switchChannel(e,s){if(this.state.channelSwitchInFlight)return;const t=this.state.sessionConfig,i=this.state.sessionManifest,a={previousChannelId:s.previousChannelId,channelId:s.channelId,reason:s.reason,manifestUrl:t.manifestUrl};this.deps.diagnose("DA-MANIFEST-CHANNEL-CHANGED",`Manifest channel changed (${s.reason}): ${s.previousChannelId??"none"} -> ${s.channelId??"none"}`,a),this.state.sessionManifest=e,this.state.channelSwitchInFlight=!0;const r=this.state.sessionGeneration,n=this.state.sessionDeliveryMode??"sgai";try{let s=this.resolveSessionDeliveryMode(e,"channel-switch");if(this.deps.cmcdAdReporter.onSessionEnd(),this.deps.cmcdAdReporter.onSessionStart(e),this.deps.lensReporter.endSession(),this.deps.lensReporter.startSession(),"ssai"===s&&"sgai"===n){const a=await this.switchToSteeredSsai(e,t);if(this.isStaleStart(r))return;a||(s="sgai",this.state.sessionDeliveryMode="sgai",await this.applySameModeChannelSwitch(i,e,t,r))}else"sgai"===s&&"ssai"===n?await this.switchToSgai(e,t,r):await this.applySameModeChannelSwitch(i,e,t,r);if(this.isStaleStart(r))return}finally{this.state.channelSwitchInFlight=!1}this.emitChannelChange({...a,previousDeliveryMode:n,deliveryMode:this.state.sessionDeliveryMode??n})}emitChannelChange(e){this.deps.emit("adchannelchange",{type:"adchannelchange",timestamp:Date.now(),...e})}async applySameModeChannelSwitch(e,s,i,a){const n=this.state.steeredSsaiActive,o=n?r(s):s;if(this.deps.breakScheduler.resetForChannelSwitch(o),this.deps.pauseAdController.updateManifest(o),this.deps.adPlayerController.setBreakResumePolicy(i.adInsertionType,s.timebase,this.deps.continueContentDuringBreak),!n&&c(e,s)){const e=this.deps.adPlayerController.updatedAssetParameters,t=this.deps.adPlayerController.sessionAssetParameterMacros;if(this.deps.adPlayerController.endGamSession(),this.deps.adPlayerController.setSessionAssetParameters(i.assetParameters),this.deps.adPlayerController.setSessionAssetParameterMacros(t),await this.startGamForManifest(s,i.assetParameters,e=>{this.deps.diagnose("DA-SESSION-STARTING",`Channel switch: ${e}`,{step:e,manifestUrl:i.manifestUrl})}),this.isStaleStart(a))return;e&&this.deps.adPlayerController.updateAssetParameters(e)}else if(n){const e=t(s);this.state.ssaiController?.setManifestAssetParameters(e.ssai?.assetParameters)}else{const e=t(s);this.deps.adPlayerController.setManifestAssetParameters(e.sgaiAssetParameters)}}async switchToSteeredSsai(e,s){const t=n(e.vendorConfiguration);if(!t)return this.deps.diagnose("DA-SSAI-UNAVAILABLE","Channel switch resolved ssai delivery but no vendor configuration provides a stitched stream; the session stays in sgai.",{manifestUrl:s.manifestUrl,previousMode:"sgai"}),!1;const i=this.deps.adPlayerController,a=i.updatedAssetParameters,o=i.sessionAssetParameterMacros;i.stopActiveBreak(),i.releaseContentHold(),i.endGamSession(),i.setSessionAssetParameters(s.assetParameters),i.setSessionAssetParameterMacros(o),a&&i.updateAssetParameters(a);const d=this.createSsaiController(t.networkCode);this.state.ssaiController=d,this.state.steeredSsaiActive=!0,this.deps.diagnose("DA-SESSION-STARTING","Channel switch: ssai-live-stream",{step:"ssai-live-stream",manifestUrl:s.manifestUrl});try{await d.startSteered(t,s.assetParameters,o),a&&d.updateAssetParameters(a);const n=r(e);return this.deps.breakScheduler.resetForChannelSwitch(n),this.deps.pauseAdController.updateManifest(n),i.setBreakResumePolicy(s.adInsertionType,e.timebase,!1),!0}catch(e){const s=e instanceof Error?e.message:String(e);return this.deps.diagnose("DA-SSAI-SESSION-FAILED",s),d.stop(),delete this.state.ssaiController,this.state.steeredSsaiActive=!1,!1}}async switchToSgai(e,s,t){const i=this.state.ssaiController?.sessionAssetParameterMacros;this.state.ssaiController?.stop(),delete this.state.ssaiController,this.state.steeredSsaiActive=!1,this.deps.breakScheduler.resetForChannelSwitch(e),this.deps.pauseAdController.updateManifest(e);const a=this.deps.adPlayerController;a.setBreakResumePolicy(s.adInsertionType,e.timebase,this.deps.continueContentDuringBreak);const r=a.updatedAssetParameters;a.endGamSession(),a.setSessionAssetParameters(s.assetParameters),a.setSessionAssetParameterMacros(i??s.assetParameterMacros),await this.startGamForManifest(e,s.assetParameters,e=>{this.deps.diagnose("DA-SESSION-STARTING",`Channel switch: ${e}`,{step:e,manifestUrl:s.manifestUrl})}),this.isStaleStart(t)||r&&a.updateAssetParameters(r)}async startGamForManifest(e,i,a){const r=t(e);if(this.deps.adPlayerController.setManifestAssetParameters(r.sgaiAssetParameters),r.unknownTypes.length>0&&this.deps.diagnose("DA-VENDOR-CONFIG-UNKNOWN-TYPE",`Ignored ${r.unknownTypes.length} vendorConfiguration `+(1===r.unknownTypes.length?"entry":"entries")+" with unrecognised type: "+`${r.unknownTypes.join(", ")}.`,{types:r.unknownTypes}),this.deps.adPlayerController.isGamEnabled())if(r.identity){this.deps.diagnose("DA-GAM-CONFIG-RESOLVED",`GAM session identity: network ${r.identity.networkCode}, custom asset ${r.identity.customAssetKey}`,{networkCode:r.identity.networkCode,customAssetKey:r.identity.customAssetKey,channelId:e.channelId,duplicateEntriesIgnored:r.duplicateGam});try{a("gam-init"),await this.deps.adPlayerController.startGamSession(r.identity,i),this.deps.log("GAM session started")}catch(e){this.deps.diagnose("DA-GAM-SESSION-FAILED",e instanceof Error?e.message:String(e))}}else s(e)&&this.deps.diagnose("DA-GAM-CONFIG-MISSING",'The break manifest carries GAM pod assets but no usable "gam" vendorConfiguration entry; those assets will be skipped.',{reason:r.reason});else s(e)&&this.deps.diagnose("DA-GAM-CONFIG-MISSING","The break manifest carries GAM pod assets but GAM is disabled for this session (gam.enabled === false); those assets will be skipped.",{reason:"gam-disabled"})}async startSsaiSession(e,s){try{if(this.state.stitcherOrigin=a(e.stitcherUrl),this.state.configSummary.gam&&(this.state.configSummary.gam={...this.state.configSummary.gam,hasAssetParameters:!!e.assetParameters}),await this.state.ssaiController.start(e),this.isStaleStart(s))return;this.deps.diagnose("DA-SESSION-STARTED",`SSAI session active for ${e.stitcherUrl}`,{stitcherUrl:e.stitcherUrl}),this.deps.log("SSAI session active")}catch(e){if(this.isStaleStart(s))return;const t=e instanceof Error?e:new Error(String(e));throw this.deps.diagnose("DA-SSAI-SESSION-FAILED",t.message),this.state.sessionActive=!1,this.deps.cmcdAdReporter.onSessionEnd(),this.deps.lensReporter.endSession(),t}}isStaleStart(e){return e!==this.state.sessionGeneration&&(this.deps.log("Session start aborted — the session ended or was replaced while starting"),!0)}}
|
|
1
|
+
import{getPreRollDelay as e}from"../types/BreakManifest";import{manifestHasGamAssets as s,resolveGamVendorConfiguration as t}from"../services/resolveGamVendorConfiguration";import{SsaiController as i}from"../services/SsaiController";import{resolveUrlOrigin as a}from"../services/urlOrigin";import{filterSsaiClientBreaks as r,resolveStitchedStreamVendor as n,SsaiUnavailableError as o}from"../services/resolveDeliveryMode";import{detectDeviceType as d}from"../services/detectDeviceType";import{decideDeliveryMode as l,detectChannelSwitch as h,gamPodIdentityChanged as c}from"../services/channelSwitch";import{prepareWebRuntime as m}from"../services/webCompatibility";const p=1e4;function S(e){return"stitcherUrl"in e}function u(e){return"string"==typeof e&&""!==e.trim()}export class SessionStarter{constructor(e,s){this.state=e,this.deps=s}async startSession(s){if("ssai"===this.deps.mode){if(!S(s)||!u(s.stitcherUrl))throw new Error('[OptiViewAds] mode "ssai" requires a stitcherUrl in startSession().')}else if(S(s)||!u(s.manifestUrl))throw new Error("[OptiViewAds] startSession requires a manifestUrl (the full break-manifest URL).");this.state.sessionActive&&(this.deps.log("Ending previous session before starting new one"),this.deps.endSessionInternal("session-restart"));const t=this.state.sessionGeneration;this.state.sessionActive=!0;try{const e=m();e&&await e}catch(e){if(this.isStaleStart(t))return;throw this.state.sessionActive=!1,e}if(this.isStaleStart(t))return;if(this.deps.lensReporter.startSession(),S(s))return s.adInsertionType&&this.deps.diagnose("DA-INSERTION-TYPE-IGNORED-SSAI",`adInsertionType "${s.adInsertionType}" ignored in SSAI mode; the stitcher controls ad insertion.`,{adInsertionType:s.adInsertionType}),void await this.startSsaiSession(s,t);const i=s.manifestUrl;this.state.sessionConfig=s,this.state.manifestOrigin=a(i),this.deps.manifestService.setManifestUrl(i),this.deps.log("Session starting, manifest URL:",i);const r=Date.now(),n=e=>{this.deps.diagnose("DA-SESSION-STARTING",`Session start: ${e}`,{step:e,manifestUrl:i,elapsedMs:Date.now()-r})};n("begin");const o=s.assetParameters;let d;this.deps.adPlayerController.setSessionAssetParameters(o),this.deps.adPlayerController.setSessionAssetParameterMacros(s.assetParameterMacros),this.state.configSummary.gam&&(this.state.configSummary.gam={...this.state.configSummary.gam,hasAssetParameters:!!o}),n("manifest-fetch");try{d=await this.deps.manifestService.fetchManifest()}catch(e){if(this.isStaleStart(t))return;throw this.failSessionStart(e)}if(this.deps.cmcdAdReporter.onSessionStart(d),!this.isStaleStart(t))if(this.state.sessionManifest=d,"ssai"!==this.resolveSessionDeliveryMode(d))try{if(await this.startGamForManifest(d,o,n),this.isStaleStart(t))return;const a=s.adInsertionType;this.deps.adPlayerController.setBreakResumePolicy(a,d.timebase,this.deps.continueContentDuringBreak),this.deps.diagnose("DA-INSERTION-TYPE-RESOLVED",void 0===a?`Ad insertion type: unset — the manifest's resumeOffset drives the resume point (timebase: ${d.timebase})`:`Ad insertion type: ${a}, client-set (timebase: ${d.timebase})`,{adInsertionType:a??null,source:void 0===a?"unset":"client",timebase:d.timebase}),n("scheduling"),this.deps.breakScheduler.updateManifest(d),this.deps.pauseAdController.updateManifest(d),this.state.pauseAdTickInterval=setInterval(()=>this.deps.pauseAdController?.tick(),250);const r=(d.adStartDelay??0)>0?void 0:d.breaks.find(s=>0===e(s));r?(this.deps.adPlayerController.holdContentForPreRoll(r),this.state.heldImmediatePreRollId=r.id,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=Date.now(),this.contentIsReady()?this.armHeldImmediatePreRoll():this.state.heldPreRollArmInterval=setInterval(()=>{this.contentIsReady()?this.armHeldImmediatePreRoll():this.armHeldImmediatePreRollIfTimedOut()},250)):this.markPlaybackStartIfPlaying(),this.startBreakScheduler(),this.startManifestPolling(),this.deps.diagnose("DA-SESSION-STARTED",`Session active for manifest ${i}`,{manifestUrl:i}),this.deps.log("Session active")}catch(e){if(this.isStaleStart(t))return;throw this.failSessionStart(e)}else await this.startSteeredSsaiSession(d,s,n)}markPlaybackStartIfPlaying(){this.deps.player.paused||this.deps.breakScheduler.markPlaybackStart()}contentIsReady(){return!Number.isNaN(this.deps.player.duration)||!this.deps.player.paused}clearHeldImmediatePreRollArmInterval(){this.state.heldPreRollArmInterval&&(clearInterval(this.state.heldPreRollArmInterval),this.state.heldPreRollArmInterval=null)}armHeldImmediatePreRoll(){this.state.heldImmediatePreRollId&&!this.state.heldPreRollArmed&&(this.clearHeldImmediatePreRollArmInterval(),this.state.heldPreRollArmed=!0,this.deps.breakScheduler.markPlaybackStart())}armHeldImmediatePreRollIfTimedOut(){if(!this.state.heldImmediatePreRollId||this.state.heldPreRollArmed)return;const e=this.state.heldPreRollWaitStartedAt;if(null===e||Date.now()-e<p)return;const s=this.state.heldImmediatePreRollId;this.armHeldImmediatePreRoll(),this.deps.diagnose("DA-PREROLL-CONTENT-WAIT-TIMEOUT","Pending pre-roll began after waiting 10000ms for content readiness.",{breakId:s,waitMs:p})}cancelHeldImmediatePreRollOnContentError(){const e=this.state.heldImmediatePreRollId;return!(!e||this.state.heldPreRollArmed)&&(this.clearHeldImmediatePreRollArmInterval(),this.deps.breakScheduler?.completeBreak(e),this.deps.adPlayerController?.releaseContentHold(),this.state.heldImmediatePreRollId=null,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=null,this.deps.diagnose("DA-PREROLL-CANCELLED-CONTENT-ERROR","A pending pre-roll was cancelled because the content failed to load before it could begin.",{breakId:e}),!0)}clearHeldImmediatePreRollArm(){this.clearHeldImmediatePreRollArmInterval(),this.state.heldImmediatePreRollId=null,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=null}failSessionStart(e){const s=e instanceof Error?e:new Error(String(e)),t=this.deps.manifestService?.takeLastFailure();return this.deps.diagnose("DA-MANIFEST-FETCH-FAILED",s.message,t?{...t}:void 0),this.state.sessionActive=!1,this.deps.cmcdAdReporter.onSessionEnd(),this.deps.lensReporter.endSession(),s}resolveSessionDeliveryMode(e,s="manifest"){const t=this.state.sessionDeliveryMode,i=l(e,d(),this.deps.hasExplicitMode?this.deps.mode:null,s);return null!==i.unknownMode&&this.deps.diagnose("DA-DELIVERY-MODE-UNKNOWN",`Delivery rule ${i.ruleIndex} carries unrecognized mode "${i.unknownMode}"; using sgai.`,{unknownMode:i.unknownMode,ruleIndex:i.ruleIndex}),this.deps.hasExplicitMode&&i.manifestMode!==i.mode&&this.deps.diagnose("DA-DELIVERY-MODE-OVERRIDDEN",`Manifest delivery rules resolve "${i.manifestMode}" but the explicit config mode "${i.mode}" takes precedence.`,{manifestMode:i.manifestMode,configuredMode:i.mode}),this.state.sessionDeliveryMode=i.mode,this.deps.diagnose("DA-DELIVERY-MODE-RESOLVED",`Delivery mode: ${i.mode}`,{mode:i.mode,ruleIndex:i.ruleIndex,source:i.source,..."channel-switch"===s?{previousMode:t}:{}}),i.mode}async startSteeredSsaiSession(e,s,t){const i=n(e.vendorConfiguration);if(!i){const e="Manifest selected ssai delivery but no vendor configuration provides a stitched stream (no gam entry with an assetKey).";throw this.deps.diagnose("DA-SSAI-UNAVAILABLE",e),this.deps.manifestService.reset(),this.state.sessionActive=!1,new o(e)}try{t("ssai-live-stream"),this.state.steeredSsaiActive=!0,this.state.ssaiController=this.createSsaiController(i.networkCode),await this.state.ssaiController.startSteered(i,s.assetParameters,s.assetParameterMacros),t("scheduling");const a=r(e);this.deps.adPlayerController.setBreakResumePolicy(s.adInsertionType,e.timebase,!1),this.deps.breakScheduler.updateManifest(a),this.deps.pauseAdController.updateManifest(a),this.state.pauseAdTickInterval=setInterval(()=>this.deps.pauseAdController?.tick(),250),this.markPlaybackStartIfPlaying(),this.startBreakScheduler(),this.startManifestPolling(),this.deps.diagnose("DA-SESSION-STARTED",`SSAI session active (manifest-steered) for ${s.manifestUrl}`,{manifestUrl:s.manifestUrl}),this.deps.log("SSAI session active (manifest-steered)")}catch(e){const s=e instanceof Error?e:new Error(String(e));throw this.deps.diagnose("DA-SSAI-SESSION-FAILED",s.message),this.state.steeredSsaiActive=!1,this.deps.manifestService.reset(),this.state.sessionActive=!1,s}}createSsaiController(e){return new i(this.deps.player,{gam:this.state.steeredSsaiOptions?.streamActivityMonitorId?{streamActivityMonitorId:this.state.steeredSsaiOptions.streamActivityMonitorId}:{},ssai:{networkCode:e},maxBitrate:this.state.steeredSsaiOptions?.maxBitrate,autoplay:this.state.steeredSsaiOptions?.autoplay,debug:this.deps.debug},{emit:(e,s)=>this.deps.emit(e,s),diagnose:(e,s,t)=>this.deps.diagnose(e,s,t),log:(...e)=>this.deps.log(...e)})}startBreakScheduler(){this.deps.breakScheduler.start((e,s)=>{this.deps.breakLifecycle.onBreakStart(e,s)},e=>!this.deps.adPlayerController.isAdPlaying()&&(this.deps.adPlayerController.preload(e).catch(s=>{this.deps.diagnose("DA-PRELOAD-FAILED",s instanceof Error?s.message:String(s),{breakId:e.id})}),!0),(e,s,t)=>{this.deps.adBreakStatusTracker.onBreakWarning(e,s,t)},e=>{this.deps.breakLifecycle.onBreakAborted(e)})}startManifestPolling(){this.deps.manifestService.startPolling(e=>{if(!this.state.sessionActive)return;const s=this.deps.notedManifestBreakIds(e);this.deps.diagnose("DA-MANIFEST-POLLED","Manifest polled",{breaks:e.breaks.length,newBreakIds:s,pollingIntervalSeconds:this.deps.manifestService.currentPollingIntervalSeconds});const t=this.state.sessionManifest,i=t?h(t,e):null;if(this.state.pendingChannelSwitch||i&&this.deps.breakScheduler.getActiveBreak())return this.state.pendingChannelSwitch=e,void this.deps.log("Channel switch deferred until the active break ends");i?this.switchChannel(e,i):this.applyManifestUpdate(e)},e=>{this.state.sessionActive&&this.deps.diagnose("DA-MANIFEST-POLL-FAILED",e.message)})}applyPendingChannelSwitch(){if(!this.state.sessionActive||!this.state.pendingChannelSwitch)return;const e=this.state.pendingChannelSwitch;this.state.pendingChannelSwitch=null;const s=h(this.state.sessionManifest,e);s?this.switchChannel(e,s):this.applyManifestUpdate(e)}applyManifestUpdate(e){this.diagnoseIgnoredDeliveryModeChange(e);const s=t(e);this.state.steeredSsaiActive?this.state.ssaiController?.setManifestAssetParameters(s.ssai?.assetParameters):this.deps.adPlayerController.setManifestAssetParameters(s.sgaiAssetParameters);const i=this.state.steeredSsaiActive?r(e):e;this.deps.breakScheduler.updateManifest(i),this.deps.pauseAdController.updateManifest(i)}diagnoseIgnoredDeliveryModeChange(e){if(!this.state.sessionDeliveryMode)return;const s=l(e,d(),null,"manifest");s.manifestMode!==this.state.sessionDeliveryMode&&this.deps.diagnose("DA-DELIVERY-MODE-CHANGE-IGNORED",`Polled manifest resolves delivery mode "${s.manifestMode}" but "${this.state.sessionDeliveryMode}" is pinned for this session.`,{polledMode:s.manifestMode,pinnedMode:this.state.sessionDeliveryMode})}async switchChannel(e,s){if(this.state.channelSwitchInFlight)return;const t=this.state.sessionConfig,i=this.state.sessionManifest,a={previousChannelId:s.previousChannelId,channelId:s.channelId,reason:s.reason,manifestUrl:t.manifestUrl};this.deps.diagnose("DA-MANIFEST-CHANNEL-CHANGED",`Manifest channel changed (${s.reason}): ${s.previousChannelId??"none"} -> ${s.channelId??"none"}`,a),this.state.sessionManifest=e,this.state.channelSwitchInFlight=!0;const r=this.state.sessionGeneration,n=this.state.sessionDeliveryMode??"sgai";try{let s=this.resolveSessionDeliveryMode(e,"channel-switch");if(this.deps.cmcdAdReporter.onSessionEnd(),this.deps.cmcdAdReporter.onSessionStart(e),this.deps.lensReporter.endSession(),this.deps.lensReporter.startSession(),"ssai"===s&&"sgai"===n){const a=await this.switchToSteeredSsai(e,t);if(this.isStaleStart(r))return;a||(s="sgai",this.state.sessionDeliveryMode="sgai",await this.applySameModeChannelSwitch(i,e,t,r))}else"sgai"===s&&"ssai"===n?await this.switchToSgai(e,t,r):await this.applySameModeChannelSwitch(i,e,t,r);if(this.isStaleStart(r))return}finally{this.state.channelSwitchInFlight=!1}this.emitChannelChange({...a,previousDeliveryMode:n,deliveryMode:this.state.sessionDeliveryMode??n})}emitChannelChange(e){this.deps.emit("adchannelchange",{type:"adchannelchange",timestamp:Date.now(),...e})}async applySameModeChannelSwitch(e,s,i,a){const n=this.state.steeredSsaiActive,o=n?r(s):s;if(this.deps.breakScheduler.resetForChannelSwitch(o),this.deps.pauseAdController.updateManifest(o),this.deps.adPlayerController.setBreakResumePolicy(i.adInsertionType,s.timebase,this.deps.continueContentDuringBreak),!n&&c(e,s)){const e=this.deps.adPlayerController.updatedAssetParameters,t=this.deps.adPlayerController.sessionAssetParameterMacros;if(this.deps.adPlayerController.endGamSession(),this.deps.adPlayerController.setSessionAssetParameters(i.assetParameters),this.deps.adPlayerController.setSessionAssetParameterMacros(t),await this.startGamForManifest(s,i.assetParameters,e=>{this.deps.diagnose("DA-SESSION-STARTING",`Channel switch: ${e}`,{step:e,manifestUrl:i.manifestUrl})}),this.isStaleStart(a))return;e&&this.deps.adPlayerController.updateAssetParameters(e)}else if(n){const e=t(s);this.state.ssaiController?.setManifestAssetParameters(e.ssai?.assetParameters)}else{const e=t(s);this.deps.adPlayerController.setManifestAssetParameters(e.sgaiAssetParameters)}}async switchToSteeredSsai(e,s){const t=n(e.vendorConfiguration);if(!t)return this.deps.diagnose("DA-SSAI-UNAVAILABLE","Channel switch resolved ssai delivery but no vendor configuration provides a stitched stream; the session stays in sgai.",{manifestUrl:s.manifestUrl,previousMode:"sgai"}),!1;const i=this.deps.adPlayerController,a=i.updatedAssetParameters,o=i.sessionAssetParameterMacros;i.stopActiveBreak(),i.releaseContentHold(),i.endGamSession(),i.setSessionAssetParameters(s.assetParameters),i.setSessionAssetParameterMacros(o),a&&i.updateAssetParameters(a);const d=this.createSsaiController(t.networkCode);this.state.ssaiController=d,this.state.steeredSsaiActive=!0,this.deps.diagnose("DA-SESSION-STARTING","Channel switch: ssai-live-stream",{step:"ssai-live-stream",manifestUrl:s.manifestUrl});try{await d.startSteered(t,s.assetParameters,o),a&&d.updateAssetParameters(a);const n=r(e);return this.deps.breakScheduler.resetForChannelSwitch(n),this.deps.pauseAdController.updateManifest(n),i.setBreakResumePolicy(s.adInsertionType,e.timebase,!1),!0}catch(e){const s=e instanceof Error?e.message:String(e);return this.deps.diagnose("DA-SSAI-SESSION-FAILED",s),d.stop(),delete this.state.ssaiController,this.state.steeredSsaiActive=!1,!1}}async switchToSgai(e,s,t){const i=this.state.ssaiController?.sessionAssetParameterMacros;this.state.ssaiController?.stop(),delete this.state.ssaiController,this.state.steeredSsaiActive=!1,this.deps.breakScheduler.resetForChannelSwitch(e),this.deps.pauseAdController.updateManifest(e);const a=this.deps.adPlayerController;a.setBreakResumePolicy(s.adInsertionType,e.timebase,this.deps.continueContentDuringBreak);const r=a.updatedAssetParameters;a.endGamSession(),a.setSessionAssetParameters(s.assetParameters),a.setSessionAssetParameterMacros(i??s.assetParameterMacros),await this.startGamForManifest(e,s.assetParameters,e=>{this.deps.diagnose("DA-SESSION-STARTING",`Channel switch: ${e}`,{step:e,manifestUrl:s.manifestUrl})}),this.isStaleStart(t)||r&&a.updateAssetParameters(r)}async startGamForManifest(e,i,a){const r=t(e);if(this.deps.adPlayerController.setManifestAssetParameters(r.sgaiAssetParameters),r.unknownTypes.length>0&&this.deps.diagnose("DA-VENDOR-CONFIG-UNKNOWN-TYPE",`Ignored ${r.unknownTypes.length} vendorConfiguration `+(1===r.unknownTypes.length?"entry":"entries")+" with unrecognised type: "+`${r.unknownTypes.join(", ")}.`,{types:r.unknownTypes}),this.deps.adPlayerController.isGamEnabled())if(r.identity){this.deps.diagnose("DA-GAM-CONFIG-RESOLVED",`GAM session identity: network ${r.identity.networkCode}, custom asset ${r.identity.customAssetKey}`,{networkCode:r.identity.networkCode,customAssetKey:r.identity.customAssetKey,channelId:e.channelId,duplicateEntriesIgnored:r.duplicateGam});try{a("gam-init"),await this.deps.adPlayerController.startGamSession(r.identity,i),this.deps.log("GAM session started")}catch(e){this.deps.diagnose("DA-GAM-SESSION-FAILED",e instanceof Error?e.message:String(e))}}else s(e)&&this.deps.diagnose("DA-GAM-CONFIG-MISSING",'The break manifest carries GAM pod assets but no usable "gam" vendorConfiguration entry; those assets will be skipped.',{reason:r.reason});else s(e)&&this.deps.diagnose("DA-GAM-CONFIG-MISSING","The break manifest carries GAM pod assets but GAM is disabled for this session (gam.enabled === false); those assets will be skipped.",{reason:"gam-disabled"})}async startSsaiSession(e,s){try{if(this.state.stitcherOrigin=a(e.stitcherUrl),this.state.configSummary.gam&&(this.state.configSummary.gam={...this.state.configSummary.gam,hasAssetParameters:!!e.assetParameters}),await this.state.ssaiController.start(e),this.isStaleStart(s))return;this.deps.diagnose("DA-SESSION-STARTED",`SSAI session active for ${e.stitcherUrl}`,{stitcherUrl:e.stitcherUrl}),this.deps.log("SSAI session active")}catch(e){if(this.isStaleStart(s))return;const t=e instanceof Error?e:new Error(String(e));throw this.deps.diagnose("DA-SSAI-SESSION-FAILED",t.message),this.state.sessionActive=!1,this.deps.cmcdAdReporter.onSessionEnd(),this.deps.lensReporter.endSession(),t}}isStaleStart(e){return e!==this.state.sessionGeneration&&(this.deps.log("Session start aborted — the session ended or was replaced while starting"),!0)}}
|
package/dist/types/Events.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface AdBreakStatus {
|
|
|
23
23
|
adIndex?: number | undefined;
|
|
24
24
|
/** Total number of ads in the break, when known. */
|
|
25
25
|
totalAds?: number | undefined;
|
|
26
|
-
/** Number of ads remaining including the currently playing one. */
|
|
26
|
+
/** Number of ads remaining including the currently playing one; after an ad ends, the ads still to come. */
|
|
27
27
|
adsRemaining?: number | undefined;
|
|
28
28
|
/** Whether the countdown is advancing (ad rendered, playback progressing). `false` while buffering or before a held pre-roll's first frame. */
|
|
29
29
|
ticking: boolean;
|
|
@@ -75,6 +75,18 @@ export type ManifestRequestInterceptor = (request: ManifestRequest, context: Man
|
|
|
75
75
|
* content player can continue using the device's decoder until the break starts.
|
|
76
76
|
* @category Configuration */
|
|
77
77
|
export type AdPreloadMode = 'parallel' | 'single-decoder' | 'none' | 'auto';
|
|
78
|
+
/**
|
|
79
|
+
* How content is recovered when an ad break ends.
|
|
80
|
+
* - `recreate`: release the ad media, rebuild the content player's media pipeline at the resume
|
|
81
|
+
* position, then play. Needed on VIZIO SmartCast, where content cannot resume in the
|
|
82
|
+
* MediaSource an ad interrupted.
|
|
83
|
+
* - `none`: resume in place (seek + play).
|
|
84
|
+
* - `auto` (default): User-Agent based — VIZIO/SmartCast → `recreate`, else `none`.
|
|
85
|
+
*
|
|
86
|
+
* Web only: the Android, iOS and React Native bridges accept and ignore this option (their
|
|
87
|
+
* native players resume in place).
|
|
88
|
+
* @category Configuration */
|
|
89
|
+
export type AdBreakContentRecovery = 'recreate' | 'none' | 'auto';
|
|
78
90
|
/**
|
|
79
91
|
* Ad insertion strategy (compositing).
|
|
80
92
|
* - `overlay`: separate `<video>` over the content player; all break formats.
|
|
@@ -112,6 +124,8 @@ export declare const DEFAULT_MODE: OptiViewAdsMode;
|
|
|
112
124
|
* MediaSource before the break and wedges their only hardware decoder (black screen, no `adbreakbegin`).
|
|
113
125
|
*/
|
|
114
126
|
export declare const DEFAULT_AD_PRELOAD_MODE: AdPreloadMode;
|
|
127
|
+
/** `auto` rebuilds the content pipeline at break end on SmartCast panels and resumes in place elsewhere. */
|
|
128
|
+
export declare const DEFAULT_AD_BREAK_CONTENT_RECOVERY: AdBreakContentRecovery;
|
|
115
129
|
/** `auto` switches iPhone/iPod to shared-element insertion and leaves every other platform on overlay. */
|
|
116
130
|
export declare const DEFAULT_AD_INSERTION_MODE: AdInsertionMode;
|
|
117
131
|
/**
|
|
@@ -127,9 +141,9 @@ export type DoubleBoxAudio = 'ad' | 'content';
|
|
|
127
141
|
/** Default double-box audio focus: the ad, the side being monetised. */
|
|
128
142
|
export declare const DEFAULT_DOUBLE_BOX_AUDIO: DoubleBoxAudio;
|
|
129
143
|
/**
|
|
130
|
-
* Default hard-cut margin (seconds)
|
|
131
|
-
*
|
|
132
|
-
* break length finish cleanly.
|
|
144
|
+
* Default hard-cut margin (seconds), counted with the effective duration from first ad playback.
|
|
145
|
+
* Startup has its own watchdog; later stalls do not extend the playback cutoff.
|
|
146
|
+
* The margin lets an ad close to the break length finish cleanly.
|
|
133
147
|
*/
|
|
134
148
|
export declare const DEFAULT_AD_BREAK_CUT_SAFETY_MARGIN_SEC = 2;
|
|
135
149
|
/**
|
|
@@ -267,6 +281,12 @@ export interface OptiViewAdsConfig {
|
|
|
267
281
|
* @see AdPreloadMode
|
|
268
282
|
*/
|
|
269
283
|
adPreload?: AdPreloadMode;
|
|
284
|
+
/**
|
|
285
|
+
* Post-break content recovery strategy.
|
|
286
|
+
* @default 'auto'
|
|
287
|
+
* @see AdBreakContentRecovery
|
|
288
|
+
*/
|
|
289
|
+
adBreakContentRecovery?: AdBreakContentRecovery;
|
|
270
290
|
/**
|
|
271
291
|
* Ad insertion strategy. `auto` switches iPhone/iPod to shared-element insertion, all other platforms overlay.
|
|
272
292
|
* @default 'auto'
|
|
@@ -323,8 +343,8 @@ export interface OptiViewAdsConfig {
|
|
|
323
343
|
*/
|
|
324
344
|
interceptManifestRequest?: ManifestRequestInterceptor;
|
|
325
345
|
/**
|
|
326
|
-
* Seconds added to
|
|
327
|
-
*
|
|
346
|
+
* Seconds added to the effective break duration, counted from first ad playback rather than
|
|
347
|
+
* loading. Later playing events do not reset it. `0` allows exactly that playback window.
|
|
328
348
|
* @default 2
|
|
329
349
|
*/
|
|
330
350
|
adBreakCutSafetyMarginSec?: number;
|