File size: 4,425 Bytes
de4b571 11fcc5a de4b571 11fcc5a de4b571 11fcc5a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
import HLS from "hls-parser";
import { createInternalStream } from "./manage.js";
import { request } from "undici";
function getURL(url) {
try {
return new URL(url);
} catch {
return null;
}
}
function transformObject(streamInfo, hlsObject) {
if (hlsObject === undefined) {
return (object) => transformObject(streamInfo, object);
}
let fullUrl;
if (getURL(hlsObject.uri)) {
fullUrl = new URL(hlsObject.uri);
} else {
fullUrl = new URL(hlsObject.uri, streamInfo.url);
}
if (fullUrl.hostname !== '127.0.0.1') {
hlsObject.uri = createInternalStream(fullUrl.toString(), streamInfo);
if (hlsObject.map) {
hlsObject.map = transformObject(streamInfo, hlsObject.map);
}
}
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 isHlsResponse(req, streamInfo) {
return HLS_MIME_TYPES.includes(req.headers['content-type'])
// bluesky's cdn responds with wrong content-type for the hls playlist,
// so we enforce it here until they fix it
|| (streamInfo.service === 'bsky' && streamInfo.url.endsWith('.m3u8'));
}
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);
}
async function getSegmentSize(url, config) {
const segmentResponse = await request(url, {
...config,
throwOnError: true
});
if (segmentResponse.headers['content-length']) {
segmentResponse.body.dump();
return +segmentResponse.headers['content-length'];
}
// if the response does not have a content-length
// header, we have to compute it ourselves
let size = 0;
for await (const data of segmentResponse.body) {
size += data.length;
}
return size;
}
export async function probeInternalHLSTunnel(streamInfo) {
const { url, headers, dispatcher, signal } = streamInfo;
// remove all falsy headers
Object.keys(headers).forEach(key => {
if (!headers[key]) delete headers[key];
});
const config = { headers, dispatcher, signal, maxRedirections: 16 };
const manifestResponse = await fetch(url, config);
const manifest = HLS.parse(await manifestResponse.text());
if (manifest.segments.length === 0)
return -1;
const segmentSamples = await Promise.all(
Array(5).fill().map(async () => {
const manifestIdx = Math.floor(Math.random() * manifest.segments.length);
const randomSegment = manifest.segments[manifestIdx];
if (!randomSegment.uri)
throw "segment is missing URI";
let segmentUrl;
if (getURL(randomSegment.uri)) {
segmentUrl = new URL(randomSegment.uri);
} else {
segmentUrl = new URL(randomSegment.uri, streamInfo.url);
}
const segmentSize = await getSegmentSize(segmentUrl, config) / randomSegment.duration;
return segmentSize;
})
);
const averageBitrate = segmentSamples.reduce((a, b) => a + b) / segmentSamples.length;
const totalDuration = manifest.segments.reduce((acc, segment) => acc + segment.duration, 0);
return averageBitrate * totalDuration;
}
|