mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
103 lines
3.6 KiB
JavaScript
103 lines
3.6 KiB
JavaScript
//#region src/dom/native-hls/m3u8-utils.ts
|
|
/**
|
|
* Returns `true` when `src` looks like an HLS playlist URL. Permissive: a
|
|
* path or query string containing `.m3u8` is enough.
|
|
*/
|
|
function looksLikeM3u8(src) {
|
|
return src.toLowerCase().includes(".m3u8");
|
|
}
|
|
/**
|
|
* Returns `true` when the playlist text is a multivariant (master) playlist.
|
|
*
|
|
* The presence of `#EXT-X-STREAM-INF` is conclusive — media playlists only
|
|
* contain `#EXTINF` segment tags.
|
|
*/
|
|
function isMultivariantPlaylist(playlist) {
|
|
return playlist.includes("#EXT-X-STREAM-INF");
|
|
}
|
|
/**
|
|
* Resolves the first media playlist URL referenced by a multivariant
|
|
* playlist, relative to `baseUrl`. Returns `null` when none is found or the
|
|
* URL cannot be parsed.
|
|
*/
|
|
function resolveFirstMediaPlaylistUrl(multivariant, baseUrl) {
|
|
const lines = multivariant.split(/\r?\n/);
|
|
const start = lines.findIndex((l) => l.startsWith("#EXT-X-STREAM-INF"));
|
|
if (start === -1) return null;
|
|
const uri = lines.slice(start + 1).map((l) => l.trim()).find((l) => l && !l.startsWith("#"));
|
|
if (!uri) return null;
|
|
try {
|
|
return new URL(uri, baseUrl).toString();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
/**
|
|
* Parses the subset of media-playlist tags needed to derive live edge state:
|
|
* `#EXT-X-PLAYLIST-TYPE`, `#EXT-X-ENDLIST`, `#EXT-X-TARGETDURATION`,
|
|
* `#EXT-X-PART-INF`.
|
|
*
|
|
* See spec:
|
|
* - VOD or `#EXT-X-ENDLIST` present → on-demand, `targetLiveWindow = NaN`.
|
|
* - `EVENT` playlist → DVR, `targetLiveWindow = Infinity`.
|
|
* - Otherwise → standard live sliding window, `targetLiveWindow = 0`.
|
|
*
|
|
* The edge offset is `PART-TARGET * 2` for low-latency live and
|
|
* `TARGETDURATION * 3` otherwise.
|
|
*/
|
|
function parseStreamInfo(playlist) {
|
|
const lines = playlist.split(/\r?\n/);
|
|
let playlistType;
|
|
let hasEndList = false;
|
|
let targetDuration;
|
|
let partTarget;
|
|
for (const raw of lines) {
|
|
const line = raw.trim();
|
|
if (line.startsWith("#EXT-X-PLAYLIST-TYPE:")) playlistType = line.slice(21).trim().toUpperCase();
|
|
else if (line === "#EXT-X-ENDLIST") hasEndList = true;
|
|
else if (line.startsWith("#EXT-X-TARGETDURATION:")) {
|
|
const value = Number(line.slice(22));
|
|
if (Number.isFinite(value)) targetDuration = value;
|
|
} else if (line.startsWith("#EXT-X-PART-INF")) {
|
|
const match = /PART-TARGET\s*=\s*([0-9.]+)/i.exec(line);
|
|
if (match) {
|
|
const value = Number(match[1]);
|
|
if (Number.isFinite(value)) partTarget = value;
|
|
}
|
|
}
|
|
}
|
|
if (playlistType === "VOD" || hasEndList) return {
|
|
targetLiveWindow: NaN,
|
|
liveEdgeStartOffset: void 0
|
|
};
|
|
return {
|
|
targetLiveWindow: playlistType === "EVENT" ? Number.POSITIVE_INFINITY : 0,
|
|
liveEdgeStartOffset: partTarget !== void 0 ? partTarget * 2 : targetDuration !== void 0 ? targetDuration * 3 : void 0
|
|
};
|
|
}
|
|
async function fetchPlaylist(url, init) {
|
|
const response = await fetch(url, init);
|
|
if (!response.ok) throw new Error(`Failed to fetch playlist (${response.status}): ${url}`);
|
|
return {
|
|
text: await response.text(),
|
|
url: response.url || url
|
|
};
|
|
}
|
|
/**
|
|
* Fetches the HLS playlist at `src`, following the first variant if it's a
|
|
* multivariant playlist, and parses it into a {@link StreamInfo}.
|
|
*
|
|
* @throws when the fetch fails or no media playlist URL can be resolved.
|
|
*/
|
|
async function getStreamInfoFromSrc(src, signal) {
|
|
const init = signal ? { signal } : {};
|
|
const { text, url } = await fetchPlaylist(src, init);
|
|
if (!isMultivariantPlaylist(text)) return parseStreamInfo(text);
|
|
const mediaUrl = resolveFirstMediaPlaylistUrl(text, url);
|
|
if (!mediaUrl) throw new Error("No media playlist URL found in multivariant playlist");
|
|
return parseStreamInfo((await fetchPlaylist(mediaUrl, init)).text);
|
|
}
|
|
//#endregion
|
|
export { getStreamInfoFromSrc, looksLikeM3u8 };
|
|
|
|
//# sourceMappingURL=m3u8-utils.js.map
|