diff --git a/src/modules/stream/internal-hls.js b/src/modules/stream/internal-hls.js new file mode 100644 index 00000000..c6a11455 --- /dev/null +++ b/src/modules/stream/internal-hls.js @@ -0,0 +1,56 @@ +import { createInternalStream } from './manage.js'; +import HLS from 'hls-parser'; +import path from "node:path"; + +function transformObject(streamInfo, hlsObject) { + if (hlsObject === undefined) { + return (object) => transformObject(streamInfo, object); + } + + const fullUrl = hlsObject.uri.startsWith("/") + ? new URL(hlsObject.uri, streamInfo.url).toString() + : new URL(path.join(streamInfo.url, "/../", hlsObject.uri)).toString(); + hlsObject.uri = createInternalStream(fullUrl, streamInfo); + + return hlsObject; +} + +function transformMasterPlaylist(streamInfo, hlsPlaylist) { + const makeInternalStream = transformObject(streamInfo); + + const makeInternalVariants = (variant) => { + variant = transformObject(streamInfo, variant); + variant.video = variant.video.map(makeInternalStream); + variant.audio = variant.audio.map(makeInternalStream); + return variant; + }; + hlsPlaylist.variants = hlsPlaylist.variants.map(makeInternalVariants); + + return hlsPlaylist; +} + +function transformMediaPlaylist(streamInfo, hlsPlaylist) { + const makeInternalSegments = transformObject(streamInfo); + hlsPlaylist.segments = hlsPlaylist.segments.map(makeInternalSegments); + hlsPlaylist.prefetchSegments = hlsPlaylist.prefetchSegments.map(makeInternalSegments); + return hlsPlaylist; +} + +const HLS_MIME_TYPES = ["application/vnd.apple.mpegurl", "audio/mpegurl", "application/x-mpegURL"]; + +export function isHlsRequest (req) { + return HLS_MIME_TYPES.includes(req.headers['content-type']); +} + +export async function handleHlsPlaylist(streamInfo, req, res) { + let hlsPlaylist = await req.body.text(); + hlsPlaylist = HLS.parse(hlsPlaylist); + + hlsPlaylist = hlsPlaylist.isMasterPlaylist + ? transformMasterPlaylist(streamInfo, hlsPlaylist) + : transformMediaPlaylist(streamInfo, hlsPlaylist); + + hlsPlaylist = HLS.stringify(hlsPlaylist); + + res.send(hlsPlaylist); +} diff --git a/src/modules/stream/internal.js b/src/modules/stream/internal.js index 833adf7d..535bba2d 100644 --- a/src/modules/stream/internal.js +++ b/src/modules/stream/internal.js @@ -2,6 +2,7 @@ import { request } from 'undici'; import { Readable } from 'node:stream'; import { assert } from 'console'; import { getHeaders, pipe } from './shared.js'; +import { handleHlsPlaylist, isHlsRequest } from './internal-hls.js'; const CHUNK_SIZE = BigInt(8e6); // 8 MB const min = (a, b) => a < b ? a : b; @@ -96,7 +97,11 @@ export async function internalStream(streamInfo, res) { if (req.statusCode < 200 || req.statusCode > 299) return res.end(); - pipe(req.body, res, () => res.end()); + if (isHlsRequest(req)) { + await handleHlsPlaylist(streamInfo, req, res); + } else { + pipe(req.body, res, () => res.end()); + } } catch { streamInfo.controller.abort(); } diff --git a/src/modules/stream/manage.js b/src/modules/stream/manage.js index 05008077..0dec1972 100644 --- a/src/modules/stream/manage.js +++ b/src/modules/stream/manage.js @@ -3,7 +3,7 @@ import { randomBytes } from "crypto"; import { nanoid } from "nanoid"; import { decryptStream, encryptStream, generateHmac } from "../sub/crypto.js"; -import { env, hlsExceptions } from "../config.js"; +import { env } from "../config.js"; import { strict as assert } from "assert"; // optional dependency @@ -106,12 +106,6 @@ export function destroyInternalStream(url) { } function wrapStream(streamInfo) { - /* m3u8 links are currently not supported - * for internal streams, skip them */ - if (hlsExceptions.includes(streamInfo.service)) { - return streamInfo; - } - const url = streamInfo.urls; if (typeof url === 'string') {