@dolby-optiview/ads-sdk-core 1.2.1 → 1.3.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/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 +45 -5
- 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 +6 -4
- 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 +8 -61
- 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 +2 -0
- package/dist/services/adPlayer/AdPlayerEvents.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 +6 -2
- package/dist/services/adPlayer/AssetPlayback.js +1 -1
- package/dist/services/adPlayer/BreakSequenceDriver.d.ts +9 -3
- package/dist/services/adPlayer/BreakSequenceDriver.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/SuccessorQueue.d.ts +36 -0
- package/dist/services/adPlayer/SuccessorQueue.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 +34 -2
- package/dist/types/Events.d.ts +20 -10
- package/dist/types/OptiViewAdsConfig.d.ts +24 -8
- package/package.json +5 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
export class AdBreakStatusTracker{constructor(t){this.deps=t,this.adBreakStatus={phase:"idle",break:null,ticking:!1},this.activeBreak=null,this.activeBreakRemainingAtStart=null,this.activeBreakStartTimestamp=null,this.activeBreakElapsedSec=0,this.currentAdStartElapsedSec=0,this.currentAdAssetId=null,this.timelineAssetId=null,this.timelineStartElapsedSec=0,this.currentAdIndex=null,this.currentAdTotal=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakWaiting=!1,this.activeBreakPlaying=!1}getAdBreakStatus(){if("active"===this.adBreakStatus.phase)return{...this.adBreakStatus,breakRemainingSec:this.computeBreakRemainingSec()};if("upcoming"===this.adBreakStatus.phase&&this.activeBreak){const t=this.deps.secondsUntilBreak(this.activeBreak.id);return{...this.adBreakStatus,secondsUntilBreak:t}}return{...this.adBreakStatus}}onBreakWarning(t,e,i){this.activeBreak=t,this.emitAdBreakStatus({phase:"upcoming",break:t,format:this.deps.effectiveFormatOf(t),secondsUntilBreak:i,ticking:!1,warningSec:e})}setAdBreakStatusActive(t,e){this.activeBreak=t,this.activeBreakRemainingAtStart=e,this.activeBreakStartTimestamp=Date.now(),this.activeBreakElapsedSec=0,this.currentAdStartElapsedSec=0,this.currentAdAssetId=null,this.timelineAssetId=null,this.timelineStartElapsedSec=0,this.currentAdIndex=null,this.currentAdTotal=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakWaiting=!1,this.activeBreakPlaying=!1,this.emitAdBreakStatus({phase:"active",break:t,format:this.deps.effectiveFormatOf(t),breakRemainingSec:this.activeBreakRemainingAtStart,ticking:!1})}setAdBreakVisible(t){this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),ticking:!0})}onAdStarted(t,e,i,a){this.currentAdIndex=i,this.currentAdAssetId=e.id,this.currentAdTotal=a,this.currentAdTime=null,this.currentAdDuration=null,this.currentAdStartElapsedSec=this.currentBreakElapsedSec(),this.activeBreakElapsedSec=this.currentAdStartElapsedSec,this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.deps.breakTransitionTimer()?.settle("into-break","ad-to-ad","break-to-break"),this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),adIndex:i,totalAds:a,adsRemaining:a-i,ticking:!0})}onAdEnded(t,e,i){this.timelineAssetId===this.currentAdAssetId&&null!==this.currentAdDuration&&Number.isFinite(this.currentAdDuration)?this.activeBreakElapsedSec=this.timelineStartElapsedSec+this.currentAdDuration:null!==this.currentAdTime&&(this.activeBreakElapsedSec=this.timelineStartElapsedSec+this.currentAdTime),this.currentAdIndex=null,this.currentAdAssetId=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakPlaying=!1,this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),adsRemaining:Math.max(0,i-e-1),breakRemainingSec:this.computeBreakRemainingSec(),ticking:!1})}onAdTimeUpdated(t,e,i,a){e.id!==this.timelineAssetId&&(this.timelineAssetId=e.id,this.timelineStartElapsedSec=null===this.currentAdIndex?this.currentBreakElapsedSec():this.currentAdStartElapsedSec),this.currentAdTime=i,this.currentAdDuration=a,this.activeBreakElapsedSec=this.timelineStartElapsedSec+i,this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),breakRemainingSec:this.computeBreakRemainingSec(),ticking:!0})}onAdPlaybackWaiting(){this.activeBreakWaiting=!0,this.activeBreakPlaying=!1,this.activeBreak&&this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(this.activeBreak),ticking:!1})}onAdPlaybackPlaying(){this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.activeBreak&&this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(this.activeBreak),ticking:!0})}buildActiveAdBreakStatus(t){return{phase:"active",break:t,format:this.deps.effectiveFormatOf(t),adIndex:this.currentAdIndex??void 0,totalAds:this.currentAdTotal??void 0,adsRemaining:null!==this.currentAdIndex&&null!==this.currentAdTotal?this.currentAdTotal-this.currentAdIndex:void 0,breakRemainingSec:this.computeBreakRemainingSec(),ticking:this.activeBreakPlaying&&!this.activeBreakWaiting}}computeBreakRemainingSec(){if(null!==this.activeBreakRemainingAtStart)return Math.max(0,this.activeBreakRemainingAtStart-this.currentBreakElapsedSec())}currentBreakElapsedSec(){let t=this.activeBreakElapsedSec;return 0===t&&null!==this.activeBreakStartTimestamp&&null===this.currentAdTime&&(t=(Date.now()-this.activeBreakStartTimestamp)/1e3),t}resetAdBreakStatus(){this.activeBreak=null,this.activeBreakRemainingAtStart=null,this.activeBreakStartTimestamp=null,this.activeBreakElapsedSec=0,this.currentAdStartElapsedSec=0,this.currentAdAssetId=null,this.timelineAssetId=null,this.timelineStartElapsedSec=0,this.currentAdIndex=null,this.currentAdTotal=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakWaiting=!1,this.activeBreakPlaying=!1,this.emitAdBreakStatus({phase:"idle",break:null,ticking:!1})}emitAdBreakStatus(t){this.statusesEqual(this.adBreakStatus,t)
|
|
1
|
+
import{resolveStatusExternalPlaybackAllowed as t}from"../services/ssbSwitchSignals";export class AdBreakStatusTracker{constructor(t){this.deps=t,this.adBreakStatus={phase:"idle",break:null,ticking:!1},this.activeBreak=null,this.activeBreakRemainingAtStart=null,this.activeBreakStartTimestamp=null,this.activeBreakElapsedSec=0,this.currentAdStartElapsedSec=0,this.currentAdAssetId=null,this.timelineAssetId=null,this.timelineStartElapsedSec=0,this.currentAdIndex=null,this.currentAdTotal=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakWaiting=!1,this.activeBreakPlaying=!1}getAdBreakStatus(){return this.withExternalPlayback(this.liveAdBreakStatus())}refreshExternalPlayback(){const e=t(this.deps.externalPlaybackAllowed(),this.adBreakStatus.phase,this.deps.deliveryMode());if(void 0===e||e===(this.lastExternalPlaybackAllowed??!0))return;const i=this.getAdBreakStatus();this.lastExternalPlaybackAllowed=i.externalPlaybackAllowed,this.deps.emit("adbreakstatus",{type:"adbreakstatus",timestamp:Date.now(),status:i})}refreshDeliveryMode(){const t=this.deps.deliveryMode();if(void 0===t||t===(this.lastDeliveryMode??"sgai"))return;const e=this.getAdBreakStatus();this.lastDeliveryMode=e.deliveryMode,this.deps.emit("adbreakstatus",{type:"adbreakstatus",timestamp:Date.now(),status:e})}liveAdBreakStatus(){if("active"===this.adBreakStatus.phase)return{...this.adBreakStatus,breakRemainingSec:this.computeBreakRemainingSec()};if("upcoming"===this.adBreakStatus.phase&&this.activeBreak){const t=this.deps.secondsUntilBreak(this.activeBreak.id);return{...this.adBreakStatus,secondsUntilBreak:t}}return{...this.adBreakStatus}}withExternalPlayback(e){const i=this.deps.deliveryMode(),a=void 0===i?e:{...e,deliveryMode:i},s=t(this.deps.externalPlaybackAllowed(),a.phase,i);return void 0===s?a:{...a,externalPlaybackAllowed:s}}onBreakWarning(t,e,i){this.activeBreak=t,this.emitAdBreakStatus({phase:"upcoming",break:t,format:this.deps.effectiveFormatOf(t),secondsUntilBreak:i,ticking:!1,warningSec:e})}setAdBreakStatusActive(t,e){this.activeBreak=t,this.activeBreakRemainingAtStart=e,this.activeBreakStartTimestamp=Date.now(),this.activeBreakElapsedSec=0,this.currentAdStartElapsedSec=0,this.currentAdAssetId=null,this.timelineAssetId=null,this.timelineStartElapsedSec=0,this.currentAdIndex=null,this.currentAdTotal=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakWaiting=!1,this.activeBreakPlaying=!1,this.emitAdBreakStatus({phase:"active",break:t,format:this.deps.effectiveFormatOf(t),breakRemainingSec:this.activeBreakRemainingAtStart,ticking:!1})}setAdBreakVisible(t){this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),ticking:!0})}onAdStarted(t,e,i,a){this.currentAdIndex=i,this.currentAdAssetId=e.id,this.currentAdTotal=a,this.currentAdTime=null,this.currentAdDuration=null,this.currentAdStartElapsedSec=this.currentBreakElapsedSec(),this.activeBreakElapsedSec=this.currentAdStartElapsedSec,this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.deps.breakTransitionTimer()?.settle("into-break","ad-to-ad","break-to-break"),this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),adIndex:i,totalAds:a,adsRemaining:a-i,ticking:!0})}onAdEnded(t,e,i){this.timelineAssetId===this.currentAdAssetId&&null!==this.currentAdDuration&&Number.isFinite(this.currentAdDuration)?this.activeBreakElapsedSec=this.timelineStartElapsedSec+this.currentAdDuration:null!==this.currentAdTime&&(this.activeBreakElapsedSec=this.timelineStartElapsedSec+this.currentAdTime),this.currentAdIndex=null,this.currentAdAssetId=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakPlaying=!1,this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),adsRemaining:Math.max(0,i-e-1),breakRemainingSec:this.computeBreakRemainingSec(),ticking:!1})}onAdTimeUpdated(t,e,i,a){e.id!==this.timelineAssetId&&(this.timelineAssetId=e.id,this.timelineStartElapsedSec=null===this.currentAdIndex?this.currentBreakElapsedSec():this.currentAdStartElapsedSec),this.currentAdTime=i,this.currentAdDuration=a,this.activeBreakElapsedSec=this.timelineStartElapsedSec+i,this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(t),breakRemainingSec:this.computeBreakRemainingSec(),ticking:!0})}onAdPlaybackWaiting(){this.activeBreakWaiting=!0,this.activeBreakPlaying=!1,this.activeBreak&&this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(this.activeBreak),ticking:!1})}onAdPlaybackPlaying(){this.activeBreakWaiting=!1,this.activeBreakPlaying=!0,this.activeBreak&&this.emitAdBreakStatus({...this.buildActiveAdBreakStatus(this.activeBreak),ticking:!0})}buildActiveAdBreakStatus(t){return{phase:"active",break:t,format:this.deps.effectiveFormatOf(t),adIndex:this.currentAdIndex??void 0,totalAds:this.currentAdTotal??void 0,adsRemaining:null!==this.currentAdIndex&&null!==this.currentAdTotal?this.currentAdTotal-this.currentAdIndex:void 0,breakRemainingSec:this.computeBreakRemainingSec(),ticking:this.activeBreakPlaying&&!this.activeBreakWaiting}}computeBreakRemainingSec(){if(null!==this.activeBreakRemainingAtStart)return Math.max(0,this.activeBreakRemainingAtStart-this.currentBreakElapsedSec())}currentBreakElapsedSec(){let t=this.activeBreakElapsedSec;return 0===t&&null!==this.activeBreakStartTimestamp&&null===this.currentAdTime&&(t=(Date.now()-this.activeBreakStartTimestamp)/1e3),t}resetAdBreakStatus(){this.activeBreak=null,this.activeBreakRemainingAtStart=null,this.activeBreakStartTimestamp=null,this.activeBreakElapsedSec=0,this.currentAdStartElapsedSec=0,this.currentAdAssetId=null,this.timelineAssetId=null,this.timelineStartElapsedSec=0,this.currentAdIndex=null,this.currentAdTotal=null,this.currentAdTime=null,this.currentAdDuration=null,this.activeBreakWaiting=!1,this.activeBreakPlaying=!1,this.lastExternalPlaybackAllowed=void 0,this.emitAdBreakStatus({phase:"idle",break:null,ticking:!1})}emitAdBreakStatus(t){if(this.statusesEqual(this.adBreakStatus,t))return;this.adBreakStatus=t;const e=this.withExternalPlayback(t);void 0!==e.externalPlaybackAllowed&&(this.lastExternalPlaybackAllowed=e.externalPlaybackAllowed),this.lastDeliveryMode=e.deliveryMode,this.deps.emit("adbreakstatus",{type:"adbreakstatus",timestamp:Date.now(),status:e})}statusesEqual(t,e){return t.phase===e.phase&&t.format===e.format&&t.secondsUntilBreak===e.secondsUntilBreak&&t.breakRemainingSec===e.breakRemainingSec&&t.adIndex===e.adIndex&&t.totalAds===e.totalAds&&t.adsRemaining===e.adsRemaining&&t.ticking===e.ticking&&t.warningSec===e.warningSec&&t.break?.id===e.break?.id}}
|
|
@@ -4,6 +4,7 @@ import { BreakScheduler, BreakStartContext } from '../services/BreakScheduler';
|
|
|
4
4
|
import { ManifestService } from '../services/ManifestService';
|
|
5
5
|
import { AdPlayerController } from '../services/AdPlayerController';
|
|
6
6
|
import { BreakTransitionTimer } from '../services/BreakTransitionTimer';
|
|
7
|
+
import type { BreakChainRecorder } from '../services/BreakChainRecorder';
|
|
7
8
|
import { CmcdAdReporter } from '../services/CmcdAdReporter';
|
|
8
9
|
import { KnownDiagnosticCode } from '../diagnostics/errorCodes';
|
|
9
10
|
import { AdBreakStatusTracker, EmitEvent } from './AdBreakStatusTracker';
|
|
@@ -15,6 +16,7 @@ export interface BreakLifecycleDeps {
|
|
|
15
16
|
breakScheduler: BreakScheduler;
|
|
16
17
|
manifestService: ManifestService;
|
|
17
18
|
breakTransitionTimer: BreakTransitionTimer | undefined;
|
|
19
|
+
breakChainRecorder?: BreakChainRecorder | undefined;
|
|
18
20
|
cmcdAdReporter: CmcdAdReporter;
|
|
19
21
|
adBreakStatusTracker: AdBreakStatusTracker;
|
|
20
22
|
emit: EmitEvent;
|
|
@@ -22,6 +24,8 @@ export interface BreakLifecycleDeps {
|
|
|
22
24
|
log: Log;
|
|
23
25
|
effectiveFormatOf: (breakInfo: Break) => BreakFormat | undefined;
|
|
24
26
|
onBreakSettled?: () => void;
|
|
27
|
+
/** A break ended and `adbreakend` was emitted. */
|
|
28
|
+
onBreakEnded?: () => void;
|
|
25
29
|
}
|
|
26
30
|
/** Scheduler and ad-player callbacks for one SGAI break: event emission, status updates, teardown reporting. */
|
|
27
31
|
export declare class BreakLifecycle {
|
|
@@ -30,6 +34,8 @@ export declare class BreakLifecycle {
|
|
|
30
34
|
private breakBeginEmittedId;
|
|
31
35
|
private pendingAdBreakBegin;
|
|
32
36
|
constructor(state: SessionState, deps: BreakLifecycleDeps);
|
|
37
|
+
/** Feed the chain report: the break began, with the successor the scheduler knows right now. */
|
|
38
|
+
private recordBreakBegan;
|
|
33
39
|
/**
|
|
34
40
|
* Emit `adbreakbegin` at most once per break id. Routing every begin through here, plus a flush
|
|
35
41
|
* before any terminal event, keeps the break bracketed even when the ad errors before its first frame.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BreakFormat as e}from"../types/BreakManifest";import{playbackErrorContext as t}from"../diagnostics/playbackErrorContext";import{adErrorCode as a}from"../types/AdError";import{VendorAdError as i}from"../types/VendorAdError";export class BreakLifecycle{constructor(e,t){this.state=e,this.deps=t,this.breakBeginEmittedId=null,this.pendingAdBreakBegin=null}emitAdBreakBegin(e,t){this.breakBeginEmittedId!==e.id&&(this.breakBeginEmittedId=e.id,this.pendingAdBreakBegin=null,this.deps.emit("adbreakbegin",{type:"adbreakbegin",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),elapsedSec:t?.elapsedSec??0,remainingSec:t?.remainingSec??e.duration??0,...t?.tuneIn?{tuneIn:{elapsedSec:t.elapsedSec,remainingSec:t.remainingSec}}:{}}))}flushPendingAdBreakBegin(){this.pendingAdBreakBegin?.()}onBreakSkipped(e,t){this.deps.emit("adbreakskipped",{type:"adbreakskipped",timestamp:Date.now(),break:e,reason:t,format:this.deps.effectiveFormatOf(e)})}async onBreakStart(r,
|
|
1
|
+
import{BreakFormat as e}from"../types/BreakManifest";import{playbackErrorContext as t}from"../diagnostics/playbackErrorContext";import{adErrorCode as a}from"../types/AdError";import{VendorAdError as i}from"../types/VendorAdError";export class BreakLifecycle{constructor(e,t){this.state=e,this.deps=t,this.breakBeginEmittedId=null,this.pendingAdBreakBegin=null,t.breakScheduler.setOnChainSuccessorChanged((e,a)=>{t.breakChainRecorder?.successorChanged(a?.id??null),t.adPlayerController.onChainSuccessorChanged()})}recordBreakBegan(e,t){const a=this.deps.breakScheduler.getChainedSuccessor(e.id);this.deps.breakChainRecorder?.breakBegan(e,t,a?.id??null)}emitAdBreakBegin(e,t){this.breakBeginEmittedId!==e.id&&(this.breakBeginEmittedId=e.id,this.pendingAdBreakBegin=null,this.deps.emit("adbreakbegin",{type:"adbreakbegin",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),elapsedSec:t?.elapsedSec??0,remainingSec:t?.remainingSec??e.duration??0,...t?.tuneIn?{tuneIn:{elapsedSec:t.elapsedSec,remainingSec:t.remainingSec}}:{}}))}flushPendingAdBreakBegin(){this.pendingAdBreakBegin?.()}onBreakSkipped(e,t){this.deps.emit("adbreakskipped",{type:"adbreakskipped",timestamp:Date.now(),break:e,reason:t,format:this.deps.effectiveFormatOf(e)})}async onBreakStart(r,s){const d=this.state.sessionGeneration;if(this.state.snapbackSeekInFlight=!1,this.deps.adPlayerController.isAdPlaying()){if(this.deps.diagnose("DA-BREAK-SUPPRESSED-OVERLAP",`Break suppressed (ad already playing): ${r.id}`,{breakId:r.id}),d!==this.state.sessionGeneration)return;return this.deps.breakScheduler.completeBreak(r.id),this.onBreakSkipped(r,"overlap"),void this.deps.onBreakSettled?.()}this.deps.adPlayerController.diagnoseUnknownVariantFormats(r);const n=this.deps.adPlayerController.breakNothingToPlayReason(r);if(n){if(this.deps.log("Break not begun (no playable variant):",r.id,n),this.deps.diagnose("DA-BREAK-NO-PLAYABLE-ASSET",`break ${r.id} has no playable variant (${n}); it will not begin.`,{breakId:r.id,reason:n}),this.deps.emit("aderror",{type:"aderror",timestamp:Date.now(),source:"ad",break:r,format:this.deps.effectiveFormatOf(r),error:new Error(`Break ${r.id} has no playable variant (${n})`)}),d!==this.state.sessionGeneration)return;return this.deps.breakScheduler.completeBreak(r.id),this.onBreakSkipped(r,"unsupported"),void this.deps.onBreakSettled?.()}this.deps.log("Break starting:",r.id,s.tuneIn?"(tune-in)":""),this.deps.breakTransitionTimer?.mark("into-break",r.id),this.breakBeginEmittedId=null,this.pendingAdBreakBegin=null;const o=this.state.heldImmediatePreRollId===r.id,p=this.deps.effectiveFormatOf(r),k=p===e.SINGLE||p===e.LSHAPE_AD,h=o&&k;o&&(this.state.heldImmediatePreRollId=null),this.deps.adBreakStatusTracker.setAdBreakStatusActive(r,void 0!==s.remainingSec?s.remainingSec:r.duration),h||this.emitAdBreakBegin(r,s),this.deps.manifestService.setActive(!0),this.deps.breakScheduler.claimChain(r.id),this.recordBreakBegan(r,!1);const l={onAdStart:(e,t,a,i,r)=>{this.deps.adBreakStatusTracker.onAdStarted(e,t,a,i),this.deps.emit("adbegin",{type:"adbegin",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,adIndex:a,totalAds:i,...r?.adId?{adId:r.adId}:{},...r?.creativeId?{creativeId:r.creativeId}:{},...void 0!==r?.podPosition?{podPosition:r.podPosition}:{}})},onAdEnd:(e,t,a,i,r)=>{this.deps.adBreakStatusTracker.onAdEnded(e,a,i),a+1<i?this.deps.breakTransitionTimer?.mark("ad-to-ad",e.id):this.deps.breakTransitionTimer?.noteOutgoingEnd(),this.deps.emit("adend",{type:"adend",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,adIndex:a,totalAds:i,...r?.adId?{adId:r.adId}:{},...r?.creativeId?{creativeId:r.creativeId}:{}})},onAdError:(e,r,s)=>{this.flushPendingAdBreakBegin(),this.deps.diagnose("DA-AD-PLAYBACK-ERROR",s.message,{breakId:e.id,assetId:r?.id,adErrorCode:a(s),...s instanceof i?{vendorCode:s.vendorCode}:{},...t(s)}),this.deps.emit("aderror",{type:"aderror",timestamp:Date.now(),source:"ad",break:e,format:this.deps.effectiveFormatOf(e),...r?{asset:r}:{},error:s})},onAdTimeUpdate:(e,t,a,i)=>{this.deps.adBreakStatusTracker.onAdTimeUpdated(e,t,a,i),this.deps.emit("adtimeupdate",{type:"adtimeupdate",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,currentTime:a,duration:i})},onAdWaiting:(e,t)=>{this.deps.adBreakStatusTracker.onAdPlaybackWaiting(),this.deps.emit("waiting",{type:"waiting",timestamp:Date.now(),source:"ad",break:e,asset:t})},onAdPlaying:(e,t)=>{this.deps.adBreakStatusTracker.onAdPlaybackPlaying(),this.deps.emit("playing",{type:"playing",timestamp:Date.now(),source:"ad",break:e,asset:t})},onBreakEnd:e=>{this.onBreakEnd(e)},onBreakCut:e=>{this.deps.cmcdAdReporter.onBreakCut(e.id)},onBreakEndedWithoutContentPause:()=>{this.onBreakEndedWithoutContentPause()},onChainedBreakBegin:e=>{this.onChainedBreakBegin(e)},onSuccessorBuffered:(e,t)=>this.deps.breakTransitionTimer?.noteSuccessorBuffered(e,t),onSuccessorDropped:e=>this.deps.breakTransitionTimer?.noteSuccessorDropped(e),onChainedBreakSkipped:e=>{d===this.state.sessionGeneration&&(this.deps.log("Chained break skipped (no playable variant):",e.id),this.deps.breakScheduler.completeBreak(e.id),this.onBreakSkipped(e,"unsupported"))},onContentResumeSeek:(e,t)=>{this.deps.breakScheduler.notifyResumeSeek(e,t)},onContentReloading:e=>{this.deps.breakScheduler.setContentReloading(e)}};h&&(this.pendingAdBreakBegin=()=>this.emitAdBreakBegin(r,s),l.onBreakVisible=e=>{this.deps.adBreakStatusTracker.setAdBreakVisible(e),this.flushPendingAdBreakBegin()}),await this.deps.adPlayerController.playBreak(r,l,{effectiveDurationSec:s.remainingSec,firstFrameGate:h})}onChainedBreakBegin(e){this.deps.log("Chained break starting:",e.id),this.deps.breakTransitionTimer?.mark("break-to-break",e.id),this.deps.breakScheduler.activateChainedBreak(e.id),this.recordBreakBegan(e,!0),this.deps.manifestService.setActive(!0),this.deps.adBreakStatusTracker.setAdBreakStatusActive(e,e.duration),this.emitAdBreakBegin(e)}onBreakEnd(e){this.deps.log("Break ended:",e.id),this.deps.breakTransitionTimer?.mark("out-of-break",e.id),this.deps.breakChainRecorder?.breakEnded(e.id),this.deps.breakScheduler.completeBreak(e.id),this.deps.manifestService.setActive(!1),this.deps.adBreakStatusTracker.resetAdBreakStatus(),this.flushPendingAdBreakBegin(),this.deps.emit("adbreakend",{type:"adbreakend",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e)}),this.deps.onBreakEnded?.(),this.deps.onBreakSettled?.()}onBreakEndedWithoutContentPause(){this.deps.breakTransitionTimer?.settle("out-of-break"),this.deps.emit("playing",{type:"playing",timestamp:Date.now(),source:"content"})}onBreakAborted(e){if(this.deps.adPlayerController.isAdPlaying()){const t=this.deps.adBreakStatusTracker.currentBreakElapsedSec();this.deps.log(`Early return — ending break '${e.id}' at ${t.toFixed(2)}s`),this.deps.cmcdAdReporter.onBreakAborted(e.id),this.deps.adPlayerController.abortBreak(t)}else this.deps.breakScheduler.completeBreak(e.id),this.deps.onBreakSettled?.()}reportAbandonedBreak(e){const t=this.deps.adBreakStatusTracker.activeBreak;if(!t)return;const a=this.deps.adBreakStatusTracker.currentAdIndex;this.deps.diagnose("DA-BREAK-ABANDONED",`Session torn down (${e}) while break ${t.id} was still in flight`+(null!==a?"; an ad began and will never end.":"."),{breakId:t.id,cause:e,phase:this.deps.adBreakStatusTracker.adBreakStatus.phase,unbalancedAdBegin:null!==a,...null!==a?{adIndex:a}:{},...null!==this.deps.adBreakStatusTracker.currentAdTotal?{totalAds:this.deps.adBreakStatusTracker.currentAdTotal}:{}})}}
|
|
@@ -1,42 +1,7 @@
|
|
|
1
|
-
import { PlayerAdapter } from '../interfaces/PlayerAdapter';
|
|
2
1
|
import { BreakManifest } from '../types/BreakManifest';
|
|
3
|
-
import { ExternalVendorConfigurationTracker } from '../services/ExternalVendorConfigurationTracker';
|
|
4
2
|
import { OptiViewAdsMode, SessionConfig, SsaiSessionConfig, StartSessionConfig } from '../types/OptiViewAdsConfig';
|
|
5
|
-
import { ManifestService } from '../services/ManifestService';
|
|
6
|
-
import { BreakScheduler } from '../services/BreakScheduler';
|
|
7
|
-
import { AdPlayerController } from '../services/AdPlayerController';
|
|
8
|
-
import { PauseAdController } from '../services/PauseAdController';
|
|
9
|
-
import { LensReporter } from '../services/LensReporter';
|
|
10
|
-
import { CmcdAdReporter } from '../services/CmcdAdReporter';
|
|
11
|
-
import { AdBreakStatusTracker, EmitEvent } from './AdBreakStatusTracker';
|
|
12
|
-
import { BreakLifecycle, Diagnose, Log } from './BreakLifecycle';
|
|
13
3
|
import { SessionState } from './SessionState';
|
|
14
|
-
|
|
15
|
-
player: PlayerAdapter;
|
|
16
|
-
debug: boolean;
|
|
17
|
-
mode: OptiViewAdsMode;
|
|
18
|
-
hasExplicitMode: boolean;
|
|
19
|
-
continueContentDuringBreak: boolean;
|
|
20
|
-
startLivePushback: () => void;
|
|
21
|
-
manifestService: ManifestService | undefined;
|
|
22
|
-
breakScheduler: BreakScheduler | undefined;
|
|
23
|
-
adPlayerController: AdPlayerController | undefined;
|
|
24
|
-
pauseAdController: PauseAdController | undefined;
|
|
25
|
-
breakLifecycle: BreakLifecycle | undefined;
|
|
26
|
-
cmcdAdReporter: CmcdAdReporter;
|
|
27
|
-
lensReporter: LensReporter;
|
|
28
|
-
adBreakStatusTracker: AdBreakStatusTracker;
|
|
29
|
-
externalVendorConfigurationTracker: ExternalVendorConfigurationTracker;
|
|
30
|
-
emit: EmitEvent;
|
|
31
|
-
diagnose: Diagnose;
|
|
32
|
-
log: Log;
|
|
33
|
-
endSessionInternal: (cause: string) => void;
|
|
34
|
-
notedManifestBreakIds: (manifest: {
|
|
35
|
-
breaks: Array<{
|
|
36
|
-
id: string;
|
|
37
|
-
}>;
|
|
38
|
-
}) => string[];
|
|
39
|
-
}
|
|
4
|
+
import type { SessionStarterDeps } from './SessionStarterDeps';
|
|
40
5
|
/** startSession() flow: validation, delivery-mode steering, GAM/SSAI start, scheduler and polling start. */
|
|
41
6
|
export declare class SessionStarter {
|
|
42
7
|
private readonly state;
|
|
@@ -47,6 +12,7 @@ export declare class SessionStarter {
|
|
|
47
12
|
* GAM session initialization. Ends any previously active session first.
|
|
48
13
|
*/
|
|
49
14
|
startSession(config: StartSessionConfig): Promise<void>;
|
|
15
|
+
discardPreparedGamSessionAfterSeek(): void;
|
|
50
16
|
/**
|
|
51
17
|
* Content already playing when the session starts emits no further `playing`; the pre-roll
|
|
52
18
|
* anchor (spec `event: 'start'`) is then the session start itself.
|
|
@@ -72,7 +38,7 @@ export declare class SessionStarter {
|
|
|
72
38
|
* rules are evaluated against the device type; an explicit constructor `mode` wins.
|
|
73
39
|
* Absent/unmatched rules and unrecognized values resolve to sgai.
|
|
74
40
|
*/
|
|
75
|
-
resolveSessionDeliveryMode(manifest: BreakManifest, source?: 'manifest' | 'channel-switch'): OptiViewAdsMode;
|
|
41
|
+
resolveSessionDeliveryMode(manifest: BreakManifest, source?: 'manifest' | 'channel-switch' | 'event-boundary'): OptiViewAdsMode;
|
|
76
42
|
/**
|
|
77
43
|
* Manifest-steered ssai: play the DAI stitched stream through the content player, keep
|
|
78
44
|
* polling the break manifest, and schedule only event-triggered breaks client-side.
|
|
@@ -86,7 +52,7 @@ export declare class SessionStarter {
|
|
|
86
52
|
startManifestPolling(): void;
|
|
87
53
|
applyPendingChannelSwitch(): void;
|
|
88
54
|
private applyManifestUpdate;
|
|
89
|
-
private
|
|
55
|
+
private handleEventBoundaryTick;
|
|
90
56
|
/**
|
|
91
57
|
* A poll returned a manifest of another channel: re-align the ad setup after the current break
|
|
92
58
|
* settles (the latest poll wins). Scheduler ledgers are rebuilt, GAM is re-opened when its
|
|
@@ -95,12 +61,9 @@ export declare class SessionStarter {
|
|
|
95
61
|
* the new manifest's `adStartDelay` applies; afterwards, the delay in effect stays.
|
|
96
62
|
*/
|
|
97
63
|
private switchChannel;
|
|
98
|
-
private emitChannelChange;
|
|
99
|
-
private adoptExternalVendorConfigurations;
|
|
100
|
-
private diagnoseExternalVendorConfigurations;
|
|
101
64
|
private applySameModeChannelSwitch;
|
|
102
|
-
|
|
103
|
-
|
|
65
|
+
switchToSteeredSsai(manifest: BreakManifest, config: SessionConfig, preserveSchedulerLedgers?: boolean): Promise<boolean>;
|
|
66
|
+
switchToSgai(manifest: BreakManifest, config: SessionConfig, generation: number, preserveSchedulerLedgers?: boolean): Promise<void>;
|
|
104
67
|
/**
|
|
105
68
|
* Resolve the GAM stream identity from the manifest and open the IMA/DAI session. GAM failure
|
|
106
69
|
* is non-fatal: non-GAM breaks still schedule, only the GAM assets are skipped.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{getPreRollDelay as e}from"../types/BreakManifest";import{manifestHasGamAssets as s,resolveGamVendorConfiguration as t}from"../services/resolveGamVendorConfiguration";import{resolveExternalVendorConfigurations as i}from"../services/resolveExternalVendorConfigurations";import{SsaiController as a}from"../services/SsaiController";import{resolveUrlOrigin as r}from"../services/urlOrigin";import{filterSsaiClientBreaks as n,resolveStitchedStreamVendor as o,SsaiUnavailableError as d}from"../services/resolveDeliveryMode";import{detectDeviceProfile as l}from"../services/detectDeviceType";import{decideDeliveryMode as h,detectChannelSwitch as c,gamPodIdentityChanged as p}from"../services/channelSwitch";import{prepareWebRuntime as m}from"../services/webCompatibility";const S=1e4;function u(e){return"stitcherUrl"in e}function f(e){return"string"==typeof e&&""!==e.trim()}export class SessionStarter{constructor(e,s){this.state=e,this.deps=s}async startSession(s){if("ssai"===this.deps.mode){if(!u(s)||!f(s.stitcherUrl))throw new Error('[OptiViewAds] mode "ssai" requires a stitcherUrl in startSession().')}else if(u(s)||!f(s.manifestUrl))throw new Error("[OptiViewAds] startSession requires a manifestUrl (the full break-manifest URL).");this.state.sessionActive&&(this.deps.log("Ending previous session before starting new one"),this.deps.endSessionInternal("session-restart"));const t=this.state.sessionGeneration;this.state.sessionActive=!0,this.state.adStartDelayElapsed=!1;try{const e=m();e&&await e}catch(e){if(this.isStaleStart(t))return;throw this.state.sessionActive=!1,e}if(this.isStaleStart(t))return;if(this.deps.lensReporter.startSession(),u(s))return s.adInsertionType&&this.deps.diagnose("DA-INSERTION-TYPE-IGNORED-SSAI",`adInsertionType "${s.adInsertionType}" ignored in SSAI mode; the stitcher controls ad insertion.`,{adInsertionType:s.adInsertionType}),void await this.startSsaiSession(s,t);const i=s.manifestUrl;this.state.sessionConfig=s,this.state.manifestOrigin=r(i),this.deps.manifestService.setManifestUrl(i),this.deps.log("Session starting, manifest URL:",i);const a=Date.now(),n=e=>{this.deps.diagnose("DA-SESSION-STARTING",`Session start: ${e}`,{step:e,manifestUrl:i,elapsedMs:Date.now()-a})};n("begin");const o=s.assetParameters;let d;this.deps.adPlayerController.setSessionAssetParameters(o),this.deps.adPlayerController.setSessionAssetParameterMacros(s.assetParameterMacros),this.state.configSummary.gam&&(this.state.configSummary.gam={...this.state.configSummary.gam,hasAssetParameters:!!o}),n("manifest-fetch");try{d=await this.deps.manifestService.fetchManifest()}catch(e){if(this.isStaleStart(t))return;throw this.failSessionStart(e)}if(this.deps.cmcdAdReporter.onSessionStart(d),!this.isStaleStart(t))if(this.state.sessionManifest=d,this.adoptExternalVendorConfigurations(d),"ssai"!==this.resolveSessionDeliveryMode(d))try{if(await this.startGamForManifest(d,o,n,t,!0),this.isStaleStart(t))return;const a=s.adInsertionType;this.deps.adPlayerController.setBreakResumePolicy(a,d.timebase,this.deps.continueContentDuringBreak),this.deps.diagnose("DA-INSERTION-TYPE-RESOLVED",void 0===a?`Ad insertion type: unset — the manifest's resumeOffset drives the resume point (timebase: ${d.timebase})`:`Ad insertion type: ${a}, client-set (timebase: ${d.timebase})`,{adInsertionType:a??null,source:void 0===a?"unset":"client",timebase:d.timebase}),n("scheduling"),this.deps.breakScheduler.updateManifest(d),this.deps.pauseAdController.updateManifest(d),this.state.pauseAdTickInterval=setInterval(()=>this.deps.pauseAdController?.tick(),250);const r=(d.adStartDelay??0)>0?void 0:d.breaks.find(s=>0===e(s));r?(this.deps.adPlayerController.holdContentForPreRoll(r),this.state.heldImmediatePreRollId=r.id,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=Date.now(),this.contentIsReady()?this.armHeldImmediatePreRoll():this.state.heldPreRollArmInterval=setInterval(()=>{this.contentIsReady()?this.armHeldImmediatePreRoll():this.armHeldImmediatePreRollIfTimedOut()},250)):this.markPlaybackStartIfPlaying(),this.deps.startLivePushback(),this.startBreakScheduler(),this.startManifestPolling(),this.deps.diagnose("DA-SESSION-STARTED",`Session active for manifest ${i}`,{manifestUrl:i}),this.deps.log("Session active")}catch(e){if(this.isStaleStart(t))return;throw this.failSessionStart(e)}else await this.startSteeredSsaiSession(d,s,n)}markPlaybackStartIfPlaying(){this.deps.player.paused||this.deps.breakScheduler.markPlaybackStart()}contentIsReady(){return!Number.isNaN(this.deps.player.duration)||!this.deps.player.paused}clearHeldImmediatePreRollArmInterval(){this.state.heldPreRollArmInterval&&(clearInterval(this.state.heldPreRollArmInterval),this.state.heldPreRollArmInterval=null)}armHeldImmediatePreRoll(){this.state.heldImmediatePreRollId&&!this.state.heldPreRollArmed&&(this.clearHeldImmediatePreRollArmInterval(),this.state.heldPreRollArmed=!0,this.deps.breakScheduler.markPlaybackStart())}armHeldImmediatePreRollIfTimedOut(){if(!this.state.heldImmediatePreRollId||this.state.heldPreRollArmed)return;const e=this.state.heldPreRollWaitStartedAt;if(null===e||Date.now()-e<S)return;const s=this.state.heldImmediatePreRollId;this.armHeldImmediatePreRoll(),this.deps.diagnose("DA-PREROLL-CONTENT-WAIT-TIMEOUT","Pending pre-roll began after waiting 10000ms for content readiness.",{breakId:s,waitMs:S})}cancelHeldImmediatePreRollOnContentError(){const e=this.state.heldImmediatePreRollId;return!(!e||this.state.heldPreRollArmed)&&(this.clearHeldImmediatePreRollArmInterval(),this.deps.breakScheduler?.completeBreak(e),this.deps.adPlayerController?.releaseContentHold(),this.state.heldImmediatePreRollId=null,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=null,this.deps.diagnose("DA-PREROLL-CANCELLED-CONTENT-ERROR","A pending pre-roll was cancelled because the content failed to load before it could begin.",{breakId:e}),!0)}clearHeldImmediatePreRollArm(){this.clearHeldImmediatePreRollArmInterval(),this.state.heldImmediatePreRollId=null,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=null}failSessionStart(e){const s=e instanceof Error?e:new Error(String(e)),t=this.deps.manifestService?.takeLastFailure();return this.deps.diagnose("DA-MANIFEST-FETCH-FAILED",s.message,t?{...t}:void 0),this.state.sessionActive=!1,this.deps.cmcdAdReporter.onSessionEnd(),this.deps.lensReporter.endSession(),s}resolveSessionDeliveryMode(e,s="manifest"){const t=this.state.sessionDeliveryMode,i=h(e,l(),this.deps.hasExplicitMode?this.deps.mode:null,s);null!==i.unknownMode&&this.deps.diagnose("DA-DELIVERY-MODE-UNKNOWN",`Delivery rule ${i.ruleIndex} carries unrecognized mode "${i.unknownMode}"; using sgai.`,{unknownMode:i.unknownMode,ruleIndex:i.ruleIndex});for(const e of i.targetingIssues)this.deps.diagnose(e.code,`Targeting ${e.field} was rejected.`,{field:e.field,...void 0===e.value?{}:{value:e.value}});return this.deps.hasExplicitMode&&i.manifestMode!==i.mode&&this.deps.diagnose("DA-DELIVERY-MODE-OVERRIDDEN",`Manifest delivery rules resolve "${i.manifestMode}" but the explicit config mode "${i.mode}" takes precedence.`,{manifestMode:i.manifestMode,configuredMode:i.mode}),this.state.sessionDeliveryMode=i.mode,this.deps.diagnose("DA-DELIVERY-MODE-RESOLVED",`Delivery mode: ${i.mode}`,{mode:i.mode,ruleIndex:i.ruleIndex,source:i.source,deviceProfile:l(),..."channel-switch"===s?{previousMode:t}:{}}),i.mode}async startSteeredSsaiSession(e,s,t){const i=o(e.vendorConfiguration);if(!i){const e="Manifest selected ssai delivery but no vendor configuration provides a stitched stream (no gam entry with an assetKey).";throw this.deps.diagnose("DA-SSAI-UNAVAILABLE",e),this.deps.manifestService.reset(),this.state.sessionActive=!1,new d(e)}try{t("ssai-live-stream"),this.state.steeredSsaiActive=!0,this.state.ssaiController=this.createSsaiController(i.networkCode),await this.state.ssaiController.startSteered(i,s.assetParameters,s.assetParameterMacros),t("scheduling");const a=n(e);this.deps.adPlayerController.setBreakResumePolicy(s.adInsertionType,e.timebase,!1),this.deps.breakScheduler.updateManifest(a),this.deps.pauseAdController.updateManifest(a),this.state.pauseAdTickInterval=setInterval(()=>this.deps.pauseAdController?.tick(),250),this.markPlaybackStartIfPlaying(),this.deps.startLivePushback(),this.startBreakScheduler(),this.startManifestPolling(),this.deps.diagnose("DA-SESSION-STARTED",`SSAI session active (manifest-steered) for ${s.manifestUrl}`,{manifestUrl:s.manifestUrl}),this.deps.log("SSAI session active (manifest-steered)")}catch(e){const s=e instanceof Error?e:new Error(String(e));throw this.deps.diagnose("DA-SSAI-SESSION-FAILED",s.message),this.state.steeredSsaiActive=!1,this.deps.manifestService.reset(),this.state.sessionActive=!1,s}}createSsaiController(e){return new a(this.deps.player,{gam:this.state.steeredSsaiOptions?.streamActivityMonitorId?{streamActivityMonitorId:this.state.steeredSsaiOptions.streamActivityMonitorId}:{},ssai:{networkCode:e},maxBitrate:this.state.steeredSsaiOptions?.maxBitrate,autoplay:this.state.steeredSsaiOptions?.autoplay,debug:this.deps.debug},{emit:(e,s)=>this.deps.emit(e,s),diagnose:(e,s,t)=>this.deps.diagnose(e,s,t),log:(...e)=>this.deps.log(...e)})}startBreakScheduler(){this.deps.breakScheduler.start((e,s)=>{this.deps.breakLifecycle.onBreakStart(e,s)},e=>!this.deps.adPlayerController.isAdPlaying()&&(this.deps.adPlayerController.preload(e).catch(s=>{this.deps.diagnose("DA-PRELOAD-FAILED",s instanceof Error?s.message:String(s),{breakId:e.id})}),!0),(e,s,t)=>{this.deps.adBreakStatusTracker.onBreakWarning(e,s,t)},e=>{this.deps.breakLifecycle.onBreakAborted(e)},(e,s)=>this.deps.breakLifecycle.onBreakSkipped(e,s))}startManifestPolling(){this.deps.manifestService.startPolling(e=>{if(!this.state.sessionActive)return;const s=this.deps.notedManifestBreakIds(e);this.deps.diagnose("DA-MANIFEST-POLLED","Manifest polled",{breaks:e.breaks.length,newBreakIds:s,pollingIntervalSeconds:this.deps.manifestService.currentPollingIntervalSeconds});const t=this.state.sessionManifest,i=t?c(t,e):null;if(this.state.pendingChannelSwitch||i&&this.deps.breakScheduler.getActiveBreak())return this.state.pendingChannelSwitch=e,void this.deps.log("Channel switch deferred until the active break ends");i?this.switchChannel(e,i):this.applyManifestUpdate(e)},e=>{this.state.sessionActive&&this.deps.diagnose("DA-MANIFEST-POLL-FAILED",e.message)})}applyPendingChannelSwitch(){if(!this.state.sessionActive||!this.state.pendingChannelSwitch)return;const e=this.state.pendingChannelSwitch;this.state.pendingChannelSwitch=null;const s=c(this.state.sessionManifest,e);s?this.switchChannel(e,s):this.applyManifestUpdate(e)}applyManifestUpdate(e){if(!this.state.sessionActive)return;this.adoptExternalVendorConfigurations(e),this.diagnoseIgnoredDeliveryModeChange(e);const s=t(e);this.state.steeredSsaiActive?this.state.ssaiController?.setManifestAssetParameters(s.ssai?.assetParameters):this.deps.adPlayerController.setManifestAssetParameters(s.sgaiAssetParameters);const i=this.state.steeredSsaiActive?n(e):e;this.deps.breakScheduler.updateManifest(i),this.deps.pauseAdController.updateManifest(i)}diagnoseIgnoredDeliveryModeChange(e){if(!this.state.sessionDeliveryMode)return;const s=h(e,l(),null,"manifest");s.manifestMode!==this.state.sessionDeliveryMode&&this.deps.diagnose("DA-DELIVERY-MODE-CHANGE-IGNORED",`Polled manifest resolves delivery mode "${s.manifestMode}" but "${this.state.sessionDeliveryMode}" is pinned for this session.`,{polledMode:s.manifestMode,pinnedMode:this.state.sessionDeliveryMode})}async switchChannel(e,s){if(this.state.channelSwitchInFlight)return;const t=this.state.sessionConfig,i=this.state.sessionManifest,a={previousChannelId:s.previousChannelId,channelId:s.channelId,reason:s.reason,manifestUrl:t.manifestUrl};this.deps.diagnose("DA-MANIFEST-CHANNEL-CHANGED",`Manifest channel changed (${s.reason}): ${s.previousChannelId??"none"} -> ${s.channelId??"none"}`,a),this.state.sessionManifest=e,this.state.channelSwitchInFlight=!0;const r=this.state.sessionGeneration,n=this.state.sessionDeliveryMode??"sgai";try{let s=this.resolveSessionDeliveryMode(e,"channel-switch");if(this.deps.cmcdAdReporter.onSessionEnd(),this.deps.cmcdAdReporter.onSessionStart(e),this.deps.lensReporter.endSession(),this.deps.lensReporter.startSession(),"ssai"===s&&"sgai"===n){const a=await this.switchToSteeredSsai(e,t);if(this.isStaleStart(r))return;a||(s="sgai",this.state.sessionDeliveryMode="sgai",await this.applySameModeChannelSwitch(i,e,t,r))}else"sgai"===s&&"ssai"===n?await this.switchToSgai(e,t,r):await this.applySameModeChannelSwitch(i,e,t,r);if(this.isStaleStart(r))return}finally{this.state.channelSwitchInFlight=!1}this.adoptExternalVendorConfigurations(e),this.deps.startLivePushback(),this.emitChannelChange({...a,previousDeliveryMode:n,deliveryMode:this.state.sessionDeliveryMode??n})}emitChannelChange(e){this.deps.emit("adchannelchange",{type:"adchannelchange",timestamp:Date.now(),...e})}adoptExternalVendorConfigurations(e){this.deps.externalVendorConfigurationTracker.adopt(e)}diagnoseExternalVendorConfigurations(e){const s=i(e);s.invalidCount>0&&this.deps.diagnose("DA-EXTERNAL-VENDOR-INVALID",`Ignored ${s.invalidCount} invalid external vendorConfiguration `+(1===s.invalidCount?"entry":"entries")+".",{count:s.invalidCount}),s.duplicateProviders.length>0&&this.deps.diagnose("DA-EXTERNAL-VENDOR-DUPLICATE",`Ignored duplicate external vendorConfiguration entries for providers: ${s.duplicateProviders.join(", ")}.`,{providers:s.duplicateProviders})}async applySameModeChannelSwitch(e,s,i,a){const r=this.state.steeredSsaiActive,o=r?n(s):s;if(this.deps.breakScheduler.resetForChannelSwitch(o),this.deps.pauseAdController.updateManifest(o),this.deps.adPlayerController.setBreakResumePolicy(i.adInsertionType,s.timebase,this.deps.continueContentDuringBreak),!r&&p(e,s)){const e=this.deps.adPlayerController.updatedAssetParameters,t=this.deps.adPlayerController.sessionAssetParameterMacros;if(this.deps.adPlayerController.endGamSession(),this.deps.adPlayerController.setSessionAssetParameters(i.assetParameters),this.deps.adPlayerController.setSessionAssetParameterMacros(t),await this.startGamForManifest(s,i.assetParameters,e=>{this.deps.diagnose("DA-SESSION-STARTING",`Channel switch: ${e}`,{step:e,manifestUrl:i.manifestUrl})},a),this.isStaleStart(a))return;e&&this.deps.adPlayerController.updateAssetParameters(e)}else if(r){const e=t(s);this.state.ssaiController?.setManifestAssetParameters(e.ssai?.assetParameters)}else{const e=t(s);this.deps.adPlayerController.setManifestAssetParameters(e.sgaiAssetParameters)}}async switchToSteeredSsai(e,s){const t=o(e.vendorConfiguration);if(!t)return this.deps.diagnose("DA-SSAI-UNAVAILABLE","Channel switch resolved ssai delivery but no vendor configuration provides a stitched stream; the session stays in sgai.",{manifestUrl:s.manifestUrl,previousMode:"sgai"}),!1;const i=this.deps.adPlayerController,a=i.updatedAssetParameters,r=i.sessionAssetParameterMacros;i.stopActiveBreak(),i.releaseContentHold(),i.endGamSession(),i.setSessionAssetParameters(s.assetParameters),i.setSessionAssetParameterMacros(r),a&&i.updateAssetParameters(a);const d=this.createSsaiController(t.networkCode);this.state.ssaiController=d,this.state.steeredSsaiActive=!0,this.deps.diagnose("DA-SESSION-STARTING","Channel switch: ssai-live-stream",{step:"ssai-live-stream",manifestUrl:s.manifestUrl});try{await d.startSteered(t,s.assetParameters,r),a&&d.updateAssetParameters(a);const o=n(e);return this.deps.breakScheduler.resetForChannelSwitch(o),this.deps.pauseAdController.updateManifest(o),i.setBreakResumePolicy(s.adInsertionType,e.timebase,!1),!0}catch(e){const s=e instanceof Error?e.message:String(e);return this.deps.diagnose("DA-SSAI-SESSION-FAILED",s),d.stop(),delete this.state.ssaiController,this.state.steeredSsaiActive=!1,!1}}async switchToSgai(e,s,t){const i=this.state.ssaiController?.sessionAssetParameterMacros;this.state.ssaiController?.stop(),delete this.state.ssaiController,this.state.steeredSsaiActive=!1,this.deps.breakScheduler.resetForChannelSwitch(e),this.deps.pauseAdController.updateManifest(e);const a=this.deps.adPlayerController;a.setBreakResumePolicy(s.adInsertionType,e.timebase,this.deps.continueContentDuringBreak);const r=a.updatedAssetParameters;a.endGamSession(),a.setSessionAssetParameters(s.assetParameters),a.setSessionAssetParameterMacros(i??s.assetParameterMacros),await this.startGamForManifest(e,s.assetParameters,e=>{this.deps.diagnose("DA-SESSION-STARTING",`Channel switch: ${e}`,{step:e,manifestUrl:s.manifestUrl})},t),this.isStaleStart(t)||r&&a.updateAssetParameters(r)}async startGamForManifest(e,i,a,r=this.state.sessionGeneration,n=!1){const o=t(e);if(this.deps.adPlayerController.setManifestAssetParameters(o.sgaiAssetParameters),o.unknownTypes.length>0&&this.deps.diagnose("DA-VENDOR-CONFIG-UNKNOWN-TYPE",`Ignored ${o.unknownTypes.length} vendorConfiguration `+(1===o.unknownTypes.length?"entry":"entries")+" with unrecognised type: "+`${o.unknownTypes.join(", ")}.`,{types:o.unknownTypes}),this.diagnoseExternalVendorConfigurations(e),!this.deps.adPlayerController.isGamEnabled())return void(s(e)&&this.deps.diagnose("DA-GAM-CONFIG-MISSING","The break manifest carries GAM pod assets but GAM is disabled for this session (gam.enabled === false); those assets will be skipped.",{reason:"gam-disabled"}));if(!o.identity)return void(s(e)&&this.deps.diagnose("DA-GAM-CONFIG-MISSING",'The break manifest carries GAM pod assets but no usable "gam" vendorConfiguration entry; those assets will be skipped.',{reason:o.reason}));this.deps.diagnose("DA-GAM-CONFIG-RESOLVED",`GAM session identity: network ${o.identity.networkCode}, custom asset ${o.identity.customAssetKey}`,{networkCode:o.identity.networkCode,customAssetKey:o.identity.customAssetKey,channelId:e.channelId,duplicateEntriesIgnored:o.duplicateGam});const d=e.adStartDelay??0;if(d>0&&!this.state.adStartDelayElapsed&&(n||this.deps.breakScheduler.isAdStartDelayActive()))return this.deps.breakScheduler.holdBreakStarts(),this.deps.diagnose("DA-GAM-START-DEFERRED",`GAM session start deferred until ${d} seconds of content have played.`,{adStartDelaySec:d}),void this.deps.breakScheduler.setOnAdStartDelayElapsed(()=>{if(this.isStaleStart(r)||!this.state.sessionActive)return;this.state.adStartDelayElapsed=!0;const s=this.state.sessionManifest??e,n=t(s).identity;n?(this.deps.adPlayerController.setManifestAssetParameters(t(s).sgaiAssetParameters),this.startResolvedGamSession(n,i,a,r,!0)):this.deps.breakScheduler.releaseBreakStarts()});this.state.adStartDelayElapsed=!0,await this.startResolvedGamSession(o.identity,i,a,r)}async startResolvedGamSession(e,s,t,i,a=!1){try{if(this.isStaleStart(i)||!this.state.sessionActive)return;t("gam-init"),await this.deps.adPlayerController.startGamSession(e,s),this.deps.log("GAM session started")}catch(e){this.deps.diagnose("DA-GAM-SESSION-FAILED",e instanceof Error?e.message:String(e))}finally{a&&!this.isStaleStart(i)&&this.state.sessionActive&&this.deps.breakScheduler.releaseBreakStarts()}}async startSsaiSession(e,s){try{if(this.state.stitcherOrigin=r(e.stitcherUrl),this.state.configSummary.gam&&(this.state.configSummary.gam={...this.state.configSummary.gam,hasAssetParameters:!!e.assetParameters}),await this.state.ssaiController.start(e),this.isStaleStart(s))return;this.deps.diagnose("DA-SESSION-STARTED",`SSAI session active for ${e.stitcherUrl}`,{stitcherUrl:e.stitcherUrl}),this.deps.startLivePushback(),this.deps.log("SSAI session active")}catch(e){if(this.isStaleStart(s))return;const t=e instanceof Error?e:new Error(String(e));throw this.deps.diagnose("DA-SSAI-SESSION-FAILED",t.message),this.state.sessionActive=!1,this.deps.cmcdAdReporter.onSessionEnd(),this.deps.lensReporter.endSession(),t}}isStaleStart(e){return e!==this.state.sessionGeneration&&(this.deps.log("Session start aborted — the session ended or was replaced while starting"),!0)}}
|
|
1
|
+
import{sessionTick as e}from"./sessionTick";import{getPreRollDelay as s}from"../types/BreakManifest";import{manifestHasGamAssets as t,resolveGamVendorConfiguration as a,resolveSsbStream as i}from"../services/resolveGamVendorConfiguration";import{SsaiController as r}from"../services/SsaiController";import{resolveUrlOrigin as n}from"../services/urlOrigin";import{filterSsaiClientBreaks as o,resolveStitchedStreamVendor as d,SsaiUnavailableError as l}from"../services/resolveDeliveryMode";import{detectChannelSwitch as h,gamPodIdentityChanged as c}from"../services/channelSwitch";import{prepareWebRuntime as p}from"../services/webCompatibility";import{adoptExternalVendorConfigurations as S,diagnoseExternalVendorConfigurations as m,diagnoseIgnoredDeliveryModeChange as f,emitChannelChange as u,handleEventBoundaryTick as A,isStaleSessionStart as y,isNonBlankString as g,isSsaiSessionConfig as v,rejectGamPodPreloadForBoundaryIdentity as P,resolveEventAtPosition as I,resolveSessionDeliveryMode as k}from"./SessionStarterHelpers";const C=1e4;export class SessionStarter{constructor(e,s){this.state=e,this.deps=s}async startSession(t){if("ssai"===this.deps.mode){if(!v(t)||!g(t.stitcherUrl))throw new Error('[OptiViewAds] mode "ssai" requires a stitcherUrl in startSession().')}else if(v(t)||!g(t.manifestUrl))throw new Error("[OptiViewAds] startSession requires a manifestUrl (the full break-manifest URL).");this.state.sessionActive&&(this.deps.log("Ending previous session before starting new one"),this.deps.endSessionInternal("session-restart"));const a=this.state.sessionGeneration;this.state.sessionActive=!0,this.state.adStartDelayElapsed=!1;try{const e=p();e&&await e}catch(e){if(this.isStaleStart(a))return;throw this.state.sessionActive=!1,e}if(this.isStaleStart(a))return;if(this.deps.lensReporter.startSession(),v(t))return t.adInsertionType&&this.deps.diagnose("DA-INSERTION-TYPE-IGNORED-SSAI",`adInsertionType "${t.adInsertionType}" ignored in SSAI mode; the stitcher controls ad insertion.`,{adInsertionType:t.adInsertionType}),void await this.startSsaiSession(t,a);const r=t.manifestUrl;this.state.sessionConfig=t,this.state.manifestOrigin=n(r),this.deps.manifestService.setManifestUrl(r),this.deps.log("Session starting, manifest URL:",r);const o=Date.now(),d=e=>{this.deps.diagnose("DA-SESSION-STARTING",`Session start: ${e}`,{step:e,manifestUrl:r,elapsedMs:Date.now()-o})};d("begin");const l=t.assetParameters;let h,c;this.deps.adPlayerController.setSessionAssetParameters(l),this.deps.adPlayerController.setSessionAssetParameterMacros(t.assetParameterMacros),this.state.configSummary.gam&&(this.state.configSummary.gam={...this.state.configSummary.gam,hasAssetParameters:!!l}),d("manifest-fetch");try{c=await this.deps.manifestService.fetchManifest();const e=I(c,Date.now(),this.deps.player);h=e.manifest,this.state.sessionManifest=c,this.state.effectiveSessionManifest=h,this.state.activeEventId=e.eventId}catch(e){if(this.isStaleStart(a))return;throw this.failSessionStart(e)}if(this.deps.cmcdAdReporter.onSessionStart(h),!this.isStaleStart(a))if(this.deps.externalPlayback.setManaged(null!==i(h.vendorConfiguration)),S(this.deps,h),"ssai"!==this.resolveSessionDeliveryMode(h))try{if(await this.startGamForManifest(h,l,d,a,!0),this.isStaleStart(a))return;const i=t.adInsertionType;this.deps.adPlayerController.setBreakResumePolicy(i,h.timebase,this.deps.continueContentDuringBreak),this.deps.diagnose("DA-INSERTION-TYPE-RESOLVED",void 0===i?`Ad insertion type: unset — the manifest's resumeOffset drives the resume point (timebase: ${h.timebase})`:`Ad insertion type: ${i}, client-set (timebase: ${h.timebase})`,{adInsertionType:i??null,source:void 0===i?"unset":"client",timebase:h.timebase}),d("scheduling"),this.deps.breakScheduler.updateManifest(h),this.deps.pauseAdController.updateManifest(h),this.state.pauseAdTickInterval=setInterval(e(this.deps),250);const n=(h.adStartDelay??0)>0?void 0:h.breaks.find(e=>0===s(e));n?(this.deps.adPlayerController.holdContentForPreRoll(n),this.state.heldImmediatePreRollId=n.id,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=Date.now(),this.contentIsReady()?this.armHeldImmediatePreRoll():this.state.heldPreRollArmInterval=setInterval(()=>{this.contentIsReady()?this.armHeldImmediatePreRoll():this.armHeldImmediatePreRollIfTimedOut()},250)):this.markPlaybackStartIfPlaying(),this.deps.startLivePushback(),this.startBreakScheduler(),this.startManifestPolling(),this.deps.diagnose("DA-SESSION-STARTED",`Session active for manifest ${r}`,{manifestUrl:r}),this.deps.log("Session active")}catch(e){if(this.isStaleStart(a))return;throw this.failSessionStart(e)}else await this.startSteeredSsaiSession(h,t,d)}discardPreparedGamSessionAfterSeek(){this.state.sessionActive&&(this.deps.adPlayerController?.gamPodBridge.discardPreparedGamSession(),this.state.eventSessionPreloadKeys.clear())}markPlaybackStartIfPlaying(){this.deps.player.paused||this.deps.breakScheduler.markPlaybackStart()}contentIsReady(){return!Number.isNaN(this.deps.player.duration)||!this.deps.player.paused}clearHeldImmediatePreRollArmInterval(){this.state.heldPreRollArmInterval&&(clearInterval(this.state.heldPreRollArmInterval),this.state.heldPreRollArmInterval=null)}armHeldImmediatePreRoll(){this.state.heldImmediatePreRollId&&!this.state.heldPreRollArmed&&(this.clearHeldImmediatePreRollArmInterval(),this.state.heldPreRollArmed=!0,this.deps.breakScheduler.markPlaybackStart())}armHeldImmediatePreRollIfTimedOut(){if(!this.state.heldImmediatePreRollId||this.state.heldPreRollArmed)return;const e=this.state.heldPreRollWaitStartedAt;if(null===e||Date.now()-e<C)return;const s=this.state.heldImmediatePreRollId;this.armHeldImmediatePreRoll(),this.deps.diagnose("DA-PREROLL-CONTENT-WAIT-TIMEOUT","Pending pre-roll began after waiting 10000ms for content readiness.",{breakId:s,waitMs:C})}cancelHeldImmediatePreRollOnContentError(){const e=this.state.heldImmediatePreRollId;return!(!e||this.state.heldPreRollArmed)&&(this.clearHeldImmediatePreRollArmInterval(),this.deps.breakScheduler?.completeBreak(e),this.deps.adPlayerController?.releaseContentHold(),this.state.heldImmediatePreRollId=null,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=null,this.deps.diagnose("DA-PREROLL-CANCELLED-CONTENT-ERROR","A pending pre-roll was cancelled because the content failed to load before it could begin.",{breakId:e}),!0)}clearHeldImmediatePreRollArm(){this.clearHeldImmediatePreRollArmInterval(),this.state.heldImmediatePreRollId=null,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=null}failSessionStart(e){const s=e instanceof Error?e:new Error(String(e)),t=this.deps.manifestService?.takeLastFailure();return this.deps.diagnose("DA-MANIFEST-FETCH-FAILED",s.message,t?{...t}:void 0),this.state.sessionActive=!1,this.deps.cmcdAdReporter.onSessionEnd(),this.deps.lensReporter.endSession(),s}resolveSessionDeliveryMode(e,s="manifest"){return k(this.state,this.deps,e,s)}async startSteeredSsaiSession(s,t,a){const i=d(s.vendorConfiguration);if(!i){const e="Manifest selected ssai delivery but no vendor configuration provides a stitched stream (no gam entry with an assetKey).";throw this.deps.diagnose("DA-SSAI-UNAVAILABLE",e),this.deps.manifestService.reset(),this.state.sessionActive=!1,new l(e)}try{a("ssai-live-stream"),this.state.steeredSsaiActive=!0,this.state.ssaiController=this.createSsaiController(i.networkCode),await this.state.ssaiController.startSteered(i,t.assetParameters,t.assetParameterMacros),a("scheduling");const r=o(s);this.deps.adPlayerController.setBreakResumePolicy(t.adInsertionType,s.timebase,!1),this.deps.breakScheduler.updateManifest(r),this.deps.pauseAdController.updateManifest(r),this.state.pauseAdTickInterval=setInterval(e(this.deps),250),this.markPlaybackStartIfPlaying(),this.deps.startLivePushback(),this.startBreakScheduler(),this.startManifestPolling(),this.deps.diagnose("DA-SESSION-STARTED",`SSAI session active (manifest-steered) for ${t.manifestUrl}`,{manifestUrl:t.manifestUrl}),this.deps.log("SSAI session active (manifest-steered)")}catch(e){const s=e instanceof Error?e:new Error(String(e));throw this.deps.diagnose("DA-SSAI-SESSION-FAILED",s.message),this.state.steeredSsaiActive=!1,this.deps.manifestService.reset(),this.state.sessionActive=!1,s}}createSsaiController(e){return new r(this.deps.player,{gam:this.state.steeredSsaiOptions?.streamActivityMonitorId?{streamActivityMonitorId:this.state.steeredSsaiOptions.streamActivityMonitorId}:{},ssai:{networkCode:e},maxBitrate:this.state.steeredSsaiOptions?.maxBitrate,autoplay:this.state.steeredSsaiOptions?.autoplay,debug:this.deps.debug},{emit:(e,s)=>this.deps.emit(e,s),diagnose:(e,s,t)=>this.deps.diagnose(e,s,t),log:(...e)=>this.deps.log(...e)})}startBreakScheduler(){this.deps.breakScheduler.setEventBoundaryCheck(e=>this.handleEventBoundaryTick(e)),this.deps.breakScheduler.start((e,s)=>{this.deps.breakLifecycle.onBreakStart(e,s)},e=>{if(this.deps.adPlayerController.isAdPlaying())return!1;if(this.deps.adPlayerController.gamPodBridge.shouldSkipGamPodPreload(e))return!1;const s=P(this.state,this.deps,this.deps.player,e);return"defer"!==s&&("mismatch"===s||this.deps.adPlayerController.preload(e).catch(s=>{this.deps.diagnose("DA-PRELOAD-FAILED",s instanceof Error?s.message:String(s),{breakId:e.id})}),!0)},(e,s,t)=>{this.deps.adBreakStatusTracker.onBreakWarning(e,s,t)},e=>{this.deps.breakLifecycle.onBreakAborted(e)},(e,s)=>this.deps.breakLifecycle.onBreakSkipped(e,s))}startManifestPolling(){this.deps.manifestService.startPolling(e=>{if(!this.state.sessionActive)return;const s=this.deps.notedManifestBreakIds(e);this.deps.diagnose("DA-MANIFEST-POLLED","Manifest polled",{breaks:e.breaks.length,newBreakIds:s,pollingIntervalSeconds:this.deps.manifestService.currentPollingIntervalSeconds});const t=this.state.sessionManifest,a=t?h(t,e):null;if(this.state.pendingChannelSwitch||a&&(this.deps.breakScheduler.getActiveBreak()||this.state.ssb))return this.state.pendingChannelSwitch=e,void this.deps.log("Channel switch deferred until the active break ends");if(a)return this.state.sessionManifest=e,this.state.pendingManifestUpdate=null,void this.switchChannel(e,a);const i="wallclock"===e.timebase&&!1!==this.deps.player.isLive&&Boolean(e.events?.length||t?.events?.length);this.state.sessionManifest=e,i?this.state.pendingManifestUpdate=e:(this.state.pendingManifestUpdate=null,this.state.effectiveSessionManifest=e,this.applyManifestUpdate(e))},e=>{this.state.sessionActive&&this.deps.diagnose("DA-MANIFEST-POLL-FAILED",e.message)})}applyPendingChannelSwitch(){if(!this.state.sessionActive||!this.state.pendingChannelSwitch||this.state.ssb)return;const e=this.state.pendingChannelSwitch;this.state.pendingChannelSwitch=null;const s=h(this.state.sessionManifest,e);s?this.switchChannel(e,s):this.applyManifestUpdate(e)}applyManifestUpdate(e){if(!this.state.sessionActive)return;S(this.deps,e),f(this.state,this.deps,e);const s=a(e);this.state.steeredSsaiActive?this.state.ssaiController?.setManifestAssetParameters(s.ssai?.assetParameters):this.deps.adPlayerController.setManifestAssetParameters(s.sgaiAssetParameters);const t=this.state.steeredSsaiActive?o(e):e;this.deps.breakScheduler.updateManifest(t),this.deps.pauseAdController.updateManifest(t)}handleEventBoundaryTick(e){return A(this.state,this.deps,e,{resolveDeliveryMode:e=>this.resolveSessionDeliveryMode(e,"event-boundary"),applyManifestUpdate:e=>this.applyManifestUpdate(e),switchChannel:(e,s,t,a)=>this.switchChannel(e,s,t,a)})}async switchChannel(e,s,t=!1,a=e){if(this.state.channelSwitchInFlight)return;const r=this.state.sessionConfig,n=this.state.effectiveSessionManifest??this.state.sessionManifest;if(!t){const s=this.deps.player.programDateTime?.getTime()??Date.now(),t=I(a,s,this.deps.player);e=t.manifest,this.state.activeEventId=t.eventId}const o=t?"event-boundary":s.reason,d={previousChannelId:s.previousChannelId,channelId:s.channelId,eventId:this.state.activeEventId,reason:o,manifestUrl:r.manifestUrl};this.deps.diagnose("DA-MANIFEST-CHANNEL-CHANGED",`Manifest channel changed (${o}): ${s.previousChannelId??"none"} -> ${s.channelId??"none"}`,d),this.state.sessionManifest=a,this.state.effectiveSessionManifest=e,this.state.pendingManifestUpdate=null,this.deps.externalPlayback.setManaged(null!==i(e.vendorConfiguration)),this.state.channelSwitchInFlight=!0;const l=this.state.sessionGeneration,h=this.state.sessionDeliveryMode??"sgai";try{let s=this.resolveSessionDeliveryMode(e,t?"event-boundary":"channel-switch");if(t?this.deps.cmcdAdReporter.updateEventId(this.state.activeEventId):(this.deps.cmcdAdReporter.onSessionEnd(),this.deps.cmcdAdReporter.onSessionStart(e),this.deps.lensReporter.endSession(),this.deps.lensReporter.startSession()),"ssai"===s&&"sgai"===h){const a=await this.switchToSteeredSsai(e,r,t);if(this.isStaleStart(l))return;a||(s="sgai",this.state.sessionDeliveryMode="sgai",await this.applySameModeChannelSwitch(n,e,r,l,t))}else"sgai"===s&&"ssai"===h?await this.switchToSgai(e,r,l,t):await this.applySameModeChannelSwitch(n,e,r,l,t);if(this.isStaleStart(l))return}finally{this.state.channelSwitchInFlight=!1,t&&this.deps.breakScheduler.releaseBreakStarts()}S(this.deps,e),this.deps.startLivePushback(),u(this.deps,{...d,previousDeliveryMode:h,deliveryMode:this.state.sessionDeliveryMode??h})}async applySameModeChannelSwitch(e,s,t,i,r=!1){const n=this.state.steeredSsaiActive,d=n?o(s):s;if(r?this.deps.breakScheduler.applyEventBoundary(d):this.deps.breakScheduler.resetForChannelSwitch(d),this.deps.pauseAdController.updateManifest(d),this.deps.adPlayerController.setBreakResumePolicy(t.adInsertionType,s.timebase,this.deps.continueContentDuringBreak),!n&&c(e,s)){const e=this.deps.adPlayerController,n=a(s);if(!(!r||!n.identity)&&await e.gamPodBridge.adoptPreparedGamSession(n.identity,t.assetParameters,n.sgaiAssetParameters))e.updatedAssetParameters&&e.updateAssetParameters(e.updatedAssetParameters);else{const a=e.updatedAssetParameters,r=e.sessionAssetParameterMacros;if(e.endGamSession(),e.setSessionAssetParameters(t.assetParameters),e.setSessionAssetParameterMacros(r),await this.startGamForManifest(s,t.assetParameters,e=>{this.deps.diagnose("DA-SESSION-STARTING",`Channel switch: ${e}`,{step:e,manifestUrl:t.manifestUrl})},i),this.isStaleStart(i))return;a&&e.updateAssetParameters(a)}}else if(n){const e=a(s);this.state.ssaiController?.setManifestAssetParameters(e.ssai?.assetParameters)}else{const e=a(s);this.deps.adPlayerController.setManifestAssetParameters(e.sgaiAssetParameters)}}async switchToSteeredSsai(e,s,t=!1){const a=d(e.vendorConfiguration);if(!a)return this.deps.diagnose("DA-SSAI-UNAVAILABLE","Channel switch resolved ssai delivery but no vendor configuration provides a stitched stream; the session stays in sgai.",{manifestUrl:s.manifestUrl,previousMode:"sgai"}),!1;const i=this.deps.adPlayerController,r=i.updatedAssetParameters,n=i.sessionAssetParameterMacros;i.stopActiveBreak(),i.releaseContentHold(),i.endGamSession(),i.setSessionAssetParameters(s.assetParameters),i.setSessionAssetParameterMacros(n),r&&i.updateAssetParameters(r);const l=this.createSsaiController(a.networkCode);this.state.ssaiController=l,this.state.steeredSsaiActive=!0,this.deps.diagnose("DA-SESSION-STARTING","Channel switch: ssai-live-stream",{step:"ssai-live-stream",manifestUrl:s.manifestUrl});try{await l.startSteered(a,s.assetParameters,n),r&&l.updateAssetParameters(r);const d=o(e);return t?this.deps.breakScheduler.applyEventBoundary(d):this.deps.breakScheduler.resetForChannelSwitch(d),this.deps.pauseAdController.updateManifest(d),i.setBreakResumePolicy(s.adInsertionType,e.timebase,!1),!0}catch(e){const s=e instanceof Error?e.message:String(e);return this.deps.diagnose("DA-SSAI-SESSION-FAILED",s),l.stop(),delete this.state.ssaiController,this.state.steeredSsaiActive=!1,!1}}async switchToSgai(e,s,t,a=!1){const i=this.state.ssaiController?.sessionAssetParameterMacros;this.state.ssaiController?.stop(),delete this.state.ssaiController,this.state.steeredSsaiActive=!1,a?this.deps.breakScheduler.applyEventBoundary(e):this.deps.breakScheduler.resetForChannelSwitch(e),this.deps.pauseAdController.updateManifest(e);const r=this.deps.adPlayerController;r.setBreakResumePolicy(s.adInsertionType,e.timebase,this.deps.continueContentDuringBreak);const n=r.updatedAssetParameters;r.endGamSession(),r.setSessionAssetParameters(s.assetParameters),r.setSessionAssetParameterMacros(i??s.assetParameterMacros),await this.startGamForManifest(e,s.assetParameters,e=>{this.deps.diagnose("DA-SESSION-STARTING",`Channel switch: ${e}`,{step:e,manifestUrl:s.manifestUrl})},t),this.isStaleStart(t)||n&&r.updateAssetParameters(n)}async startGamForManifest(e,s,i,r=this.state.sessionGeneration,n=!1){const o=a(e);if(this.deps.adPlayerController.setManifestAssetParameters(o.sgaiAssetParameters),o.unknownTypes.length>0&&this.deps.diagnose("DA-VENDOR-CONFIG-UNKNOWN-TYPE",`Ignored ${o.unknownTypes.length} vendorConfiguration `+(1===o.unknownTypes.length?"entry":"entries")+" with unrecognised type: "+`${o.unknownTypes.join(", ")}.`,{types:o.unknownTypes}),m(this.deps,e),!this.deps.adPlayerController.isGamEnabled())return void(t(e)&&this.deps.diagnose("DA-GAM-CONFIG-MISSING","The break manifest carries GAM pod assets but GAM is disabled for this session (gam.enabled === false); those assets will be skipped.",{reason:"gam-disabled"}));if(!o.identity)return void(t(e)&&this.deps.diagnose("DA-GAM-CONFIG-MISSING",'The break manifest carries GAM pod assets but no usable "gam" vendorConfiguration entry; those assets will be skipped.',{reason:o.reason}));this.deps.diagnose("DA-GAM-CONFIG-RESOLVED",`GAM session identity: network ${o.identity.networkCode}, custom asset ${o.identity.customAssetKey}`,{networkCode:o.identity.networkCode,customAssetKey:o.identity.customAssetKey,channelId:e.channelId,duplicateEntriesIgnored:o.duplicateGam});const d=e.adStartDelay??0;if(d>0&&!this.state.adStartDelayElapsed&&(n||this.deps.breakScheduler.isAdStartDelayActive()))return this.deps.breakScheduler.holdBreakStarts(),this.deps.diagnose("DA-GAM-START-DEFERRED",`GAM session start deferred until ${d} seconds of content have played.`,{adStartDelaySec:d}),void this.deps.breakScheduler.setOnAdStartDelayElapsed(()=>{if(this.isStaleStart(r)||!this.state.sessionActive)return;this.state.adStartDelayElapsed=!0;const t=this.state.sessionManifest??e,n=a(t).identity;n?(this.deps.adPlayerController.setManifestAssetParameters(a(t).sgaiAssetParameters),this.startResolvedGamSession(n,s,i,r,!0)):this.deps.breakScheduler.releaseBreakStarts()});this.state.adStartDelayElapsed=!0,await this.startResolvedGamSession(o.identity,s,i,r)}async startResolvedGamSession(e,s,t,a,i=!1){try{if(this.isStaleStart(a)||!this.state.sessionActive)return;t("gam-init"),await this.deps.adPlayerController.startGamSession(e,s),this.deps.log("GAM session started")}catch(e){this.deps.diagnose("DA-GAM-SESSION-FAILED",e instanceof Error?e.message:String(e))}finally{i&&!this.isStaleStart(a)&&this.state.sessionActive&&this.deps.breakScheduler.releaseBreakStarts()}}async startSsaiSession(e,s){try{if(this.state.stitcherOrigin=n(e.stitcherUrl),this.state.configSummary.gam&&(this.state.configSummary.gam={...this.state.configSummary.gam,hasAssetParameters:!!e.assetParameters}),await this.state.ssaiController.start(e),this.isStaleStart(s))return;this.deps.diagnose("DA-SESSION-STARTED",`SSAI session active for ${e.stitcherUrl}`,{stitcherUrl:e.stitcherUrl}),this.deps.startLivePushback(),this.deps.log("SSAI session active")}catch(e){if(this.isStaleStart(s))return;const t=e instanceof Error?e:new Error(String(e));throw this.deps.diagnose("DA-SSAI-SESSION-FAILED",t.message),this.state.sessionActive=!1,this.deps.cmcdAdReporter.onSessionEnd(),this.deps.lensReporter.endSession(),t}}isStaleStart(e){return y(this.state,this.deps,e)}}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { PlayerAdapter } from '../interfaces/PlayerAdapter';
|
|
2
|
+
import type { OptiViewAdsMode } from '../types/OptiViewAdsConfig';
|
|
3
|
+
import type { ManifestService } from '../services/ManifestService';
|
|
4
|
+
import type { BreakScheduler } from '../services/BreakScheduler';
|
|
5
|
+
import type { AdPlayerController } from '../services/AdPlayerController';
|
|
6
|
+
import type { PauseAdController } from '../services/PauseAdController';
|
|
7
|
+
import type { LensReporter } from '../services/LensReporter';
|
|
8
|
+
import type { CmcdAdReporter } from '../services/CmcdAdReporter';
|
|
9
|
+
import type { ExternalPlaybackGate } from '../services/ExternalPlaybackGate';
|
|
10
|
+
import type { ExternalVendorConfigurationTracker } from '../services/ExternalVendorConfigurationTracker';
|
|
11
|
+
import type { AdBreakStatusTracker, EmitEvent } from './AdBreakStatusTracker';
|
|
12
|
+
import type { BreakLifecycle, Diagnose, Log } from './BreakLifecycle';
|
|
13
|
+
export interface SessionStarterDeps {
|
|
14
|
+
externalPlayback: ExternalPlaybackGate;
|
|
15
|
+
player: PlayerAdapter;
|
|
16
|
+
debug: boolean;
|
|
17
|
+
mode: OptiViewAdsMode;
|
|
18
|
+
hasExplicitMode: boolean;
|
|
19
|
+
continueContentDuringBreak: boolean;
|
|
20
|
+
startLivePushback: () => void;
|
|
21
|
+
manifestService: ManifestService | undefined;
|
|
22
|
+
breakScheduler: BreakScheduler | undefined;
|
|
23
|
+
adPlayerController: AdPlayerController | undefined;
|
|
24
|
+
pauseAdController: PauseAdController | undefined;
|
|
25
|
+
breakLifecycle: BreakLifecycle | undefined;
|
|
26
|
+
cmcdAdReporter: CmcdAdReporter;
|
|
27
|
+
lensReporter: LensReporter;
|
|
28
|
+
adBreakStatusTracker: AdBreakStatusTracker;
|
|
29
|
+
externalVendorConfigurationTracker: ExternalVendorConfigurationTracker;
|
|
30
|
+
emit: EmitEvent;
|
|
31
|
+
diagnose: Diagnose;
|
|
32
|
+
log: Log;
|
|
33
|
+
endSessionInternal: (cause: string) => void;
|
|
34
|
+
notedManifestBreakIds: (manifest: {
|
|
35
|
+
breaks: Array<{
|
|
36
|
+
id: string;
|
|
37
|
+
}>;
|
|
38
|
+
}) => string[];
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { PlayerAdapter } from '../interfaces/PlayerAdapter';
|
|
2
|
+
import type { Break, BreakManifest } from '../types/BreakManifest';
|
|
3
|
+
import type { OptiViewAdsMode, SessionDeliveryMode, SsaiSessionConfig, StartSessionConfig } from '../types/OptiViewAdsConfig';
|
|
4
|
+
import { type ChannelSwitch } from '../services/channelSwitch';
|
|
5
|
+
import type { SessionState } from './SessionState';
|
|
6
|
+
import type { SessionStarterDeps } from './SessionStarterDeps';
|
|
7
|
+
export declare const EVENT_SESSION_PRELOAD_LEAD_MS = 8000;
|
|
8
|
+
export declare function isSsaiSessionConfig(config: StartSessionConfig): config is SsaiSessionConfig;
|
|
9
|
+
export declare function isNonBlankString(value: unknown): value is string;
|
|
10
|
+
export declare function resolveEventAtPosition(manifest: BreakManifest, positionMs: number, player: PlayerAdapter): {
|
|
11
|
+
manifest: BreakManifest;
|
|
12
|
+
eventId: string | null;
|
|
13
|
+
};
|
|
14
|
+
export type GamPodPreloadBoundaryDecision = 'none' | 'defer' | 'mismatch';
|
|
15
|
+
export declare function rejectGamPodPreloadForBoundaryIdentity(state: SessionState, deps: SessionStarterDeps, player: PlayerAdapter, breakInfo: Break): GamPodPreloadBoundaryDecision;
|
|
16
|
+
export declare function diagnoseIgnoredDeliveryModeChange(state: SessionState, deps: SessionStarterDeps, manifest: BreakManifest): void;
|
|
17
|
+
export declare function resolveSessionDeliveryMode(state: SessionState, deps: SessionStarterDeps, manifest: BreakManifest, source?: 'manifest' | 'channel-switch' | 'event-boundary'): OptiViewAdsMode;
|
|
18
|
+
export declare function isStaleSessionStart(state: SessionState, deps: SessionStarterDeps, generation: number): boolean;
|
|
19
|
+
export declare function emitChannelChange(deps: SessionStarterDeps, context: {
|
|
20
|
+
previousChannelId: string | null;
|
|
21
|
+
channelId: string | null;
|
|
22
|
+
eventId: string | null;
|
|
23
|
+
reason: ChannelSwitch['reason'];
|
|
24
|
+
manifestUrl: string;
|
|
25
|
+
previousDeliveryMode: SessionDeliveryMode;
|
|
26
|
+
deliveryMode: SessionDeliveryMode;
|
|
27
|
+
}): void;
|
|
28
|
+
export declare function adoptExternalVendorConfigurations(deps: SessionStarterDeps, manifest: BreakManifest): void;
|
|
29
|
+
export declare function diagnoseExternalVendorConfigurations(deps: SessionStarterDeps, manifest: BreakManifest): void;
|
|
30
|
+
type EventBoundaryActions = {
|
|
31
|
+
resolveDeliveryMode: (manifest: BreakManifest) => OptiViewAdsMode;
|
|
32
|
+
applyManifestUpdate: (manifest: BreakManifest) => void;
|
|
33
|
+
switchChannel: (manifest: BreakManifest, change: ChannelSwitch, eventBoundary: boolean, rawManifest: BreakManifest) => Promise<void>;
|
|
34
|
+
};
|
|
35
|
+
export declare function handleEventBoundaryTick(state: SessionState, deps: SessionStarterDeps, positionMs: number | null, actions: EventBoundaryActions): boolean;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Timebase as e}from"../types/BreakManifest";import{resolveExternalVendorConfigurations as n}from"../services/resolveExternalVendorConfigurations";import{detectDeviceProfile as t}from"../services/detectDeviceType";import{isGamVendorParameters as s}from"../services/GamPodUrlBuilder";import{decideDeliveryMode as i,detectChannelSwitch as o,gamPodIdentityChanged as r}from"../services/channelSwitch";import{nextEventBoundaryMs as a,resolveEventManifest as d}from"../services/resolveChannelEvents";import{filterSsaiClientBreaks as l}from"../services/resolveDeliveryMode";import{gamPodIdentityMismatches as u,resolveGamVendorConfiguration as f}from"../services/resolveGamVendorConfiguration";export const EVENT_SESSION_PRELOAD_LEAD_MS=8e3;export function isSsaiSessionConfig(e){return"stitcherUrl"in e}export function isNonBlankString(e){return"string"==typeof e&&""!==e.trim()}export function resolveEventAtPosition(n,t,s){return n.timebase!==e.WALLCLOCK||!1===s.isLive?{manifest:n,eventId:n.eventId??null}:d(n,t)}export function rejectGamPodPreloadForBoundaryIdentity(n,t,i,o){const r=n.pendingManifestUpdate??n.sessionManifest;if(!r||r.timebase!==e.WALLCLOCK||!1===i.isLive||"string"!=typeof o.start)return"none";const a=Date.parse(o.start);if(!Number.isFinite(a))return"none";const l=d(r,a).manifest,c=f(l).identity;if(!c)return"none";const v=n.effectiveSessionManifest??r,m=f(v).identity;if(m&&c.networkCode===m.networkCode&&c.customAssetKey===m.customAssetKey)return"none";const p=(Array.isArray(o.variant)?o.variant:[o.variant]).flatMap(e=>e.assets).filter(e=>"vendor"===e.type&&"gam"===e.vendor&&s(e.vendorParameters));if(0===p.length)return"none";const y=p.find(e=>0===u(c,e.vendorParameters).length);if(y)return"defer";const g=p[0];if(!g)return"none";for(const e of u(c,g.vendorParameters))t.diagnose("DA-GAM-POD-IDENTITY-MISMATCH",`GAM asset ${g.id} skipped: vendorParameters.${e.field} "${e.assetValue}" does not match the stream session's "${e.sessionValue}".`,{assetId:g.id,field:e.field,assetValue:e.assetValue,sessionValue:e.sessionValue});return"mismatch"}export function diagnoseIgnoredDeliveryModeChange(e,n,s){if(!e.sessionDeliveryMode)return;const o="ssb"===e.sessionDeliveryMode?e.ssb?.previousMode:e.sessionDeliveryMode;if(!o)return;const r=i(s,t(),null,"manifest");r.manifestMode!==o&&n.diagnose("DA-DELIVERY-MODE-CHANGE-IGNORED",`Polled manifest resolves delivery mode "${r.manifestMode}" but "${o}" is pinned for this session.`,{polledMode:r.manifestMode,pinnedMode:o})}export function resolveSessionDeliveryMode(e,n,s,o="manifest"){const r=e.sessionDeliveryMode,a=i(s,t(),n.hasExplicitMode?n.mode:null,o);null!==a.unknownMode&&n.diagnose("DA-DELIVERY-MODE-UNKNOWN",`Delivery rule ${a.ruleIndex} carries unrecognized mode "${a.unknownMode}"; using sgai.`,{unknownMode:a.unknownMode,ruleIndex:a.ruleIndex});for(const e of a.targetingIssues)n.diagnose(e.code,`Targeting ${e.field} was rejected.`,{field:e.field,...void 0===e.value?{}:{value:e.value}});return n.hasExplicitMode&&a.manifestMode!==a.mode&&n.diagnose("DA-DELIVERY-MODE-OVERRIDDEN",`Manifest delivery rules resolve "${a.manifestMode}" but the explicit config mode "${a.mode}" takes precedence.`,{manifestMode:a.manifestMode,configuredMode:a.mode}),e.sessionDeliveryMode=a.mode,n.diagnose("DA-DELIVERY-MODE-RESOLVED",`Delivery mode: ${a.mode}`,{mode:a.mode,ruleIndex:a.ruleIndex,source:a.source,deviceProfile:t(),..."manifest"!==o?{previousMode:r}:{}}),a.mode}export function isStaleSessionStart(e,n,t){return t!==e.sessionGeneration&&(n.log("Session start aborted — the session ended or was replaced while starting"),!0)}export function emitChannelChange(e,n){e.emit("adchannelchange",{type:"adchannelchange",timestamp:Date.now(),...n})}export function adoptExternalVendorConfigurations(e,n){e.externalVendorConfigurationTracker.adopt(n)}export function diagnoseExternalVendorConfigurations(e,t){const s=n(t);s.invalidCount>0&&e.diagnose("DA-EXTERNAL-VENDOR-INVALID",`Ignored ${s.invalidCount} invalid external vendorConfiguration `+(1===s.invalidCount?"entry":"entries")+".",{count:s.invalidCount}),s.duplicateProviders.length>0&&e.diagnose("DA-EXTERNAL-VENDOR-DUPLICATE",`Ignored duplicate external vendorConfiguration entries for providers: ${s.duplicateProviders.join(", ")}.`,{providers:s.duplicateProviders})}export function handleEventBoundaryTick(n,s,d,u){const c=n.sessionManifest;if(!n.sessionActive||!c||c.timebase!==e.WALLCLOCK||!1===s.player.isLive||null===d||!Number.isFinite(d))return!0;const v=n.pendingManifestUpdate,m=v??c,p=resolveEventAtPosition(m,d,s.player),y=p.eventId!==n.activeEventId;if(y){if(n.channelSwitchInFlight)return!1;if(s.breakScheduler.getActiveBreak())return!0}if(!y)return v&&(n.pendingManifestUpdate=null,n.effectiveSessionManifest=p.manifest,u.applyManifestUpdate(p.manifest)),function(e,n,s,o){const d=a(s,o),l=null===d?null:d-o;if(null===d||null===l||l<0||l>8e3||e.steeredSsaiActive)return void n.adPlayerController.gamPodBridge.discardPreparedGamSession();const u=resolveEventAtPosition(s,o,n.player).manifest,c=resolveEventAtPosition(s,d,n.player).manifest,v=n.hasExplicitMode?n.mode:null,m=i(u,t(),v,"event-boundary").mode,p=i(c,t(),v,"event-boundary").mode,y=f(u),g=f(c);if("sgai"!==m||"sgai"!==p||!g.identity||y.identity?.customAssetKey===g.identity.customAssetKey||!r(u,c)||!function(e,n){return e.breaks.some(e=>{const t="string"==typeof e.start?Date.parse(e.start):Number.NaN,s=Array.isArray(e.variant)?e.variant:[e.variant];return Number.isFinite(t)&&t>=n&&t<=n+8e3&&s.some(e=>e.assets.some(e=>"vendor"===e.type&&"gam"===e.vendor))})}(c,d))return void n.adPlayerController.gamPodBridge.discardPreparedGamSession();const M=g.identity,h=`${d}:${M.networkCode}:${M.customAssetKey}`;if(e.eventSessionPreloadKeys.has(h))return;e.eventSessionPreloadKeys.add(h),n.adPlayerController.gamPodBridge.prepareNextGamSession(M,g.sgaiAssetParameters).then(e=>{e&&n.diagnose("DA-GAM-SESSION-PRELOADED","GAM session preloaded for the next event.",{boundaryMs:d,networkCode:M.networkCode,customAssetKey:M.customAssetKey})}).catch(e=>{n.log("Next-event GAM session preload failed",e)})}(n,s,m,d),!0;const g=n.effectiveSessionManifest??c,M=i(p.manifest,t(),s.hasExplicitMode?s.mode:null,"event-boundary").mode,h=o(g,p.manifest)??(M!==n.sessionDeliveryMode?{previousChannelId:g.channelId??null,channelId:p.manifest.channelId??g.channelId??null,reason:"event-boundary"}:null);if(v&&(n.pendingManifestUpdate=null),s.breakScheduler.holdBreakStarts(),n.activeEventId=p.eventId,h)return u.switchChannel(p.manifest,{...h,reason:"event-boundary"},!0,m),!1;n.effectiveSessionManifest=p.manifest;try{!function(e,n,t,s,i,o){const r=e.sessionDeliveryMode??"sgai",a=o.resolveDeliveryMode(s);adoptExternalVendorConfigurations(n,s);const d=f(s);e.steeredSsaiActive?e.ssaiController?.setManifestAssetParameters(d.ssai?.assetParameters):n.adPlayerController.setManifestAssetParameters(d.sgaiAssetParameters);const u=e.steeredSsaiActive?l(s):s;n.breakScheduler.applyEventBoundary(u),n.pauseAdController.updateManifest(u),n.cmcdAdReporter.updateEventId(i);const c=s.channelId??t.channelId??null;emitChannelChange(n,{previousChannelId:t.channelId??c,channelId:c,eventId:i,reason:"event-boundary",manifestUrl:e.sessionConfig.manifestUrl,previousDeliveryMode:r,deliveryMode:a})}(n,s,g,p.manifest,p.eventId,u)}finally{s.breakScheduler.releaseBreakStarts()}return!0}
|
|
@@ -1,7 +1,22 @@
|
|
|
1
|
-
import { OptiViewAdsMode, SessionConfig } from '../types/OptiViewAdsConfig';
|
|
1
|
+
import { OptiViewAdsMode, SessionConfig, SessionDeliveryMode } from '../types/OptiViewAdsConfig';
|
|
2
2
|
import { BreakManifest } from '../types/BreakManifest';
|
|
3
|
+
import type { ExternalPlaybackAllowedTrigger } from '../services/ExternalPlaybackGate';
|
|
4
|
+
import type { SsbSwitchPhase } from '../services/ssbSwitchSignals';
|
|
3
5
|
import { SsaiController } from '../services/SsaiController';
|
|
4
6
|
import { ConfigSummary } from '../diagnostics/DiagnosticEvent';
|
|
7
|
+
export interface SsbSwitchState {
|
|
8
|
+
previousMode: OptiViewAdsMode;
|
|
9
|
+
previousSource: string;
|
|
10
|
+
previousPosition: number;
|
|
11
|
+
previousLive: boolean;
|
|
12
|
+
switchedAt: number;
|
|
13
|
+
/** What opened the gate: the request itself or the retry after a break. */
|
|
14
|
+
trigger: ExternalPlaybackAllowedTrigger;
|
|
15
|
+
phase: SsbSwitchPhase;
|
|
16
|
+
activeAt: number | null;
|
|
17
|
+
/** Set once the `adchannelchange` into `ssb` went out; the restore emits its counterpart only then. */
|
|
18
|
+
announced: boolean;
|
|
19
|
+
}
|
|
5
20
|
/** Mutable session state owned by `OptiViewAds` and shared by reference with the `session/` modules. */
|
|
6
21
|
export declare class SessionState {
|
|
7
22
|
sessionActive: boolean;
|
|
@@ -12,7 +27,8 @@ export declare class SessionState {
|
|
|
12
27
|
*/
|
|
13
28
|
sessionGeneration: number;
|
|
14
29
|
adStartDelayElapsed: boolean;
|
|
15
|
-
sessionDeliveryMode:
|
|
30
|
+
sessionDeliveryMode: SessionDeliveryMode | null;
|
|
31
|
+
ssb: SsbSwitchState | null;
|
|
16
32
|
steeredSsaiActive: boolean;
|
|
17
33
|
steeredSsaiOptions: {
|
|
18
34
|
maxBitrate: number | undefined;
|
|
@@ -35,6 +51,10 @@ export declare class SessionState {
|
|
|
35
51
|
snapbackSeekInFlight: boolean;
|
|
36
52
|
sessionConfig: SessionConfig | null;
|
|
37
53
|
sessionManifest: BreakManifest | null;
|
|
54
|
+
effectiveSessionManifest: BreakManifest | null;
|
|
55
|
+
activeEventId: string | null;
|
|
56
|
+
eventSessionPreloadKeys: Set<string>;
|
|
57
|
+
pendingManifestUpdate: BreakManifest | null;
|
|
38
58
|
channelSwitchInFlight: boolean;
|
|
39
59
|
pendingChannelSwitch: BreakManifest | null;
|
|
40
60
|
clearSessionIdentity(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export class SessionState{constructor(){this.sessionActive=!1,this.sessionGeneration=0,this.adStartDelayElapsed=!1,this.sessionDeliveryMode=null,this.steeredSsaiActive=!1,this.steeredSsaiOptions=null,this.pauseAdTickInterval=null,this.heldPreRollArmInterval=null,this.manifestOrigin=null,this.stitcherOrigin=null,this.heldImmediatePreRollId=null,this.heldPreRollArmed=!1,this.heldPreRollWaitStartedAt=null,this.snapbackSeekInFlight=!1,this.sessionConfig=null,this.sessionManifest=null,this.channelSwitchInFlight=!1,this.pendingChannelSwitch=null}clearSessionIdentity(){this.sessionDeliveryMode=null,this.sessionManifest=null,this.channelSwitchInFlight=!1,this.pendingChannelSwitch=null}}
|
|
1
|
+
export class SessionState{constructor(){this.sessionActive=!1,this.sessionGeneration=0,this.adStartDelayElapsed=!1,this.sessionDeliveryMode=null,this.ssb=null,this.steeredSsaiActive=!1,this.steeredSsaiOptions=null,this.pauseAdTickInterval=null,this.heldPreRollArmInterval=null,this.manifestOrigin=null,this.stitcherOrigin=null,this.heldImmediatePreRollId=null,this.heldPreRollArmed=!1,this.heldPreRollWaitStartedAt=null,this.snapbackSeekInFlight=!1,this.sessionConfig=null,this.sessionManifest=null,this.effectiveSessionManifest=null,this.activeEventId=null,this.eventSessionPreloadKeys=new Set,this.pendingManifestUpdate=null,this.channelSwitchInFlight=!1,this.pendingChannelSwitch=null}clearSessionIdentity(){this.sessionDeliveryMode=null,this.sessionManifest=null,this.effectiveSessionManifest=null,this.activeEventId=null,this.eventSessionPreloadKeys.clear(),this.pendingManifestUpdate=null,this.channelSwitchInFlight=!1,this.pendingChannelSwitch=null}}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ExternalPlaybackAllowedTrigger } from '../services/ExternalPlaybackGate';
|
|
2
|
+
import type { BreakManifest } from '../types/BreakManifest';
|
|
3
|
+
import type { SessionConfig } from '../types/OptiViewAdsConfig';
|
|
4
|
+
import type { SessionStarterDeps } from './SessionStarterDeps';
|
|
5
|
+
import type { SessionState } from './SessionState';
|
|
6
|
+
/** How long the receiver gets to take the stream once it is ready to play. */
|
|
7
|
+
export declare const SSB_ACTIVE_TIMEOUT_MS = 1500;
|
|
8
|
+
/** Cap on loading the SSB stream (a cold DAI session needs a couple of seconds). */
|
|
9
|
+
export declare const SSB_LOAD_TIMEOUT_MS = 15000;
|
|
10
|
+
export declare const SSB_RECEIVER_PAUSE_WINDOW_MS = 3000;
|
|
11
|
+
export declare const SSB_RESTORE_PLAYING_BACKSTOP_MS = 10000;
|
|
12
|
+
export declare const SSB_RESTORE_ATTEMPTS = 2;
|
|
13
|
+
/** The session re-entry points the restore reuses from the channel-switch code. */
|
|
14
|
+
export interface SsbSessionStarter {
|
|
15
|
+
switchToSgai(manifest: BreakManifest, config: SessionConfig, generation: number, preserveSchedulerLedgers: boolean): Promise<void>;
|
|
16
|
+
switchToSteeredSsai(manifest: BreakManifest, config: SessionConfig, preserveSchedulerLedgers: boolean): Promise<boolean>;
|
|
17
|
+
applyPendingChannelSwitch(): void;
|
|
18
|
+
}
|
|
19
|
+
type SwitchTrigger = ExternalPlaybackAllowedTrigger;
|
|
20
|
+
/**
|
|
21
|
+
* Plays the channel's SSB stream on the AirPlay receiver and brings the original content back
|
|
22
|
+
* when AirPlay ends: switch behind the closed gate, open it, verify the receiver took the
|
|
23
|
+
* stream, restore the previous source and delivery mode afterwards.
|
|
24
|
+
*/
|
|
25
|
+
export declare class SsbSwitchController {
|
|
26
|
+
private readonly state;
|
|
27
|
+
private readonly starter;
|
|
28
|
+
private readonly deps;
|
|
29
|
+
private timers;
|
|
30
|
+
private runtimeParams;
|
|
31
|
+
private sessionMacros;
|
|
32
|
+
private previousPaused;
|
|
33
|
+
private streamReady;
|
|
34
|
+
constructor(state: SessionState, starter: SsbSessionStarter, deps: SessionStarterDeps);
|
|
35
|
+
/** True from the switch until the restore completes; the session's other content handlers stand down. */
|
|
36
|
+
get isActive(): boolean;
|
|
37
|
+
/** The `ExternalPlaybackGate` allowed hook. */
|
|
38
|
+
onAllowed: (trigger: SwitchTrigger) => void;
|
|
39
|
+
dispose(): void;
|
|
40
|
+
/** The session ended: forget the switch without touching the player. */
|
|
41
|
+
reset(): void;
|
|
42
|
+
private readonly onExternalPlaybackChange;
|
|
43
|
+
private readonly onPause;
|
|
44
|
+
private markActive;
|
|
45
|
+
private fail;
|
|
46
|
+
private restoreFromSsb;
|
|
47
|
+
private restoreSgai;
|
|
48
|
+
private reloadPrevious;
|
|
49
|
+
private waitForPlaying;
|
|
50
|
+
private rollSession;
|
|
51
|
+
private emitChange;
|
|
52
|
+
private later;
|
|
53
|
+
private clearTimers;
|
|
54
|
+
}
|
|
55
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{buildAssetParameterMacroTable as e}from"../services/assetParameterMacroTable";import{resolveSsbStream as s}from"../services/resolveGamVendorConfiguration";import{resolveSsbRestoreChange as t}from"../services/ssbSwitchSignals";import{buildSsbStreamUrl as i,resolveSsbRestorePosition as r}from"../services/ssbStream";export const SSB_ACTIVE_TIMEOUT_MS=1500;export const SSB_LOAD_TIMEOUT_MS=15e3;export const SSB_RECEIVER_PAUSE_WINDOW_MS=3e3;export const SSB_RESTORE_PLAYING_BACKSTOP_MS=1e4;export const SSB_RESTORE_ATTEMPTS=2;export class SsbSwitchController{constructor(t,r,a){this.state=t,this.starter=r,this.deps=a,this.timers=new Set,this.previousPaused=!1,this.streamReady=!1,this.onAllowed=t=>{const{state:r}=this,{player:a}=this.deps,o=r.effectiveSessionManifest??r.sessionManifest,n=o?s(o.vendorConfiguration):null,l=r.sessionConfig;if(!r.sessionActive||r.ssb||!o||!n||!l)return;const c=a.sourceUrl;if("string"!=typeof c||""===c.trim())return void this.deps.diagnose("DA-SSB-SWITCH-FAILED","The content adapter reports no source to restore; AirPlay stays on the device.",{reason:"no-restore-source",trigger:t});const h=this.deps.adPlayerController;this.runtimeParams=h?.updatedAssetParameters,this.sessionMacros=h?.sessionAssetParameterMacros,this.previousPaused=a.paused;const d=r.sessionDeliveryMode??"sgai";r.ssb={previousMode:d,previousSource:c,previousPosition:a.currentTime,previousLive:a.isLive??!Number.isFinite(a.duration),switchedAt:Date.now(),trigger:t,phase:"switching",activeAt:null,announced:!1},h?.stopActiveBreak(),h?.releaseContentHold(),h?.endGamSession(),r.steeredSsaiActive&&(r.ssaiController?.stop(),delete r.ssaiController,r.steeredSsaiActive=!1),this.deps.breakScheduler?.suspend(),this.deps.pauseAdController?.reset(),r.sessionDeliveryMode="ssb",this.deps.diagnose("DA-DELIVERY-MODE-RESOLVED","AirPlay started: playing the SSB stream",{mode:"ssb",source:"external-playback",previousMode:d,trigger:t}),this.rollSession(o);const p=i(n,{manifest:n.assetParameters,session:l.assetParameters,runtime:this.runtimeParams},e({customerMacros:h?.sessionAssetParameterMacros??{},element:null,breakDurationSec:void 0})),u=r.ssb;this.streamReady=!1;const m=()=>r.ssb===u&&"switching"===u.phase,S=()=>{this.streamReady=!0,m()&&("active"!==a.externalPlaybackState?this.later(()=>{m()&&("active"===a.externalPlaybackState?this.markActive(u):this.fail("external-not-active",t,p))},1500):this.markActive(u))};a.load(p).then(()=>(S(),this.previousPaused||r.ssb!==u||"restoring"===u.phase?void 0:a.play())).catch(e=>this.fail("load-error",t,p,e)),a.setExternalPlaybackAllowed?.(!0),this.later(()=>{m()&&!this.streamReady&&this.fail("load-timeout",t,p)},15e3)},this.onExternalPlaybackChange=e=>{const{state:s}=this,t=s.ssb,i=e;t&&("active"===i?.state&&"switching"===t.phase&&this.streamReady?this.markActive(t):"inactive"===i?.state&&"restoring"!==t.phase&&this.restoreFromSsb("external-playback-ended"))},this.onPause=()=>{const e=this.state.ssb;"active"===e?.phase&&null!==e.activeAt&&Date.now()-e.activeAt<=3e3&&this.fail("receiver-paused",e.trigger)},a.player.on("externalplaybackchange",this.onExternalPlaybackChange),a.player.on("pause",this.onPause)}get isActive(){return null!==this.state.ssb}dispose(){this.clearTimers(),this.state.ssb=null,this.deps.player.off("externalplaybackchange",this.onExternalPlaybackChange),this.deps.player.off("pause",this.onPause)}reset(){this.clearTimers(),this.streamReady=!1,this.state.ssb=null}markActive(e){this.clearTimers(),e.phase="active",e.activeAt=Date.now(),this.later(()=>{this.state.ssb===e&&"active"===e.phase&&(e.announced=!0,this.emitChange(e.previousMode,"ssb"),this.deps.adBreakStatusTracker.refreshDeliveryMode())},3e3)}fail(e,s,t,i){this.state.ssb&&"restoring"!==this.state.ssb.phase&&(this.deps.diagnose("DA-SSB-SWITCH-FAILED",`The SSB stream switch failed: ${e}`,{reason:e,uri:t,trigger:s,...void 0===i?{}:{message:i instanceof Error?i.message:String(i)}}),this.restoreFromSsb("switch-failed"))}async restoreFromSsb(e){const{state:s}=this,{player:i}=this.deps,r=s.ssb,a=s.effectiveSessionManifest??s.sessionManifest,o=s.sessionConfig,{starter:n}=this;if(!(r&&"restoring"!==r.phase&&a&&o&&n))return;r.phase="restoring",this.clearTimers(),i.setExternalPlaybackAllowed?.(!1);const l=s.sessionGeneration,c=this.deps.breakScheduler;c?.setContentReloading(!0);let h=!1;if("ssai"===r.previousMode&&(h=await n.switchToSteeredSsai(a,o,!0)),h||(h=await this.restoreSgai(r,a,o,n,l)),s.ssb!==r)return;if(!h)return r.phase="failed",void c?.setContentReloading(!1);if(await this.waitForPlaying(),s.ssb!==r)return;c?.setContentReloading(!1),c?.skipPendingBreaksDueBy("played-by-ssb"),c?.resume();const d="ssai"===r.previousMode&&s.steeredSsaiActive?"ssai":"sgai";s.sessionDeliveryMode=d,s.ssb=null,this.deps.diagnose("DA-DELIVERY-MODE-RESOLVED","AirPlay ended: content restored",{mode:d,source:"external-playback-ended",cause:e}),this.rollSession(a);const p=t(r.announced,d);p&&this.emitChange(p.previousDeliveryMode,p.deliveryMode),this.deps.adBreakStatusTracker.refreshDeliveryMode(),n.applyPendingChannelSwitch()}async restoreSgai(e,s,t,i,a){const{player:o}=this.deps,{state:n}=this;if(!await this.reloadPrevious(e.previousSource))return!1;if(n.ssb!==e)return!0;const l=Number.isFinite(o.duration)?o.duration:null,c=r({live:e.previousLive,recordedPosition:e.previousPosition,duration:l});null!==c&&o.seek(c),this.previousPaused||await o.play().catch(()=>{}),await i.switchToSgai(s,t,a,!0);const h=this.deps.adPlayerController;return this.sessionMacros&&h?.setSessionAssetParameterMacros(this.sessionMacros),this.runtimeParams&&h?.updateAssetParameters(this.runtimeParams),!0}async reloadPrevious(e){let s;for(let t=1;t<=2;t++)try{return await this.deps.player.load(e),!0}catch(e){s=e}return this.deps.diagnose("DA-SSB-RESTORE-FAILED","The previous content source could not be reloaded.",{attempt:2,reason:"load-error",source:e,message:s instanceof Error?s.message:String(s)}),!1}waitForPlaying(){return new Promise(e=>{const s=()=>{this.deps.player.off("playing",s),e()};this.deps.player.on("playing",s),this.later(s,1e4)})}rollSession(e){this.deps.cmcdAdReporter.onSessionEnd(),this.deps.cmcdAdReporter.onSessionStart(e),this.deps.lensReporter.endSession(),this.deps.lensReporter.startSession()}emitChange(e,s){const{state:t}=this,i=t.effectiveSessionManifest??t.sessionManifest,r=i?.channelId??null;this.deps.emit("adchannelchange",{type:"adchannelchange",timestamp:Date.now(),previousChannelId:r,channelId:r,eventId:t.activeEventId,reason:"external-playback",manifestUrl:t.sessionConfig?.manifestUrl??"",previousDeliveryMode:e,deliveryMode:s})}later(e,s){const t=setTimeout(()=>{this.timers.delete(t),e()},s);this.timers.add(t)}clearTimers(){for(const e of this.timers)clearTimeout(e);this.timers.clear()}}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BreakTransitionTimer } from '../services/BreakTransitionTimer';
|
|
2
|
+
import { BreakChainRecorder } from '../services/BreakChainRecorder';
|
|
3
|
+
import type { KnownDiagnosticCode } from '../diagnostics/errorCodes';
|
|
4
|
+
type Diagnose = (code: KnownDiagnosticCode, message: string, context?: Record<string, unknown>) => void;
|
|
5
|
+
/**
|
|
6
|
+
* The transition timer (each transition a `DA-BREAK-TRANSITION`) and the chain recorder (each
|
|
7
|
+
* chain a `DA-BREAK-CHAIN`) it feeds, so transition cost is comparable across preload modes and devices.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createBreakMeasurement(diagnose: Diagnose, modes: {
|
|
10
|
+
adPreload: string;
|
|
11
|
+
adInsertion: string;
|
|
12
|
+
}): {
|
|
13
|
+
timer: BreakTransitionTimer;
|
|
14
|
+
recorder: BreakChainRecorder;
|
|
15
|
+
};
|
|
16
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{BreakTransitionTimer as e}from"../services/BreakTransitionTimer";import{BreakChainRecorder as a}from"../services/BreakChainRecorder";export function createBreakMeasurement(r,n){const s=new a(e=>{r("DA-BREAK-CHAIN",`Chain ${e.chainId}: ${e.breaks.length} break(s) in ${e.totalMs}ms`,{breakId:e.chainId,...e})});return{timer:new e(e=>{r("DA-BREAK-TRANSITION",`Transition ${e.kind} took ${e.durationMs}ms`,{transition:e.kind,durationMs:e.durationMs,breakId:e.breakId,adPreload:n.adPreload,adInsertion:n.adInsertion,...e.phases?{phases:e.phases}:{},...e.path?{path:e.path}:{},...void 0!==e.leadMs?{leadMs:e.leadMs}:{}}),s.transitionMeasured(e)}),recorder:s}}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PlayerAdapter } from '../interfaces/PlayerAdapter';
|
|
2
|
+
import type { Break } from '../types/BreakManifest';
|
|
3
|
+
import type { SessionState } from './SessionState';
|
|
4
|
+
export interface SnapbackDeps {
|
|
5
|
+
player: PlayerAdapter;
|
|
6
|
+
state: Pick<SessionState, 'snapbackSeekInFlight'>;
|
|
7
|
+
activeBreak(): Break | null | undefined;
|
|
8
|
+
/** A viewer seek (not our own corrective one) landed. */
|
|
9
|
+
onViewerSeek(): void;
|
|
10
|
+
log(...args: unknown[]): void;
|
|
11
|
+
}
|
|
12
|
+
/** The content `seeked` handler: snaps a viewer seek back to the break start while the break locks it. */
|
|
13
|
+
export declare function createSnapbackHandler(deps: SnapbackDeps): () => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function createSnapbackHandler(e){return()=>{if(e.state.snapbackSeekInFlight)return void(e.state.snapbackSeekInFlight=!1);e.onViewerSeek();const t=e.activeBreak();if(!t?.controls?.snapback)return;const a="number"==typeof t.start?t.start:e.player.currentTime;Math.abs(e.player.currentTime-a)<=.5||(e.log("Snapback: seek detected during locked break, snapping back to break start"),e.state.snapbackSeekInFlight=!0,e.player.seek(a))}}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PlayerAdapter } from '../interfaces/PlayerAdapter';
|
|
2
|
+
import type { KnownDiagnosticCode } from '../diagnostics/errorCodes';
|
|
3
|
+
import type { BreakScheduler } from '../services/BreakScheduler';
|
|
4
|
+
import { ExternalPlaybackGate, type ExternalPlaybackAllowedTrigger } from '../services/ExternalPlaybackGate';
|
|
5
|
+
import type { SsbSwitchPhase } from '../services/ssbSwitchSignals';
|
|
6
|
+
import type { ExternalPlaybackConfig } from '../types/OptiViewAdsConfig';
|
|
7
|
+
export declare const DEFAULT_EXTERNAL_PLAYBACK_BLOCK_BEFORE_BREAK_SEC = 5;
|
|
8
|
+
/** Non-finite values fall back to the default; a negative window means "only during a break". */
|
|
9
|
+
export declare function resolveBlockBeforeBreakSec(config: ExternalPlaybackConfig | undefined): number;
|
|
10
|
+
export interface ExternalPlaybackGateWiring {
|
|
11
|
+
scheduler(): BreakScheduler | undefined;
|
|
12
|
+
diagnose(code: KnownDiagnosticCode, message: string, context?: Record<string, unknown>): void;
|
|
13
|
+
/** Runs after the allowed diagnostic; the SSB switch. */
|
|
14
|
+
onAllowed?(trigger: ExternalPlaybackAllowedTrigger): void;
|
|
15
|
+
/** The phase of the SSB switch in progress; defaults to none. */
|
|
16
|
+
ssbSwitchPhase?(): SsbSwitchPhase | null;
|
|
17
|
+
}
|
|
18
|
+
/** The session's AirPlay gate, fed by the break scheduler; the allowed hook only reports for now. */
|
|
19
|
+
export declare function createExternalPlaybackGate(player: PlayerAdapter, config: ExternalPlaybackConfig | undefined, wiring: ExternalPlaybackGateWiring): ExternalPlaybackGate;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{ExternalPlaybackGate as e}from"../services/ExternalPlaybackGate";export const DEFAULT_EXTERNAL_PLAYBACK_BLOCK_BEFORE_BREAK_SEC=5;export function resolveBlockBeforeBreakSec(e){const r=e?.blockBeforeBreakSec;return"number"==typeof r&&Number.isFinite(r)?Math.max(0,r):5}export function createExternalPlaybackGate(r,n,t){return new e({player:r,blockBeforeBreakSec:resolveBlockBeforeBreakSec(n),diagnose:t.diagnose,guardInput:()=>{const e=t.scheduler(),r=e?.getNextPendingBreak()??null;return{activeBreakId:e?.getActiveBreak()?.id??null,nextBreakId:r?.id??null,secondsUntilNextBreak:r?.secondsUntil??null}},ssbSwitchPhase:()=>t.ssbSwitchPhase?.()??null,onAllowed:e=>{t.diagnose("DA-EXTERNAL-PLAYBACK-ALLOWED",`AirPlay allowed (${e})`,{trigger:e}),t.onAllowed?.(e)}})}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AdBreakStatusTracker } from './AdBreakStatusTracker';
|
|
2
|
+
import type { PauseAdController } from '../services/PauseAdController';
|
|
3
|
+
/** The 250 ms session tick: advances the pause-ad timer and the AirPlay guard's `adbreakstatus`. */
|
|
4
|
+
export declare function sessionTick(deps: {
|
|
5
|
+
pauseAdController?: PauseAdController | undefined;
|
|
6
|
+
adBreakStatusTracker: AdBreakStatusTracker;
|
|
7
|
+
}): () => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function sessionTick(r){return()=>{r.pauseAdController?.tick(),r.adBreakStatusTracker.refreshExternalPlayback()}}
|