// ==UserScript==
// @name 大魔王视频助手<移动版>
// @namespace http://tampermonkey.net/
// @version 1.1
// @description 自动接管视频,精美悬浮播放,去除广告片段,旋转手机无缝进全屏,全屏下支持滑动快进/快退。提升手机网页看视频体验!
// @author bug大魔王
// @match *://*/*
// @require https://cdn.jsdelivr.net/npm/hls.js@1.5.8/dist/hls.min.js
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_notification
// @grant GM_info
// @run-at document-start
// @icon https://n.sinaimg.cn/sinacn10/576/w700h676/20181001/87c2-hkvrhpr8368697.jpg
// @downloadURL https://update.greasyfork.icu/scripts/542626/%E5%A4%A7%E9%AD%94%E7%8E%8B%E8%A7%86%E9%A2%91%E5%8A%A9%E6%89%8B%3C%E7%A7%BB%E5%8A%A8%E7%89%88%3E.user.js
// @updateURL https://update.greasyfork.icu/scripts/542626/%E5%A4%A7%E9%AD%94%E7%8E%8B%E8%A7%86%E9%A2%91%E5%8A%A9%E6%89%8B%3C%E7%A7%BB%E5%8A%A8%E7%89%88%3E.meta.js
// ==/UserScript==
(function () {
'use strict';
const SCRIPT_NAME = '大魔王视频助手';
const SCRIPT_VERSION = '1.1';
const IS_TOP_FRAME = window.self === window.top;
const MESSAGE_TYPES = {
M3U8_COMMAND: 'M3U8_PLAYER_COMMAND_V2_FINAL',
DIAG_HANDSHAKE: 'DMZ_DIAG_HANDSHAKE_V2_FINAL',
};
const IframeTerminator = {
observer: null,
keywords: ['/acs/phone/', 'googleads', 'googlesyndication', 'doubleclick.net', '/ad/'],
processedIframes: new WeakSet(),
terminate(iframe) { if (this.processedIframes.has(iframe)) return false; try { if (iframe.src && this.keywords.some(keyword => iframe.src.includes(keyword))) { ActionLogger.log(`IframeTerminator: 禁用干扰性 iFrame -> ${iframe.src}`, 'TERMINATOR'); this.processedIframes.add(iframe); iframe.src = 'about:blank'; iframe.style.setProperty('visibility', 'hidden', 'important'); iframe.style.setProperty('border', 'none', 'important'); return true; } } catch (e) { /* ignore */ } return false; },
scanAndTerminate(node) { if (!node || node.nodeType !== Node.ELEMENT_NODE) return; const iframes = node.matches('iframe') ? [node] : node.querySelectorAll('iframe'); iframes.forEach(iframe => this.terminate(iframe)); },
start() {
if (this.observer || !IS_TOP_FRAME) return;
this.observer = new MutationObserver(mutations => { for (const mutation of mutations) { for (const addedNode of mutation.addedNodes) { this.scanAndTerminate(addedNode); } } });
this.observer.observe(document.documentElement, { childList: true, subtree: true });
if (document.readyState === 'loading') { window.addEventListener('DOMContentLoaded', ()=>this.scanAndTerminate(document.documentElement), { once: true }); } else { this.scanAndTerminate(document.documentElement); }
},
stop() { if (this.observer) { this.observer.disconnect(); this.observer = null; } }
};
const ActionLogger = { logs: [], maxEntries: 150, log(message, type = 'INFO') { if (this.logs.length >= this.maxEntries) { this.logs.shift(); } const logEntry = { type, message, frame: IS_TOP_FRAME ? 'Top' : 'iFrame' }; this.logs.push(logEntry); if (window.DiagnosticsTool) DiagnosticsTool.logEvent(logEntry); }, getLogs() { return this.logs; } };
const DiagnosticsTool = {
panelId: 'dmz-diagnostics-panel',
eventTimeline: [],
maxLogEntries: 300,
startTime: new Date(),
takeoverSource: null,
iframeHandshakeStatus: {},
captureTakeoverSource(sourceName) { this.takeoverSource = sourceName; },
logEvent(event) {
if (this.eventTimeline.length >= this.maxLogEntries) { this.eventTimeline.shift(); }
const now = new Date();
event.time = now;
event.relativeTime = `+${((now - this.startTime) / 1000).toFixed(3)}s`;
this.eventTimeline.push(event);
},
init() {
this.logEvent({type: 'LIFECYCLE', message: `脚本 v${SCRIPT_VERSION} 开始执行 (document-start)`});
window.addEventListener('message', (event) => {
if (event.data?.type === MESSAGE_TYPES.DIAG_HANDSHAKE && event.data.action === 'request') {
event.source.postMessage({ type: MESSAGE_TYPES.DIAG_HANDSHAKE, action: 'response', version: SCRIPT_VERSION }, event.origin);
}
});
if (IS_TOP_FRAME) {
window.addEventListener('message', (event) => {
if(event.data?.type === MESSAGE_TYPES.DIAG_HANDSHAKE && event.data.action === 'response') {
for (const key in this.iframeHandshakeStatus) {
if (this.iframeHandshakeStatus[key].status === 'pending') {
try {
if (new URL(key).origin === event.origin) {
this.iframeHandshakeStatus[key] = { status: 'success', version: event.data.version };
return;
}
} catch (e) { /* ignore */ }
}
}
}
});
}
},
async run() {
if (!IS_TOP_FRAME) { alert('诊断功能只能在顶层页面启动。'); return; }
ActionLogger.log('诊断工具已启动,开始生成报告...', 'DIAG');
try {
await this.performIframeHandshake();
const report = this.generateReport();
this.showPanel(report);
} catch (error) { console.error(`[${SCRIPT_NAME}] 诊断工具崩溃:`, error); }
},
async performIframeHandshake() {
ActionLogger.log('开始 iFrame 握手测试...', 'HANDSHAKE');
this.iframeHandshakeStatus = {};
const iframes = document.querySelectorAll('iframe');
if (iframes.length === 0) return;
const handshakePromises = Array.from(iframes).map((iframe, index) => {
return new Promise(resolve => {
const iframeSrc = iframe.src || `internal-iframe-${index}`;
this.iframeHandshakeStatus[iframeSrc] = { status: 'pending' };
try {
if (iframe.contentWindow) { iframe.contentWindow.postMessage({ type: MESSAGE_TYPES.DIAG_HANDSHAKE, action: 'request' }, '*'); }
else { throw new Error('No contentWindow'); }
} catch (e) { this.iframeHandshakeStatus[iframeSrc] = { status: 'error', reason: '跨域安全限制' }; resolve(); return; }
setTimeout(() => {
if (this.iframeHandshakeStatus[iframeSrc]?.status === 'pending') { this.iframeHandshakeStatus[iframeSrc] = { status: 'timeout' }; }
resolve();
}, 1000);
});
});
await Promise.all(handshakePromises);
ActionLogger.log('iFrame 握手测试完成。', 'HANDSHAKE');
},
showPanel(reportText) { if (document.getElementById(this.panelId)) return; const panelCSS = ` #${this.panelId} { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.8); z-index: 2147483647 !important; display: flex; justify-content: center; align-items: center; font-family: sans-serif; } #${this.panelId} .wrapper { width: 90%; max-width: 800px; height: 85%; max-height: 700px; background: #282c34; color: #abb2bf; border-radius: 8px; box-shadow: 0 5px 25px rgba(0,0,0,0.5); display: flex; flex-direction: column; } #${this.panelId} h2 { margin: 0; padding: 15px 20px; color: #61afef; border-bottom: 1px solid #3b4048; font-size: 1.2em; } #${this.panelId} textarea { flex-grow: 1; background: #21252b; color: #abb2bf; border: none; padding: 15px; margin: 0; font-family: monospace; font-size: 12px; resize: none; white-space: pre; } #${this.panelId} .footer { padding: 10px 20px; display: flex; justify-content: flex-end; gap: 15px; border-top: 1px solid #3b4048; } #${this.panelId} button { padding: 8px 16px; border: 1px solid #61afef; background: transparent; color: #61afef; border-radius: 4px; cursor: pointer; transition: all 0.2s; } #${this.panelId} button:hover { background: #61afef; color: #282c34; }`; const style = document.createElement('style'); style.textContent = panelCSS; const panel = document.createElement('div'); panel.id = this.panelId; panel.innerHTML = `
${SCRIPT_NAME} 诊断报告
`; document.body.appendChild(panel); document.head.appendChild(style); panel.querySelector('textarea').value = reportText; panel.querySelector('#dmz-diag-copy').addEventListener('click', (e) => { navigator.clipboard.writeText(reportText).then(() => { e.target.textContent = '已复制!'; setTimeout(() => { e.target.textContent = '复制到剪贴板'; }, 2000); }); }); panel.querySelector('#dmz-diag-close').addEventListener('click', () => { panel.remove(); style.remove(); }); },
generateReport() {
const nl = '\n';
const section = (title) => nl + `--- ${title} ---` + nl;
let report = `### ${SCRIPT_NAME} 诊断报告 ###` + nl;
report += `版本: ${SCRIPT_VERSION}` + nl;
const siteAnalysis = this.analyzeSiteStructure();
report += this.generateHumanFriendlySummary(siteAnalysis);
report += section("🕵️ 现场勘查报告 (开发者专用)");
report += `页面URL: ${window.location.href}` + nl;
if (siteAnalysis.iframes.length > 0) {
report += section("📢 iFrame 通信握手测试");
siteAnalysis.iframes.forEach((f, i) => {
const status = this.iframeHandshakeStatus[f.src || `internal-iframe-${i}`] || { status: 'unknown' };
let statusText = '未知';
switch(status.status) {
case 'success': statusText = `✅ 成功!(版本: ${status.version})`; break;
case 'timeout': statusText = `❌ 失败 (超时无响应)`; break;
case 'error': statusText = `❌ 失败 (${status.reason})`; break;
}
report += `[${i+1}] iFrame (${f.src || '空'}): ${statusText}` + nl;
});
}
report += section(`🕒 统一事件时间轴 (最新 ${this.maxLogEntries} 条)`);
[...this.eventTimeline].sort((a, b) => b.time - a.time).forEach(log => {
report += `[${log.relativeTime}] [${log.type}](${log.frame}) ${log.message}` + nl;
});
return report;
},
analyzeSiteStructure() {
const iframes = Array.from(document.querySelectorAll('iframe')).map(f => {
let originInfo = '同源';
try {
const hasAccess = f.contentWindow && f.contentWindow.document;
if (!hasAccess) throw new Error('Access Denied');
} catch (e) {
let origin = '未知';
try { origin = new URL(f.src, window.location.href).origin; } catch (e) { /* ignore */ }
originInfo = `跨域 (origin: ${origin})`;
}
return { src: f.src, originInfo };
});
return { iframes };
},
generateHumanFriendlySummary(siteAnalysis) {
const nl = '\n';
let story = nl + '--- ✅ 小白看板 (用户友好诊断) ---' + nl + nl;
const findLast = (typeOrSubstring) => [...this.eventTimeline].reverse().find(e => e.type.includes(typeOrSubstring) || e.message.includes(typeOrSubstring));
const playerCreationEvent = findLast("创建播放器基础容器");
const isOurPlayerPresent = !!(window.PlayerManager && PlayerManager.shadowRoot?.getElementById(C.PLAYER_ID));
const revealEvent = findLast('PLAYER_REVEAL');
const hlsParsedEvent = findLast('HLS: MANIFEST_PARSED');
const fatalErrorEvent = this.eventTimeline.find(e => e.type === 'ERROR' && e.message.includes('fatal=true'));
if (revealEvent) {
story += '🕵️ **[事件回放]**' + nl;
story += `1. **捕获成功!** 脚本通过【${this.takeoverSource || '智能分析'}】成功捕获到了视频信号!` + nl;
story += '2. 脚本响应信号,并为您创建了专属的悬浮播放器。' + nl;
story += '3. 播放器成功加载视频数据并展开动画,进入播放状态。' + nl + nl;
if (fatalErrorEvent && fatalErrorEvent.time > revealEvent.time) {
story += '⚠️ **[后续问题]**' + nl;
story += `不过,在播放过程中视频流似乎中断了。错误详情: ${fatalErrorEvent.message}` + nl + nl;
story += 'ախ **[最终诊断]**' + nl;
story += ' - ⚠️ **播放中断:视频流传输失败。**';
} else {
story += 'ախ **[最终诊断]**' + nl;
story += ' - ✅ **正在播放中...** 目前一切顺利!';
}
return story;
}
if (playerCreationEvent) {
story += '🕵️ **[事件回放]**' + nl;
story += `1. **捕获成功!** 脚本通过【${this.takeoverSource || '智能分析'}】成功捕获到了一个视频信号!` + nl;
story += '2. 脚本响应信号,并成功为您创建了专属的悬浮播放器(您看到的卷轴)。' + nl;
if (fatalErrorEvent) {
story += '3. 但在尝试为您加载视频核心内容时,遇到了一个无法恢复的致命错误。' + nl + nl;
story += '❓ **[为什么卷轴出现了但视频没出来,甚至直接消失了?]**' + nl;
story += `这是脚本的【安全保护机制】。当遇到致命错误时(例如视频防盗链、内容已删除),脚本会主动停止加载并关闭播放器,以避免您的浏览器卡死。` + nl;
story += `本次遇到的错误详情: ${fatalErrorEvent.message}` + nl + nl;
const httpCodeMatch = fatalErrorEvent.message.match(/Response: \[(\d{3})\]/);
const httpCode = httpCodeMatch ? parseInt(httpCodeMatch[1], 10) : null;
story += 'ախ **[最终诊断]**' + nl;
switch (httpCode) {
case 403: story += ` - ❌ **接管失败:防盗链机制 (错误码: 403 Forbidden)。** 对方服务器拒绝了我们的访问请求。`; break;
case 404: story += ` - ❌ **接管失败:视频内容不存在 (错误码: 404 Not Found)。** 视频可能已被删除。`; break;
case 0: story += ` - ❌ **接管失败:网络策略问题 (CORS)。** 您的浏览器出于安全原因阻止了脚本访问视频数据。`; break;
default: story += ` - ❌ **接管失败:未知网络问题。** 可能是网络不稳定或服务器临时故障。`; break;
}
} else if (hlsParsedEvent) {
story += '3. 播放器成功读取了视频的“目录”(M3U8),但在根据目录去获取第一片视频内容时卡住了。' + nl + nl;
story += '❓ **[为什么只看到卷轴,但播放器没有展开?]**' + nl;
story += ' 这正是问题的核心。我们拿到了视频的“地图”,但在去取回第一块“宝藏”(视频数据)时被拦截或超时了。没有取回任何实质内容,播放器就无法知道视频的尺寸、时长等信息,因此无法执行“展开”动画。' + nl + nl;
story += 'ախ **[最终诊断]**' + nl;
story += ` - 🟡 **接管不完全:视频元数据加载失败。** 这通常也是由更隐蔽的防盗链或网络问题导致的。`;
} else if (!isOurPlayerPresent) {
story += '3. 但由于某个内部错误,播放器创建后被意外移除了。' + nl;
story += '4. **关键缺陷:** 脚本在“引退”后,未能将原页面的播放器彻底“中和”。' + nl + nl;
story += '❓ **[为什么我看到原生播放器在播放/可以播放?]**' + nl;
story += ' 这暴露了脚本的一个核心Bug。它在自己接管失败并退场后,忘记了去“清场”,导致被它尝试取代的目标(原生播放器)得以“复活”并继续播放。' + nl + nl;
story += 'ախ **[最终诊断]**' + nl;
story += ' - 🟡 **接管失败:核心逻辑缺陷 (未中和原生播放器)。**';
}
else {
story += '3. ...但播放器未能成功解析视频。这可能是一个内部Bug。' + nl + nl;
story += 'ախ **[最终诊断]**' + nl + ' - 🟡 **接管异常:播放流程中断。**' + nl + ' - 脚本捕获到了视频,但未能启动播放器。请将日志反馈给开发者。';
}
return story;
}
const playerIframe = siteAnalysis.iframes.find(f => f.originInfo.includes('跨域') && /(play|video|player|v.|m3u8)/i.test(f.src));
if (playerIframe) {
const handshakeResult = this.iframeHandshakeStatus[playerIframe.src || ''] || {};
story += '🕵️ **[现场分析]**' + nl;
story += `我发现视频很可能被关在一个来自【${new URL(playerIframe.src).origin}】的“小黑屋”(跨域iFrame)里。这是一种常见的网站保护措施。` + nl;
story += '为了从“小黑屋”里取出视频,我派了一个“内应”(子脚本)进去并尝试与它建立秘密通信。' + nl + nl;
if (handshakeResult.status === 'success') {
story += '✅ **[通信成功]**' + nl;
story += `好消息!我们成功与“小黑屋”里的内应接上了头 (版本: ${handshakeResult.version})。通信渠道已打通!` + nl + nl;
story += '❓ **[那为什么还不播放?]**' + nl;
story += '万事俱备,只欠东风。虽然我们已经具备了“里应外合”播放视频的技术能力,但当前版本的脚本可能还没有针对这个特定网站的协同播放逻辑。' + nl + nl;
story += 'ախ **[最终诊断]**' + nl;
story += ' - **可接管潜力:极高!** 脚本已在主页面和播放器iFrame中同时存活并建立通信。' + nl + nl;
story += '💡 **[给您的建议]**' + nl + ' - **请务必将这份诊断报告完整地复制并反馈给脚本开发者!**';
} else {
story += '❌ **[通信失败]**' + nl;
story += '坏消息,我们的“内应”在“小黑屋”里失联了 (超时无响应)。这很可能是因为“小黑屋”有严格的安保系统(CSP安全策略),阻止了我们的“内应”运行。' + nl + nl;
story += 'ախ **[最终诊断]**' + nl;
story += ' - **接管失败:目标在跨域iFrame中,且内部脚本未能存活。**' + nl + nl;
story += '💡 **[给您的建议]**' + nl + ' - 这种情况说明网站防御森严,建议您在该网站上使用原生播放器观看。';
}
return story;
}
story += '🕵️ **[当前状态]**' + nl;
story += '脚本已启动,正在页面上积极地巡逻中... 目前尚未发现任何可供接管的视频信号。' + nl + nl;
story += '❓ **[这意味着什么?]**' + nl;
story += '请您先在页面上播放视频,脚本会在视频开始加载时自动进行拦截和接管。如果播放后这里依然没有反应,说明视频源隐藏得非常深。' + nl + nl;
story += 'ախ **[最终诊断]**' + nl;
story += ' - **待命中...**';
return story;
}
};
const C = { ROOT_ID: 'dmz-host-container', PLAYER_ID: 'dmz-custom-player', SETTINGS_PANEL_ID: 'dmz-settings-panel', SETTINGS_WRAPPER_CLASS: 'dmz-settings-wrapper', MAX_RETRY_COUNT: 3, MAX_M3U8_REDIRECT_DEPTH: 5, DOM_SCAN_DEBOUNCE_MS: 300, PLAYER_INITIAL_HEIGHT_PX: '36px', VIDEO_WRAPPER_CLASS: 'dmz-video-wrapper', CLOSE_BUTTON_CLASS: 'dmz-close-button', DRAG_HANDLE_CLASS: 'dmz-drag-handle', SCREW_EFFECT_CLASS: 'screw-effect', INDICATOR_CLASS: 'indicator', PLAYER_STYLES_ID: 'dmz-player-styles', SETTINGS_STYLES_ID: 'dmz-settings-panel-styles', SETTINGS_GRID_CLASS: 'settings-grid', SETTINGS_CARD_CLASS: 'settings-card', SETTINGS_CARD_FULL_WIDTH_CLASS: 'full-width', SETTINGS_TITLE_CLASS: 'settings-title', SETTINGS_CARD_INFO_CLASS: 'settings-card-info', OPTION_ITEM_CLASS: 'option-item', OPTION_ITEM_COL_CLASS: 'option-item-col', SETTINGS_FOOTER_CLASS: 'settings-footer', SETTINGS_BTN_CLASS: 'settings-btn', SETTINGS_BTN_CLOSE_CLASS: 'close', SETTINGS_BTN_SAVE_CLASS: 'save', SETTINGS_BTN_RESET_CLASS: 'reset', SETTINGS_BTN_ACTION_CLASS: 'action', SWITCH_CLASS: 'switch', SLIDER_CLASS: 'slider', REVEAL_ANIM_PHASE1_MS: 900, REVEAL_ANIM_PHASE2_MS: 1400, PLAYER_HOOKER_TARGETS: ['aliplayer', 'DPlayer', 'TCPlayer', 'xgplayer', 'Chimee', 'videojs', 'player'] };
const PLAYER_CSS = ` :host { all: initial; position: fixed !important; background: transparent; overflow: visible; z-index: 2147483647 !important; display: flex; flex-direction: column; gap: 0; padding: 0; box-sizing: border-box; pointer-events: auto; transition: width .9s cubic-bezier(.4,0,.2,1), height 1.4s cubic-bezier(.4,0,.2,1), transform 1.4s cubic-bezier(.4,0,.2,1), opacity .3s ease, gap .9s cubic-bezier(.4,0,.2,1); will-change: transform, width, height, opacity, gap; } .${C.VIDEO_WRAPPER_CLASS} { width: 100%; flex-grow: 1; display: flex; justify-content: center; align-items: center; background: rgba(28,28,30,.85); backdrop-filter: blur(18px) saturate(180%); -webkit-backdrop-filter: blur(18px) saturate(180%); border-radius: 20px; position: relative; overflow: hidden; box-shadow: 0 12px 32px rgba(0,0,0,.25), 0 2px 6px rgba(0,0,0,.1); transition: max-height 1.4s cubic-bezier(.4,0,.2,1), flex-grow 1.4s cubic-bezier(.4,0,.2,1), background-color .5s ease, backdrop-filter .5s ease, -webkit-backdrop-filter .5s ease; max-height: 0; flex-grow: 0; } #${C.PLAYER_ID} { width: 100%; height: 100%; object-fit: contain; background: #000; border-radius: 20px; opacity: 0; transition: opacity 1s ease .6s; } .${C.CLOSE_BUTTON_CLASS} { position: absolute; top: 12px; left: 12px; background: rgba(0,0,0,.4); color: #fff; width: 26px; height: 26px; border-radius: 50%; display: flex; justify-content: center; align-items: center; font-size: 20px; line-height: 1; font-weight: 700; cursor: pointer; z-index: 10; transition: background .2s ease, transform .2s ease; border: 1px solid rgba(255,255,255,.1); } .${C.CLOSE_BUTTON_CLASS}:hover { background: rgba(230,50,90,.9); transform: scale(1.1); } .${C.DRAG_HANDLE_CLASS} { width: calc(100% - 20px); margin: 0 auto; height: 18px; flex-shrink: 0; cursor: move; display: flex; justify-content: center; align-items: center; position: relative; background: linear-gradient(to bottom, #404040, #181818); border-radius: 9px; box-shadow: 0 1px 2px rgba(0,0,0,.7) inset; -webkit-tap-highlight-color: transparent; overflow: hidden; } .${C.DRAG_HANDLE_CLASS} .${C.SCREW_EFFECT_CLASS} { position: absolute; top: 0; height: 100%; width: calc(50% - 69px); background-image: repeating-linear-gradient(-55deg, rgba(0,0,0,.5), rgba(0,0,0,.5) 1.5px, transparent 1.5px, transparent 4px); } .${C.DRAG_HANDLE_CLASS} .${C.SCREW_EFFECT_CLASS}.left { left: 9px; } .${C.DRAG_HANDLE_CLASS} .${C.SCREW_EFFECT_CLASS}.right { right: 9px; } .${C.DRAG_HANDLE_CLASS} .${C.INDICATOR_CLASS} { width: 120px; height: 4px; background: linear-gradient(to right, #7a0000, #d43f3f, #7a0000); border-radius: 2px; box-shadow: 0 0 5px rgba(255,77,77,.7), 0 0 2px rgba(0,0,0,.6) inset; }`;
const SETTINGS_CSS = ` #${C.SETTINGS_PANEL_ID} { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(10,10,15,.98); backdrop-filter: blur(8px); z-index: 2147483647 !important; font-family: sans-serif; display: flex; flex-direction: column; overflow-y: auto; padding: 15px; box-sizing: border-box; } .${C.SETTINGS_WRAPPER_CLASS} { max-width: 1000px; width: 100%; margin: 0 auto; background: rgba(30,30,40,.9); border-radius: 12px; padding: 20px; border: 1px solid rgba(255,0,100,.4); } .${C.SETTINGS_WRAPPER_CLASS} h2 { text-align: center; margin: 0 0 20px; font-size: 24px; color: #fff; text-shadow: 0 0 8px rgba(255,0,100,.7); } .${C.SETTINGS_GRID_CLASS} { display: grid; grid-template-columns: 1fr; gap: 15px; margin-bottom: 20px; } .${C.SETTINGS_CARD_CLASS} { background: rgba(40,40,50,.8); padding: 15px; border-radius: 10px; border: 1px solid rgba(255,0,100,.2); } .${C.SETTINGS_CARD_CLASS}.${C.SETTINGS_CARD_FULL_WIDTH_CLASS} { grid-column: 1 / -1; } .${C.SETTINGS_TITLE_CLASS} { color: #ff9cff; margin-top: 0; padding-bottom: 10px; border-bottom: 1px solid rgba(255,0,100,.3); font-size: 18px; } .${C.SETTINGS_CARD_INFO_CLASS} { background: rgba(255,235,59,.1); border: 1px solid rgba(255,235,59,.5); padding: 15px; border-radius: 8px; color: #fff3e0; margin-top: 10px; } .${C.SETTINGS_CARD_INFO_CLASS} code { background: rgba(0,0,0,.3); padding: 2px 5px; border-radius: 4px; font-family: monospace; } .${C.OPTION_ITEM_CLASS}, .${C.OPTION_ITEM_COL_CLASS} { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; color: #e5e5ea; font-size: 16px; } .${C.OPTION_ITEM_COL_CLASS} { flex-direction: column; align-items: flex-start; } .${C.OPTION_ITEM_COL_CLASS} label { margin-bottom: 8px; } .${C.OPTION_ITEM_COL_CLASS} input, .${C.OPTION_ITEM_COL_CLASS} textarea { width: 100%; box-sizing: border-box; background: rgba(20,20,30,.8); border: 1px solid rgba(255,0,100,.5); border-radius: 8px; padding: 10px; color: #fff; font-size: 14px; } textarea { resize: vertical; } .${C.SETTINGS_FOOTER_CLASS} { display: flex; justify-content: space-around; padding-top: 15px; border-top: 1px solid rgba(255,0,100,.3); gap: 15px; } .${C.SETTINGS_BTN_CLASS} { flex: 1; padding: 12px 25px; border: none; border-radius: 8px; cursor: pointer; color: #fff; font-size: 16px; font-weight: 600; box-shadow: 0 4px 10px rgba(0,0,0,.3); } .${C.SETTINGS_BTN_CLASS}.${C.SETTINGS_BTN_CLOSE_CLASS} { background-color: #555; } .${C.SETTINGS_BTN_CLASS}.${C.SETTINGS_BTN_SAVE_CLASS} { background-color: #e53935; } .${C.SETTINGS_BTN_CLASS}.${C.SETTINGS_BTN_RESET_CLASS} { background-color: #f57c00; } .${C.SETTINGS_BTN_CLASS}.${C.SETTINGS_BTN_ACTION_CLASS} { background-color: #007aff; margin-top: 10px; width: 100%; flex: none; } .${C.SWITCH_CLASS} { position: relative; display: inline-block; width: 51px; height: 31px; } .${C.SWITCH_CLASS} input { opacity: 0; width: 0; height: 0; } .${C.SLIDER_CLASS} { position: absolute; cursor: pointer; inset: 0; background-color: #58585a; transition: .4s; border-radius: 31px; } .${C.SLIDER_CLASS}:before { position: absolute; content: ""; height: 27px; width: 27px; left: 2px; bottom: 2px; background-color: #fff; transition: .4s; border-radius: 50%; } input:checked + .${C.SLIDER_CLASS} { background-color: #34c759; } input:checked + .${C.SLIDER_CLASS}:before { transform: translateX(20px); }`;
const Utils = { debounce(func, wait) { let timeout; return function(...args) { const context = this; clearTimeout(timeout); timeout = setTimeout(() => func.apply(context, args), wait); }; }, createSwitchableModule(module) { return { ...module, enable() { if (!this.isActive && this.activate) { this.activate(); ActionLogger.log(`模块 ${module.name || ''} 已启用。`, 'MODULE'); } }, disable() { if (this.isActive && this.deactivate) { this.deactivate(); ActionLogger.log(`模块 ${module.name || ''} 已禁用。`, 'MODULE'); } }, }; }, wildcardToRegex(pattern) { const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*'); return new RegExp(`^${escaped}$`); } };
const StyleManager = { isSettingsInjected: false, injectPlayerStyles(shadowRoot) { if (shadowRoot.querySelector(`#${C.PLAYER_STYLES_ID}`)) return; const styleSheet = document.createElement('style'); styleSheet.id = C.PLAYER_STYLES_ID; styleSheet.textContent = PLAYER_CSS; shadowRoot.appendChild(styleSheet); }, injectSettingsStyles() { if (this.isSettingsInjected || !IS_TOP_FRAME || document.getElementById(C.SETTINGS_STYLES_ID)) return; const styleSheet = document.createElement('style'); styleSheet.id = C.SETTINGS_STYLES_ID; styleSheet.textContent = SETTINGS_CSS; document.head.appendChild(styleSheet); this.isSettingsInjected = true; }, };
const SettingsManager = {
defaults: {
blacklist: ['github.com', 'stackoverflow.com', 'developer.mozilla.org'],
keywords: ['.js', '.png', '.gif', '/headers/', '#EXT-X-DISCONTINUITY', 'preview.m3u8'],
isSmartSlicingEnabled: true,
autoPlay: true,
enableNetworkInterceptor: true,
enablePlayerTakeover: true,
defaultPlaybackRate: 1.0,
crossFrameSupport: true,
maxRetryCount: C.MAX_RETRY_COUNT,
floatingPlayerPos: { left: 'calc(50vw - 150px)', top: '80px', width: '300px' },
},
config: {},
async load() {
this.config = await GM_getValue('dmz_v2_settings', {});
if (this.config.floatingPlayerPos && this.config.floatingPlayerPos.height) {
delete this.config.floatingPlayerPos.height;
}
this.config = { ...this.defaults, ...this.config };
ActionLogger.log("配置加载完成", "CONFIG");
},
async save(newConfig, applyLive = true) {
const oldConfig = { ...this.config };
this.config = { ...this.config, ...newConfig };
await GM_setValue('dmz_v2_settings', this.config);
ActionLogger.log("配置已保存", "CONFIG");
if (applyLive) this.applyLiveSettings(oldConfig, this.config);
},
applyLiveSettings(oldConfig, newConfig) {
MANAGED_MODULES.forEach(({ module, configKey }) => {
if (oldConfig[configKey] !== newConfig[configKey]) {
newConfig[configKey] ? module.enable() : module.disable();
}
});
FrameCommunicator.showNotification('设置已保存并应用。部分更改可能需要刷新页面才能生效。');
},
async reset() {
const oldConfig = { ...this.config };
this.config = { ...this.defaults };
await GM_setValue('dmz_v2_settings', this.config);
this.applyLiveSettings(oldConfig, this.config);
ActionLogger.log("配置已重置为默认值", "CONFIG");
FrameCommunicator.showNotification('已恢复为默认设置。');
return this.config;
}
};
const FrameCommunicator = {
pendingVideoUrl: null,
init() { window.addEventListener('message', this.handleMessage.bind(this)); },
removeListeners() { window.removeEventListener('message', this.handleMessage.bind(this)); },
handleMessage(event) {
if (event.data?.type !== MESSAGE_TYPES.M3U8_COMMAND) return;
if (!SettingsManager.config.crossFrameSupport) return;
ActionLogger.log(`从 ${event.origin} 收到消息: ${event.data.action}`, 'COMM');
if (IS_TOP_FRAME) {
DiagnosticsTool.captureTakeoverSource(`Cross-Frame:${event.origin}`);
if (PlayerManager.isPlayerActiveOrInitializing) { ActionLogger.log('播放器正忙 (可能在切换),缓存播放请求...', 'COMM_CACHE'); this.pendingVideoUrl = event.data.content; return; }
this.pendingVideoUrl = null;
switch (event.data.action) {
case 'PLAY_M3U8': PlayerManager.play(event.data.content); break;
case 'PLAY_NORMAL_VIDEO': PlayerManager.playNormalVideo(event.data.content); break;
}
}
},
postToTopFrame(message) { if (!IS_TOP_FRAME) { try { let targetOrigin = '*'; try { targetOrigin = window.top.location.origin; } catch (e) { /* cross-origin frame */ } ActionLogger.log(`向顶层窗口发送消息: ${message.action}`, 'COMM'); window.top.postMessage(message, targetOrigin); } catch (e) { ActionLogger.log(`跨 frame 通信失败: ${e.message}`, 'ERROR'); } } },
showNotification(text, isError = false) { GM_notification({ title: SCRIPT_NAME, text, silent: !isError, timeout: isError ? 5000 : 3000 }); }
};
const FullscreenHandler = class { constructor(video) { this.videoElement = video; this.isVerticalVideo = false; this.isProcessing = false; this.activeLock = null; this.handleOrientationChange = this.handleOrientationChange.bind(this); this.handleFullscreenChange = this.handleFullscreenChange.bind(this); this.updateVideoOrientation = this.updateVideoOrientation.bind(this); this.updateVideoOrientation(); video.addEventListener('loadedmetadata', this.updateVideoOrientation, { once: true }); const orientationApi = screen.orientation || screen; orientationApi.addEventListener('change', this.handleOrientationChange); document.addEventListener('fullscreenchange', this.handleFullscreenChange); document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange); } updateVideoOrientation() { if (this.videoElement?.videoWidth > 0) { this.isVerticalVideo = this.videoElement.videoHeight > this.videoElement.videoWidth; } } destroy() { if (!this.videoElement) return; this.videoElement.removeEventListener('loadedmetadata', this.updateVideoOrientation); const orientationApi = screen.orientation || screen; orientationApi.removeEventListener('change', this.handleOrientationChange); document.removeEventListener('fullscreenchange', this.handleFullscreenChange); document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange); this.unlockOrientation(); this.videoElement = null; } unlockOrientation() { if (screen.orientation?.unlock) { screen.orientation.unlock(); } this.activeLock = null; } async handleOrientationChange() { if (this.isProcessing) return; this.isProcessing = true; await new Promise(resolve => setTimeout(resolve, 250)); if (!this.videoElement) { this.isProcessing = false; return; } const isLandscape = window.innerWidth > window.innerHeight; const isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement); try { if (isLandscape && !isFullscreen) { await this.enterFullscreen(); } else if (!isLandscape && isFullscreen && this.activeLock) { await this.exitFullscreen(); } } catch (err) { /* ignore */ } finally { setTimeout(() => { this.isProcessing = false; }, 100); } } async enterFullscreen() { const targetOrientation = this.isVerticalVideo ? 'portrait-primary' : 'landscape'; this.activeLock = targetOrientation.startsWith('portrait') ? 'portrait' : 'landscape'; if (screen.orientation?.lock) { try { await screen.orientation.lock(targetOrientation); } catch (lockError) { /* ignore */ } } const requestFS = this.videoElement.requestFullscreen || this.videoElement.webkitRequestFullscreen; if(requestFS) await requestFS.call(this.videoElement); } async exitFullscreen() { if (document.fullscreenElement) { const exitFS = document.exitFullscreen || document.webkitExitFullscreen; if(exitFS) await exitFS.call(document); } } handleFullscreenChange() { const isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement); if (!isFullscreen) this.unlockOrientation(); } };
const SwipeToSeekHandler = class { constructor(video) { this.videoElement = video; this.isEnabled = false; this.isSwiping = false; this.startX = 0; this.startCurrentTime = 0; this.seekFactor = 0.35; this.handleTouchStart = this.handleTouchStart.bind(this); this.handleTouchMove = this.handleTouchMove.bind(this); this.handleTouchEnd = this.handleTouchEnd.bind(this); this.videoElement.addEventListener('touchstart', this.handleTouchStart, { passive: false }); this.videoElement.addEventListener('touchmove', this.handleTouchMove, { passive: false }); this.videoElement.addEventListener('touchend', this.handleTouchEnd); this.videoElement.addEventListener('touchcancel', this.handleTouchEnd); } destroy() { if (!this.videoElement) return; this.videoElement.removeEventListener('touchstart', this.handleTouchStart); this.videoElement.removeEventListener('touchmove', this.handleTouchMove); this.videoElement.removeEventListener('touchend', this.handleTouchEnd); this.videoElement.removeEventListener('touchcancel', this.handleTouchEnd); this.videoElement = null; } enable() { this.isEnabled = true; } disable() { this.isEnabled = false; } handleTouchStart(e) { if (!this.isEnabled || e.touches.length !== 1) return; this.isSwiping = false; this.startX = e.touches[0].clientX; this.startCurrentTime = this.videoElement.currentTime; } handleTouchMove(e) { if (!this.isEnabled || !this.startX) return; const deltaX = e.touches[0].clientX - this.startX; if (!this.isSwiping && Math.abs(deltaX) < 10) return; e.preventDefault(); this.isSwiping = true; const screenWidth = window.innerWidth; const seekSeconds = (deltaX / screenWidth) * this.videoElement.duration * this.seekFactor; let newTime = this.startCurrentTime + seekSeconds; newTime = Math.max(0, Math.min(newTime, this.videoElement.duration)); this.videoElement.currentTime = newTime; } handleTouchEnd() { if (this.isEnabled) { this.startX = 0; this.isSwiping = false; } } };
const Draggable = class { constructor(element, handles) { this.element = element; this.handles = handles; this.isDragging = false; this.isMoveScheduled = false; this.dragStartPos = { x: 0, y: 0 }; this.elementStartPos = { x: 0, y: 0 }; this.handleDragStart = this.handleDragStart.bind(this); this.handleDragMove = this.handleDragMove.bind(this); this.handleDragEnd = this.handleDragEnd.bind(this); this.addListeners(); } addListeners() { this.handles.forEach(handle => { handle.addEventListener('mousedown', this.handleDragStart); handle.addEventListener('touchstart', this.handleDragStart, { passive: false }); }); } destroy() { if (!this.element) return; this.handles.forEach(handle => { handle.removeEventListener('mousedown', this.handleDragStart); handle.removeEventListener('touchstart', this.handleDragStart); }); document.removeEventListener('mousemove', this.handleDragMove); document.removeEventListener('mouseup', this.handleDragEnd); document.removeEventListener('touchmove', this.handleDragMove); document.removeEventListener('touchend', this.handleDragEnd); this.element = null; } handleDragStart(e) { if (!this.element) return; e.preventDefault(); this.isDragging = true; document.body.style.touchAction = 'none'; this.element.style.transition = 'none'; const rect = this.element.getBoundingClientRect(); this.element.style.left = `${rect.left}px`; this.element.style.top = `${rect.top}px`; this.element.style.transform = ''; this.elementStartPos = { x: rect.left, y: rect.top }; const currentPos = e.touches ? e.touches[0] : e; this.dragStartPos = { x: currentPos.clientX, y: currentPos.clientY }; document.addEventListener('mousemove', this.handleDragMove); document.addEventListener('mouseup', this.handleDragEnd); document.addEventListener('touchmove', this.handleDragMove, { passive: false }); document.addEventListener('touchend', this.handleDragEnd); } handleDragMove(e) { if (!this.isDragging || !this.element || this.isMoveScheduled) return; e.preventDefault(); const currentPos = e.touches ? e.touches[0] : e; const clientX = currentPos.clientX; const clientY = currentPos.clientY; this.isMoveScheduled = true; requestAnimationFrame(() => { if (!this.isDragging) { this.isMoveScheduled = false; return; } const deltaX = clientX - this.dragStartPos.x; const deltaY = clientY - this.dragStartPos.y; let newX = this.elementStartPos.x + deltaX; let newY = this.elementStartPos.y + deltaY; const vw = window.innerWidth, vh = window.innerHeight; newX = Math.max(0, Math.min(newX, vw - this.element.offsetWidth)); newY = Math.max(0, Math.min(newY, vh - this.element.offsetHeight)); this.element.style.left = `${newX}px`; this.element.style.top = `${newY}px`; this.isMoveScheduled = false; }); } handleDragEnd() { if (!this.isDragging || !this.element) return; this.isDragging = false; document.body.style.touchAction = ''; document.removeEventListener('mousemove', this.handleDragMove); document.removeEventListener('mouseup', this.handleDragEnd); document.removeEventListener('touchmove', this.handleDragMove); document.removeEventListener('touchend', this.handleDragEnd); this.element.style.transition = ''; } };
const PlayerManager = {
hostElement: null, shadowRoot: null, hlsInstance: null, retryCount: 0, retryTimer: null, currentBlobUrl: null,
draggableInstance: null, swipeSeekHandler: null, fullscreenHandler: null,
isPlayerActiveOrInitializing: false,
isInternalRequest: false,
_prepareForPlayback() {
if (this.isPlayerActiveOrInitializing) {
ActionLogger.log("播放请求被忽略:播放器已存在或正在初始化", "WARN");
return false;
}
this.isPlayerActiveOrInitializing = true;
CoreLogic.findAllVideosAndAudioInPage().forEach(m => CoreLogic.neutralizeOriginalPlayer(m));
return true;
},
createBasePlayerContainer() {
ActionLogger.log("创建播放器基础容器...", "PLAYER");
this.cleanup(true);
this.hostElement = document.createElement('div');
this.hostElement.id = C.ROOT_ID;
Object.assign(this.hostElement.style, { left: '50%', top: '0px', width: '280px', height: C.PLAYER_INITIAL_HEIGHT_PX, gap: '0px', opacity: '0', transform: 'translateX(-50%) translateY(0px)' });
document.body.appendChild(this.hostElement);
this.shadowRoot = this.hostElement.attachShadow({ mode: 'open' });
StyleManager.injectPlayerStyles(this.shadowRoot);
requestAnimationFrame(() => { if (this.hostElement) this.hostElement.style.opacity = '1'; });
const container = document.createElement('div');
const headerBar = this._createDragHandleBar();
const videoWrapper = document.createElement('div');
videoWrapper.className = C.VIDEO_WRAPPER_CLASS;
videoWrapper.style.backgroundColor = '#000';
videoWrapper.style.backdropFilter = 'none';
videoWrapper.style.webkitBackdropFilter = 'none';
const closeBtn = document.createElement('div');
closeBtn.innerHTML = '×';
closeBtn.className = C.CLOSE_BUTTON_CLASS;
closeBtn.addEventListener('click', (e) => { e.stopPropagation(); this.cleanup(); });
const video = document.createElement('video');
video.id = C.PLAYER_ID;
video.controls = true;
video.autoplay = SettingsManager.config.autoPlay;
video.playbackRate = SettingsManager.config.defaultPlaybackRate;
video.setAttribute('playsinline', '');
videoWrapper.append(closeBtn, video);
const footerBar = this._createDragHandleBar();
container.append(headerBar, videoWrapper, footerBar);
this.shadowRoot.appendChild(container);
this.draggableInstance = new Draggable(this.hostElement, [headerBar, footerBar]);
this.swipeSeekHandler = new SwipeToSeekHandler(video);
this.fullscreenHandler = new FullscreenHandler(video);
const toggleSwipeOnFullscreen = () => { if (document.fullscreenElement || document.webkitFullscreenElement) { this.swipeSeekHandler.enable(); } else { this.swipeSeekHandler.disable(); } };
video.addEventListener('fullscreenchange', toggleSwipeOnFullscreen);
video.addEventListener('webkitfullscreenchange', toggleSwipeOnFullscreen);
ActionLogger.log("播放器基础容器创建完成。", "PLAYER");
return { video, videoWrapper };
},
_createDragHandleBar() { const handle = document.createElement('div'); handle.className = C.DRAG_HANDLE_CLASS; handle.innerHTML = ``; return handle; },
revealPlayer(video, videoWrapper) {
ActionLogger.log(`播放器成功加载元数据,开始执行展开动画!`, "PLAYER_REVEAL");
if (!this.hostElement || !video || !video.videoWidth || !video.videoHeight) { ActionLogger.log("播放器展开失败:必要元素或视频尺寸不存在", "WARN"); return; }
const isVertical = video.videoHeight > video.videoWidth;
ActionLogger.log(`展开播放器,视频方向: ${isVertical ? '竖屏' : '横屏'}`, "PLAYER");
const finalWidth = isVertical ? '60vw' : '100vw';
const finalTranslateY = isVertical ? '192px' : '60px';
requestAnimationFrame(() => {
if (!this.hostElement) return;
this.hostElement.style.width = finalWidth;
this.hostElement.style.transform = `translateX(-50%) translateY(${finalTranslateY})`;
});
setTimeout(() => {
if (!this.hostElement) return;
this.hostElement.style.gap = '3px';
videoWrapper.style.maxHeight = '80vh';
videoWrapper.style.flexGrow = '1';
video.style.opacity = '1';
if (SettingsManager.config.autoPlay) { video.play().catch(e => ActionLogger.log(`自动播放失败: ${e.message}`, "WARN")); }
}, C.REVEAL_ANIM_PHASE1_MS);
setTimeout(() => {
if (!videoWrapper) return;
videoWrapper.style.backgroundColor = '';
videoWrapper.style.backdropFilter = '';
videoWrapper.style.webkitBackdropFilter = '';
}, C.REVEAL_ANIM_PHASE1_MS + C.REVEAL_ANIM_PHASE2_MS);
},
play(m3u8Content) {
ActionLogger.log("收到 M3U8 播放请求", "PLAYER");
if (!this._prepareForPlayback()) return;
if (!IS_TOP_FRAME && SettingsManager.config.crossFrameSupport) {
CoreLogic.sendPlayCommand('m3u8', m3u8Content);
this.isPlayerActiveOrInitializing = false;
return;
}
const { video, videoWrapper } = this.createBasePlayerContainer();
const blob = new Blob([m3u8Content], { type: 'application/vnd.apple.mpegurl' }); this.currentBlobUrl = URL.createObjectURL(blob);
const onReady = () => this.revealPlayer(video, videoWrapper);
try {
this.isInternalRequest = true; ActionLogger.log("设置内部请求豁免标志 (isInternalRequest = true)", "PLAYER");
if (Hls.isSupported()) {
ActionLogger.log("使用 Hls.js 播放", "PLAYER");
const hlsConfig = { maxBufferSize: 120 * 1024 * 1024, maxMaxBufferLength: 90, maxRetryCount: SettingsManager.config.maxRetryCount, debug: false, xhrSetup: function(xhr, url) { xhr.setRequestHeader('Referer', window.location.href); }, abrEwmaDefaultEstimate: 1000000, maxBufferHole: 0.5, };
const hls = new Hls(hlsConfig); this.hlsInstance = hls;
hls.loadSource(this.currentBlobUrl); hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => { ActionLogger.log("Hls.js: MANIFEST_PARSED 事件触发", "HLS: MANIFEST_PARSED"); this.retryCount = 0; video.addEventListener('loadedmetadata', onReady, { once: true }); });
hls.on(Hls.Events.ERROR, (event, data) => {
let errorDetails = `type=${data.type}, details=${data.details}, fatal=${data.fatal}`;
if (data.response) { errorDetails += `. Response: [${data.response.code}] ${data.response.text || ''}`; }
ActionLogger.log(`Hls.js 错误: ${errorDetails}`, 'ERROR');
if (data.fatal) { if (data.type === Hls.ErrorTypes.NETWORK_ERROR) { this.handleNetworkError(hls); } else { hls.destroy(); this.cleanup(); } }
});
} else if (video.canPlayType('application/vnd.apple.mpegurl')) { ActionLogger.log("使用原生 HLS 播放", "PLAYER"); video.src = this.currentBlobUrl; video.addEventListener('loadedmetadata', onReady, { once: true });
} else { FrameCommunicator.showNotification('当前浏览器不支持HLS播放', true); this.cleanup(); ActionLogger.log('浏览器不支持 HLS 播放', 'ERROR'); }
} catch (e) { FrameCommunicator.showNotification(`播放器初始化失败: ${e.message}`, true); this.cleanup(); ActionLogger.log(`播放器初始化异常: ${e.message}`, 'ERROR'); }
finally { setTimeout(() => { this.isInternalRequest = false; ActionLogger.log("取消内部请求豁免标志 (isInternalRequest = false)", "PLAYER"); }, 500); }
},
handleNetworkError(hls) { this.retryCount++; if (this.retryCount <= SettingsManager.config.maxRetryCount) { const delay = Math.pow(2, this.retryCount) * 1000; ActionLogger.log(`HLS 网络错误,将在 ${delay}ms 后进行第 ${this.retryCount} 次重试`, "WARN"); if (this.retryTimer) clearTimeout(this.retryTimer); this.retryTimer = setTimeout(() => hls.startLoad(), delay); } else { ActionLogger.log(`HLS 网络错误:超过最大重试次数`, "ERROR"); hls.destroy(); this.cleanup(); } },
playNormalVideo(videoUrl) {
ActionLogger.log(`收到常规视频播放请求: ${videoUrl}`, "PLAYER");
if (!this._prepareForPlayback()) return;
if (!IS_TOP_FRAME && SettingsManager.config.crossFrameSupport) {
CoreLogic.sendPlayCommand('normal', videoUrl);
this.isPlayerActiveOrInitializing = false;
return;
}
const { video, videoWrapper } = this.createBasePlayerContainer();
const onReady = () => { video.removeEventListener('error', onError); this.revealPlayer(video, videoWrapper); };
const onError = () => { video.removeEventListener('loadedmetadata', onReady); ActionLogger.log(`常规视频加载失败: ${videoUrl}`, 'ERROR'); this.cleanup(); };
video.addEventListener('loadedmetadata', onReady, { once: true });
video.addEventListener('error', onError, { once: true });
video.src = videoUrl;
},
cleanup(isInternalCall = false) {
ActionLogger.log(`开始清理播放器... (内部调用: ${isInternalCall})`, "PLAYER");
if (this.retryTimer) clearTimeout(this.retryTimer);
if (this.hlsInstance) { this.hlsInstance.destroy(); this.hlsInstance = null; ActionLogger.log("HLS 实例已销毁", "HLS"); }
if (this.currentBlobUrl) { URL.revokeObjectURL(this.currentBlobUrl); this.currentBlobUrl = null; }
if (this.hostElement) { this.draggableInstance?.destroy(); this.swipeSeekHandler?.destroy(); this.fullscreenHandler?.destroy(); this.hostElement.remove(); this.hostElement = null; this.shadowRoot = null; this.draggableInstance = null; this.swipeSeekHandler = null; this.fullscreenHandler = null; }
if (!isInternalCall) {
this.isPlayerActiveOrInitializing = false;
ActionLogger.log("播放器状态锁已释放", "PLAYER");
if (FrameCommunicator.pendingVideoUrl) {
ActionLogger.log('处理缓存的播放请求...', 'PLAYER_CACHE');
const urlToPlay = FrameCommunicator.pendingVideoUrl;
FrameCommunicator.pendingVideoUrl = null;
setTimeout(() => CoreLogic.handleVideoSrc(urlToPlay, 'Cached Request'), 50);
}
}
this.isInternalRequest = false;
}
};
const CoreLogic = {
playerIframe: null,
playerIframeObserver: null,
async _fetchM3u8Text(url) { ActionLogger.log(`获取 M3U8 文本: ${url}`, "CORE"); const response = await fetch(url, { cache: 'no-store' }); if (!response.ok) throw new Error(`获取 M3U8 失败: ${response.status} ${response.statusText}`); return await response.text(); },
_sliceAdSegments(lines) { if (!SettingsManager.config.isSmartSlicingEnabled) { ActionLogger.log("智能切片已禁用,跳过广告切除", "CORE"); return lines; } const adMarkers = ['#EXT-X-DISCONTINUITY', ...SettingsManager.config.keywords]; const segmentIndicesToRemove = new Set(); let inAdSegment = false; for (let j = 0; j < lines.length; j++) { const line = lines[j].trim(); if (adMarkers.some(marker => line.includes(marker))) { inAdSegment = true; segmentIndicesToRemove.add(j); if (j > 0 && lines[j - 1].trim().startsWith('#EXTINF')) segmentIndicesToRemove.add(j - 1); } else if (line.endsWith('.ts') && inAdSegment) { segmentIndicesToRemove.add(j); } else if (line.startsWith('#EXTINF')) { inAdSegment = false; } } if(segmentIndicesToRemove.size > 0) ActionLogger.log(`智能切片:发现并标记了 ${segmentIndicesToRemove.size} 行(含广告相关)待移除`, "CORE"); return segmentIndicesToRemove.size > 0 ? lines.filter((_, index) => !segmentIndicesToRemove.has(index)) : lines; },
_resolveRelativeUrls(lines, baseUrl) {
ActionLogger.log(`解析M3U8中的相对URL,基础URL: ${baseUrl}`, "CORE");
const resolveUrl = (relative, base) => new URL(relative, base).href;
return lines.map(line => {
const t = line.trim();
if (!t) return line;
try {
if ((/\.(ts|mp4|m3u8?)(\?.*)?$/).test(t) && !/^(https?:)?\/\//.test(t)) { return resolveUrl(t, baseUrl); }
if (t.startsWith('#EXT-X-KEY') || t.startsWith('#EXT-X-MEDIA')) {
const match = t.match(/URI="([^"]+)"/);
if (match && match[1] && !/^(https?:)?\/\//.test(match[1])) {
ActionLogger.log(`在 ${t.split(':')[0]} 中发现并解析相对URI: ${match[1]}`, 'CORE_URL_RESOLVE');
return t.replace(match[1], resolveUrl(match[1], baseUrl));
}
}
} catch (e) { ActionLogger.log(`URL 解析失败: "${line}",基础URL: "${baseUrl}"`, "WARN"); }
return line;
});
},
async processM3U8(initialText, initialUrl) { ActionLogger.log(`开始处理M3U8: ${initialUrl}`, "CORE"); let currentText = initialText, currentUrl = initialUrl; try { for (let i = 0; i < C.MAX_M3U8_REDIRECT_DEPTH; i++) { const lines = currentText.split('\n').map(l => l.trim()).filter(Boolean); const isMasterPlaylist = lines.some(l => l.startsWith('#EXT-X-STREAM-INF')); const hasMediaSegments = lines.some(l => l.startsWith('#EXTINF')); let nextUrlLine = null; if (isMasterPlaylist && !hasMediaSegments) { nextUrlLine = lines.find(l => !l.startsWith('#') && (l.endsWith('.m3u8') || l.endsWith('.m3u'))); } else if (lines.length < 5 && !hasMediaSegments) { nextUrlLine = lines.find(l => !l.startsWith('#') && (l.endsWith('.m3u8') || l.endsWith('.m3u'))); } if (nextUrlLine) { currentUrl = new URL(nextUrlLine, currentUrl).href; ActionLogger.log(`M3U8重定向(层级 ${i+1}): 发现主播放列表,跳转到 ${currentUrl}`, "CORE"); currentText = await this._fetchM3u8Text(currentUrl); continue; } break; } let finalLines = this._sliceAdSegments(currentText.split('\n')); finalLines = this._resolveRelativeUrls(finalLines, currentUrl); let result = finalLines.join('\n'); if (!result.trim().startsWith('#EXTM3U')) result = '#EXTM3U\n' + result; ActionLogger.log(`M3U8处理完成: ${currentUrl}`, "CORE"); return result; } catch (e) { ActionLogger.log(`处理M3U8时出错: ${initialUrl}, 错误: ${e.message}`, "ERROR"); return initialText; } },
async handleFoundM3U8(url, sourceName, m3u8Text = null) {
DiagnosticsTool.captureTakeoverSource(sourceName);
ActionLogger.log(`发现 M3U8 来源 '${sourceName}': ${url}`, "CORE");
try {
const textToProcess = m3u8Text || await this._fetchM3u8Text(url);
const processedContent = await this.processM3U8(textToProcess, url);
if (processedContent) { this.sendPlayCommand('m3u8', processedContent); }
else { ActionLogger.log("M3U8处理后内容为空", "WARN"); }
} catch (error) { FrameCommunicator.showNotification(`处理M3U8失败: ${error.message}`, true); ActionLogger.log(`处理M3U8失败: ${error.message}`, "ERROR"); }
},
handleVideoSrc(src, sourceName) {
if (!src || (!src.startsWith('http') && !src.startsWith('blob:'))) return false;
ActionLogger.log(`处理视频源 '${sourceName}': ${src}`, "CORE");
if (src.toLowerCase().includes('.m3u8') || src.toLowerCase().includes('.m3u')) {
this.handleFoundM3U8(src, sourceName);
} else {
DiagnosticsTool.captureTakeoverSource(sourceName);
this.sendPlayCommand('normal', src);
}
return true;
},
sendPlayCommand(type, content) {
const actionMap = { m3u8: 'PLAY_M3U8', normal: 'PLAY_NORMAL_VIDEO' };
const message = { type: MESSAGE_TYPES.M3U8_COMMAND, action: actionMap[type], content };
if (!IS_TOP_FRAME) {
ActionLogger.log(`iFrame: 中和本地所有媒体元素,然后将播放任务上报给顶层`, 'CORE_IFRAME');
this.findAllVideosAndAudioInPage().forEach(m => this.neutralizeOriginalPlayer(m));
FrameCommunicator.postToTopFrame(message);
} else {
type === 'm3u8' ? PlayerManager.play(content) : PlayerManager.playNormalVideo(content);
}
},
findAllVideosAndAudioInPage() {
const media = [];
const visitedRoots = new Set();
function findMedia(node) {
if (!node || visitedRoots.has(node)) return;
visitedRoots.add(node);
try {
node.querySelectorAll('video, audio').forEach(m => media.push(m));
node.querySelectorAll('*').forEach(el => { if (el.shadowRoot) findMedia(el.shadowRoot); });
} catch(e) { /* ignore errors */ }
}
if (document.body) findMedia(document.body);
return media;
},
neutralizeOriginalPlayer(mediaElement) {
if (!mediaElement || mediaElement.id === C.PLAYER_ID || mediaElement.dataset.dmzNeutralized === 'true') return;
const elementType = mediaElement.tagName;
ActionLogger.log(`开始强力中和原生 ${elementType}: ID=${mediaElement.id}, Class=${mediaElement.className}`, 'CORE_NEUTRALIZE');
mediaElement.dataset.dmzNeutralized = 'true';
mediaElement.pause();
mediaElement.muted = true;
if (mediaElement.volume) mediaElement.volume = 0;
mediaElement.removeAttribute('src');
mediaElement.querySelectorAll('source').forEach(source => source.remove());
if (mediaElement.networkState !== mediaElement.NETWORK_EMPTY) { mediaElement.load(); }
mediaElement.style.setProperty('visibility', 'hidden', 'important');
mediaElement.style.setProperty('display', 'none', 'important');
try {
Object.defineProperties(mediaElement, {
'play': { value: () => Promise.resolve(), configurable: true },
'pause': { value: () => {}, configurable: true },
'load': { value: () => {}, configurable: true }
});
ActionLogger.log(`${elementType} 的 play/pause/load 方法已被劫持。`, 'CORE_NEUTRALIZE');
} catch(e) { ActionLogger.log(`劫持 ${elementType} 方法失败: ${e.message}`, 'WARN'); }
const observer = new MutationObserver((mutations) => {
let changed = false;
mutations.forEach(mutation => {
if (mutation.type === 'attributes' && (mutation.attributeName === 'src' || mutation.attributeName === 'muted' || mutation.attributeName === 'controls')) {
if (mediaElement.hasAttribute('src')) { mediaElement.removeAttribute('src'); changed = true; }
if (mediaElement.muted === false) { mediaElement.muted = true; changed = true; }
}
});
if(changed) ActionLogger.log(`${elementType} 被外部脚本修改,已自动恢复中和状态。`, 'CORE_NEUTRALIZE');
});
observer.observe(mediaElement, { attributes: true });
}
};
const SPANavigator = {
lastUrl: window.location.href,
init() {
const originalPushState = history.pushState, originalReplaceState = history.replaceState;
history.pushState = (...args) => { originalPushState.apply(history, args); this.onUrlChange(); };
history.replaceState = (...args) => { originalReplaceState.apply(history, args); this.onUrlChange(); };
window.addEventListener('popstate', () => this.onUrlChange());
},
onUrlChange() {
requestAnimationFrame(() => {
if (window.location.href !== this.lastUrl) {
ActionLogger.log(`URL 变化 (SPA): ${this.lastUrl} -> ${window.location.href}`, 'NAV');
this.lastUrl = window.location.href;
PlayerManager.cleanup();
DOMScanner.stop();
requestAnimationFrame(() => { setTimeout(() => { DOMScanner.isStopped = false; DOMScanner.init(); }, 0); });
}
});
}
};
const DOMScanner = {
observer: null, isStopped: false,
init() {
if (this.observer || this.isStopped) return;
ActionLogger.log("DOM 扫描器启动", 'SCAN');
this.scanPage();
this.observer = new MutationObserver(() => this.scanPage());
const observeTarget = document.body || document.documentElement;
this.observer.observe(observeTarget, { childList: true, subtree: true });
},
stop() {
if (this.observer) { this.observer.disconnect(); this.observer = null; ActionLogger.log("DOM 扫描器停止", 'SCAN'); }
this.isStopped = true;
},
scanPage: Utils.debounce(function() {
if (this.isStopped) return;
if (PlayerManager.isPlayerActiveOrInitializing) return;
for (const media of CoreLogic.findAllVideosAndAudioInPage()) { if (this.processMediaElement(media)) { this.stop(); return; } }
}, C.DOM_SCAN_DEBOUNCE_MS),
async processMediaElement(media) {
if (media.dataset.handledByDmz || media.id === C.PLAYER_ID) return false;
media.dataset.handledByDmz = 'true';
if (media.tagName === 'VIDEO') {
const targetSrc = media.src || (media.querySelector('source[src]')?.src);
if (targetSrc && targetSrc.startsWith('http')) {
ActionLogger.log(`DOM扫描:发现标准HTTP源 ${targetSrc}`, 'SCAN');
if (CoreLogic.handleVideoSrc(targetSrc, 'DOM Scan (HTTP)')) { return true; }
}
if (targetSrc && targetSrc.startsWith('blob:')) {
ActionLogger.log(`DOM扫描:发现Blob源,尝试终极手段`, 'SCAN');
try {
const response = await fetch(targetSrc);
const m3u8Text = await response.text();
if (m3u8Text && m3u8Text.includes('#EXTM3U')) {
ActionLogger.log(`DOM扫描:从Blob源成功提取M3U8内容`, 'SCAN');
await CoreLogic.handleFoundM3U8(targetSrc, 'DOM Scan (Blob)', m3u8Text);
return true;
} else { ActionLogger.log(`DOM扫描:Blob源内容不是有效的M3U8`, 'SCAN_WARN'); }
} catch (error) { ActionLogger.log(`DOM扫描:读取Blob源失败: ${error.message}`, 'ERROR'); }
}
}
return false;
}
};
const NetworkInterceptor = Utils.createSwitchableModule({
name: 'NetworkInterceptor',
originalXhrOpen: null,
originalFetch: null,
isActive: false,
activate() {
if (this.isActive) return;
this.isActive = true;
const onM3U8Found = (url, type, text) => {
if (SettingsManager.config.keywords.some(keyword => url.includes(keyword)) || /\.(js|css|png|jpg|gif)$/.test(url)) return;
if (url.includes('.m3u8') || url.includes('.m3u') || (text && text.trim().startsWith('#EXTM3U'))) {
CoreLogic.handleFoundM3U8(url, `NET_HOOK_${type}`, text);
}
};
try {
this.originalXhrOpen = XMLHttpRequest.prototype.open;
this.originalFetch = window.fetch;
XMLHttpRequest.prototype.open = function(method, url, ...rest) {
if (typeof url === 'string') {
if (PlayerManager.isInternalRequest && url.startsWith('blob:')) {
/* 忽略由我们自己播放器发起的内部 Blob 请求 */
} else {
this.addEventListener('load', () => {
if (this.status >= 200 && this.status < 400 && this.responseText) {
onM3U8Found(url, 'XHR', this.responseText);
}
}, { once: true });
}
}
return NetworkInterceptor.originalXhrOpen.apply(this, [method, url, ...rest]);
};
window.fetch = async function(...args) {
const request = new Request(args[0], args[1]);
if (PlayerManager.isInternalRequest && request.url.startsWith('blob:')) {
return NetworkInterceptor.originalFetch.apply(this, args);
}
const response = await NetworkInterceptor.originalFetch.apply(this, args);
if (response.ok) {
const clonedResponse = response.clone();
const contentType = response.headers.get('content-type') || '';
if ((request.url.includes('.m3u8') || request.url.includes('.m3u')) || contentType.includes('application/vnd.apple.mpegurl') || contentType.includes('text/plain')) {
clonedResponse.text().then(text => onM3U8Found(request.url, 'Fetch', text));
}
}
return response;
};
} catch (e) {
ActionLogger.log(`网络拦截器初始化失败: ${e.message}`, 'ERROR');
this.deactivate();
}
},
deactivate() {
if (!this.isActive) return;
if (this.originalXhrOpen) {
XMLHttpRequest.prototype.open = this.originalXhrOpen;
this.originalXhrOpen = null;
}
if (this.originalFetch) {
window.fetch = this.originalFetch;
this.originalFetch = null;
}
this.isActive = false;
}
});
const PlayerHooker = Utils.createSwitchableModule({
name: 'PlayerHooker',
targets: C.PLAYER_HOOKER_TARGETS,
originalPlayers: {},
isActive: false,
extractSource: (config) => { if (!config) return null; const sourceUrl = config.source || (config.video && config.video.url) || config.url || config.src; if (typeof sourceUrl === 'string' && (sourceUrl.includes('.m3u8') || sourceUrl.includes('.m3u'))) { return sourceUrl; } if (Array.isArray(config.sources)) { for (const srcObj of config.sources) { if (srcObj.src && (srcObj.src.includes('.m3u8') || srcObj.src.includes('.m3u'))) { return srcObj.src; } } } return null; },
activate() {
if (this.isActive) return;
this.isActive = true;
this.targets.forEach(playerName => {
try {
Object.defineProperty(window, playerName, {
configurable: true,
set: (newPlayer) => {
if (!this.originalPlayers[playerName]) {
this.originalPlayers[playerName] = newPlayer;
ActionLogger.log(`监测到播放器库 ${playerName} 加载`, 'HOOK');
}
},
get: () => {
const original = this.originalPlayers[playerName];
return (...args) => {
const m3u8Url = this.extractSource(args[0]);
let diagMessage = '';
try { diagMessage = JSON.stringify(args[0], null, 2); } catch (e) { diagMessage = `[无法序列化配置: ${e.message}]`; }
if (m3u8Url) {
DiagnosticsTool.logEvent({type: 'PLAYER_HOOK', message: `[${playerName}] 调用被成功拦截, 提取到源. Config: \n${diagMessage}`});
CoreLogic.handleFoundM3U8(m3u8Url, `${playerName} Takeover`);
const dummyPlayer = new Proxy({}, {
get: function(target, prop) {
if (prop in target) return target[prop];
const dummyMethod = () => dummyPlayer;
return dummyMethod;
}
});
return dummyPlayer;
}
DiagnosticsTool.logEvent({type: 'PLAYER_HOOK_FAIL', message: `[${playerName}] 调用被拦截, 但未提取到源. Config: \n${diagMessage}`});
return original ? Reflect.construct(original, args) : null;
};
}
});
} catch (e) { ActionLogger.log(`接管播放器 ${playerName} 失败: ${e.message}`, 'ERROR'); }
});
},
deactivate() { if (!this.isActive) return; this.targets.forEach(playerName => { if (this.originalPlayers[playerName]) { try { Object.defineProperty(window, playerName, { value: this.originalPlayers[playerName], writable: true, configurable: true, enumerable: true }); } catch (e) { ActionLogger.log(`恢复播放器 ${playerName} 失败: ${e.message}`, 'WARN'); } } }); this.originalPlayers = {}; this.isActive = false; }
});
const MANAGED_MODULES = [ { module: PlayerHooker, configKey: 'enablePlayerTakeover' }, { module: NetworkInterceptor, configKey: 'enableNetworkInterceptor' } ];
const SETTINGS_UI_CONFIG = [ { type: 'info', content: `💡 作者(bug 大魔王 qq634016189)感言: 本人毫无代码基础,指挥 gemini2.5pro做的脚本。` }, { group: '播放控制', items: [ { id: 'auto-play-toggle', label: '自动播放', type: 'switch', configKey: 'autoPlay' }, { id: 'smart-slice-toggle', label: '智能广告切除 (M3U8)', type: 'switch', configKey: 'isSmartSlicingEnabled' } ]}, { group: '高级设置', items: [ { id: 'playback-rate-input', label: '默认播放倍速', type: 'number', configKey: 'defaultPlaybackRate', props: { step: "0.25", min: "0.5" } }, { id: 'max-retries-input', label: '最大重试次数', type: 'number', configKey: 'maxRetryCount', props: { min: "1", max: "10" } }, ]}, { group: '站点管理', items: [ { id: 'site-blacklist-input', label: '黑名单 (在这些网站上禁用脚本,一行一个)', type: 'textarea', configKey: 'blacklist', props: { rows: "8" } }, { id: 'add-to-blacklist-btn', label: '添加当前站点到黑名单', type: 'button' } ] }, { group: '内容过滤', items: [ { id: 'm3u8-keywords-input', label: '广告关键词 (一行一个)', type: 'textarea', configKey: 'keywords', props: { rows: "10" } }, ]} ];
const SettingsUI = {
generateHTML(config) { let gridHTML = ''; config.forEach(section => { if (section.type === 'info') { gridHTML += ``; return; } let itemsHTML = section.items.map(item => { const props = item.props ? Object.entries(item.props).map(([k, v]) => `${k}="${v}"`).join(' ') : ''; switch(item.type) { case 'switch': return ``; case 'number': return ``; case 'textarea': return ``; case 'button': return ``; default: return ''; } }).join(''); gridHTML += `${section.group}
${itemsHTML}`; }); return `${SCRIPT_NAME} 设置 (v${SCRIPT_VERSION})
${gridHTML}
`; },
loadDataToUI(panel, configData, uiConfig) { uiConfig.flatMap(s => s.items || []).forEach(item => { if (!item.configKey) return; const el = panel.querySelector(`#${item.id}`); if (!el) return; const value = configData[item.configKey]; switch(item.type) { case 'switch': el.checked = value; break; case 'textarea': el.value = Array.isArray(value) ? value.join('\n') : value; break; case 'number': el.value = value; break; } }); },
saveDataFromUI(panel, uiConfig) { const newConfig = {}; uiConfig.flatMap(s => s.items || []).forEach(item => { if (!item.configKey) return; const el = panel.querySelector(`#${item.id}`); if (!el) return; switch(item.type) { case 'switch': newConfig[item.configKey] = el.checked; break; case 'textarea': newConfig[item.configKey] = el.value.split('\n').map(s => s.trim()).filter(Boolean); break; case 'number': newConfig[item.configKey] = parseFloat(el.value) || (item.configKey === 'maxRetryCount' ? C.MAX_RETRY_COUNT : 1.0); break; } }); return newConfig; }
};
function registerSettingsMenu() {
GM_registerMenuCommand(`⚙️ ${SCRIPT_NAME} 设置`, () => {
if (!IS_TOP_FRAME) { alert('请在顶层页面打开设置面板。'); return; }
if (document.getElementById(C.SETTINGS_PANEL_ID)) return;
ActionLogger.log("打开设置面板", 'UI');
StyleManager.injectSettingsStyles();
const panel = document.createElement('div');
panel.id = C.SETTINGS_PANEL_ID;
panel.innerHTML = SettingsUI.generateHTML(SETTINGS_UI_CONFIG);
document.body.appendChild(panel);
SettingsUI.loadDataToUI(panel, SettingsManager.config, SETTINGS_UI_CONFIG);
panel.querySelector('#dmz-settings-close-btn').addEventListener('click', () => panel.remove());
panel.querySelector('#dmz-settings-save-btn').addEventListener('click', async () => { const newConfig = SettingsUI.saveDataFromUI(panel, SETTINGS_UI_CONFIG); await SettingsManager.save(newConfig); panel.remove(); });
panel.querySelector('#dmz-settings-reset-btn').addEventListener('click', async () => { if (window.confirm('您确定要将所有设置恢复为默认值吗?此操作不可撤销。')) { const newConfig = await SettingsManager.reset(); SettingsUI.loadDataToUI(panel, newConfig, SETTINGS_UI_CONFIG); } });
const blacklistBtn = panel.querySelector('#add-to-blacklist-btn');
const blacklistTextarea = panel.querySelector('#site-blacklist-input');
if (blacklistBtn && blacklistTextarea) {
blacklistBtn.addEventListener('click', () => {
const currentHostname = window.location.hostname;
const currentList = blacklistTextarea.value.split('\n').map(s => s.trim()).filter(Boolean);
if (currentList.includes(currentHostname)) { FrameCommunicator.showNotification(`"${currentHostname}" 已在黑名单中。`); return; }
currentList.push(currentHostname);
blacklistTextarea.value = currentList.join('\n');
FrameCommunicator.showNotification(`已将 "${currentHostname}" 添加到黑名单,保存后生效。`);
});
}
});
GM_registerMenuCommand(`🩺 ${SCRIPT_NAME} 运行诊断`, () => DiagnosticsTool.run());
}
(async function main() {
// 全局模块初始化
window.PlayerManager = PlayerManager;
window.DiagnosticsTool = DiagnosticsTool;
DiagnosticsTool.init();
const originalCreateObjectURL = URL.createObjectURL;
URL.createObjectURL = function(obj) {
if (obj instanceof Blob) {
if (PlayerManager.isPlayerActiveOrInitializing || obj.isHandledByDmz) {
return originalCreateObjectURL.apply(URL, arguments);
}
const reader = new FileReader();
reader.onload = function() {
const text = reader.result;
if (typeof text === 'string' && text.trim().startsWith('#EXTM3U')) {
ActionLogger.log(`通过 createObjectURL 劫持到 M3U8 Blob (内容判断)`, 'BLOB_HIJACK');
obj.isHandledByDmz = true;
const blobIdUrl = `blob:${location.protocol}//${location.host}/${Date.now()}.m3u8`;
CoreLogic.handleFoundM3U8(blobIdUrl, 'createObjectURL Hijack', text);
}
};
reader.onerror = function() { ActionLogger.log(`读取Blob内容失败`, 'BLOB_HIJACK_ERROR'); };
const slice = obj.slice(0, 1024);
reader.readAsText(slice);
}
return originalCreateObjectURL.apply(URL, arguments);
};
IframeTerminator.start();
await SettingsManager.load();
const hostname = window.location.hostname;
const isBlacklisted = SettingsManager.config.blacklist.some(domain => {
try {
const regex = Utils.wildcardToRegex(domain.trim());
return regex.test(hostname);
} catch (e) {
ActionLogger.log(`黑名单规则 "${domain}" 无效,已跳过`, 'WARN');
return false;
}
});
if (isBlacklisted) {
ActionLogger.log(`已根据黑名单在 ${hostname} 停用。`, 'INIT');
IframeTerminator.stop();
return;
}
ActionLogger.log(`脚本已在 ${window.location.href} 激活 (上下文: ${IS_TOP_FRAME ? 'Top' : 'iFrame'})`, 'INIT');
console.log(`[${SCRIPT_NAME}] v${SCRIPT_VERSION} 已在 ${window.location.href} 激活`);
FrameCommunicator.init();
if (IS_TOP_FRAME) {
SPANavigator.init();
}
MANAGED_MODULES.forEach(({ module, configKey }) => {
if (SettingsManager.config[configKey]) { module.enable(); }
});
registerSettingsMenu();
const startScanner = () => DOMScanner.init();
if (document.readyState === 'loading') {
window.addEventListener('DOMContentLoaded', startScanner, { once: true });
} else {
startScanner();
}
window.addEventListener('beforeunload', () => {
ActionLogger.log("页面即将卸载,开始清理...", 'LIFECYCLE');
PlayerManager.cleanup();
DOMScanner.stop();
FrameCommunicator.removeListeners();
MANAGED_MODULES.forEach(({ module }) => module.disable());
IframeTerminator.stop();
});
})();
})();