diff --git a/packages/stitcher/src/filters.ts b/packages/stitcher/src/filters.ts index 0df125cd..12ad152f 100644 --- a/packages/stitcher/src/filters.ts +++ b/packages/stitcher/src/filters.ts @@ -85,10 +85,14 @@ export function filterMasterPlaylist(master: MasterPlaylist, filter: Filter) { } if (filter.audioLanguage !== undefined) { const list = parseFilterToList(filter.audioLanguage); - master.variants.filter((variant) => { - variant.audio = variant.audio.filter( - (audio) => !audio.language || list.includes(audio.language), - ); + master.renditions = master.renditions.filter((rendition) => { + if (rendition.type === "AUDIO") { + if (rendition.language && list.includes(rendition.language)) { + return true; + } + return false; + } + return true; }); } } diff --git a/packages/stitcher/src/parser/helpers.ts b/packages/stitcher/src/parser/helpers.ts index 5e2e03c3..66d9de21 100644 --- a/packages/stitcher/src/parser/helpers.ts +++ b/packages/stitcher/src/parser/helpers.ts @@ -1,18 +1,3 @@ -import type { Rendition, Variant } from "./types"; - -export function getRenditions(variants: Variant[]) { - const group = new Set(); - variants.forEach((variant) => { - variant.audio.forEach((rendition) => { - group.add(rendition); - }); - variant.subtitles.forEach((rendition) => { - group.add(rendition); - }); - }); - return Array.from(group.values()); -} - export function mapAttributes( param: string, callback: (key: string, value: string) => void, diff --git a/packages/stitcher/src/parser/index.ts b/packages/stitcher/src/parser/index.ts index c0559a96..fb2ebb97 100644 --- a/packages/stitcher/src/parser/index.ts +++ b/packages/stitcher/src/parser/index.ts @@ -1,5 +1,4 @@ export { parseMasterPlaylist, parseMediaPlaylist } from "./parse"; export { stringifyMasterPlaylist, stringifyMediaPlaylist } from "./stringify"; -export { getRenditions } from "./helpers"; export * from "./types"; diff --git a/packages/stitcher/src/parser/lexical-parse.ts b/packages/stitcher/src/parser/lexical-parse.ts index 5ec1842e..c14294fd 100644 --- a/packages/stitcher/src/parser/lexical-parse.ts +++ b/packages/stitcher/src/parser/lexical-parse.ts @@ -54,14 +54,12 @@ export interface StreamInf { subtitles?: string; } -export type MediaType = "AUDIO" | "SUBTITLES"; - export interface Media { - type: MediaType; + type: "AUDIO" | "SUBTITLES"; groupId: string; name: string; language?: string; - uri: string; + uri?: string; channels?: string; } @@ -187,7 +185,6 @@ function parseLine(line: string): Tag | null { assert(attrs.type, "EXT-X-MEDIA: no type"); assert(attrs.groupId, "EXT-X-MEDIA: no groupId"); assert(attrs.name, "EXT-X-MEDIA: no name"); - assert(attrs.uri, "EXT-X-MEDIA: no uri"); return [ name, diff --git a/packages/stitcher/src/parser/parse.ts b/packages/stitcher/src/parser/parse.ts index 544c3fbe..b58dc4b3 100644 --- a/packages/stitcher/src/parser/parse.ts +++ b/packages/stitcher/src/parser/parse.ts @@ -1,6 +1,6 @@ import { assert } from "shared/assert"; import { lexicalParse } from "./lexical-parse"; -import type { Media, StreamInf, Tag } from "./lexical-parse"; +import type { Tag } from "./lexical-parse"; import type { DateRange, MasterPlaylist, @@ -13,6 +13,45 @@ import type { } from "./types"; import type { DateTime } from "luxon"; +function formatMasterPlaylist(tags: Tag[]): MasterPlaylist { + let independentSegments = false; + const variants: Variant[] = []; + const renditions: Rendition[] = []; + + tags.forEach(([name, value], index) => { + if (name === "EXT-X-INDEPENDENT-SEGMENTS") { + independentSegments = true; + } + if (name === "EXT-X-MEDIA") { + renditions.push({ + type: value.type, + groupId: value.groupId, + name: value.name, + uri: value.uri, + channels: value.channels, + language: value.language, + }); + } + if (name === "EXT-X-STREAM-INF") { + const uri = nextLiteral(tags, index); + variants.push({ + uri, + bandwidth: value.bandwidth, + resolution: value.resolution, + codecs: value.codecs, + audio: value.audio, + subtitles: value.subtitles, + }); + } + }); + + return { + independentSegments, + variants, + renditions, + }; +} + function formatMediaPlaylist(tags: Tag[]): MediaPlaylist { let targetDuration: number | undefined; let endlist = false; @@ -20,7 +59,6 @@ function formatMediaPlaylist(tags: Tag[]): MediaPlaylist { let independentSegments = false; let mediaSequenceBase: number | undefined; let discontinuitySequenceBase: number | undefined; - let map: MediaInitializationSection | undefined; const dateRanges: DateRange[] = []; tags.forEach(([name, value]) => { @@ -33,9 +71,6 @@ function formatMediaPlaylist(tags: Tag[]): MediaPlaylist { if (name === "EXT-X-PLAYLIST-TYPE") { playlistType = value; } - if (name === "EXT-X-MAP") { - map = value; - } if (name === "EXT-X-INDEPENDENT-SEGMENTS") { independentSegments = true; } @@ -53,7 +88,12 @@ function formatMediaPlaylist(tags: Tag[]): MediaPlaylist { const segments: Segment[] = []; let segmentStart = -1; - tags.forEach(([name], index) => { + let map: MediaInitializationSection | undefined; + tags.forEach(([name, value], index) => { + if (name === "EXT-X-MAP") { + map = value; + } + if (isSegmentTag(name)) { segmentStart = index - 1; } @@ -86,6 +126,31 @@ function formatMediaPlaylist(tags: Tag[]): MediaPlaylist { }; } +function nextLiteral(tags: Tag[], index: number) { + if (!tags[index + 1]) { + throw new Error("Expecting next tag to be found"); + } + const tag = tags[index + 1]; + if (!tag) { + throw new Error(`Expected valid tag on ${index + 1}`); + } + const [name, value] = tag; + if (name !== "LITERAL") { + throw new Error("Expecting next tag to be a literal"); + } + return value; +} + +function isSegmentTag(name: Tag[0]) { + switch (name) { + case "EXTINF": + case "EXT-X-DISCONTINUITY": + case "EXT-X-PROGRAM-DATE-TIME": + return true; + } + return false; +} + function parseSegment( tags: Tag[], uri: string, @@ -118,120 +183,6 @@ function parseSegment( }; } -function createRendition(media: Media, renditions: Map) { - let rendition = renditions.get(media.uri); - if (rendition) { - return rendition; - } - - rendition = { - type: media.type, - groupId: media.groupId, - name: media.name, - language: media.language, - uri: media.uri, - channels: media.channels, - }; - - renditions.set(media.uri, rendition); - - return rendition; -} - -function addRendition( - variant: Variant, - media: Media, - renditions: Map, -) { - const rendition = createRendition(media, renditions); - - if (media.type === "AUDIO") { - variant.audio.push(rendition); - } - - if (media.type === "SUBTITLES") { - variant.subtitles.push(rendition); - } -} - -function parseVariant( - tags: Tag[], - streamInf: StreamInf, - uri: string, - renditions: Map, -) { - const variant: Variant = { - uri, - bandwidth: streamInf.bandwidth, - resolution: streamInf.resolution, - codecs: streamInf.codecs, - audio: [], - subtitles: [], - }; - - for (const [name, value] of tags) { - if (name === "EXT-X-MEDIA") { - if ( - streamInf.audio === value.groupId || - streamInf.subtitles === value.groupId - ) { - addRendition(variant, value, renditions); - } - } - } - - return variant; -} - -function formatMasterPlaylist(tags: Tag[]): MasterPlaylist { - const variants: Variant[] = []; - let independentSegments = false; - - const renditions = new Map(); - - tags.forEach(([name, value], index) => { - if (name === "EXT-X-STREAM-INF") { - const uri = nextLiteral(tags, index); - const variant = parseVariant(tags, value, uri, renditions); - variants.push(variant); - } - if (name === "EXT-X-INDEPENDENT-SEGMENTS") { - independentSegments = true; - } - }); - - return { - independentSegments, - variants, - }; -} - -function nextLiteral(tags: Tag[], index: number) { - if (!tags[index + 1]) { - throw new Error("Expecting next tag to be found"); - } - const tag = tags[index + 1]; - if (!tag) { - throw new Error(`Expected valid tag on ${index + 1}`); - } - const [name, value] = tag; - if (name !== "LITERAL") { - throw new Error("Expecting next tag to be a literal"); - } - return value; -} - -function isSegmentTag(name: Tag[0]) { - switch (name) { - case "EXTINF": - case "EXT-X-DISCONTINUITY": - case "EXT-X-MAP": - case "EXT-X-PROGRAM-DATE-TIME": - return true; - } - return false; -} - export function parseMasterPlaylist(text: string) { const tags = lexicalParse(text); return formatMasterPlaylist(tags); diff --git a/packages/stitcher/src/parser/stringify.ts b/packages/stitcher/src/parser/stringify.ts index d5767fda..d08e09c1 100644 --- a/packages/stitcher/src/parser/stringify.ts +++ b/packages/stitcher/src/parser/stringify.ts @@ -1,68 +1,11 @@ -import { assert } from "shared/assert"; -import { Lines } from "./lines"; import type { - DateRange, MasterPlaylist, MediaInitializationSection, MediaPlaylist, - Rendition, - Segment, - Variant, } from "./types"; -function buildRendition(lines: Lines, rendition: Rendition) { - const attrs = [ - `TYPE=${rendition.type}`, - `GROUP-ID="${rendition.groupId}"`, - `NAME="${rendition.name}"`, - ]; - if (rendition.language) { - attrs.push(`LANGUAGE="${rendition.language}"`); - } - if (rendition.uri) { - attrs.push(`URI="${rendition.uri}"`); - } - if (rendition.channels) { - attrs.push(`CHANNELS="${rendition.channels}"`); - } - lines.push(`#EXT-X-MEDIA:${attrs.join(",")}`); -} - -function buildVariant(lines: Lines, variant: Variant) { - const attrs = [`BANDWIDTH=${variant.bandwidth}`]; - - if (variant.codecs) { - attrs.push(`CODECS="${variant.codecs}"`); - } - - if (variant.resolution) { - attrs.push( - `RESOLUTION=${variant.resolution.width}x${variant.resolution.height}`, - ); - } - - if (variant.audio.length) { - assert(variant.audio[0]); - attrs.push(`AUDIO="${variant.audio[0].groupId}"`); - for (const rendition of variant.audio) { - buildRendition(lines, rendition); - } - } - - if (variant.subtitles.length) { - assert(variant.subtitles[0]); - attrs.push(`SUBTITLES="${variant.subtitles[0].groupId}"`); - for (const rendition of variant.subtitles) { - buildRendition(lines, rendition); - } - } - - lines.push(`#EXT-X-STREAM-INF:${attrs.join(",")}`); - lines.push(variant.uri); -} - export function stringifyMasterPlaylist(playlist: MasterPlaylist) { - const lines = new Lines(); + const lines: string[] = []; lines.push("#EXTM3U", "#EXT-X-VERSION:8"); @@ -70,65 +13,66 @@ export function stringifyMasterPlaylist(playlist: MasterPlaylist) { lines.push("#EXT-X-INDEPENDENT-SEGMENTS"); } - playlist.variants.forEach((variant) => { - buildVariant(lines, variant); + playlist.renditions.forEach((rendition) => { + const attrs = [ + `TYPE=${rendition.type}`, + `GROUP-ID="${rendition.groupId}"`, + `NAME="${rendition.name}"`, + ]; + if (rendition.language) { + attrs.push(`LANGUAGE="${rendition.language}"`); + } + if (rendition.uri) { + attrs.push(`URI="${rendition.uri}"`); + } + if (rendition.channels) { + attrs.push(`CHANNELS="${rendition.channels}"`); + } + lines.push(`#EXT-X-MEDIA:${attrs.join(",")}`); }); - return lines.join("\n"); -} - -function buildSegment(lines: Lines, segment: Segment) { - if (segment.discontinuity) { - lines.push(`#EXT-X-DISCONTINUITY`); - } - - if (segment.map) { - buildMap(lines, segment.map); - } - - if (segment.programDateTime) { - lines.push(`#EXT-X-PROGRAM-DATE-TIME:${segment.programDateTime.toISO()}`); - } - - let duration = segment.duration.toFixed(3); - if (duration.match(/\./)) { - duration = duration.replace(/\.?0+$/, ""); - } - - lines.push(`#EXTINF:${duration}`); - - lines.push(segment.uri); -} - -function buildMap(lines: Lines, map: MediaInitializationSection) { - const attrs = [`URI="${map.uri}"`]; - lines.push(`#EXT-X-MAP:${attrs.join(",")}`); -} - -function buildDateRange(lines: Lines, dateRange: DateRange) { - const attrs = [ - `ID="${dateRange.id}"`, - `CLASS="${dateRange.classId}"`, - `START-DATE="${dateRange.startDate.toISO()}"`, - ]; - - if (dateRange.clientAttributes) { - const entries = Object.entries(dateRange.clientAttributes); - for (const [key, value] of entries) { - if (typeof value === "string") { - attrs.push(`X-${key}="${value}"`); + playlist.variants.forEach((variant) => { + const attrs = [`BANDWIDTH=${variant.bandwidth}`]; + if (variant.codecs) { + attrs.push(`CODECS="${variant.codecs}"`); + } + if (variant.resolution) { + attrs.push( + `RESOLUTION=${variant.resolution.width}x${variant.resolution.height}`, + ); + } + if (variant.audio) { + if ( + !playlist.renditions.find( + (rendition) => + rendition.type === "AUDIO" && rendition.groupId === variant.audio, + ) + ) { + return; } - if (typeof value === "number") { - attrs.push(`X-${key}=${value}`); + attrs.push(`AUDIO="${variant.audio}"`); + } + if (variant.subtitles) { + if ( + !playlist.renditions.find( + (rendition) => + rendition.type === "SUBTITLES" && + rendition.groupId === variant.subtitles, + ) + ) { + return; } + attrs.push(`SUBTITLES="${variant.subtitles}"`); } - } + lines.push(`#EXT-X-STREAM-INF:${attrs.join(",")}`); + lines.push(variant.uri); + }); - lines.push(`#EXT-X-DATERANGE:${attrs.join(",")}`); + return lines.join("\n"); } export function stringifyMediaPlaylist(playlist: MediaPlaylist) { - const lines = new Lines(); + const lines: string[] = []; lines.push( "#EXTM3U", @@ -154,8 +98,35 @@ export function stringifyMediaPlaylist(playlist: MediaPlaylist) { lines.push(`#EXT-X-PLAYLIST-TYPE:${playlist.playlistType}`); } + let lastMap: MediaInitializationSection | undefined; + playlist.segments.forEach((segment) => { - buildSegment(lines, segment); + // See https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-16#section-4.4.4.5 + // It applies to every Media Segment that appears after it in the Playlist until the next + // EXT-X-MAP tag or until the end of the Playlist. + if (segment.map !== lastMap) { + if (segment.map) { + const attrs = [`URI="${segment.map.uri}"`]; + lines.push(`#EXT-X-MAP:${attrs.join(",")}`); + } + lastMap = segment.map; + } + + if (segment.discontinuity) { + lines.push(`#EXT-X-DISCONTINUITY`); + } + + if (segment.programDateTime) { + lines.push(`#EXT-X-PROGRAM-DATE-TIME:${segment.programDateTime.toISO()}`); + } + + let duration = segment.duration.toFixed(3); + if (duration.match(/\./)) { + duration = duration.replace(/\.?0+$/, ""); + } + lines.push(`#EXTINF:${duration}`); + + lines.push(segment.uri); }); if (playlist.endlist) { @@ -163,7 +134,25 @@ export function stringifyMediaPlaylist(playlist: MediaPlaylist) { } playlist.dateRanges.forEach((dateRange) => { - buildDateRange(lines, dateRange); + const attrs = [ + `ID="${dateRange.id}"`, + `CLASS="${dateRange.classId}"`, + `START-DATE="${dateRange.startDate.toISO()}"`, + ]; + + if (dateRange.clientAttributes) { + const entries = Object.entries(dateRange.clientAttributes); + for (const [key, value] of entries) { + if (typeof value === "string") { + attrs.push(`X-${key}="${value}"`); + } + if (typeof value === "number") { + attrs.push(`X-${key}=${value}`); + } + } + } + + lines.push(`#EXT-X-DATERANGE:${attrs.join(",")}`); }); return lines.join("\n"); diff --git a/packages/stitcher/src/parser/types.ts b/packages/stitcher/src/parser/types.ts index c4729599..96df9998 100644 --- a/packages/stitcher/src/parser/types.ts +++ b/packages/stitcher/src/parser/types.ts @@ -5,14 +5,12 @@ export interface Resolution { height: number; } -export type RenditionType = "AUDIO" | "SUBTITLES"; - export interface Rendition { - type: RenditionType; groupId: string; name: string; + type: "AUDIO" | "SUBTITLES"; + uri?: string; language?: string; - uri: string; channels?: string; } @@ -21,13 +19,14 @@ export interface Variant { bandwidth: number; codecs?: string; resolution?: Resolution; - audio: Rendition[]; - subtitles: Rendition[]; + audio?: string; + subtitles?: string; } export interface MasterPlaylist { independentSegments?: boolean; variants: Variant[]; + renditions: Rendition[]; } export interface MediaInitializationSection { diff --git a/packages/stitcher/src/playlist.ts b/packages/stitcher/src/playlist.ts index d3352568..423f7ecf 100644 --- a/packages/stitcher/src/playlist.ts +++ b/packages/stitcher/src/playlist.ts @@ -8,7 +8,6 @@ import { import { encrypt } from "./lib/crypto"; import { createUrl, joinUrl, resolveUri } from "./lib/url"; import { - getRenditions, parseMasterPlaylist, parseMediaPlaylist, stringifyMasterPlaylist, @@ -17,7 +16,7 @@ import { import { updateSession } from "./session"; import { fetchVmap, toAdBreakTimeOffset } from "./vmap"; import type { Filter } from "./filters"; -import type { MasterPlaylist, MediaPlaylist, RenditionType } from "./parser"; +import type { MasterPlaylist, MediaPlaylist } from "./parser"; import type { Session } from "./session"; import type { Interstitial } from "./types"; import type { VmapAdBreak } from "./vmap"; @@ -142,7 +141,7 @@ export function createMasterUrl(params: { function createMediaUrl(params: { url: string; sessionId?: string; - type?: RenditionType; + type?: "AUDIO" | "SUBTITLES"; }) { return createUrl("out/playlist.m3u8", { eurl: encrypt(params.url), @@ -166,16 +165,17 @@ export function rewriteMasterPlaylistUrls( }); } - const renditions = getRenditions(master.variants); - - renditions.forEach((rendition) => { + for (const rendition of master.renditions) { + if (!rendition.uri) { + continue; + } const url = joinUrl(params.origUrl, rendition.uri); rendition.uri = createMediaUrl({ url, sessionId: params.session?.id, type: rendition.type, }); - }); + } } export function rewriteMediaPlaylistUrls( diff --git a/packages/stitcher/test/__snapshots__/interstitials.test.ts.snap b/packages/stitcher/test/__snapshots__/interstitials.test.ts.snap index 15824c82..650a73d2 100644 --- a/packages/stitcher/test/__snapshots__/interstitials.test.ts.snap +++ b/packages/stitcher/test/__snapshots__/interstitials.test.ts.snap @@ -1,18 +1,5 @@ // Bun Snapshot v1, https://goo.gl/fbAQLP -exports[`getAssets should get assets by interstitials 1`] = ` -[ - { - "duration": 25, - "url": "https://mock.com/ad_1/master.m3u8", - }, - { - "kind": "ad", - "url": "https://mock.com/interstitial1/master.m3u8", - }, -] -`; - exports[`getStaticDateRanges should create dateRanges for vod 1`] = ` [ { @@ -616,3 +603,16 @@ exports[`getStaticDateRanges should create dateRanges for live 1`] = ` }, ] `; + +exports[`getAssets should get assets by interstitials 1`] = ` +[ + { + "duration": 25, + "url": "https://mock.com/ad_1/master.m3u8", + }, + { + "kind": "ad", + "url": "https://mock.com/interstitial1/master.m3u8", + }, +] +`; diff --git a/packages/stitcher/test/__snapshots__/playlist.test.ts.snap b/packages/stitcher/test/__snapshots__/playlist.test.ts.snap index 8345dd43..44626b3a 100644 --- a/packages/stitcher/test/__snapshots__/playlist.test.ts.snap +++ b/packages/stitcher/test/__snapshots__/playlist.test.ts.snap @@ -2,25 +2,25 @@ exports[`rewriteMasterPlaylistUrls should rewrite 1`] = ` { + "renditions": [ + { + "groupId": "group_1", + "name": "audio_1", + "type": "AUDIO", + "uri": "stitcher-endpoint/out/playlist.m3u8?eurl=MDcxZDFiMTA1OTVkNWExNDBjMTkxNjVjMTgwYTEyNWIwODBkMWIxMDBjNWExNDQxMGE1Yg%3D%3D&type=AUDIO", + }, + { + "groupId": "group_2", + "name": "subtitles_1", + "type": "SUBTITLES", + "uri": "stitcher-endpoint/out/playlist.m3u8?eurl=MDcxZDFiMTA1OTVkNWExNDBjMTkxNjVjMTgwYTEyNWIwNDBkMTkxZDE2MGExNTFkMTI0ZDEyNDcxYzRh&type=SUBTITLES", + }, + ], "variants": [ { - "audio": [ - { - "groupId": "group_1", - "name": "audio_1", - "type": "AUDIO", - "uri": "stitcher-endpoint/out/playlist.m3u8?eurl=MDcxZDFiMTA1OTVkNWExNDBjMTkxNjVjMTgwYTEyNWIwODBkMWIxMDBjNWExNDQxMGE1Yg%3D%3D&type=AUDIO", - }, - ], + "audio": "group_1", "bandwidth": 1000, - "subtitles": [ - { - "groupId": "group_1", - "name": "subtitles_1", - "type": "SUBTITLES", - "uri": "stitcher-endpoint/out/playlist.m3u8?eurl=MDcxZDFiMTA1OTVkNWExNDBjMTkxNjVjMTgwYTEyNWIwNDBkMTkxZDE2MGExNTFkMTI0ZDEyNDcxYzRh&type=SUBTITLES", - }, - ], + "subtitles": "group_2", "uri": "stitcher-endpoint/out/playlist.m3u8?eurl=MDcxZDFiMTA1OTVkNWExNDBjMTkxNjVjMTgwYTEyNWIxMzAxMWIwYzBjNWExNDQxMGE1Yg%3D%3D", }, ], @@ -29,25 +29,25 @@ exports[`rewriteMasterPlaylistUrls should rewrite 1`] = ` exports[`rewriteMasterPlaylistUrls should include session id 1`] = ` { + "renditions": [ + { + "groupId": "group_1", + "name": "audio_1", + "type": "AUDIO", + "uri": "stitcher-endpoint/out/playlist.m3u8?eurl=MDcxZDFiMTA1OTVkNWExNDBjMTkxNjVjMTgwYTEyNWIwODBkMWIxMDBjNWExNDQxMGE1Yg%3D%3D&sid=36bab417-0952-4c23-bdf0-9a424e4651ad&type=AUDIO", + }, + { + "groupId": "group_2", + "name": "subtitles_1", + "type": "SUBTITLES", + "uri": "stitcher-endpoint/out/playlist.m3u8?eurl=MDcxZDFiMTA1OTVkNWExNDBjMTkxNjVjMTgwYTEyNWIwNDBkMTkxZDE2MGExNTFkMTI0ZDEyNDcxYzRh&sid=36bab417-0952-4c23-bdf0-9a424e4651ad&type=SUBTITLES", + }, + ], "variants": [ { - "audio": [ - { - "groupId": "group_1", - "name": "audio_1", - "type": "AUDIO", - "uri": "stitcher-endpoint/out/playlist.m3u8?eurl=MDcxZDFiMTA1OTVkNWExNDBjMTkxNjVjMTgwYTEyNWIwODBkMWIxMDBjNWExNDQxMGE1Yg%3D%3D&sid=36bab417-0952-4c23-bdf0-9a424e4651ad&type=AUDIO", - }, - ], + "audio": "group_1", "bandwidth": 1000, - "subtitles": [ - { - "groupId": "group_1", - "name": "subtitles_1", - "type": "SUBTITLES", - "uri": "stitcher-endpoint/out/playlist.m3u8?eurl=MDcxZDFiMTA1OTVkNWExNDBjMTkxNjVjMTgwYTEyNWIwNDBkMTkxZDE2MGExNTFkMTI0ZDEyNDcxYzRh&sid=36bab417-0952-4c23-bdf0-9a424e4651ad&type=SUBTITLES", - }, - ], + "subtitles": "group_2", "uri": "stitcher-endpoint/out/playlist.m3u8?eurl=MDcxZDFiMTA1OTVkNWExNDBjMTkxNjVjMTgwYTEyNWIxMzAxMWIwYzBjNWExNDQxMGE1Yg%3D%3D&sid=36bab417-0952-4c23-bdf0-9a424e4651ad", }, ], diff --git a/packages/stitcher/test/test-data.ts b/packages/stitcher/test/test-data.ts index d3bca6a7..ebfbe075 100644 --- a/packages/stitcher/test/test-data.ts +++ b/packages/stitcher/test/test-data.ts @@ -4,26 +4,26 @@ import type { Session } from "../src/session"; export function fakeMasterPlaylist(): MasterPlaylist { return { + renditions: [ + { + type: "AUDIO", + groupId: "group_1", + name: "audio_1", + uri: "audio.m3u8", + }, + { + type: "SUBTITLES", + groupId: "group_2", + name: "subtitles_1", + uri: "subtitles.m3u8", + }, + ], variants: [ { uri: "video.m3u8", bandwidth: 1000, - audio: [ - { - type: "AUDIO", - groupId: "group_1", - name: "audio_1", - uri: "audio.m3u8", - }, - ], - subtitles: [ - { - type: "SUBTITLES", - groupId: "group_1", - name: "subtitles_1", - uri: "subtitles.m3u8", - }, - ], + audio: "group_1", + subtitles: "group_2", }, ], };