@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
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sponsor-logo slot selection for a **feature** ad unit (Insight, Prediction and Trivia today;
|
|
3
|
+
* Poll is the same shape and is a separate ticket).
|
|
4
|
+
*
|
|
5
|
+
* Deliberately NOT `selectCompanions` (`./external-ad.ts`). That rule is the *display*-unit rule:
|
|
6
|
+
* it promotes a companion to the logo slot only when the tag ships an exact 300x75 `<Companion>`
|
|
7
|
+
* next to at least one other. A feature ad-unit tag is typically a **single** creative — often a
|
|
8
|
+
* lone NonLinear `<StaticResource>` — where the ad IS the logo, which is exactly the shape that
|
|
9
|
+
* rule returns nothing for. `selectCompanions` stays untouched: SC-335's three shipped units
|
|
10
|
+
* depend on its exact behaviour and impression counts are billing-visible.
|
|
11
|
+
*
|
|
12
|
+
* Mirrors iOS `SLRVASTSlotRole.logo` + `SLRVASTAssetPool`
|
|
13
|
+
* (`StreamLayerSDK/Frameworks/SLRGAMFramework/Sources/VASTSlotFiller/`):
|
|
14
|
+
*
|
|
15
|
+
* - **Images only.** A video-only tag yields no logo. `<Icon>` is never projected — icons hang off
|
|
16
|
+
* the `<Linear>` creative, which this selector does not look at. A `<StaticResource>` the tag
|
|
17
|
+
* declares as a non-image `creativeType` is skipped too, so a companion that lists a script
|
|
18
|
+
* before its image still resolves to the image (see `isImageResource`).
|
|
19
|
+
* - **Origin-agnostic.** A lone NonLinear `<StaticResource>` is as valid as a `<Companion>`.
|
|
20
|
+
* - **Aspect-dominant** toward 1:1: the smallest `|log(assetAspect) - log(1)|` wins, ties broken by
|
|
21
|
+
* larger area, then by higher pixel density. Aspect is a *preference, never a filter* — the only
|
|
22
|
+
* hard constraint is the asset kind, so a badly shaped asset still wins if it is the only one.
|
|
23
|
+
* - Assets whose intrinsic size the tag never declared rank last, but still beat nothing.
|
|
24
|
+
*
|
|
25
|
+
* The module also hosts iOS's second logo role, `SLRVASTSlotRole.cardLogo` (`selectCardLogo`) —
|
|
26
|
+
* the same ranking narrowed to `<Companion>` origins, for the hero-card display units.
|
|
27
|
+
*/
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
|
29
|
+
/// <reference path="../module.d.ts" />
|
|
30
|
+
/** `width`/`height` are XML attributes (strings at runtime); 0 and NaN both mean "not declared". */
|
|
31
|
+
const toSize = (value) => {
|
|
32
|
+
const size = Number(value);
|
|
33
|
+
return size > 0 ? size : undefined;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* VAST 4 `<CompanionAds pxratio>`; `@dailymotion/vast-client` defaults it to `'1'` and the
|
|
37
|
+
* `@types/vast-client` shim does not declare it, hence the loose read.
|
|
38
|
+
*/
|
|
39
|
+
const toPxRatio = (variation) => {
|
|
40
|
+
return Number(variation.pxratio) || 1;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* `<StaticResource creativeType>`. The slot draws an `<img>`, so a resource the tag declares as
|
|
44
|
+
* anything else — the `application/x-shockwave-flash` or `application/javascript` creative a
|
|
45
|
+
* companion may list beside its image — must never fill it: it would swap the proto logo for a
|
|
46
|
+
* broken image AND still fire an impression for something nobody ever saw.
|
|
47
|
+
*
|
|
48
|
+
* An ABSENT type is not a rejection. VAST requires the attribute but real GAM image companions
|
|
49
|
+
* routinely omit it, and this module's only hard constraint is the asset kind, so an undeclared
|
|
50
|
+
* type stays a candidate. `@dailymotion/vast-client` writes `0` for a missing NonLinear
|
|
51
|
+
* `creativeType` and `null` for a missing companion one; both are falsy, hence the single guard.
|
|
52
|
+
*/
|
|
53
|
+
const isImageResource = (creativeType) => {
|
|
54
|
+
if (!creativeType) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return String(creativeType).toLowerCase().indexOf('image/') === 0;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* The first resource a `<Companion>` declares as an image (or leaves undeclared). NOT
|
|
61
|
+
* `staticResources[0]`: the order in the tag is the advertiser's, and a non-image resource listed
|
|
62
|
+
* first must not hide a usable image behind it.
|
|
63
|
+
*/
|
|
64
|
+
const pickCompanionImage = (variation) => {
|
|
65
|
+
const resources = variation.staticResources || [];
|
|
66
|
+
for (let i = 0; i < resources.length; i++) {
|
|
67
|
+
const resource = resources[i];
|
|
68
|
+
if (resource && resource.url && isImageResource(resource.creativeType)) {
|
|
69
|
+
return resource.url;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
};
|
|
74
|
+
/** Distance from square on a log scale, so 2:1 and 1:2 are equally far from 1:1. */
|
|
75
|
+
const aspectDistance = (asset) => {
|
|
76
|
+
if (!asset.width || !asset.height) {
|
|
77
|
+
// Unknown intrinsic size — rank last, but still a candidate.
|
|
78
|
+
return Infinity;
|
|
79
|
+
}
|
|
80
|
+
return Math.abs(Math.log(asset.width / asset.height));
|
|
81
|
+
};
|
|
82
|
+
const collectAssetsFrom = (ad) => {
|
|
83
|
+
const assets = [];
|
|
84
|
+
for (let i = 0; i < ad.creatives.length; i++) {
|
|
85
|
+
const creative = ad.creatives[i];
|
|
86
|
+
if (creative.type === 'companion') {
|
|
87
|
+
const companionCreative = creative;
|
|
88
|
+
const variations = companionCreative.variations || [];
|
|
89
|
+
for (let j = 0; j < variations.length; j++) {
|
|
90
|
+
const variation = variations[j];
|
|
91
|
+
const imageUrl = variation && pickCompanionImage(variation);
|
|
92
|
+
if (imageUrl) {
|
|
93
|
+
assets.push({
|
|
94
|
+
imageUrl,
|
|
95
|
+
clickUrl: variation.companionClickThroughURLTemplate ?? undefined,
|
|
96
|
+
width: toSize(variation.width),
|
|
97
|
+
height: toSize(variation.height),
|
|
98
|
+
origin: 'companion',
|
|
99
|
+
creative: companionCreative,
|
|
100
|
+
variation,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (creative.type === 'nonlinear') {
|
|
107
|
+
const nonLinearCreative = creative;
|
|
108
|
+
const variations = nonLinearCreative.variations || [];
|
|
109
|
+
for (let j = 0; j < variations.length; j++) {
|
|
110
|
+
const variation = variations[j];
|
|
111
|
+
// `type` is where the parser puts a `<NonLinear>`'s `creativeType`, and it is written last
|
|
112
|
+
// from the `<StaticResource>` element, so it describes `staticResource` whenever that is
|
|
113
|
+
// set. An HTML- or iframe-typed variation carries no `staticResource` and drops out here.
|
|
114
|
+
const imageUrl = variation && isImageResource(variation.type) ? variation.staticResource : undefined;
|
|
115
|
+
if (imageUrl) {
|
|
116
|
+
assets.push({
|
|
117
|
+
imageUrl,
|
|
118
|
+
// 6.4.2 emits `nonlinearClickThroughURLTemplate` (lower-case `l`); the shim declares
|
|
119
|
+
// the upper-case spelling. The runtime one is authoritative — see `getNonLinear`.
|
|
120
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
121
|
+
// @ts-ignore
|
|
122
|
+
clickUrl: variation.nonlinearClickThroughURLTemplate ?? undefined,
|
|
123
|
+
width: toSize(variation.width),
|
|
124
|
+
height: toSize(variation.height),
|
|
125
|
+
origin: 'nonlinear',
|
|
126
|
+
creative: nonLinearCreative,
|
|
127
|
+
variation,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return assets;
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* The shared ranking both roles use: aspect distance from 1:1, ties broken by larger area, then by
|
|
137
|
+
* higher pixel density. Aspect is a preference, never a filter — the best of a bad set still wins.
|
|
138
|
+
*/
|
|
139
|
+
const rankBest = (assets) => {
|
|
140
|
+
if (!assets.length) {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
let best = assets[0];
|
|
144
|
+
let bestDistance = aspectDistance(best);
|
|
145
|
+
let bestArea = (best.width || 0) * (best.height || 0);
|
|
146
|
+
let bestPxRatio = toPxRatio(best.variation);
|
|
147
|
+
for (let i = 1; i < assets.length; i++) {
|
|
148
|
+
const asset = assets[i];
|
|
149
|
+
const distance = aspectDistance(asset);
|
|
150
|
+
const area = (asset.width || 0) * (asset.height || 0);
|
|
151
|
+
const pxRatio = toPxRatio(asset.variation);
|
|
152
|
+
const wins = distance !== bestDistance ? distance < bestDistance : area !== bestArea ? area > bestArea : pxRatio > bestPxRatio;
|
|
153
|
+
if (wins) {
|
|
154
|
+
best = asset;
|
|
155
|
+
bestDistance = distance;
|
|
156
|
+
bestArea = area;
|
|
157
|
+
bestPxRatio = pxRatio;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return best;
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Pick the image a parsed VAST ad offers for a sponsor-logo slot, or `undefined` when it offers
|
|
164
|
+
* none (a video-only tag, or an empty response). A caller that gets `undefined` must fall back to
|
|
165
|
+
* its own proto/Studio logo rather than suppressing the card.
|
|
166
|
+
*/
|
|
167
|
+
export const collectAssets = (ad) => collectAssetsFrom(ad);
|
|
168
|
+
export const selectSponsorLogo = (ad) => rankBest(collectAssetsFrom(ad));
|
|
169
|
+
/**
|
|
170
|
+
* iOS `SLRVASTSlotRole.cardLogo` — the sponsor logo of a **hero card** (1:1 / 2:1 / Picture in
|
|
171
|
+
* Picture / Side-by-Side / Sponsor Frame). Same 1:1 aspect-dominant ranking as `.logo`, plus the
|
|
172
|
+
* one constraint that distinguishes the two roles:
|
|
173
|
+
*
|
|
174
|
+
* - **`requiredOrigin: .companion`.** A NonLinear image is a hero creative, never a logo. iOS's
|
|
175
|
+
* reason: a leftover hero-type asset — a second `<NonLinear>` overlay, or a stray image still in
|
|
176
|
+
* the pool after the hero role claimed its own — must never leak into the card's logo slot,
|
|
177
|
+
* where it would render at logo size as an unreadable smudge of the advertiser's key art.
|
|
178
|
+
* `nonlinear_full` is the fixture that pins it: `.logo` takes the 500x500 NonLinear, `.cardLogo`
|
|
179
|
+
* must take the 200x200 `<Companion>`.
|
|
180
|
+
* - **`exclude`** mirrors iOS's **greedy** `pool.select([hero, .cardLogo])`: roles resolve in array
|
|
181
|
+
* order and an asset a role claims is removed from the pool for every later role. We have no
|
|
182
|
+
* pool object, so the caller passes the assets the hero already claimed and they are filtered
|
|
183
|
+
* out here by `imageUrl`.
|
|
184
|
+
*/
|
|
185
|
+
export const selectCardLogo = (ad, exclude) => {
|
|
186
|
+
const claimed = (exclude || []).map((item) => item?.imageUrl).filter((url) => !!url);
|
|
187
|
+
return rankBest(collectAssetsFrom(ad).filter((asset) => asset.origin === 'companion' && claimed.indexOf(asset.imageUrl) === -1));
|
|
188
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createLogger } from '@streamlayer/sdk-web-logger';
|
|
2
|
+
import { ssaiq } from './ssai-log.js';
|
|
2
3
|
const log = createLogger('ssai:detector');
|
|
3
4
|
const RANGE_EXPIRY_MS = 10 * 60 * 1000;
|
|
4
5
|
const ACTIVE_POLL_MS = 250;
|
|
@@ -488,16 +489,32 @@ export const createSsaiDetector = (options) => {
|
|
|
488
489
|
: videoRef && manifestUrl
|
|
489
490
|
? 'manifest-poll'
|
|
490
491
|
: 'none';
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
492
|
+
/**
|
|
493
|
+
* The `[label]` a detector event's log line opens with — what KIND of thing
|
|
494
|
+
* happened, for a reader scanning a window: a manifest tag came in (`hls`), a
|
|
495
|
+
* break edge fired (`break`), or playback crossed a tracking marker (`beacon`).
|
|
496
|
+
*/
|
|
497
|
+
const eventLabel = (type) => {
|
|
498
|
+
if (type.indexOf('progress:') === 0)
|
|
499
|
+
return 'beacon';
|
|
500
|
+
if (type.indexOf('discovered') !== -1 || type === 'range-dropped')
|
|
501
|
+
return 'hls';
|
|
502
|
+
return 'break';
|
|
503
|
+
};
|
|
504
|
+
/**
|
|
505
|
+
* One call per detector event, feeding both consumers at once: the capped
|
|
506
|
+
* ring buffer behind `getDebugSnapshot()` and the `ssaiq` narrative logger.
|
|
507
|
+
* `data` is whatever the detector already holds for the event — the parsed
|
|
508
|
+
* range, marker or break window — so a log reader sees exactly what the
|
|
509
|
+
* detector acted on, with no second parse of the manifest for its benefit.
|
|
510
|
+
*/
|
|
511
|
+
const recordDebugEvent = (type, detail, programMs, data) => {
|
|
512
|
+
const at = programMs !== undefined ? programMs : currentProgramTimeMs();
|
|
513
|
+
debugEvents.push({ wallMs: Date.now(), programMs: at, type, detail });
|
|
498
514
|
if (debugEvents.length > DEBUG_EVENTS_MAX) {
|
|
499
515
|
debugEvents.splice(0, debugEvents.length - DEBUG_EVENTS_MAX);
|
|
500
516
|
}
|
|
517
|
+
ssaiq.info({ ...data, programMs: at }, `[${eventLabel(type)}] ${type}${detail ? ` ${detail}` : ''}`);
|
|
501
518
|
};
|
|
502
519
|
const getVideo = () => player?.media ?? video ?? null;
|
|
503
520
|
// Memoized view over the three marker maps. `updateActiveAd` polls 4×/s and
|
|
@@ -547,7 +564,6 @@ export const createSsaiDetector = (options) => {
|
|
|
547
564
|
}
|
|
548
565
|
seenDroppedKeys.add(entry.key);
|
|
549
566
|
recordDebugEvent('range-dropped', entry.detail);
|
|
550
|
-
log.warn({ detail: entry.detail }, 'ssai DATERANGE dropped');
|
|
551
567
|
}
|
|
552
568
|
continue;
|
|
553
569
|
}
|
|
@@ -555,7 +571,7 @@ export const createSsaiDetector = (options) => {
|
|
|
555
571
|
const existing = rangesById.get(entry.range.id);
|
|
556
572
|
if (!existing) {
|
|
557
573
|
changed = true;
|
|
558
|
-
recordDebugEvent('range-discovered', `${entry.range.creativeId} ${debugClock(entry.range.startMs)}+${entry.range.durationSec}s
|
|
574
|
+
recordDebugEvent('range-discovered', `${entry.range.creativeId} ${debugClock(entry.range.startMs)}+${entry.range.durationSec}s`, undefined, { range: entry.range });
|
|
559
575
|
notifyAdDiscovered(entry.range.creativeId);
|
|
560
576
|
}
|
|
561
577
|
else if (existing.startMs !== entry.range.startMs || existing.endMs !== entry.range.endMs) {
|
|
@@ -566,7 +582,7 @@ export const createSsaiDetector = (options) => {
|
|
|
566
582
|
else if (entry.kind === 'marker') {
|
|
567
583
|
if (!seenMarkerAdIds.has(entry.marker.adId)) {
|
|
568
584
|
seenMarkerAdIds.add(entry.marker.adId);
|
|
569
|
-
recordDebugEvent('marker-ad-discovered', `${entry.marker.adId} ${entry.marker.event}@${debugClock(entry.marker.timeMs)}
|
|
585
|
+
recordDebugEvent('marker-ad-discovered', `${entry.marker.adId} ${entry.marker.event}@${debugClock(entry.marker.timeMs)}`, undefined, { marker: entry.marker });
|
|
570
586
|
notifyAdDiscovered(entry.marker.adId);
|
|
571
587
|
}
|
|
572
588
|
const existing = markersById.get(entry.marker.id);
|
|
@@ -579,7 +595,7 @@ export const createSsaiDetector = (options) => {
|
|
|
579
595
|
const existing = adBreaksById.get(entry.window.id);
|
|
580
596
|
if (!existing) {
|
|
581
597
|
changed = true;
|
|
582
|
-
recordDebugEvent('adbreak-discovered', `${debugClock(entry.window.startMs)}-${debugClock(entry.window.endMs)}
|
|
598
|
+
recordDebugEvent('adbreak-discovered', `${debugClock(entry.window.startMs)}-${debugClock(entry.window.endMs)}`, undefined, { window: entry.window });
|
|
583
599
|
}
|
|
584
600
|
else if (existing.startMs !== entry.window.startMs || existing.endMs !== entry.window.endMs) {
|
|
585
601
|
changed = true;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Logger } from '@streamlayer/sdk-web-logger';
|
|
2
|
+
/**
|
|
3
|
+
* `ssaiq` — the SSAI ad NARRATIVE log: one line per thing that happens to a
|
|
4
|
+
* server-stitched ad, in the order it happens. The manifest tag the playout
|
|
5
|
+
* published, the metadata round trip it triggers, the break the SDK opens from
|
|
6
|
+
* it, and every beacon fired against it.
|
|
7
|
+
*
|
|
8
|
+
* Named for the Quortex/Synamedia DAI integration it was built for, but it is
|
|
9
|
+
* the path itself that is logged, so a StreamLayer-dialect stream tells the same
|
|
10
|
+
* story. Detector internals — clock sources, poll ticks, fold bookkeeping — stay
|
|
11
|
+
* on `ssai:*`, where `SL_DEBUG` can reach them without filling a demo window.
|
|
12
|
+
*
|
|
13
|
+
* A host publishes it to the page with `SL_LOG_EVENTS=ssaiq` (see
|
|
14
|
+
* `sdk-web-logger`); the demo's log window renders exactly that.
|
|
15
|
+
*/
|
|
16
|
+
export declare const ssaiq: Logger;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createLogger } from '@streamlayer/sdk-web-logger';
|
|
2
|
+
/**
|
|
3
|
+
* `ssaiq` — the SSAI ad NARRATIVE log: one line per thing that happens to a
|
|
4
|
+
* server-stitched ad, in the order it happens. The manifest tag the playout
|
|
5
|
+
* published, the metadata round trip it triggers, the break the SDK opens from
|
|
6
|
+
* it, and every beacon fired against it.
|
|
7
|
+
*
|
|
8
|
+
* Named for the Quortex/Synamedia DAI integration it was built for, but it is
|
|
9
|
+
* the path itself that is logged, so a StreamLayer-dialect stream tells the same
|
|
10
|
+
* story. Detector internals — clock sources, poll ticks, fold bookkeeping — stay
|
|
11
|
+
* on `ssai:*`, where `SL_DEBUG` can reach them without filling a demo window.
|
|
12
|
+
*
|
|
13
|
+
* A host publishes it to the page with `SL_LOG_EVENTS=ssaiq` (see
|
|
14
|
+
* `sdk-web-logger`); the demo's log window renders exactly that.
|
|
15
|
+
*/
|
|
16
|
+
// Explicit type: the inferred pino type is not nameable across package
|
|
17
|
+
// boundaries (TS2742) when this file is compiled on its own.
|
|
18
|
+
export const ssaiq = createLogger('ssaiq');
|
|
@@ -1,7 +1,53 @@
|
|
|
1
1
|
import { createLogger } from '@streamlayer/sdk-web-logger';
|
|
2
2
|
import { VASTClient } from '@dailymotion/vast-client';
|
|
3
|
+
import { ssaiq } from './ssai-log.js';
|
|
3
4
|
const log = createLogger('ssai:provider');
|
|
4
5
|
export const DEFAULT_CREATIVE_API_BASE = 'https://hls.next.streamlayer.io/api/creatives/';
|
|
6
|
+
/**
|
|
7
|
+
* One creative-metadata round trip, on the narrative log: the request the SDK
|
|
8
|
+
* made and the body the ad service answered with, verbatim. This is the half of
|
|
9
|
+
* the story the manifest cannot tell — the ad's identity comes from the playout,
|
|
10
|
+
* its content comes from here.
|
|
11
|
+
*/
|
|
12
|
+
const fetchCreativeJson = async (providerId, url, signal) => {
|
|
13
|
+
// `adApiBase` is customer infrastructure and may carry a key in its query;
|
|
14
|
+
// the request uses the URL as given, the log gets it with such values masked.
|
|
15
|
+
const shown = maskUrlSecrets(url);
|
|
16
|
+
ssaiq.info({ provider: providerId, url: shown }, `[GET] → ${shown}`);
|
|
17
|
+
let response;
|
|
18
|
+
try {
|
|
19
|
+
response = await fetch(url, { signal });
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
// An abort is the SDK cancelling its own request (break ended, creative
|
|
23
|
+
// changed) — a note, not a failure.
|
|
24
|
+
if (error?.name === 'AbortError') {
|
|
25
|
+
ssaiq.debug({ provider: providerId, url: shown }, `[GET] ✗ aborted ${shown}`);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
ssaiq.warn({ provider: providerId, url: shown, error: String(error) }, `[GET] ✗ ${shown}`);
|
|
29
|
+
}
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
ssaiq.warn({ provider: providerId, url: shown, status: response.status }, `[GET] ✗ ${response.status} ${shown}`);
|
|
34
|
+
throw new Error(`${providerId} creative request failed: ${response.status}`);
|
|
35
|
+
}
|
|
36
|
+
let raw;
|
|
37
|
+
try {
|
|
38
|
+
raw = (await response.json());
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
// A 200 with a body that is not JSON would otherwise leave the log looking
|
|
42
|
+
// like a request still in flight.
|
|
43
|
+
ssaiq.warn({ provider: providerId, url: shown, error: String(error) }, `[GET] ✗ ${response.status} ${shown} — not JSON`);
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
ssaiq.info({ provider: providerId, status: response.status, body: raw }, `[GET] ← ${response.status} ad metadata — ${raw?.advertiser || raw?.title || raw?.creativeId || 'ad'}`);
|
|
47
|
+
return raw;
|
|
48
|
+
};
|
|
49
|
+
/** `?apiKey=…` → `?apiKey=«redacted»`, for URLs that go on a screen. Regex, not `URL`: this ships to 2016 TV engines. */
|
|
50
|
+
const maskUrlSecrets = (url) => url.replace(/([?&][^=&]*(?:token|secret|password|key|authorization)[^=&]*=)[^&]*/gi, '$1«redacted»');
|
|
5
51
|
/**
|
|
6
52
|
* Shared low-level beacon helper. Prefers `navigator.sendBeacon` and falls back
|
|
7
53
|
* to a no-cors `fetch`. Used by the default impression path and available to
|
|
@@ -100,12 +146,7 @@ export const createStreamLayerProvider = (config = {}) => {
|
|
|
100
146
|
return {
|
|
101
147
|
id: 'streamlayer',
|
|
102
148
|
resolve: async (creativeId, signal) => {
|
|
103
|
-
const
|
|
104
|
-
const response = await fetch(url, { signal });
|
|
105
|
-
if (!response.ok) {
|
|
106
|
-
throw new Error(`creative request failed: ${response.status}`);
|
|
107
|
-
}
|
|
108
|
-
const raw = (await response.json());
|
|
149
|
+
const raw = await fetchCreativeJson('streamlayer', buildCreativeUrl(creativeApiBase, creativeId), signal);
|
|
109
150
|
return mapApiResponse({ ...raw, creativeId: raw.creativeId || creativeId });
|
|
110
151
|
},
|
|
111
152
|
fireImpression: fireDefaultImpression,
|
|
@@ -149,11 +190,7 @@ export const createQuortexProvider = (config = {}) => {
|
|
|
149
190
|
if (!adApiBase) {
|
|
150
191
|
throw new Error('quortex provider is missing `adApiBase`');
|
|
151
192
|
}
|
|
152
|
-
const
|
|
153
|
-
if (!response.ok) {
|
|
154
|
-
throw new Error(`quortex creative request failed: ${response.status}`);
|
|
155
|
-
}
|
|
156
|
-
const raw = (await response.json());
|
|
193
|
+
const raw = await fetchCreativeJson('quortex', buildQuortexUrl(adApiBase, tenantName, adId), signal);
|
|
157
194
|
return mapApiResponse({ ...raw, creativeId: raw.creativeId || adId });
|
|
158
195
|
},
|
|
159
196
|
fireImpression: fireDefaultImpression,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
declare module '@dailymotion/vast-client' {
|
|
2
|
+
export interface VastAd {}
|
|
3
|
+
export interface VastCompanionAd {}
|
|
4
|
+
export interface VastCreativeCompanion {}
|
|
5
|
+
export interface VastCreativeLinear {}
|
|
6
|
+
export interface VastCreativeNonLinear {}
|
|
7
|
+
export interface VastNonLinearAd {}
|
|
8
|
+
export interface VastResponse {}
|
|
9
|
+
}
|
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@streamlayer/feature-advertisement",
|
|
3
|
-
"version": "1.1.1-rc.
|
|
3
|
+
"version": "1.1.1-rc.2.pf58ff685",
|
|
4
4
|
"peerDependencies": {
|
|
5
5
|
"@dailymotion/vast-client": "^6.4.5",
|
|
6
6
|
"@streamlayer/sl-eslib": "^5.268.1",
|
|
7
7
|
"core-js": "^3.50.0",
|
|
8
8
|
"uuid": "^11.1.0",
|
|
9
9
|
"nanostores": "^1.1.0",
|
|
10
|
-
"@streamlayer/sdk-web-core": "
|
|
11
|
-
"@streamlayer/sdk-web-api": "
|
|
12
|
-
"@streamlayer/sdk-web-interfaces": "
|
|
13
|
-
"@streamlayer/sdk-web-types": "
|
|
14
|
-
"@streamlayer/sdk-web-logger": "
|
|
15
|
-
"@streamlayer/sdk-web-storage": "
|
|
10
|
+
"@streamlayer/sdk-web-core": "1.23.2-rc.2.pf58ff685",
|
|
11
|
+
"@streamlayer/sdk-web-api": "1.18.2-rc.2.pf58ff685",
|
|
12
|
+
"@streamlayer/sdk-web-interfaces": "1.11.2-rc.2.pf58ff685",
|
|
13
|
+
"@streamlayer/sdk-web-types": "2.0.2-rc.2.pf58ff685",
|
|
14
|
+
"@streamlayer/sdk-web-logger": "1.2.2-rc.2.pf58ff685",
|
|
15
|
+
"@streamlayer/sdk-web-storage": "1.2.2-rc.2.pf58ff685"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@types/vast-client": "^3.0.4"
|
|
@@ -56,4 +56,4 @@
|
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"vmap": "^3.2.0"
|
|
58
58
|
}
|
|
59
|
-
}
|
|
59
|
+
}
|