@streamlayer/feature-advertisement 1.0.0 → 1.1.1-rc.10.p6eb7304a

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.
Files changed (105) hide show
  1. package/classic/package.json +5 -0
  2. package/lib/advertisement.d.ts +6 -6
  3. package/lib/advertisement.js +32 -17
  4. package/lib/cjs/advertisement.d.ts +64 -0
  5. package/lib/cjs/advertisement.js +322 -0
  6. package/lib/cjs/index.d.ts +21 -0
  7. package/lib/cjs/index.js +41 -0
  8. package/lib/cjs/package.json +1 -0
  9. package/lib/cjs/units/ad-macros.d.ts +7 -0
  10. package/lib/cjs/units/ad-macros.js +91 -0
  11. package/lib/cjs/units/external-ad.d.ts +127 -0
  12. package/lib/cjs/units/external-ad.js +421 -0
  13. package/lib/cjs/units/external-pause-ad.d.ts +50 -0
  14. package/lib/cjs/units/external-pause-ad.js +259 -0
  15. package/lib/cjs/units/gam.d.ts +26 -0
  16. package/lib/cjs/units/gam.js +152 -0
  17. package/lib/cjs/units/parse-vast-extensions.d.ts +26 -0
  18. package/lib/cjs/units/parse-vast-extensions.js +56 -0
  19. package/lib/cjs/units/pause-ad-card.d.ts +49 -0
  20. package/lib/cjs/units/pause-ad-card.js +136 -0
  21. package/lib/cjs/units/pause-ad.d.ts +16 -0
  22. package/lib/cjs/units/pause-ad.js +52 -0
  23. package/lib/cjs/units/sponsor-logo.d.ts +78 -0
  24. package/lib/cjs/units/sponsor-logo.js +194 -0
  25. package/lib/cjs/units/ssai-ad-break.d.ts +21 -0
  26. package/lib/cjs/units/ssai-ad-break.js +11 -0
  27. package/lib/cjs/units/ssai-creative.d.ts +89 -0
  28. package/lib/cjs/units/ssai-creative.js +136 -0
  29. package/lib/cjs/units/ssai-detector.d.ts +180 -0
  30. package/lib/cjs/units/ssai-detector.js +1172 -0
  31. package/lib/cjs/units/ssai-log.d.ts +16 -0
  32. package/lib/cjs/units/ssai-log.js +23 -0
  33. package/lib/cjs/units/ssai-preload.d.ts +20 -0
  34. package/lib/cjs/units/ssai-preload.js +62 -0
  35. package/lib/cjs/units/ssai-providers.d.ts +117 -0
  36. package/lib/cjs/units/ssai-providers.js +300 -0
  37. package/lib/cjs/units/vast-tracker.d.ts +45 -0
  38. package/lib/cjs/units/vast-tracker.js +64 -0
  39. package/lib/classic/_virtual/_rollupPluginBabelHelpers.js +71 -0
  40. package/lib/classic/advertisement.js +348 -0
  41. package/lib/classic/cjs/_virtual/_rollupPluginBabelHelpers.js +77 -0
  42. package/lib/classic/cjs/advertisement.js +350 -0
  43. package/lib/classic/cjs/index.js +49 -0
  44. package/lib/classic/cjs/package.json +1 -0
  45. package/lib/classic/cjs/units/ad-macros.js +115 -0
  46. package/lib/classic/cjs/units/external-ad.js +500 -0
  47. package/lib/classic/cjs/units/external-pause-ad.js +278 -0
  48. package/lib/classic/cjs/units/gam.js +173 -0
  49. package/lib/classic/cjs/units/parse-vast-extensions.js +60 -0
  50. package/lib/classic/cjs/units/pause-ad-card.js +134 -0
  51. package/lib/classic/cjs/units/pause-ad.js +72 -0
  52. package/lib/classic/cjs/units/sponsor-logo.js +201 -0
  53. package/lib/classic/cjs/units/ssai-ad-break.js +11 -0
  54. package/lib/classic/cjs/units/ssai-creative.js +142 -0
  55. package/lib/classic/cjs/units/ssai-detector.js +1195 -0
  56. package/lib/classic/cjs/units/ssai-log.js +23 -0
  57. package/lib/classic/cjs/units/ssai-preload.js +72 -0
  58. package/lib/classic/cjs/units/ssai-providers.js +361 -0
  59. package/lib/classic/cjs/units/vast-tracker.js +81 -0
  60. package/lib/classic/index.js +29 -0
  61. package/lib/classic/units/ad-macros.js +113 -0
  62. package/lib/classic/units/external-ad.js +491 -0
  63. package/lib/classic/units/external-pause-ad.js +275 -0
  64. package/lib/classic/units/gam.js +171 -0
  65. package/lib/classic/units/parse-vast-extensions.js +58 -0
  66. package/lib/classic/units/pause-ad-card.js +131 -0
  67. package/lib/classic/units/pause-ad.js +70 -0
  68. package/lib/classic/units/sponsor-logo.js +197 -0
  69. package/lib/classic/units/ssai-ad-break.js +9 -0
  70. package/lib/classic/units/ssai-creative.js +140 -0
  71. package/lib/classic/units/ssai-detector.js +1192 -0
  72. package/lib/classic/units/ssai-log.js +21 -0
  73. package/lib/classic/units/ssai-preload.js +68 -0
  74. package/lib/classic/units/ssai-providers.js +351 -0
  75. package/lib/classic/units/vast-tracker.js +77 -0
  76. package/lib/index.d.ts +15 -12
  77. package/lib/index.js +14 -7
  78. package/lib/units/ad-macros.d.ts +7 -0
  79. package/lib/units/ad-macros.js +87 -0
  80. package/lib/units/external-ad.d.ts +71 -2
  81. package/lib/units/external-ad.js +94 -66
  82. package/lib/units/external-pause-ad.js +7 -1
  83. package/lib/units/gam.d.ts +11 -1
  84. package/lib/units/gam.js +65 -17
  85. package/lib/units/parse-vast-extensions.d.ts +10 -1
  86. package/lib/units/parse-vast-extensions.js +5 -1
  87. package/lib/units/pause-ad-card.d.ts +49 -0
  88. package/lib/units/pause-ad-card.js +130 -0
  89. package/lib/units/pause-ad.d.ts +1 -18
  90. package/lib/units/pause-ad.js +2 -49
  91. package/lib/units/sponsor-logo.d.ts +78 -0
  92. package/lib/units/sponsor-logo.js +188 -0
  93. package/lib/units/ssai-ad-break.d.ts +1 -1
  94. package/lib/units/ssai-creative.d.ts +1 -1
  95. package/lib/units/ssai-creative.js +1 -1
  96. package/lib/units/ssai-detector.d.ts +1 -1
  97. package/lib/units/ssai-detector.js +27 -11
  98. package/lib/units/ssai-log.d.ts +16 -0
  99. package/lib/units/ssai-log.js +18 -0
  100. package/lib/units/ssai-preload.d.ts +1 -1
  101. package/lib/units/ssai-providers.d.ts +1 -1
  102. package/lib/units/ssai-providers.js +48 -11
  103. package/lib/units/vast-tracker.d.ts +1 -1
  104. package/lib/vast-client.d.ts +9 -0
  105. package/package.json +43 -10
