T-HUB/packages/egress/src/hls.js
glass beadian aa5e262f3a Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives:
publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style
column wall with hostile-content-safe rendering, live control room with Auto
Director, bridged chat governance with polls and quorum, distributed sandboxed
transcode workers, encrypted content-addressed archive, monthly analytics.

MIT licensed. See README.md and docs/.
2026-10-10 11:56:42 +00:00

124 lines
No EOL
5.4 KiB
JavaScript

import { EgressError } from "./errors.js";
import { EgressPolicy } from "./policy.js";
/** Parse attribute boundaries rather than finding URI= inside other quoted values. */
function attributeUris(attributes, offset) {
const uris = [];
if (!/^\s*[A-Z0-9-]+\s*=/.test(attributes))
return uris;
let cursor = 0;
const seen = new Set();
while (cursor < attributes.length) {
const key = /^\s*([A-Z0-9-]+)\s*=\s*/.exec(attributes.slice(cursor));
if (!key)
throw new EgressError("INVALID_PLAYLIST", "Malformed HLS attributes");
cursor += key[0].length;
const quoted = attributes[cursor] === '"';
const start = cursor + (quoted ? 1 : 0);
const end = quoted ? attributes.indexOf('"', start) : attributes.indexOf(",", start);
const valueEnd = end < 0 && !quoted ? attributes.length : end;
if (valueEnd < 0)
throw new EgressError("INVALID_PLAYLIST", "Unterminated HLS attribute");
if (key[1] === "URI" || key[1] === "SERVER-URI") {
if (!quoted || start === valueEnd || seen.has(key[1]))
throw new EgressError("INVALID_PLAYLIST", "Invalid HLS URI attribute");
seen.add(key[1]);
uris.push({ start: offset + start, end: offset + valueEnd, value: attributes.slice(start, valueEnd) });
}
cursor = valueEnd + (quoted ? 1 : 0);
while (/\s/.test(attributes[cursor] ?? "") && cursor < attributes.length)
cursor++;
if (cursor === attributes.length)
break;
if (attributes[cursor] !== "," || cursor + 1 === attributes.length)
throw new EgressError("INVALID_PLAYLIST", "Invalid HLS attribute suffix");
cursor++;
}
return uris;
}
function playlistUris(playlist) {
if (!/^#EXTM3U(?:\r?\n|$)/.test(playlist) || playlist.includes("\u0000"))
throw new EgressError("INVALID_PLAYLIST", "Missing HLS header");
const uris = [];
let offset = 0;
let awaitingVariant = false;
let variants = 0;
let media = false;
for (const line of playlist.split(/(?<=\n)/)) {
const text = line.replace(/\r?\n$/, "").trim();
if (text && !text.startsWith("#")) {
const start = offset + line.indexOf(text);
uris.push({ start, end: start + text.length, value: text });
if (awaitingVariant) {
variants++;
awaitingVariant = false;
}
}
else if (text.startsWith("#EXT")) {
if (text.startsWith("#EXTINF:"))
media = true;
if (text.startsWith("#EXT-X-STREAM-INF:")) {
if (awaitingVariant)
throw new EgressError("INVALID_PLAYLIST", "Variant URI is missing");
awaitingVariant = true;
}
const colon = line.indexOf(":");
if (colon >= 0) {
const attributes = line.slice(colon + 1).replace(/\r?\n$/, "");
const attributesFound = attributeUris(attributes, offset + colon + 1);
uris.push(...attributesFound);
if (text.startsWith("#EXT-X-I-FRAME-STREAM-INF:")) {
if (!attributesFound.length)
throw new EgressError("INVALID_PLAYLIST", "I-frame URI is missing");
variants++;
}
}
}
offset += line.length;
}
if (awaitingVariant)
throw new EgressError("INVALID_PLAYLIST", "Variant URI is missing");
return { uris, master: variants > 0 && !media };
}
async function resolveUris(uris, base, policy) {
const baseUrl = (await policy.validate(base)).url;
const endpoints = [];
// Sequential validation bounds DNS work even for large playlists; shared URI pins within this parse.
const cache = new Map();
for (const occurrence of uris) {
if (!occurrence.value || occurrence.value.includes("{$") || /[\s\\\u0000-\u001f\u007f]/u.test(occurrence.value))
throw new EgressError("INVALID_PLAYLIST", "Invalid or unexpanded HLS URI");
let absolute;
try {
absolute = new URL(occurrence.value, baseUrl);
}
catch {
throw new EgressError("INVALID_PLAYLIST", "Invalid HLS URI");
}
let endpoint = cache.get(absolute.href);
if (!endpoint) {
endpoint = await policy.validate(absolute);
cache.set(absolute.href, endpoint);
}
endpoints.push(endpoint);
}
return endpoints;
}
/** Validates master variants and URI-bearing tags (renditions, keys, maps, etc.) with DNS pinning. */
export async function validateMasterPlaylist(playlist, base, policy = new EgressPolicy()) {
const { uris, master } = playlistUris(playlist);
if (!master)
throw new EgressError("INVALID_PLAYLIST", "Expected an HLS master playlist");
return resolveUris(uris, base, policy);
}
/** Produces absolute, validated child URLs, preserving comments, attributes and newline style. */
export async function rewriteSegmentUris(playlist, base, policy = new EgressPolicy()) {
const { uris } = playlistUris(playlist);
const endpoints = await resolveUris(uris, base, policy);
let rewritten = playlist;
for (let index = uris.length - 1; index >= 0; index--) {
const uri = uris[index];
rewritten = rewritten.slice(0, uri.start) + endpoints[index].url.href + rewritten.slice(uri.end);
}
return rewritten;
}
//# sourceMappingURL=hls.js.map