跳到主要内容

CF Worker 代理视频流,直连播放 pixman 直播源

  • 新帖
  • 热门
  • 精选

CF Worker 代理视频流,直连播放 pixman 直播源

stlyx 2y ago in IPTV · 1 views

基于之前有人做的 xiaoya CF worker 里代理直播流的代码,进行了一波魔改,实现直连播放视频流。

使用方法: 创建 CF Worker,粘贴下面代码,修改 pixman服务所在域名 CFWorker路由域名 你自己的YouTube播放列表 等变量。

聚合 m3u URL: https://{CFWorker路由域名}/

直连 m3u URL: https://{CFWorker路由域名}/noproxy

原理: https://{CFWorker路由域名}/proxy/{被代理的URL} 这个路由代理一切,并把返回的 m3u8 链接也替换掉。

效果: Youtube、4gtv 非常丝滑。但是 mytvsuper 在 tvbox 里还是有点卡,可能是码率比较高,可以在视频流里选低一点的分辨率。看看有没有大神继续优化一下这个代码。

// 聚合以下所有源,mod:获取后对每个源进行修改(代理视频流等),filter:过滤获取到的源
const SRC = [
  {
    name: '央视频',
    url: 'http://pixman服务所在域名:5000/ysp.m3u'
  },
  {
    name: 'MyTVSuper',
    url: 'http://pixman服务所在域名:5000/mytvsuper-tivimate.m3u',
    mod: (noproxy) => noproxy ? identity : proxify
  },
  {
    name: '四季',
    url: 'http://pixman服务所在域名:5000/4gtv.m3u',
    mod: (noproxy) => noproxy ? identity : proxify
  },
  {
    name: '油管',
    url: 'http://pixman服务所在域名:5000/youtube/list/你自己的YouTube播放列表',
    mod: (noproxy) => noproxy ? identity : proxify
  }
]

// 要代理的域名
const PROXY_DOMAINS = [
  'pixman服务所在域名',
  '[^/]+\.hinet\.net',
  '[^/]+\.googlevideo\.com',
  '[^/]+\.tvb.com(:\d+)?'
];

// 不修改
function identity(it) {return it;}

// 替换要代理的域名为 Worker 代理 URL
function proxify(it) {
  for (const dom of PROXY_DOMAINS) {
    it = it.replace(new RegExp('https?://' + dom, 'g'), 'https://CFWorker路由域名/proxy/$&');
  }
  return it;
}


import { connect } from "cloudflare:sockets";

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    if (url.pathname.startsWith('/proxy')) {
      return handleProxy(request, env, ctx);
    } else {
      return handleList(request, env, ctx);
    }
  }
};

