import { matchTag, parseCodecs } from "./parse-attributes.js"; import { resolveUrl } from "./resolve-url.js"; import { generateId } from "@videojs/utils/string"; //#region src/media/hls/parse-multivariant.ts /** * Parse HLS multivariant playlist into a Presentation. * * Returns Presentation with partially resolved tracks (no segment information). * Tracks contain metadata from multivariant playlist (bandwidth, resolution, codecs) * but segment information is added when media playlists are fetched. * * @param text - Raw playlist text content * @param unresolved - Unresolved presentation (contains URL for base URL resolution) * @returns Presentation with partially resolved tracks (duration is undefined) */ function parseMultivariantPlaylist(text, unresolved) { const baseUrl = unresolved.url; const lines = text.split(/\r?\n/); const streams = []; const audioRenditions = []; const subtitleRenditions = []; let pendingStreamInfo = null; for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#") && !trimmed.startsWith("#EXT")) continue; if (trimmed === "#EXTM3U" || trimmed.startsWith("#EXT-X-VERSION:") || trimmed.startsWith("#EXT-X-INDEPENDENT-SEGMENTS")) continue; const mediaAttrs = matchTag(trimmed, "EXT-X-MEDIA"); if (mediaAttrs) { const type = mediaAttrs.get("TYPE"); const groupId = mediaAttrs.get("GROUP-ID"); const name = mediaAttrs.get("NAME"); if (type === "AUDIO" && groupId && name) { const uri = mediaAttrs.get("URI"); audioRenditions.push({ groupId, name, language: mediaAttrs.get("LANGUAGE"), uri: uri ? resolveUrl(uri, baseUrl) : void 0, default: mediaAttrs.getBool("DEFAULT"), autoselect: mediaAttrs.getBool("AUTOSELECT"), channels: mediaAttrs.getInt("CHANNELS") }); } if (type === "SUBTITLES" && groupId && name) { const uri = mediaAttrs.get("URI"); if (uri) subtitleRenditions.push({ groupId, name, language: mediaAttrs.get("LANGUAGE"), uri: resolveUrl(uri, baseUrl), default: mediaAttrs.getBool("DEFAULT"), autoselect: mediaAttrs.getBool("AUTOSELECT"), forced: mediaAttrs.getBool("FORCED") }); } continue; } const streamInfAttrs = matchTag(trimmed, "EXT-X-STREAM-INF"); if (streamInfAttrs) { pendingStreamInfo = { bandwidth: streamInfAttrs.getInt("BANDWIDTH", 0), resolution: streamInfAttrs.getResolution("RESOLUTION"), codecs: streamInfAttrs.get("CODECS"), frameRate: streamInfAttrs.getFrameRate("FRAME-RATE"), audioGroupId: streamInfAttrs.get("AUDIO") }; continue; } if (!trimmed.startsWith("#") && pendingStreamInfo) { streams.push({ ...pendingStreamInfo, uri: resolveUrl(trimmed, baseUrl) }); pendingStreamInfo = null; } } const videoStreams = []; const audioOnlyStreams = []; for (const stream of streams) { if (!stream.codecs) { videoStreams.push(stream); continue; } const parsedCodecs = parseCodecs(stream.codecs); if (stream.codecs.split(",").length === 1) if (parsedCodecs.audio && !parsedCodecs.video) audioOnlyStreams.push(stream); else videoStreams.push(stream); else videoStreams.push(stream); } const videoTracksByUrl = /* @__PURE__ */ new Map(); for (const stream of videoStreams) { const existing = videoTracksByUrl.get(stream.uri); if (existing) { if (stream.audioGroupId && !existing.audioGroupIds?.includes(stream.audioGroupId)) existing.audioGroupIds = [...existing.audioGroupIds ?? [], stream.audioGroupId]; if (stream.bandwidth < existing.bandwidth) existing.bandwidth = stream.bandwidth; continue; } const codecs = stream.codecs ? parseCodecs(stream.codecs) : void 0; const track = { type: "video", id: generateId(), url: stream.uri, bandwidth: stream.bandwidth, mimeType: "video/mp4", codecs: [] }; if (stream.resolution?.width !== void 0) track.width = stream.resolution.width; if (stream.resolution?.height !== void 0) track.height = stream.resolution.height; if (codecs?.video) track.codecs = [codecs.video]; if (stream.frameRate) track.frameRate = stream.frameRate; if (stream.audioGroupId) track.audioGroupIds = [stream.audioGroupId]; videoTracksByUrl.set(stream.uri, track); } const videoTracks = [...videoTracksByUrl.values()]; const audioOnlyTracks = audioOnlyStreams.map((stream) => { const codecs = stream.codecs ? parseCodecs(stream.codecs) : void 0; return { type: "audio", id: generateId(), url: stream.uri, bandwidth: stream.bandwidth, mimeType: "audio/mp4", codecs: codecs?.audio ? [codecs.audio] : [], groupId: stream.audioGroupId || "default", name: "Default", sampleRate: 48e3, channels: 2 }; }); const audioTracks = [...audioRenditions.map((rendition) => { let audioCodecs; for (const stream of streams) if (stream.audioGroupId === rendition.groupId && stream.codecs) { const codecs = parseCodecs(stream.codecs); if (codecs.audio) { audioCodecs = [codecs.audio]; break; } } const track = { type: "audio", id: generateId(), url: rendition.uri ?? "", groupId: rendition.groupId, name: rendition.name, mimeType: "audio/mp4", bandwidth: 0, sampleRate: 48e3, channels: rendition.channels ?? 2, codecs: [] }; if (rendition.language) track.language = rendition.language; if (audioCodecs) track.codecs = audioCodecs; if (rendition.default) track.default = rendition.default; if (rendition.autoselect) track.autoselect = rendition.autoselect; return track; }), ...audioOnlyTracks]; const textTracks = subtitleRenditions.map((rendition) => { const track = { type: "text", id: generateId(), url: rendition.uri, groupId: rendition.groupId, label: rendition.name, kind: "subtitles", mimeType: "text/vtt", bandwidth: 0 }; if (rendition.language) track.language = rendition.language; if (rendition.default && rendition.autoselect) track.default = true; if (rendition.autoselect) track.autoselect = rendition.autoselect; if (rendition.forced) track.forced = rendition.forced; return track; }); const selectionSets = []; if (videoTracks.length > 0) { const videoSwitchingSet = { id: generateId(), type: "video", tracks: videoTracks }; const videoSelectionSet = { id: generateId(), type: "video", switchingSets: [videoSwitchingSet] }; selectionSets.push(videoSelectionSet); } if (audioTracks.length > 0) { const audioSwitchingSet = { id: generateId(), type: "audio", tracks: audioTracks }; const audioSelectionSet = { id: generateId(), type: "audio", switchingSets: [audioSwitchingSet] }; selectionSets.push(audioSelectionSet); } if (textTracks.length > 0) { const textSwitchingSet = { id: generateId(), type: "text", tracks: textTracks }; const textSelectionSet = { id: generateId(), type: "text", switchingSets: [textSwitchingSet] }; selectionSets.push(textSelectionSet); } return { id: generateId(), url: unresolved.url, startTime: 0, selectionSets }; } //#endregion export { parseMultivariantPlaylist }; //# sourceMappingURL=parse-multivariant.js.map