@@ -0,0 +1,16 @@
1
+ import { type Logger } from '@streamlayer/sdk-web-logger';
2
+ /**
3
+ * `ssaiq` — the SSAI ad NARRATIVE log: one line per thing that happens to a
4
+ * server-stitched ad, in the order it happens. The manifest tag the playout
5
+ * published, the metadata round trip it triggers, the break the SDK opens from
6
+ * it, and every beacon fired against it.
7
+ *
8
+ * Named for the Quortex/Synamedia DAI integration it was built for, but it is
9
+ * the path itself that is logged, so a StreamLayer-dialect stream tells the same
10
+ * story. Detector internals — clock sources, poll ticks, fold bookkeeping — stay
11
+ * on `ssai:*`, where `SL_DEBUG` can reach them without filling a demo window.
12
+ *
13
+ * A host publishes it to the page with `SL_LOG_EVENTS=ssaiq` (see
14
+ * `sdk-web-logger`); the demo's log window renders exactly that.
15
+ */
16
+ export declare const ssaiq: Logger;
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ var sdkWebLogger = require('@streamlayer/sdk-web-logger');
4
+
5
+ /**
6
+ * `ssaiq` — the SSAI ad NARRATIVE log: one line per thing that happens to a
7
+ * server-stitched ad, in the order it happens. The manifest tag the playout
8
+ * published, the metadata round trip it triggers, the break the SDK opens from
9
+ * it, and every beacon fired against it.
10
+ *
11
+ * Named for the Quortex/Synamedia DAI integration it was built for, but it is
12
+ * the path itself that is logged, so a StreamLayer-dialect stream tells the same
13
+ * story. Detector internals — clock sources, poll ticks, fold bookkeeping — stay
14
+ * on `ssai:*`, where `SL_DEBUG` can reach them without filling a demo window.
15
+ *
16
+ * A host publishes it to the page with `SL_LOG_EVENTS=ssaiq` (see
17
+ * `sdk-web-logger`); the demo's log window renders exactly that.
18
+ */
19
+ // Explicit type: the inferred pino type is not nameable across package
20
+ // boundaries (TS2742) when this file is compiled on its own.
21
+ const ssaiq = sdkWebLogger.createLogger('ssaiq');
22
+
23
+ exports.ssaiq = ssaiq;
@@ -0,0 +1,20 @@
1
+ import type { SsaiCreative } from './ssai-creative.js';
2
+ /**
3
+ * How long the overlay is willing to wait for artwork. A slow or dead image host
4
+ * must never hold the bar back — the ad is already playing, and a bar that shows
5
+ * late is worse than one that shows without its logo (the logo is hidden on error
6
+ * anyway). Warming the browser cache is the goal; blocking is not.
7
+ */
8
+ export declare const DEFAULT_PRELOAD_TIMEOUT_MS = 2000;
9
+ /**
10
+ * Fetch images into the browser cache and resolve once they are all settled, or
11
+ * once `timeoutMs` elapses — whichever comes first. Never rejects: a creative
12
+ * whose artwork fails is still a creative worth showing.
13
+ */
14
+ export declare const preloadImages: (urls: (string | undefined | null)[], timeoutMs?: number) => Promise<void>;
15
+ /**
16
+ * Warm every image the creative carries — the overlay's brand logo, and the
17
+ * companion banner for hosts that render one — so the bar fades in complete
18
+ * rather than popping its artwork in a beat later.
19
+ */
20
+ export declare const preloadCreativeAssets: (creative: SsaiCreative, timeoutMs?: number) => Promise<void>;
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ var sdkWebLogger = require('@streamlayer/sdk-web-logger');
4
+
5
+ const log = sdkWebLogger.createLogger('ssai:preload');
6
+ /**
7
+ * How long the overlay is willing to wait for artwork. A slow or dead image host
8
+ * must never hold the bar back — the ad is already playing, and a bar that shows
9
+ * late is worse than one that shows without its logo (the logo is hidden on error
10
+ * anyway). Warming the browser cache is the goal; blocking is not.
11
+ */
12
+ const DEFAULT_PRELOAD_TIMEOUT_MS = 2000;
13
+ /**
14
+ * Fetch images into the browser cache and resolve once they are all settled, or
15
+ * once `timeoutMs` elapses — whichever comes first. Never rejects: a creative
16
+ * whose artwork fails is still a creative worth showing.
17
+ */
18
+ const preloadImages = (urls, timeoutMs = DEFAULT_PRELOAD_TIMEOUT_MS) => {
19
+ const targets = urls.filter((url) => !!url);
20
+ // `Image` is absent in SSR/worker contexts; there is nothing to warm there.
21
+ if (targets.length === 0 || typeof Image === 'undefined') {
22
+ return Promise.resolve();
23
+ }
24
+ return new Promise((resolve) => {
25
+ let settled = false;
26
+ let remaining = targets.length;
27
+ const finish = () => {
28
+ if (settled) {
29
+ return;
30
+ }
31
+ settled = true;
32
+ clearTimeout(timer);
33
+ resolve();
34
+ };
35
+ const timer = setTimeout(() => {
36
+ log.debug({ remaining }, 'ssai preload timed out, showing anyway');
37
+ finish();
38
+ }, timeoutMs);
39
+ const one = () => {
40
+ remaining -= 1;
41
+ if (remaining <= 0) {
42
+ finish();
43
+ }
44
+ };
45
+ for (const url of targets) {
46
+ const image = new Image();
47
+ image.onload = one;
48
+ image.onerror = one;
49
+ image.src = url;
50
+ }
51
+ });
52
+ };
53
+ /**
54
+ * Warm every image the creative carries — the overlay's brand logo, and the
55
+ * companion banner for hosts that render one — so the bar fades in complete
56
+ * rather than popping its artwork in a beat later.
57
+ */
58
+ const preloadCreativeAssets = (creative, timeoutMs) => preloadImages([creative.iconUrl, creative.companion?.staticResource], timeoutMs);
59
+
60
+ exports.DEFAULT_PRELOAD_TIMEOUT_MS = DEFAULT_PRELOAD_TIMEOUT_MS;
61
+ exports.preloadCreativeAssets = preloadCreativeAssets;
62
+ exports.preloadImages = preloadImages;
@@ -0,0 +1,117 @@
1
+ import type { SsaiCreative } from './ssai-creative.js';
2
+ export declare const DEFAULT_CREATIVE_API_BASE = "https://hls.next.streamlayer.io/api/creatives/";
3
+ /**
4
+ * A creative provider encapsulates everything that differs between ad sources:
5
+ * how a creativeId resolves to the normalized {@link SsaiCreative}, and how
6
+ * impressions (and, in the future, other tracking events) are fired. The overlay
7
+ * UI only ever sees the normalized shape, so it never learns about providers.
8
+ */
9
+ export interface SsaiCreativeProvider {
10
+ /** Stable id for logging/diagnostics, e.g. 'streamlayer' | 'vast'. */
11
+ readonly id: string;
12
+ /** Resolve a creativeId (from EXT-X-DATERANGE) to the normalized SsaiCreative. */
13
+ resolve(creativeId: string, signal: AbortSignal): Promise<SsaiCreative>;
14
+ /**
15
+ * Fire an impression for a resolved creative. Provider-specific (URL shape,
16
+ * beacon vs pixel vs SDK call). Optional — falls back to the shared default
17
+ * (first trackingEvent named 'impression' via sendBeacon/fetch) when omitted.
18
+ */
19
+ fireImpression?(creative: SsaiCreative): void;
20
+ /** Optional hook for other tracking events (click, quartiles) — future-facing, no-op default. */
21
+ fireTracking?(creative: SsaiCreative, event: string): void;
22
+ }
23
+ /**
24
+ * Shared low-level beacon helper. Prefers `navigator.sendBeacon` and falls back
25
+ * to a no-cors `fetch`. Used by the default impression path and available to
26
+ * providers that fire URL-based tracking pixels.
27
+ */
28
+ export declare const fireBeacon: (url: string) => void;
29
+ /**
30
+ * Fire every tracking event with the given name, and report how many went out.
31
+ *
32
+ * ALL matches, not the first: the creative contract states that a creative may
33
+ * repeat an `event` value to carry several URLs, and that consumers must not
34
+ * assume uniqueness. Today's payloads carry one URL per event, so this is not a
35
+ * change in what goes on the wire — it just stops being wrong the day a server
36
+ * sends two.
37
+ */
38
+ export declare const fireTrackingEvents: (creative: SsaiCreative, event: string) => number;
39
+ /**
40
+ * Shared default impression behavior: fire the `impression` tracking events.
41
+ * Used by the StreamLayer provider and as the fallback in
42
+ * {@link SsaiCreativeCache} when a provider doesn't override `fireImpression`.
43
+ * Exported so both call sites share one definition.
44
+ */
45
+ export declare const fireDefaultImpression: (creative: SsaiCreative) => void;
46
+ export type StreamLayerProviderConfig = {
47
+ /** Base URL of the StreamLayer JSON creative metadata API. */
48
+ creativeApiBase?: string;
49
+ };
50
+ /**
51
+ * StreamLayer's internal creative provider. Resolves creatives from the JSON
52
+ * creative API (`GET {creativeApiBase}{encodeURIComponent(creativeId)}`) and
53
+ * fires impressions from the `'impression'` tracking event via `sendBeacon`.
54
+ */
55
+ export declare const createStreamLayerProvider: (config?: StreamLayerProviderConfig) => SsaiCreativeProvider;
56
+ export type QuortexProviderConfig = {
57
+ /**
58
+ * Full URL of the tenant's ad metadata endpoint, e.g.
59
+ * `https://ads.example.com/get_ad` or, with the tenant baked in,
60
+ * `https://ads.example.com/get_ad?TenantName=acme`. Required — this is
61
+ * customer infrastructure, so there is no sensible SDK-level default.
62
+ */
63
+ adApiBase?: string;
64
+ /**
65
+ * Tenant the ad ids belong to, appended as the `TenantName` query parameter.
66
+ * Optional: omit it when `adApiBase` already carries the tenant.
67
+ */
68
+ tenantName?: string;
69
+ };
70
+ /**
71
+ * Quortex / Synamedia DAI ad metadata provider.
72
+ *
73
+ * `GET {adApiBase}?TenantName={tenantName}&adID={adId}&info=1` (the `TenantName`
74
+ * part omitted when `adApiBase` already carries it) — the ad id comes
75
+ * from the manifest's `X-AD-ID` DATERANGE attribute (see `ssai-detector.ts`, which
76
+ * reconstructs the ad window from the tracking-marker burst those tags form).
77
+ *
78
+ * The response is byte-compatible with the StreamLayer creative API, so the same
79
+ * `mapApiResponse` normalizes both.
80
+ *
81
+ * Known limitation: impressions fire the `impression` tracking event from the API
82
+ * response, not the per-session beacon the manifest carries in `X-AD-URL`. The two
83
+ * differ in `trackingid`/`USERID`, so Quortex-side attribution will see the
84
+ * generic id rather than the viewer's session until the manifest beacons are
85
+ * plumbed through to the provider.
86
+ */
87
+ export declare const createQuortexProvider: (config?: QuortexProviderConfig) => SsaiCreativeProvider;
88
+ export type VastProviderConfig = Record<string, never>;
89
+ /**
90
+ * Maps a VAST tag into the normalized SsaiCreative shape.
91
+ *
92
+ * Note: `fireImpression` is intentionally left to the shared default for now.
93
+ * Proper VAST impression/tracking wiring through `VASTTracker` is a documented
94
+ * follow-up; it requires retaining the parsed ad/tracker per creative.
95
+ */
96
+ export declare const createVastProvider: (_config?: VastProviderConfig) => SsaiCreativeProvider;
97
+ export type SsaiProviderName = 'streamlayer' | 'quortex' | 'vast';
98
+ /** Union of every built-in provider's config; each factory reads its own keys. */
99
+ export type SsaiProviderConfig = StreamLayerProviderConfig & QuortexProviderConfig;
100
+ /** Built-in provider factories keyed by name. */
101
+ export declare const BUILTIN_PROVIDERS: Record<SsaiProviderName, (config?: SsaiProviderConfig) => SsaiCreativeProvider>;
102
+ export type SsaiProviderOption =
103
+ /** A ready provider instance (built-in or fully custom). */
104
+ SsaiCreativeProvider
105
+ /** A built-in provider selected by name. */
106
+ | SsaiProviderName
107
+ /** A built-in provider selected by name with optional config. */
108
+ | {
109
+ name: SsaiProviderName;
110
+ config?: SsaiProviderConfig;
111
+ };
112
+ /**
113
+ * Resolve a {@link SsaiProviderOption} into a concrete {@link SsaiCreativeProvider}.
114
+ * Defaults to the StreamLayer provider (using `fallbackConfig`) when no option is
115
+ * supplied.
116
+ */
117
+ export declare const resolveProvider: (option?: SsaiProviderOption, fallbackConfig?: SsaiProviderConfig) => SsaiCreativeProvider;
@@ -0,0 +1,300 @@
1
+ 'use strict';
2
+
3
+ var sdkWebLogger = require('@streamlayer/sdk-web-logger');
4
+ var vastClient = require('@dailymotion/vast-client');
5
+ var ssaiLog = require('./ssai-log.js');
6
+
7
+ const log = sdkWebLogger.createLogger('ssai:provider');
8
+ const DEFAULT_CREATIVE_API_BASE = 'https://hls.next.streamlayer.io/api/creatives/';
9
+ /**
10
+ * One creative-metadata round trip, on the narrative log: the request the SDK
11
+ * made and the body the ad service answered with, verbatim. This is the half of
12
+ * the story the manifest cannot tell — the ad's identity comes from the playout,
13
+ * its content comes from here.
14
+ */
15
+ const fetchCreativeJson = async (providerId, url, signal) => {
16
+ // `adApiBase` is customer infrastructure and may carry a key in its query;
17
+ // the request uses the URL as given, the log gets it with such values masked.
18
+ const shown = maskUrlSecrets(url);
19
+ ssaiLog.ssaiq.info({ provider: providerId, url: shown }, `[GET] → ${shown}`);
20
+ let response;
21
+ try {
22
+ response = await fetch(url, { signal });
23
+ }
24
+ catch (error) {
25
+ // An abort is the SDK cancelling its own request (break ended, creative
26
+ // changed) — a note, not a failure.
27
+ if (error?.name === 'AbortError') {
28
+ ssaiLog.ssaiq.debug({ provider: providerId, url: shown }, `[GET] ✗ aborted ${shown}`);
29
+ }
30
+ else {
31
+ ssaiLog.ssaiq.warn({ provider: providerId, url: shown, error: String(error) }, `[GET] ✗ ${shown}`);
32
+ }
33
+ throw error;
34
+ }
35
+ if (!response.ok) {
36
+ ssaiLog.ssaiq.warn({ provider: providerId, url: shown, status: response.status }, `[GET] ✗ ${response.status} ${shown}`);
37
+ throw new Error(`${providerId} creative request failed: ${response.status}`);
38
+ }
39
+ let raw;
40
+ try {
41
+ raw = (await response.json());
42
+ }
43
+ catch (error) {
44
+ // A 200 with a body that is not JSON would otherwise leave the log looking
45
+ // like a request still in flight.
46
+ ssaiLog.ssaiq.warn({ provider: providerId, url: shown, error: String(error) }, `[GET] ✗ ${response.status} ${shown} — not JSON`);
47
+ throw error;
48
+ }
49
+ ssaiLog.ssaiq.info({ provider: providerId, status: response.status, body: raw }, `[GET] ← ${response.status} ad metadata — ${raw?.advertiser || raw?.title || raw?.creativeId || 'ad'}`);
50
+ return raw;
51
+ };
52
+ /** `?apiKey=…` → `?apiKey=«redacted»`, for URLs that go on a screen. Regex, not `URL`: this ships to 2016 TV engines. */
53
+ const maskUrlSecrets = (url) => url.replace(/([?&][^=&]*(?:token|secret|password|key|authorization)[^=&]*=)[^&]*/gi, '$1«redacted»');
54
+ /**
55
+ * Shared low-level beacon helper. Prefers `navigator.sendBeacon` and falls back
56
+ * to a no-cors `fetch`. Used by the default impression path and available to
57
+ * providers that fire URL-based tracking pixels.
58
+ */
59
+ const fireBeacon = (url) => {
60
+ try {
61
+ if (typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function') {
62
+ navigator.sendBeacon(url);
63
+ }
64
+ else {
65
+ void fetch(url, { mode: 'no-cors' }).catch((error) => {
66
+ log.warn(error, 'ssai beacon failed');
67
+ });
68
+ }
69
+ }
70
+ catch (error) {
71
+ log.warn(error, 'ssai beacon failed');
72
+ }
73
+ };
74
+ const resolveAdvertiser = (advertiser, title) => {
75
+ if (advertiser) {
76
+ return advertiser;
77
+ }
78
+ if (title) {
79
+ return title;
80
+ }
81
+ return 'Ad';
82
+ };
83
+ const buildCreativeUrl = (base, creativeId) => {
84
+ const normalizedBase = base.endsWith('/') ? base : `${base}/`;
85
+ return `${normalizedBase}${encodeURIComponent(creativeId)}`;
86
+ };
87
+ const mapApiResponse = (raw) => {
88
+ const trackingEvents = Array.isArray(raw.trackingEvents)
89
+ ? raw.trackingEvents
90
+ .filter((entry) => Boolean(entry?.event && entry?.url))
91
+ .map((entry) => ({ event: entry.event, url: entry.url }))
92
+ : [];
93
+ const rawCompanion = Array.isArray(raw.companions)
94
+ ? raw.companions.find((entry) => !!entry?.staticResource)
95
+ : undefined;
96
+ const companion = rawCompanion?.staticResource
97
+ ? {
98
+ staticResource: rawCompanion.staticResource,
99
+ width: Number(rawCompanion.width) || 0,
100
+ height: Number(rawCompanion.height) || 0,
101
+ clickThrough: rawCompanion.clickThrough,
102
+ clickTracking: Array.isArray(rawCompanion.clickTracking)
103
+ ? rawCompanion.clickTracking.filter((url) => typeof url === 'string' && !!url)
104
+ : [],
105
+ }
106
+ : undefined;
107
+ return {
108
+ creativeId: raw.creativeId,
109
+ advertiser: resolveAdvertiser(raw.advertiser, raw.title),
110
+ title: raw.title || resolveAdvertiser(raw.advertiser, raw.title),
111
+ iconUrl: raw.iconUrl || undefined,
112
+ clickThroughUrl: raw.clickThroughUrl || undefined,
113
+ trackingEvents,
114
+ companion,
115
+ };
116
+ };
117
+ /**
118
+ * Fire every tracking event with the given name, and report how many went out.
119
+ *
120
+ * ALL matches, not the first: the creative contract states that a creative may
121
+ * repeat an `event` value to carry several URLs, and that consumers must not
122
+ * assume uniqueness. Today's payloads carry one URL per event, so this is not a
123
+ * change in what goes on the wire — it just stops being wrong the day a server
124
+ * sends two.
125
+ */
126
+ const fireTrackingEvents = (creative, event) => {
127
+ const matches = creative.trackingEvents.filter((entry) => entry.event === event);
128
+ for (const match of matches) {
129
+ fireBeacon(match.url);
130
+ }
131
+ return matches.length;
132
+ };
133
+ /**
134
+ * Shared default impression behavior: fire the `impression` tracking events.
135
+ * Used by the StreamLayer provider and as the fallback in
136
+ * {@link SsaiCreativeCache} when a provider doesn't override `fireImpression`.
137
+ * Exported so both call sites share one definition.
138
+ */
139
+ const fireDefaultImpression = (creative) => {
140
+ fireTrackingEvents(creative, 'impression');
141
+ };
142
+ /**
143
+ * StreamLayer's internal creative provider. Resolves creatives from the JSON
144
+ * creative API (`GET {creativeApiBase}{encodeURIComponent(creativeId)}`) and
145
+ * fires impressions from the `'impression'` tracking event via `sendBeacon`.
146
+ */
147
+ const createStreamLayerProvider = (config = {}) => {
148
+ const creativeApiBase = config.creativeApiBase ?? DEFAULT_CREATIVE_API_BASE;
149
+ return {
150
+ id: 'streamlayer',
151
+ resolve: async (creativeId, signal) => {
152
+ const raw = await fetchCreativeJson('streamlayer', buildCreativeUrl(creativeApiBase, creativeId), signal);
153
+ return mapApiResponse({ ...raw, creativeId: raw.creativeId || creativeId });
154
+ },
155
+ fireImpression: fireDefaultImpression,
156
+ };
157
+ };
158
+ const buildQuortexUrl = (base, tenantName, adId) => {
159
+ // Deliberately string concatenation rather than `URL`/`URLSearchParams`: this
160
+ // package ships to 2016-era TV engines (see the chrome-32 classic build).
161
+ const separator = base.indexOf('?') === -1 ? '?' : '&';
162
+ const tenant = tenantName ? `TenantName=${encodeURIComponent(tenantName)}&` : '';
163
+ return `${base}${separator}${tenant}adID=${encodeURIComponent(adId)}&info=1`;
164
+ };
165
+ /**
166
+ * Quortex / Synamedia DAI ad metadata provider.
167
+ *
168
+ * `GET {adApiBase}?TenantName={tenantName}&adID={adId}&info=1` (the `TenantName`
169
+ * part omitted when `adApiBase` already carries it) — the ad id comes
170
+ * from the manifest's `X-AD-ID` DATERANGE attribute (see `ssai-detector.ts`, which
171
+ * reconstructs the ad window from the tracking-marker burst those tags form).
172
+ *
173
+ * The response is byte-compatible with the StreamLayer creative API, so the same
174
+ * `mapApiResponse` normalizes both.
175
+ *
176
+ * Known limitation: impressions fire the `impression` tracking event from the API
177
+ * response, not the per-session beacon the manifest carries in `X-AD-URL`. The two
178
+ * differ in `trackingid`/`USERID`, so Quortex-side attribution will see the
179
+ * generic id rather than the viewer's session until the manifest beacons are
180
+ * plumbed through to the provider.
181
+ */
182
+ const createQuortexProvider = (config = {}) => {
183
+ const { adApiBase, tenantName } = config;
184
+ if (!adApiBase) {
185
+ // Loud, but not thrown: this runs inside the host's render effect via
186
+ // `startSsaiMonitoring`, and taking the app down over a config typo is worse
187
+ // than an overlay that never appears and says why.
188
+ log.error({ adApiBase, tenantName }, 'quortex provider requires `adApiBase`');
189
+ }
190
+ return {
191
+ id: 'quortex',
192
+ resolve: async (adId, signal) => {
193
+ if (!adApiBase) {
194
+ throw new Error('quortex provider is missing `adApiBase`');
195
+ }
196
+ const raw = await fetchCreativeJson('quortex', buildQuortexUrl(adApiBase, tenantName, adId), signal);
197
+ return mapApiResponse({ ...raw, creativeId: raw.creativeId || adId });
198
+ },
199
+ fireImpression: fireDefaultImpression,
200
+ };
201
+ };
202
+ // --- VAST provider ---------------------------------------------------------
203
+ const mapVastResponse = async (creativeId, signal) => {
204
+ const vastClient$1 = new vastClient.VASTClient();
205
+ const parsedVAST = await vastClient$1.get(creativeId, { timeout: 8000 });
206
+ if (signal.aborted) {
207
+ throw new DOMException('aborted', 'AbortError');
208
+ }
209
+ const validAd = parsedVAST.ads.find((ad) => ad.creatives.length > 0);
210
+ if (!validAd) {
211
+ throw new Error('no_valid_ad');
212
+ }
213
+ const advertiserValue = Array.isArray(validAd.advertiser)
214
+ ? validAd.advertiser.find((entry) => !!entry?.value)?.value
215
+ : undefined;
216
+ const advertiser = resolveAdvertiser(advertiserValue, validAd.title || undefined);
217
+ let iconUrl;
218
+ let clickThroughUrl;
219
+ let companion;
220
+ for (const creative of validAd.creatives) {
221
+ if (creative.type === 'linear') {
222
+ const linear = creative;
223
+ iconUrl = iconUrl || linear.icons?.find((icon) => !!icon?.staticResource)?.staticResource || undefined;
224
+ clickThroughUrl = clickThroughUrl || linear.videoClickThroughURLTemplate?.url;
225
+ }
226
+ if (creative.type === 'companion') {
227
+ const variation = creative.variations?.[0];
228
+ const staticResource = variation?.staticResources?.[0]?.url;
229
+ if (staticResource && !companion) {
230
+ companion = {
231
+ staticResource,
232
+ width: Number(variation?.width) || 0,
233
+ height: Number(variation?.height) || 0,
234
+ clickThrough: variation?.companionClickThroughURLTemplate?.url,
235
+ clickTracking: (variation?.companionClickTrackingURLTemplates ?? [])
236
+ .map((entry) => entry?.url)
237
+ .filter((url) => !!url),
238
+ };
239
+ }
240
+ }
241
+ }
242
+ return {
243
+ creativeId,
244
+ advertiser,
245
+ title: validAd.title || advertiser,
246
+ iconUrl,
247
+ clickThroughUrl,
248
+ trackingEvents: [],
249
+ companion,
250
+ };
251
+ };
252
+ /**
253
+ * Maps a VAST tag into the normalized SsaiCreative shape.
254
+ *
255
+ * Note: `fireImpression` is intentionally left to the shared default for now.
256
+ * Proper VAST impression/tracking wiring through `VASTTracker` is a documented
257
+ * follow-up; it requires retaining the parsed ad/tracker per creative.
258
+ */
259
+ const createVastProvider = (_config = {}) => {
260
+ return {
261
+ id: 'vast',
262
+ resolve: mapVastResponse,
263
+ };
264
+ };
265
+ /** Built-in provider factories keyed by name. */
266
+ const BUILTIN_PROVIDERS = {
267
+ streamlayer: (config) => createStreamLayerProvider(config),
268
+ quortex: (config) => createQuortexProvider(config),
269
+ vast: () => createVastProvider(),
270
+ };
271
+ const isProviderInstance = (option) => {
272
+ return typeof option === 'object' && typeof option.resolve === 'function';
273
+ };
274
+ /**
275
+ * Resolve a {@link SsaiProviderOption} into a concrete {@link SsaiCreativeProvider}.
276
+ * Defaults to the StreamLayer provider (using `fallbackConfig`) when no option is
277
+ * supplied.
278
+ */
279
+ const resolveProvider = (option, fallbackConfig) => {
280
+ if (!option) {
281
+ return createStreamLayerProvider(fallbackConfig);
282
+ }
283
+ if (typeof option === 'string') {
284
+ return BUILTIN_PROVIDERS[option](fallbackConfig);
285
+ }
286
+ if (isProviderInstance(option)) {
287
+ return option;
288
+ }
289
+ return BUILTIN_PROVIDERS[option.name](option.config ?? fallbackConfig);
290
+ };
291
+
292
+ exports.BUILTIN_PROVIDERS = BUILTIN_PROVIDERS;
293
+ exports.DEFAULT_CREATIVE_API_BASE = DEFAULT_CREATIVE_API_BASE;
294
+ exports.createQuortexProvider = createQuortexProvider;
295
+ exports.createStreamLayerProvider = createStreamLayerProvider;
296
+ exports.createVastProvider = createVastProvider;
297
+ exports.fireBeacon = fireBeacon;
298
+ exports.fireDefaultImpression = fireDefaultImpression;
299
+ exports.fireTrackingEvents = fireTrackingEvents;
300
+ exports.resolveProvider = resolveProvider;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * One place that constructs a `VASTTracker`.
3
+ *
4
+ * Every StreamLayer surface that reports VAST beacons — the GAM video player, both pause-ad
5
+ * renderers and the Standard Ad sidebar — needs the same `(ad, creative, variation)` triple, and
6
+ * each was assembling it by hand behind its own `@ts-ignore`. The parsed shapes those triples
7
+ * come from (`AdData` / `AdCompanion` / `AdNonLinear`) are defined in this package, so the
8
+ * mapping belongs here too.
9
+ *
10
+ * Deliberately *only* construction: when a beacon may fire, and how a surface de-duplicates its
11
+ * impression, stays with that surface. Those policies differ on purpose (per-ad-id in module
12
+ * scope for the sidebar, per component instance for the pause ads, per tracker identity for the
13
+ * player) and they are billing-visible, so collapsing them here would silently change counts.
14
+ */
15
+ import { VASTTracker, VastAd, VastCompanionAd, VastCreativeCompanion, VastCreativeLinear, VastCreativeNonLinear, VastNonLinearAd } from '@dailymotion/vast-client';
16
+ import type { AdCompanion, AdData } from './external-ad.js';
17
+ /** The creative (and, for non-linear creatives, the variation) a tracker reports through. */
18
+ export type VastTrackerTarget = {
19
+ ad: VastAd;
20
+ /** `<Linear>`, `<NonLinearAds>` or `<CompanionAds>` — whichever owns the `<TrackingEvents>`. */
21
+ creative: VastCreativeLinear | VastCreativeNonLinear | VastCreativeCompanion;
22
+ /**
23
+ * The `<NonLinear>` / `<Companion>` variation. Omitted for a linear creative; required for the
24
+ * others, since the variation is what owns their `creativeView`, click-through and click
25
+ * trackers.
26
+ */
27
+ variation?: VastNonLinearAd | VastCompanionAd;
28
+ };
29
+ export declare const createVastTracker: ({ ad, creative, variation }: VastTrackerTarget) => VASTTracker;
30
+ /**
31
+ * A tracker for a single `<Companion>`. A `<CompanionAds>` creative has no creative-level
32
+ * `<TrackingEvents>` — each variation owns its own `creativeView` — so a companion is reported
33
+ * through its own tracker. `undefined` when the companion was parsed without them (nothing to
34
+ * report against).
35
+ */
36
+ export declare const createCompanionTracker: (ad: VastAd, companion: AdCompanion) => VASTTracker | undefined;
37
+ /**
38
+ * The creative a parsed ad reports its impression / playback beacons through, in the order the
39
+ * shells draw them: the `<Linear>` creative first, then the `<NonLinear>` still, then — for a tag
40
+ * whose only creative is `<CompanionAds>` — the companion that was actually drawn.
41
+ *
42
+ * `drawnCompanion` is a parameter rather than `adData.companion` because only the rendering shell
43
+ * knows which companion reached the screen, and reporting one that did not is a false view.
44
+ */
45
+ export declare const resolveVastTrackerTarget: (adData: AdData, drawnCompanion?: AdCompanion) => VastTrackerTarget | undefined;
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+
3
+ var vastClient = require('@dailymotion/vast-client');
4
+
5
+ /**
6
+ * One place that constructs a `VASTTracker`.
7
+ *
8
+ * Every StreamLayer surface that reports VAST beacons — the GAM video player, both pause-ad
9
+ * renderers and the Standard Ad sidebar — needs the same `(ad, creative, variation)` triple, and
10
+ * each was assembling it by hand behind its own `@ts-ignore`. The parsed shapes those triples
11
+ * come from (`AdData` / `AdCompanion` / `AdNonLinear`) are defined in this package, so the
12
+ * mapping belongs here too.
13
+ *
14
+ * Deliberately *only* construction: when a beacon may fire, and how a surface de-duplicates its
15
+ * impression, stays with that surface. Those policies differ on purpose (per-ad-id in module
16
+ * scope for the sidebar, per component instance for the pause ads, per tracker identity for the
17
+ * player) and they are billing-visible, so collapsing them here would silently change counts.
18
+ */
19
+ // eslint-disable-next-line @typescript-eslint/triple-slash-reference
20
+ /// <reference path="../module.d.ts" />
21
+ const createVastTracker = ({ ad, creative, variation }) =>
22
+ // The first argument is the `VASTClient` the tracker may update; every surface here drives
23
+ // playback itself and only wants the beacons, so it is always `null`.
24
+ //
25
+ // The type shim declares the 3rd argument as `VastCreativeLinear` and the 4th as another
26
+ // *creative*. At runtime `vast-client` accepts any creative in the 3rd and expects the
27
+ // `<Companion>` / `<NonLinear>` *variation* in the 4th. The casts encode that runtime contract
28
+ // once here rather than at every call site.
29
+ new vastClient.VASTTracker(null, ad, creative, variation);
30
+ /**
31
+ * A tracker for a single `<Companion>`. A `<CompanionAds>` creative has no creative-level
32
+ * `<TrackingEvents>` — each variation owns its own `creativeView` — so a companion is reported
33
+ * through its own tracker. `undefined` when the companion was parsed without them (nothing to
34
+ * report against).
35
+ */
36
+ const createCompanionTracker = (ad, companion) => companion.creative && companion.variation
37
+ ? createVastTracker({ ad, creative: companion.creative, variation: companion.variation })
38
+ : undefined;
39
+ /**
40
+ * The creative a parsed ad reports its impression / playback beacons through, in the order the
41
+ * shells draw them: the `<Linear>` creative first, then the `<NonLinear>` still, then — for a tag
42
+ * whose only creative is `<CompanionAds>` — the companion that was actually drawn.
43
+ *
44
+ * `drawnCompanion` is a parameter rather than `adData.companion` because only the rendering shell
45
+ * knows which companion reached the screen, and reporting one that did not is a false view.
46
+ */
47
+ const resolveVastTrackerTarget = (adData, drawnCompanion) => {
48
+ // `AdData.creative` is not optional in the type but `getCreative` can return nothing: a
49
+ // programmatic tag does not have to carry a `<Linear>` creative.
50
+ if (adData.creative) {
51
+ return { ad: adData.ad, creative: adData.creative };
52
+ }
53
+ if (adData.nonLinear) {
54
+ return { ad: adData.ad, creative: adData.nonLinear.creative, variation: adData.nonLinear.variation };
55
+ }
56
+ if (drawnCompanion?.creative && drawnCompanion.variation) {
57
+ return { ad: adData.ad, creative: drawnCompanion.creative, variation: drawnCompanion.variation };
58
+ }
59
+ return undefined;
60
+ };
61
+
62
+ exports.createCompanionTracker = createCompanionTracker;
63
+ exports.createVastTracker = createVastTracker;
64
+ exports.resolveVastTrackerTarget = resolveVastTrackerTarget;