@dolby-optiview/ads-sdk-core 1.0.0-beta.1
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 +190 -0
- package/dist/OptiViewAds.js +1 -0
- package/dist/config/keys.d.ts +23 -0
- package/dist/config/keys.js +1 -0
- package/dist/diagnostics/DiagnosticBuffer.d.ts +33 -0
- package/dist/diagnostics/DiagnosticBuffer.js +1 -0
- package/dist/diagnostics/DiagnosticEvent.d.ts +71 -0
- package/dist/diagnostics/DiagnosticEvent.js +1 -0
- package/dist/diagnostics/debugLog.d.ts +8 -0
- package/dist/diagnostics/debugLog.js +1 -0
- package/dist/diagnostics/errorCodes.d.ts +45 -0
- package/dist/diagnostics/errorCodes.js +1 -0
- package/dist/diagnostics/index.d.ts +6 -0
- package/dist/diagnostics/index.js +1 -0
- package/dist/diagnostics/playbackErrorContext.d.ts +22 -0
- package/dist/diagnostics/playbackErrorContext.js +1 -0
- package/dist/generated/diagnosticCodes.d.ts +487 -0
- package/dist/generated/diagnosticCodes.js +1 -0
- package/dist/generated/version.d.ts +5 -0
- package/dist/generated/version.js +1 -0
- package/dist/id3/buildId3.d.ts +25 -0
- package/dist/id3/buildId3.js +1 -0
- package/dist/id3/index.d.ts +3 -0
- package/dist/id3/index.js +1 -0
- package/dist/id3/parseId3Geob.d.ts +28 -0
- package/dist/id3/parseId3Geob.js +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +1 -0
- package/dist/interfaces/PlayerAdapter.d.ts +163 -0
- package/dist/interfaces/PlayerAdapter.js +1 -0
- package/dist/internal/index.d.ts +1 -0
- package/dist/internal/index.js +1 -0
- package/dist/server/index.d.ts +20 -0
- package/dist/server/index.js +1 -0
- package/dist/services/AdBreakSequencer.d.ts +146 -0
- package/dist/services/AdBreakSequencer.js +1 -0
- package/dist/services/AdPlayerController.d.ts +495 -0
- package/dist/services/AdPlayerController.js +1 -0
- package/dist/services/BreakScheduler.d.ts +215 -0
- package/dist/services/BreakScheduler.js +1 -0
- package/dist/services/BreakTransitionTimer.d.ts +48 -0
- package/dist/services/BreakTransitionTimer.js +1 -0
- package/dist/services/CmcdAdReporter.d.ts +81 -0
- package/dist/services/CmcdAdReporter.js +1 -0
- package/dist/services/GamPodAdTracker.d.ts +65 -0
- package/dist/services/GamPodAdTracker.js +1 -0
- package/dist/services/GamPodUrlBuilder.d.ts +30 -0
- package/dist/services/GamPodUrlBuilder.js +1 -0
- package/dist/services/GamStreamManager.d.ts +67 -0
- package/dist/services/GamStreamManager.js +1 -0
- package/dist/services/LensReporter.d.ts +30 -0
- package/dist/services/LensReporter.js +1 -0
- package/dist/services/ManifestService.d.ts +91 -0
- package/dist/services/ManifestService.js +1 -0
- package/dist/services/MetadataCueTracker.d.ts +44 -0
- package/dist/services/MetadataCueTracker.js +1 -0
- package/dist/services/PauseAdController.d.ts +108 -0
- package/dist/services/PauseAdController.js +1 -0
- package/dist/services/RewindRearm.d.ts +109 -0
- package/dist/services/RewindRearm.js +1 -0
- package/dist/services/SsaiController.d.ts +59 -0
- package/dist/services/SsaiController.js +1 -0
- package/dist/services/StitcherUrlBuilder.d.ts +34 -0
- package/dist/services/StitcherUrlBuilder.js +1 -0
- package/dist/services/VastAdManager.d.ts +89 -0
- package/dist/services/VastAdManager.js +1 -0
- package/dist/services/adPlayer/AdPlayerEvents.d.ts +39 -0
- package/dist/services/adPlayer/AdPlayerEvents.js +1 -0
- package/dist/services/adPlayer/AdPlayerState.d.ts +98 -0
- package/dist/services/adPlayer/AdPlayerState.js +1 -0
- package/dist/services/adPlayer/AdPreloader.d.ts +54 -0
- package/dist/services/adPlayer/AdPreloader.js +1 -0
- package/dist/services/adPlayer/AdStage.d.ts +57 -0
- package/dist/services/adPlayer/AdStage.js +1 -0
- package/dist/services/adPlayer/AssetParameterResolver.d.ts +44 -0
- package/dist/services/adPlayer/AssetParameterResolver.js +1 -0
- package/dist/services/adPlayer/AssetPlayback.d.ts +71 -0
- package/dist/services/adPlayer/AssetPlayback.js +1 -0
- package/dist/services/adPlayer/BreakSequenceDriver.d.ts +46 -0
- package/dist/services/adPlayer/BreakSequenceDriver.js +1 -0
- package/dist/services/adPlayer/ContentResumeCoordinator.d.ts +44 -0
- package/dist/services/adPlayer/ContentResumeCoordinator.js +1 -0
- package/dist/services/adPlayer/ContinueContentPlayout.d.ts +26 -0
- package/dist/services/adPlayer/ContinueContentPlayout.js +1 -0
- package/dist/services/adPlayer/GamPodBridge.d.ts +66 -0
- package/dist/services/adPlayer/GamPodBridge.js +1 -0
- package/dist/services/adPlayer/PauseAdPresenter.d.ts +60 -0
- package/dist/services/adPlayer/PauseAdPresenter.js +1 -0
- package/dist/services/adPlayer/PresentationModeTracker.d.ts +66 -0
- package/dist/services/adPlayer/PresentationModeTracker.js +1 -0
- package/dist/services/adPlayer/SharedElementBreakPlayer.d.ts +54 -0
- package/dist/services/adPlayer/SharedElementBreakPlayer.js +1 -0
- package/dist/services/adPlayer/VariantSelector.d.ts +38 -0
- package/dist/services/adPlayer/VariantSelector.js +1 -0
- package/dist/services/adPlayer/connectPauseAd.d.ts +10 -0
- package/dist/services/adPlayer/connectPauseAd.js +1 -0
- package/dist/services/adPlayer/liveEdge.d.ts +6 -0
- package/dist/services/adPlayer/liveEdge.js +1 -0
- package/dist/services/assetParameterMacroTable.d.ts +9 -0
- package/dist/services/assetParameterMacroTable.js +1 -0
- package/dist/services/assetParameterMacros.d.ts +69 -0
- package/dist/services/assetParameterMacros.js +1 -0
- package/dist/services/breakTiming.d.ts +60 -0
- package/dist/services/breakTiming.js +1 -0
- package/dist/services/channelSwitch.d.ts +32 -0
- package/dist/services/channelSwitch.js +1 -0
- package/dist/services/detectDeviceType.d.ts +15 -0
- package/dist/services/detectDeviceType.js +1 -0
- package/dist/services/detectInsertionMode.d.ts +38 -0
- package/dist/services/detectInsertionMode.js +1 -0
- package/dist/services/detectPreloadMode.d.ts +13 -0
- package/dist/services/detectPreloadMode.js +1 -0
- package/dist/services/ed25519.d.ts +19 -0
- package/dist/services/ed25519.js +1 -0
- package/dist/services/fetchWithTimeout.d.ts +35 -0
- package/dist/services/fetchWithTimeout.js +1 -0
- package/dist/services/lensBackend.d.ts +16 -0
- package/dist/services/lensBackend.js +1 -0
- package/dist/services/lensBackendDevelopment.d.ts +1 -0
- package/dist/services/lensBackendDevelopment.js +1 -0
- package/dist/services/lensBackendProduction.d.ts +9 -0
- package/dist/services/lensBackendProduction.js +1 -0
- package/dist/services/manifestSignature.d.ts +43 -0
- package/dist/services/manifestSignature.js +1 -0
- package/dist/services/manifestTrustedKeys.d.ts +9 -0
- package/dist/services/manifestTrustedKeys.js +1 -0
- package/dist/services/parseBreakManifest.d.ts +15 -0
- package/dist/services/parseBreakManifest.js +1 -0
- package/dist/services/parseVastCompanion.d.ts +31 -0
- package/dist/services/parseVastCompanion.js +1 -0
- package/dist/services/parseVastLinear.d.ts +36 -0
- package/dist/services/parseVastLinear.js +1 -0
- package/dist/services/pictureInPicture.d.ts +28 -0
- package/dist/services/pictureInPicture.js +1 -0
- package/dist/services/resolveAssetParameters.d.ts +6 -0
- package/dist/services/resolveAssetParameters.js +1 -0
- package/dist/services/resolveDeliveryMode.d.ts +54 -0
- package/dist/services/resolveDeliveryMode.js +1 -0
- package/dist/services/resolveGamVendorConfiguration.d.ts +55 -0
- package/dist/services/resolveGamVendorConfiguration.js +1 -0
- package/dist/services/resolvePauseResumePoint.d.ts +32 -0
- package/dist/services/resolvePauseResumePoint.js +1 -0
- package/dist/services/resolvePollingDecision.d.ts +24 -0
- package/dist/services/resolvePollingDecision.js +1 -0
- package/dist/services/resolveResumePoint.d.ts +38 -0
- package/dist/services/resolveResumePoint.js +1 -0
- package/dist/services/resolveTargetedUri.d.ts +22 -0
- package/dist/services/resolveTargetedUri.js +1 -0
- package/dist/services/resolveVastAdmission.d.ts +34 -0
- package/dist/services/resolveVastAdmission.js +1 -0
- package/dist/services/selectVariant.d.ts +47 -0
- package/dist/services/selectVariant.js +1 -0
- package/dist/services/urlOrigin.d.ts +23 -0
- package/dist/services/urlOrigin.js +1 -0
- package/dist/session/AdBreakStatusTracker.d.ts +49 -0
- package/dist/session/AdBreakStatusTracker.js +1 -0
- package/dist/session/BreakLifecycle.d.ts +62 -0
- package/dist/session/BreakLifecycle.js +1 -0
- package/dist/session/SessionStarter.d.ts +98 -0
- package/dist/session/SessionStarter.js +1 -0
- package/dist/session/SessionState.d.ts +37 -0
- package/dist/session/SessionState.js +1 -0
- package/dist/types/BreakManifest.d.ts +335 -0
- package/dist/types/BreakManifest.js +1 -0
- package/dist/types/Events.d.ts +245 -0
- package/dist/types/Events.js +1 -0
- package/dist/types/OptiViewAdsConfig.d.ts +425 -0
- package/dist/types/OptiViewAdsConfig.js +1 -0
- package/dist/types/VendorAdError.d.ts +12 -0
- package/dist/types/VendorAdError.js +1 -0
- package/dist/vendor/lens-sdk/THIRD-PARTY-NOTICES.txt +266 -0
- package/dist/vendor/lens-sdk/compat.d.ts +9 -0
- package/dist/vendor/lens-sdk/encoding-cmcd.d.ts +9 -0
- package/dist/vendor/lens-sdk/encoding-json.d.ts +9 -0
- package/dist/vendor/lens-sdk/encoding.d.ts +42 -0
- package/dist/vendor/lens-sdk/environment.d.ts +35 -0
- package/dist/vendor/lens-sdk/index.d.ts +3 -0
- package/dist/vendor/lens-sdk/index.js +1 -0
- package/dist/vendor/lens-sdk/legacy.d.ts +12 -0
- package/dist/vendor/lens-sdk/lens-core-Q5lbQGMC.js +1 -0
- package/dist/vendor/lens-sdk/lens-global.d.ts +23 -0
- package/dist/vendor/lens-sdk/lens.d.ts +21 -0
- package/dist/vendor/lens-sdk/transport.d.ts +46 -0
- package/dist/vendor/lens-sdk/types.d.ts +218 -0
- package/dist/vendor/lens-sdk/user-id.d.ts +5 -0
- package/dist/vendor/lens-sdk/validation.d.ts +30 -0
- package/package.json +57 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const t=t=>Math.round(1e3*t)/1e3;export class ContinueContentPlayout{constructor(t,e){this.state=t,this.deps=e}beginContinueContentPlayout(){if(this.state.continueContentDuringBreakActive)return;this.state.continueContentDuringBreakActive=!0;const e=this.deps.contentPlayer.setVideoQuality;if(this.state.continueQualityOverrideApplied=!1,e){try{this.state.continueQualityOverrideApplied=!0===e.call(this.deps.contentPlayer,"lowest")}catch(t){this.deps.log("continueContentDuringBreak: quality selection failed:",t)}this.state.continueQualityOverrideApplied||this.deps.diagnose("DA-CONTINUE-PLAYOUT-QUALITY-UNAVAILABLE","Content continues during the break without a quality constraint",{breakId:this.state.currentBreak?.id,reason:"not-applied"})}else this.deps.diagnose("DA-CONTINUE-PLAYOUT-QUALITY-UNAVAILABLE","Content continues during the break without a quality constraint",{breakId:this.state.currentBreak?.id,reason:"unsupported"});this.state.keepContentPlayingHandler||(this.state.keepContentPlayingHandler=()=>{this.state.continueContentDuringBreakActive&&this.deps.contentPlayer.play().catch(()=>this.abandonContinueContentPlayout("content-play-rejected"))},this.deps.contentPlayer.on("pause",this.state.keepContentPlayingHandler)),this.state.continuePlayoutStartWallMs=this.deps.clock.now(),this.state.continueRateWritten=null,this.state.continueRateWrittenAtMs=0,this.state.continueRateHonoured=!0,this.state.continueMaxAbsDriftSec=0,this.state.continueCorrections=[],this.state.continueDriftUnrecoverable=!1,this.state.continuePlayoutTicker=setInterval(()=>this.tickContinueContentPlayout(),250),this.deps.log(`continueContentDuringBreak: content keeps playing from ${this.state.contentPositionAtBreakStart}s`+(this.state.continueQualityOverrideApplied?", lowest quality requested":", no quality override")),this.deps.diagnose("DA-CONTINUE-PLAYOUT-BEGIN","Content keeps playing through the break instead of pausing; no resume seek will be issued",{breakId:this.state.currentBreak?.id,anchor:t(this.state.contentPositionAtBreakStart),breakDurationSec:this.state.effectiveBreakDurationSec??this.state.currentBreak?.duration??null,edgeDistanceSec:this.deps.distanceToLiveEdgeSec()})}secondsUntilCut(t){const e=this.state.effectiveBreakDurationSec??this.state.currentBreak?.duration??0,n=this.state.adSecondsPlayedThisBreak+this.state.currentAssetElapsedSec;return Math.min(e-n,e-t)}restoreContinueQuality(t){if(this.state.continueQualityOverrideApplied){this.state.continueQualityOverrideApplied=!1;try{this.deps.contentPlayer.setVideoQuality?.(null),this.deps.log(`continueContentDuringBreak: quality restore requested (${t})`)}catch(t){this.deps.log("continueContentDuringBreak: quality restore failed:",t)}}}currentContinueDriftSec(){const t=(this.deps.clock.now()-this.state.continuePlayoutStartWallMs)/1e3;return this.deps.contentPlayer.currentTime-(this.state.contentPositionAtBreakStart+t)}abandonContinueContentPlayout(e){if(!this.state.continueContentDuringBreakActive)return;const n=this.currentContinueDriftSec(),i=this.state.currentBreak?.id;this.endContinueContentPlayout(),this.state.contentPaused=!0,this.deps.gateContentForBreak(),this.deps.diagnose("DA-CONTINUE-PLAYOUT-ABANDONED",`Content could not be kept playing (${e}); the break reverted to pause and resume`,{breakId:i,reason:e,driftSec:t(n)})}endContinueContentPlayout(){if(!this.state.continueContentDuringBreakActive)return;this.state.continueContentDuringBreakActive=!1,this.state.continuePlayoutTicker&&(clearInterval(this.state.continuePlayoutTicker),this.state.continuePlayoutTicker=null),this.setContinueRate(1),this.state.keepContentPlayingHandler&&(this.deps.contentPlayer.off("pause",this.state.keepContentPlayingHandler),this.state.keepContentPlayingHandler=null),this.restoreContinueQuality("break-end");const e=this.currentContinueDriftSec(),n=this.deps.contentPlayer.currentTime,i=this.state.currentBreak,s=i?this.deps.plannedResumeTarget(i):null;this.deps.diagnose("DA-CONTINUE-PLAYOUT-END",`Content played through the break and reached the cut ${e.toFixed(3)}s from the break timeline, with no resume seek`,{breakId:this.state.currentBreak?.id,landed:t(n),target:null===s?null:t(s),errorSec:null===s?null:t(n-s),driftSec:t(e),maxDriftSec:t(this.state.continueMaxAbsDriftSec),corrections:this.state.continueCorrections,rateHonoured:this.state.continueRateHonoured,driftUnrecoverable:this.state.continueDriftUnrecoverable})}setContinueRate(t){void 0!==this.deps.contentPlayer.playbackRate&&(1===t&&null===this.state.continueRateWritten||(this.deps.contentPlayer.playbackRate=t,this.state.continueRateWritten=t,this.state.continueRateWrittenAtMs=this.deps.clock.now(),1===t&&(this.state.continueRateWritten=null)))}tickContinueContentPlayout(){if(!this.state.continueContentDuringBreakActive)return;const e=(this.deps.clock.now()-this.state.continuePlayoutStartWallMs)/1e3,n=this.state.contentPositionAtBreakStart+e,i=this.deps.contentPlayer.currentTime-n,s=Math.abs(i);s>this.state.continueMaxAbsDriftSec&&(this.state.continueMaxAbsDriftSec=s);const a=this.secondsUntilCut(e);if(this.state.continueQualityOverrideApplied&&a<=15&&this.restoreContinueQuality("staged"),a<=3)return void this.setContinueRate(1);if(null!==this.state.continueRateWritten&&void 0!==this.deps.contentPlayer.playbackRate&&Math.abs(this.deps.contentPlayer.playbackRate-this.state.continueRateWritten)>.005)return this.state.continueRateHonoured=!1,this.deps.log("continueContentDuringBreak: engine took over playbackRate; standing down"),void this.setContinueRate(1);if(!this.state.continueRateHonoured)return;if(s<=.25)return void this.setContinueRate(1);if(this.deps.clock.now()-this.state.continueRateWrittenAtMs<1e3)return;const r=Math.max(a-3,1),o=Math.min(1.08,Math.max(.92,1-i/r));!this.state.continueDriftUnrecoverable&&s>(1.08-1)*r&&(this.state.continueDriftUnrecoverable=!0,this.deps.diagnose("DA-CONTINUE-PLAYOUT-DRIFT",`Content drifted ${i.toFixed(3)}s from the break timeline, more than the ${r.toFixed(1)}s left can remove; correcting at the rate bound and carrying the rest`,{breakId:this.state.currentBreak?.id,driftSec:t(i),remainingSec:t(r),rate:o,seekIssued:!1})),this.state.continueCorrections.push(Math.round(1e3*(o-1))/1e3),this.setContinueRate(o)}}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { PlayerAdapter, TimedMetadataCue } from '../../interfaces/PlayerAdapter';
|
|
2
|
+
import type { Asset, Break, VendorAsset } from '../../types/BreakManifest';
|
|
3
|
+
import type { GamConfig } from '../../types/OptiViewAdsConfig';
|
|
4
|
+
import { type GamPodAdInfo, type GamPodAdStep } from '../GamPodAdTracker';
|
|
5
|
+
import { type GamSessionIdentity } from '../resolveGamVendorConfiguration';
|
|
6
|
+
import type { AdBreakStep } from '../AdBreakSequencer';
|
|
7
|
+
import type { AdPlayerState } from './AdPlayerState';
|
|
8
|
+
export interface GamEventCallbacks {
|
|
9
|
+
onAdFirstQuartile?: (breakInfo: Break, asset: Asset, adIndex?: number, totalAds?: number) => void;
|
|
10
|
+
onAdMidpoint?: (breakInfo: Break, asset: Asset, adIndex?: number, totalAds?: number) => void;
|
|
11
|
+
onAdThirdQuartile?: (breakInfo: Break, asset: Asset, adIndex?: number, totalAds?: number) => void;
|
|
12
|
+
}
|
|
13
|
+
export interface GamPodBridgeDeps {
|
|
14
|
+
contentPlayer: PlayerAdapter;
|
|
15
|
+
debug: boolean;
|
|
16
|
+
getAdPlayer: () => PlayerAdapter | null;
|
|
17
|
+
getAdContainer: () => HTMLElement | null;
|
|
18
|
+
isSharedElement: () => boolean;
|
|
19
|
+
getCurrentBreak: () => Break | null;
|
|
20
|
+
getCurrentAsset: () => Asset | null;
|
|
21
|
+
getEffectiveBreakDuration: () => number | null;
|
|
22
|
+
getAdPlayerStreamRemainingSec: () => number;
|
|
23
|
+
emitStep: (step: AdBreakStep) => void;
|
|
24
|
+
emitAdStart: (breakInfo: Break, asset: Asset, index: number, total: number, identity?: {
|
|
25
|
+
adId?: string | undefined;
|
|
26
|
+
creativeId?: string | undefined;
|
|
27
|
+
podPosition?: number | undefined;
|
|
28
|
+
}) => void;
|
|
29
|
+
emitAdEnd: (breakInfo: Break, asset: Asset, index: number, total: number, identity?: {
|
|
30
|
+
adId?: string | undefined;
|
|
31
|
+
creativeId?: string | undefined;
|
|
32
|
+
podPosition?: number | undefined;
|
|
33
|
+
}) => void;
|
|
34
|
+
onAdEnded: () => void;
|
|
35
|
+
pushAssetParameters: (assetParameters: Record<string, string> | undefined, assetId?: string) => void;
|
|
36
|
+
resolveSessionAdTagParameters: () => Record<string, string> | undefined;
|
|
37
|
+
diagnose: (code: string, message: string, context?: Record<string, unknown>) => void;
|
|
38
|
+
log: (...args: unknown[]) => void;
|
|
39
|
+
}
|
|
40
|
+
/** Owns the IMA stream session and per-ad expansion of GAM pod assets. */
|
|
41
|
+
export declare class GamPodBridge {
|
|
42
|
+
private readonly state;
|
|
43
|
+
private readonly deps;
|
|
44
|
+
constructor(state: AdPlayerState, deps: GamPodBridgeDeps);
|
|
45
|
+
setConfig(config: GamConfig | undefined): void;
|
|
46
|
+
setEventCallbacks(callbacks: GamEventCallbacks): void;
|
|
47
|
+
get eventCallbacks(): GamEventCallbacks;
|
|
48
|
+
startGamSession(identity: GamSessionIdentity, assetParameters?: Record<string, string>): Promise<void>;
|
|
49
|
+
endGamSession(): void;
|
|
50
|
+
initializeGam(): Promise<void>;
|
|
51
|
+
isGamEnabled(): boolean;
|
|
52
|
+
isGamReady(): boolean;
|
|
53
|
+
isGamPodAsset(asset: Asset): boolean;
|
|
54
|
+
isExpandingPodAsset(asset: Asset | undefined): boolean;
|
|
55
|
+
beginGamPodTracking(asset: Asset): void;
|
|
56
|
+
fallBackToPodLevelReporting(): void;
|
|
57
|
+
clearGamPodReportTimer(): void;
|
|
58
|
+
resetGamPodTracking(): void;
|
|
59
|
+
onGamPodAdStarted(info?: GamPodAdInfo): void;
|
|
60
|
+
emitGamPodSteps(steps: GamPodAdStep[]): void;
|
|
61
|
+
emitGamQuartile(callback: ((breakInfo: Break, asset: Asset, adIndex?: number, totalAds?: number) => void) | undefined): void;
|
|
62
|
+
buildGamUrl(asset: VendorAsset, breakDuration: number): Promise<string | null>;
|
|
63
|
+
processTimedMetadata(cue: TimedMetadataCue): void;
|
|
64
|
+
destroy(): void;
|
|
65
|
+
private callbacks;
|
|
66
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Vendor as t}from"../../types/BreakManifest";import{GamPodAdTracker as e}from"../GamPodAdTracker";import{GamStreamManager as s}from"../GamStreamManager";import{isGamVendorParameters as a}from"../GamPodUrlBuilder";import{gamPodIdentityMismatches as i}from"../resolveGamVendorConfiguration";export class GamPodBridge{constructor(t,e){this.state=t,this.deps=e,this.callbacks={}}setConfig(t){this.state.gamConfig=t}setEventCallbacks(t){this.callbacks=t}get eventCallbacks(){return this.callbacks}async startGamSession(t,e){this.state.currentGamIdentity=t,this.state.sessionAssetParameters=e,this.state.pushedAssetParameters=null,await this.initializeGam()}endGamSession(){this.state.gamStreamManager?.reset(),this.state.gamInitPromise=null,this.state.currentGamIdentity=void 0,this.state.sessionAssetParameters=void 0,this.state.manifestAssetParameters=void 0,this.state.sessionAssetParameterMacros={},this.state.updatedAssetParameters=void 0,this.state.pushedAssetParameters=null}async initializeGam(){if(!this.state.currentGamIdentity)return;if(this.deps.log("Initializing GAM stream manager..."),this.state.gamStreamManager)this.state.gamStreamManager.reset();else{const t=this.deps.isSharedElement()?this.deps.contentPlayer.videoElement:this.deps.getAdPlayer()?.videoElement;if(!t)return void console.warn("[OptiViewAds] GAM requires an adapter that exposes videoElement. GAM will be skipped.");const e=this.deps.isSharedElement()?void 0:this.deps.getAdContainer()||void 0;this.state.gamStreamManager=new s(t,e,this.deps.debug)}this.state.gamStreamManager.setCallbacks({onAdFirstQuartile:()=>this.emitGamQuartile(this.callbacks.onAdFirstQuartile),onAdMidpoint:()=>this.emitGamQuartile(this.callbacks.onAdMidpoint),onAdThirdQuartile:()=>this.emitGamQuartile(this.callbacks.onAdThirdQuartile),onAdStarted:t=>{this.deps.log("GAM ad started"),this.onGamPodAdStarted(t)},onAdComplete:()=>{this.deps.log("GAM ad complete"),"per-ad"===this.state.gamPodMode&&this.state.gamPodTracker&&this.emitGamPodSteps(this.state.gamPodTracker.onAdComplete())},onAdBreakEnded:()=>{this.deps.log("GAM ad break ended");const t=this.deps.getAdPlayerStreamRemainingSec();if(t>.25)return this.deps.log(`GAM pod has ${t.toFixed(3)}s of slate left; playing it out`),void("per-ad"===this.state.gamPodMode&&this.state.gamPodTracker&&this.emitGamPodSteps(this.state.gamPodTracker.finish()));this.deps.onAdEnded()},onError:t=>this.deps.log("GAM error:",t)});const t=this.state.currentGamIdentity,e=this.deps.resolveSessionAdTagParameters(),a={networkCode:t.networkCode,customAssetKey:t.customAssetKey,...e&&{adTagParameters:e},...this.state.gamConfig?.streamActivityMonitorId&&{streamActivityMonitorId:this.state.gamConfig.streamActivityMonitorId}};this.state.gamInitPromise=this.state.gamStreamManager.initialize(a);try{await this.state.gamInitPromise,this.deps.log("GAM stream manager initialized, streamId:",this.state.gamStreamManager.getStreamId())}catch(t){throw this.state.gamInitPromise=null,t}}isGamEnabled(){return!1!==this.state.gamConfig?.enabled}isGamReady(){return!0===this.state.gamStreamManager?.isInitialized()}isGamPodAsset(e){return"vendor"===e.type&&e.vendor===t.GAM&&this.isGamReady()}isExpandingPodAsset(t){return("pending"===this.state.gamPodMode||"per-ad"===this.state.gamPodMode)&&(!!t&&t.id===this.state.gamPodAsset?.id)}beginGamPodTracking(t){this.resetGamPodTracking(),this.state.gamPodAsset=t,this.state.gamPodTracker=new e(t),this.state.gamPodMode="pending",this.state.gamPodReportTimer=setTimeout(()=>{this.deps.log("No IMA pod ad reported in time; reporting the pod as a single ad"),this.fallBackToPodLevelReporting()},2e3)}fallBackToPodLevelReporting(){if("pending"!==this.state.gamPodMode)return;this.clearGamPodReportTimer(),this.state.gamPodMode="legacy",this.state.gamPodTracker=null;const t=this.state.withheldPodAdBegin;this.state.withheldPodAdBegin=null,t&&this.deps.emitStep(t)}clearGamPodReportTimer(){this.state.gamPodReportTimer&&(clearTimeout(this.state.gamPodReportTimer),this.state.gamPodReportTimer=null)}resetGamPodTracking(){this.clearGamPodReportTimer(),this.state.gamPodMode="none",this.state.gamPodTracker=null,this.state.gamPodAsset=null,this.state.withheldPodAdBegin=null}onGamPodAdStarted(t){t&&this.state.gamPodTracker&&("pending"===this.state.gamPodMode&&(this.clearGamPodReportTimer(),this.state.withheldPodAdBegin=null,this.state.gamPodMode="per-ad"),"per-ad"===this.state.gamPodMode&&this.emitGamPodSteps(this.state.gamPodTracker.onAdStarted(t)))}emitGamPodSteps(t){const e=this.deps.getCurrentBreak();if(e)for(const s of t){const t={adId:s.adId,creativeId:s.creativeId,podPosition:s.adPosition};"adbegin"===s.type?this.deps.emitAdStart(e,s.asset,s.adIndex,s.totalAds,t):this.deps.emitAdEnd(e,s.asset,s.adIndex,s.totalAds,t)}}emitGamQuartile(t){const e=this.deps.getCurrentBreak();if(!t||!e)return;const s="per-ad"===this.state.gamPodMode?this.state.gamPodTracker?.currentAd:null;if(s)return void t(e,s.asset,s.adIndex,s.totalAds);const a=this.deps.getCurrentAsset();a&&t(e,a)}async buildGamUrl(t,e){if(!a(t.vendorParameters))return this.deps.log("Invalid GAM vendor parameters:",t.vendorParameters),null;if(this.state.gamInitPromise&&await this.state.gamInitPromise,!this.state.gamStreamManager?.isInitialized())throw this.deps.log("GAM stream manager not initialized"),new Error("GAM stream manager not initialized. The stream identity comes from the break manifest's vendorConfiguration — check for DA-GAM-CONFIG-MISSING.");const s=t.vendorParameters;if(this.state.currentGamIdentity){const e=i(this.state.currentGamIdentity,s);for(const s of e)this.deps.diagnose("DA-GAM-POD-IDENTITY-MISMATCH",`GAM asset ${t.id} skipped: vendorParameters.${s.field} "${s.assetValue}" does not match the stream session's "${s.sessionValue}".`,{assetId:t.id,field:s.field,assetValue:s.assetValue,sessionValue:s.sessionValue});if(e.length>0)return null}this.deps.pushAssetParameters(t.assetParameters,t.id);const r=this.state.gamStreamManager.buildPodUrl(t.uri,e,s);return this.deps.log("Built GAM pod URL:",r),r}processTimedMetadata(t){this.state.gamStreamManager?.processTimedMetadata(t)}destroy(){this.resetGamPodTracking(),this.state.gamStreamManager?.destroy(),this.state.gamStreamManager=null,this.state.gamInitPromise=null}}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { PlayerAdapter } from '../../interfaces/PlayerAdapter';
|
|
2
|
+
import type { AssetUri } from '../../types/BreakManifest';
|
|
3
|
+
import type { PauseAdResolution } from '../PauseAdController';
|
|
4
|
+
export interface PauseAdPresenterDeps {
|
|
5
|
+
contentPlayer: PlayerAdapter;
|
|
6
|
+
getStage: () => HTMLElement | null;
|
|
7
|
+
resolveUri: (uri: string | AssetUri[]) => string | null;
|
|
8
|
+
diagnose: (code: string, message: string, context?: Record<string, unknown>) => void;
|
|
9
|
+
log: (...args: unknown[]) => void;
|
|
10
|
+
}
|
|
11
|
+
/** Owns the full-surface pause ad and its resume/close controls. */
|
|
12
|
+
export declare class PauseAdPresenter {
|
|
13
|
+
private readonly deps;
|
|
14
|
+
private pauseAdContainer;
|
|
15
|
+
private pauseAdVideoEl;
|
|
16
|
+
private pauseAdFadeTimer;
|
|
17
|
+
private pauseAdPinnedPosition;
|
|
18
|
+
/** Resume/close controls of the mounted pause ad, hidden until the controller allows resuming. */
|
|
19
|
+
private pauseAdResumeControls;
|
|
20
|
+
private pauseAdCreativeEndedCallback;
|
|
21
|
+
constructor(deps: PauseAdPresenterDeps);
|
|
22
|
+
/**
|
|
23
|
+
* Full-screen pause ad over the paused content: an `<img>` or a muted, play-once `<video>`
|
|
24
|
+
* as click-through target, plus SDK-rendered resume and close buttons because the player's
|
|
25
|
+
* own controls are covered. For `vast` the tag is fetched and its CompanionAds StaticResource
|
|
26
|
+
* (image) or Linear MediaFile (video) is parsed.
|
|
27
|
+
*/
|
|
28
|
+
showPauseImage(resolution: PauseAdResolution): Promise<void>;
|
|
29
|
+
/** A video creative is stopped immediately so it does not keep playing under the fade. */
|
|
30
|
+
hidePauseImage(): void;
|
|
31
|
+
/** Show the resume and close controls of the mounted pause ad (the viewer may resume content). */
|
|
32
|
+
revealPauseAdResume(): void;
|
|
33
|
+
/** Invoked when a video pause-ad creative ends (or cannot play). Pass `null` to detach. */
|
|
34
|
+
setPauseAdCreativeEndedCallback(callback: ((breakId: string) => void) | null): void;
|
|
35
|
+
destroy(): void;
|
|
36
|
+
/** Remove the pause ad immediately (no fade) — used on replace / destroy. */
|
|
37
|
+
private removePauseAdNow;
|
|
38
|
+
/** Pause and detach the pause-ad <video>, if one is mounted. */
|
|
39
|
+
private stopPauseAdVideo;
|
|
40
|
+
/**
|
|
41
|
+
* A bare `play()`, no seek — except on a live stream whose DVR window moved past the pinned
|
|
42
|
+
* position while the ad was up; then playback re-anchors to the live edge.
|
|
43
|
+
*/
|
|
44
|
+
private resumeFromPauseAd;
|
|
45
|
+
/** Back edge of the content player's seekable range, or NaN when unknown. */
|
|
46
|
+
private seekableStart;
|
|
47
|
+
/** Live edge of the content player's seekable range, or NaN when unknown. */
|
|
48
|
+
private seekableEnd;
|
|
49
|
+
/** A centered circular play button that resumes content on click. */
|
|
50
|
+
private buildPauseResumeButton;
|
|
51
|
+
/** A top-right close button that also resumes content on click. */
|
|
52
|
+
private buildPauseCloseButton;
|
|
53
|
+
/**
|
|
54
|
+
* For `static` the asset uri is the creative URL. For `vast` the tag is parsed by media type:
|
|
55
|
+
* video reads the Linear MediaFile, image the CompanionAds StaticResource. Null when none is usable.
|
|
56
|
+
*/
|
|
57
|
+
private resolvePauseAsset;
|
|
58
|
+
/** Fire companion creativeView impression beacons (fire-and-forget). */
|
|
59
|
+
private firePauseImpressions;
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{MediaType as e}from"../../types/BreakManifest";import{AUX_FETCH_TIMEOUT_MS as t,fetchWithTimeout as s}from"../fetchWithTimeout";import{parseVastCompanionImage as i}from"../parseVastCompanion";import{parseVastLinearMediaFile as a}from"../parseVastLinear";import{resolvePauseResumePoint as n}from"../resolvePauseResumePoint";export class PauseAdPresenter{constructor(e){this.deps=e,this.pauseAdContainer=null,this.pauseAdVideoEl=null,this.pauseAdFadeTimer=null,this.pauseAdPinnedPosition=0,this.pauseAdResumeControls=[],this.pauseAdCreativeEndedCallback=null}async showPauseImage(e){const t=this.deps.getStage();if(!t)return;const s=await this.resolvePauseAsset(e);if(!s)return void this.deps.diagnose("DA-PAUSE-AD-NO-ASSET",`Pause ad '${e.break.id}' asset could not be resolved; nothing shown.`,{breakId:e.break.id,source:e.source,mediaType:e.mediaType});this.removePauseAdNow(),this.pauseAdPinnedPosition=this.deps.contentPlayer.currentTime;const i=document.createElement("div");if(i.className="dolby-pause-ad",i.style.cssText="position:absolute;top:0;left:0;right:0;bottom:0;z-index:150;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.35);opacity:0;transition:opacity 300ms ease-in-out;"+(s.clickThrough?"cursor:pointer;":""),"video"===s.kind){const e=document.createElement("video");e.className="dolby-pause-ad-video",e.style.cssText="max-width:100%;max-height:100%;object-fit:contain;",e.muted=!0,e.loop=!1,e.autoplay=!0,e.playsInline=!0,e.setAttribute("playsinline",""),e.src=s.url,this.pauseAdVideoEl=e,i.appendChild(e)}else{const e=document.createElement("img");e.className="dolby-pause-ad-image",e.style.cssText="max-width:100%;max-height:100%;object-fit:contain;",e.src=s.url,i.appendChild(e)}if(s.clickThrough){const e=s.clickThrough;i.addEventListener("click",()=>{window.open(e,"_blank","noopener")})}const a=this.buildPauseResumeButton(),n=this.buildPauseCloseButton();if(a.style.display="none",n.style.display="none",this.pauseAdResumeControls=[a,n],i.appendChild(a),i.appendChild(n),this.pauseAdVideoEl){const t=this.pauseAdVideoEl,s=e.break.id,i=()=>{this.pauseAdVideoEl===t&&this.pauseAdCreativeEndedCallback?.(s)};t.addEventListener("ended",i,{once:!0}),t.addEventListener("error",i,{once:!0}),t.play().catch(i)}t.appendChild(i),this.pauseAdContainer=i,requestAnimationFrame(()=>{this.pauseAdContainer===i&&(i.style.opacity="1")}),this.firePauseImpressions(s.impressions),this.deps.log("Pause ad shown:",e.break.id,e.source,e.mediaType)}hidePauseImage(){const e=this.pauseAdContainer;e&&(this.stopPauseAdVideo(),this.pauseAdContainer=null,e.style.opacity="0",this.pauseAdFadeTimer&&clearTimeout(this.pauseAdFadeTimer),this.pauseAdFadeTimer=setTimeout(()=>{e.parentElement&&e.parentElement.removeChild(e),this.pauseAdFadeTimer=null},300))}revealPauseAdResume(){for(const e of this.pauseAdResumeControls)e.style.display=""}setPauseAdCreativeEndedCallback(e){this.pauseAdCreativeEndedCallback=e}destroy(){this.removePauseAdNow()}removePauseAdNow(){this.pauseAdFadeTimer&&(clearTimeout(this.pauseAdFadeTimer),this.pauseAdFadeTimer=null),this.stopPauseAdVideo(),this.pauseAdContainer?.parentElement&&this.pauseAdContainer.parentElement.removeChild(this.pauseAdContainer),this.pauseAdContainer=null}stopPauseAdVideo(){this.pauseAdResumeControls=[];const e=this.pauseAdVideoEl;if(e){try{e.pause(),e.removeAttribute("src")}catch{}this.pauseAdVideoEl=null}}resumeFromPauseAd(){const e=n(this.pauseAdPinnedPosition,this.seekableStart(),this.seekableEnd());null!==e&&(this.deps.diagnose("DA-PAUSE-AD-DVR-EXPIRED",`Pause outlived the DVR window; resuming at the live edge (${e.toFixed(2)}s).`,{pinnedPosition:this.pauseAdPinnedPosition,liveEdge:e}),this.deps.contentPlayer.seek(e)),this.deps.contentPlayer.play()}seekableStart(){const e=this.deps.contentPlayer.videoElement?.seekable;return e&&0!==e.length?e.start(0):Number.NaN}seekableEnd(){const e=this.deps.contentPlayer.videoElement?.seekable;return e&&0!==e.length?e.end(e.length-1):Number.NaN}buildPauseResumeButton(){const e=document.createElement("button");return e.className="dolby-pause-ad-resume",e.setAttribute("aria-label","Resume playback"),e.style.cssText="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:72px;height:72px;border:none;border-radius:50%;cursor:pointer;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;",e.innerHTML='<svg width="32" height="32" viewBox="0 0 24 24" fill="#fff" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>',e.addEventListener("click",e=>{e.stopPropagation(),this.resumeFromPauseAd()}),e}buildPauseCloseButton(){const e=document.createElement("button");return e.className="dolby-pause-ad-close",e.setAttribute("aria-label","Close pause ad and resume"),e.style.cssText="position:absolute;top:8px;right:8px;width:32px;height:32px;border:none;border-radius:50%;cursor:pointer;background:rgba(0,0,0,0.6);color:#fff;font-size:18px;line-height:32px;text-align:center;padding:0;",e.textContent="✕",e.addEventListener("click",e=>{e.stopPropagation(),this.resumeFromPauseAd()}),e}async resolvePauseAsset(n){const o=n.asset,r=n.mediaType===e.VIDEO?"video":"image";if("static"===n.source){const e=this.deps.resolveUri(o.uri);if(!e)return null;const t=o.interaction?.clickThrough;return{kind:r,url:e,...t?{clickThrough:t}:{},impressions:[]}}const d=this.deps.resolveUri(o.uri);if(!d)return null;try{const e=await s(d,{},t),n=await e.text();if("video"===r){const e=a(n);if(!e)return null;const t=e.clickThrough??o.interaction?.clickThrough;return{kind:r,url:e.mediaFileUrl,...t?{clickThrough:t}:{},impressions:e.impressions}}const l=i(n);if(!l)return null;const u=l.clickThrough??o.interaction?.clickThrough;return{kind:r,url:l.imageUrl,...u?{clickThrough:u}:{},impressions:l.impressions}}catch(e){return this.deps.log("Pause VAST fetch/parse failed:",e),null}}firePauseImpressions(e){for(const t of e)try{(new Image).src=t}catch{}}}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { PlayerAdapter } from '../../interfaces/PlayerAdapter';
|
|
2
|
+
import { BreakFormat, type Break, type BreakVariant } from '../../types/BreakManifest';
|
|
3
|
+
import type { AdPlayerState } from './AdPlayerState';
|
|
4
|
+
export interface PresentationModeTrackerDeps {
|
|
5
|
+
contentPlayer: PlayerAdapter;
|
|
6
|
+
getAdPlayer: () => PlayerAdapter | null;
|
|
7
|
+
insertionMode: 'overlay' | 'shared-element' | 'adaptive';
|
|
8
|
+
getCurrentBreak: () => Break | null;
|
|
9
|
+
getCurrentFormat: () => BreakFormat | undefined;
|
|
10
|
+
isSharedElementActive: () => boolean;
|
|
11
|
+
getVariant: (breakInfo: Break) => BreakVariant | null;
|
|
12
|
+
isAdPlaying: () => boolean;
|
|
13
|
+
hasCurrentAsset: () => boolean;
|
|
14
|
+
contentSourceIsReloadable: () => boolean;
|
|
15
|
+
adoptPictureInPictureMidBreak: () => void;
|
|
16
|
+
restoreLayout: (format?: BreakFormat) => void;
|
|
17
|
+
applyLayout: (variant: BreakVariant) => void;
|
|
18
|
+
show: () => void;
|
|
19
|
+
playAdThroughContentElement: () => Promise<void>;
|
|
20
|
+
getContentPositionAtBreakStart: () => number;
|
|
21
|
+
setContentPositionAtBreakStart: (position: number) => void;
|
|
22
|
+
gateContentForBreak: () => void;
|
|
23
|
+
diagnose: (code: string, message: string, context?: Record<string, unknown>) => void;
|
|
24
|
+
log: (...args: unknown[]) => void;
|
|
25
|
+
}
|
|
26
|
+
/** Owns fullscreen and picture-in-picture state and presentation-mode handoffs. */
|
|
27
|
+
export declare class PresentationModeTracker {
|
|
28
|
+
private readonly state;
|
|
29
|
+
private readonly deps;
|
|
30
|
+
constructor(state: AdPlayerState, deps: PresentationModeTrackerDeps);
|
|
31
|
+
currentInsertionMode(): 'overlay' | 'shared-element';
|
|
32
|
+
effectiveInsertionMode(): 'overlay' | 'shared-element';
|
|
33
|
+
private isContentPictureInPicture;
|
|
34
|
+
private elementHasPictureInPicture;
|
|
35
|
+
private isContentFullscreen;
|
|
36
|
+
setupFullscreenTracking(): void;
|
|
37
|
+
setupPictureInPictureTracking(): void;
|
|
38
|
+
private pipEnterHandler;
|
|
39
|
+
private pipExitHandler;
|
|
40
|
+
private pipPresentationModeHandler;
|
|
41
|
+
rebindContentElementIfSwapped(): void;
|
|
42
|
+
teardownFullscreenTracking(): void;
|
|
43
|
+
teardownPictureInPictureTracking(): void;
|
|
44
|
+
isPictureInPicture(): boolean;
|
|
45
|
+
pictureInPictureOwner(): 'content' | 'ad' | null;
|
|
46
|
+
/**
|
|
47
|
+
* Preferred: move the window to the ad element (content undisturbed). Safari refuses that,
|
|
48
|
+
* so the fallback plays the ad through the content element, which already owns the window;
|
|
49
|
+
* only possible when `restorableContentSource()` says content can be brought back.
|
|
50
|
+
*/
|
|
51
|
+
moveAdIntoPictureInPictureWindow(): Promise<void>;
|
|
52
|
+
adoptPictureInPictureMidBreak(): void;
|
|
53
|
+
restorePictureInPictureToContent(): Promise<void>;
|
|
54
|
+
transferPictureInPictureToAd(): Promise<void>;
|
|
55
|
+
private canReceivePictureInPictureWindow;
|
|
56
|
+
/** Whether the AD element is the one currently presented in picture-in-picture. */
|
|
57
|
+
private adElementHasPictureInPicture;
|
|
58
|
+
/** Put `video` in picture-in-picture, through whichever API the browser implements. */
|
|
59
|
+
private enterPictureInPicture;
|
|
60
|
+
/**
|
|
61
|
+
* `webkitSetPresentationMode` returns `void` and transitions asynchronously, so reading the
|
|
62
|
+
* mode back on the next microtask sees the old value; resolve on the matching
|
|
63
|
+
* `webkitpresentationmodechanged`, give up only on timeout.
|
|
64
|
+
*/
|
|
65
|
+
private setWebkitPresentationMode;
|
|
66
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{BreakFormat as e}from"../../types/BreakManifest";import{pipOverridesFormat as t,pipSuppressesBackdrop as n}from"../pictureInPicture";export class PresentationModeTracker{constructor(e,t){this.state=e,this.deps=t,this.pipEnterHandler=null,this.pipExitHandler=null,this.pipPresentationModeHandler=null}currentInsertionMode(){return this.deps.isSharedElementActive()?"shared-element":this.effectiveInsertionMode()}effectiveInsertionMode(){return"shared-element"===this.deps.insertionMode||this.isContentPictureInPicture()&&this.deps.contentSourceIsReloadable()&&!this.canReceivePictureInPictureWindow()||"adaptive"===this.deps.insertionMode&&this.isContentFullscreen()?"shared-element":"overlay"}isContentPictureInPicture(){const e=this.deps.getAdPlayer()?.videoElement,t=this.deps.contentPlayer.videoElement;return this.elementHasPictureInPicture(t)||this.elementHasPictureInPicture(e)}elementHasPictureInPicture(e){return!!e&&("picture-in-picture"===e.webkitPresentationMode||"undefined"!=typeof document&&document.pictureInPictureElement===e)}isContentFullscreen(){const e=this.deps.contentPlayer.videoElement;return e&&"boolean"==typeof e.webkitDisplayingFullscreen?e.webkitDisplayingFullscreen:this.state.isContentFullscreen}setupFullscreenTracking(){const e=this.deps.contentPlayer.videoElement;e&&(this.state.trackedContentVideo=e,this.state.fullscreenEnterHandler=()=>{this.state.isContentFullscreen=!0},this.state.fullscreenExitHandler=()=>{this.state.isContentFullscreen=!1},e.addEventListener("webkitbeginfullscreen",this.state.fullscreenEnterHandler),e.addEventListener("webkitendfullscreen",this.state.fullscreenExitHandler))}setupPictureInPictureTracking(){const e=this.deps.contentPlayer.videoElement;e&&(this.state.trackedContentVideo=e,this.pipEnterHandler=()=>{this.state.contentPictureInPicture=!0,this.deps.adoptPictureInPictureMidBreak()},this.pipExitHandler=()=>{this.state.pipTransferredToAd||(this.state.contentPictureInPicture=!1)},this.pipPresentationModeHandler=()=>{const t=e.webkitPresentationMode;this.state.contentPictureInPicture="picture-in-picture"===t,this.state.contentPictureInPicture&&this.deps.adoptPictureInPictureMidBreak()},e.addEventListener("enterpictureinpicture",this.pipEnterHandler),e.addEventListener("leavepictureinpicture",this.pipExitHandler),e.addEventListener("webkitpresentationmodechanged",this.pipPresentationModeHandler))}rebindContentElementIfSwapped(){const e=this.deps.contentPlayer.videoElement??null;if(e===this.state.trackedContentVideo)return;if(!this.state.trackedContentVideo||!e)return;const t=this.state.trackedContentVideo;this.teardownPictureInPictureTracking(),this.teardownFullscreenTracking(),this.state.trackedContentVideo=e,this.setupPictureInPictureTracking(),this.setupFullscreenTracking(),this.state.contentPictureInPicture=this.elementHasPictureInPicture(e),this.deps.diagnose("DA-CONTENT-ELEMENT-SWAPPED","the content player replaced its <video>",{wasConnected:t.isConnected,pictureInPicture:this.state.contentPictureInPicture})}teardownFullscreenTracking(){const e=this.state.trackedContentVideo??this.deps.contentPlayer.videoElement;e&&(this.state.fullscreenEnterHandler&&e.removeEventListener("webkitbeginfullscreen",this.state.fullscreenEnterHandler),this.state.fullscreenExitHandler&&e.removeEventListener("webkitendfullscreen",this.state.fullscreenExitHandler)),this.state.fullscreenEnterHandler=null,this.state.fullscreenExitHandler=null,this.state.isContentFullscreen=!1}teardownPictureInPictureTracking(){const e=this.state.trackedContentVideo??this.deps.contentPlayer.videoElement;e&&(this.pipEnterHandler&&e.removeEventListener("enterpictureinpicture",this.pipEnterHandler),this.pipExitHandler&&e.removeEventListener("leavepictureinpicture",this.pipExitHandler),this.pipPresentationModeHandler&&e.removeEventListener("webkitpresentationmodechanged",this.pipPresentationModeHandler)),this.pipEnterHandler=null,this.pipExitHandler=null,this.pipPresentationModeHandler=null,this.state.contentPictureInPicture=!1}isPictureInPicture(){return this.isContentPictureInPicture()}pictureInPictureOwner(){const e=this.deps.getAdPlayer()?.videoElement;if(this.elementHasPictureInPicture(e))return"ad";const t=this.deps.contentPlayer.videoElement;return t&&this.elementHasPictureInPicture(t)?"content":null}async moveAdIntoPictureInPictureWindow(){this.canReceivePictureInPictureWindow()&&(await this.transferPictureInPictureToAd(),this.state.pipTransferredToAd)||this.deps.isAdPlaying()&&this.deps.getCurrentBreak()&&this.deps.hasCurrentAsset()&&(this.deps.isSharedElementActive()||(this.deps.contentSourceIsReloadable()?await this.deps.playAdThroughContentElement():this.deps.diagnose("DA-SHARED-ELEMENT-UNSUPPORTED","The window could not be moved to the ad, and content cannot be reloaded, so the ad cannot be shown in the picture-in-picture window",{reason:"no-restorable-content-source",midBreak:!0})))}adoptPictureInPictureMidBreak(){if(!this.deps.isAdPlaying()||this.state.pipAdoptedForBreak)return;const i=this.deps.getCurrentFormat();if(n(i,!0))return this.state.pipAdoptedForBreak=!0,this.deps.restoreLayout(i),void this.deps.diagnose("DA-PIP-FORMAT-OVERRIDE","Entered picture-in-picture during an lshape_content break; the window shows content",{declared:i,effective:i,reason:"picture-in-picture",midBreak:!0});if(!t(i,!0))return void this.moveAdIntoPictureInPictureWindow();this.state.pipAdoptedForBreak=!0,this.state.contentPaused||(this.state.contentPaused=!0,this.deps.setContentPositionAtBreakStart(this.deps.contentPlayer.currentTime),this.deps.gateContentForBreak()),this.deps.restoreLayout(i);const r=this.deps.getCurrentBreak(),s=r?this.deps.getVariant(r):null;s&&(this.deps.applyLayout(s),this.deps.show()),this.deps.diagnose("DA-PIP-FORMAT-OVERRIDE",`Entered picture-in-picture during a ${i} break; it now plays as single`,{declared:i,effective:e.SINGLE,reason:"picture-in-picture",midBreak:!0}),this.moveAdIntoPictureInPictureWindow()}async restorePictureInPictureToContent(){if(!this.state.pipTransferredToAd)return;this.state.pipTransferredToAd=!1;const e=this.deps.contentPlayer.videoElement;if(e&&this.adElementHasPictureInPicture())try{await this.enterPictureInPicture(e),this.deps.log("PiP window handed back to content")}catch(e){this.deps.diagnose("DA-PIP-TRANSFER-FAILED",`Could not return the picture-in-picture window to content: ${e instanceof Error?e.message:String(e)}`,{direction:"to-content"})}}async transferPictureInPictureToAd(){if(!this.isContentPictureInPicture())return;if(this.state.pipTransferredToAd)return;const e=this.deps.getAdPlayer()?.videoElement;if(e&&!this.elementHasPictureInPicture(e))try{if(await this.enterPictureInPicture(e),!this.adElementHasPictureInPicture())throw new Error("the browser did not move the window to the ad element");this.state.pipTransferredToAd=!0,this.deps.log("PiP window handed to the ad element")}catch(e){this.deps.diagnose("DA-PIP-TRANSFER-FAILED",`Could not move the picture-in-picture window to the ad: ${e instanceof Error?e.message:String(e)}`,{direction:"to-ad"})}}canReceivePictureInPictureWindow(){const e=this.deps.getAdPlayer()?.videoElement;return!!e&&("function"==typeof e.requestPictureInPicture&&"function"!=typeof e.webkitSetPresentationMode)}adElementHasPictureInPicture(){return this.elementHasPictureInPicture(this.deps.getAdPlayer()?.videoElement)}async enterPictureInPicture(e){"function"!=typeof e.webkitSetPresentationMode?await(e.requestPictureInPicture?.()):await this.setWebkitPresentationMode(e,"picture-in-picture")}setWebkitPresentationMode(e,t){return new Promise((n,i)=>{if(e.webkitPresentationMode===t)return void n();const r=t=>{e.removeEventListener("webkitpresentationmodechanged",s),clearTimeout(o),t?i(t):n()},s=()=>{e.webkitPresentationMode===t&&r()};e.addEventListener("webkitpresentationmodechanged",s);const o=setTimeout(()=>r(new Error(`the browser did not enter '${t}' within 3000ms`)),3e3);try{e.webkitSetPresentationMode?.(t)}catch(e){r(e instanceof Error?e:new Error(String(e)))}})}}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { PlayerAdapter } from '../../interfaces/PlayerAdapter';
|
|
2
|
+
import type { Asset, Break } from '../../types/BreakManifest';
|
|
3
|
+
import type { AdPlayerCallbacks } from '../AdPlayerController';
|
|
4
|
+
import type { AdPlayerState } from './AdPlayerState';
|
|
5
|
+
export interface SharedElementBreakPlayerDeps {
|
|
6
|
+
contentPlayer: PlayerAdapter;
|
|
7
|
+
getAdPlayer: () => PlayerAdapter | null;
|
|
8
|
+
resolveAssetUri: (asset: Asset, duration: number) => Promise<string | null>;
|
|
9
|
+
liveEdgeDistanceSec: () => number | null;
|
|
10
|
+
rebindContentElementIfSwapped: () => void;
|
|
11
|
+
createAdAdapter: (container: HTMLElement, video?: HTMLVideoElement) => PlayerAdapter;
|
|
12
|
+
reportAdProgress: (player: PlayerAdapter) => void;
|
|
13
|
+
diagnose: (code: string, message: string, context?: Record<string, unknown>) => void;
|
|
14
|
+
log: (...args: unknown[]) => void;
|
|
15
|
+
clearContentGate: () => void;
|
|
16
|
+
hide: () => void;
|
|
17
|
+
waitForLiveEdge: () => Promise<number | null>;
|
|
18
|
+
getVariant: (breakInfo: Break) => {
|
|
19
|
+
format: string;
|
|
20
|
+
} | null;
|
|
21
|
+
getAssets: (breakInfo: Break) => Asset[];
|
|
22
|
+
}
|
|
23
|
+
/** Owns playback through the content element and restoration after shared-element breaks. */
|
|
24
|
+
export declare class SharedElementBreakPlayer {
|
|
25
|
+
private readonly state;
|
|
26
|
+
private readonly deps;
|
|
27
|
+
private contentElementAdPlayer;
|
|
28
|
+
private sharedAdSource;
|
|
29
|
+
private sharedAdEndedHandler;
|
|
30
|
+
private sharedAdTimeUpdateHandler;
|
|
31
|
+
private sharedAdFirstFrameTimer;
|
|
32
|
+
private sharedRestoreWatchdog;
|
|
33
|
+
constructor(state: AdPlayerState, deps: SharedElementBreakPlayerDeps);
|
|
34
|
+
sharedElementAdPlayer(): PlayerAdapter | null;
|
|
35
|
+
private sharedAdStep;
|
|
36
|
+
private clearSharedAdFirstFrameWatchdog;
|
|
37
|
+
private armSharedAdFirstFrameWatchdog;
|
|
38
|
+
private attachSharedAdListeners;
|
|
39
|
+
detachSharedAdListeners(): void;
|
|
40
|
+
/**
|
|
41
|
+
* Runs before the element is resolved: THEOplayer replaces its `<video>` when its source is
|
|
42
|
+
* cleared, so an element captured first would be a dead node. Engines that swap in place
|
|
43
|
+
* (hls.js) do not implement this.
|
|
44
|
+
*/
|
|
45
|
+
releaseContentElementFromContentEngine(): Promise<void>;
|
|
46
|
+
releaseContentElementAdPlayer(): void;
|
|
47
|
+
playAdThroughContentElement(): Promise<void>;
|
|
48
|
+
playSharedElementBreak(breakInfo: Break, callbacks: AdPlayerCallbacks): Promise<void>;
|
|
49
|
+
onSharedElementAdEnded(): void;
|
|
50
|
+
restoreSharedElementContent(breakInfo?: Break | null): void;
|
|
51
|
+
private armSharedRestoreWatchdog;
|
|
52
|
+
clearSharedRestoreWatchdog(): void;
|
|
53
|
+
restorableContentSource(): string | null;
|
|
54
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{BreakFormat as e,isPostRollBreak as t}from"../../types/BreakManifest";export class SharedElementBreakPlayer{constructor(e,t){this.state=e,this.deps=t,this.contentElementAdPlayer=null,this.sharedAdSource=null,this.sharedAdEndedHandler=null,this.sharedAdTimeUpdateHandler=null,this.sharedAdFirstFrameTimer=null,this.sharedRestoreWatchdog=null}sharedElementAdPlayer(){const e=this.deps.contentPlayer.videoElement;if(!e)return null;if(this.contentElementAdPlayer?.videoElement===e)return this.contentElementAdPlayer;this.releaseContentElementAdPlayer();const t=this.deps.createAdAdapter(document.createElement("div"),e);return t.videoElement!==e?(this.deps.log("shared-element: createAdAdapter ignored the content element; falling back to the content player for ad playback"),t.destroy(),null):(this.contentElementAdPlayer=t,t)}async sharedAdStep(e,t,s){try{await s()}catch(s){const a=t.videoElement,n=s instanceof Error?s.message:String(s);throw new Error(`shared-element ${e} failed: ${n} (readyState=${a?.readyState??"-"} paused=${a?.paused??"-"} networkState=${a?.networkState??"-"})`,{cause:s})}}clearSharedAdFirstFrameWatchdog(){this.sharedAdFirstFrameTimer&&(clearTimeout(this.sharedAdFirstFrameTimer),this.sharedAdFirstFrameTimer=null)}armSharedAdFirstFrameWatchdog(e,t){this.clearSharedAdFirstFrameWatchdog(),this.sharedAdFirstFrameTimer=setTimeout(()=>{if(this.sharedAdFirstFrameTimer=null,!this.state.isPlaying||!this.state.currentBreak)return;const s=e.videoElement,a=this.deps.contentPlayer.videoElement,n=s?.currentSrc??"",r=`readyState=${s?.readyState??"-"} networkState=${s?.networkState??"-"} paused=${s?.paused??"-"} connected=${s?.isConnected??"-"} srcKind=${n.startsWith("blob:")?"blob":n?"url":"none"} sameElementAsContentPlayer=${String(s===a)}`;this.deps.log(`shared-element: no first frame within ${t}ms (${r})`),this.deps.diagnose("DA-AD-PLAYBACK-ERROR",`the ad never rendered a frame on the content element within ${t}ms`,{breakId:this.state.currentBreak.id,detail:r});const d=this.state.currentAsset;d&&this.state.callbacks?.onAdError(this.state.currentBreak,d,new Error(`shared-element ad produced no frames within ${t}ms (${r})`)),this.onSharedElementAdEnded()},t)}attachSharedAdListeners(e){this.sharedAdSource=e,this.sharedAdEndedHandler=()=>this.onSharedElementAdEnded(),e.on("ended",this.sharedAdEndedHandler),this.sharedAdTimeUpdateHandler=()=>{this.sharedAdFirstFrameTimer&&e.currentTime>0&&this.clearSharedAdFirstFrameWatchdog(),this.deps.reportAdProgress(e)},e.on("timeupdate",this.sharedAdTimeUpdateHandler)}detachSharedAdListeners(){const e=this.sharedAdSource??this.deps.contentPlayer;this.sharedAdSource=null,this.state.sharedAdClaimActive=!1,this.clearSharedAdFirstFrameWatchdog(),this.sharedAdTimeUpdateHandler&&(e.off("timeupdate",this.sharedAdTimeUpdateHandler),this.sharedAdTimeUpdateHandler=null),this.sharedAdEndedHandler&&(e.off("ended",this.sharedAdEndedHandler),this.sharedAdEndedHandler=null)}async releaseContentElementFromContentEngine(){if(this.deps.contentPlayer.releaseMediaElement)try{await this.deps.contentPlayer.releaseMediaElement()}catch(e){this.deps.log("shared-element: the content engine did not release the element:",e)}}releaseContentElementAdPlayer(){if(this.contentElementAdPlayer){try{this.contentElementAdPlayer.destroy()}catch(e){this.deps.log("shared-element: releasing the ad player on the content element failed:",e)}this.contentElementAdPlayer=null}}async playAdThroughContentElement(){const e=this.state.currentAsset,t=this.state.currentBreak;if(!e||!t)return;const s=this.state.effectiveBreakDurationSec??t.duration,a=await this.deps.resolveAssetUri(e,s);if(!a)return void this.deps.log("shared-element: no URI for the running ad; leaving it on the ad element");const n=this.deps.getAdPlayer()?.currentTime??0;this.state.savedContentSrc=this.restorableContentSource(),this.state.savedContentTime=this.deps.contentPlayer.currentTime,this.state.savedContentLive=!Number.isFinite(this.deps.contentPlayer.duration),this.state.savedContentEdgeDistance=this.state.savedContentLive?this.deps.liveEdgeDistanceSec():null,this.state.sharedAdClaimActive=!0,await this.releaseContentElementFromContentEngine(),this.deps.rebindContentElementIfSwapped();const r=this.sharedElementAdPlayer()??this.deps.contentPlayer;try{this.deps.getAdPlayer()?.pause(),this.deps.clearContentGate(),await r.load(a),this.attachSharedAdListeners(r),n>0&&r.seek(n),await r.play(),this.deps.hide(),this.deps.log("shared-element: the ad now plays through the content element")}catch(e){this.deps.log("shared-element: could not move the ad onto the content element:",e),this.state.sharedAdClaimActive=!1,this.state.savedContentSrc=null,this.state.savedContentEdgeDistance=null}}async playSharedElementBreak(t,s){const a=this.deps.getVariant(t);if(!a)return this.deps.log("shared-element: no valid variant, skipping break:",t.id),void s.onBreakEnd(t);if(a.format===e.LSHAPE_CONTENT)return this.deps.log("shared-element: skipping lshape_content break (no fullscreen video asset):",t.id),void s.onBreakEnd(t);const n=this.deps.getAssets(t)[0];if(!n)return this.deps.log("shared-element: no asset to play, skipping break:",t.id),void s.onBreakEnd(t);a.format!==e.SINGLE&&this.deps.log(`shared-element: downgrading ${a.format} to a single fullscreen ad:`,t.id),this.state.currentBreak=t,this.state.currentAsset=n,this.state.callbacks=s,this.state.isPlaying=!0;const r=this.state.effectiveBreakDurationSec??t.duration,d=await this.deps.resolveAssetUri(n,r);if(!d)return this.deps.log("shared-element: no valid URI, skipping break:",t.id),this.state.isPlaying=!1,this.state.currentBreak=null,this.state.currentAsset=null,void s.onBreakEnd(t);this.state.savedContentSrc=this.restorableContentSource(),this.state.savedContentTime=this.deps.contentPlayer.currentTime,this.state.savedContentLive=!Number.isFinite(this.deps.contentPlayer.duration),this.state.savedContentEdgeDistance=this.state.savedContentLive?this.deps.liveEdgeDistanceSec():null,this.state.sharedAdClaimActive=!0,await this.releaseContentElementFromContentEngine(),this.deps.rebindContentElementIfSwapped();const i=this.sharedElementAdPlayer()??this.deps.contentPlayer;if(r&&r>0){const e=r+this.state.adBreakCutSafetyMarginSec;this.state.durationTimer=setTimeout(()=>{this.deps.log("shared-element: break-cut timer expired, ending break"),this.onSharedElementAdEnded()},1e3*e)}try{this.deps.clearContentGate(),await this.sharedAdStep("load",i,()=>i.load(d)),this.attachSharedAdListeners(i),this.armSharedAdFirstFrameWatchdog(i,Math.max(Math.min(1e3*(r??0)-100,5e3),3e3)),await this.sharedAdStep("play",i,()=>i.play()),this.state.callbacks?.onAdStart(t,n,0,1)}catch(e){this.deps.log("shared-element: ad playback failed:",e),this.state.callbacks?.onAdError(t,n,e instanceof Error?e:new Error(String(e))),this.onSharedElementAdEnded()}}onSharedElementAdEnded(){if(!this.state.isPlaying||!this.state.currentBreak)return;const e=this.state.currentBreak,t=this.state.currentAsset;this.detachSharedAdListeners(),this.state.durationTimer&&(clearTimeout(this.state.durationTimer),this.state.durationTimer=null),this.state.isPlaying=!1,this.state.currentBreak=null,t&&this.state.callbacks?.onAdEnd(e,t,0,1),this.state.currentAsset=null,this.restoreSharedElementContent(e),this.state.callbacks?.onBreakEnd(e)}restoreSharedElementContent(e){const s=this.state.savedContentSrc,a=this.state.savedContentTime,n=this.state.savedContentLive,r=this.state.savedContentEdgeDistance;if(this.state.savedContentSrc=null,this.state.savedContentEdgeDistance=null,!s)return this.deps.log("shared-element: no saved content source to restore"),void this.releaseContentElementAdPlayer();e&&"number"==typeof e.start&&this.state.callbacks?.onContentResumeSeek?.(n?this.deps.contentPlayer.currentTime:a,e.start+e.duration),this.state.callbacks?.onContentReloading?.(!0);const d=!!e&&t(e),i=async()=>{if(await this.deps.contentPlayer.load(s),n){if(!d&&null!==r&&r>0){const e=await this.deps.waitForLiveEdge();if(null!==e){const t=Math.max(e-r,0);this.deps.log(`shared-element: live resume ${r.toFixed(2)}s behind the edge (${t.toFixed(2)}s)`),this.deps.contentPlayer.seek(t)}}}else this.deps.contentPlayer.seek(a);d||await this.deps.contentPlayer.play()};i().catch(async t=>{this.deps.log("shared-element: content restore failed, retrying once:",t);try{await i()}catch(t){this.deps.diagnose("DA-SHARED-ELEMENT-RESTORE-FAILED",`Content did not resume after the shared-element break: ${t instanceof Error?t.message:String(t)}`,{breakId:e?.id,live:n,attempts:2})}}).finally(()=>{this.state.contentPaused=!1,this.deps.clearContentGate(),this.releaseContentElementAdPlayer(),this.deps.rebindContentElementIfSwapped(),this.state.callbacks?.onContentReloading?.(!1),d||this.armSharedRestoreWatchdog(e)})}armSharedRestoreWatchdog(e){this.sharedRestoreWatchdog&&clearTimeout(this.sharedRestoreWatchdog);const t=this.deps.contentPlayer.currentTime;this.sharedRestoreWatchdog=setTimeout(()=>{this.sharedRestoreWatchdog=null;const s=this.deps.contentPlayer.currentTime-t;(this.deps.contentPlayer.paused||s<1)&&(this.deps.diagnose("DA-CONTENT-RESUME-STALLED",`Content did not resume within 5s of the shared-element restore (paused: ${this.deps.contentPlayer.paused}, advanced: ${s.toFixed(1)}s); nudging play()`,{breakId:e?.id??"?",paused:this.deps.contentPlayer.paused,advancedSec:s}),this.deps.contentPlayer.play().catch(e=>{this.deps.log("shared-element: watchdog play() nudge failed:",e)}))},5e3)}clearSharedRestoreWatchdog(){this.sharedRestoreWatchdog&&(clearTimeout(this.sharedRestoreWatchdog),this.sharedRestoreWatchdog=null)}restorableContentSource(){const e=this.deps.contentPlayer.sourceUrl;if(e&&!e.startsWith("blob:"))return e;const t=this.deps.contentPlayer.videoElement,s=t?.currentSrc||t?.src||"";return s.length>0&&!s.startsWith("blob:")?s:null}}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Break, BreakFormat, BreakVariant } from '../../types/BreakManifest';
|
|
2
|
+
import type { ResolvedPreloadMode } from '../detectPreloadMode';
|
|
3
|
+
import { type EffectiveInsertionMode, type NoPlayableAssetReason } from '../AdBreakSequencer';
|
|
4
|
+
import { type VariantSelectionInputs } from '../selectVariant';
|
|
5
|
+
import type { AdPlayerState } from './AdPlayerState';
|
|
6
|
+
export interface VariantSelectorDeps {
|
|
7
|
+
getEffectiveInsertionMode: () => EffectiveInsertionMode;
|
|
8
|
+
singleSurfaceDevice: boolean;
|
|
9
|
+
preloadMode: ResolvedPreloadMode;
|
|
10
|
+
getIsContentPictureInPicture: () => boolean;
|
|
11
|
+
getIsGamReady: () => boolean;
|
|
12
|
+
getSupportsParallelBuffering: () => boolean | undefined;
|
|
13
|
+
diagnose: (code: string, message: string, context?: Record<string, unknown>) => void;
|
|
14
|
+
}
|
|
15
|
+
export declare class VariantSelector {
|
|
16
|
+
private readonly state;
|
|
17
|
+
private readonly deps;
|
|
18
|
+
constructor(state: AdPlayerState, deps: VariantSelectorDeps);
|
|
19
|
+
/**
|
|
20
|
+
* Returns why content may or may not keep playing through this break. The opt-in requires an
|
|
21
|
+
* explicit replacement single break and two independent media surfaces/decoders; optional
|
|
22
|
+
* quality control is best effort and independent of manifest timebase and stream type.
|
|
23
|
+
*/
|
|
24
|
+
continueContentDuringBreakReason(format: BreakFormat): 'ok' | 'disabled' | 'not-replacement' | 'format' | 'shared-element' | 'picture-in-picture' | 'single-decoder';
|
|
25
|
+
/** Device class plus the contextual playable-format set (PiP narrows to a single small surface). */
|
|
26
|
+
selectionInputs(): VariantSelectionInputs;
|
|
27
|
+
breakNothingToPlayReason(breakInfo: Break): NoPlayableAssetReason | null;
|
|
28
|
+
/**
|
|
29
|
+
* GAM pods are only playable through a live IMA session; without one a GAM-only break is
|
|
30
|
+
* treated as filtered out (fail-open) instead of erroring on its first asset. An
|
|
31
|
+
* initialization still in flight is awaited by buildGamUrl, so the break is kept.
|
|
32
|
+
*/
|
|
33
|
+
onlyGamAssetsWithoutSession(breakInfo: Break, insertionMode: EffectiveInsertionMode, selection: VariantSelectionInputs): boolean;
|
|
34
|
+
unknownVariantFormatsOf(breakInfo: Break): string[];
|
|
35
|
+
diagnoseUnknownVariantFormats(breakInfo: Break): void;
|
|
36
|
+
/** Manifest `targeting` + format-playability walk, with the last-resort downgrades behind it. */
|
|
37
|
+
getVariant(breakInfo: Break): BreakVariant | null;
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{BreakFormat as e,Vendor as t,isVendorAsset as s}from"../../types/BreakManifest";import{detectDeviceType as n}from"../detectDeviceType";import{assetsOfVariant as i,breakPlanEmptyReason as r,extractAssets as o}from"../AdBreakSequencer";import{playableFormatsFor as a,selectVariantDetailed as c}from"../selectVariant";export class VariantSelector{constructor(e,t){this.state=e,this.deps=t}continueContentDuringBreakReason(t){return this.state.continueContentDuringBreak?"replacement"!==this.state.adInsertionType?"not-replacement":t!==e.SINGLE?"format":"shared-element"===this.deps.getEffectiveInsertionMode()?"shared-element":this.deps.getIsContentPictureInPicture()?"picture-in-picture":"single-decoder"===this.deps.preloadMode||!1===this.deps.getSupportsParallelBuffering()?"single-decoder":"ok":"disabled"}selectionInputs(){return void 0===this.state.detectedDeviceType&&(this.state.detectedDeviceType=n()),{deviceType:this.state.detectedDeviceType,playableFormats:a({pictureInPicture:this.deps.getIsContentPictureInPicture(),singleSurfaceDevice:this.deps.singleSurfaceDevice})}}breakNothingToPlayReason(e){const t=this.deps.getEffectiveInsertionMode(),s=this.selectionInputs(),n=r(e,t,s);return null!==n?n:this.onlyGamAssetsWithoutSession(e,t,s)?"no-assets":null}onlyGamAssetsWithoutSession(e,n,i){if(this.deps.getIsGamReady()||null!==this.state.gamInitPromise)return!1;const r=o(e,i),a="shared-element"===n?r.slice(0,1):r;return a.length>0&&a.every(e=>s(e)&&e.vendor===t.GAM)}unknownVariantFormatsOf(e){return c(e,this.selectionInputs()).skippedUnknownFormats}diagnoseUnknownVariantFormats(e){for(const t of this.unknownVariantFormatsOf(e))this.deps.diagnose("DA-BREAK-VARIANT-FORMAT-UNKNOWN",`break ${e.id}: variant format ${""===t?"(absent)":`"${t}"`} is not recognised; the variant was skipped.`,{breakId:e.id,format:t})}getVariant(t){const{selected:s,deviceQualified:n}=c(t,this.selectionInputs());return s||(n?{format:e.SINGLE,assets:i(n).slice(0,1)}:null)}}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AdPlayerController } from '../AdPlayerController';
|
|
2
|
+
import { PauseAdController, type PauseAdDiagnoseCallback } from '../PauseAdController';
|
|
3
|
+
/**
|
|
4
|
+
* Binds the pause-ad trigger (fed by content pause/resume) to the render layer, which
|
|
5
|
+
* owns rendering and VAST resolution, and reports creative completion back to it.
|
|
6
|
+
*/
|
|
7
|
+
export declare function connectPauseAd(adPlayerController: AdPlayerController, options: {
|
|
8
|
+
debug: boolean;
|
|
9
|
+
diagnose: PauseAdDiagnoseCallback;
|
|
10
|
+
}): PauseAdController;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{PauseAdController as e}from"../PauseAdController";export function connectPauseAd(a,n){const o=a.pauseAdPresenter,s=new e({debug:n.debug});return s.setCallbacks({onShow:e=>{a.showPauseImage(e)},onHide:()=>a.hidePauseImage(),onResumeAllowed:()=>o.revealPauseAdResume(),diagnose:n.diagnose}),o.setPauseAdCreativeEndedCallback(e=>s.onCreativeEnded(e)),s}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { PlayerAdapter } from '../../interfaces/PlayerAdapter';
|
|
2
|
+
export declare const SEEK_END_SENTINEL_HORIZON_SEC: number;
|
|
3
|
+
export declare function liveEdgeDistanceSec(contentPlayer: PlayerAdapter): number | null;
|
|
4
|
+
export declare function isPlausibleSeekEnd(contentPlayer: PlayerAdapter, end: number): boolean;
|
|
5
|
+
export declare function contentSeekableEnd(contentPlayer: PlayerAdapter): number | null;
|
|
6
|
+
export declare function liveSafeSeekEnd(contentPlayer: PlayerAdapter): number | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const SEEK_END_SENTINEL_HORIZON_SEC=86400;export function liveEdgeDistanceSec(e){const n=e.videoElement,t=n?.seekable;if(!t||0===t.length)return null;const i=t.end(t.length-1);if(!Number.isFinite(i))return null;const l=i-e.currentTime;return l>=0?l:0}export function isPlausibleSeekEnd(e,n){return!!Number.isFinite(n)&&n<e.currentTime+86400}export function contentSeekableEnd(e){const n=e.seekableEnd;if("number"==typeof n&&isPlausibleSeekEnd(e,n))return n;const t=e.videoElement?.seekable;if(!t||0===t.length)return null;const i=t.end(t.length-1);return isPlausibleSeekEnd(e,i)?i:null}export function liveSafeSeekEnd(e){if(Number.isFinite(e.duration))return null;const n=[],t=e.maxLiveSeekPosition;"number"==typeof t&&Number.isFinite(t)&&n.push(t);const i=contentSeekableEnd(e);return null!==i&&n.push(i),n.length?Math.min(...n):null}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { AssetParameterMacroTable, AssetParameterMacros } from './assetParameterMacros';
|
|
2
|
+
export declare function buildAssetParameterMacroTable(input: {
|
|
3
|
+
customerMacros: AssetParameterMacros;
|
|
4
|
+
element: {
|
|
5
|
+
clientWidth: number;
|
|
6
|
+
clientHeight: number;
|
|
7
|
+
} | null;
|
|
8
|
+
breakDurationSec: number | undefined;
|
|
9
|
+
}): AssetParameterMacroTable;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{OPTIVIEW_ASSET_PARAMETER_MACROS as e,resolveAssetParameterMacro as r}from"./assetParameterMacros";export function buildAssetParameterMacroTable(t){const o={};for(const r of Object.values(e))o[r]=()=>{switch(r){case e.USER_AGENT:return"undefined"!=typeof navigator?navigator.userAgent:void 0;case e.PLAYER_WIDTH:return t.element?String(Math.round(t.element.clientWidth)):void 0;case e.PLAYER_HEIGHT:return t.element?String(Math.round(t.element.clientHeight)):void 0;case e.BREAK_DURATION_MS:return void 0===t.breakDurationSec?void 0:String(Math.round(1e3*t.breakDurationSec));case e.BREAK_DURATION_SECONDS:return void 0===t.breakDurationSec?void 0:String(t.breakDurationSec);default:return}};for(const e of Object.keys(t.customerMacros)){const n=o[e];o[e]=()=>r(t.customerMacros[e])??n?.()??void 0}return o}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Macro substitution for asset-parameter values and the query-string append
|
|
3
|
+
* that follows it, as pure functions shared (by behavioural contract) with the
|
|
4
|
+
* Kotlin and Swift cores and locked by the `asset-parameter-macros` conformance
|
|
5
|
+
* fixture.
|
|
6
|
+
*
|
|
7
|
+
* A macro is a full `$NAME$` token (`[A-Za-z0-9_]` name, both `$` included), so
|
|
8
|
+
* `"$OPTIVIEW_PLAYER_WIDTH$x$OPTIVIEW_PLAYER_HEIGHT$"` becomes `"1920x1080"` and
|
|
9
|
+
* `$APP$` never touches `$APP_NAME$`. A value is scanned left to right; a `$` that
|
|
10
|
+
* closes a token cannot open the next one. A `$` with no closing `$` (also `$$`
|
|
11
|
+
* and `${...}`) is left unchanged. Per token:
|
|
12
|
+
*
|
|
13
|
+
* 1. The macro resolves to a string → the token is replaced by it.
|
|
14
|
+
* 2. It does not resolve and the token is `$OPTIVIEW_*`, or a registered
|
|
15
|
+
* customer macro has no value → the token is kept literally and reported in
|
|
16
|
+
* `unresolved` (the caller diagnoses `DA-ASSET-MACRO-UNKNOWN`).
|
|
17
|
+
* 3. Any other unresolved token is kept literally, since it may be a vendor
|
|
18
|
+
* macro the ad server itself expands, and is not reported.
|
|
19
|
+
*
|
|
20
|
+
* Matching is case-sensitive. Who fills the macro table is platform glue: the
|
|
21
|
+
* runtime puts the customer's registered macros in front of the built-in
|
|
22
|
+
* `$OPTIVIEW_*` set, so a customer may override a built-in.
|
|
23
|
+
*/
|
|
24
|
+
export declare const OPTIVIEW_MACRO_PREFIX = "$OPTIVIEW_";
|
|
25
|
+
/** Built-in asset-parameter tokens for the user agent, player dimensions, and break duration. @category Manifest Customization */
|
|
26
|
+
export declare const OPTIVIEW_ASSET_PARAMETER_MACROS: {
|
|
27
|
+
readonly USER_AGENT: '$OPTIVIEW_USER_AGENT$';
|
|
28
|
+
readonly PLAYER_WIDTH: '$OPTIVIEW_PLAYER_WIDTH$';
|
|
29
|
+
readonly PLAYER_HEIGHT: '$OPTIVIEW_PLAYER_HEIGHT$';
|
|
30
|
+
readonly BREAK_DURATION_MS: '$OPTIVIEW_BREAK_DURATION_MS$';
|
|
31
|
+
readonly BREAK_DURATION_SECONDS: '$OPTIVIEW_BREAK_DURATION_SECONDS$';
|
|
32
|
+
};
|
|
33
|
+
export type AssetParameterMacroScope = 'session' | 'break';
|
|
34
|
+
export declare const OPTIVIEW_ASSET_PARAMETER_MACRO_SCOPES: Record<string, AssetParameterMacroScope>;
|
|
35
|
+
/** A macro's replacement text, or `undefined` when no value is available. @category Manifest Customization */
|
|
36
|
+
export type AssetParameterMacroValue = string | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* A customer macro: a fixed value, or a callback invoked with no arguments
|
|
39
|
+
* right before the URL is built. `undefined` means "no value" and falls
|
|
40
|
+
* through to the built-ins.
|
|
41
|
+
* @category Manifest Customization */
|
|
42
|
+
export type AssetParameterMacro = AssetParameterMacroValue | (() => AssetParameterMacroValue);
|
|
43
|
+
/** Customer-registered macros, keyed by the full token (`'$CUSTOM_MACRO$'`). @category Manifest Customization */
|
|
44
|
+
export type AssetParameterMacros = Record<string, AssetParameterMacro>;
|
|
45
|
+
/**
|
|
46
|
+
* Every macro that has a definition, keyed by token, each producing its value
|
|
47
|
+
* on demand (`undefined` = no value right now).
|
|
48
|
+
*/
|
|
49
|
+
export type AssetParameterMacroTable = Record<string, () => AssetParameterMacroValue>;
|
|
50
|
+
export declare function omitBreakScopedParameters(params: Record<string, string> | undefined): Record<string, string> | undefined;
|
|
51
|
+
export declare function resolveAssetParameterMacro(macro: AssetParameterMacro): AssetParameterMacroValue;
|
|
52
|
+
export interface UnresolvedAssetParameterMacro {
|
|
53
|
+
key: string;
|
|
54
|
+
macro: string;
|
|
55
|
+
}
|
|
56
|
+
export interface AppliedAssetParameters {
|
|
57
|
+
/** The substituted map, or `undefined` when the input was empty. */
|
|
58
|
+
params: Record<string, string> | undefined;
|
|
59
|
+
/** Unresolved macros, ordered by sorted parameter key and scan order. */
|
|
60
|
+
unresolved: UnresolvedAssetParameterMacro[];
|
|
61
|
+
}
|
|
62
|
+
export declare function applyAssetParameterMacros(params: Record<string, string> | undefined, macros: AssetParameterMacroTable): AppliedAssetParameters;
|
|
63
|
+
/**
|
|
64
|
+
* Append `params` to `url` as `key=value` query parameters, before any
|
|
65
|
+
* `#fragment`, reusing an existing `?`. Keys are sorted so the three cores
|
|
66
|
+
* produce the same bytes regardless of their map ordering, and both halves are
|
|
67
|
+
* `encodeURIComponent`-encoded. Returns `url` unchanged when `params` is empty.
|
|
68
|
+
*/
|
|
69
|
+
export declare function appendQueryParameters(url: string, params: Record<string, string> | undefined): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const OPTIVIEW_MACRO_PREFIX="$OPTIVIEW_";export const OPTIVIEW_ASSET_PARAMETER_MACROS={USER_AGENT:"$OPTIVIEW_USER_AGENT$",PLAYER_WIDTH:"$OPTIVIEW_PLAYER_WIDTH$",PLAYER_HEIGHT:"$OPTIVIEW_PLAYER_HEIGHT$",BREAK_DURATION_MS:"$OPTIVIEW_BREAK_DURATION_MS$",BREAK_DURATION_SECONDS:"$OPTIVIEW_BREAK_DURATION_SECONDS$"};export const OPTIVIEW_ASSET_PARAMETER_MACRO_SCOPES={[OPTIVIEW_ASSET_PARAMETER_MACROS.USER_AGENT]:"session",[OPTIVIEW_ASSET_PARAMETER_MACROS.PLAYER_WIDTH]:"session",[OPTIVIEW_ASSET_PARAMETER_MACROS.PLAYER_HEIGHT]:"session",[OPTIVIEW_ASSET_PARAMETER_MACROS.BREAK_DURATION_MS]:"break",[OPTIVIEW_ASSET_PARAMETER_MACROS.BREAK_DURATION_SECONDS]:"break"};export function omitBreakScopedParameters(e){if(!e)return;const t=Object.fromEntries(Object.entries(e).filter(([,e])=>!Object.entries(OPTIVIEW_ASSET_PARAMETER_MACRO_SCOPES).some(([t,n])=>"break"===n&&e.includes(t))));return Object.keys(t).length>0?t:void 0}export function resolveAssetParameterMacro(e){return"function"==typeof e?e():e}export function applyAssetParameterMacros(t,n){const E=t??{},o={},r=[];for(const t of Object.keys(E).sort()){const s=e(E[t],n);o[t]=s.value,r.push(...s.unresolved.map(e=>({key:t,macro:e})))}return{params:Object.keys(E).length>0?o:void 0,unresolved:r}}function e(e,t){let E="",o=0;const r=[],s=new Set;for(;o<e.length;){const A=e.indexOf("$",o);if(-1===A){E+=e.slice(o);break}E+=e.slice(o,A);const _=n(e,A);if(void 0===_){E+="$",o=A+1;continue}const R=t[_]?.();void 0!==R?E+=R:(E+=_,!_.startsWith("$OPTIVIEW_")&&!(_ in t)||s.has(_)||(r.push(_),s.add(_))),o=A+_.length}return{value:E,unresolved:r}}function t(e){return e>="A"&&e<="Z"||e>="a"&&e<="z"||e>="0"&&e<="9"||"_"===e}function n(e,n){let E=n+1;for(;E<e.length&&t(e[E]);)E++;return E>n+1&&"$"===e[E]?e.slice(n,E+1):void 0}export function appendQueryParameters(e,t){const n=t??{},E=Object.keys(n).sort();if(0===E.length)return e;const o=E.map(e=>`${encodeURIComponent(e)}=${encodeURIComponent(n[e])}`).join("&"),r=e.indexOf("#"),s=-1===r?e:e.slice(0,r),A=-1===r?"":e.slice(r),_=s.includes("?")?s.endsWith("?")||s.endsWith("&")?"":"&":"?";return`${s}${_}${o}${A}`}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { PlayerAdapter } from '../interfaces/PlayerAdapter';
|
|
2
|
+
import { Break, Timebase } from '../types/BreakManifest';
|
|
3
|
+
import type { MetadataCueTracker } from './MetadataCueTracker';
|
|
4
|
+
import type { Clock, SchedulerDiagnose } from './BreakScheduler';
|
|
5
|
+
/**
|
|
6
|
+
* Playing content that exposes no PDT is direct evidence about the stream, so waiting out the
|
|
7
|
+
* full grace window only delays scheduling (and can drop a tune-in below its minimum). The
|
|
8
|
+
* short settle absorbs the one-tick race before the adapter's first PDT sample.
|
|
9
|
+
*/
|
|
10
|
+
export declare const PDT_PLAYBACK_SETTLE_MS = 1000;
|
|
11
|
+
export interface BreakTimingState {
|
|
12
|
+
timebase: Timebase;
|
|
13
|
+
/** One-shot DA-PDT-MISSING guard for the per-tick fallback in {@link getCurrentTime}. */
|
|
14
|
+
pdtMissingWarned: boolean;
|
|
15
|
+
/** Once a stream has proven it carries PDT, the clock fallback is never substituted again. */
|
|
16
|
+
pdtSeen: boolean;
|
|
17
|
+
/** Wallclock (ms) of the first wallclock-timebase tick; the PDT grace window is measured from here. */
|
|
18
|
+
firstPdtAttemptMs: number | null;
|
|
19
|
+
/**
|
|
20
|
+
* Wallclock (ms) when content was first seen PLAYING with no PDT. After
|
|
21
|
+
* {@link PDT_PLAYBACK_SETTLE_MS} the stream has answered and the clock is used without waiting out the grace window.
|
|
22
|
+
*/
|
|
23
|
+
playbackStartedAtMs: number | null;
|
|
24
|
+
/** Break ids for which DA-ANVATO-CUE-PENDING has been emitted (one-shot). */
|
|
25
|
+
anvatoPendingDiagnosed: Set<string>;
|
|
26
|
+
/** Break ids for which DA-ANVATO-CUE-MATCHED has been emitted (one-shot). */
|
|
27
|
+
anvatoMatchedDiagnosed: Set<string>;
|
|
28
|
+
}
|
|
29
|
+
export declare function createBreakTimingState(): BreakTimingState;
|
|
30
|
+
export interface BreakTimingDeps {
|
|
31
|
+
player: PlayerAdapter;
|
|
32
|
+
clock: Clock;
|
|
33
|
+
cueTracker: MetadataCueTracker | null;
|
|
34
|
+
diagnose: SchedulerDiagnose | null;
|
|
35
|
+
log: (...args: unknown[]) => void;
|
|
36
|
+
pdtGraceSeconds: number;
|
|
37
|
+
pdtGraceMs: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Timebase position. Wallclock reads the stream's Program Date Time so breaks match the live
|
|
41
|
+
* playhead. Without a usable PDT this returns `null` and the tick decides nothing, until the
|
|
42
|
+
* stream has had the chance to answer: content PLAYING for {@link PDT_PLAYBACK_SETTLE_MS}
|
|
43
|
+
* with no PDT, or the `pdtGraceSeconds` backstop for a player that never starts. Then the
|
|
44
|
+
* injected clock is used (one-shot DA-PDT-MISSING).
|
|
45
|
+
*
|
|
46
|
+
* Substituting the clock earlier is wrong: a session starts before the player has a stream,
|
|
47
|
+
* and the clock runs AHEAD of the live playhead by the stream latency (~14s observed), so a
|
|
48
|
+
* break in that gap fired as a bogus tune-in and was then re-armed by the PDT-vs-clock step.
|
|
49
|
+
* Once PDT has been seen and goes missing, the tick is skipped until the next sample.
|
|
50
|
+
*/
|
|
51
|
+
export declare function getCurrentTime(state: BreakTimingState, deps: BreakTimingDeps): number | null;
|
|
52
|
+
/** One-shot DA-PDT-MISSING: no usable PDT while playing (`playbackStarted`) or for the whole grace window. */
|
|
53
|
+
export declare function warnPdtMissing(state: BreakTimingState, deps: BreakTimingDeps, playbackStarted: boolean): void;
|
|
54
|
+
export declare function getBreakStartTime(state: BreakTimingState, deps: BreakTimingDeps, breakInfo: Break): number | null;
|
|
55
|
+
/**
|
|
56
|
+
* Resolve a PTS break's start via the Anvato cue whose `pts` matches the manifest `start`.
|
|
57
|
+
* Null while no matching cue was observed; pending and first match are each diagnosed once.
|
|
58
|
+
*/
|
|
59
|
+
export declare function resolveAnvatoStartTime(state: BreakTimingState, deps: BreakTimingDeps, breakInfo: Break, startPts: number): number | null;
|
|
60
|
+
export declare function secondsUntilBreak(breakStartTime: number, currentTime: number): number;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Timebase as t}from"../types/BreakManifest";export const PDT_PLAYBACK_SETTLE_MS=1e3;export function createBreakTimingState(){return{timebase:t.WALLCLOCK,pdtMissingWarned:!1,pdtSeen:!1,firstPdtAttemptMs:null,playbackStartedAtMs:null,anvatoPendingDiagnosed:new Set,anvatoMatchedDiagnosed:new Set}}export function getCurrentTime(e,a){if(e.timebase!==t.WALLCLOCK)return 1e3*a.player.currentTime;const n=a.player.programDateTime;if(n){const t=n.getTime();if(!isNaN(t))return e.pdtSeen=!0,t}if(e.pdtSeen)return null;const r=a.clock.now();null===e.firstPdtAttemptMs&&(e.firstPdtAttemptMs=r),null===e.playbackStartedAtMs&&a.player.currentTime>0&&(e.playbackStartedAtMs=r);const s=null!==e.playbackStartedAtMs&&r-e.playbackStartedAtMs>=1e3,i=r-e.firstPdtAttemptMs>=a.pdtGraceMs;return s||i?(warnPdtMissing(e,a,s),r):null}export function warnPdtMissing(t,e,a){if(t.pdtMissingWarned)return;t.pdtMissingWarned=!0;const n=a?"content is playing and exposes none":`none appeared within ${e.pdtGraceSeconds}s`;e.log(`No Program Date Time on wallclock stream (${n}); falling back to the system clock`),e.diagnose?.("DA-PDT-MISSING",`No EXT-X-PROGRAM-DATE-TIME available (${n}); wallclock-timebase breaks are matched against the system clock instead of the stream position.`,{graceSeconds:e.pdtGraceSeconds,playbackStarted:a})}export function getBreakStartTime(e,a,n){if(e.timebase===t.WALLCLOCK){if("string"!=typeof n.start)return a.log("Invalid break start for wallclock timebase:",n.start),null;const t=new Date(n.start);return isNaN(t.getTime())?(a.log("Invalid break start date:",n.start),null):t.getTime()}return"number"!=typeof n.start?(a.log(`Invalid break start for ${e.timebase} timebase:`,n.start),null):a.cueTracker&&e.timebase===t.PTS?resolveAnvatoStartTime(e,a,n,n.start):1e3*n.start}export function resolveAnvatoStartTime(t,e,a,n){const r=e.cueTracker.mediaTimeForPts(n);return null===r?(t.anvatoPendingDiagnosed.has(a.id)||(t.anvatoPendingDiagnosed.add(a.id),e.log(`⌛ Break "${a.id}" awaiting Anvato cue for PTS ${n}`),e.diagnose?.("DA-ANVATO-CUE-PENDING",`No Anvato cue observed for break "${a.id}" (pts ${n}); break stays pending.`,{breakId:a.id,pts:n})),null):(t.anvatoMatchedDiagnosed.has(a.id)||(t.anvatoMatchedDiagnosed.add(a.id),e.log(`🎯 Break "${a.id}" matched Anvato cue: pts ${n} → ${r}s`),e.diagnose?.("DA-ANVATO-CUE-MATCHED",`Anvato cue matched for break "${a.id}": pts ${n} plays at media time ${r}s.`,{breakId:a.id,pts:n,mediaTime:r})),1e3*r)}export function secondsUntilBreak(t,e){const a=Math.round((t-e)/1e3);return a>0?a:0}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { BreakManifest } from '../types/BreakManifest';
|
|
2
|
+
import { DeviceType } from '../types/BreakManifest';
|
|
3
|
+
import type { AdChannelChangeReason } from '../types/Events';
|
|
4
|
+
import type { OptiViewAdsMode } from '../types/OptiViewAdsConfig';
|
|
5
|
+
export interface ChannelSwitch {
|
|
6
|
+
reason: AdChannelChangeReason;
|
|
7
|
+
/** `null` when the manifest carried no (or a blank) `channelId`. */
|
|
8
|
+
previousChannelId: string | null;
|
|
9
|
+
channelId: string | null;
|
|
10
|
+
}
|
|
11
|
+
export type DeliveryModeSource = 'manifest' | 'channel-switch' | 'config';
|
|
12
|
+
export interface DeliveryModeDecision {
|
|
13
|
+
/** Mode the manifest rules resolve to before the explicit config mode is applied. */
|
|
14
|
+
manifestMode: OptiViewAdsMode;
|
|
15
|
+
/** Effective mode. */
|
|
16
|
+
mode: OptiViewAdsMode;
|
|
17
|
+
ruleIndex: number | null;
|
|
18
|
+
unknownMode: string | null;
|
|
19
|
+
source: DeliveryModeSource;
|
|
20
|
+
}
|
|
21
|
+
export declare function decideDeliveryMode(manifest: BreakManifest, deviceType: DeviceType, explicitMode: OptiViewAdsMode | null, source: 'manifest' | 'channel-switch'): DeliveryModeDecision;
|
|
22
|
+
/** Blank ids count as absent: the backend omits the field, a proxy may emit `""`. */
|
|
23
|
+
export declare function normalizeChannelId(channelId: string | undefined): string | null;
|
|
24
|
+
/** True when the new manifest needs a new IMA session (different network or custom asset key). */
|
|
25
|
+
export declare function gamPodIdentityChanged(previous: BreakManifest, next: BreakManifest): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Compare the manifest the session was configured from with a freshly polled one. Any change
|
|
28
|
+
* of channel identity, or of the session-level setup derived from the manifest (GAM stream
|
|
29
|
+
* identity, timebase) while the id is unchanged, needs the ad setup to be rebuilt. Break-list
|
|
30
|
+
* and `delivery` changes alone are ordinary updates and return `null`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function detectChannelSwitch(previous: BreakManifest, next: BreakManifest): ChannelSwitch | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{resolveGamVendorConfiguration as e}from"./resolveGamVendorConfiguration";import{resolveDeliveryMode as n,resolveStitchedStreamVendor as o}from"./resolveDeliveryMode";export function decideDeliveryMode(e,o,r,t){const d=n(e.delivery,o);return{manifestMode:d.mode,mode:r??d.mode,ruleIndex:d.ruleIndex,unknownMode:d.unknownMode,source:r?"config":t}}export function normalizeChannelId(e){return"string"==typeof e&&""!==e.trim()?e:null}function r(n){const o=e(n).identity;return o?`${o.networkCode}|${o.customAssetKey}`:""}export function gamPodIdentityChanged(e,n){return r(e)!==r(n)}function t(e){const n=o(e.vendorConfiguration);return n?`${n.networkCode}|${n.assetKey}`:""}export function detectChannelSwitch(e,n){const o=normalizeChannelId(e.channelId),r=normalizeChannelId(n.channelId),d={previousChannelId:o,channelId:r};return o!==r?null===o?{...d,reason:"channel-id-added"}:null===r?{...d,reason:"channel-id-removed"}:{...d,reason:"channel-id-changed"}:gamPodIdentityChanged(e,n)||t(e)!==t(n)?{...d,reason:"vendor-configuration-changed"}:e.timebase!==n.timebase?{...d,reason:"timebase-changed"}:null}
|