diff --git a/packages/stitcher/src/interstitials.ts b/packages/stitcher/src/interstitials.ts index de227a97..96ddd258 100644 --- a/packages/stitcher/src/interstitials.ts +++ b/packages/stitcher/src/interstitials.ts @@ -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((interstitial) => { + const startDate = interstitial.dateTime; + const assetListUrl = getAssetListUrl(interstitial, session); const clientAttributes: Record = { RESTRICT: "SKIP,JUMP", "ASSET-LIST": assetListUrl, "CONTENT-MAY-VARY": "YES", "TIMELINE-OCCUPIES": "POINT", - "TIMELINE-STYLE": getTimelineStyle(interstitials), + "TIMELINE-STYLE": getTimelineStyle(interstitial), }; if (!isLive) { @@ -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(); - +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"; } diff --git a/packages/stitcher/src/playlist.ts b/packages/stitcher/src/playlist.ts index bfbcd375..d3352568 100644 --- a/packages/stitcher/src/playlist.ts +++ b/packages/stitcher/src/playlist.ts @@ -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 { @@ -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"; @@ -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; } @@ -205,6 +216,8 @@ export function mapAdBreaksToSessionInterstitials( session: Session, adBreaks: VmapAdBreak[], ) { + const interstitials: Interstitial[] = []; + for (const adBreak of adBreaks) { const timeOffset = toAdBreakTimeOffset(adBreak); @@ -214,7 +227,7 @@ export function mapAdBreaksToSessionInterstitials( const dateTime = session.startTime.plus({ seconds: timeOffset }); - session.interstitials.push({ + interstitials.push({ dateTime, vast: { url: adBreak.vastUrl, @@ -222,4 +235,6 @@ export function mapAdBreaksToSessionInterstitials( }, }); } + + return interstitials; } diff --git a/packages/stitcher/src/routes/session.ts b/packages/stitcher/src/routes/session.ts index f085893d..f1934e5f 100644 --- a/packages/stitcher/src/routes/session.ts +++ b/packages/stitcher/src/routes/session.ts @@ -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( diff --git a/packages/stitcher/src/session.ts b/packages/stitcher/src/session.ts index bf6c6865..f236fcad 100644 --- a/packages/stitcher/src/session.ts +++ b/packages/stitcher/src/session.ts @@ -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"; @@ -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; @@ -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 @@ -79,33 +83,21 @@ export async function updateSession(session: Session) { function mapSessionInterstitials( startTime: DateTime, interstitials: SessionInterstitial[], -): Interstitial[] { - return interstitials.reduce((acc, item) => { +) { + return interstitials.map((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, + }; + }); } diff --git a/packages/stitcher/src/types.ts b/packages/stitcher/src/types.ts index 9aabab02..5326d966 100644 --- a/packages/stitcher/src/types.ts +++ b/packages/stitcher/src/types.ts @@ -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; }