async function handleList(request, env, ctx) {
  let text = `#EXTM3U
#EXTM3U x-tvg-url="https://assets.livednow.com/epg.xml"

`;
  const REQ = SRC.map(src => ({
    ...src,
    response: fetchOverTcp(new Request(src.url))
  }));
  for (const src of REQ) {
    const respText = await (await src.response).text();
    let channels = respText.split(/^#EXT/gm).map(it => '#EXT' + it).filter(it => it.startsWith('#EXTINF'));
    if (src.filter) {
      const beforeLen = channels.length;
      channels = channels.filter(src.filter);
      const afterLen = channels.length;
      console.log(`filter ${src.name} ${beforeLen} -> ${afterLen}`);
    }
    if (src.mod) {
      const noproxy = request.url.indexOf('noproxy') > -1;
      channels = channels.map(src.mod(noproxy));
    }
    for (const chan of channels) {
      text += chan;
    }
  }

  return new Response(text);
}

async function handleProxy(request, env, ctx) {
  let target = new URL(request.url.split('/proxy/')[1]);
  console.log('proxying', target);
  let resp = await fetchOverTcp(new Request(target, {redirect: 'manual'}));
  if ([301,302,303,307,308].includes(resp.status)) {
    target = new URL(resp.headers.get('location'), target);
    const newHeader = new Headers(resp.headers);
    newHeader.set('location', proxify(target.href));
    const redirectResp = new Response(undefined, {
      status: resp.status,
      statusText: resp.statusText,
      headers: newHeader
    })
    return redirectResp;
  }
  if (resp.headers.get('content-type') === 'application/vnd.apple.mpegurl') {
    let respText = await resp.text();
    respText = proxify(respText);
    return new Response(respText);
  }
  return resp;
}

async function fetchOverTcp(request) {
  let url = new URL(request.url);
  let req = new Request(url, request);
  let port_string = url.port;
  if (!port_string) {
    port_string = url.protocol === "http:" ? "80" : "443";
  }
  let port = parseInt(port_string);

  if (
    (url.protocol === "https:" && port === 443) ||
    (url.protocol === "http:" && port === 80)
  ) {
    // CF标准的反代不支持IP地址,所以IPV6要走TCP代理
    if (!isIP(url.host)) {
      console.log('fetch natively');
      return await fetch(req);
    }
  }

  // 创建 TCP 连接
  let tcpSocket = connect(
    {
      hostname: url.hostname,
      port: port,
    },
    JSON.parse('{"secureTransport": "starttls"}')
  );

  if (url.protocol === "https:") {
    tcpSocket = tcpSocket.startTls();
  }

  try {
    const writer = tcpSocket.writable.getWriter();

    // 构造请求头部
    let headersString = "";
    let bodyString = "";

    for (let [name, value] of req.headers) {
      if (
        name === "connection" ||
        name === "host" ||
        name === "accept-encoding"
      ) {
        continue;
      }
      headersString += `${name}: ${value}\r\n`;
    }
    headersString += `connection: close\r\n`;
    headersString += `accept-encoding: identity\r\n`;

    let fullpath = url.pathname;

    // 如果有查询参数,将其添加到路径
    if (url.search) {
      fullpath += url.search.replace(/%3F/g, "?");
    }

    const body = await req.text();
    bodyString = `${body}`;

    // 发送请求
    await writer.write(
      new TextEncoder().encode(
        `${req.method} ${fullpath} HTTP/1.0\r\nHost: ${url.hostname}:${port}\r\n${headersString}\r\n${bodyString}`
      )
    );
    writer.releaseLock();

    // 获取响应
    const response = await constructHttpResponse(tcpSocket);

    console.log(
      "fetchOverTcp response headers",
      JSON.parse(
        JSON.stringify(
          Object.fromEntries(Array.from(response.headers.entries()))
        )
      )
    );

    return response;
  } catch (error) {
    console.log("fetchOverTcp Exception", error);
    tcpSocket.close();
    return new Response(error.stack, { status: 500 });
  }
}

async function constructHttpResponse(tcpSocket, timeout) {
  const reader = tcpSocket.readable.getReader();
  let remainingData = new Uint8Array(0);
  try {
    // 读取响应数据
    while (true) {
      const { value, done } = await reader.read();
      const newData = new Uint8Array(remainingData.length + value.length);
      newData.set(remainingData);
      newData.set(value, remainingData.length);
      remainingData = newData;
      const index = indexOfDoubleCRLF(remainingData);
      if (index !== -1) {
        reader.releaseLock();
        const headerBytes = remainingData.subarray(0, index);
        const bodyBytes = remainingData.subarray(index + 4);

        const header = new TextDecoder().decode(headerBytes);
        const [statusLine, ...headers] = header.split("\r\n");
        const [httpVersion, statusCode, ...tmpStatusText] =
          statusLine.split(" ");
        let statusText = tmpStatusText.join(" ");

        // 构造 Response 对象
        let responseHeaders = JSON.parse("{}");
        headers.forEach((header) => {
          const [name, value] = header.split(": ");
          responseHeaders[name.toLowerCase()] = value;
        });

        responseHeaders = JSON.parse(JSON.stringify(responseHeaders));
        console.log("orginal responseHeaders", responseHeaders);

        const responseInit = {
          status: parseInt(statusCode),
          statusText,
          headers: new Headers(responseHeaders),
        };

        console.log("statusCode", statusCode);

        let readable = null;
        let writable = null;
        let stream = null;
        if (responseHeaders["content-length"]) {
          stream = new FixedLengthStream(
            parseInt(responseHeaders["content-length"])
          );
        } else {
          stream = new TransformStream();
        }
        readable = stream.readable;
        writable = stream.writable;

        //规避CF问题,延迟1ms执行
        function delayedExecution() {
          setTimeout(() => {
            let writer = writable.getWriter();
            writer.write(bodyBytes);
            writer.releaseLock();
            tcpSocket.readable.pipeTo(writable);
          }, 1);
        }
        delayedExecution();

        return new Response(readable, responseInit);
      }
      if (done) {
        tcpSocket.close();
        break;
      }
    }

    console.log("Response Done!");
    return new Response();
  } catch (error) {
    console.log("Construct Response Exception", error);
    tcpSocket.close();
  }
}

function indexOfDoubleCRLF(data) {
  if (data.length < 4) {
    return -1;
  }
  for (let i = 0; i < data.length - 3; i++) {
    if (
      data[i] === 13 &&
      data[i + 1] === 10 &&
      data[i + 2] === 13 &&
      data[i + 3] === 10
    ) {
      return i;
    }
  }
  return -1;
}

function isIPv4(str) {
  // IPv4正则表达式
  const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
  return ipv4Regex.test(str);
}

function isIPv6(str) {
  let new_str = str.replace(/\[|\]/g, "");
  const ipv6Regex =
    /^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/;
  return ipv6Regex.test(new_str);
}

function isIP(str) {
  // 判断是IPv4还是IPv6
  if (str.includes("[") && str.includes("]")) {
    return isIPv6(str);
  } else {
    return isIPv4(str);
  }
}
Jack #31 2024年07月09日

发现运行后4gtv没加代理,加了4gtv和litv的标签后就可以都转了,试了下不代理可以看但有点卡

回复此楼 修改
diypad #32 2024年07月10日

请教下,用这个worker.js部署后看不卡,最新服务器重新部署更换成AlmaLinux 8 64 Bit了,重新搭建pixman和cf worker之后播放卡得很,挂梯子播放才不卡。请问有解决方法么

回复此楼 修改
ck95960 #33 2024年07月22日

部署后打不开,这是什么情况

回复此楼 修改
icyboy168 #34 2024年07月25日

牛人越来越多了

回复此楼 修改
ccf #35 2024年10月17日

现在还好使么,好些放不出来了

回复此楼 修改
Anaconda #36 2025年06月03日

还是放不出来,得挂梯啊

回复此楼 修改
  • 上一页
  • 1
  • 2
收到新回复,点击立即加载
需要 登录 后方可回复, 如果你还没有账号请 注册新账号
cs6pf
@stlyx
统计信息
会员
1,786
话题
259
回帖
2,955
活跃会员 查看排行
Devin
Koguan
nt01xp
jmk92
859876741
kewycjy
cnvc
kkk00011
0xyyyy
wayne630
zf7222749
zmy123
Unlovely
bos5201314
scjxx
once
wykds
LeoXD
a15ban
gally

A digital lifestyle community co-maintained by Pixer

本网站服务器由 提供,CDN 由 提供,基于 Homeland 构建,采用 Docker 部署。