Skip to content

Commit

Permalink
Pod signaling
Browse files Browse the repository at this point in the history
  • Loading branch information
matvp91 committed Jan 4, 2025
1 parent d33699f commit 0297fa4
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 56 deletions.
3 changes: 3 additions & 0 deletions packages/stitcher/src/lib/math.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function preciseFloat(value: number) {
return Math.round((value + Number.EPSILON) * 100) / 100;
}
53 changes: 40 additions & 13 deletions packages/stitcher/src/playlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
mergeInterstitials,
} from "./interstitials";
import { encrypt } from "./lib/crypto";
import { preciseFloat } from "./lib/math";
import { createUrl, joinUrl } from "./lib/url";
import {
parseMasterPlaylist,
Expand All @@ -14,12 +15,12 @@ import {
stringifyMediaPlaylist,
} from "./parser";
import { updateSession } from "./session";
import { getSignalingForAsset } from "./signaling";
import { getPodSignaling, getSlotSignaling } from "./signaling";
import { fetchVmap, toAdBreakTimeOffset } from "./vmap";
import type { Filter } from "./filters";
import type { MasterPlaylist, MediaPlaylist } from "./parser";
import type { Session } from "./session";
import type { Interstitial } from "./types";
import type { HlsAsset, HlsAssetList, Interstitial } from "./types";
import type { VmapAdBreak } from "./vmap";
import type { DateTime } from "luxon";

Expand Down Expand Up @@ -77,16 +78,37 @@ export async function formatMediaPlaylist(
export async function formatAssetList(session: Session, dateTime: DateTime) {
const assets = await getAssets(session, dateTime);

return {
ASSETS: assets.map((asset) => {
return {
URI: asset.url,
DURATION: asset.duration,
"SPRS-KIND": asset.kind,
"X-AD-CREATIVE-SIGNALING": getSignalingForAsset(assets, asset),
};
}),
let hasSignaling = false;

const ASSETS = assets.map<HlsAsset>((asset) => {
const result: HlsAsset = {
URI: asset.url,
DURATION: asset.duration,
"SPRS-KIND": asset.kind,
};

const signaling = getSlotSignaling(assets, asset);
if (signaling) {
result["X-AD-CREATIVE-SIGNALING"] = signaling;
hasSignaling = true;
}

return result;
});

const result: HlsAssetList = {
ASSETS,
};

if (hasSignaling) {
const relativeStart = session.startTime.diff(dateTime, "seconds");
result["X-AD-CREATIVE-SIGNALING"] = getPodSignaling(
assets,
relativeStart.seconds,
);
}

return result;
}

async function fetchMasterPlaylist(url: string) {
Expand All @@ -109,10 +131,12 @@ export async function fetchDuration(url: string) {
}

const media = await fetchMediaPlaylist(joinUrl(url, variant.uri));
return media.segments.reduce((acc, segment) => {
const duration = media.segments.reduce((acc, segment) => {
acc += segment.duration;
return acc;
}, 0);

return preciseFloat(duration);
}

export function createMasterUrl(params: {
Expand Down Expand Up @@ -233,7 +257,10 @@ export function mapAdBreaksToSessionInterstitials(
chunks: [
{
type: "vast",
data: { url: adBreak.vastUrl, data: adBreak.vastData },
data: {
url: adBreak.vastUrl,
data: adBreak.vastData,
},
},
],
});
Expand Down
145 changes: 107 additions & 38 deletions packages/stitcher/src/signaling.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,138 @@
import type { InterstitialAsset } from "./types";

interface SignalingEvent {
type: "impression" | "quartile" | "clickthrough";
// The types below define the SVTA Ad Signaling Spec version 2,
// if we make changes here, we should make them in @superstreamer/player too.
// We consume these events in the player to fire beacons per pod.

interface AdTrackingEvent {
type: "impression" | "quartile" | "clickthrough" | "podstart" | "podend";
start?: number;
urls: string[];
}

const QUARTILE_EVENTS: Record<string, number> = {
start: 0,
firstQuartile: 0.25,
midpoint: 0.5,
thirdQuartile: 0.75,
complete: 1,
};
interface AdTrackingSlot {
type: "linear";
start: number;
duration: number;
tracking: AdTrackingEvent[];
}

interface AdTrackingPod {
start: number;
duration: number;
tracking: AdTrackingEvent[];
}

export interface AdTrackingPodEnvelope {
version: 2;
type: "pod";
payload: AdTrackingPod[];
}

export interface AdTrackingSlotEnvelope {
version: 2;
type: "slot";
payload: AdTrackingSlot[];
}

export function getSignalingForAsset(
export function getSlotSignaling(
assets: InterstitialAsset[],
asset: InterstitialAsset,
) {
const { duration, tracking } = asset;
if (!tracking) {
): AdTrackingSlotEnvelope | null {
const startTime = getAssetStartTime(assets, asset);

const slot = mapAssetToSlot(asset, startTime);
if (!slot) {
return null;
}

const assetIndex = assets.indexOf(asset);
const startTime = assets.splice(0, assetIndex).reduce((acc, asset) => {
return {
version: 2,
type: "slot",
payload: [slot],
};
}

export function getPodSignaling(
assets: InterstitialAsset[],
start: number,
): AdTrackingPodEnvelope {
const duration = assets.reduce((acc, asset) => {
acc += asset.duration;
return acc;
}, 0);

const signalingEvents: SignalingEvent[] = [];
return {
version: 2,
type: "pod",
payload: [
{
start,
duration,
tracking: [],
},
],
};
}

signalingEvents.push({
type: "impression",
start: 0,
urls: tracking.impression,
});
const QUARTILE_EVENTS: Record<string, number> = {
start: 0,
firstQuartile: 0.25,
midpoint: 0.5,
thirdQuartile: 0.75,
complete: 1,
};

signalingEvents.push({
type: "clickthrough",
urls: tracking.clickThrough,
});
function mapAssetToSlot(
asset: InterstitialAsset,
start: number,
): AdTrackingSlot | null {
if (!asset.tracking) {
return null;
}

const events: AdTrackingEvent[] = [
{
type: "impression",
start: 0,
urls: asset.tracking.impression,
},
{
type: "clickthrough",
urls: asset.tracking.clickThrough,
},
];

// Map each tracking URL to their corresponding quartile.
Object.entries(tracking).forEach(([name, urls]) => {
Object.entries(asset.tracking).forEach(([name, urls]) => {
const offset = QUARTILE_EVENTS[name];
if (offset !== undefined) {
signalingEvents.push({
events.push({
type: "quartile",
start: duration * offset,
start: asset.duration * offset,
urls,
});
}
});

return {
version: 2,
type: "slot",
payload: [
{
type: "linear",
start: startTime,
duration: asset.duration,
tracking: signalingEvents,
},
],
type: "linear",
start,
duration: asset.duration,
tracking: events,
};
}

function getAssetStartTime(
assets: InterstitialAsset[],
target: InterstitialAsset,
) {
let startTime = 0;
for (const asset of assets) {
if (asset === target) {
break;
}
startTime += asset.duration;
}
return startTime;
}
28 changes: 23 additions & 5 deletions packages/stitcher/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import type {
AdTrackingPodEnvelope,
AdTrackingSlotEnvelope,
} from "./signaling";
import type { DateTime } from "luxon";

export interface InterstitialVast {
Expand All @@ -9,11 +13,13 @@ export interface InterstitialAsset {
url: string;
duration: number;
kind?: "ad" | "bumper";
tracking?: {
impression: string[];
clickThrough: string[];
[key: string]: string[];
};
tracking?: InterstitialAssetTracking;
}

export interface InterstitialAssetTracking {
impression: string[];
clickThrough: string[];
[key: string]: string[];
}

export interface InterstitialAssetList {
Expand All @@ -30,3 +36,15 @@ export interface Interstitial {
duration?: number;
chunks: InterstitialChunk[];
}

export interface HlsAssetList {
ASSETS: HlsAsset[];
"X-AD-CREATIVE-SIGNALING"?: AdTrackingPodEnvelope;
}

export interface HlsAsset {
URI: string;
DURATION: number;
"SPRS-KIND"?: "ad" | "bumper";
"X-AD-CREATIVE-SIGNALING"?: AdTrackingSlotEnvelope;
}

0 comments on commit 0297fa4

Please sign in to comment.