@streamlayer/feature-advertisement 1.1.1-rc.1345 → 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.
- package/lib/advertisement.js +24 -9
- package/lib/cjs/advertisement.js +24 -9
- package/lib/cjs/index.d.ts +2 -0
- package/lib/cjs/index.js +2 -0
- package/lib/cjs/units/ad-macros.d.ts +7 -0
- package/lib/cjs/units/ad-macros.js +91 -0
- package/lib/cjs/units/external-ad.d.ts +69 -0
- package/lib/cjs/units/external-ad.js +93 -62
- package/lib/cjs/units/external-pause-ad.js +7 -1
- package/lib/cjs/units/gam.d.ts +11 -1
- package/lib/cjs/units/gam.js +65 -17
- package/lib/cjs/units/parse-vast-extensions.d.ts +10 -1
- package/lib/cjs/units/parse-vast-extensions.js +5 -1
- package/lib/cjs/units/sponsor-logo.d.ts +78 -0
- package/lib/cjs/units/sponsor-logo.js +194 -0
- package/lib/cjs/units/ssai-detector.js +27 -11
- package/lib/cjs/units/ssai-log.d.ts +16 -0
- package/lib/cjs/units/ssai-log.js +23 -0
- package/lib/cjs/units/ssai-providers.js +48 -11
- package/lib/classic/advertisement.js +34 -17
- package/lib/classic/cjs/advertisement.js +34 -17
- package/lib/classic/cjs/index.js +2 -0
- package/lib/classic/cjs/units/ad-macros.js +115 -0
- package/lib/classic/cjs/units/external-ad.js +111 -64
- package/lib/classic/cjs/units/external-pause-ad.js +5 -1
- package/lib/classic/cjs/units/gam.js +73 -10
- package/lib/classic/cjs/units/parse-vast-extensions.js +5 -1
- package/lib/classic/cjs/units/sponsor-logo.js +201 -0
- package/lib/classic/cjs/units/ssai-detector.js +33 -8
- package/lib/classic/cjs/units/ssai-log.js +23 -0
- package/lib/classic/cjs/units/ssai-providers.js +86 -24
- package/lib/classic/index.js +1 -0
- package/lib/classic/units/ad-macros.js +113 -0
- package/lib/classic/units/external-ad.js +109 -65
- package/lib/classic/units/external-pause-ad.js +5 -1
- package/lib/classic/units/gam.js +73 -10
- package/lib/classic/units/parse-vast-extensions.js +5 -1
- package/lib/classic/units/sponsor-logo.js +197 -0
- package/lib/classic/units/ssai-detector.js +33 -8
- package/lib/classic/units/ssai-log.js +21 -0
- package/lib/classic/units/ssai-providers.js +86 -24
- package/lib/index.d.ts +2 -0
- package/lib/index.js +1 -0
- package/lib/units/ad-macros.d.ts +7 -0
- package/lib/units/ad-macros.js +87 -0
- package/lib/units/external-ad.d.ts +69 -0
- package/lib/units/external-ad.js +92 -64
- package/lib/units/external-pause-ad.js +7 -1
- package/lib/units/gam.d.ts +11 -1
- package/lib/units/gam.js +65 -17
- package/lib/units/parse-vast-extensions.d.ts +10 -1
- package/lib/units/parse-vast-extensions.js +5 -1
- package/lib/units/sponsor-logo.d.ts +78 -0
- package/lib/units/sponsor-logo.js +188 -0
- package/lib/units/ssai-detector.js +27 -11
- package/lib/units/ssai-log.d.ts +16 -0
- package/lib/units/ssai-log.js +18 -0
- package/lib/units/ssai-providers.js +48 -11
- package/lib/vast-client.d.ts +9 -0
- 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
|
|
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
|
|
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.
|
|
160
|
-
*
|
|
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
|
|
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(
|
|
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
|
-
|
|
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';
|
package/lib/classic/units/gam.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
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]
|
|
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 };
|