Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cue support #154

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion packages/stitcher/src/interstitials.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createUrl } from "./lib/url";
import { getAssetsFromVast } from "./vast";
import type { DateRange } from "./parser";
import type { CueOut, DateRange, MediaPlaylist } from "./parser";
import type { Session } from "./session";
import type { Interstitial, InterstitialAsset } from "./types";
import type { DateTime } from "luxon";
Expand Down Expand Up @@ -112,3 +112,29 @@ function getTimelineStyle(interstitial: Interstitial) {
}
return "PRIMARY";
}

export function insertInterstitialsFromCuesMap(
cuesMap: {
dateTime: DateTime;
cueOut: CueOut;
}[],
media: MediaPlaylist,
) {
for (const item of cuesMap) {
const clientAttributes: Record<string, number | string> = {
RESTRICT: "SKIP,JUMP",
"ASSET-LIST": createUrl("out/asset-list.json", {
dt: item.dateTime.toISO(),
sid: "live",
}),
// "PLAYOUT-LIMIT": item.cueOut.duration,
};

media.dateRanges.push({
classId: "com.apple.hls.interstitial",
id: `${item.dateTime.toMillis()}`,
startDate: item.dateTime.minus({ milliseconds: 1 }),
clientAttributes,
});
}
}
31 changes: 30 additions & 1 deletion packages/stitcher/src/parser/lexical-parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const EMPTY_TAGS = [
"EXT-X-ENDLIST",
"EXT-X-I-FRAMES-ONLY",
"EXT-X-INDEPENDENT-SEGMENTS",
"EXT-X-CUE-IN",
] as const;

