diff --git a/packages/stitcher/src/parser/parse.ts b/packages/stitcher/src/parser/parse.ts index 32a1d42c..b58dc4b3 100644 --- a/packages/stitcher/src/parser/parse.ts +++ b/packages/stitcher/src/parser/parse.ts @@ -88,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; } @@ -100,7 +105,7 @@ function formatMediaPlaylist(tags: Tag[]): MediaPlaylist { const segmentTags = tags.slice(segmentStart, index + 1); const uri = nextLiteral(segmentTags, segmentTags.length - 2); - const segment = parseSegment(segmentTags, uri); + const segment = parseSegment(segmentTags, uri, map); segments.push(segment); segmentStart = -1; @@ -140,18 +145,20 @@ 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; } -function parseSegment(tags: Tag[], uri: string): Segment { +function parseSegment( + tags: Tag[], + uri: string, + map?: MediaInitializationSection, +): Segment { let duration: number | undefined; let discontinuity: boolean | undefined; let programDateTime: DateTime | undefined; - let map: MediaInitializationSection | undefined; tags.forEach(([name, value]) => { if (name === "EXTINF") { @@ -163,9 +170,6 @@ function parseSegment(tags: Tag[], uri: string): Segment { if (name === "EXT-X-PROGRAM-DATE-TIME") { programDateTime = value; } - if (name === "EXT-X-MAP") { - map = value; - } }); assert(duration, "parseSegment: duration not found"); diff --git a/packages/stitcher/src/parser/stringify.ts b/packages/stitcher/src/parser/stringify.ts index 195ae818..d08e09c1 100644 --- a/packages/stitcher/src/parser/stringify.ts +++ b/packages/stitcher/src/parser/stringify.ts @@ -1,4 +1,8 @@ -import type { MasterPlaylist, MediaPlaylist } from "./types"; +import type { + MasterPlaylist, + MediaInitializationSection, + MediaPlaylist, +} from "./types"; export function stringifyMasterPlaylist(playlist: MasterPlaylist) { const lines: string[] = []; @@ -94,10 +98,18 @@ export function stringifyMediaPlaylist(playlist: MediaPlaylist) { lines.push(`#EXT-X-PLAYLIST-TYPE:${playlist.playlistType}`); } + let lastMap: MediaInitializationSection | undefined; + playlist.segments.forEach((segment) => { - if (segment.map) { - const attrs = [`URI="${segment.map.uri}"`]; - lines.push(`#EXT-X-MAP:${attrs.join(",")}`); + // 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) {