// ==UserScript== // @name 哔哩哔哩 CDN 优选 // @namespace http://tampermonkey.net/ // @version 1.3 // @description 哔哩哔哩 CDN 优选,逻辑参考了 https://github.com/guozhigq/pilipala,大部分代码由 ai 完成。 // @author Moranjianghe // @match *://*.bilibili.com/* // @icon https://www.bilibili.com/favicon.ico // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @run-at document-start // @connect proxy-tf-all-ws.bilivideo.com // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; /** * B站视频与直播CDN优化与MCDN代理类 */ class BilibiliCDNOptimizer { constructor() { // CDN 节点列表,与 pilipala 保持一致,只保留最稳定的三个 this.cdnList = { 'ali': 'upos-sz-mirrorali.bilivideo.com', // 阿里云 (推荐) 'cos': 'upos-sz-mirrorcos.bilivideo.com', // 腾讯云 'hw': 'upos-sz-mirrorhw.bilivideo.com', // 华为云 }; // 使用与 pilipala 相同的正则表达式模式 this.upgcxcodeRegex = /(http|https):\/\/(.*?)\/upgcxcode\//; // 初始化设置 this.enableCDNOptimize = GM_getValue('enableCDNOptimize', true); this.enableMCDNProxy = GM_getValue('enableMCDNProxy', true); this.preferredCDN = GM_getValue('preferredCDN', 'ali'); this.debugMode = GM_getValue('debugMode', false); // 备用URL存储 this.backupUrls = new Map(); // 初始化后台拦截 this.initInterceptors(); // 注册菜单命令 this.registerMenuCommands(); this.log("B站视频与直播CDN优化与MCDN代理已初始化"); } /** * 日志记录函数 * @param {string} message - 日志消息 */ log(message) { if (this.debugMode) { console.log(`[CDN] ${message}`); } } /** * 注册用户脚本菜单命令 */ registerMenuCommands() { // CDN优化开关 GM_registerMenuCommand(`${this.enableCDNOptimize ? '✅' : '❌'} CDN优化`, () => { this.enableCDNOptimize = !this.enableCDNOptimize; GM_setValue('enableCDNOptimize', this.enableCDNOptimize); this.log(`CDN优化已${this.enableCDNOptimize ? '启用' : '禁用'}`); location.reload(); }); // MCDN代理开关 GM_registerMenuCommand(`${this.enableMCDNProxy ? '✅' : '❌'} MCDN代理`, () => { this.enableMCDNProxy = !this.enableMCDNProxy; GM_setValue('enableMCDNProxy', this.enableMCDNProxy); this.log(`MCDN代理已${this.enableMCDNProxy ? '启用' : '禁用'}`); location.reload(); }); // CDN选择菜单 GM_registerMenuCommand(`🔄 当前CDN: ${this.preferredCDN}`, () => { const cdnKeys = Object.keys(this.cdnList); const currentIndex = cdnKeys.indexOf(this.preferredCDN); const nextIndex = (currentIndex + 1) % cdnKeys.length; this.preferredCDN = cdnKeys[nextIndex]; GM_setValue('preferredCDN', this.preferredCDN); this.log(`已切换CDN为: ${this.preferredCDN}`); alert(`已切换CDN为: ${this.preferredCDN} (${this.cdnList[this.preferredCDN]})`); }); // 调试模式开关 GM_registerMenuCommand(`${this.debugMode ? '✅' : '❌'} 调试模式`, () => { this.debugMode = !this.debugMode; GM_setValue('debugMode', this.debugMode); this.log(`调试模式已${this.debugMode ? '启用' : '禁用'}`); }); } /** * 初始化网络请求拦截器 */ initInterceptors() { // 拦截XMLHttpRequest this.interceptXHR(); // 拦截Fetch this.interceptFetch(); // 监听DOM变化处理视频元素 this.observeDOM(); // 拦截媒体源扩展(MSE) this.interceptMediaSource(); } /** * 拦截XMLHttpRequest请求 */ interceptXHR() { const self = this; const originalXHROpen = XMLHttpRequest.prototype.open; const originalXHRSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url, async, user, password) { // 保存原始URL以便在send中使用 this._originalUrl = url; const optimizedUrl = self.optimizeUrl(url); if (optimizedUrl && optimizedUrl !== url) { self.log(`XHR请求已优化: ${url} -> ${optimizedUrl}`); originalXHROpen.call(this, method, optimizedUrl, async !== false, user, password); } else { originalXHROpen.call(this, method, url, async !== false, user, password); } }; // 拦截响应以提取备用URL XMLHttpRequest.prototype.send = function(body) { const xhr = this; const originalUrl = xhr._originalUrl; // 检查是否是视频信息API或直播流API if (originalUrl && ( originalUrl.includes('/x/player/playurl') || originalUrl.includes('/x/player/wbi/playurl') || originalUrl.includes('/pgc/player/web/playurl') || // 直播相关API originalUrl.includes('/live_stream/v1/') || originalUrl.includes('/xlive/web-room/v1/') || originalUrl.includes('/live_player/v1/') )) { // 添加响应监听器 xhr.addEventListener('load', function() { try { if (xhr.responseType === 'json' || xhr.responseType === '') { const response = xhr.responseType === 'json' ? xhr.response : JSON.parse(xhr.responseText); self.extractBackupUrls(response); } } catch (e) { self.log(`提取备用URL错误: ${e.message}`); } }); } originalXHRSend.call(this, body); }; } /** * 拦截Fetch请求 */ interceptFetch() { const self = this; const originalFetch = window.fetch; window.fetch = function(resource, init) { let url = ''; let originalRequest = null; if (typeof resource === 'string') { url = resource; const optimizedUrl = self.optimizeUrl(url); if (optimizedUrl && optimizedUrl !== url) { self.log(`Fetch请求已优化: ${url} -> ${optimizedUrl}`); resource = optimizedUrl; } } else if (resource instanceof Request) { url = resource.url; originalRequest = resource.clone(); const optimizedUrl = self.optimizeUrl(url); if (optimizedUrl && optimizedUrl !== url) { self.log(`Fetch Request已优化: ${url} -> ${optimizedUrl}`); resource = new Request(optimizedUrl, resource); } } // 判断是否是视频信息API或直播流API const isVideoApi = url && ( url.includes('/x/player/playurl') || url.includes('/x/player/wbi/playurl') || url.includes('/pgc/player/web/playurl') || // 直播相关API url.includes('/live_stream/v1/') || url.includes('/xlive/web-room/v1/') || url.includes('/live_player/v1/') ); // 执行原始fetch请求 return originalFetch.call(window, resource, init).then(response => { // 如果是视频API,提取备用URL if (isVideoApi) { response.clone().json().then(data => { self.extractBackupUrls(data); }).catch(err => { self.log(`提取备用URL错误: ${err.message}`); }); } return response; }); }; } /** * 拦截MediaSource */ interceptMediaSource() { if (window.MediaSource) { const self = this; const originalAddSourceBuffer = MediaSource.prototype.addSourceBuffer; MediaSource.prototype.addSourceBuffer = function(mimeType) { self.log(`MediaSource添加缓冲区: ${mimeType}`); const sourceBuffer = originalAddSourceBuffer.call(this, mimeType); return sourceBuffer; }; } } /** * 获取URL对应的备用URL * @param {string} url - 原始URL * @returns {string|null} 备用URL或null */ getBackupUrl(url) { // 只取 baseUrl 部分 const baseInputUrl = url.split('?')[0]; if (this.backupUrls.has(baseInputUrl)) { return this.backupUrls.get(baseInputUrl); } return null; } /** * 从API响应中提取备用URL * @param {Object} data - API响应数据 */ extractBackupUrls(data) { try { if (data && data.data) { const responseData = data.data; // 处理普通视频的备用URL if (responseData.durl && Array.isArray(responseData.durl)) { responseData.durl.forEach((item) => { if (item.url) { const baseUrl = item.url.split('?')[0]; if (item.backup_url && Array.isArray(item.backup_url)) { item.backup_url.forEach(backupUrl => { if (backupUrl && backupUrl.includes('http')) { this.backupUrls.set(baseUrl, backupUrl); this.log(`提取到备用URL: ${backupUrl} (对应 ${baseUrl})`); } }); } } }); } // 处理DASH格式 if (responseData.dash) { if (responseData.dash.video && Array.isArray(responseData.dash.video)) { responseData.dash.video.forEach(video => { if (video.base_url && video.backup_url && Array.isArray(video.backup_url)) { const baseUrl = video.base_url.split('?')[0]; video.backup_url.forEach(backupUrl => { if (backupUrl && backupUrl.includes('http')) { this.backupUrls.set(baseUrl, backupUrl); this.log(`提取到视频备用URL: ${backupUrl} (对应 ${baseUrl})`); } }); } }); } if (responseData.dash.audio && Array.isArray(responseData.dash.audio)) { responseData.dash.audio.forEach(audio => { if (audio.base_url && audio.backup_url && Array.isArray(audio.backup_url)) { const baseUrl = audio.base_url.split('?')[0]; audio.backup_url.forEach(backupUrl => { if (backupUrl && backupUrl.includes('http')) { this.backupUrls.set(baseUrl, backupUrl); this.log(`提取到音频备用URL: ${backupUrl} (对应 ${baseUrl})`); } }); } }); } } // 处理直播流格式 if (responseData.playurl_info && responseData.playurl_info.playurl) { const playurl = responseData.playurl_info.playurl; if (playurl.stream && Array.isArray(playurl.stream)) { playurl.stream.forEach(stream => { if (stream.format && Array.isArray(stream.format)) { stream.format.forEach(format => { if (format.codec && Array.isArray(format.codec)) { format.codec.forEach(codec => { // 处理直播流URLs if (codec.url_info && Array.isArray(codec.url_info)) { const baseUrl = codec.base_url.split('?')[0]; // 找到第一个有效URL codec.url_info.some(urlInfo => { if (urlInfo && urlInfo.host && codec.base_url) { const fullUrl = urlInfo.host + codec.base_url + (urlInfo.extra || ''); if (fullUrl && fullUrl.includes('http')) { this.backupUrls.set(baseUrl, fullUrl); this.log(`提取到直播备用URL: ${fullUrl} (对应 ${baseUrl})`); return true; } } return false; }); } }); } }); } }); } } } } catch (e) { this.log(`解析备用URL错误: ${e.message}`); } } /** * 判断URL是否是直播流URL * @param {string} url - 要检查的URL * @returns {boolean} 是否是直播流URL */ isLiveStreamUrl(url) { return url && ( url.includes('/live-bvc/') || url.includes('/live_stream/v1/') || url.includes('/xlive/web-room/v1/') || url.includes('/live_player/v1/') || url.includes('/live-bvc/') ); } /** * 优化视频URL,综合处理备用URL、MCDN代理和CDN优选 * @param {string} originalUrl - 原始URL * @param {string} backupUrl - 备用URL * @returns {string} 优化后的URL */ optimizeVideoUrl(originalUrl, backupUrl = '') { // 只处理 http/https 协议 if (!/^https?:\/\//.test(originalUrl)) { return originalUrl; } this.log(`原始URL: ${originalUrl}`); this.log(`备用URL: ${backupUrl}`); // 检查CDN优化是否启用 const enableCdn = this.enableCDNOptimize; if (!enableCdn && !this.enableMCDNProxy) { this.log('CDN优化和MCDN代理都已禁用,使用原始URL'); return originalUrl; } // 判断是否是直播流 const isLiveStream = this.isLiveStreamUrl(originalUrl); if (isLiveStream) { this.log('检测到直播流URL'); } // 参考 pilipala 的实现,简化备用 URL 处理逻辑 // 优先使用 backupUrl,通常是 upgcxcode 地址,播放更稳定 let videoUrl = ''; if (backupUrl && backupUrl.includes('http')) { videoUrl = backupUrl; this.log('使用备用URL'); } else { videoUrl = originalUrl; this.log('使用原始URL'); } // 参考 pilipala 的实现方法 // 处理 mcdn 域名 - 使用代理 if (this.enableMCDNProxy && videoUrl.includes(".mcdn.bilivideo")) { this.log(`检测到mcdn域名: ${videoUrl}`); const proxyUrl = `https://proxy-tf-all-ws.bilivideo.com/?url=${encodeURIComponent(videoUrl)}`; this.log(`使用代理: ${proxyUrl}`); return proxyUrl; } // 处理 upgcxcode 路径,替换为优选 CDN if (enableCdn && videoUrl.includes("/upgcxcode/")) { this.log(`检测到upgcxcode路径,替换CDN`); // 获取用户选择的 CDN const preferredCdn = this.preferredCDN; // 获取对应的 CDN 主机名 const cdn = this.cdnList[preferredCdn] || this.cdnList['ali']; // 使用正则表达式替换域名部分,与 pilipala 保持一致的替换方式 const replacedUrl = videoUrl.replace(this.upgcxcodeRegex, `https://${cdn}/upgcxcode/`); this.log(`替换CDN: ${preferredCdn} -> ${cdn}`); return replacedUrl; } // 处理直播流的CDN优化 if (isLiveStream && enableCdn) { // 如果直播流包含CDN主机名,尝试优化 for (const domain of Object.values(this.cdnList)) { if (videoUrl.includes(domain)) { this.log('直播流已经使用预设CDN,无需优化'); return videoUrl; } } // 检测非直播流URL模式 if (videoUrl.includes('hls') || videoUrl.includes('flv')) { const preferredCdn = this.preferredCDN; const cdn = this.cdnList[preferredCdn] || this.cdnList['ali']; // 尝试从URL中提取主机名,然后用优选CDN替换 const urlObj = new URL(videoUrl); const oldHost = urlObj.host; // 只有当主机名不包含预设CDN时进行替换 if (!oldHost.includes('upos-sz-mirror') && oldHost !== cdn) { this.log(`直播流尝试优化CDN: ${oldHost} -> ${cdn}`); // 只有具有普通CDN结构的URL才尝试替换 if (oldHost.includes('bilivideo.com')) { urlObj.host = cdn; const newUrl = urlObj.toString(); this.log(`直播流CDN优化: ${videoUrl} -> ${newUrl}`); return newUrl; } } } } this.log('无需优化,返回原始URL'); return videoUrl; } /** * 优化普通URL,用于XHR/Fetch拦截 * @param {string} url - 原始URL * @returns {string} 优化后的URL */ optimizeUrl(url) { if (!url) return url; try { // 获取可能的备用URL const backupUrl = this.getBackupUrl(url); // 使用完整的优化逻辑 return this.optimizeVideoUrl(url, backupUrl); } catch (error) { this.log(`URL优化错误: ${error.message}`); return url; } } /** * 使用代理服务器代理MCDN请求 * @param {string} url - MCDN URL * @returns {string} 代理后的URL */ proxyMCDN(url) { const proxyUrl = `https://proxy-tf-all-ws.bilivideo.com/?url=${encodeURIComponent(url)}`; this.log(`MCDN代理URL: ${proxyUrl}`); return proxyUrl; } /** * 替换CDN为优选节点 * @param {string} url - 包含upgcxcode的URL * @returns {string} 替换CDN后的URL */ replaceCDN(url) { const cdn = this.cdnList[this.preferredCDN] || this.cdnList['ali']; // 使用与 pilipala 相同的正则表达式模式 const replacedUrl = url.replace(this.upgcxcodeRegex, `https://${cdn}/upgcxcode/`); this.log(`替换CDN: ${this.preferredCDN} (${cdn})`); return replacedUrl; } /** * 获取当前CDN信息 * @returns {Object} CDN信息 */ getCDNInfo() { return { key: this.preferredCDN, host: this.cdnList[this.preferredCDN], enabled: this.enableCDNOptimize }; } /** * 获取MCDN代理信息 * @returns {Object} MCDN代理信息 */ getMCDNProxyInfo() { return { enabled: this.enableMCDNProxy, proxy: 'proxy-tf-all-ws.bilivideo.com' }; } /** * 获取备用URL统计信息 * @returns {Object} 备用URL统计 */ getBackupUrlStats() { return { count: this.backupUrls.size, urls: Array.from(this.backupUrls.entries()).slice(0, 5) // 仅返回前5个示例 }; } /** * 观察DOM变化以处理视频元素 */ observeDOM() { const self = this; // 在DOM加载完成后执行 window.addEventListener('DOMContentLoaded', () => { self.processVideoElements(); // 观察DOM变化 const observer = new MutationObserver(() => { self.processVideoElements(); }); observer.observe(document.body, { childList: true, subtree: true }); self.log('DOM观察器已激活'); }); } /** * 处理页面中的视频元素 */ processVideoElements() { // 查找视频和直播元素,可能需要处理URL const videoElements = document.querySelectorAll('video'); for (const video of videoElements) { if (video.src && video.src.startsWith('http')) { const originalSrc = video.src; const optimizedSrc = this.optimizeUrl(originalSrc); if (optimizedSrc !== originalSrc) { this.log(`优化视频/直播元素URL: ${originalSrc} -> ${optimizedSrc}`); video.src = optimizedSrc; } } // 处理可能的source子元素 const sources = video.querySelectorAll('source'); for (const source of sources) { if (source.src && source.src.startsWith('http')) { const originalSrc = source.src; const optimizedSrc = this.optimizeUrl(originalSrc); if (optimizedSrc !== originalSrc) { this.log(`优化视频/直播source元素URL: ${originalSrc} -> ${optimizedSrc}`); source.src = optimizedSrc; } } } } } } // 创建并初始化B站CDN优化器 const bilibiliCDNOptimizer = new BilibiliCDNOptimizer(); // 将优化器实例暴露到全局,方便调试 window.bilibiliCDNOptimizer = bilibiliCDNOptimizer; })();