const NUMBER_TAGS = [
Expand All @@ -40,7 +41,12 @@ export type Tag =
| ["EXT-X-STREAM-INF", StreamInf]
| ["EXT-X-MEDIA", Media]
| ["EXT-X-MAP", MediaInitializationSection]
| ["EXT-X-DATERANGE", DateRange];
| ["EXT-X-DATERANGE", DateRange]
| ["EXT-X-CUE-OUT", CueOut];

export interface CueOut {
duration: number;
}

export interface ExtInf {
duration: number;
Expand Down Expand Up @@ -276,6 +282,29 @@ function parseLine(line: string): Tag | null {
];
}

case "EXT-X-CUE-OUT": {
assert(param, "EXT-X-CUE-OUT: no param");

const attrs: Partial<CueOut> = {};

mapAttributes(param, (key, value) => {
switch (key) {
case "DURATION":
attrs.duration = Number.parseFloat(value);
break;
}
});

assert(attrs.duration, "EXT-X-CUE-OUT: no duration");

return [
name,
{
duration: attrs.duration,
},
];
}

default:
return null;
}
Expand Down
18 changes: 17 additions & 1 deletion packages/stitcher/src/parser/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { assert } from "shared/assert";
import { lexicalParse } from "./lexical-parse";
import type { Tag } from "./lexical-parse";
import type {
CueOut,
DateRange,
MasterPlaylist,
MediaInitializationSection,
Expand Down Expand Up @@ -94,7 +95,9 @@ function formatMediaPlaylist(tags: Tag[]): MediaPlaylist {
map = value;
}

if (isSegmentTag(name)) {
// TODO: We can have multiple tags covering a segment, do not select
// a new segmentStart when we already have one. This merely means there are multiple.
if (isSegmentTag(name) && segmentStart === -1) {
segmentStart = index - 1;
}

Expand Down Expand Up @@ -146,6 +149,9 @@ function isSegmentTag(name: Tag[0]) {
case "EXTINF":
case "EXT-X-DISCONTINUITY":
case "EXT-X-PROGRAM-DATE-TIME":
case "EXT-X-CUE-OUT":
case "EXT-X-CUE-IN":
case "EXT-X-MAP":
return true;
}
return false;
Expand All @@ -159,6 +165,8 @@ function parseSegment(
let duration: number | undefined;
let discontinuity: boolean | undefined;
let programDateTime: DateTime | undefined;
let cueOut: CueOut | undefined;
let cueIn: boolean | undefined;

tags.forEach(([name, value]) => {
if (name === "EXTINF") {
Expand All @@ -170,6 +178,12 @@ function parseSegment(
if (name === "EXT-X-PROGRAM-DATE-TIME") {
programDateTime = value;
}
if (name === "EXT-X-CUE-OUT") {
cueOut = value;
}
if (name === "EXT-X-CUE-IN") {
cueIn = true;
}
});

assert(duration, "parseSegment: duration not found");
Expand All @@ -180,6 +194,8 @@ function parseSegment(
discontinuity,
map,
programDateTime,
cueOut,
cueIn,
};
}

Expand Down
8 changes: 8 additions & 0 deletions packages/stitcher/src/parser/stringify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,14 @@ export function stringifyMediaPlaylist(playlist: MediaPlaylist) {
lines.push(`#EXT-X-DISCONTINUITY`);
}

if (segment.cueOut) {
lines.push(`#EXT-X-CUE-OUT:DURATION=${segment.cueOut.duration}`);
}

if (segment.cueIn) {
lines.push("#EXT-X-CUE-IN");
}

if (segment.programDateTime) {
lines.push(`#EXT-X-PROGRAM-DATE-TIME:${segment.programDateTime.toISO()}`);
}
Expand Down
6 changes: 6 additions & 0 deletions packages/stitcher/src/parser/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,18 @@ export interface MediaInitializationSection {
uri: string;
}

export interface CueOut {
duration: number;
}

export interface Segment {
uri: string;
duration: number;
discontinuity?: boolean;
map?: MediaInitializationSection;
programDateTime?: DateTime;
cueOut?: CueOut;
cueIn?: boolean;
}

export type PlaylistType = "EVENT" | "VOD";
Expand Down
49 changes: 48 additions & 1 deletion packages/stitcher/src/playlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { filterMasterPlaylist, formatFilterToQueryParam } from "./filters";
import {
getAssets,
getStaticDateRanges,
insertInterstitialsFromCuesMap,
mergeInterstitials,
} from "./interstitials";
import { encrypt } from "./lib/crypto";
Expand All @@ -14,9 +15,10 @@ import {
stringifyMediaPlaylist,
} from "./parser";
import { updateSession } from "./session";
import { getAssetsFromVast } from "./vast";
import { fetchVmap, toAdBreakTimeOffset } from "./vmap";
import type { Filter } from "./filters";
import type { MasterPlaylist, MediaPlaylist } from "./parser";
import type { CueOut, MasterPlaylist, MediaPlaylist } from "./parser";
import type { Session } from "./session";
import type { Interstitial } from "./types";
import type { VmapAdBreak } from "./vmap";
Expand Down Expand Up @@ -70,6 +72,8 @@ export async function formatMediaPlaylist(

rewriteMediaPlaylistUrls(media, mediaUrl);

rewriteCues(media);

return stringifyMediaPlaylist(media);
}

Expand Down Expand Up @@ -190,6 +194,33 @@ export function rewriteMediaPlaylistUrls(
});
}

function rewriteCues(media: MediaPlaylist) {
const cuesMap: {
dateTime: DateTime;
cueOut: CueOut;
}[] = [];

for (const segment of media.segments) {
if (segment.cueIn) {
delete segment.cueIn;
segment.discontinuity = true;
}
if (segment.cueOut) {
if (!segment.programDateTime) {
throw new Error("No PDT");
}
cuesMap.push({
dateTime: segment.programDateTime,
cueOut: segment.cueOut,
});
delete segment.cueOut;
segment.discontinuity = true;
}
}

insertInterstitialsFromCuesMap(cuesMap, media);
}

async function initSessionOnMasterReq(session: Session) {
let storeSession = false;

Expand Down Expand Up @@ -242,3 +273,19 @@ export function mapAdBreaksToSessionInterstitials(

return interstitials;
}

export async function formatLiveAssetList() {
const randNumber = Math.trunc(Math.random() * 100_000);
const assets = await getAssetsFromVast({
url: `https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/single_ad_samples&sz=640x480&cust_params=sample_ct%3Dlinear&ciu_szs=300x250%2C728x90&gdfp_req=1&output=vast&unviewed_position_start=1&env=vp&impl=s&correlator=${randNumber}`,
});

return {
ASSETS: assets.map((asset) => {
return {
URI: asset.url,
DURATION: asset.duration,
};
}),
};
}
7 changes: 7 additions & 0 deletions packages/stitcher/src/routes/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { decrypt } from "../lib/crypto";
import {
createMasterUrl,
formatAssetList,
formatLiveAssetList,
formatMasterPlaylist,
formatMediaPlaylist,
} from "../playlist";
Expand Down Expand Up @@ -181,6 +182,12 @@ export const sessionRoutes = new Elysia()
"/out/asset-list.json",
async ({ query }) => {
const sessionId = query.sid;

// TODO: THIS IS A BAD IDEA
if (sessionId === "live") {
return await formatLiveAssetList();
}

const dateTime = DateTime.fromISO(query.dt);

const session = await getSession(sessionId);
Expand Down
Loading