@streamlayer/feature-advertisement 1.1.1-rc.1350 → 1.1.1-rc.2.pf58ff685

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 (60) hide show
  1. package/lib/advertisement.js +24 -9
  2. package/lib/cjs/advertisement.js +24 -9
  3. package/lib/cjs/index.d.ts +2 -0
  4. package/lib/cjs/index.js +2 -0
  5. package/lib/cjs/units/ad-macros.d.ts +7 -0
  6. package/lib/cjs/units/ad-macros.js +91 -0
  7. package/lib/cjs/units/external-ad.d.ts +69 -0
  8. package/lib/cjs/units/external-ad.js +93 -62
  9. package/lib/cjs/units/external-pause-ad.js +7 -1
  10. package/lib/cjs/units/gam.d.ts +11 -1
  11. package/lib/cjs/units/gam.js +65 -17
  12. package/lib/cjs/units/parse-vast-extensions.d.ts +10 -1
  13. package/lib/cjs/units/parse-vast-extensions.js +5 -1
  14. package/lib/cjs/units/sponsor-logo.d.ts +78 -0
  15. package/lib/cjs/units/sponsor-logo.js +194 -0
  16. package/lib/cjs/units/ssai-detector.js +27 -11
  17. package/lib/cjs/units/ssai-log.d.ts +16 -0
  18. package/lib/cjs/units/ssai-log.js +23 -0
  19. package/lib/cjs/units/ssai-providers.js +48 -11
  20. package/lib/classic/advertisement.js +34 -17
  21. package/lib/classic/cjs/advertisement.js +34 -17
  22. package/lib/classic/cjs/index.js +2 -0
  23. package/lib/classic/cjs/units/ad-macros.js +115 -0
  24. package/lib/classic/cjs/units/external-ad.js +111 -64
  25. package/lib/classic/cjs/units/external-pause-ad.js +5 -1
  26. package/lib/classic/cjs/units/gam.js +73 -10
  27. package/lib/classic/cjs/units/parse-vast-extensions.js +5 -1
  28. package/lib/classic/cjs/units/sponsor-logo.js +201 -0
  29. package/lib/classic/cjs/units/ssai-detector.js +33 -8
  30. package/lib/classic/cjs/units/ssai-log.js +23 -0
  31. package/lib/classic/cjs/units/ssai-providers.js +86 -24
  32. package/lib/classic/index.js +1 -0
  33. package/lib/classic/units/ad-macros.js +113 -0
  34. package/lib/classic/units/external-ad.js +109 -65
  35. package/lib/classic/units/external-pause-ad.js +5 -1
  36. package/lib/classic/units/gam.js +73 -10
  37. package/lib/classic/units/parse-vast-extensions.js +5 -1
  38. package/lib/classic/units/sponsor-logo.js +197 -0
  39. package/lib/classic/units/ssai-detector.js +33 -8
  40. package/lib/classic/units/ssai-log.js +21 -0
  41. package/lib/classic/units/ssai-providers.js +86 -24
  42. package/lib/index.d.ts +2 -0
  43. package/lib/index.js +1 -0
  44. package/lib/units/ad-macros.d.ts +7 -0
  45. package/lib/units/ad-macros.js +87 -0
  46. package/lib/units/external-ad.d.ts +69 -0
  47. package/lib/units/external-ad.js +92 -64
  48. package/lib/units/external-pause-ad.js +7 -1
  49. package/lib/units/gam.d.ts +11 -1
  50. package/lib/units/gam.js +65 -17
  51. package/lib/units/parse-vast-extensions.d.ts +10 -1
  52. package/lib/units/parse-vast-extensions.js +5 -1
  53. package/lib/units/sponsor-logo.d.ts +78 -0
  54. package/lib/units/sponsor-logo.js +188 -0
  55. package/lib/units/ssai-detector.js +27 -11
  56. package/lib/units/ssai-log.d.ts +16 -0
  57. package/lib/units/ssai-log.js +18 -0
  58. package/lib/units/ssai-providers.js +48 -11
  59. package/lib/vast-client.d.ts +9 -0
  60. package/package.json +8 -8
@@ -27,6 +27,7 @@ import '@streamlayer/sdk-web-api/classic';
27
27
  import VMAP from 'vmap';
28
28
  import { gam } from './gam.js';
29
29
  import { parseVastExtension } from './parse-vast-extensions.js';
30
+ import { collectAssets, selectCardLogo } from './sponsor-logo.js';
30
31
 
31
32
  const log = createLogger('ui:gam-external-ad');
32
33
  /** Default budget for a single VAST resolution before it is treated as "no ad". */
