Cloudflare视频代理服务

支持多种视频格式代理播放 - 自动格式识别与手动选择

视频代理测试

准备就绪,请输入视频URL

无法自动识别视频格式,请手动选择:

HLS/m3u8
流媒体播放列表
MP4
标准视频格式
FLV
Flash视频格式
WebM
开源媒体格式
MPEG-DASH
自适应流媒体

视频将在此处播放

输入URL后点击"播放视频"按钮

使用说明
Worker代码
部署指南

使用说明

自动格式识别

系统会自动分析URL和响应头来识别视频格式:

  • 通过URL扩展名(.mp4, .m3u8等)
  • 通过Content-Type响应头
  • 通过文件签名(魔数)检测

手动格式选择

当自动识别失败时:

  • 系统会显示格式选择界面
  • 用户可以选择合适的视频格式
  • 选择后系统会使用指定格式播放

集成到项目

在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>

Cloudflare Worker 代码

functions/proxy.js
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代理代码

Cloudflare Pages设置

  • 在Cloudflare Pages创建新项目
  • 连接您的GitHub/GitLab仓库
  • 构建设置:
    框架预设: None
    构建命令: (空)
    构建输出目录: public
    根目录: /
  • Functions设置:
    Functions目录: functions
  • 环境变量:无需特殊设置

常见问题解决

  • 确保Functions目录设置为"functions"
  • 检查控制台错误信息
  • 验证代理URL格式:/proxy?url=...
  • 测试CORS和Range请求处理
  • 确保目标网站允许代理访问
  • 如果使用自定义域名,确保已正确配置DNS