diff --git a/src/routes/m3u8-proxy.ts b/src/routes/m3u8-proxy.ts index 80f85ac..4674ea4 100644 --- a/src/routes/m3u8-proxy.ts +++ b/src/routes/m3u8-proxy.ts @@ -16,7 +16,9 @@ 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; @@ -25,6 +27,7 @@ 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; @@ -155,6 +158,9 @@ export function getCacheStats() { }; } +/** + * Proxies m3u8 files and replaces the content to point to the proxy + */ async function proxyM3U8(event: any) { const url = getQuery(event).url as string; const headersParam = getQuery(event).headers as string; @@ -179,6 +185,7 @@ 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), } @@ -190,17 +197,20 @@ 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) { @@ -210,6 +220,7 @@ 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) { @@ -222,6 +233,7 @@ 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))}`); @@ -229,10 +241,12 @@ 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': '*', @@ -243,6 +257,7 @@ 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[] = []; @@ -251,6 +266,7 @@ async function proxyM3U8(event: any) { 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) { @@ -265,6 +281,7 @@ 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); @@ -274,6 +291,7 @@ async function proxyM3U8(event: any) { newLines.push(line); } } else { + // Comment or empty line, preserve it newLines.push(line); } } @@ -290,6 +308,7 @@ async function proxyM3U8(event: any) { }); } + // Set appropriate headers setResponseHeaders(event, { 'Content-Type': 'application/vnd.apple.mpegurl', 'Access-Control-Allow-Origin': '*', @@ -319,6 +338,7 @@ export function handleCacheStats(event: any) { } export default defineEventHandler(async (event) => { + // Handle CORS preflight requests if (isPreflightRequest(event)) return handleCors(event, {}); if (event.path === '/cache-stats') { diff --git a/src/routes/ts-proxy.ts b/src/routes/ts-proxy.ts index 07f2bab..b19c7c4 100644 --- a/src/routes/ts-proxy.ts +++ b/src/routes/ts-proxy.ts @@ -2,6 +2,7 @@ import { setResponseHeaders } from 'h3'; import { getCachedSegment } from './m3u8-proxy'; export default defineEventHandler(async (event) => { + // Handle CORS preflight requests if (isPreflightRequest(event)) return handleCors(event, {}); const url = getQuery(event).url as string; @@ -33,7 +34,7 @@ export default defineEventHandler(async (event) => { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': '*', 'Access-Control-Allow-Methods': '*', - 'Cache-Control': 'public, max-age=3600' + 'Cache-Control': 'public, max-age=3600' // Allow caching of TS segments }); return cachedSegment.data; @@ -42,6 +43,7 @@ export default defineEventHandler(async (event) => { const response = await globalThis.fetch(url, { method: 'GET', 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), } @@ -56,9 +58,10 @@ export default defineEventHandler(async (event) => { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': '*', 'Access-Control-Allow-Methods': '*', - 'Cache-Control': 'public, max-age=3600' + 'Cache-Control': 'public, max-age=3600' // Allow caching of TS segments }); + // Return the binary data directly return new Uint8Array(await response.arrayBuffer()); } catch (error: any) { console.error('Error proxying TS file:', error);