@@ -54,7 +55,7 @@ const withTimeout = (promise, ms) => {
54
55
  * Get the first valid creative from an ad
55
56
  */
56
57
  const getCreative = ad => {
57
- return ad.creatives.find(creative => creative.mediaFiles);
58
+ return ad.creatives.find(creative => getMediaFileUrl(creative));
58
59
  };
59
60
  /**
60
61
  * Get the URL of the first media file in the creative
@@ -77,32 +78,10 @@ const getClickThrough = creative => {
77
78
  // @ts-ignore — vast-client types `videoClickThroughURLTemplate` loosely
78
79
  return (creative === null || creative === void 0 || (_creative$videoClickT = creative.videoClickThroughURLTemplate) === null || _creative$videoClickT === void 0 ? void 0 : _creative$videoClickT.url) || undefined;
79
80
  };
80
- /**
81
- * Extract every `<Companion>` variation of the first `<CompanionAds>` creative that carries a
82
- * `<StaticResource>`, in document order. `width`/`height` are XML attributes (strings at
83
- * runtime), normalised to numbers so the size mapping can compare them.
84
- */
85
- const getCompanions = ad => {
86
- const creative = ad.creatives.find(item => item.type === 'companion');
87
- return ((creative === null || creative === void 0 ? void 0 : creative.variations) || []).reduce((acc, variation) => {
88
- var _variation$staticReso, _variation$companionC;
89
- const imageUrl = variation === null || variation === void 0 || (_variation$staticReso = variation.staticResources) === null || _variation$staticReso === void 0 || (_variation$staticReso = _variation$staticReso[0]) === null || _variation$staticReso === void 0 ? void 0 : _variation$staticReso.url;
90
- if (!imageUrl) {
91
- return acc;
92
- }
93
- return acc.concat({
94
- imageUrl,
95
- clickUrl: (_variation$companionC = variation.companionClickThroughURLTemplate) !== null && _variation$companionC !== void 0 ? _variation$companionC : undefined,
96
- width: Number(variation.width) || undefined,
97
- height: Number(variation.height) || undefined,
98
- creative,
99
- variation
100
- });
101
- }, []);
102
- };
103
81
  /** Product companion mapping: the 300x75 companion is the logo, the banner-sized one the media. */
104
82
  const LOGO_COMPANION_WIDTH = 300;
105
83
  const LOGO_COMPANION_HEIGHT = 75;
84
+ const isLogoSized = companion => (companion === null || companion === void 0 ? void 0 : companion.width) === LOGO_COMPANION_WIDTH && (companion === null || companion === void 0 ? void 0 : companion.height) === LOGO_COMPANION_HEIGHT;
106
85
  const companionArea = companion => (companion.width || 0) * (companion.height || 0);
107
86
  /**
108
87
  * Split a `<CompanionAds>` creative's variations into the logo and media slots.
@@ -110,9 +89,18 @@ const companionArea = companion => (companion.width || 0) * (companion.height ||
110
89
  * The pair mapping activates only for a tag that ships an exact 300x75 companion next to at
111
90
  * least one other. Deliberately conservative pending broader product sizes: on any other tag
112
91
  * no companion is promoted to the logo slot and the first one stays the media/strip candidate.
92
+ *
93
+ * Both halves of that gate are load-bearing and neither is an aspect rule: a 320x80 companion is
94
+ * the same 4:1 shape as 300x75 and is still rejected, and a lone 300x75 companion is rejected too
95
+ * (the Standard Ad Sidebar recovers that one as `AdData.loneLogo` when a Linear / NonLinear is
96
+ * the media, so it is not needed here).
97
+ * This is **narrower than iOS**, which has no size rule at all and fills its logo slot from a
98
+ * single companion — see `docs/wiki/programmatic-ads-display-ios-parity.md`. Exported for
99
+ * `external-ad.spec.ts` only; `src/index.ts` re-exports names explicitly, so this stays out of the
100
+ * package's public API.
113
101
  */
114
102
  const selectCompanions = companions => {
115
- const logo = companions.length > 1 ? companions.find(companion => companion.width === LOGO_COMPANION_WIDTH && companion.height === LOGO_COMPANION_HEIGHT) : undefined;
103
+ const logo = companions.length > 1 ? companions.find(companion => isLogoSized(companion)) : undefined;
116
104
  if (!logo) {
117
105
  return {
118
106
  logo: undefined,
@@ -125,42 +113,27 @@ const selectCompanions = companions => {
125
113
  media
126
114
  };
127
115
  };
128
- /**
129
- * Extract the first `<NonLinearAds>` creative's static-image variation. Both the creative (which
130
- * owns the `<TrackingEvents>`) and the variation (which owns the click-through) are kept —
131
- * `VASTTracker` needs both.
132
- *
133
- * `@dailymotion/vast-client` 6.4.2 emits `nonlinearClickThroughURLTemplate` (lower-case `l`),
134
- * which the `@types/vast-client` shim declares as `nonLinearClickThroughURLTemplate` — the
135
- * runtime spelling is authoritative.
136
- */
137
- const getNonLinear = ad => {
138
- var _creative$variations, _variation$nonlinearC;
139
- const creative = ad.creatives.find(item => item.type === 'nonlinear');
140
- const variation = creative === null || creative === void 0 || (_creative$variations = creative.variations) === null || _creative$variations === void 0 ? void 0 : _creative$variations[0];
141
- const imageUrl = variation === null || variation === void 0 ? void 0 : variation.staticResource;
142
- if (!creative || !variation || !imageUrl) {
143
- return undefined;
144
- }
145
- return {
146
- imageUrl,
147
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
148
- // @ts-ignore — see the runtime-vs-shim spelling note above
149
- clickUrl: (_variation$nonlinearC = variation.nonlinearClickThroughURLTemplate) !== null && _variation$nonlinearC !== void 0 ? _variation$nonlinearC : undefined,
150
- width: Number(variation.width) || undefined,
151
- height: Number(variation.height) || undefined,
152
- creative,
153
- variation
154
- };
155
- };
156
116
  /**
157
117
  * Map a parsed VAST response into our `AdData` shape. Shared by both acquisition
158
118
  * paths — the GAM/VMAP tag flow and the Prebid auction flow — so the two always
159
- * produce identical downstream data. Returns `undefined` when the response has no
160
- * ad carrying creatives.
119
+ * produce identical downstream data.
120
+ *
121
+ * Returns `undefined` for a no-ad, which is two distinct shapes: a response with no ad carrying
122
+ * creatives, and an ad whose creatives carry no RENDERABLE asset — no linear `<MediaFile>`, no
123
+ * `<NonLinear>` still and no `<Companion>` image. Both callers `filter(Boolean)` the result, so a
124
+ * no-ad leaves `ads: []` and the card keeps whatever the server configured for it.
125
+ *
126
+ * Exported for `vast-mapping.spec.ts`, which drives it over the same fixture corpus the iOS suite
127
+ * uses. `src/index.ts` re-exports names explicitly, so this stays out of the package's public API.
161
128
  */
162
129
  const buildAdData = (parsedVAST, order) => {
163
130
  var _validAd$extensions$f;
131
+ // Defensive, and unreachable through either production path: `@dailymotion/vast-client` discards
132
+ // an ad whose creatives all fail to parse (empty `<Linear>`, empty/missing `<Creatives>`, unknown
133
+ // creative element) before `parseVAST` resolves, so it never yields an Ad with zero creatives.
134
+ // Kept because it costs nothing and `buildAdData` is also reachable from tests with a hand-built
135
+ // response — see the `invalid_ad` case in `vast-mapping.spec.ts`, which pins the parser contract
136
+ // this relies on rather than pretending to exercise the guard.
164
137
  const validAd = parsedVAST.ads.find(ads => ads.creatives.length > 0);
165
138
  if (!validAd) {
166
139
  log.warn({
@@ -175,13 +148,52 @@ const buildAdData = (parsedVAST, order) => {
175
148
  const parsedExtension = parseVastExtension(extension);
176
149
  const creative = getCreative(validAd);
177
150
  const mediaFileUrl = getMediaFileUrl(creative);
178
- const companions = getCompanions(validAd);
151
+ const images = collectAssets(validAd);
152
+ const companions = images.filter(asset => asset.origin === 'companion');
153
+ const nonLinear = images.find(asset => asset.origin === 'nonlinear');
154
+ // An ad the parser KEPT whose creatives carry nothing any shell can draw is a no-ad, exactly
155
+ // like a response with no ad at all. vast-client only discards a creative it cannot parse; a
156
+ // `<NonLinear>` or `<Companion>` whose `<StaticResource>` body is empty parses fine and just
157
+ // yields no url, so the guard above cannot catch it. Without this the fabricated promotion still
158
+ // took the slot and drew the ad's TEXT — title and description over an empty media box — which
159
+ // is a billable impression for a creative that does not exist.
160
+ //
161
+ // "Renderable" is the union of the three slots the shells can fill: a linear `<MediaFile>`, a
162
+ // `<NonLinear>` still, or a `<Companion>` image. `collectAssets` already drops the imageless
163
+ // ones, so a non-empty `companions` means at least one drawable image. A tag carrying only a
164
+ // `<Mezzanine>` never reaches here — the parser drops that Linear itself — but it lands in the
165
+ // same place either way.
166
+ if (!mediaFileUrl && !nonLinear && companions.length === 0) {
167
+ log.warn({
168
+ order,
169
+ adId: validAd.id,
170
+ creatives: validAd.creatives.length
171
+ }, 'vast ad carries no renderable asset (no media file, no nonlinear, no companion) — treated as no-ad');
172
+ return undefined;
173
+ }
179
174
  const {
180
175
  logo,
181
176
  media: mediaCompanion
182
177
  } = selectCompanions(companions);
183
- const nonLinear = getNonLinear(validAd);
184
178
  const clickThrough = getClickThrough(creative) || (nonLinear === null || nonLinear === void 0 ? void 0 : nonLinear.clickUrl) || (mediaCompanion === null || mediaCompanion === void 0 ? void 0 : mediaCompanion.clickUrl);
179
+ // Mirror iOS's greedy `pool.select([hero, .cardLogo])`: the hero claims first and a claimed
180
+ // asset is gone for later roles. Web's hero precedence (`createActiveAdFromVast`) is linear
181
+ // video → pair media companion (unreachable here: a `logo` short-circuits the whole selection)
182
+ // → NonLinear still → lone companion, so a companion is only unavailable to the logo slot when
183
+ // it IS the hero, i.e. when the tag ships neither. `selectCardLogo` already filters to companion
184
+ // origins, so the NonLinear never needs excluding.
185
+ //
186
+ // Excluding `companions[0]` (which is what `mediaCompanion` is when there is no `logo`) rather
187
+ // than the largest-area asset is a deliberate difference from iOS's `areaDominant` hero: web's
188
+ // content slot draws the FIRST companion in document order, so document order is the correct
189
+ // criterion for "what the hero already claimed" on this platform.
190
+ const companionIsHero = !mediaFileUrl && !nonLinear;
191
+ const cardLogoAsset = logo ? undefined : selectCardLogo(validAd, companionIsHero ? [mediaCompanion] : []);
192
+ // Share the selected resource wrapper so drawn-companion identity remains stable.
193
+ const cardLogo = cardLogoAsset ? companions.find(item => item.variation === cardLogoAsset.variation && item.imageUrl === cardLogoAsset.imageUrl) : undefined;
194
+ // Same collected instance as `companion` (identity matters to `drawnCompanions`). A lone
195
+ // companion never satisfies the pair rule, so this can only fill a slot `logo` left empty.
196
+ const loneLogo = companions.length === 1 && !companionIsHero && isLogoSized(companions[0]) ? companions[0] : undefined;
185
197
  log.info({
186
198
  order,
187
199
  adId: validAd.id,
@@ -201,6 +213,14 @@ const buildAdData = (parsedVAST, order) => {
201
213
  size: `${mediaCompanion.width}x${mediaCompanion.height}`,
202
214
  url: mediaCompanion.imageUrl
203
215
  } : undefined,
216
+ cardLogo: cardLogo ? {
217
+ size: `${cardLogo.width}x${cardLogo.height}`,
218
+ url: cardLogo.imageUrl
219
+ } : undefined,
220
+ loneLogo: loneLogo ? {
221
+ size: `${loneLogo.width}x${loneLogo.height}`,
222
+ url: loneLogo.imageUrl
223
+ } : undefined,
204
224
  clickThrough: !!clickThrough
205
225
  }
206
226
  }, 'vast ad parsed');
@@ -210,6 +230,8 @@ const buildAdData = (parsedVAST, order) => {
210
230
  mediaFileUrl,
211
231
  companion: companions[0],
212
232
  logo,
233
+ cardLogo,
234
+ loneLogo,
213
235
  mediaCompanion,
214
236
  nonLinear,
215
237
  clickThrough,
@@ -238,8 +260,12 @@ const createRequestId = () => `sl-web-${Date.now().toString(36)}-${Math.random()
238
260
  * - `storedauctionresponse` → `imp.ext.prebid.storedauctionresponse.id` (staging mock).
239
261
  * - `test=1` → top-level `test` flag.
240
262
  * - `w` / `h` → video dimensions (default 1920×1080).
263
+ *
264
+ * The host's GDPR consent (`AdTagParams`) rides the OpenRTB 2.5 extension fields Prebid Server
265
+ * reads — `regs.ext.gdpr` and `user.ext.consent` — the same signal the `${GDPR}` /
266
+ * `${GDPR_CONSENT}` macros carry for a VAST tag. Each is sent only when the host passed it.
241
267
  */
242
- const buildAuctionRequest = url => {
268
+ const buildAuctionRequest = (url, adTagParams) => {
243
269
  var _ref, _params$get, _params$get2;
244
270
  const params = url.searchParams;
245
271
  const width = Number(params.get('w')) || 1920;
@@ -286,6 +312,20 @@ const buildAuctionRequest = url => {
286
312
  if (params.get('test') === '1') {
287
313
  request.test = 1;
288
314
  }
315
+ if ((adTagParams === null || adTagParams === void 0 ? void 0 : adTagParams.gdprApplies) !== undefined) {
316
+ request.regs = {
317
+ ext: {
318
+ gdpr: adTagParams.gdprApplies ? 1 : 0
319
+ }
320
+ };
321
+ }
322
+ if (adTagParams !== null && adTagParams !== void 0 && adTagParams.tcString) {
323
+ request.user = {
324
+ ext: {
325
+ consent: adTagParams.tcString
326
+ }
327
+ };
328
+ }
289
329
  return request;
290
330
  };
291
331
  /**
@@ -294,14 +334,14 @@ const buildAuctionRequest = url => {
294
334
  * so the winner plays first (RFC §2). A single bad bid must not drop the batch.
295
335
  */
296
336
  const fetchPrebidAds = /*#__PURE__*/function () {
297
- var _ref2 = _asyncToGenerator(function* (prebidUrl) {
337
+ var _ref2 = _asyncToGenerator(function* (prebidUrl, adTagParams) {
298
338
  var _auction$seatbid, _params$get3, _params$get4;
299
339
  const response = yield fetch(prebidUrl.toString(), {
300
340
  method: 'POST',
301
341
  headers: {
302
342
  'Content-Type': 'application/json'
303
343
  },
304
- body: JSON.stringify(buildAuctionRequest(prebidUrl))
344
+ body: JSON.stringify(buildAuctionRequest(prebidUrl, adTagParams))
305
345
  });
306
346
  if (!response.ok) {
307
347
  throw new Error(`prebid auction failed with status ${response.status}`);
@@ -344,7 +384,7 @@ const fetchPrebidAds = /*#__PURE__*/function () {
344
384
  muted
345
385
  };
346
386
  });
347
- return function fetchPrebidAds(_x) {
387
+ return function fetchPrebidAds(_x, _x2) {
348
388
  return _ref2.apply(this, arguments);
349
389
  };
350
390
  }();
@@ -365,13 +405,17 @@ const externalAd = instance => {
365
405
  return undefined;
366
406
  }
367
407
  })();
408
+ // The host's CURRENT macro values, read per request rather than baked into the store: the
409
+ // store is cached by url for the session, and consent routinely arrives after the SDK
410
+ // starts (a CMP answers asynchronously) or changes mid-session.
411
+ const adTagParams = instance.sdk.options.get().adTagParams;
368
412
  // Prebid links hit our Prebid Server auction endpoint instead of a VAST tag.
369
413
  // Run the OpenRTB auction and parse the winning bid's inline VAST (`adm`) into
370
414
  // the same `AdStore` shape the GAM/VMAP path returns. See rfcs/prebid.
371
415
  if (parsedAdUrl && isPrebidUrl(parsedAdUrl)) {
372
- return fetchPrebidAds(parsedAdUrl);
416
+ return fetchPrebidAds(parsedAdUrl, adTagParams);
373
417
  }
374
- const generateUrl = gam(adUrl);
418
+ const generateUrl = gam(adUrl, undefined, adTagParams);
375
419
  const generatedUrl = yield generateUrl();
376
420
  const noncedLink = generatedUrl === null || generatedUrl === void 0 ? void 0 : generatedUrl.url;
377
421
  if (!noncedLink) {
@@ -437,11 +481,11 @@ const externalAd = instance => {
437
481
  nonceManager
438
482
  };
439
483
  });
440
- return function fetcher(_x2) {
484
+ return function fetcher(_x3) {
441
485
  return _ref3.apply(this, arguments);
442
486
  };
443
487
  }()
444
488
  });
445
489
  };
446
490
 
447
- export { externalAd };
491
+ export { buildAdData, buildAuctionRequest, externalAd, selectCompanions };
@@ -12,6 +12,7 @@ import { eventBus } from '@streamlayer/sdk-web-interfaces/classic';
12
12
  import { createLogger } from '@streamlayer/sdk-web-logger/classic';
13
13
  import '@streamlayer/sdk-web-api/classic';
14
14
  import { VASTClient } from '@dailymotion/vast-client';
15
+ import { substituteAdTagMacros } from './ad-macros.js';
15
16
 
16
17
  const FULLBLEED_VIDEO_TEMPLATES = ['fullbleed-video', 'fullbleed-video-center', 'fullbleed-video-left'];
17
18
  /**
@@ -126,7 +127,10 @@ const externalPauseAd = instance => {
126
127
  var _options$template2, _parsedVAST$ads$find;
127
128
  // `resolveAll: true` follows every `VASTAdTagURI` wrapper chain and resolves all
128
129
  // Ads (the default only resolves the first standalone Ad and stops at the wrapper).
129
- const parsedVAST = yield vastClient.get(adUrl, {
130
+ // Pause-ad tags do not go through `gam()`, so fill the macros here — with the host's values
131
+ // as they are NOW, since this fetcher re-runs on every pause. The store key stays the
132
+ // configured url: `ExposedPauseAd` compares against it.
133
+ const parsedVAST = yield vastClient.get(substituteAdTagMacros(adUrl, instance.sdk.options.get().adTagParams), {
130
134
  resolveAll: true
131
135
  });
132
136
  const template = (_options$template2 = options.template) !== null && _options$template2 !== void 0 ? _options$template2 : 'default';
@@ -1,5 +1,7 @@
1
1
  import { asyncToGenerator as _asyncToGenerator } from '../_virtual/_rollupPluginBabelHelpers.js';
2
+ import 'core-js/modules/es.error.cause.js';
2
3
  import 'core-js/modules/es.array.iterator.js';
4
+ import 'core-js/modules/es.promise.js';
3
5
  import 'core-js/modules/web.dom-collections.iterator.js';
4
6
  import 'core-js/modules/web.url.js';
5
7
  import 'core-js/modules/web.url.to-json.js';
@@ -9,11 +11,73 @@ import 'core-js/modules/web.url-search-params.has.js';
9
11
  import 'core-js/modules/web.url-search-params.size.js';
10
12
  import { safeLocalStorage, safeSessionStorage } from '@streamlayer/sdk-web-storage/classic';
11
13
  import { v4, validate, v5 } from 'uuid';
14
+ import { substituteAdTagMacros } from './ad-macros.js';
12
15
 
13
16
  const urlIsNotGam = url => {
14
17
  return url.origin !== 'https://pubads.g.doubleclick.net';
15
18
  };
16
- const gam = (url, adContainer) => {
19
+ const palScriptUrl = 'https://imasdk.googleapis.com/pal/sdkloader/pal.js';
20
+ const getPal = () => {
21
+ // @ts-expect-error Google PAL is supplied by the host or the script loader below.
22
+ return typeof goog === 'undefined' ? undefined : goog.pal;
23
+ };
24
+ let palReady;
25
+ const ensurePal = /*#__PURE__*/function () {
26
+ var _ref = _asyncToGenerator(function* () {
27
+ if (getPal()) {
28
+ return;
29
+ }
30
+ if (!palReady) {
31
+ palReady = new Promise((resolve, reject) => {
32
+ if (typeof document === 'undefined') {
33
+ reject(new Error('Google PAL requires a browser document'));
34
+ return;
35
+ }
36
+ const existing = document.querySelector(`script[src="${palScriptUrl}"]`);
37
+ const script = existing || document.createElement('script');
38
+ const finish = error => {
39
+ clearTimeout(timeout);
40
+ script.removeEventListener('load', onLoad);
41
+ script.removeEventListener('error', onError);
42
+ if (error) {
43
+ if (!existing) {
44
+ script.remove();
45
+ }
46
+ reject(error);
47
+ } else {
48
+ resolve();
49
+ }
50
+ };
51
+ const onLoad = () => finish(getPal() ? undefined : new Error('Google PAL did not initialize'));
52
+ const onError = () => finish(new Error('Google PAL failed to load'));
53
+ const timeout = setTimeout(() => finish(new Error('Google PAL loading timed out')), 10000);
54
+ script.addEventListener('load', onLoad);
55
+ script.addEventListener('error', onError);
56
+ if (!existing) {
57
+ script.async = true;
58
+ script.src = palScriptUrl;
59
+ document.head.appendChild(script);
60
+ }
61
+ }).catch(error => {
62
+ palReady = undefined;
63
+ throw error;
64
+ });
65
+ }
66
+ yield palReady;
67
+ palReady = undefined;
68
+ });
69
+ return function ensurePal() {
70
+ return _ref.apply(this, arguments);
71
+ };
72
+ }();
73
+ const gam = (rawUrl, adContainer, adTagParams) => {
74
+ // Every VAST/VMAP tag funnels through here, so this is where the macros get filled — for GAM and
75
+ // for any other vendor alike. Studio stores a programmatic tag verbatim, so an unexpanded
76
+ // `[timestamp]` / `[clickmacro]` / `${GDPR}` would otherwise reach the ad server literal, and a
77
+ // literal macro comes back copied into the vendor's click-through. `adTagParams` are the host's
78
+ // consent and click values, read by the caller at request time. The GAM `[placeholder]` tokens
79
+ // are left alone here and filled per parameter below.
80
+ const url = substituteAdTagMacros(rawUrl, adTagParams);
17
81
  const parsedUrl = (() => {
18
82
  try {
19
83
  const parsedUrl = new URL(url);
@@ -34,22 +98,21 @@ const gam = (url, adContainer) => {
34
98
  };
35
99
  });
36
100
  }
37
- // @ts-expect-error goog is injected by the externally-loaded Google PAL SDK
38
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
- const pal = typeof goog === 'undefined' ? undefined : goog.pal;
40
- if (!pal || !parsedUrl) {
101
+ if (!parsedUrl) {
41
102
  // eslint-disable-next-line require-await
42
103
  return /*#__PURE__*/_asyncToGenerator(function* () {
43
104
  return undefined;
44
105
  });
45
106
  }
46
- const consentSettings = new pal.ConsentSettings();
47
- consentSettings.allowStorage = false;
48
- const nonceLoader = new pal.NonceLoader(consentSettings);
49
107
  const generateUrl = /*#__PURE__*/function () {
50
- var _ref3 = _asyncToGenerator(function* () {
108
+ var _ref4 = _asyncToGenerator(function* () {
51
109
  try {
52
110
  var _adContainer$clientHe, _adContainer$clientWi;
111
+ yield ensurePal();
112
+ const pal = getPal();
113
+ const consentSettings = new pal.ConsentSettings();
114
+ consentSettings.allowStorage = false;
115
+ const nonceLoader = new pal.NonceLoader(consentSettings);
53
116
  const parsedUrl = new URL(url);
54
117
  const request = new pal.NonceRequest();
55
118
  let deviceId = safeLocalStorage().getItem('sl-device-id') || v4();
@@ -99,7 +162,7 @@ const gam = (url, adContainer) => {
99
162
  }
100
163
  });
101
164
  return function generateUrl() {
102
- return _ref3.apply(this, arguments);
165
+ return _ref4.apply(this, arguments);
103
166
  };
104
167
  }();
105
168
  return generateUrl;
@@ -4,6 +4,10 @@ import 'core-js/modules/esnext.iterator.find.js';
4
4
  import 'core-js/modules/esnext.iterator.map.js';
5
5
  import { AdType } from '@streamlayer/sl-eslib/interactive/interactive.common_pb';
6
6
 
7
+ /**
8
+ * Extension root element name → the placement it names. Only `DoubleBox` is defined by the
9
+ * StreamLayer extension schema today; anything else is a vendor extension we do not interpret.
10
+ */
7
11
  const MAP_EXTENSION_NAME_AD_TYPE = {
8
12
  DoubleBox: AdType.SIDE_BY_SIDE
9
13
  };
@@ -43,7 +47,7 @@ function parseVastExtension(root) {
43
47
  };
44
48
  return {
45
49
  adName,
46
- adType: MAP_EXTENSION_NAME_AD_TYPE[root.name] || AdType.SIDE_BY_SIDE,
50
+ adType: MAP_EXTENSION_NAME_AD_TYPE[root.name],
47
51
  landscapeBackground: (_backgrounds$find = backgrounds.find(b => b.orientation === 'landscape')) === null || _backgrounds$find === void 0 ? void 0 : _backgrounds$find.url,
48
52
  portraitBackground: (_backgrounds$find2 = backgrounds.find(b => b.orientation === 'portrait')) === null || _backgrounds$find2 === void 0 ? void 0 : _backgrounds$find2.url,
49
53
  sponsorLogo: (_logos$find = logos.find(l => l.role === 'header')) === null || _logos$find === void 0 ? void 0 : _logos$find.url,
@@ -0,0 +1,197 @@
1
+ import 'core-js/modules/es.array.push.js';
2
+ import 'core-js/modules/esnext.iterator.constructor.js';
3
+ import 'core-js/modules/esnext.iterator.filter.js';
4
+ import 'core-js/modules/esnext.iterator.map.js';
5
+
6
+ /**
7
+ * Sponsor-logo slot selection for a **feature** ad unit (Insight, Prediction and Trivia today;
8
+ * Poll is the same shape and is a separate ticket).
9
+ *
10
+ * Deliberately NOT `selectCompanions` (`./external-ad.ts`). That rule is the *display*-unit rule:
11
+ * it promotes a companion to the logo slot only when the tag ships an exact 300x75 `<Companion>`
12
+ * next to at least one other. A feature ad-unit tag is typically a **single** creative — often a
13
+ * lone NonLinear `<StaticResource>` — where the ad IS the logo, which is exactly the shape that
14
+ * rule returns nothing for. `selectCompanions` stays untouched: SC-335's three shipped units
15
+ * depend on its exact behaviour and impression counts are billing-visible.
16
+ *
17
+ * Mirrors iOS `SLRVASTSlotRole.logo` + `SLRVASTAssetPool`
18
+ * (`StreamLayerSDK/Frameworks/SLRGAMFramework/Sources/VASTSlotFiller/`):
19
+ *
20
+ * - **Images only.** A video-only tag yields no logo. `<Icon>` is never projected — icons hang off
21
+ * the `<Linear>` creative, which this selector does not look at. A `<StaticResource>` the tag
22
+ * declares as a non-image `creativeType` is skipped too, so a companion that lists a script
23
+ * before its image still resolves to the image (see `isImageResource`).
24
+ * - **Origin-agnostic.** A lone NonLinear `<StaticResource>` is as valid as a `<Companion>`.
25
+ * - **Aspect-dominant** toward 1:1: the smallest `|log(assetAspect) - log(1)|` wins, ties broken by
26
+ * larger area, then by higher pixel density. Aspect is a *preference, never a filter* — the only
27
+ * hard constraint is the asset kind, so a badly shaped asset still wins if it is the only one.
28
+ * - Assets whose intrinsic size the tag never declared rank last, but still beat nothing.
29
+ *
30
+ * The module also hosts iOS's second logo role, `SLRVASTSlotRole.cardLogo` (`selectCardLogo`) —
31
+ * the same ranking narrowed to `<Companion>` origins, for the hero-card display units.
32
+ */
33
+ // eslint-disable-next-line @typescript-eslint/triple-slash-reference
34
+ /// <reference path="../module.d.ts" />
35
+ /** `width`/`height` are XML attributes (strings at runtime); 0 and NaN both mean "not declared". */
36
+ const toSize = value => {
37
+ const size = Number(value);
38
+ return size > 0 ? size : undefined;
39
+ };
40
+ /**
41
+ * VAST 4 `<CompanionAds pxratio>`; `@dailymotion/vast-client` defaults it to `'1'` and the
42
+ * `@types/vast-client` shim does not declare it, hence the loose read.
43
+ */
44
+ const toPxRatio = variation => {
45
+ return Number(variation.pxratio) || 1;
46
+ };
47
+ /**
48
+ * `<StaticResource creativeType>`. The slot draws an `<img>`, so a resource the tag declares as
49
+ * anything else — the `application/x-shockwave-flash` or `application/javascript` creative a
50
+ * companion may list beside its image — must never fill it: it would swap the proto logo for a
51
+ * broken image AND still fire an impression for something nobody ever saw.
52
+ *
53
+ * An ABSENT type is not a rejection. VAST requires the attribute but real GAM image companions
54
+ * routinely omit it, and this module's only hard constraint is the asset kind, so an undeclared
55
+ * type stays a candidate. `@dailymotion/vast-client` writes `0` for a missing NonLinear
56
+ * `creativeType` and `null` for a missing companion one; both are falsy, hence the single guard.
57
+ */
58
+ const isImageResource = creativeType => {
59
+ if (!creativeType) {
60
+ return true;
61
+ }
62
+ return String(creativeType).toLowerCase().indexOf('image/') === 0;
63
+ };
64
+ /**
65
+ * The first resource a `<Companion>` declares as an image (or leaves undeclared). NOT
66
+ * `staticResources[0]`: the order in the tag is the advertiser's, and a non-image resource listed
67
+ * first must not hide a usable image behind it.
68
+ */
69
+ const pickCompanionImage = variation => {
70
+ const resources = variation.staticResources || [];
71
+ for (let i = 0; i < resources.length; i++) {
72
+ const resource = resources[i];
73
+ if (resource && resource.url && isImageResource(resource.creativeType)) {
74
+ return resource.url;
75
+ }
76
+ }
77
+ return undefined;
78
+ };
79
+ /** Distance from square on a log scale, so 2:1 and 1:2 are equally far from 1:1. */
80
+ const aspectDistance = asset => {
81
+ if (!asset.width || !asset.height) {
82
+ // Unknown intrinsic size — rank last, but still a candidate.
83
+ return Infinity;
84
+ }
85
+ return Math.abs(Math.log(asset.width / asset.height));
86
+ };
87
+ const collectAssetsFrom = ad => {
88
+ const assets = [];
89
+ for (let i = 0; i < ad.creatives.length; i++) {
90
+ const creative = ad.creatives[i];
91
+ if (creative.type === 'companion') {
92
+ const companionCreative = creative;
93
+ const variations = companionCreative.variations || [];
94
+ for (let j = 0; j < variations.length; j++) {
95
+ const variation = variations[j];
96
+ const imageUrl = variation && pickCompanionImage(variation);
97
+ if (imageUrl) {
98
+ var _variation$companionC;
99
+ assets.push({
100
+ imageUrl,
101
+ clickUrl: (_variation$companionC = variation.companionClickThroughURLTemplate) !== null && _variation$companionC !== void 0 ? _variation$companionC : undefined,
102
+ width: toSize(variation.width),
103
+ height: toSize(variation.height),
104
+ origin: 'companion',
105
+ creative: companionCreative,
106
+ variation
107
+ });
108
+ }
109
+ }
110
+ continue;
111
+ }
112
+ if (creative.type === 'nonlinear') {
113
+ const nonLinearCreative = creative;
114
+ const variations = nonLinearCreative.variations || [];
115
+ for (let j = 0; j < variations.length; j++) {
116
+ const variation = variations[j];
117
+ // `type` is where the parser puts a `<NonLinear>`'s `creativeType`, and it is written last
118
+ // from the `<StaticResource>` element, so it describes `staticResource` whenever that is
119
+ // set. An HTML- or iframe-typed variation carries no `staticResource` and drops out here.
120
+ const imageUrl = variation && isImageResource(variation.type) ? variation.staticResource : undefined;
121
+ if (imageUrl) {
122
+ var _variation$nonlinearC;
123
+ assets.push({
124
+ imageUrl,
125
+ // 6.4.2 emits `nonlinearClickThroughURLTemplate` (lower-case `l`); the shim declares
126
+ // the upper-case spelling. The runtime one is authoritative — see `getNonLinear`.
127
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
128
+ // @ts-ignore
129
+ clickUrl: (_variation$nonlinearC = variation.nonlinearClickThroughURLTemplate) !== null && _variation$nonlinearC !== void 0 ? _variation$nonlinearC : undefined,
130
+ width: toSize(variation.width),
131
+ height: toSize(variation.height),
132
+ origin: 'nonlinear',
133
+ creative: nonLinearCreative,
134
+ variation
135
+ });
136
+ }
137
+ }
138
+ }
139
+ }
140
+ return assets;
141
+ };
142
+ /**
143
+ * The shared ranking both roles use: aspect distance from 1:1, ties broken by larger area, then by
144
+ * higher pixel density. Aspect is a preference, never a filter — the best of a bad set still wins.
145
+ */
146
+ const rankBest = assets => {
147
+ if (!assets.length) {
148
+ return undefined;
149
+ }
150
+ let best = assets[0];
151
+ let bestDistance = aspectDistance(best);
152
+ let bestArea = (best.width || 0) * (best.height || 0);
153
+ let bestPxRatio = toPxRatio(best.variation);
154
+ for (let i = 1; i < assets.length; i++) {
155
+ const asset = assets[i];
156
+ const distance = aspectDistance(asset);
157
+ const area = (asset.width || 0) * (asset.height || 0);
158
+ const pxRatio = toPxRatio(asset.variation);
159
+ const wins = distance !== bestDistance ? distance < bestDistance : area !== bestArea ? area > bestArea : pxRatio > bestPxRatio;
160
+ if (wins) {
161
+ best = asset;
162
+ bestDistance = distance;
163
+ bestArea = area;
164
+ bestPxRatio = pxRatio;
165
+ }
166
+ }
167
+ return best;
168
+ };
169
+ /**
170
+ * Pick the image a parsed VAST ad offers for a sponsor-logo slot, or `undefined` when it offers
171
+ * none (a video-only tag, or an empty response). A caller that gets `undefined` must fall back to
172
+ * its own proto/Studio logo rather than suppressing the card.
173
+ */
174
+ const collectAssets = ad => collectAssetsFrom(ad);
175
+ const selectSponsorLogo = ad => rankBest(collectAssetsFrom(ad));
176
+ /**
177
+ * iOS `SLRVASTSlotRole.cardLogo` — the sponsor logo of a **hero card** (1:1 / 2:1 / Picture in
178
+ * Picture / Side-by-Side / Sponsor Frame). Same 1:1 aspect-dominant ranking as `.logo`, plus the
179
+ * one constraint that distinguishes the two roles:
180
+ *
181
+ * - **`requiredOrigin: .companion`.** A NonLinear image is a hero creative, never a logo. iOS's
182
+ * reason: a leftover hero-type asset — a second `<NonLinear>` overlay, or a stray image still in
183
+ * the pool after the hero role claimed its own — must never leak into the card's logo slot,
184
+ * where it would render at logo size as an unreadable smudge of the advertiser's key art.
185
+ * `nonlinear_full` is the fixture that pins it: `.logo` takes the 500x500 NonLinear, `.cardLogo`
186
+ * must take the 200x200 `<Companion>`.
187
+ * - **`exclude`** mirrors iOS's **greedy** `pool.select([hero, .cardLogo])`: roles resolve in array
188
+ * order and an asset a role claims is removed from the pool for every later role. We have no
189
+ * pool object, so the caller passes the assets the hero already claimed and they are filtered
190
+ * out here by `imageUrl`.
191
+ */
192
+ const selectCardLogo = (ad, exclude) => {
193
+ const claimed = (exclude || []).map(item => item === null || item === void 0 ? void 0 : item.imageUrl).filter(url => !!url);
194
+ return rankBest(collectAssetsFrom(ad).filter(asset => asset.origin === 'companion' && claimed.indexOf(asset.imageUrl) === -1));
195
+ };
196
+
197
+ export { collectAssets, selectCardLogo, selectSponsorLogo };