Skip to content

Commit

Permalink
New interstitials API
Browse files Browse the repository at this point in the history
  • Loading branch information
matvp91 committed Dec 9, 2024
1 parent 95d53f7 commit 598bb7c
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 118 deletions.
112 changes: 58 additions & 54 deletions packages/stitcher/src/interstitials.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
import { DateTime } from "luxon";
import { createUrl } from "./lib/url";
import { getAssetsFromVast } from "./vast";
import type { DateRange } from "./parser";
import type { Session } from "./session";
import type { Interstitial, InterstitialAsset } from "./types";
import type { DateTime } from "luxon";

export function getStaticDateRanges(session: Session, isLive: boolean) {
const group = getGroupedInterstitials(session.interstitials);

const dateRanges: DateRange[] = [];

for (const [ts, interstitials] of group.entries()) {
const startDate = DateTime.fromMillis(ts);

const assetListUrl = getAssetListUrl(startDate, interstitials, session);
return session.interstitials.map<DateRange>((interstitial) => {
const startDate = interstitial.dateTime;
const assetListUrl = getAssetListUrl(interstitial, session);

const clientAttributes: Record<string, number | string> = {
RESTRICT: "SKIP,JUMP",
"ASSET-LIST": assetListUrl,
"CONTENT-MAY-VARY": "YES",
"TIMELINE-OCCUPIES": "POINT",
"TIMELINE-STYLE": getTimelineStyle(interstitials),
"TIMELINE-STYLE": getTimelineStyle(interstitial),
};

if (!isLive) {
Expand All @@ -36,79 +31,88 @@ export function getStaticDateRanges(session: Session, isLive: boolean) {
clientAttributes["CUE"] = cue.join(",");
}

dateRanges.push({
return {
classId: "com.apple.hls.interstitial",
id: `${ts}`,
id: `${startDate.toMillis()}`,
startDate,
clientAttributes,
});
}

return dateRanges;
};
});
}

export async function getAssets(session: Session, dateTime: DateTime) {
const assets: InterstitialAsset[] = [];

const interstitials = session.interstitials.filter((interstitial) =>
const interstitial = session.interstitials.find((interstitial) =>
interstitial.dateTime.equals(dateTime),
);

for (const interstitial of interstitials) {
if (interstitial.vast) {
const nextAssets = await getAssetsFromVast(interstitial.vast);
assets.push(...nextAssets);
}
if (interstitial?.vast) {
const nextAssets = await getAssetsFromVast(interstitial.vast);
assets.push(...nextAssets);
}

if (interstitial.asset) {
assets.push(interstitial.asset);
}
if (interstitial?.assets) {
assets.push(...interstitial.assets);
}

return assets;
}

function getGroupedInterstitials(interstitials: Interstitial[]) {
const result = new Map<number, Interstitial[]>();

export function appendInterstitials(
source: Interstitial[],
interstitials: Interstitial[],
) {
for (const interstitial of interstitials) {
const ts = interstitial.dateTime.toMillis();
let items = result.get(ts);
if (!items) {
items = [];
result.set(ts, items);
const target = source.find((item) =>
item.dateTime.equals(interstitial.dateTime),
);

if (!target) {
source.push(interstitial);
continue;
}
items.push(interstitial);
}

return result;
}
if (interstitial.assets) {
if (!target.assets) {
target.assets = interstitial.assets;
} else {
target.assets.push(...interstitial.assets);
}
}

function getAssetListUrl(
dateTime: DateTime,
interstitials: Interstitial[],
session?: Session,
) {
if (interstitials.length === 1 && interstitials[0]?.assetList) {
return interstitials[0].assetList.url;
if (interstitial.vast) {
target.vast = interstitial.vast;
}

if (interstitial.assetList) {
target.assetList = interstitial.assetList;
}
}
}

function getAssetListUrl(interstitial: Interstitial, session?: Session) {
if (interstitial.assetList) {
return interstitial.assetList.url;
}
return createUrl("out/asset-list.json", {
dt: dateTime.toISO(),
dt: interstitial.dateTime.toISO(),
sid: session?.id,
});
}

function getTimelineStyle(interstitials: Interstitial[]) {
for (const interstitial of interstitials) {
if (
// If interstitial is an ad.
interstitial.asset?.kind === "ad" ||
// If interstitial is a VAST, thus it contains ads.
interstitial.vast
) {
return "HIGHLIGHT";
function getTimelineStyle(interstitial: Interstitial) {
if (interstitial.assets) {
for (const asset of interstitial.assets) {
if (asset.kind === "ad") {
return "HIGHLIGHT";
}
}
}

if (interstitial.vast) {
return "HIGHLIGHT";
}

return "PRIMARY";
}
21 changes: 18 additions & 3 deletions packages/stitcher/src/playlist.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { assert } from "shared/assert";
import { filterMasterPlaylist, formatFilterToQueryParam } from "./filters";
import { getAssets, getStaticDateRanges } from "./interstitials";
import {
appendInterstitials,
getAssets,
getStaticDateRanges,
} from "./interstitials";
import { encrypt } from "./lib/crypto";
import { createUrl, joinUrl, resolveUri } from "./lib/url";
import {
Expand All @@ -15,6 +19,7 @@ import { fetchVmap, toAdBreakTimeOffset } from "./vmap";
import type { Filter } from "./filters";
import type { MasterPlaylist, MediaPlaylist, RenditionType } from "./parser";
import type { Session } from "./session";
import type { Interstitial } from "./types";
import type { VmapAdBreak } from "./vmap";
import type { DateTime } from "luxon";

Expand Down Expand Up @@ -190,8 +195,14 @@ async function initSessionOnMasterReq(session: Session) {

if (session.vmap) {
const vmap = await fetchVmap(session.vmap);

delete session.vmap;
mapAdBreaksToSessionInterstitials(session, vmap.adBreaks);

const interstitials = mapAdBreaksToSessionInterstitials(
session,
vmap.adBreaks,
);
appendInterstitials(session.interstitials, interstitials);

storeSession = true;
}
Expand All @@ -205,6 +216,8 @@ export function mapAdBreaksToSessionInterstitials(
session: Session,
adBreaks: VmapAdBreak[],
) {
const interstitials: Interstitial[] = [];

for (const adBreak of adBreaks) {
const timeOffset = toAdBreakTimeOffset(adBreak);

Expand All @@ -214,12 +227,14 @@ export function mapAdBreaksToSessionInterstitials(

const dateTime = session.startTime.plus({ seconds: timeOffset });

session.interstitials.push({
interstitials.push({
dateTime,
vast: {
url: adBreak.vastUrl,
data: adBreak.vastData,
},
});
}

return interstitials;
}
36 changes: 17 additions & 19 deletions packages/stitcher/src/routes/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,31 +37,29 @@ export const sessionRoutes = new Elysia()
}),
interstitials: t.Optional(
t.Array(
t.Intersect([
t.Object({
time: t.Union([t.Number(), t.String()]),
}),
t.Union([
t.Object({
type: t.Literal("asset"),
uri: t.String(),
kind: t.Optional(
t.Union([t.Literal("ad"), t.Literal("bumper")]),
),
}),
t.Object({
time: t.Union([t.Number(), t.String()]),
assets: t.Optional(
t.Array(
t.Object({
uri: t.String(),
kind: t.Optional(
t.Union([t.Literal("ad"), t.Literal("bumper")]),
),
}),
),
),
vast: t.Optional(
t.Object({
type: t.Literal("vast"),
url: t.String(),
}),
),
assetList: t.Optional(
t.Object({
type: t.Literal("assetList"),
url: t.String(),
}),
]),
]),
{
description: "Manual HLS interstitial insertion.",
},
),
}),
),
),
filter: t.Optional(
Expand Down
68 changes: 30 additions & 38 deletions packages/stitcher/src/session.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { randomUUID } from "crypto";
import { DateTime } from "luxon";
import { kv } from "./adapters/kv";
import { appendInterstitials } from "./interstitials";
import { JSON } from "./lib/json";
import { resolveUri } from "./lib/url";
import type { Interstitial } from "./types";
Expand All @@ -17,17 +18,19 @@ export interface Session {
interstitials: Interstitial[];
}

type SessionInterstitial = {
interface SessionInterstitial {
time: number | string;
} & (
| {
type: "asset";
uri: string;
kind?: "ad" | "bumper";
}
| { type: "vast"; url: string }
| { type: "assetList"; url: string }
);
assets?: {
uri: string;
kind?: "ad" | "bumper";
}[];
vast?: {
url: string;
};
assetList?: {
url: string;
};
}

export async function createSession(params: {
uri: string;
Expand All @@ -49,10 +52,11 @@ export async function createSession(params: {
};

if (params.interstitials) {
session.interstitials = mapSessionInterstitials(
const interstitials = mapSessionInterstitials(
startTime,
params.interstitials,
);
appendInterstitials(session.interstitials, interstitials);
}

// We'll initially store the session for 10 minutes, if it's not been consumed
Expand All @@ -79,33 +83,21 @@ export async function updateSession(session: Session) {
function mapSessionInterstitials(
startTime: DateTime,
interstitials: SessionInterstitial[],
): Interstitial[] {
return interstitials.reduce<Interstitial[]>((acc, item) => {
) {
return interstitials.map<Interstitial>((item) => {
const { time, assets, ...rest } = item;
const dateTime =
typeof item.time === "string"
? DateTime.fromISO(item.time)
: startTime.plus({ seconds: item.time });

if (item.type === "asset") {
acc.push({
dateTime,
asset: {
url: resolveUri(item.uri),
kind: item.kind,
},
});
} else if (item.type === "vast") {
acc.push({
dateTime,
vast: { url: item.url },
});
} else if (item.type === "assetList") {
acc.push({
dateTime,
assetList: { url: item.url },
});
}
typeof time === "string"
? DateTime.fromISO(time)
: startTime.plus({ seconds: time });

return acc;
}, []);
return {
dateTime,
assets: assets?.map((asset) => {
const { uri, ...rest } = asset;
return { url: resolveUri(uri), ...rest };
}),
...rest,
};
});
}
10 changes: 6 additions & 4 deletions packages/stitcher/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ export interface InterstitialAsset {
kind?: "ad" | "bumper";
}

export interface InterstitialAssetList {
url: string;
}

export interface Interstitial {
dateTime: DateTime;
asset?: InterstitialAsset;
assets?: InterstitialAsset[];
vast?: InterstitialVast;
assetList?: {
url: string;
};
assetList?: InterstitialAssetList;
}

0 comments on commit 598bb7c

Please sign in to comment.