-
-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Stitcher interstitials support for live streams (#128)
* Stitcher changes * Support for streaming HLS * Wait for session to update * Different interstitial structure * Simplified code
- Loading branch information
Showing
20 changed files
with
1,408 additions
and
300 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
const kv = new Map<string, string>(); | ||
|
||
export async function set(key: string, value: string, ttl: number) { | ||
kv.set(key, value); | ||
await Promise.resolve(ttl); | ||
} | ||
|
||
export async function get(key: string) { | ||
return Promise.resolve(kv.get(key) ?? null); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,152 +1,115 @@ | ||
import { Group } from "./lib/group"; | ||
import { makeUrl, resolveUri } from "./lib/url"; | ||
import { createUrl } from "./lib/url"; | ||
import { fetchDuration } from "./playlist"; | ||
import { getAdMediasFromAdBreak } from "./vast"; | ||
import { toAdBreakTimeOffset } from "./vmap"; | ||
import type { DateRange } from "./parser"; | ||
import { getAdMediasFromVast } from "./vast"; | ||
import type { Session } from "./session"; | ||
import type { AdMedia } from "./vast"; | ||
import type { VmapResponse } from "./vmap"; | ||
import type { Interstitial, InterstitialAssetType } from "./types"; | ||
import type { DateTime } from "luxon"; | ||
|
||
export type InterstitialType = "ad" | "bumper"; | ||
|
||
export interface Interstitial { | ||
timeOffset: number; | ||
url: string; | ||
duration?: number; | ||
type?: InterstitialType; | ||
} | ||
|
||
interface InterstitialAsset { | ||
URI: string; | ||
DURATION: number; | ||
"SPRS-TYPE"?: InterstitialType; | ||
} | ||
|
||
export function getStaticDateRanges(startTime: DateTime, session: Session) { | ||
const group = new Group<number, InterstitialType>(); | ||
|
||
if (session.vmapResponse) { | ||
for (const adBreak of session.vmapResponse.adBreaks) { | ||
const timeOffset = toAdBreakTimeOffset(adBreak); | ||
if (timeOffset !== null) { | ||
group.add(timeOffset, "ad"); | ||
} | ||
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); | ||
} | ||
} | ||
|
||
if (session.interstitials) { | ||
for (const interstitial of session.interstitials) { | ||
group.add(interstitial.timeOffset, interstitial.type); | ||
const type = getInterstitialType(interstitial); | ||
if (type && !item.types.includes(type)) { | ||
item.types.push(type); | ||
} | ||
} | ||
|
||
const dateRanges: DateRange[] = []; | ||
|
||
group.forEach((timeOffset, types) => { | ||
const startDate = startTime.plus({ seconds: timeOffset }); | ||
|
||
const assetListUrl = makeAssetListUrl({ | ||
timeOffset, | ||
return group.map((item) => { | ||
const assetListUrl = createAssetListUrl({ | ||
dateTime: item.dateTime, | ||
session, | ||
}); | ||
|
||
const clientAttributes: Record<string, number | string> = { | ||
RESTRICT: "SKIP,JUMP", | ||
"RESUME-OFFSET": 0, | ||
"ASSET-LIST": assetListUrl, | ||
CUE: "ONCE", | ||
}; | ||
|
||
if (timeOffset === 0) { | ||
const isPreroll = item.dateTime.equals(session.startTime); | ||
|
||
if (!isLive || isPreroll) { | ||
clientAttributes["RESUME-OFFSET"] = 0; | ||
} | ||
|
||
if (isPreroll) { | ||
clientAttributes["CUE"] += ",PRE"; | ||
} | ||
|
||
if (types.length) { | ||
clientAttributes["SPRS-TYPES"] = types.join(","); | ||
if (item.types.length) { | ||
clientAttributes["SPRS-TYPES"] = item.types.join(","); | ||
} | ||
|
||
dateRanges.push({ | ||
return { | ||
classId: "com.apple.hls.interstitial", | ||
id: `sdr${timeOffset}`, | ||
startDate, | ||
id: `${item.dateTime.toUnixInteger()}`, | ||
startDate: item.dateTime, | ||
clientAttributes, | ||
}); | ||
}; | ||
}); | ||
|
||
return dateRanges; | ||
} | ||
|
||
export async function getAssets(session: Session, timeOffset?: number) { | ||
const assets: InterstitialAsset[] = []; | ||
export async function getAssets(session: Session, dateTime: DateTime) { | ||
const assets: { | ||
URI: string; | ||
DURATION: number; | ||
"SPRS-TYPE"?: InterstitialAssetType; | ||
}[] = []; | ||
|
||
if (timeOffset !== undefined) { | ||
if (session.vmapResponse) { | ||
const items = await getAssetsFromVmap(session.vmapResponse, timeOffset); | ||
assets.push(...items); | ||
} | ||
|
||
if (session.interstitials) { | ||
const items = await getAssetsFromGroup(session.interstitials, timeOffset); | ||
assets.push(...items); | ||
} | ||
} | ||
|
||
return assets; | ||
} | ||
|
||
async function getAssetsFromVmap(vmap: VmapResponse, timeOffset: number) { | ||
const adBreaks = vmap.adBreaks.filter( | ||
(adBreak) => toAdBreakTimeOffset(adBreak) === timeOffset, | ||
const interstitials = session.interstitials.filter((interstitial) => | ||
interstitial.dateTime.equals(dateTime), | ||
); | ||
const assets: InterstitialAsset[] = []; | ||
|
||
const adMedias: AdMedia[] = []; | ||
for (const adBreak of adBreaks) { | ||
adMedias.push(...(await getAdMediasFromAdBreak(adBreak))); | ||
} | ||
|
||
for (const adMedia of adMedias) { | ||
assets.push({ | ||
URI: resolveUri(`asset://${adMedia.assetId}`), | ||
DURATION: adMedia.duration, | ||
"SPRS-TYPE": "ad", | ||
}); | ||
} | ||
|
||
return assets; | ||
} | ||
|
||
async function getAssetsFromGroup( | ||
interstitials: Interstitial[], | ||
timeOffset: number, | ||
) { | ||
const assets: InterstitialAsset[] = []; | ||
|
||
for (const interstitial of interstitials) { | ||
if (interstitial.timeOffset !== timeOffset) { | ||
continue; | ||
const adMedias = await getAdMediasFromVast(interstitial); | ||
for (const adMedia of adMedias) { | ||
assets.push({ | ||
URI: adMedia.masterUrl, | ||
DURATION: adMedia.duration, | ||
"SPRS-TYPE": "ad", | ||
}); | ||
} | ||
|
||
let duration = interstitial.duration; | ||
if (!duration) { | ||
duration = await fetchDuration(interstitial.url); | ||
if (interstitial.asset) { | ||
assets.push({ | ||
URI: interstitial.asset.url, | ||
DURATION: await fetchDuration(interstitial.asset.url), | ||
"SPRS-TYPE": interstitial.asset.type, | ||
}); | ||
} | ||
|
||
assets.push({ | ||
URI: interstitial.url, | ||
DURATION: duration, | ||
"SPRS-TYPE": interstitial.type, | ||
}); | ||
} | ||
|
||
return assets; | ||
} | ||
|
||
function makeAssetListUrl(params: { timeOffset: number; session?: Session }) { | ||
return makeUrl("out/asset-list.json", { | ||
timeOffset: params.timeOffset, | ||
function createAssetListUrl(params: { dateTime: DateTime; session?: Session }) { | ||
return createUrl("out/asset-list.json", { | ||
dt: params.dateTime.toISO(), | ||
sid: params.session?.id, | ||
}); | ||
} | ||
|
||
function getInterstitialType( | ||
interstitial: Interstitial, | ||
): InterstitialAssetType | undefined { | ||
if (interstitial.vastData || interstitial.vastUrl) { | ||
return "ad"; | ||
} | ||
return interstitial.asset?.type; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,8 @@ | ||
import { createApiClient } from "@superstreamer/api/client"; | ||
import { env } from "../env"; | ||
|
||
export const api = createApiClient(env.PUBLIC_API_ENDPOINT, { | ||
apiKey: env.SUPER_SECRET, | ||
}); | ||
export const api = env.PUBLIC_API_ENDPOINT | ||
? createApiClient(env.PUBLIC_API_ENDPOINT, { | ||
apiKey: env.SUPER_SECRET, | ||
}) | ||
: null; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,26 +1,29 @@ | ||
export class Group<K = unknown, V = unknown> { | ||
constructor(public map = new Map<K, Set<V>>()) {} | ||
private map_ = new Map<K, Set<V>>(); | ||
|
||
add(key: K, value?: V) { | ||
let set = this.map.get(key); | ||
let set = this.map_.get(key); | ||
if (!set) { | ||
set = new Set(); | ||
this.map.set(key, set); | ||
this.map_.set(key, set); | ||
} | ||
if (value !== undefined) { | ||
set.add(value); | ||
} | ||
} | ||
|
||
forEach(callback: (value: K, items: V[]) => void) { | ||
Array.from(this.map.entries()).forEach(([key, set]) => { | ||
Array.from(this.map_.entries()).forEach(([key, set]) => { | ||
const items = Array.from(set.values()); | ||
callback(key, items); | ||
}); | ||
} | ||
|
||
get(key: K) { | ||
const set = this.map.get(key); | ||
return set ? Array.from(set.values()) : []; | ||
map<R>(callback: (value: K, items: V[]) => R) { | ||
const result: R[] = []; | ||
this.forEach((value, items) => { | ||
result.push(callback(value, items)); | ||
}); | ||
return result; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.