@dolby-optiview/ads-sdk-core 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/OptiViewAds.d.ts +5 -4
- package/dist/OptiViewAds.js +1 -1
- package/dist/OptiViewAdsExternalVendors.d.ts +32 -0
- package/dist/external-playback/index.d.ts +4 -0
- package/dist/external-playback/index.js +1 -0
- package/dist/external-playback/observeExternalPlayback.d.ts +19 -0
- package/dist/external-playback/observeExternalPlayback.js +1 -0
- package/dist/external-playback/reconnectHold.d.ts +19 -0
- package/dist/external-playback/reconnectHold.js +1 -0
- package/dist/generated/diagnosticCodes.d.ts +46 -6
- package/dist/generated/diagnosticCodes.js +1 -1
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.js +1 -1
- package/dist/interfaces/AdSourceQueue.d.ts +19 -0
- package/dist/interfaces/AdSourceQueue.js +1 -0
- package/dist/interfaces/PlayerAdapter.d.ts +31 -1
- package/dist/internal/index.d.ts +2 -0
- package/dist/internal/index.js +1 -1
- package/dist/services/AdBreakSequencer.d.ts +33 -4
- package/dist/services/AdBreakSequencer.js +1 -1
- package/dist/services/AdPlayerController.d.ts +12 -73
- package/dist/services/AdPlayerController.js +1 -1
- package/dist/services/BreakChain.d.ts +43 -0
- package/dist/services/BreakChain.js +1 -0
- package/dist/services/BreakChainRecorder.d.ts +63 -0
- package/dist/services/BreakChainRecorder.js +1 -0
- package/dist/services/BreakPreloadLedger.d.ts +13 -0
- package/dist/services/BreakPreloadLedger.js +1 -0
- package/dist/services/BreakScheduler.d.ts +20 -6
- package/dist/services/BreakScheduler.js +1 -1
- package/dist/services/BreakTransitionTimer.d.ts +27 -2
- package/dist/services/BreakTransitionTimer.js +1 -1
- package/dist/services/CmcdAdReporter.d.ts +1 -0
- package/dist/services/CmcdAdReporter.js +1 -1
- package/dist/services/ExternalPlaybackGate.d.ts +40 -0
- package/dist/services/ExternalPlaybackGate.js +1 -0
- package/dist/services/LiveStartPushbackController.d.ts +5 -10
- package/dist/services/LiveStartPushbackController.js +1 -1
- package/dist/services/ManifestService.d.ts +2 -1
- package/dist/services/ManifestService.js +1 -1
- package/dist/services/ReloadGuard.d.ts +11 -0
- package/dist/services/ReloadGuard.js +1 -0
- package/dist/services/RewindRearm.d.ts +3 -1
- package/dist/services/RewindRearm.js +1 -1
- package/dist/services/SsaiController.js +1 -1
- package/dist/services/adPlayer/AdPlayerCallbacks.d.ts +63 -0
- package/dist/services/adPlayer/AdPlayerCallbacks.js +1 -0
- package/dist/services/adPlayer/AdPlayerEvents.d.ts +3 -7
- package/dist/services/adPlayer/AdPlayerEvents.js +1 -1
- package/dist/services/adPlayer/AdPlayerState.d.ts +0 -1
- package/dist/services/adPlayer/AdPlayerState.js +1 -1
- package/dist/services/adPlayer/AdPreloader.d.ts +2 -2
- package/dist/services/adPlayer/AdPreloader.js +1 -1
- package/dist/services/adPlayer/AdStage.d.ts +7 -4
- package/dist/services/adPlayer/AdStage.js +1 -1
- package/dist/services/adPlayer/AssetParameterResolver.d.ts +10 -2
- package/dist/services/adPlayer/AssetParameterResolver.js +1 -1
- package/dist/services/adPlayer/AssetPlayback.d.ts +9 -2
- package/dist/services/adPlayer/AssetPlayback.js +1 -1
- package/dist/services/adPlayer/AudioController.d.ts +58 -0
- package/dist/services/adPlayer/AudioController.js +1 -0
- package/dist/services/adPlayer/BreakSequenceDriver.d.ts +9 -3
- package/dist/services/adPlayer/BreakSequenceDriver.js +1 -1
- package/dist/services/adPlayer/ContentResumeCoordinator.d.ts +2 -0
- package/dist/services/adPlayer/ContentResumeCoordinator.js +1 -1
- package/dist/services/adPlayer/GamPodBridge.d.ts +11 -1
- package/dist/services/adPlayer/GamPodBridge.js +1 -1
- package/dist/services/adPlayer/PauseAdPresenter.d.ts +3 -1
- package/dist/services/adPlayer/PauseAdPresenter.js +1 -1
- package/dist/services/adPlayer/PreparedPresentation.js +1 -1
- package/dist/services/adPlayer/PresentationModeTracker.d.ts +1 -1
- package/dist/services/adPlayer/PresentationModeTracker.js +1 -1
- package/dist/services/adPlayer/SharedElementBreakPlayer.d.ts +3 -0
- package/dist/services/adPlayer/SharedElementBreakPlayer.js +1 -1
- package/dist/services/adPlayer/SuccessorQueue.d.ts +36 -0
- package/dist/services/adPlayer/SuccessorQueue.js +1 -0
- package/dist/services/adPlayer/SurfaceRevealReporter.d.ts +28 -0
- package/dist/services/adPlayer/SurfaceRevealReporter.js +1 -0
- package/dist/services/adPlayer/presentation/DomPresentationHost.d.ts +22 -0
- package/dist/services/adPlayer/presentation/DomPresentationHost.js +1 -0
- package/dist/services/adPlayer/presentation/LayoutTransform.d.ts +28 -0
- package/dist/services/adPlayer/presentation/LayoutTransform.js +1 -0
- package/dist/services/adPlayer/presentation/PresentationLayout.d.ts +19 -0
- package/dist/services/adPlayer/presentation/PresentationLayout.js +1 -0
- package/dist/services/assetParameterMacroTable.js +1 -1
- package/dist/services/assetParameterMacros.d.ts +9 -9
- package/dist/services/assetParameterMacros.js +1 -1
- package/dist/services/channelSwitch.d.ts +2 -2
- package/dist/services/externalPlaybackDiagnostic.d.ts +3 -0
- package/dist/services/externalPlaybackDiagnostic.js +1 -0
- package/dist/services/externalPlaybackGuard.d.ts +24 -0
- package/dist/services/externalPlaybackGuard.js +1 -0
- package/dist/services/parseBreakManifest.d.ts +2 -1
- package/dist/services/parseBreakManifest.js +1 -1
- package/dist/services/resolveChannelEvents.d.ts +17 -0
- package/dist/services/resolveChannelEvents.js +1 -0
- package/dist/services/resolveExternalVendorConfigurations.js +1 -1
- package/dist/services/resolveGamVendorConfiguration.d.ts +6 -1
- package/dist/services/resolveGamVendorConfiguration.js +1 -1
- package/dist/services/ssbStream.d.ts +39 -0
- package/dist/services/ssbStream.js +1 -0
- package/dist/services/ssbSwitchSignals.d.ts +15 -0
- package/dist/services/ssbSwitchSignals.js +1 -0
- package/dist/services/staticAssetUrl.d.ts +28 -0
- package/dist/services/staticAssetUrl.js +1 -0
- package/dist/session/AdBreakStatusTracker.d.ts +18 -0
- package/dist/session/AdBreakStatusTracker.js +1 -1
- package/dist/session/BreakLifecycle.d.ts +6 -0
- package/dist/session/BreakLifecycle.js +1 -1
- package/dist/session/SessionStarter.d.ts +6 -43
- package/dist/session/SessionStarter.js +1 -1
- package/dist/session/SessionStarterDeps.d.ts +39 -0
- package/dist/session/SessionStarterDeps.js +1 -0
- package/dist/session/SessionStarterHelpers.d.ts +36 -0
- package/dist/session/SessionStarterHelpers.js +1 -0
- package/dist/session/SessionState.d.ts +22 -2
- package/dist/session/SessionState.js +1 -1
- package/dist/session/SsbSwitch.d.ts +55 -0
- package/dist/session/SsbSwitch.js +1 -0
- package/dist/session/breakMeasurement.d.ts +16 -0
- package/dist/session/breakMeasurement.js +1 -0
- package/dist/session/contentSnapback.d.ts +13 -0
- package/dist/session/contentSnapback.js +1 -0
- package/dist/session/createExternalPlaybackGate.d.ts +19 -0
- package/dist/session/createExternalPlaybackGate.js +1 -0
- package/dist/session/sessionTick.d.ts +7 -0
- package/dist/session/sessionTick.js +1 -0
- package/dist/types/BreakManifest.d.ts +58 -5
- package/dist/types/Events.d.ts +21 -10
- package/dist/types/ExternalVendorConfiguration.d.ts +3 -1
- package/dist/types/OptiViewAdsConfig.d.ts +24 -8
- package/package.json +5 -1
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { Clock } from './BreakScheduler';
|
|
2
2
|
/**
|
|
3
3
|
* `into-break`: break triggered → first ad renders. `ad-to-ad`: ad ended → next ad renders.
|
|
4
|
-
* `break-to-break`:
|
|
4
|
+
* `break-to-break`: outgoing break's last ad ended → the chained break's first ad renders.
|
|
5
|
+
* `out-of-break`: break ended → content playing.
|
|
5
6
|
*/
|
|
6
7
|
export type BreakTransitionKind = 'into-break' | 'ad-to-ad' | 'break-to-break' | 'out-of-break';
|
|
8
|
+
/**
|
|
9
|
+
* How the incoming break's first ad reached the ad player at a `break-to-break` switch:
|
|
10
|
+
* buffered ahead (`queued`), buffered after the successor changed during the last ad
|
|
11
|
+
* (`late-queued`), or loaded at the switch (`cold`).
|
|
12
|
+
*/
|
|
13
|
+
export type BreakSwitchPath = 'queued' | 'late-queued' | 'cold';
|
|
7
14
|
/** A named checkpoint inside a transition, so a slow transition can be attributed to a phase. */
|
|
8
15
|
export interface TransitionPhase {
|
|
9
16
|
/** Checkpoint name, e.g. `uri-resolved`, `load-resolved`, `play-called`. */
|
|
@@ -22,6 +29,10 @@ export interface BreakTransitionMeasurement {
|
|
|
22
29
|
breakId: string;
|
|
23
30
|
/** Ordered checkpoints from {@link BreakTransitionTimer.phase}; omitted when none, keeping the default report shape. */
|
|
24
31
|
phases?: TransitionPhase[];
|
|
32
|
+
/** `break-to-break`, and `ad-to-ad` in the TS core. */
|
|
33
|
+
path?: BreakSwitchPath;
|
|
34
|
+
/** With `path`, when the incoming ad was buffered: milliseconds it was buffered before the switch. */
|
|
35
|
+
leadMs?: number;
|
|
25
36
|
}
|
|
26
37
|
/**
|
|
27
38
|
* Measures the gap where the viewer sees neither outgoing nor incoming media; each mark/settle
|
|
@@ -33,9 +44,23 @@ export declare class BreakTransitionTimer {
|
|
|
33
44
|
private readonly onMeasurement;
|
|
34
45
|
private readonly clock;
|
|
35
46
|
private pending;
|
|
47
|
+
/** When the outgoing break's last ad ended: where a following `break-to-break` switch starts. */
|
|
48
|
+
private outgoingEndAtMs;
|
|
49
|
+
/** The successor the renderer buffered behind the playing ad, and when. */
|
|
50
|
+
private buffered;
|
|
36
51
|
constructor(onMeasurement: (measurement: BreakTransitionMeasurement) => void, clock?: Clock);
|
|
37
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Record the start of a transition. Supersedes any still-pending one. A `break-to-break`
|
|
54
|
+
* switch starts at the outgoing ad's end when one was noted, and takes the buffered path.
|
|
55
|
+
*/
|
|
38
56
|
mark(kind: BreakTransitionKind, breakId: string): void;
|
|
57
|
+
/** The outgoing break's last ad ended (`atMs`, default now): the start of a chained switch. */
|
|
58
|
+
noteOutgoingEnd(atMs?: number): void;
|
|
59
|
+
/** The renderer buffered `breakId`'s first ad behind the playing ad; `late` after a change during the last ad. */
|
|
60
|
+
noteSuccessorBuffered(breakId: string, late: boolean): void;
|
|
61
|
+
/** The renderer discarded the buffered `breakId`. */
|
|
62
|
+
noteSuccessorDropped(breakId: string): void;
|
|
63
|
+
private clearSwitchAnchors;
|
|
39
64
|
/** No-op when nothing is pending; a superseded transition drops its phases with it. */
|
|
40
65
|
phase(name: string): void;
|
|
41
66
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{systemClock as
|
|
1
|
+
import{systemClock as t}from"./BreakScheduler";export class BreakTransitionTimer{constructor(e,s=t){this.onMeasurement=e,this.clock=s,this.pending=null,this.outgoingEndAtMs=null,this.buffered=null}mark(t,e){const s=this.clock.now();if("ad-to-ad"===t){const n=this.buffered?.breakId===e?this.buffered:null;return n&&(this.buffered=null),void(this.pending={kind:t,breakId:e,startedAtMs:s,phases:[],path:n?"queued":"cold",...n?{leadMs:Math.max(0,s-n.atMs)}:{}})}if("break-to-break"!==t)return"into-break"===t&&this.clearSwitchAnchors(),void(this.pending={kind:t,breakId:e,startedAtMs:s,phases:[]});const n=this.outgoingEndAtMs??s,a=this.buffered?.breakId===e?this.buffered:null;this.pending={kind:t,breakId:e,startedAtMs:n,phases:[],path:a?a.late?"late-queued":"queued":"cold",...a?{leadMs:Math.max(0,n-a.atMs)}:{}},this.clearSwitchAnchors()}noteOutgoingEnd(t=this.clock.now()){this.outgoingEndAtMs=t}noteSuccessorBuffered(t,e){this.buffered={breakId:t,atMs:this.clock.now(),late:e}}noteSuccessorDropped(t){this.buffered?.breakId===t&&(this.buffered=null)}clearSwitchAnchors(){this.outgoingEndAtMs=null,this.buffered=null}phase(t){const e=this.pending;if(!e)return;const s=Math.max(0,this.clock.now()-e.startedAtMs),n=e.phases[e.phases.length-1]?.atMs??0;e.phases.push({name:t,atMs:s,sinceLastMs:Math.max(0,s-n)})}settle(...t){const e=this.pending;e&&t.includes(e.kind)&&(this.pending=null,"out-of-break"===e.kind&&this.clearSwitchAnchors(),this.onMeasurement({kind:e.kind,breakId:e.breakId,durationMs:Math.max(0,this.clock.now()-e.startedAtMs),...e.phases.length>0?{phases:e.phases}:{},...e.path?{path:e.path}:{},...void 0!==e.leadMs?{leadMs:e.leadMs}:{}}))}reset(){this.pending=null,this.clearSwitchAnchors()}}
|
|
@@ -60,6 +60,7 @@ export declare class CmcdAdReporter {
|
|
|
60
60
|
* manifest version, and the optional identity ids (org/channel/event).
|
|
61
61
|
*/
|
|
62
62
|
onSessionStart(manifest: BreakManifest): void;
|
|
63
|
+
updateEventId(eventId: string | null): void;
|
|
63
64
|
/** Clear all telemetry state and the heartbeat payload. */
|
|
64
65
|
onSessionEnd(): void;
|
|
65
66
|
private finalizeActiveBreak;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{LensEventType as e,LensToken as t}from"../vendor/lens-sdk/index.js";import{AssetType as i,BreakFormat as s,getBreakEvent as a}from"../types/BreakManifest";import{adErrorCode as d}from"../types/AdError";import{VendorAdError as r}from"../types/VendorAdError";import{SDK_VERSION as n}from"../generated/version";const h={[s.SINGLE]:"s",[s.DOUBLE]:"d",[s.LSHAPE_AD]:"la",[s.LSHAPE_CONTENT]:"lc",[s.OVERLAY]:"o"},o={[i.STATIC]:"s",[i.VAST]:"va",[i.VENDOR]:"ve"};function l(e){const t=a(e);if(null!==t)return t;const{start:i}=e;return"number"==typeof i?String(Math.round(1e3*i)):"string"==typeof i?i:i.event}function c(e){return o[e]??e}export class CmcdAdReporter{constructor(e,t){this.getHandle=e,this.log=t,this.sessionKeys={},this.activeBreakId=null,this.breakBeginTimestamp=null,this.breakAvailableMs=null,this.breakRequestedMs=null,this.firstAdReported=!1,this.breakFilledMs=0,this.breakWatchedMs=0,this.activeAdKeys=null,this.activeAdCompanion=null,this.activeAdAssetId=null,this.activeAdIsVendor=!1,this.activeAdBaseSec=0,this.breakLastTimeSec=0,this.pendingAdStart=null,this.activeAdWatchedSec=0,this.activeAdDeclaredSec=null,this.activeAdIsVast=!1,this.activeAdResolvedSec=null,this.skipRequestedBreakId=null,this.abortedBreakId=null,this.cutBreakId=null,this.erroredAssetId=null,this.finalizedBreakId=null}onSessionStart(e){this.sessionKeys={adv:n,admv:e.version,...e.organizationId?{adoid:e.organizationId}:{},...e.channelId?{adcid:e.channelId}:{},...e.eventId?{adeid:e.eventId}:{}},this.updateHeartbeat()}onSessionEnd(){this.finalizeActiveBreak(),this.sessionKeys={},this.resetBreakState(),this.guard("setHeartbeatPayload",e=>e.setHeartbeatPayload())}finalizeActiveBreak(){if(null===this.activeBreakId)return;const e=this.activeBreakId;this.emitPendingAdStart(null),null!==this.activeAdKeys&&this.emitAdEnd(this.activeAdKeys,this.activeAdCompanion,"a"),this.emitBreakEnd(e),this.finalizedBreakId=e}onSkipRequested(e){this.skipRequestedBreakId=e}onBreakAborted(e){this.abortedBreakId=e}onBreakCut(e){this.cutBreakId=e}onEvent(e){switch(e.type){case"adbreakbegin":this.onBreakBegin(e);break;case"adbegin":this.onAdBegin(e);break;case"adtimeupdate":{const t=e;this.breakLastTimeSec=Math.max(this.breakLastTimeSec,t.currentTime);null!==this.pendingAdStart&&(t.asset.id===this.pendingAdStart.assetId||this.pendingAdStart.isVendor&&t.break.id===this.activeBreakId)&&t.currentTime>(this.pendingAdStart?.isVendor?this.pendingAdStart.baseSec:0)&&null!==this.breakBeginTimestamp&&this.emitPendingAdStart(t.timestamp-this.breakBeginTimestamp);if(t.asset.id===this.activeAdAssetId||this.activeAdIsVendor&&t.break.id===this.activeBreakId){const e=t.currentTime-this.activeAdBaseSec;this.activeAdWatchedSec=Math.max(this.activeAdWatchedSec,e),this.activeAdIsVast&&Number.isFinite(t.duration)&&t.duration>0&&(this.activeAdResolvedSec=t.duration)}break}case"adend":this.onAdEnd(e);break;case"adbreakend":this.onBreakEnd(e);break;case"adfirstquartile":case"admidpoint":case"adthirdquartile":this.onMilestone(e);break;case"adclick":this.onClick(e);break;case"aderror":this.onAdError(e)}}onBreakBegin(i){this.resetBreakState(),this.finalizedBreakId=null,this.activeBreakId=i.break.id,this.breakBeginTimestamp=i.timestamp;const s=Math.round(1e3*i.remainingSec),a=this.requestedPlayableMs(i.break,i.format,s)??s;this.breakAvailableMs=s,this.breakRequestedMs=a,this.guard("abs",d=>{return d.emit(e.AD_BREAK_START,{custom:{...this.sessionKeys,brid:i.break.id,brst:l(i.break),brmd:Math.round(1e3*i.break.duration),brad:s,brrd:a,...i.format?{brf:new t((r=i.format,h[r]??r))}:{}}});var r}),this.updateHeartbeat()}onAdBegin(e){this.emitPendingAdStart(null);const s=e.asset,a=s.interaction?.clickThrough,d=!this.firstAdReported&&null!==this.breakBeginTimestamp;this.firstAdReported=!0;const r={brid:e.break.id,adasid:s.id,adid:e.adId??s.id,...void 0!==e.podPosition?{adpos:String(e.podPosition)}:{},...void 0!==s.duration?{adasd:Math.round(1e3*s.duration)}:{},adast:new t(c(s.type)),...a?{adcl:!0}:{},...e.creativeId?{adgcrid:e.creativeId}:{},...e.adId&&"string"==typeof s.uri&&"vendor"===s.type?{adgpid:s.uri}:{}};this.activeAdKeys=r,this.activeAdCompanion=s.companion?{id:s.companion.id,parentAssetId:s.id}:null,this.activeAdAssetId=s.id,this.activeAdIsVendor=s.type===i.VENDOR,this.activeAdBaseSec=this.activeAdIsVendor?this.breakLastTimeSec:0,this.activeAdWatchedSec=0,this.activeAdDeclaredSec=s.duration??null,this.activeAdIsVast=s.type===i.VAST,this.activeAdResolvedSec=null;const n={...this.sessionKeys,...r},h=s.companion?{...this.sessionKeys,brid:e.break.id,adasid:s.companion.id,adid:s.companion.id,adast:new t(c(s.companion.type)),adcomp:!0,adpasid:s.id}:null;d?this.pendingAdStart={assetId:s.id,custom:n,companionCustom:h,isVendor:this.activeAdIsVendor,baseSec:this.activeAdBaseSec}:this.emitAdStart(n,h),this.updateHeartbeat()}emitAdStart(t,i){this.guard("as",i=>i.emit(e.AD_START,{custom:t})),i&&this.guard("as",t=>t.emit(e.AD_START,{custom:i}))}emitPendingAdStart(e){const t=this.pendingAdStart;if(!t)return;this.pendingAdStart=null;const i=null!==e?{...t.custom,brttf:e}:t.custom;this.emitAdStart(i,t.companionCustom)}onAdEnd(e){if(e.break.id===this.finalizedBreakId)return;this.emitPendingAdStart(null);const t=e.asset,i=this.adEndReason(e),s=this.activeAdKeys??{brid:e.break.id,adasid:t.id,adid:e.adId??t.id},a=t.companion?{id:t.companion.id,parentAssetId:t.id}:null;this.emitAdEnd(s,a,i)}emitAdEnd(i,s,a){const d=this.activeAdFilledSec(),r=null!==d?Math.round(1e3*d):null,n=Math.round(1e3*this.activeAdWatchedSec),h=null!==r?Math.min(n,r):n;"s"===a&&this.guard("sk",t=>t.emit(e.AD_SKIP,{custom:{...this.sessionKeys,...i}})),this.guard("ae",s=>s.emit(e.AD_END,{custom:{...this.sessionKeys,...i,ader:new t(a),adwd:h}})),s&&this.guard("ae",d=>d.emit(e.AD_END,{custom:{...this.sessionKeys,brid:i.brid,adasid:s.id,adid:s.id,adcomp:!0,adpasid:s.parentAssetId,ader:new t(a)}})),this.breakWatchedMs+=h,this.breakFilledMs+=null!==r?r:h,this.activeAdKeys=null,this.activeAdCompanion=null,this.activeAdAssetId=null,this.activeAdIsVendor=!1,this.activeAdBaseSec=0,this.activeAdWatchedSec=0,this.activeAdDeclaredSec=null,this.activeAdIsVast=!1,this.activeAdResolvedSec=null,this.erroredAssetId===i.adasid&&(this.erroredAssetId=null),this.updateHeartbeat()}onBreakEnd(e){if(e.break.id!==this.finalizedBreakId){if(this.emitPendingAdStart(null),null!==this.breakAvailableMs){const t=this.requestedPlayableMs(e.break,e.format,this.breakAvailableMs);null!==t&&(this.breakRequestedMs=t)}this.emitBreakEnd(e.break.id)}}emitBreakEnd(t){const i=this.breakAvailableMs,s=null!==this.breakRequestedMs&&null!==i?Math.min(this.breakRequestedMs,i):this.breakRequestedMs,a=null!==s?Math.min(this.breakFilledMs,s):this.breakFilledMs,d=Math.min(this.breakWatchedMs,a);this.guard("abe",r=>r.emit(e.AD_BREAK_END,{custom:{...this.sessionKeys,brid:t,...null!==i?{brad:i}:{},...null!==s?{brrd:s}:{},brfd:a,brwd:d}})),this.resetBreakState(),this.updateHeartbeat()}onMilestone(e){this.emitPendingAdStart(null),this.emitCustomWithAdContext(e.type,e.break,e.asset)}onClick(e){this.emitPendingAdStart(null),this.emitCustomWithAdContext("adclick",e.break,e.asset)}onAdError(e){"ad"===e.source&&(this.emitPendingAdStart(null),e.asset&&(this.erroredAssetId=e.asset.id),this.guard("aderror",t=>t.emitCustom("aderror",{custom:{...this.sessionKeys,adec:e.asset?d(e.error):"DA-BREAK-NO-PLAYABLE-ASSET",...e.error instanceof r?{adevc:e.error.vendorCode}:{},...e.break&&e.asset?this.adContextKeys(e.break,e.asset):{...e.break?{brid:e.break.id}:{},...e.asset?{adasid:e.asset.id,adid:e.asset.id}:{}}}})))}emitCustomWithAdContext(e,t,i){this.guard(e,s=>s.emitCustom(e,{custom:{...this.sessionKeys,...this.adContextKeys(t,i)}}))}activeAdFilledSec(){const e=this.activeAdDeclaredSec,t=this.activeAdResolvedSec;return null===t?e:null===e?t:Math.min(t,e)}adContextKeys(e,t){return(t.id===this.activeAdAssetId||this.activeAdIsVendor&&e.id===this.activeBreakId)&&null!==this.activeAdKeys?{...this.activeAdKeys}:{brid:e.id,adasid:t.id,...void 0!==this.activeAdKeys?.adid?{adid:this.activeAdKeys.adid}:{}}}adEndReason(e){return this.skipRequestedBreakId===e.break.id?(this.skipRequestedBreakId=null,"s"):this.erroredAssetId===e.asset.id?"e":this.abortedBreakId===e.break.id?"a":this.cutBreakId===e.break.id?(this.cutBreakId=null,"a"):"c"}requestedPlayableMs(e,t,i){const s=Array.isArray(e.variant)?e.variant:[e.variant],a=s.find(e=>e.format===t)??s[0];if(!a)return null;if(0===a.assets.length)return null;const d=a.assets.map(e=>e.duration);if(d.some(e=>void 0===e||!Number.isFinite(e)||e<=0))return i;const r=d.reduce((e,t)=>e+(t??0),0);return Math.min(Math.round(1e3*r),i)}updateHeartbeat(){this.guard("setHeartbeatPayload",e=>e.setHeartbeatPayload({custom:{...this.sessionKeys,...this.activeBreakId?{brid:this.activeBreakId}:{},...void 0!==this.activeAdKeys?.adid?{adid:this.activeAdKeys.adid}:{}}}))}resetBreakState(){this.emitPendingAdStart(null),this.activeBreakId=null,this.breakBeginTimestamp=null,this.breakAvailableMs=null,this.breakRequestedMs=null,this.firstAdReported=!1,this.breakFilledMs=0,this.breakWatchedMs=0,this.activeAdKeys=null,this.activeAdCompanion=null,this.activeAdAssetId=null,this.activeAdIsVendor=!1,this.activeAdBaseSec=0,this.breakLastTimeSec=0,this.activeAdWatchedSec=0,this.activeAdDeclaredSec=null,this.activeAdIsVast=!1,this.activeAdResolvedSec=null,this.skipRequestedBreakId=null,this.abortedBreakId=null,this.cutBreakId=null,this.erroredAssetId=null}guard(e,t){const i=this.getHandle();if(i)try{t(i)}catch(t){this.log(`Lens CMCD ${e} failed:`,t)}}}
|
|
1
|
+
import{LensEventType as e,LensToken as t}from"../vendor/lens-sdk/index.js";import{AssetType as i,BreakFormat as s,getBreakEvent as a}from"../types/BreakManifest";import{adErrorCode as d}from"../types/AdError";import{VendorAdError as r}from"../types/VendorAdError";import{SDK_VERSION as n}from"../generated/version";const h={[s.SINGLE]:"s",[s.DOUBLE]:"d",[s.LSHAPE_AD]:"la",[s.LSHAPE_CONTENT]:"lc",[s.OVERLAY]:"o"},o={[i.STATIC]:"s",[i.VAST]:"va",[i.VENDOR]:"ve"};function l(e){const t=a(e);if(null!==t)return t;const{start:i}=e;return"number"==typeof i?String(Math.round(1e3*i)):"string"==typeof i?i:i.event}function c(e){return o[e]??e}export class CmcdAdReporter{constructor(e,t){this.getHandle=e,this.log=t,this.sessionKeys={},this.activeBreakId=null,this.breakBeginTimestamp=null,this.breakAvailableMs=null,this.breakRequestedMs=null,this.firstAdReported=!1,this.breakFilledMs=0,this.breakWatchedMs=0,this.activeAdKeys=null,this.activeAdCompanion=null,this.activeAdAssetId=null,this.activeAdIsVendor=!1,this.activeAdBaseSec=0,this.breakLastTimeSec=0,this.pendingAdStart=null,this.activeAdWatchedSec=0,this.activeAdDeclaredSec=null,this.activeAdIsVast=!1,this.activeAdResolvedSec=null,this.skipRequestedBreakId=null,this.abortedBreakId=null,this.cutBreakId=null,this.erroredAssetId=null,this.finalizedBreakId=null}onSessionStart(e){this.sessionKeys={adv:n,admv:e.version,...e.organizationId?{adoid:e.organizationId}:{},...e.channelId?{adcid:e.channelId}:{},...e.eventId?{adeid:e.eventId}:{}},this.updateHeartbeat()}updateEventId(e){null===e?delete this.sessionKeys.adeid:this.sessionKeys.adeid=e,this.updateHeartbeat()}onSessionEnd(){this.finalizeActiveBreak(),this.sessionKeys={},this.resetBreakState(),this.guard("setHeartbeatPayload",e=>e.setHeartbeatPayload())}finalizeActiveBreak(){if(null===this.activeBreakId)return;const e=this.activeBreakId;this.emitPendingAdStart(null),null!==this.activeAdKeys&&this.emitAdEnd(this.activeAdKeys,this.activeAdCompanion,"a"),this.emitBreakEnd(e),this.finalizedBreakId=e}onSkipRequested(e){this.skipRequestedBreakId=e}onBreakAborted(e){this.abortedBreakId=e}onBreakCut(e){this.cutBreakId=e}onEvent(e){switch(e.type){case"adbreakbegin":this.onBreakBegin(e);break;case"adbegin":this.onAdBegin(e);break;case"adtimeupdate":{const t=e;this.breakLastTimeSec=Math.max(this.breakLastTimeSec,t.currentTime);null!==this.pendingAdStart&&(t.asset.id===this.pendingAdStart.assetId||this.pendingAdStart.isVendor&&t.break.id===this.activeBreakId)&&t.currentTime>(this.pendingAdStart?.isVendor?this.pendingAdStart.baseSec:0)&&null!==this.breakBeginTimestamp&&this.emitPendingAdStart(t.timestamp-this.breakBeginTimestamp);if(t.asset.id===this.activeAdAssetId||this.activeAdIsVendor&&t.break.id===this.activeBreakId){const e=t.currentTime-this.activeAdBaseSec;this.activeAdWatchedSec=Math.max(this.activeAdWatchedSec,e),this.activeAdIsVast&&Number.isFinite(t.duration)&&t.duration>0&&(this.activeAdResolvedSec=t.duration)}break}case"adend":this.onAdEnd(e);break;case"adbreakend":this.onBreakEnd(e);break;case"adfirstquartile":case"admidpoint":case"adthirdquartile":this.onMilestone(e);break;case"adclick":this.onClick(e);break;case"aderror":this.onAdError(e)}}onBreakBegin(i){this.resetBreakState(),this.finalizedBreakId=null,this.activeBreakId=i.break.id,this.breakBeginTimestamp=i.timestamp;const s=Math.round(1e3*i.remainingSec),a=this.requestedPlayableMs(i.break,i.format,s)??s;this.breakAvailableMs=s,this.breakRequestedMs=a,this.guard("abs",d=>{return d.emit(e.AD_BREAK_START,{custom:{...this.sessionKeys,brid:i.break.id,brst:l(i.break),brmd:Math.round(1e3*i.break.duration),brad:s,brrd:a,...i.format?{brf:new t((r=i.format,h[r]??r))}:{}}});var r}),this.updateHeartbeat()}onAdBegin(e){this.emitPendingAdStart(null);const s=e.asset,a=s.interaction?.clickThrough,d=!this.firstAdReported&&null!==this.breakBeginTimestamp;this.firstAdReported=!0;const r={brid:e.break.id,adasid:s.id,adid:e.adId??s.id,...void 0!==e.podPosition?{adpos:String(e.podPosition)}:{},...void 0!==s.duration?{adasd:Math.round(1e3*s.duration)}:{},adast:new t(c(s.type)),...a?{adcl:!0}:{},...e.creativeId?{adgcrid:e.creativeId}:{},...e.adId&&"string"==typeof s.uri&&"vendor"===s.type?{adgpid:s.uri}:{}};this.activeAdKeys=r,this.activeAdCompanion=s.companion?{id:s.companion.id,parentAssetId:s.id}:null,this.activeAdAssetId=s.id,this.activeAdIsVendor=s.type===i.VENDOR,this.activeAdBaseSec=this.activeAdIsVendor?this.breakLastTimeSec:0,this.activeAdWatchedSec=0,this.activeAdDeclaredSec=s.duration??null,this.activeAdIsVast=s.type===i.VAST,this.activeAdResolvedSec=null;const n={...this.sessionKeys,...r},h=s.companion?{...this.sessionKeys,brid:e.break.id,adasid:s.companion.id,adid:s.companion.id,adast:new t(c(s.companion.type)),adcomp:!0,adpasid:s.id}:null;d?this.pendingAdStart={assetId:s.id,custom:n,companionCustom:h,isVendor:this.activeAdIsVendor,baseSec:this.activeAdBaseSec}:this.emitAdStart(n,h),this.updateHeartbeat()}emitAdStart(t,i){this.guard("as",i=>i.emit(e.AD_START,{custom:t})),i&&this.guard("as",t=>t.emit(e.AD_START,{custom:i}))}emitPendingAdStart(e){const t=this.pendingAdStart;if(!t)return;this.pendingAdStart=null;const i=null!==e?{...t.custom,brttf:e}:t.custom;this.emitAdStart(i,t.companionCustom)}onAdEnd(e){if(e.break.id===this.finalizedBreakId)return;this.emitPendingAdStart(null);const t=e.asset,i=this.adEndReason(e),s=this.activeAdKeys??{brid:e.break.id,adasid:t.id,adid:e.adId??t.id},a=t.companion?{id:t.companion.id,parentAssetId:t.id}:null;this.emitAdEnd(s,a,i)}emitAdEnd(i,s,a){const d=this.activeAdFilledSec(),r=null!==d?Math.round(1e3*d):null,n=Math.round(1e3*this.activeAdWatchedSec),h=null!==r?Math.min(n,r):n;"s"===a&&this.guard("sk",t=>t.emit(e.AD_SKIP,{custom:{...this.sessionKeys,...i}})),this.guard("ae",s=>s.emit(e.AD_END,{custom:{...this.sessionKeys,...i,ader:new t(a),adwd:h}})),s&&this.guard("ae",d=>d.emit(e.AD_END,{custom:{...this.sessionKeys,brid:i.brid,adasid:s.id,adid:s.id,adcomp:!0,adpasid:s.parentAssetId,ader:new t(a)}})),this.breakWatchedMs+=h,this.breakFilledMs+=null!==r?r:h,this.activeAdKeys=null,this.activeAdCompanion=null,this.activeAdAssetId=null,this.activeAdIsVendor=!1,this.activeAdBaseSec=0,this.activeAdWatchedSec=0,this.activeAdDeclaredSec=null,this.activeAdIsVast=!1,this.activeAdResolvedSec=null,this.erroredAssetId===i.adasid&&(this.erroredAssetId=null),this.updateHeartbeat()}onBreakEnd(e){if(e.break.id!==this.finalizedBreakId){if(this.emitPendingAdStart(null),null!==this.breakAvailableMs){const t=this.requestedPlayableMs(e.break,e.format,this.breakAvailableMs);null!==t&&(this.breakRequestedMs=t)}this.emitBreakEnd(e.break.id)}}emitBreakEnd(t){const i=this.breakAvailableMs,s=null!==this.breakRequestedMs&&null!==i?Math.min(this.breakRequestedMs,i):this.breakRequestedMs,a=null!==s?Math.min(this.breakFilledMs,s):this.breakFilledMs,d=Math.min(this.breakWatchedMs,a);this.guard("abe",r=>r.emit(e.AD_BREAK_END,{custom:{...this.sessionKeys,brid:t,...null!==i?{brad:i}:{},...null!==s?{brrd:s}:{},brfd:a,brwd:d}})),this.resetBreakState(),this.updateHeartbeat()}onMilestone(e){this.emitPendingAdStart(null),this.emitCustomWithAdContext(e.type,e.break,e.asset)}onClick(e){this.emitPendingAdStart(null),this.emitCustomWithAdContext("adclick",e.break,e.asset)}onAdError(e){"ad"===e.source&&(this.emitPendingAdStart(null),e.asset&&(this.erroredAssetId=e.asset.id),this.guard("aderror",t=>t.emitCustom("aderror",{custom:{...this.sessionKeys,adec:e.asset?d(e.error):"DA-BREAK-NO-PLAYABLE-ASSET",...e.error instanceof r?{adevc:e.error.vendorCode}:{},...e.break&&e.asset?this.adContextKeys(e.break,e.asset):{...e.break?{brid:e.break.id}:{},...e.asset?{adasid:e.asset.id,adid:e.asset.id}:{}}}})))}emitCustomWithAdContext(e,t,i){this.guard(e,s=>s.emitCustom(e,{custom:{...this.sessionKeys,...this.adContextKeys(t,i)}}))}activeAdFilledSec(){const e=this.activeAdDeclaredSec,t=this.activeAdResolvedSec;return null===t?e:null===e?t:Math.min(t,e)}adContextKeys(e,t){return(t.id===this.activeAdAssetId||this.activeAdIsVendor&&e.id===this.activeBreakId)&&null!==this.activeAdKeys?{...this.activeAdKeys}:{brid:e.id,adasid:t.id,...void 0!==this.activeAdKeys?.adid?{adid:this.activeAdKeys.adid}:{}}}adEndReason(e){return this.skipRequestedBreakId===e.break.id?(this.skipRequestedBreakId=null,"s"):this.erroredAssetId===e.asset.id?"e":this.abortedBreakId===e.break.id?"a":this.cutBreakId===e.break.id?(this.cutBreakId=null,"a"):"c"}requestedPlayableMs(e,t,i){const s=Array.isArray(e.variant)?e.variant:[e.variant],a=s.find(e=>e.format===t)??s[0];if(!a)return null;if(0===a.assets.length)return null;const d=a.assets.map(e=>e.duration);if(d.some(e=>void 0===e||!Number.isFinite(e)||e<=0))return i;const r=d.reduce((e,t)=>e+(t??0),0);return Math.min(Math.round(1e3*r),i)}updateHeartbeat(){this.guard("setHeartbeatPayload",e=>e.setHeartbeatPayload({custom:{...this.sessionKeys,...this.activeBreakId?{brid:this.activeBreakId}:{},...void 0!==this.activeAdKeys?.adid?{adid:this.activeAdKeys.adid}:{}}}))}resetBreakState(){this.emitPendingAdStart(null),this.activeBreakId=null,this.breakBeginTimestamp=null,this.breakAvailableMs=null,this.breakRequestedMs=null,this.firstAdReported=!1,this.breakFilledMs=0,this.breakWatchedMs=0,this.activeAdKeys=null,this.activeAdCompanion=null,this.activeAdAssetId=null,this.activeAdIsVendor=!1,this.activeAdBaseSec=0,this.breakLastTimeSec=0,this.activeAdWatchedSec=0,this.activeAdDeclaredSec=null,this.activeAdIsVast=!1,this.activeAdResolvedSec=null,this.skipRequestedBreakId=null,this.abortedBreakId=null,this.cutBreakId=null,this.erroredAssetId=null}guard(e,t){const i=this.getHandle();if(i)try{t(i)}catch(t){this.log(`Lens CMCD ${e} failed:`,t)}}}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { KnownDiagnosticCode } from '../diagnostics/errorCodes';
|
|
2
|
+
import type { PlayerAdapter } from '../interfaces/PlayerAdapter';
|
|
3
|
+
import { type ExternalPlaybackGuardInput } from './externalPlaybackGuard';
|
|
4
|
+
import { type SsbSwitchPhase } from './ssbSwitchSignals';
|
|
5
|
+
export type ExternalPlaybackAllowedTrigger = 'connecting' | 'active' | 'break-ended';
|
|
6
|
+
export interface ExternalPlaybackGateDeps {
|
|
7
|
+
player: PlayerAdapter;
|
|
8
|
+
blockBeforeBreakSec: number;
|
|
9
|
+
/** The break timeline right now; all nulls when there is no scheduler (SSAI mode). */
|
|
10
|
+
guardInput(): Omit<ExternalPlaybackGuardInput, 'blockBeforeBreakSec'>;
|
|
11
|
+
diagnose(code: KnownDiagnosticCode, message: string, context?: Record<string, unknown>): void;
|
|
12
|
+
/** An AirPlay request passed the guard; the hook for whoever takes over the receiver. */
|
|
13
|
+
onAllowed(trigger: ExternalPlaybackAllowedTrigger): void;
|
|
14
|
+
/** The phase of the SSB switch that owns the route, or null when none is in progress. */
|
|
15
|
+
ssbSwitchPhase(): SsbSwitchPhase | null;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Keeps the content player off AirPlay while the channel offers an SSB stream: the gate is closed
|
|
19
|
+
* for the session, a request during or just before a break is reported as blocked, and a blocked
|
|
20
|
+
* request is evaluated again when the break ends if the route is still on AirPlay.
|
|
21
|
+
*/
|
|
22
|
+
export declare class ExternalPlaybackGate {
|
|
23
|
+
private readonly deps;
|
|
24
|
+
private managedByUs;
|
|
25
|
+
private closedByUs;
|
|
26
|
+
private pending;
|
|
27
|
+
private lastState;
|
|
28
|
+
constructor(deps: ExternalPlaybackGateDeps);
|
|
29
|
+
get managed(): boolean;
|
|
30
|
+
/** Called on session start and channel switch: true when the manifest carries an SSB stream. */
|
|
31
|
+
setManaged(managed: boolean): void;
|
|
32
|
+
/** The `externalplaybackchange` listener: reports the change, then guards a new AirPlay request. */
|
|
33
|
+
readonly onPlayerChange: (payload?: unknown) => void;
|
|
34
|
+
handleBreakEnded(): void;
|
|
35
|
+
isAllowedNow(): boolean;
|
|
36
|
+
dispose(): void;
|
|
37
|
+
private release;
|
|
38
|
+
private decide;
|
|
39
|
+
private evaluate;
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{diagnoseExternalPlaybackChange as e}from"./externalPlaybackDiagnostic";import{decideExternalPlaybackGuard as s}from"./externalPlaybackGuard";import{resolveExternalPlaybackRequestTrigger as t,shouldEvaluateExternalPlaybackRequest as a}from"./ssbSwitchSignals";export class ExternalPlaybackGate{constructor(s){this.deps=s,this.managedByUs=!1,this.closedByUs=!1,this.pending=!1,this.lastState="inactive",this.onPlayerChange=s=>{e(s,this.deps.diagnose);const a=s?.state;if(void 0===a)return;const i=t(a,this.lastState,this.pending);this.lastState=a,i?this.evaluate(i):"inactive"===a&&(this.pending=!1)}}get managed(){return this.managedByUs}setManaged(e){const{player:s}=this.deps;this.managedByUs=e&&"function"==typeof s.setExternalPlaybackAllowed,this.managedByUs&&!this.closedByUs?(s.setExternalPlaybackAllowed?.(!1),this.closedByUs=!0):this.managedByUs||this.release()}handleBreakEnded(){if(!this.pending)return;const e=this.deps.player.externalPlaybackState;"connecting"===e||"active"===e?this.evaluate("break-ended"):this.pending=!1}isAllowedNow(){return this.decide().allowed}dispose(){this.managedByUs=!1,this.pending=!1,this.release()}release(){this.pending=!1,this.closedByUs&&(this.closedByUs=!1,this.deps.player.setExternalPlaybackAllowed?.(!0))}decide(){return s({...this.deps.guardInput(),blockBeforeBreakSec:this.deps.blockBeforeBreakSec})}evaluate(e){if(!this.managedByUs||!a(this.deps.ssbSwitchPhase()))return;const t=this.deps.guardInput(),i=s({...t,blockBeforeBreakSec:this.deps.blockBeforeBreakSec});if(i.allowed)return this.pending=!1,void this.deps.onAllowed(e);this.pending=!0,this.deps.diagnose("DA-EXTERNAL-PLAYBACK-BLOCKED",`AirPlay blocked: ${i.reason}`,{reason:i.reason,breakId:i.breakId,secondsUntilBreak:t.secondsUntilNextBreak,trigger:e})}}
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import type { PlayerAdapter } from '../interfaces/PlayerAdapter';
|
|
2
|
-
import type { LiveStartPushbackConfig
|
|
3
|
-
/**
|
|
4
|
-
export
|
|
5
|
-
enabled: boolean;
|
|
6
|
-
enabledBy: 'config' | 'ptsSource';
|
|
7
|
-
}
|
|
8
|
-
/** An explicit `enabled` wins; otherwise the adjustment follows Anvato cue signaling. */
|
|
9
|
-
export declare function resolveLiveStartEnablement(config: LiveStartPushbackConfig | undefined, ptsSource: PtsSource | undefined): LiveStartEnablement;
|
|
2
|
+
import type { LiveStartPushbackConfig } from '../types/OptiViewAdsConfig';
|
|
3
|
+
/** The startup adjustment is opt-in: only an explicit `enabled: true` turns it on. */
|
|
4
|
+
export declare function isLiveStartPushbackEnabled(config: LiveStartPushbackConfig | undefined): boolean;
|
|
10
5
|
interface LiveStartPushbackDeps {
|
|
11
6
|
blocked: () => boolean;
|
|
12
7
|
setSeeking: (active: boolean) => void;
|
|
@@ -21,8 +16,8 @@ export declare class LiveStartPushbackController {
|
|
|
21
16
|
private startOffsetRequested;
|
|
22
17
|
private generation;
|
|
23
18
|
private cleanupLanding;
|
|
24
|
-
private readonly
|
|
25
|
-
constructor(player: PlayerAdapter, config: LiveStartPushbackConfig | undefined, deps: LiveStartPushbackDeps
|
|
19
|
+
private readonly enabled;
|
|
20
|
+
constructor(player: PlayerAdapter, config: LiveStartPushbackConfig | undefined, deps: LiveStartPushbackDeps);
|
|
26
21
|
start(): void;
|
|
27
22
|
stop(): void;
|
|
28
23
|
private disarm;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{contentSeekableEnd as e}from"./adPlayer/liveEdge";import{resolveLiveStartPushback as t}from"./resolveLiveStartPushback";export function
|
|
1
|
+
import{contentSeekableEnd as e}from"./adPlayer/liveEdge";import{resolveLiveStartPushback as t}from"./resolveLiveStartPushback";export function isLiveStartPushbackEnabled(e){return!0===e?.enabled}const i=e=>"number"==typeof e&&Number.isFinite(e)&&e>0,s=e=>Math.round(1e3*e)/1e3;export class LiveStartPushbackController{constructor(a,n,r){this.player=a,this.config=n,this.deps=r,this.armed=!1,this.playbackSeen=!1,this.startOffsetRequested=!1,this.generation=0,this.cleanupLanding=null,this.onPlaying=()=>{this.playbackSeen=!0,this.attempt()},this.attempt=()=>{if(!this.playbackSeen)return;if(!this.armed||this.deps.blocked()||this.player.paused||!1===this.player.playbackReady)return;const a=this.player.isLive??(this.player.duration===1/0||!Number.isFinite(this.player.duration)&&null);if(null===a)return;const n=this.startOffsetRequested;if(this.disarm(),this.startOffsetRequested=!1,!a)return;const r=this.player.targetDuration,l=i(r)?r:this.config?.targetDurationSeconds,o=this.config?.minimumDistanceTargetDurations??5,d=this.player.currentTime,u=e(this.player),h=this.publishedEdge(u),f=this.player.videoElement?.seekable,g=this.player.seekableStart??(f?.length?f.start(f.length-1):null),p={enabledBy:"config",position:d,seekableStart:g,seekableEnd:u,liveEdge:h,liveEdgeSource:i(this.player.liveEdge)?"adapter":"seekable",targetDurationSec:i(l)?l:null,targetDurationSource:i(r)?"adapter":i(l)?"config":"unavailable",requestedOffsetTargetDurations:o,startOffsetRequested:n};let c=null;if(i(l)?i(o)&&Number.isFinite(l*o)?(null===h||null===g||!Number.isFinite(g)||h<=g||!Number.isFinite(d))&&(c="no-range"):c="invalid-config":c="no-target-duration",!c&&i(l)&&null!==h&&Object.assign(p,{initialOffsetSec:s(h-d),initialOffsetTargetDurations:s((h-d)/l),requestedOffsetSec:s(o*l)}),!this.enabled)return void this.skip("disabled",p);if(c||!i(l)||null===h||null===g)return void this.skip(c??"no-range",p);if(n&&h-d>=o*l-.25)return void this.reportStartOffset(d,h,l,o,p);const y=t(d,g,h,l,o);if(null===y)return void this.skip(h-d>=o*l?"already-behind":"no-backward-target",p);Object.assign(p,{target:y,rewindSec:s(d-y),rewindTargetDurations:s((d-y)/l),targetOffsetSec:s(h-y),targetOffsetTargetDurations:s((h-y)/l),windowClamped:y>h-o*l,method:"seek"});const D=this.generation;this.deps.setSeeking(!0);const S=t=>{if(D!==this.generation||!this.cleanupLanding)return;this.cleanupLanding();const i="seeked"===t?this.player.currentTime:null,a=this.publishedEdge(e(this.player)),n=null!==i&&Number.isFinite(i)&&null!==a?a-i:null;this.deps.diagnose("DA-LIVE-START-PUSHBACK-LANDED",`Live start landing: TD=${l}s, offset=${null===n?"unknown":`${s(n)}s (${s(n/l)}xTD)`}, ${t}`,{...p,reason:t,landed:i,liveEdgeAtLanding:a,landedOffsetSec:null===n?null:s(n),landedOffsetTargetDurations:null===n?null:s(n/l),errorSec:null===i?null:s(i-y)})},m=()=>S("seeked"),b=setTimeout(()=>S("timeout"),1e4);if(this.cleanupLanding=()=>{this.cleanupLanding=null,clearTimeout(b),this.player.off("seeked",m),this.deps.setSeeking(!1)},this.player.on("seeked",m),this.deps.diagnose("DA-LIVE-START-PUSHBACK",`Live start: TD=${l}s, offset=${p.initialOffsetSec}s (${p.initialOffsetTargetDurations}xTD), requested=${o*l}s (${o}xTD), rewind=${p.rewindSec}s (${p.rewindTargetDurations}xTD), target=${y}s`,p),D===this.generation)try{this.player.seek(y)}catch{S("seek-failed")}},this.enabled=isLiveStartPushbackEnabled(n)}start(){this.stop(),this.armed=!0,this.playbackSeen=!this.player.paused&&!1!==this.player.playbackReady;const e=this.config?.minimumDistanceTargetDurations??5,t=i(this.player.targetDuration)?this.player.targetDuration:i(this.config?.targetDurationSeconds)?this.config.targetDurationSeconds:null;this.startOffsetRequested=this.enabled&&i(e)&&!0===this.player.requestLiveStartOffset?.(e,t),this.player.on("playing",this.onPlaying),this.player.on("timeupdate",this.attempt),this.attempt()}stop(){this.generation++,this.disarm(),this.cleanupLanding?.()}disarm(){this.armed=!1,this.player.off("playing",this.onPlaying),this.player.off("timeupdate",this.attempt),this.startOffsetRequested&&this.player.requestLiveStartOffset?.(null,null)}reportStartOffset(e,t,i,a,n){const r=t-a*i,l=t-e,o={...n,target:s(r),rewindSec:0,rewindTargetDurations:0,targetOffsetSec:s(a*i),targetOffsetTargetDurations:s(a),windowClamped:!1,method:"start-offset"};this.deps.diagnose("DA-LIVE-START-PUSHBACK",`Live start: TD=${i}s, began ${s(l)}s (${s(l/i)}xTD) behind live via the engine start offset, requested=${s(a*i)}s (${a}xTD), no seek`,o),this.deps.diagnose("DA-LIVE-START-PUSHBACK-LANDED",`Live start landing: TD=${i}s, offset=${s(l)}s (${s(l/i)}xTD), start-offset`,{...o,reason:"start-offset",landed:e,liveEdgeAtLanding:t,landedOffsetSec:s(l),landedOffsetTargetDurations:s(l/i),errorSec:s(e-r)})}publishedEdge(e){const t=this.player.liveEdge;return!i(t)||t-this.player.currentTime>=86400?e:null===e?t:Math.max(t,e)}skip(e,t){this.deps.diagnose("DA-LIVE-START-PUSHBACK-SKIPPED",`Live start unchanged: ${e}, TD=${t.targetDurationSec??"unknown"}s, offset=${t.initialOffsetSec??"unknown"}s (${t.initialOffsetTargetDurations??"unknown"}xTD)`,{...t,reason:e})}}
|
|
@@ -2,7 +2,7 @@ import { BreakManifest } from '../types/BreakManifest';
|
|
|
2
2
|
import { ManifestRequestInterceptor, ManifestResponseInterceptor } from '../types/OptiViewAdsConfig';
|
|
3
3
|
import { ManifestSignatureKey } from './manifestSignature';
|
|
4
4
|
/** Narrow diagnostic sink so the service does not depend on the full diagnostics module. */
|
|
5
|
-
export type ManifestDiagnose = (code: 'DA-MANIFEST-INTERCEPT-FAILED' | 'DA-MANIFEST-REQUEST-INTERCEPT-FAILED' | 'DA-MANIFEST-SIGNATURE-INVALID', message: string, context?: Record<string, unknown>) => void;
|
|
5
|
+
export type ManifestDiagnose = (code: 'DA-MANIFEST-INTERCEPT-FAILED' | 'DA-MANIFEST-REQUEST-INTERCEPT-FAILED' | 'DA-MANIFEST-SIGNATURE-INVALID' | 'DA-MANIFEST-EVENT-INVALID', message: string, context?: Record<string, unknown>) => void;
|
|
6
6
|
/** Fetches, verifies, parses and polls break manifests. */
|
|
7
7
|
/**
|
|
8
8
|
* Whole-request budget for one manifest fetch. Matches the Android OkHttp `callTimeout` and
|
|
@@ -47,6 +47,7 @@ export declare class ManifestService {
|
|
|
47
47
|
private trustedKeys;
|
|
48
48
|
/** Per-host tally of manifest reads this session, and the last failure. */
|
|
49
49
|
private readonly hostTally;
|
|
50
|
+
private readonly reportedEventIssues;
|
|
50
51
|
private lastFailure;
|
|
51
52
|
constructor(debug?: boolean, interceptor?: ManifestResponseInterceptor, diagnose?: ManifestDiagnose, requestInterceptor?: ManifestRequestInterceptor, trustedKeys?: readonly ManifestSignatureKey[]);
|
|
52
53
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fetchWithTimeout as t}from"./fetchWithTimeout";import{verifyManifestSignature as e,MANIFEST_SIGNATURE_HEADER as
|
|
1
|
+
import{fetchWithTimeout as t}from"./fetchWithTimeout";import{verifyManifestSignature as e,MANIFEST_SIGNATURE_HEADER as s}from"./manifestSignature";import{embeddedManifestKeys as i}from"./manifestTrustedKeys";import{parseBreakManifest as r}from"./parseBreakManifest";import{resolvePollingDecision as n}from"./resolvePollingDecision";import{debugLog as o}from"../diagnostics/debugLog";export const MANIFEST_FETCH_TIMEOUT_MS=15e3;export class ManifestService{get currentPollingIntervalSeconds(){return this.pollingIntervalSeconds}constructor(t=!1,e,s,r,n){this.manifestUrl=null,this.pollingInterval=null,this.pollingIntervalSeconds=null,this.pollCallbacks=null,this.currentManifest=null,this.isActive=!1,this.hostTally=new Map,this.reportedEventIssues=new Set,this.lastFailure=null,this.debug=t,this.interceptor=e,this.diagnose=s,this.requestInterceptor=r,this.trustedKeys=n??i()}takeLastFailure(){const t=this.lastFailure;return this.lastFailure=null,t}record(t,e,s,i){const r=function(t){try{return new URL(t).host}catch{return t}}(t),n=this.hostTally.get(r)??{ok:0,fail:0};if("ok"===e)return n.ok++,void this.hostTally.set(r,n);this.lastFailure={url:t,host:r,outcome:e,ms:s,hostOk:n.ok,hostFail:n.fail,...void 0===i?{}:{status:i}},n.fail++,this.hostTally.set(r,n)}setManifestUrl(t){this.manifestUrl=t,this.reportedEventIssues.clear()}async fetchManifest(){if(!this.manifestUrl)throw new Error("Manifest URL not set. Call setManifestUrl() before fetching.");this.log("Fetching manifest from:",this.manifestUrl);const t=await this.fetchManifestData(this.manifestUrl),e=this.parseManifest(t),s=await this.applyInterceptor(e);return this.currentManifest=s,this.log("Manifest loaded:",s.breaks.length,"breaks"),s}async fetchManifestData(i){const r={url:i};let n;if(this.requestInterceptor)try{n=await this.requestInterceptor({...r},{url:i})??void 0}catch(t){const e=t instanceof Error?t.message:String(t);this.diagnose?.("DA-MANIFEST-REQUEST-INTERCEPT-FAILED",e,{url:i}),this.log("Manifest request interceptor threw; using network fetch:",e),n=void 0}if(n&&"body"in n)return this.log("Manifest request interceptor returned a mock response (no network)"),"string"==typeof n.body?JSON.parse(n.body):n.body;const o=n??r,l=Date.now();let a;try{a=await t(o.url,o.headers?{headers:o.headers}:{},15e3,"Failed to fetch manifest: no response within 15000ms")}catch(t){const e=t instanceof Error&&t.message.includes("no response within");throw this.record(o.url,e?"timeout":"network",Date.now()-l),t}if(!a.ok)throw this.record(o.url,"http",Date.now()-l,a.status),new Error(`Failed to fetch manifest: ${a.status} ${a.statusText}`);this.record(o.url,"ok",Date.now()-l);const h=new Uint8Array(await a.arrayBuffer()),c=a.headers.get(s),u=e(c,h,this.trustedKeys);if(!u.valid){const t=`Manifest signature verification failed: ${u.reason}`;throw this.diagnose?.("DA-MANIFEST-SIGNATURE-INVALID",t,{url:o.url,reason:u.reason}),this.log("Rejecting manifest:",t),new Error(t)}return JSON.parse((new TextDecoder).decode(h))}async applyInterceptor(t){if(!this.interceptor)return t;const e=this.manifestUrl??"";try{return await this.interceptor(t,{url:e})}catch(s){const i=s instanceof Error?s.message:String(s);return this.diagnose?.("DA-MANIFEST-INTERCEPT-FAILED",i,{url:e}),this.log("Manifest interceptor threw; using un-modified manifest:",i),t}}parseManifest(t){return r(t,t=>{this.reportedEventIssues.has(t.key)||(this.reportedEventIssues.add(t.key),this.diagnose?.("DA-MANIFEST-EVENT-INVALID",t.message,t.context))})}startPolling(t,e){this.pollCallbacks={onUpdate:t,onError:e},this.applyPollingDecision()}stopPolling(){this.pollCallbacks=null,this.clearPollingTimer()}setActive(t){this.isActive!==t&&(this.isActive=t,this.log("Active state changed:",t),this.applyPollingDecision())}applyPollingDecision(){if(!this.pollCallbacks)return;const t=n(this.currentManifest?.polling,this.isActive);if("once"===t.mode)return this.pollingInterval&&this.log("Manifest has no polling object — fetch-once, stopping timer"),void this.clearPollingTimer();this.pollingInterval&&this.pollingIntervalSeconds===t.intervalSeconds||(this.clearPollingTimer(),this.log("Starting polling with interval:",t.intervalSeconds,"seconds"),this.pollingIntervalSeconds=t.intervalSeconds,this.pollingInterval=setInterval(async()=>{const t=this.pollCallbacks;if(t){try{const e=await this.fetchManifest();e.breaks.length>0&&this.log(`📡 Manifest: ${e.breaks.length} break(s)`),t.onUpdate(e)}catch(e){t.onError(e instanceof Error?e:new Error(String(e)))}this.applyPollingDecision()}},1e3*t.intervalSeconds))}clearPollingTimer(){this.pollingInterval&&(clearInterval(this.pollingInterval),this.pollingInterval=null,this.pollingIntervalSeconds=null,this.log("Polling stopped"))}getManifest(){return this.currentManifest}reset(){this.stopPolling(),this.currentManifest=null,this.manifestUrl=null,this.isActive=!1,this.reportedEventIssues.clear()}destroy(){this.stopPolling(),this.currentManifest=null,this.manifestUrl=null,this.reportedEventIssues.clear()}log(...t){this.debug&&o("[OptiViewAds:ManifestService]",...t)}}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Marks a window as reload noise: on while armed, released by a timer, re-armable, cancellable. */
|
|
2
|
+
export declare class ReloadGuard {
|
|
3
|
+
private readonly setReloading;
|
|
4
|
+
private readonly windowMs;
|
|
5
|
+
private timer;
|
|
6
|
+
constructor(setReloading: (reloading: boolean) => void, windowMs: number);
|
|
7
|
+
/** Starts (or restarts) the window. */
|
|
8
|
+
arm(): void;
|
|
9
|
+
/** Cancels a pending release without signalling. */
|
|
10
|
+
dispose(): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class ReloadGuard{constructor(t,i){this.setReloading=t,this.windowMs=i,this.timer=null}arm(){this.setReloading(!0),this.timer&&clearTimeout(this.timer),this.timer=setTimeout(()=>{this.timer=null,this.setReloading(!1)},this.windowMs)}dispose(){this.timer&&clearTimeout(this.timer),this.timer=null}}
|
|
@@ -2,6 +2,7 @@ import { BreakState } from './BreakScheduler';
|
|
|
2
2
|
import type { Clock } from './BreakScheduler';
|
|
3
3
|
import { Break } from '../types/BreakManifest';
|
|
4
4
|
import type { BreakTimingState } from './breakTiming';
|
|
5
|
+
import type { BreakPreloadLedger } from './BreakPreloadLedger';
|
|
5
6
|
export interface TrackedBreak {
|
|
6
7
|
break: Break;
|
|
7
8
|
state: BreakState;
|
|
@@ -10,7 +11,7 @@ export interface RewindRearmDeps {
|
|
|
10
11
|
breaks: Map<string, TrackedBreak>;
|
|
11
12
|
retiredBreaks: Map<string, Break>;
|
|
12
13
|
completedBreakIds: Set<string>;
|
|
13
|
-
|
|
14
|
+
preloadLedger: BreakPreloadLedger;
|
|
14
15
|
timing: BreakTimingState;
|
|
15
16
|
clock: Clock;
|
|
16
17
|
log: (...args: unknown[]) => void;
|
|
@@ -74,6 +75,7 @@ export declare class RewindRearm {
|
|
|
74
75
|
/** Anchor played-time tracking at the position where content playback began. */
|
|
75
76
|
markPlaybackStart(positionMs: number | null): void;
|
|
76
77
|
setAdStartDelayMs(ms: number): void;
|
|
78
|
+
ignoreAdStartDelay(): void;
|
|
77
79
|
/**
|
|
78
80
|
* The schedule now follows another channel: pre-roll `delay` counts from here. Once playback
|
|
79
81
|
* has started, `adStartDelay` is frozen (also for later polls); before that the next manifest's
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BreakState as e}from"./BreakScheduler";import{Timebase as s}from"../types/BreakManifest";export class RewindRearm{constructor(e){this.deps=e,this._adStartDelayMs=0,this.adStartDelayElapsedLogged=!1,this.adStartDelayIgnored=!1,this.channelPlayedBaselineMs=0,this._playbackStarted=!1,this._playedMs=0,this.lastPlayedPositionMs=null,this.lastPlayedClockMs=null,this.contentReloading=!1,this.resumeSeekBaselineMs=null,this.resumeSeekBreakEndMs=null,this.resumeSeekInFlight=!1,this.pendingRewindFromMs=null,this.lastCurrentTime=null,this.REWIND_JITTER_TOLERANCE_MS=2e3}isAdStartDelayActive(){return this._adStartDelayMs>0&&this._playedMs<this._adStartDelayMs}get playbackStarted(){return this._playbackStarted}get playedMs(){return this._playedMs}markPlaybackStart(e){this._playbackStarted=!0,this.lastPlayedPositionMs=e,this.lastPlayedClockMs=this.deps.clock.now()}setAdStartDelayMs(e){this.adStartDelayIgnored||(this._adStartDelayMs=e)}
|
|
1
|
+
import{BreakState as e}from"./BreakScheduler";import{Timebase as s}from"../types/BreakManifest";export class RewindRearm{constructor(e){this.deps=e,this._adStartDelayMs=0,this.adStartDelayElapsedLogged=!1,this.adStartDelayIgnored=!1,this.channelPlayedBaselineMs=0,this._playbackStarted=!1,this._playedMs=0,this.lastPlayedPositionMs=null,this.lastPlayedClockMs=null,this.contentReloading=!1,this.resumeSeekBaselineMs=null,this.resumeSeekBreakEndMs=null,this.resumeSeekInFlight=!1,this.pendingRewindFromMs=null,this.lastCurrentTime=null,this.REWIND_JITTER_TOLERANCE_MS=2e3}isAdStartDelayActive(){return this._adStartDelayMs>0&&this._playedMs<this._adStartDelayMs}get playbackStarted(){return this._playbackStarted}get playedMs(){return this._playedMs}markPlaybackStart(e){this._playbackStarted=!0,this.lastPlayedPositionMs=e,this.lastPlayedClockMs=this.deps.clock.now()}setAdStartDelayMs(e){this.adStartDelayIgnored||(this._adStartDelayMs=e)}ignoreAdStartDelay(){this._playbackStarted&&(this._adStartDelayMs=0,this.adStartDelayIgnored=!0)}beginChannel(){this.channelPlayedBaselineMs=this._playedMs,this.ignoreAdStartDelay()}get channelPlayedMs(){return this._playedMs-this.channelPlayedBaselineMs}get adStartDelayMs(){return this._adStartDelayMs}setContentReloading(e){this.contentReloading=e,e||(this.lastCurrentTime=null),this.pendingRewindFromMs=null}notifyResumeSeek(e,s){Number.isFinite(e)&&Number.isFinite(s)&&(this.resumeSeekBaselineMs=1e3*e,this.resumeSeekBreakEndMs=1e3*s,this.resumeSeekInFlight=!0,this.lastCurrentTime=this.resumeSeekBaselineMs,this.pendingRewindFromMs=null)}notifyViewerSeek(){this.resumeSeekBaselineMs=null,this.resumeSeekBreakEndMs=null,this.resumeSeekInFlight=!1}trackPlayedTime(e){if(!this._playbackStarted)return;const s=this.deps.clock.now();if(null===e)return this.lastPlayedPositionMs=null,void(this.lastPlayedClockMs=s);if(null!==this.lastPlayedPositionMs&&null!==this.lastPlayedClockMs){const t=e-this.lastPlayedPositionMs,i=s-this.lastPlayedClockMs;this._playedMs+=Math.min(Math.max(0,t),Math.max(0,i)),this._adStartDelayMs>0&&this._playedMs>=this._adStartDelayMs&&!this.adStartDelayElapsedLogged&&(this.adStartDelayElapsedLogged=!0,this.deps.log(`⏰ Ad start delay elapsed (${this._adStartDelayMs/1e3}s played)`))}this.lastPlayedPositionMs=e,this.lastPlayedClockMs=s}rearmOnRewind(e){const s=this.lastCurrentTime;if(this.lastCurrentTime=e,null!==this.resumeSeekBreakEndMs&&e>this.resumeSeekBreakEndMs&&(this.resumeSeekBaselineMs=null,this.resumeSeekBreakEndMs=null,this.resumeSeekInFlight=!1),this.contentReloading)this.pendingRewindFromMs=null;else{if(null!==this.pendingRewindFromMs){const s=this.pendingRewindFromMs;if(this.pendingRewindFromMs=null,this.isRewindBeyondJitter(s,e))return void this.rearmRetiredBreaks(e)}null===s||e>=s?this.resumeSeekInFlight&&null!==s&&e>s&&(this.resumeSeekInFlight=!1,this.resumeSeekBaselineMs=null,this.resumeSeekBreakEndMs=null):this.isRewindBeyondJitter(s,e)&&(this.resumeSeekInFlight&&null!==this.resumeSeekBreakEndMs&&e>=0&&e<=this.resumeSeekBreakEndMs||(this.pendingRewindFromMs=s))}}reset(){this.lastCurrentTime=null,this._adStartDelayMs=0,this.adStartDelayElapsedLogged=!1,this._playbackStarted=!1,this._playedMs=0,this.adStartDelayIgnored=!1,this.channelPlayedBaselineMs=0,this.lastPlayedPositionMs=null,this.lastPlayedClockMs=null,this.resumeSeekBaselineMs=null,this.resumeSeekBreakEndMs=null,this.resumeSeekInFlight=!1,this.pendingRewindFromMs=null}isRewindBeyondJitter(e,t){return!(t>=e)&&!(this.deps.timing.timebase===s.WALLCLOCK&&e-t<=this.REWIND_JITTER_TOLERANCE_MS)}rearmRetiredBreaks(s){if(this.resumeSeekBaselineMs=null,this.resumeSeekBreakEndMs=null,this.resumeSeekInFlight=!1,0!==this.deps.retiredBreaks.size)for(const[t,i]of[...this.deps.retiredBreaks]){const a=this.deps.breaks.get(t);if(a&&(a.state===e.PENDING||a.state===e.ACTIVE||a.state===e.CHAINED))continue;const l=this.deps.getBreakStartTime(i);if(null===l)continue;s<l+1e3*(i.duration??0)&&(this.deps.retiredBreaks.delete(t),this.deps.completedBreakIds.delete(t),this.deps.preloadLedger.deletePreloaded(t),this.deps.breaks.set(t,{break:i,state:e.PENDING}),this.deps.log(`↩️ Re-armed break "${t}" after backward seek`))}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{objectEntries as e}from"./runtimeObject";import{applyAssetParameterMacros as t}from"./assetParameterMacros";import{buildAssetParameterMacroTable as a}from"./assetParameterMacroTable";import{resolveAssetParameters as s}from"./resolveAssetParameters";import{AssetType as r,MediaType as i,BreakFormat as o}from"../types/BreakManifest";import{GAM_DUPLICATE_METADATA_DIAGNOSTIC_MESSAGE as n,GamStreamManager as m}from"./GamStreamManager";import{resolveBreakFormat as d}from"./AdBreakSequencer";import{buildStitcherMasterUrl as c}from"./StitcherUrlBuilder";import{resolveExternalVendorParameters as h}from"./resolveExternalVendorConfigurations";export class SsaiController{constructor(e,t,a){this.player=e,this.options=t,this.callbacks=a,this.gamStreamManager=null,this.breakCounter=0,this.adIndexInBreak=0,this.currentBreak=null,this.currentAsset=null,this.customerMacros={},this.onTimedMetadata=e=>{if(!e)return;const t=e;this.gamStreamManager?.processTimedMetadata(t)}}async start(e){const t=this.player.videoElement;if(!t)throw new Error("SSAI mode requires a content PlayerAdapter that exposes a videoElement (for IMA DAI).");if(!e.customAssetKey)throw new Error("SSAI mode requires a customAssetKey in startSession().");this.sessionConfigParameters=e.assetParameters,this.updatedParameters=void 0,this.manifestParameters=void 0,this.customerMacros={...e.assetParameterMacros};const a=new m(t,void 0,this.options.debug);a.setCallbacks(this.buildGamCallbacks()),this.gamStreamManager=a,await a.initialize({networkCode:this.options.ssai.networkCode,customAssetKey:e.customAssetKey,adTagParameters:this.resolveAdTagParameters(),streamActivityMonitorId:this.options.gam.streamActivityMonitorId});const s=a.getStreamId();if(!s)throw new Error("SSAI mode: IMA DAI session did not return a stream id.");const r=c({stitcherUrl:e.stitcherUrl,streamId:s,maxBitrate:this.options.maxBitrate});this.callbacks.log("SSAI stitched master URL:",r),this.player.on("timedmetadata",this.onTimedMetadata),await this.player.load(r),this.options.autoplay&&await this.player.play()}async startSteered(e,t,a){const s=this.player.videoElement;if(!s)throw new Error("SSAI mode requires a content PlayerAdapter that exposes a videoElement (for IMA DAI).");this.sessionConfigParameters=t,this.updatedParameters=void 0,this.manifestParameters=e.assetParameters,this.customerMacros={...a};const r=new m(s,void 0,this.options.debug);r.setCallbacks(this.buildGamCallbacks()),this.gamStreamManager=r;const i=await r.initializeLiveStream({assetKey:e.assetKey,networkCode:e.networkCode,adTagParameters:this.resolveAdTagParameters(),streamActivityMonitorId:this.options.gam.streamActivityMonitorId});this.callbacks.log("SSAI full-service DAI stream URL:",i),this.player.on("timedmetadata",this.onTimedMetadata),await this.player.load(i),this.options.autoplay&&await this.player.play()}resolveAdTagParameters(){const e=s(this.sessionConfigParameters,this.updatedParameters,this.manifestParameters,void 0),r=t(e,a({customerMacros:this.customerMacros,element:this.player.videoElement??null,breakDurationSec:void 0})
|
|
1
|
+
import{objectEntries as e}from"./runtimeObject";import{applyAssetParameterMacros as t}from"./assetParameterMacros";import{buildAssetParameterMacroTable as a}from"./assetParameterMacroTable";import{resolveAssetParameters as s}from"./resolveAssetParameters";import{AssetType as r,MediaType as i,BreakFormat as o}from"../types/BreakManifest";import{GAM_DUPLICATE_METADATA_DIAGNOSTIC_MESSAGE as n,GamStreamManager as m}from"./GamStreamManager";import{resolveBreakFormat as d}from"./AdBreakSequencer";import{buildStitcherMasterUrl as c}from"./StitcherUrlBuilder";import{resolveExternalVendorParameters as h}from"./resolveExternalVendorConfigurations";export class SsaiController{constructor(e,t,a){this.player=e,this.options=t,this.callbacks=a,this.gamStreamManager=null,this.breakCounter=0,this.adIndexInBreak=0,this.currentBreak=null,this.currentAsset=null,this.customerMacros={},this.onTimedMetadata=e=>{if(!e)return;const t=e;this.gamStreamManager?.processTimedMetadata(t)}}async start(e){const t=this.player.videoElement;if(!t)throw new Error("SSAI mode requires a content PlayerAdapter that exposes a videoElement (for IMA DAI).");if(!e.customAssetKey)throw new Error("SSAI mode requires a customAssetKey in startSession().");this.sessionConfigParameters=e.assetParameters,this.updatedParameters=void 0,this.manifestParameters=void 0,this.customerMacros={...e.assetParameterMacros};const a=new m(t,void 0,this.options.debug);a.setCallbacks(this.buildGamCallbacks()),this.gamStreamManager=a,await a.initialize({networkCode:this.options.ssai.networkCode,customAssetKey:e.customAssetKey,adTagParameters:this.resolveAdTagParameters(),streamActivityMonitorId:this.options.gam.streamActivityMonitorId});const s=a.getStreamId();if(!s)throw new Error("SSAI mode: IMA DAI session did not return a stream id.");const r=c({stitcherUrl:e.stitcherUrl,streamId:s,maxBitrate:this.options.maxBitrate});this.callbacks.log("SSAI stitched master URL:",r),this.player.on("timedmetadata",this.onTimedMetadata),await this.player.load(r),this.options.autoplay&&await this.player.play()}async startSteered(e,t,a){const s=this.player.videoElement;if(!s)throw new Error("SSAI mode requires a content PlayerAdapter that exposes a videoElement (for IMA DAI).");this.sessionConfigParameters=t,this.updatedParameters=void 0,this.manifestParameters=e.assetParameters,this.customerMacros={...a};const r=new m(s,void 0,this.options.debug);r.setCallbacks(this.buildGamCallbacks()),this.gamStreamManager=r;const i=await r.initializeLiveStream({assetKey:e.assetKey,networkCode:e.networkCode,adTagParameters:this.resolveAdTagParameters(),streamActivityMonitorId:this.options.gam.streamActivityMonitorId});this.callbacks.log("SSAI full-service DAI stream URL:",i),this.player.on("timedmetadata",this.onTimedMetadata),await this.player.load(i),this.options.autoplay&&await this.player.play()}resolveAdTagParameters(){const e=s(this.sessionConfigParameters,this.updatedParameters,this.manifestParameters,void 0),r=t(e,a({customerMacros:this.customerMacros,element:this.player.videoElement??null,breakDurationSec:void 0}));for(const{key:e,macro:t}of r.unresolved)this.callbacks.diagnose("DA-ASSET-MACRO-UNKNOWN",`Asset parameter '${e}' sent with literal macro '${t}': the macro has no value.`,{key:e,macro:t});for(const{key:e,macro:t,pair:a}of r.omitted)this.callbacks.diagnose("DA-ASSET-PARAMETER-OMITTED",`Asset parameter '${e}' not sent: macro '${t}' is empty.`,{key:e,macro:t,...void 0!==a?{pair:a}:{}});return r.params}updateAssetParameters(e){this.updatedParameters=e,this.gamStreamManager?.replaceAdTagParameters(this.resolveAdTagParameters()??{})}resolveExternalVendorConfiguration(e){const t=h(e,s(this.sessionConfigParameters,this.updatedParameters,void 0,void 0),a({customerMacros:this.customerMacros,element:this.player.videoElement??null,breakDurationSec:void 0}));for(const{key:e,macro:a}of t.unresolved)this.callbacks.diagnose("DA-ASSET-MACRO-UNKNOWN",`Asset parameter '${e}' sent with literal macro '${a}': the macro has no value.`,{key:e,macro:a});for(const{key:e,macro:a}of t.omitted)this.callbacks.diagnose("DA-ASSET-PARAMETER-OMITTED",`Asset parameter '${e}' not sent: macro '${a}' is empty.`,{key:e,macro:a});return t.configuration}updateAssetParameterMacros(t){const a={...this.customerMacros};for(const[s,r]of e(t))void 0===r?delete a[s]:a[s]=r;this.customerMacros=a,this.callbacks.diagnose("DA-ASSET-PARAMETERS-UPDATED","Asset parameter macros updated.",{source:"macros",keys:Object.keys(t).sort()}),this.gamStreamManager?.replaceAdTagParameters(this.resolveAdTagParameters()??{})}get sessionAssetParameterMacros(){return{...this.customerMacros}}setManifestAssetParameters(e){(function(e,t){e&&0===Object.keys(e).length&&(e=void 0);t&&0===Object.keys(t).length&&(t=void 0);if(e===t)return!0;if(!e||!t)return!1;const a=Object.keys(e);return a.length===Object.keys(t).length&&a.every(a=>e[a]===t[a])})(this.manifestParameters,e)||(this.manifestParameters=e,this.gamStreamManager&&(this.gamStreamManager.replaceAdTagParameters(this.resolveAdTagParameters()??{}),this.callbacks.diagnose("DA-ASSET-PARAMETERS-UPDATED","Manifest asset parameters changed; IMA targeting updated.",{source:"manifest",keys:Object.keys(e??{}).sort()})))}stop(){this.player.off("timedmetadata",this.onTimedMetadata),this.gamStreamManager?.destroy(),this.gamStreamManager=null,this.sessionConfigParameters=void 0,this.updatedParameters=void 0,this.manifestParameters=void 0,this.customerMacros={},this.currentBreak=null,this.currentAsset=null,this.breakCounter=0,this.adIndexInBreak=0}buildGamCallbacks(){return{onAdBreakStarted:()=>{this.breakCounter+=1,this.adIndexInBreak=0,this.currentBreak=this.synthBreak(`ssai-break-${this.breakCounter}`),this.callbacks.emit("adbreakbegin",{type:"adbreakbegin",timestamp:Date.now(),break:this.currentBreak,format:d(this.currentBreak),elapsedSec:0,remainingSec:this.currentBreak.duration})},onAdStarted:e=>{const t=this.ensureBreak(),a=this.adIndexInBreak;this.adIndexInBreak+=1,this.currentAsset=this.synthAsset(`${t.id}-ad-${this.adIndexInBreak}`),this.callbacks.emit("adbegin",{type:"adbegin",timestamp:Date.now(),break:t,format:d(t),asset:this.currentAsset,adIndex:a,totalAds:0,...void 0!==e?.adPosition&&Number.isInteger(e.adPosition)&&e.adPosition>=1?{podPosition:e.adPosition}:{}})},onAdFirstQuartile:()=>this.emitQuartile("adfirstquartile"),onAdMidpoint:()=>this.emitQuartile("admidpoint"),onAdThirdQuartile:()=>this.emitQuartile("adthirdquartile"),onDuplicateMetadata:e=>{this.callbacks.diagnose("DA-GAM-DUPLICATE-METADATA",n,{...this.currentAsset?{assetId:this.currentAsset.id}:{},startTime:e})},onAdComplete:()=>{const e=this.ensureBreak(),t=this.currentAsset??this.synthAsset(`${e.id}-ad`);this.callbacks.emit("adend",{type:"adend",timestamp:Date.now(),break:e,format:d(e),asset:t,adIndex:Math.max(0,this.adIndexInBreak-1),totalAds:0}),this.currentAsset=null},onAdBreakEnded:()=>{const e=this.ensureBreak();this.callbacks.emit("adbreakend",{type:"adbreakend",timestamp:Date.now(),break:e,format:d(e)}),this.currentBreak=null,this.currentAsset=null},onError:e=>{this.callbacks.diagnose("DA-SSAI-IMA-ERROR",e),this.callbacks.emit("aderror",{type:"aderror",timestamp:Date.now(),source:"ad",...this.currentBreak?{break:this.currentBreak,format:d(this.currentBreak)}:{},...this.currentAsset?{asset:this.currentAsset}:{},error:new Error(e)})}}}emitQuartile(e){const t=this.ensureBreak(),a=this.currentAsset??this.synthAsset(`${t.id}-ad`);this.callbacks.emit(e,{type:e,timestamp:Date.now(),break:t,format:d(t),asset:a})}ensureBreak(){return this.currentBreak||(this.breakCounter+=1,this.currentBreak=this.synthBreak(`ssai-break-${this.breakCounter}`)),this.currentBreak}synthBreak(e){return{id:e,start:this.player.currentTime,duration:0,variant:{format:o.SINGLE,assets:[]}}}synthAsset(e){return{id:e,type:r.STATIC,mediaType:i.VIDEO,uri:""}}}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Asset, Break } from '../../types/BreakManifest';
|
|
2
|
+
/** Creative identity IMA reports for an ad of a GAM pod; absent for other asset kinds. */
|
|
3
|
+
export interface AdCreativeIdentity {
|
|
4
|
+
adId?: string | undefined;
|
|
5
|
+
creativeId?: string | undefined;
|
|
6
|
+
/** 1-based position within the pod (IMA `getAdPodInfo().getAdPosition()` / VAST `sequence`); absent when unreported. */
|
|
7
|
+
podPosition?: number | undefined;
|
|
8
|
+
}
|
|
9
|
+
export interface AdPlayerCallbacks {
|
|
10
|
+
onAdStart: (breakInfo: Break, asset: Asset, index: number, total: number, identity?: AdCreativeIdentity) => void;
|
|
11
|
+
onAdEnd: (breakInfo: Break, asset: Asset, index: number, total: number, identity?: AdCreativeIdentity) => void;
|
|
12
|
+
/** `asset` is undefined for a break-level error (no playable variant, the break never began). */
|
|
13
|
+
onAdError: (breakInfo: Break, asset: Asset | undefined, error: Error) => void;
|
|
14
|
+
onAdTimeUpdate: (breakInfo: Break, asset: Asset, currentTime: number, duration: number) => void;
|
|
15
|
+
onAdWaiting: (breakInfo: Break, asset: Asset) => void;
|
|
16
|
+
onAdPlaying: (breakInfo: Break, asset: Asset) => void;
|
|
17
|
+
onBreakEnd: (breakInfo: Break) => void;
|
|
18
|
+
/** The break-cut timer expired with an ad still in flight; fired before that ad's `adend`. */
|
|
19
|
+
onBreakCut?: (breakInfo: Break) => void;
|
|
20
|
+
/**
|
|
21
|
+
* A content-covering break ended before its content pause landed, so no player transition
|
|
22
|
+
* announces the resume. Only the deferred-pause renderers (Kotlin/Swift) fire this.
|
|
23
|
+
*/
|
|
24
|
+
onBreakEndedWithoutContentPause?: (breakInfo: Break) => void;
|
|
25
|
+
/** Ad overlay became visible; gates `adbreakbegin` so the AD badge never precedes the first ad frame. */
|
|
26
|
+
onBreakVisible?: (breakInfo: Break) => void;
|
|
27
|
+
/** The current break soft-ended into a chained successor (overlay held, no content resume). */
|
|
28
|
+
onChainedBreakBegin?: (next: Break) => void;
|
|
29
|
+
/** A chained successor resolved but has nothing to play; the SDK must retire it (completeBreak). */
|
|
30
|
+
onChainedBreakSkipped?: (next: Break) => void;
|
|
31
|
+
/**
|
|
32
|
+
* The ad player buffered the next ad of `breakId` behind the playing ad: the playing break's next
|
|
33
|
+
* ad, or a chained break's first ad (`late` after a change during the last ad).
|
|
34
|
+
*/
|
|
35
|
+
onSuccessorBuffered?: (breakId: string, late: boolean) => void;
|
|
36
|
+
/** The ad player discarded the buffered ad of `breakId`. */
|
|
37
|
+
onSuccessorDropped?: (breakId: string) => void;
|
|
38
|
+
/**
|
|
39
|
+
* Fired just before the runtime's own post-break resume-seek, so the scheduler does not read
|
|
40
|
+
* the backward step as a DVR seek-back and re-arm the break.
|
|
41
|
+
*/
|
|
42
|
+
onContentResumeSeek?: (targetSeconds: number, breakEndSeconds: number) => void;
|
|
43
|
+
/**
|
|
44
|
+
* Brackets a content RELOAD (shared-element break end), so the playhead passing through the
|
|
45
|
+
* start of the freshly loaded stream is not read as a seek back before the break.
|
|
46
|
+
*/
|
|
47
|
+
onContentReloading?: (active: boolean) => void;
|
|
48
|
+
}
|
|
49
|
+
export interface PlayBreakOptions {
|
|
50
|
+
/** Seconds to present the break for (tune-in breaks: the remaining time); defaults to `break.duration`. */
|
|
51
|
+
effectiveDurationSec?: number;
|
|
52
|
+
/** Defer the overlay and `onBreakVisible` until the first ad frame renders (held delay:0 fullscreen pre-rolls). */
|
|
53
|
+
firstFrameGate?: boolean;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* IMA quartile callbacks. `adIndex`/`totalAds` attribute the milestone to one ad of a
|
|
57
|
+
* multi-ad GAM pod; omitted when there is no per-ad breakdown.
|
|
58
|
+
*/
|
|
59
|
+
export interface GamEventCallbacks {
|
|
60
|
+
onAdFirstQuartile?: (breakInfo: Break, asset: Asset, adIndex?: number, totalAds?: number) => void;
|
|
61
|
+
onAdMidpoint?: (breakInfo: Break, asset: Asset, adIndex?: number, totalAds?: number) => void;
|
|
62
|
+
onAdThirdQuartile?: (breakInfo: Break, asset: Asset, adIndex?: number, totalAds?: number) => void;
|
|
63
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PlayerAdapter,
|
|
1
|
+
import type { PlayerAdapter, TimedMetadataCue } from '../../interfaces/PlayerAdapter';
|
|
2
2
|
import { type Break } from '../../types/BreakManifest';
|
|
3
3
|
import type { GamEventCallbacks } from '../AdPlayerController';
|
|
4
4
|
import type { AdBreakSequencer, AdBreakTransition } from '../AdBreakSequencer';
|
|
@@ -8,12 +8,6 @@ export interface AdPlayerEventsDeps {
|
|
|
8
8
|
getContentPlayer: () => PlayerAdapter;
|
|
9
9
|
getGamEventCallbacks: () => GamEventCallbacks;
|
|
10
10
|
getSequencer: () => AdBreakSequencer | null;
|
|
11
|
-
getAudioFocus: () => 'content' | 'ad' | null;
|
|
12
|
-
getSyncing: () => boolean;
|
|
13
|
-
setSyncing: (value: boolean) => void;
|
|
14
|
-
setAdVolumeChangeHandler: (handler: PlayerAdapterEventHandler | null) => void;
|
|
15
|
-
setContentVolumeHandler: (handler: (() => void) | null) => void;
|
|
16
|
-
notifyVolumeChange: () => void;
|
|
17
11
|
onAdEnded: () => void;
|
|
18
12
|
processTimedMetadata: (cue: TimedMetadataCue) => void;
|
|
19
13
|
toAdError: (mediaError?: unknown) => Error;
|
|
@@ -21,6 +15,8 @@ export interface AdPlayerEventsDeps {
|
|
|
21
15
|
clearFirstFrameResolver: () => void;
|
|
22
16
|
drive: (transition: AdBreakTransition) => Promise<void>;
|
|
23
17
|
setPendingAdError: (value: Error | null) => void;
|
|
18
|
+
/** Drop the ad queued behind the failed one: it loads as today instead. */
|
|
19
|
+
dropQueuedAd: () => void;
|
|
24
20
|
log: (...args: unknown[]) => void;
|
|
25
21
|
maybePrepositionContentForResume: () => void;
|
|
26
22
|
maybeCatchUpContentForResume: (breakInfo: Break) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AssetType as t}from"../../types/BreakManifest";export class AdPlayerEvents{constructor(t,e){this.state=t,this.deps=e}recordAdProgress(t){!Number.isFinite(t)||t<=0||(this.deps.onGamPodAdProgress(t),t>this.state.currentAssetElapsedSec&&(this.state.currentAssetElapsedSec=t))}settleAssetProgress(){let t=this.state.currentAssetElapsedSec;if(t<=0){const e=this.state.currentAsset?.duration;"number"==typeof e&&Number.isFinite(e)&&e>0&&(t=e)}this.state.adSecondsPlayedThisBreak+=t,this.state.currentAssetElapsedSec=0}reportAdProgress(e){if(this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&(this.recordAdProgress(e.currentTime),this.deps.maybePrepositionContentForResume(),this.state.currentBreak&&this.deps.maybeCatchUpContentForResume(this.state.currentBreak),this.state.callbacks?.onAdTimeUpdate(this.state.currentBreak,this.state.currentAsset,e.currentTime,e.duration),this.state.currentAsset.type===t.STATIC&&e.duration>0)){const t=e.currentTime/e.duration,s=this.deps.getStaticQuartilesFired(),r=this.deps.getGamEventCallbacks();t>=.25&&!s.has("first")&&(s.add("first"),r.onAdFirstQuartile?.(this.state.currentBreak,this.state.currentAsset)),t>=.5&&!s.has("mid")&&(s.add("mid"),r.onAdMidpoint?.(this.state.currentBreak,this.state.currentAsset)),t>=.75&&!s.has("third")&&(s.add("third"),r.onAdThirdQuartile?.(this.state.currentBreak,this.state.currentAsset))}}setupEventListeners(){const t=this.deps.getAdPlayer();
|
|
1
|
+
import{AssetType as t}from"../../types/BreakManifest";export class AdPlayerEvents{constructor(t,e){this.state=t,this.deps=e}recordAdProgress(t){!Number.isFinite(t)||t<=0||(this.deps.onGamPodAdProgress(t),t>this.state.currentAssetElapsedSec&&(this.state.currentAssetElapsedSec=t))}settleAssetProgress(){let t=this.state.currentAssetElapsedSec;if(t<=0){const e=this.state.currentAsset?.duration;"number"==typeof e&&Number.isFinite(e)&&e>0&&(t=e)}this.state.adSecondsPlayedThisBreak+=t,this.state.currentAssetElapsedSec=0}reportAdProgress(e){if(this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&(this.recordAdProgress(e.currentTime),this.deps.maybePrepositionContentForResume(),this.state.currentBreak&&this.deps.maybeCatchUpContentForResume(this.state.currentBreak),this.state.callbacks?.onAdTimeUpdate(this.state.currentBreak,this.state.currentAsset,e.currentTime,e.duration),this.state.currentAsset.type===t.STATIC&&e.duration>0)){const t=e.currentTime/e.duration,s=this.deps.getStaticQuartilesFired(),r=this.deps.getGamEventCallbacks();t>=.25&&!s.has("first")&&(s.add("first"),r.onAdFirstQuartile?.(this.state.currentBreak,this.state.currentAsset)),t>=.5&&!s.has("mid")&&(s.add("mid"),r.onAdMidpoint?.(this.state.currentBreak,this.state.currentAsset)),t>=.75&&!s.has("third")&&(s.add("third"),r.onAdThirdQuartile?.(this.state.currentBreak,this.state.currentAsset))}}setupEventListeners(){const t=this.deps.getAdPlayer();t&&(t.on("ended",()=>{this.deps.onAdEnded()}),t.on("timedmetadata",t=>{t&&this.deps.processTimedMetadata(t)}),t.on("error",t=>{this.onAdError(this.deps.toAdError(t))}),t.on("timeupdate",()=>this.reportAdProgress(t)),t.on("waiting",()=>{this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&this.state.callbacks?.onAdWaiting(this.state.currentBreak,this.state.currentAsset)}),t.on("playing",()=>{if(this.state.isPlaying&&this.state.currentBreak&&this.state.currentAsset&&!t.paused){if(this.state.startPlaybackCutoff?.(),this.state.firstFrameResolver)return this.deps.revealFirstFrame(),this.state.firstFrameResolver(),void this.state.callbacks?.onAdPlaying(this.state.currentBreak,this.state.currentAsset);this.state.callbacks?.onAdPlaying(this.state.currentBreak,this.state.currentAsset)}}))}onAdError(t){const e=this.deps.getSequencer(),s=this.state.currentBreak;if(!s||!e)return;const r=this.state.preloadGeneration,a=this.state.erroredAsset;if(a?.breakInfo!==s||a.generation!==r){if(this.state.erroredAsset={breakInfo:s,generation:r},this.state.firstFrameRejector){const e=this.state.firstFrameRejector;this.deps.clearFirstFrameResolver(),e(t)}this.deps.log("Ad error:",t.message),this.deps.dropQueuedAd(),this.deps.setPendingAdError(t),this.deps.drive(e.onOutcome({kind:"asset-errored"}))}else this.deps.log("Ignoring further ad error for the same asset:",t.message)}}
|
|
@@ -102,7 +102,6 @@ export interface AdPlayerState {
|
|
|
102
102
|
continueCorrections: number[];
|
|
103
103
|
continueDriftUnrecoverable: boolean;
|
|
104
104
|
continueRefusalReported: boolean;
|
|
105
|
-
contentMutedBeforeCatchup: boolean | null;
|
|
106
105
|
resumeAlignCorrectionsMs: number[];
|
|
107
106
|
resumeLandedProbe: (() => void) | null;
|
|
108
107
|
adSecondsPlayedThisBreak: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export function invalidateMediaPreload(e){return e.preparedMedia=null,++e.preloadGeneration}export function createAdPlayerState(){return{currentBreak:null,currentAsset:null,isPlaying:!1,contentPaused:!1,callbacks:null,durationTimer:null,startPlaybackCutoff:null,effectiveBreakDurationSec:null,adInsertionType:void 0,breakTimebase:null,adBreakCutSafetyMarginSec:0,detectedDeviceType:void 0,sessionAssetParameters:void 0,updatedAssetParameters:void 0,manifestAssetParameters:void 0,sessionAssetParameterMacros:{},pushedAssetParameters:null,gamStreamManager:null,gamPodMode:"none",gamPodTracker:null,gamPodAsset:null,earlyGamPodAdStart:null,withheldPodAdBegin:null,gamConfig:void 0,gamInitPromise:null,currentGamIdentity:void 0,playerContainer:null,stage:null,contentVideoElement:null,adContainer:null,adMediaContainer:null,adPlayer:null,companionElement:null,overlayImageEl:null,isContentFullscreen:!1,trackedContentVideo:null,fullscreenEnterHandler:null,fullscreenExitHandler:null,contentPictureInPicture:!1,pipTransferredToAd:!1,pipAdoptedForBreak:!1,sharedAdClaimActive:!1,savedContentSrc:null,savedContentTime:0,savedContentLive:!1,savedContentEdgeDistance:null,earlyReturnResumeOffsetSec:null,contentPositionAtBreakStart:0,resumeAnchorIsCue:!1,resumeAnchorIsPdt:!1,resumeAnchorSeekableEnd:null,resumePrepositionedTo:null,resumePrepositionSettle:null,resumeCatchupActive:!1,resumeCatchupRan:!1,lastResumeAlignRemainingSec:Number.POSITIVE_INFINITY,resumeAlignBlockedDiagnosed:!1,lastResumeSafeEnd:Number.NEGATIVE_INFINITY,resumeSawSeekDone:!1,resumeBestSawMarginSec:Number.NEGATIVE_INFINITY,resumeAlignTicks:0,resumeSawSteps:[],continueContentDuringBreak:!1,continueQualityOverrideApplied:!1,continueContentDuringBreakActive:!1,keepContentPlayingHandler:null,continuePlayoutStartWallMs:0,continuePlayoutTicker:null,continueRateWritten:null,continueRateWrittenAtMs:0,continueRateHonoured:!0,continueMaxAbsDriftSec:0,continueCorrections:[],continueDriftUnrecoverable:!1,continueRefusalReported:!1,
|
|
1
|
+
export function invalidateMediaPreload(e){return e.preparedMedia=null,++e.preloadGeneration}export function createAdPlayerState(){return{currentBreak:null,currentAsset:null,isPlaying:!1,contentPaused:!1,callbacks:null,durationTimer:null,startPlaybackCutoff:null,effectiveBreakDurationSec:null,adInsertionType:void 0,breakTimebase:null,adBreakCutSafetyMarginSec:0,detectedDeviceType:void 0,sessionAssetParameters:void 0,updatedAssetParameters:void 0,manifestAssetParameters:void 0,sessionAssetParameterMacros:{},pushedAssetParameters:null,gamStreamManager:null,gamPodMode:"none",gamPodTracker:null,gamPodAsset:null,earlyGamPodAdStart:null,withheldPodAdBegin:null,gamConfig:void 0,gamInitPromise:null,currentGamIdentity:void 0,playerContainer:null,stage:null,contentVideoElement:null,adContainer:null,adMediaContainer:null,adPlayer:null,companionElement:null,overlayImageEl:null,isContentFullscreen:!1,trackedContentVideo:null,fullscreenEnterHandler:null,fullscreenExitHandler:null,contentPictureInPicture:!1,pipTransferredToAd:!1,pipAdoptedForBreak:!1,sharedAdClaimActive:!1,savedContentSrc:null,savedContentTime:0,savedContentLive:!1,savedContentEdgeDistance:null,earlyReturnResumeOffsetSec:null,contentPositionAtBreakStart:0,resumeAnchorIsCue:!1,resumeAnchorIsPdt:!1,resumeAnchorSeekableEnd:null,resumePrepositionedTo:null,resumePrepositionSettle:null,resumeCatchupActive:!1,resumeCatchupRan:!1,lastResumeAlignRemainingSec:Number.POSITIVE_INFINITY,resumeAlignBlockedDiagnosed:!1,lastResumeSafeEnd:Number.NEGATIVE_INFINITY,resumeSawSeekDone:!1,resumeBestSawMarginSec:Number.NEGATIVE_INFINITY,resumeAlignTicks:0,resumeSawSteps:[],continueContentDuringBreak:!1,continueQualityOverrideApplied:!1,continueContentDuringBreakActive:!1,keepContentPlayingHandler:null,continuePlayoutStartWallMs:0,continuePlayoutTicker:null,continueRateWritten:null,continueRateWrittenAtMs:0,continueRateHonoured:!0,continueMaxAbsDriftSec:0,continueCorrections:[],continueDriftUnrecoverable:!1,continueRefusalReported:!1,resumeAlignCorrectionsMs:[],resumeLandedProbe:null,adSecondsPlayedThisBreak:0,currentAssetElapsedSec:0,preparedMedia:null,preloadGeneration:0,erroredAsset:null,firstFrameGateActive:!1,firstFrameGateShown:!1,firstFrameResolver:null,firstFrameRejector:null,firstFrameTimeout:null,adStartDeadlineMs:null,adStartBudgetMs:0,vastAdManager:null}}
|
|
@@ -19,8 +19,8 @@ export interface AdPreloaderDeps {
|
|
|
19
19
|
effectiveInsertionMode: () => string;
|
|
20
20
|
getVariant: (breakInfo: Break) => BreakVariant | null;
|
|
21
21
|
getAssets: (breakInfo: Break) => Asset[];
|
|
22
|
-
|
|
23
|
-
resolveAssetUri: (asset: Asset, duration: number) => Promise<string | null>;
|
|
22
|
+
buildStaticAssetUrl: (asset: StaticAsset, breakInfo: Break) => string | null;
|
|
23
|
+
resolveAssetUri: (asset: Asset, duration: number, breakInfo: Break) => Promise<string | null>;
|
|
24
24
|
getCompanionVariant: (breakInfo: Break) => BreakVariant | null;
|
|
25
25
|
getAdPlayer: () => PlayerAdapter | null;
|
|
26
26
|
getAdContainer: () => HTMLDivElement | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AssetType as e,BreakFormat as t,MediaType as s,Vendor as a,isVastAsset as r,isVendorAsset as i}from"../../types/BreakManifest";import{AUX_FETCH_TIMEOUT_MS as d,fetchWithTimeout as o}from"../fetchWithTimeout";import{resolveVastAdmission as n}from"../resolveVastAdmission";import{isImaCsaiSdkAvailable as l,VastAdManager as p}from"../VastAdManager";import{invalidateMediaPreload as h}from"./AdPlayerState";import{AdError as m}from"../../types/AdError";export const AD_LOAD_RETRY_AFTER_MS=2e3;export function adLoadShowsNoProgress(e){return!e||0===e.readyState&&0===e.buffered.length}export class AdPreloader{constructor(e){this.deps=e,this.warmedBreakIds=new Set}classifyCreative(e){const t=(e.split(/[?#]/)[0]??"").toLowerCase();return t.endsWith(".m3u8")||t.endsWith(".mpd")?"stream":/\.(mp4|m4v|m4a|webm|mov|ogg|ogv|mp3)$/.test(t)?"progressive":"unknown"}diagnosePreloadPath(e,t,s){const a=this.classifyCreative(t);this.deps.diagnose("DA-PRELOAD-PATH",`Ad preload path: ${s} (${a} creative)`,{breakId:e,adPreload:this.deps.preloadMode,path:s,creative:a})}async preload(a){if(this.deps.isPlaying())return void this.deps.log("Preload skipped: ad already playing");const i=h(this.deps.state),d=this.deps.getAdPlayer();if("none"===this.deps.preloadMode){const t=this.deps.getAssets(a)[0],s=t&&t.type===e.STATIC?this.deps.
|
|
1
|
+
import{AssetType as e,BreakFormat as t,MediaType as s,Vendor as a,isVastAsset as r,isVendorAsset as i}from"../../types/BreakManifest";import{AUX_FETCH_TIMEOUT_MS as d,fetchWithTimeout as o}from"../fetchWithTimeout";import{resolveVastAdmission as n}from"../resolveVastAdmission";import{isImaCsaiSdkAvailable as l,VastAdManager as p}from"../VastAdManager";import{invalidateMediaPreload as h}from"./AdPlayerState";import{AdError as m}from"../../types/AdError";export const AD_LOAD_RETRY_AFTER_MS=2e3;export function adLoadShowsNoProgress(e){return!e||0===e.readyState&&0===e.buffered.length}export class AdPreloader{constructor(e){this.deps=e,this.warmedBreakIds=new Set}classifyCreative(e){const t=(e.split(/[?#]/)[0]??"").toLowerCase();return t.endsWith(".m3u8")||t.endsWith(".mpd")?"stream":/\.(mp4|m4v|m4a|webm|mov|ogg|ogv|mp3)$/.test(t)?"progressive":"unknown"}diagnosePreloadPath(e,t,s){const a=this.classifyCreative(t);this.deps.diagnose("DA-PRELOAD-PATH",`Ad preload path: ${s} (${a} creative)`,{breakId:e,adPreload:this.deps.preloadMode,path:s,creative:a})}async preload(a){if(this.deps.isPlaying())return void this.deps.log("Preload skipped: ad already playing");const i=h(this.deps.state),d=this.deps.getAdPlayer();if("none"===this.deps.preloadMode){const t=this.deps.getAssets(a)[0],s=t&&t.type===e.STATIC?this.deps.buildStaticAssetUrl(t,a)??"":"";return this.diagnosePreloadPath(a.id,s,"none"),void this.deps.log('Preload skipped: adPreload "none"')}if("shared-element"===this.deps.effectiveInsertionMode()){const s=this.deps.getVariant(a);if(s&&s.format!==t.LSHAPE_CONTENT){const t=this.deps.getAssets(a)[0];if(t&&t.type===e.STATIC){const e=this.deps.buildStaticAssetUrl(t,a);e&&await this.warmCache([e])}}return}const o=this.deps.getAssets(a);if(0===o.length)return void this.deps.log("No assets to preload");this.preloadCompanion(a);const n=this.deps.getVariant(a);if(n?.format===t.LSHAPE_CONTENT)return;if(o[0].mediaType===s.IMAGE){const e=await this.deps.resolveAssetUri(o[0],a.duration,a);if(e){(new Image).src=e,this.deps.log("Preloading overlay image:",e)}return}if(r(o[0]))return void await this.preloadVastAsset(o[0],a);const l=await this.deps.resolveAssetUri(o[0],a.duration,a);if(l){if(d&&this.ownsPreload(i,d))if("single-decoder"!==this.deps.preloadMode){this.deps.log("Preloading ad (parallel):",l),this.diagnosePreloadPath(a.id,l,"parallel");try{if(await d.load(l),!this.ownsPreload(i,d))return;this.deps.state.preparedMedia={breakId:a.id,assetId:o[0].id,uri:l,player:d},this.deps.log("Preload complete for break:",a.id)}catch(e){this.deps.log("Preload failed (will load on demand):",e)}}else{if(this.deps.log("Preloading ad (single-decoder, detached):",l),this.diagnosePreloadPath(a.id,l,"single-decoder"),await this.warmCache([l]),!this.ownsPreload(i,d))return;try{await(d.preload?.(l))}catch(e){this.deps.log("Detached prefetch failed (will load on demand):",e)}}}else this.deps.log("No valid URI for preload")}ownsPreload(e,t){return e===this.deps.state.preloadGeneration&&!this.deps.isPlaying()&&t===this.deps.getAdPlayer()}async preloadVastAsset(e,s){const a=this.deps.getVariant(s),r=a?.format??t.SINGLE;if(!n({format:r,ssai:!1}).admitted)return void this.deps.log("VAST preload skipped: format not supported",r);if(!l())return void this.deps.log("VAST preload skipped: IMA client-side SDK unavailable");const i=this.deps.getAdPlayer(),d=this.deps.getAdContainer();if(!i||!d)return void this.deps.log("VAST preload skipped: ad player not initialized");const o=i.videoElement;if(!o)return void this.deps.log("VAST preload skipped: no ad video element");const h=this.deps.buildVastTagUrl(e,s);if(h){this.deps.state.vastAdManager||(this.deps.state.vastAdManager=new p(d,o,this.deps.debug)),this.deps.log("Preloading VAST asset via IMA CSAI:",h);try{await this.deps.state.vastAdManager.preloadVast(h,{muted:i.muted}),this.deps.log("VAST preload complete for break:",s.id)}catch(t){const a=t instanceof Error?t:new Error(String(t));this.deps.log("VAST preload failed (will load on demand):",a.message),this.deps.diagnose("DA-PRELOAD-FAILED",a.message,{breakId:s.id,assetId:e.id})}}else this.deps.log("No valid VAST tag URL for preload")}preloadChainedSuccessor(e){const t=this.deps.resolveChainedSuccessor(e);t&&this.preloadNext(t)}clearWarmedBreaks(){this.warmedBreakIds.clear()}async preloadNext(s){if(this.warmedBreakIds.has(s.id))return;this.preloadCompanion(s);const a=this.deps.getVariant(s);if(!a||a.format===t.LSHAPE_CONTENT)return;const r=this.deps.getAssets(s)[0];if(!r||r.type!==e.STATIC)return void this.deps.log("Chained successor first asset not static; skipping media warm:",s.id);const i=this.deps.buildStaticAssetUrl(r,s);if(i){this.warmedBreakIds.add(s.id),this.deps.log("Warming chained successor media:",s.id,i),await this.warmCache([i]);try{await(this.deps.getAdPlayer()?.preload?.(i))}catch(e){this.deps.log("Chained prefetch failed (will load on demand):",e)}}}revealFirstFrame(){if(!this.deps.state.firstFrameGateActive||this.deps.state.firstFrameGateShown)return;const e=this.deps.getCurrentBreak();if(!e)return;this.deps.state.firstFrameGateShown=!0,this.deps.show();const t=this.deps.getVariant(e);t&&this.deps.reapplyAdContainerLayout(t),this.deps.onBreakVisible(e)}armAdStartBudget(e){const t=1e3*((this.deps.state.effectiveBreakDurationSec??this.deps.state.currentBreak?.duration??0)+this.deps.state.adBreakCutSafetyMarginSec),s=i(e)&&e.vendor===a.GAM?this.deps.gamPodAdStartBudgetMs:this.deps.adStartBudgetMs,r=Math.max(Math.min(t-100,s),this.deps.minAdStartBudgetMs);this.deps.state.adStartBudgetMs=r,this.deps.state.adStartDeadlineMs=this.deps.clock.now()+r}remainingAdStartBudgetMs(){return Math.max(0,(this.deps.state.adStartDeadlineMs??this.deps.clock.now())-this.deps.clock.now())}loadWithinBudget(e,t){const s=this.deps.state.preloadGeneration,a=this.remainingAdStartBudgetMs(),r=this.deps.clock.now();return new Promise((i,d)=>{let o=!1,n=!1;const l=e=>{o||(o=!0,clearTimeout(p),null!==h&&clearTimeout(h),e.ok?i(!0):d(e.error))},p=setTimeout(()=>{s===this.deps.state.preloadGeneration&&this.deps.onAdError(new m("Ad load timeout","DA-AD-START-TIMEOUT")),o||(o=!0,null!==h&&clearTimeout(h),i(!1))},a);let h=null;a>2e3&&(h=setTimeout(()=>{if(o||s!==this.deps.state.preloadGeneration)return;const a=this.deps.clock.now()-r,i=this.deps.getAdPlayer()?.videoElement;adLoadShowsNoProgress(i)?(n=!0,this.deps.log(`Ad load for ${t.assetId} stalled after ${a}ms; retrying once`),this.deps.diagnose("DA-AD-LOAD-RETRIED",`Ad load for ${t.assetId} in break ${t.breakId} had not settled after ${a}ms; issued a second load()`,{breakId:t.breakId,assetId:t.assetId,uri:t.uri,elapsedMs:a,remainingBudgetMs:this.remainingAdStartBudgetMs()}),e().then(()=>l({ok:!0}),e=>l({ok:!1,error:e}))):this.deps.log(`Ad load for ${t.assetId} still pending after ${a}ms but the element is progressing (readyState=${i?.readyState}, buffered=${i?.buffered.length}); no retry`)},2e3)),e().then(()=>l({ok:!0}),e=>{n||l({ok:!1,error:e})})})}async waitForFirstFrame(){return new Promise((e,t)=>{this.deps.state.firstFrameResolver=()=>{this.clearFirstFrameResolver(),e()},this.deps.state.firstFrameRejector=e=>{this.clearFirstFrameResolver(),t(e)},this.deps.state.firstFrameTimeout=setTimeout(()=>{this.deps.onAdError(new m("First frame timeout","DA-AD-START-TIMEOUT"))},this.remainingAdStartBudgetMs())})}clearFirstFrame(e="Break ended before first frame"){if(this.deps.state.adStartDeadlineMs=null,this.deps.state.adStartBudgetMs=0,this.deps.state.firstFrameTimeout&&(clearTimeout(this.deps.state.firstFrameTimeout),this.deps.state.firstFrameTimeout=null),this.deps.state.firstFrameRejector){const t=this.deps.state.firstFrameRejector;this.deps.state.firstFrameResolver=null,this.deps.state.firstFrameRejector=null,t(new Error(e))}}clearFirstFrameResolver(){this.deps.state.adStartDeadlineMs=null,this.deps.state.adStartBudgetMs=0,this.deps.state.firstFrameTimeout&&(clearTimeout(this.deps.state.firstFrameTimeout),this.deps.state.firstFrameTimeout=null),this.deps.state.firstFrameResolver=null,this.deps.state.firstFrameRejector=null}preloadCompanion(s){const a=this.deps.getCompanionVariant(s);if(!a)return;if(a.format===t.LSHAPE_CONTENT){const t=a.assets[0];if(!t||t.type!==e.STATIC)return;const r=this.deps.buildStaticAssetUrl(t,s);if(!r)return;return(new Image).src=r,void this.deps.log("Preloading lshape_content backdrop:",r)}if(a.format!==t.DOUBLE&&a.format!==t.LSHAPE_AD)return;const r=a.assets[0]?.companion;if(!r||r.type!==e.STATIC)return;const i=this.deps.buildStaticAssetUrl(r,s);if(!i)return;(new Image).src=i,this.deps.log("Preloading companion:",i)}async warmCache(e){"function"==typeof fetch&&await Promise.allSettled(e.map(e=>o(e,{mode:"no-cors"},d).catch(()=>{})))}}
|