@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,91 @@
|
|
|
1
|
+
import { BreakManifest } from '../types/BreakManifest';
|
|
2
|
+
import { ManifestRequestInterceptor, ManifestResponseInterceptor } from '../types/OptiViewAdsConfig';
|
|
3
|
+
import { ManifestSignatureKey } from './manifestSignature';
|
|
4
|
+
/** Narrow diagnostic sink so the service does not depend on the full diagnostics module. */
|
|
5
|
+
export type ManifestDiagnose = (code: 'DA-MANIFEST-INTERCEPT-FAILED' | 'DA-MANIFEST-REQUEST-INTERCEPT-FAILED' | 'DA-MANIFEST-SIGNATURE-INVALID', message: string, context?: Record<string, unknown>) => void;
|
|
6
|
+
/** Fetches, verifies, parses and polls break manifests. */
|
|
7
|
+
/**
|
|
8
|
+
* Whole-request budget for one manifest fetch. Matches the Android OkHttp `callTimeout` and
|
|
9
|
+
* stays inside a typical 30s caller budget so the failure is a specific manifest error.
|
|
10
|
+
*/
|
|
11
|
+
export declare const MANIFEST_FETCH_TIMEOUT_MS = 15000;
|
|
12
|
+
/**
|
|
13
|
+
* Evidence carried on every failed manifest read. Kept small so it is never sampled or dropped.
|
|
14
|
+
* `url` is the only field that shows a WRONG target (a base URL pointing at the API host 404s
|
|
15
|
+
* every manifest); `host` is what groups failures by origin. `hostOk`/`hostFail` (same host,
|
|
16
|
+
* same session) separate a dead backend from this client's path. No dns/connect/tls split: `fetch` cannot produce it.
|
|
17
|
+
*/
|
|
18
|
+
export interface ManifestFetchEvidence {
|
|
19
|
+
/** The exact URL that was read — the only field that shows a WRONG target. */
|
|
20
|
+
url: string;
|
|
21
|
+
/** Host of {@link url}, so failures can be grouped by origin. */
|
|
22
|
+
host: string;
|
|
23
|
+
/** How it failed: no answer, an HTTP error, or the connection itself. */
|
|
24
|
+
outcome: 'timeout' | 'http' | 'network';
|
|
25
|
+
/** HTTP status when a response arrived at all; omitted otherwise. */
|
|
26
|
+
status?: number;
|
|
27
|
+
/** Wall time spent on this attempt. */
|
|
28
|
+
ms: number;
|
|
29
|
+
/** Manifest reads to this host that SUCCEEDED earlier in this session. */
|
|
30
|
+
hostOk: number;
|
|
31
|
+
/** Manifest reads to this host that failed earlier in this session. */
|
|
32
|
+
hostFail: number;
|
|
33
|
+
}
|
|
34
|
+
export declare class ManifestService {
|
|
35
|
+
private manifestUrl;
|
|
36
|
+
private pollingInterval;
|
|
37
|
+
private pollingIntervalSeconds;
|
|
38
|
+
/** Current polling cadence, or null when not running. Bounds every latency measured against a poll. */
|
|
39
|
+
get currentPollingIntervalSeconds(): number | null;
|
|
40
|
+
private pollCallbacks;
|
|
41
|
+
private currentManifest;
|
|
42
|
+
private isActive;
|
|
43
|
+
private debug;
|
|
44
|
+
private interceptor;
|
|
45
|
+
private diagnose;
|
|
46
|
+
private requestInterceptor;
|
|
47
|
+
private trustedKeys;
|
|
48
|
+
/** Per-host tally of manifest reads this session, and the last failure. */
|
|
49
|
+
private readonly hostTally;
|
|
50
|
+
private lastFailure;
|
|
51
|
+
constructor(debug?: boolean, interceptor?: ManifestResponseInterceptor, diagnose?: ManifestDiagnose, requestInterceptor?: ManifestRequestInterceptor, trustedKeys?: readonly ManifestSignatureKey[]);
|
|
52
|
+
/**
|
|
53
|
+
* Evidence for the most recent failed read. Kept as a field rather than on the thrown error:
|
|
54
|
+
* `fetchWithTimeout` re-wraps a timeout into a new Error, and a field survives that on every platform.
|
|
55
|
+
*/
|
|
56
|
+
takeLastFailure(): ManifestFetchEvidence | null;
|
|
57
|
+
/** Count an outcome against its host and, when it failed, keep the evidence. */
|
|
58
|
+
private record;
|
|
59
|
+
/** Must be called before fetchManifest() or startPolling(). */
|
|
60
|
+
setManifestUrl(url: string): void;
|
|
61
|
+
/** Fetch the manifest once. */
|
|
62
|
+
fetchManifest(): Promise<BreakManifest>;
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the raw manifest body after the request interceptor (mock body, redirect, or
|
|
65
|
+
* headers). A thrown/rejected interceptor is non-fatal: diagnose and fetch the original URL.
|
|
66
|
+
*/
|
|
67
|
+
private fetchManifestData;
|
|
68
|
+
/** The interceptor's manifest, or the original when none is set or it throws. */
|
|
69
|
+
private applyInterceptor;
|
|
70
|
+
/** Parse and validate via the shared pure parser. */
|
|
71
|
+
private parseManifest;
|
|
72
|
+
/**
|
|
73
|
+
* Cadence comes only from the manifest's `polling` object (`active` during a break, `idle`
|
|
74
|
+
* otherwise). Without one no timer starts; the manifest is fetched once per session.
|
|
75
|
+
*/
|
|
76
|
+
startPolling(onUpdate: (manifest: BreakManifest) => void, onError: (error: Error) => void): void;
|
|
77
|
+
/** Stop polling for manifest updates. */
|
|
78
|
+
stopPolling(): void;
|
|
79
|
+
/** A running timer is cancelled and restarted with the interval the new state dictates. */
|
|
80
|
+
setActive(active: boolean): void;
|
|
81
|
+
/** Re-resolve the polling decision; a polled manifest may change or remove its `polling` object. */
|
|
82
|
+
private applyPollingDecision;
|
|
83
|
+
private clearPollingTimer;
|
|
84
|
+
/** Get the current manifest. */
|
|
85
|
+
getManifest(): BreakManifest | null;
|
|
86
|
+
/** Reset session state (manifest URL and cached manifest) before starting a new session. */
|
|
87
|
+
reset(): void;
|
|
88
|
+
/** Clean up resources. */
|
|
89
|
+
destroy(): void;
|
|
90
|
+
private log;
|
|
91
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{fetchWithTimeout as t}from"./fetchWithTimeout";import{verifyManifestSignature as e,MANIFEST_SIGNATURE_HEADER as i}from"./manifestSignature";import{embeddedManifestKeys as s}from"./manifestTrustedKeys";import{parseBreakManifest as n}from"./parseBreakManifest";import{resolvePollingDecision as r}from"./resolvePollingDecision";import{debugLog as l}from"../diagnostics/debugLog";export const MANIFEST_FETCH_TIMEOUT_MS=15e3;export class ManifestService{get currentPollingIntervalSeconds(){return this.pollingIntervalSeconds}constructor(t=!1,e,i,n,r){this.manifestUrl=null,this.pollingInterval=null,this.pollingIntervalSeconds=null,this.pollCallbacks=null,this.currentManifest=null,this.isActive=!1,this.hostTally=new Map,this.lastFailure=null,this.debug=t,this.interceptor=e,this.diagnose=i,this.requestInterceptor=n,this.trustedKeys=r??s()}takeLastFailure(){const t=this.lastFailure;return this.lastFailure=null,t}record(t,e,i,s){const n=function(t){try{return new URL(t).host}catch{return t}}(t),r=this.hostTally.get(n)??{ok:0,fail:0};if("ok"===e)return r.ok++,void this.hostTally.set(n,r);this.lastFailure={url:t,host:n,outcome:e,ms:i,hostOk:r.ok,hostFail:r.fail,...void 0===s?{}:{status:s}},r.fail++,this.hostTally.set(n,r)}setManifestUrl(t){this.manifestUrl=t}async fetchManifest(){if(!this.manifestUrl)throw new Error("Manifest URL not set. Call setManifestUrl() before fetching.");this.log("Fetching manifest from:",this.manifestUrl);const t=await this.fetchManifestData(this.manifestUrl),e=this.parseManifest(t),i=await this.applyInterceptor(e);return this.currentManifest=i,this.log("Manifest loaded:",i.breaks.length,"breaks"),i}async fetchManifestData(s){const n={url:s};let r;if(this.requestInterceptor)try{r=await this.requestInterceptor({...n},{url:s})??void 0}catch(t){const e=t instanceof Error?t.message:String(t);this.diagnose?.("DA-MANIFEST-REQUEST-INTERCEPT-FAILED",e,{url:s}),this.log("Manifest request interceptor threw; using network fetch:",e),r=void 0}if(r&&"body"in r)return this.log("Manifest request interceptor returned a mock response (no network)"),"string"==typeof r.body?JSON.parse(r.body):r.body;const l=r??n,o=Date.now();let a;try{a=await t(l.url,l.headers?{headers:l.headers}:{},15e3,"Failed to fetch manifest: no response within 15000ms")}catch(t){const e=t instanceof Error&&t.message.includes("no response within");throw this.record(l.url,e?"timeout":"network",Date.now()-o),t}if(!a.ok)throw this.record(l.url,"http",Date.now()-o,a.status),new Error(`Failed to fetch manifest: ${a.status} ${a.statusText}`);this.record(l.url,"ok",Date.now()-o);const h=new Uint8Array(await a.arrayBuffer()),c=a.headers.get(i),f=e(c,h,this.trustedKeys);if(!f.valid){const t=`Manifest signature verification failed: ${f.reason}`;throw this.diagnose?.("DA-MANIFEST-SIGNATURE-INVALID",t,{url:l.url,reason:f.reason}),this.log("Rejecting manifest:",t),new Error(t)}return JSON.parse((new TextDecoder).decode(h))}async applyInterceptor(t){if(!this.interceptor)return t;const e=this.manifestUrl??"";try{return await this.interceptor(t,{url:e})}catch(i){const s=i instanceof Error?i.message:String(i);return this.diagnose?.("DA-MANIFEST-INTERCEPT-FAILED",s,{url:e}),this.log("Manifest interceptor threw; using un-modified manifest:",s),t}}parseManifest(t){return n(t)}startPolling(t,e){this.pollCallbacks={onUpdate:t,onError:e},this.applyPollingDecision()}stopPolling(){this.pollCallbacks=null,this.clearPollingTimer()}setActive(t){this.isActive!==t&&(this.isActive=t,this.log("Active state changed:",t),this.applyPollingDecision())}applyPollingDecision(){if(!this.pollCallbacks)return;const t=r(this.currentManifest?.polling,this.isActive);if("once"===t.mode)return this.pollingInterval&&this.log("Manifest has no polling object — fetch-once, stopping timer"),void this.clearPollingTimer();this.pollingInterval&&this.pollingIntervalSeconds===t.intervalSeconds||(this.clearPollingTimer(),this.log("Starting polling with interval:",t.intervalSeconds,"seconds"),this.pollingIntervalSeconds=t.intervalSeconds,this.pollingInterval=setInterval(async()=>{const t=this.pollCallbacks;if(t){try{const e=await this.fetchManifest();e.breaks.length>0&&this.log(`📡 Manifest: ${e.breaks.length} break(s)`),t.onUpdate(e)}catch(e){t.onError(e instanceof Error?e:new Error(String(e)))}this.applyPollingDecision()}},1e3*t.intervalSeconds))}clearPollingTimer(){this.pollingInterval&&(clearInterval(this.pollingInterval),this.pollingInterval=null,this.pollingIntervalSeconds=null,this.log("Polling stopped"))}getManifest(){return this.currentManifest}reset(){this.stopPolling(),this.currentManifest=null,this.manifestUrl=null,this.isActive=!1}destroy(){this.stopPolling(),this.currentManifest=null,this.manifestUrl=null}log(...t){this.debug&&l("[OptiViewAds:ManifestService]",...t)}}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { TimedMetadataCue } from '../interfaces/PlayerAdapter';
|
|
2
|
+
/** ID3 GEOB description of an Anvato break-signaling frame. */
|
|
3
|
+
export declare const ANVATO_ID3_DESCRIPTION = "Anvatos";
|
|
4
|
+
/** MIME type of Anvato GEOB frames; must match when surfaced, may be omitted by parsers that expose none. */
|
|
5
|
+
export declare const ANVATO_ID3_MIME_TYPE = "application/json";
|
|
6
|
+
/** DASH/CMAF emsg scheme id URI of an Anvato break-signaling event. */
|
|
7
|
+
export declare const ANVATO_EMSG_SCHEME_ID_URI = "urn:anvato:es1:052016";
|
|
8
|
+
/** Standardized "ID3 in emsg" schemes (CTA-5001 / Apple): message data is a complete raw ID3v2 tag. */
|
|
9
|
+
export declare const ID3_IN_EMSG_SCHEME_ID_URIS: readonly string[];
|
|
10
|
+
/** Returns true for the standardized ID3-in-emsg scheme id URIs. */
|
|
11
|
+
export declare function isId3InEmsgScheme(schemeIdUri: string | undefined): boolean;
|
|
12
|
+
/** Tolerance (seconds) matching a manifest `start` PTS to a cue `pts`; encoder/backend rounding differs sub-millisecond. */
|
|
13
|
+
export declare const PTS_MATCH_TOLERANCE_SECONDS = 0.005;
|
|
14
|
+
/** Anvato marker: GEOB with description "Anvatos" (MIME `application/json` when surfaced) or emsg with the Anvato scheme. */
|
|
15
|
+
export declare function isAnvatoCue(cue: TimedMetadataCue): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* The Anvato payload (`type=cue&pts=…`) from an HLS ID3 GEOB cue, a direct Anvato emsg, or
|
|
18
|
+
* ID3-in-emsg (`raw` holds a complete ID3v2 tag); null when the cue is not Anvato signaling.
|
|
19
|
+
*/
|
|
20
|
+
export declare function anvatoPayloadFromCue(cue: TimedMetadataCue): string | null;
|
|
21
|
+
/**
|
|
22
|
+
* Hand-written form-urlencoded parse so the Kotlin/Swift cores reproduce the exact semantics:
|
|
23
|
+
* split on `&`, then on the FIRST `=`, `+` as space, percent-decode, raw text on failure, first duplicate wins.
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseAnvatoPayload(payload: string): Record<string, string>;
|
|
26
|
+
/**
|
|
27
|
+
* Maps a break's manifest `pts` to the media time its Anvato cue was observed at. With
|
|
28
|
+
* `ptsSource: 'anvatoCue'` a PTS break is only schedulable once its cue was seen. Portable brain logic.
|
|
29
|
+
*/
|
|
30
|
+
export declare class MetadataCueTracker {
|
|
31
|
+
/** Observed cue `pts` parameter (seconds) → cue media time (seconds). */
|
|
32
|
+
private mediaTimeByPts;
|
|
33
|
+
/** Ignores non-Anvato cues, non-`type=cue` payloads, and non-finite `pts`. True when a cue was recorded or refreshed. */
|
|
34
|
+
handleCue(cue: TimedMetadataCue): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Media time of the cue matching `pts` within {@link PTS_MATCH_TOLERANCE_SECONDS}, or null.
|
|
37
|
+
* Closest wins; ties resolve to the lower cue PTS so the result is observation-order independent.
|
|
38
|
+
*/
|
|
39
|
+
mediaTimeForPts(pts: number): number | null;
|
|
40
|
+
/** Number of distinct cue PTS values currently tracked. */
|
|
41
|
+
get size(): number;
|
|
42
|
+
/** Drop all tracked cues (session end / destroy). */
|
|
43
|
+
clear(): void;
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{decodeUtf8 as e,parseId3GeobFrames as t}from"../id3/parseId3Geob";export const ANVATO_ID3_DESCRIPTION="Anvatos";export const ANVATO_ID3_MIME_TYPE="application/json";export const ANVATO_EMSG_SCHEME_ID_URI="urn:anvato:es1:052016";export const ID3_IN_EMSG_SCHEME_ID_URIS=["https://aomedia.org/emsg/ID3","https://developer.apple.com/streaming/emsg-id3"];export function isId3InEmsgScheme(e){return void 0!==e&&ID3_IN_EMSG_SCHEME_ID_URIS.includes(e)}export const PTS_MATCH_TOLERANCE_SECONDS=.005;export function isAnvatoCue(e){return"id3"===e.type?"Anvatos"===e.description&&(void 0===e.mimeType||"application/json"===e.mimeType):"emsg"===e.type&&"urn:anvato:es1:052016"===e.schemeIdUri}export function anvatoPayloadFromCue(n){if(isAnvatoCue(n))return n.messageData??null;if("emsg"===n.type&&isId3InEmsgScheme(n.schemeIdUri)&&n.raw)for(const o of t(n.raw))if("Anvatos"===o.description&&"application/json"===o.mimeType)return e(o.data);return null}export function parseAnvatoPayload(e){const t={};for(const o of e.split("&")){if(""===o)continue;const e=o.indexOf("="),i=-1===e?o:o.slice(0,e),s=-1===e?"":o.slice(e+1),r=n(i);r in t||(t[r]=n(s))}return t}function n(e){const t=e.replace(/\+/g," ");try{return decodeURIComponent(t)}catch{return t}}export class MetadataCueTracker{constructor(){this.mediaTimeByPts=new Map}handleCue(e){if(!Number.isFinite(e.startTime))return!1;const t=anvatoPayloadFromCue(e);if(!t)return!1;const n=parseAnvatoPayload(t);if("cue"!==n.type)return!1;const o=Number(n.pts);return!!Number.isFinite(o)&&(this.mediaTimeByPts.set(o,e.startTime),!0)}mediaTimeForPts(e){let t=null,n=1/0;for(const o of this.mediaTimeByPts.keys()){const i=Math.abs(o-e);i>.005||(i<n||i===n&&o<t)&&(t=o,n=i)}return null===t?null:this.mediaTimeByPts.get(t)}get size(){return this.mediaTimeByPts.size}clear(){this.mediaTimeByPts.clear()}}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Asset, Break, BreakManifest, MediaType } from '../types/BreakManifest';
|
|
2
|
+
import { Clock } from './BreakScheduler';
|
|
3
|
+
/** The resolved pause ad: `source` picks direct load (`static`) or VAST fetch (`vast`); `mediaType` picks image overlay or muted play-once `<video>`. */
|
|
4
|
+
export interface PauseAdResolution {
|
|
5
|
+
/** The pause break (`start: { type: 'event', event: 'pause' }`). */
|
|
6
|
+
break: Break;
|
|
7
|
+
/** The first asset of the first variant (image or video). */
|
|
8
|
+
asset: Asset;
|
|
9
|
+
/** How the asset is sourced. */
|
|
10
|
+
source: 'static' | 'vast';
|
|
11
|
+
/** Whether the asset is an image or a video. */
|
|
12
|
+
mediaType: MediaType;
|
|
13
|
+
}
|
|
14
|
+
/** Fired when the pause ad should be shown. */
|
|
15
|
+
export type PauseAdShowCallback = (resolution: PauseAdResolution) => void;
|
|
16
|
+
/** Fired when the pause ad should be hidden (on resume or duration cap). */
|
|
17
|
+
export type PauseAdHideCallback = (breakId: string) => void;
|
|
18
|
+
/** Fired once per shown pause ad when the viewer may resume content (see {@link PauseAdController.canResume}). */
|
|
19
|
+
export type PauseAdResumeAllowedCallback = (breakId: string) => void;
|
|
20
|
+
/** Structured diagnostic sink. */
|
|
21
|
+
export type PauseAdDiagnoseCallback = (code: 'DA-PAUSE-AD-SHOWN' | 'DA-PAUSE-AD-DISMISSED' | 'DA-PAUSE-AD-NO-ASSET' | 'DA-PAUSE-AD-RESUME-ALLOWED' | 'DA-PAUSE-AD-RESUME-BLOCKED', message: string, context?: Record<string, unknown>) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Event-driven pause-ad state machine (spec `EventTrigger` with `event: 'pause'`).
|
|
24
|
+
*
|
|
25
|
+
* Every pause consumes the next pause break in manifest order, wrapping to the first once the
|
|
26
|
+
* list is exhausted. The break's `start.delay` must elapse while still paused before it shows;
|
|
27
|
+
* `duration` caps the display. While shown, content may resume only once `controls.skipOffset`
|
|
28
|
+
* elapsed — or, without a skipOffset, once a video creative ended or an image reached its
|
|
29
|
+
* duration cap (an image with neither can be resumed at once, so the viewer is never locked).
|
|
30
|
+
*
|
|
31
|
+
* DOM-free and clock-injected so the conformance harness can step {@link tick}. Rendering, VAST
|
|
32
|
+
* fetching and tracking live in the render layer. SGAI-only (not constructed in SSAI mode).
|
|
33
|
+
*/
|
|
34
|
+
export declare class PauseAdController {
|
|
35
|
+
private clock;
|
|
36
|
+
private debug;
|
|
37
|
+
private onShow;
|
|
38
|
+
private onHide;
|
|
39
|
+
private onResumeAllowed;
|
|
40
|
+
private diagnose;
|
|
41
|
+
/** Pause breaks of the current manifest, in manifest order. */
|
|
42
|
+
private pauseBreaks;
|
|
43
|
+
/** Index into {@link pauseBreaks} of the break the next pause will consume. */
|
|
44
|
+
private nextIndex;
|
|
45
|
+
/** The ad currently shown, or null when hidden. */
|
|
46
|
+
private current;
|
|
47
|
+
/** Whether content is currently paused. */
|
|
48
|
+
private paused;
|
|
49
|
+
/** Clock time (ms) when the current pause began, or null when playing. */
|
|
50
|
+
private pausedAtMs;
|
|
51
|
+
/** Ad selected for the current pause, waiting for its `start.delay`; null once shown or when none is usable. */
|
|
52
|
+
private pending;
|
|
53
|
+
/** Clock time (ms) when the ad was shown, or null when hidden. */
|
|
54
|
+
private shownAtMs;
|
|
55
|
+
/** Whether resume has already been announced for the shown ad. */
|
|
56
|
+
private resumeAnnounced;
|
|
57
|
+
/** Whether the shown video creative ended (or cannot play). */
|
|
58
|
+
private creativeEnded;
|
|
59
|
+
constructor(options?: {
|
|
60
|
+
clock?: Clock;
|
|
61
|
+
debug?: boolean;
|
|
62
|
+
});
|
|
63
|
+
/** Register the show/hide/resume-allowed/diagnostic callbacks. */
|
|
64
|
+
setCallbacks(callbacks: {
|
|
65
|
+
onShow?: PauseAdShowCallback;
|
|
66
|
+
onHide?: PauseAdHideCallback;
|
|
67
|
+
onResumeAllowed?: PauseAdResumeAllowedCallback;
|
|
68
|
+
diagnose?: PauseAdDiagnoseCallback;
|
|
69
|
+
}): void;
|
|
70
|
+
/** Replace the pause-break list. A shown ad stays up; the rotation index is kept (wrapped). */
|
|
71
|
+
updateManifest(manifest: BreakManifest): void;
|
|
72
|
+
/** Notify the controller that content was paused (from the `pause` event). */
|
|
73
|
+
onPause(): void;
|
|
74
|
+
/**
|
|
75
|
+
* Notify the controller that content resumed (from the `playing` event). Returns false when the
|
|
76
|
+
* shown ad must still be watched ({@link canResume}); the caller then pauses content again and
|
|
77
|
+
* the ad stays up.
|
|
78
|
+
*/
|
|
79
|
+
onResume(): boolean;
|
|
80
|
+
/** Arm-after-`delay` show, `skipOffset` reveal and `duration`-cap dismiss; periodic in production, stepped by the harness. */
|
|
81
|
+
tick(): void;
|
|
82
|
+
/** Whether the pause ad is currently shown. */
|
|
83
|
+
isShowing(): boolean;
|
|
84
|
+
/** Notify the controller that the shown video creative ended or cannot play (from the renderer). */
|
|
85
|
+
onCreativeEnded(breakId: string): void;
|
|
86
|
+
/**
|
|
87
|
+
* Whether the viewer may resume content now. Always true when no ad is shown. While shown:
|
|
88
|
+
* `controls.skipOffset` elapsed. Without a skipOffset the ad must be watched fully: a video
|
|
89
|
+
* until it ends, an image until the duration cap (no cap: at once).
|
|
90
|
+
*/
|
|
91
|
+
canResume(): boolean;
|
|
92
|
+
private evaluate;
|
|
93
|
+
/**
|
|
94
|
+
* Next usable pause break in manifest order, starting at the rotation index and wrapping once.
|
|
95
|
+
* Unusable breaks are skipped (diagnosed) so they never swallow a pause.
|
|
96
|
+
*/
|
|
97
|
+
private selectNext;
|
|
98
|
+
private resolve;
|
|
99
|
+
private show;
|
|
100
|
+
private announceResumeIfAllowed;
|
|
101
|
+
private hide;
|
|
102
|
+
/** Clear runtime state between sessions while keeping registered callbacks. */
|
|
103
|
+
reset(): void;
|
|
104
|
+
/** Clear all state (session end / destroy). */
|
|
105
|
+
destroy(): void;
|
|
106
|
+
private clearState;
|
|
107
|
+
private log;
|
|
108
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{AssetType as e,isEventTrigger as s,isPauseBreak as t,MediaType as n}from"../types/BreakManifest";import{systemClock as i}from"./BreakScheduler";import{debugLog as h}from"../diagnostics/debugLog";export class PauseAdController{constructor(e){this.onShow=null,this.onHide=null,this.onResumeAllowed=null,this.diagnose=null,this.pauseBreaks=[],this.nextIndex=0,this.current=null,this.paused=!1,this.pausedAtMs=null,this.pending=null,this.shownAtMs=null,this.resumeAnnounced=!1,this.creativeEnded=!1,this.clock=e?.clock??i,this.debug=e?.debug??!1}setCallbacks(e){this.onShow=e.onShow??null,this.onHide=e.onHide??null,this.onResumeAllowed=e.onResumeAllowed??null,this.diagnose=e.diagnose??null}updateManifest(e){this.pauseBreaks=e.breaks.filter(t),0===this.pauseBreaks.length?this.nextIndex=0:this.nextIndex%=this.pauseBreaks.length}onPause(){this.paused||(this.paused=!0,this.pausedAtMs=this.clock.now(),this.pending=this.selectNext(),this.log("content paused"),this.evaluate())}onResume(){if(!this.paused&&null===this.shownAtMs)return!0;if(null!==this.shownAtMs&&!this.canResume()){const e=this.current;return this.diagnose?.("DA-PAUSE-AD-RESUME-BLOCKED",`Pause ad "${e.break.id}" must still be watched; content stays paused.`,{breakId:e.break.id,elapsedSec:(this.clock.now()-this.shownAtMs)/1e3,skipOffset:e.break.controls?.skipOffset??null}),!1}return this.paused=!1,this.pausedAtMs=null,this.pending=null,this.hide("resume"),!0}tick(){this.evaluate()}isShowing(){return null!==this.shownAtMs}onCreativeEnded(e){null!==this.shownAtMs&&this.current?.break.id===e&&(this.creativeEnded=!0,this.announceResumeIfAllowed())}canResume(){if(null===this.shownAtMs||!this.current)return!0;const e=this.clock.now()-this.shownAtMs,s=this.current.break.controls?.skipOffset;if(void 0!==s)return e>=1e3*s;if(this.current.mediaType===n.VIDEO)return this.creativeEnded;const t=this.current.break.duration??0;return t<=0||e>=1e3*t}evaluate(){if(null!==this.shownAtMs&&this.current){const e=this.current.break.duration??0;return e>0&&this.clock.now()-this.shownAtMs>=1e3*e?void this.hide("duration"):void this.announceResumeIfAllowed()}if(!this.paused||null===this.pausedAtMs||!this.pending)return;const{index:e,resolution:t}=this.pending,{start:n}=t.break,i=s(n)&&n.delay||0;this.clock.now()-this.pausedAtMs<1e3*i||(this.pending=null,this.nextIndex=(e+1)%this.pauseBreaks.length,this.show(t))}selectNext(){const e=this.pauseBreaks.length;for(let s=0;s<e;s++){const t=(this.nextIndex+s)%e,n=this.resolve(this.pauseBreaks[t]);if(n)return{index:t,resolution:n}}return null}resolve(s){const t=Array.isArray(s.variant)?s.variant[0]:s.variant,i=(t?.assets??[])[0];if(!i||i.mediaType!==n.IMAGE&&i.mediaType!==n.VIDEO)return this.diagnose?.("DA-PAUSE-AD-NO-ASSET",`Pause break "${s.id}" has no image or video asset; skipping.`,{breakId:s.id}),null;return{break:s,asset:i,source:i.type===e.VAST?"vast":"static",mediaType:i.mediaType}}show(e){this.current=e,this.shownAtMs=this.clock.now(),this.resumeAnnounced=!1,this.creativeEnded=!1,this.log(`showing pause ad "${e.break.id}" (${e.source}, ${e.mediaType})`),this.diagnose?.("DA-PAUSE-AD-SHOWN",`Pause ad "${e.break.id}" shown.`,{breakId:e.break.id,source:e.source,mediaType:e.mediaType,skipOffset:e.break.controls?.skipOffset??null}),this.onShow?.(e),this.announceResumeIfAllowed()}announceResumeIfAllowed(){if(this.resumeAnnounced||!this.current||!this.canResume())return;this.resumeAnnounced=!0;const e=this.current.break.id;this.log(`pause ad "${e}" may be resumed`),this.diagnose?.("DA-PAUSE-AD-RESUME-ALLOWED",`Pause ad "${e}" may be resumed.`,{breakId:e}),this.onResumeAllowed?.(e)}hide(e){if(null===this.shownAtMs)return;const s=this.current?.break.id??"";this.shownAtMs=null,this.current=null,this.resumeAnnounced=!1,this.creativeEnded=!1,this.log(`hiding pause ad "${s}" (${e})`),this.diagnose?.("DA-PAUSE-AD-DISMISSED",`Pause ad "${s}" dismissed (${e}).`,{breakId:s,reason:e}),this.onHide?.(s)}reset(){this.clearState()}destroy(){this.clearState(),this.onShow=null,this.onHide=null,this.onResumeAllowed=null,this.diagnose=null}clearState(){this.pauseBreaks=[],this.nextIndex=0,this.current=null,this.paused=!1,this.pausedAtMs=null,this.pending=null,this.shownAtMs=null,this.resumeAnnounced=!1}log(...e){this.debug&&h("[PauseAdController]",...e)}}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { BreakState } from './BreakScheduler';
|
|
2
|
+
import type { Clock } from './BreakScheduler';
|
|
3
|
+
import { Break } from '../types/BreakManifest';
|
|
4
|
+
import type { BreakTimingState } from './breakTiming';
|
|
5
|
+
export interface TrackedBreak {
|
|
6
|
+
break: Break;
|
|
7
|
+
state: BreakState;
|
|
8
|
+
}
|
|
9
|
+
export interface RewindRearmDeps {
|
|
10
|
+
breaks: Map<string, TrackedBreak>;
|
|
11
|
+
retiredBreaks: Map<string, Break>;
|
|
12
|
+
completedBreakIds: Set<string>;
|
|
13
|
+
preloadedBreakIds: Set<string>;
|
|
14
|
+
timing: BreakTimingState;
|
|
15
|
+
clock: Clock;
|
|
16
|
+
log: (...args: unknown[]) => void;
|
|
17
|
+
getBreakStartTime: (breakInfo: Break) => number | null;
|
|
18
|
+
}
|
|
19
|
+
export declare class RewindRearm {
|
|
20
|
+
private readonly deps;
|
|
21
|
+
/**
|
|
22
|
+
* `adStartDelay` in ms: played media time before ANY break may start. Breaks that pass during
|
|
23
|
+
* the delay are not started retroactively but stay unconsumed. Zero means no delay.
|
|
24
|
+
*/
|
|
25
|
+
private _adStartDelayMs;
|
|
26
|
+
private adStartDelayElapsedLogged;
|
|
27
|
+
private adStartDelayIgnored;
|
|
28
|
+
private channelPlayedBaselineMs;
|
|
29
|
+
/** Set once content playback began this session (spec `event: 'start'`); pre-rolls wait for it. */
|
|
30
|
+
private _playbackStarted;
|
|
31
|
+
/**
|
|
32
|
+
* Played media time since playback start, shared by `adStartDelay` and pre-roll delays. Forward
|
|
33
|
+
* progress per tick is capped at the wallclock elapsed, so a paused player credits nothing and
|
|
34
|
+
* a seek credits at most one tick.
|
|
35
|
+
*/
|
|
36
|
+
private _playedMs;
|
|
37
|
+
/** Timebase position (ms) at the previous played-time sample. */
|
|
38
|
+
private lastPlayedPositionMs;
|
|
39
|
+
/** Wallclock (ms) at the previous played-time sample. */
|
|
40
|
+
private lastPlayedClockMs;
|
|
41
|
+
/**
|
|
42
|
+
* Set while content is RELOADED rather than seeked (shared-element path). The playhead passes
|
|
43
|
+
* through ~0, which the rewind detector would read as a seek before the break and re-arm it;
|
|
44
|
+
* during a reload there is no meaningful previous position to compare against.
|
|
45
|
+
*/
|
|
46
|
+
private contentReloading;
|
|
47
|
+
/**
|
|
48
|
+
* Guard for the SDK's own post-break resume-seek ({@link notifyResumeSeek}): a player may
|
|
49
|
+
* emit a seek adjustment after the target is accepted, so a backward step inside the finished
|
|
50
|
+
* break window is internal while armed. Cleared on the first forward sample.
|
|
51
|
+
*/
|
|
52
|
+
private resumeSeekBaselineMs;
|
|
53
|
+
private resumeSeekBreakEndMs;
|
|
54
|
+
private resumeSeekInFlight;
|
|
55
|
+
/**
|
|
56
|
+
* Pre-step position of a backward step awaiting confirmation. A rewind counts only when two
|
|
57
|
+
* consecutive samples sit behind it: a single backward sample can be a transient (PiP
|
|
58
|
+
* hand-off, stale position mid-recovery).
|
|
59
|
+
*/
|
|
60
|
+
private pendingRewindFromMs;
|
|
61
|
+
/** currentTime (ms) observed on the previous tick, for backward-seek detection. */
|
|
62
|
+
private lastCurrentTime;
|
|
63
|
+
/**
|
|
64
|
+
* Minimum backward step for a WALLCLOCK rewind to count as a DVR seek-back. Live PDT is
|
|
65
|
+
* fragment-anchored and jitters backward by fractions of a second, which would re-fire the
|
|
66
|
+
* break just watched. PTS positions are exact, so no jitter tolerance applies there.
|
|
67
|
+
*/
|
|
68
|
+
private readonly REWIND_JITTER_TOLERANCE_MS;
|
|
69
|
+
constructor(deps: RewindRearmDeps);
|
|
70
|
+
/** While true no break may start and consumers must not apply break-driven restrictions (e.g. pause ads). */
|
|
71
|
+
isAdStartDelayActive(): boolean;
|
|
72
|
+
get playbackStarted(): boolean;
|
|
73
|
+
get playedMs(): number;
|
|
74
|
+
/** Anchor played-time tracking at the position where content playback began. */
|
|
75
|
+
markPlaybackStart(positionMs: number | null): void;
|
|
76
|
+
setAdStartDelayMs(ms: number): void;
|
|
77
|
+
/**
|
|
78
|
+
* The schedule now follows another channel: pre-roll `delay` counts from here. Once playback
|
|
79
|
+
* has started, `adStartDelay` is frozen (also for later polls); before that the next manifest's
|
|
80
|
+
* value applies.
|
|
81
|
+
*/
|
|
82
|
+
beginChannel(): void;
|
|
83
|
+
/** Played time since the current channel took over (whole session for the first channel). */
|
|
84
|
+
get channelPlayedMs(): number;
|
|
85
|
+
get adStartDelayMs(): number;
|
|
86
|
+
/** Bracket a content RELOAD so the new stream's start is not read as a backward seek. `false` also discards the stale previous position. */
|
|
87
|
+
setContentReloading(active: boolean): void;
|
|
88
|
+
/**
|
|
89
|
+
* The runtime is about to resume-seek content to `targetSeconds` after a break ending at
|
|
90
|
+
* `breakEndSeconds`. Arms a guard so the small backward step is not read as a DVR seek-back;
|
|
91
|
+
* a genuine seek below the cue still re-arms. PTS timebase only (`seconds * 1000`).
|
|
92
|
+
*/
|
|
93
|
+
notifyResumeSeek(targetSeconds: number, breakEndSeconds: number): void;
|
|
94
|
+
/** A viewer seek through the SDK: drop the resume-seek guard so the step is judged by the normal rules. */
|
|
95
|
+
notifyViewerSeek(): void;
|
|
96
|
+
/** Forward progress is capped at the wallclock elapsed: a paused player credits nothing, a seek at most one tick. */
|
|
97
|
+
trackPlayedTime(currentTime: number | null): void;
|
|
98
|
+
/**
|
|
99
|
+
* Re-arm retired position breaks when the playhead moves backward past their end (DVR seek
|
|
100
|
+
* back). Live breaks are untouched. A rewind needs TWO consecutive samples behind the
|
|
101
|
+
* pre-step position (see `pendingRewindFromMs`), so a one-sample transient never re-arms.
|
|
102
|
+
*/
|
|
103
|
+
rearmOnRewind(currentTime: number): void;
|
|
104
|
+
reset(): void;
|
|
105
|
+
/** True when `currentTime` sits behind `from` by more than live PDT jitter. */
|
|
106
|
+
private isRewindBeyondJitter;
|
|
107
|
+
/** Confirmed DVR rewind: drop the resume guard and re-arm retired breaks. */
|
|
108
|
+
private rearmRetiredBreaks;
|
|
109
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{BreakState as e}from"./BreakScheduler";import{Timebase as s}from"../types/BreakManifest";export class RewindRearm{constructor(e){this.deps=e,this._adStartDelayMs=0,this.adStartDelayElapsedLogged=!1,this.adStartDelayIgnored=!1,this.channelPlayedBaselineMs=0,this._playbackStarted=!1,this._playedMs=0,this.lastPlayedPositionMs=null,this.lastPlayedClockMs=null,this.contentReloading=!1,this.resumeSeekBaselineMs=null,this.resumeSeekBreakEndMs=null,this.resumeSeekInFlight=!1,this.pendingRewindFromMs=null,this.lastCurrentTime=null,this.REWIND_JITTER_TOLERANCE_MS=2e3}isAdStartDelayActive(){return this._adStartDelayMs>0&&this._playedMs<this._adStartDelayMs}get playbackStarted(){return this._playbackStarted}get playedMs(){return this._playedMs}markPlaybackStart(e){this._playbackStarted=!0,this.lastPlayedPositionMs=e,this.lastPlayedClockMs=this.deps.clock.now()}setAdStartDelayMs(e){this.adStartDelayIgnored||(this._adStartDelayMs=e)}beginChannel(){this.channelPlayedBaselineMs=this._playedMs,this._playbackStarted&&(this._adStartDelayMs=0,this.adStartDelayIgnored=!0)}get channelPlayedMs(){return this._playedMs-this.channelPlayedBaselineMs}get adStartDelayMs(){return this._adStartDelayMs}setContentReloading(e){this.contentReloading=e,e||(this.lastCurrentTime=null),this.pendingRewindFromMs=null}notifyResumeSeek(e,s){Number.isFinite(e)&&Number.isFinite(s)&&(this.resumeSeekBaselineMs=1e3*e,this.resumeSeekBreakEndMs=1e3*s,this.resumeSeekInFlight=!0,this.lastCurrentTime=this.resumeSeekBaselineMs,this.pendingRewindFromMs=null)}notifyViewerSeek(){this.resumeSeekBaselineMs=null,this.resumeSeekBreakEndMs=null,this.resumeSeekInFlight=!1}trackPlayedTime(e){if(!this._playbackStarted)return;const s=this.deps.clock.now();if(null===e)return this.lastPlayedPositionMs=null,void(this.lastPlayedClockMs=s);if(null!==this.lastPlayedPositionMs&&null!==this.lastPlayedClockMs){const t=e-this.lastPlayedPositionMs,i=s-this.lastPlayedClockMs;this._playedMs+=Math.min(Math.max(0,t),Math.max(0,i)),this._adStartDelayMs>0&&this._playedMs>=this._adStartDelayMs&&!this.adStartDelayElapsedLogged&&(this.adStartDelayElapsedLogged=!0,this.deps.log(`⏰ Ad start delay elapsed (${this._adStartDelayMs/1e3}s played)`))}this.lastPlayedPositionMs=e,this.lastPlayedClockMs=s}rearmOnRewind(e){const s=this.lastCurrentTime;if(this.lastCurrentTime=e,null!==this.resumeSeekBreakEndMs&&e>this.resumeSeekBreakEndMs&&(this.resumeSeekBaselineMs=null,this.resumeSeekBreakEndMs=null,this.resumeSeekInFlight=!1),this.contentReloading)this.pendingRewindFromMs=null;else{if(null!==this.pendingRewindFromMs){const s=this.pendingRewindFromMs;if(this.pendingRewindFromMs=null,this.isRewindBeyondJitter(s,e))return void this.rearmRetiredBreaks(e)}null===s||e>=s?this.resumeSeekInFlight&&null!==s&&e>s&&(this.resumeSeekInFlight=!1,this.resumeSeekBaselineMs=null,this.resumeSeekBreakEndMs=null):this.isRewindBeyondJitter(s,e)&&(this.resumeSeekInFlight&&null!==this.resumeSeekBreakEndMs&&e>=0&&e<=this.resumeSeekBreakEndMs||(this.pendingRewindFromMs=s))}}reset(){this.lastCurrentTime=null,this._adStartDelayMs=0,this.adStartDelayElapsedLogged=!1,this._playbackStarted=!1,this._playedMs=0,this.adStartDelayIgnored=!1,this.channelPlayedBaselineMs=0,this.lastPlayedPositionMs=null,this.lastPlayedClockMs=null,this.resumeSeekBaselineMs=null,this.resumeSeekBreakEndMs=null,this.resumeSeekInFlight=!1,this.pendingRewindFromMs=null}isRewindBeyondJitter(e,t){return!(t>=e)&&!(this.deps.timing.timebase===s.WALLCLOCK&&e-t<=this.REWIND_JITTER_TOLERANCE_MS)}rearmRetiredBreaks(s){if(this.resumeSeekBaselineMs=null,this.resumeSeekBreakEndMs=null,this.resumeSeekInFlight=!1,0!==this.deps.retiredBreaks.size)for(const[t,i]of[...this.deps.retiredBreaks]){const a=this.deps.breaks.get(t);if(a&&(a.state===e.PENDING||a.state===e.ACTIVE||a.state===e.CHAINED))continue;const l=this.deps.getBreakStartTime(i);if(null===l)continue;s<l+1e3*(i.duration??0)&&(this.deps.retiredBreaks.delete(t),this.deps.completedBreakIds.delete(t),this.deps.preloadedBreakIds.delete(t),this.deps.breaks.set(t,{break:i,state:e.PENDING}),this.deps.log(`↩️ Re-armed break "${t}" after backward seek`))}}}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { PlayerAdapter } from '../interfaces/PlayerAdapter';
|
|
2
|
+
import type { GamConfig, SsaiConfig, SsaiSessionConfig } from '../types/OptiViewAdsConfig';
|
|
3
|
+
import type { StitchedStreamVendor } from './resolveDeliveryMode';
|
|
4
|
+
import { AssetParameterMacros } from './assetParameterMacros';
|
|
5
|
+
import type { OptiViewAdsEventMap, OptiViewAdsEventType } from '../types/Events';
|
|
6
|
+
/** Org-level options the SSAI controller needs from the SDK config. */
|
|
7
|
+
export interface SsaiControllerOptions {
|
|
8
|
+
gam: GamConfig;
|
|
9
|
+
/** SSAI has no break manifest, so its GAM identity is client-side. See SsaiConfig. */
|
|
10
|
+
ssai: SsaiConfig;
|
|
11
|
+
maxBitrate?: number | undefined;
|
|
12
|
+
autoplay?: boolean | undefined;
|
|
13
|
+
debug?: boolean | undefined;
|
|
14
|
+
}
|
|
15
|
+
/** Callbacks the controller uses to surface events/diagnostics back to the SDK. */
|
|
16
|
+
export interface SsaiControllerCallbacks {
|
|
17
|
+
emit<T extends OptiViewAdsEventType>(event: T, data: OptiViewAdsEventMap[T]): void;
|
|
18
|
+
diagnose(code: string, message: string, context?: Record<string, unknown>): void;
|
|
19
|
+
log(...args: unknown[]): void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Orchestrates Server-Side Ad Insertion: one pre-stitched stream on the CONTENT player. Creates
|
|
23
|
+
* an IMA DAI pod-serving session for a `stream_id`, loads the stitcher master URL, forwards
|
|
24
|
+
* `timedmetadata` to IMA for tracking, and re-emits IMA's ad lifecycle as SDK events. No
|
|
25
|
+
* manifest polling, scheduling, or ad-player overlay runs in this mode.
|
|
26
|
+
*/
|
|
27
|
+
export declare class SsaiController {
|
|
28
|
+
private readonly player;
|
|
29
|
+
private readonly options;
|
|
30
|
+
private readonly callbacks;
|
|
31
|
+
private gamStreamManager;
|
|
32
|
+
private readonly onTimedMetadata;
|
|
33
|
+
private breakCounter;
|
|
34
|
+
private adIndexInBreak;
|
|
35
|
+
private currentBreak;
|
|
36
|
+
private currentAsset;
|
|
37
|
+
private sessionConfigParameters;
|
|
38
|
+
private updatedParameters;
|
|
39
|
+
private manifestParameters;
|
|
40
|
+
private customerMacros;
|
|
41
|
+
constructor(player: PlayerAdapter, options: SsaiControllerOptions, callbacks: SsaiControllerCallbacks);
|
|
42
|
+
/** Create the IMA stream, load the stitched master, wire timed-metadata forwarding, optionally play. */
|
|
43
|
+
start(config: SsaiSessionConfig): Promise<void>;
|
|
44
|
+
/** Manifest-steered ssai: IMA `LiveStreamRequest` for the gam `assetKey`; IMA supplies the stream URL directly. */
|
|
45
|
+
startSteered(vendor: StitchedStreamVendor, sessionConfigParameters?: Record<string, string>, macros?: AssetParameterMacros): Promise<void>;
|
|
46
|
+
private resolveAdTagParameters;
|
|
47
|
+
updateAssetParameters(params: Record<string, string>): void;
|
|
48
|
+
setManifestAssetParameters(params: Record<string, string> | undefined): void;
|
|
49
|
+
/** Stop timed-metadata forwarding and destroy IMA. */
|
|
50
|
+
stop(): void;
|
|
51
|
+
/** Map IMA stream events onto SDK events with synthesized stable ids (no manifest entry exists). `totalAds` is 0 (unknown). */
|
|
52
|
+
private buildGamCallbacks;
|
|
53
|
+
private emitQuartile;
|
|
54
|
+
private ensureBreak;
|
|
55
|
+
/** Build a minimal synthetic Break for an SSAI ad pod. */
|
|
56
|
+
private synthBreak;
|
|
57
|
+
/** Build a minimal synthetic vendor-less Asset for an SSAI ad. */
|
|
58
|
+
private synthAsset;
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{applyAssetParameterMacros as e,omitBreakScopedParameters as t}from"./assetParameterMacros";import{buildAssetParameterMacroTable as a}from"./assetParameterMacroTable";import{resolveAssetParameters as s}from"./resolveAssetParameters";import{AssetType as r,MediaType as i,BreakFormat as n}from"../types/BreakManifest";import{GamStreamManager as o}from"./GamStreamManager";import{resolveBreakFormat as m}from"./AdBreakSequencer";import{buildStitcherMasterUrl as d}from"./StitcherUrlBuilder";export class SsaiController{constructor(e,t,a){this.player=e,this.options=t,this.callbacks=a,this.gamStreamManager=null,this.breakCounter=0,this.adIndexInBreak=0,this.currentBreak=null,this.currentAsset=null,this.customerMacros={},this.onTimedMetadata=e=>{if(!e)return;const t=e;"presented"!==t.phase&&this.gamStreamManager?.processTimedMetadata(t)}}async start(e){const t=this.player.videoElement;if(!t)throw new Error("SSAI mode requires a content PlayerAdapter that exposes a videoElement (for IMA DAI).");if(!e.customAssetKey)throw new Error("SSAI mode requires a customAssetKey in startSession().");this.sessionConfigParameters=e.assetParameters,this.updatedParameters=void 0,this.manifestParameters=void 0,this.customerMacros={...e.assetParameterMacros};const a=new o(t,void 0,this.options.debug);a.setCallbacks(this.buildGamCallbacks()),this.gamStreamManager=a,await a.initialize({networkCode:this.options.ssai.networkCode,customAssetKey:e.customAssetKey,adTagParameters:this.resolveAdTagParameters(),streamActivityMonitorId:this.options.gam.streamActivityMonitorId});const s=a.getStreamId();if(!s)throw new Error("SSAI mode: IMA DAI session did not return a stream id.");const r=d({stitcherUrl:e.stitcherUrl,streamId:s,maxBitrate:this.options.maxBitrate});this.callbacks.log("SSAI stitched master URL:",r),this.player.on("timedmetadata",this.onTimedMetadata),await this.player.load(r),this.options.autoplay&&await this.player.play()}async startSteered(e,t,a){const s=this.player.videoElement;if(!s)throw new Error("SSAI mode requires a content PlayerAdapter that exposes a videoElement (for IMA DAI).");this.sessionConfigParameters=t,this.updatedParameters=void 0,this.manifestParameters=e.assetParameters,this.customerMacros={...a};const r=new o(s,void 0,this.options.debug);r.setCallbacks(this.buildGamCallbacks()),this.gamStreamManager=r;const i=await r.initializeLiveStream({assetKey:e.assetKey,networkCode:e.networkCode,adTagParameters:this.resolveAdTagParameters(),streamActivityMonitorId:this.options.gam.streamActivityMonitorId});this.callbacks.log("SSAI full-service DAI stream URL:",i),this.player.on("timedmetadata",this.onTimedMetadata),await this.player.load(i),this.options.autoplay&&await this.player.play()}resolveAdTagParameters(){const r=s(this.sessionConfigParameters,this.updatedParameters,this.manifestParameters,void 0),i=e(t(r),a({customerMacros:this.customerMacros,element:this.player.videoElement??null,breakDurationSec:void 0}));for(const{key:e,macro:t}of i.unresolved)this.callbacks.diagnose("DA-ASSET-MACRO-UNKNOWN",`Asset parameter '${e}' sent with literal macro '${t}': the macro has no value.`,{key:e,macro:t});return i.params}updateAssetParameters(e){this.updatedParameters=e,this.gamStreamManager?.replaceAdTagParameters(this.resolveAdTagParameters()??{})}setManifestAssetParameters(e){(function(e,t){e&&0===Object.keys(e).length&&(e=void 0);t&&0===Object.keys(t).length&&(t=void 0);if(e===t)return!0;if(!e||!t)return!1;const a=Object.keys(e);return a.length===Object.keys(t).length&&a.every(a=>e[a]===t[a])})(this.manifestParameters,e)||(this.manifestParameters=e,this.gamStreamManager&&(this.gamStreamManager.replaceAdTagParameters(this.resolveAdTagParameters()??{}),this.callbacks.diagnose("DA-ASSET-PARAMETERS-UPDATED","Manifest asset parameters changed; IMA targeting updated.",{source:"manifest",keys:Object.keys(e??{}).sort()})))}stop(){this.player.off("timedmetadata",this.onTimedMetadata),this.gamStreamManager?.destroy(),this.gamStreamManager=null,this.sessionConfigParameters=void 0,this.updatedParameters=void 0,this.manifestParameters=void 0,this.customerMacros={},this.currentBreak=null,this.currentAsset=null,this.breakCounter=0,this.adIndexInBreak=0}buildGamCallbacks(){return{onAdBreakStarted:()=>{this.breakCounter+=1,this.adIndexInBreak=0,this.currentBreak=this.synthBreak(`ssai-break-${this.breakCounter}`),this.callbacks.emit("adbreakbegin",{type:"adbreakbegin",timestamp:Date.now(),break:this.currentBreak,format:m(this.currentBreak),elapsedSec:0,remainingSec:this.currentBreak.duration})},onAdStarted:e=>{const t=this.ensureBreak(),a=this.adIndexInBreak;this.adIndexInBreak+=1,this.currentAsset=this.synthAsset(`${t.id}-ad-${this.adIndexInBreak}`),this.callbacks.emit("adbegin",{type:"adbegin",timestamp:Date.now(),break:t,format:m(t),asset:this.currentAsset,adIndex:a,totalAds:0,...void 0!==e?.adPosition&&Number.isInteger(e.adPosition)&&e.adPosition>=1?{podPosition:e.adPosition}:{}})},onAdFirstQuartile:()=>this.emitQuartile("adfirstquartile"),onAdMidpoint:()=>this.emitQuartile("admidpoint"),onAdThirdQuartile:()=>this.emitQuartile("adthirdquartile"),onAdComplete:()=>{const e=this.ensureBreak(),t=this.currentAsset??this.synthAsset(`${e.id}-ad`);this.callbacks.emit("adend",{type:"adend",timestamp:Date.now(),break:e,format:m(e),asset:t,adIndex:Math.max(0,this.adIndexInBreak-1),totalAds:0}),this.currentAsset=null},onAdBreakEnded:()=>{const e=this.ensureBreak();this.callbacks.emit("adbreakend",{type:"adbreakend",timestamp:Date.now(),break:e,format:m(e)}),this.currentBreak=null,this.currentAsset=null},onError:e=>{this.callbacks.diagnose("DA-SSAI-IMA-ERROR",e),this.callbacks.emit("aderror",{type:"aderror",timestamp:Date.now(),source:"ad",...this.currentBreak?{break:this.currentBreak,format:m(this.currentBreak)}:{},...this.currentAsset?{asset:this.currentAsset}:{},error:new Error(e)})}}}emitQuartile(e){const t=this.ensureBreak(),a=this.currentAsset??this.synthAsset(`${t.id}-ad`);this.callbacks.emit(e,{type:e,timestamp:Date.now(),break:t,format:m(t),asset:a})}ensureBreak(){return this.currentBreak||(this.breakCounter+=1,this.currentBreak=this.synthBreak(`ssai-break-${this.breakCounter}`)),this.currentBreak}synthBreak(e){return{id:e,start:this.player.currentTime,duration:0,variant:{format:n.SINGLE,assets:[]}}}synthAsset(e){return{id:e,type:r.STATIC,mediaType:i.VIDEO,uri:""}}}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inputs for building an SSAI stitcher master-playlist URL.
|
|
3
|
+
*/
|
|
4
|
+
export interface StitcherMasterUrlParams {
|
|
5
|
+
/**
|
|
6
|
+
* Full stitcher master-playlist URL as supplied by the caller, e.g.
|
|
7
|
+
* `https://stitch.example.com/ssai/v1/acme/sports/master.m3u8`. Used verbatim —
|
|
8
|
+
* the SDK only appends query parameters, never path segments.
|
|
9
|
+
*/
|
|
10
|
+
stitcherUrl: string;
|
|
11
|
+
/** IMA DAI stream id obtained from the `GamStreamManager` session. */
|
|
12
|
+
streamId: string;
|
|
13
|
+
/**
|
|
14
|
+
* Optional maximum rendition bitrate (bits/sec). When set, the stitcher drops
|
|
15
|
+
* variants whose `BANDWIDTH` exceeds it from the returned master playlist.
|
|
16
|
+
*/
|
|
17
|
+
maxBitrate?: number | undefined;
|
|
18
|
+
/** Extra query parameters to append (e.g. ad-tag passthrough). */
|
|
19
|
+
extraParams?: Record<string, string> | undefined;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Append the SSAI session parameters to the caller's stitcher master URL:
|
|
23
|
+
* `stream_id`, an optional `max_bitrate`, then any `extraParams` in insertion
|
|
24
|
+
* order. The stitcher uses `stream_id` to bind the stitched stream to the IMA
|
|
25
|
+
* DAI session so the in-stream ID3 metadata it injects can be matched back for
|
|
26
|
+
* ad tracking. Query keys/values are percent-encoded with {@link encodeRfc3986}.
|
|
27
|
+
*
|
|
28
|
+
* The caller's URL may already carry a query (a token, a tenant id), in which
|
|
29
|
+
* case the parameters are joined with `&`. A fragment is dropped: it is not sent
|
|
30
|
+
* to the server and would otherwise end up in the middle of the query string.
|
|
31
|
+
*
|
|
32
|
+
* @throws if any required field is missing/blank.
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildStitcherMasterUrl(params: StitcherMasterUrlParams): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function r(r){return encodeURIComponent(r).replace(/[!'()*]/g,r=>"%"+r.charCodeAt(0).toString(16).toUpperCase())}export function buildStitcherMasterUrl(t){const{stitcherUrl:e,streamId:i,maxBitrate:n,extraParams:o}=t;if(!e)throw new Error("buildStitcherMasterUrl: stitcherUrl is required");if(!i)throw new Error("buildStitcherMasterUrl: streamId is required");const s=e.indexOf("#"),c=-1===s?e:e.slice(0,s),a=[`stream_id=${r(i)}`];if(void 0!==n&&a.push(`max_bitrate=${r(String(n))}`),o)for(const[t,e]of Object.entries(o))a.push(`${r(t)}=${r(e)}`);const d=c.includes("?")?"&":"?";return`${c}${d}${a.join("&")}`}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side (CSAI) VAST playout via the IMA HTML5 SDK (`google.ima`, loader `ima3.js`), as
|
|
3
|
+
* opposed to {@link GamStreamManager}'s DAI surface (`google.ima.dai.api`, `ima3_dai.js`). IMA
|
|
4
|
+
* fetches the VAST tag and owns the ad media in the SDK's overlay container for the ad's duration.
|
|
5
|
+
*
|
|
6
|
+
* @see https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side
|
|
7
|
+
*/
|
|
8
|
+
/** Ad-lifecycle callbacks surfaced to {@link AdPlayerController}. */
|
|
9
|
+
export interface VastAdCallbacks {
|
|
10
|
+
/** `podPosition`: IMA's 1-based `getAdPodInfo().getAdPosition()` (carries the VAST `sequence`); undefined when none. */
|
|
11
|
+
onAdStarted?: (podPosition?: number) => void;
|
|
12
|
+
/** IMA does not drive the ad element's `timeupdate`, so this is the only progress signal for the break countdown. */
|
|
13
|
+
onAdProgress?: (currentTime: number, duration: number) => void;
|
|
14
|
+
onAdFirstQuartile?: () => void;
|
|
15
|
+
onAdMidpoint?: () => void;
|
|
16
|
+
onAdThirdQuartile?: () => void;
|
|
17
|
+
onContentPauseRequested?: () => void;
|
|
18
|
+
onContentResumeRequested?: () => void;
|
|
19
|
+
}
|
|
20
|
+
/** Whether `ima3.js` is present; the caller emits `DA-VAST-IMA-SDK-MISSING` when false. */
|
|
21
|
+
export declare function isImaCsaiSdkAvailable(): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* One CSAI VAST playout at a time, reused across breaks. `preloadVast` then `startPreloaded`
|
|
24
|
+
* warms the ad before break start; `playVast()` is the single-shot path.
|
|
25
|
+
*/
|
|
26
|
+
export declare class VastAdManager {
|
|
27
|
+
private adContainer;
|
|
28
|
+
private adVideoElement;
|
|
29
|
+
private adDisplayContainer;
|
|
30
|
+
private adsLoader;
|
|
31
|
+
private adsManager;
|
|
32
|
+
private callbacks;
|
|
33
|
+
private debug;
|
|
34
|
+
private preloadResolve;
|
|
35
|
+
private preloadReject;
|
|
36
|
+
private startResolve;
|
|
37
|
+
private startReject;
|
|
38
|
+
private pendingMuted;
|
|
39
|
+
private settled;
|
|
40
|
+
private preloaded;
|
|
41
|
+
private started;
|
|
42
|
+
private currentAdDurationSec;
|
|
43
|
+
constructor(adContainer: HTMLElement, adVideoElement: HTMLVideoElement, debug?: boolean);
|
|
44
|
+
/** Set ad-lifecycle callbacks (replaces any previous set). */
|
|
45
|
+
setCallbacks(callbacks: VastAdCallbacks): void;
|
|
46
|
+
/**
|
|
47
|
+
* Resize the AdsManager to the ad element's current size. `preloadVast()` initialized IMA
|
|
48
|
+
* before the break layout existed, and a resize before `start()` is not always honoured (no
|
|
49
|
+
* renderer yet), so it runs again on IMA `STARTED` and on container resize.
|
|
50
|
+
*/
|
|
51
|
+
resize(): void;
|
|
52
|
+
/** Fetch + parse + AdsManager init() with enablePreloading; does NOT call start(). Rejects on missing SDK or AD_ERROR. */
|
|
53
|
+
preloadVast(adTagUrl: string, options?: {
|
|
54
|
+
muted?: boolean;
|
|
55
|
+
}): Promise<void>;
|
|
56
|
+
/** Start a preloaded ad; resolves when all ads complete, rejects on AD_ERROR. */
|
|
57
|
+
startPreloaded(): Promise<void>;
|
|
58
|
+
/** Whether a preloaded ad is ready to start (else fall back to `playVast()`). */
|
|
59
|
+
hasPreloadedManager(): boolean;
|
|
60
|
+
/** Apply a mute change made between preload and play. */
|
|
61
|
+
setMuted(muted: boolean): void;
|
|
62
|
+
/** IMA CSAI drives ad audio through `AdsManager.setVolume()`, independent of the `<video>` volume. No-op without an AdsManager. */
|
|
63
|
+
setVolume(volume: number): void;
|
|
64
|
+
/** `preloadVast()` followed immediately by `startPreloaded()`. */
|
|
65
|
+
playVast(adTagUrl: string, options?: {
|
|
66
|
+
muted?: boolean;
|
|
67
|
+
}): Promise<void>;
|
|
68
|
+
private setupContainerAndLoader;
|
|
69
|
+
private onAdsManagerLoaded;
|
|
70
|
+
private wireManagerEvents;
|
|
71
|
+
/** `currentTime = duration − getRemainingTime()`; no-op until STARTED captured a duration. */
|
|
72
|
+
private emitAdProgress;
|
|
73
|
+
private onAdError;
|
|
74
|
+
private succeedStart;
|
|
75
|
+
private failStart;
|
|
76
|
+
private resolvePreload;
|
|
77
|
+
private rejectPreload;
|
|
78
|
+
private clearPreloadPromises;
|
|
79
|
+
private clearStartPromises;
|
|
80
|
+
private slotWidth;
|
|
81
|
+
private slotHeight;
|
|
82
|
+
/** Tear down only the per-playout IMA objects (keeps callbacks). */
|
|
83
|
+
private teardownManager;
|
|
84
|
+
/** Reset state before a new playout. Safe to call repeatedly. */
|
|
85
|
+
reset(): void;
|
|
86
|
+
/** Destroy the manager and drop callbacks. */
|
|
87
|
+
destroy(): void;
|
|
88
|
+
private log;
|
|
89
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{debugLog as e}from"../diagnostics/debugLog";import{VendorAdError as t}from"../types/VendorAdError";export function isImaCsaiSdkAvailable(){return"undefined"!=typeof google&&void 0!==google.ima&&void 0!==google.ima.AdsLoader&&void 0!==google.ima.AdDisplayContainer}export class VastAdManager{constructor(e,t,s=!1){this.adContainer=e,this.adVideoElement=t,this.adDisplayContainer=null,this.adsLoader=null,this.adsManager=null,this.callbacks={},this.preloadResolve=null,this.preloadReject=null,this.startResolve=null,this.startReject=null,this.pendingMuted=!1,this.settled=!1,this.preloaded=!1,this.started=!1,this.currentAdDurationSec=null,this.debug=s}setCallbacks(e){this.callbacks=e}resize(){this.adsManager&&(this.adsManager.resize(this.slotWidth(),this.slotHeight(),google.ima.ViewMode.NORMAL),this.log("VAST AdsManager resized to",this.slotWidth(),"x",this.slotHeight()))}preloadVast(e,t){return this.reset(),isImaCsaiSdkAvailable()?new Promise((s,a)=>{this.preloadResolve=s,this.preloadReject=a;try{this.setupContainerAndLoader();const s=new google.ima.AdsRequest;s.adTagUrl=e,s.linearAdSlotWidth=this.slotWidth(),s.linearAdSlotHeight=this.slotHeight(),s.setAdWillAutoPlay(!0),this.pendingMuted=t?.muted??!1,s.setAdWillPlayMuted(this.pendingMuted),this.log("Preloading VAST ads:",e),this.adsLoader.requestAds(s)}catch(e){this.rejectPreload(e instanceof Error?e:new Error(String(e)))}}):Promise.reject(new Error('Google IMA client-side SDK not found. Include <script src="https://imasdk.googleapis.com/js/sdkloader/ima3.js"><\/script> in your page.'))}startPreloaded(){return new Promise((e,t)=>{if(this.adsManager&&this.preloaded&&!this.started){this.started=!0,this.settled=!1,this.startResolve=e,this.startReject=t;try{this.resize(),this.adsManager.start(),this.log("VAST AdsManager started")}catch(e){this.failStart(e instanceof Error?e:new Error(String(e)))}}else t(new Error("No preloaded VAST AdsManager; call preloadVast() first"))})}hasPreloadedManager(){return null!==this.adsManager&&this.preloaded&&!this.started&&!this.settled}setMuted(e){this.pendingMuted=e,this.adsManager?.setVolume(e?0:1)}setVolume(e){this.pendingMuted=e<=0,this.adsManager?.setVolume(e)}playVast(e,t){return this.preloadVast(e,t).then(()=>this.startPreloaded())}setupContainerAndLoader(){this.adDisplayContainer=new google.ima.AdDisplayContainer(this.adContainer,this.adVideoElement),this.adDisplayContainer.initialize(),this.adsLoader=new google.ima.AdsLoader(this.adDisplayContainer),this.adsLoader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,e=>this.onAdsManagerLoaded(e)),this.adsLoader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR,e=>this.onAdError(e))}onAdsManagerLoaded(e){try{const t=new google.ima.AdsRenderingSettings;t.restoreCustomPlaybackStateOnAdBreakComplete=!0,t.enablePreloading=!0;const s=e.getAdsManager(this.adVideoElement,t);this.adsManager=s,this.wireManagerEvents(s),this.pendingMuted&&s.setVolume(0),s.init(this.slotWidth(),this.slotHeight(),google.ima.ViewMode.NORMAL),this.preloaded=!0,this.resolvePreload(),this.log("VAST AdsManager initialized and ready")}catch(e){this.rejectPreload(e instanceof Error?e:new Error(String(e)))}}wireManagerEvents(e){const t=google.ima.AdEvent.Type;e.addEventListener(t.STARTED,e=>{this.resize();const t=e.getAd?.(),s=t?.getDuration?.();this.currentAdDurationSec="number"==typeof s&&Number.isFinite(s)&&s>0?s:null;const a=t?.getAdPodInfo?.()?.getAdPosition?.();this.callbacks.onAdStarted?.("number"==typeof a&&Number.isFinite(a)&&a>=1?Math.floor(a):void 0)}),e.addEventListener(t.AD_PROGRESS,()=>this.emitAdProgress()),e.addEventListener(t.FIRST_QUARTILE,()=>this.callbacks.onAdFirstQuartile?.()),e.addEventListener(t.MIDPOINT,()=>this.callbacks.onAdMidpoint?.()),e.addEventListener(t.THIRD_QUARTILE,()=>this.callbacks.onAdThirdQuartile?.()),e.addEventListener(t.CONTENT_PAUSE_REQUESTED,()=>this.callbacks.onContentPauseRequested?.()),e.addEventListener(t.CONTENT_RESUME_REQUESTED,()=>this.callbacks.onContentResumeRequested?.()),e.addEventListener(t.ALL_ADS_COMPLETED,()=>this.succeedStart()),e.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR,e=>this.onAdError(e))}emitAdProgress(){if(!this.callbacks.onAdProgress||!this.adsManager)return;const e=this.currentAdDurationSec;if(null==e)return;const t=this.adsManager.getRemainingTime();if(!Number.isFinite(t)||t<0)return;const s=Math.max(0,Math.min(e,e-t));this.callbacks.onAdProgress(s,e)}onAdError(e){const s=e?.getError?.(),a=s?.getMessage?.()??"Unknown IMA CSAI error",i=s?.getErrorCode?.();this.log("VAST IMA error:",a);const r="number"==typeof i?new t(a,String(i)):new Error(a);this.startReject?this.failStart(r):this.preloadReject&&this.rejectPreload(r)}succeedStart(){this.settled||(this.settled=!0,this.log("VAST playout complete"),this.startResolve?.(),this.clearStartPromises(),this.teardownManager())}failStart(e){this.settled||(this.settled=!0,this.startReject?.(e),this.clearStartPromises(),this.teardownManager())}resolvePreload(){this.preloadResolve?.(),this.clearPreloadPromises()}rejectPreload(e){this.preloadReject?.(e),this.clearPreloadPromises(),this.teardownManager()}clearPreloadPromises(){this.preloadResolve=null,this.preloadReject=null}clearStartPromises(){this.startResolve=null,this.startReject=null}slotWidth(){return this.adVideoElement.clientWidth||640}slotHeight(){return this.adVideoElement.clientHeight||360}teardownManager(){this.adsManager&&(this.adsManager.destroy(),this.adsManager=null),this.adsLoader&&(this.adsLoader.destroy(),this.adsLoader=null),this.adDisplayContainer&&(this.adDisplayContainer.destroy(),this.adDisplayContainer=null)}reset(){this.teardownManager(),this.settled=!1,this.preloaded=!1,this.started=!1,this.pendingMuted=!1,this.currentAdDurationSec=null,this.clearPreloadPromises(),this.clearStartPromises()}destroy(){this.reset(),this.callbacks={}}log(...t){this.debug&&e("[VastAdManager]",...t)}}
|