diff --git a/packages/hlsjs/README.md b/packages/hlsjs/README.md index 6197dc17..edc2883c 100644 --- a/packages/hlsjs/README.md +++ b/packages/hlsjs/README.md @@ -11,3 +11,9 @@ Using https://www.svgrepo.com/collection/solar-broken-line-icons for icons. - Selecting audio and quality during interstitials works for the main asset, but subtitles not. - Seeking to `this.getInterstitialsManager_().integrated.duration` does not work properly. + +- InterstitialEvent should contain custom tags (like X-MIX-TYPES). + +- Need to talk about ABR & interstitials, do they share the same instance? Same bandwidth estimation? That might give troubles as for all we know eg; interstitials might be on a different CDN. + +- Custom HLS tags? Is this needed? diff --git a/packages/hlsjs/lib/main.ts b/packages/hlsjs/lib/main.ts index db1baa7b..d1e3b1e1 100644 --- a/packages/hlsjs/lib/main.ts +++ b/packages/hlsjs/lib/main.ts @@ -2,7 +2,7 @@ import Hls from "hls.js"; import update from "immutability-helper"; import EventEmitter from "eventemitter3"; import type { Spec } from "immutability-helper"; -import type { Level, MediaPlaylist } from "hls.js"; +import type { InterstitialScheduleItem, Level, MediaPlaylist } from "hls.js"; export { Root as HlsUi } from "./ui"; @@ -123,10 +123,20 @@ export class HlsFacade extends EventEmitter { }); hls.on(Hls.Events.INTERSTITIALS_UPDATED, (_, data) => { + const isAd = (item: InterstitialScheduleItem) => { + const types = item.event?.dateRange.attr.enumeratedStringList( + "X-MIX-TYPES", + { + ad: false, + }, + ); + return !!types?.ad; + }; + this.setState_({ cuePoints: { $set: data.schedule.reduce((acc, item) => { - if (!acc.includes(item.start)) { + if (isAd(item) && !acc.includes(item.start)) { acc.push(item.start); } return acc; diff --git a/packages/hlsjs/src/main.tsx b/packages/hlsjs/src/main.tsx index 23b85112..3ac92889 100644 --- a/packages/hlsjs/src/main.tsx +++ b/packages/hlsjs/src/main.tsx @@ -15,8 +15,11 @@ Object.assign(window, { facade }); hls.loadSource( // "https://streamer.ams3.cdn.digitaloceanspaces.com/package/846ed9ef-b11f-43a4-9d31-0cecc1b7468c/hls/master.m3u8", - "https://playertest.longtailvideo.com/adaptive/elephants_dream_v4/redundant.m3u8", + // "https://playertest.longtailvideo.com/adaptive/elephants_dream_v4/redundant.m3u8", // "http://127.0.0.1:52002/session/260c015b-966f-4cc4-8373-ab859379a27d/master.m3u8", + // "http://127.0.0.1:52002/session/d8f508ab-8c93-41fa-9afe-63a784477d8b/master.m3u8", + "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8", + // "http://127.0.0.1:52002/session/a7b1551f-5baf-4859-b783-b3c674a77690/master.m3u8", ); const root = ReactDOM.createRoot(document.getElementById("root")!); diff --git a/packages/stitcher/extern/hls-parser/stringify.ts b/packages/stitcher/extern/hls-parser/stringify.ts index c32df2b6..3a03f161 100644 --- a/packages/stitcher/extern/hls-parser/stringify.ts +++ b/packages/stitcher/extern/hls-parser/stringify.ts @@ -370,6 +370,12 @@ function buildMediaPlaylist( `X-RESTRICT="${interstitial.restrict}"`, ); + if (interstitial.custom) { + Object.entries(interstitial.custom).forEach(([key, value]) => { + params.push(`X-${key}="${value}"`); + }); + } + lines.push(`#EXT-X-DATERANGE:${params.join(",")}`); } } diff --git a/packages/stitcher/extern/hls-parser/types.ts b/packages/stitcher/extern/hls-parser/types.ts index ed11d892..8bc93da4 100644 --- a/packages/stitcher/extern/hls-parser/types.ts +++ b/packages/stitcher/extern/hls-parser/types.ts @@ -60,6 +60,7 @@ class Interstitial { startDate: Date; resumeOffset?: number; restrict?: string; + custom?: Record; constructor({ id, @@ -69,6 +70,7 @@ class Interstitial { startDate, resumeOffset = 0, restrict = "SKIP,JUMP", + custom, }: any) { this.id = id; this.uri = uri; @@ -77,6 +79,7 @@ class Interstitial { this.startDate = startDate; this.resumeOffset = resumeOffset; this.restrict = restrict; + this.custom = custom; } } diff --git a/packages/stitcher/src/playlist.ts b/packages/stitcher/src/playlist.ts index 8ec7533e..e2020afd 100644 --- a/packages/stitcher/src/playlist.ts +++ b/packages/stitcher/src/playlist.ts @@ -3,7 +3,7 @@ import parseFilepath from "parse-filepath"; import { env } from "./env.js"; import { MasterPlaylist, MediaPlaylist } from "../extern/hls-parser/types.js"; import createError from "@fastify/error"; -import type { Session, Interstitial } from "./types.js"; +import type { Session, Interstitial, InterstitialType } from "./types.js"; const PlaylistUnavailableError = createError<[string]>( "PLAYLIST_UNAVAILABLE", @@ -80,6 +80,7 @@ export async function formatAssetList(session: Session, timeOffset: number) { return { URI: uri, DURATION: await getDuration(uri), + "MIX-TYPE": interstitial.type, }; }), ); @@ -118,18 +119,40 @@ function addInterstitials( media.segments[0].programDateTime = new Date(now); interstitials - .reduce((acc, interstitial) => { - if (!acc.includes(interstitial.timeOffset)) { - acc.push(interstitial.timeOffset); + .reduce< + { + timeOffset: number; + types: InterstitialType[]; + }[] + >((acc, interstitial) => { + let foundItem = acc.find( + (item) => item.timeOffset === interstitial.timeOffset, + ); + if (!foundItem) { + foundItem = { + timeOffset: interstitial.timeOffset, + types: [], + }; + acc.push(foundItem); + } + + if (interstitial.type && !foundItem.types.includes(interstitial.type)) { + foundItem.types.push(interstitial.type); } + return acc; }, []) - .forEach((timeOffset) => { + .forEach((item) => { + const custom = { + "MIX-TYPES": item.types.join(","), + }; + media.interstitials.push( new hlsParser.types.Interstitial({ - id: `${timeOffset}`, - startDate: new Date(now + timeOffset * 1000), - list: `/session/${sessionId}/asset-list.json?timeOffset=${timeOffset}`, + id: `${item.timeOffset}`, + startDate: new Date(now + item.timeOffset * 1000), + list: `/session/${sessionId}/asset-list.json?timeOffset=${item.timeOffset}`, + custom, }), ); }); diff --git a/packages/stitcher/src/session.ts b/packages/stitcher/src/session.ts index 907eaf6f..402e4973 100644 --- a/packages/stitcher/src/session.ts +++ b/packages/stitcher/src/session.ts @@ -47,6 +47,7 @@ export async function createSession(data: { interstitials.push({ timeOffset: 0, assetId: data.bumperAssetId, + type: "bumper", }); } diff --git a/packages/stitcher/src/types.ts b/packages/stitcher/src/types.ts index f3dc7c5f..1541c63b 100644 --- a/packages/stitcher/src/types.ts +++ b/packages/stitcher/src/types.ts @@ -5,7 +5,10 @@ export type Session = { maxResolution: number; }; +export type InterstitialType = "ad" | "bumper"; + export type Interstitial = { timeOffset: number; assetId: string; + type?: InterstitialType; }; diff --git a/packages/stitcher/src/vast.ts b/packages/stitcher/src/vast.ts index deeca326..32a37762 100644 --- a/packages/stitcher/src/vast.ts +++ b/packages/stitcher/src/vast.ts @@ -22,6 +22,7 @@ export async function extractInterstitialFromVmapAdbreak(adBreak: VmapAdBreak) { interstitials.push({ timeOffset: adBreak.timeOffset, assetId: adMedia.assetId, + type: "ad", }); } else { scheduleForPackage(adMedia);