支持多种视频格式代理播放 - 自动格式识别与手动选择
视频将在此处播放
输入URL后点击"播放视频"按钮
系统会自动分析URL和响应头来识别视频格式:
当自动识别失败时:
在HTML中使用代理服务:
<video controls>
<source
src="https://your-pages.dev/proxy?url=VIDEO_URL"
type="video/mp4">
</video>
<!-- 对于HLS流 -->
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<script>
if(Hls.isSupported()) {
const video = document.getElementById('video');
const hls = new Hls();
hls.loadSource('https://your-pages.dev/proxy?url=HLS_URL');
hls.attachMedia(video);
}
</script>
export async function onRequest(context) {
const { request } = context;
return handleRequest(request);
}
async function handleRequest(request) {
// 处理OPTIONS请求
if (request.method === 'OPTIONS') {
return handleOptions();
}
const url = new URL(request.url);
const videoUrl = url.searchParams.get('url');
// 验证URL参数
if (!videoUrl) {
return new Response('Missing "url" parameter', { status: 400 });
}
try {
new URL(videoUrl); // 验证URL格式
} catch (err) {
return new Response('Invalid URL format', { status: 400 });
}
// 处理HLS播放列表
if (isHLSRequest(videoUrl, request)) {
return handleHLSRequest(request, videoUrl, url);
}
// 处理普通媒体请求
return proxyMediaRequest(request, videoUrl);
}
// 判断是否为HLS请求
function isHLSRequest(videoUrl, request) {
// 通过URL扩展名判断
if (videoUrl.includes('.m3u8') || videoUrl.includes('.m3u')) {
return true;
}
// 通过format参数判断
const formatParam = new URL(request.url).searchParams.get('format');
if (formatParam === 'hls') {
return true;
}
return false;
}
// 处理HLS播放列表
async function handleHLSRequest(request, videoUrl, proxyUrl) {
const response = await fetch(videoUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36',
'Referer': new URL(videoUrl).origin,
},
redirect: 'follow'
});
if (!response.ok) {
return new Response(`HLS fetch error: ${response.status}`, { status: 502 });
}
let text = await response.text();
const baseUrl = new URL(videoUrl);
// 重写播放列表中的URL
text = text.replace(/(\n#?)([^\n]+\.(ts|m3u8?|key)([^\n]*)?/g, (match, p1, p2) => {
try {
const segmentUrl = new URL(p2, baseUrl).toString();
// 使用当前代理服务的URL
const newUrl = `${proxyUrl.origin}/proxy?url=${encodeURIComponent(segmentUrl)}`;
return `${p1}${newUrl}`;
} catch (e) {
return match;
}
});
// 设置响应头
const headers = new Headers(response.headers);
headers.set('Content-Type', 'application/vnd.apple.mpegurl');
headers.set('Access-Control-Allow-Origin', '*');
headers.set('Cache-Control', 'public, max-age=30');
return new Response(text, { headers });
}
// 代理普通媒体请求
async function proxyMediaRequest(request, videoUrl) {
const proxyRequest = new Request(videoUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36',
'Referer': new URL(videoUrl).origin,
'Range': request.headers.get('Range') || ''
},
redirect: 'follow'
});
try {
const response = await fetch(proxyRequest);
if (!response.ok) {
return new Response(`Upstream error: ${response.status}`, { status: 502 });
}
// 设置响应头
const headers = new Headers(response.headers);
headers.set('Access-Control-Allow-Origin', '*');
headers.set('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS');
headers.append('Vary', 'Origin');
// 获取格式参数
const formatParam = new URL(request.url).searchParams.get('format');
// 设置Content-Type
if (formatParam) {
// 用户指定的格式
headers.set('Content-Type', getMimeType(formatParam));
} else if (!headers.has('Content-Type')) {
// 自动检测格式
const contentType = detectContentType(videoUrl, response);
headers.set('Content-Type', contentType);
}
// 处理Range请求
const range = request.headers.get('Range');
if (range) {
headers.set('Accept-Ranges', 'bytes');
headers.set('Content-Range', response.headers.get('Content-Range') || `bytes 0-${response.headers.get('Content-Length') - 1}/${response.headers.get('Content-Length')}`);
}
return new Response(response.body, {
status: response.status,
headers: headers
});
} catch (err) {
return new Response(`Proxy error: ${err.message}`, { status: 500 });
}
}
// 根据格式参数获取MIME类型
function getMimeType(format) {
const formatMap = {
'mp4': 'video/mp4',
'hls': 'application/vnd.apple.mpegurl',
'flv': 'video/x-flv',
'webm': 'video/webm',
'dash': 'application/dash+xml'
};
return formatMap[format] || 'video/mp4';
}
// 自动检测内容类型
function detectContentType(videoUrl, response) {
// 尝试从URL扩展名判断
const url = new URL(videoUrl);
const pathname = url.pathname.toLowerCase();
const ext = pathname.split('.').pop();
const extMap = {
'mp4': 'video/mp4',
'm4v': 'video/mp4',
'webm': 'video/webm',
'flv': 'video/x-flv',
'ts': 'video/MP2T',
'm3u8': 'application/vnd.apple.mpegurl',
'm3u': 'application/vnd.apple.mpegurl',
'mpd': 'application/dash+xml'
};
if (extMap[ext]) {
return extMap[ext];
}
// 尝试从响应头读取
const contentType = response.headers.get('Content-Type');
if (contentType && (contentType.startsWith('video/') || contentType.startsWith('application/'))) {
return contentType;
}
// 默认值
return 'video/mp4';
}
// 处理 OPTIONS 请求 (CORS 预检)
function handleOptions() {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
'Access-Control-Allow-Headers': '*',
'Access-Control-Max-Age': '86400'
}
});
}
完整项目应包含以下文件:
cloudflare-video-proxy/
├── public/
│ ├── index.html # 此文件
│ └── style.css # 样式文件
└── functions/
└── proxy.js # Worker代理代码
框架预设: None 构建命令: (空) 构建输出目录: public 根目录: /
Functions目录: functions