Skip to content

Commit

Permalink
feat: Discriminated unions for interstitials type (#139)
Browse files Browse the repository at this point in the history
* Added discriminated unions for interstitials type

* Added assetList type

* Added timeline style

* Added tests

* New interstitials API

* Fix snapshots
  • Loading branch information
matvp91 authored Dec 10, 2024
1 parent 3383c09 commit 2b59303
Show file tree
Hide file tree
Showing 12 changed files with 483 additions and 233 deletions.
144 changes: 74 additions & 70 deletions packages/stitcher/src/interstitials.ts
Original file line number Diff line number Diff line change
@@ -1,114 +1,118 @@
import { createUrl } from "./lib/url";
import { fetchDuration } from "./playlist";
import { getAdMediasFromVast } from "./vast";
import { getAssetsFromVast } from "./vast";
import type { DateRange } from "./parser";
import type { Session } from "./session";
import type { Interstitial, InterstitialAssetType } from "./types";
import type { Interstitial, InterstitialAsset } from "./types";
import type { DateTime } from "luxon";

export function getStaticDateRanges(session: Session, isLive: boolean) {
const group: {
dateTime: DateTime;
types: InterstitialAssetType[];
}[] = [];

for (const interstitial of session.interstitials) {
let item = group.find((item) =>
item.dateTime.equals(interstitial.dateTime),
);

if (!item) {
item = {
dateTime: interstitial.dateTime,
types: [],
};
group.push(item);
}

const type = getInterstitialType(interstitial);
if (type && !item.types.includes(type)) {
item.types.push(type);
}
}

return group.map((item) => {
const assetListUrl = createAssetListUrl({
dateTime: item.dateTime,
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,
CUE: "ONCE",
"CONTENT-MAY-VARY": "YES",
"TIMELINE-OCCUPIES": "POINT",
"TIMELINE-STYLE": getTimelineStyle(interstitial),
};

if (!isLive) {
clientAttributes["RESUME-OFFSET"] = 0;
}

const isPreroll = item.dateTime.equals(session.startTime);
if (isPreroll) {
clientAttributes["CUE"] += ",PRE";
const cue: string[] = ["ONCE"];
if (startDate.equals(session.startTime)) {
cue.push("PRE");
}

if (item.types.length) {
clientAttributes["SPRS-TYPES"] = item.types.join(",");
if (cue.length) {
clientAttributes["CUE"] = cue.join(",");
}

return {
classId: "com.apple.hls.interstitial",
id: `${item.dateTime.toUnixInteger()}`,
startDate: item.dateTime,
id: `${startDate.toMillis()}`,
startDate,
clientAttributes,
};
});
}

export async function getAssets(session: Session, dateTime: DateTime) {
const assets: {
URI: string;
DURATION: number;
"SPRS-TYPE"?: InterstitialAssetType;
}[] = [];
const assets: InterstitialAsset[] = [];

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

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

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

return assets;
}

export function appendInterstitials(
source: Interstitial[],
interstitials: Interstitial[],
) {
for (const interstitial of interstitials) {
const adMedias = await getAdMediasFromVast(interstitial);
for (const adMedia of adMedias) {
assets.push({
URI: adMedia.masterUrl,
DURATION: adMedia.duration,
"SPRS-TYPE": "ad",
});
const target = source.find((item) =>
item.dateTime.equals(interstitial.dateTime),
);

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

if (interstitial.asset) {
assets.push({
URI: interstitial.asset.url,
DURATION: await fetchDuration(interstitial.asset.url),
"SPRS-TYPE": interstitial.asset.type,
});
if (interstitial.assets) {
if (!target.assets) {
target.assets = interstitial.assets;
} else {
target.assets.push(...interstitial.assets);
}
}
}

return assets;
if (interstitial.vast) {
target.vast = interstitial.vast;
}

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

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

function getInterstitialType(
interstitial: Interstitial,
): InterstitialAssetType | undefined {
if (interstitial.vastData || interstitial.vastUrl) {
return "ad";
function getTimelineStyle(interstitial: Interstitial) {
if (interstitial.assets) {
for (const asset of interstitial.assets) {
if (asset.kind === "ad") {
return "HIGHLIGHT";
}
}
}
return interstitial.asset?.type;

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

return "PRIMARY";
}
50 changes: 34 additions & 16 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 @@ -71,8 +76,17 @@ export async function formatMediaPlaylist(

export async function formatAssetList(session: Session, dateTime: DateTime) {
const assets = await getAssets(session, dateTime);

const assetsPromises = assets.map(async (asset) => {
return {
URI: asset.url,
DURATION: asset.duration ?? (await fetchDuration(asset.url)),
"SPRS-KIND": asset.kind,
};
});

return {
ASSETS: assets,
ASSETS: await Promise.all(assetsPromises),
};
}

Expand Down Expand Up @@ -181,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 @@ -196,6 +216,8 @@ export function mapAdBreaksToSessionInterstitials(
session: Session,
adBreaks: VmapAdBreak[],
) {
const interstitials: Interstitial[] = [];

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

Expand All @@ -205,18 +227,14 @@ export function mapAdBreaksToSessionInterstitials(

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

if (adBreak.vastUrl) {
session.interstitials.push({
dateTime,
vastUrl: adBreak.vastUrl,
});
}

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

return interstitials;
}
26 changes: 20 additions & 6 deletions packages/stitcher/src/routes/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,27 @@ export const sessionRoutes = new Elysia()
t.Array(
t.Object({
time: t.Union([t.Number(), t.String()]),
vastUrl: t.Optional(t.String()),
uri: t.Optional(t.String()),
type: t.Optional(t.Union([t.Literal("ad"), t.Literal("bumper")])),
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({
url: t.String(),
}),
),
assetList: t.Optional(
t.Object({
url: t.String(),
}),
),
}),
{
description: "Manual HLS interstitial insertion.",
},
),
),
filter: t.Optional(
Expand Down
Loading

0 comments on commit 2b59303

Please sign in to comment.