基于之前有人做的 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);
}
}