vastlint-client 0.4.20
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/README.md +21 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/media.d.ts +4 -0
- package/dist/media.js +113 -0
- package/dist/playback-queue.d.ts +2 -0
- package/dist/playback-queue.js +550 -0
- package/dist/playback.d.ts +2 -0
- package/dist/playback.js +420 -0
- package/dist/resolved.d.ts +7 -0
- package/dist/resolved.js +488 -0
- package/dist/session.d.ts +2 -0
- package/dist/session.js +818 -0
- package/dist/tracking.d.ts +7 -0
- package/dist/tracking.js +161 -0
- package/dist/types.d.ts +374 -0
- package/dist/types.js +1 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# vastlint-client
|
|
2
|
+
|
|
3
|
+
Headless VAST session/runtime package for the `vastlint` monorepo.
|
|
4
|
+
|
|
5
|
+
Current status:
|
|
6
|
+
|
|
7
|
+
- XML-backed sessions are implemented.
|
|
8
|
+
- Root URL loading and recursive wrapper-chain resolution are implemented.
|
|
9
|
+
- Per-hop validation, Rust-backed hop metadata extraction, and chain summary state are exposed on the session snapshot.
|
|
10
|
+
- A derived `resolvedAd` model is exposed on the session snapshot for playback-facing consumers.
|
|
11
|
+
- A pod-aware `resolvedAds` array is now exposed on the session snapshot so multi-`<Ad sequence="...">` responses stay separated instead of collapsing into one document-wide playback model.
|
|
12
|
+
- `resolvedAd` now includes parsed companion ads and icons from the final creative.
|
|
13
|
+
- `resolvedAd` now also includes universal ad IDs, categories, ad verifications, and ad-pod metadata.
|
|
14
|
+
- Standalone media selection helpers rank and choose media files from `resolvedAd` using MIME, delivery, bitrate, and dimension preferences.
|
|
15
|
+
- Tracker primitives are implemented for impression, error, viewability, click-tracking, and named `<Tracking event="...">` pixels, with click-through URLs exposed in session tracking state.
|
|
16
|
+
- `createVastSession()` now also exposes low-level pod-aware helpers: `getAdTrackingTargets(adSelector, event)` and `trackAd(adSelector, event, options)`, where `adSelector` can be an index, `{ adId }`, or `{ sequence }`.
|
|
17
|
+
- `createVastSession()` also exposes companion-specific helpers: `getAdCompanions(adSelector)`, `getCompanionTrackingTargets(adSelector, companionSelector, event)`, and `trackCompanion(adSelector, companionSelector, event, options)`.
|
|
18
|
+
- A headless playback controller now layers media selection and player-event tracking on top of a resolved session for impression, quartiles, click, viewability, mute, pause/resume, fullscreen, skip, and error dispatch.
|
|
19
|
+
- A headless playback queue controller now consumes `resolvedAds` and advances through ad pods with per-ad tracking dispatch.
|
|
20
|
+
- Build-first Node runtime tests now cover wrapper resolution, resolved metadata extraction, and the playback controller event flow.
|
|
21
|
+
- The package is intentionally framework-agnostic.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createVastSession } from "./session.js";
|
|
2
|
+
export { rankMediaFiles, selectMediaFile, selectResolvedAdMediaFile } from "./media.js";
|
|
3
|
+
export { createVastPlaybackController } from "./playback.js";
|
|
4
|
+
export { createVastPlaybackQueueController } from "./playback-queue.js";
|
|
5
|
+
export { selectTrackingTargets } from "./tracking.js";
|
|
6
|
+
export type { VastAdIdSelector, VastAdIndexSelector, VastAdPodMetadata, VastAdSelector, VastAdSequenceSelector, VastCompanionAdSlotSelector, VastAdVerification, VastCategory, VastCompanionAd, VastCompanionIdSelector, VastCompanionIndexSelector, VastCompanionSelector, VastCreativeResource, VastCreativeResourceKind, VastIcon, VastMediaSelectionCandidate, VastMediaSelectionOptions, VastMediaSelectionResult, VastPlaybackClickResult, VastPlaybackController, VastPlaybackControllerOptions, VastPlaybackMilestones, VastPlaybackQueueClickResult, VastPlaybackQueueController, VastPlaybackQueueControllerOptions, VastPlaybackQueueItem, VastPlaybackQueueSnapshot, VastPlaybackSnapshot, VastPlaybackStatus, VastPlaybackViewability, VastResolvedAd, VastUniversalAdId, VastVerificationResource, VastVerificationResourceKind, VastSession, VastSessionEvent, VastSessionEventType, VastTrackingDispatchResult, VastTrackingPlan, VastTrackingState, VastTrackingTarget, VastTrackOptions, VastTrackableEvent, VastSessionOptions, VastSessionSnapshot, VastSessionSource, VastSessionStatus, VastUrlSource, VastWrapperHop, VastXmlSource, } from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createVastSession } from "./session.js";
|
|
2
|
+
export { rankMediaFiles, selectMediaFile, selectResolvedAdMediaFile } from "./media.js";
|
|
3
|
+
export { createVastPlaybackController } from "./playback.js";
|
|
4
|
+
export { createVastPlaybackQueueController } from "./playback-queue.js";
|
|
5
|
+
export { selectTrackingTargets } from "./tracking.js";
|
package/dist/media.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { VastMediaFile, VastMediaSelectionCandidate, VastMediaSelectionOptions, VastMediaSelectionResult, VastResolvedAd } from "./types.js";
|
|
2
|
+
export declare function rankMediaFiles(mediaFiles: readonly VastMediaFile[], options?: VastMediaSelectionOptions): VastMediaSelectionCandidate[];
|
|
3
|
+
export declare function selectMediaFile(mediaFiles: readonly VastMediaFile[], options?: VastMediaSelectionOptions): VastMediaSelectionResult;
|
|
4
|
+
export declare function selectResolvedAdMediaFile(resolvedAd: VastResolvedAd | null, options?: VastMediaSelectionOptions): VastMediaSelectionResult;
|
package/dist/media.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
function normalizeValue(value) {
|
|
2
|
+
return value.trim().toLowerCase();
|
|
3
|
+
}
|
|
4
|
+
function parseOptionalNumber(value) {
|
|
5
|
+
const parsed = Number.parseInt(value, 10);
|
|
6
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
7
|
+
}
|
|
8
|
+
function buildPreferredIndex(values) {
|
|
9
|
+
return new Map((values ?? []).map((value, index) => [normalizeValue(value), index]));
|
|
10
|
+
}
|
|
11
|
+
function sortCandidates(left, right) {
|
|
12
|
+
return right.score - left.score || left.mediaFile.url.localeCompare(right.mediaFile.url);
|
|
13
|
+
}
|
|
14
|
+
function scoreDistance(target, actual, weight) {
|
|
15
|
+
if (target === undefined || actual === null) {
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
return Math.max(0, weight - Math.abs(target - actual));
|
|
19
|
+
}
|
|
20
|
+
function scoreMediaFile(mediaFile, options, preferredMimeTypes, preferredDelivery) {
|
|
21
|
+
const reasons = [];
|
|
22
|
+
let score = 0;
|
|
23
|
+
const mimeType = normalizeValue(mediaFile.mimeType);
|
|
24
|
+
const delivery = normalizeValue(mediaFile.delivery);
|
|
25
|
+
const width = parseOptionalNumber(mediaFile.width);
|
|
26
|
+
const height = parseOptionalNumber(mediaFile.height);
|
|
27
|
+
const bitrate = parseOptionalNumber(mediaFile.bitrate);
|
|
28
|
+
const supportedMimeTypes = (options.supportedMimeTypes ?? []).map(normalizeValue);
|
|
29
|
+
if (supportedMimeTypes.length > 0) {
|
|
30
|
+
if (!mimeType || !supportedMimeTypes.includes(mimeType)) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
score += 400;
|
|
34
|
+
reasons.push(`supported MIME type ${mimeType}`);
|
|
35
|
+
}
|
|
36
|
+
const preferredMimeIndex = preferredMimeTypes.get(mimeType);
|
|
37
|
+
if (preferredMimeIndex !== undefined) {
|
|
38
|
+
score += 300 - preferredMimeIndex * 25;
|
|
39
|
+
reasons.push(`preferred MIME type ${mimeType}`);
|
|
40
|
+
}
|
|
41
|
+
else if (mimeType.startsWith("video/")) {
|
|
42
|
+
score += 75;
|
|
43
|
+
reasons.push(`video MIME type ${mimeType}`);
|
|
44
|
+
}
|
|
45
|
+
const preferredDeliveryIndex = preferredDelivery.get(delivery);
|
|
46
|
+
if (preferredDeliveryIndex !== undefined) {
|
|
47
|
+
score += 120 - preferredDeliveryIndex * 15;
|
|
48
|
+
reasons.push(`preferred delivery ${delivery}`);
|
|
49
|
+
}
|
|
50
|
+
else if (delivery) {
|
|
51
|
+
score += 20;
|
|
52
|
+
reasons.push(`delivery ${delivery}`);
|
|
53
|
+
}
|
|
54
|
+
if (bitrate !== null) {
|
|
55
|
+
score += 25;
|
|
56
|
+
reasons.push(`declares bitrate ${bitrate}`);
|
|
57
|
+
}
|
|
58
|
+
if (width !== null && height !== null) {
|
|
59
|
+
score += 25;
|
|
60
|
+
reasons.push(`declares dimensions ${width}x${height}`);
|
|
61
|
+
}
|
|
62
|
+
if (options.maxBitrate !== undefined && bitrate !== null) {
|
|
63
|
+
if (bitrate > options.maxBitrate) {
|
|
64
|
+
score -= 200;
|
|
65
|
+
reasons.push(`bitrate ${bitrate} exceeds max ${options.maxBitrate}`);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
score += 40;
|
|
69
|
+
reasons.push(`bitrate ${bitrate} is within max ${options.maxBitrate}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const bitrateScore = scoreDistance(options.targetBitrate, bitrate, 160);
|
|
73
|
+
if (bitrateScore > 0 && bitrate !== null && options.targetBitrate !== undefined) {
|
|
74
|
+
score += bitrateScore;
|
|
75
|
+
reasons.push(`bitrate ${bitrate} is close to target ${options.targetBitrate}`);
|
|
76
|
+
}
|
|
77
|
+
const widthScore = scoreDistance(options.targetWidth, width, 120);
|
|
78
|
+
if (widthScore > 0 && width !== null && options.targetWidth !== undefined) {
|
|
79
|
+
score += widthScore;
|
|
80
|
+
reasons.push(`width ${width} is close to target ${options.targetWidth}`);
|
|
81
|
+
}
|
|
82
|
+
const heightScore = scoreDistance(options.targetHeight, height, 120);
|
|
83
|
+
if (heightScore > 0 && height !== null && options.targetHeight !== undefined) {
|
|
84
|
+
score += heightScore;
|
|
85
|
+
reasons.push(`height ${height} is close to target ${options.targetHeight}`);
|
|
86
|
+
}
|
|
87
|
+
if (mediaFile.url) {
|
|
88
|
+
score += 5;
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
mediaFile: { ...mediaFile },
|
|
92
|
+
score,
|
|
93
|
+
reasons,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export function rankMediaFiles(mediaFiles, options = {}) {
|
|
97
|
+
const preferredMimeTypes = buildPreferredIndex(options.preferredMimeTypes);
|
|
98
|
+
const preferredDelivery = buildPreferredIndex(options.preferredDelivery);
|
|
99
|
+
return mediaFiles
|
|
100
|
+
.map((mediaFile) => scoreMediaFile(mediaFile, options, preferredMimeTypes, preferredDelivery))
|
|
101
|
+
.filter((candidate) => candidate !== null)
|
|
102
|
+
.sort(sortCandidates);
|
|
103
|
+
}
|
|
104
|
+
export function selectMediaFile(mediaFiles, options = {}) {
|
|
105
|
+
const candidates = rankMediaFiles(mediaFiles, options);
|
|
106
|
+
return {
|
|
107
|
+
selected: candidates[0]?.mediaFile ?? null,
|
|
108
|
+
candidates,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export function selectResolvedAdMediaFile(resolvedAd, options = {}) {
|
|
112
|
+
return selectMediaFile(resolvedAd?.mediaFiles ?? [], options);
|
|
113
|
+
}
|