Spaces:
Sleeping
Sleeping
File size: 5,869 Bytes
4bea261 | 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 143 144 145 146 147 148 149 | export const OPTIONS = async () => {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Range',
'Access-Control-Max-Age': '86400',
},
});
};
export const GET = async ({ request, url }) => {
const targetUrl = url.searchParams.get('url');
if (!targetUrl) {
return new Response(JSON.stringify({ error: 'Missing target URL' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
try {
const headers = new Headers();
// Use the origin of the target URL as the referer to bypass simple checks
const targetOrigin = new URL(targetUrl).origin;
headers.set('Referer', targetOrigin + '/');
headers.set('Origin', targetOrigin);
headers.set('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
// Forward Range header for smooth scrubbing/seeking (HTTP 206 Partial Content support)
const rangeHeader = request.headers.get('Range') || request.headers.get('range');
if (rangeHeader) {
headers.set('Range', rangeHeader);
}
const response = await fetch(targetUrl, { headers });
if (!response.ok) {
// If it's an image and it's not found, return a transparent 1x1 pixel
// This prevents the player from retrying and avoids broken image icons in the UI
const isImage = targetUrl.match(/\.(jpg|jpeg|png|gif|webp|svg)(\?.*)?$/i) ||
targetUrl.toLowerCase().includes('preview.jpg') ||
targetUrl.toLowerCase().includes('thumb.jpg');
if (isImage && response.status === 404) {
// 1x1 transparent GIF
const transparentPixel = Buffer.from('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'base64');
return new Response(transparentPixel, {
status: 200,
headers: {
'Content-Type': 'image/gif',
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'public, max-age=86400'
}
});
}
console.warn(`[Proxy] Failed to fetch ${targetUrl}: ${response.status} ${response.statusText}`);
return new Response(null, { status: response.status });
}
const contentType = response.headers.get('Content-Type') || '';
// If it's a playlist (m3u8), we might need to rewrite relative paths to absolute ones
// so that the player doesn't try to load them relative to the proxy URL.
if (contentType.includes('mpegurl') || contentType.includes('x-mpegURL') || targetUrl.endsWith('.m3u8')) {
let text = await response.text();
const urlObj = new URL(targetUrl);
const baseUrl = targetUrl.substring(0, targetUrl.lastIndexOf('/') + 1);
// Rewrite relative paths to absolute URLs pointing back to the proxy
// This ensures that all segments also go through the proxy to bypass CORS
const proxiedText = text.split('\n').map(line => {
const trimmed = line.trim();
if (!trimmed) return line;
// If it is a tag/comment line
if (trimmed.startsWith('#')) {
// Check for URI attribute in EXT tags (like EXT-X-KEY, EXT-X-MAP, EXT-X-MEDIA)
if (trimmed.includes('URI=')) {
return trimmed.replace(/URI=(["'])([^"'\r\n]+)\1/g, (match, quote, p1) => {
let absolutePath = p1;
if (!p1.startsWith('http')) {
absolutePath = p1.startsWith('/')
? urlObj.origin + p1
: baseUrl + p1;
}
return `URI=${quote}/api/proxy-media?url=${encodeURIComponent(absolutePath)}${quote}`;
});
}
return line;
}
// If it is a segment URL (non-tag line)
// If it's already an absolute URL, wrap it in proxy
if (trimmed.startsWith('http')) {
return `/api/proxy-media?url=${encodeURIComponent(trimmed)}`;
}
// If it's a relative path, make it absolute and wrap it
const absolutePath = trimmed.startsWith('/')
? urlObj.origin + trimmed
: baseUrl + trimmed;
return `/api/proxy-media?url=${encodeURIComponent(absolutePath)}`;
}).join('\n');
return new Response(proxiedText, {
status: 200,
headers: {
'Content-Type': 'application/x-mpegURL',
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'no-cache'
}
});
}
// For other media (images, segments), stream the response directly to the browser
// This achieves zero-latency start and supports seeking (206 Partial Content)
const status = response.status;
const responseHeaders = new Headers({
'Content-Type': contentType,
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'public, max-age=3600'
});
// Copy Range-related headers from destination response if they exist
const contentRange = response.headers.get('Content-Range') || response.headers.get('content-range');
if (contentRange) {
responseHeaders.set('Content-Range', contentRange);
}
const contentLength = response.headers.get('Content-Length') || response.headers.get('content-length');
if (contentLength) {
responseHeaders.set('Content-Length', contentLength);
}
const acceptRanges = response.headers.get('Accept-Ranges') || response.headers.get('accept-ranges');
if (acceptRanges) {
responseHeaders.set('Accept-Ranges', acceptRanges);
}
return new Response(response.body, {
status: status,
headers: responseHeaders
});
} catch (error) {
console.error(`[Proxy] Error:`, error);
return new Response(null, { status: 500 });
}
};
|