@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,50 @@
1
+ import { createSingleStore, StreamLayerContext } from '@streamlayer/sdk-web-interfaces';
2
+ import '@streamlayer/sdk-web-api';
3
+ export type GAMStaticCore = {
4
+ id: string;
5
+ parentId: string;
6
+ url: string;
7
+ imageSrc?: string;
8
+ /**
9
+ * Linear MP4 from the VAST response → renders full-bleed `<video>` (SC-322).
10
+ * Only produced when the host asked for it via one of the `fullbleed-video*`
11
+ * values of `ExternalPauseAdOptions.template`; a `'default'` ad can never carry
12
+ * this field.
13
+ */
14
+ backgroundVideoSrc?: string;
15
+ adUrl?: string;
16
+ ad?: any;
17
+ creative?: any;
18
+ variation?: any;
19
+ };
20
+ /**
21
+ * Which VAST creative this request resolves, set by the host on
22
+ * `pauseAdExternalUrls[].template`:
23
+ * - `'default'` — static image (nonlinear → companion) → sidebar overlay.
24
+ * - `'fullbleed-video*'` — the **linear** creative's MP4 → full-screen video (SC-322).
25
+ *
26
+ * This is a resolution STRATEGY, not a renderer hint the fetcher may ignore. The
27
+ * `fullbleed-video` variants differ ONLY in the chrome the renderer draws around
28
+ * the video, so creative resolution treats them identically.
29
+ */
30
+ export type PauseAdTemplate = 'default' | 'fullbleed-video' | 'fullbleed-video-center' | 'fullbleed-video-left';
31
+ /**
32
+ * `indexOf` rather than `Array.prototype.includes`: this package ships to Chrome 32
33
+ * TV webviews and must stay ES5-compatible without a polyfill.
34
+ */
35
+ export declare const isFullbleedVideoTemplate: (template?: PauseAdTemplate) => boolean;
36
+ export type ExternalPauseAdOptions = {
37
+ refetchInterval: number;
38
+ prefetch: boolean;
39
+ $enabled: boolean;
40
+ /**
41
+ * Creative-resolution strategy. Defaults to `'default'`.
42
+ *
43
+ * NOT reactive — it is part of the store's IDENTITY (see the composite Map key
44
+ * in `Advertisement.getExternalPauseAd`), not of its state. `$enabled` is the
45
+ * only reactive option here; `refetchInterval`/`prefetch` are read once at
46
+ * store-creation time.
47
+ */
48
+ template?: PauseAdTemplate;
49
+ };
50
+ export declare const externalPauseAd: (instance: StreamLayerContext) => (gamUrl: string, $enabled: ReturnType<typeof createSingleStore<boolean>>, options: ExternalPauseAdOptions) => import("@nanostores/query", { with: { "resolution-mode": "import" } }).FetcherStore<GAMStaticCore | undefined, any>;
@@ -0,0 +1,259 @@
1
+ 'use strict';
2
+
3
+ var sdkWebInterfaces = require('@streamlayer/sdk-web-interfaces');
4
+ var sdkWebLogger = require('@streamlayer/sdk-web-logger');
5
+ require('@streamlayer/sdk-web-api');
6
+ var vastClient$1 = require('@dailymotion/vast-client');
7
+ var adMacros = require('./ad-macros.js');
8
+
9
+ // Pins the `@dailymotion/vast-client` → `@types/vast-client` shim: TypeDoc
10
+ // compiles this file through dependent packages' programs, whose tsconfig
11
+ // `include` does not cover this package's ambient declarations.
12
+ // eslint-disable-next-line @typescript-eslint/triple-slash-reference
13
+ /// <reference path="../module.d.ts" />
14
+ const FULLBLEED_VIDEO_TEMPLATES = [
15
+ 'fullbleed-video',
16
+ 'fullbleed-video-center',
17
+ 'fullbleed-video-left',
18
+ ];
19
+ /**
20
+ * `indexOf` rather than `Array.prototype.includes`: this package ships to Chrome 32
21
+ * TV webviews and must stay ES5-compatible without a polyfill.
22
+ */
23
+ const isFullbleedVideoTemplate = (template) => !!template && FULLBLEED_VIDEO_TEMPLATES.indexOf(template) !== -1;
24
+ const MP4_MIME = 'video/mp4';
25
+ // Programmatic spec (Index Exchange): bitrate window for a linear pause-ad MP4.
26
+ const MIN_BITRATE = 5000;
27
+ const MAX_BITRATE = 40000;
28
+ const TARGET_WIDTH = 1920;
29
+ const TARGET_HEIGHT = 1080;
30
+ const bitrateDistanceToWindow = (bitrate) => {
31
+ if (bitrate < MIN_BITRATE)
32
+ return MIN_BITRATE - bitrate;
33
+ if (bitrate > MAX_BITRATE)
34
+ return bitrate - MAX_BITRATE;
35
+ return 0;
36
+ };
37
+ const resolutionDistanceToTarget = (file) => Math.abs((file.width ?? 0) - TARGET_WIDTH) + Math.abs((file.height ?? 0) - TARGET_HEIGHT);
38
+ /**
39
+ * Pick the best linear MP4 from a VAST creative's media files: prefer the
40
+ * highest bitrate inside the 5000–40000 kbps window; if none qualify, pick the
41
+ * one closest to the window. Ties break toward 1920×1080. Returns `undefined`
42
+ * when no MP4 is present so image-only VAST keeps working.
43
+ */
44
+ const selectMp4MediaFile = (mediaFiles = []) => {
45
+ const mp4s = (mediaFiles ?? []).filter((file) => !!file?.fileURL && file.mimeType === MP4_MIME);
46
+ if (mp4s.length === 0) {
47
+ return undefined;
48
+ }
49
+ const inWindow = mp4s.filter((file) => {
50
+ const bitrate = file.bitrate ?? 0;
51
+ return bitrate >= MIN_BITRATE && bitrate <= MAX_BITRATE;
52
+ });
53
+ if (inWindow.length > 0) {
54
+ return [...inWindow].sort((a, b) => {
55
+ const byBitrate = (b.bitrate ?? 0) - (a.bitrate ?? 0);
56
+ return byBitrate !== 0 ? byBitrate : resolutionDistanceToTarget(a) - resolutionDistanceToTarget(b);
57
+ })[0];
58
+ }
59
+ return [...mp4s].sort((a, b) => {
60
+ const byWindow = bitrateDistanceToWindow(a.bitrate ?? 0) - bitrateDistanceToWindow(b.bitrate ?? 0);
61
+ return byWindow !== 0 ? byWindow : resolutionDistanceToTarget(a) - resolutionDistanceToTarget(b);
62
+ })[0];
63
+ };
64
+ const vastClient = new vastClient$1.VASTClient();
65
+ const log = sdkWebLogger.createLogger('ui:gam-static');
66
+ const preloadImage = (src) => {
67
+ return new Promise((resolve, reject) => {
68
+ const image = new Image();
69
+ image.onload = () => resolve(src);
70
+ image.onerror = () => reject();
71
+ image.src = src;
72
+ });
73
+ };
74
+ const externalPauseAd = (instance) => {
75
+ return (gamUrl, $enabled, options) =>
76
+ // `template` MUST be a key part: nanoquery joins parts with `''` (no separator)
77
+ // and its `cache` Map is global per transport, so two stores that produce the
78
+ // same joined key share data AND cross-trigger each other's `revalidate()`.
79
+ // The `tpl:` prefix keeps the url/template boundary unambiguous. Never let this
80
+ // part be `undefined` — an undefined part nulls the WHOLE key and permanently
81
+ // suspends fetching (that is exactly how `$enabled: false` works).
82
+ instance.transport.nanoquery.createFetcherStore([gamUrl, $enabled, `tpl:${options.template ?? 'default'}`], {
83
+ dedupeTime: options.prefetch ? options.refetchInterval : Infinity,
84
+ revalidateInterval: options.refetchInterval > 0 ? options.refetchInterval : 0,
85
+ // A creative-type mismatch is DETERMINISTIC: this tag returned no usable linear
86
+ // MP4 and retrying it can never change that, so cancel nanoquery's BACKOFF RETRY
87
+ // LOOP (any falsy return does) instead of re-hitting the ad server for the rest of
88
+ // the session — with the default `pauseAdDelay: 0` the store is enabled from mount.
89
+ // This only cancels the retry loop: `revalidateInterval` still re-runs the fetcher
90
+ // on its timer, and `Advertisement.getExternalPauseAd` forces a `revalidate()` on
91
+ // every `$enabled` false → true edge, so with `pauseAdDelay > 0` the tag is still
92
+ // re-requested on every pause.
93
+ // Transient failures (network, CORS, malformed XML, 5xx) still retry; the backoff
94
+ // below mirrors nanoquery's `defaultOnErrorRetry` so the two templates stay on the
95
+ // same curve. The jitter is not decorative — it spreads out retries that would
96
+ // otherwise fire in lockstep when the failure is correlated across stores/clients
97
+ // (ad-server 5xx, CORS), which the `pauseAdDelay: 0` prefetch-at-mount makes more
98
+ // likely.
99
+ // On `default` this is `undefined`, i.e. exactly as if the option were not passed —
100
+ // true as long as the shared nanoquery instance sets no global `onErrorRetry` (an
101
+ // explicit `undefined` DOES shadow a global one, and only then does nanoquery's
102
+ // destructuring default restore `defaultOnErrorRetry`).
103
+ onErrorRetry: isFullbleedVideoTemplate(options.template)
104
+ ? ({ error, retryCount }) => {
105
+ if (error instanceof Error && error.message === 'no_valid_ad') {
106
+ return false;
107
+ }
108
+ return ~~((Math.random() + 0.5) * (1 << Math.min(retryCount, 8))) * 2000;
109
+ }
110
+ : undefined,
111
+ fetcher: async (adUrl) => {
112
+ if (!adUrl || typeof adUrl !== 'string') {
113
+ throw new Error('ad url is empty');
114
+ }
115
+ sdkWebInterfaces.eventBus.emit('exposedPauseAd', {
116
+ action: 'load',
117
+ payload: {},
118
+ });
119
+ try {
120
+ // `resolveAll: true` follows every `VASTAdTagURI` wrapper chain and resolves all
121
+ // Ads (the default only resolves the first standalone Ad and stops at the wrapper).
122
+ // Pause-ad tags do not go through `gam()`, so fill the macros here — with the host's values
123
+ // as they are NOW, since this fetcher re-runs on every pause. The store key stays the
124
+ // configured url: `ExposedPauseAd` compares against it.
125
+ const parsedVAST = await vastClient.get(adMacros.substituteAdTagMacros(adUrl, instance.sdk.options.get().adTagParams), {
126
+ resolveAll: true,
127
+ });
128
+ const template = options.template ?? 'default';
129
+ // `fullbleed-video` needs an ad that actually carries a usable linear MP4. With
130
+ // `resolveAll: true` the response can be a pod/wrapper chain, so prefer the first
131
+ // ad that satisfies the template and fall back to the first ad with any creative
132
+ // (which preserves the existing `no_valid_ad` error and event sequencing).
133
+ const validAd = isFullbleedVideoTemplate(template)
134
+ ? (parsedVAST.ads.find((ad) => ad.creatives.some((creative) => creative.type === 'linear' &&
135
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
136
+ // @ts-ignore vast-client linear creative carries mediaFiles
137
+ selectMp4MediaFile(creative.mediaFiles)?.fileURL)) ?? parsedVAST.ads.find((ads) => ads.creatives.length > 0))
138
+ : parsedVAST.ads.find((ads) => ads.creatives.length > 0);
139
+ if (validAd) {
140
+ const parentId = validAd.id || '';
141
+ log.debug(validAd, 'validAd');
142
+ // The HOST decides which creative wins (SC-322-2). `fullbleed-video` asks
143
+ // for the linear MP4 explicitly, so it is resolved FIRST and never falls
144
+ // back to a static image — the nonlinear/companion branches below `return`
145
+ // early, so without this a VAST carrying BOTH an image and a linear MP4
146
+ // (e.g. Google `single_ad_samples`) would never reach the linear creative.
147
+ // `default` keeps the pre-SC-322 nonlinear → companion order below and can
148
+ // never produce a video.
149
+ if (isFullbleedVideoTemplate(template)) {
150
+ const linear = validAd.creatives.find((creative) => creative.type === 'linear' &&
151
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
152
+ // @ts-ignore vast-client linear creative carries mediaFiles
153
+ selectMp4MediaFile(creative.mediaFiles)?.fileURL);
154
+ if (linear) {
155
+ log.debug(linear, 'linear');
156
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
157
+ // @ts-ignore vast-client linear creative carries mediaFiles
158
+ const mediaFile = selectMp4MediaFile(linear.mediaFiles);
159
+ if (mediaFile?.fileURL) {
160
+ const id = linear.adId || linear.id || parentId;
161
+ sdkWebInterfaces.eventBus.emit('exposedPauseAd', {
162
+ action: 'loaded',
163
+ payload: {
164
+ id,
165
+ parentId,
166
+ },
167
+ });
168
+ return {
169
+ id,
170
+ parentId,
171
+ url: adUrl,
172
+ backgroundVideoSrc: mediaFile.fileURL,
173
+ ad: validAd,
174
+ creative: linear,
175
+ };
176
+ }
177
+ }
178
+ // Host asked for full-bleed video and this VAST carries no usable linear
179
+ // MP4 → fail closed so `ExposedPauseAd`'s error effect auto-closes the ad,
180
+ // exactly like any other unusable VAST. See §5 of the brief.
181
+ throw new Error('no_valid_ad');
182
+ }
183
+ const nonlinear = validAd.creatives.find((creative) => creative.type === 'nonlinear');
184
+ if (nonlinear) {
185
+ const id = nonlinear.adId || nonlinear.id || parentId;
186
+ log.debug(nonlinear, 'nonlinear');
187
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
188
+ // @ts-ignore
189
+ const variation = nonlinear.variations?.[0];
190
+ const staticResource = variation?.staticResource;
191
+ const adUrlTemplate = variation?.nonlinearClickThroughURLTemplate;
192
+ if (staticResource) {
193
+ await preloadImage(staticResource);
194
+ sdkWebInterfaces.eventBus.emit('exposedPauseAd', {
195
+ action: 'loaded',
196
+ payload: {
197
+ id,
198
+ parentId,
199
+ },
200
+ });
201
+ return {
202
+ id,
203
+ parentId,
204
+ url: adUrl,
205
+ imageSrc: staticResource,
206
+ adUrl: adUrlTemplate,
207
+ ad: validAd,
208
+ creative: nonlinear,
209
+ variation,
210
+ };
211
+ }
212
+ }
213
+ const companion = validAd.creatives.find((creative) => creative.type === 'companion');
214
+ log.debug(companion, 'companion');
215
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
216
+ // @ts-ignore
217
+ const variation = companion?.variations?.[0];
218
+ const staticResource = variation?.staticResources?.[0]?.url;
219
+ const adUrlTemplate = variation?.companionClickThroughURLTemplate;
220
+ // `&& companion` is for TS narrowing only (companion.adId access below);
221
+ // it is runtime-redundant since staticResource derives from companion.
222
+ if (staticResource && companion) {
223
+ const id = companion.adId || companion.id || parentId;
224
+ await preloadImage(staticResource);
225
+ sdkWebInterfaces.eventBus.emit('exposedPauseAd', {
226
+ action: 'loaded',
227
+ payload: {
228
+ id,
229
+ parentId,
230
+ },
231
+ });
232
+ return {
233
+ id,
234
+ parentId,
235
+ url: adUrl,
236
+ imageSrc: staticResource,
237
+ adUrl: adUrlTemplate,
238
+ ad: validAd,
239
+ creative: companion,
240
+ variation,
241
+ };
242
+ }
243
+ }
244
+ throw new Error('no_valid_ad');
245
+ }
246
+ catch (err) {
247
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
248
+ // @ts-expect-error
249
+ if (err.message !== 'no_valid_ad' && err.message !== 'ad url is empty') {
250
+ console.error(err);
251
+ }
252
+ throw err;
253
+ }
254
+ },
255
+ });
256
+ };
257
+
258
+ exports.externalPauseAd = externalPauseAd;
259
+ exports.isFullbleedVideoTemplate = isFullbleedVideoTemplate;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Google PAL nonce manager + ad-tag resolution for programmatic (GAM) VAST requests.
3
+ *
4
+ * Lives here because the VAST fetcher (`units/external-ad.ts`) is its only consumer: this is the
5
+ * request side of an ad tag, not the promotion side. The *composition* of a tag URL from the proto
6
+ * (`processGamAdvertisement` / `selectGamOptions`) stays in `feature-gamification`, next to the
7
+ * `$store` bridge that is its only consumer — so the two feature packages share no code and
8
+ * neither depends on the other.
9
+ */
10
+ import type { AdTagParams } from '@streamlayer/sdk-web-core';
11
+ export type PalNonceManager = {
12
+ getNonce: () => string;
13
+ sendAdClick: () => void;
14
+ sendPlaybackStart: () => void;
15
+ sendPlaybackEnd: () => void;
16
+ };
17
+ export type GamNonceResult = {
18
+ url: URL;
19
+ manager?: PalNonceManager;
20
+ };
21
+ export declare const gam: (rawUrl: string, adContainer?: HTMLDivElement, adTagParams?: AdTagParams) => (() => Promise<{
22
+ url: URL;
23
+ }>) | (() => Promise<{
24
+ url: URL;
25
+ manager: PalNonceManager;
26
+ } | undefined>);
@@ -0,0 +1,152 @@
1
+ 'use strict';
2
+
3
+ var sdkWebStorage = require('@streamlayer/sdk-web-storage');
4
+ var uuid = require('uuid');
5
+ var adMacros = require('./ad-macros.js');
6
+
7
+ const urlIsNotGam = (url) => {
8
+ return url.origin !== 'https://pubads.g.doubleclick.net';
9
+ };
10
+ const palScriptUrl = 'https://imasdk.googleapis.com/pal/sdkloader/pal.js';
11
+ const getPal = () => {
12
+ // @ts-expect-error Google PAL is supplied by the host or the script loader below.
13
+ return typeof goog === 'undefined' ? undefined : goog.pal;
14
+ };
15
+ let palReady;
16
+ const ensurePal = async () => {
17
+ if (getPal()) {
18
+ return;
19
+ }
20
+ if (!palReady) {
21
+ palReady = new Promise((resolve, reject) => {
22
+ if (typeof document === 'undefined') {
23
+ reject(new Error('Google PAL requires a browser document'));
24
+ return;
25
+ }
26
+ const existing = document.querySelector(`script[src="${palScriptUrl}"]`);
27
+ const script = existing || document.createElement('script');
28
+ const finish = (error) => {
29
+ clearTimeout(timeout);
30
+ script.removeEventListener('load', onLoad);
31
+ script.removeEventListener('error', onError);
32
+ if (error) {
33
+ if (!existing) {
34
+ script.remove();
35
+ }
36
+ reject(error);
37
+ }
38
+ else {
39
+ resolve();
40
+ }
41
+ };
42
+ const onLoad = () => finish(getPal() ? undefined : new Error('Google PAL did not initialize'));
43
+ const onError = () => finish(new Error('Google PAL failed to load'));
44
+ const timeout = setTimeout(() => finish(new Error('Google PAL loading timed out')), 10000);
45
+ script.addEventListener('load', onLoad);
46
+ script.addEventListener('error', onError);
47
+ if (!existing) {
48
+ script.async = true;
49
+ script.src = palScriptUrl;
50
+ document.head.appendChild(script);
51
+ }
52
+ }).catch((error) => {
53
+ palReady = undefined;
54
+ throw error;
55
+ });
56
+ }
57
+ await palReady;
58
+ palReady = undefined;
59
+ };
60
+ const gam = (rawUrl, adContainer, adTagParams) => {
61
+ // Every VAST/VMAP tag funnels through here, so this is where the macros get filled — for GAM and
62
+ // for any other vendor alike. Studio stores a programmatic tag verbatim, so an unexpanded
63
+ // `[timestamp]` / `[clickmacro]` / `${GDPR}` would otherwise reach the ad server literal, and a
64
+ // literal macro comes back copied into the vendor's click-through. `adTagParams` are the host's
65
+ // consent and click values, read by the caller at request time. The GAM `[placeholder]` tokens
66
+ // are left alone here and filled per parameter below.
67
+ const url = adMacros.substituteAdTagMacros(rawUrl, adTagParams);
68
+ const parsedUrl = (() => {
69
+ try {
70
+ const parsedUrl = new URL(url);
71
+ return parsedUrl;
72
+ }
73
+ catch (_err) {
74
+ return undefined;
75
+ }
76
+ })();
77
+ // Non-GAM VAST/VMAP URLs carry no GAM ad-request params and need no PAL nonce, so
78
+ // resolve them directly. PAL is only required for pubads.g.doubleclick.net requests —
79
+ // raw-VAST consumers (e.g. injected sidebar / l-bar) must not depend on the Google PAL
80
+ // SDK being loaded, so this must run before touching `goog`.
81
+ if (parsedUrl && urlIsNotGam(parsedUrl)) {
82
+ // eslint-disable-next-line require-await
83
+ return async () => {
84
+ return { url: parsedUrl };
85
+ };
86
+ }
87
+ if (!parsedUrl) {
88
+ // eslint-disable-next-line require-await
89
+ return async () => {
90
+ return undefined;
91
+ };
92
+ }
93
+ const generateUrl = async () => {
94
+ try {
95
+ await ensurePal();
96
+ const pal = getPal();
97
+ const consentSettings = new pal.ConsentSettings();
98
+ consentSettings.allowStorage = false;
99
+ const nonceLoader = new pal.NonceLoader(consentSettings);
100
+ const parsedUrl = new URL(url);
101
+ const request = new pal.NonceRequest();
102
+ let deviceId = sdkWebStorage.safeLocalStorage().getItem('sl-device-id') || uuid.v4();
103
+ let sessionId = sdkWebStorage.safeSessionStorage().getItem('sl-ad-session-id');
104
+ if (!sessionId) {
105
+ sessionId = uuid.v4();
106
+ sdkWebStorage.safeSessionStorage().setItem('sl-ad-session-id', sessionId);
107
+ }
108
+ if (!uuid.validate(deviceId)) {
109
+ deviceId = uuid.v5(`https://${deviceId}`, uuid.v5.URL);
110
+ }
111
+ if (!parsedUrl.searchParams.get('url')) {
112
+ parsedUrl.searchParams.set('url', 'https://streamlayer.io');
113
+ }
114
+ if (!parsedUrl.searchParams.get('description_url')) {
115
+ parsedUrl.searchParams.set('description_url', 'https://streamlayer.io');
116
+ }
117
+ request.adWillAutoPlay = parsedUrl.searchParams.get('vpa') === 'auto';
118
+ request.adWillPlayMuted = parsedUrl.searchParams.get('vpmute') === '1';
119
+ request.continuousPlayback = true;
120
+ request.descriptionUrl = parsedUrl.searchParams.get('description_url');
121
+ request.iconsSupported = true;
122
+ request.sessionId = deviceId;
123
+ // Player support for VPAID 2.0, OMID 1.0, and SIMID 1.1
124
+ request.supportedApiFrameworks = '2,7,9';
125
+ request.videoHeight = adContainer?.clientHeight ?? 0;
126
+ request.videoWidth = adContainer?.clientWidth ?? 0;
127
+ const nonceManager = await nonceLoader.loadNonceManager(request);
128
+ if (parsedUrl.searchParams.get('is_lat') === '[placeholder]') {
129
+ parsedUrl.searchParams.set('is_lat', '1');
130
+ }
131
+ if (parsedUrl.searchParams.get('rdid') === '[placeholder]') {
132
+ parsedUrl.searchParams.set('rdid', deviceId);
133
+ }
134
+ if (parsedUrl.searchParams.get('correlator') === '[placeholder]') {
135
+ parsedUrl.searchParams.set('correlator', sessionId);
136
+ }
137
+ parsedUrl.searchParams.set('wta', '1');
138
+ parsedUrl.searchParams.set('givn', nonceManager.getNonce());
139
+ return {
140
+ url: parsedUrl,
141
+ manager: nonceManager,
142
+ };
143
+ }
144
+ catch (adError) {
145
+ console.log('AdsManager could not be started', adError);
146
+ return undefined;
147
+ }
148
+ };
149
+ return generateUrl;
150
+ };
151
+
152
+ exports.gam = gam;
@@ -0,0 +1,26 @@
1
+ import { AdType } from '@streamlayer/sl-eslib/interactive/interactive.common_pb';
2
+ export interface VastCreative {
3
+ adName: string;
4
+ /**
5
+ * The placement this extension NAMES, or `undefined` when its root element is not one we map.
6
+ *
7
+ * Deliberately not defaulted here. An unrecognised extension means "this tag says nothing about
8
+ * placement", which must not outrank a placement the promotion proto already stated — defaulting
9
+ * it to `SIDE_BY_SIDE` at this layer silently re-routed a configured Sidebar/L-Bar, because the
10
+ * consumers join with `extension?.adType ?? placement`. The default now lives in
11
+ * `resolveExternalAdType`, applied only after the proto placement has had its turn.
12
+ */
13
+ adType?: AdType;
14
+ landscapeBackground?: string;
15
+ portraitBackground?: string;
16
+ sponsorLogo?: string;
17
+ ctaLabel?: string;
18
+ }
19
+ interface VastNode {
20
+ name: string;
21
+ value: string | null;
22
+ attributes: Record<string, string>;
23
+ children: VastNode[];
24
+ }
25
+ export declare function parseVastExtension(root: VastNode): VastCreative | undefined;
26
+ export {};
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ var interactive_common_pb = require('@streamlayer/sl-eslib/interactive/interactive.common_pb');
4
+
5
+ /**
6
+ * Extension root element name → the placement it names. Only `DoubleBox` is defined by the
7
+ * StreamLayer extension schema today; anything else is a vendor extension we do not interpret.
8
+ */
9
+ const MAP_EXTENSION_NAME_AD_TYPE = {
10
+ DoubleBox: interactive_common_pb.AdType.SIDE_BY_SIDE,
11
+ };
12
+ function parseVastExtension(root) {
13
+ if (!root) {
14
+ return undefined;
15
+ }
16
+ const child = (node, name) => node.children.find((c) => c.name === name);
17
+ const childrenByName = (node, name) => node.children.filter((c) => c.name === name);
18
+ const urlOf = (node) => child(node, 'Url')?.value ?? '';
19
+ const num = (v) => (v == null ? NaN : Number(v));
20
+ const adName = child(root, 'AdName')?.value ?? '';
21
+ const backgroundsNode = child(root, 'Backgrounds');
22
+ const backgrounds = backgroundsNode
23
+ ? childrenByName(backgroundsNode, 'Background').map((bg) => ({
24
+ orientation: bg.attributes.orientation,
25
+ width: num(bg.attributes.width),
26
+ height: num(bg.attributes.height),
27
+ type: bg.attributes.type,
28
+ url: urlOf(bg),
29
+ }))
30
+ : [];
31
+ const logosNode = child(root, 'Logos');
32
+ const logos = logosNode
33
+ ? childrenByName(logosNode, 'Logo').map((logo) => ({
34
+ role: logo.attributes.role,
35
+ width: num(logo.attributes.width),
36
+ height: num(logo.attributes.height),
37
+ type: logo.attributes.type,
38
+ url: urlOf(logo),
39
+ }))
40
+ : [];
41
+ const ctaNode = child(root, 'CTA');
42
+ const cta = {
43
+ label: ctaNode ? (child(ctaNode, 'Label')?.value ?? '') : '',
44
+ type: ctaNode ? (child(ctaNode, 'Type')?.value ?? '') : '',
45
+ };
46
+ return {
47
+ adName,
48
+ adType: MAP_EXTENSION_NAME_AD_TYPE[root.name],
49
+ landscapeBackground: backgrounds.find((b) => b.orientation === 'landscape')?.url,
50
+ portraitBackground: backgrounds.find((b) => b.orientation === 'portrait')?.url,
51
+ sponsorLogo: logos.find((l) => l.role === 'header')?.url,
52
+ ctaLabel: cta.label,
53
+ };
54
+ }
55
+
56
+ exports.parseVastExtension = parseVastExtension;
@@ -0,0 +1,49 @@
1
+ import { type Question } from '@streamlayer/sl-eslib/interactive/interactive.common_pb';
2
+ export type PauseAdLayout = 'left' | 'center' | 'image';
3
+ /**
4
+ * Which full-bleed renderer draws the card. Absent === `'standard'`.
5
+ *
6
+ * - `'standard'` — the Standard/Image pause ad (promotion card).
7
+ * - `'insight'` — the Insight Pause Ad (factoid card): sponsor logo, heading and a
8
+ * single-column body beside an optional media image (SC-353).
9
+ */
10
+ export type PauseAdKind = 'standard' | 'insight';
11
+ /** How the Insight media image is cropped — mirrors Studio's Media switcher. */
12
+ export type PauseAdMediaMode = 'image' | 'circle' | 'none';
13
+ /**
14
+ * Insight body cap (SC-353). Studio enforces it on the Additional Content field; the
15
+ * SDK clamps as well so a card authored before the limit landed cannot overflow the
16
+ * screen — the format is explicitly designed to fit without scrolling.
17
+ */
18
+ export declare const INSIGHT_BODY_MAX_LENGTH = 600;
19
+ export type PauseAdCard = {
20
+ id: string;
21
+ parentId: string;
22
+ /** Optional: a video-only card (programmatic VAST, SC-322) has no image. */
23
+ backgroundImageSrc?: string;
24
+ /** Linear MP4 from VAST → renders full-bleed `<video>` instead of the image (SC-322). */
25
+ backgroundVideoSrc?: string;
26
+ layout: PauseAdLayout;
27
+ isInteractive: boolean;
28
+ logoSrc?: string;
29
+ title?: string;
30
+ description?: string;
31
+ ctaLabel?: string;
32
+ ctaUrl?: string;
33
+ qrCodeUrl?: string;
34
+ qrCodeLabel?: string;
35
+ /** Absent === `'standard'`. */
36
+ kind?: PauseAdKind;
37
+ /** Insight only: the Media image (`FactoidOptions.image`). */
38
+ mediaSrc?: string;
39
+ /** Insight only: how `mediaSrc` is cropped. */
40
+ mediaMode?: PauseAdMediaMode;
41
+ };
42
+ /**
43
+ * The `pauseAdList` response, shaped for the renderers: keep the cards the SDK has chrome for,
44
+ * turn each into a `PauseAdCard`, drop the ones that cannot be drawn.
45
+ *
46
+ * Pure on purpose — `pause-ad.ts` owns the store and the transport, this owns the card shapes,
47
+ * and only this half needs unit tests.
48
+ */
49
+ export declare const parsePauseAdCards: (questions: Question[]) => PauseAdCard[];