@dolby-optiview/ads-sdk-core 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/OptiViewAds.d.ts +190 -0
- package/dist/OptiViewAds.js +1 -0
- package/dist/config/keys.d.ts +23 -0
- package/dist/config/keys.js +1 -0
- package/dist/diagnostics/DiagnosticBuffer.d.ts +33 -0
- package/dist/diagnostics/DiagnosticBuffer.js +1 -0
- package/dist/diagnostics/DiagnosticEvent.d.ts +71 -0
- package/dist/diagnostics/DiagnosticEvent.js +1 -0
- package/dist/diagnostics/debugLog.d.ts +8 -0
- package/dist/diagnostics/debugLog.js +1 -0
- package/dist/diagnostics/errorCodes.d.ts +45 -0
- package/dist/diagnostics/errorCodes.js +1 -0
- package/dist/diagnostics/index.d.ts +6 -0
- package/dist/diagnostics/index.js +1 -0
- package/dist/diagnostics/playbackErrorContext.d.ts +22 -0
- package/dist/diagnostics/playbackErrorContext.js +1 -0
- package/dist/generated/diagnosticCodes.d.ts +487 -0
- package/dist/generated/diagnosticCodes.js +1 -0
- package/dist/generated/version.d.ts +5 -0
- package/dist/generated/version.js +1 -0
- package/dist/id3/buildId3.d.ts +25 -0
- package/dist/id3/buildId3.js +1 -0
- package/dist/id3/index.d.ts +3 -0
- package/dist/id3/index.js +1 -0
- package/dist/id3/parseId3Geob.d.ts +28 -0
- package/dist/id3/parseId3Geob.js +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +1 -0
- package/dist/interfaces/PlayerAdapter.d.ts +163 -0
- package/dist/interfaces/PlayerAdapter.js +1 -0
- package/dist/internal/index.d.ts +1 -0
- package/dist/internal/index.js +1 -0
- package/dist/server/index.d.ts +20 -0
- package/dist/server/index.js +1 -0
- package/dist/services/AdBreakSequencer.d.ts +146 -0
- package/dist/services/AdBreakSequencer.js +1 -0
- package/dist/services/AdPlayerController.d.ts +495 -0
- package/dist/services/AdPlayerController.js +1 -0
- package/dist/services/BreakScheduler.d.ts +215 -0
- package/dist/services/BreakScheduler.js +1 -0
- package/dist/services/BreakTransitionTimer.d.ts +48 -0
- package/dist/services/BreakTransitionTimer.js +1 -0
- package/dist/services/CmcdAdReporter.d.ts +81 -0
- package/dist/services/CmcdAdReporter.js +1 -0
- package/dist/services/GamPodAdTracker.d.ts +65 -0
- package/dist/services/GamPodAdTracker.js +1 -0
- package/dist/services/GamPodUrlBuilder.d.ts +30 -0
- package/dist/services/GamPodUrlBuilder.js +1 -0
- package/dist/services/GamStreamManager.d.ts +67 -0
- package/dist/services/GamStreamManager.js +1 -0
- package/dist/services/LensReporter.d.ts +30 -0
- package/dist/services/LensReporter.js +1 -0
- package/dist/services/ManifestService.d.ts +91 -0
- package/dist/services/ManifestService.js +1 -0
- package/dist/services/MetadataCueTracker.d.ts +44 -0
- package/dist/services/MetadataCueTracker.js +1 -0
- package/dist/services/PauseAdController.d.ts +108 -0
- package/dist/services/PauseAdController.js +1 -0
- package/dist/services/RewindRearm.d.ts +109 -0
- package/dist/services/RewindRearm.js +1 -0
- package/dist/services/SsaiController.d.ts +59 -0
- package/dist/services/SsaiController.js +1 -0
- package/dist/services/StitcherUrlBuilder.d.ts +34 -0
- package/dist/services/StitcherUrlBuilder.js +1 -0
- package/dist/services/VastAdManager.d.ts +89 -0
- package/dist/services/VastAdManager.js +1 -0
- package/dist/services/adPlayer/AdPlayerEvents.d.ts +39 -0
- package/dist/services/adPlayer/AdPlayerEvents.js +1 -0
- package/dist/services/adPlayer/AdPlayerState.d.ts +98 -0
- package/dist/services/adPlayer/AdPlayerState.js +1 -0
- package/dist/services/adPlayer/AdPreloader.d.ts +54 -0
- package/dist/services/adPlayer/AdPreloader.js +1 -0
- package/dist/services/adPlayer/AdStage.d.ts +57 -0
- package/dist/services/adPlayer/AdStage.js +1 -0
- package/dist/services/adPlayer/AssetParameterResolver.d.ts +44 -0
- package/dist/services/adPlayer/AssetParameterResolver.js +1 -0
- package/dist/services/adPlayer/AssetPlayback.d.ts +71 -0
- package/dist/services/adPlayer/AssetPlayback.js +1 -0
- package/dist/services/adPlayer/BreakSequenceDriver.d.ts +46 -0
- package/dist/services/adPlayer/BreakSequenceDriver.js +1 -0
- package/dist/services/adPlayer/ContentResumeCoordinator.d.ts +44 -0
- package/dist/services/adPlayer/ContentResumeCoordinator.js +1 -0
- package/dist/services/adPlayer/ContinueContentPlayout.d.ts +26 -0
- package/dist/services/adPlayer/ContinueContentPlayout.js +1 -0
- package/dist/services/adPlayer/GamPodBridge.d.ts +66 -0
- package/dist/services/adPlayer/GamPodBridge.js +1 -0
- package/dist/services/adPlayer/PauseAdPresenter.d.ts +60 -0
- package/dist/services/adPlayer/PauseAdPresenter.js +1 -0
- package/dist/services/adPlayer/PresentationModeTracker.d.ts +66 -0
- package/dist/services/adPlayer/PresentationModeTracker.js +1 -0
- package/dist/services/adPlayer/SharedElementBreakPlayer.d.ts +54 -0
- package/dist/services/adPlayer/SharedElementBreakPlayer.js +1 -0
- package/dist/services/adPlayer/VariantSelector.d.ts +38 -0
- package/dist/services/adPlayer/VariantSelector.js +1 -0
- package/dist/services/adPlayer/connectPauseAd.d.ts +10 -0
- package/dist/services/adPlayer/connectPauseAd.js +1 -0
- package/dist/services/adPlayer/liveEdge.d.ts +6 -0
- package/dist/services/adPlayer/liveEdge.js +1 -0
- package/dist/services/assetParameterMacroTable.d.ts +9 -0
- package/dist/services/assetParameterMacroTable.js +1 -0
- package/dist/services/assetParameterMacros.d.ts +69 -0
- package/dist/services/assetParameterMacros.js +1 -0
- package/dist/services/breakTiming.d.ts +60 -0
- package/dist/services/breakTiming.js +1 -0
- package/dist/services/channelSwitch.d.ts +32 -0
- package/dist/services/channelSwitch.js +1 -0
- package/dist/services/detectDeviceType.d.ts +15 -0
- package/dist/services/detectDeviceType.js +1 -0
- package/dist/services/detectInsertionMode.d.ts +38 -0
- package/dist/services/detectInsertionMode.js +1 -0
- package/dist/services/detectPreloadMode.d.ts +13 -0
- package/dist/services/detectPreloadMode.js +1 -0
- package/dist/services/ed25519.d.ts +19 -0
- package/dist/services/ed25519.js +1 -0
- package/dist/services/fetchWithTimeout.d.ts +35 -0
- package/dist/services/fetchWithTimeout.js +1 -0
- package/dist/services/lensBackend.d.ts +16 -0
- package/dist/services/lensBackend.js +1 -0
- package/dist/services/lensBackendDevelopment.d.ts +1 -0
- package/dist/services/lensBackendDevelopment.js +1 -0
- package/dist/services/lensBackendProduction.d.ts +9 -0
- package/dist/services/lensBackendProduction.js +1 -0
- package/dist/services/manifestSignature.d.ts +43 -0
- package/dist/services/manifestSignature.js +1 -0
- package/dist/services/manifestTrustedKeys.d.ts +9 -0
- package/dist/services/manifestTrustedKeys.js +1 -0
- package/dist/services/parseBreakManifest.d.ts +15 -0
- package/dist/services/parseBreakManifest.js +1 -0
- package/dist/services/parseVastCompanion.d.ts +31 -0
- package/dist/services/parseVastCompanion.js +1 -0
- package/dist/services/parseVastLinear.d.ts +36 -0
- package/dist/services/parseVastLinear.js +1 -0
- package/dist/services/pictureInPicture.d.ts +28 -0
- package/dist/services/pictureInPicture.js +1 -0
- package/dist/services/resolveAssetParameters.d.ts +6 -0
- package/dist/services/resolveAssetParameters.js +1 -0
- package/dist/services/resolveDeliveryMode.d.ts +54 -0
- package/dist/services/resolveDeliveryMode.js +1 -0
- package/dist/services/resolveGamVendorConfiguration.d.ts +55 -0
- package/dist/services/resolveGamVendorConfiguration.js +1 -0
- package/dist/services/resolvePauseResumePoint.d.ts +32 -0
- package/dist/services/resolvePauseResumePoint.js +1 -0
- package/dist/services/resolvePollingDecision.d.ts +24 -0
- package/dist/services/resolvePollingDecision.js +1 -0
- package/dist/services/resolveResumePoint.d.ts +38 -0
- package/dist/services/resolveResumePoint.js +1 -0
- package/dist/services/resolveTargetedUri.d.ts +22 -0
- package/dist/services/resolveTargetedUri.js +1 -0
- package/dist/services/resolveVastAdmission.d.ts +34 -0
- package/dist/services/resolveVastAdmission.js +1 -0
- package/dist/services/selectVariant.d.ts +47 -0
- package/dist/services/selectVariant.js +1 -0
- package/dist/services/urlOrigin.d.ts +23 -0
- package/dist/services/urlOrigin.js +1 -0
- package/dist/session/AdBreakStatusTracker.d.ts +49 -0
- package/dist/session/AdBreakStatusTracker.js +1 -0
- package/dist/session/BreakLifecycle.d.ts +62 -0
- package/dist/session/BreakLifecycle.js +1 -0
- package/dist/session/SessionStarter.d.ts +98 -0
- package/dist/session/SessionStarter.js +1 -0
- package/dist/session/SessionState.d.ts +37 -0
- package/dist/session/SessionState.js +1 -0
- package/dist/types/BreakManifest.d.ts +335 -0
- package/dist/types/BreakManifest.js +1 -0
- package/dist/types/Events.d.ts +245 -0
- package/dist/types/Events.js +1 -0
- package/dist/types/OptiViewAdsConfig.d.ts +425 -0
- package/dist/types/OptiViewAdsConfig.js +1 -0
- package/dist/types/VendorAdError.d.ts +12 -0
- package/dist/types/VendorAdError.js +1 -0
- package/dist/vendor/lens-sdk/THIRD-PARTY-NOTICES.txt +266 -0
- package/dist/vendor/lens-sdk/compat.d.ts +9 -0
- package/dist/vendor/lens-sdk/encoding-cmcd.d.ts +9 -0
- package/dist/vendor/lens-sdk/encoding-json.d.ts +9 -0
- package/dist/vendor/lens-sdk/encoding.d.ts +42 -0
- package/dist/vendor/lens-sdk/environment.d.ts +35 -0
- package/dist/vendor/lens-sdk/index.d.ts +3 -0
- package/dist/vendor/lens-sdk/index.js +1 -0
- package/dist/vendor/lens-sdk/legacy.d.ts +12 -0
- package/dist/vendor/lens-sdk/lens-core-Q5lbQGMC.js +1 -0
- package/dist/vendor/lens-sdk/lens-global.d.ts +23 -0
- package/dist/vendor/lens-sdk/lens.d.ts +21 -0
- package/dist/vendor/lens-sdk/transport.d.ts +46 -0
- package/dist/vendor/lens-sdk/types.d.ts +218 -0
- package/dist/vendor/lens-sdk/user-id.d.ts +5 -0
- package/dist/vendor/lens-sdk/validation.d.ts +30 -0
- package/package.json +57 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { PlayerAdapter } from '../interfaces/PlayerAdapter';
|
|
2
|
+
import { Break, BreakManifest } from '../types/BreakManifest';
|
|
3
|
+
import { type ResolvedBreakWarnings } from '../types/OptiViewAdsConfig';
|
|
4
|
+
import type { MetadataCueTracker } from './MetadataCueTracker';
|
|
5
|
+
/** Wallclock time source, so a driver (e.g. the conformance harness) can step time deterministically. */
|
|
6
|
+
export interface Clock {
|
|
7
|
+
/** Current wallclock time in milliseconds since the epoch. */
|
|
8
|
+
now(): number;
|
|
9
|
+
}
|
|
10
|
+
/** Default {@link Clock} backed by `Date.now()`. */
|
|
11
|
+
export declare const systemClock: Clock;
|
|
12
|
+
export declare enum BreakState {
|
|
13
|
+
/** Break is known but not yet due */
|
|
14
|
+
PENDING = "pending",
|
|
15
|
+
/** Claimed as part of a chain; the chain advance plays it, so the time-based check must not trigger it. */
|
|
16
|
+
CHAINED = "chained",
|
|
17
|
+
/** Break is currently active */
|
|
18
|
+
ACTIVE = "active",
|
|
19
|
+
/** Break has been completed */
|
|
20
|
+
COMPLETED = "completed",
|
|
21
|
+
/** Break was skipped (e.g., already passed) */
|
|
22
|
+
SKIPPED = "skipped"
|
|
23
|
+
}
|
|
24
|
+
/** Tune-in (join-in-progress) configuration. */
|
|
25
|
+
export interface TuneInOptions {
|
|
26
|
+
/** Whether tune-in breaks are detected and triggered. */
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
/** Minimum remaining seconds for a join-in-progress break to still be triggered; below this it is skipped. */
|
|
29
|
+
minBreakDurationSeconds: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* How a break was triggered. `remainingSec` is always the MEASURED time left. `tuneIn`
|
|
33
|
+
* describes the JOIN, not the playback: do not gate the effective duration on it, a viewer
|
|
34
|
+
* inside the start tolerance is also part-way in.
|
|
35
|
+
*/
|
|
36
|
+
export interface BreakStartContext {
|
|
37
|
+
/** True when the viewer joined while the break was already in progress. */
|
|
38
|
+
tuneIn: boolean;
|
|
39
|
+
/** Seconds already elapsed since the break's start (0 for on-time triggers). */
|
|
40
|
+
elapsedSec: number;
|
|
41
|
+
/** Effective remaining duration, in seconds, the break should be presented for. */
|
|
42
|
+
remainingSec: number;
|
|
43
|
+
}
|
|
44
|
+
export type BreakStartCallback = (breakInfo: Break, context: BreakStartContext) => void;
|
|
45
|
+
/**
|
|
46
|
+
* Fired PRELOAD_AHEAD_SECONDS before a break. Return `false` if the preload cannot be accepted
|
|
47
|
+
* now (e.g. an ad is playing); the scheduler retries on the next tick.
|
|
48
|
+
*/
|
|
49
|
+
export type BreakApproachingCallback = (breakInfo: Break) => boolean | void;
|
|
50
|
+
/** Fired once per configured warning threshold before a non-pre-roll break; pre-rolls are exempt. */
|
|
51
|
+
export type BreakWarningCallback = (breakInfo: Break, warningSec: number, secondsUntil: number) => void;
|
|
52
|
+
/**
|
|
53
|
+
* The ACTIVE break disappeared from a polled manifest (spec "early return"): end it now and
|
|
54
|
+
* resume at `break.start + seconds played`. Fired at most once per break.
|
|
55
|
+
*/
|
|
56
|
+
export type BreakAbortedCallback = (breakInfo: Break) => void;
|
|
57
|
+
/** Structured-diagnostic sink (e.g. DA-PDT-MISSING). Optional; absent means silent. */
|
|
58
|
+
export type SchedulerDiagnose = (code: string, message: string, context?: Record<string, unknown>) => void;
|
|
59
|
+
export declare class BreakScheduler {
|
|
60
|
+
private player;
|
|
61
|
+
private breaks;
|
|
62
|
+
private readonly timing;
|
|
63
|
+
private readonly timingDeps;
|
|
64
|
+
private readonly rewind;
|
|
65
|
+
private checkInterval;
|
|
66
|
+
private onBreakStart;
|
|
67
|
+
private onBreakApproaching;
|
|
68
|
+
private onBreakWarning;
|
|
69
|
+
private onBreakAborted;
|
|
70
|
+
/** Break ids whose early-return abort was already notified; polls keep arriving during the teardown. */
|
|
71
|
+
private abortNotifiedBreakIds;
|
|
72
|
+
private preloadedBreakIds;
|
|
73
|
+
private breakWarnings;
|
|
74
|
+
/** Warning thresholds already fired per break, so each `breakWarnings.seconds` value emits once. */
|
|
75
|
+
private breakWarningFired;
|
|
76
|
+
/**
|
|
77
|
+
* Completed break ids. The manifest keeps delivering a completed break, so without this
|
|
78
|
+
* ledger {@link updateManifest} would re-add it as PENDING and replay it (most visibly pre-rolls).
|
|
79
|
+
*/
|
|
80
|
+
private completedBreakIds;
|
|
81
|
+
/** Breaks already reported as DA-BREAK-EVENT-UNSUPPORTED, so a re-poll does not repeat the diagnostic. */
|
|
82
|
+
private unsupportedEventBreakIds;
|
|
83
|
+
/** Wallclock instant content playback ended (spec `event: 'end'`), or null while content plays. */
|
|
84
|
+
private playbackEndedAtMs;
|
|
85
|
+
private debug;
|
|
86
|
+
private chainingEnabled;
|
|
87
|
+
private maxChainGapMs;
|
|
88
|
+
private tuneInEnabled;
|
|
89
|
+
private minBreakDurationMs;
|
|
90
|
+
private clock;
|
|
91
|
+
private diagnose;
|
|
92
|
+
/**
|
|
93
|
+
* Anvato cue tracker (`ptsSource: 'anvatoCue'`): a PTS break's start is the media time of the cue whose
|
|
94
|
+
* `pts` matches; without a matching cue the break stays PENDING. Null when disabled.
|
|
95
|
+
*/
|
|
96
|
+
private cueTracker;
|
|
97
|
+
/** Break ids already armed by a presentation-time cue (one-shot). */
|
|
98
|
+
private anvatoEnteredDiagnosed;
|
|
99
|
+
/** Completed/skipped position breaks, kept so a DVR seek back can re-arm them. Pre-rolls are never recorded. */
|
|
100
|
+
private retiredBreaks;
|
|
101
|
+
/**
|
|
102
|
+
* Lowest timebase position seen this session. A break whose start the playhead approached
|
|
103
|
+
* from BEFORE was caught a tick late (play the remainder); one whose start was never ahead
|
|
104
|
+
* of the playhead is a genuine join-in-progress and must obey the tune-in rules, however
|
|
105
|
+
* close to the start the join landed. The minimum keeps the DVR contract: seeking back re-earns it.
|
|
106
|
+
*/
|
|
107
|
+
private minSeenTime;
|
|
108
|
+
/**
|
|
109
|
+
* Slack for the join discriminator: the first tick lags session start by up to one interval,
|
|
110
|
+
* so a break missed by less than this is boundary noise. A genuine join lands seconds past the start.
|
|
111
|
+
*/
|
|
112
|
+
private readonly JOIN_DISCRIMINATOR_SLACK_MS;
|
|
113
|
+
/**
|
|
114
|
+
* Deliberately larger than start tolerance + the renderer's break-cut margin: a monetized
|
|
115
|
+
* tune-in remainder legitimately runs until ~end + margin, and a tighter escape aborted
|
|
116
|
+
* healthy breaks mid-ad. The escape is the safety net for stuck breaks, so it fires last.
|
|
117
|
+
*/
|
|
118
|
+
private readonly WINDOW_ESCAPE_GRACE_MS;
|
|
119
|
+
/** Break start matching tolerance (ms); 5 seconds covers polling delays. */
|
|
120
|
+
private readonly TIMING_TOLERANCE_MS;
|
|
121
|
+
/** Seconds before break start to fire the preload callback; 8s gives the IMA VAST round-trip runway. */
|
|
122
|
+
private readonly PRELOAD_AHEAD_SECONDS;
|
|
123
|
+
constructor(player: PlayerAdapter, debug?: boolean, chaining?: {
|
|
124
|
+
enabled: boolean;
|
|
125
|
+
maxGapSeconds: number;
|
|
126
|
+
}, clock?: Clock, tuneIn?: TuneInOptions, diagnose?: SchedulerDiagnose | null, breakWarnings?: ResolvedBreakWarnings, cueTracker?: MetadataCueTracker | null, pdtGraceSeconds?: number);
|
|
127
|
+
/**
|
|
128
|
+
* Claim every break that chains after `breakId`: each successor within the chaining gap is
|
|
129
|
+
* marked CHAINED so the time-based check does not trigger it independently. The
|
|
130
|
+
* AdPlayerController drives the chain as each break ends. No-op when chaining is disabled.
|
|
131
|
+
*/
|
|
132
|
+
claimChain(breakId: string): void;
|
|
133
|
+
/** The PENDING/CHAINED break starting within the chaining gap after this break's end, or null. */
|
|
134
|
+
getChainedSuccessor(breakId: string): Break | null;
|
|
135
|
+
/** Promote a CHAINED (or PENDING) break to ACTIVE as the chain advances, so completeBreak() cleans it up. */
|
|
136
|
+
activateChainedBreak(breakId: string): void;
|
|
137
|
+
/** Earliest break starting within the chaining gap after `current`, restricted to `states`. */
|
|
138
|
+
private findSuccessor;
|
|
139
|
+
updateManifest(manifest: BreakManifest): void;
|
|
140
|
+
/**
|
|
141
|
+
* Re-align the schedule to a manifest from a different channel (see `detectChannelSwitch`).
|
|
142
|
+
* Every ledger of the previous channel is dropped so a break id reused by the new channel
|
|
143
|
+
* is not suppressed as "already completed". An ACTIVE break is filtered from the new manifest
|
|
144
|
+
* and ends as an early return. Before playback starts the new manifest's `adStartDelay` applies;
|
|
145
|
+
* after it, the delay in effect is kept.
|
|
146
|
+
*/
|
|
147
|
+
resetForChannelSwitch(manifest: BreakManifest): void;
|
|
148
|
+
/**
|
|
149
|
+
* `event: 'start'` and `event: 'end'` are scheduled here (pause breaks are filtered out before).
|
|
150
|
+
* Unknown events are reported once and dropped, so they never fire as timeline breaks.
|
|
151
|
+
*/
|
|
152
|
+
private isSchedulableEvent;
|
|
153
|
+
private describeStart;
|
|
154
|
+
/**
|
|
155
|
+
* Content playback began (spec `event: 'start'`): pre-rolls may trigger and played time starts
|
|
156
|
+
* counting. Once per session; a replay in the same session does not re-fire pre-rolls.
|
|
157
|
+
*/
|
|
158
|
+
markPlaybackStart(): void;
|
|
159
|
+
/**
|
|
160
|
+
* Content playback ended (spec `event: 'end'`): post-rolls may trigger once their `delay` has
|
|
161
|
+
* elapsed on the wallclock. A completed post-roll never replays (see {@link completedBreakIds}).
|
|
162
|
+
*/
|
|
163
|
+
markPlaybackEnd(): void;
|
|
164
|
+
/** Start monitoring for break start times. */
|
|
165
|
+
start(onBreakStart: BreakStartCallback, onBreakApproaching?: BreakApproachingCallback, onBreakWarning?: BreakWarningCallback, onBreakAborted?: BreakAbortedCallback): void;
|
|
166
|
+
/** One scheduling check. Drivers (tests, conformance harness) call it directly to step time deterministically. */
|
|
167
|
+
tick(): void;
|
|
168
|
+
stop(): void;
|
|
169
|
+
completeBreak(breakId: string): void;
|
|
170
|
+
/** Bracket a content RELOAD so the new stream's start is not read as a backward seek. `false` also discards the stale previous position. */
|
|
171
|
+
setContentReloading(active: boolean): void;
|
|
172
|
+
/**
|
|
173
|
+
* The runtime is about to resume-seek content to `targetSeconds` after a break ending at
|
|
174
|
+
* `breakEndSeconds`. Arms a guard so the small backward step is not read as a DVR seek-back;
|
|
175
|
+
* a genuine seek below the cue still re-arms. PTS timebase only (`seconds * 1000`).
|
|
176
|
+
*/
|
|
177
|
+
notifyResumeSeek(targetSeconds: number, breakEndSeconds: number): void;
|
|
178
|
+
/** A viewer seek through the SDK: drop the resume-seek guard so the step is judged by the normal rules. */
|
|
179
|
+
notifyViewerSeek(): void;
|
|
180
|
+
private cleanupBreaks;
|
|
181
|
+
getActiveBreak(): Break | null;
|
|
182
|
+
/** Rounded seconds until a PENDING break starts, or `null`; refreshes the `upcoming` countdown without polling events. */
|
|
183
|
+
getSecondsUntilBreak(breakId: string): number | null;
|
|
184
|
+
/**
|
|
185
|
+
* Advance the schedule NOW because a cue reached the playhead: the 250ms tick discovers a start
|
|
186
|
+
* ~125ms late on average, an adapter reporting PRESENTATION knows the instant. Runs
|
|
187
|
+
* {@link checkBreaks} so every tick guard applies unchanged. A cue cannot present before the
|
|
188
|
+
* playhead reaches it, so this can only fire on time, never early.
|
|
189
|
+
* Returns the id of the break this armed, or null.
|
|
190
|
+
*/
|
|
191
|
+
notifyCuePresented(pts: number): string | null;
|
|
192
|
+
private checkBreaks;
|
|
193
|
+
/** Pre-rolls fire once `delay` seconds of content have PLAYED since playback start (spec `event: 'start'`). */
|
|
194
|
+
private checkPreRollBreak;
|
|
195
|
+
/**
|
|
196
|
+
* Post-rolls fire `delay` seconds of wallclock after content playback ended (spec `event: 'end'`).
|
|
197
|
+
* An ad-free start still active at that moment keeps the break pending, not consumed.
|
|
198
|
+
*/
|
|
199
|
+
private checkPostRollBreak;
|
|
200
|
+
/** While true no break may start and consumers must not apply break-driven restrictions (e.g. pause ads). */
|
|
201
|
+
isAdStartDelayActive(): boolean;
|
|
202
|
+
private getCurrentTime;
|
|
203
|
+
/** Fire the preload callback at most once per break; a `false` return is not ledgered and retries next tick. */
|
|
204
|
+
private maybePreload;
|
|
205
|
+
private getBreakStartTime;
|
|
206
|
+
/**
|
|
207
|
+
* Side-effect-free read of the timebase position (no PDT grace bookkeeping, no clock
|
|
208
|
+
* fallback). Used to anchor played time when playback starts.
|
|
209
|
+
*/
|
|
210
|
+
private peekPosition;
|
|
211
|
+
destroy(): void;
|
|
212
|
+
/** Fire configured pre-break warnings, each threshold at most once and only while `secondsUntil > 0`. Pre-rolls excluded. */
|
|
213
|
+
private checkWarnings;
|
|
214
|
+
private log;
|
|
215
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{BreakEvent as e,Timebase as t,getBreakEvent as a,getPostRollDelay as i,getPreRollDelay as r,isEventTrigger as s,isPauseBreak as n}from"../types/BreakManifest";import{DEFAULT_BREAK_WARNINGS as d,DEFAULT_PDT_GRACE_SECONDS as o}from"../types/OptiViewAdsConfig";import{PTS_MATCH_TOLERANCE_SECONDS as l}from"./MetadataCueTracker";import{debugLog as h}from"../diagnostics/debugLog";import{createBreakTimingState as k,getBreakStartTime as c,getCurrentTime as u,secondsUntilBreak as b}from"./breakTiming";import{RewindRearm as g}from"./RewindRearm";export const systemClock={now:()=>Date.now()};export var BreakState;!function(e){e.PENDING="pending",e.CHAINED="chained",e.ACTIVE="active",e.COMPLETED="completed",e.SKIPPED="skipped"}(BreakState||(BreakState={}));export class BreakScheduler{constructor(e,t=!1,a={enabled:!1,maxGapSeconds:0},i=systemClock,r={enabled:!1,minBreakDurationSeconds:0},s=null,n=d,l=null,h=o){this.breaks=new Map,this.timing=k(),this.checkInterval=null,this.onBreakStart=null,this.onBreakApproaching=null,this.onBreakWarning=null,this.onBreakAborted=null,this.abortNotifiedBreakIds=new Set,this.preloadedBreakIds=new Set,this.breakWarnings=d,this.breakWarningFired=new Map,this.completedBreakIds=new Set,this.unsupportedEventBreakIds=new Set,this.playbackEndedAtMs=null,this.anvatoEnteredDiagnosed=new Set,this.retiredBreaks=new Map,this.minSeenTime=null,this.JOIN_DISCRIMINATOR_SLACK_MS=1e3,this.WINDOW_ESCAPE_GRACE_MS=12e3,this.TIMING_TOLERANCE_MS=5e3,this.PRELOAD_AHEAD_SECONDS=8,this.player=e,this.debug=t,this.chainingEnabled=a.enabled,this.maxChainGapMs=1e3*Math.max(0,a.maxGapSeconds),this.tuneInEnabled=r.enabled,this.minBreakDurationMs=1e3*Math.max(0,r.minBreakDurationSeconds),this.clock=i,this.diagnose=s,this.breakWarnings=n,this.cueTracker=l;const c=Math.max(0,h);this.timingDeps={player:this.player,clock:this.clock,cueTracker:this.cueTracker,diagnose:this.diagnose,log:(...e)=>this.log(...e),pdtGraceSeconds:c,pdtGraceMs:1e3*c},this.rewind=new g({breaks:this.breaks,retiredBreaks:this.retiredBreaks,completedBreakIds:this.completedBreakIds,preloadedBreakIds:this.preloadedBreakIds,timing:this.timing,clock:this.clock,log:(...e)=>this.log(...e),getBreakStartTime:e=>this.getBreakStartTime(e)})}claimChain(e){if(!this.chainingEnabled)return;let t=this.breaks.get(e)?.break??null;for(;t;){const e=this.findSuccessor(t);if(!e)break;const a=this.breaks.get(e.id);if(!a||a.state!==BreakState.PENDING)break;a.state=BreakState.CHAINED,this.log(`🔗 Break "${e.id}" claimed as chained after "${t.id}"`),t=e}}getChainedSuccessor(e){if(!this.chainingEnabled)return null;const t=this.breaks.get(e)?.break;return t?this.findSuccessor(t,[BreakState.PENDING,BreakState.CHAINED]):null}activateChainedBreak(e){const t=this.breaks.get(e);t&&(t.state=BreakState.ACTIVE)}findSuccessor(e,t=[BreakState.PENDING]){const a=this.getBreakStartTime(e);if(null===a)return null;const i=a+1e3*(e.duration??0);let r=null;for(const s of this.breaks.values()){if(s.break.id===e.id)continue;if(!t.includes(s.state))continue;const n=this.getBreakStartTime(s.break);if(null===n)continue;if(n<=a)continue;const d=n-i;d>this.maxChainGapMs||(d<-this.TIMING_TOLERANCE_MS||(!r||n<r.start)&&(r={break:s.break,start:n}))}return r?.break??null}updateManifest(e){this.timing.timebase=e.timebase,this.rewind.setAdStartDelayMs(1e3*Math.max(0,e.adStartDelay??0));for(const t of e.breaks){if(n(t))continue;if(!this.isSchedulableEvent(t))continue;if(this.completedBreakIds.has(t.id))continue;const e=this.breaks.get(t.id);e?e.state===BreakState.PENDING&&(e.break=t):(this.breaks.set(t.id,{break:t,state:BreakState.PENDING}),this.log(`📅 Break scheduled: "${t.id}" at ${this.describeStart(t)}`))}const t=new Set(e.breaks.map(e=>e.id));for(const[e,a]of this.breaks)t.has(e)||a.state!==BreakState.ACTIVE||this.abortNotifiedBreakIds.has(e)||(this.abortNotifiedBreakIds.add(e),this.log("Break removed from manifest while active (early return):",e),this.diagnose?.("DA-BREAK-EARLY-RETURN","Active break removed from manifest; returning to content early",{breakId:e}),this.onBreakAborted?.(a.break));this.cleanupBreaks(t)}resetForChannelSwitch(e){const t=this.getActiveBreak();for(const e of[...this.breaks.keys()])e!==t?.id&&this.breaks.delete(e);for(const e of[...this.abortNotifiedBreakIds])e!==t?.id&&this.abortNotifiedBreakIds.delete(e);this.completedBreakIds.clear(),this.retiredBreaks.clear(),this.preloadedBreakIds.clear(),this.breakWarningFired.clear(),this.unsupportedEventBreakIds.clear(),this.anvatoEnteredDiagnosed.clear(),this.timing.anvatoPendingDiagnosed.clear(),this.timing.anvatoMatchedDiagnosed.clear(),this.playbackEndedAtMs=null,this.rewind.beginChannel();const a=t?{...e,breaks:e.breaks.filter(e=>e.id!==t.id)}:e;this.updateManifest(a)}isSchedulableEvent(t){return!s(t.start)||t.start.event===e.START||t.start.event===e.END||(this.unsupportedEventBreakIds.has(t.id)||(this.unsupportedEventBreakIds.add(t.id),this.log(`⚠️ Break ignored: "${t.id}" (unsupported event trigger "${t.start.event}")`),this.diagnose?.("DA-BREAK-EVENT-UNSUPPORTED","Break triggered by an event this SDK version does not schedule; the break was ignored",{breakId:t.id,event:t.start.event})),!1)}describeStart(e){const t=r(e);if(null!==t)return t>0?`playback start + ${t}s`:"playback start";const a=i(e);return null!==a?a>0?`playback end + ${a}s`:"playback end":"string"==typeof e.start?new Date(e.start).toLocaleTimeString():String(e.start)}markPlaybackStart(){null!==this.playbackEndedAtMs&&(this.playbackEndedAtMs=null,this.log("Content replayed; pending post-rolls cancelled")),this.rewind.playbackStarted||(this.rewind.markPlaybackStart(this.peekPosition()),this.log("Content playback started at",new Date(this.clock.now()).toISOString()))}markPlaybackEnd(){null===this.playbackEndedAtMs&&(this.playbackEndedAtMs=this.clock.now(),this.log("Content playback ended at",new Date(this.playbackEndedAtMs).toISOString()))}start(e,t,a,i){this.onBreakStart=e,this.onBreakApproaching=t??null,this.onBreakWarning=a??null,this.onBreakAborted=i??null,this.checkInterval&&clearInterval(this.checkInterval),this.checkInterval=setInterval(()=>{this.tick()},250),this.log("Scheduler started")}tick(){this.checkBreaks()}stop(){this.checkInterval&&(clearInterval(this.checkInterval),this.checkInterval=null),this.onBreakStart=null,this.log("Scheduler stopped")}completeBreak(e){this.completedBreakIds.add(e),this.breakWarningFired.delete(e),this.abortNotifiedBreakIds.delete(e);const t=this.breaks.get(e);t&&(this.log("Break completed and removed:",e),null===a(t.break)&&null===this.cueTracker&&this.retiredBreaks.set(e,t.break),this.breaks.delete(e),this.preloadedBreakIds.delete(e))}setContentReloading(e){this.rewind.setContentReloading(e)}notifyResumeSeek(e,t){this.rewind.notifyResumeSeek(e,t)}notifyViewerSeek(){this.rewind.notifyViewerSeek()}cleanupBreaks(e){const t=[];for(const[a,i]of this.breaks)i.state!==BreakState.COMPLETED&&i.state!==BreakState.SKIPPED&&(e.has(a)||i.state===BreakState.ACTIVE)||t.push(a);for(const e of t)this.breaks.delete(e),this.preloadedBreakIds.delete(e),this.breakWarningFired.delete(e)}getActiveBreak(){for(const e of this.breaks.values())if(e.state===BreakState.ACTIVE)return e.break;return null}getSecondsUntilBreak(e){const t=this.breaks.get(e);if(!t||t.state!==BreakState.PENDING)return null;const a=this.getCurrentTime();if(null===a)return null;const i=this.getBreakStartTime(t.break);return null===i?null:b(i,a)}notifyCuePresented(e){if(null===this.cueTracker||this.timing.timebase!==t.PTS)return null;if(!Number.isFinite(e))return null;let a=null;for(const t of this.breaks.values())if(t.state===BreakState.PENDING&&"number"==typeof t.break.start&&!(Math.abs(t.break.start-e)>l)){a=t.break.id;break}if(null===a)return null;const i=this.player.currentTime;this.checkBreaks();const r=this.breaks.get(a);return r&&r.state===BreakState.ACTIVE?this.anvatoEnteredDiagnosed.has(a)?null:(this.anvatoEnteredDiagnosed.add(a),this.log(`🎯 Break "${a}" armed by cue presentation (pts ${e})`),this.diagnose?.("DA-ANVATO-CUE-ENTERED",`Break "${a}" started from the presented cue (pts ${e}) rather than the scheduler tick.`,{breakId:a,pts:e,mediaTimeSec:i}),a):null}checkBreaks(){const e=this.getCurrentTime();this.rewind.trackPlayedTime(e);const a=this.rewind.isAdStartDelayActive();null!==e&&(this.minSeenTime=null===this.minSeenTime?e:Math.min(this.minSeenTime,e),this.rewind.rearmOnRewind(e));for(const s of this.breaks.values()){if(s.state===BreakState.ACTIVE&&this.timing.timebase===t.WALLCLOCK&&null!==e&&!this.abortNotifiedBreakIds.has(s.break.id)){const t=this.getBreakStartTime(s.break),a=1e3*(s.break.duration??0);null!==t&&a>0&&e>t+a+this.WINDOW_ESCAPE_GRACE_MS&&(this.abortNotifiedBreakIds.add(s.break.id),this.log(`⚠️ Break window escaped: "${s.break.id}" (ended ${Math.round((e-t-a)/1e3)}s ago on the stream clock)`),this.diagnose?.("DA-BREAK-WINDOW-ESCAPED","Active break outlived its wallclock window; returning to content",{breakId:s.break.id,overrunSec:Math.round((e-t-a)/100)/10}),this.onBreakAborted?.(s.break));continue}if(s.state!==BreakState.PENDING)continue;const n=r(s.break);if(null!==n){this.checkPreRollBreak(s,n,a);continue}const d=i(s.break);if(null!==d){this.checkPostRollBreak(s,d,a);continue}if(null===e)continue;if(a)continue;null!==this.cueTracker&&this.timing.timebase===t.PTS&&this.maybePreload(s.break,"manifest arrival — Anvato flow");const o=this.getBreakStartTime(s.break);if(null===o)continue;const l=e-o,h=Math.round(-l/1e3);if(this.checkWarnings(s.break,h),h>0&&h<=15&&h%2==0&&this.log(`⏱️ Break "${s.break.id}" starts in ${h}s`),h>0&&h<=this.PRELOAD_AHEAD_SECONDS&&this.maybePreload(s.break,`${h}s ahead`),l>=0&&l<=this.TIMING_TOLERANCE_MS){const e=Math.max(0,l),t=1e3*(s.break.duration??0)-e;if(t<=0){this.log(`⚠️ Break missed: "${s.break.id}" (ended ${Math.round(-t/1e3)}s ago, inside the start tolerance)`),s.state=BreakState.SKIPPED,null===this.cueTracker&&this.retiredBreaks.set(s.break.id,s.break);break}if(null!==this.minSeenTime&&o<this.minSeenTime-this.JOIN_DISCRIMINATOR_SLACK_MS&&(!this.tuneInEnabled||t<this.minBreakDurationMs)){this.log(`⚠️ Break missed: "${s.break.id}" (joined in progress inside the start tolerance, ${Math.round(t/1e3)}s remaining < tune-in rules)`),s.state=BreakState.SKIPPED,null===this.cueTracker&&this.retiredBreaks.set(s.break.id,s.break);break}this.log(`🎬 BREAK TRIGGERED: "${s.break.id}"`),s.state=BreakState.ACTIVE,this.onBreakStart&&this.onBreakStart(s.break,{tuneIn:!1,elapsedSec:e/1e3,remainingSec:t/1e3});break}if(l>this.TIMING_TOLERANCE_MS){const t=o+1e3*(s.break.duration??0)-e;if(this.tuneInEnabled&&t>=this.minBreakDurationMs&&t>0){this.log(`🎬 TUNE-IN BREAK TRIGGERED: "${s.break.id}" (${Math.round(t/1e3)}s remaining)`),s.state=BreakState.ACTIVE,this.onBreakStart&&this.onBreakStart(s.break,{tuneIn:!0,elapsedSec:l/1e3,remainingSec:t/1e3});break}this.log(`⚠️ Break missed: "${s.break.id}" (was ${Math.round(l/1e3)}s ago)`),s.state=BreakState.SKIPPED,null===this.cueTracker&&this.retiredBreaks.set(s.break.id,s.break)}}}checkPreRollBreak(e,t,a){if(!this.rewind.playbackStarted)return;const i=1e3*t;if(this.rewind.adStartDelayMs>0&&i<this.rewind.adStartDelayMs)return this.log(`⚠️ Pre-roll skipped: "${e.break.id}" (delay ${t}s < adStartDelay ${this.rewind.adStartDelayMs/1e3}s)`),void(e.state=BreakState.SKIPPED);const r=this.rewind.channelPlayedMs,s=Math.round((i-r)/1e3);s>0&&s<=15&&s%2==0&&this.log(`⏱️ Pre-roll "${e.break.id}" starts in ${s}s`),s>0&&s<=this.PRELOAD_AHEAD_SECONDS&&this.maybePreload(e.break,`pre-roll, ${s}s ahead`),r>=i&&!a&&(this.log(`🎬 PRE-ROLL TRIGGERED: "${e.break.id}"${t>0?` (after ${t}s delay)`:""}`),e.state=BreakState.ACTIVE,this.onBreakStart&&this.onBreakStart(e.break,{tuneIn:!1,elapsedSec:0,remainingSec:e.break.duration??0}))}checkPostRollBreak(e,t,a){if(null===this.playbackEndedAtMs)return;const i=1e3*t,r=this.clock.now()-this.playbackEndedAtMs,s=Math.round((i-r)/1e3);s>0&&s<=this.PRELOAD_AHEAD_SECONDS&&this.maybePreload(e.break,`post-roll, ${s}s ahead`),r>=i&&!a&&(this.log(`🎬 POST-ROLL TRIGGERED: "${e.break.id}"${t>0?` (after ${t}s delay)`:""}`),e.state=BreakState.ACTIVE,this.onBreakStart&&this.onBreakStart(e.break,{tuneIn:!1,elapsedSec:0,remainingSec:e.break.duration??0}))}isAdStartDelayActive(){return this.rewind.isAdStartDelayActive()}getCurrentTime(){return u(this.timing,this.timingDeps)}maybePreload(e,t){if(this.preloadedBreakIds.has(e.id)||!this.onBreakApproaching)return;this.log(`⏳ Preloading break "${e.id}" (${t})`);!1!==this.onBreakApproaching(e)&&this.preloadedBreakIds.add(e.id)}getBreakStartTime(e){return s(e.start)?null:c(this.timing,this.timingDeps,e)}peekPosition(){if(this.timing.timebase!==t.WALLCLOCK)return 1e3*this.player.currentTime;const e=this.player.programDateTime?.getTime();return void 0===e||isNaN(e)?null:e}destroy(){this.stop(),this.breaks.clear(),this.preloadedBreakIds.clear(),this.completedBreakIds.clear(),this.retiredBreaks.clear(),this.minSeenTime=null,this.unsupportedEventBreakIds.clear(),this.playbackEndedAtMs=null,this.rewind.reset(),this.breakWarningFired.clear(),this.abortNotifiedBreakIds.clear(),this.timing.pdtMissingWarned=!1,this.timing.pdtSeen=!1,this.timing.firstPdtAttemptMs=null,this.timing.playbackStartedAtMs=null,this.timing.anvatoPendingDiagnosed.clear(),this.timing.anvatoMatchedDiagnosed.clear(),this.cueTracker?.clear()}checkWarnings(e,t){if(t<=0||0===this.breakWarnings.seconds.length)return;let a=this.breakWarningFired.get(e.id);a||(a=new Set,this.breakWarningFired.set(e.id,a));for(const i of this.breakWarnings.seconds)a.has(i)||t<=i&&(a.add(i),this.log(`Break warning for "${e.id}" (${t}s <= ${i}s)`),this.onBreakWarning?.(e,i,t))}log(...e){this.debug&&h("[OptiViewAds:BreakScheduler]",...e)}}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Clock } from './BreakScheduler';
|
|
2
|
+
/**
|
|
3
|
+
* `into-break`: break triggered → first ad renders. `ad-to-ad`: ad ended → next ad renders.
|
|
4
|
+
* `break-to-break`: chained break → its first ad renders. `out-of-break`: break ended → content playing.
|
|
5
|
+
*/
|
|
6
|
+
export type BreakTransitionKind = 'into-break' | 'ad-to-ad' | 'break-to-break' | 'out-of-break';
|
|
7
|
+
/** A named checkpoint inside a transition, so a slow transition can be attributed to a phase. */
|
|
8
|
+
export interface TransitionPhase {
|
|
9
|
+
/** Checkpoint name, e.g. `uri-resolved`, `load-resolved`, `play-called`. */
|
|
10
|
+
name: string;
|
|
11
|
+
/** Milliseconds from the start of the transition to this checkpoint. */
|
|
12
|
+
atMs: number;
|
|
13
|
+
/** Milliseconds spent since the previous checkpoint (or the start). */
|
|
14
|
+
sinceLastMs: number;
|
|
15
|
+
}
|
|
16
|
+
/** One completed transition measurement. */
|
|
17
|
+
export interface BreakTransitionMeasurement {
|
|
18
|
+
kind: BreakTransitionKind;
|
|
19
|
+
/** Wallclock milliseconds the transition took. */
|
|
20
|
+
durationMs: number;
|
|
21
|
+
/** The break the transition led into, or out of. */
|
|
22
|
+
breakId: string;
|
|
23
|
+
/** Ordered checkpoints from {@link BreakTransitionTimer.phase}; omitted when none, keeping the default report shape. */
|
|
24
|
+
phases?: TransitionPhase[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Measures the gap where the viewer sees neither outgoing nor incoming media; each mark/settle
|
|
28
|
+
* becomes a `DA-BREAK-TRANSITION` diagnostic. Side-effect free with an injected clock so the
|
|
29
|
+
* measurement points port to native runtimes. One pending transition at a time: a new mark
|
|
30
|
+
* supersedes an unsettled one, so an aborted transition is dropped rather than misreported.
|
|
31
|
+
*/
|
|
32
|
+
export declare class BreakTransitionTimer {
|
|
33
|
+
private readonly onMeasurement;
|
|
34
|
+
private readonly clock;
|
|
35
|
+
private pending;
|
|
36
|
+
constructor(onMeasurement: (measurement: BreakTransitionMeasurement) => void, clock?: Clock);
|
|
37
|
+
/** Record the start of a transition. Supersedes any still-pending one. */
|
|
38
|
+
mark(kind: BreakTransitionKind, breakId: string): void;
|
|
39
|
+
/** No-op when nothing is pending; a superseded transition drops its phases with it. */
|
|
40
|
+
phase(name: string): void;
|
|
41
|
+
/**
|
|
42
|
+
* Settle the pending transition when it is one of `kinds`. No-op otherwise, so a late second
|
|
43
|
+
* `playing` cannot double-report. Several kinds because one settle point (an ad rendering) closes any of them.
|
|
44
|
+
*/
|
|
45
|
+
settle(...kinds: BreakTransitionKind[]): void;
|
|
46
|
+
/** Drop any pending measurement without reporting it. */
|
|
47
|
+
reset(): void;
|
|
48
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{systemClock as s}from"./BreakScheduler";export class BreakTransitionTimer{constructor(t,e=s){this.onMeasurement=t,this.clock=e,this.pending=null}mark(s,t){this.pending={kind:s,breakId:t,startedAtMs:this.clock.now(),phases:[]}}phase(s){const t=this.pending;if(!t)return;const e=Math.max(0,this.clock.now()-t.startedAtMs),n=t.phases[t.phases.length-1]?.atMs??0;t.phases.push({name:s,atMs:e,sinceLastMs:Math.max(0,e-n)})}settle(...s){const t=this.pending;t&&s.includes(t.kind)&&(this.pending=null,this.onMeasurement({kind:t.kind,breakId:t.breakId,durationMs:Math.max(0,this.clock.now()-t.startedAtMs),...t.phases.length>0?{phases:t.phases}:{}}))}reset(){this.pending=null}}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { LensHandle } from '../vendor/lens-sdk/index.js';
|
|
2
|
+
import type { BreakManifest } from '../types/BreakManifest';
|
|
3
|
+
import type { OptiViewAdsEventMap, OptiViewAdsEventType } from '../types/Events';
|
|
4
|
+
/**
|
|
5
|
+
* Reports CMCD ad telemetry through the Lens handle owned by `LensReporter`:
|
|
6
|
+
* the standard ad lifecycle events (abs/as/ae/sk/abe), the custom milestone
|
|
7
|
+
* events (quartiles, click, aderror), and the heartbeat context payload.
|
|
8
|
+
*
|
|
9
|
+
* Internal — fed from the SDK's unified event funnel, never exported publicly.
|
|
10
|
+
* Every Lens call is isolated: a Lens failure is logged and can never affect
|
|
11
|
+
* playback. Heartbeat emission itself stays Lens-owned (`t` is never emitted
|
|
12
|
+
* here), as does platform reporting (no `p` key).
|
|
13
|
+
*/
|
|
14
|
+
export declare class CmcdAdReporter {
|
|
15
|
+
private readonly getHandle;
|
|
16
|
+
private readonly log;
|
|
17
|
+
/** Session-scoped keys stamped on every event and heartbeat payload. */
|
|
18
|
+
private sessionKeys;
|
|
19
|
+
private activeBreakId;
|
|
20
|
+
private breakBeginTimestamp;
|
|
21
|
+
private firstAdReported;
|
|
22
|
+
/** Playable (non-companion) ad milliseconds delivered so far in the break. */
|
|
23
|
+
private breakFilledMs;
|
|
24
|
+
/** Milliseconds of ad media actually watched so far in the break. */
|
|
25
|
+
private breakWatchedMs;
|
|
26
|
+
private activeAdKeys;
|
|
27
|
+
private activeAdAssetId;
|
|
28
|
+
/**
|
|
29
|
+
* The break's first `as` payload, held until the ad renders its first frame
|
|
30
|
+
* (the first `adtimeupdate` with `currentTime > 0`) so `brttf` measures
|
|
31
|
+
* break entry to first rendered frame. Flushed without `brttf` if the ad
|
|
32
|
+
* ends, errors, or reports a milestone before any frame renders.
|
|
33
|
+
*/
|
|
34
|
+
private pendingAdStart;
|
|
35
|
+
private activeAdWatchedSec;
|
|
36
|
+
private activeAdDeclaredSec;
|
|
37
|
+
private skipRequestedBreakId;
|
|
38
|
+
private abortedBreakId;
|
|
39
|
+
private erroredAssetId;
|
|
40
|
+
constructor(getHandle: () => LensHandle | null, log: (...args: unknown[]) => void);
|
|
41
|
+
/**
|
|
42
|
+
* Seed the session-scoped keys from the fetched manifest: SDK version,
|
|
43
|
+
* manifest version, and the optional identity ids (org/channel/event).
|
|
44
|
+
*/
|
|
45
|
+
onSessionStart(manifest: BreakManifest): void;
|
|
46
|
+
/** Clear all telemetry state and the heartbeat payload. */
|
|
47
|
+
onSessionEnd(): void;
|
|
48
|
+
/** The viewer requested a skip — the next `adend` for this break is a skip. */
|
|
49
|
+
onSkipRequested(breakId: string): void;
|
|
50
|
+
/** The break is being aborted (early return / teardown) — the next `adend` is an abort. */
|
|
51
|
+
onBreakAborted(breakId: string): void;
|
|
52
|
+
/** Feed one SDK event from the unified funnel. */
|
|
53
|
+
onEvent(event: OptiViewAdsEventMap[OptiViewAdsEventType]): void;
|
|
54
|
+
private onBreakBegin;
|
|
55
|
+
private onAdBegin;
|
|
56
|
+
/** Emit an `as` pair: the playable ad and, when present, its companion. */
|
|
57
|
+
private emitAdStart;
|
|
58
|
+
/**
|
|
59
|
+
* Flush the held first-ad `as`, stamping `brttf` when the first frame
|
|
60
|
+
* rendered (`brttfMs` set) and omitting it when it never did (`null`).
|
|
61
|
+
*/
|
|
62
|
+
private emitPendingAdStart;
|
|
63
|
+
private onAdEnd;
|
|
64
|
+
private onBreakEnd;
|
|
65
|
+
private onMilestone;
|
|
66
|
+
private onClick;
|
|
67
|
+
private onAdError;
|
|
68
|
+
private emitCustomWithAdContext;
|
|
69
|
+
private adEndReason;
|
|
70
|
+
/**
|
|
71
|
+
* Sum the declared durations of the playable ads of the selected variant
|
|
72
|
+
* (companions excluded), clamped to what is actually available. Null when
|
|
73
|
+
* no asset declares a duration — nothing meaningful to request.
|
|
74
|
+
*/
|
|
75
|
+
private requestedPlayableMs;
|
|
76
|
+
/** Re-derive the heartbeat context payload: session keys + active break/ad. */
|
|
77
|
+
private updateHeartbeat;
|
|
78
|
+
private resetBreakState;
|
|
79
|
+
/** Run one Lens call, isolating any failure from playback. */
|
|
80
|
+
private guard;
|
|
81
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LensEventType as e,LensToken as t}from"../vendor/lens-sdk/index.js";import{AssetType as i,BreakFormat as s}from"../types/BreakManifest";import{VendorAdError as a}from"../types/VendorAdError";import{SDK_VERSION as d}from"../generated/version";const r={[s.SINGLE]:"s",[s.DOUBLE]:"d",[s.LSHAPE_AD]:"la",[s.LSHAPE_CONTENT]:"lc",[s.OVERLAY]:"o"},n={[i.STATIC]:"s",[i.VAST]:"va",[i.VENDOR]:"ve"};function o(e){return n[e]??e}export class CmcdAdReporter{constructor(e,t){this.getHandle=e,this.log=t,this.sessionKeys={},this.activeBreakId=null,this.breakBeginTimestamp=null,this.firstAdReported=!1,this.breakFilledMs=0,this.breakWatchedMs=0,this.activeAdKeys=null,this.activeAdAssetId=null,this.pendingAdStart=null,this.activeAdWatchedSec=0,this.activeAdDeclaredSec=null,this.skipRequestedBreakId=null,this.abortedBreakId=null,this.erroredAssetId=null}onSessionStart(e){this.sessionKeys={adv:d,admv:e.version,...e.organizationId?{adoid:e.organizationId}:{},...e.channelId?{adcid:e.channelId}:{},...e.eventId?{adeid:e.eventId}:{}},this.updateHeartbeat()}onSessionEnd(){this.sessionKeys={},this.resetBreakState(),this.guard("setHeartbeatPayload",e=>e.setHeartbeatPayload())}onSkipRequested(e){this.skipRequestedBreakId=e}onBreakAborted(e){this.abortedBreakId=e}onEvent(e){switch(e.type){case"adbreakbegin":this.onBreakBegin(e);break;case"adbegin":this.onAdBegin(e);break;case"adtimeupdate":{const t=e;null!==this.pendingAdStart&&t.asset.id===this.pendingAdStart.assetId&&t.currentTime>0&&null!==this.breakBeginTimestamp&&this.emitPendingAdStart(t.timestamp-this.breakBeginTimestamp),t.asset.id===this.activeAdAssetId&&(this.activeAdWatchedSec=Math.max(this.activeAdWatchedSec,t.currentTime));break}case"adend":this.onAdEnd(e);break;case"adbreakend":this.onBreakEnd(e);break;case"adfirstquartile":case"admidpoint":case"adthirdquartile":this.onMilestone(e);break;case"adclick":this.onClick(e);break;case"aderror":this.onAdError(e)}}onBreakBegin(i){this.resetBreakState(),this.activeBreakId=i.break.id,this.breakBeginTimestamp=i.timestamp;const s=Math.round(1e3*i.remainingSec),a=this.requestedPlayableMs(i.break,i.format,s);this.guard("abs",d=>{return d.emit(e.AD_BREAK_START,{custom:{...this.sessionKeys,brid:i.break.id,brst:String(i.break.start),brmd:Math.round(1e3*i.break.duration),brad:s,...null!==a?{brrd:a}:{},...i.format?{brf:new t((n=i.format,r[n]??n))}:{}}});var n}),this.updateHeartbeat()}onAdBegin(e){this.emitPendingAdStart(null);const i=e.asset,s=i.interaction?.clickThrough,a=!this.firstAdReported&&null!==this.breakBeginTimestamp;this.firstAdReported=!0;const d={brid:e.break.id,adasid:i.id,adid:e.adId??i.id,...void 0!==e.podPosition?{adpos:String(e.podPosition)}:{},...void 0!==i.duration?{adasd:Math.round(1e3*i.duration)}:{},adast:new t(o(i.type)),...s?{adcl:!0}:{},...e.creativeId?{adgcrid:e.creativeId}:{},...e.adId&&"string"==typeof i.uri&&"vendor"===i.type?{adgpid:i.uri}:{}};this.activeAdKeys=d,this.activeAdAssetId=i.id,this.activeAdWatchedSec=0,this.activeAdDeclaredSec=i.duration??null;const r={...this.sessionKeys,...d},n=i.companion?{...this.sessionKeys,brid:e.break.id,adasid:i.companion.id,adid:i.companion.id,adast:new t(o(i.companion.type)),adcomp:!0,adpasid:i.id}:null;a?this.pendingAdStart={assetId:i.id,custom:r,companionCustom:n}:this.emitAdStart(r,n),this.updateHeartbeat()}emitAdStart(t,i){this.guard("as",i=>i.emit(e.AD_START,{custom:t})),i&&this.guard("as",t=>t.emit(e.AD_START,{custom:i}))}emitPendingAdStart(e){const t=this.pendingAdStart;if(!t)return;this.pendingAdStart=null;const i=null!==e?{...t.custom,brttf:e}:t.custom;this.emitAdStart(i,t.companionCustom)}onAdEnd(i){this.emitPendingAdStart(null);const s=i.asset,a=this.adEndReason(i),d=this.activeAdKeys??{brid:i.break.id,adasid:s.id,adid:i.adId??s.id},r=null!==this.activeAdDeclaredSec?Math.round(1e3*this.activeAdDeclaredSec):null,n=Math.round(1e3*this.activeAdWatchedSec),o=null!==r?Math.min(n,r):n;if("s"===a&&this.guard("sk",t=>t.emit(e.AD_SKIP,{custom:{...this.sessionKeys,...d}})),this.guard("ae",i=>i.emit(e.AD_END,{custom:{...this.sessionKeys,...d,ader:new t(a),adwd:o}})),s.companion){const d=s.companion;this.guard("ae",r=>r.emit(e.AD_END,{custom:{...this.sessionKeys,brid:i.break.id,adasid:d.id,adid:d.id,adcomp:!0,adpasid:s.id,ader:new t(a)}}))}this.breakWatchedMs+=o,this.breakFilledMs+=null!==r?r:o,this.activeAdKeys=null,this.activeAdAssetId=null,this.activeAdWatchedSec=0,this.activeAdDeclaredSec=null,this.erroredAssetId===s.id&&(this.erroredAssetId=null),this.updateHeartbeat()}onBreakEnd(t){this.emitPendingAdStart(null),this.guard("abe",i=>i.emit(e.AD_BREAK_END,{custom:{...this.sessionKeys,brid:t.break.id,brfd:this.breakFilledMs,brwd:this.breakWatchedMs}})),this.resetBreakState(),this.updateHeartbeat()}onMilestone(e){this.emitPendingAdStart(null),this.emitCustomWithAdContext(e.type,e.break,e.asset)}onClick(e){this.emitPendingAdStart(null),this.emitCustomWithAdContext("adclick",e.break,e.asset)}onAdError(e){"ad"===e.source&&(this.emitPendingAdStart(null),e.asset&&(this.erroredAssetId=e.asset.id),this.guard("aderror",t=>t.emitCustom("aderror",{custom:{...this.sessionKeys,adec:e.asset?"DA-AD-PLAYBACK-ERROR":"DA-BREAK-NO-PLAYABLE-ASSET",...e.error instanceof a?{adevc:e.error.vendorCode}:{},...e.break?{brid:e.break.id}:{},...e.asset?{adasid:e.asset.id,adid:e.asset.id}:{}}})))}emitCustomWithAdContext(e,t,i){this.guard(e,s=>s.emitCustom(e,{custom:{...this.sessionKeys,brid:t.id,adasid:i.id,...void 0!==this.activeAdKeys?.adid?{adid:this.activeAdKeys.adid}:{}}}))}adEndReason(e){return this.skipRequestedBreakId===e.break.id?(this.skipRequestedBreakId=null,"s"):this.erroredAssetId===e.asset.id?"e":this.abortedBreakId===e.break.id?"a":"c"}requestedPlayableMs(e,t,i){const s=Array.isArray(e.variant)?e.variant:[e.variant],a=s.find(e=>e.format===t)??s[0];if(!a)return null;const d=a.assets.reduce((e,t)=>e+(t.duration??0),0);return d<=0?null:Math.min(Math.round(1e3*d),i)}updateHeartbeat(){this.guard("setHeartbeatPayload",e=>e.setHeartbeatPayload({custom:{...this.sessionKeys,...this.activeBreakId?{brid:this.activeBreakId}:{},...void 0!==this.activeAdKeys?.adid?{adid:this.activeAdKeys.adid}:{}}}))}resetBreakState(){this.emitPendingAdStart(null),this.activeBreakId=null,this.breakBeginTimestamp=null,this.firstAdReported=!1,this.breakFilledMs=0,this.breakWatchedMs=0,this.activeAdKeys=null,this.activeAdAssetId=null,this.activeAdWatchedSec=0,this.activeAdDeclaredSec=null,this.skipRequestedBreakId=null,this.abortedBreakId=null,this.erroredAssetId=null}guard(e,t){const i=this.getHandle();if(i)try{t(i)}catch(t){this.log(`Lens CMCD ${e} failed:`,t)}}}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Asset } from '../types/BreakManifest';
|
|
2
|
+
/**
|
|
3
|
+
* Portable, DOM- and IMA-free authority on the per-ad events INSIDE a GAM DAI pod. A pod is a
|
|
4
|
+
* single `vendor` asset, so {@link AdBreakSequencer} can only describe it as `ad 1/1`; IMA
|
|
5
|
+
* reports the real composition per ad at runtime via `StreamEvent.getAd().getAdPodInfo()`.
|
|
6
|
+
* Contract: every `adbegin` gets exactly one `adend`; a new ad closes the previous one even if
|
|
7
|
+
* IMA never reported completion; {@link finish} closes whatever is open; re-reporting the open
|
|
8
|
+
* ad is idempotent (IMA re-fires `STARTED` when a viewer seeks back into an ad).
|
|
9
|
+
*/
|
|
10
|
+
/** One IMA pod-ad report, normalized by the render layer so this module never sees an IMA object. */
|
|
11
|
+
export interface GamPodAdInfo {
|
|
12
|
+
/** 1-based position of this ad within the pod (`getAdPosition()`); absent when IMA reports none. */
|
|
13
|
+
adPosition?: number | undefined;
|
|
14
|
+
/** Ads in the pod (`getTotalAds()`); 0 or absent when IMA does not know. */
|
|
15
|
+
totalAds?: number | undefined;
|
|
16
|
+
/** IMA's ad id, when available (`getAdId()`). */
|
|
17
|
+
adId?: string | undefined;
|
|
18
|
+
/** IMA's creative id, when available (`getCreativeId()`). */
|
|
19
|
+
creativeId?: string | undefined;
|
|
20
|
+
}
|
|
21
|
+
/** One per-ad event for the driver to emit, in order. */
|
|
22
|
+
export interface GamPodAdStep {
|
|
23
|
+
type: 'adbegin' | 'adend';
|
|
24
|
+
asset: Asset;
|
|
25
|
+
/** 0-based index of the ad within the pod. */
|
|
26
|
+
adIndex: number;
|
|
27
|
+
/** Ads in the pod, as reported (never less than `adIndex + 1`). */
|
|
28
|
+
totalAds: number;
|
|
29
|
+
/** The runtime pod position exactly as IMA reported it; absent when it reported none. */
|
|
30
|
+
adPosition?: number | undefined;
|
|
31
|
+
adId?: string | undefined;
|
|
32
|
+
creativeId?: string | undefined;
|
|
33
|
+
}
|
|
34
|
+
/** The ad currently open, used to attribute quartiles to the right ad. */
|
|
35
|
+
export interface GamPodCurrentAd {
|
|
36
|
+
asset: Asset;
|
|
37
|
+
adIndex: number;
|
|
38
|
+
totalAds: number;
|
|
39
|
+
/** The runtime pod position exactly as IMA reported it; absent when it reported none. */
|
|
40
|
+
adPosition?: number | undefined;
|
|
41
|
+
adId?: string | undefined;
|
|
42
|
+
creativeId?: string | undefined;
|
|
43
|
+
}
|
|
44
|
+
/** Per-ad asset for `adPosition` of `podAsset`: id `<podAssetId>-ad-<position>`, other fields carried over unchanged. */
|
|
45
|
+
export declare function derivePodAdAsset(podAsset: Asset, adPosition: number): Asset;
|
|
46
|
+
export declare class GamPodAdTracker {
|
|
47
|
+
private readonly podAsset;
|
|
48
|
+
private open;
|
|
49
|
+
private openPosition;
|
|
50
|
+
private reportedAnyAd;
|
|
51
|
+
private finished;
|
|
52
|
+
constructor(podAsset: Asset);
|
|
53
|
+
/** The ad currently open, or null. Drives quartile attribution. */
|
|
54
|
+
get currentAd(): GamPodCurrentAd | null;
|
|
55
|
+
/** True once IMA reported at least one ad; while false the pod is still reported the legacy way. */
|
|
56
|
+
get sawAnyAd(): boolean;
|
|
57
|
+
/** Closes the previous ad if it never completed, then opens the reported one. Idempotent for the open ad. */
|
|
58
|
+
onAdStarted(info: GamPodAdInfo): GamPodAdStep[];
|
|
59
|
+
/** IMA reported the open ad completing. No-op when nothing is open. */
|
|
60
|
+
onAdComplete(): GamPodAdStep[];
|
|
61
|
+
/** Close whatever is open and refuse further reports, so a late IMA event cannot reopen a finished pod. */
|
|
62
|
+
finish(): GamPodAdStep[];
|
|
63
|
+
/** Emit the `adend` for the open ad, if any, and clear it. */
|
|
64
|
+
private closeOpen;
|
|
65
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function derivePodAdAsset(t,e){return{...t,id:`${t.id}-ad-${e}`}}export class GamPodAdTracker{constructor(t){this.podAsset=t,this.open=null,this.openPosition=0,this.reportedAnyAd=!1,this.finished=!1}get currentAd(){return this.open}get sawAnyAd(){return this.reportedAnyAd}onAdStarted(t){if(this.finished)return[];const e=function(t){if("number"!=typeof t||!Number.isFinite(t))return;const e=Math.floor(t);return e>=1?e:void 0}(t.adPosition),i=e??1;if(this.open&&this.openPosition===i)return[];const n=this.closeOpen(),s=i-1;var o,r;return this.open={asset:derivePodAdAsset(this.podAsset,i),adIndex:s,totalAds:(o=t.totalAds,r=i,"number"==typeof o&&Number.isFinite(o)?Math.max(r,Math.floor(o)):r),adPosition:e,adId:t.adId,creativeId:t.creativeId},this.openPosition=i,this.reportedAnyAd=!0,n.push({type:"adbegin",...this.open}),n}onAdComplete(){return this.finished?[]:this.closeOpen()}finish(){if(this.finished)return[];const t=this.closeOpen();return this.finished=!0,t}closeOpen(){if(!this.open)return[];const t=[{type:"adend",...this.open}];return this.open=null,this.openPosition=0,t}}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parameters from the VendorAsset.vendorParameters for GAM pod serving.
|
|
3
|
+
*/
|
|
4
|
+
export interface GamVendorParameters {
|
|
5
|
+
type: 'pod';
|
|
6
|
+
eabnVersion: 'V1' | 'V2';
|
|
7
|
+
networkCode: string;
|
|
8
|
+
customAssetKey: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Validates that the given object contains valid GAM vendor parameters.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isGamVendorParameters(params: unknown): params is GamVendorParameters;
|
|
14
|
+
/**
|
|
15
|
+
* Build a Google DAI pod manifest URL.
|
|
16
|
+
*
|
|
17
|
+
* @param podId - The pod/ad break identifier (from VendorAsset.uri)
|
|
18
|
+
* @param params - GAM vendor parameters from the manifest
|
|
19
|
+
* @param streamId - The stream session ID
|
|
20
|
+
* @param durationSec - The break duration in seconds
|
|
21
|
+
* @returns The full pod manifest URL
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // EABN V2 URL format:
|
|
25
|
+
* // https://dai.google.com/linear/pods/v1/hls/network/{networkCode}/custom_asset/{customAssetKey}/ad_break_id/{podId}.m3u8?stream_id={streamId}&pd={durationMs}
|
|
26
|
+
*
|
|
27
|
+
* // EABN V1 URL format:
|
|
28
|
+
* // https://dai.google.com/linear/pods/v1/hls/network/{networkCode}/custom_asset/{customAssetKey}/pod/{podId}.m3u8?stream_id={streamId}&pd={durationMs}
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildGamPodUrl(podId: string, params: GamVendorParameters, streamId: string, durationSec: number): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function isGamVendorParameters(e){if("object"!=typeof e||null===e)return!1;const t=e;return"pod"===t.type&&("V1"===t.eabnVersion||"V2"===t.eabnVersion)&&"string"==typeof t.networkCode&&"string"==typeof t.customAssetKey}export function buildGamPodUrl(e,t,o,r){return`${`https://dai.google.com/linear/pods/v1/hls/network/${t.networkCode}/custom_asset/${t.customAssetKey}`}${"V2"===t.eabnVersion?`/ad_break_id/${e}.m3u8`:`/pod/${e}.m3u8`}?stream_id=${o}&pd=${1e3*Math.ceil(r)}`}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { TimedMetadataCue } from '../interfaces/PlayerAdapter';
|
|
2
|
+
import { GamVendorParameters } from './GamPodUrlBuilder';
|
|
3
|
+
import type { GamPodAdInfo } from './GamPodAdTracker';
|
|
4
|
+
/** Callback interface for IMA ad events. */
|
|
5
|
+
export interface GamAdEventCallbacks {
|
|
6
|
+
onAdBreakStarted?: () => void;
|
|
7
|
+
onAdBreakEnded?: () => void;
|
|
8
|
+
/** Carries IMA's pod position/total ({@link GamPodAdInfo}) so a multi-ad pod is reported ad by ad; `undefined` when IMA attached no ad. */
|
|
9
|
+
onAdStarted?: (info?: GamPodAdInfo) => void;
|
|
10
|
+
onAdComplete?: () => void;
|
|
11
|
+
onAdFirstQuartile?: () => void;
|
|
12
|
+
onAdMidpoint?: () => void;
|
|
13
|
+
onAdThirdQuartile?: () => void;
|
|
14
|
+
onError?: (message: string) => void;
|
|
15
|
+
}
|
|
16
|
+
/** Configuration for GamStreamManager initialization. */
|
|
17
|
+
export interface GamStreamConfig {
|
|
18
|
+
networkCode: string;
|
|
19
|
+
customAssetKey: string;
|
|
20
|
+
adTagParameters?: Record<string, string> | undefined;
|
|
21
|
+
streamActivityMonitorId?: string | undefined;
|
|
22
|
+
}
|
|
23
|
+
/** Configuration for a full-service DAI livestream session (manifest-steered ssai). */
|
|
24
|
+
export interface GamLiveStreamConfig {
|
|
25
|
+
assetKey: string;
|
|
26
|
+
networkCode?: string | undefined;
|
|
27
|
+
adTagParameters?: Record<string, string> | undefined;
|
|
28
|
+
streamActivityMonitorId?: string | undefined;
|
|
29
|
+
}
|
|
30
|
+
/** Manages IMA DAI stream sessions for GAM pod serving and provides the streamId for pod manifest requests. */
|
|
31
|
+
export declare class GamStreamManager {
|
|
32
|
+
private videoElement;
|
|
33
|
+
private adUiContainer?;
|
|
34
|
+
private streamManager;
|
|
35
|
+
private streamId;
|
|
36
|
+
private config;
|
|
37
|
+
private initPromise;
|
|
38
|
+
private debug;
|
|
39
|
+
private callbacks;
|
|
40
|
+
constructor(videoElement: HTMLVideoElement, adUiContainer?: HTMLElement | undefined, debug?: boolean);
|
|
41
|
+
/** Set callbacks for ad events. */
|
|
42
|
+
setCallbacks(callbacks: GamAdEventCallbacks): void;
|
|
43
|
+
/** Check if IMA SDK is available in the global scope. */
|
|
44
|
+
private isImaSdkAvailable;
|
|
45
|
+
/** Always creates a new session; resolves when the streamId is available. */
|
|
46
|
+
initialize(config: GamStreamConfig): Promise<void>;
|
|
47
|
+
/** Full-service DAI livestream (IMA `LiveStreamRequest`); resolves with the stitched stream URL from IMA's LOADED event. */
|
|
48
|
+
initializeLiveStream(config: GamLiveStreamConfig): Promise<string>;
|
|
49
|
+
private doInitialize;
|
|
50
|
+
/** resolve/reject are used only for the initial STREAM_INITIALIZED / ERROR sequence. */
|
|
51
|
+
private setupEventListeners;
|
|
52
|
+
/** Current streamId, or null if not initialized. */
|
|
53
|
+
getStreamId(): string | null;
|
|
54
|
+
/** Whether a valid streamId is available. */
|
|
55
|
+
isInitialized(): boolean;
|
|
56
|
+
/** IMA `replaceAdTagParameters()` updates targeting for future breaks without re-initializing. No-op (warning) before init. */
|
|
57
|
+
replaceAdTagParameters(params: Record<string, string>): void;
|
|
58
|
+
/** Forward a timed-metadata marker to IMA for SSAI ad tracking. Prefers raw ID3 bytes (IMA parses them itself). */
|
|
59
|
+
processTimedMetadata(cue: TimedMetadataCue): void;
|
|
60
|
+
/** Build a pod manifest URL with the current streamId. Throws if not initialized. */
|
|
61
|
+
buildPodUrl(podId: string, durationSeconds: number, params: GamVendorParameters): string;
|
|
62
|
+
/** Reset before starting a new playback session. */
|
|
63
|
+
reset(): void;
|
|
64
|
+
/** Destroy the stream manager and clean up resources. */
|
|
65
|
+
destroy(): void;
|
|
66
|
+
private log;
|
|
67
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{buildGamPodUrl as t}from"./GamPodUrlBuilder";import{debugLog as e}from"../diagnostics/debugLog";export class GamStreamManager{constructor(t,e,a=!1){this.videoElement=t,this.adUiContainer=e,this.streamManager=null,this.streamId=null,this.config=null,this.initPromise=null,this.callbacks={},this.debug=a}setCallbacks(t){this.callbacks=t}isImaSdkAvailable(){return"undefined"!=typeof google&&void 0!==google.ima&&void 0!==google.ima.dai&&void 0!==google.ima.dai.api}async initialize(t){return this.streamManager&&this.reset(),this.config=t,this.initPromise=this.doInitialize(),this.initPromise}async initializeLiveStream(t){if(this.streamManager&&this.reset(),!this.isImaSdkAvailable())throw new Error('Google IMA DAI SDK not found. Include <script src="https://imasdk.googleapis.com/js/sdkloader/ima3_dai.js"><\/script> in your page.');const e=this.adUiContainer||this.videoElement.parentElement||document.body;this.streamManager=new google.ima.dai.api.StreamManager(this.videoElement,e);const a=new google.ima.dai.api.LiveStreamRequest;return a.assetKey=t.assetKey,a.format="hls",t.networkCode&&(a.networkCode=t.networkCode),t.adTagParameters&&(a.adTagParameters=t.adTagParameters),t.streamActivityMonitorId&&(a.streamActivityMonitorId=t.streamActivityMonitorId),new Promise((e,i)=>{this.setupEventListeners(void 0,i,t=>e(t)),this.log(`Requesting live stream for asset: ${t.assetKey}`),this.streamManager.requestStream(a)})}async doInitialize(){if(!this.isImaSdkAvailable())throw new Error('Google IMA DAI SDK not found. Include <script src="https://imasdk.googleapis.com/js/sdkloader/ima3_dai.js"><\/script> in your page.');if(!this.config)throw new Error("GamStreamManager config not set");this.log("Initializing IMA StreamManager...");const t=this.adUiContainer||this.videoElement.parentElement||document.body;this.streamManager=new google.ima.dai.api.StreamManager(this.videoElement,t);const e=new google.ima.dai.api.PodStreamRequest;return e.networkCode=this.config.networkCode,e.customAssetKey=this.config.customAssetKey,e.format="hls",this.config.adTagParameters&&(e.adTagParameters=this.config.adTagParameters),this.config.streamActivityMonitorId&&(e.streamActivityMonitorId=this.config.streamActivityMonitorId),new Promise((t,a)=>{this.setupEventListeners(t,a),this.log(`Requesting stream for network: ${this.config.networkCode}, asset: ${this.config.customAssetKey}`),this.streamManager.requestStream(e)})}setupEventListeners(t,e,a){if(!this.streamManager)return;const i=google.ima.dai.api.StreamEvent.Type;this.streamManager.addEventListener(i.STREAM_INITIALIZED,e=>{const a=e.getStreamData();this.streamId=a.streamId,this.log(`Stream initialized, streamId: ${this.streamId}`),t?.()}),this.streamManager.addEventListener(i.LOADED,t=>{const e=t.getStreamData();this.streamId=e.streamId,e.url&&(this.log(`Stream loaded, url: ${e.url}`),a?.(e.url))}),this.streamManager.addEventListener(i.AD_BREAK_STARTED,()=>{this.log("Ad break started"),this.callbacks.onAdBreakStarted?.()}),this.streamManager.addEventListener(i.AD_BREAK_ENDED,()=>{this.log("Ad break ended"),this.callbacks.onAdBreakEnded?.()}),this.streamManager.addEventListener(i.STARTED,t=>{const e=function(t){const e=t.getAd?.();if(!e)return;const a=e.getAdPodInfo?.();return{adPosition:a?.getAdPosition(),totalAds:a?.getTotalAds(),adId:e.getAdId?.(),creativeId:e.getCreativeId?.()}}(t);this.log(`Ad started (ad ${e?.adPosition??"?"} of ${e?.totalAds??"?"})`),this.callbacks.onAdStarted?.(e)}),this.streamManager.addEventListener(i.COMPLETE,()=>{this.log("Ad complete"),this.callbacks.onAdComplete?.()}),this.streamManager.addEventListener(i.FIRST_QUARTILE,()=>{this.log("Ad first quartile"),this.callbacks.onAdFirstQuartile?.()}),this.streamManager.addEventListener(i.MIDPOINT,()=>{this.log("Ad midpoint"),this.callbacks.onAdMidpoint?.()}),this.streamManager.addEventListener(i.THIRD_QUARTILE,()=>{this.log("Ad third quartile"),this.callbacks.onAdThirdQuartile?.()}),this.streamManager.addEventListener(i.ERROR,t=>{const a=t.getStreamData().errorMessage||"Unknown IMA error";this.log(`IMA error: ${a}`),e?.(new Error(`IMA stream initialization failed: ${a}`)),this.callbacks.onError?.(a)})}getStreamId(){return this.streamId}isInitialized(){return null!==this.streamId}replaceAdTagParameters(t){this.streamManager&&this.streamId?(this.streamManager.replaceAdTagParameters(t),this.log("Ad tag parameters replaced")):this.log("replaceAdTagParameters called but stream not initialized — ignoring")}processTimedMetadata(t){this.streamManager&&(t.raw&&t.raw.length>0?this.streamManager.processMetadata("ID3",t.raw,t.startTime):t.frames&&Object.keys(t.frames).length>0?this.streamManager.onTimedMetadata(t.frames):t.messageData&&this.streamManager.onTimedMetadata({TXXX:t.messageData}))}buildPodUrl(e,a,i){if(!this.streamId)throw new Error("GamStreamManager not initialized. Call initialize() first.");const s=t(e,i,this.streamId,a);return this.log(`Built pod URL: ${s}`),s}reset(){this.streamManager&&this.streamManager.reset(),this.streamId=null,this.initPromise=null,this.streamManager=null,this.log("Stream manager reset")}destroy(){this.reset(),this.config=null,this.callbacks={}}log(t){this.debug&&e(`[GamStreamManager] ${t}`)}}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { LensHandle } from '../vendor/lens-sdk/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* The Ads SDK's product identity in Lens: bound at join, spanning every session
|
|
4
|
+
* of this SDK instance. Lens destination filters key on it.
|
|
5
|
+
*/
|
|
6
|
+
export declare const LENS_NAMESPACE = "ads";
|
|
7
|
+
/**
|
|
8
|
+
* Ties Lens observability to the Ads SDK session lifecycle. Always on and not
|
|
9
|
+
* integrator-configurable — no public config option reaches this class.
|
|
10
|
+
*
|
|
11
|
+
* One reporter per OptiViewAds instance, joining Lens on first use: two SDK
|
|
12
|
+
* instances on a page must not share a handle, since starting a session on a
|
|
13
|
+
* shared handle would roll the other instance's session out from under it.
|
|
14
|
+
*
|
|
15
|
+
* Every Lens call is wrapped: observability must never be able to break ad
|
|
16
|
+
* playback, so a broken Lens degrades to a debug log and nothing else.
|
|
17
|
+
*/
|
|
18
|
+
export declare class LensReporter {
|
|
19
|
+
private readonly log;
|
|
20
|
+
private lensHandle;
|
|
21
|
+
private joinFailed;
|
|
22
|
+
private destroyed;
|
|
23
|
+
constructor(log?: (...args: unknown[]) => void);
|
|
24
|
+
/** The joined Lens handle, for the follow-up that emits CMCD ad events. */
|
|
25
|
+
get handle(): LensHandle | null;
|
|
26
|
+
startSession(): void;
|
|
27
|
+
endSession(): void;
|
|
28
|
+
destroy(): void;
|
|
29
|
+
private join;
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Lens as s}from"../vendor/lens-sdk/index.js";import{configureLensBackend as e}from"./lensBackend";export const LENS_NAMESPACE="ads";export class LensReporter{constructor(s=()=>{}){this.log=s,this.lensHandle=null,this.joinFailed=!1,this.destroyed=!1}get handle(){return this.lensHandle}startSession(){const s=this.join();if(s)try{s.startSession()}catch(s){this.log("Lens session start failed:",s)}}endSession(){if(this.lensHandle)try{this.lensHandle.endSession()}catch(s){this.log("Lens session end failed:",s)}}destroy(){this.destroyed=!0;const s=this.lensHandle;if(this.lensHandle=null,s)try{s.dispose()}catch(s){this.log("Lens dispose failed:",s)}}join(){if(this.lensHandle)return this.lensHandle;if(this.destroyed||this.joinFailed)return null;try{e()}catch(s){this.log("Lens backend selection failed:",s)}try{this.lensHandle=s.join({namespace:"ads"})}catch(s){this.joinFailed=!0,this.log("Lens join failed:",s)}return this.lensHandle}}
|