@streamlayer/feature-advertisement 1.1.1-rc.1350 → 1.1.1-rc.3.pda7fd93b

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