Files
v10/dist/dev/dom/native-hls/m3u8-utils.js
T

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