Files
v10/dist/dev/media/hls/parse-multivariant.js
T

223 lines
7.0 KiB
JavaScript

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