@streamlayer/feature-advertisement 1.2.0 → 1.2.1-rc.11.pd437c711
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/classic/package.json +5 -0
- package/lib/advertisement.d.ts +6 -6
- package/lib/advertisement.js +9 -9
- package/lib/cjs/advertisement.d.ts +64 -0
- package/lib/cjs/advertisement.js +322 -0
- package/lib/cjs/index.d.ts +21 -0
- package/lib/cjs/index.js +41 -0
- package/lib/cjs/package.json +1 -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 +127 -0
- package/lib/cjs/units/external-ad.js +421 -0
- package/lib/cjs/units/external-pause-ad.d.ts +50 -0
- package/lib/cjs/units/external-pause-ad.js +259 -0
- package/lib/cjs/units/gam.d.ts +26 -0
- package/lib/cjs/units/gam.js +152 -0
- package/lib/cjs/units/parse-vast-extensions.d.ts +26 -0
- package/lib/cjs/units/parse-vast-extensions.js +56 -0
- package/lib/cjs/units/pause-ad-card.d.ts +49 -0
- package/lib/cjs/units/pause-ad-card.js +136 -0
- package/lib/cjs/units/pause-ad.d.ts +16 -0
- package/lib/cjs/units/pause-ad.js +52 -0
- package/lib/cjs/units/sponsor-logo.d.ts +78 -0
- package/lib/cjs/units/sponsor-logo.js +194 -0
- package/lib/cjs/units/ssai-ad-break.d.ts +21 -0
- package/lib/cjs/units/ssai-ad-break.js +11 -0
- package/lib/cjs/units/ssai-creative.d.ts +89 -0
- package/lib/cjs/units/ssai-creative.js +136 -0
- package/lib/cjs/units/ssai-detector.d.ts +180 -0
- package/lib/cjs/units/ssai-detector.js +1172 -0
- package/lib/cjs/units/ssai-log.d.ts +16 -0
- package/lib/cjs/units/ssai-log.js +23 -0
- package/lib/cjs/units/ssai-preload.d.ts +20 -0
- package/lib/cjs/units/ssai-preload.js +62 -0
- package/lib/cjs/units/ssai-providers.d.ts +117 -0
- package/lib/cjs/units/ssai-providers.js +300 -0
- package/lib/cjs/units/vast-tracker.d.ts +45 -0
- package/lib/cjs/units/vast-tracker.js +64 -0
- package/lib/classic/_virtual/_rollupPluginBabelHelpers.js +71 -0
- package/lib/classic/advertisement.js +348 -0
- package/lib/classic/cjs/_virtual/_rollupPluginBabelHelpers.js +77 -0
- package/lib/classic/cjs/advertisement.js +350 -0
- package/lib/classic/cjs/index.js +49 -0
- package/lib/classic/cjs/package.json +1 -0
- package/lib/classic/cjs/units/ad-macros.js +115 -0
- package/lib/classic/cjs/units/external-ad.js +500 -0
- package/lib/classic/cjs/units/external-pause-ad.js +278 -0
- package/lib/classic/cjs/units/gam.js +173 -0
- package/lib/classic/cjs/units/parse-vast-extensions.js +60 -0
- package/lib/classic/cjs/units/pause-ad-card.js +134 -0
- package/lib/classic/cjs/units/pause-ad.js +72 -0
- package/lib/classic/cjs/units/sponsor-logo.js +201 -0
- package/lib/classic/cjs/units/ssai-ad-break.js +11 -0
- package/lib/classic/cjs/units/ssai-creative.js +142 -0
- package/lib/classic/cjs/units/ssai-detector.js +1195 -0
- package/lib/classic/cjs/units/ssai-log.js +23 -0
- package/lib/classic/cjs/units/ssai-preload.js +72 -0
- package/lib/classic/cjs/units/ssai-providers.js +361 -0
- package/lib/classic/cjs/units/vast-tracker.js +81 -0
- package/lib/classic/index.js +29 -0
- package/lib/classic/units/ad-macros.js +113 -0
- package/lib/classic/units/external-ad.js +491 -0
- package/lib/classic/units/external-pause-ad.js +275 -0
- package/lib/classic/units/gam.js +171 -0
- package/lib/classic/units/parse-vast-extensions.js +58 -0
- package/lib/classic/units/pause-ad-card.js +131 -0
- package/lib/classic/units/pause-ad.js +70 -0
- package/lib/classic/units/sponsor-logo.js +197 -0
- package/lib/classic/units/ssai-ad-break.js +9 -0
- package/lib/classic/units/ssai-creative.js +140 -0
- package/lib/classic/units/ssai-detector.js +1192 -0
- package/lib/classic/units/ssai-log.js +21 -0
- package/lib/classic/units/ssai-preload.js +68 -0
- package/lib/classic/units/ssai-providers.js +351 -0
- package/lib/classic/units/vast-tracker.js +77 -0
- package/lib/index.d.ts +15 -14
- package/lib/index.js +9 -9
- package/lib/units/external-ad.d.ts +2 -2
- package/lib/units/external-ad.js +3 -3
- package/lib/units/external-pause-ad.js +1 -1
- package/lib/units/gam.js +1 -1
- package/lib/units/pause-ad.d.ts +1 -1
- package/lib/units/pause-ad.js +1 -1
- package/lib/units/sponsor-logo.d.ts +12 -4
- package/lib/units/sponsor-logo.js +4 -3
- package/lib/units/ssai-ad-break.d.ts +1 -1
- package/lib/units/ssai-creative.d.ts +1 -1
- package/lib/units/ssai-creative.js +1 -1
- package/lib/units/ssai-detector.d.ts +1 -1
- package/lib/units/ssai-detector.js +1 -1
- package/lib/units/ssai-preload.d.ts +1 -1
- package/lib/units/ssai-providers.d.ts +1 -1
- package/lib/units/ssai-providers.js +1 -1
- package/lib/units/vast-tracker.d.ts +1 -1
- package/lib/vast-client.d.ts +9 -0
- package/package.json +42 -9
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var interactive_common_pb = require('@streamlayer/sl-eslib/interactive/interactive.common_pb');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Insight body cap (SC-353). Studio enforces it on the Additional Content field; the
|
|
7
|
+
* SDK clamps as well so a card authored before the limit landed cannot overflow the
|
|
8
|
+
* screen — the format is explicitly designed to fit without scrolling.
|
|
9
|
+
*/
|
|
10
|
+
const INSIGHT_BODY_MAX_LENGTH = 600;
|
|
11
|
+
/**
|
|
12
|
+
* Sponsor logo for a factoid card. Same General-tab opt-in as the promotion path below,
|
|
13
|
+
* but the logo lives on `question.promotion.sponsor` (a factoid card carries no
|
|
14
|
+
* `PromotionOptions`) and Studio writes the uploaded file to `logo`, not `centerLogo`.
|
|
15
|
+
*
|
|
16
|
+
* `logo` therefore WINS over `centerLogo`: the latter is an optional field carried over from the
|
|
17
|
+
* notification/header surfaces, so a card that kept a stale `centerLogo` from an earlier revision
|
|
18
|
+
* would otherwise render it instead of the Insight logo the author just uploaded.
|
|
19
|
+
*/
|
|
20
|
+
const resolveInsightSponsorLogo = (question) => {
|
|
21
|
+
const sponsor = question.promotion?.sponsor;
|
|
22
|
+
if (sponsor?.logoPosition === interactive_common_pb.NotificationSponsorLogoMode.NOTIFICATION_SPONSOR_LOGO_NONE) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
return sponsor?.useGeneralLogo === interactive_common_pb.UseGeneralLogo.ENABLED
|
|
26
|
+
? question.promotion?.generalTabMetadata?.advertiserTransparentLogo || undefined
|
|
27
|
+
: sponsor?.logo || sponsor?.centerLogo || undefined;
|
|
28
|
+
};
|
|
29
|
+
const MEDIA_MODE_BY_PROTO = {
|
|
30
|
+
[interactive_common_pb.FactoidMediaMode.IMAGE]: 'image',
|
|
31
|
+
[interactive_common_pb.FactoidMediaMode.CIRCLE]: 'circle',
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Insight Pause Ad, Full Bleed (SC-353). A factoid card, so none of the promotion
|
|
35
|
+
* plumbing above applies: the heading is the question subject, the body is
|
|
36
|
+
* `FactoidOptions.body`, and the sponsor logo comes off `question.promotion.sponsor`.
|
|
37
|
+
*
|
|
38
|
+
* `FACTOID_MEDIA_MODE_VIDEO` is deliberately treated as "no media": the full-bleed
|
|
39
|
+
* format has no player chrome and no way to start playback, unlike the sidebar variant.
|
|
40
|
+
*/
|
|
41
|
+
const parseInsightPauseAdCard = (question) => {
|
|
42
|
+
const factoidOptions = question.options?.options.case === 'factoid' ? question.options.options.value : undefined;
|
|
43
|
+
if (!factoidOptions) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const mediaMode = MEDIA_MODE_BY_PROTO[factoidOptions.mode] ?? 'none';
|
|
47
|
+
const body = factoidOptions.body.slice(0, INSIGHT_BODY_MAX_LENGTH);
|
|
48
|
+
return {
|
|
49
|
+
id: question.id,
|
|
50
|
+
parentId: question.eventId || question.id,
|
|
51
|
+
// `landscapeUrl` ONLY. `portraitUrl` keeps stale artwork from earlier revisions of the
|
|
52
|
+
// card that Studio no longer shows in its Background Image slot, so falling back to it
|
|
53
|
+
// paints a background the author has already removed. Unlike the standard card, an
|
|
54
|
+
// Insight one renders fine with no background at all — its copy is the ad.
|
|
55
|
+
backgroundImageSrc: question.backgroundImage?.landscapeUrl || undefined,
|
|
56
|
+
kind: 'insight',
|
|
57
|
+
layout: 'left',
|
|
58
|
+
isInteractive: false,
|
|
59
|
+
logoSrc: resolveInsightSponsorLogo(question),
|
|
60
|
+
title: question.subject || undefined,
|
|
61
|
+
description: body || undefined,
|
|
62
|
+
mediaSrc: mediaMode === 'none' ? undefined : factoidOptions.image || undefined,
|
|
63
|
+
mediaMode,
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
const parsePauseAdCard = (question) => {
|
|
67
|
+
if (question.options?.options.case === 'factoid') {
|
|
68
|
+
return parseInsightPauseAdCard(question);
|
|
69
|
+
}
|
|
70
|
+
const backgroundImageSrc = question.backgroundImage?.landscapeUrl;
|
|
71
|
+
if (!backgroundImageSrc) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const promotionOptions = question.options?.options.case === 'promotion' ? question.options.options.value : undefined;
|
|
75
|
+
const pauseAdOptions = promotionOptions?.pauseAdOptions;
|
|
76
|
+
const optionsSponsor = promotionOptions?.sponsor;
|
|
77
|
+
const layoutProto = pauseAdOptions?.layout ?? interactive_common_pb.PauseAdLayout.UNSET;
|
|
78
|
+
const layout = layoutProto === interactive_common_pb.PauseAdLayout.CENTER ? 'center' : layoutProto === interactive_common_pb.PauseAdLayout.IMAGE ? 'image' : 'left';
|
|
79
|
+
const isInteractive = promotionOptions?.buttonType === interactive_common_pb.PromotionButtonType.SEND_TO_USER;
|
|
80
|
+
const hasQr = pauseAdOptions?.qrCode === interactive_common_pb.QRCode.QR_CODE_ENABLED && !!pauseAdOptions?.qrCodeUrl;
|
|
81
|
+
let ctaLabel;
|
|
82
|
+
let ctaUrl;
|
|
83
|
+
if (isInteractive && promotionOptions?.buttonOptions.case === 'sendToUserOptions') {
|
|
84
|
+
const accountInfo = promotionOptions.buttonOptions.value.userAccountInfo;
|
|
85
|
+
const isPhone = accountInfo === interactive_common_pb.UserAccountInfo.PHONE;
|
|
86
|
+
ctaUrl = isPhone ? 'sendToPhone:' : 'sendToEmail:';
|
|
87
|
+
ctaLabel = isPhone ? 'Send info to account phone' : 'Send info to account email';
|
|
88
|
+
}
|
|
89
|
+
const logoSrc = optionsSponsor?.logoPosition === interactive_common_pb.NotificationSponsorLogoMode.NOTIFICATION_SPONSOR_LOGO_NONE
|
|
90
|
+
? undefined
|
|
91
|
+
: optionsSponsor?.useGeneralLogo === interactive_common_pb.UseGeneralLogo.ENABLED
|
|
92
|
+
? question.promotion?.generalTabMetadata?.advertiserTransparentLogo || undefined
|
|
93
|
+
: optionsSponsor?.centerLogo || undefined;
|
|
94
|
+
return {
|
|
95
|
+
id: question.id,
|
|
96
|
+
parentId: question.eventId || question.id,
|
|
97
|
+
backgroundImageSrc,
|
|
98
|
+
layout,
|
|
99
|
+
logoSrc,
|
|
100
|
+
title: promotionOptions?.banner?.title || undefined,
|
|
101
|
+
description: promotionOptions?.banner?.body || undefined,
|
|
102
|
+
isInteractive,
|
|
103
|
+
ctaLabel: ctaLabel || undefined,
|
|
104
|
+
ctaUrl: ctaUrl || undefined,
|
|
105
|
+
qrCodeUrl: hasQr ? (pauseAdOptions?.qrCodeUrl ?? undefined) : undefined,
|
|
106
|
+
qrCodeLabel: hasQr ? (pauseAdOptions?.qrCodeLabel ?? undefined) : undefined,
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Which cards from `pauseAdList` the SDK can actually draw. Both renderers are full bleed; a
|
|
111
|
+
* Sidebar-format pause ad has no chrome yet and is skipped.
|
|
112
|
+
*/
|
|
113
|
+
const isFullBleedPauseAd = (question) => {
|
|
114
|
+
if (question.options?.options.case === 'factoid') {
|
|
115
|
+
// Insight Pause Ad (SC-353). Studio does not write `CardFormat` on these cards yet
|
|
116
|
+
// (SLW-7062), so an UNSET format is taken as Full Bleed — the only Insight pause-ad chrome
|
|
117
|
+
// the SDK has. An explicit Sidebar stays out. Drop the UNSET arm once Studio sets the field.
|
|
118
|
+
return question.format === interactive_common_pb.CardFormat.FULL_BLEED || question.format === interactive_common_pb.CardFormat.UNSET;
|
|
119
|
+
}
|
|
120
|
+
const promotionOptions = question.options?.options.case === 'promotion' ? question.options.options.value : undefined;
|
|
121
|
+
return promotionOptions?.format === interactive_common_pb.PromotionFormat.FULL_BLEED || question.format === interactive_common_pb.CardFormat.FULL_BLEED;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* The `pauseAdList` response, shaped for the renderers: keep the cards the SDK has chrome for,
|
|
125
|
+
* turn each into a `PauseAdCard`, drop the ones that cannot be drawn.
|
|
126
|
+
*
|
|
127
|
+
* Pure on purpose — `pause-ad.ts` owns the store and the transport, this owns the card shapes,
|
|
128
|
+
* and only this half needs unit tests.
|
|
129
|
+
*/
|
|
130
|
+
const parsePauseAdCards = (questions) => questions
|
|
131
|
+
.filter(isFullBleedPauseAd)
|
|
132
|
+
.map(parsePauseAdCard)
|
|
133
|
+
.filter((card) => card !== null);
|
|
134
|
+
|
|
135
|
+
exports.INSIGHT_BODY_MAX_LENGTH = INSIGHT_BODY_MAX_LENGTH;
|
|
136
|
+
exports.parsePauseAdCards = parsePauseAdCards;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createSingleStore, StreamLayerContext } from '@streamlayer/sdk-web-interfaces';
|
|
2
|
+
import '@streamlayer/sdk-web-api';
|
|
3
|
+
import '@streamlayer/sdk-web-core/store';
|
|
4
|
+
import { type PauseAdCard } from './pause-ad-card.js';
|
|
5
|
+
export type PauseAdListOptions = {
|
|
6
|
+
refetchInterval: number;
|
|
7
|
+
$enabled: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare const pauseAdListStore: (instance: StreamLayerContext) => ($enabled: ReturnType<typeof createSingleStore<boolean>>, options: PauseAdListOptions) => {
|
|
10
|
+
$list: import("@nanostores/query", { with: { "resolution-mode": "import" } }).FetcherStore<PauseAdCard[], any>;
|
|
11
|
+
$index: import("nanostores", { with: { "resolution-mode": "import" } }).PreinitializedWritableAtom<number> & object;
|
|
12
|
+
next: () => void;
|
|
13
|
+
reset: () => void;
|
|
14
|
+
getCurrent: () => PauseAdCard | undefined;
|
|
15
|
+
};
|
|
16
|
+
export type PauseAdStore = ReturnType<ReturnType<typeof pauseAdListStore>>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var sdkWebLogger = require('@streamlayer/sdk-web-logger');
|
|
4
|
+
var nanostores = require('nanostores');
|
|
5
|
+
require('@streamlayer/sdk-web-api');
|
|
6
|
+
require('@streamlayer/sdk-web-core/store');
|
|
7
|
+
var interactive_feed_pb = require('@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb');
|
|
8
|
+
var pauseAdCard = require('./pause-ad-card.js');
|
|
9
|
+
|
|
10
|
+
const log = sdkWebLogger.createLogger('advertisement:pause-ad');
|
|
11
|
+
const pauseAdListStore = (instance) => {
|
|
12
|
+
const $slStreamId = instance.stores.slStreamId.getAtomStore();
|
|
13
|
+
return ($enabled, options) => {
|
|
14
|
+
const { client, queryKey } = instance.transport.createPromiseClient(interactive_feed_pb.Feed, {
|
|
15
|
+
method: 'pauseAdList',
|
|
16
|
+
params: [$slStreamId, $enabled],
|
|
17
|
+
});
|
|
18
|
+
const $list = instance.transport.nanoquery.createFetcherStore(queryKey, {
|
|
19
|
+
dedupeTime: 0,
|
|
20
|
+
revalidateInterval: options.refetchInterval > 0 ? options.refetchInterval : 0,
|
|
21
|
+
fetcher: async (_, __, eventId, isEnabled) => {
|
|
22
|
+
if (!eventId || typeof eventId !== 'string' || !isEnabled) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
log.debug({ eventId }, 'fetching pause ad list');
|
|
26
|
+
const res = await client.pauseAdList({ eventId });
|
|
27
|
+
const cards = pauseAdCard.parsePauseAdCards(res.data ?? []);
|
|
28
|
+
log.debug({ count: cards.length }, 'parsed %d fullbleed pause ad cards');
|
|
29
|
+
return cards;
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
const $index = nanostores.atom(0);
|
|
33
|
+
const next = () => {
|
|
34
|
+
const list = $list.get().data;
|
|
35
|
+
if (!list?.length)
|
|
36
|
+
return;
|
|
37
|
+
$index.set(($index.get() + 1) % list.length);
|
|
38
|
+
};
|
|
39
|
+
const reset = () => {
|
|
40
|
+
$index.set(0);
|
|
41
|
+
};
|
|
42
|
+
const getCurrent = () => {
|
|
43
|
+
const list = $list.get().data;
|
|
44
|
+
if (!list?.length)
|
|
45
|
+
return undefined;
|
|
46
|
+
return list[$index.get()];
|
|
47
|
+
};
|
|
48
|
+
return { $list, $index, next, reset, getCurrent };
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
exports.pauseAdListStore = pauseAdListStore;
|
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
import { VastCompanionAd, VastCreativeCompanion, VastCreativeNonLinear, VastNonLinearAd } from '@dailymotion/vast-client';
|
|
29
|
+
/**
|
|
30
|
+
* One image asset that may fill a sponsor-logo slot. Carries the creative + variation pair so the
|
|
31
|
+
* caller can build a `VASTTracker` for it through `createVastTracker` without re-deriving them.
|
|
32
|
+
*/
|
|
33
|
+
export type SponsorLogoAsset = {
|
|
34
|
+
imageUrl: string;
|
|
35
|
+
clickUrl?: string;
|
|
36
|
+
width?: number;
|
|
37
|
+
height?: number;
|
|
38
|
+
/** Which VAST element the image came from. The slot does not care; the beacons do. */
|
|
39
|
+
origin: 'companion' | 'nonlinear';
|
|
40
|
+
/** The `<CompanionAds>` / `<NonLinearAds>` creative — the `VASTTracker` 3rd argument. */
|
|
41
|
+
creative: VastCreativeCompanion | VastCreativeNonLinear;
|
|
42
|
+
/** The `<Companion>` / `<NonLinear>` variation — the 4th argument; owns `creativeView`. */
|
|
43
|
+
variation: VastCompanionAd | VastNonLinearAd;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* What a caller hands the selector. A parsed `@dailymotion/vast-client` ad satisfies it.
|
|
47
|
+
* The published declarations must not name that package's types: its own `.d.ts` does not
|
|
48
|
+
* export them, and the local shim that does is not part of the tarball.
|
|
49
|
+
*/
|
|
50
|
+
export interface SponsorLogoSource {
|
|
51
|
+
creatives?: unknown;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Pick the image a parsed VAST ad offers for a sponsor-logo slot, or `undefined` when it offers
|
|
55
|
+
* none (a video-only tag, or an empty response). A caller that gets `undefined` must fall back to
|
|
56
|
+
* its own proto/Studio logo rather than suppressing the card.
|
|
57
|
+
*/
|
|
58
|
+
export declare const collectAssets: (ad: SponsorLogoSource) => SponsorLogoAsset[];
|
|
59
|
+
export declare const selectSponsorLogo: (ad: SponsorLogoSource) => SponsorLogoAsset | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* iOS `SLRVASTSlotRole.cardLogo` — the sponsor logo of a **hero card** (1:1 / 2:1 / Picture in
|
|
62
|
+
* Picture / Side-by-Side / Sponsor Frame). Same 1:1 aspect-dominant ranking as `.logo`, plus the
|
|
63
|
+
* one constraint that distinguishes the two roles:
|
|
64
|
+
*
|
|
65
|
+
* - **`requiredOrigin: .companion`.** A NonLinear image is a hero creative, never a logo. iOS's
|
|
66
|
+
* reason: a leftover hero-type asset — a second `<NonLinear>` overlay, or a stray image still in
|
|
67
|
+
* the pool after the hero role claimed its own — must never leak into the card's logo slot,
|
|
68
|
+
* where it would render at logo size as an unreadable smudge of the advertiser's key art.
|
|
69
|
+
* `nonlinear_full` is the fixture that pins it: `.logo` takes the 500x500 NonLinear, `.cardLogo`
|
|
70
|
+
* must take the 200x200 `<Companion>`.
|
|
71
|
+
* - **`exclude`** mirrors iOS's **greedy** `pool.select([hero, .cardLogo])`: roles resolve in array
|
|
72
|
+
* order and an asset a role claims is removed from the pool for every later role. We have no
|
|
73
|
+
* pool object, so the caller passes the assets the hero already claimed and they are filtered
|
|
74
|
+
* out here by `imageUrl`.
|
|
75
|
+
*/
|
|
76
|
+
export declare const selectCardLogo: (ad: SponsorLogoSource, exclude?: ({
|
|
77
|
+
imageUrl?: string;
|
|
78
|
+
} | undefined)[]) => SponsorLogoAsset | undefined;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sponsor-logo slot selection for a **feature** ad unit (Insight, Prediction and Trivia today;
|
|
5
|
+
* Poll is the same shape and is a separate ticket).
|
|
6
|
+
*
|
|
7
|
+
* Deliberately NOT `selectCompanions` (`./external-ad.ts`). That rule is the *display*-unit rule:
|
|
8
|
+
* it promotes a companion to the logo slot only when the tag ships an exact 300x75 `<Companion>`
|
|
9
|
+
* next to at least one other. A feature ad-unit tag is typically a **single** creative — often a
|
|
10
|
+
* lone NonLinear `<StaticResource>` — where the ad IS the logo, which is exactly the shape that
|
|
11
|
+
* rule returns nothing for. `selectCompanions` stays untouched: SC-335's three shipped units
|
|
12
|
+
* depend on its exact behaviour and impression counts are billing-visible.
|
|
13
|
+
*
|
|
14
|
+
* Mirrors iOS `SLRVASTSlotRole.logo` + `SLRVASTAssetPool`
|
|
15
|
+
* (`StreamLayerSDK/Frameworks/SLRGAMFramework/Sources/VASTSlotFiller/`):
|
|
16
|
+
*
|
|
17
|
+
* - **Images only.** A video-only tag yields no logo. `<Icon>` is never projected — icons hang off
|
|
18
|
+
* the `<Linear>` creative, which this selector does not look at. A `<StaticResource>` the tag
|
|
19
|
+
* declares as a non-image `creativeType` is skipped too, so a companion that lists a script
|
|
20
|
+
* before its image still resolves to the image (see `isImageResource`).
|
|
21
|
+
* - **Origin-agnostic.** A lone NonLinear `<StaticResource>` is as valid as a `<Companion>`.
|
|
22
|
+
* - **Aspect-dominant** toward 1:1: the smallest `|log(assetAspect) - log(1)|` wins, ties broken by
|
|
23
|
+
* larger area, then by higher pixel density. Aspect is a *preference, never a filter* — the only
|
|
24
|
+
* hard constraint is the asset kind, so a badly shaped asset still wins if it is the only one.
|
|
25
|
+
* - Assets whose intrinsic size the tag never declared rank last, but still beat nothing.
|
|
26
|
+
*
|
|
27
|
+
* The module also hosts iOS's second logo role, `SLRVASTSlotRole.cardLogo` (`selectCardLogo`) —
|
|
28
|
+
* the same ranking narrowed to `<Companion>` origins, for the hero-card display units.
|
|
29
|
+
*/
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
|
31
|
+
/// <reference path="../module.d.ts" />
|
|
32
|
+
/** `width`/`height` are XML attributes (strings at runtime); 0 and NaN both mean "not declared". */
|
|
33
|
+
const toSize = (value) => {
|
|
34
|
+
const size = Number(value);
|
|
35
|
+
return size > 0 ? size : undefined;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* VAST 4 `<CompanionAds pxratio>`; `@dailymotion/vast-client` defaults it to `'1'` and the
|
|
39
|
+
* `@types/vast-client` shim does not declare it, hence the loose read.
|
|
40
|
+
*/
|
|
41
|
+
const toPxRatio = (variation) => {
|
|
42
|
+
return Number(variation.pxratio) || 1;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* `<StaticResource creativeType>`. The slot draws an `<img>`, so a resource the tag declares as
|
|
46
|
+
* anything else — the `application/x-shockwave-flash` or `application/javascript` creative a
|
|
47
|
+
* companion may list beside its image — must never fill it: it would swap the proto logo for a
|
|
48
|
+
* broken image AND still fire an impression for something nobody ever saw.
|
|
49
|
+
*
|
|
50
|
+
* An ABSENT type is not a rejection. VAST requires the attribute but real GAM image companions
|
|
51
|
+
* routinely omit it, and this module's only hard constraint is the asset kind, so an undeclared
|
|
52
|
+
* type stays a candidate. `@dailymotion/vast-client` writes `0` for a missing NonLinear
|
|
53
|
+
* `creativeType` and `null` for a missing companion one; both are falsy, hence the single guard.
|
|
54
|
+
*/
|
|
55
|
+
const isImageResource = (creativeType) => {
|
|
56
|
+
if (!creativeType) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
return String(creativeType).toLowerCase().indexOf('image/') === 0;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* The first resource a `<Companion>` declares as an image (or leaves undeclared). NOT
|
|
63
|
+
* `staticResources[0]`: the order in the tag is the advertiser's, and a non-image resource listed
|
|
64
|
+
* first must not hide a usable image behind it.
|
|
65
|
+
*/
|
|
66
|
+
const pickCompanionImage = (variation) => {
|
|
67
|
+
const resources = variation.staticResources || [];
|
|
68
|
+
for (let i = 0; i < resources.length; i++) {
|
|
69
|
+
const resource = resources[i];
|
|
70
|
+
if (resource && resource.url && isImageResource(resource.creativeType)) {
|
|
71
|
+
return resource.url;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
};
|
|
76
|
+
/** Distance from square on a log scale, so 2:1 and 1:2 are equally far from 1:1. */
|
|
77
|
+
const aspectDistance = (asset) => {
|
|
78
|
+
if (!asset.width || !asset.height) {
|
|
79
|
+
// Unknown intrinsic size — rank last, but still a candidate.
|
|
80
|
+
return Infinity;
|
|
81
|
+
}
|
|
82
|
+
return Math.abs(Math.log(asset.width / asset.height));
|
|
83
|
+
};
|
|
84
|
+
const collectAssetsFrom = (ad) => {
|
|
85
|
+
const assets = [];
|
|
86
|
+
for (let i = 0; i < ad.creatives.length; i++) {
|
|
87
|
+
const creative = ad.creatives[i];
|
|
88
|
+
if (creative.type === 'companion') {
|
|
89
|
+
const companionCreative = creative;
|
|
90
|
+
const variations = companionCreative.variations || [];
|
|
91
|
+
for (let j = 0; j < variations.length; j++) {
|
|
92
|
+
const variation = variations[j];
|
|
93
|
+
const imageUrl = variation && pickCompanionImage(variation);
|
|
94
|
+
if (imageUrl) {
|
|
95
|
+
assets.push({
|
|
96
|
+
imageUrl,
|
|
97
|
+
clickUrl: variation.companionClickThroughURLTemplate ?? undefined,
|
|
98
|
+
width: toSize(variation.width),
|
|
99
|
+
height: toSize(variation.height),
|
|
100
|
+
origin: 'companion',
|
|
101
|
+
creative: companionCreative,
|
|
102
|
+
variation,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (creative.type === 'nonlinear') {
|
|
109
|
+
const nonLinearCreative = creative;
|
|
110
|
+
const variations = nonLinearCreative.variations || [];
|
|
111
|
+
for (let j = 0; j < variations.length; j++) {
|
|
112
|
+
const variation = variations[j];
|
|
113
|
+
// `type` is where the parser puts a `<NonLinear>`'s `creativeType`, and it is written last
|
|
114
|
+
// from the `<StaticResource>` element, so it describes `staticResource` whenever that is
|
|
115
|
+
// set. An HTML- or iframe-typed variation carries no `staticResource` and drops out here.
|
|
116
|
+
const imageUrl = variation && isImageResource(variation.type) ? variation.staticResource : undefined;
|
|
117
|
+
if (imageUrl) {
|
|
118
|
+
assets.push({
|
|
119
|
+
imageUrl,
|
|
120
|
+
// 6.4.2 emits `nonlinearClickThroughURLTemplate` (lower-case `l`); the shim declares
|
|
121
|
+
// the upper-case spelling. The runtime one is authoritative — see `getNonLinear`.
|
|
122
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
123
|
+
// @ts-ignore
|
|
124
|
+
clickUrl: variation.nonlinearClickThroughURLTemplate ?? undefined,
|
|
125
|
+
width: toSize(variation.width),
|
|
126
|
+
height: toSize(variation.height),
|
|
127
|
+
origin: 'nonlinear',
|
|
128
|
+
creative: nonLinearCreative,
|
|
129
|
+
variation,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return assets;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* The shared ranking both roles use: aspect distance from 1:1, ties broken by larger area, then by
|
|
139
|
+
* higher pixel density. Aspect is a preference, never a filter — the best of a bad set still wins.
|
|
140
|
+
*/
|
|
141
|
+
const rankBest = (assets) => {
|
|
142
|
+
if (!assets.length) {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
let best = assets[0];
|
|
146
|
+
let bestDistance = aspectDistance(best);
|
|
147
|
+
let bestArea = (best.width || 0) * (best.height || 0);
|
|
148
|
+
let bestPxRatio = toPxRatio(best.variation);
|
|
149
|
+
for (let i = 1; i < assets.length; i++) {
|
|
150
|
+
const asset = assets[i];
|
|
151
|
+
const distance = aspectDistance(asset);
|
|
152
|
+
const area = (asset.width || 0) * (asset.height || 0);
|
|
153
|
+
const pxRatio = toPxRatio(asset.variation);
|
|
154
|
+
const wins = distance !== bestDistance ? distance < bestDistance : area !== bestArea ? area > bestArea : pxRatio > bestPxRatio;
|
|
155
|
+
if (wins) {
|
|
156
|
+
best = asset;
|
|
157
|
+
bestDistance = distance;
|
|
158
|
+
bestArea = area;
|
|
159
|
+
bestPxRatio = pxRatio;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return best;
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Pick the image a parsed VAST ad offers for a sponsor-logo slot, or `undefined` when it offers
|
|
166
|
+
* none (a video-only tag, or an empty response). A caller that gets `undefined` must fall back to
|
|
167
|
+
* its own proto/Studio logo rather than suppressing the card.
|
|
168
|
+
*/
|
|
169
|
+
const collectAssets = (ad) => collectAssetsFrom(ad);
|
|
170
|
+
const selectSponsorLogo = (ad) => rankBest(collectAssetsFrom(ad));
|
|
171
|
+
/**
|
|
172
|
+
* iOS `SLRVASTSlotRole.cardLogo` — the sponsor logo of a **hero card** (1:1 / 2:1 / Picture in
|
|
173
|
+
* Picture / Side-by-Side / Sponsor Frame). Same 1:1 aspect-dominant ranking as `.logo`, plus the
|
|
174
|
+
* one constraint that distinguishes the two roles:
|
|
175
|
+
*
|
|
176
|
+
* - **`requiredOrigin: .companion`.** A NonLinear image is a hero creative, never a logo. iOS's
|
|
177
|
+
* reason: a leftover hero-type asset — a second `<NonLinear>` overlay, or a stray image still in
|
|
178
|
+
* the pool after the hero role claimed its own — must never leak into the card's logo slot,
|
|
179
|
+
* where it would render at logo size as an unreadable smudge of the advertiser's key art.
|
|
180
|
+
* `nonlinear_full` is the fixture that pins it: `.logo` takes the 500x500 NonLinear, `.cardLogo`
|
|
181
|
+
* must take the 200x200 `<Companion>`.
|
|
182
|
+
* - **`exclude`** mirrors iOS's **greedy** `pool.select([hero, .cardLogo])`: roles resolve in array
|
|
183
|
+
* order and an asset a role claims is removed from the pool for every later role. We have no
|
|
184
|
+
* pool object, so the caller passes the assets the hero already claimed and they are filtered
|
|
185
|
+
* out here by `imageUrl`.
|
|
186
|
+
*/
|
|
187
|
+
const selectCardLogo = (ad, exclude) => {
|
|
188
|
+
const claimed = (exclude || []).map((item) => item?.imageUrl).filter((url) => !!url);
|
|
189
|
+
return rankBest(collectAssetsFrom(ad).filter((asset) => asset.origin === 'companion' && claimed.indexOf(asset.imageUrl) === -1));
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
exports.collectAssets = collectAssets;
|
|
193
|
+
exports.selectCardLogo = selectCardLogo;
|
|
194
|
+
exports.selectSponsorLogo = selectSponsorLogo;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createSingleStore } from '@streamlayer/sdk-web-interfaces';
|
|
2
|
+
import type { SsaiCreative } from './ssai-creative.js';
|
|
3
|
+
/**
|
|
4
|
+
* 'ending' = the ad's window closes within the detector's lead (~0.5s): the
|
|
5
|
+
* overlay's cue to play its closing animation so it completes AT the boundary
|
|
6
|
+
* rather than after it. Beacons are unaffected — playback still owns those.
|
|
7
|
+
*/
|
|
8
|
+
export type SsaiAdBreakStatus = 'detecting' | 'ready' | 'ending' | 'ended';
|
|
9
|
+
export type SsaiAdBreakState = {
|
|
10
|
+
creativeId: string;
|
|
11
|
+
creative?: SsaiCreative;
|
|
12
|
+
status: SsaiAdBreakStatus;
|
|
13
|
+
startMs: number;
|
|
14
|
+
durationSec: number;
|
|
15
|
+
};
|
|
16
|
+
export type SsaiAdBreakStore = ReturnType<typeof createSingleStore<SsaiAdBreakState | null>>;
|
|
17
|
+
/**
|
|
18
|
+
* Imperative atom driven directly by the detector (no nanoquery — the trigger
|
|
19
|
+
* is push-based, not request/response).
|
|
20
|
+
*/
|
|
21
|
+
export declare const createSsaiAdBreakStore: () => SsaiAdBreakStore;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var sdkWebInterfaces = require('@streamlayer/sdk-web-interfaces');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Imperative atom driven directly by the detector (no nanoquery — the trigger
|
|
7
|
+
* is push-based, not request/response).
|
|
8
|
+
*/
|
|
9
|
+
const createSsaiAdBreakStore = () => sdkWebInterfaces.createSingleStore(null);
|
|
10
|
+
|
|
11
|
+
exports.createSsaiAdBreakStore = createSsaiAdBreakStore;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { DEFAULT_CREATIVE_API_BASE, type SsaiCreativeProvider } from './ssai-providers.js';
|
|
2
|
+
export { DEFAULT_CREATIVE_API_BASE };
|
|
3
|
+
export type SsaiTrackingEvent = {
|
|
4
|
+
event: string;
|
|
5
|
+
url: string;
|
|
6
|
+
};
|
|
7
|
+
export type SsaiCompanion = {
|
|
8
|
+
staticResource: string;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
clickThrough?: string;
|
|
12
|
+
/** Beacons fired when the companion is clicked. May be empty. */
|
|
13
|
+
clickTracking: string[];
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Lifecycle events the SDK fires beacons for, in the order they occur.
|
|
17
|
+
* `impression` is separate: it is tied to the overlay actually being rendered,
|
|
18
|
+
* not to playback position — see `SsaiCreativeCache.fireImpression`.
|
|
19
|
+
*/
|
|
20
|
+
export type SsaiProgressEvent = 'start' | 'firstQuartile' | 'midpoint' | 'thirdQuartile' | 'complete';
|
|
21
|
+
export type SsaiCreative = {
|
|
22
|
+
creativeId: string;
|
|
23
|
+
advertiser: string;
|
|
24
|
+
title: string;
|
|
25
|
+
iconUrl?: string;
|
|
26
|
+
clickThroughUrl?: string;
|
|
27
|
+
trackingEvents: SsaiTrackingEvent[];
|
|
28
|
+
companion?: SsaiCompanion;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Per-instance cache + impression-firing wrapper around a SsaiCreativeProvider.
|
|
32
|
+
* Caches resolved creatives by creativeId so repeated active-ad polls and pod
|
|
33
|
+
* repeats don't refetch. In-flight fetches are cancellable via AbortController.
|
|
34
|
+
*/
|
|
35
|
+
export declare class SsaiCreativeCache {
|
|
36
|
+
private provider;
|
|
37
|
+
private cache;
|
|
38
|
+
private inflight;
|
|
39
|
+
constructor(provider: SsaiCreativeProvider);
|
|
40
|
+
/**
|
|
41
|
+
* The already-resolved creative, or undefined — synchronous on purpose: a
|
|
42
|
+
* prefetched creative lets the ad break flip straight to 'ready' with no
|
|
43
|
+
* async gap (which the UI would render as a blink between two ads of a pod).
|
|
44
|
+
*/
|
|
45
|
+
peek: (creativeId: string) => SsaiCreative | undefined;
|
|
46
|
+
resolve: (creativeId: string) => Promise<SsaiCreative>;
|
|
47
|
+
/**
|
|
48
|
+
* Cancel an in-flight fetch (e.g. the break ended before it resolved).
|
|
49
|
+
*/
|
|
50
|
+
cancel: (creativeId: string) => void;
|
|
51
|
+
/**
|
|
52
|
+
* Fire the impression for a creative. Delegates to the provider's
|
|
53
|
+
* `fireImpression` when present, otherwise the shared default (first
|
|
54
|
+
* trackingEvent named 'impression' via sendBeacon/fetch). Called once per
|
|
55
|
+
* visible appearance of the bar (the UI dedupes the ~250ms active-poll so a
|
|
56
|
+
* single continuous appearance fires exactly one impression). Each genuine pod
|
|
57
|
+
* insertion of the same creative is a distinct impression and re-fires.
|
|
58
|
+
*/
|
|
59
|
+
fireImpression: (creative: SsaiCreative) => void;
|
|
60
|
+
/**
|
|
61
|
+
* Fire the impression for an already-resolved creativeId. No-op if the
|
|
62
|
+
* creative isn't cached yet (the bar only renders once resolved, so by the
|
|
63
|
+
* time the UI calls this the creative is present).
|
|
64
|
+
*/
|
|
65
|
+
fireImpressionById: (creativeId: string) => void;
|
|
66
|
+
/**
|
|
67
|
+
* Fire a lifecycle / click beacon. Delegates to the provider's `fireTracking`
|
|
68
|
+
* when it defines one, otherwise fires every matching `trackingEvents` URL.
|
|
69
|
+
*/
|
|
70
|
+
fireTracking: (creative: SsaiCreative, event: string) => void;
|
|
71
|
+
/**
|
|
72
|
+
* Fire a beacon for a creativeId that may still be resolving.
|
|
73
|
+
*
|
|
74
|
+
* `start` in particular lands the instant the break opens, which is usually
|
|
75
|
+
* BEFORE the metadata request has come back — dropping it would silently lose
|
|
76
|
+
* the first beacon of every ad. Resolution is already in flight by then and
|
|
77
|
+
* `resolve` returns that same promise, so this attaches to it rather than
|
|
78
|
+
* issuing a second request. A creative that never resolves fires nothing,
|
|
79
|
+
* which is the correct outcome: there are no URLs to fire.
|
|
80
|
+
*/
|
|
81
|
+
fireTrackingById: (creativeId: string, event: string) => void;
|
|
82
|
+
/**
|
|
83
|
+
* Fire the companion's `clickTracking` beacons. Separate from
|
|
84
|
+
* {@link fireTracking} because these live on the companion, not in
|
|
85
|
+
* `trackingEvents` — a provider's `fireTracking` override has no say over them.
|
|
86
|
+
*/
|
|
87
|
+
fireCompanionClickById: (creativeId: string) => void;
|
|
88
|
+
destroy: () => void;
|
|
89
|
+
}
|