mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
223 lines
7.0 KiB
JavaScript
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
|