Skip to content

Commit 2b59303

Browse files
authored
feat: Discriminated unions for interstitials type (#139)
* Added discriminated unions for interstitials type * Added assetList type * Added timeline style * Added tests * New interstitials API * Fix snapshots
1 parent 3383c09 commit 2b59303

File tree

12 files changed

+483
-233
lines changed

12 files changed

+483
-233
lines changed
Lines changed: 74 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,114 +1,118 @@
11
import { createUrl } from "./lib/url";
2-
import { fetchDuration } from "./playlist";
3-
import { getAdMediasFromVast } from "./vast";
2+
import { getAssetsFromVast } from "./vast";
3+
import type { DateRange } from "./parser";
44
import type { Session } from "./session";
5-
import type { Interstitial, InterstitialAssetType } from "./types";
5+
import type { Interstitial, InterstitialAsset } from "./types";
66
import type { DateTime } from "luxon";
77

88
export function getStaticDateRanges(session: Session, isLive: boolean) {
9-
const group: {
10-
dateTime: DateTime;
11-
types: InterstitialAssetType[];
12-
}[] = [];
13-
14-
for (const interstitial of session.interstitials) {
15-
let item = group.find((item) =>
16-
item.dateTime.equals(interstitial.dateTime),
17-
);
18-
19-
if (!item) {
20-
item = {
21-
dateTime: interstitial.dateTime,
22-
types: [],
23-
};
24-
group.push(item);
25-
}
26-
27-
const type = getInterstitialType(interstitial);
28-
if (type && !item.types.includes(type)) {
29-
item.types.push(type);
30-
}
31-
}
32-
33-
return group.map((item) => {
34-
const assetListUrl = createAssetListUrl({
35-
dateTime: item.dateTime,
36-
session,
37-
});
9+
return session.interstitials.map<DateRange>((interstitial) => {
10+
const startDate = interstitial.dateTime;
11+
const assetListUrl = getAssetListUrl(interstitial, session);
3812

3913
const clientAttributes: Record<string, number | string> = {
4014
RESTRICT: "SKIP,JUMP",
4115
"ASSET-LIST": assetListUrl,
42-
CUE: "ONCE",
16+
"CONTENT-MAY-VARY": "YES",
17+
"TIMELINE-OCCUPIES": "POINT",
18+
"TIMELINE-STYLE": getTimelineStyle(interstitial),
4319
};
4420

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

49-
const isPreroll = item.dateTime.equals(session.startTime);
50-
if (isPreroll) {
51-
clientAttributes["CUE"] += ",PRE";
25+
const cue: string[] = ["ONCE"];
26+
if (startDate.equals(session.startTime)) {
27+
cue.push("PRE");
5228
}
5329

54-
if (item.types.length) {
55-
clientAttributes["SPRS-TYPES"] = item.types.join(",");
30+
if (cue.length) {
31+
clientAttributes["CUE"] = cue.join(",");
5632
}
5733

5834
return {
5935
classId: "com.apple.hls.interstitial",
60-
id: `${item.dateTime.toUnixInteger()}`,
61-
startDate: item.dateTime,
36+
id: `${startDate.toMillis()}`,
37+
startDate,
6238
clientAttributes,
6339
};
6440
});
6541
}
6642

6743
export async function getAssets(session: Session, dateTime: DateTime) {
68-
const assets: {
69-
URI: string;
70-
DURATION: number;
71-
"SPRS-TYPE"?: InterstitialAssetType;
72-
}[] = [];
44+
const assets: InterstitialAsset[] = [];
7345

74-
const interstitials = session.interstitials.filter((interstitial) =>
46+
const interstitial = session.interstitials.find((interstitial) =>
7547
interstitial.dateTime.equals(dateTime),
7648
);
7749

50+
if (interstitial?.vast) {
51+
const nextAssets = await getAssetsFromVast(interstitial.vast);
52+
assets.push(...nextAssets);
53+
}
54+
55+
if (interstitial?.assets) {
56+
assets.push(...interstitial.assets);
57+
}
58+
59+
return assets;
60+
}
61+
62+
export function appendInterstitials(
63+
source: Interstitial[],
64+
interstitials: Interstitial[],
65+
) {
7866
for (const interstitial of interstitials) {
79-
const adMedias = await getAdMediasFromVast(interstitial);
80-
for (const adMedia of adMedias) {
81-
assets.push({
82-
URI: adMedia.masterUrl,
83-
DURATION: adMedia.duration,
84-
"SPRS-TYPE": "ad",
85-
});
67+
const target = source.find((item) =>
68+
item.dateTime.equals(interstitial.dateTime),
69+
);
70+
71+
if (!target) {
72+
source.push(interstitial);
73+
continue;
8674
}
8775

88-
if (interstitial.asset) {
89-
assets.push({
90-
URI: interstitial.asset.url,
91-
DURATION: await fetchDuration(interstitial.asset.url),
92-
"SPRS-TYPE": interstitial.asset.type,
93-
});
76+
if (interstitial.assets) {
77+
if (!target.assets) {
78+
target.assets = interstitial.assets;
79+
} else {
80+
target.assets.push(...interstitial.assets);
81+
}
9482
}
95-
}
9683

97-
return assets;
84+
if (interstitial.vast) {
85+
target.vast = interstitial.vast;
86+
}
87+
88+
if (interstitial.assetList) {
89+
target.assetList = interstitial.assetList;
90+
}
91+
}
9892
}
9993

100-
function createAssetListUrl(params: { dateTime: DateTime; session?: Session }) {
94+
function getAssetListUrl(interstitial: Interstitial, session?: Session) {
95+
if (interstitial.assetList) {
96+
return interstitial.assetList.url;
97+
}
10198
return createUrl("out/asset-list.json", {
102-
dt: params.dateTime.toISO(),
103-
sid: params.session?.id,
99+
dt: interstitial.dateTime.toISO(),
100+
sid: session?.id,
104101
});
105102
}
106103

107-
function getInterstitialType(
108-
interstitial: Interstitial,
109-
): InterstitialAssetType | undefined {
110-
if (interstitial.vastData || interstitial.vastUrl) {
111-
return "ad";
104+
function getTimelineStyle(interstitial: Interstitial) {
105+
if (interstitial.assets) {
106+
for (const asset of interstitial.assets) {
107+
if (asset.kind === "ad") {
108+
return "HIGHLIGHT";
109+
}
110+
}
112111
}
113-
return interstitial.asset?.type;
112+
113+
if (interstitial.vast) {
114+
return "HIGHLIGHT";
115+
}
116+
117+
return "PRIMARY";
114118
}

packages/stitcher/src/playlist.ts

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { assert } from "shared/assert";
22
import { filterMasterPlaylist, formatFilterToQueryParam } from "./filters";
3-
import { getAssets, getStaticDateRanges } from "./interstitials";
3+
import {
4+
appendInterstitials,
5+
getAssets,
6+
getStaticDateRanges,
7+
} from "./interstitials";
48
import { encrypt } from "./lib/crypto";
59
import { createUrl, joinUrl, resolveUri } from "./lib/url";
610
import {
@@ -15,6 +19,7 @@ import { fetchVmap, toAdBreakTimeOffset } from "./vmap";
1519
import type { Filter } from "./filters";
1620
import type { MasterPlaylist, MediaPlaylist, RenditionType } from "./parser";
1721
import type { Session } from "./session";
22+
import type { Interstitial } from "./types";
1823
import type { VmapAdBreak } from "./vmap";
1924
import type { DateTime } from "luxon";
2025

@@ -71,8 +76,17 @@ export async function formatMediaPlaylist(
7176

7277
export async function formatAssetList(session: Session, dateTime: DateTime) {
7378
const assets = await getAssets(session, dateTime);
79+
80+
const assetsPromises = assets.map(async (asset) => {
81+
return {
82+
URI: asset.url,
83+
DURATION: asset.duration ?? (await fetchDuration(asset.url)),
84+
"SPRS-KIND": asset.kind,
85+
};
86+
});
87+
7488
return {
75-
ASSETS: assets,
89+
ASSETS: await Promise.all(assetsPromises),
7690
};
7791
}
7892

@@ -181,8 +195,14 @@ async function initSessionOnMasterReq(session: Session) {
181195

182196
if (session.vmap) {
183197
const vmap = await fetchVmap(session.vmap);
198+
184199
delete session.vmap;
185-
mapAdBreaksToSessionInterstitials(session, vmap.adBreaks);
200+
201+
const interstitials = mapAdBreaksToSessionInterstitials(
202+
session,
203+
vmap.adBreaks,
204+
);
205+
appendInterstitials(session.interstitials, interstitials);
186206

187207
storeSession = true;
188208
}
@@ -196,6 +216,8 @@ export function mapAdBreaksToSessionInterstitials(
196216
session: Session,
197217
adBreaks: VmapAdBreak[],
198218
) {
219+
const interstitials: Interstitial[] = [];
220+
199221
for (const adBreak of adBreaks) {
200222
const timeOffset = toAdBreakTimeOffset(adBreak);
201223

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

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

208-
if (adBreak.vastUrl) {
209-
session.interstitials.push({
210-
dateTime,
211-
vastUrl: adBreak.vastUrl,
212-
});
213-
}
214-
215-
if (adBreak.vastData) {
216-
session.interstitials.push({
217-
dateTime,
218-
vastData: adBreak.vastData,
219-
});
220-
}
230+
interstitials.push({
231+
dateTime,
232+
vast: {
233+
url: adBreak.vastUrl,
234+
data: adBreak.vastData,
235+
},
236+
});
221237
}
238+
239+
return interstitials;
222240
}

packages/stitcher/src/routes/session.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,27 @@ export const sessionRoutes = new Elysia()
3939
t.Array(
4040
t.Object({
4141
time: t.Union([t.Number(), t.String()]),
42-
vastUrl: t.Optional(t.String()),
43-
uri: t.Optional(t.String()),
44-
type: t.Optional(t.Union([t.Literal("ad"), t.Literal("bumper")])),
42+
assets: t.Optional(
43+
t.Array(
44+
t.Object({
45+
uri: t.String(),
46+
kind: t.Optional(
47+
t.Union([t.Literal("ad"), t.Literal("bumper")]),
48+
),
49+
}),
50+
),
51+
),
52+
vast: t.Optional(
53+
t.Object({
54+
url: t.String(),
55+
}),
56+
),
57+
assetList: t.Optional(
58+
t.Object({
59+
url: t.String(),
60+
}),
61+
),
4562
}),
46-
{
47-
description: "Manual HLS interstitial insertion.",
48-
},
4963
),
5064
),
5165
filter: t.Optional(

0 commit comments

Comments
 (0)