diff --git a/src/routes/m3u8-proxy.ts b/src/routes/m3u8-proxy.ts index 2d5bc5e..a49291e 100644 --- a/src/routes/m3u8-proxy.ts +++ b/src/routes/m3u8-proxy.ts @@ -16,9 +16,7 @@ function parseURL(req_url: string, baseUrl?: string) { return null; } - // Scheme is omitted if (req_url.lastIndexOf("//", 0) === -1) { - // "//" is omitted req_url = "//" + req_url; } req_url = (match[4] === "443" ? "https:" : "http:") + req_url; @@ -27,7 +25,6 @@ function parseURL(req_url: string, baseUrl?: string) { try { const parsed = new URL(req_url); if (!parsed.hostname) { - // "http://:1/" and "http:/notenoughslashes" could end up here return null; } return parsed.href; @@ -36,9 +33,49 @@ function parseURL(req_url: string, baseUrl?: string) { } } -/** - * Proxies m3u8 files and replaces the content to point to the proxy - */ +const segmentCache: Map }> = new Map(); + +async function prefetchSegment(url: string, headers: HeadersInit) { + if (segmentCache.has(url)) { + return; + } + + try { + const response = await globalThis.fetch(url, { + method: 'GET', + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0', + ...(headers as HeadersInit), + } + }); + + if (!response.ok) { + console.error(`Failed to prefetch TS segment: ${response.status} ${response.statusText}`); + return; + } + + const data = new Uint8Array(await response.arrayBuffer()); + + const responseHeaders: Record = {}; + response.headers.forEach((value, key) => { + responseHeaders[key] = value; + }); + + segmentCache.set(url, { + data, + headers: responseHeaders + }); + + console.log(`Prefetched and cached segment: ${url}`); + } catch (error) { + console.error(`Error prefetching segment ${url}:`, error); + } +} + +export function getCachedSegment(url: string) { + return segmentCache.get(url); +} + async function proxyM3U8(event: any) { const url = getQuery(event).url as string; const headersParam = getQuery(event).headers as string; @@ -63,7 +100,6 @@ async function proxyM3U8(event: any) { try { const response = await globalThis.fetch(url, { headers: { - // Default User-Agent (from src/utils/headers.ts) 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0', ...(headers as HeadersInit), } @@ -75,20 +111,17 @@ async function proxyM3U8(event: any) { const m3u8Content = await response.text(); - // Get the base URL for the host const host = getRequestHost(event); const proto = getRequestProtocol(event); const baseProxyUrl = `${proto}://${host}`; if (m3u8Content.includes("RESOLUTION=")) { - // This is a master playlist with multiple quality variants const lines = m3u8Content.split("\n"); const newLines: string[] = []; for (const line of lines) { if (line.startsWith("#")) { if (line.startsWith("#EXT-X-KEY:")) { - // Proxy the key URL const regex = /https?:\/\/[^\""\s]+/g; const keyUrl = regex.exec(line)?.[0]; if (keyUrl) { @@ -98,7 +131,6 @@ async function proxyM3U8(event: any) { newLines.push(line); } } else if (line.startsWith("#EXT-X-MEDIA:")) { - // Proxy alternative media URLs (like audio streams) const regex = /https?:\/\/[^\""\s]+/g; const mediaUrl = regex.exec(line)?.[0]; if (mediaUrl) { @@ -111,7 +143,6 @@ async function proxyM3U8(event: any) { newLines.push(line); } } else if (line.trim()) { - // This is a quality variant URL const variantUrl = parseURL(line, url); if (variantUrl) { newLines.push(`${baseProxyUrl}/m3u8-proxy?url=${encodeURIComponent(variantUrl)}&headers=${encodeURIComponent(JSON.stringify(headers))}`); @@ -119,12 +150,10 @@ async function proxyM3U8(event: any) { newLines.push(line); } } else { - // Empty line, preserve it newLines.push(line); } } - // Set appropriate headers setResponseHeaders(event, { 'Content-Type': 'application/vnd.apple.mpegurl', 'Access-Control-Allow-Origin': '*', @@ -135,19 +164,21 @@ async function proxyM3U8(event: any) { return newLines.join("\n"); } else { - // This is a media playlist with segments const lines = m3u8Content.split("\n"); const newLines: string[] = []; + const segmentUrls: string[] = []; + for (const line of lines) { if (line.startsWith("#")) { if (line.startsWith("#EXT-X-KEY:")) { - // Proxy the key URL const regex = /https?:\/\/[^\""\s]+/g; const keyUrl = regex.exec(line)?.[0]; if (keyUrl) { const proxyKeyUrl = `${baseProxyUrl}/ts-proxy?url=${encodeURIComponent(keyUrl)}&headers=${encodeURIComponent(JSON.stringify(headers))}`; newLines.push(line.replace(keyUrl, proxyKeyUrl)); + + prefetchSegment(keyUrl, headers as HeadersInit); } else { newLines.push(line); } @@ -155,20 +186,29 @@ async function proxyM3U8(event: any) { newLines.push(line); } } else if (line.trim() && !line.startsWith("#")) { - // This is a segment URL (.ts file) const segmentUrl = parseURL(line, url); if (segmentUrl) { + segmentUrls.push(segmentUrl); + newLines.push(`${baseProxyUrl}/ts-proxy?url=${encodeURIComponent(segmentUrl)}&headers=${encodeURIComponent(JSON.stringify(headers))}`); } else { newLines.push(line); } } else { - // Comment or empty line, preserve it newLines.push(line); } } - // Set appropriate headers + if (segmentUrls.length > 0) { + console.log(`Starting to prefetch ${segmentUrls.length} segments for ${url}`); + + Promise.all(segmentUrls.map(segmentUrl => + prefetchSegment(segmentUrl, headers as HeadersInit) + )).catch(error => { + console.error('Error prefetching segments:', error); + }); + } + setResponseHeaders(event, { 'Content-Type': 'application/vnd.apple.mpegurl', 'Access-Control-Allow-Origin': '*', @@ -189,8 +229,7 @@ async function proxyM3U8(event: any) { } export default defineEventHandler(async (event) => { - // Handle CORS preflight requests if (isPreflightRequest(event)) return handleCors(event, {}); return await proxyM3U8(event); -}); \ No newline at end of file +});