@dolby-optiview/ads-sdk-core 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/OptiViewAds.d.ts +8 -4
- package/dist/OptiViewAds.js +1 -1
- package/dist/OptiViewAdsExternalVendors.d.ts +47 -0
- package/dist/OptiViewAdsExternalVendors.js +1 -0
- package/dist/generated/diagnosticCodes.d.ts +118 -3
- 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/index.d.ts +1 -0
- package/dist/id3/index.js +1 -1
- package/dist/id3/parseId3TextFrames.d.ts +8 -0
- package/dist/id3/parseId3TextFrames.js +1 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.js +1 -1
- package/dist/interfaces/PlayerAdapter.d.ts +5 -0
- package/dist/services/AdBreakSequencer.js +1 -1
- package/dist/services/AdPlayerController.d.ts +8 -1
- package/dist/services/AdPlayerController.js +1 -1
- package/dist/services/BreakScheduler.d.ts +31 -56
- package/dist/services/BreakScheduler.js +1 -1
- package/dist/services/BreakSchedulerTypes.d.ts +55 -0
- package/dist/services/BreakSchedulerTypes.js +1 -0
- package/dist/services/ExternalVendorConfigurationTracker.d.ts +13 -0
- package/dist/services/ExternalVendorConfigurationTracker.js +1 -0
- package/dist/services/GamStreamManager.d.ts +6 -0
- package/dist/services/GamStreamManager.js +1 -1
- package/dist/services/ImaMetadataDeduplicator.d.ts +5 -0
- package/dist/services/ImaMetadataDeduplicator.js +1 -0
- package/dist/services/MetadataCueTracker.d.ts +27 -1
- package/dist/services/MetadataCueTracker.js +1 -1
- package/dist/services/SsaiController.d.ts +2 -0
- package/dist/services/SsaiController.js +1 -1
- package/dist/services/adPlayer/AdPlayerEvents.js +1 -1
- package/dist/services/adPlayer/AdPlayerState.d.ts +13 -0
- package/dist/services/adPlayer/AdPlayerState.js +1 -1
- package/dist/services/adPlayer/AdPreloader.js +1 -1
- package/dist/services/adPlayer/AdStage.d.ts +7 -3
- package/dist/services/adPlayer/AdStage.js +1 -1
- package/dist/services/adPlayer/AssetParameterResolver.d.ts +2 -0
- package/dist/services/adPlayer/AssetParameterResolver.js +1 -1
- package/dist/services/adPlayer/AssetPlayback.js +1 -1
- package/dist/services/adPlayer/ContentRecovery.js +1 -1
- package/dist/services/adPlayer/ContentResumeCoordinator.d.ts +50 -3
- package/dist/services/adPlayer/ContentResumeCoordinator.js +1 -1
- package/dist/services/adPlayer/GamPodBridge.js +1 -1
- package/dist/services/adPlayer/SharedElementBreakPlayer.js +1 -1
- package/dist/services/adPlayer/VariantSelector.js +1 -1
- package/dist/services/adPlayer/pdtClockResume.d.ts +57 -0
- package/dist/services/adPlayer/pdtClockResume.js +1 -0
- package/dist/services/adPlayer/streamClockMapping.d.ts +65 -0
- package/dist/services/adPlayer/streamClockMapping.js +1 -0
- package/dist/services/adPlayer/streamClockResume.d.ts +94 -0
- package/dist/services/adPlayer/streamClockResume.js +1 -0
- package/dist/services/anvatoCueRuntime.d.ts +12 -0
- package/dist/services/anvatoCueRuntime.js +1 -1
- package/dist/services/assetParameterMacros.d.ts +0 -1
- package/dist/services/assetParameterMacros.js +1 -1
- package/dist/services/breakTiming.d.ts +9 -0
- package/dist/services/breakTiming.js +1 -1
- package/dist/services/channelSwitch.d.ts +3 -1
- package/dist/services/channelSwitch.js +1 -1
- package/dist/services/detectDeviceType.d.ts +7 -2
- package/dist/services/detectDeviceType.js +1 -1
- package/dist/services/diagnosticReport.d.ts +3 -0
- package/dist/services/diagnosticReport.js +1 -0
- package/dist/services/externalVendorConfigurationGetter.d.ts +12 -0
- package/dist/services/externalVendorConfigurationGetter.js +1 -0
- package/dist/services/matchTargeting.d.ts +19 -0
- package/dist/services/matchTargeting.js +1 -0
- package/dist/services/resolveDeliveryMode.d.ts +3 -1
- package/dist/services/resolveDeliveryMode.js +1 -1
- package/dist/services/resolveExternalVendorConfigurations.d.ts +20 -0
- package/dist/services/resolveExternalVendorConfigurations.js +1 -0
- package/dist/services/resolveGamVendorConfiguration.js +1 -1
- package/dist/services/resolveResumeClamp.d.ts +42 -0
- package/dist/services/resolveResumeClamp.js +1 -0
- package/dist/services/resolveTargetedUri.d.ts +2 -1
- package/dist/services/resolveTargetedUri.js +1 -1
- package/dist/services/selectVariant.d.ts +4 -0
- package/dist/services/selectVariant.js +1 -1
- package/dist/services/toPlaybackError.d.ts +1 -0
- package/dist/services/toPlaybackError.js +1 -0
- package/dist/services/wallclockBreakWindow.d.ts +41 -0
- package/dist/services/wallclockBreakWindow.js +1 -0
- package/dist/session/BreakLifecycle.d.ts +2 -0
- package/dist/session/BreakLifecycle.js +1 -1
- package/dist/session/SessionStarter.d.ts +6 -1
- package/dist/session/SessionStarter.js +1 -1
- package/dist/session/SessionState.d.ts +1 -0
- package/dist/session/SessionState.js +1 -1
- package/dist/types/BreakManifest.d.ts +39 -1
- package/dist/types/BreakManifest.js +1 -1
- package/dist/types/Events.d.ts +16 -1
- package/dist/types/ExternalVendorConfiguration.d.ts +5 -0
- package/dist/types/ExternalVendorConfiguration.js +1 -0
- package/dist/types/OptiViewAdsConfig.d.ts +2 -1
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { PlayerAdapter } from '../../interfaces/PlayerAdapter';
|
|
1
|
+
import type { PlayerAdapter, TimedMetadataCue } from '../../interfaces/PlayerAdapter';
|
|
2
2
|
import type { Break } from '../../types/BreakManifest';
|
|
3
3
|
import type { Clock } from '../BreakScheduler';
|
|
4
4
|
import type { AdPlayerState } from './AdPlayerState';
|
|
5
|
-
|
|
5
|
+
import { type BreakEndOnStreamClock } from './streamClockResume';
|
|
6
6
|
export interface ContentResumeCoordinatorDeps {
|
|
7
7
|
contentPlayer: PlayerAdapter;
|
|
8
8
|
clock: Clock;
|
|
@@ -22,7 +22,24 @@ export declare class ContentResumeCoordinator {
|
|
|
22
22
|
* the SDK has no cue tracker (every timebase except `ptsSource: 'anvatoCue'`).
|
|
23
23
|
*/
|
|
24
24
|
private cueAnchorResolver;
|
|
25
|
+
/** The last resolved resume point before the live-window clamp; null when no seek was resolved. */
|
|
26
|
+
private resumePolicyTarget;
|
|
27
|
+
/** The break's matched Anvato `cue_end`; null without a cue tracker or before it is matched. */
|
|
28
|
+
private cueEndResolver;
|
|
29
|
+
private readonly streamClock;
|
|
30
|
+
private readonly pdtClock;
|
|
25
31
|
constructor(state: AdPlayerState, deps: ContentResumeCoordinatorDeps);
|
|
32
|
+
/**
|
|
33
|
+
* Supply the break's matched Anvato `cue_end` (`ptsSource: 'anvatoCue'`): the stream's own end
|
|
34
|
+
* of the break, used as the resume point in place of the cue plus the duration.
|
|
35
|
+
*/
|
|
36
|
+
setCueEndResolver(resolver: ((breakInfo: Break) => BreakEndOnStreamClock | null) | null): void;
|
|
37
|
+
/** Feed a timed-metadata cue; Anvato beacons map currentTime to the stream clock. */
|
|
38
|
+
ingestStreamClockCue(cue: TimedMetadataCue): void;
|
|
39
|
+
/** The content pipeline was rebuilt: its timeline has to be mapped to the stream clock again. */
|
|
40
|
+
noteContentReload(): void;
|
|
41
|
+
/** Stops the stream-clock landing measurements (controller teardown). */
|
|
42
|
+
dispose(): void;
|
|
26
43
|
/**
|
|
27
44
|
* Supply the in-stream cue position used to anchor the resume in place of the pause position.
|
|
28
45
|
*
|
|
@@ -39,6 +56,12 @@ export declare class ContentResumeCoordinator {
|
|
|
39
56
|
* that latency into the resume target and lands content late by the same amount.
|
|
40
57
|
*/
|
|
41
58
|
captureBreakAnchor(breakInfo: Break): void;
|
|
59
|
+
/**
|
|
60
|
+
* A wallclock break's anchor through the stream's Program Date Time: the content time of the
|
|
61
|
+
* manifest start, wherever the pause landed. Null off the wallclock timebase or without a PDT
|
|
62
|
+
* sample, which keeps the pause position. Reports a cut that landed late on the stream clock.
|
|
63
|
+
*/
|
|
64
|
+
private pdtBreakAnchor;
|
|
42
65
|
/**
|
|
43
66
|
* Replace the anchor part-way through a break, when content kept playing until now
|
|
44
67
|
* (picture-in-picture adoption).
|
|
@@ -60,14 +83,33 @@ export declare class ContentResumeCoordinator {
|
|
|
60
83
|
*/
|
|
61
84
|
resumeAnchorCushionSec(): number | null;
|
|
62
85
|
isPlausibleSeekEnd(end: number): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Where the run-up steers content: the stream-clock target once beacons since the run-up's last
|
|
88
|
+
* seek map it, else the break end on the Program Date Time, else the matched `cue_end`, else
|
|
89
|
+
* the policy's resume point.
|
|
90
|
+
*/
|
|
63
91
|
plannedResumeTarget(breakInfo: Break): number | null;
|
|
92
|
+
/** The policy's resume point from the anchor, ignoring the stream's own clock. */
|
|
93
|
+
computedResumeTarget(breakInfo: Break): number | null;
|
|
94
|
+
/** Replacement resumes at the break end: the only policy the stream's end of the break can serve. */
|
|
95
|
+
resumesAtBreakEnd(breakInfo: Break): boolean;
|
|
96
|
+
private resolvePolicy;
|
|
97
|
+
/** The matched `cue_end`'s currentTime, when the policy resumes at the break end. */
|
|
98
|
+
private cueEndTarget;
|
|
64
99
|
maybePrepositionContentForResume(): void;
|
|
65
100
|
maybeCatchUpContentForResume(breakInfo: Break): void;
|
|
66
101
|
endResumeCatchup(keepPlaying?: boolean): void;
|
|
67
102
|
get resumeHandoverInMotion(): boolean;
|
|
68
103
|
resumeAlignSummary(): Record<string, unknown>;
|
|
69
104
|
cancelResumeLandedProbe(): void;
|
|
70
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Reports where the resume seek landed (`DA-RESUME-LANDED`) on the next `seeked` event.
|
|
107
|
+
*
|
|
108
|
+
* @param target - The position the player was asked to seek to (after any live-window clamp).
|
|
109
|
+
* @param policyTarget - The resume point before the clamp; the landing is also measured against
|
|
110
|
+
* it, because an exact landing on a clamped seek still leaves the viewer short.
|
|
111
|
+
*/
|
|
112
|
+
probeResumeLanding(breakId: string, target: number, policyTarget?: number | null): void;
|
|
71
113
|
waitForLiveEdge(isCurrent?: () => boolean): Promise<number | null>;
|
|
72
114
|
resetBreakResumeState(): void;
|
|
73
115
|
beginContinueContentPlayout(): void;
|
|
@@ -86,5 +128,10 @@ export declare class ContentResumeCoordinator {
|
|
|
86
128
|
* seek is needed (policy says resume in place, or content was already pre-positioned there).
|
|
87
129
|
*/
|
|
88
130
|
resolveResumeTarget(breakInfo: Break, earlyReturnOffsetSec?: number | null): number | null;
|
|
131
|
+
/**
|
|
132
|
+
* DA-RESUME-CUE-END at the hand-back, when a matched `cue_end` decides the resume without the
|
|
133
|
+
* stream clock (no beacons, or none since the run-up's last seek). Returns its target, or null.
|
|
134
|
+
*/
|
|
135
|
+
private reportCueEnd;
|
|
89
136
|
applyResumeSeek(breakInfo: Break, earlyReturnOffsetSec?: number | null): void;
|
|
90
137
|
}
|
|
@@ -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,liveSafeSeekStart as u,liveSeekableWindowSec as l,unreachableEdgeSec as d}from"./liveEdge";export const START_CLAMP_MAX_WINDOW_FRACTION=.5;const h=.04,c=e=>Math.round(1e3*e)/1e3;export class ContentResumeCoordinator{constructor(e,t){this.state=e,this.deps=t,this.cueAnchorResolver=null,this.continuePlayout=new n(this.state,{contentPlayer:this.deps.contentPlayer,clock:this.deps.clock,gateContentForBreak:()=>this.deps.gateContentForBreak(),distanceToLiveEdgeSec:()=>this.distanceToLiveEdgeSec(),plannedResumeTarget:e=>this.plannedResumeTarget(e),diagnose:(e,t,s)=>this.deps.diagnose(e,t,s),log:(...e)=>this.deps.log(...e)})}setCueAnchorResolver(e){this.cueAnchorResolver=e}captureBreakAnchor(e){this.resetBreakResumeState();const t=this.cueAnchorResolver?.(e)??null,s=null!==t&&Number.isFinite(t);this.state.contentPositionAtBreakStart=s?t:this.deps.contentPlayer.currentTime,this.state.resumeAnchorIsCue=s,this.state.resumeAnchorSeekableEnd=this.contentSeekableEnd()}reanchorMidBreak(e){this.state.contentPositionAtBreakStart=e,this.state.resumeAnchorIsCue=!1,this.state.resumeAnchorSeekableEnd=this.contentSeekableEnd()}liveEdgeDistanceSec(){return r(this.deps.contentPlayer)}liveSafeSeekEnd(){return o(this.deps.contentPlayer)}liveSafeSeekStart(){return u(this.deps.contentPlayer)}liveSeekableWindowSec(){return l(this.deps.contentPlayer)}contentSeekableEnd(){return i(this.deps.contentPlayer)}resumeAnchorCushionSec(){const e=this.state.resumeAnchorSeekableEnd;return null!==e&&Number.isFinite(e)?c(e-this.state.contentPositionAtBreakStart):null}isPlausibleSeekEnd(e){return a(this.deps.contentPlayer,e)}plannedResumeTarget(e){if(t(e))return null;const n=s({anchor:this.state.contentPositionAtBreakStart,duration:e.duration,adDuration:e.duration,...this.state.adInsertionType?{insertionType:this.state.adInsertionType}:{},..."number"==typeof e.resumeOffset?{resumeOffset:e.resumeOffset}:{}});return n.seek?n.target:null}maybePrepositionContentForResume(){if("shared-element"===this.deps.getInsertionMode())return;if(!this.state.contentPaused||null!==this.state.resumePrepositionedTo)return;if(null!==this.state.resumePrepositionSettle)return;if(this.state.resumeCatchupActive)return;if(Number.isFinite(this.deps.contentPlayer.duration))return;const e=this.state.currentBreak;if(!e)return;const t=this.plannedResumeTarget(e);if(null===t)return;const s=this.contentSeekableEnd();if(null===s)return;const n=this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec,i=(e.duration??0)-n;let a;if(t<=s)a=t;else{if(!(i<=3))return;a=s}this.deps.log(a===t?`Resume pre-positioning: seeking content to ${t}s during the break (window reaches ${s}s)`:`Resume pre-positioning: window only reaches ${s}s and the ad ends in ${i}s — moving content as close to ${t}s as exists`),this.deps.contentPlayer.seek(a);const r=setTimeout(()=>{this.state.resumePrepositionSettle=null;const e=this.deps.contentPlayer.currentTime;Math.abs(e-a)<=h?(this.state.resumePrepositionedTo=e,this.deps.log(`Resume pre-positioned: content is at ${e}s`)):this.deps.log(`Resume pre-position rejected: asked ${a}s, landed ${e}s — window is short, will retry`)},250);this.state.resumePrepositionSettle=r}maybeCatchUpContentForResume(e){if("shared-element"===this.deps.getInsertionMode())return;if(!this.state.contentPaused||Number.isFinite(this.deps.contentPlayer.duration))return;const t=this.plannedResumeTarget(e);if(null===t)return;const s=this.deps.contentPlayer.currentTime;if(this.state.resumeCatchupActive){const n=(e.duration??0)-(this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec);if(s>t+1)return this.state.resumeCatchupActive=!1,this.deps.contentPlayer.pause(),this.state.resumePrepositionedTo=this.deps.contentPlayer.currentTime,void this.deps.log(`Resume catch-up halted: content reached ${this.deps.contentPlayer.currentTime}s, past the ${t}s target`);if(n<=30&&n>=1){this.state.resumeAlignTicks+=1;const e=s+n,i=e-t;if(Math.abs(i)>h){const a=s-i,r=this.liveSafeSeekEnd();if(null!==r){const e=r>this.state.lastResumeSafeEnd+.25;e&&Number.isFinite(this.state.lastResumeSafeEnd)&&this.state.resumeSawSteps.push(Math.round(100*(r-this.state.lastResumeSafeEnd))/100),r>this.state.lastResumeSafeEnd&&(this.state.lastResumeSafeEnd=r);const s=r-a;if(s>this.state.resumeBestSawMarginSec&&(this.state.resumeBestSawMarginSec=s),a>r)return void(this.state.resumeAlignBlockedDiagnosed||(this.state.resumeAlignBlockedDiagnosed=!0,this.deps.log(`Resume align waiting: ${a}s is past the published edge ${r}s (${Math.round(1e3*(a-r))}ms short) — rolling until a manifest step brings it in reach`),this.deps.diagnose?.("DA-RESUME-ALIGN-BLOCKED",`Resume alignment cannot reach ${t}s yet: the published edge is ${r}s`,{target:t,requested:a,safeEnd:r,shortfallSec:Math.round(1e3*(a-r))/1e3,remainingSec:n})));if(!e&&this.state.resumeSawSeekDone)return;this.state.resumeSawSeekDone=!0}if(this.state.lastResumeAlignRemainingSec-n<.5)return;this.state.lastResumeAlignRemainingSec=n,this.deps.log(`Resume align: landing ${e}s vs target ${t}s (${Math.round(1e3*i)}ms) — content to ${a}s, ${n}s of ad left`),this.state.resumeAlignCorrectionsMs.push(Math.round(1e3*i)),this.deps.contentPlayer.seek(a)}}return}if(t-s<=h)return;const n=this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec,i=(e.duration??0)-n;if(i<=0)return;if(i>30)return;const a=this.contentSeekableEnd();if(null===a)return;const r=t-i+.3,o=Math.min(r,a);this.state.resumeCatchupActive=!0,this.state.resumeCatchupRan=!0,this.state.contentMutedBeforeCatchup=this.deps.contentPlayer.muted,this.deps.contentPlayer.muted=!0,this.deps.log(`Resume catch-up: content to ${o}s, playing toward ${t}s with ${i}s of ad left`),this.deps.diagnose?.("DA-RESUME-CATCHUP",`Break ${e.id}: playing content from ${o}s toward ${t}s with ${i}s of ad left`,{breakId:e.id,startPos:o,target:t,remainingAdSec:Math.round(1e3*i)/1e3,publishedEnd:a,clampedStart:o<r}),Math.abs(s-o)>h&&this.deps.contentPlayer.seek(o),this.deps.contentPlayer.play()}endResumeCatchup(e=!1){this.state.resumeCatchupActive&&(this.state.resumeCatchupActive=!1,e||this.deps.contentPlayer.pause()),null!==this.state.contentMutedBeforeCatchup&&(this.deps.contentPlayer.muted=this.state.contentMutedBeforeCatchup,this.state.contentMutedBeforeCatchup=null)}get resumeHandoverInMotion(){return this.state.resumeCatchupActive}resumeAlignSummary(){const e=this.state.resumeAlignCorrectionsMs;return{catchUpRan:this.state.resumeCatchupRan||this.state.resumeCatchupActive||e.length>0||null!==this.state.resumePrepositionedTo,alignCorrections:e.length,placedOnStep:this.state.resumeSawSeekDone,alignTicks:this.state.resumeAlignTicks,sawSteps:this.state.resumeSawSteps.slice(0,12),sawStepCount:this.state.resumeSawSteps.length,bestSawMarginSec:Number.isFinite(this.state.resumeBestSawMarginSec)?Math.round(1e3*this.state.resumeBestSawMarginSec)/1e3:null,alignCorrectionsMs:e.slice(0,12),alignLastErrorMs:e.length?e[e.length-1]:null}}cancelResumeLandedProbe(){const e=this.state.resumeLandedProbe;this.state.resumeLandedProbe=null,e?.()}probeResumeLanding(e,t){this.cancelResumeLandedProbe();let s=!1;const n=(n,i)=>{if(s)return;s=!0,this.cancelResumeLandedProbe();const a=null===n?null:Math.round(1e3*(n-t))/1e3;this.deps.log(null===n?`Resume landing unknown for break ${e} (${i})`:`Resume landed at ${n}s, ${a}s from the ${t}s asked for (break ${e})`),this.deps.diagnose?.("DA-RESUME-LANDED",null===n?`Resume landing for break ${e} could not be measured (${i})`:`Resume for break ${e} landed at ${n}s, ${a}s from the ${t}s requested`,{breakId:e,target:t,landed:n,errorSec:a,...i?{reason:i}:{}})},i=()=>n(this.deps.contentPlayer.currentTime),a=setTimeout(()=>n(null,"no seeked event"),2e3);this.state.resumeLandedProbe=()=>{clearTimeout(a),this.deps.contentPlayer.off("seeked",i)},this.deps.contentPlayer.on("seeked",i)}async waitForLiveEdge(e=()=>!0){for(let t=0;t<20;t++){if(!e())return null;const t=this.deps.contentPlayer.videoElement,s=t?.seekable;if(s&&s.length>0){const e=s.end(s.length-1);if(Number.isFinite(e)&&e>0)return e}if(await new Promise(e=>setTimeout(e,100)),!e())return null}return this.deps.log("shared-element: no seekable range after restore; staying at the live edge"),null}resetBreakResumeState(){this.state.resumeAnchorIsCue=!1,this.state.resumeAnchorSeekableEnd=null,this.state.resumePrepositionedTo=null,this.state.resumeCatchupActive=!1,this.state.resumeCatchupRan=!1,this.state.contentMutedBeforeCatchup=null,this.state.resumeAlignCorrectionsMs=[],this.state.lastResumeAlignRemainingSec=Number.POSITIVE_INFINITY,this.state.resumeAlignBlockedDiagnosed=!1,this.state.lastResumeSafeEnd=Number.NEGATIVE_INFINITY,this.state.resumeSawSeekDone=!1,this.state.resumeBestSawMarginSec=Number.NEGATIVE_INFINITY,this.state.resumeAlignTicks=0,this.state.resumeSawSteps=[],this.state.resumePrepositionSettle&&(clearTimeout(this.state.resumePrepositionSettle),this.state.resumePrepositionSettle=null),this.state.adSecondsPlayedThisBreak=0,this.state.currentAssetElapsedSec=0}beginContinueContentPlayout(){this.continuePlayout.beginContinueContentPlayout()}distanceToLiveEdgeSec(){const e=this.contentSeekableEnd();return null===e?null:c(e-this.deps.contentPlayer.currentTime)}secondsUntilCut(e){return this.continuePlayout.secondsUntilCut(e)}restoreContinueQuality(e){this.continuePlayout.restoreContinueQuality(e)}currentContinueDriftSec(){return this.continuePlayout.currentContinueDriftSec()}abandonContinueContentPlayout(e){this.continuePlayout.abandonContinueContentPlayout(e)}endContinueContentPlayout(){this.continuePlayout.endContinueContentPlayout()}setContinueRate(e){this.continuePlayout.setContinueRate(e)}tickContinueContentPlayout(){this.continuePlayout.tickContinueContentPlayout()}resolveResumeTarget(t,n=null){const i=null!==n,a=this.state.resumeAnchorIsCue&&!i,r=i?n:a?t.duration:this.state.adSecondsPlayedThisBreak,o=s({anchor:this.state.contentPositionAtBreakStart,duration:t.duration,adDuration:r,...i?{}:{insertionType:this.state.adInsertionType},...i?{resumeOffset:n}:"number"==typeof t.resumeOffset?{resumeOffset:t.resumeOffset}:{}});if(o.clamped&&this.deps.diagnose?.("DA-RESUME-OFFSET-CLAMPED",`Resume offset shortened to the ${o.offset}s of ad actually played for break ${t.id}`,{breakId:t.id,offset:o.offset,source:o.source}),!o.seek)return this.deps.log(`Resume: no seek for break ${t.id} (offset 0, ${o.source})`),null;this.deps.log(`Resume-seek to ${o.target}s (offset ${o.offset}s, ${o.source}) for break ${t.id}`);const u=this.resumeAlignSummary(),l=this.liveSafeSeekEnd();this.deps.diagnose?.("DA-RESUME-SEEK",`Resume-seek to ${o.target}s (offset ${o.offset}s, ${o.source}) for break ${t.id}`,{breakId:t.id,target:o.target,offset:o.offset,source:o.source,anchorPosition:this.state.contentPositionAtBreakStart,anchorIsCue:this.state.resumeAnchorIsCue,anchorSeekableEnd:this.state.resumeAnchorSeekableEnd,anchorCushionSec:this.resumeAnchorCushionSec(),safeEnd:l,seekableEndNow:this.contentSeekableEnd(),liveEdge:this.deps.contentPlayer.liveEdge??null,unreachableEdgeSec:d(this.deps.contentPlayer),...u.catchUpRan?u:{}});(this.state.breakTimebase===e.PTS||this.state.breakTimebase===e.MEDIATIME)&&"number"==typeof t.start&&this.state.callbacks?.onContentResumeSeek?.(o.target,t.start+t.duration);let c=o.target,m=0;null!==l&&c>l&&(m=Math.round(1e3*(c-l))/1e3,c=l);let p=m>0?"end":null;const g=this.liveSafeSeekStart();if(null!==g&&c<g){const e=Math.round(1e3*(g-c))/1e3,s=this.liveSeekableWindowSec(),n=null===s?null:.5*s;null!==n&&e<=n?(m=e,c=g,p="start"):(this.deps.log(`Resume-seek: ${o.target}s is ${e}s behind the element's live window start (window ${s}s); not clamping`),this.deps.diagnose?.("DA-RESUME-START-CLAMP-SKIPPED",`Resume target ${o.target}s for break ${t.id} is ${e}s behind the element's seekable start but the gap exceeds half the ${s}s window; left unchanged`,{breakId:t.id,requested:o.target,seekableStart:g,behindSec:e,windowSec:s,maxBehindSec:n,...this.resumeAlignSummary()}))}if(null!==this.state.resumePrepositionedTo&&Math.abs(this.deps.contentPlayer.currentTime-c)<=h){const e=this.deps.contentPlayer.currentTime;return this.deps.log(`Resume: content already at ${e}s for break ${t.id} (pre-positioned during the break)`+(m>0?` — ${m}s short of policy, those frames are not published yet`:"")+" — no seek"),this.deps.diagnose?.("DA-RESUME-PREPOSITIONED",`Break ${t.id} resumed without a seek: content was moved to ${e}s during the break`+(m>0?` (${m}s short of the ${o.target}s policy target)`:""),{breakId:t.id,target:o.target,prepositionedTo:this.state.resumePrepositionedTo,landed:e,shortfallSec:m,exact:0===m,...this.resumeAlignSummary()}),null}return m>0&&(this.deps.log("start"===p?`Resume-seek clamped: ${o.target}s is behind the live window start (already evicted); seeking ${c}s instead (${m}s early)`:`Resume-seek clamped: ${o.target}s is beyond what the stream has published; seeking ${c}s instead (${m}s short)`),this.deps.diagnose?.("DA-RESUME-TARGET-CLAMPED","start"===p?`Resume target ${o.target}s for break ${t.id} is behind the live window start (already evicted); clamped to ${c}s`:`Resume target ${o.target}s for break ${t.id} is beyond the published window; clamped to ${c}s`,{breakId:t.id,requested:o.target,clamped:c,shortfallSec:m,bound:p,...this.resumeAlignSummary()})),c}applyResumeSeek(e,t=null){const s=this.resolveResumeTarget(e,t);null!==s&&(this.probeResumeLanding(e.id,s),this.deps.contentPlayer.seek(s))}}
|
|
1
|
+
import{Timebase as e,isPostRollBreak as t}from"../../types/BreakManifest";import{resolveResumePoint as s}from"../resolveResumePoint";import{resolveResumeClamp as n}from"../resolveResumeClamp";import{BREAK_CUT_LATE_TOLERANCE_SEC as r,resolveWallclockBreakWindow as a}from"../wallclockBreakWindow";import{ContinueContentPlayout as i}from"./ContinueContentPlayout";import{StreamClockResume as o}from"./streamClockResume";import{PdtClockResume as u,programDateTimeSample as l}from"./pdtClockResume";import{contentSeekableEnd as d,isPlausibleSeekEnd as h,liveEdgeDistanceSec as c,liveSafeSeekEnd as m,liveSafeSeekStart as p,liveSeekableWindowSec as g,unreachableEdgeSec as k}from"./liveEdge";const S=.04,f=e=>Math.round(1e3*e)/1e3;export class ContentResumeCoordinator{constructor(e,t){this.state=e,this.deps=t,this.cueAnchorResolver=null,this.resumePolicyTarget=null,this.cueEndResolver=null,this.continuePlayout=new i(this.state,{contentPlayer:this.deps.contentPlayer,clock:this.deps.clock,gateContentForBreak:()=>this.deps.gateContentForBreak(),distanceToLiveEdgeSec:()=>this.distanceToLiveEdgeSec(),plannedResumeTarget:e=>this.plannedResumeTarget(e),diagnose:(e,t,s)=>this.deps.diagnose(e,t,s),log:(...e)=>this.deps.log(...e)}),this.streamClock=new o({contentPlayer:this.deps.contentPlayer,diagnose:(e,t,s)=>this.deps.diagnose(e,t,s),activeBreak:()=>this.state.currentBreak,anchorIsCue:()=>this.state.resumeAnchorIsCue,breakEnd:e=>this.cueEndResolver?.(e)??null,computedTarget:e=>this.computedResumeTarget(e),resumesAtBreakEnd:e=>this.resumesAtBreakEnd(e),now:()=>this.deps.clock.now()}),this.pdtClock=new u({contentPlayer:this.deps.contentPlayer,diagnose:(e,t,s)=>this.deps.diagnose(e,t,s),anchorIsPdt:()=>this.state.resumeAnchorIsPdt,computedTarget:e=>this.computedResumeTarget(e),resumesAtBreakEnd:e=>this.resumesAtBreakEnd(e),now:()=>this.deps.clock.now()})}setCueEndResolver(e){this.cueEndResolver=e}ingestStreamClockCue(e){this.streamClock.ingestCue(e)}noteContentReload(){this.streamClock.noteReload()}dispose(){this.streamClock.cancelLanding(),this.pdtClock.cancelLanding()}setCueAnchorResolver(e){this.cueAnchorResolver=e}captureBreakAnchor(e){this.resetBreakResumeState();const t=this.cueAnchorResolver?.(e)??null,s=null!==t&&Number.isFinite(t),n=s?null:this.pdtBreakAnchor(e);this.state.contentPositionAtBreakStart=s?t:n??this.deps.contentPlayer.currentTime,this.state.resumeAnchorIsCue=s,this.state.resumeAnchorIsPdt=null!==n,this.state.resumeAnchorSeekableEnd=this.contentSeekableEnd(),this.streamClock.beginBreak(),this.pdtClock.beginBreak()}pdtBreakAnchor(t){if(this.state.breakTimebase!==e.WALLCLOCK||"string"!=typeof t.start)return null;const s=l(this.deps.contentPlayer);if(!s)return null;const n=a({startMs:Date.parse(t.start),durationSec:t.duration??0,sample:s});return n?(n.sampleLateSec>r&&this.deps.diagnose?.("DA-BREAK-CUT-LATE",`Break ${t.id}: content was cut ${n.sampleLateSec}s after the break start on the stream clock; the viewer saw the stream's own break until then`,{breakId:t.id,lateSec:n.sampleLateSec,start:t.start,cutPdt:new Date(s.programDateTimeMs).toISOString(),cutAt:f(s.currentTime),anchor:n.anchor}),n.anchor):null}reanchorMidBreak(e){this.state.contentPositionAtBreakStart=e,this.state.resumeAnchorIsCue=!1,this.state.resumeAnchorIsPdt=!1,this.state.resumeAnchorSeekableEnd=this.contentSeekableEnd()}liveEdgeDistanceSec(){return c(this.deps.contentPlayer)}liveSafeSeekEnd(){return m(this.deps.contentPlayer)}liveSafeSeekStart(){return p(this.deps.contentPlayer)}liveSeekableWindowSec(){return g(this.deps.contentPlayer)}contentSeekableEnd(){return d(this.deps.contentPlayer)}resumeAnchorCushionSec(){const e=this.state.resumeAnchorSeekableEnd;return null!==e&&Number.isFinite(e)?f(e-this.state.contentPositionAtBreakStart):null}isPlausibleSeekEnd(e){return h(this.deps.contentPlayer,e)}plannedResumeTarget(e){const t=this.computedResumeTarget(e);return null===t?null:this.streamClock.target(e)??this.pdtClock.target(e)??this.cueEndTarget(e)??t}computedResumeTarget(e){if(t(e))return null;const s=this.resolvePolicy(e,this.state.contentPositionAtBreakStart);return s.seek?s.target:null}resumesAtBreakEnd(e){if(t(e))return!1;const s=this.resolvePolicy(e,0);return s.seek&&Math.abs(s.offset-(e.duration??0))<.001}resolvePolicy(e,t){return s({anchor:t,duration:e.duration,adDuration:e.duration,...this.state.adInsertionType?{insertionType:this.state.adInsertionType}:{},..."number"==typeof e.resumeOffset?{resumeOffset:e.resumeOffset}:{}})}cueEndTarget(e){if(!this.state.resumeAnchorIsCue||!this.resumesAtBreakEnd(e))return null;const t=this.cueEndResolver?.(e)?.mediaTime;return"number"==typeof t&&Number.isFinite(t)?t:null}maybePrepositionContentForResume(){if("shared-element"===this.deps.getInsertionMode())return;if(!this.state.contentPaused||null!==this.state.resumePrepositionedTo)return;if(null!==this.state.resumePrepositionSettle)return;if(this.state.resumeCatchupActive)return;if(Number.isFinite(this.deps.contentPlayer.duration))return;const e=this.state.currentBreak;if(!e)return;const t=this.plannedResumeTarget(e);if(null===t)return;const s=this.contentSeekableEnd();if(null===s)return;const n=this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec,r=(e.duration??0)-n;let a;if(t<=s)a=t;else{if(!(r<=3))return;a=s}this.deps.log(a===t?`Resume pre-positioning: seeking content to ${t}s during the break (window reaches ${s}s)`:`Resume pre-positioning: window only reaches ${s}s and the ad ends in ${r}s — moving content as close to ${t}s as exists`),this.streamClock.noteSeek(a),this.streamClock.arm(),this.deps.contentPlayer.seek(a);const i=setTimeout(()=>{this.state.resumePrepositionSettle=null;const e=this.deps.contentPlayer.currentTime;Math.abs(e-a)<=S?(this.state.resumePrepositionedTo=e,this.deps.log(`Resume pre-positioned: content is at ${e}s`)):this.deps.log(`Resume pre-position rejected: asked ${a}s, landed ${e}s — window is short, will retry`)},250);this.state.resumePrepositionSettle=i}maybeCatchUpContentForResume(e){if("shared-element"===this.deps.getInsertionMode())return;if(!this.state.contentPaused||Number.isFinite(this.deps.contentPlayer.duration))return;const t=this.plannedResumeTarget(e);if(null===t)return;const s=this.deps.contentPlayer.currentTime;if(this.state.resumeCatchupActive){const n=(e.duration??0)-(this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec);if(s>t+1)return this.state.resumeCatchupActive=!1,this.deps.contentPlayer.pause(),this.state.resumePrepositionedTo=this.deps.contentPlayer.currentTime,void this.deps.log(`Resume catch-up halted: content reached ${this.deps.contentPlayer.currentTime}s, past the ${t}s target`);if(n<=30&&n>=1){this.state.resumeAlignTicks+=1;const e=s+n,r=e-t;if(Math.abs(r)>S){const a=s-r,i=this.liveSafeSeekEnd();if(null!==i){const e=i>this.state.lastResumeSafeEnd+.25;e&&Number.isFinite(this.state.lastResumeSafeEnd)&&this.state.resumeSawSteps.push(Math.round(100*(i-this.state.lastResumeSafeEnd))/100),i>this.state.lastResumeSafeEnd&&(this.state.lastResumeSafeEnd=i);const s=i-a;if(s>this.state.resumeBestSawMarginSec&&(this.state.resumeBestSawMarginSec=s),a>i)return void(this.state.resumeAlignBlockedDiagnosed||(this.state.resumeAlignBlockedDiagnosed=!0,this.deps.log(`Resume align waiting: ${a}s is past the published edge ${i}s (${Math.round(1e3*(a-i))}ms short) — rolling until a manifest step brings it in reach`),this.deps.diagnose?.("DA-RESUME-ALIGN-BLOCKED",`Resume alignment cannot reach ${t}s yet: the published edge is ${i}s`,{target:t,requested:a,safeEnd:i,shortfallSec:Math.round(1e3*(a-i))/1e3,remainingSec:n})));if(!e&&this.state.resumeSawSeekDone)return;this.state.resumeSawSeekDone=!0}if(this.state.lastResumeAlignRemainingSec-n<.5)return;this.state.lastResumeAlignRemainingSec=n,this.deps.log(`Resume align: landing ${e}s vs target ${t}s (${Math.round(1e3*r)}ms) — content to ${a}s, ${n}s of ad left`),this.state.resumeAlignCorrectionsMs.push(Math.round(1e3*r)),this.streamClock.noteSeek(a),this.deps.contentPlayer.seek(a)}}return}if(t-s<=S)return;const n=this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec,r=(e.duration??0)-n;if(r<=0)return;if(r>30)return;const a=this.contentSeekableEnd();if(null===a)return;const i=t-r+.3,o=Math.min(i,a);this.state.resumeCatchupActive=!0,this.state.resumeCatchupRan=!0,this.state.contentMutedBeforeCatchup=this.deps.contentPlayer.muted,this.deps.contentPlayer.muted=!0,this.deps.log(`Resume catch-up: content to ${o}s, playing toward ${t}s with ${r}s of ad left`),this.deps.diagnose?.("DA-RESUME-CATCHUP",`Break ${e.id}: playing content from ${o}s toward ${t}s with ${r}s of ad left`,{breakId:e.id,startPos:o,target:t,remainingAdSec:Math.round(1e3*r)/1e3,publishedEnd:a,clampedStart:o<i}),Math.abs(s-o)>S&&(this.streamClock.noteSeek(o),this.deps.contentPlayer.seek(o)),this.streamClock.arm(),this.deps.contentPlayer.play()}endResumeCatchup(e=!1){this.state.resumeCatchupActive&&(this.state.resumeCatchupActive=!1,e||this.deps.contentPlayer.pause()),null!==this.state.contentMutedBeforeCatchup&&(this.deps.contentPlayer.muted=this.state.contentMutedBeforeCatchup,this.state.contentMutedBeforeCatchup=null)}get resumeHandoverInMotion(){return this.state.resumeCatchupActive}resumeAlignSummary(){const e=this.state.resumeAlignCorrectionsMs;return{catchUpRan:this.state.resumeCatchupRan||this.state.resumeCatchupActive||e.length>0||null!==this.state.resumePrepositionedTo,alignCorrections:e.length,placedOnStep:this.state.resumeSawSeekDone,alignTicks:this.state.resumeAlignTicks,sawSteps:this.state.resumeSawSteps.slice(0,12),sawStepCount:this.state.resumeSawSteps.length,bestSawMarginSec:Number.isFinite(this.state.resumeBestSawMarginSec)?Math.round(1e3*this.state.resumeBestSawMarginSec)/1e3:null,alignCorrectionsMs:e.slice(0,12),alignLastErrorMs:e.length?e[e.length-1]:null}}cancelResumeLandedProbe(){const e=this.state.resumeLandedProbe;this.state.resumeLandedProbe=null,e?.()}probeResumeLanding(e,t,s=null){this.cancelResumeLandedProbe();let n=!1;const r=(r,a)=>{if(n)return;n=!0,this.cancelResumeLandedProbe();const i=null===r?null:Math.round(1e3*(r-t))/1e3,o=null===r||null===s?null:Math.round(1e3*(r-s))/1e3,u=null!==s&&Math.abs(s-t)>=5e-4&&null!==o?`; ${Math.abs(o)}s ${o<0?"before":"after"} the ${s}s resume point`:"";this.deps.log(null===r?`Resume landing unknown for break ${e} (${a})`:`Resume landed at ${r}s, ${i}s from the ${t}s asked for${u} (break ${e})`),this.deps.diagnose?.("DA-RESUME-LANDED",null===r?`Resume landing for break ${e} could not be measured (${a})`:`Resume for break ${e} landed at ${r}s, ${i}s from the ${t}s requested${u}`,{breakId:e,target:t,landed:r,errorSec:i,...null===s?{}:{policyTarget:s,policyErrorSec:o},...a?{reason:a}:{}})},a=()=>{this.streamClock.updateLanding(e,this.deps.contentPlayer.currentTime),r(this.deps.contentPlayer.currentTime)},i=setTimeout(()=>r(null,"no seeked event"),2e3);this.state.resumeLandedProbe=()=>{clearTimeout(i),this.deps.contentPlayer.off("seeked",a)},this.deps.contentPlayer.on("seeked",a)}async waitForLiveEdge(e=()=>!0){for(let t=0;t<20;t++){if(!e())return null;const t=this.deps.contentPlayer.videoElement,s=t?.seekable;if(s&&s.length>0){const e=s.end(s.length-1);if(Number.isFinite(e)&&e>0)return e}if(await new Promise(e=>setTimeout(e,100)),!e())return null}return this.deps.log("shared-element: no seekable range after restore; staying at the live edge"),null}resetBreakResumeState(){this.state.resumeAnchorIsCue=!1,this.state.resumeAnchorIsPdt=!1,this.state.resumeAnchorSeekableEnd=null,this.state.resumePrepositionedTo=null,this.state.resumeCatchupActive=!1,this.state.resumeCatchupRan=!1,this.state.contentMutedBeforeCatchup=null,this.state.resumeAlignCorrectionsMs=[],this.state.lastResumeAlignRemainingSec=Number.POSITIVE_INFINITY,this.state.resumeAlignBlockedDiagnosed=!1,this.state.lastResumeSafeEnd=Number.NEGATIVE_INFINITY,this.state.resumeSawSeekDone=!1,this.state.resumeBestSawMarginSec=Number.NEGATIVE_INFINITY,this.state.resumeAlignTicks=0,this.state.resumeSawSteps=[],this.state.resumePrepositionSettle&&(clearTimeout(this.state.resumePrepositionSettle),this.state.resumePrepositionSettle=null),this.state.adSecondsPlayedThisBreak=0,this.state.currentAssetElapsedSec=0}beginContinueContentPlayout(){this.continuePlayout.beginContinueContentPlayout()}distanceToLiveEdgeSec(){const e=this.contentSeekableEnd();return null===e?null:f(e-this.deps.contentPlayer.currentTime)}secondsUntilCut(e){return this.continuePlayout.secondsUntilCut(e)}restoreContinueQuality(e){this.continuePlayout.restoreContinueQuality(e)}currentContinueDriftSec(){return this.continuePlayout.currentContinueDriftSec()}abandonContinueContentPlayout(e){this.continuePlayout.abandonContinueContentPlayout(e)}endContinueContentPlayout(){this.continuePlayout.endContinueContentPlayout()}setContinueRate(e){this.continuePlayout.setContinueRate(e)}tickContinueContentPlayout(){this.continuePlayout.tickContinueContentPlayout()}resolveResumeTarget(t,r=null){this.resumePolicyTarget=null;const a=null!==r,i=(this.state.resumeAnchorIsCue||this.state.resumeAnchorIsPdt)&&!a,o=a?r:i?t.duration:this.state.adSecondsPlayedThisBreak,u=s({anchor:this.state.contentPositionAtBreakStart,duration:t.duration,adDuration:o,...a?{}:{insertionType:this.state.adInsertionType},...a?{resumeOffset:r}:"number"==typeof t.resumeOffset?{resumeOffset:t.resumeOffset}:{}});if(u.clamped&&this.deps.diagnose?.("DA-RESUME-OFFSET-CLAMPED",`Resume offset shortened to the ${u.offset}s of ad actually played for break ${t.id}`,{breakId:t.id,offset:u.offset,source:u.source}),!u.seek)return this.deps.log(`Resume: no seek for break ${t.id} (offset 0, ${u.source})`),null;const l=this.streamClock.handBackTarget(t,a)??this.pdtClock.handBackTarget(t,a),d=null!==l||a?null:this.reportCueEnd(t,u.target),h=l??d??u.target;this.resumePolicyTarget=h;const c=this.streamClock.seekBasis(t)??this.pdtClock.seekBasis()??(null!==d?"cue_end":`offset ${u.offset}s, ${u.source}`);this.deps.log(`Resume-seek to ${h}s (${c}) for break ${t.id}`);const m=this.resumeAlignSummary(),p=this.liveSafeSeekEnd();this.deps.diagnose?.("DA-RESUME-SEEK",`Resume-seek to ${h}s (${c}) for break ${t.id}`,{breakId:t.id,target:h,offset:u.offset,source:u.source,...h===u.target?{}:{computedTarget:u.target},...null===d?{}:{cueEndTarget:d},...this.streamClock.seekContext(t,h,u.target),...this.pdtClock.seekContext(t,h,u.target),anchorPosition:this.state.contentPositionAtBreakStart,anchorIsCue:this.state.resumeAnchorIsCue,anchorIsPdt:this.state.resumeAnchorIsPdt,anchorSeekableEnd:this.state.resumeAnchorSeekableEnd,anchorCushionSec:this.resumeAnchorCushionSec(),safeEnd:p,seekableEndNow:this.contentSeekableEnd(),liveEdge:this.deps.contentPlayer.liveEdge??null,unreachableEdgeSec:k(this.deps.contentPlayer),...m.catchUpRan?m:{}}),this.streamClock.reportCorrection(t,h,u.target,a),this.pdtClock.reportCorrection(t,h,u.target,a);(this.state.breakTimebase===e.PTS||this.state.breakTimebase===e.MEDIATIME)&&"number"==typeof t.start&&this.state.callbacks?.onContentResumeSeek?.(h,t.start+t.duration);const g=this.liveSafeSeekStart(),f=n({target:h,safeStart:g,safeEnd:p,windowSec:this.liveSeekableWindowSec()}),{target:C,shortfallSec:P,bound:b}=f;if(f.startClampSkipped){const{behindSec:e,windowSec:s,maxBehindSec:n}=f.startClampSkipped;this.deps.log(`Resume-seek: ${h}s is ${e}s behind the element's live window start (window ${s}s); not clamping`),this.deps.diagnose?.("DA-RESUME-START-CLAMP-SKIPPED",`Resume target ${h}s for break ${t.id} is ${e}s behind the element's seekable start but the gap exceeds half the ${s}s window; left unchanged`,{breakId:t.id,requested:h,seekableStart:g,behindSec:e,windowSec:s,maxBehindSec:n,...this.resumeAlignSummary()})}if(null!==this.state.resumePrepositionedTo&&Math.abs(this.deps.contentPlayer.currentTime-C)<=S){const e=this.deps.contentPlayer.currentTime;return this.deps.log(`Resume: content already at ${e}s for break ${t.id} (pre-positioned during the break)`+(P>0?` — ${P}s short of policy, those frames are not published yet`:"")+" — no seek"),this.deps.diagnose?.("DA-RESUME-PREPOSITIONED",`Break ${t.id} resumed without a seek: content was moved to ${e}s during the break`+(P>0?` (${P}s short of the ${h}s policy target)`:""),{breakId:t.id,target:h,prepositionedTo:this.state.resumePrepositionedTo,landed:e,shortfallSec:P,exact:0===P,...this.resumeAlignSummary()}),this.streamClock.armLanding(t,e),this.pdtClock.armLanding(t),null}return P>0&&(this.deps.log("start"===b?`Resume-seek clamped: ${h}s is behind the live window start (already evicted); seeking ${C}s instead (${P}s early)`:`Resume-seek clamped: ${h}s is beyond what the stream has published; seeking ${C}s instead (${P}s short)`),this.deps.diagnose?.("DA-RESUME-TARGET-CLAMPED","start"===b?`Resume target ${h}s for break ${t.id} is behind the live window start (already evicted); clamped to ${C}s`:`Resume target ${h}s for break ${t.id} is beyond the published window; clamped to ${C}s`,{breakId:t.id,requested:h,clamped:C,shortfallSec:P,bound:b,...this.resumeAlignSummary()})),this.streamClock.armLanding(t,C),this.pdtClock.armLanding(t),C}reportCueEnd(e,t){const s=this.state.resumeAnchorIsCue?this.cueEndResolver?.(e):null;if(!s||!Number.isFinite(s.mediaTime))return null;const n=this.resumesAtBreakEnd(e),r=f(s.mediaTime-t);return this.deps.diagnose("DA-RESUME-CUE-END",n?`Break ${e.id}: resuming on the stream's cue_end at ${s.mediaTime}s (${r}s from the computed ${t}s)`:`Break ${e.id}: cue_end at ${s.mediaTime}s not used; the resume policy does not resume at the break end`,{breakId:e.id,computedTarget:t,cueEndTarget:s.mediaTime,deltaSec:r,applied:n,stage:"hand-back",corrected:!1}),n?s.mediaTime:null}applyResumeSeek(e,t=null){const s=this.resolveResumeTarget(e,t);null!==s&&(this.probeResumeLanding(e.id,s,this.resumePolicyTarget),this.streamClock.noteSeek(s),this.deps.contentPlayer.seek(s))}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Vendor as t}from"../../types/BreakManifest";import{GamPodAdTracker as e}from"../GamPodAdTracker";import{GamStreamManager as
|
|
1
|
+
import{Vendor as t}from"../../types/BreakManifest";import{GamPodAdTracker as e}from"../GamPodAdTracker";import{GAM_DUPLICATE_METADATA_DIAGNOSTIC_MESSAGE as s,GamStreamManager as a}from"../GamStreamManager";import{isGamVendorParameters as i}from"../GamPodUrlBuilder";import{gamPodIdentityMismatches as d}from"../resolveGamVendorConfiguration";export class GamPodBridge{constructor(t,e){this.state=t,this.deps=e,this.identityMismatchAssetId=null,this.midBreakEndPosition=null,this.callbacks={}}setConfig(t){this.state.gamConfig=t}setEventCallbacks(t){this.callbacks=t}get eventCallbacks(){return this.callbacks}async startGamSession(t,e){this.state.currentGamIdentity=t,this.state.sessionAssetParameters=e,this.state.pushedAssetParameters=null,await this.initializeGam()}endGamSession(){this.state.gamStreamManager?.reset(),this.state.gamInitPromise=null,this.state.currentGamIdentity=void 0,this.state.sessionAssetParameters=void 0,this.state.manifestAssetParameters=void 0,this.state.sessionAssetParameterMacros={},this.state.updatedAssetParameters=void 0,this.state.pushedAssetParameters=null,this.state.earlyGamPodAdStart=null}async initializeGam(){if(!this.state.currentGamIdentity)return;if(this.deps.log("Initializing GAM stream manager..."),this.state.gamStreamManager)this.state.gamStreamManager.reset();else{const t=this.deps.isSharedElement()?this.deps.contentPlayer.videoElement:this.deps.getAdPlayer()?.videoElement;if(!t)return void console.warn("[OptiViewAds] GAM requires an adapter that exposes videoElement. GAM will be skipped.");const e=this.deps.isSharedElement()?void 0:this.deps.getAdContainer()||void 0;this.state.gamStreamManager=new a(t,e,this.deps.debug)}this.state.gamStreamManager.setCallbacks({onAdFirstQuartile:()=>this.emitGamQuartile(this.callbacks.onAdFirstQuartile),onAdMidpoint:()=>this.emitGamQuartile(this.callbacks.onAdMidpoint),onAdThirdQuartile:()=>this.emitGamQuartile(this.callbacks.onAdThirdQuartile),onAdStarted:t=>{this.deps.log("GAM ad started"),this.onGamPodAdStarted(t)},onAdComplete:()=>{this.deps.log("GAM ad complete"),"per-ad"===this.state.gamPodMode&&this.state.gamPodTracker&&this.emitGamPodSteps(this.state.gamPodTracker.onAdComplete())},onAdBreakEnded:()=>{this.deps.log("GAM ad break ended");const t=this.deps.getAdPlayerStreamRemainingSec();t>.25?this.onAdBreakEndedWithPodLeft(t):this.deps.onAdEnded()},onError:t=>this.deps.log("GAM error:",t),onDuplicateMetadata:t=>{this.deps.diagnose("DA-GAM-DUPLICATE-METADATA",s,{assetId:this.state.gamPodAsset?.id,startTime:t})}});const t=this.state.currentGamIdentity,e=this.deps.resolveSessionAdTagParameters(),i={networkCode:t.networkCode,customAssetKey:t.customAssetKey,...e&&{adTagParameters:e},...this.state.gamConfig?.streamActivityMonitorId&&{streamActivityMonitorId:this.state.gamConfig.streamActivityMonitorId}};this.state.gamInitPromise=this.state.gamStreamManager.initialize(i);try{await this.state.gamInitPromise,this.deps.log("GAM stream manager initialized, streamId:",this.state.gamStreamManager.getStreamId())}catch(t){throw this.state.gamInitPromise=null,t}}isGamEnabled(){return!1!==this.state.gamConfig?.enabled}isGamReady(){return!0===this.state.gamStreamManager?.isInitialized()}isGamPodAsset(e){return"vendor"===e.type&&e.vendor===t.GAM&&this.isGamReady()}isExpandingPodAsset(t){return("pending"===this.state.gamPodMode||"per-ad"===this.state.gamPodMode)&&(!!t&&t.id===this.state.gamPodAsset?.id)}beginGamPodTracking(t){this.resetGamPodTracking(),this.state.gamPodAsset=t,this.state.gamPodTracker=new e(t),this.state.gamPodMode="pending";const s=this.state.earlyGamPodAdStart;this.state.earlyGamPodAdStart=null,s&&this.onGamPodAdStarted(s)}fallBackToPodLevelReporting(){if("pending"!==this.state.gamPodMode)return;this.state.gamPodMode="legacy",this.state.gamPodTracker=null;const t=this.state.withheldPodAdBegin;this.state.withheldPodAdBegin=null,t&&this.deps.emitStep(t)}resetGamPodTracking(){this.state.gamPodMode="none",this.state.gamPodTracker=null,this.state.gamPodAsset=null,this.state.withheldPodAdBegin=null,this.midBreakEndPosition=null}onGamPodAdProgress(t){"pending"===this.state.gamPodMode&&(t<2||(this.deps.log("No IMA pod ad reported after the pod started playing; reporting the pod as a single ad"),this.fallBackToPodLevelReporting()))}onGamPodAdStarted(t){t&&("none"!==this.state.gamPodMode?this.state.gamPodTracker&&("pending"===this.state.gamPodMode&&(this.state.withheldPodAdBegin=null,this.state.gamPodMode="per-ad"),"per-ad"===this.state.gamPodMode&&(this.logRestartedPodPosition(t),this.emitGamPodSteps(this.state.gamPodTracker.onAdStarted(t)))):this.state.earlyGamPodAdStart=t)}logRestartedPodPosition(t){const e=this.midBreakEndPosition;null!==e&&(this.midBreakEndPosition=null,"number"==typeof t.adPosition&&t.adPosition<=e&&this.deps.log(`IMA restarted pod ad positions after a mid-pod ad break end (ad ${t.adPosition} after ad ${e})`))}onAdBreakEndedWithPodLeft(t){const e="per-ad"===this.state.gamPodMode?this.state.gamPodTracker:null;if(!e||e.reportedLastAd)return this.deps.log(`GAM pod has ${t.toFixed(3)}s of slate left; playing it out`),void(e&&this.emitGamPodSteps(e.finish()));const s=e.lastAdPosition,a=e.lastTotalAds,i=a>0?` of ${a}`:"";this.deps.log(`IMA ad break ended after ad ${s}${i}; ${t.toFixed(3)}s of pod left, waiting for the next ad`),this.deps.diagnose("DA-GAM-POD-MID-BREAK-END",`IMA reported the ad break ended after ad ${s}${i} with ${t.toFixed(3)}s of pod left; per-ad reporting continues until the pod ends.`,{assetId:this.state.gamPodAsset?.id,adPosition:s,totalAds:a,remainingSec:t}),this.midBreakEndPosition=s,this.emitGamPodSteps(e.onAdBreakEnded())}emitGamPodSteps(t){const e=this.deps.getCurrentBreak();if(e)for(const s of t){const t={adId:s.adId,creativeId:s.creativeId,podPosition:s.adPosition};"adbegin"===s.type?this.deps.emitAdStart(e,s.asset,s.adIndex,s.totalAds,t):this.deps.emitAdEnd(e,s.asset,s.adIndex,s.totalAds,t)}}emitGamQuartile(t){const e=this.deps.getCurrentBreak();if(!t||!e)return;const s="per-ad"===this.state.gamPodMode?this.state.gamPodTracker?.currentAd:null;if(s)return void t(e,s.asset,s.adIndex,s.totalAds);const a=this.deps.getCurrentAsset();a&&t(e,a)}async buildGamUrl(t,e){if(this.identityMismatchAssetId=null,!i(t.vendorParameters))return this.deps.log("Invalid GAM vendor parameters:",t.vendorParameters),null;if(this.state.gamInitPromise&&await this.state.gamInitPromise,!this.state.gamStreamManager?.isInitialized())throw this.deps.log("GAM stream manager not initialized"),new Error("GAM stream manager not initialized. The stream identity comes from the break manifest's vendorConfiguration — check for DA-GAM-CONFIG-MISSING.");const s=t.vendorParameters;if(this.state.currentGamIdentity){const e=d(this.state.currentGamIdentity,s);for(const s of e)this.deps.diagnose("DA-GAM-POD-IDENTITY-MISMATCH",`GAM asset ${t.id} skipped: vendorParameters.${s.field} "${s.assetValue}" does not match the stream session's "${s.sessionValue}".`,{assetId:t.id,field:s.field,assetValue:s.assetValue,sessionValue:s.sessionValue});if(e.length>0)return this.identityMismatchAssetId=t.id,null}this.state.earlyGamPodAdStart=null,this.deps.pushAssetParameters(t.assetParameters,t.id);const a=this.state.gamStreamManager.buildPodUrl(t.uri,e,s);return this.deps.log("Built GAM pod URL:",a),a}processTimedMetadata(t){this.state.gamStreamManager?.processTimedMetadata(t)}destroy(){this.resetGamPodTracking(),this.state.earlyGamPodAdStart=null,this.state.gamStreamManager?.destroy(),this.state.gamStreamManager=null,this.state.gamInitPromise=null}hasIdentityMismatch(t){return this.identityMismatchAssetId===t.id}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BreakFormat as e,isPostRollBreak as t}from"../../types/BreakManifest";import{armPlaybackCutoff as s,clearPlaybackCutoff as a}from"./PlaybackCutoff";import{AdError as n}from"../../types/AdError";import{classifyMediaError as r,toClassifiedAdError as i}from"./classifyMediaError";function d(e){return e?{src:e.getAttribute("src"),srcObject:e.srcObject,sourceChild:null!==e.querySelector("source")}:null}export class SharedElementBreakPlayer{constructor(e,t){this.state=e,this.deps=t,this.contentElementAdPlayer=null,this.sharedAdSource=null,this.sharedAdEndedHandler=null,this.sharedAdTimeUpdateHandler=null,this.sharedAdTimedMetadataHandler=null,this.sharedAdWaitingHandler=null,this.sharedAdPlayingHandler=null,this.sharedAdFirstFrameTimer=null,this.sharedRestoreWatchdog=null,this.sharedRestore=null,this.sharedRestoreGeneration=0,this.sharedActivityGeneration=0}sharedElementAdPlayer(){const e=this.deps.contentPlayer.videoElement;if(!e)return null;if(this.contentElementAdPlayer?.videoElement===e)return this.contentElementAdPlayer;this.releaseContentElementAdPlayer();const t=this.deps.createAdAdapter(document.createElement("div"),e);return t.videoElement!==e?(this.deps.log("shared-element: createAdAdapter ignored the content element; falling back to the content player for ad playback"),t.destroy(),null):(this.contentElementAdPlayer=t,t)}async sharedAdStep(e,t,s){try{await s()}catch(s){const a=t.videoElement,i=s instanceof Error?s.message:String(s),d=new n(`shared-element ${e} failed: ${i} (readyState=${a?.readyState??"-"} paused=${a?.paused??"-"} networkState=${a?.networkState??"-"})`,r(s));throw d.cause=s,d}}clearSharedAdFirstFrameWatchdog(){this.sharedAdFirstFrameTimer&&(clearTimeout(this.sharedAdFirstFrameTimer),this.sharedAdFirstFrameTimer=null)}armSharedAdFirstFrameWatchdog(e,t){this.clearSharedAdFirstFrameWatchdog(),this.sharedAdFirstFrameTimer=setTimeout(()=>{if(this.sharedAdFirstFrameTimer=null,!this.state.isPlaying||!this.state.currentBreak)return;const s=e.videoElement,a=this.deps.contentPlayer.videoElement,r=s?.currentSrc??"",i=`readyState=${s?.readyState??"-"} networkState=${s?.networkState??"-"} paused=${s?.paused??"-"} connected=${s?.isConnected??"-"} srcKind=${r.startsWith("blob:")?"blob":r?"url":"none"} sameElementAsContentPlayer=${String(s===a)}`;this.deps.log(`shared-element: no first frame within ${t}ms (${i})`),this.deps.diagnose("DA-AD-PLAYBACK-ERROR",`the ad never rendered a frame on the content element within ${t}ms`,{breakId:this.state.currentBreak.id,detail:i});const d=this.state.currentAsset;d&&this.state.callbacks?.onAdError(this.state.currentBreak,d,new n(`shared-element ad produced no frames within ${t}ms (${i})`,"DA-AD-START-TIMEOUT")),this.onSharedElementAdEnded()},t)}attachSharedAdListeners(e){this.sharedAdSource=e,this.sharedAdEndedHandler=()=>this.onSharedElementAdEnded(),e.on("ended",this.sharedAdEndedHandler),this.sharedAdTimeUpdateHandler=()=>{this.sharedAdFirstFrameTimer&&e.currentTime>0&&this.clearSharedAdFirstFrameWatchdog(),this.deps.reportAdProgress(e)},e.on("timeupdate",this.sharedAdTimeUpdateHandler),this.sharedAdTimedMetadataHandler=e=>{e&&this.deps.processTimedMetadata(e)},e.on("timedmetadata",this.sharedAdTimedMetadataHandler),this.sharedAdWaitingHandler=()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&this.state.callbacks?.onAdWaiting(this.state.currentBreak,this.state.currentAsset)},e.on("waiting",this.sharedAdWaitingHandler),this.sharedAdPlayingHandler=()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&!e.paused&&(this.clearSharedAdFirstFrameWatchdog(),this.state.startPlaybackCutoff?.(),this.state.callbacks?.onAdPlaying(this.state.currentBreak,this.state.currentAsset))},e.on("playing",this.sharedAdPlayingHandler)}detachSharedAdListeners(){const e=this.sharedAdSource??this.deps.contentPlayer;this.sharedAdSource=null,this.state.sharedAdClaimActive=!1,this.clearSharedAdFirstFrameWatchdog(),this.sharedAdTimedMetadataHandler&&(e.off("timedmetadata",this.sharedAdTimedMetadataHandler),this.sharedAdTimedMetadataHandler=null),this.sharedAdWaitingHandler&&(e.off("waiting",this.sharedAdWaitingHandler),this.sharedAdWaitingHandler=null),this.sharedAdPlayingHandler&&(e.off("playing",this.sharedAdPlayingHandler),this.sharedAdPlayingHandler=null),this.sharedAdTimeUpdateHandler&&(e.off("timeupdate",this.sharedAdTimeUpdateHandler),this.sharedAdTimeUpdateHandler=null),this.sharedAdEndedHandler&&(e.off("ended",this.sharedAdEndedHandler),this.sharedAdEndedHandler=null)}async releaseContentElementFromContentEngine(){if(this.deps.contentPlayer.releaseMediaElement)try{await this.deps.contentPlayer.releaseMediaElement()}catch(e){this.deps.log("shared-element: the content engine did not release the element:",e)}}releaseContentElementAdPlayer(){const e=this.contentElementAdPlayer;if(e){this.contentElementAdPlayer=null;try{e.destroy()}catch(e){this.deps.log("shared-element: releasing the ad player on the content element failed:",e)}}}async playAdThroughContentElement(){const e=this.state.currentAsset,t=this.state.currentBreak;if(!e||!t)return;const s=this.state.effectiveBreakDurationSec??t.duration,a=await this.deps.resolveAssetUri(e,s);if(!a)return void this.deps.log("shared-element: no URI for the running ad; leaving it on the ad element");const n=this.deps.getAdPlayer()?.currentTime??0;this.state.savedContentSrc=this.restorableContentSource(),this.state.savedContentTime=this.deps.contentPlayer.currentTime,this.state.savedContentLive=!Number.isFinite(this.deps.contentPlayer.duration),this.state.savedContentEdgeDistance=this.state.savedContentLive?this.deps.liveEdgeDistanceSec():null,this.state.sharedAdClaimActive=!0,await this.releaseContentElementFromContentEngine(),this.deps.rebindContentElementIfSwapped();const r=this.sharedElementAdPlayer()??this.deps.contentPlayer;try{this.deps.getAdPlayer()?.pause(),this.deps.clearContentGate(),await r.load(a),this.attachSharedAdListeners(r),n>0&&r.seek(n),await r.play(),this.deps.hide(),this.deps.log("shared-element: the ad now plays through the content element")}catch(e){this.deps.log("shared-element: could not move the ad onto the content element:",e),this.state.sharedAdClaimActive=!1,this.state.savedContentSrc=null,this.state.savedContentEdgeDistance=null}}async playSharedElementBreak(t,a){const r=this.deps.getVariant(t);if(!r)return this.deps.log("shared-element: no valid variant, skipping break:",t.id),void a.onBreakEnd(t);if(r.format===e.LSHAPE_CONTENT)return this.deps.log("shared-element: skipping lshape_content break (no fullscreen video asset):",t.id),void a.onBreakEnd(t);const d=this.deps.getAssets(t)[0];if(!d)return this.deps.log("shared-element: no asset to play, skipping break:",t.id),void a.onBreakEnd(t);r.format!==e.SINGLE&&this.deps.log(`shared-element: downgrading ${r.format} to a single fullscreen ad:`,t.id),this.state.currentBreak=t,this.state.currentAsset=d,this.state.callbacks=a,this.state.isPlaying=!0;const h=this.state.effectiveBreakDurationSec??t.duration,l=await this.deps.resolveAssetUri(d,h);if(!l)return this.deps.log("shared-element: no valid URI, skipping break:",t.id),this.state.isPlaying=!1,this.state.currentBreak=null,this.state.currentAsset=null,void a.onBreakEnd(t);this.state.savedContentSrc=this.restorableContentSource(),this.state.savedContentTime=this.deps.contentPlayer.currentTime,this.state.savedContentLive=!Number.isFinite(this.deps.contentPlayer.duration),this.state.savedContentEdgeDistance=this.state.savedContentLive?this.deps.liveEdgeDistanceSec():null,this.state.sharedAdClaimActive=!0,await this.releaseContentElementFromContentEngine(),this.deps.rebindContentElementIfSwapped();const o=this.sharedElementAdPlayer()??this.deps.contentPlayer;if(h&&h>0){const e=h+this.state.adBreakCutSafetyMarginSec;s(this.state,e,()=>{this.deps.log("shared-element: break-cut timer expired, ending break"),this.state.isPlaying&&this.state.currentBreak&&this.state.callbacks?.onBreakCut?.(this.state.currentBreak),this.onSharedElementAdEnded()},()=>{this.state.callbacks?.onAdError(t,d,new n("Ad playback startup timed out","DA-AD-START-TIMEOUT")),this.onSharedElementAdEnded()})}try{if(this.deps.clearContentGate(),await this.sharedAdStep("load",o,()=>o.load(l)),!this.state.isPlaying||this.state.currentBreak!==t)return;if(this.attachSharedAdListeners(o),this.armSharedAdFirstFrameWatchdog(o,Math.max(Math.min(1e3*(h??0)-100,5e3),3e3)),await this.sharedAdStep("play",o,()=>o.play()),!this.state.isPlaying||this.state.currentBreak!==t)return;this.state.callbacks?.onAdStart(t,d,0,1)}catch(e){if(!this.state.isPlaying||this.state.currentBreak!==t)return;this.deps.log("shared-element: ad playback failed:",e),this.state.callbacks?.onAdError(t,d,i(e)),this.onSharedElementAdEnded()}}onSharedElementAdEnded(){if(!this.state.isPlaying||!this.state.currentBreak)return;const e=this.state.currentBreak,t=this.state.currentAsset,s=this.state.callbacks,n=this.sharedRestoreGeneration;this.detachSharedAdListeners(),a(this.state),this.state.isPlaying=!1,this.state.currentBreak=null,this.state.currentAsset=null,t&&s?.onAdEnd(e,t,0,1),n===this.sharedRestoreGeneration&&this.restoreSharedElementContent(e),s?.onBreakEnd(e)}restoreSharedElementContent(e){const s=this.state.savedContentSrc;if(!s){if(this.sharedRestore)return;return this.deps.log("shared-element: no saved content source to restore"),void this.releaseContentElementAdPlayer()}if(!this.beginSharedActivity())return;const a=this.state.savedContentTime,n=this.state.savedContentLive,r=this.state.savedContentEdgeDistance,i=this.state.callbacks;this.state.savedContentSrc=null,this.state.savedContentEdgeDistance=null;const d={src:s,time:a,live:n,edgeDistance:r,breakInfo:e,postRoll:!!e&&t(e),onContentReloading:i?.onContentReloading?.bind(i),adPlayer:this.contentElementAdPlayer,reloading:!1,cleanedUp:!1,cancelled:!1,finishingActivity:null,attempt:null};this.sharedRestore=d,e&&"number"==typeof e.start&&i?.onContentResumeSeek?.(n?this.deps.contentPlayer.currentTime:a,e.start+e.duration),this.sharedRestore===d&&(d.reloading=!0,d.onContentReloading?.(!0),this.sharedRestore===d&&this.runSharedRestore(d))}async runSharedRestore(e){for(let t=1;t<=2;t++){const s={phase:"load",deadline:0,timer:null,settle:null};e.attempt=s;const a=await new Promise(t=>{s.settle=e=>{s.settle&&(s.settle=null,s.timer&&clearTimeout(s.timer),s.timer=null,t(e))},this.armSharedRestoreDeadline(s,1e4),this.reloadAndResume(e,s).then(()=>s.settle?.("success"),()=>s.settle?.("rejection"))});if(this.sharedRestore!==e||e.attempt!==s)return;if("success"===a||"cancelled"===a)break;if(2===t?this.deps.diagnose("DA-SHARED-ELEMENT-RESTORE-FAILED",`Content did not resume after the shared-element break: ${s.phase} ${a}`,{breakId:e.breakInfo?.id,live:e.live,attempts:t,phase:s.phase,reason:a}):this.deps.log(`shared-element: content restore ${s.phase} ${a}, retrying once`),this.sharedRestore!==e||e.attempt!==s)return}this.finishSharedRestore(e,!0)}armSharedRestoreDeadline(e,t){e.timer&&clearTimeout(e.timer),e.deadline=performance.now()+t,e.timer=setTimeout(()=>e.settle?.("timeout"),t)}ownsSharedRestoreAttempt(e,t){return!(this.sharedRestore!==e||e.attempt!==t||!t.settle)&&(performance.now()<t.deadline||(t.settle("timeout"),!1))}async reloadAndResume(e,t){const{src:s,time:a,live:n,edgeDistance:r,postRoll:i}=e,h=()=>this.ownsSharedRestoreAttempt(e,t);if(!h())return;if(e.adPlayer?.releaseMediaElement&&(await this.releaseAdEngineGrip(e),!h()))return;const l=this.deps.contentPlayer.videoElement??null,o=d(l);if(await this.deps.contentPlayer.load(s),h()){if(this.armSharedRestoreDeadline(t,5e3),n){if(!i&&null!==r&&r>0){t.phase="live-edge";const e=await this.deps.waitForLiveEdge(h);if(!h())return;if(null!==e){const s=Math.max(e-r,0);if(this.deps.log(`shared-element: live resume ${r.toFixed(2)}s behind the edge (${s.toFixed(2)}s)`),!h())return;t.phase="seek",this.deps.contentPlayer.seek(s)}}}else t.phase="seek",this.deps.contentPlayer.seek(a);h()&&!i&&(t.phase="play",await this.deps.contentPlayer.play(),h()&&await this.waitForContentEngineToTakeElement(l,o,h))}}async releaseAdEngineGrip(e){const t=e.adPlayer;if(t?.releaseMediaElement)try{await t.releaseMediaElement()}catch(e){this.deps.log("shared-element: the ad engine did not release the element:",e)}}async waitForContentEngineToTakeElement(e,t,s){if(!(e&&t&&(a=t,a.src||a.srcObject||a.sourceChild)))return;var a;const n=()=>{if(this.deps.contentPlayer.videoElement!==e)return!0;const s=d(e);if(!s)return!1;const a=!!s.src&&s.src!==t.src,n=!!s.srcObject&&s.srcObject!==t.srcObject;return a||n},r=performance.now();for(;!n()&&s()&&performance.now()-r<1500;)await new Promise(e=>setTimeout(e,25));if(!s())return;const i=Math.round(performance.now()-r);this.deps.log(`shared-element: content engine ${n()?"took":"did not take"} the element after ${i}ms`)}finishSharedRestore(e,t){this.sharedRestore===e&&(null===e.finishingActivity?(e.finishingActivity=this.sharedActivityGeneration,e.cleanedUp||(this.state.contentPaused=!1,this.contentElementAdPlayer===e.adPlayer&&this.releaseContentElementAdPlayer(),this.sharedRestore===e&&this.deps.clearContentGate(),this.sharedRestore===e&&this.deps.rebindContentElementIfSwapped(),e.cleanedUp=!0),this.endSharedRestoreReloading(e),e.finishingActivity=null,this.sharedRestore===e&&(!t||e.cancelled||e.postRoll?this.sharedRestore=null:this.armSharedRestoreWatchdog(e))):e.finishingActivity!==this.sharedActivityGeneration&&(this.endSharedRestoreReloading(e),this.sharedRestore===e&&(this.sharedRestore=null)))}endSharedRestoreReloading(e){e.reloading&&(e.reloading=!1,e.onContentReloading?.(!1))}armSharedRestoreWatchdog(e){const t=this.deps.contentPlayer.currentTime;this.sharedRestoreWatchdog=setTimeout(()=>{if(this.sharedRestore!==e)return;this.sharedRestoreWatchdog=null;const s=this.deps.contentPlayer.currentTime-t;(this.deps.contentPlayer.paused||s<1)&&(this.deps.diagnose("DA-CONTENT-RESUME-STALLED",`Content did not resume within 5s of the shared-element restore (paused: ${this.deps.contentPlayer.paused}, advanced: ${s.toFixed(1)}s); nudging play()`,{breakId:e.breakInfo?.id??"?",paused:this.deps.contentPlayer.paused,advancedSec:s}),this.sharedRestore===e&&this.deps.contentPlayer.play().catch(()=>{this.sharedRestore===e&&this.deps.log("shared-element: watchdog play() nudge failed")}))},5e3)}beginSharedActivity(){return this.sharedActivityGeneration++,this.cancelSharedRestore()}cancelSharedRestore(){const e=this.sharedActivityGeneration;this.sharedRestoreGeneration++,this.sharedRestoreWatchdog&&(clearTimeout(this.sharedRestoreWatchdog),this.sharedRestoreWatchdog=null);const t=this.sharedRestore;return t&&(t.cancelled=!0,t.attempt?.settle?.("cancelled"),this.finishSharedRestore(t,!1)),e===this.sharedActivityGeneration}restorableContentSource(){const e=this.deps.contentPlayer.sourceUrl;if(e&&!e.startsWith("blob:"))return e;const t=this.deps.contentPlayer.videoElement,s=t?.currentSrc||t?.src||"";return s.length>0&&!s.startsWith("blob:")?s:null}}
|
|
1
|
+
import{BreakFormat as e,isPostRollBreak as t}from"../../types/BreakManifest";import{armPlaybackCutoff as s,clearPlaybackCutoff as a}from"./PlaybackCutoff";import{AdError as n}from"../../types/AdError";import{classifyMediaError as r,toClassifiedAdError as i}from"./classifyMediaError";function d(e){return e?{src:e.getAttribute("src"),srcObject:e.srcObject,sourceChild:null!==e.querySelector("source")}:null}export class SharedElementBreakPlayer{constructor(e,t){this.state=e,this.deps=t,this.contentElementAdPlayer=null,this.sharedAdSource=null,this.sharedAdEndedHandler=null,this.sharedAdTimeUpdateHandler=null,this.sharedAdTimedMetadataHandler=null,this.sharedAdWaitingHandler=null,this.sharedAdPlayingHandler=null,this.sharedAdFirstFrameTimer=null,this.sharedRestoreWatchdog=null,this.sharedRestore=null,this.sharedRestoreGeneration=0,this.sharedActivityGeneration=0}sharedElementAdPlayer(){const e=this.deps.contentPlayer.videoElement;if(!e)return null;if(this.contentElementAdPlayer?.videoElement===e)return this.contentElementAdPlayer;this.releaseContentElementAdPlayer();const t=this.deps.createAdAdapter(document.createElement("div"),e);return t!==this.deps.contentPlayer&&t.prepareForAdPlayback?.(),t.videoElement!==e?(this.deps.log("shared-element: createAdAdapter ignored the content element; falling back to the content player for ad playback"),t.destroy(),null):(this.contentElementAdPlayer=t,t)}async sharedAdStep(e,t,s){try{await s()}catch(s){const a=t.videoElement,i=s instanceof Error?s.message:String(s),d=new n(`shared-element ${e} failed: ${i} (readyState=${a?.readyState??"-"} paused=${a?.paused??"-"} networkState=${a?.networkState??"-"})`,r(s));throw d.cause=s,d}}clearSharedAdFirstFrameWatchdog(){this.sharedAdFirstFrameTimer&&(clearTimeout(this.sharedAdFirstFrameTimer),this.sharedAdFirstFrameTimer=null)}armSharedAdFirstFrameWatchdog(e,t){this.clearSharedAdFirstFrameWatchdog(),this.sharedAdFirstFrameTimer=setTimeout(()=>{if(this.sharedAdFirstFrameTimer=null,!this.state.isPlaying||!this.state.currentBreak)return;const s=e.videoElement,a=this.deps.contentPlayer.videoElement,r=s?.currentSrc??"",i=`readyState=${s?.readyState??"-"} networkState=${s?.networkState??"-"} paused=${s?.paused??"-"} connected=${s?.isConnected??"-"} srcKind=${r.startsWith("blob:")?"blob":r?"url":"none"} sameElementAsContentPlayer=${String(s===a)}`;this.deps.log(`shared-element: no first frame within ${t}ms (${i})`),this.deps.diagnose("DA-AD-PLAYBACK-ERROR",`the ad never rendered a frame on the content element within ${t}ms`,{breakId:this.state.currentBreak.id,detail:i});const d=this.state.currentAsset;d&&this.state.callbacks?.onAdError(this.state.currentBreak,d,new n(`shared-element ad produced no frames within ${t}ms (${i})`,"DA-AD-START-TIMEOUT")),this.onSharedElementAdEnded()},t)}attachSharedAdListeners(e){this.sharedAdSource=e,this.sharedAdEndedHandler=()=>this.onSharedElementAdEnded(),e.on("ended",this.sharedAdEndedHandler),this.sharedAdTimeUpdateHandler=()=>{this.sharedAdFirstFrameTimer&&e.currentTime>0&&this.clearSharedAdFirstFrameWatchdog(),this.deps.reportAdProgress(e)},e.on("timeupdate",this.sharedAdTimeUpdateHandler),this.sharedAdTimedMetadataHandler=e=>{e&&this.deps.processTimedMetadata(e)},e.on("timedmetadata",this.sharedAdTimedMetadataHandler),this.sharedAdWaitingHandler=()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&this.state.callbacks?.onAdWaiting(this.state.currentBreak,this.state.currentAsset)},e.on("waiting",this.sharedAdWaitingHandler),this.sharedAdPlayingHandler=()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&!e.paused&&(this.clearSharedAdFirstFrameWatchdog(),this.state.startPlaybackCutoff?.(),this.state.callbacks?.onAdPlaying(this.state.currentBreak,this.state.currentAsset))},e.on("playing",this.sharedAdPlayingHandler)}detachSharedAdListeners(){const e=this.sharedAdSource??this.deps.contentPlayer;this.sharedAdSource=null,this.state.sharedAdClaimActive=!1,this.clearSharedAdFirstFrameWatchdog(),this.sharedAdTimedMetadataHandler&&(e.off("timedmetadata",this.sharedAdTimedMetadataHandler),this.sharedAdTimedMetadataHandler=null),this.sharedAdWaitingHandler&&(e.off("waiting",this.sharedAdWaitingHandler),this.sharedAdWaitingHandler=null),this.sharedAdPlayingHandler&&(e.off("playing",this.sharedAdPlayingHandler),this.sharedAdPlayingHandler=null),this.sharedAdTimeUpdateHandler&&(e.off("timeupdate",this.sharedAdTimeUpdateHandler),this.sharedAdTimeUpdateHandler=null),this.sharedAdEndedHandler&&(e.off("ended",this.sharedAdEndedHandler),this.sharedAdEndedHandler=null)}async releaseContentElementFromContentEngine(){if(this.deps.contentPlayer.releaseMediaElement)try{await this.deps.contentPlayer.releaseMediaElement()}catch(e){this.deps.log("shared-element: the content engine did not release the element:",e)}}releaseContentElementAdPlayer(){const e=this.contentElementAdPlayer;if(e){this.contentElementAdPlayer=null;try{e.destroy()}catch(e){this.deps.log("shared-element: releasing the ad player on the content element failed:",e)}}}async playAdThroughContentElement(){const e=this.state.currentAsset,t=this.state.currentBreak;if(!e||!t)return;const s=this.state.effectiveBreakDurationSec??t.duration,a=await this.deps.resolveAssetUri(e,s);if(!a)return void this.deps.log("shared-element: no URI for the running ad; leaving it on the ad element");const n=this.deps.getAdPlayer()?.currentTime??0;this.state.savedContentSrc=this.restorableContentSource(),this.state.savedContentTime=this.deps.contentPlayer.currentTime,this.state.savedContentLive=!Number.isFinite(this.deps.contentPlayer.duration),this.state.savedContentEdgeDistance=this.state.savedContentLive?this.deps.liveEdgeDistanceSec():null,this.state.sharedAdClaimActive=!0,await this.releaseContentElementFromContentEngine(),this.deps.rebindContentElementIfSwapped();const r=this.sharedElementAdPlayer()??this.deps.contentPlayer;try{this.deps.getAdPlayer()?.pause(),this.deps.clearContentGate(),await r.load(a),this.attachSharedAdListeners(r),n>0&&r.seek(n),await r.play(),this.deps.hide(),this.deps.log("shared-element: the ad now plays through the content element")}catch(e){this.deps.log("shared-element: could not move the ad onto the content element:",e),this.state.sharedAdClaimActive=!1,this.state.savedContentSrc=null,this.state.savedContentEdgeDistance=null}}async playSharedElementBreak(t,a){const r=this.deps.getVariant(t);if(!r)return this.deps.log("shared-element: no valid variant, skipping break:",t.id),void a.onBreakEnd(t);if(r.format===e.LSHAPE_CONTENT)return this.deps.log("shared-element: skipping lshape_content break (no fullscreen video asset):",t.id),void a.onBreakEnd(t);const d=this.deps.getAssets(t)[0];if(!d)return this.deps.log("shared-element: no asset to play, skipping break:",t.id),void a.onBreakEnd(t);r.format!==e.SINGLE&&this.deps.log(`shared-element: downgrading ${r.format} to a single fullscreen ad:`,t.id),this.state.currentBreak=t,this.state.currentAsset=d,this.state.callbacks=a,this.state.isPlaying=!0;const h=this.state.effectiveBreakDurationSec??t.duration,l=await this.deps.resolveAssetUri(d,h);if(!l)return this.deps.log("shared-element: no valid URI, skipping break:",t.id),this.state.isPlaying=!1,this.state.currentBreak=null,this.state.currentAsset=null,void a.onBreakEnd(t);this.state.savedContentSrc=this.restorableContentSource(),this.state.savedContentTime=this.deps.contentPlayer.currentTime,this.state.savedContentLive=!Number.isFinite(this.deps.contentPlayer.duration),this.state.savedContentEdgeDistance=this.state.savedContentLive?this.deps.liveEdgeDistanceSec():null,this.state.sharedAdClaimActive=!0,await this.releaseContentElementFromContentEngine(),this.deps.rebindContentElementIfSwapped();const o=this.sharedElementAdPlayer()??this.deps.contentPlayer;if(h&&h>0){const e=h+this.state.adBreakCutSafetyMarginSec;s(this.state,e,()=>{this.deps.log("shared-element: break-cut timer expired, ending break"),this.state.isPlaying&&this.state.currentBreak&&this.state.callbacks?.onBreakCut?.(this.state.currentBreak),this.onSharedElementAdEnded()},()=>{this.state.callbacks?.onAdError(t,d,new n("Ad playback startup timed out","DA-AD-START-TIMEOUT")),this.onSharedElementAdEnded()})}try{if(this.deps.clearContentGate(),await this.sharedAdStep("load",o,()=>o.load(l)),!this.state.isPlaying||this.state.currentBreak!==t)return;if(this.attachSharedAdListeners(o),this.armSharedAdFirstFrameWatchdog(o,Math.max(Math.min(1e3*(h??0)-100,5e3),3e3)),await this.sharedAdStep("play",o,()=>o.play()),!this.state.isPlaying||this.state.currentBreak!==t)return;this.state.callbacks?.onAdStart(t,d,0,1)}catch(e){if(!this.state.isPlaying||this.state.currentBreak!==t)return;this.deps.log("shared-element: ad playback failed:",e),this.state.callbacks?.onAdError(t,d,i(e)),this.onSharedElementAdEnded()}}onSharedElementAdEnded(){if(!this.state.isPlaying||!this.state.currentBreak)return;const e=this.state.currentBreak,t=this.state.currentAsset,s=this.state.callbacks,n=this.sharedRestoreGeneration;this.detachSharedAdListeners(),a(this.state),this.state.isPlaying=!1,this.state.currentBreak=null,this.state.currentAsset=null,t&&s?.onAdEnd(e,t,0,1),n===this.sharedRestoreGeneration&&this.restoreSharedElementContent(e),s?.onBreakEnd(e)}restoreSharedElementContent(e){const s=this.state.savedContentSrc;if(!s){if(this.sharedRestore)return;return this.deps.log("shared-element: no saved content source to restore"),void this.releaseContentElementAdPlayer()}if(!this.beginSharedActivity())return;const a=this.state.savedContentTime,n=this.state.savedContentLive,r=this.state.savedContentEdgeDistance,i=this.state.callbacks;this.state.savedContentSrc=null,this.state.savedContentEdgeDistance=null;const d={src:s,time:a,live:n,edgeDistance:r,breakInfo:e,postRoll:!!e&&t(e),onContentReloading:i?.onContentReloading?.bind(i),adPlayer:this.contentElementAdPlayer,reloading:!1,cleanedUp:!1,cancelled:!1,finishingActivity:null,attempt:null};this.sharedRestore=d,e&&"number"==typeof e.start&&i?.onContentResumeSeek?.(n?this.deps.contentPlayer.currentTime:a,e.start+e.duration),this.sharedRestore===d&&(d.reloading=!0,d.onContentReloading?.(!0),this.sharedRestore===d&&this.runSharedRestore(d))}async runSharedRestore(e){for(let t=1;t<=2;t++){const s={phase:"load",deadline:0,timer:null,settle:null};e.attempt=s;const a=await new Promise(t=>{s.settle=e=>{s.settle&&(s.settle=null,s.timer&&clearTimeout(s.timer),s.timer=null,t(e))},this.armSharedRestoreDeadline(s,1e4),this.reloadAndResume(e,s).then(()=>s.settle?.("success"),()=>s.settle?.("rejection"))});if(this.sharedRestore!==e||e.attempt!==s)return;if("success"===a||"cancelled"===a)break;if(2===t?this.deps.diagnose("DA-SHARED-ELEMENT-RESTORE-FAILED",`Content did not resume after the shared-element break: ${s.phase} ${a}`,{breakId:e.breakInfo?.id,live:e.live,attempts:t,phase:s.phase,reason:a}):this.deps.log(`shared-element: content restore ${s.phase} ${a}, retrying once`),this.sharedRestore!==e||e.attempt!==s)return}this.finishSharedRestore(e,!0)}armSharedRestoreDeadline(e,t){e.timer&&clearTimeout(e.timer),e.deadline=performance.now()+t,e.timer=setTimeout(()=>e.settle?.("timeout"),t)}ownsSharedRestoreAttempt(e,t){return!(this.sharedRestore!==e||e.attempt!==t||!t.settle)&&(performance.now()<t.deadline||(t.settle("timeout"),!1))}async reloadAndResume(e,t){const{src:s,time:a,live:n,edgeDistance:r,postRoll:i}=e,h=()=>this.ownsSharedRestoreAttempt(e,t);if(!h())return;if(e.adPlayer?.releaseMediaElement&&(await this.releaseAdEngineGrip(e),!h()))return;const l=this.deps.contentPlayer.videoElement??null,o=d(l);if(await this.deps.contentPlayer.load(s),h()){if(this.armSharedRestoreDeadline(t,5e3),n){if(!i&&null!==r&&r>0){t.phase="live-edge";const e=await this.deps.waitForLiveEdge(h);if(!h())return;if(null!==e){const s=Math.max(e-r,0);if(this.deps.log(`shared-element: live resume ${r.toFixed(2)}s behind the edge (${s.toFixed(2)}s)`),!h())return;t.phase="seek",this.deps.contentPlayer.seek(s)}}}else t.phase="seek",this.deps.contentPlayer.seek(a);h()&&!i&&(t.phase="play",await this.deps.contentPlayer.play(),h()&&await this.waitForContentEngineToTakeElement(l,o,h))}}async releaseAdEngineGrip(e){const t=e.adPlayer;if(t?.releaseMediaElement)try{await t.releaseMediaElement()}catch(e){this.deps.log("shared-element: the ad engine did not release the element:",e)}}async waitForContentEngineToTakeElement(e,t,s){if(!(e&&t&&(a=t,a.src||a.srcObject||a.sourceChild)))return;var a;const n=()=>{if(this.deps.contentPlayer.videoElement!==e)return!0;const s=d(e);if(!s)return!1;const a=!!s.src&&s.src!==t.src,n=!!s.srcObject&&s.srcObject!==t.srcObject;return a||n},r=performance.now();for(;!n()&&s()&&performance.now()-r<1500;)await new Promise(e=>setTimeout(e,25));if(!s())return;const i=Math.round(performance.now()-r);this.deps.log(`shared-element: content engine ${n()?"took":"did not take"} the element after ${i}ms`)}finishSharedRestore(e,t){this.sharedRestore===e&&(null===e.finishingActivity?(e.finishingActivity=this.sharedActivityGeneration,e.cleanedUp||(this.state.contentPaused=!1,this.contentElementAdPlayer===e.adPlayer&&this.releaseContentElementAdPlayer(),this.sharedRestore===e&&this.deps.clearContentGate(),this.sharedRestore===e&&this.deps.rebindContentElementIfSwapped(),e.cleanedUp=!0),this.endSharedRestoreReloading(e),e.finishingActivity=null,this.sharedRestore===e&&(!t||e.cancelled||e.postRoll?this.sharedRestore=null:this.armSharedRestoreWatchdog(e))):e.finishingActivity!==this.sharedActivityGeneration&&(this.endSharedRestoreReloading(e),this.sharedRestore===e&&(this.sharedRestore=null)))}endSharedRestoreReloading(e){e.reloading&&(e.reloading=!1,e.onContentReloading?.(!1))}armSharedRestoreWatchdog(e){const t=this.deps.contentPlayer.currentTime;this.sharedRestoreWatchdog=setTimeout(()=>{if(this.sharedRestore!==e)return;this.sharedRestoreWatchdog=null;const s=this.deps.contentPlayer.currentTime-t;(this.deps.contentPlayer.paused||s<1)&&(this.deps.diagnose("DA-CONTENT-RESUME-STALLED",`Content did not resume within 5s of the shared-element restore (paused: ${this.deps.contentPlayer.paused}, advanced: ${s.toFixed(1)}s); nudging play()`,{breakId:e.breakInfo?.id??"?",paused:this.deps.contentPlayer.paused,advancedSec:s}),this.sharedRestore===e&&this.deps.contentPlayer.play().catch(()=>{this.sharedRestore===e&&this.deps.log("shared-element: watchdog play() nudge failed")}))},5e3)}beginSharedActivity(){return this.sharedActivityGeneration++,this.cancelSharedRestore()}cancelSharedRestore(){const e=this.sharedActivityGeneration;this.sharedRestoreGeneration++,this.sharedRestoreWatchdog&&(clearTimeout(this.sharedRestoreWatchdog),this.sharedRestoreWatchdog=null);const t=this.sharedRestore;return t&&(t.cancelled=!0,t.attempt?.settle?.("cancelled"),this.finishSharedRestore(t,!1)),e===this.sharedActivityGeneration}restorableContentSource(){const e=this.deps.contentPlayer.sourceUrl;if(e&&!e.startsWith("blob:"))return e;const t=this.deps.contentPlayer.videoElement,s=t?.currentSrc||t?.src||"";return s.length>0&&!s.startsWith("blob:")?s:null}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BreakFormat as e,Vendor as t,isVendorAsset as s}from"../../types/BreakManifest";import{
|
|
1
|
+
import{BreakFormat as e,Vendor as t,isVendorAsset as s}from"../../types/BreakManifest";import{detectDeviceProfile as i}from"../detectDeviceType";import{assetsOfVariant as n,breakPlanEmptyReason as r,extractAssets as o}from"../AdBreakSequencer";import{playableFormatsFor as a,selectVariantDetailed as d}from"../selectVariant";export class VariantSelector{constructor(e,t){this.state=e,this.deps=t}continueContentDuringBreakReason(t){return this.state.continueContentDuringBreak?"replacement"!==this.state.adInsertionType?"not-replacement":t!==e.SINGLE?"format":"shared-element"===this.deps.getEffectiveInsertionMode()?"shared-element":this.deps.getIsContentPictureInPicture()?"picture-in-picture":"single-decoder"===this.deps.preloadMode||!1===this.deps.getSupportsParallelBuffering()?"single-decoder":"ok":"disabled"}selectionInputs(){if(void 0===this.state.detectedDeviceProfile){const e=i();void 0!==this.state.detectedDeviceType?e.deviceType=this.state.detectedDeviceType:this.state.detectedDeviceType=e.deviceType,this.state.detectedDeviceProfile=e}return{deviceType:this.state.detectedDeviceType,deviceProfile:this.state.detectedDeviceProfile,playableFormats:a({pictureInPicture:this.deps.getIsContentPictureInPicture(),singleSurfaceDevice:this.deps.singleSurfaceDevice})}}breakNothingToPlayReason(e){const t=this.deps.getEffectiveInsertionMode(),s=this.selectionInputs(),i=r(e,t,s);return null!==i?i:this.onlyGamAssetsWithoutSession(e,t,s)?"no-assets":null}onlyGamAssetsWithoutSession(e,i,n){if(this.deps.getIsGamReady()||null!==this.state.gamInitPromise)return!1;const r=o(e,n),a="shared-element"===i?r.slice(0,1):r;return a.length>0&&a.every(e=>s(e)&&e.vendor===t.GAM)}unknownVariantFormatsOf(e){return d(e,this.selectionInputs()).skippedUnknownFormats}diagnoseUnknownVariantFormats(e){for(const t of this.unknownVariantFormatsOf(e))this.deps.diagnose("DA-BREAK-VARIANT-FORMAT-UNKNOWN",`break ${e.id}: variant format ${""===t?"(absent)":`"${t}"`} is not recognised; the variant was skipped.`,{breakId:e.id,format:t})}getVariant(t){const{selected:s,deviceQualified:i,targetingIssues:r}=d(t,this.selectionInputs());for(const e of r)this.deps.diagnose(e.code,`Targeting ${e.field} was rejected.`,{field:e.field,...void 0===e.value?{}:{value:e.value}});return s||(i?{format:e.SINGLE,assets:n(i).slice(0,1)}:null)}}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { PlayerAdapter } from '../../interfaces/PlayerAdapter';
|
|
2
|
+
import type { Break } from '../../types/BreakManifest';
|
|
3
|
+
import { type ProgramDateTimeSample } from '../wallclockBreakWindow';
|
|
4
|
+
/** The content player's current position with the stream's Program Date Time at it, or null without one. */
|
|
5
|
+
export declare function programDateTimeSample(player: PlayerAdapter): ProgramDateTimeSample | null;
|
|
6
|
+
/** The manifest `start + duration` of a wallclock break, epoch milliseconds; null off the wallclock timebase. */
|
|
7
|
+
export declare function wallclockBreakEndMs(breakInfo: Break): number | null;
|
|
8
|
+
export interface PdtClockResumeDeps {
|
|
9
|
+
contentPlayer: PlayerAdapter;
|
|
10
|
+
diagnose: (code: string, message: string, context?: Record<string, unknown>) => void;
|
|
11
|
+
/** Whether the break's anchor was placed through the stream's Program Date Time. */
|
|
12
|
+
anchorIsPdt: () => boolean;
|
|
13
|
+
/** The policy's resume point on the currentTime timeline, ignoring the stream. */
|
|
14
|
+
computedTarget: (breakInfo: Break) => number | null;
|
|
15
|
+
/** Replacement resumes at the break end, the only policy the manifest window can serve. */
|
|
16
|
+
resumesAtBreakEnd: (breakInfo: Break) => boolean;
|
|
17
|
+
/** Wall clock in milliseconds. */
|
|
18
|
+
now: () => number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Live resume on the stream's Program Date Time (`timebase: 'wallclock'`): the resume point is
|
|
22
|
+
* the manifest `start + duration`, a wallclock instant, converted to currentTime with the
|
|
23
|
+
* mapping the content player reports right now, so a currentTime re-anchored by a seek during
|
|
24
|
+
* the break cannot move it. The web twin of the iOS runtime's `OverlayAdRenderer+PdtClock`; the
|
|
25
|
+
* beacon-based sibling is `StreamClockResume`. See `docs/ios-live-resume-v2.md` §17.
|
|
26
|
+
*/
|
|
27
|
+
export declare class PdtClockResume {
|
|
28
|
+
private readonly deps;
|
|
29
|
+
/** The hand-back resumed on the Program Date Time target. */
|
|
30
|
+
applied: boolean;
|
|
31
|
+
private reportedTarget;
|
|
32
|
+
private offsetAtBreakStart;
|
|
33
|
+
private landingCheck;
|
|
34
|
+
constructor(deps: PdtClockResumeDeps);
|
|
35
|
+
/** Content paused for a new break: per-break state starts over, the current mapping is the reference. */
|
|
36
|
+
beginBreak(): void;
|
|
37
|
+
/** The resume point in currentTime under the mapping the player reports now, or null. */
|
|
38
|
+
target(breakInfo: Break): number | null;
|
|
39
|
+
/** The hand-back's target, recording whether it applies; null for an early return. */
|
|
40
|
+
handBackTarget(breakInfo: Break, earlyReturn: boolean): number | null;
|
|
41
|
+
/** What DA-RESUME-SEEK's target is based on, when the Program Date Time decided it. */
|
|
42
|
+
seekBasis(): string | null;
|
|
43
|
+
/** The Program Date Time fields of DA-RESUME-SEEK; empty unless the hand-back resumed on it. */
|
|
44
|
+
seekContext(breakInfo: Break, target: number, computed: number): Record<string, unknown>;
|
|
45
|
+
/**
|
|
46
|
+
* DA-RESUME-PDT-CORRECTED, once per break at the hand-back of a PDT-anchored break: how far the
|
|
47
|
+
* stream's clock moved the resume point from the anchor-based target, or why it could not.
|
|
48
|
+
*/
|
|
49
|
+
reportCorrection(breakInfo: Break, target: number, computed: number, earlyReturn: boolean): void;
|
|
50
|
+
/**
|
|
51
|
+
* Arms DA-RESUME-PDT-LANDED when the hand-back resumed on the Program Date Time: the first
|
|
52
|
+
* position sample after the hand-back measures where content landed on the stream clock.
|
|
53
|
+
*/
|
|
54
|
+
armLanding(breakInfo: Break): void;
|
|
55
|
+
cancelLanding(): void;
|
|
56
|
+
private reportTarget;
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{contentTimeForWallclock as e}from"../wallclockBreakWindow";const t=e=>Math.round(1e3*e)/1e3||0;export function programDateTimeSample(e){const t=e.programDateTime?.getTime(),r=e.currentTime;return void 0!==t&&Number.isFinite(t)&&Number.isFinite(r)?{currentTime:r,programDateTimeMs:t}:null}export function wallclockBreakEndMs(e){if("string"!=typeof e.start)return null;const t=Date.parse(e.start);return Number.isFinite(t)?t+1e3*(e.duration??0):null}export class PdtClockResume{constructor(e){this.deps=e,this.applied=!1,this.reportedTarget=null,this.offsetAtBreakStart=null,this.landingCheck=null}beginBreak(){this.cancelLanding(),this.applied=!1,this.reportedTarget=null;const e=programDateTimeSample(this.deps.contentPlayer);this.offsetAtBreakStart=e?e.programDateTimeMs/1e3-e.currentTime:null}target(t){if(!this.deps.anchorIsPdt()||!this.deps.resumesAtBreakEnd(t))return null;const r=wallclockBreakEndMs(t),a=programDateTimeSample(this.deps.contentPlayer);if(null===r||!a)return null;const n=e(a,r);return this.reportTarget(t,n,a,r),n}handBackTarget(e,t){const r=t?null:this.target(e);return this.applied=null!==r,r}seekBasis(){return this.applied?"on the stream clock, program-date-time":null}seekContext(e,r,a){const n=wallclockBreakEndMs(e),i=programDateTimeSample(this.deps.contentPlayer);return this.applied&&null!==n&&i?{pdtTarget:t(r),computedTarget:a,landingPdt:new Date(n).toISOString(),pdtOffsetSec:t(i.programDateTimeMs/1e3-i.currentTime)}:{}}reportCorrection(e,r,a,n){if(n||!this.deps.anchorIsPdt())return;const i={breakId:e.id,applied:this.applied,computedTarget:a},s=programDateTimeSample(this.deps.contentPlayer);if(!this.applied||!s)return i.reason="no program-date-time sample at the hand-back",void this.deps.diagnose("DA-RESUME-PDT-CORRECTED",`Break ${e.id}: no program-date-time sample at the hand-back; the stream clock did not place the resume`,i);const o=s.programDateTimeMs/1e3-s.currentTime,d=t(r-a),l=null===this.offsetAtBreakStart?null:t(this.offsetAtBreakStart-o);Object.assign(i,{pdtTarget:t(r),correctionSec:d,currentTimeShiftSec:l}),this.deps.diagnose("DA-RESUME-PDT-CORRECTED",`Break ${e.id}: the program-date-time placed the resume at ${t(r)}s, ${d}s from the anchor target ${a}s`+(null===l?"":`; currentTime moved ${l}s against the stream during the break`),i)}armLanding(e){const r=wallclockBreakEndMs(e);if(!this.applied||null===r)return;this.cancelLanding();const a=e.id,n=this.deps.now(),i=()=>{const e=programDateTimeSample(this.deps.contentPlayer);if(!e)return;this.cancelLanding();const i=t((e.programDateTimeMs-r)/1e3);this.deps.diagnose("DA-RESUME-PDT-LANDED",`Resume for break ${a} landed ${Math.abs(i)}s ${i<0?"before":"after"} the break end on the program-date-time`,{breakId:a,landingPdt:new Date(r).toISOString(),landedPdt:new Date(e.programDateTimeMs).toISOString(),landedAt:t(e.currentTime),errorSec:i,sinceHandBackSec:t((this.deps.now()-n)/1e3)})},s=this.deps.contentPlayer,o=setTimeout(()=>{this.cancelLanding(),this.deps.diagnose("DA-RESUME-PDT-LANDED",`Resume landing for break ${a} could not be measured on the program-date-time (no sample)`,{breakId:a,landingPdt:new Date(r).toISOString(),reason:"no program-date-time sample after the hand-back"})},2e3);this.landingCheck={breakId:a,landingMs:r,timer:o,release:()=>{s.off("seeked",i),s.off("timeupdate",i)}},s.on("seeked",i),s.on("timeupdate",i)}cancelLanding(){const e=this.landingCheck;e&&(this.landingCheck=null,clearTimeout(e.timer),e.release())}reportTarget(e,r,a,n){if(null!==this.reportedTarget&&Math.abs(r-this.reportedTarget)<.1)return;const i=null===this.reportedTarget;this.reportedTarget=r;const s=this.deps.computedTarget(e),o={breakId:e.id,target:t(r),landingPdt:new Date(n).toISOString(),pdtOffsetSec:t(a.programDateTimeMs/1e3-a.currentTime),currentTime:t(a.currentTime),remapped:!i};null!==s&&(o.computedTarget=s,o.deltaSec=t(r-s)),this.deps.diagnose("DA-RESUME-PDT-MAP",`Break ${e.id}: the break end maps to ${t(r)}s on the program-date-time`+(null===s?"":` (${t(r-s)}s from the anchor target ${s}s)`)+(i?"":"; currentTime moved against the stream"),o)}}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stream's own clock against the content player's currentTime, measured from Anvato
|
|
3
|
+
* `type=beacon` ID3 tags (one every 3s on NFL Channel). The TS twin of the iOS runtime's
|
|
4
|
+
* `StreamClockMapping`.
|
|
5
|
+
*
|
|
6
|
+
* A beacon carries the stream clock in `pts` — the same clock as the break's `cue` and `cue_end` —
|
|
7
|
+
* and is delivered at its position in currentTime, so `pts − startTime` is the mapping in force.
|
|
8
|
+
* Only beacons delivered after the last seek that may have restarted the buffer count:
|
|
9
|
+
* {@link StreamClockMapping.markSeek} starts over. See `docs/ios-live-resume-v2.md` §16.
|
|
10
|
+
*/
|
|
11
|
+
export interface StreamClockAnchor {
|
|
12
|
+
/** The beacon's `pts`, in the feed's own unit. */
|
|
13
|
+
raw: number;
|
|
14
|
+
/** Where the player delivered it, in seconds of currentTime. */
|
|
15
|
+
mediaTime: number;
|
|
16
|
+
}
|
|
17
|
+
export type StreamClockUpdate = {
|
|
18
|
+
kind: 'none';
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'mapped';
|
|
21
|
+
offsetSec: number;
|
|
22
|
+
anchors: number;
|
|
23
|
+
} | {
|
|
24
|
+
kind: 'remapped';
|
|
25
|
+
fromSec: number;
|
|
26
|
+
toSec: number;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Beacons are packed into ID3 PES packets, so their delivery alternates by ~0.134s around the `pts`
|
|
30
|
+
* cadence; the mean over the beacons since the last seek cancels that, and parse-time deliveries
|
|
31
|
+
* can arrive out of order, so two delivered in a row need not be neighbours.
|
|
32
|
+
*/
|
|
33
|
+
export declare const STREAM_CLOCK_ANCHORS_KEPT = 10;
|
|
34
|
+
/** A mapping from fewer beacons than this still carries part of the packet jitter. */
|
|
35
|
+
export declare const STREAM_CLOCK_SETTLED_ANCHORS = 2;
|
|
36
|
+
/** An offset step beyond this between consecutive beacons is a remap, not packing jitter. */
|
|
37
|
+
export declare const STREAM_CLOCK_REMAP_THRESHOLD_SEC = 0.2;
|
|
38
|
+
/**
|
|
39
|
+
* The feed's `pts` unit from two consecutive beacons: their `pts` advance by ~1000× or ~1× the
|
|
40
|
+
* currentTime between them. Anything else (a seek in between, an out-of-order delivery) says nothing.
|
|
41
|
+
*/
|
|
42
|
+
export declare function streamClockUnit(rawDelta: number, mediaDelta: number): number | null;
|
|
43
|
+
export declare class StreamClockMapping {
|
|
44
|
+
/**
|
|
45
|
+
* Raw `pts` units per second: 1000 when the feed's `pts` is milliseconds (NFL Channel), 1 when
|
|
46
|
+
* seconds. Learned once from two beacons' cadence and kept for the session.
|
|
47
|
+
*/
|
|
48
|
+
unitsPerSecond: number | null;
|
|
49
|
+
/** Bumped by {@link markSeek}; every anchor belongs to the generation it was delivered in. */
|
|
50
|
+
generation: number;
|
|
51
|
+
private lastBeacon;
|
|
52
|
+
private anchors;
|
|
53
|
+
/** Beacons behind the current mapping (0 before the first one after the last seek). */
|
|
54
|
+
get anchorCount(): number;
|
|
55
|
+
/** A seek that may have restarted the buffer: the mapping must be measured again. */
|
|
56
|
+
markSeek(): void;
|
|
57
|
+
/** Records a beacon; see {@link StreamClockUpdate} for what it did to the mapping. */
|
|
58
|
+
ingestBeacon(raw: number, mediaTime: number): StreamClockUpdate;
|
|
59
|
+
/** Mean `stream − currentTime`, in seconds, over the beacons since the last seek; null before one. */
|
|
60
|
+
get offsetSec(): number | null;
|
|
61
|
+
/** A raw `pts` (a beacon's, a cue's, a break's manifest start) in seconds of the stream clock. */
|
|
62
|
+
streamSeconds(raw: number): number | null;
|
|
63
|
+
/** Where a stream-clock instant is in currentTime under the current mapping. */
|
|
64
|
+
mediaTimeForStreamSeconds(seconds: number): number | null;
|
|
65
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const STREAM_CLOCK_ANCHORS_KEPT=10;export const STREAM_CLOCK_SETTLED_ANCHORS=2;export const STREAM_CLOCK_REMAP_THRESHOLD_SEC=.2;export function streamClockUnit(n,t){if(!(n>0&&t>.5))return null;const e=n/t;return e>800&&e<1250?1e3:e>.8&&e<1.25?1:null}export class StreamClockMapping{constructor(){this.unitsPerSecond=null,this.generation=0,this.lastBeacon=null,this.anchors=[]}get anchorCount(){return this.anchors.length}markSeek(){this.generation+=1,this.anchors=[]}ingestBeacon(n,t){if(!Number.isFinite(n)||!Number.isFinite(t))return{kind:"none"};null===this.unitsPerSecond&&this.lastBeacon&&(this.unitsPerSecond=streamClockUnit(n-this.lastBeacon.raw,t-this.lastBeacon.mediaTime)),this.lastBeacon={raw:n,mediaTime:t};const e=this.unitsPerSecond;if(null===e||this.anchors.some(t=>t.raw===n))return{kind:"none"};const s={raw:n,mediaTime:t},i=n/e-t,r=this.offsetSec;return null!==r&&Math.abs(i-r)>.2?(this.anchors=[s],{kind:"remapped",fromSec:r,toSec:i}):(this.anchors.push(s),this.anchors.length>10&&this.anchors.splice(0,this.anchors.length-10),{kind:"mapped",offsetSec:this.offsetSec??i,anchors:this.anchors.length})}get offsetSec(){const n=this.unitsPerSecond;return null===n||0===this.anchors.length?null:this.anchors.reduce((t,e)=>t+e.raw/n-e.mediaTime,0)/this.anchors.length}streamSeconds(n){return Number.isFinite(n)&&null!==this.unitsPerSecond?n/this.unitsPerSecond:null}mediaTimeForStreamSeconds(n){const t=this.offsetSec;return Number.isFinite(n)&&null!==t?n-t:null}}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { PlayerAdapter, TimedMetadataCue } from '../../interfaces/PlayerAdapter';
|
|
2
|
+
import type { Break } from '../../types/BreakManifest';
|
|
3
|
+
import { StreamClockMapping } from './streamClockMapping';
|
|
4
|
+
/** The stream's own end of a break, matched to it on the stream clock. */
|
|
5
|
+
export interface BreakEndOnStreamClock {
|
|
6
|
+
/** Where the player delivered the `cue_end`, in seconds of currentTime. */
|
|
7
|
+
mediaTime: number;
|
|
8
|
+
/** `cue_end` pts minus the break's start pts, in seconds. */
|
|
9
|
+
lengthSec: number;
|
|
10
|
+
}
|
|
11
|
+
/** A resume point on the stream clock, and what fixed it. */
|
|
12
|
+
export interface StreamClockLanding {
|
|
13
|
+
seconds: number;
|
|
14
|
+
source: 'cue_end' | 'cue+duration';
|
|
15
|
+
}
|
|
16
|
+
export interface StreamClockResumeDeps {
|
|
17
|
+
contentPlayer: PlayerAdapter;
|
|
18
|
+
diagnose: (code: string, message: string, context?: Record<string, unknown>) => void;
|
|
19
|
+
/** The break being resumed, while one is. */
|
|
20
|
+
activeBreak: () => Break | null;
|
|
21
|
+
/** Whether the break's anchor is its in-stream cue (the stream clock needs the cue's pts). */
|
|
22
|
+
anchorIsCue: () => boolean;
|
|
23
|
+
/** The break's matched `cue_end`, or null. */
|
|
24
|
+
breakEnd: (breakInfo: Break) => BreakEndOnStreamClock | null;
|
|
25
|
+
/** The policy's resume point on the currentTime timeline, ignoring the stream. */
|
|
26
|
+
computedTarget: (breakInfo: Break) => number | null;
|
|
27
|
+
/** Replacement resumes at the break end, the only policy the stream's end of the break can serve. */
|
|
28
|
+
resumesAtBreakEnd: (breakInfo: Break) => boolean;
|
|
29
|
+
/** Wall clock in milliseconds. */
|
|
30
|
+
now: () => number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Live resume on the stream's own clock (`ptsSource: 'anvatoCue'`), web: the resume point is a
|
|
34
|
+
* stream-clock instant, converted to currentTime with the mapping that beacons parsed since the
|
|
35
|
+
* run-up's last seek measure, and re-mapped on every beacon until the hand-back. The TS twin of
|
|
36
|
+
* the iOS runtime's `OverlayAdRenderer+StreamClock`; see `docs/ios-live-resume-v2.md` §16.
|
|
37
|
+
*/
|
|
38
|
+
export declare class StreamClockResume {
|
|
39
|
+
private readonly deps;
|
|
40
|
+
readonly clock: StreamClockMapping;
|
|
41
|
+
/** The run-up made its opening move: from here the stream clock may map the resume point. */
|
|
42
|
+
armed: boolean;
|
|
43
|
+
/** The hand-back resumed on the stream-clock target. */
|
|
44
|
+
applied: boolean;
|
|
45
|
+
private reportedTarget;
|
|
46
|
+
private reportedGeneration;
|
|
47
|
+
private offsetAtBreakStart;
|
|
48
|
+
private remaps;
|
|
49
|
+
private landingCheck;
|
|
50
|
+
private landingTimer;
|
|
51
|
+
constructor(deps: StreamClockResumeDeps);
|
|
52
|
+
/** The stream carries beacons, so its clock places the resume point once the run-up is mapped. */
|
|
53
|
+
get decides(): boolean;
|
|
54
|
+
/** Content paused for a new break: per-break state starts over, the current mapping is the reference. */
|
|
55
|
+
beginBreak(): void;
|
|
56
|
+
/** The run-up's opening move (catch-up start or pre-positioning). */
|
|
57
|
+
arm(): void;
|
|
58
|
+
/** Before an SDK seek: one that may restart the buffer means the mapping is measured again. */
|
|
59
|
+
noteSeek(position: number): void;
|
|
60
|
+
/** The content source was rebuilt (pipeline recreate): its timeline has to be mapped again. */
|
|
61
|
+
noteReload(): void;
|
|
62
|
+
/**
|
|
63
|
+
* Records an Anvato `type=beacon` cue; other cues are ignored. Presentation re-deliveries are
|
|
64
|
+
* skipped: they repeat a cue parsed earlier, possibly before the last seek.
|
|
65
|
+
*/
|
|
66
|
+
ingestCue(cue: TimedMetadataCue): void;
|
|
67
|
+
/**
|
|
68
|
+
* The break's resume point on the stream clock: its matched `cue_end`, else the cue plus the
|
|
69
|
+
* break duration. Null for a break that was not cue-matched or before the feed's unit is known.
|
|
70
|
+
*/
|
|
71
|
+
landing(breakInfo: Break): StreamClockLanding | null;
|
|
72
|
+
/** The resume point in currentTime under the mapping measured since the run-up's last seek, or null. */
|
|
73
|
+
target(breakInfo: Break): number | null;
|
|
74
|
+
/** The hand-back's stream-clock target, recording whether it applies; null for an early return. */
|
|
75
|
+
handBackTarget(breakInfo: Break, earlyReturn: boolean): number | null;
|
|
76
|
+
/** What DA-RESUME-SEEK's target is based on, when the stream clock decided it. */
|
|
77
|
+
seekBasis(breakInfo: Break): string | null;
|
|
78
|
+
/** The stream-clock fields of DA-RESUME-SEEK; empty unless the hand-back resumes on the stream clock. */
|
|
79
|
+
seekContext(breakInfo: Break, target: number, computed: number): Record<string, unknown>;
|
|
80
|
+
/**
|
|
81
|
+
* DA-RESUME-PTS-CORRECTED, once per break at the hand-back, whenever the stream carries beacons:
|
|
82
|
+
* how far the stream clock moved the resume point from the currentTime target, or why it did not.
|
|
83
|
+
*/
|
|
84
|
+
reportCorrection(breakInfo: Break, target: number, computed: number, earlyReturn: boolean): void;
|
|
85
|
+
/** Arms the landing measurement when the hand-back resumed on the stream clock. */
|
|
86
|
+
armLanding(breakInfo: Break, resumedAt: number): void;
|
|
87
|
+
/** The landing probe measured where the hand-back seek really landed. */
|
|
88
|
+
updateLanding(breakId: string, landed: number): void;
|
|
89
|
+
cancelLanding(): void;
|
|
90
|
+
private reportTarget;
|
|
91
|
+
/** DA-RESUME-PTS-LANDED: where content resumed, on the stream clock, against the resume point. */
|
|
92
|
+
private finishLanding;
|
|
93
|
+
private bufferedState;
|
|
94
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{anvatoPayloadFromCue as e,parseAnvatoPayload as t}from"../MetadataCueTracker";import{STREAM_CLOCK_SETTLED_ANCHORS as n,StreamClockMapping as s}from"./streamClockMapping";const r=e=>Math.round(1e3*e)/1e3;export class StreamClockResume{constructor(e){this.deps=e,this.clock=new s,this.armed=!1,this.applied=!1,this.reportedTarget=null,this.reportedGeneration=null,this.offsetAtBreakStart=null,this.remaps=0,this.landingCheck=null,this.landingTimer=null}get decides(){return null!==this.clock.unitsPerSecond}beginBreak(){this.cancelLanding(),this.armed=!1,this.applied=!1,this.reportedTarget=null,this.reportedGeneration=null,this.remaps=0,this.offsetAtBreakStart=this.clock.offsetSec}arm(){this.armed=!0}noteSeek(e){!0!==this.bufferedState(e)&&this.clock.markSeek()}noteReload(){this.clock.markSeek()}ingestCue(n){if("presented"===n.phase||!Number.isFinite(n.startTime))return;const s=e(n);if(!s)return;const i=t(s);if("beacon"!==i.type)return;const a=Number(i.pts);if(!Number.isFinite(a)||""===i.pts)return;const c=this.clock.ingestBeacon(a,n.startTime),o=this.deps.activeBreak();"remapped"===c.kind&&(o&&(this.remaps+=1),(this.armed||this.landingCheck)&&this.deps.diagnose("DA-RESUME-PTS-REMAP",`currentTime moved ${r(c.toSec-c.fromSec)}s against the stream clock; the resume point is mapped again`,{breakId:o?.id??this.landingCheck?.breakId??"",fromOffsetSec:r(c.fromSec),toOffsetSec:r(c.toSec),deltaSec:r(c.toSec-c.fromSec),currentTime:r(this.deps.contentPlayer.currentTime),stage:this.landingCheck?"after-hand-back":"run-up"})),o&&this.armed&&this.reportTarget(o),this.finishLanding(!1)}landing(e){const t=e.start;if(!this.deps.anchorIsCue()||"number"!=typeof t)return null;const n=this.clock.streamSeconds(t);if(null===n)return null;const s=this.deps.breakEnd(e);return s&&Number.isFinite(s.lengthSec)&&s.lengthSec>0?{seconds:n+s.lengthSec,source:"cue_end"}:{seconds:n+(e.duration??0),source:"cue+duration"}}target(e){if(!this.armed||!this.deps.resumesAtBreakEnd(e))return null;const t=this.landing(e);return t?this.clock.mediaTimeForStreamSeconds(t.seconds):null}handBackTarget(e,t){const n=t?null:this.target(e);return this.applied=null!==n,n}seekBasis(e){const t=this.applied?this.landing(e):null;return t?`on the stream clock, ${t.source}`:null}seekContext(e,t,n){const s=this.applied?this.landing(e):null,i=this.clock.offsetSec;return s&&null!==i?{streamClockTarget:r(t),computedTarget:n,landingStreamSec:r(s.seconds),landingSource:s.source,streamClockOffsetSec:r(i),streamClockBeacons:this.clock.anchorCount}:{}}reportCorrection(e,t,n,s){if(s||!this.armed||!this.decides)return;const i={breakId:e.id,applied:this.applied,computedTarget:n,remaps:this.remaps},a=this.deps.breakEnd(e);a&&(i.cueEndTarget=a.mediaTime);const c=this.applied?this.landing(e):null,o=this.clock.offsetSec;if(!c||null===o)return i.reason="no beacon since the run-up's last seek",void this.deps.diagnose("DA-RESUME-PTS-CORRECTED",`Break ${e.id}: no beacon since the run-up's last seek; the stream clock did not place the resume`,i);const d=r(t-n),l=null===this.offsetAtBreakStart?null:r(this.offsetAtBreakStart-o);Object.assign(i,{streamClockTarget:r(t),correctionSec:d,currentTimeShiftSec:l,landingSource:c.source,landingStreamSec:r(c.seconds),offsetSec:r(o),beacons:this.clock.anchorCount}),this.deps.diagnose("DA-RESUME-PTS-CORRECTED",`Break ${e.id}: the stream clock placed the resume at ${r(t)}s (${c.source}), ${d}s from the currentTime target ${n}s`+(null===l?"":`; currentTime moved ${l}s against the stream during the break`),i)}armLanding(e,t){const n=this.applied?this.landing(e):null;n&&(this.cancelLanding(),this.landingCheck={breakId:e.id,landing:n,resumedAt:t,handBackAtMs:this.deps.now()},this.landingTimer=setTimeout(()=>this.finishLanding(!0),15e3))}updateLanding(e,t){this.landingCheck?.breakId===e&&Number.isFinite(t)&&(this.landingCheck.resumedAt=t)}cancelLanding(){this.landingTimer&&clearTimeout(this.landingTimer),this.landingTimer=null,this.landingCheck=null}reportTarget(e){const t=this.landing(e),n=this.target(e),s=this.clock.offsetSec;if(!t||null===n||null===s)return;const i=this.clock.generation;if(i===this.reportedGeneration&&null!==this.reportedTarget&&Math.abs(n-this.reportedTarget)<.1)return;this.reportedTarget=n,this.reportedGeneration=i;const a=this.deps.computedTarget(e),c={breakId:e.id,target:r(n),landingStreamSec:r(t.seconds),landingSource:t.source,offsetSec:r(s),beacons:this.clock.anchorCount,generation:i,currentTime:r(this.deps.contentPlayer.currentTime)};null!==a&&(c.computedTarget=a,c.deltaSec=r(n-a)),this.deps.diagnose("DA-RESUME-PTS-MAP",`Break ${e.id}: resume point (${t.source}) maps to ${r(n)}s`+(null===a?"":` (${r(n-a)}s from the computed ${a}s)`),c)}finishLanding(e){const t=this.landingCheck;if(!t)return;const s=(this.deps.now()-t.handBackAtMs)/1e3,i={breakId:t.breakId,landingStreamSec:r(t.landing.seconds),landingSource:t.landing.source,resumedAt:r(t.resumedAt),sinceHandBackSec:r(s)},a=this.clock.offsetSec;if(null===a){if(!e)return;return this.cancelLanding(),i.reason="no beacon after the hand-back",void this.deps.diagnose("DA-RESUME-PTS-LANDED",`Resume landing for break ${t.breakId} could not be measured on the stream clock (no beacon)`,i)}if(!e&&this.clock.anchorCount<n&&s<6)return;this.cancelLanding();const c=t.resumedAt+a,o=r(c-t.landing.seconds);Object.assign(i,{landedStreamSec:r(c),errorSec:o,offsetSec:r(a),beacons:this.clock.anchorCount}),this.deps.diagnose("DA-RESUME-PTS-LANDED",`Resume for break ${t.breakId} landed ${Math.abs(o)}s ${o<0?"before":"after"} the stream's resume point (${t.landing.source})`,i)}bufferedState(e){const t=this.deps.contentPlayer.videoElement?.buffered;if(!t||!Number.isFinite(e))return null;for(let n=0;n<t.length;n++)if(e>=t.start(n)&&e<t.end(n))return!0;return!1}}
|
|
@@ -2,12 +2,24 @@ import type { TimedMetadataCue } from '../interfaces/PlayerAdapter';
|
|
|
2
2
|
import { MetadataCueTracker } from './MetadataCueTracker';
|
|
3
3
|
import type { BreakScheduler } from './BreakScheduler';
|
|
4
4
|
import type { Break } from '../types/BreakManifest';
|
|
5
|
+
import type { BreakEndOnStreamClock } from './adPlayer/streamClockResume';
|
|
6
|
+
import type { AdPlayerController } from './AdPlayerController';
|
|
5
7
|
/**
|
|
6
8
|
* The media time of the in-stream cue that matched `breakInfo`, for anchoring the content resume;
|
|
7
9
|
* null when there is no cue tracker (every timebase except `ptsSource: 'anvatoCue'`), when the
|
|
8
10
|
* break carries no numeric PTS start, or when no cue has been observed for it.
|
|
9
11
|
*/
|
|
10
12
|
export declare function cueAnchorForBreak(metadataCueTracker: MetadataCueTracker | null, breakInfo: Break): number | null;
|
|
13
|
+
/**
|
|
14
|
+
* The break's `cue_end` when one was matched to it on the stream clock and its length is accepted
|
|
15
|
+
* (within tolerance of the duration); null otherwise.
|
|
16
|
+
*/
|
|
17
|
+
export declare function cueEndForBreak(metadataCueTracker: MetadataCueTracker | null, breakInfo: Break): BreakEndOnStreamClock | null;
|
|
18
|
+
/**
|
|
19
|
+
* Anchor the content resume on the break's in-stream cue and end it on the stream's `cue_end`,
|
|
20
|
+
* both read from the session's tracker (null when `ptsSource` is not `'anvatoCue'`).
|
|
21
|
+
*/
|
|
22
|
+
export declare function wireCueResume(controller: AdPlayerController, tracker: () => MetadataCueTracker | null): void;
|
|
11
23
|
export declare function armBreakFromPresentedCue(cue: TimedMetadataCue, metadataCueTracker: MetadataCueTracker | null, breakScheduler: BreakScheduler | undefined): void;
|
|
12
24
|
export declare function notedManifestBreakIds(manifest: {
|
|
13
25
|
breaks: Array<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{isAnvatoCue as e,parseAnvatoPayload as t}from"./MetadataCueTracker";export function cueAnchorForBreak(e,t){const r=t.start;return e&&"number"==typeof r&&Number.isFinite(r)?e.mediaTimeForPts(r):null}export function
|
|
1
|
+
import{isAnvatoCue as e,parseAnvatoPayload as t}from"./MetadataCueTracker";export function cueAnchorForBreak(e,t){const r=t.start;return e&&"number"==typeof r&&Number.isFinite(r)?e.mediaTimeForPts(r):null}export function cueEndForBreak(e,t){const r=t.start;if(!e||"number"!=typeof r||!Number.isFinite(r))return null;const n=e.breakEnd(r,t.duration);return n?.accepted?{mediaTime:n.mediaTime,lengthSec:n.lengthSec}:null}export function wireCueResume(e,t){e.setResumeCueAnchorResolver(e=>cueAnchorForBreak(t(),e)),e.setResumeCueEndResolver(e=>cueEndForBreak(t(),e))}export function armBreakFromPresentedCue(r,n,s){if(!n||!s||!e(r))return;const o=t(r.messageData??"");if("cue"!==o.type)return;const i=Number(o.pts);Number.isFinite(i)&&s.notifyCuePresented(i)}export function notedManifestBreakIds(e,t){const r=[];for(const n of e.breaks)t.has(n.id)||(t.add(n.id),r.push(n.id));return r}export function diagnoseAnvatoCueObserved(r,n,s,o,i,u){if(!s||!e(r))return;const a=r.messageData??"",c=t(a);if("cue"!==c.type)return;const d=`${r.startTime}|${a}`;if(o.has(d))return;o.add(d);const m=Number(c.pts);u("DA-ANVATO-CUE-OBSERVED",`Anvato cue observed: pts=${c.pts} at media time ${r.startTime}s`,{pts:Number.isFinite(m)?m:c.pts,mediaTime:r.startTime,playheadSec:i,recorded:n,payload:a.slice(0,300)})}
|
|
@@ -50,7 +50,6 @@ export type AssetParameterMacros = Record<string, AssetParameterMacro>;
|
|
|
50
50
|
* on demand (`undefined` = no value right now).
|
|
51
51
|
*/
|
|
52
52
|
export type AssetParameterMacroTable = Record<string, () => AssetParameterMacroValue>;
|
|
53
|
-
export declare function omitBreakScopedParameters(params: Record<string, string> | undefined): Record<string, string> | undefined;
|
|
54
53
|
export declare function resolveAssetParameterMacro(macro: AssetParameterMacro): AssetParameterMacroValue;
|
|
55
54
|
export interface UnresolvedAssetParameterMacro {
|
|
56
55
|
key: string;
|