Skip to content

Commit 908106a

Browse files
authored
[stitcher] Refactor API structure (#157)
* Added new api structure * Merge vast * Changed some more stuff * Added session related info * Removed unused code * Do not use tseep eval func
1 parent b924320 commit 908106a

File tree

21 files changed

+445
-1244
lines changed

21 files changed

+445
-1244
lines changed

packages/player/src/hls-player.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Hls from "hls.js";
22
import { assert } from "shared/assert";
3-
import { EventEmitter } from "tseep";
3+
import { EventEmitter } from "tseep/lib/ee-safe";
44
import { EventManager } from "./event-manager";
55
import { getLangCode } from "./helpers";
66
import { getState, State } from "./state";
@@ -236,7 +236,7 @@ export class HlsPlayer {
236236
}
237237

238238
get live() {
239-
return this.hls_?.levels[this.hls_.currentLevel]?.details?.live ?? false;
239+
return getState(this.state_, "live");
240240
}
241241

242242
get cuePoints() {
@@ -396,7 +396,9 @@ export class HlsPlayer {
396396
const listen = this.eventManager_.listen(this.media_);
397397

398398
listen("canplay", () => {
399-
this.state_?.setReady();
399+
const live =
400+
this.hls_?.levels[this.hls_.currentLevel]?.details?.live ?? false;
401+
this.state_?.setReady(live);
400402
});
401403

402404
listen("play", () => {

packages/player/src/state.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ interface StateProperties {
3636
volume: number;
3737
seeking: boolean;
3838
cuePoints: number[];
39+
live: boolean;
3940
}
4041

4142
const noState: StateProperties = {
@@ -52,6 +53,7 @@ const noState: StateProperties = {
5253
volume: 1,
5354
seeking: false,
5455
cuePoints: [],
56+
live: false,
5557
};
5658

5759
export class State implements StateProperties {
@@ -61,10 +63,11 @@ export class State implements StateProperties {
6163
this.requestTimingSync();
6264
}
6365

64-
setReady() {
66+
setReady(live: boolean) {
6567
if (this.ready) {
6668
return;
6769
}
70+
this.live = live;
6871
this.ready = true;
6972
this.requestTimingSync();
7073
this.params_.onEvent(Events.READY);
@@ -241,6 +244,7 @@ export class State implements StateProperties {
241244
volume = noState.volume;
242245
seeking = noState.seeking;
243246
cuePoints = noState.cuePoints;
247+
live = noState.live;
244248
}
245249

246250
export function getState<N extends keyof StateProperties>(

packages/stitcher/src/filters.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export interface Filter {
88

99
export function formatFilterToQueryParam(filter?: Filter) {
1010
if (!filter) {
11-
return undefined;
11+
filter = {};
1212
}
1313
return btoa(JSON.stringify(filter));
1414
}
Lines changed: 112 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,90 @@
1-
import { createUrl } from "./lib/url";
2-
import { getAssetsFromVast } from "./vast";
3-
import type { DateRange } from "./parser";
1+
import { DateTime } from "luxon";
2+
import { HashGroup } from "./lib/hash-group";
3+
import { createUrl, replaceUrlParams } from "./lib/url";
4+
import { getAssetsFromVastData, getAssetsFromVastUrl } from "./vast";
5+
import type { DateRange, Segment } from "./parser";
46
import type { Session } from "./session";
5-
import type { Interstitial, InterstitialAsset } from "./types";
6-
import type { DateTime } from "luxon";
7+
import type { Asset } from "./types";
78

8-
export function getStaticDateRanges(session: Session, isLive: boolean) {
9-
return session.interstitials.map<DateRange>((interstitial) => {
10-
const startDate = interstitial.dateTime;
11-
const assetListUrl = getAssetListUrl(interstitial, session);
9+
// An item describes what we'd like to collect for a particular date.
10+
interface DateGroupItem {
11+
timelineStyle: "HIGHLIGHT" | "PRIMARY";
12+
replaceContent: boolean;
13+
maxDuration?: number;
14+
}
15+
16+
export function getStaticDateRanges(
17+
session: Session,
18+
segments: Segment[],
19+
isLive: boolean,
20+
) {
21+
const dateGroup = new HashGroup<number, DateGroupItem>({
22+
getDefaultValue: () => ({
23+
timelineStyle: "PRIMARY",
24+
replaceContent: isLive ? true : false,
25+
}),
26+
});
27+
28+
// Collect dateRanges from splice points.
29+
for (const segment of segments) {
30+
if (segment.spliceInfo?.type !== "OUT" || !segment.programDateTime) {
31+
continue;
32+
}
33+
34+
const key = segment.programDateTime.toMillis();
35+
const item = dateGroup.get(key);
36+
37+
item.timelineStyle = "HIGHLIGHT";
38+
item.maxDuration = segment.spliceInfo.duration;
39+
}
40+
41+
// Collect dateRanges from each event defined in the session.
42+
for (const event of session.events) {
43+
const key = event.dateTime.toMillis();
44+
const item = dateGroup.get(key);
45+
46+
if (event.vast) {
47+
// If we resolved the event by a vast, we know it's an ad and can mark it
48+
// as HIGHLIGHT on the timeline.
49+
item.timelineStyle = "HIGHLIGHT";
50+
}
51+
52+
if (
53+
!event.maxDuration ||
54+
(item.maxDuration && event.maxDuration > item.maxDuration)
55+
) {
56+
// If we have a max duration for this event, we'll save it for this interstitial. Always takes the
57+
// largest maxDuration across events.
58+
item.maxDuration = event.maxDuration;
59+
}
60+
}
61+
62+
return dateGroup.toEntries().map<DateRange>(([key, item]) => {
63+
const startDate = DateTime.fromMillis(key);
64+
65+
const assetListUrl = createUrl("out/asset-list.json", {
66+
dt: startDate.toISO(),
67+
sid: session.id,
68+
mdur: item.maxDuration,
69+
});
1270

1371
const clientAttributes: Record<string, number | string> = {
1472
RESTRICT: "SKIP,JUMP",
1573
"ASSET-LIST": assetListUrl,
1674
"CONTENT-MAY-VARY": "YES",
1775
"TIMELINE-OCCUPIES": "POINT",
18-
"TIMELINE-STYLE": getTimelineStyle(interstitial),
76+
"TIMELINE-STYLE": item.timelineStyle,
1977
};
2078

21-
if (!isLive) {
79+
if (!item.replaceContent) {
2280
clientAttributes["RESUME-OFFSET"] = 0;
2381
}
2482

25-
if (interstitial.duration) {
26-
clientAttributes["PLAYOUT-LIMIT"] = interstitial.duration;
83+
if (item.maxDuration !== undefined) {
84+
clientAttributes["PLAYOUT-LIMIT"] = item.maxDuration;
2785
}
2886

29-
const cue: string[] = ["ONCE"];
87+
const cue: string[] = [];
3088
if (startDate.equals(session.startTime)) {
3189
cue.push("PRE");
3290
}
@@ -44,71 +102,53 @@ export function getStaticDateRanges(session: Session, isLive: boolean) {
44102
});
45103
}
46104

47-
export async function getAssets(session: Session, dateTime: DateTime) {
48-
const interstitial = session.interstitials.find((interstitial) =>
49-
interstitial.dateTime.equals(dateTime),
105+
export async function getAssets(
106+
session: Session,
107+
dateTime: DateTime,
108+
maxDuration?: number,
109+
): Promise<Asset[]> {
110+
// Filter all events for a particular dateTime, we'll need to transform these to
111+
// a list of assets.
112+
const events = session.events.filter((event) =>
113+
event.dateTime.equals(dateTime),
50114
);
51115

52-
if (!interstitial) {
53-
return [];
54-
}
55-
56-
const assets: InterstitialAsset[] = [];
57-
58-
for (const chunk of interstitial.chunks) {
59-
if (chunk.type === "vast") {
60-
const nextAssets = await getAssetsFromVast(chunk.data);
61-
assets.push(...nextAssets);
62-
}
63-
if (chunk.type === "asset") {
64-
assets.push(chunk.data);
116+
const assets: Asset[] = [];
117+
118+
for (const event of events) {
119+
if (event.vast) {
120+
const { url, data } = event.vast;
121+
122+
// The event contains a VAST url.
123+
if (url) {
124+
const vastUrl = replaceUrlParams(url, {
125+
maxDuration,
126+
});
127+
const vastAssets = await getAssetsFromVastUrl(vastUrl);
128+
assets.push(...vastAssets);
129+
}
130+
131+
// The event contains inline VAST data.
132+
if (data) {
133+
const vastAssets = await getAssetsFromVastData(data);
134+
assets.push(...vastAssets);
135+
}
65136
}
66-
}
67137

68-
return assets;
69-
}
70-
71-
export function mergeInterstitials(
72-
source: Interstitial[],
73-
interstitials: Interstitial[],
74-
) {
75-
for (const interstitial of interstitials) {
76-
const target = source.find((item) =>
77-
item.dateTime.equals(interstitial.dateTime),
78-
);
79-
80-
if (!target) {
81-
source.push(interstitial);
82-
} else {
83-
// If we found a source for the particular dateTime, we push the
84-
// other chunks at the end.
85-
target.chunks.push(...interstitial.chunks);
138+
// The event contains a list of assets, explicitly defined.
139+
if (event.assets) {
140+
assets.push(...event.assets);
86141
}
87142
}
88-
}
89143

90-
function getAssetListUrl(interstitial: Interstitial, session?: Session) {
91-
const assetListChunks = interstitial.chunks.filter(
92-
(chunk) => chunk.type === "assetList",
93-
);
94-
if (assetListChunks.length === 1 && assetListChunks[0]) {
95-
return assetListChunks[0].data.url;
144+
// If we have a generic vast config on our session, use that one to resolve (eg; for live streams)
145+
if (session.vast?.url) {
146+
const vastUrl = replaceUrlParams(session.vast.url, {
147+
maxDuration,
148+
});
149+
const tempAssets = await getAssetsFromVastUrl(vastUrl);
150+
assets.push(...tempAssets);
96151
}
97152

98-
return createUrl("out/asset-list.json", {
99-
dt: interstitial.dateTime.toISO(),
100-
sid: session?.id,
101-
});
102-
}
103-
104-
function getTimelineStyle(interstitial: Interstitial) {
105-
for (const chunk of interstitial.chunks) {
106-
if (chunk.type === "asset" && chunk.data.kind === "ad") {
107-
return "HIGHLIGHT";
108-
}
109-
if (chunk.type === "vast") {
110-
return "HIGHLIGHT";
111-
}
112-
}
113-
return "PRIMARY";
153+
return assets;
114154
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export class HashGroup<K, T> {
2+
private map_ = new Map<K, T>();
3+
4+
constructor(
5+
private params_: {
6+
getDefaultValue: () => T;
7+
},
8+
) {}
9+
10+
get(key: K) {
11+
let value = this.map_.get(key);
12+
if (!value) {
13+
value = this.params_.getDefaultValue();
14+
this.map_.set(key, value);
15+
}
16+
return value;
17+
}
18+
19+
toEntries() {
20+
return [...this.map_.entries()];
21+
}
22+
}

packages/stitcher/src/lib/url.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,23 @@ export function createUrl(
6565
) {
6666
return buildUrl(`${env.PUBLIC_STITCHER_ENDPOINT}/${path}`, params);
6767
}
68+
69+
export function replaceUrlParams(
70+
url: string,
71+
params?: Record<string, string | number | undefined>,
72+
) {
73+
const allParams = {
74+
...params,
75+
// Default params defined below.
76+
random: Math.floor(Math.random() * 10_000),
77+
};
78+
79+
Object.entries(allParams).forEach(([key, value]) => {
80+
if (value === undefined) {
81+
return;
82+
}
83+
url = url.replaceAll(`{${key}}`, value.toString());
84+
});
85+
86+
return url;
87+
}

packages/stitcher/src/parser/lexical-parse.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const EMPTY_TAGS = [
1919
"EXT-X-ENDLIST",
2020
"EXT-X-I-FRAMES-ONLY",
2121
"EXT-X-INDEPENDENT-SEGMENTS",
22+
"EXT-X-CUE-IN",
2223
] as const;
2324

2425
const NUMBER_TAGS = [
@@ -40,7 +41,8 @@ export type Tag =
4041
| ["EXT-X-STREAM-INF", StreamInf]
4142
| ["EXT-X-MEDIA", Media]
4243
| ["EXT-X-MAP", MediaInitializationSection]
43-
| ["EXT-X-DATERANGE", DateRange];
44+
| ["EXT-X-DATERANGE", DateRange]
45+
| ["EXT-X-CUE-OUT", CueOut];
4446

4547
export interface ExtInf {
4648
duration: number;
@@ -63,6 +65,10 @@ export interface Media {
6365
channels?: string;
6466
}
6567

68+
export interface CueOut {
69+
duration: number;
70+
}
71+
6672
function parseLine(line: string): Tag | null {
6773
const [name, param] = splitLine(line);
6874

@@ -276,6 +282,29 @@ function parseLine(line: string): Tag | null {
276282
];
277283
}
278284

285+
case "EXT-X-CUE-OUT": {
286+
assert(param, "EXT-X-CUE-OUT: no param");
287+
288+
const attrs: Partial<CueOut> = {};
289+
290+
mapAttributes(param, (key, value) => {
291+
switch (key) {
292+
case "DURATION":
293+
attrs.duration = Number.parseFloat(value);
294+
break;
295+
}
296+
});
297+
298+
assert(attrs.duration, "EXT-X-CUE-OUT: no duration");
299+
300+
return [
301+
name,
302+
{
303+
duration: attrs.duration,
304+
},
305+
];
306+
}
307+
279308
default:
280309
return null;
281310
}

0 commit comments

Comments
 (0)