// ==UserScript== // @name 大魔王视频助手<移动版> // @namespace http://tampermonkey.net/ // @version 1.3 // @description 悬浮播放器、横竖屏识别、拖拽记忆、自动接管、倍速播放、手势控制、M3U8广告过滤、诊断工具。 // @author bug大魔王 // @match *://*/* // @connect * // @require https://unpkg.com/hls.js@1.5.20/dist/hls.min.js // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_notification // @grant GM_info // @grant GM_xmlhttpRequest // @run-at document-start // @icon https://i.postimg.cc/Cxxtnwm7/file-00000000037872078ccd248cf86f31d8.png // ==/UserScript== (function () { 'use strict'; const CONSTANTS = { SCRIPT_NAME: '大魔王视频助手', SCRIPT_VERSION: '1.3', IS_TOP_FRAME: window.self === window.top, Hls: window.Hls, IDS: { ROOT: 'dmz-host-container', PLAYER: 'dmz-custom-player', SETTINGS_PANEL: 'dmz-settings-panel', PLAYER_STYLES: 'dmz-player-styles', }, CLASSES: { VIDEO_WRAPPER: 'dmz-video-wrapper', CLOSE_BUTTON: 'dmz-close-button', DRAG_HANDLE: 'dmz-drag-handle', SCREW_EFFECT: 'screw-effect', INDICATOR: 'indicator', SETTINGS_GRID: 'settings-grid', SETTINGS_CARD: 'settings-card', SETTINGS_BTN: 'settings-btn', SWITCH: 'switch', SLIDER: 'slider', VISIBLE: 'visible', HIDDEN: 'hidden', }, LIMITS: { MAX_M3U8_REDIRECT_DEPTH: 5, MAX_LOG_ENTRIES: 400, LONG_PRESS_DURATION_MS: 400, RETRY_MAX: 3, }, MESSAGE_TYPES: { M3U8_COMMAND: 'M3U8_PLAYER_COMMAND_V2_FINAL', SANDBOX_URL_FOUND: 'SANDBOX_URL_FOUND_V1', RAW_SIGNAL_FORWARDED: 'RAW_SIGNAL_FORWARDED_V1', ACTION_LOG_FORWARDED: 'ACTION_LOG_FORWARDED_V1', NEUTRALIZE_COMMAND: 'DMZ_NEUTRALIZE_COMMAND_V1', ACTIVATE_AUTOPLAY: 'DMZ_ACTIVATE_AUTOPLAY_V1', QUERY_IS_MAIN_PLAYER: 'DMZ_QUERY_IS_MAIN_PLAYER_V1', IFRAME_TRIGGER_CLICK: 'DMZ_IFRAME_TRIGGER_CLICK_V1', RESTORE_COMMAND: 'DMZ_RESTORE_COMMAND_V1', FORCE_PAUSE_ALL: 'DMZ_FORCE_PAUSE_ALL_V1', CROSS_FETCH_REQUEST: 'DMZ_CROSS_FETCH_REQ_V1', CROSS_FETCH_RESPONSE: 'DMZ_CROSS_FETCH_RES_V1', I_AM_BLOCKED_BY_AD: 'DMZ_I_AM_BLOCKED_BY_AD_V1', }, LOG_TYPES: { INFO: '🛠️ 系统', MODULE: '📡 模块', CONFIG: '⚙️ 配置', ERROR: '❌ 错误', WARN: '⚠️ 警告', INIT: '🚀 初始化', LIFECYCLE: '♻️ 生命周期', UI: '🧩 UI', COMM: '📨 通信', PLAYER: '📺 Ply', PLAYER_REVEAL: '✨ 渲染', PLAYER_LOCK: '🔐 锁定', PLAYBACK_SWITCH: '🔁 切换', HLS: '📽️ HLS', STREAM_SELECT: '🎚️ 选流', CORE: '🚩 中心', CORE_SLICE: '✂️ 广告过滤', CORE_EXEC: '💥 执行', CORE_IFRAME: '📦 沙箱', CORE_NEUTRALIZE: '🔇 中和', CORE_URL_RESOLVE: '🧭 解析', SCAN: '🔍 扫描', SCAN_WARN: '☢️ 注意', HOOK: '⚔️ 劫持', DECRYPTION_HOOK: '🧬 解混淆', NAV: '🧭 导航', TAKEOVER_ATTEMPT: '🎯 侦测', TAKEOVER_SUCCESS: '🪩 接管', TAKEOVER_FAIL: '📛 失败', }, TAKEOVER_SOURCES: { DOM_HTTP: '页面元素扫描', DOM_ATTR: '页面元素扫描(data-*)', NET_XHR: '网络拦截', NET_FETCH: '网络拦截', CROSS_FRAME: 'iFrame信使', DECRYPTION_HOOK_ATOB: '主动解混淆劫持(atob)', DECRYPTION_HOOK_JSON: '主动解混淆劫持(JSON)', DECRYPTION_HOOK_ATTR: '主动解混淆劫持(attr)', PLAYER_HOOK: (name) => `播放器接口劫持(${name})`, }, SELECTORS: { PLAYER_ELEMENTS: 'video, iframe, .jwplayer, .prism-player, .dplayer, .plyr, .art-video-player, .video-js, [class*="player"], [id*="player"], .video-container', PLAYER_CONTEXT: 'video, iframe, .jwplayer, .prism-player, .dplayer, .plyr, .art-video-player, .video-js, [class*="player"], [id*="player"], .video-container, .fp-ui', DMZ_EXCLUSION: '.dmz-video-wrapper, .dmz-iframe-remote-trigger, .dmz-switch-overlay', }, }; const SHARED_PATTERNS = { NEGATIVE_UI_KEYWORDS: /(translate|comment|chat|disqus|feedback|reply|submit|login|signup|ad-|ads|tracker|history|record|记录|历史|mini|pip|picture-in-picture|fullscreen|volume|setting|mute|card|list|item|thumb|poster|recommend|related|next|prev|episode|guess|more|score|评分)/, NEGATIVE_PLAYBACK_CONTROLS: /(history|record|记录|历史|setting|设置|speed|速度|mini|pip|picture-in-picture|fullscreen|volume|mute|card|list|item|thumb|poster|recommend|related|next|prev|episode|guess|more)/, }; function installAntiConsoleModalShield() { const suspiciousModalPattern = /(请关闭控制台|关闭控制台|close\s+console|developer\s*tools|devtools|打开\s*f12|f12\s*已打开)/i; const shouldBlockModal = (message) => { if (typeof message !== 'string') { return false; } return suspiciousModalPattern.test(message); }; const wrapModal = (originalFn, fallbackValue) => { if (typeof originalFn !== 'function') { return originalFn; } return function (...args) { try { if (shouldBlockModal(String(args[0] ?? ''))) { return fallbackValue; } } catch (e) { console.debug('Modal shield inspection failed:', e); } return originalFn.apply(this, args); }; }; try { window.alert = wrapModal(window.alert, undefined); window.confirm = wrapModal(window.confirm, true); window.prompt = wrapModal(window.prompt, ''); } catch (e) { console.debug('Failed to install modal shield:', e); } } installAntiConsoleModalShield(); const ICONS = { BRIGHTNESS: '', VOLUME_SIDE: '', BIG_PLAY: '', BIG_PAUSE: '', FORWARD: '', REWIND: '', VOLUME_ON: '', VOLUME_OFF: '', FS_ENTER: '', FS_EXIT: '', MORE: '', PIP: '', PLAYBACK_RATE: '', }; const CSS = { PLAYER: `:host{all:initial;position:fixed!important;background:transparent;overflow:visible;z-index:2147483646!important;display:flex;flex-direction:column;gap:0;padding:0;box-sizing:border-box;pointer-events:none;opacity:0;transition:opacity .3s ease-out;will-change:transform,opacity;backface-visibility:hidden;transform:translateZ(0)}:host(.dmz-visible){opacity:1}:host(.dmz-no-transition){transition:none!important}.${CONSTANTS.CLASSES.VIDEO_WRAPPER}{cursor:pointer;width:100%;flex-grow:1;display:flex;justify-content:center;align-items:center;background:#000;border-radius:20px;position:relative;overflow:hidden;box-shadow:0 10px 25px -5px rgba(0,0,0,.3);max-height:0;flex-grow:0;-webkit-tap-highlight-color:transparent;pointer-events:auto;transition:max-height 1s cubic-bezier(.4,0,.2,1);will-change:max-height;touch-action:none;transform:translate3d(0,0,0)}#${CONSTANTS.IDS.PLAYER}{width:100%;height:100%;object-fit:contain;background:#000;border-radius:20px;opacity:0;transition:opacity .3s ease;pointer-events:none}.${CONSTANTS.CLASSES.CLOSE_BUTTON}{position:absolute;top:12px;right: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:30;border:1px solid rgba(255,255,255,.1);outline:none;-webkit-tap-highlight-color:transparent;opacity:0;visibility:hidden;pointer-events:none;transition:background .2s ease,transform .2s ease,opacity .3s ease,visibility .3s ease}.${CONSTANTS.CLASSES.VIDEO_WRAPPER}.dmz-controls-visible .${CONSTANTS.CLASSES.CLOSE_BUTTON}{opacity:1;visibility:visible;pointer-events:auto}.${CONSTANTS.CLASSES.CLOSE_BUTTON}:hover{background:rgba(230,50,90,.9);transform:scale(1.1)}.${CONSTANTS.CLASSES.CLOSE_BUTTON}:active{background:rgba(128,128,128,.5)}.dmz-control-button{display:flex;justify-content:center;align-items:center;width:30px;height:30px;background:transparent;border:none;padding:0;cursor:pointer;flex-shrink:0;color:#fff;outline:none;-webkit-tap-highlight-color:transparent;border-radius:50%;transition:background-color .2s ease}.dmz-control-button:active{background:rgba(128,128,128,.4)}.dmz-control-button svg{width:22px;height:22px;fill:currentColor;pointer-events:none}.dmz-big-play-button-container{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;justify-content:center;align-items:center;pointer-events:none;z-index:22;opacity:0;transition:opacity .3s ease}.${CONSTANTS.CLASSES.VIDEO_WRAPPER}.dmz-controls-visible .dmz-big-play-button-container{opacity:1}.dmz-big-play-button{width:64px;height:64px;background:rgba(28,28,30,.5);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border:2px solid rgba(255,255,255,.8);border-radius:50%;display:flex;justify-content:center;align-items:center;padding:0;cursor:pointer;pointer-events:auto;color:#fff;outline:none;-webkit-tap-highlight-color:transparent;transition:background .2s ease,transform .2s ease}.dmz-big-play-button:hover{transform:scale(1.1);background:rgba(28,28,30,.7)}.dmz-big-play-button:active{background:rgba(80,80,80,.6);transform:scale(1.05)}.dmz-big-play-button svg{width:32px;height:32px;fill:currentColor}.${CONSTANTS.CLASSES.DRAG_HANDLE}{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:transparent;border-radius:9px;box-shadow:none;-webkit-tap-highlight-color:transparent;overflow:hidden;touch-action:none;pointer-events:auto}.${CONSTANTS.CLASSES.DRAG_HANDLE} .screw-effect{position:absolute;top:0;height:100%;width:calc(50% - 69px);background-image:none}.${CONSTANTS.CLASSES.DRAG_HANDLE} .indicator{width:80px;height:5px;background:#e0e0e0;border-radius:2.5px;box-shadow:0 0 3px rgba(0,0,0,.2);opacity:0;transition:opacity .3s ease,background-color .3s ease}.${CONSTANTS.CLASSES.DRAG_HANDLE}.dmz-indicator-visible .indicator {opacity: 1}@media (prefers-color-scheme: dark) { .${CONSTANTS.CLASSES.DRAG_HANDLE} .indicator { background: #424242; box-shadow: 0 0 3px rgba(0,0,0,.5); } }.dmz-controls-bar{position:absolute;bottom:3px;left:10px;right:10px;width:auto;height:auto;background:rgba(28,28,30,.6);backdrop-filter:blur(15px);-webkit-backdrop-filter:blur(15px);border-radius:10px;display:flex;flex-direction:column;align-items:center;padding:2px 10px;box-sizing:border-box;opacity:1;visibility:visible;transition:opacity .3s ease,visibility .3s ease;z-index:25;pointer-events:auto;touch-action:none}.dmz-controls-bar.hidden{opacity:0;visibility:hidden;pointer-events:none}.dmz-time-display{color:#fff;font-size:13px;text-shadow:0 0 3px rgba(0,0,0,.8);margin:0 8px;font-family:sans-serif}.dmz-time-separator{color:#ccc;margin:0 4px;font-size:13px}.dmz-bottom-controls{width:100%;height:32px;display:flex;align-items:center}.dmz-spacer{flex-grow:1}.dmz-progress-container{width:100%;height:15px;display:flex;align-items:center}.dmz-progress-bar{flex-grow:1;height:100%;display:flex;align-items:center;padding:0;cursor:pointer;-webkit-tap-highlight-color:transparent}.dmz-progress-rail{width:100%;height:4px;background:rgba(255,255,255,.3);position:relative;border-radius:2px}.dmz-progress-buffer{position:absolute;left:0;top:0;height:100%;width:0;background:rgba(255,255,255,.6);border-radius:2px;transition:width .1s linear}.dmz-progress-played{position:absolute;left:0;top:0;height:100%;width:0;background:#38b6ff;border-radius:2px;will-change:width,left;transition:none}.dmz-progress-handle{position:absolute;top:50%;left:0;width:12px;height:12px;background:#fff;border-radius:50%;transform:translate(-50%,-50%);box-shadow:0 0 5px rgba(0,0,0,.5);pointer-events:none;will-change:width,left;transition:none}.dmz-more-menu-container{position:relative}.dmz-more-menu{position:absolute;bottom:calc(100% + 5px);right:0;background:rgba(28,28,30,.8);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border-radius:8px;width:180px;z-index:30;list-style:none;margin:0;padding:5px;opacity:0;visibility:hidden;transition:opacity .2s ease,visibility .2s ease;transform-origin:bottom right}.dmz-more-menu.visible{opacity:1;visibility:visible}.dmz-menu-item{display:flex;align-items:center;padding:8px 12px;color:#fff;font-size:14px;cursor:pointer;border-radius:6px}.dmz-menu-item:hover{background:rgba(255,255,255,.1)}.dmz-menu-item.disabled{color:#777;cursor:not-allowed;background:transparent!important}.dmz-menu-item svg{width:20px;height:20px;margin-right:10px;fill:currentColor;pointer-events:none}.dmz-menu-item .dmz-menu-item-label{flex-grow:1;pointer-events:none}.dmz-menu-item .dmz-menu-item-value{color:#aaa;pointer-events:none}.dmz-playback-rates{display:none}.dmz-playback-rates.visible{display:block}.dmz-playback-rates .dmz-menu-item.active{color:#38b6ff;font-weight:700}.dmz-gesture-indicator-wrapper{position:absolute;top:40px;left:50%;transform:translateX(-50%);background:rgba(28,28,30,.7);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);color:#fff;padding:6px 12px;border-radius:18px;font-size:13px;font-family:sans-serif;display:flex;align-items:center;gap:6px;z-index:20;opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease;pointer-events:none}.dmz-gesture-indicator-wrapper.visible{opacity:1;visibility:visible}.dmz-indicator-icon{display:flex;justify-content:center;align-items:center;width:24px;height:24px;color:#fff}.dmz-indicator-icon svg{width:100%;height:100%;fill:currentColor}.dmz-indicator-text{min-width:120px;text-align:center}.dmz-brightness-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background:#000;opacity:0;pointer-events:none;z-index:15;transition:opacity .1s ease}.dmz-side-indicator-container{position:absolute;top:50%;transform:translateY(-50%);display:flex;flex-direction:column;align-items:center;gap:10px;opacity:0;transition:opacity .3s ease;pointer-events:none;z-index:20}.dmz-side-indicator-container.visible{opacity:1}.dmz-side-indicator-container.left{left:30px}.dmz-side-indicator-container.right{right:30px}.dmz-side-indicator-bar-wrapper{width:6px;height:120px;background:rgba(28,28,30,.7);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border-radius:3px;display:flex;align-items:flex-end;overflow:hidden;border:1px solid rgba(255,255,255,.15);box-sizing:border-box}.dmz-side-indicator-fill{width:100%;height:0;background:#38b6ff;border-radius:3px}.dmz-side-indicator-fill.transition-active{transition:height .1s linear}.dmz-side-indicator-icon{width:24px;height:24px}.dmz-side-indicator-icon svg{width:100%;height:100%;fill:#fff;filter:drop-shadow(0 0 2px rgba(0,0,0,.5))}`, PANEL: `:host{all:initial;box-sizing:border-box;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483647;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,.5);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);--s-bg:rgba(28,28,34,.95);--s-border:rgba(56,182,255,.5);--s-shadow:rgba(0,0,0,.4);--s-text-main:#e5e5ea;--s-text-title:#fff;--s-text-label:#fff;--s-text-value:#ccc;--s-text-dim:#aaa;--s-card-bg:rgba(40,40,50,.8);--s-card-border:rgba(255,255,255,.1);--s-textarea-bg:#111;--s-input-bg:rgba(20,20,30,.8);--s-input-border:rgba(56,182,255,.5);--s-switch-bg:#58585a;--s-switch-handle:#fff;--s-switch-checked:#34c759;--s-info:#61dafb;--s-nav-btn-bg:rgba(255,255,255,.08);--s-nav-btn-bg-active:var(--s-info)}:host(.visible){display:flex}@media (prefers-color-scheme:light){:host{--s-bg:rgba(252,252,252,.95);--s-border:rgba(0,122,255,.4);--s-shadow:rgba(0,0,0,.15);--s-text-main:#333;--s-text-title:#000;--s-text-label:#111;--s-text-value:#555;--s-text-dim:#666;--s-card-bg:rgba(255,255,255,.8);--s-card-border:rgba(0,0,0,.1);--s-textarea-bg:#f0f0f0;--s-input-bg:rgb(229,229,234);--s-input-border:rgba(0,0,0,.2);--s-switch-bg:#ccc;--s-info:#007aff;--s-nav-btn-bg:rgba(0,0,0,.05)}}.dmz-unified-panel-wrapper{width:90%;max-width:550px;max-height:95%;background:var(--s-bg);border:1px solid var(--s-border);border-radius:12px;box-shadow:0 8px 32px var(--s-shadow);color:var(--s-text-main);display:flex;flex-direction:column;margin-bottom:env(safe-area-inset-bottom,20px)}.dmz-unified-panel-wrapper *{box-sizing:border-box}.panel-header{flex-shrink:0;padding:15px}.dmz-panel-nav{display:flex;justify-content:center;gap:10px;padding:0 15px 15px;border-bottom:1px solid var(--s-card-border)}.dmz-unified-panel-content{flex-grow:1;overflow-y:auto;overscroll-behavior:contain}.dmz-panel-pane{display:none;padding:15px;gap:15px;flex-direction:column}.dmz-panel-pane.active{display:flex}.dmz-unified-panel-footer{display:none;align-items:center;justify-content:flex-end;border-top:1px solid var(--s-card-border);gap:10px;flex-shrink:0;padding:15px;min-height:60px}.dmz-unified-panel-footer.active{display:flex}.title-bar{display:flex;justify-content:space-between;align-items:center}.title-bar h3{margin:0;font-size:18px;color:var(--s-text-title);text-shadow:0 0 5px rgba(56,182,255,.7);font-weight:600}.title-bar .close-btn{background:transparent;border:none;color:rgba(255,82,82,.7);font-size:26px;cursor:pointer;padding:0;font-weight:700;width:30px;height:30px;display:flex;align-items:center;justify-content:center;line-height:1}.title-bar .close-btn:hover{color:rgba(255,82,82,1)}.dmz-nav-btn{flex-grow:1;padding:5px;border-radius:6px;border:none;background:var(--s-nav-btn-bg);color:var(--s-text-dim);font-size:14px;font-weight:600;cursor:pointer;transition:background .2s,color .2s}.dmz-nav-btn.active{background:var(--s-nav-btn-bg-active);color:#fff}.copy-report-btn{display:block;width:100%;margin:0 0 10px;background:rgba(56,182,255,.15);border:1px solid rgba(56,182,255,.3);color:var(--s-info);padding:8px;border-radius:6px;cursor:pointer;font-size:14px;font-weight:600;text-align:center}.copy-report-btn:hover{background:rgba(56,182,255,.3)}.status-banner{padding:10px 15px;border-radius:8px;background:rgba(56,182,255,.15);border:1px solid rgba(56,182,255,.3);display:flex;align-items:center;gap:12px;font-size:16px;font-weight:600}.status-banner-icon{font-size:22px;width:22px;height:22px;display:flex;align-items:center;justify-content:center}.cards-container{display:grid;grid-template-columns:1fr 1fr;gap:15px}.info-card{background:var(--s-card-bg);padding:12px 15px;border-radius:10px;border:1px solid var(--s-card-border);display:flex;flex-direction:column;gap:10px}.info-card.full-width{grid-column:1 / -1}.card-title{font-size:12px;color:var(--s-text-dim);font-weight:600;margin:0 0 5px;padding-bottom:5px;border-bottom:1px solid var(--s-card-border)}.card-content-item{display:flex;flex-direction:column;gap:8px;font-size:14px;align-items:flex-start}.label-wrapper{display:flex;justify-content:space-between;align-items:center;width:100%}.label{font-weight:600;color:var(--s-text-label)}.card-content-item .value{font-size:13px;color:var(--s-text-value);word-break:break-all}.card-content-item .value.success{color:#28a745}.card-content-item .value.warning{color:#ffc107}.card-content-item .value.error{color:#dc3545}.card-content-item .value ul{padding-left:18px;margin:5px 0 0}.card-content-item .value code{background:rgba(0,0,0,.05);padding:2px 5px;border-radius:4px;font-family:monospace;border:1px solid rgba(0,0,0,.05)}.copy-btn{background:rgba(0,122,255,.1);border:1px solid rgba(0,122,255,.2);color:#007aff;padding:3px 10px;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600;transition:background .2s ease,color .2s ease}.copy-btn:hover{background:rgba(0,122,255,.2);color:#0056b3}.details-toggle{cursor:pointer;color:var(--s-info);margin-top:10px;text-align:center;font-weight:600;padding:8px;background:rgba(0,122,255,.08);border-radius:8px}textarea{width:100%;box-sizing:border-box;background:var(--s-textarea-bg);color:var(--s-text-value);border:1px solid var(--s-card-border);border-radius:6px;font-family:monospace;font-size:10px;resize:vertical;padding:8px;margin-top:8px}textarea[data-info="m3u8-raw"],textarea[data-info="m3u8-processed"]{min-height:50px}.dmz-log-container{width:100%;box-sizing:border-box;background:var(--s-textarea-bg);color:var(--s-text-value);border:1px solid var(--s-card-border);border-radius:6px;font-family:monospace;font-size:10px;resize:vertical;padding:8px;margin-top:8px;min-height:300px;max-height:300px;overflow-y:auto}.dmz-log-container pre{margin:0;white-space:pre-wrap;word-break:break-all}.log-entry{line-height:1.4}.log-collapsible{cursor:pointer;padding:4px 8px;background:rgba(56,182,255,.15);border-radius:4px;margin:4px 0;user-select:none;transition:background .2s}.log-collapsible:hover{background:rgba(56,182,255,.3)}.log-collapsed-content .log-entry{padding-left:15px;border-left:2px solid rgba(255,255,255,.2)}.${CONSTANTS.CLASSES.SETTINGS_GRID}{display:flex;flex-direction:column;gap:15px}.${CONSTANTS.CLASSES.SETTINGS_CARD}{background:var(--s-card-bg);padding:12px 15px;border-radius:10px;border:1px solid var(--s-card-border)}h3.settings-title{color:var(--s-info);margin:0 0 10px;padding-bottom:10px;border-bottom:1px solid var(--s-card-border);font-size:16px;font-weight:600}.settings-card-info{background:rgba(56,182,255,.1);border:1px solid rgba(56,182,255,.25);padding:12px;border-radius:8px;color:var(--s-text-dim);margin-top:0;font-size:13px}.settings-card-info b{font-weight:700;color:var(--s-text-main)}.settings-card-info code{background:rgba(0,0,0,.05);padding:2px 5px;border-radius:4px;font-family:monospace;border:1px solid rgba(0,0,0,.08)}.option-item,.option-item-col{display:flex;justify-content:space-between;align-items:center;padding:8px 0;color:var(--s-text-main);font-size:15px}.option-item-col{flex-direction:column;align-items:flex-start;gap:8px}.option-item-col label{font-weight:600}.option-item-col input,.option-item-col textarea{width:100%;box-sizing:border-box;background:var(--s-input-bg);border:1px solid var(--s-input-border);border-radius:6px;padding:10px;color:var(--s-text-main);font-family:sans-serif;font-size:14px}#site-blacklist-input{resize:vertical;min-height:120px;font-family:monospace}.${CONSTANTS.CLASSES.SETTINGS_BTN}.action{background:rgba(0,122,255,.2);color:var(--s-info);margin-top:10px;width:100%;flex:none;border:1px solid rgba(0,122,255,.3)}.${CONSTANTS.CLASSES.SETTINGS_BTN}.action:hover{background:rgba(0,122,255,.3)}.${CONSTANTS.CLASSES.SETTINGS_BTN}{flex:0 1 auto;padding:8px 20px;border:none;border-radius:8px;cursor:pointer;color:#fff;font-size:15px;font-weight:600;box-shadow:0 2px 5px rgba(0,0,0,.1);transition:transform .1s ease,background .2s ease}.${CONSTANTS.CLASSES.SETTINGS_BTN}:active{transform:scale(0.97)}.${CONSTANTS.CLASSES.SETTINGS_BTN}.close{background:#555}.${CONSTANTS.CLASSES.SETTINGS_BTN}.save{background:#007aff}.${CONSTANTS.CLASSES.SETTINGS_BTN}.reset{background:#007aff}.${CONSTANTS.CLASSES.SWITCH}{position:relative;display:inline-block;width:51px;height:31px}.${CONSTANTS.CLASSES.SWITCH} input{opacity:0;width:0;height:0}.${CONSTANTS.CLASSES.SLIDER}{position:absolute;cursor:pointer;inset:0;background:var(--s-switch-bg);transition:.4s;border-radius:31px}.${CONSTANTS.CLASSES.SLIDER}:before{position:absolute;content:"";height:27px;width:27px;left:2px;bottom:2px;background:var(--s-switch-handle);transition:.4s;border-radius:50%;box-shadow:0 1px 3px rgba(0,0,0,.2)}input:checked+.${CONSTANTS.CLASSES.SLIDER}{background:var(--s-switch-checked)}input:checked+.${CONSTANTS.CLASSES.SLIDER}:before{transform:translateX(20px)}`, }; const DomUtilsCore = { applyOption(el, key, value) { if (key === 'style' && typeof value === 'object') { Object.assign(el.style, value); return; } if (key in el) { el[key] = value; return; } el.setAttribute(key, value); }, appendChildren(el, children = []) { children.forEach((child) => el.append(child)); return el; }, buildAttrSignature(el) { if (!el) { return ''; } return ( el.id + ' ' + el.className + ' ' + (el.getAttribute('aria-label') || '') + ' ' + (el.getAttribute('title') || '') + ' ' + (el.textContent || '') ) .toLowerCase() .replace(/\s+/g, ' ') .trim(); }, setIconVisibility(showIcon, hideIcons = []) { if (showIcon) { showIcon.style.display = 'block'; } hideIcons.forEach((icon) => { if (icon) { icon.style.display = 'none'; } }); }, toggleVisibilityClass(element, visibleClass, forceState) { if (!element) { return false; } element.classList.toggle(visibleClass, forceState); return element.classList.contains(visibleClass); }, setTemporaryButtonState( button, { successText, originalContent, duration = 1500, successBgColor = '' } ) { const originalWidth = button.style.width; const originalBgColor = button.style.backgroundColor; const content = originalContent ?? button.innerHTML; const preciseWidth = button.getBoundingClientRect().width; button.style.width = `${preciseWidth}px`; button.textContent = successText; if (successBgColor) { button.style.backgroundColor = successBgColor; } setTimeout(() => { button.innerHTML = content; button.style.width = originalWidth; button.style.backgroundColor = originalBgColor; }, duration); }, }; const ClipboardUtils = { writeTextWithClipboardApi(text, successCallback, fallbackCallback) { return navigator.clipboard .writeText(text) .then(successCallback) .catch(() => fallbackCallback(text)); }, writeTextWithExecCommand(text, successCallback, fallbackCallback) { try { const textArea = document.createElement('textarea'); textArea.value = text; Object.assign(textArea.style, { position: 'fixed', top: '0', left: '0', width: '2em', height: '2em', padding: '0', border: 'none', outline: 'none', boxShadow: 'none', background: 'transparent', color: 'transparent' }); let root = document.body; if (document.activeElement && document.activeElement.shadowRoot) { root = document.activeElement.shadowRoot; } root.appendChild(textArea); textArea.focus(); textArea.select(); const successful = document.execCommand('copy'); root.removeChild(textArea); if (successful) { successCallback(); } else { fallbackCallback(text); } } catch (err) { fallbackCallback(text); } }, writeText(text, successCallback = () => {}) { if (typeof GM_setClipboard === 'function') { GM_setClipboard(text, 'text'); successCallback(); return Promise.resolve(); } const fallbackCallback = (fallbackText) => { try { const overlay = document.createElement('div'); Object.assign(overlay.style, { position: 'fixed', top: '0', left: '0', width: '100%', height: '100%', background: 'rgba(0,0,0,0.85)', zIndex: '2147483647', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '20px', boxSizing: 'border-box' }); const title = document.createElement('div'); title.textContent = '浏览器限制自动复制,请手动长按全选复制:'; title.style.cssText = 'color: #fff; margin-bottom: 10px; font-size: 14px;'; const ta = document.createElement('textarea'); ta.value = fallbackText; ta.readOnly = true; ta.style.cssText = 'width: 100%; max-width: 500px; height: 60%; background: #111; color: #38b6ff; border: 1px solid #38b6ff; padding: 10px; border-radius: 8px; font-family: monospace; font-size: 12px; resize: none;'; const btn = document.createElement('button'); btn.textContent = '关闭'; btn.style.cssText = 'margin-top: 15px; padding: 8px 24px; background: #ff3b30; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;'; btn.onclick = () => overlay.remove(); overlay.append(title, ta, btn); document.body.appendChild(overlay); setTimeout(() => { ta.focus(); ta.select(); }, 100); } catch (e) { console.debug('Fallback copy failed:', e); } }; if (navigator.clipboard && window.isSecureContext) { return this.writeTextWithClipboardApi(text, successCallback, fallbackCallback); } return this.writeTextWithExecCommand(text, successCallback, fallbackCallback); }, }; const TimingUtils = { debounce(func, wait) { let timeout; return function (...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; }, clear(timerId) { if (timerId) { clearTimeout(timerId); } return null; }, }; const MediaViewportUtils = { _primaryRectCache: new WeakMap(), isVisibleElement(el) { return !!(el && el.offsetParent !== null && el.offsetWidth > 0 && el.offsetHeight > 0); }, getPrimaryMediaCandidateScore(el, rect, viewportWidth, viewportHeight) { const area = rect.width * rect.height; const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; const attr = (el.id + ' ' + el.className + ' ' + (el.getAttribute('title') || '')).toLowerCase(); let score = area; if (/(player|video|jw|prism|plyr|dplayer|art)/.test(attr)) { score += 180000; } if (centerY <= viewportHeight * 0.68) { score += 120000; } if (centerY > viewportHeight * 0.8) { score -= 320000; } score -= Math.abs(centerX - viewportWidth / 2) * 120; score -= Math.abs(centerY - viewportHeight * 0.32) * 80; return score; }, getPrimaryMediaViewportRect(root = document) { const scope = root && typeof root.querySelectorAll === 'function' ? root : document; const now = performance.now(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const cacheKey = `${viewportWidth}x${viewportHeight}|${document.visibilityState}|${document.readyState}`; const cached = this._primaryRectCache.get(scope); if (cached && cached.key === cacheKey && now - cached.stamp <= 180) { return cached.value; } let bestRect = null; let bestScore = -Infinity; for (const el of scope.querySelectorAll(CONSTANTS.SELECTORS.PLAYER_ELEMENTS)) { if (!this.isVisibleElement(el)) { continue; } if (el.closest(CONSTANTS.SELECTORS.DMZ_EXCLUSION)) { continue; } const rect = el.getBoundingClientRect(); if (rect.width < 160 || rect.height < 90) { continue; } if (rect.bottom < 0 || rect.top > viewportHeight || rect.right < 0 || rect.left > viewportWidth) { continue; } const score = this.getPrimaryMediaCandidateScore(el, rect, viewportWidth, viewportHeight); if (score > bestScore) { bestScore = score; bestRect = rect; } } const value = !bestRect ? null : { left: Math.max(0, bestRect.left - 40), top: Math.max(0, bestRect.top - 40), right: Math.min(viewportWidth, bestRect.right + 40), bottom: Math.min(viewportHeight, bestRect.bottom + 60), width: bestRect.width, height: bestRect.height, }; this._primaryRectCache.set(scope, { key: cacheKey, stamp: now, value, }); return value; }, isElementInPrimaryMediaRegion(el, root = document, mediaRect = null) { if (!el || typeof el.getBoundingClientRect !== 'function') { return false; } const rect = el.getBoundingClientRect(); if (rect.width <= 0 || rect.height <= 0) { return false; } const resolvedMediaRect = mediaRect || this.getPrimaryMediaViewportRect(root); const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; if (!resolvedMediaRect) { return centerY <= window.innerHeight * 0.68; } return ( centerX >= resolvedMediaRect.left && centerX <= resolvedMediaRect.right && centerY >= resolvedMediaRect.top && centerY <= resolvedMediaRect.bottom ); }, }; const PlayerBindingCore = { bindVideoListeners(video, eventNames, cleanupFns, onSignal, listenerOptions = undefined) { eventNames.forEach((eventName) => { const handler = () => onSignal(); if (listenerOptions === undefined) { video.addEventListener(eventName, handler); } else { video.addEventListener(eventName, handler, listenerOptions); } cleanupFns.push(() => video.removeEventListener(eventName, handler, listenerOptions)); }); }, }; const DomQueryUtils = { queryVisibleElements(selectors, root, isVisibleElement) { const normalizedSelectors = Array.isArray(selectors) ? selectors : [selectors]; return Array.from(root.querySelectorAll(normalizedSelectors.join(', '))).filter((el) => isVisibleElement(el)); }, queryFirstVisiblePerSelector( selectors, root, getPrimaryMediaViewportRect, isVisibleElement, isElementInPrimaryMediaRegion ) { const mediaRect = getPrimaryMediaViewportRect(root); return selectors .map((selector) => { let firstVisible = null; let inRegion = null; for (const el of root.querySelectorAll(selector)) { if (!isVisibleElement(el)) { continue; } if (!firstVisible) { firstVisible = el; } if (!inRegion && isElementInPrimaryMediaRegion(el, root, mediaRect)) { inRegion = el; if (firstVisible) { break; } } } return inRegion || firstVisible || null; }) .filter(Boolean); }, }; const InteractionUtils = { findInteractiveAncestor(startNode, { stopNode = document.body, maxDepth = Infinity } = {}) { let current = startNode; let depth = 0; while (current && current !== stopNode && depth <= maxDepth) { const style = window.getComputedStyle(current); if ( current.tagName === 'BUTTON' || style.cursor === 'pointer' || current.getAttribute('role') === 'button' ) { return current; } current = current.parentElement; depth++; } return null; }, dispatchMouseEventSequence(target, eventTypes, eventOptions = {}, errorLabel = 'Event dispatch failed:') { if (!target) { return; } const rect = typeof target.getBoundingClientRect === 'function' ? target.getBoundingClientRect() : { left: 0, top: 0, width: 0, height: 0 }; const resolvedClientX = Number.isFinite(eventOptions.clientX) ? eventOptions.clientX : rect.left + rect.width / 2; const resolvedClientY = Number.isFinite(eventOptions.clientY) ? eventOptions.clientY : rect.top + rect.height / 2; const baseOptions = { bubbles: true, cancelable: true, composed: true, view: window, clientX: resolvedClientX, clientY: resolvedClientY, screenX: resolvedClientX, screenY: resolvedClientY, button: 0, buttons: 1, ...eventOptions, }; eventTypes.forEach((eventType) => { try { const options = { ...baseOptions, buttons: /(?:up|end|cancel|click)$/.test(eventType) ? 0 : (baseOptions.buttons ?? 1), }; if (eventType.startsWith('pointer') && typeof PointerEvent === 'function') { target.dispatchEvent( new PointerEvent(eventType, { pointerId: 1, pointerType: 'touch', isPrimary: true, ...options, }) ); return; } if (eventType.startsWith('touch')) { const touchLike = { identifier: Date.now(), target, clientX: options.clientX, clientY: options.clientY, screenX: options.screenX, screenY: options.screenY, pageX: options.clientX + window.scrollX, pageY: options.clientY + window.scrollY, radiusX: 11, radiusY: 11, rotationAngle: 0, force: 1, }; if (typeof TouchEvent === 'function' && typeof Touch === 'function') { const touch = new Touch(touchLike); target.dispatchEvent( new TouchEvent(eventType, { ...options, touches: eventType === 'touchend' || eventType === 'touchcancel' ? [] : [touch], targetTouches: eventType === 'touchend' || eventType === 'touchcancel' ? [] : [touch], changedTouches: [touch], }) ); return; } const fallbackTouchEvent = new Event(eventType, options); Object.defineProperties(fallbackTouchEvent, { touches: { value: eventType === 'touchend' || eventType === 'touchcancel' ? [] : [touchLike], }, targetTouches: { value: eventType === 'touchend' || eventType === 'touchcancel' ? [] : [touchLike], }, changedTouches: { value: [touchLike] }, }); target.dispatchEvent(fallbackTouchEvent); return; } target.dispatchEvent(new MouseEvent(eventType, options)); } catch (e) { console.debug(errorLabel, e); } }); }, safeNativeClick(target, errorLabel = 'Native click failed:') { if (!target) { return false; } try { target.click(); return true; } catch (e) { console.debug(errorLabel, e); return false; } }, startPollingTask(interval, onTick, errorLabel = 'Polling task failed:') { let timerId = null; timerId = setInterval(() => { try { if (onTick(timerId) === false) { clearInterval(timerId); } } catch (e) { clearInterval(timerId); console.debug(errorLabel, e); } }, interval); return timerId; }, }; const UrlUtils = { wildcardToRegex(pattern) { return new RegExp(`^${pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')}$`); }, isM3U8(url) { if (typeof url !== 'string') { return false; } const lower = url.toLowerCase(); return ( lower.includes('.m3u8') || lower.includes('.m3u') || lower.includes('/m3u8/') || lower.includes('application/x-mpegurl') || lower.includes('application/vnd.apple.mpegurl') ); }, formatTime(seconds) { if (isNaN(seconds) || seconds < 0) { return '00:00'; } const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60) .toString() .padStart(2, '0'); const s = Math.floor(seconds % 60) .toString() .padStart(2, '0'); return h > 0 ? `${h}:${m}:${s}` : `${m}:${s}`; }, getVideoFormat(url, isM3U8) { if (typeof url !== 'string') { return { name: '未知', type: '未知' }; } if (isM3U8(url)) { return { name: 'M3U8', type: '流式传输' }; } const cleanUrl = url.split('?')[0].toLowerCase(); const formats = [ { ext: '.mp4', name: 'MP4' }, { ext: '.mkv', name: 'MKV' }, { ext: '.webm', name: 'WebM' }, { ext: '.flv', name: 'FLV' }, ]; const format = formats.find((f) => cleanUrl.endsWith(f.ext)); if (format) { return { name: format.name, type: '常规文件' }; } try { const urlObj = new URL(url); for (const [key, value] of urlObj.searchParams.entries()) { const lowerValue = value.toLowerCase(); const paramFormat = formats.find((f) => lowerValue.endsWith(f.ext)); if (paramFormat) { return { name: paramFormat.name, type: '常规文件' }; } if ( (key.includes('type') || key.includes('format')) && formats.some((f) => lowerValue.includes(f.ext.substring(1))) ) { return { name: formats.find((f) => lowerValue.includes(f.ext.substring(1))).name, type: '常规文件', }; } } } catch (e) { console.debug('Failed to parse video format:', e); } return { name: '媒体流', type: '常规文件' }; }, }; const IframeProbeUtils = { probe(src, context, sourceName, cache, inflight) { if (typeof src !== 'string' || !/^https?:/i.test(src)) { return; } const cacheKey = src.split('#')[0]; const now = Date.now(); const lastProbeAt = cache.get(cacheKey) || 0; if (inflight.has(cacheKey) || now - lastProbeAt < 8000) { return; } inflight.add(cacheKey); cache.set(cacheKey, now); if (cache.size > 200) { const entries = Array.from(cache.entries()).sort((a, b) => a[1] - b[1]); entries.slice(0, 50).forEach(([key]) => cache.delete(key)); } fetch(src) .then((res) => res.text()) .then((html) => { const match = html.match(/((?:https?:)?\/\/[^'"]+\.m3u8(?:[\?#][^'"]*)?)/i); if (match && !context.playerManager.isPlayerActiveOrInitializing) { const rawUrl = match[1] || ''; const m3u8Url = rawUrl.startsWith('//') ? `${window.location.protocol}${rawUrl}` : rawUrl.replace(/\\/g, ''); context.coreLogic.addTakeoverCandidate({ url: m3u8Url, sourceName, }); } }) .catch(() => {}) .finally(() => { inflight.delete(cacheKey); }); }, }; const PagePathScoreUtils = { getNormalizedPath() { try { const loc = window.top.location; const normalizedPath = decodeURIComponent((loc.pathname || '').toLowerCase()); const normalizedSearch = (loc.search || '').toLowerCase(); const hashRoute = this.extractHashRoute(loc.hash || ''); if (hashRoute) { return { path: hashRoute.path, full: hashRoute.full, hostname: loc.hostname.toLowerCase(), }; } return { path: normalizedPath, full: decodeURIComponent(normalizedPath + normalizedSearch), hostname: loc.hostname.toLowerCase(), }; } catch (e) { const normalizedPath = window.location.pathname.toLowerCase(); const normalizedSearch = window.location.search.toLowerCase(); const hashRoute = this.extractHashRoute(window.location.hash || ''); if (hashRoute) { return { path: hashRoute.path, full: hashRoute.full, hostname: window.location.hostname.toLowerCase(), }; } return { path: normalizedPath, full: normalizedPath + normalizedSearch, hostname: window.location.hostname.toLowerCase(), }; } }, extractHashRoute(hash = '') { const normalizedHash = decodeURIComponent(String(hash || '').trim().toLowerCase()); if (!normalizedHash) { return null; } const route = normalizedHash.replace(/^#!/, '#').slice(1); if (!route || (!route.startsWith('/') && !route.startsWith('?'))) { return null; } const [rawPath = '', rawQuery = ''] = route.split('?'); const path = rawPath || '/'; const full = rawQuery ? `${path}?${rawQuery}` : path; return { path, full, }; }, hasGenericDetailRoute(path = '') { return /(?:^|\/)[a-z0-9_-]*(?:detail|details|view|content|item|info|overview|intro)(?:\/|$)/i.test(path); }, hasStrongDetailIdentifier(path = '', full = '', patterns) { const paramIdMatch = full.match(patterns.PARAM_ID); if (paramIdMatch) { const value = paramIdMatch[2] || ''; if (value.length >= 4 && !patterns.INVALID_ID.test(value)) { return true; } } return ( patterns.PATH_ID.test(path) || /\/\d{4,}(?:\.html)?\/?$/i.test(path) || /\/[a-f0-9]{8,}(?:\.html)?\/?$/i.test(path) || /\/[a-z0-9_-]*\d[a-z0-9_-]{5,}(?:\.html)?\/?$/i.test(path) ); }, isWeakDetailPath(path = '', full = '', patterns) { return this.hasGenericDetailRoute(path) && !this.hasStrongDetailIdentifier(path, full, patterns); }, calculatePathScore(path, full, hostname, patterns) { let score = 0; score += this.getPrimaryPathScore(path, full, patterns); score += this.getDetailPathScore(path, full, patterns); score += this.getIdentifierPathScore(path, patterns); score += this.getGeneralPathScore(path, full, hostname, patterns); score += this.getNegativePathScore(path, full, patterns); score += this.getSpecialPathAdjustment(path, full, patterns, score); return score; }, getPrimaryPathScore(path, full, patterns) { if ( path.indexOf('/play/') !== -1 || path.indexOf('/watch/') !== -1 || path.indexOf('/vodplay/') !== -1 || path.indexOf('/view/') !== -1 || path.indexOf('/videos/') !== -1 || path.indexOf('/video/') !== -1 || path.indexOf('-video/') !== -1 || path.indexOf('/video-') !== -1 || path.indexOf('video.') !== -1 || path.indexOf('/short/') !== -1 || path.indexOf('/shorties/') !== -1 || path.indexOf('/movie/') !== -1 || path.indexOf('/film/') !== -1 || path.indexOf('/movies/') !== -1 || path.indexOf('/tvshows/') !== -1 || path.indexOf('/drama/') !== -1 || full.includes('.m3u8') ) { return 100; } if (/(?:^|\/)(?:p|play|drama|movie|film|short)\/\d+(?:[\/_-]\d+)?(?:\.html|\/|$)/i.test(path)) { return 100; } return patterns.HIGH_SCORE_PATH.test(full) ? 100 : 0; }, getDetailPathScore(path, full, patterns) { if ( patterns.DETAIL_PATH.test(path) || patterns.HIGH_SCORE_FULL.test(full) || (this.hasGenericDetailRoute(path) && this.hasStrongDetailIdentifier(path, full, patterns)) ) { return 100; } if (!patterns.DETAIL_SPLIT.test(path)) { return 0; } const afterDetail = path.split(patterns.DETAIL_SPLIT)[1]; return afterDetail && !/^\d+(\.html)?\/?$/.test(afterDetail) ? 100 : 0; }, getIdentifierPathScore(path, patterns) { if (patterns.HEX_ROOT.test(path) && /[a-f]/i.test(path) && /[0-9]/.test(path)) { return 120; } let score = 0; if (patterns.MULTI_NUM.test(path) && !patterns.YEAR.test(path) && !patterns.FILTER.test(path)) { score += 80; } if ( /(?:^|\/)\d+\/\d+(?:[\/_-]\d+)?\.html/i.test(path) && !/(?:^|\/)(?:19|20)\d{2}\/\d+(?:\/\d+)?\.html/i.test(path) ) { score += 80; } if (patterns.CODE.test(path)) { score += 50; } if (patterns.LONG_ID.test(path)) { score += 60; } return score; }, getGeneralPathScore(path, full, hostname, patterns) { let score = 0; const hasStrongDetailContext = this.hasGenericDetailRoute(path) && this.hasStrongDetailIdentifier(path, full, patterns); if (patterns.MEDIUM_SCORE.test(full)) { score += 30; } if (patterns.LOW_SCORE.test(full)) { score += 20; } if (patterns.MINIMAL_SCORE.test(full)) { score += 10; } if (/\.html$/.test(path)) { score += 10; } if (/(?:^|[\/])\d+\.html$/.test(path)) { score += 30; } if (hostname.startsWith('live.') || /forumdisplay/i.test(full)) { score -= 60; } else if (patterns.NEGATIVE_KEYWORDS.test(full) && !hasStrongDetailContext) { score -= 60; } return score; }, getNegativePathScore(path, full, patterns) { let score = 0; const hasStrongDetailContext = this.hasGenericDetailRoute(path) && this.hasStrongDetailIdentifier(path, full, patterns); if (/(?:^|[\/_\?&=-])(tvshows)(?:$|[\/_\?&=-]|\.)/i.test(full)) { score -= 100; } if (patterns.NEGATIVE_PATH.test(path)) { score -= 200; } if (/(^|\/)(movies?|videos?|tvshows?|drama|anime|dongman|vod|shorties?|shorts?|film|shows?|series|episodes?|programs?)\/?$/i.test(path) && !/[\?&](id|vid|aid|cid|v|f|play|name)=/.test(full)) { score -= 200; } if (patterns.STRONG_NEGATIVE.test(path)) { score -= 200; } if (/(vodshow|videoshow)/i.test(path)) { score -= 200; } if (/\/[-_]?(dongman|tvshows)\/+\d+(\.html)?\/?$/i.test(path)) { score -= 200; } if (/\/[-_]?(detail|intro|overview)\/+[a-z0-9-_]+(\.html)?\/?$/i.test(path) && !hasStrongDetailContext) { score -= 200; } if (/(^|\/)(t|topic|thread|post|forum|discuss|article)\/(\d+|[^\/]+\/\d+)(\/|\.html|$)/i.test(path)) { score -= 200; } if (/(^|\/)(v|vod|video)\/\d+(\.html\/?|\/?)$/i.test(path)) { score -= 180; } if (/^\/[a-z-]+\/\d+\.html$/i.test(path) && !hasStrongDetailContext) { score -= 30; } if (/(^|\/|[-_])index\d+(\.html)?\/?$/i.test(path)) { score -= 200; } if (/^\/(?=.*[a-z])(?=.*\d)[a-z0-9]{6,20}\/?$/i.test(path)) { score -= 100; } if (/\/(?=.*[a-z])(?=.*\d)[a-z0-9]{6,20}\/\d{1,3}(\.html)?\/?$/i.test(path)) { score -= 100; } if (/[-_]{2,}\d+/i.test(path)) { score -= 100; } if (patterns.LIST_KEYWORDS.test(full) && !hasStrongDetailContext) { score -= 100; } return score; }, getSpecialPathAdjustment(path, full, patterns, currentScore) { let score = 0; if (patterns.SORT_PARAMS.test(full)) { score -= 60; } if (/\/category\/[^\/]+\/\d{4,}(\.html)?\/?$/i.test(path)) { score += 150; } if (patterns.YEAR.test(path) && currentScore + score < 100) { score -= 50; } if ((this.isRepositoryDocumentLikePath(path, full) || this.isSourceOrDocumentFilePath(path)) && !this.hasStrongMediaPathContext(path, full, patterns)) { score -= 180; } return score; }, calculateIdScore(path, full, patterns) { const parts = this.collectIdParts(path, full, patterns); const idMatchBonus = this.getIdMatchBonus(path, full, patterns); const bestPartScore = this.scoreIdParts(parts, patterns); const score = idMatchBonus + bestPartScore; return this.applyGenericDetailIdClamp(path, full, score, bestPartScore, patterns); }, applyGenericDetailIdClamp(path, full, score, bestPartScore, patterns) { if (score <= 0 || bestPartScore < 60) { return score; } if (this.hasStrongMediaPathContext(path, full, patterns)) { return score; } if (this.isRepositoryDocumentLikePath(path, full) || this.isSourceOrDocumentFilePath(path)) { return Math.min(score, 10); } const pureNumericLeaf = /\/\d{5,}(?:\.html)?\/?$/i.test(path); const genericDetailShell = /^\/[^\/?#]+\/[^\/?#]+\/\d{5,}(?:\.html)?\/?$/i.test(path); const articleishPath = /(^|\/)(item|entry|doc|docs|article|news|post|topic|thread|subject|book|read|pedia|wiki|baike|intro|overview)(\/|$)/i.test(path) || patterns.NEGATIVE_KEYWORDS.test(full); if (pureNumericLeaf && (genericDetailShell || articleishPath)) { return Math.min(score, 20); } return score; }, isRepositoryDocumentLikePath(path, full) { return ( /(^|\/)(blob|tree|raw|src|source|commit|commits|compare|pull|pulls|merge_request|merge_requests|issues?|wiki|repository|repo)(\/|$)/i.test(path) || /[?&](path|file|filepath|ref|branch|plain|tab)=/i.test(full) ); }, isSourceOrDocumentFilePath(path) { return /\.(?:md|markdown|txt|rst|adoc|wiki|json|ya?ml|toml|ini|cfg|conf|log|js|mjs|cjs|jsx|ts|tsx|css|scss|less|html?|xml|svg|py|java|kt|go|rs|c|cc|cpp|h|hpp|sh|bash|zsh|ps1)(?:$|[?#])/i.test(path); }, hasStrongMediaPathContext(path, full, patterns) { return ( (patterns.MULTI_NUM.test(path) && !patterns.YEAR.test(path) && !patterns.FILTER.test(path)) || patterns.HIGH_SCORE_PATH.test(full) || patterns.MEDIUM_SCORE.test(full) || patterns.MINIMAL_SCORE.test(full) || patterns.PATH_ID.test(path) || /(^|\/)(play|watch|vodplay|player|video|videos|movie|movies|film|drama|episode|short|shorties|stream|embed|tvshows)(\/|$)|\.m3u8(?:[?#]|$)/i.test(path) ); }, collectIdParts(path, full, patterns) { const parts = path.split(/\/|\./).filter((part) => part && part.length > 0); const paramIdMatch = full.match(patterns.PARAM_ID); const pathIdMatch = path.match(patterns.PATH_ID); if (paramIdMatch) { parts.push(paramIdMatch[2]); } if (pathIdMatch) { parts.push(pathIdMatch[2]); } return parts; }, getIdMatchBonus(path, full, patterns) { let score = 0; const paramIdMatch = full.match(patterns.PARAM_ID); const pathIdMatch = path.match(patterns.PATH_ID); if (paramIdMatch && ['v', 'f', 'id'].includes(paramIdMatch[1])) { score += 30; } if (pathIdMatch) { score += 30; } return score; }, scoreIdParts(parts, patterns) { let best = 0; for (const part of parts) { const partScore = this.scoreSingleIdPart(part, patterns); if (partScore === null) { continue; } if (partScore > best) { best = partScore; } } return best; }, scoreSingleIdPart(part, patterns) { if (patterns.INVALID_ID.test(part) || /^(19|20)\d{2}$/.test(part)) { return null; } if (part.length >= 5) { if (/[a-zA-Z]/.test(part) && /[0-9]/.test(part)) { return 60; } if (/^[a-zA-Z_-]+$/.test(part)) { if (part.length > 20) { return (part.match(/-/g) || []).length >= 3 ? 30 : -30; } return 10; } } if (/^[-_]?\d+$/.test(part)) { return part.replace(/[-_]/g, '').length >= 6 ? 60 : 20; } return null; }, }; const DynamicPlayButtonUtils = { discover(root, helpers) { const { findInteractiveAncestor, isVisibleElement, getPrimaryMediaViewportRect, isElementInPrimaryMediaRegion } = helpers; const candidates = new Set(); const mediaRect = getPrimaryMediaViewportRect(root); try { const addCandidate = (el) => { if (!el) { return; } const candidate = findInteractiveAncestor(el, { stopNode: root, maxDepth: 4, }) || el; if (!candidate || candidate.closest(CONSTANTS.SELECTORS.DMZ_EXCLUSION)) { return; } if (!isVisibleElement(candidate)) { return; } if (!isElementInPrimaryMediaRegion(candidate, root, mediaRect)) { return; } const attrStr = DomUtils.getAttrSignature(candidate); if (SHARED_PATTERNS.NEGATIVE_PLAYBACK_CONTROLS.test(attrStr)) { return; } if (candidate.closest('[class*="prev"], [class*="next"]')) { return; } const parentLink = candidate.closest('a'); if ( parentLink && parentLink.href && !parentLink.href.includes('javascript') && !candidate.closest(CONSTANTS.SELECTORS.PLAYER_ELEMENTS) ) { return; } candidates.add(candidate); }; root.querySelectorAll('video').forEach((v) => { const rect = v.getBoundingClientRect(); if (rect.width > 50 && rect.height > 50) { const el = document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2); if (el && el !== v && !el.closest(CONSTANTS.SELECTORS.DMZ_EXCLUSION)) { addCandidate(el); } } }); root.querySelectorAll( '[aria-label*="play" i], [aria-label*="播放"],[title*="play" i], [title*="播放"]' ).forEach((el) => { if (!el.closest(CONSTANTS.SELECTORS.DMZ_EXCLUSION) && isVisibleElement(el)) { const label = (el.getAttribute('aria-label') || el.getAttribute('title') || '').toLowerCase(); if (SHARED_PATTERNS.NEGATIVE_PLAYBACK_CONTROLS.test(label)) { return; } addCandidate(el); } }); root.querySelectorAll('svg').forEach((svg) => { if (svg.closest(CONSTANTS.SELECTORS.DMZ_EXCLUSION)) { return; } const html = svg.innerHTML.toLowerCase(); if ( html.includes('l11-7') || html.includes('polygon') || /play|bofang/.test(svg.getAttribute('class') || '') ) { const rect = svg.getBoundingClientRect(); if (rect.width >= 15 && rect.height >= 15) { addCandidate(svg.parentElement || svg); } } }); } catch (e) { console.debug('Dynamic button discovery failed:', e); } return Array.from(candidates).filter((el) => isElementInPrimaryMediaRegion(el, root, mediaRect)); }, }; const PlayerBindingRuntimeUtils = { createCleanupBag() { const cleanupFns = []; return { cleanupFns, run() { cleanupFns.splice(0).forEach((fn) => fn()); }, }; }, cancelVideoFrameCallback(video, frameCallbackId) { if (frameCallbackId && typeof video.cancelVideoFrameCallback === 'function') { video.cancelVideoFrameCallback(frameCallbackId); } return null; }, bindVideoErrorOnce(video, cleanupFns, onError) { const handleError = () => { onError(); }; video.addEventListener('error', handleError, { once: true }); cleanupFns.push(() => video.removeEventListener('error', handleError)); }, bindFirstMatchingVideoSignal(video, eventNames, cleanupFns, onSignal, listenerOptions = { once: true }) { let settled = false; const bindings = []; const finalize = () => { if (settled) { return; } settled = true; bindings.forEach(({ eventName, handler, options }) => { video.removeEventListener(eventName, handler, options); }); onSignal(); }; eventNames.forEach((eventName) => { const handler = () => finalize(); const binding = { eventName, handler, options: listenerOptions }; bindings.push(binding); video.addEventListener(eventName, handler, listenerOptions); cleanupFns.push(() => video.removeEventListener(binding.eventName, binding.handler, binding.options)); }); }, startVideoFrameLoop(video, onFrame, storeId, shouldContinue) { if (typeof video.requestVideoFrameCallback !== 'function') { return; } const handleFrame = () => { onFrame(); if (shouldContinue()) { storeId(video.requestVideoFrameCallback(handleFrame)); } }; storeId(video.requestVideoFrameCallback(handleFrame)); }, toggleDocumentPointerEvents(manager, operation, bindings) { bindings.forEach(([eventName, handlerKey]) => { document[operation](eventName, manager[handlerKey], { capture: true }); }); }, }; const RefactorCollectionUtils = { hasOwn(target, key) { return Object.prototype.hasOwnProperty.call(target, key); }, snapshot(items) { return Array.isArray(items) ? [...items] : []; }, freezeCollection(items) { return Object.freeze( items.map((item) => (item && typeof item === 'object' ? Object.freeze({ ...item }) : item)) ); }, }; const EventBusCore = { ensureBucket(listeners, event) { if (!listeners[event]) { listeners[event] = []; } return listeners[event]; }, on(listeners, event, callback) { if (typeof callback !== 'function') { return () => {}; } const bucket = this.ensureBucket(listeners, event); if (!bucket.includes(callback)) { bucket.push(callback); } return () => this.off(listeners, event, callback); }, off(listeners, event, callback) { if (!listeners[event]) { return; } listeners[event] = listeners[event].filter((cb) => cb !== callback); if (listeners[event].length === 0) { delete listeners[event]; } }, emit(listeners, event, data) { if (!listeners[event]) { return; } RefactorCollectionUtils.snapshot(listeners[event]).forEach((cb) => cb(data)); }, }; const EventBus = { listeners: {}, on(event, callback) { return EventBusCore.on(this.listeners, event, callback); }, off(event, callback) { return EventBusCore.off(this.listeners, event, callback); }, emit(event, data) { return EventBusCore.emit(this.listeners, event, data); }, }; const State = { config: {}, }; class BaseModule { constructor(context, name) { this.context = context; this.name = name; } log(msg, type = CONSTANTS.LOG_TYPES.INFO) { this.context.actionLogger.log(msg, type); } init() {} enable() { this.init(); } disable() {} } const PlayerUiDescriptors = { BOTTOM_CONTROL_ITEMS: [ { tag: 'span', attrs: { className: 'dmz-time-display dmz-current-time', textContent: '00:00', }, }, { tag: 'div', attrs: { className: 'dmz-time-separator', textContent: '/', }, }, { tag: 'span', attrs: { className: 'dmz-time-display dmz-total-time', textContent: '00:00', }, }, { tag: 'div', attrs: { className: 'dmz-spacer' }, }, { tag: 'button', attrs: { className: 'dmz-control-button dmz-mute-btn', innerHTML: ICONS.VOLUME_ON + ICONS.VOLUME_OFF, }, }, { tag: 'button', attrs: { className: 'dmz-control-button dmz-fullscreen-btn', innerHTML: ICONS.FS_ENTER + ICONS.FS_EXIT, }, }, ], MORE_MENU_ITEMS: [ { className: 'dmz-menu-item dmz-pip-btn', innerHTML: ICONS.PIP + '画中画', }, { className: 'dmz-menu-item dmz-playback-rate-btn', innerHTML: ICONS.PLAYBACK_RATE + '播放速度正常', }, ], PLAYBACK_RATE_ITEMS: [ { className: 'dmz-menu-item', dataRate: '0.5', textContent: '0.5x', }, { className: 'dmz-menu-item active', dataRate: '1.0', textContent: '正常', }, { className: 'dmz-menu-item', dataRate: '1.5', textContent: '1.5x', }, { className: 'dmz-menu-item', dataRate: '2.0', textContent: '2.0x', }, ], }; const PlayerUiFactoryUtils = { createNodes(descriptors) { return descriptors.map(({ tag, attrs }) => DomUtils.create(tag, attrs)); }, createMenuItem(descriptor) { const attrs = { className: descriptor.className, }; if (descriptor.innerHTML) { attrs.innerHTML = descriptor.innerHTML; } if (descriptor.textContent) { attrs.textContent = descriptor.textContent; } if (descriptor.dataRate) { attrs['data-rate'] = descriptor.dataRate; } return DomUtils.create('div', attrs); }, createMenuItems(descriptors) { return descriptors.map((descriptor) => this.createMenuItem(descriptor)); }, createProgressRail() { return DomUtils.create('div', { className: 'dmz-progress-rail' }, [ DomUtils.create('div', { className: 'dmz-progress-buffer' }), DomUtils.create('div', { className: 'dmz-progress-played' }), DomUtils.create('div', { className: 'dmz-progress-handle' }), ]); }, }; const PlayerDomFactory = { createDragHandleBar() { return DomUtils.create('div', { className: CONSTANTS.CLASSES.DRAG_HANDLE }, [ DomUtils.create('div', { className: `${CONSTANTS.CLASSES.SCREW_EFFECT} left`, }), DomUtils.create('div', { className: CONSTANTS.CLASSES.INDICATOR }), DomUtils.create('div', { className: `${CONSTANTS.CLASSES.SCREW_EFFECT} right`, }), ]); }, createControlsBar(manager) { return DomUtils.create('div', { className: 'dmz-controls-bar' }, [ this.createBottomControls(manager), this.createProgressContainer(), ]); }, createBottomControls(manager) { return DomUtils.create('div', { className: 'dmz-bottom-controls' }, [ ...PlayerUiFactoryUtils.createNodes(PlayerUiDescriptors.BOTTOM_CONTROL_ITEMS), this.createMoreMenuContainer(manager), ]); }, createMoreMenuContainer(manager) { return DomUtils.create('div', { className: 'dmz-more-menu-container' }, [ DomUtils.create('button', { className: 'dmz-control-button dmz-more-btn', innerHTML: ICONS.MORE, }), this.createMoreMenu(manager), ]); }, createMoreMenu(manager) { return DomUtils.create('div', { className: 'dmz-more-menu' }, [ ...PlayerUiFactoryUtils.createMenuItems(PlayerUiDescriptors.MORE_MENU_ITEMS), this.createPlaybackRatesMenu(), ]); }, createPlaybackRatesMenu() { return DomUtils.create( 'div', { className: 'dmz-playback-rates' }, PlayerUiFactoryUtils.createMenuItems(PlayerUiDescriptors.PLAYBACK_RATE_ITEMS) ); }, createProgressContainer() { return DomUtils.create('div', { className: 'dmz-progress-container' }, [ DomUtils.create('div', { className: 'dmz-progress-bar' }, [ PlayerUiFactoryUtils.createProgressRail(), ]), ]); }, createHostElement() { const hostElement = DomUtils.create('div', { id: CONSTANTS.IDS.ROOT, style: { left: '50%', top: '0px', width: '280px', height: '36px', gap: '0px', transform: 'translateX(-50%) translateY(0px)', }, }); hostElement.addEventListener( 'click', (e) => { if (e.target.closest(`.${CONSTANTS.CLASSES.VIDEO_WRAPPER}, .${CONSTANTS.CLASSES.DRAG_HANDLE}`)) { e.stopPropagation(); } }, true ); return hostElement; }, createCloseButton(manager) { const closeButton = DomUtils.create('div', { className: CONSTANTS.CLASSES.CLOSE_BUTTON, textContent: '×', }); const handleClose = (e) => { e.stopPropagation(); e.preventDefault(); manager.context.coreLogic.restorePageToOriginalState(); }; closeButton.addEventListener('touchend', handleClose); closeButton.addEventListener('click', handleClose); return closeButton; }, createPlayerVideoElement(manager) { const savedConfig = manager.context.settingsManager.config; const actualVideo = DomUtils.create('video', { id: CONSTANTS.IDS.PLAYER, playsInline: true, autoplay: savedConfig.autoPlay, preload: 'auto', }); manager.videoElement = actualVideo; actualVideo.playsInline = true; actualVideo.setAttribute('playsinline', ''); actualVideo.setAttribute('webkit-playsinline', ''); actualVideo.setAttribute('x5-playsinline', 'true'); actualVideo.setAttribute('x5-video-player-type', 'h5'); actualVideo.defaultMuted = true; actualVideo.volume = savedConfig.lastVolume; actualVideo.playbackRate = savedConfig.defaultPlaybackRate; this.applySavedMutePreference(manager, actualVideo, savedConfig); return actualVideo; }, applySavedMutePreference(manager, video, savedConfig) { if (savedConfig.isMuted) { video.muted = true; manager.log('应用用户偏好:静音。', CONSTANTS.LOG_TYPES.PLAYER); return; } video.muted = true; video.dataset.dmzStrategicMute = 'true'; manager.log('应用用户偏好:策略性静音已启动。', CONSTANTS.LOG_TYPES.PLAYER); }, createVideoWrapper(manager, closeButton, actualVideo) { const videoWrapper = DomUtils.create('div', { className: CONSTANTS.CLASSES.VIDEO_WRAPPER, innerHTML: `
${ICONS.BRIGHTNESS}
${ICONS.VOLUME_SIDE}
${ICONS.FORWARD}${ICONS.REWIND}
`, }); videoWrapper.prepend(closeButton, actualVideo); videoWrapper.append(this.createControlsBar(manager)); this.bindWrapperActivity(manager, videoWrapper); return videoWrapper; }, bindWrapperActivity(manager, videoWrapper) { videoWrapper.addEventListener('touchstart', () => manager.resetIndicatorTimeout(), { passive: true }); videoWrapper.addEventListener('mousemove', () => manager.resetIndicatorTimeout()); }, createPlayerShell(manager, videoWrapper) { const headerBar = this.createDragHandleBar(); const footerBar = this.createDragHandleBar(); manager.dragHandles = [headerBar, footerBar]; return DomUtils.create('div', {}, [headerBar, videoWrapper, footerBar]); }, }; const CoreStateFactory = { createDefaults() { return { failedCandidateKeys: new Set(), failedRiskyM3u8Patterns: new Set(), lastSuccessfulUrl: null, pageVideoAssociations: new Map(), globalSeenVideoUrls: new Set(), currentDecisionHadMediaElement: false, decisionMade: false, decisionInProgress: false, takeoverCandidates: new Map(), lastCandidatesBackup: new Map(), lastPostedUrl: null, currentRequestId: 0, activeNeutralizers: new Set(), currentPlayingQuality: null, backupBufferTimer: null, isBufferingBackupCandidates: false, bufferedBackupCandidates: new Map(), hiddenIframeElement: null, originalIframeSrc: '', neutralizedMedia: new Set(), enforcerInterval: null, passiveCandidates: new Map(), userIntendsToSwitch: false, userActionTimeout: null, m3u8SessionCache: new Map(), m3u8FetchInflight: new Map(), m3u8ProcessedCache: new Map(), keyBinaryCache: new Map(), keyFetchInflight: new Map(), keyBlobUrlCache: new Map(), currentDecisionSourceName: '', currentDecisionM3u8Text: null, pendingSpaCandidate: null, pendingSpaCandidates: [], pendingSpaTimer: null, pendingRecoveryTimer: null, lastSpaNavigationAt: 0, lastSpaNavigationSignature: '', lastSpaNavigationWasColdStart: false, mediaQueryCache: { stamp: 0, result: [], }, }; }, }; const CoreEnforcer = { start(core) { if (core.enforcerInterval) { return; } if (!core._lastEnforcerStartAt || Date.now() - core._lastEnforcerStartAt > 1500) { core.log('启动【持久化压制器】,确保原生播放器静默。', CONSTANTS.LOG_TYPES.CORE_NEUTRALIZE); } core._lastEnforcerStartAt = Date.now(); core.enforcerInterval = Utils.startPollingTask( 1000, () => { const isPlaying = core.context.playerManager.isPlayerActiveOrInitializing; if (!isPlaying && core.neutralizedMedia.size === 0) { this.stop(core); return false; } core.neutralizedMedia.forEach((media) => { if (!document.body.contains(media)) { core.neutralizedMedia.delete(media); } else if (media.paused === false || media.muted === false) { media.pause(); media.muted = true; } }); if (CONSTANTS.IS_TOP_FRAME && isPlaying) { core.context.playerManager.broadcastPauseToFrames(); } return true; }, 'Persistent enforcer failed:' ); }, stop(core) { if (!core.enforcerInterval) { return; } if (!core._lastEnforcerStopAt || Date.now() - core._lastEnforcerStopAt > 1500) { core.log('停止【持久化压制器】。', CONSTANTS.LOG_TYPES.CORE_NEUTRALIZE); } core._lastEnforcerStopAt = Date.now(); clearInterval(core.enforcerInterval); core.enforcerInterval = null; core.neutralizedMedia.clear(); }, }; const CoreRemoteTrigger = { prepareRuntime(core, domScanner, mode = 'switch') { core.isSystemHalted = false; core.setUserIntentToSwitch(mode); domScanner.isStopped = false; }, tryPromoteCurrentFrameUrl(core) { const directUrl = core._extractUrlFromParameter(window.location.href); if (!(directUrl && directUrl !== window.location.href && /^https?:\/\//.test(directUrl))) { return false; } if (!/(?:\.m3u8|\.mp4|\.m3u|\.flv|\.mpd)(?:[?#]|$)|\/(?:hls|play|vod|stream|video)\//i.test(directUrl)) { return false; } core.log( `[跨域交互]|☎️|检测到当前iFrame地址内已携带直连参数,优先上报:${directUrl.slice(-60)}`, CONSTANTS.LOG_TYPES.CORE_EXEC ); core.addTakeoverCandidate({ url: directUrl, sourceName: '跨域遥控触发(URL参数直连)', }); return true; }, tryReplayMemory(core) { if (!(core.lastSuccessfulUrl && core.lastSuccessfulUrl.startsWith('http'))) { return false; } core.log( `[记忆回溯]|🌀|远程触发:优先复用历史播放记录:${core.lastSuccessfulUrl.slice(-50)}。`, CONSTANTS.LOG_TYPES.CORE_EXEC ); core.addTakeoverCandidate({ url: core.lastSuccessfulUrl, sourceName: '跨域遥控触发器(顶层记忆重播)', }); return true; }, hideSwitchOverlays() { document.querySelectorAll('.dmz-switch-overlay').forEach((element) => { element.style.display = 'none'; }); }, resolveMediaUrl(core, media) { let targetUrl = media.src || media.querySelector('source')?.src || media.dataset.dmzOriginalSrc; if ((!targetUrl || targetUrl.startsWith('blob:')) && core.lastSuccessfulUrl) { targetUrl = core.lastSuccessfulUrl; } return targetUrl; }, forceSignalGeneration(media) { media.dataset.dmzAllowSignalGeneration = 'true'; try { media.currentTime = 0; media.play(); } catch (e) { console.debug('Failed to force play media:', e); } setTimeout(() => { delete media.dataset.dmzAllowSignalGeneration; media.pause(); }, 1500); }, handleMedia(core, media) { delete media.dataset.dmzSourceLocked; const targetUrl = this.resolveMediaUrl(core, media); if (targetUrl && targetUrl.startsWith('http')) { core.addTakeoverCandidate({ url: targetUrl, sourceName: '跨域遥控触发', }); return; } this.forceSignalGeneration(media); }, handleClick(core) { const { domScanner } = core.context; core.log(`[跨域交互]|☎️|收到来自顶层的激活指令,开始执行内部唤醒程序...`, CONSTANTS.LOG_TYPES.CORE_EXEC); this.prepareRuntime(core, domScanner, 'replay'); if (this.tryPromoteCurrentFrameUrl(core)) { return; } if (this.tryReplayMemory(core)) { return; } this.hideSwitchOverlays(); PageScanner.scanForFlashvars(core.context); PageScanner.scanForEmbeddedData(core.context); core.neutralizedMedia.forEach((media) => this.handleMedia(core, media)); domScanner.scanPage(); }, }; class GMFragmentLoader extends CONSTANTS.Hls.DefaultConfig.loader { constructor(config) { super(config); } load(context, config, callbacks) { if (typeof GM_xmlhttpRequest === 'undefined' || context.url.startsWith('blob:') || context.url.startsWith('data:')) { return super.load(context, config, callbacks); } this.context = context; this.callbacks = callbacks; this.stats = { trequest: performance.now(), tfirst: 0, tload: 0, loaded: 0, total: 0 }; this.request = GM_xmlhttpRequest({ method: 'GET', url: context.url, responseType: context.responseType || 'arraybuffer', timeout: config.timeout || 20000, headers: { 'Referer': window.location.href, 'Accept': '*/*' }, onload: (res) => { this.stats.tfirst = Math.max(this.stats.trequest, performance.now()); this.stats.tload = Math.max(this.stats.tfirst, performance.now()); if (res.status >= 200 && res.status < 300) { let data = res.response; if (context.responseType !== 'arraybuffer') { data = res.responseText || data; } this.stats.loaded = data ? (data.byteLength || data.length || 0) : 0; this.stats.total = this.stats.loaded; callbacks.onSuccess({ url: res.finalUrl || context.url, data: data }, this.stats, context, res); } else { callbacks.onError({ code: res.status, text: res.statusText }, context, res); } }, onerror: (err) => callbacks.onError({ code: 0, text: err.error || 'Network Error' }, context, null), ontimeout: () => callbacks.onTimeout(this.stats, context, null), onabort: () => callbacks.onAbort && callbacks.onAbort(this.stats, context, null) }); } abort() { if (this.request && typeof this.request.abort === 'function') { this.request.abort(); } else { super.abort(); } } destroy() { this.abort(); super.destroy(); } } const PlayerHlsController = { attachPlayback(manager, videoElement, isFirstPlay) { if (!CONSTANTS.Hls?.isSupported()) { manager.log('HLS.js 不支持M3U8播放。', CONSTANTS.LOG_TYPES.ERROR); manager.cleanup(); return; } manager.currentPlaybackEngine = 'HLS.js (v1.5.20 Optimized)'; const hls = new CONSTANTS.Hls(this.getConfig()); manager.hlsInstance = hls; hls.attachMedia(videoElement); this.bindEvents(manager, hls, videoElement, isFirstPlay); }, bindEvents(manager, hls, videoElement, isFirstPlay) { const { diagnosticsTool } = manager.context; const hlsState = { firstFragStart: 0, consecutiveNetworkErrors: 0 }; hls.on(CONSTANTS.Hls.Events.MEDIA_ATTACHED, () => { this.handleMediaAttached(manager, hls); }); hls.on(CONSTANTS.Hls.Events.MANIFEST_PARSED, (event, data) => { this.handleManifestParsed(manager, hls, videoElement, isFirstPlay, data, hlsState); }); hls.on(CONSTANTS.Hls.Events.FRAG_PARSED, (event, data) => { this.handleFragmentParsed(manager, hls, data); }); hls.on(CONSTANTS.Hls.Events.LEVEL_UPDATED, (event, data) => { this.handleLevelUpdated(manager, hls, data); }); hls.on(CONSTANTS.Hls.Events.FRAG_LOADING, () => { if (!hlsState.firstFragStart) { hlsState.firstFragStart = performance.now(); } }); hls.on(CONSTANTS.Hls.Events.FRAG_LOADED, (event, data) => { this.handleFragmentLoaded(manager, data, hlsState); }); hls.on(CONSTANTS.Hls.Events.KEY_LOADED, () => { diagnosticsTool.playbackHealth.key.status = 'success'; }); hls.on(CONSTANTS.Hls.Events.ERROR, (event, data) => { this.handleError(manager, hls, data, hlsState); }); }, handleMediaAttached(manager, hls) { manager.context.coreLogic.sandboxManager.cleanup(); manager.log('媒体元素已绑定,立即加载数据源...', CONSTANTS.LOG_TYPES.HLS); hls.loadSource(manager.m3u8BlobUrl); }, capturePredefinedResolution(manager, source, width, height) { if (manager.currentVideoResolution || manager.predefinedResolution || !width || !height) { return; } manager.predefinedResolution = { width, height }; manager.log( `⚡️ [HLS优化] 通过${source} 获取真实尺寸:${width}x${height},立即渲染。`, CONSTANTS.LOG_TYPES.PLAYER_REVEAL ); }, handleManifestParsed(manager, hls, videoElement, isFirstPlay, data, hlsState) { const { diagnosticsTool } = manager.context; this.applyBestManifestLevel(manager, hls, data.levels); manager.log(`清单解析完成,发现${data.levels.length}个清晰度,开始缓冲...`, CONSTANTS.LOG_TYPES.HLS); diagnosticsTool.playbackHealth.manifest.status = 'success'; hlsState.consecutiveNetworkErrors = 0; this.autoplayAfterManifest(manager, videoElement, isFirstPlay); }, applyBestManifestLevel(manager, hls, levels) { if (!(levels && levels.length > 0)) { return; } const bestChoice = this.getBestLevelChoice(levels); hls.startLevel = bestChoice.index; hls.nextLevel = bestChoice.index; const levelInfo = this.extractLevelInfo(bestChoice.level); this.captureManifestResolution(manager, levelInfo.width, levelInfo.height); manager.log( `强制高分辨率|等级Lv${bestChoice.index}:${this.formatLevelInfo(levelInfo)}`, CONSTANTS.LOG_TYPES.HLS ); }, extractLevelInfo(bestLevel) { const attrs = bestLevel.attrs || {}; return { width: bestLevel.width || (attrs.RESOLUTION ? attrs.RESOLUTION.width : 0), height: bestLevel.height || (attrs.RESOLUTION ? attrs.RESOLUTION.height : 0), bitrate: bestLevel.bitrate || attrs.BANDWIDTH || 0, }; }, captureManifestResolution(manager, width, height) { if (width && height) { this.capturePredefinedResolution(manager, 'MANIFEST_PARSED', width, height); } }, formatLevelInfo(levelInfo) { if (levelInfo.width && levelInfo.height) { return `${levelInfo.width}x${levelInfo.height} | ${Math.round(levelInfo.bitrate / 1024)}kbps`; } return `${Math.round(levelInfo.bitrate / 1024)}kbps`; }, autoplayAfterManifest(manager, videoElement, isFirstPlay) { const { settingsManager } = manager.context; if (!(isFirstPlay || settingsManager.config.autoPlay)) { return; } const playPromise = videoElement.play(); if (playPromise !== undefined) { playPromise.catch(() => {}); } }, getBestLevelChoice(levels) { return levels .map((level, index) => ({ level, index })) .sort((a, b) => { const areaA = (a.level.width || 0) * (a.level.height || 0); const areaB = (b.level.width || 0) * (b.level.height || 0); if (areaA !== areaB) { return areaB - areaA; } return (b.level.bitrate || 0) - (a.level.bitrate || 0); })[0]; }, handleFragmentParsed(manager, hls, data) { const { diagnosticsTool } = manager.context; if (diagnosticsTool.playbackHealth.segments.status !== 'success') { diagnosticsTool.playbackHealth.segments.status = 'success'; } if (data.frag && hls.levels[data.frag.level]) { const level = hls.levels[data.frag.level]; this.capturePredefinedResolution(manager, 'FRAG_PARSED', level.width, level.height); } }, handleLevelUpdated(manager, hls, data) { if (data.details && hls.levels[data.level]) { const level = hls.levels[data.level]; this.capturePredefinedResolution(manager, 'LEVEL_UPDATED', level.width, level.height); } }, handleFragmentLoaded(manager, data, hlsState) { const { diagnosticsTool } = manager.context; if (diagnosticsTool.playbackHealth.segments.status !== 'success') { const now = performance.now(); const manualTime = hlsState.firstFragStart ? now - hlsState.firstFragStart : 0; const stats = data.stats || {}; const fragStats = (data.frag ? data.frag.stats : {}) || {}; const bytes = (data.payload ? data.payload.byteLength || data.payload.length : 0) || stats.loaded || stats.total || fragStats.loaded || fragStats.total || 0; const timeStr = manualTime > 1 ? manualTime.toFixed(0) : bytes > 0 ? '<1' : '0(缓存)'; const size = bytes > 0 ? (bytes < 1024 ? bytes + 'B' : (bytes / 1024).toFixed(1) + 'KB') : '缓存命中'; const timeDisplay = manualTime > 1000 ? `${(manualTime / 1000).toFixed(1)}s` : `${timeStr}ms`; manager.log(`首个分片下载成功(${size}),耗时${timeDisplay}。`, CONSTANTS.LOG_TYPES.HLS); } diagnosticsTool.playbackHealth.segments.status = 'success'; hlsState.consecutiveNetworkErrors = 0; }, handleError(manager, hls, data, hlsState) { if (!data.fatal) { return; } if (this.handleNetworkError(manager, hls, data, hlsState)) { return; } if (this.handleMediaError(manager, hls, data)) { return; } this.handleFatalError(manager, hls, data); }, handleNetworkError(manager, hls, data, hlsState) { if (data.type !== CONSTANTS.Hls.ErrorTypes.NETWORK_ERROR) { return false; } hlsState.consecutiveNetworkErrors++; manager.log(`⚠️HLS 网络波动:${data.details}`, CONSTANTS.LOG_TYPES.WARN); if (hlsState.consecutiveNetworkErrors <= 3) { hls.startLoad(); } else { manager.log(`❌ HLS 网络错误重试上限,停止播放。`, CONSTANTS.LOG_TYPES.ERROR); } return true; }, handleMediaError(manager, hls, data) { if (data.type !== CONSTANTS.Hls.ErrorTypes.MEDIA_ERROR) { return false; } manager.log('HLS 媒体解码异常,尝试恢复...', CONSTANTS.LOG_TYPES.HLS); hls.recoverMediaError(); return true; }, handleFatalError(manager, hls, data) { manager.log(`HLS致命错误:${data.type}`, CONSTANTS.LOG_TYPES.ERROR); hls.destroy(); manager.cleanup(); }, getConfig() { return { ...this.getBufferConfig(), ...this.getLoadingConfig(), ...this.getCodecConfig(), }; }, getBufferConfig() { return { debug: false, enableWorker: true, lowLatencyMode: false, backBufferLength: 90, maxBufferLength: 60, maxMaxBufferLength: 600, maxBufferSize: 120 * 1024 * 1024, maxBufferHole: 2.5, maxSeekHole: 2.0, }; }, getLoadingConfig() { return { startFragPrefetch: true, maxLoadingDelay: 1, minAutoBitrate: 0, manifestLoadingTimeOut: 15000, manifestLoadingMaxRetry: 3, manifestLoadingRetryDelay: 500, fragLoadingTimeOut: 25000, fragLoadingMaxRetry: 5, levelLoadingTimeOut: 15000, fLoader: GMFragmentLoader, keyLoader: GMFragmentLoader, }; }, getCodecConfig() { return { defaultAudioCodec: 'mp4a.40.2', enableSoftwareAES: true, capLevelToPlayerSize: false, }; }, }; const PlayerCleanupDescriptors = { PLAYER_UI_REFERENCE_KEYS: [ 'hostElement', 'shadowRoot', 'videoElement', 'draggableInstance', 'fullscreenGestureHandler', 'customControlsHandler', 'fullscreenHandler', ], PLAYER_RUNTIME_RESET_MAP: { isPlayerActiveOrInitializing: false, currentVideoType: null, currentVideoUrl: null, currentVideoFormat: null, currentPlaybackEngine: null, lastM3u8Content: '', currentRequestUrl: null, }, HIDDEN_IFRAME_STYLE_KEYS: [ ['visibility', 'originalIframeVisibilityStyle'], ['pointerEvents', 'originalIframePointerEventsStyle'], ], }; const PlayerCleanupFactoryUtils = { clearStreamingTimers(manager) { manager.indicatorHideTimeout = PlayerBindingHelpers.clearTimer(manager.indicatorHideTimeout); }, destroyStreamingArtifacts(manager) { if (manager.hlsInstance) { manager.hlsInstance.detachMedia(); manager.hlsInstance.destroy(); } if (manager.m3u8BlobUrl) { URL.revokeObjectURL(manager.m3u8BlobUrl); manager.m3u8BlobUrl = null; } }, resetUiReferences(manager) { PlayerCleanupDescriptors.PLAYER_UI_REFERENCE_KEYS.forEach((key) => { manager[key] = null; }); }, resetRuntimeState(manager) { Object.entries(PlayerCleanupDescriptors.PLAYER_RUNTIME_RESET_MAP).forEach(([key, value]) => { manager[key] = value; }); }, restoreHiddenIframeStyles(coreLogic) { PlayerCleanupDescriptors.HIDDEN_IFRAME_STYLE_KEYS.forEach(([styleKey, snapshotKey]) => { coreLogic.hiddenIframeElement.style[styleKey] = coreLogic[snapshotKey]; coreLogic[snapshotKey] = ''; }); }, }; const PlayerCleanupManager = { cleanup(manager, isInternalCall = false) { const { diagnosticsTool, coreLogic } = manager.context; manager.log(`开始清理播放器... (内部调用:${isInternalCall})。`, CONSTANTS.LOG_TYPES.PLAYER); this.prepare(manager); this.runSteps(manager, isInternalCall, diagnosticsTool, coreLogic); manager.isInternalRequest = false; }, prepare(manager) { window.removeEventListener('orientationchange', manager.boundHandleOrientationChange); window.removeEventListener('resize', manager.boundHandleViewportChange); window.visualViewport?.removeEventListener('resize', manager.boundHandleViewportChange); manager.originalPlayerStyle = null; manager.currentVideoResolution = null; manager.viewportClampTaskToken += 1; manager.isUsingSavedPlayerPosition = false; }, runSteps(manager, isInternalCall, diagnosticsTool, coreLogic) { this.restoreRemoteTriggers(); this.stopPersistentEnforcerIfNeeded(isInternalCall, coreLogic); this.cleanupStreamingResources(manager); this.cleanupPlayerUi(manager); if (!isInternalCall) { this.resetPlaybackContext(manager, diagnosticsTool, coreLogic); } }, stopPersistentEnforcerIfNeeded(isInternalCall, coreLogic) { if (!isInternalCall) { coreLogic.stopPersistentEnforcer(); } }, restoreRemoteTriggers() { document.querySelectorAll('.dmz-iframe-remote-trigger').forEach((el) => { el.style.display = 'flex'; if (el.dmzUpdatePos) { el.dmzUpdatePos(); } }); }, cleanupStreamingResources(manager) { try { PlayerCleanupFactoryUtils.clearStreamingTimers(manager); PlayerCleanupFactoryUtils.destroyStreamingArtifacts(manager); } catch (e) { manager.log(`清理HLS或Blob时发生错误:${e.message}`, CONSTANTS.LOG_TYPES.ERROR); } finally { manager.hlsInstance = null; manager.indicatorHideTimeout = null; } }, cleanupPlayerUi(manager) { if (!manager.hostElement) { return; } try { this.cleanupVideoElementNode(manager); this.destroyPlayerUiModules(manager); manager.hostElement.remove(); } catch (e) { manager.log(`清理播放器UI模块时发生错误:${e.message}`, CONSTANTS.LOG_TYPES.ERROR); } finally { this.resetPlayerUiReferences(manager); } }, cleanupVideoElementNode(manager) { if (!manager.videoElement) { return; } manager.videoElement.pause(); manager.videoElement.removeAttribute('src'); manager.videoElement.load(); }, destroyPlayerUiModules(manager) { manager.draggableInstance?.destroy(); manager.fullscreenGestureHandler?.deactivate(); manager.customControlsHandler?.destroy(); manager.fullscreenHandler?.destroy(); }, resetPlayerUiReferences(manager) { PlayerCleanupFactoryUtils.resetUiReferences(manager); }, resetPlaybackContext(manager, diagnosticsTool, coreLogic) { try { this.resetBufferedBackupCandidates(coreLogic); this.resetDiagnosticsState(diagnosticsTool); } catch (e) { manager.log(`清理核心状态时发生错误:${e.message}`, CONSTANTS.LOG_TYPES.ERROR); } finally { this.resetPlayerRuntimeState(manager, coreLogic); } }, resetBufferedBackupCandidates(coreLogic) { if (coreLogic.backupBufferTimer) { clearTimeout(coreLogic.backupBufferTimer); coreLogic.backupBufferTimer = null; } coreLogic.isBufferingBackupCandidates = false; coreLogic.bufferedBackupCandidates.clear(); }, resetDiagnosticsState(diagnosticsTool) { diagnosticsTool.lastProcessedM3u8 = null; diagnosticsTool.resetPlaybackHealth(); diagnosticsTool.resetSlicingReport(); diagnosticsTool.takeoverEvidence = { sources: [], url: null }; }, resetPlayerRuntimeState(manager, coreLogic) { PlayerCleanupFactoryUtils.resetRuntimeState(manager); manager.log('播放器状态锁已释放', CONSTANTS.LOG_TYPES.PLAYER_LOCK); this.restoreHiddenIframeState(manager, coreLogic); }, restoreHiddenIframeState(manager, coreLogic) { if (!coreLogic.hiddenIframeElement) { return; } manager.log(`检测到被隐藏的Iframe,开始恢复并重新加载...`, CONSTANTS.LOG_TYPES.LIFECYCLE); if (coreLogic.originalIframeSrc) { coreLogic.hiddenIframeElement.src = coreLogic.originalIframeSrc; } PlayerCleanupFactoryUtils.restoreHiddenIframeStyles(coreLogic); coreLogic.hiddenIframeElement = null; coreLogic.originalIframeSrc = ''; }, }; const CoreRestoreManager = { restorePageToOriginalState(core) { if (core.isRestoreInProgress) { return; } this.beginRestoreSequence(core); this.cleanupRestoreInfrastructure(core); this.restoreInteractiveState(core); this.finalizeRestoreSequence(core); }, beginRestoreSequence(core) { core.isRestoreInProgress = true; core.isSystemHalted = true; core.context.playerManager.cleanup(); core.log( `[系统熔断]|🚨|收到关闭指令(${CONSTANTS.IS_TOP_FRAME ? '顶层' : '子框架'}),执行深度清理...`, CONSTANTS.LOG_TYPES.LIFECYCLE ); this.restoreFrames(core); core.context.domScanner.stop(); }, cleanupRestoreInfrastructure(core) { this.disconnectDomScanner(core); this.stopIframeTerminator(core); core.stopPersistentEnforcer(); }, disconnectDomScanner(core) { if (core.context.domScanner.observer) { core.context.domScanner.observer.disconnect(); } if (core.context.domScanner.heartbeatTimer) { clearInterval(core.context.domScanner.heartbeatTimer); } }, stopIframeTerminator(core) { if (core.context.iframeTerminator) { try { core.context.iframeTerminator.stop(); } catch (e) { console.debug('Failed to stop iframe terminator:', e); } } }, restoreInteractiveState(core) { this.restoreDom(); this.restoreMedia(core); this.clearNeutralizers(core); this.disableHookModules(core); this.restoreHiddenIframeElement(core); core.sandboxManager.cleanup(); }, clearNeutralizers(core) { core.neutralizedMedia.clear(); core.activeNeutralizers.forEach((obs) => obs.disconnect()); core.activeNeutralizers.clear(); }, disableHookModules(core) { [NetworkInterceptor, PlayerHooker, DecryptionHooker, JsonInspector, SetAttributeHooker].forEach((mod) => { if (mod && mod.isActive) { mod.disable(core.context); } }); }, restoreHiddenIframeElement(core) { if (core.hiddenIframeElement) { core.hiddenIframeElement.style.visibility = core.originalIframeVisibilityStyle || 'visible'; core.hiddenIframeElement.style.pointerEvents = core.originalIframePointerEventsStyle || 'auto'; if (core.hiddenIframeElement.src === 'about:blank' && core.originalIframeSrc) { core.hiddenIframeElement.src = core.originalIframeSrc; } core.hiddenIframeElement = null; } }, finalizeRestoreSequence(core) { if (CONSTANTS.IS_TOP_FRAME) { core.log('✅原生环境已恢复(Top)。', CONSTANTS.LOG_TYPES.LIFECYCLE); core.context.frameCommunicator.showNotification('沉浸模式已退出,所有框架已恢复原生状态。'); } core.isRestoreInProgress = false; }, restoreFrames(core) { this.restoreWindowFrames(); this.restoreIframeElements(); }, restoreWindowFrames() { try { const frames = window.frames; for (let i = 0; i < frames.length; i++) { this.postRestoreToFrame(frames[i], 'Failed to restore child frame:'); } } catch (e) { console.debug('Failed to access window frames:', e); } }, restoreIframeElements() { document.querySelectorAll('iframe').forEach((iframe) => { if (iframe.contentWindow) { this.postRestoreToFrame(iframe.contentWindow, 'Failed to restore iframe contentWindow:'); } this.restoreIframeVisualState(iframe); this.restoreIframeSrc(iframe); }); }, postRestoreToFrame(targetWindow, errorPrefix) { try { targetWindow.postMessage({ type: CONSTANTS.MESSAGE_TYPES.RESTORE_COMMAND }, '*'); } catch (e) { console.debug(errorPrefix, e); } }, restoreIframeVisualState(iframe) { iframe.style.removeProperty('visibility'); iframe.style.removeProperty('opacity'); iframe.style.removeProperty('pointer-events'); }, restoreIframeSrc(iframe) { if (iframe.src !== 'about:blank' || !iframe.dataset.dmzOriginalSrc) { return; } iframe.src = iframe.dataset.dmzOriginalSrc; delete iframe.dataset.dmzOriginalSrc; }, restoreDom() { const safeRemove = (el) => { if (el) { try { if (el.cleanup) { el.cleanup(); } else { el.remove(); } } catch (e) { console.debug('Failed to clean up element:', e); } } }; document.querySelectorAll('.dmz-switch-overlay, .dmz-iframe-remote-trigger').forEach(safeRemove); document .querySelectorAll('iframe[data-dmz-has-trigger]') .forEach((iframe) => delete iframe.dataset.dmzHasTrigger); }, removeRestoreOverlays(media) { if (media.dmzOverlay) { media.dmzOverlay.remove(); media.dmzOverlay = null; } if (!media.parentElement) { return; } const overlay = media.parentElement.querySelector('.dmz-switch-overlay'); if (overlay) { overlay.remove(); } }, restoreMediaPlaybackMethods(media) { try { if (media.play.toString().indexOf('native') === -1) { delete media.play; } if (media.pause.toString().indexOf('native') === -1) { delete media.pause; } } catch (e) {} }, removePreventPlayGuards(media) { if (!media._dmzPreventPlay) { return; } media.removeEventListener('play', media._dmzPreventPlay, true); media.removeEventListener('playing', media._dmzPreventPlay, true); delete media._dmzPreventPlay; }, clearNeutralizedMediaState(media) { media.removeAttribute('data-dmz-neutralized'); delete media.dataset.dmzOriginalSrc; delete media.dataset.dmzSourceLocked; delete media.dataset.dmzSentinelActive; }, restoreMediaPresentation(media) { Object.assign(media.style, { visibility: '', opacity: '', pointerEvents: '', }); media.muted = false; media.controls = true; }, restoreMediaVolume(media) { if (media.dataset.dmzOriginalVolume) { media.volume = parseFloat(media.dataset.dmzOriginalVolume); delete media.dataset.dmzOriginalVolume; return; } media.volume = 1.0; }, resumeRestoredMedia(media) { if (media.paused) { media.play().catch(() => {}); } }, restoreMediaParentContainer(media) { if (media.parentElement && media.parentElement.style.position === 'relative') { media.parentElement.style.position = ''; } }, restoreSingleMedia(media) { this.removeRestoreOverlays(media); media.dataset.dmzAllowSignalGeneration = 'true'; this.restoreMediaPlaybackMethods(media); this.removePreventPlayGuards(media); this.clearNeutralizedMediaState(media); this.restoreMediaPresentation(media); this.restoreMediaVolume(media); this.resumeRestoredMedia(media); this.restoreMediaParentContainer(media); }, restoreMedia(core) { const allMedia = core.findAllVideosAndAudioInPage(); new Set([...core.neutralizedMedia, ...allMedia]).forEach((media) => { try { this.restoreSingleMedia(media); } catch (e) { console.debug('Failed to restore media:', e); } }); }, }; const CoreQualityManager = { evaluateQualityOverride(core, candidateInfo) { const { playerManager } = core.context; const currentUrl = playerManager.currentVideoUrl || (core.decisionInProgress ? core.lastSuccessfulUrl : ''); const newUrl = candidateInfo.url || ''; if (!currentUrl || !newUrl || currentUrl === newUrl) { return false; } const currentHadMediaElement = core.currentDecisionHadMediaElement; const newHasMediaElement = !!candidateInfo.mediaElement; const qualityContext = this.buildQualityOverrideContext(core, candidateInfo, currentUrl); if (newHasMediaElement && !currentHadMediaElement) { if (this.shouldGrantDomEntitySovereignty(core, candidateInfo, playerManager, qualityContext)) { core.log(`[DOM实体主权]|👑|确认当前为虚空/低置信接管,实体媒体夺回播放权。`, CONSTANTS.LOG_TYPES.CORE_EXEC); this.executeQualityOverrideTakeover(core, candidateInfo, playerManager); return true; } core.log( `[DOM实体主权]|🛡️|检测到当前已存在高置信/同家族候选,实体媒体降级为备份评估。`, CONSTANTS.LOG_TYPES.INFO ); } if (this.shouldLockSameFamilyQuality(core, candidateInfo, playerManager, qualityContext)) { return true; } if (this.trySeamlessCdnUpgrade(core, candidateInfo, playerManager, qualityContext)) { return true; } if (currentUrl.startsWith('http') && newUrl.startsWith('http')) { const currentSegments = this.extractComparableUrlSegments(currentUrl); const nextSegments = this.extractComparableUrlSegments(newUrl); if (currentSegments.length > 0 && nextSegments.length > 0) { const hasStrongAffinity = this.hasStrongSegmentAffinity(currentSegments, nextSegments); if (!hasStrongAffinity) { const isTrustOverride = qualityContext.newReliabilityScore >= qualityContext.currentReliabilityScore + 4 || qualityContext.compositeNewScore >= qualityContext.compositeCurrentScore + 3; if (!isTrustOverride) { core.log( `[信号隔离]|🚧|路径亲缘不足且质量优势不明显,拒绝跨源替换 ` + `(可靠:${qualityContext.newReliabilityScore} vs ${qualityContext.currentReliabilityScore} / ` + `综合:${qualityContext.compositeNewScore.toFixed(1)} vs ${qualityContext.compositeCurrentScore.toFixed(1)})。`, CONSTANTS.LOG_TYPES.TAKEOVER_ATTEMPT ); return false; } else { core.log( `[信号破壁]|🔥|路径亲缘不足,但新信号具备明显质量优势,强制放行!`, CONSTANTS.LOG_TYPES.TAKEOVER_ATTEMPT ); } } else { core.log( `[信号亲缘]|🧬|检测到新旧资源存在强关联,允许继续评估升级。`, CONSTANTS.LOG_TYPES.TAKEOVER_ATTEMPT ); } } } if (!this.shouldUpgradeHiddenPlayer(playerManager, qualityContext)) { return false; } core.log( `[候选快切]|⚡️|检测到更优或更稳信号(综合:${qualityContext.compositeNewScore.toFixed(1)} vs ${qualityContext.compositeCurrentScore.toFixed(1)}),执行抢断!`, CONSTANTS.LOG_TYPES.CORE_EXEC ); this.stashQualityFallbackCandidate(core, currentUrl, qualityContext.currentScore); this.executeQualityOverrideTakeover(core, candidateInfo, playerManager); return true; }, buildQualityOverrideContext(core, candidateInfo, currentUrl) { const { playerManager } = core.context; const targetUrl = core.decisionInProgress && core.lastSuccessfulUrl ? core.lastSuccessfulUrl : currentUrl; const isNewM3U8 = Utils.isM3U8(candidateInfo.url) || (candidateInfo.m3u8Text && candidateInfo.m3u8Text.includes('#EXTM3U')); const isCurrentM3U8 = playerManager.currentVideoFormat?.name === 'M3U8' || (core.lastSuccessfulUrl && Utils.isM3U8(core.lastSuccessfulUrl)); const targetIsM3 = core.decisionInProgress ? Utils.isM3U8(targetUrl) : isCurrentM3U8; const targetCandidate = { url: targetUrl, sourceName: core.currentDecisionSourceName || '', m3u8Text: core.currentDecisionM3u8Text || null, }; const newScore = this.getQualityScore(candidateInfo.url || '', isNewM3U8); const currentScore = this.getQualityScore(targetUrl, targetIsM3); const newReliabilityScore = this.getCandidateReliabilityScore(core, candidateInfo); const currentReliabilityScore = this.getCandidateReliabilityScore(core, targetCandidate); const newResolutionRank = this.getResolutionRank(candidateInfo.url || ''); const currentResolutionRank = this.getResolutionRank(targetUrl); return { targetUrl, targetIsM3, targetCandidate, newScore, currentScore, newReliabilityScore, currentReliabilityScore, compositeNewScore: newScore + newReliabilityScore, compositeCurrentScore: currentScore + currentReliabilityScore, newResolutionRank, currentResolutionRank, sameFamily: this.isSameSignalFamily(targetUrl, candidateInfo.url || ''), }; }, getQualityScore(url, isM3) { let score = 0; if ( /(\/|_|-)(240|360|480)p(\.h264|\.av1)?\.mp4\.m3u8(\?|$)/i.test(url) || /(\/|_|-)(240|360)[pP](\?|$)/.test(url) ) { score -= 50; } else { if (/4k|2160[pP]|3840[-x]2160/i.test(url)) { score += 10; } else if (/1080[pP]|1920[-x]1080/i.test(url)) { score += 8; } else if (/720[pP]|1280[-x]720/i.test(url)) { score += 6; } else if (/480[pP]|854[-x]480/i.test(url)) { score += 2; } } if (/master\.m3u8|playlist\.m3u8/i.test(url) || (isM3 && !/(240|360|480|720|1080|2160)p/i.test(url))) { score += 5; } return isM3 ? score + 2.5 : score; }, getCandidateReliabilityScore(core, candidateInfo = {}) { const url = candidateInfo.url || ''; const sourceName = candidateInfo.sourceName || ''; const hasInlineM3u8Text = typeof candidateInfo.m3u8Text === 'string' && candidateInfo.m3u8Text.includes('#EXTM3U'); let score = 0; if (!url) { return score; } if (hasInlineM3u8Text) { score += 30; } if (Utils.isM3U8(url)) { if (/[?&](sign|token|auth|expires|t)=/i.test(url)) { score += 18; } if (/master\.m3u8|playlist\.m3u8/i.test(url)) { score += 6; } if (this.isRiskyUnsignedM3u8Candidate(core, candidateInfo)) { score -= 24; } if (this.hasKnown403Risk(core, url)) { score -= 30; } } else if (/\.(mp4|webm|ogg|mov|mkv)(\?|$)/i.test(url)) { score += 12; } if (/网络拦截|XHR|Fetch|原生播放器触发器\(直连\)|跨域遥控触发|历史复用|PlayerHooker/i.test(sourceName)) { score += 8; } if (/JSON|DECRYPTION|ATTR/.test(sourceName) && !hasInlineM3u8Text && Utils.isM3U8(url)) { score -= 6; } if (/(freepv|litevideo|sample|preview|trailer|teaser|yugao|pianhua|ad_url)/i.test(url)) { score -= 200; } if (/360p|480p|sd/i.test(url)) { score -= 10; } return score; }, getResolutionRank(url = '') { const normalized = String(url || '').toLowerCase(); if (/4k|2160[pP]|3840[-x]2160/i.test(normalized)) { return 6; } if (/1440[pP]|2560[-x]1440/i.test(normalized)) { return 5; } if (/1080[pP]|1920[-x]1080/i.test(normalized)) { return 4; } if (/720[pP]|1280[-x]720/i.test(normalized)) { return 3; } if (/480[pP]|854[-x]480/i.test(normalized)) { return 2; } if (/360[pP]|640[-x]360/i.test(normalized)) { return 1; } if (/240[pP]|426[-x]240/i.test(normalized)) { return 0; } return -1; }, getSignalFamilyHostKey(hostname = '') { const normalizedHost = String(hostname || '').toLowerCase(); if (/\.phncdn\.com$/.test(normalizedHost)) { return 'phncdn.com'; } return normalizedHost; }, getSignalFamilyKey(url) { if (typeof url !== 'string' || !url) { return null; } try { const urlObj = new URL(url, window.location.href); const hostKey = this.getSignalFamilyHostKey(urlObj.hostname); const segments = urlObj.pathname .split('/') .filter(Boolean) .map((segment) => String(segment || '') .toLowerCase() .replace(/\.(m3u8|mp4|webm|ogg|mov|mkv|ts|mpd)$/i, '') .replace(/(?:^|[_-])(2160p|1440p|1080p|720p|480p|360p|240p)(?=$|[_-])/ig, '_') .replace(/(?:^|[_-])(3840[-x]2160|2560[-x]1440|1920[-x]1080|1280[-x]720|854[-x]480|640[-x]360|426[-x]240)(?=$|[_-])/ig, '_') .replace(/(?:^|[_-])(?:master|playlist|index(?:-v\d+(?:-[a-z0-9]+)*)?)(?=$|[_-])/ig, '_') .replace(/(?:^|[_-])\d+k(?=$|[_-])/ig, '_') .replace(/[_-]{2,}/g, '_') .replace(/^[_-]+|[_-]+$/g, '') .trim() ) .filter((segment) => segment && segment.length >= 3); if (segments.length === 0) { return `${hostKey}${urlObj.pathname}`; } return `${hostKey}/${segments.join('/')}`; } catch (e) { return String(url).split('?')[0].toLowerCase(); } }, isSameSignalFamily(currentUrl, nextUrl) { if (!currentUrl || !nextUrl) { return false; } const currentFamilyKey = this.getSignalFamilyKey(currentUrl); const nextFamilyKey = this.getSignalFamilyKey(nextUrl); return !!(currentFamilyKey && nextFamilyKey && currentFamilyKey === nextFamilyKey); }, shouldGrantDomEntitySovereignty(core, candidateInfo, playerManager, qualityContext) { if (!candidateInfo.mediaElement) { return false; } if (!qualityContext.targetUrl || qualityContext.targetUrl.startsWith('blob:')) { return true; } if (!(playerManager.isPlayerActiveOrInitializing || core.decisionInProgress)) { return true; } if (qualityContext.sameFamily) { return qualityContext.newResolutionRank > qualityContext.currentResolutionRank; } if (qualityContext.targetIsM3 && qualityContext.currentReliabilityScore >= qualityContext.newReliabilityScore) { return false; } return qualityContext.compositeCurrentScore <= 0; }, rememberSuppressedCandidate(core, candidateInfo) { const key = core._getNormalizationKey(candidateInfo.url); if (key && !core.lastCandidatesBackup.has(key)) { core.lastCandidatesBackup.set(key, { ...candidateInfo, sources: new Set([candidateInfo.sourceName]), }); } if (candidateInfo.url && !candidateInfo.url.startsWith('blob:')) { core.passiveCandidates.set(candidateInfo.url, candidateInfo); if (core.passiveCandidates.size > 15) { core.passiveCandidates.delete(core.passiveCandidates.keys().next().value); } } }, shouldLockSameFamilyQuality(core, candidateInfo, playerManager, qualityContext) { if (!qualityContext.sameFamily) { return false; } const currentRank = qualityContext.currentResolutionRank; const nextRank = qualityContext.newResolutionRank; const hasStablePlayback = core.isBufferingBackupCandidates || !!playerManager.currentVideoUrl || !!playerManager.hostElement?.classList.contains('dmz-visible'); if (currentRank >= 0 && nextRank >= 0 && nextRank < currentRank) { core.log( `[画质锁]|🔒|同家族候选禁止降级(${nextRank} < ${currentRank}),转存为备份:${candidateInfo.sourceName}`, CONSTANTS.LOG_TYPES.INFO ); this.rememberSuppressedCandidate(core, candidateInfo); return true; } if (hasStablePlayback && currentRank >= 0 && nextRank >= 0 && nextRank === currentRank) { core.log( `[稳定性护盾]|🛡️|同家族同画质晚到信号不再重建,转存为备份:${candidateInfo.sourceName}`, CONSTANTS.LOG_TYPES.INFO ); this.rememberSuppressedCandidate(core, candidateInfo); return true; } if (hasStablePlayback && qualityContext.sameFamily && qualityContext.compositeNewScore <= qualityContext.compositeCurrentScore) { core.log( `[稳定性护盾]|🛡️|同家族晚到信号优势不足,维持当前播放并转存备份:${candidateInfo.sourceName}`, CONSTANTS.LOG_TYPES.INFO ); this.rememberSuppressedCandidate(core, candidateInfo); return true; } return false; }, looksLikeUnsignedGateM3u8(url) { return !!( url && Utils.isM3U8(url) && !/[?&](sign|token|auth|expires|t)=/i.test(url) && (/\/video\/m3u8\//i.test(url) || /\/index\.m3u8(\?|$)/i.test(url)) ); }, isRiskyUnsignedM3u8Candidate(core, candidateInfo = {}) { if (!this.looksLikeUnsignedGateM3u8(candidateInfo.url || '')) { return false; } if (typeof candidateInfo.m3u8Text === 'string' && candidateInfo.m3u8Text.includes('#EXTM3U')) { return false; } return /JSON|DECRYPTION|ATTR|扫描|DOM/.test(candidateInfo.sourceName || ''); }, get403RiskPatternKey(url) { if (!this.looksLikeUnsignedGateM3u8(url)) { return null; } try { const urlObj = new URL(url, window.location.href); return `${urlObj.hostname}${urlObj.pathname}`; } catch (e) { return String(url).split('?')[0]; } }, hasKnown403Risk(core, url) { const riskKey = this.get403RiskPatternKey(url); return !!(riskKey && core.failedRiskyM3u8Patterns?.has(riskKey)); }, trySeamlessCdnUpgrade(core, candidateInfo, playerManager, qualityContext) { try { const host = window.location.hostname; const isTargetLocal = qualityContext.targetUrl.startsWith('/') || qualityContext.targetUrl.includes(host); const isNewRemote = candidateInfo.url.startsWith('http') && !candidateInfo.url.includes(host); if (!isTargetLocal || !isNewRemote) { return false; } const currentSegments = this.extractComparableUrlSegments(qualityContext.targetUrl); const nextSegments = this.extractComparableUrlSegments(candidateInfo.url); if (!this.hasStrongSegmentAffinity(currentSegments, nextSegments)) { return false; } core.log( `[纠错]|🎊|捕捉到更优的绝对路径信号(CDN),执行无缝切换: ${candidateInfo.url.slice(-50)}`, CONSTANTS.LOG_TYPES.CORE_EXEC ); this.executeQualityOverrideTakeover(core, candidateInfo, playerManager); return true; } catch (e) { console.debug('Failed to evaluate quality override paths:', e); return false; } }, extractComparableUrlSegments(url) { if (typeof url !== 'string') { return []; } let segments = []; try { const urlObj = new URL(url, window.location.href); segments = urlObj.pathname.split('/').filter(Boolean); urlObj.searchParams.forEach((val, key) => { const normalizedKey = String(key || '').toLowerCase(); if (/^(token|expires?|expiry|signature|sig|auth|key-pair-id)$/i.test(normalizedKey)) { return; } if (typeof val === 'string' && val.length >= 6 && !/^(1920|1080|720|480|360)$/i.test(val)) { segments.push(val); } }); } catch (e) { segments = url.split('?')[0].split('/').filter(Boolean); } return segments .map((segment) => String(segment) .toLowerCase() .replace(/\.(m3u8|mp4|flv|webm|mkv|ts|mpd|avi|html?|php)$/i, '') .trim() ) .filter((segment) => { if (segment.length < 6) { return false; } if (/^(1920|1280|1080|960|854|720|640|608|480|492|360|240|144)[xp]?\d*$/i.test(segment)) { return false; } if ( /^(amplify_video|ext_tw_video|video|videos|playlist|master|index|manifest|segment|fragment|media|stream|play|vod|hls|dash|audio|embed|player|api)$/i.test( segment ) ) { return false; } return /[0-9]/.test(segment) || /^[a-z0-9_-]{8,}$/i.test(segment); }); }, hasStrongSegmentAffinity(currentSegments, nextSegments) { if (!Array.isArray(currentSegments) || !Array.isArray(nextSegments)) { return false; } for (const aRaw of currentSegments) { for (const bRaw of nextSegments) { const a = String(aRaw || '').toLowerCase(); const b = String(bRaw || '').toLowerCase(); if (!a || !b) { continue; } if (a === b) { return true; } const minLen = Math.min(a.length, b.length); if (minLen >= 12 && (a.includes(b) || b.includes(a))) { return true; } if (a.length >= 16 && b.length >= 16) { const tailA12 = a.slice(-12); const tailB12 = b.slice(-12); const tailA16 = a.slice(-16); const tailB16 = b.slice(-16); if (tailA16 === tailB16 || tailA12 === tailB12) { return true; } } } } return false; }, shouldUpgradeHiddenPlayer(playerManager, qualityContext) { return ( !playerManager.hostElement?.classList.contains('dmz-visible') && qualityContext.compositeNewScore > qualityContext.compositeCurrentScore ); }, stashQualityFallbackCandidate(core, currentUrl, currentScore) { if (!currentUrl || currentUrl.startsWith('blob:')) { return; } const victimKey = core._getNormalizationKey(currentUrl); if (victimKey) { core.takeoverCandidates.set(victimKey, { url: currentUrl, sourceName: '画质降级保底(历史信号)', score: currentScore, }); } }, findBestMatchingCandidate(core, baseCandidate) { const baseSegments = this.extractComparableUrlSegments(baseCandidate.url); if (baseSegments.length === 0) { return null; } let best = null; let bestScore = core._getRecoveryCandidateScore(baseCandidate); const allPools = [ ...core.passiveCandidates.values(), ...core.lastCandidatesBackup.values(), ...core.takeoverCandidates.values(), ]; for (const c of allPools) { if (!c.url || c.url === baseCandidate.url) { continue; } const cSegments = this.extractComparableUrlSegments(c.url); if (cSegments.length > 0 && this.hasStrongSegmentAffinity(baseSegments, cSegments)) { const score = core._getRecoveryCandidateScore(c); if (score > bestScore) { bestScore = score; best = c; } } } if (best) { core.log( `[主权升级]|🚀|为正片找到了更优画质储备: ${best.url.slice(-40)}`, CONSTANTS.LOG_TYPES.CORE_EXEC ); } return best; }, executeQualityOverrideTakeover(core, candidateInfo, playerManager) { core.decisionInProgress = true; core.decisionMade = false; core.isIntentionalSwitch = true; this.preparePlayerForQualityOverride(playerManager); core._executeTakeover(candidateInfo); }, preparePlayerForQualityOverride(playerManager) { if (!playerManager.isPlayerActiveOrInitializing) { return; } try { playerManager.cleanup(true); } catch (e) { console.debug('Failed to cleanup player during quality override:', e); } playerManager.isPlayerActiveOrInitializing = false; }, }; const CorePlaybackDirector = { isSandboxReloadContext() { return new URLSearchParams(window.location.search).has('dmz_sandbox'); }, buildSandboxFoundCandidate(type, content) { return { url: type === 'm3u8' ? content.finalUrl : content, sourceName: CONSTANTS.TAKEOVER_SOURCES.CROSS_FRAME + '(沙箱重载)', m3u8Text: type === 'm3u8' ? content.original : null, }; }, postSandboxFoundCandidate(core, frameCommunicator, type, content) { frameCommunicator.postToTopFrame({ type: CONSTANTS.MESSAGE_TYPES.SANDBOX_URL_FOUND, candidateInfo: this.buildSandboxFoundCandidate(type, content), }); }, buildCrossFramePlayMessage(core, type, content, playerManager, diagnosticsTool, actionLogger) { return { type: CONSTANTS.MESSAGE_TYPES.M3U8_COMMAND, action: type === 'm3u8' ? 'PLAY_M3U8' : 'PLAY_NORMAL_VIDEO', content: type === 'm3u8' ? content.processed : content, originalContent: type === 'm3u8' ? content.original : null, finalUrl: core.lastPostedUrl, videoFormat: playerManager.currentVideoFormat, playbackHealth: diagnosticsTool.playbackHealth, slicingReport: diagnosticsTool.slicingReport, iframeLogs: actionLogger.getLogs().filter((log) => log.type === CONSTANTS.LOG_TYPES.CORE_SLICE), }; }, postCrossFramePlayCommand( core, type, content, playerManager, diagnosticsTool, frameCommunicator, actionLogger ) { core.lastPostedUrl = type === 'm3u8' ? content.finalUrl : content; core.findAllVideosAndAudioInPage().forEach((m) => core.neutralizeOriginalPlayer(m)); frameCommunicator.postToTopFrame( this.buildCrossFramePlayMessage(core, type, content, playerManager, diagnosticsTool, actionLogger) ); }, broadcastPauseToChildFrames() { try { const frames = window.frames; for (let i = 0; i < frames.length; i++) { frames[i].postMessage({ type: CONSTANTS.MESSAGE_TYPES.FORCE_PAUSE_ALL }, '*'); } } catch (e) { console.debug('Failed to broadcast pause command from top frame:', e); } }, async sendPlayCommand(core, type, content) { const { playerManager, diagnosticsTool, frameCommunicator, actionLogger } = core.context; if (this.isSandboxReloadContext()) { this.postSandboxFoundCandidate(core, frameCommunicator, type, content); return; } if (this.shouldAbortPlayCommand(playerManager)) { return; } if (this.shouldPostCrossFramePlayCommand()) { this.postCrossFramePlayCommand( core, type, content, playerManager, diagnosticsTool, frameCommunicator, actionLogger ); return; } await this.playInTopFrame(playerManager, content); }, shouldAbortPlayCommand(playerManager) { return playerManager.isPlayerActiveOrInitializing; }, shouldPostCrossFramePlayCommand() { return !CONSTANTS.IS_TOP_FRAME; }, async playInTopFrame(playerManager, content) { await playerManager.play(content); this.broadcastPauseToChildFrames(); }, }; const SpaCandidateBuffer = { TTL: 6000, MAX_ITEMS: 5, enqueue(core, candidateInfo) { const now = Date.now(); const queue = this._prune(core.pendingSpaCandidates || [], now); const key = this._getKey(candidateInfo); const nextQueue = queue.filter((item) => item.key !== key); nextQueue.push({ key, ts: now, candidate: candidateInfo }); if (nextQueue.length > this.MAX_ITEMS) { nextQueue.splice(0, nextQueue.length - this.MAX_ITEMS); } core.pendingSpaCandidates = nextQueue; core.pendingSpaCandidate = candidateInfo; this._armClearTimer(core); }, drain(core) { const now = Date.now(); const queue = this._prune(core.pendingSpaCandidates || [], now).map((item) => item.candidate); core.pendingSpaCandidates = []; core.pendingSpaCandidate = null; if (core.pendingSpaTimer) { clearTimeout(core.pendingSpaTimer); core.pendingSpaTimer = null; } return queue; }, _armClearTimer(core) { if (core.pendingSpaTimer) { clearTimeout(core.pendingSpaTimer); } core.pendingSpaTimer = setTimeout(() => { core.pendingSpaCandidates = []; core.pendingSpaCandidate = null; core.pendingSpaTimer = null; }, this.TTL); }, _prune(queue, now = Date.now()) { return queue.filter((item) => now - item.ts <= this.TTL); }, _getKey(candidateInfo) { return [ candidateInfo?.finalUrl || candidateInfo?.url || candidateInfo?.requestUrl || '', candidateInfo?.sourceName || '', candidateInfo?.type || '', ].join('|'); }, }; const CoreCandidateIntake = { async addTakeoverCandidate(core, candidateInfo) { if (core.isSystemHalted) { SpaCandidateBuffer.enqueue(core, candidateInfo); return; } if (this.prepareCandidate(core, candidateInfo)) { return; } const { playerManager, frameCommunicator } = core.context; if (this.consumePreTopFrameCandidate(core, candidateInfo, playerManager)) { return; } if (!CONSTANTS.IS_TOP_FRAME) { this.forwardCandidateFromIframe(core, candidateInfo, frameCommunicator); return; } await this.finalizeTopFrameCandidate(core, candidateInfo); }, prepareCandidate(core, candidateInfo) { if (this.isInvalidTakeoverUrl(candidateInfo.url)) { return true; } candidateInfo.url = core._extractUrlFromParameter(candidateInfo.url); const cacheKey = core._getNormalizationKey(candidateInfo.url); this.hydrateCandidateCache(core, candidateInfo, cacheKey); return this.isGhostSignal(core, candidateInfo); }, isInvalidTakeoverUrl(url) { if ( !url || url.length < 15 || url === 'https://' || url === 'http://' || /blank\.mp4|empty\.mp4/i.test(url) ) { return true; } try { const pathEnd = new URL(url, window.location.href).pathname.split('/').pop(); if ( pathEnd && pathEnd.length > 40 && !pathEnd.includes('.') && /^[a-zA-Z0-9\+\/]+={0,2}$/.test(pathEnd) ) { if (/={1,2}$/.test(pathEnd) || pathEnd.includes('AAAA') || pathEnd.includes('wAA')) { return true; } } } catch (e) {} return false; }, hydrateCandidateCache(core, candidateInfo, cacheKey) { if (candidateInfo.m3u8Text) { core.m3u8SessionCache.set(cacheKey, candidateInfo.m3u8Text); } else if (cacheKey && core.m3u8SessionCache.has(cacheKey)) { candidateInfo.m3u8Text = core.m3u8SessionCache.get(cacheKey); core.log(`[缓存命中] ⚡️复用历史M3U8数据,规避一次性链接失效。`, CONSTANTS.LOG_TYPES.INFO); } }, isGhostSignal(core, candidateInfo) { if (!candidateInfo.url) { return false; } if ( candidateInfo.sourceName?.includes('触发器') || /顶层记忆|历史重播|遥控触发|重播/.test(candidateInfo.sourceName || '') ) { return false; } const candidateBase = candidateInfo.url.split('?')[0]; const candidateKey = core._getNormalizationKey(candidateInfo.url) || candidateBase; if (core.staleVideoUrl) { const staleBase = core.staleVideoUrl.split('?')[0]; if (candidateBase === staleBase || candidateInfo.url.includes(staleBase)) { core.log( `👻[幽灵过滤]|拦截到刚刚废弃的信号(残留DOM):${candidateInfo.url.slice(-30)}`, CONSTANTS.LOG_TYPES.WARN ); return true; } } if (core.globalSeenVideoUrls && core.globalSeenVideoUrls.has(candidateKey)) { const currentPage = window.location.href.split('#')[0]; const allowedForThisPage = core.pageVideoAssociations?.get(currentPage); if (!allowedForThisPage || !allowedForThisPage.has(candidateKey)) { core.log( `👻[时空过滤]|拦截到非当前页面的历史残留信号:${candidateInfo.url.slice(-30)}`, CONSTANTS.LOG_TYPES.WARN ); return true; } } return false; }, consumePreTopFrameCandidate(core, candidateInfo, playerManager) { this.handleTriggerSignal(core, candidateInfo); if (this.handleBlobCandidate(core, candidateInfo)) { return true; } this.resetForSwitchIfNeeded(core, playerManager); return this.shouldHandleAsPassiveCandidate(core, candidateInfo, playerManager); }, handleTriggerSignal(core, candidateInfo) { if (!candidateInfo.sourceName?.includes('触发器')) { return; } const isReplayTrigger = /顶层记忆|历史重播|遥控触发|重播/.test(candidateInfo.sourceName || ''); core.log( `👆收到[${candidateInfo.sourceName}]信号,强制确认为用户交互意图。`, CONSTANTS.LOG_TYPES.PLAYBACK_SWITCH ); core.setUserIntentToSwitch(isReplayTrigger ? 'replay' : 'switch'); }, handleBlobCandidate(core, candidateInfo) { if (!candidateInfo.url || !candidateInfo.url.startsWith('blob:')) { return false; } if (candidateInfo.sourceName.includes('网络拦截')) { core.log( `[门禁]|🚧|🟢放行|网络拦截Blob(高置信度)|来源:${candidateInfo.sourceName}。`, CONSTANTS.LOG_TYPES.TAKEOVER_ATTEMPT ); return false; } if (/扫描|DOM|ATTR/.test(candidateInfo.sourceName)) { core.log( `[门禁]|🚧|⛔️拦截|DOM扫描Blob(MSE对象无法跨实例复用)|来源:${candidateInfo.sourceName}。`, CONSTANTS.LOG_TYPES.TAKEOVER_ATTEMPT ); return true; } if (core.lastSuccessfulUrl) { core.log(`⚠️检测到Blob URL变动,尝试使用核心档案中的真实地址。`, CONSTANTS.LOG_TYPES.WARN); return false; } core.log( `[门禁]|🚧|尝试放行|Blob URL(未知来源)|来源:${candidateInfo.sourceName}。`, CONSTANTS.LOG_TYPES.TAKEOVER_ATTEMPT ); return false; }, resetForSwitchIfNeeded(core, playerManager) { if (!playerManager.isPlayerActiveOrInitializing || !core.userIntendsToSwitch) { return; } core.log(`[最高指令]|📳|👇用户换集意图触发,强制刷新所有决策!`, CONSTANTS.LOG_TYPES.PLAYBACK_SWITCH); playerManager.cleanup(); Object.assign(core, { decisionMade: false, decisionInProgress: false, userIntendsToSwitch: false, isBufferingBackupCandidates: false, m3u8SessionCache: new Map(), m3u8FetchInflight: new Map(), m3u8ProcessedCache: new Map(), failedCandidateKeys: new Set(), failedRiskyM3u8Patterns: new Set(), }); core.takeoverCandidates.clear(); core.lastCandidatesBackup.clear(); clearTimeout(core.backupBufferTimer); }, shouldHandleAsPassiveCandidate(core, candidateInfo, playerManager) { if (!(core.decisionInProgress || playerManager.isPlayerActiveOrInitializing) || core.userIntendsToSwitch) { return false; } if (CoreQualityManager.evaluateQualityOverride(core, candidateInfo)) { return true; } if (candidateInfo.mediaElement && !PageScanner.isPreRollAdByPathFeatures(candidateInfo.url, core.context)) { core.neutralizeOriginalPlayer(candidateInfo.mediaElement); } if (core.isBufferingBackupCandidates) { core.log( `[稳定性护盾]|🛡️|当前播放稳定,已拦截(${candidateInfo.sourceName})的平级/低级干扰,转为后台备份。`, CONSTANTS.LOG_TYPES.INFO ); this.bufferBackupCandidate(core, candidateInfo); return true; } this.cachePassiveCandidate(core, candidateInfo); return true; }, bufferBackupCandidate(core, candidateInfo) { const targetUrl = core.lastSuccessfulUrl || core.context.playerManager.currentVideoUrl; if (targetUrl && candidateInfo.url && !candidateInfo.url.startsWith('blob:')) { const isSameFamily = CoreQualityManager.isSameSignalFamily(targetUrl, candidateInfo.url); const currSegs = CoreQualityManager.extractComparableUrlSegments(targetUrl); const nextSegs = CoreQualityManager.extractComparableUrlSegments(candidateInfo.url); const hasAffinity = CoreQualityManager.hasStrongSegmentAffinity(currSegs, nextSegs); if (!isSameFamily && !hasAffinity) { return; } } const key = core._getNormalizationKey(candidateInfo.url); if (key && !core.lastCandidatesBackup.has(key)) { core.lastCandidatesBackup.set(key, { ...candidateInfo, sources: new Set([candidateInfo.sourceName]), }); } }, cachePassiveCandidate(core, candidateInfo) { if (candidateInfo.url && !candidateInfo.url.startsWith('blob:')) { core.passiveCandidates.set(candidateInfo.url, candidateInfo); if (core.passiveCandidates.size > 15) { core.passiveCandidates.delete(core.passiveCandidates.keys().next().value); } } }, forwardCandidateFromIframe(core, candidateInfo, frameCommunicator) { core.log(`[iFrame勘查员]发现目标,打包情报上报...`, CONSTANTS.LOG_TYPES.COMM); core.decisionInProgress = true; frameCommunicator.postToTopFrame({ type: CONSTANTS.MESSAGE_TYPES.RAW_SIGNAL_FORWARDED, payload: { url: candidateInfo.url, sourceName: candidateInfo.sourceName, m3u8Text: candidateInfo.m3u8Text || null, survey: candidateInfo.survey, mediaElement: null, }, }); }, ensureBehavioralFilter(core) { if (!core.context.behavioralFilter) { core.context.behavioralFilter = new BehavioralFilter(core.context); } return core.context.behavioralFilter; }, async finalizeTopFrameCandidate(core, candidateInfo) { try { const analysisResult = await this.ensureBehavioralFilter(core).analyze(candidateInfo); if (!analysisResult.isLegitimate) { return; } if (this.handleAnalysisRaceLoss(core, candidateInfo)) { return; } await this.executeCandidateDecision(core, candidateInfo); } catch (err) { core.log(`[决策]|⚖️|接管执行失败:${err.message}`, CONSTANTS.LOG_TYPES.TAKEOVER_FAIL); core.decisionInProgress = false; core.reportPlaybackFailure(); } }, handleAnalysisRaceLoss(core, candidateInfo) { if ( !(core.decisionInProgress || core.context.playerManager.isPlayerActiveOrInitializing) || core.userIntendsToSwitch ) { return false; } if (CoreQualityManager.evaluateQualityOverride(core, candidateInfo)) { return true; } if (core.isBufferingBackupCandidates) { const targetUrl = core.lastSuccessfulUrl || core.context.playerManager.currentVideoUrl; if (targetUrl && candidateInfo.url && !candidateInfo.url.startsWith('blob:')) { const isSameFamily = CoreQualityManager.isSameSignalFamily(targetUrl, candidateInfo.url); const currSegs = CoreQualityManager.extractComparableUrlSegments(targetUrl); const nextSegs = CoreQualityManager.extractComparableUrlSegments(candidateInfo.url); const hasAffinity = CoreQualityManager.hasStrongSegmentAffinity(currSegs, nextSegs); if (!isSameFamily && !hasAffinity) { return true; } } const key = core._getNormalizationKey(candidateInfo.url); if (key && !core.lastCandidatesBackup.has(key)) { core.lastCandidatesBackup.set(key, { ...candidateInfo, sources: new Set([candidateInfo.sourceName]), }); core.log( `[并发竞争] 🥈在分析期间失去主导权,转存为同源备份: ${candidateInfo.url.slice(-30)}`, CONSTANTS.LOG_TYPES.INFO ); } } return true; }, async executeCandidateDecision(core, candidateInfo) { core.decisionInProgress = true; core.log( `[决策]|⚖️|🔒信号「${candidateInfo.sourceName}」通过门禁检查,系统上锁并执行接管。`, CONSTANTS.LOG_TYPES.INFO ); core.sandboxManager.cancelCountdown(`捕获到有效信号:${candidateInfo.sourceName}`); core.sandboxManager.cleanup(); core.log( `[决策]|⚖️|✅合格信号已确认「来源:${candidateInfo.sourceName}」,立即执行接管!`, CONSTANTS.LOG_TYPES.TAKEOVER_SUCCESS ); this.startBackupBufferWindow(core); await core._executeTakeover(candidateInfo); }, startBackupBufferWindow(core) { core.log(`[备份]|💾|3秒备份窗口已在后台开启...`, CONSTANTS.LOG_TYPES.INFO); core.isBufferingBackupCandidates = true; core.backupBufferTimer = setTimeout(() => { core.isBufferingBackupCandidates = false; core.log(`[备份]|💾|3秒备份窗口关闭。`, CONSTANTS.LOG_TYPES.INFO); }, 3000); }, }; const FailureRecoveryDescriptors = { RETRYABLE_REASON_PATTERN: /(playback error|metadata load timeout|metadata timeout|sandbox timeout|mediaerror|hls|decode|network)/, METADATA_TIMEOUT_PATTERN: /metadata load timeout|metadata timeout/, DEFER_THRESHOLD_MS: 6500, METADATA_DEFER_THRESHOLD_MS: 6200, METADATA_RECOVERY_TARGET_MS: 7000, COLD_START_RECOVERY_TARGET_MS: 4200, NORMAL_RECOVERY_TARGET_MS: 3800, RECOVERY_DELAY_WINDOWS: { metadata: { min: 700, max: 1100, fallback: 900 }, coldStart: { min: 900, max: 1400, fallback: 1000 }, normal: { min: 800, max: 2200, fallback: 1200 }, }, }; const FailureRecoveryFactoryUtils = { getReasonText(failureContext = {}) { return (failureContext.reason || 'Playback Error').toLowerCase(); }, clampDelay(remaining, windowConfig) { return Math.max( windowConfig.min, Math.min(windowConfig.max, remaining > 0 ? remaining : windowConfig.fallback) ); }, collectCandidatePool(core) { return new Map([ ...core.takeoverCandidates, ...core.bufferedBackupCandidates, ...core.lastCandidatesBackup, ]); }, }; const CoreFailureRecovery = { reportPlaybackFailure(core, failureContext = {}) { const currentActiveUrl = core.context.playerManager.currentVideoUrl; const failedUrl = failureContext.failedUrl || currentActiveUrl; if (this.shouldIgnorePlaybackFailure(failedUrl, currentActiveUrl)) { return; } if (this.consumeIntentionalSwitchFailure(core)) { return; } const { playerManager, frameCommunicator } = core.context; this.registerFailedCandidate(core, failedUrl, failureContext.reason); this.cleanupFailedPlayer(playerManager); this.releaseDecisionState(core); const validCandidates = this.collectValidRecoveryCandidates(core); if (this.shouldDeferFailureRecovery(core, failureContext, validCandidates, frameCommunicator)) { this.scheduleDeferredRecovery(core, failureContext, frameCommunicator); return; } this.clearRecoveryCandidatePools(core); this.dispatchRecoveryFallback(core, validCandidates, frameCommunicator); }, shouldDeferFailureRecovery(core, failureContext, validCandidates, frameCommunicator) { if (validCandidates.size > 0 || frameCommunicator.pendingMainPlayerSource) { return false; } if (core.isSystemHalted || core.userIntendsToSwitch || core.sandboxManager.hasTriggeredSandbox) { return false; } const reason = FailureRecoveryFactoryUtils.getReasonText(failureContext); if (!FailureRecoveryDescriptors.RETRYABLE_REASON_PATTERN.test(reason)) { return false; } const navAt = core.lastSpaNavigationAt || 0; if (!navAt || core.navigationRequestId <= 0) { return false; } const elapsed = Date.now() - navAt; if (elapsed < 0 || elapsed >= FailureRecoveryDescriptors.DEFER_THRESHOLD_MS) { return false; } if (FailureRecoveryDescriptors.METADATA_TIMEOUT_PATTERN.test(reason)) { return elapsed < FailureRecoveryDescriptors.METADATA_DEFER_THRESHOLD_MS; } return true; }, scheduleDeferredRecovery(core, failureContext, frameCommunicator) { const delayMs = this.getDeferredRecoveryDelayMs(core, failureContext); if (!(delayMs > 0)) { return; } if (core.pendingRecoveryTimer) { clearTimeout(core.pendingRecoveryTimer); core.pendingRecoveryTimer = null; } core.log( `[FTR]|🧰|检测到页面仍在路由/数据稳定阶段,延迟故障恢复${Math.round(delayMs)}ms,继续等待新信号。`, CONSTANTS.LOG_TYPES.CORE_EXEC ); core.pendingRecoveryTimer = setTimeout(() => { core.pendingRecoveryTimer = null; this.finalizeDeferredRecovery(core, frameCommunicator, failureContext); }, delayMs); }, getDeferredRecoveryDelayMs(core, failureContext = {}) { const elapsed = Date.now() - (core.lastSpaNavigationAt || 0); const reason = FailureRecoveryFactoryUtils.getReasonText(failureContext); if (FailureRecoveryDescriptors.METADATA_TIMEOUT_PATTERN.test(reason)) { const remaining = FailureRecoveryDescriptors.METADATA_RECOVERY_TARGET_MS - elapsed; return FailureRecoveryFactoryUtils.clampDelay( remaining, FailureRecoveryDescriptors.RECOVERY_DELAY_WINDOWS.metadata ); } if (core.lastSpaNavigationWasColdStart && elapsed < FailureRecoveryDescriptors.COLD_START_RECOVERY_TARGET_MS) { const remaining = FailureRecoveryDescriptors.COLD_START_RECOVERY_TARGET_MS - elapsed; return FailureRecoveryFactoryUtils.clampDelay( remaining, FailureRecoveryDescriptors.RECOVERY_DELAY_WINDOWS.coldStart ); } const remaining = FailureRecoveryDescriptors.NORMAL_RECOVERY_TARGET_MS - elapsed; return FailureRecoveryFactoryUtils.clampDelay( remaining, FailureRecoveryDescriptors.RECOVERY_DELAY_WINDOWS.normal ); }, finalizeDeferredRecovery(core, frameCommunicator, failureContext = {}) { const { playerManager } = core.context; if (playerManager.isPlayerActiveOrInitializing || core.decisionInProgress || core.decisionMade) { return; } const validCandidates = this.collectValidRecoveryCandidates(core); this.clearRecoveryCandidatePools(core); if (validCandidates.size > 0 || frameCommunicator.pendingMainPlayerSource) { core.log( `[FTR]|🧰|延迟观察窗口结束,检测到新的可用信号,恢复正常重决策。`, CONSTANTS.LOG_TYPES.CORE_EXEC ); } this.dispatchRecoveryFallback(core, validCandidates, frameCommunicator); }, consumeIntentionalSwitchFailure(core) { if (!core.isIntentionalSwitch) { return false; } core.isIntentionalSwitch = false; return true; }, cleanupFailedPlayer(playerManager) { if (!playerManager.isPlayerActiveOrInitializing) { return; } playerManager.cleanup(); }, releaseDecisionState(core) { core.decisionMade = false; core.decisionInProgress = false; core.currentDecisionSourceName = ''; core.currentDecisionM3u8Text = null; core.log(`[FTR]|🧰|🔐系统决策锁已解除,恢复监听状态。`, CONSTANTS.LOG_TYPES.CORE_EXEC); }, clearRecoveryCandidatePools(core) { core.takeoverCandidates.clear(); core.bufferedBackupCandidates.clear(); core.lastCandidatesBackup.clear(); }, dispatchRecoveryFallback(core, validCandidates, frameCommunicator) { if (validCandidates.size > 0) { this.recoverFromValidCandidates(core, validCandidates); return; } if (frameCommunicator.pendingMainPlayerSource) { this.recoverFromPendingMainPlayer(core, frameCommunicator); return; } this.handleSandboxRecoveryFallback(core); }, handleSandboxRecoveryFallback(core) { if (core.sandboxManager.canRetryBridgeFlow()) { core.log(`[FTR]|🧰|桥接页仍在推进阶段,执行桥接专用重试。`, CONSTANTS.LOG_TYPES.CORE_EXEC); core.sandboxManager.reload(false); return; } if (!core.sandboxManager.hasTriggeredSandbox && !core.isSystemHalted) { core.log(`[FTR]|🧰|所有已知信号均失效,尝试最后手段:沙箱重载。`, CONSTANTS.LOG_TYPES.CORE_EXEC); core.sandboxManager.reload(); return; } core.log(`[FTR]|🧰|无可用信号且沙箱已使用过(或系统已休眠),停止尝试。`, CONSTANTS.LOG_TYPES.CORE_EXEC); }, shouldIgnorePlaybackFailure(failedUrl, currentActiveUrl) { if (!failedUrl || !currentActiveUrl || failedUrl === currentActiveUrl) { return false; } return this.getFailureBaseUrl(currentActiveUrl) === this.getFailureBaseUrl(failedUrl); }, getFailureBaseUrl(url) { return url ? url.split('?')[0].split('#')[0] : ''; }, registerFailedCandidate(core, failedUrl, reason) { const failedKey = core._getNormalizationKey(failedUrl); if (!core.failedCandidateKeys) { core.failedCandidateKeys = new Set(); } if (failedKey) { core.failedCandidateKeys.add(failedKey); } if ((reason || '').includes('403')) { const riskKey = CoreQualityManager.get403RiskPatternKey(failedUrl); if (riskKey) { core.failedRiskyM3u8Patterns.add(riskKey); } } if (failedKey) { core.log( `[FTR]|🧰|信号源(${failedUrl ? failedUrl.slice(-50) : '未知'}) 因(${reason || 'Playback Error'})已被加入会话黑名单。`, CONSTANTS.LOG_TYPES.CORE_EXEC ); } }, collectValidRecoveryCandidates(core) { const validCandidates = new Map(); FailureRecoveryFactoryUtils.collectCandidatePool(core).forEach((value, key) => { if (!core.failedCandidateKeys.has(key)) { validCandidates.set(key, value); } }); return validCandidates; }, recoverFromValidCandidates(core, validCandidates) { core.log( `[FTR]|🧰|在备用信号中发现${validCandidates.size}个新选项,立即重新决策...`, CONSTANTS.LOG_TYPES.CORE_EXEC ); const bestAlternative = core._getBestCandidate(Array.from(validCandidates.values())); if (bestAlternative) { core._executeTakeover(bestAlternative); } }, recoverFromPendingMainPlayer(core, frameCommunicator) { core.log(`[FTR]|🧰|启用候补Iframe信号,尝试激活...`, CONSTANTS.LOG_TYPES.CORE_EXEC); const { source, origin, frameElement } = frameCommunicator.pendingMainPlayerSource; Object.assign(frameCommunicator, { mainPlayerFrameSource: source, mainPlayerFrameOrigin: origin, mainPlayerIframeElement: frameElement, pendingMainPlayerSource: null, }); source.postMessage({ type: CONSTANTS.MESSAGE_TYPES.ACTIVATE_AUTOPLAY }, origin); setTimeout(() => { if (!core.context.playerManager.isPlayerActiveOrInitializing) { core.log(`[FTR]|🧰|候补Iframe激活超时(未检测到播放),强制执行沙箱重载。`, CONSTANTS.LOG_TYPES.WARN); core.sandboxManager.reload(); } }, 1000); }, }; const DomScannerMediaPipeline = { async processMediaElement(scanner, media) { const { coreLogic } = scanner.context; if (this.shouldSkipMediaElement(coreLogic, media)) { return false; } const targetSrc = this.getMediaElementSource(media); if (!this.validateMediaElementSource(scanner, media, targetSrc)) { return false; } if (this.isMutedLoopingDecoration(media)) { return false; } if (!this.lockMediaElementSource(media, targetSrc)) { return false; } coreLogic.neutralizeOriginalPlayer(media, 'active'); const candidateInfo = this.buildDomCandidateInfo(media, targetSrc); if (media.tagName === 'VIDEO') { this.enrichCandidateFromDataset(scanner, media, candidateInfo); } coreLogic.addTakeoverCandidate(candidateInfo); return true; }, shouldSkipMediaElement(coreLogic, media) { return coreLogic.isSystemHalted || coreLogic.isRestoreInProgress || media.id === CONSTANTS.IDS.PLAYER; }, getMediaElementSource(media) { const getValidSrc = (el) => { if (!el) { return null; } const attrSrc = el.getAttribute('src'); if ( attrSrc && attrSrc.length > 40 && !attrSrc.includes('/') && !attrSrc.includes('.') && /^[a-zA-Z0-9\+\/]+={0,2}$/.test(attrSrc) ) { return null; } return el.src; }; return getValidSrc(media) || getValidSrc(media.querySelector('source[src]')); }, validateMediaElementSource(scanner, media, targetSrc) { if (!targetSrc || media.dataset.dmzIsAd === 'true' || media.dataset.dmzIgnored === 'true') { return false; } if (PageScanner.isPreRollAdByPathFeatures(targetSrc, scanner.context)) { media.dataset.dmzIsAd = 'true'; return false; } return true; }, isMutedLoopingDecoration(media) { if (!(media.loop && media.muted && media.autoplay && !media.controls)) { return false; } media.dataset.dmzIgnored = 'true'; return true; }, lockMediaElementSource(media, targetSrc) { if (media.dataset.dmzSourceLocked === targetSrc) { return false; } media.dataset.dmzSourceLocked = targetSrc; return true; }, buildDomCandidateInfo(media, targetSrc) { const rect = media.getBoundingClientRect(); const style = window.getComputedStyle(media); const isPrimary = Utils.isElementInPrimaryMediaRegion(media); return { mediaElement: media, isPrimaryMedia: isPrimary, survey: { rect: { width: rect.width, height: rect.height }, videoSize: media.tagName === 'VIDEO' && media.videoWidth ? { w: media.videoWidth, h: media.videoHeight } : null, computedStyle: { display: style.display, visibility: style.visibility, }, }, url: targetSrc, sourceName: CONSTANTS.TAKEOVER_SOURCES.DOM_HTTP, }; }, enrichCandidateFromDataset(scanner, media, candidateInfo) { for (const key in media.dataset) { const resolvedUrl = this.resolveMediaDatasetUrl(media.dataset[key]); if (!resolvedUrl) { continue; } const ext = /\.(m3u8|mp4|flv|webm|mkv)(\?|$)/i; if (ext.test(resolvedUrl) && !PageScanner.isPreRollAdByPathFeatures(resolvedUrl, scanner.context)) { candidateInfo.url = resolvedUrl; candidateInfo.sourceName = CONSTANTS.TAKEOVER_SOURCES.DOM_ATTR; break; } } }, resolveMediaDatasetUrl(val) { if (!(typeof val === 'string' && (val.includes('http') || val.includes('{')))) { return null; } try { if (val.trim().startsWith('{')) { const cfg = JSON.parse(val); return cfg.src || cfg.url || cfg.videoUrl || null; } if (val.startsWith('http')) { return val; } } catch (e) { console.debug('Failed to parse DOM attributes:', e); } return null; }, }; const DomUtils = { core: DomUtilsCore, create(tag, options = {}, children = []) { const el = document.createElement(tag); Object.entries(options).forEach(([key, value]) => { this.core.applyOption(el, key, value); }); return this.core.appendChildren(el, children); }, toggleIcon(showIcon, hideIcons = []) { this.core.setIconVisibility(showIcon, hideIcons); }, toggleVisibility(element, forceState) { return this.core.toggleVisibilityClass(element, CONSTANTS.CLASSES.VISIBLE, forceState); }, createCopyButtonWithFeedback(textToCopy, buttonText = '复制') { const button = this.create('button', { className: 'copy-btn', title: '复制', textContent: buttonText, }); button.addEventListener('click', (e) => { e.stopPropagation(); Utils.copyToClipboard(textToCopy, () => { button.textContent = '✔ 已复制!'; setTimeout(() => { button.textContent = buttonText; }, 1500); }); }); return button; }, getAttrSignature(el) { return this.core.buildAttrSignature(el); }, showButtonFeedback(button, config) { this.core.setTemporaryButtonState(button, config); }, }; const Utils = { clipboard: ClipboardUtils, timing: TimingUtils, mediaRegion: MediaViewportUtils, _iframeProbeCache: new Map(), _iframeProbeInflight: new Set(), NoisePatterns: { HIGH_SCORE_PATH: /(?:^|[\/_\?&=-])(play|watch|vodplay|m3u8|player|view|video|videos|movie|movies|film|drama|shorties|short|tvshows|jav|subtitles|photo)(?:$|[\/_\?&=-]|\d|\.)/i, HIGH_SCORE_FULL: /(?:[\?&](?:vid|video(?:_?(?:id|key))?|media_?id|vod_?id|content_?id|item_?id)=)|(?:[\?&]id=[a-f0-9]{8,})/i, DETAIL_PATH: /(gua_details|vod\/details|archives)[/_]/, DETAIL_SPLIT: /(?:^|[\/_])detail[\/_]/, MULTI_NUM: /(?:^|[\/_-])\d+[-_]\d+(?:[-_]\d+)?(?:[\/_\.]|$)/, YEAR: /[-_](19|20)\d{2}([-_]|$)/, FILTER: /[-_]0[-_]0/, CODE: /(?:^|[\/_-])[a-z]{2,}-?\d{3,}(?:$|[\/_\.]|html)/i, LONG_ID: /\/[a-z0-9-_]{3,}\/\d{6,}(\.html)?\/?$/, HEX_ROOT: /^\/[a-f0-9]{6,32}\/?$/i, MEDIUM_SCORE: /(?:^|[\/_\?&=-])(video|bangumi|anime|dsj|dy|zy|dm|tv)(?:$|[\/_\?&=-]|\d)/i, LOW_SCORE: /(?:^|[\/_\?&=-])(episode|chapter)(?:$|[\/_\?&=-])/i, MINIMAL_SCORE: /(?:^|[\/_\?&=-])vod(?:$|[\/_\?&=-]|\d)/i, PARAM_ID: /[\?&](id|vid|aid|cid|v|f|p|video_id|videoid|video_key|videokey|media_id|mediaid|vod_id|vodid|content_id|contentid|item_id|itemid|episode_id|episodeid|ep|eid)=([a-zA-Z0-9-_]+)/, PATH_ID: /[\/](id|vid|aid|cid|v|f|p|vod|play|detail|details|view|content|item)[\/]([a-zA-Z0-9-_]+)/, INVALID_ID: /^(video|videos|vod|view|drama|movie|movies|short|shorties|detail|details|archives|dongman|anime|category|tag|label|search|index|home|about|contact|help|feed|sitemap|overview|mail|email|product|features|upgrade|support|install|terms|privacy|policy|html|htm|php|jsp|asp|page|pages|list|tvshows|porn-?video|jav-?video|adult-?video|tube|porn|jav|av|play|topic|thread|post)$/i, NEGATIVE_KEYWORDS: /(?:^|[\/_\-])(status|topic|post|tweet|detail|intro|book|read|novel|xiaoshuo|manhua|comic|article|news|doc|download|shop|cart|donate|login|auth|user|profile|archives|forum|thread|scripts|greasyfork|channel|board|subject|pages|overview|mail|email|product|about|help|faq|welcome|contact|terms|privacy|policy|install|upgrade|support|features|live|vodtype|voddetail|dongman|edit|editor|console|dashboard|workbench|build|deploy|project|cloud|service|workers|landing|student|pricing|billing|marketing|solution)(?:$|[\/_\-]|\.)|[\?&](status|topic|post|tweet|detail|intro|book|read|novel|xiaoshuo|manhua|comic|article|news|doc|download|shop|cart|donate|login|auth|user|profile|archives|forum|thread|scripts|greasyfork|channel|board|subject|pages|overview|mail|email|product|about|help|faq|welcome|contact|terms|privacy|policy|install|upgrade|support|features|live|vodtype|voddetail|dongman|edit|editor|console|dashboard|workbench|build|deploy|project|cloud|service|workers|landing|student|pricing|billing|marketing|solution)=/i, NEGATIVE_PATH: /(^|\/)(s|search|query|results?|find)\/?$/i, STRONG_NEGATIVE: /^\/(jav|porn|tube|subtitles|chinese-subtitles|channels|models|stars|albums|full[-_]?(porn|film|vod)|[a-z0-9]+[-_]videos?)\/?$/i, LIST_KEYWORDS: /(?:^|[\/_-])(list|cate|category|categories|tag|tags|sort|search|filter|index|home|default|genres|orderby|region|cats|pages?|type)(?:$|[\/_-]|\.)|[\?&](list|cate|category|categories|tag|tags|sort|search|filter|index|home|default|genres|orderby|region|cats|pages?|type)=/i, SORT_PARAMS: /[\?&](page|q|query|sort|order|by)=/i, }, debounce(func, wait) { return this.timing.debounce(func, wait); }, findInteractiveAncestor(startNode, options = {}) { return InteractionUtils.findInteractiveAncestor(startNode, options); }, isVisibleElement(el) { return this.mediaRegion.isVisibleElement(el); }, _getPrimaryMediaCandidateScore(el, rect, viewportWidth, viewportHeight) { return this.mediaRegion.getPrimaryMediaCandidateScore(el, rect, viewportWidth, viewportHeight); }, getPrimaryMediaViewportRect(root = document) { return this.mediaRegion.getPrimaryMediaViewportRect(root); }, isElementInPrimaryMediaRegion(el, root = document, mediaRect = null) { return this.mediaRegion.isElementInPrimaryMediaRegion(el, root, mediaRect); }, queryVisibleElements(selectors, root = document) { return DomQueryUtils.queryVisibleElements(selectors, root, (el) => this.isVisibleElement(el)); }, queryFirstVisiblePerSelector(selectors, root = document) { return DomQueryUtils.queryFirstVisiblePerSelector( selectors, root, (scope) => this.getPrimaryMediaViewportRect(scope), (el) => this.isVisibleElement(el), (el, scope, mediaRect) => this.isElementInPrimaryMediaRegion(el, scope, mediaRect) ); }, dispatchMouseEventSequence(target, eventTypes, eventOptions = {}, errorLabel = 'Event dispatch failed:') { InteractionUtils.dispatchMouseEventSequence(target, eventTypes, eventOptions, errorLabel); }, safeNativeClick(target, errorLabel = 'Native click failed:') { return InteractionUtils.safeNativeClick(target, errorLabel); }, startPollingTask(interval, onTick, errorLabel = 'Polling task failed:') { return InteractionUtils.startPollingTask(interval, onTick, errorLabel); }, wildcardToRegex(pattern) { return UrlUtils.wildcardToRegex(pattern); }, probeIframeForM3u8(src, context, sourceName) { IframeProbeUtils.probe(src, context, sourceName, this._iframeProbeCache, this._iframeProbeInflight); }, isM3U8(url) { return UrlUtils.isM3U8(url); }, copyToClipboard(text, successCallback) { return this.clipboard.writeText(text, successCallback); }, formatTime(seconds) { return UrlUtils.formatTime(seconds); }, getVideoFormat(url) { return UrlUtils.getVideoFormat(url, (value) => this.isM3U8(value)); }, _getNormalizedPath() { return PagePathScoreUtils.getNormalizedPath(); }, _calculatePathScore(path, full, hostname) { return PagePathScoreUtils.calculatePathScore(path, full, hostname, this.NoisePatterns); }, _calculateIdScore(path, full) { return PagePathScoreUtils.calculateIdScore(path, full, this.NoisePatterns); }, isHighNoisePage(context = null) { return PageSleepModel.evaluate(context).reason; }, discoverDynamicPlayButtons(root = document) { return DynamicPlayButtonUtils.discover(root, { findInteractiveAncestor: (el, options) => this.findInteractiveAncestor(el, options), isVisibleElement: (el) => this.isVisibleElement(el), getPrimaryMediaViewportRect: (scope) => this.getPrimaryMediaViewportRect(scope), isElementInPrimaryMediaRegion: (el, scope, mediaRect) => this.isElementInPrimaryMediaRegion(el, scope, mediaRect), }); }, }; const PageSleepModel = { _scoreCache: new Map(), THRESHOLDS: { SLEEP_SCORE: 50, MIN_MEDIA_WIDTH: 220, MIN_MEDIA_HEIGHT: 124, DOM_MEDIA_RECT: 12, DOM_VIDEO_TAG: 18, DOM_PLAYER_ATTR: 12, DOM_MEDIA_IFRAME: 10, DOM_PLAY_BUTTON: 8, PENDING_CANDIDATE: 12, }, evaluate(context = null) { const urlScore = this._getUrlScore(); const domEvidence = this._getDomEvidence(); const runtimeEvidence = this._getRuntimeEvidence(context); const adjustedUrlScore = this._getAdjustedUrlScore(urlScore, domEvidence, runtimeEvidence); const score = adjustedUrlScore + domEvidence.score + runtimeEvidence.score; return { score, urlScore: adjustedUrlScore, domScore: domEvidence.score, runtimeScore: runtimeEvidence.score, reasons: [...urlScore.reasons, ...domEvidence.reasons, ...runtimeEvidence.reasons], isSleeping: score < this.THRESHOLDS.SLEEP_SCORE, reason: score >= this.THRESHOLDS.SLEEP_SCORE ? false : `特征不足(得分${score})`, }; }, _getUrlScore() { const cacheKey = window.location.href; if (this._scoreCache.has(cacheKey)) { return this._scoreCache.get(cacheKey); } const { path, full, hostname } = Utils._getNormalizedPath(); const pathScore = Utils._calculatePathScore(path, full, hostname); const idScore = Utils._calculateIdScore(path, full); const result = { score: pathScore + idScore, reasons: [`path:${pathScore}`, `id:${idScore}`], }; this._scoreCache.set(cacheKey, result); return result; }, _getAdjustedUrlScore(urlScore, domEvidence, runtimeEvidence) { if (domEvidence.score > 0 || runtimeEvidence.score > 0) { return urlScore.score; } const { path, full } = Utils._getNormalizedPath(); if (this._isWeakDetailPath(path, full)) { return Math.min(urlScore.score, 24); } return urlScore.score; }, _isWeakDetailPath(path = '', full = '') { return PagePathScoreUtils.isWeakDetailPath(path, full, Utils.NoisePatterns); }, _getDomEvidence() { const T = this.THRESHOLDS; let score = 0; const reasons = []; try { const scope = document; const primaryMedia = this._findPrimaryMediaElement(scope); if (primaryMedia) { const rect = primaryMedia.getBoundingClientRect(); if (rect.width >= T.MIN_MEDIA_WIDTH && rect.height >= T.MIN_MEDIA_HEIGHT) { score += T.DOM_MEDIA_RECT; reasons.push('dom:primary_media'); } const attr = ( primaryMedia.id + ' ' + primaryMedia.className + ' ' + (primaryMedia.getAttribute('title') || '') ).toLowerCase(); if (primaryMedia.tagName === 'VIDEO') { score += T.DOM_VIDEO_TAG; reasons.push('dom:video_tag'); } else if ( primaryMedia.tagName === 'IFRAME' && /(play|video|vod|embed|stream|player|m3u8)/i.test(primaryMedia.src || '') ) { score += T.DOM_MEDIA_IFRAME; reasons.push('dom:media_iframe'); } if (/(player|video|jw|prism|dplayer|plyr|art)/.test(attr)) { score += T.DOM_PLAYER_ATTR; reasons.push('dom:player_attr'); } } if (document.readyState !== 'loading') { const dynamicButtons = Utils.discoverDynamicPlayButtons(scope); if (dynamicButtons.length > 0) { score += T.DOM_PLAY_BUTTON; reasons.push('dom:play_button'); } } } catch (e) { console.debug('PageSleepModel DOM evidence failed:', e); } return { score, reasons }; }, _findPrimaryMediaElement(root = document) { const scope = root && typeof root.querySelectorAll === 'function' ? root : document; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; let bestElement = null; let bestScore = -Infinity; for (const el of scope.querySelectorAll(CONSTANTS.SELECTORS.PLAYER_ELEMENTS)) { if (!Utils.isVisibleElement(el)) { continue; } if (el.closest(CONSTANTS.SELECTORS.DMZ_EXCLUSION)) { continue; } const rect = el.getBoundingClientRect(); if (rect.width < this.THRESHOLDS.MIN_MEDIA_WIDTH || rect.height < this.THRESHOLDS.MIN_MEDIA_HEIGHT) { continue; } if (rect.bottom < 0 || rect.top > viewportHeight || rect.right < 0 || rect.left > viewportWidth) { continue; } const score = Utils._getPrimaryMediaCandidateScore(el, rect, viewportWidth, viewportHeight); if (score > bestScore) { bestScore = score; bestElement = el; } } return bestElement; }, _getRuntimeEvidence(context) { const reasons = []; let score = 0; const pendingCount = context?.coreLogic?.pendingSpaCandidates?.length || 0; if (pendingCount > 0) { score += this.THRESHOLDS.PENDING_CANDIDATE; reasons.push('runtime:pending_candidate'); } return { score, reasons }; }, }; const SmartClickModel = { _clickedAt: new WeakMap(), _observedVideos: new WeakSet(), _playbackEventMarks: new WeakMap(), _lastPlaybackStateCache: { root: null, stamp: 0, value: null, }, _lastPlaybackAt: 0, _lastGlobalClickAt: 0, _lastClickFingerprint: null, CONFIG: { CLICK_COOLDOWN_MS: 300, GLOBAL_CLICK_SETTLE_MS: 500, PLAYBACK_SUCCESS_WINDOW_MS: 5000, MIN_CONFIDENCE_SCORE: 80, MIN_VIDEO_CONFIDENCE_SCORE: 200, FALLBACK_MIN_SCORE: 100, TIMEUPDATE_THROTTLE_MS: 800, PLAYBACK_STATE_CACHE_MS: 180, }, REGEX: { POSITIVE: /(play|start|resume|watch|bofang|播放|开始|继续|prism|jw|plyr|dplayer|video-js|big-play|overlaid|center-play|play-icon|player_overlays_play_button)/, NEGATIVE: /(translate|comment|chat|disqus|feedback|reply|submit|login|signup|register|share|download|like|collect|favorite|follow|unfollow|close|skip|ad-|ads|advert|tracker|history|record|记录|历史|mini|pip|picture-in-picture|fullscreen|volume|setting|mute|menu|subtitle|captions|danmu|弹幕|card|list|item|thumb|poster|recommend|related|next|prev|episode|guess|more|season|route|line|线路|选集|导航|search|menu|score|评分)/, PLAYER: /(player|video|media|stream|overlay|jw|prism|dplayer|plyr|art|vjs|fp-ui)/, }, ensurePlaybackObservers(root = document) { const scope = root && typeof root.querySelectorAll === 'function' ? root : document; scope.querySelectorAll('video').forEach((video) => { if (this._observedVideos.has(video)) { return; } this._observedVideos.add(video); const mark = (event) => { if (event?.type === 'timeupdate') { const now = performance.now(); const lastMark = this._playbackEventMarks.get(video) || 0; if (now - lastMark < this.CONFIG.TIMEUPDATE_THROTTLE_MS) { return; } this._playbackEventMarks.set(video, now); } if (!video.ended && !video.paused && video.readyState >= 2) { this.markPlaybackSuccess(video); } }; ['playing', 'timeupdate', 'loadeddata', 'canplay'].forEach((type) => { video.addEventListener(type, mark, { passive: true }); }); }); }, markPlaybackSuccess(video = null) { this._lastPlaybackAt = Date.now(); if (video && video.dataset) { video.dataset.dmzPlaybackConfirmed = '1'; } }, getPlaybackState(root = document) { this.ensurePlaybackObservers(root); const now = performance.now(); if ( this._lastPlaybackStateCache.root === root && this._lastPlaybackStateCache.value && now - this._lastPlaybackStateCache.stamp <= this.CONFIG.PLAYBACK_STATE_CACHE_MS ) { return this._lastPlaybackStateCache.value; } let result; if (Date.now() - this._lastPlaybackAt <= this.CONFIG.PLAYBACK_SUCCESS_WINDOW_MS) { result = { isPlaying: true, reason: 'recent_playback_event' }; } else { const scope = root && typeof root.querySelectorAll === 'function' ? root : document; const videos = Array.from(scope.querySelectorAll('video')); let best = null; let bestScore = -Infinity; for (const video of videos) { const score = this._getVideoPlaybackScore(video); if (score > bestScore) { bestScore = score; best = video; } if (this._isVideoActuallyPlaying(video)) { this.markPlaybackSuccess(video); result = { isPlaying: true, reason: 'visible_video_playing', video }; break; } } if (!result) { result = { isPlaying: false, reason: best ? 'best_video_not_playing' : 'no_video', }; } } this._lastPlaybackStateCache = { root, stamp: now, value: result, }; return result; }, shouldStopAutomation(context = null, root = document) { if (context?.playerManager?.isPlayerActiveOrInitializing || context?.coreLogic?.decisionInProgress) { return true; } return this.getPlaybackState(root).isPlaying; }, _isVideoActuallyPlaying(video) { if (!video || video.readyState < 2 || video.ended) { return false; } const rect = typeof video.getBoundingClientRect === 'function' ? video.getBoundingClientRect() : { width: 0, height: 0 }; if (rect.width < 120 || rect.height < 68) { return false; } return ( !video.paused && (video.currentTime > 0.12 || !!video.dataset.dmzPlaybackConfirmed || this._isTimeAdvancing(video)) ); }, _isTimeAdvancing(video) { const now = performance.now(); const lastTime = Number(video.dataset.dmzLastTime || 0); const lastStamp = Number(video.dataset.dmzLastStamp || 0); video.dataset.dmzLastTime = String(video.currentTime || 0); video.dataset.dmzLastStamp = String(now); if (!lastStamp) { return false; } return video.currentTime > lastTime && now - lastStamp <= 1500; }, _getVideoPlaybackScore(video) { const rect = video.getBoundingClientRect(); let score = rect.width * rect.height; if (Utils.isElementInPrimaryMediaRegion(video)) { score += 150000; } if (video.readyState >= 2) { score += 50000; } if (!video.paused) { score += 300000; } if (video.currentTime > 0) { score += 200000; } return score; }, getCandidateScore(el, options = {}) { if (!el || typeof el.getBoundingClientRect !== 'function') { return -Infinity; } const rect = el.getBoundingClientRect(); if (rect.width <= 0 || rect.height <= 0) { return -Infinity; } const interactive = Utils.findInteractiveAncestor(el, { stopNode: document.body, maxDepth: 4, }) || el; const target = interactive || el; const targetRect = target.getBoundingClientRect(); const attrStr = DomUtils.getAttrSignature(target); const area = targetRect.width * targetRect.height; let score = 0; const inMediaRegion = Utils.isElementInPrimaryMediaRegion(target); const hasPlayerContext = this._hasPlayerContext(target); if (!inMediaRegion) { score -= 180; } else { score += 90; } if (!hasPlayerContext) { score -= 120; } else { score += 80; } if (this.REGEX.POSITIVE.test(attrStr)) { score += 180; } if ( /(big-play|overlaid|center-play|play-icon|play-button|prism-big-play-btn|vjs-big-play-button|plyr__control--overlaid|dplayer-mobile-play|art-control-play|fp-ui)/.test( attrStr ) ) { score += 170; } if (target.tagName === 'BUTTON' || target.getAttribute('role') === 'button') { score += 90; } if (target.tagName === 'VIDEO') { score += options.allowVideoElement ? 30 : -220; } if (/^a$/i.test(target.tagName) && !(target.href || '').includes('javascript')) { score -= 160; } if (this.REGEX.NEGATIVE.test(attrStr)) { score -= 260; } if (area < 256) { score -= 120; } else if (area >= 400 && area <= 30000) { score += 70; } else if (area > window.innerWidth * window.innerHeight * 0.4) { score -= 180; } const mediaRect = Utils.getPrimaryMediaViewportRect(document); if (mediaRect) { const centerX = targetRect.left + targetRect.width / 2; const centerY = targetRect.top + targetRect.height / 2; const mediaCenterX = (mediaRect.left + mediaRect.right) / 2; const mediaCenterY = (mediaRect.top + mediaRect.bottom) / 2; const centerDistance = Math.abs(centerX - mediaCenterX) + Math.abs(centerY - mediaCenterY); score += Math.max(0, 700 - centerDistance) / 8; } if ( target.closest( '[id*="comment"],[class*="comment"],[id*="chat"],[class*="chat"],[class*="prev"],[class*="next"],[class*="episode"],[class*="recommend"],[class*="related"]' ) ) { score -= 300; } return score; }, isHighConfidenceTarget(el, options = {}) { const score = this.getCandidateScore(el, options); const threshold = el?.tagName === 'VIDEO' && options.allowVideoElement ? this.CONFIG.MIN_VIDEO_CONFIDENCE_SCORE : this.CONFIG.MIN_CONFIDENCE_SCORE; return Number.isFinite(score) && score >= threshold; }, canClickTarget(el) { if (!el) { return false; } const now = Date.now(); if (this.shouldStopAutomation(null)) { return false; } if (this._lastGlobalClickAt && now - this._lastGlobalClickAt < this.CONFIG.GLOBAL_CLICK_SETTLE_MS) { return false; } const last = this._clickedAt.get(el) || 0; return now - last >= this.CONFIG.CLICK_COOLDOWN_MS; }, recordClick(el) { if (el) { const now = Date.now(); this._clickedAt.set(el, now); this._lastGlobalClickAt = now; this._lastClickFingerprint = this.getElementFingerprint(el); } }, isInSettleWindow() { return ( !!this._lastGlobalClickAt && Date.now() - this._lastGlobalClickAt < this.CONFIG.GLOBAL_CLICK_SETTLE_MS ); }, getElementFingerprint(el) { if (!el || typeof el.getBoundingClientRect !== 'function') { return null; } const rect = el.getBoundingClientRect(); return [ el.tagName || '', el.id || '', String(el.className || '').slice(0, 120), Math.round(rect.left), Math.round(rect.top), Math.round(rect.width), Math.round(rect.height), ].join('|'); }, resolveClickTarget(el) { if (!el) { return null; } return ( Utils.findInteractiveAncestor(el, { stopNode: document.body, maxDepth: 4, }) || el ); }, getFallbackConfidence(el) { const score = this.getCandidateScore(el, { allowVideoElement: el?.tagName === 'VIDEO', }); return Number.isFinite(score) ? score : -Infinity; }, isSafeFallbackTarget(el) { if (!el || !Utils.isElementInPrimaryMediaRegion(el) || !this._hasPlayerContext(el)) { return false; } const score = this.getFallbackConfidence(el); if (score < this.CONFIG.FALLBACK_MIN_SCORE) { return false; } const mediaRect = Utils.getPrimaryMediaViewportRect(document); if (!mediaRect || typeof el.getBoundingClientRect !== 'function') { return true; } const rect = el.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; const mediaCenterX = (mediaRect.left + mediaRect.right) / 2; const mediaCenterY = (mediaRect.top + mediaRect.bottom) / 2; const dx = Math.abs(centerX - mediaCenterX); const dy = Math.abs(centerY - mediaCenterY); const maxDx = Math.max(48, (mediaRect.right - mediaRect.left) * 0.32); const maxDy = Math.max(36, (mediaRect.bottom - mediaRect.top) * 0.28); return dx <= maxDx && dy <= maxDy; }, _hasPlayerContext(el) { return !!el?.closest(CONSTANTS.SELECTORS.PLAYER_CONTEXT); }, }; const EventSubscriptionUtils = { add(store, element, eventType, handler, options) { if (!element || typeof element.addEventListener !== 'function' || typeof handler !== 'function') { return () => {}; } const entry = { element, eventType, handler, options }; element.addEventListener(eventType, handler, options); store.push(entry); return () => this.remove(store, entry); }, remove(store, entry) { if (!entry) { return; } entry.element?.removeEventListener(entry.eventType, entry.handler, entry.options); const index = store.indexOf(entry); if (index !== -1) { store.splice(index, 1); } }, removeAll(store) { for (const entry of RefactorCollectionUtils.snapshot(store)) { this.remove(store, entry); } }, }; const LogEntryFactory = { create(message, type, details = null) { return { type: type || CONSTANTS.LOG_TYPES.INFO, message, frame: CONSTANTS.IS_TOP_FRAME ? 'Top' : 'iFrame', timestamp: Date.now(), details: details && typeof details === 'object' ? { ...details } : null, }; }, }; const LogStoreUtils = { push(store, entry, maxEntries) { const limit = Number.isFinite(maxEntries) && maxEntries > 0 ? maxEntries : Infinity; while (store.length >= limit) { store.shift(); } store.push(entry); return entry; }, getAll(store) { return RefactorCollectionUtils.snapshot(store); }, }; class EventManager { constructor() { this.listeners = []; } add(element, eventType, handler, options) { return EventSubscriptionUtils.add(this.listeners, element, eventType, handler, options); } removeAll() { EventSubscriptionUtils.removeAll(this.listeners); } } class ActionLogger { constructor() { this.logs = []; this.maxEntries = CONSTANTS.LIMITS.MAX_LOG_ENTRIES; this.context = null; } setContext(context) { this.context = context; } log(message, type = CONSTANTS.LOG_TYPES.INFO, details = null) { const logEntry = LogStoreUtils.push( this.logs, LogEntryFactory.create(message, type, details), this.maxEntries ); EventBus.emit('LOG_ENTRY', logEntry); } getLogs() { return LogStoreUtils.getAll(this.logs); } } class SettingsManager extends BaseModule { constructor(context) { super(context, 'SettingsManager'); this.defaults = { autoPlay: true, blacklist: ['github.com', 'stackoverflow.com', 'developer.mozilla.org', '*youtube.com'], crossFrameSupport: true, defaultPlaybackRate: 1.0, longPressRate: 2.0, enableDecryptionHook: true, enableNetworkInterceptor: true, enablePlayerTakeover: true, isMuted: false, isSmartSlicingEnabled: true, lastVolume: 0.8, maxRetryCount: 1, enableJsonInspector: true, enableSetAttributeHooker: true, }; this.config = {}; } async load() { this.config = (await GM_getValue('dmz_v2_settings', {})) ?? {}; if (this.config.floatingPlayerPos) { delete this.config.floatingPlayerPos; } this.config = { ...this.defaults, ...this.config }; State.config = this.config; this.log('加载完成。', CONSTANTS.LOG_TYPES.CONFIG); } async save(newConfig, applyLive = true) { const oldConfig = { ...this.config }; this.config = { ...this.config, ...newConfig }; State.config = this.config; await GM_setValue('dmz_v2_settings', this.config); if (applyLive) { this.log('配置已保存', CONSTANTS.LOG_TYPES.CONFIG); EventBus.emit('CONFIG_UPDATED', { oldConfig, newConfig: this.config }); this.context.frameCommunicator.showNotification('设置已保存并应用。部分更改可能需要刷新页面才能生效。'); } } async reset() { const oldConfig = { ...this.config }; const persistentState = { lastVolume: this.config.lastVolume, isMuted: this.config.isMuted, }; this.config = { ...this.defaults, ...persistentState }; State.config = this.config; await GM_setValue('dmz_v2_settings', this.config); EventBus.emit('CONFIG_UPDATED', { oldConfig, newConfig: this.config }); this.log('配置已重置为默认值', CONSTANTS.LOG_TYPES.CONFIG); this.context.frameCommunicator.showNotification('已恢复为默认设置。'); return this.config; } _getSiteKey(hostname) { const host = hostname || window.location.hostname; const match = host.match( /([a-z0-9-]+\.(?:com|net|org|gov|edu|[a-z]{2})\.[a-z]{2}|[a-z0-9-]+\.[a-z0-9-]+)$/i ); return match ? match[0] : host; } async savePlayerPosition(hostname, orientationKey, pos) { const positions = await GM_getValue('dmz_player_positions_v2', {}); const siteKey = this._getSiteKey(hostname); if (!positions[siteKey]) { positions[siteKey] = {}; } positions[siteKey][orientationKey] = pos; await GM_setValue('dmz_player_positions_v2', positions); } async loadPlayerPosition(hostname, orientationKey) { const positions = await GM_getValue('dmz_player_positions_v2', {}); const siteKey = this._getSiteKey(hostname); return positions?.[siteKey]?.[orientationKey] || null; } } const DiagnosticsDescriptors = { MAX_TIMELINE_ENTRIES: Number.MAX_SAFE_INTEGER, PLAYBACK_HEALTH_TEMPLATE: { manifest: { status: 'pending', code: null }, key: { status: 'pending', code: null }, media: { status: 'pending', reason: null }, segments: { status: 'pending', errorCount: 0, consecutiveErrors: 0 }, }, SLICING_FEATURE_KEYS: ['URL_PATTERN', 'BEHAVIOR_MODEL'], }; const DiagnosticsFactoryUtils = { createTakeoverSnapshot(candidate = null) { if (!candidate) { return null; } return { sources: Array.from(candidate.sources ?? []), sourceName: Array.from(candidate.sources ?? []).join(' + '), url: candidate.url ?? null, iframeSource: candidate.iframeSource || null, iframeOrigin: candidate.iframeOrigin || null, viaMediaElement: !!candidate.mediaElement, hasInlineM3u8Text: !!candidate.m3u8Text, capturedAt: new Date().toISOString(), }; }, createTakeoverEvidence(candidate = null) { const snapshot = this.createTakeoverSnapshot(candidate); return { sources: snapshot?.sources ?? [], url: snapshot?.url ?? null, iframeSource: snapshot?.iframeSource ?? null, iframeOrigin: snapshot?.iframeOrigin ?? null, initialWinner: snapshot, finalWinner: snapshot, switchHistory: [], suppressedCandidates: [], switchCount: 0, }; }, createPlaybackHealth() { return { manifest: { ...DiagnosticsDescriptors.PLAYBACK_HEALTH_TEMPLATE.manifest }, key: { ...DiagnosticsDescriptors.PLAYBACK_HEALTH_TEMPLATE.key }, media: { ...DiagnosticsDescriptors.PLAYBACK_HEALTH_TEMPLATE.media }, segments: { ...DiagnosticsDescriptors.PLAYBACK_HEALTH_TEMPLATE.segments }, }; }, createSlicingReport() { return { totalGroups: 0, slicedGroups: 0, slicedSegments: 0, slicedDuration: 0, slicedTimeRanges: [], activatedEngines: new Set(), foundFeatures: new Map( DiagnosticsDescriptors.SLICING_FEATURE_KEYS.map((key) => [key, new Set()]) ), }; }, annotateTimelineEvent(event, startTime, sequence) { const now = new Date(); return { ...event, time: now, relativeTime: `+${((now - startTime) / 1000).toFixed(3)}s`, sequence, }; }, }; class DiagnosticsTool extends BaseModule { constructor(context) { super(context, 'DiagnosticsTool'); this.eventTimeline = []; this.fatalNetworkErrors =[]; this.maxLogEntries = DiagnosticsDescriptors.MAX_TIMELINE_ENTRIES; this.startTime = new Date(); this.takeoverEvidence = DiagnosticsFactoryUtils.createTakeoverEvidence(); this.lastProcessedM3u8 = null; this.playbackHealth = {}; this.slicingReport = {}; this.logSequence = 0; this._lastReportSequence = -1; this._cachedReportData = null; this.boundLogEvent = (event) => this.logEvent(event); this.resetPlaybackHealth(); } init() { EventBus.on('LOG_ENTRY', this.boundLogEvent); } captureTakeoverEvidence(candidate) { const snapshot = DiagnosticsFactoryUtils.createTakeoverSnapshot(candidate); if (!snapshot) { return; } if (!this.takeoverEvidence?.initialWinner) { this.takeoverEvidence = DiagnosticsFactoryUtils.createTakeoverEvidence(candidate); return; } const evidence = this.takeoverEvidence; const finalWinner = evidence.finalWinner; const sameUrl = (finalWinner?.url || '') === (snapshot.url || ''); const sameSource = (finalWinner?.sourceName || '') === (snapshot.sourceName || ''); evidence.sources = snapshot.sources; evidence.url = snapshot.url; evidence.iframeSource = snapshot.iframeSource; evidence.iframeOrigin = snapshot.iframeOrigin; if (!sameUrl || !sameSource) { evidence.switchHistory = Array.isArray(evidence.switchHistory) ? evidence.switchHistory : []; evidence.switchHistory.push({ from: this._serializeTakeoverSnapshot(finalWinner), to: this._serializeTakeoverSnapshot(snapshot), switchedAt: new Date().toISOString(), }); evidence.switchCount = evidence.switchHistory.length; } evidence.finalWinner = snapshot; } logEvent(event) { const timelineEvent = DiagnosticsFactoryUtils.annotateTimelineEvent( event, this.startTime, this.logSequence++ ); this.eventTimeline.push(timelineEvent); if (timelineEvent.type === 'FATAL_NET_ERROR') { this.fatalNetworkErrors.push(timelineEvent); } this._cachedReportData = null; } resetPlaybackHealth() { this.playbackHealth = DiagnosticsFactoryUtils.createPlaybackHealth(); } resetSlicingReport() { this.slicingReport = DiagnosticsFactoryUtils.createSlicingReport(); } _serializeSlicingReport(report = this.slicingReport ?? {}) { return { totalGroups: report.totalGroups ?? 0, slicedGroups: report.slicedGroups ?? 0, slicedSegments: report.slicedSegments ?? 0, slicedDuration: report.slicedDuration ?? 0, slicedTimeRanges: Array.isArray(report.slicedTimeRanges) ? report.slicedTimeRanges.map((range) => ({ ...range })) : [], activatedEngines: Array.from(report.activatedEngines ?? []), foundFeatures: Object.fromEntries( Array.from(report.foundFeatures ?? []).map(([key, value]) => [key, Array.from(value ?? [])]) ), }; } _serializeTimelineEvent(event) { const { frame, ...rest } = event || {}; return { ...rest, time: event.time instanceof Date ? event.time.toISOString() : event.time, details: event.details && typeof event.details === 'object' ? { ...event.details } : null, }; } _pickRecentTimelineEvents(limit = null) { const events = Array.isArray(this.eventTimeline) ? this.eventTimeline : []; const picked = typeof limit === 'number' ? events.slice(-limit) : events; const ordered = picked.slice().reverse(); return this._collapseAdjacentDuplicateLogs(ordered).map((event) => this._serializeTimelineEvent(event)); } _serializeTakeoverSnapshot(snapshot) { if (!snapshot) { return null; } return { ...snapshot, sources: Array.from(snapshot.sources ?? []), }; } _serializeTakeoverEvidence() { const evidence = this.takeoverEvidence ?? DiagnosticsFactoryUtils.createTakeoverEvidence(); return { ...evidence, sources: Array.from(evidence.sources ?? []), initialWinner: this._serializeTakeoverSnapshot(evidence.initialWinner), finalWinner: this._serializeTakeoverSnapshot(evidence.finalWinner), switchHistory: Array.from(evidence.switchHistory ?? []).map((entry) => { if (entry && (entry.from || entry.to)) { return { from: this._serializeTakeoverSnapshot(entry.from), to: this._serializeTakeoverSnapshot(entry.to), switchedAt: entry.switchedAt ?? null, }; } return this._serializeTakeoverSnapshot(entry); }), suppressedCandidates: Array.from(evidence.suppressedCandidates ?? []).map((entry) => this._serializeTakeoverSnapshot(entry)), switchCount: evidence.switchCount ?? 0, }; } _serializePlaybackHealth() { const health = JSON.parse(JSON.stringify(this.playbackHealth ?? {})); const playerManager = this.context?.playerManager; const formatName = playerManager?.currentVideoFormat?.name ?? ''; const isM3U8Type = formatName === 'M3U8' || !!playerManager?.lastM3u8Content; if (!isM3U8Type) { health.manifest = { status: 'not_applicable', code: null, reason: 'normal_file' }; health.key = { status: 'not_applicable', code: null, reason: 'normal_file' }; health.segments = { status: 'not_applicable', errorCount: 0, consecutiveErrors: 0, reason: 'normal_file', }; } return health; } _collapseAdjacentDuplicateLogs(logs) { if (!Array.isArray(logs) || logs.length <= 1) { return Array.isArray(logs) ? logs.map((log) => (log && typeof log === 'object' && !RefactorCollectionUtils.hasOwn(log, 'repeatCount') ? { ...log, repeatCount: 1 } : log)) : []; } const collapsed = []; let previous = null; let count = 0; const flush = () => { if (!previous) { return; } if (count > 1) { collapsed.push({ ...previous, repeatCount: count, message: `${previous.message}(同类×${count})`, }); } else { collapsed.push({ ...previous, repeatCount: 1, }); } }; logs.forEach((log) => { const isSameAsPrevious = previous && previous.type === log.type && previous.message === log.message; if (isSameAsPrevious) { count += 1; return; } flush(); previous = log; count = 1; }); flush(); return collapsed; } _extractTimelineUrl(message = '') { if (typeof message !== 'string' || !message) { return null; } const match = message.match(/https?:\/\/\S+/); return match ? match[0] : null; } _createPlaybackSessionBase(lockEvent = null, startHintEvent = null, index = 0) { const hintTime = startHintEvent?.time instanceof Date ? startHintEvent.time.toISOString() : (startHintEvent?.time || null); const lockTime = lockEvent?.time instanceof Date ? lockEvent.time.toISOString() : (lockEvent?.time || null); return { sessionId: index + 1, startSequence: lockEvent?.sequence ?? startHintEvent?.sequence ?? null, startRelativeTime: lockEvent?.relativeTime ?? startHintEvent?.relativeTime ?? null, startedAt: lockTime ?? hintTime, triggerEvent: startHintEvent ? this._serializeTimelineEvent(startHintEvent) : null, lockEvent: lockEvent ? this._serializeTimelineEvent(lockEvent) : null, triggerType: startHintEvent?.message?.includes('播放器触发器被点击') ? 'manual_click' : 'auto_or_passive', sourceName: null, initialSourceName: null, finalSourceName: null, currentUrl: lockEvent ? this._extractTimelineUrl(lockEvent.message) : null, initialUrl: null, finalUrl: null, urls: [], switchCount: 0, sessionSwitchHistory: [], streamSelection: { isMasterPlaylist: false, variantCount: 0, selectedQualityLabel: null, selectedVariantUrl: null, }, keyPrefetch: { detectedCount: 0, successCount: 0, failedCount: 0, messages: [], }, health: { manifest: 'pending', key: 'pending', media: 'pending', segments: 'pending', }, outcome: 'pending', outcomeMessage: null, eventRange: { startSequence: lockEvent?.sequence ?? startHintEvent?.sequence ?? null, endSequence: lockEvent?.sequence ?? startHintEvent?.sequence ?? null, }, eventCount: 0, events: [], }; } _attachEventToPlaybackSession(session, event) { if (!session || !event) { return; } const serialized = this._serializeTimelineEvent(event); session.events.push(serialized); session.eventCount = session.events.length; session.eventRange.endSequence = event.sequence ?? session.eventRange.endSequence; const url = this._extractTimelineUrl(event.message); if (url) { session.currentUrl = url; if (!session.initialUrl) { session.initialUrl = url; } session.finalUrl = url; if (!session.urls.includes(url)) { session.urls.push(url); } } if (typeof event.message === 'string') { if (event.message.includes('来源:')) { const sourceMatch = event.message.match(/来源:([^。\]]+)/); if (sourceMatch?.[1]) { const nextSource = sourceMatch[1].trim(); if (!session.initialSourceName) { session.initialSourceName = nextSource; } if (session.finalSourceName && session.finalSourceName !== nextSource) { session.sessionSwitchHistory.push({ from: session.finalSourceName, to: nextSource, relativeTime: event.relativeTime ?? null, sequence: event.sequence ?? null, }); } session.finalSourceName = nextSource; session.sourceName = nextSource; session.switchCount = session.sessionSwitchHistory.length; } } if (event.message.includes('发现MasterPlaylist')) { session.streamSelection.isMasterPlaylist = true; } else if (event.message.includes('解析完成,共')) { const countMatch = event.message.match(/解析完成,共(\d+)个流/); const qualityMatch = event.message.match(/自动选择最高清晰度:([^。]+)/); session.streamSelection.variantCount = countMatch ? parseInt(countMatch[1], 10) : session.streamSelection.variantCount; session.streamSelection.selectedQualityLabel = qualityMatch?.[1]?.trim() || session.streamSelection.selectedQualityLabel; } else if (event.message.includes('M3U8处理完成: 最终URL为') && url) { session.streamSelection.selectedVariantUrl = url; } if (event.message.includes('[Key预取]|🔑|检测到')) { const countMatch = event.message.match(/检测到(\d+)个唯一密钥/); session.keyPrefetch.detectedCount = countMatch ? parseInt(countMatch[1], 10) : session.keyPrefetch.detectedCount; session.keyPrefetch.messages.push(event.message); } else if (event.message.includes('[Key预取]|🔑|已预取')) { const countMatch = event.message.match(/已预取(\d+)个密钥/); session.keyPrefetch.successCount = countMatch ? parseInt(countMatch[1], 10) : session.keyPrefetch.successCount; session.keyPrefetch.messages.push(event.message); session.health.key = 'success'; } else if (event.message.includes('[Key预取]|🔑|预取失败')) { session.keyPrefetch.failedCount += 1; session.keyPrefetch.messages.push(event.message); session.health.key = 'error'; } if (event.message.includes('清单解析完成')) { session.health.manifest = 'success'; } if (event.message.includes('首个分片下载成功')) { session.health.segments = 'success'; } if (event.message.includes('视频元数据就绪') || event.message.includes('自动播放|▶️|已启动')) { session.health.media = 'success'; session.outcome = 'success'; session.outcomeMessage = event.message; } else if (event.message.includes('密钥加载失败')) { session.health.key = 'error'; session.outcome = 'key_error'; session.outcomeMessage = event.message; } else if (event.message.includes('视频元数据加载超时')) { session.health.media = 'error'; session.outcome = 'metadata_timeout'; session.outcomeMessage = event.message; } else if (event.message.includes('网络错误重试上限') || event.message.includes('停止播放')) { session.health.segments = 'error'; session.outcome = 'network_error'; session.outcomeMessage = event.message; } } } _buildPlaybackSessions() { const sessions = []; const timeline = Array.isArray(this.eventTimeline) ? this.eventTimeline.slice() : []; let currentSession = null; let pendingTriggerEvent = null; const startSession = (lockEvent = null) => { currentSession = this._createPlaybackSessionBase(lockEvent, pendingTriggerEvent, sessions.length); sessions.push(currentSession); pendingTriggerEvent = null; }; timeline.forEach((event) => { const message = event?.message || ''; const isManualTrigger = message.includes('播放器触发器被点击,系统已重置,准备捕获视频流') || message.includes('用户换集意图触发') || message.includes('检测到重播意图'); if (isManualTrigger) { pendingTriggerEvent = event; return; } const isLockEvent = message.includes('决策锁定:['); if (isLockEvent) { if (!currentSession || pendingTriggerEvent) { startSession(event); } } if (!currentSession && (isLockEvent || message.includes('指令已接收,准备渲染视频内容。'))) { startSession(isLockEvent ? event : null); } if (currentSession) { this._attachEventToPlaybackSession(currentSession, event); } }); return sessions.map((session) => ({ ...session, finalUrl: session.finalUrl || session.currentUrl || null, sourceName: session.finalSourceName || session.initialSourceName || session.sourceName || null, })); } _normalizeSessionHealth(session) { const health = session?.health ? { ...session.health } : { manifest: 'pending', key: 'pending', media: 'pending', segments: 'pending', }; const playerManager = this.context?.playerManager; const formatName = playerManager?.currentVideoFormat?.name ?? ''; const sessionUrls = Array.isArray(session?.urls) ? session.urls : []; const isM3U8Type = formatName === 'M3U8' || sessionUrls.some((url) => UrlUtils.isM3U8(url)) || !!playerManager?.lastM3u8Content; if (!isM3U8Type) { return { manifest: 'not_applicable', key: 'not_applicable', media: health.media, segments: 'not_applicable', }; } return health; } _summarizePlaybackSession(session) { if (!session) { return null; } return { sessionId: session.sessionId, startSequence: session.startSequence, startRelativeTime: session.startRelativeTime, startedAt: session.startedAt, triggerType: session.triggerType, sourceName: session.sourceName, initialSourceName: session.initialSourceName, finalSourceName: session.finalSourceName, currentUrl: session.currentUrl, initialUrl: session.initialUrl, urls: Array.isArray(session.urls) ? session.urls.slice() : [], keyPrefetch: { detectedCount: session.keyPrefetch?.detectedCount ?? 0, successCount: session.keyPrefetch?.successCount ?? 0, failedCount: session.keyPrefetch?.failedCount ?? 0, messages: Array.isArray(session.keyPrefetch?.messages) ? session.keyPrefetch.messages.slice() : [], }, health: this._normalizeSessionHealth(session), streamSelection: session.streamSelection ? { ...session.streamSelection } : null, switchCount: session.switchCount ?? 0, sessionSwitchHistory: Array.isArray(session.sessionSwitchHistory) ? session.sessionSwitchHistory.map((entry) => ({ ...entry })) : [], outcome: session.outcome, outcomeMessage: session.outcomeMessage, eventRange: session.eventRange ? { ...session.eventRange } : null, eventCount: session.eventCount ?? 0, finalUrl: session.finalUrl, }; } getStructuredReportData() { if (this._lastReportSequence === this.logSequence && this._cachedReportData) { return this._cachedReportData; } this._cachedReportData = { scriptName: CONSTANTS.SCRIPT_NAME, scriptVersion: CONSTANTS.SCRIPT_VERSION, pageUrl: window.location.href, startedAt: this.startTime.toISOString(), totalEventCount: this.eventTimeline.length, totalFatalNetworkErrors: this.fatalNetworkErrors.length, takeoverEvidence: this._serializeTakeoverEvidence(), playbackHealth: this._serializePlaybackHealth(), slicingReport: this._serializeSlicingReport(), fatalNetworkErrors: this.fatalNetworkErrors.map((event) => this._serializeTimelineEvent(event)), playSessions: this._buildPlaybackSessions().map((session) => this._summarizePlaybackSession(session)).filter(Boolean).reverse(), eventTimeline: this._pickRecentTimelineEvents(), }; this._lastReportSequence = this.logSequence; return this._cachedReportData; } _collectM3u8PathStats(lines, baseUrl) { const pathStats = new Map(); for (const line of lines) { const trimmed = line.trim(); if (!this._isM3u8PathStatLine(trimmed)) { continue; } this._recordM3u8PathStat(pathStats, trimmed, baseUrl); } return pathStats; } _isM3u8PathStatLine(trimmed) { return (trimmed.endsWith('.ts') || trimmed.includes('.jpeg')) && !trimmed.startsWith('#'); } _recordM3u8PathStat(pathStats, trimmed, baseUrl) { try { const url = new URL(trimmed, baseUrl); const path = url.pathname; const lastSlash = path.lastIndexOf('/'); const basePath = lastSlash > -1 ? path.substring(0, lastSlash + 1) : '/'; pathStats.set(basePath, (pathStats.get(basePath) || 0) + 1); } catch (e) { console.debug('Failed to parse URL in M3U8 pattern analysis:', e); } } _collectDistinctPathSuggestions(sortedPaths) { const [mainPath, mainPathCount] = sortedPaths[0]; const suggestions = new Set(); for (let i = 1; i < sortedPaths.length; i++) { const [suspectPath, suspectPathCount] = sortedPaths[i]; if (suspectPathCount >= mainPathCount / 2) { continue; } const keyword = this._extractDistinctPathKeyword(mainPath, suspectPath); if (!keyword) { continue; } suggestions.add(keyword); this.log(`基于路径独特性发现高嫌疑关键词: ${keyword}`, CONSTANTS.LOG_TYPES.CORE_SLICE); } return suggestions; } _extractDistinctPathKeyword(mainPath, suspectPath) { const adParts = suspectPath.split('/').filter(Boolean); const baselineParts = mainPath.split('/').filter(Boolean); for (let j = 0; j < Math.min(adParts.length, baselineParts.length); j++) { if (adParts[j] === baselineParts[j]) { continue; } if (/^\d+$/.test(adParts[j]) || /^(1080|720|480|360)p?$/i.test(adParts[j])) { continue; } return adParts[j]; } return null; } _commitUrlPatternSuggestions(suggestions) { if (suggestions.size === 0) { return null; } this.slicingReport.activatedEngines.add('URL_PATTERN'); suggestions.forEach((s) => this.slicingReport.foundFeatures.get('URL_PATTERN').add(s)); return Array.from(suggestions); } analyzeUrlPatterns(m3u8Content, baseUrl) { if (!m3u8Content || typeof m3u8Content !== 'string' || !baseUrl) { return null; } const pathStats = this._collectM3u8PathStats(m3u8Content.split('\n'), baseUrl); if (pathStats.size <= 1) { this.log(`所有分片路径统一,无法通过路径独特性分析。`, CONSTANTS.LOG_TYPES.CORE_SLICE); return null; } const sortedPaths = Array.from(pathStats.entries()).sort((a, b) => b[1] - a[1]); const suggestions = this._collectDistinctPathSuggestions(sortedPaths); return this._commitUrlPatternSuggestions(suggestions); } _buildDeveloperReportHeader() { const nl = '\n'; let reportHeader = `大魔王视频助手 开发者日志 (版本: ${CONSTANTS.SCRIPT_VERSION})${nl}页面URL: ${window.location.href}${nl}`; if (this.fatalNetworkErrors.length > 0) { reportHeader += `--- 深度网络故障分析 ---${nl}`; this.fatalNetworkErrors.forEach((err, i) => { reportHeader += `[错误${i + 1}]@${err.relativeTime}:${err.message}${nl}`; }); } return reportHeader + `--- 统一事件时间轴 ---${nl}`; } _escapeDeveloperReportHtml(s) { return s.replace(//g, '>'); } _sortDeveloperTimelineLogs() { const sortedLogs = [...this.eventTimeline].sort((a, b) => b.time - a.time || b.sequence - a.sequence); return this._collapseAdjacentDuplicateLogs(sortedLogs); } _createCollapsedNoiseLogBlock(collapsedLines) { return `
[+] 折叠了 ${collapsedLines.length} 条 "广告扫描/过滤" 噪音日志... (点击展开)
`; } _createCollapsedLongLogBlock(currentLineHtml) { return `
[+] 折叠了过长的硬编码数据... (点击展开)
`; } _isCollapsibleNoiseLog(log) { const collapsePattern = /(嵌入式扫描器发现广告链接,已跳过|\[路径特征分析\] URL判定为广告)/; return ( (log.type === CONSTANTS.LOG_TYPES.SCAN_WARN && collapsePattern.test(log.message)) || log.type === CONSTANTS.LOG_TYPES.CORE_SLICE ); } _shouldCollapseLongDeveloperLog(log) { return ( log.message.length > 500 && (log.message.includes('base64,') || log.message.includes('M3U8处理完成')) ); } _formatDeveloperLogLine(log) { const formattedType = log.type.replace(' ', ' [') + ']'; const detailText = log.details ? ` | ${JSON.stringify(log.details)}` : ''; const logMessage = `[${log.relativeTime}] ${formattedType} ${log.message}${detailText}`; return `
${this._escapeDeveloperReportHtml(logMessage)}
`; } _formatDeveloperLogLineText(log) { const formattedType = log.type.replace(' ', ' [') + ']'; const detailText = log.details ? ` | ${JSON.stringify(log.details)}` : ''; return `[${log.relativeTime}] ${formattedType} ${log.message}${detailText}`; } generateDeveloperReport() { const reportHeader = this._buildDeveloperReportHeader(); const sortedLogs = this._sortDeveloperTimelineLogs(); const lines = sortedLogs.map((log) => this._formatDeveloperLogLine(log)); return `
${this._escapeDeveloperReportHtml(reportHeader).replace(/\n/g, '
')}
${lines.join('')}`; } generateDeveloperReportText() { const reportHeader = this._buildDeveloperReportHeader(); const sortedLogs = this._sortDeveloperTimelineLogs(); const lines = [reportHeader.trimEnd()]; sortedLogs.forEach((log) => { lines.push(this._formatDeveloperLogLineText(log)); }); return lines.join('\n'); } _createDeveloperReportState(reportHeader) { return { htmlOutput: `
${this._escapeDeveloperReportHtml(reportHeader).replace(/\n/g, '
')}
`, collapsedLines: [], isCollapsing: false, }; } _appendDeveloperReportLog(reportState, log) { const currentLineHtml = this._formatDeveloperLogLine(log); if (this._isCollapsibleNoiseLog(log)) { this._appendCollapsedDeveloperReportLine(reportState, currentLineHtml); return; } this._flushDeveloperReportCollapseIfNeeded(reportState); reportState.htmlOutput += this._shouldCollapseLongDeveloperLog(log) ? this._createCollapsedLongLogBlock(currentLineHtml) : currentLineHtml; } _appendCollapsedDeveloperReportLine(reportState, currentLineHtml) { reportState.isCollapsing = true; reportState.collapsedLines.push(currentLineHtml); } _flushDeveloperReportCollapseIfNeeded(reportState) { if (!reportState.isCollapsing) { return; } this._flushCollapsedDeveloperReportLines(reportState); reportState.isCollapsing = false; } _flushCollapsedDeveloperReportLines(reportState) { if (reportState.collapsedLines.length === 0) { return; } reportState.htmlOutput += this._createCollapsedNoiseLogBlock(reportState.collapsedLines); reportState.collapsedLines = []; } } class FrameCommunicator extends BaseModule { constructor(context) { super(context, 'FrameCommunicator'); this.boundHandleMessage = this.handleMessage.bind(this); this.pendingCrossFetches = new Map(); this.crossFetchIframes = new Map(); this._iframeResolutionCache = { stamp: 0, all: [], visible: [], windowMap: new WeakMap(), }; this._commandCooldownMap = new WeakMap(); this.dispatchMap = { [CONSTANTS.MESSAGE_TYPES.IFRAME_TRIGGER_CLICK]: this._handleTriggerClick, DMZ_FALLBACK_SANDBOX_V1: this._handleFallbackSandbox, DMZ_FORCE_SLEEP: this._handleForceSleep, [CONSTANTS.MESSAGE_TYPES.RAW_SIGNAL_FORWARDED]: this._handleRawSignal, [CONSTANTS.MESSAGE_TYPES.QUERY_IS_MAIN_PLAYER]: this._handleQueryMainPlayer, [CONSTANTS.MESSAGE_TYPES.ACTION_LOG_FORWARDED]: this._handleLogForward, [CONSTANTS.MESSAGE_TYPES.ACTIVATE_AUTOPLAY]: this._handleActivateAutoplay, [CONSTANTS.MESSAGE_TYPES.NEUTRALIZE_COMMAND]: this._handleNeutralize, [CONSTANTS.MESSAGE_TYPES.RESTORE_COMMAND]: this._handleRestore, [CONSTANTS.MESSAGE_TYPES.FORCE_PAUSE_ALL]: this._handleForcePause, [CONSTANTS.MESSAGE_TYPES.M3U8_COMMAND]: this._handleM3u8Command, [CONSTANTS.MESSAGE_TYPES.SANDBOX_URL_FOUND]: this._handleSandboxUrlFound, [CONSTANTS.MESSAGE_TYPES.CROSS_FETCH_REQUEST]: this._handleCrossFetchRequest, [CONSTANTS.MESSAGE_TYPES.CROSS_FETCH_RESPONSE]: this._handleCrossFetchResponse, [CONSTANTS.MESSAGE_TYPES.I_AM_BLOCKED_BY_AD]: this._handleBlockedByAd, }; } _handleBlockedByAd(data, event) { if (CONSTANTS.IS_TOP_FRAME) { this.log(`[信使] 收到士兵被阻断的反馈,立即启动沙箱...`, CONSTANTS.LOG_TYPES.COMM); this.context.coreLogic.sandboxManager.reload(true); } } _handleCrossFetchResponse(data, event) { if (CONSTANTS.IS_TOP_FRAME) { const pending = this.pendingCrossFetches.get(data.requestId); if (pending) { if (data.error) { pending.reject(new Error(data.error)); } else { pending.resolve(data.text); } this.pendingCrossFetches.delete(data.requestId); } } } async _handleCrossFetchRequest(data, event) { try { const response = await fetch(data.url); if (!response.ok) { throw new Error(`Status ${response.status}`); } const text = await response.text(); event.source.postMessage( { type: CONSTANTS.MESSAGE_TYPES.CROSS_FETCH_RESPONSE, requestId: data.requestId, text: text, }, event.origin ); } catch (err) { event.source.postMessage( { type: CONSTANTS.MESSAGE_TYPES.CROSS_FETCH_RESPONSE, requestId: data.requestId, error: err.message, }, event.origin ); } } requestCrossFrameFetch(url) { return new Promise((resolve, reject) => { const requestId = Date.now() + Math.random().toString(); this.pendingCrossFetches.set(requestId, { resolve, reject }); const origin = new URL(url).origin; let iframe = this.crossFetchIframes.get(origin); const sendMsg = () => { if (iframe.contentWindow) { iframe.contentWindow.postMessage( { type: CONSTANTS.MESSAGE_TYPES.CROSS_FETCH_REQUEST, url: url, requestId: requestId, }, '*' ); } }; if (!iframe) { iframe = document.createElement('iframe'); iframe.style.display = 'none'; iframe.src = origin + '/'; document.body.appendChild(iframe); this.crossFetchIframes.set(origin, iframe); iframe.onload = sendMsg; iframe.onerror = () => reject(new Error('Iframe load error')); } else { sendMsg(); } setTimeout(() => { if (this.pendingCrossFetches.has(requestId)) { this.pendingCrossFetches.delete(requestId); reject(new Error('Cross frame fetch timeout')); } }, 3500); }); } _handleSandboxUrlFound(data, event) { if (CONSTANTS.IS_TOP_FRAME) { this.log(`[沙箱信使] 收到沙箱成功解析的底层播放地址,移交审查...`, CONSTANTS.LOG_TYPES.COMM); this.context.coreLogic.addTakeoverCandidate(data.candidateInfo); } } _handleM3u8Command(data, event) { if (CONSTANTS.IS_TOP_FRAME) { this.log(`接收到子框架加工完成的视频流,准备顶层渲染...`, CONSTANTS.LOG_TYPES.COMM); this.context.coreLogic.currentRequestId++; const videoData = data.action === 'PLAY_M3U8' ? { original: data.originalContent, processed: data.content, finalUrl: data.finalUrl, } : data.content; if (data.videoFormat) { this.context.playerManager.currentVideoFormat = data.videoFormat; } if (data.playbackHealth) { this.context.diagnosticsTool.playbackHealth = data.playbackHealth; } if (data.slicingReport) { this.context.diagnosticsTool.slicingReport = data.slicingReport; if (this.context.unifiedPanelManager?.hostElement?.classList?.contains(CONSTANTS.CLASSES.VISIBLE)) { requestAnimationFrame(() => { this.context.infoPanelManager.update(); this.context.infoPanelManager.renderDeveloperLog(); setTimeout(() => this.context.infoPanelManager.update(), 80); setTimeout(() => this.context.infoPanelManager.update(), 400); }); } } this.context.coreLogic.lastSuccessfulUrl = data.finalUrl; if (data.finalUrl && !data.finalUrl.startsWith('blob:')) { const finalKey = this.context.coreLogic._getNormalizationKey(data.finalUrl); if (finalKey) { this.context.coreLogic.globalSeenVideoUrls.add(finalKey); const currentPage = window.location.href.split('#')[0]; if (!this.context.coreLogic.pageVideoAssociations.has(currentPage)) { this.context.coreLogic.pageVideoAssociations.set(currentPage, new Set()); } this.context.coreLogic.pageVideoAssociations.get(currentPage).add(finalKey); } } this.context.coreLogic.decisionInProgress = true; this.context.playerManager.play(videoData); } } init() { window.addEventListener('message', this.boundHandleMessage); EventBus.on('LOG_ENTRY', (logEntry) => { const targetTypes = [ CONSTANTS.LOG_TYPES.CORE_NEUTRALIZE, CONSTANTS.LOG_TYPES.SCAN, CONSTANTS.LOG_TYPES.SCAN_WARN, CONSTANTS.LOG_TYPES.CORE_IFRAME, CONSTANTS.LOG_TYPES.CORE_EXEC, CONSTANTS.LOG_TYPES.CORE, CONSTANTS.LOG_TYPES.TAKEOVER_ATTEMPT, CONSTANTS.LOG_TYPES.TAKEOVER_FAIL, CONSTANTS.LOG_TYPES.WARN, CONSTANTS.LOG_TYPES.ERROR, ]; if (!CONSTANTS.IS_TOP_FRAME && targetTypes.includes(logEntry.type)) { this.postToTopFrame({ type: CONSTANTS.MESSAGE_TYPES.ACTION_LOG_FORWARDED, logEntry: { type: logEntry.type, message: logEntry.message }, }); } }); } handleMessage(event) { const data = event.data; if (!data || !data.type) { return; } const handler = this.dispatchMap[data.type]; if (handler) { handler.call(this, data, event); } } _getCachedIframeResolution(forceRefresh = false) { const now = performance.now(); if (!forceRefresh && now - this._iframeResolutionCache.stamp <= 800) { return this._iframeResolutionCache; } const all = Array.from(document.querySelectorAll('iframe')); const visible = []; const windowMap = new WeakMap(); all.forEach((frame) => { if (frame.contentWindow) { windowMap.set(frame.contentWindow, frame); } if (frame.offsetWidth > 50 && frame.offsetHeight > 50 && !this._isSandboxStyleFrame(frame)) { visible.push(frame); } }); this._iframeResolutionCache = { stamp: now, all, visible, windowMap, }; return this._iframeResolutionCache; } _postFrameCommandThrottled(targetWindow, origin, type, windowMs = 700) { if (!targetWindow) { return; } const now = Date.now(); const cooldowns = this._commandCooldownMap.get(targetWindow) || {}; const lastSentAt = Number(cooldowns[type] || 0); if (now - lastSentAt < windowMs) { return; } cooldowns[type] = now; this._commandCooldownMap.set(targetWindow, cooldowns); try { targetWindow.postMessage({ type }, origin); } catch (e) {} } _handleTriggerClick(data, event) { if (CONSTANTS.IS_TOP_FRAME) { this.context.coreLogic.handleRemoteTriggerClick(); } else { this.context.coreLogic.handleRemoteTriggerClick(); } } _handleFallbackSandbox() { if (!CONSTANTS.IS_TOP_FRAME) { this.log('收到顶层回退指令,执行本地沙箱重载...', CONSTANTS.LOG_TYPES.CORE_EXEC); this.context.coreLogic.sandboxManager.reload(); } } _handleForceSleep() { if (!CONSTANTS.IS_TOP_FRAME) { this.log('收到顶层休眠指令,停止所有活动。', CONSTANTS.LOG_TYPES.LIFECYCLE); this.context.coreLogic.restorePageToOriginalState(); } } _handleRawSignal(data, event) { if (!CONSTANTS.IS_TOP_FRAME) { return; } if (this.context.coreLogic.isSystemHalted || this.context.coreLogic.isRestoreInProgress) { this._postRestoreCommandToFrame(event); return; } if (this.context.playerManager.isPlayerActiveOrInitializing || this.context.coreLogic.decisionInProgress) { this._postFrameCommandThrottled( event.source, event.origin, CONSTANTS.MESSAGE_TYPES.FORCE_PAUSE_ALL, 700 ); } const candidate = data.payload; const sourceFrame = this._resolveSignalSourceFrame(event.source); if (sourceFrame) { if (candidate.survey && candidate.survey.videoSize) { sourceFrame.dataset.dmzVideoWidth = candidate.survey.videoSize.w; sourceFrame.dataset.dmzVideoHeight = candidate.survey.videoSize.h; } this.context.coreLogic.overlayManager.attachIframeTrigger(sourceFrame, event.source); } const simplifiedOrigin = event.origin .replace(/^https?:\/\/(?:www\.)?/, '') .split('/')[0] .split('.') .slice(-2) .join('.'); this.log(`收到|🛰|[iFrame信使@${simplifiedOrigin}]的新情报,正在提交审查...`, CONSTANTS.LOG_TYPES.CORE); if (sourceFrame) { const now = Date.now(); sourceFrame.dmzMediaEvidence = Math.max(Number(sourceFrame.dmzMediaEvidence || 0), 2); sourceFrame.dmzCooldownUntil = now + 8000; sourceFrame.dmzSettledUntil = now + 10000; } this.context.coreLogic.addTakeoverCandidate({ ...candidate, sourceName: `${CONSTANTS.TAKEOVER_SOURCES.CROSS_FRAME} (${candidate.sourceName})`, iframeSource: event.source, iframeOrigin: event.origin, }); } _postRestoreCommandToFrame(event) { try { event.source.postMessage({ type: CONSTANTS.MESSAGE_TYPES.RESTORE_COMMAND }, event.origin); } catch (e) { console.debug('Failed to post restore command:', e); } } _getIframePromotionKey(frame, origin = '') { try { const src = String(frame?.src || origin || '').toLowerCase(); const host = src ? new URL(src, location.href).hostname : String(origin || '').toLowerCase(); const rect = frame?.getBoundingClientRect?.() || { width: frame?.offsetWidth || 0, height: frame?.offsetHeight || 0, }; return `${host}|${Math.round(rect.width || 0)}x${Math.round(rect.height || 0)}`; } catch (e) { return String(origin || frame?.src || 'unknown'); } } _shouldThrottlePromotion(frame, origin = '', windowMs = 1600) { if (!frame) { return false; } const now = Date.now(); const key = this._getIframePromotionKey(frame, origin); const lastKey = String(frame.dmzLastPromotionKey || ''); const lastAt = Number(frame.dmzLastPromotionAt || 0); if (lastKey === key && lastAt && now - lastAt < windowMs) { return true; } frame.dmzLastPromotionKey = key; frame.dmzLastPromotionAt = now; return false; } _logCommanderLockOnce(frame, origin, score, windowMs = 3200) { const now = Date.now(); if (frame) { const key = this._getIframePromotionKey(frame, origin); if ( String(frame.dmzLastCommanderLogKey || '') === key && Number(frame.dmzLastCommanderLogAt || 0) && now - Number(frame.dmzLastCommanderLogAt || 0) < windowMs ) { return false; } frame.dmzLastCommanderLogKey = key; frame.dmzLastCommanderLogAt = now; } this.log(`指挥官:锁定目标[${origin}] (得分:${score}),执行激活!`, CONSTANTS.LOG_TYPES.CORE_IFRAME); return true; } _shouldSuppressForwardedFrameLog(originKey, message, windowMs = 4000) { if (!originKey || !message) { return false; } if (!this._frameLogDedupMap) { this._frameLogDedupMap = new Map(); } const normalized = String(message).replace(/\s+/g, ' ').trim(); const key = `${originKey}|${normalized}`; const now = Date.now(); const last = Number(this._frameLogDedupMap.get(key) || 0); if (last && now - last < windowMs) { return true; } this._frameLogDedupMap.set(key, now); return false; } _logIframeRedirectOnce(frame, origin = '', windowMs = 8000) { if (!frame) { return; } if (Number(frame.dmzSettledUntil || 0) > Date.now()) { return; } const now = Date.now(); const key = this._getIframePromotionKey(frame, origin); if ( String(frame.dmzLastRedirectLogKey || '') === key && Number(frame.dmzLastRedirectLogAt || 0) && now - Number(frame.dmzLastRedirectLogAt || 0) < windowMs ) { return; } frame.dmzLastRedirectLogKey = key; frame.dmzLastRedirectLogAt = now; this.log( `[修正] 无法精确匹配源或信号来自沙箱,触发器已重定向至高置信度外层iFrame。`, CONSTANTS.LOG_TYPES.UI ); } _isSourceInSandbox(frameSource) { const sandboxIframe = this.context.coreLogic.sandboxManager.iframeSandbox; if (!sandboxIframe || !sandboxIframe.contentWindow) { return false; } let found = false; const checkTree = (win) => { if (found) { return; } if (win === frameSource) { found = true; return; } try { for (let i = 0; i < win.frames.length; i++) { checkTree(win.frames[i]); } } catch (e) {} }; checkTree(sandboxIframe.contentWindow); return found; } _resolveSignalSourceFrame(frameSource) { if (this._isSourceInSandbox(frameSource)) { return null; } const iframeSnapshot = this._getCachedIframeResolution(); let sourceFrame = iframeSnapshot.windowMap.get(frameSource) || null; if (sourceFrame && this._isSandboxStyleFrame(sourceFrame)) { return null; } if (sourceFrame) { return sourceFrame; } const visibleIframes = iframeSnapshot.visible; if (visibleIframes.length === 0) { return null; } const ranked = visibleIframes .map((frame) => ({ frame, score: this._getSignalFrameScore(frame), profile: this._getFrameContextProfile(frame, frame?.src || ''), })) .sort( (a, b) => b.score - a.score || b.frame.offsetWidth * b.frame.offsetHeight - a.frame.offsetWidth * a.frame.offsetHeight ); const best = ranked[0]; if (!best) { return null; } if (!this._canPromoteIframeToTarget(best.frame, best.frame?.src || '', 'resolve-signal')) { return null; } this._logIframeRedirectOnce(best.frame, best.frame?.src || ''); return best.frame; } _isSandboxStyleFrame(frame) { return ( frame.id === 'dmz_sandbox_frame' || frame.style.opacity === '0.001' || frame.style.opacity === '0.01' || frame.style.width === '1px' ); } _isDefinitelyNonVideoIframe(frame, origin = '') { if (!frame) { return true; } if (this._isSandboxStyleFrame(frame)) { return true; } const src = String(frame?.src || '').toLowerCase(); const attrStr = ( String(frame?.id || '') + ' ' + String(frame?.className || '') + ' ' + String(frame?.getAttribute?.('title') || '') + ' ' + String(frame?.getAttribute?.('name') || '') + ' ' + String(frame?.getAttribute?.('aria-label') || '') ).toLowerCase(); const ancestorNodes = []; let cur = frame?.parentElement || null; for (let i = 0; cur && i < 5; i++, cur = cur.parentElement) { ancestorNodes.push((cur.id || '') + ' ' + (cur.className || '')); } const ancestorText = ancestorNodes.join(' ').toLowerCase(); const semanticText = `${src} ${origin} ${attrStr} ${ancestorText}`; const hardNegative = /(comment|comments|reply|review|disqus|discussion|forum|community|social|share|sharing|login|signin|signup|register|account|oauth|auth|captcha|recaptcha|newsletter|subscribe|widget|toolbar|floating|popup|banner|ads?|advert|tracker|analytics|cookie|consent)/i; const hardPositive = /(play|player|video|vod|watch|stream|m3u8|mp4|hls|dash|media|embed|source|jwplayer|dplayer|videojs|plyr)/i; const hardNegativeHost = /(^|[/.:-])(disqus|accounts\.google|googleapis|gstatic|recaptcha|facebook|instagram|twitter|x\.com|tiktok|reddit|pinterest|addthis|sharethis|taboola|outbrain|doubleclick|googlesyndication|criteo|hotjar|intercom|zendesk|onesignal)([/.:-]|$)/i; if (hardNegativeHost.test(semanticText)) { return true; } if (hardNegative.test(semanticText) && !hardPositive.test(semanticText)) { return true; } const rect = frame?.getBoundingClientRect?.() || { left: 0, top: 0, width: frame?.offsetWidth || 0, height: frame?.offsetHeight || 0, }; const width = Number(rect.width || frame?.offsetWidth || 0); const height = Number(rect.height || frame?.offsetHeight || 0); const viewportW = Math.max(window.innerWidth || document.documentElement.clientWidth || 0, 1); const viewportH = Math.max(window.innerHeight || document.documentElement.clientHeight || 0, 1); const centerX = (Number(rect.left || 0) + width / 2) / viewportW; const centerY = (Number(rect.top || 0) + height / 2) / viewportH; const farFromMainViewport = centerY > 0.82 || centerY < 0.08 || centerX < 0.1 || centerX > 0.9; const tinyOrPeripheral = (width < 260 || height < 160 || width * height < 90000) && farFromMainViewport; if ( hardNegative.test(semanticText) && (farFromMainViewport || tinyOrPeripheral) && !frame?.dmzMediaEvidence ) { return true; } if ( !frame?.dmzMediaEvidence && Number(frame?.dmzActivationMisses || 0) >= 1 && !hardPositive.test(semanticText) ) { return true; } if (frame?.dmzHardRejected) { return true; } return false; } _canPromoteIframeToTarget(frame, origin = '', reason = '') { if (!frame) { return false; } if (this._isDefinitelyNonVideoIframe(frame, origin)) { return false; } if (frame?.dmzHardRejected && !frame?.dmzMediaEvidence) { return false; } const score = this._scoreFrameContext(frame, origin); const profile = this._getFrameContextProfile(frame, origin); const hasMediaishContext = /(play|player|video|vod|watch|stream|episode|media|m3u8|mp4|hls|source|embed)/i.test( profile.semanticText ); const hasDirectPath = /(^|\/)(play|player|embed|watch)(\/|$)|\.m3u8(?:[?#]|$)|master\.m3u8(?:[?#]|$)|index\.m3u8(?:[?#]|$)/i.test( profile.src ); const hasEvidence = Number(frame?.dmzMediaEvidence || 0) > 0; if (hasEvidence) { return true; } if (Number(frame?.dmzActivationMisses || 0) >= 1 && !hasDirectPath) { return false; } if (!hasMediaishContext && !hasDirectPath) { return false; } if (score < 60 && !hasDirectPath) { return false; } return true; } _tokenizeHostLike(value) { return ( String(value || '') .toLowerCase() .match(/[a-z0-9]{3,}/g) || [] ); } _getFrameContextProfile(frame, origin = '') { const src = String(frame?.src || '').toLowerCase(); const rect = frame?.getBoundingClientRect?.() || { width: frame?.offsetWidth || 0, height: frame?.offsetHeight || 0, left: 0, top: 0, }; const width = Math.round(rect.width || frame?.offsetWidth || 0); const height = Math.round(rect.height || frame?.offsetHeight || 0); const area = width * height; const ratio = height > 0 ? width / height : 0; const attrStr = ( String(frame?.id || '') + ' ' + String(frame?.className || '') + ' ' + String(frame?.getAttribute?.('title') || '') + ' ' + String(frame?.getAttribute?.('name') || '') ).toLowerCase(); const allowStr = ( String(frame?.getAttribute?.('allow') || '') + ' ' + String(frame?.getAttribute?.('sandbox') || '') ).toLowerCase(); const parentText = frame?.parentElement ? (frame.parentElement.id || '') + ' ' + (frame.parentElement.className || '') : ''; const ancestorText = Array.from(frame?.closest ? [frame.closest('[class],[id]')] : []) .filter(Boolean) .map((el) => (el.id || '') + ' ' + (el.className || '')) .join(' ') .toLowerCase() + ' ' + String(parentText || '').toLowerCase(); const semanticText = `${src} ${origin} ${attrStr} ${allowStr} ${ancestorText}`; const viewportW = Math.max(window.innerWidth || document.documentElement.clientWidth || 0, 1); const viewportH = Math.max(window.innerHeight || document.documentElement.clientHeight || 0, 1); const centerX = (rect.left || 0) + width / 2; const centerY = (rect.top || 0) + height / 2; const centerBias = 1 - Math.min( 1.4, Math.abs(centerX - viewportW / 2) / viewportW + Math.abs(centerY - viewportH / 2) / viewportH ); let hostAffinity = 0; try { const pageHost = location.hostname.replace(/^www\./, ''); const frameHost = new URL(src, location.href).hostname.replace(/^www\./, ''); const pageCore = pageHost.split('.')[0]; if (pageHost === frameHost) { hostAffinity = 30; } else if (frameHost.includes(pageCore) || pageHost.includes(frameHost.split('.')[0])) { hostAffinity = 24; } else { const pageTokens = new Set(this._tokenizeHostLike(pageHost)); const frameTokens = new Set(this._tokenizeHostLike(frameHost + ' ' + origin)); let overlap = 0; for (const token of frameTokens) { if (pageTokens.has(token)) { overlap++; } } if (overlap > 0) { hostAffinity = 10 + Math.min(24, overlap * 8); } else if (frameTokens.size > 0) { hostAffinity = -6; } } } catch (e) {} return { src, rect, width, height, area, ratio, attrStr, allowStr, ancestorText, semanticText, centerBias, hostAffinity, }; } _scoreFrameContext(frame, origin = '') { const p = this._getFrameContextProfile(frame, origin); let score = 0; if (p.width < 50 || p.height < 50) { return -999; } if (p.area > 220000) { score += 36; } else if (p.area > 120000) { score += 24; } else if (p.area > 50000) { score += 12; } else { score -= 18; } if (p.ratio > 1.15 && p.ratio < 2.4) { score += 18; } else if (p.ratio >= 0.48 && p.ratio <= 0.75) { score += 8; } else if (p.ratio < 0.42 || p.ratio > 3.2) { score -= 28; } if (p.centerBias > 0.55) { score += 16; } else if (p.centerBias < 0.18) { score -= 16; } if ( /(play|player|video|vod|watch|stream|episode|media|m3u8|mp4|hls|source|dplayer|jwplayer|plyr|videojs)/i.test( p.semanticText ) ) { score += 34; } if (/(^|\/)play(\/|)|\/embed\/|\/player\/|\.m3u8|master\.m3u8|index\.m3u8|\/watch\//i.test(p.src)) { score += 28; } if ( /(comment|reply|forum|discuss|social|share|login|signin|signup|auth|oauth|captcha|widget|chat|ads?|banner|popup|subscribe|newsletter)/i.test( p.semanticText ) ) { score -= 90; } if (this._isDefinitelyNonVideoIframe(frame, origin) && !frame?.dmzMediaEvidence) { score -= 180; } if ( /(search|results|tag|category|archive|recommend|related|popular|trending)/i.test(p.semanticText) && !/(play|player|video|vod|watch|stream|m3u8)/i.test(p.semanticText) ) { score -= 36; } if ( /(allowfullscreen|fullscreen|autoplay|encrypted-media|picture-in-picture)/i.test(p.allowStr) || frame.hasAttribute('allowfullscreen') ) { score += 16; } if (/(comments?|review|disqus|respond|reply|login|signin|signup|share|social)/i.test(p.ancestorText)) { score -= 56; } score += p.hostAffinity; if (/(^|\/)play(\/| )|\/embed\/|\/player\/|\/vod\//i.test(p.src) && /[a-f0-9]{16,}/i.test(p.src)) { score += 20; } score -= Math.min(48, Number(frame?.dmzActivationMisses || 0) * 12); score += Math.min(60, Number(frame?.dmzMediaEvidence || 0) * 20); if (!frame?.dmzMediaEvidence && Number(frame?.dmzActivationAttempts || 0) >= 3) { score -= 18; } return score; } _getSignalFrameScore(frame) { return this._scoreFrameContext(frame, frame?.src || ''); } _handleQueryMainPlayer(data, event) { if (!CONSTANTS.IS_TOP_FRAME) { return; } if (this.context.coreLogic.isSystemHalted || this.context.coreLogic.isRestoreInProgress) { this._postForceSleepCommand(event); return; } this._pauseFrameMediaIfNeeded(event); const sourceFrame = this._resolveSignalSourceFrame(event.source); const origin = event.origin.toLowerCase(); if (sourceFrame) { sourceFrame.dmzMediaEvidence = Math.max(Number(sourceFrame.dmzMediaEvidence || 0), 1); sourceFrame.dmzActivationMisses = 0; if (this._shouldIgnoreQueryMainPlayerFrame(sourceFrame)) { return; } const frameScore = this._getQueryMainPlayerFrameScore(sourceFrame, origin); const src = (sourceFrame.src || '').toLowerCase(); const attrStr = ( sourceFrame.id + ' ' + sourceFrame.className + ' ' + (sourceFrame.getAttribute('title') || '') + ' ' + (sourceFrame.getAttribute('name') || '') ).toLowerCase(); const isTopActive = this.context.playerManager.isPlayerActiveOrInitializing || this.context.coreLogic.decisionInProgress; const shouldAttachTrigger = isTopActive || frameScore >= 35 || (frameScore >= 18 && (sourceFrame.hasAttribute('allowfullscreen') || /(play|video|vod|embed|stream|m3u8|player|jw|prism)/i.test(src) || /(player|video|media|stream|jw|prism)/i.test(attrStr))); if (shouldAttachTrigger) { this.context.coreLogic.overlayManager.attachIframeTrigger(sourceFrame, event.source); this._activateFrameAutoplayIfIdle(event, frameScore); } return; } this._blindActivateFrameOrigin(event, origin); } _postForceSleepCommand(event) { try { event.source.postMessage({ type: 'DMZ_FORCE_SLEEP' }, event.origin); } catch (e) { console.debug('Failed to force sleep iframe:', e); } } _pauseFrameMediaIfNeeded(event) { if ( !( this.context.playerManager.isPlayerActiveOrInitializing || this.context.coreLogic.takeoverCandidates.size > 0 || this.context.coreLogic.decisionInProgress ) ) { return; } try { event.source.postMessage({ type: CONSTANTS.MESSAGE_TYPES.FORCE_PAUSE_ALL }, event.origin); } catch (e) { console.debug('Failed to pause iframe media:', e); } } _shouldIgnoreQueryMainPlayerFrame(sourceFrame) { if (sourceFrame === this.context.coreLogic.sandboxManager.iframeSandbox) { return true; } return !this._canPromoteIframeToTarget(sourceFrame, sourceFrame?.src || '', 'query-main-player'); } _getQueryMainPlayerFrameScore(sourceFrame, origin) { return this._scoreFrameContext(sourceFrame, origin); } _activateFrameAutoplayIfIdle(event, score) { if (this.context.playerManager.isPlayerActiveOrInitializing || this.context.coreLogic.decisionInProgress) { return; } const sourceFrame = this._resolveSignalSourceFrame(event.source); if (sourceFrame) { const now = Date.now(); const lastActivatedAt = Number(sourceFrame.dmzLastActivatedAt || 0); if (!this._canPromoteIframeToTarget(sourceFrame, event.origin, 'activate-autoplay')) { sourceFrame.dmzActivationMisses = Number(sourceFrame.dmzActivationMisses || 0) + 1; sourceFrame.dmzCooldownUntil = now + 3000; if (!sourceFrame.dmzMediaEvidence && sourceFrame.dmzActivationMisses >= 3) { sourceFrame.dmzHardRejected = true; } return; } if (lastActivatedAt && now - lastActivatedAt < 1400) { return; } const cooldownUntil = Number(sourceFrame.dmzCooldownUntil || 0); if (cooldownUntil && now < cooldownUntil && !sourceFrame.dmzMediaEvidence) { return; } const settledUntil = Number(sourceFrame.dmzSettledUntil || 0); if (settledUntil && now < settledUntil) { return; } sourceFrame.dmzLastActivatedAt = now; sourceFrame.dmzActivationAttempts = Number(sourceFrame.dmzActivationAttempts || 0) + 1; if (this._shouldThrottlePromotion(sourceFrame, event.origin, 1700)) { return; } if (!sourceFrame.dmzMediaEvidence && sourceFrame.dmzActivationAttempts > 1 && score < 100) { sourceFrame.dmzActivationMisses = Number(sourceFrame.dmzActivationMisses || 0) + 1; } if (!sourceFrame.dmzMediaEvidence && Number(sourceFrame.dmzActivationMisses || 0) >= 3) { sourceFrame.dmzCooldownUntil = now + 8000; this.log(`[加速] iFrame连续无媒体证据,进入冷却观察: ${event.origin}`, CONSTANTS.LOG_TYPES.INFO); return; } } this.context.coreLogic.sandboxManager._markBridgeNestedEvidence(sourceFrame?.src || event.origin); this.context.coreLogic.sandboxManager._recordSandboxProgress('commander_lock', event.origin); this._logCommanderLockOnce(sourceFrame, event.origin, score); event.source.postMessage({ type: CONSTANTS.MESSAGE_TYPES.ACTIVATE_AUTOPLAY }, event.origin); this.context.coreLogic.sandboxManager.extendCountdown(4000, '等待高分Iframe响应'); } _blindActivateFrameOrigin(event, origin) { if (!/(play|video|vod|embed|stream|m3u8)/i.test(origin)) { return; } const sourceFrame = this._resolveSignalSourceFrame(event.source); if (sourceFrame) { if (this._isDefinitelyNonVideoIframe(sourceFrame, origin)) { sourceFrame.dmzHardRejected = true; return; } if (!this._canPromoteIframeToTarget(sourceFrame, origin, 'blind-activate')) { return; } } if (this.context.playerManager.isPlayerActiveOrInitializing || this.context.coreLogic.decisionInProgress) { return; } if (sourceFrame && this._shouldThrottlePromotion(sourceFrame, origin, 1800)) { return; } this.context.coreLogic.sandboxManager._markBridgeNestedEvidence(sourceFrame?.src || event.origin); this.context.coreLogic.sandboxManager._recordSandboxProgress('nested_iframe', event.origin); this.log(`指挥官:发现嵌套目标[${event.origin}],盲发激活指令!`, CONSTANTS.LOG_TYPES.CORE_IFRAME); event.source.postMessage({ type: CONSTANTS.MESSAGE_TYPES.ACTIVATE_AUTOPLAY }, event.origin); } _handleLogForward(data, event) { if (CONSTANTS.IS_TOP_FRAME) { const { logEntry } = data; if (logEntry) { const simplifiedOrigin = event.origin .replace(/^https?:\/\/(?:www\.)?/, '') .split('/')[0] .split('.') .slice(-2) .join('.'); const msg = String(logEntry.message || ''); const shouldDedup = /(DOM扫描器已激活|启动【持久化压制器】|停止【持久化压制器】|开始改造原生 VIDEO|士兵:收到指挥官的激活授权|士兵:检测到播放已启动)/.test( msg ); if (/DOM扫描器已激活/.test(msg)) { this.context.coreLogic.sandboxManager._recordSandboxProgress('inner_scanner', simplifiedOrigin); } else if (/士兵:收到指挥官的激活授权/.test(msg)) { this.context.coreLogic.sandboxManager._recordSandboxProgress( 'soldier_started', simplifiedOrigin ); } else if (/士兵:检测到播放已启动|视频已开始播放|loadedmetadata|canplay/i.test(msg)) { this.context.coreLogic.sandboxManager._recordSandboxProgress('media_ready', simplifiedOrigin); } else if (/DOM扫描Blob|开始改造原生 VIDEO|启动【持久化压制器】/.test(msg)) { this.context.coreLogic.sandboxManager._recordSandboxProgress('media_boot', simplifiedOrigin); } if (shouldDedup && this._shouldSuppressForwardedFrameLog(simplifiedOrigin, msg, 4500)) { return; } this.log(`[iFrame@${simplifiedOrigin}]${msg}`, logEntry.type); } } } _handleActivateAutoplay() { if (CONSTANTS.IS_TOP_FRAME) return; const now = Date.now(); if (this._lastActivationTime && now - this._lastActivationTime < 3000) return; this._lastActivationTime = now; SmartClickModel.ensurePlaybackObservers(document); if (SmartClickModel.shouldStopAutomation(this.context)) return; if (this._exactTargetingActive) return; this._exactTargetingActive = true; this.context.coreLogic.sandboxManager._recordSandboxProgress( 'soldier_started', window.location.hostname || 'subframe' ); this.log('士兵:已放弃盲猜模式,启动【确定性实体侦测】...', CONSTANTS.LOG_TYPES.CORE_IFRAME); this._performExactTargeting(); } _performExactTargeting() { let clickAttempted = false; let observer = null; let timeoutTimer = null; const checkAndClick = () => { if (clickAttempted) return true; let target = null; const video = document.querySelector('video'); if (video) { const rect = video.getBoundingClientRect(); if (rect.width < 30 || rect.height < 30) return false; const style = window.getComputedStyle(video); if (style.display === 'none' || style.visibility === 'hidden') return false; if (video.readyState >= 2 || !video.paused) { this.log(`士兵:播放器已在运行中,中止打击。`, CONSTANTS.LOG_TYPES.CORE_EXEC); clickAttempted = true; return true; } target = video; const container = video.closest('.prism-player, .video-js, .jwplayer, .dplayer, .plyr, .art-video-player, [class*="player"]'); if (container) { const officialBtn = container.querySelector( '.prism-big-play-btn, .vjs-big-play-button, .jw-display-icon-display, .dplayer-mobile-play, .plyr__control--overlaid, .art-control-play,[class*="big-play"]' ); if (officialBtn && officialBtn.offsetWidth > 0 && officialBtn.offsetHeight > 0) { target = officialBtn; } } } else { const playBtn = document.querySelector('.prism-big-play-btn, .vjs-big-play-button, .jw-display-icon-display, .dplayer-mobile-play, .plyr__control--overlaid, .art-control-play,[class*="big-play"], [class*="play-button"], [class*="play-icon"], .fp-ui,[aria-label*="play" i], [title*="play" i]'); if (playBtn && playBtn.offsetWidth > 0 && playBtn.offsetHeight > 0) { const style = window.getComputedStyle(playBtn); if (style.display !== 'none' && style.visibility !== 'hidden') { target = playBtn; } } else { return false; } } if (!target) return false; this.log(`士兵:锁定打击目标 (${target.tagName}),准备一击致命!`, CONSTANTS.LOG_TYPES.CORE_EXEC); const targetRect = target.getBoundingClientRect(); const cx = targetRect.left + targetRect.width / 2; const cy = targetRect.top + targetRect.height / 2; Utils.dispatchMouseEventSequence(target,['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'], { clientX: cx, clientY: cy }, 'Exact target dispatch failed:'); Utils.safeNativeClick(target, 'Exact native click failed:'); clickAttempted = true; setTimeout(() => { const checkVideo = document.querySelector('video'); if (checkVideo && checkVideo.paused && checkVideo.readyState < 2) { this.log(`士兵:一击未生效(疑被防弹窗遮罩吃掉点击),执行精准补刀...`, CONSTANTS.LOG_TYPES.WARN); clickAttempted = false; } else { this.log(`士兵:激活指令已发送!等待网络层捕获数据...`, CONSTANTS.LOG_TYPES.CORE_EXEC); } }, 2500); return true; }; observer = new MutationObserver(() => { if (checkAndClick()) { observer.disconnect(); clearTimeout(timeoutTimer); } }); observer.observe(document.body || document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter:['style', 'class', 'src'] }); if (checkAndClick()) { observer.disconnect(); } else { timeoutTimer = setTimeout(() => { observer.disconnect(); if (!clickAttempted) { this._exactTargetingActive = false; this.log(`士兵:等待超时,目标播放器未能成功渲染实体。`, CONSTANTS.LOG_TYPES.WARN); const isBlocked = document.querySelector('.countdown, [class*="ad-time"],[id*="ad-time"], .video-ad'); if (isBlocked) { this.postToTopFrame({ type: CONSTANTS.MESSAGE_TYPES.I_AM_BLOCKED_BY_AD }); } } }, 10000); } } _handleNeutralize() { if (!CONSTANTS.IS_TOP_FRAME) { try { window.stop(); } catch (e) { console.debug('Failed to stop window loading:', e); } } } _handleRestore() { this.context.coreLogic.restorePageToOriginalState(); } _handleForcePause() { if ( CONSTANTS.IS_TOP_FRAME || /(recaptcha|google|facebook|twitter|instagram|disqus|comment|widget|chat|analytics)/.test( window.location.hostname ) ) { return; } const mediaList = this.context.coreLogic.findAllVideosAndAudioInPage(); if (mediaList.length === 0) { return; } let neutralizedCount = 0; mediaList.forEach((media) => { if (media.id !== CONSTANTS.IDS.PLAYER) { const result = this.context.coreLogic.neutralizeOriginalPlayer(media, 'active'); if (result) { neutralizedCount++; } } }); if (neutralizedCount > 0) { this.log( `收到顶层压制指令,已强制静音并暂停 (${neutralizedCount}个媒体)。`, CONSTANTS.LOG_TYPES.CORE_NEUTRALIZE ); } } postToTopFrame(message) { if (!CONSTANTS.IS_TOP_FRAME) { try { let targetOrigin = '*'; try { targetOrigin = window.top.location.origin; } catch (e) { console.debug('Failed to read top origin:', e); } this.log(`向顶层窗口发送消息:${message.action || message.type}`, CONSTANTS.LOG_TYPES.COMM); window.top.postMessage(message, targetOrigin); } catch (e) { this.log(`跨frame通信失败:${e.message}`, CONSTANTS.LOG_TYPES.ERROR); } } } showNotification(text, isError = false) { if (typeof GM_notification === 'function') { GM_notification({ title: CONSTANTS.SCRIPT_NAME, text, silent: !isError, timeout: isError ? 5000 : 3000, }); } else { console.log(`[${CONSTANTS.SCRIPT_NAME}] ${text}`); } } } class StyleManager extends BaseModule { constructor(context) { super(context, 'StyleManager'); } injectPlayerStyles(shadowRoot) { if (!shadowRoot.querySelector(`#${CONSTANTS.IDS.PLAYER_STYLES}`)) { const style = document.createElement('style'); style.id = CONSTANTS.IDS.PLAYER_STYLES; style.textContent = CSS.PLAYER; shadowRoot.appendChild(style); } } } const SettingsUiDescriptors = { SECTIONS: [ { type: 'info', content: `💡 作者:bug大魔王: 脚本好用的话,点个赞,喜欢可以来tg群组交流 https://t.me/+HbQE7TdRe-4yMGM1 。`, }, { group: '播放控制', items: [ { id: 'auto-play-toggle', label: '自动播放', type: 'switch', configKey: 'autoPlay', }, { id: 'smart-slice-toggle', label: '广告净化', type: 'switch', configKey: 'isSmartSlicingEnabled', }, { id: 'playback-rate-input', label: '默认播放倍速', type: 'number', configKey: 'defaultPlaybackRate', props: { step: '0.25', min: '0.5', max: '5.0' }, }, { id: 'long-press-rate-input', label: '长按倍速', type: 'number', configKey: 'longPressRate', props: { step: '0.25', min: '1.0', max: '5.0' }, }, ], }, { group: '高级设置', items: [ { id: 'max-retries-input', label: '网络最大重试次数 (快速失败建议为0)', type: 'number', configKey: 'maxRetryCount', props: { min: '0', max: '10' }, }, { id: 'json-inspector-toggle', label: '深度JSON解析劫持 (增强成功率)', type: 'switch', configKey: 'enableJsonInspector', }, { id: 'set-attribute-hooker-toggle', label: '底层播放器属性劫持 (增强成功率)', type: 'switch', configKey: 'enableSetAttributeHooker', }, ], }, { group: '站点管理', items: [ { id: 'site-blacklist-input', label: '黑名单 (在这些网站上禁用脚本,一行一个)', type: 'textarea', configKey: 'blacklist', props: { rows: '8', id: 'site-blacklist-input' }, }, { id: 'add-to-blacklist-btn', label: '添加当前站点到黑名单', type: 'button', }, ], }, ], HANDLERS: { switch: { getValue: (el) => el.checked, setValue: (el, value) => { el.checked = !!value; }, }, textarea: { getValue: (el) => el.value .split('\n') .map((s) => s.trim()) .filter(Boolean), setValue: (el, value) => { el.value = Array.isArray(value) ? value.join('\n') : (value ?? ''); }, }, number: { getValue: (el, item) => parseFloat(el.value) || (item.configKey === 'maxRetryCount' ? 3 : 1.0), setValue: (el, value) => { el.value = value; }, }, }, }; const SettingsUiFactoryUtils = { createLabeledInput(item) { const label = DomUtils.create('label', { htmlFor: item.id, textContent: item.label, }); const input = DomUtils.create(item.type === 'number' ? 'input' : 'textarea', { id: item.id, ...item.props, }); if (item.type === 'number') { input.type = 'number'; } return DomUtils.create('div', { className: 'option-item-col' }, [label, input]); }, createSwitchItem(item) { return DomUtils.create('div', { className: 'option-item' }, [ DomUtils.create('label', { htmlFor: item.id, textContent: item.label }), DomUtils.create('label', { className: CONSTANTS.CLASSES.SWITCH }, [ DomUtils.create('input', { type: 'checkbox', id: item.id }), DomUtils.create('span', { className: CONSTANTS.CLASSES.SLIDER }), ]), ]); }, createButtonItem(item) { return DomUtils.create('button', { id: item.id, className: `${CONSTANTS.CLASSES.SETTINGS_BTN} action`, textContent: item.label, }); }, createItemElement(item) { switch (item.type) { case 'switch': return this.createSwitchItem(item); case 'number': case 'textarea': return this.createLabeledInput(item); case 'button': return this.createButtonItem(item); default: return null; } }, createInfoSection(section) { return DomUtils.create('div', { className: 'settings-card-info', innerHTML: section.content, }); }, createSectionCard(section) { const card = DomUtils.create('div', { className: CONSTANTS.CLASSES.SETTINGS_CARD, }); card.appendChild( DomUtils.create('h3', { className: 'settings-title', textContent: section.group, }) ); section.items.forEach((item) => { const el = this.createItemElement(item); if (el) { card.appendChild(el); } }); return card; }, appendSection(container, section) { if (section.type === 'info') { container.appendChild(this.createInfoSection(section)); return; } container.appendChild(this.createSectionCard(section)); }, }; const UnifiedPanelDescriptors = { NAV_ITEMS: [ { id: 'nav-btn-diagnostics', className: 'dmz-nav-btn', textContent: '诊断报告', }, { id: 'nav-btn-settings', className: 'dmz-nav-btn', textContent: '功能设置', }, ], FOOTER_ITEMS: [ { id: 'dmz-settings-reset-btn', className: `${CONSTANTS.CLASSES.SETTINGS_BTN} reset`, textContent: '恢复默认', }, { id: 'dmz-settings-save-btn', className: `${CONSTANTS.CLASSES.SETTINGS_BTN} save`, textContent: '保存并应用', }, ], }; const UnifiedPanelFactoryUtils = { createButtons(descriptors) { return descriptors.map((descriptor) => DomUtils.create('button', { id: descriptor.id, className: descriptor.className, textContent: descriptor.textContent, }) ); }, }; class SettingsUI extends BaseModule { constructor(context) { super(context, 'SettingsUI'); this.UI_CONFIG = SettingsUiDescriptors.SECTIONS; this.UI_HANDLERS = SettingsUiDescriptors.HANDLERS; } _getConfigItems() { return this.UI_CONFIG.flatMap((section) => section.items ?? []); } _appendSection(contentArea, section) { SettingsUiFactoryUtils.appendSection(contentArea, section); } generateSettingsContent() { const contentArea = DomUtils.create('div', { className: CONSTANTS.CLASSES.SETTINGS_GRID, }); this.UI_CONFIG.forEach((section) => this._appendSection(contentArea, section)); return contentArea; } _applyItemValue(panel, item, data) { if (!item.configKey) { return; } const el = panel.querySelector(`#${item.id}`); const handler = this.UI_HANDLERS[item.type]; if (el && handler) { handler.setValue(el, data[item.configKey], item); } } loadDataToUI(panel, data) { this._getConfigItems().forEach((item) => this._applyItemValue(panel, item, data)); } _readItemValue(panel, item, newConfig) { if (!item.configKey) { return; } const el = panel.querySelector(`#${item.id}`); const handler = this.UI_HANDLERS[item.type]; if (el && handler) { newConfig[item.configKey] = handler.getValue(el, item); } } saveDataFromUI(panel) { const newConfig = {}; this._getConfigItems().forEach((item) => this._readItemValue(panel, item, newConfig)); return newConfig; } } const InfoPanelDescriptors = { DIAGNOSTICS_TEMPLATE: `

[广告过滤]净化报告

净化结果
分析摘要

视频与播放诊断

接管方式
视频格式
播放分辨率
来源地址

播放健康度诊断

播放列表 (M3U8)
内容解密 (Key)
视频流 (TS)
媒体文件加载
▼ 展开开发者日志
`, AD_FILTER_SELECTORS: { adFilter: '[data-info="ad-filter"]', adAnalysisResult: '[data-info="ad-analysis-result"]', }, M3U8_HEALTH_ITEMS: [ { selector: '[data-info="health-m3u8"]', statusKey: 'manifest', successText: '播放列表已就绪', errorText: '播放列表加载失败', pendingText: '正在请求播放列表...', codeKey: 'code', }, { selector: '[data-info="health-key"]', statusKey: 'key', successText: '密钥获取成功', notEncryptedText: '内容未加密,无需解密', errorText: '密钥获取失败', pendingText: '正在分析加密...', codeKey: 'code', }, { selector: '[data-info="health-ts"]', statusKey: 'segments', successText: '视频流缓冲中 (加载正常)', errorText: '视频流加载不畅', pendingText: '等待视频流加载...', }, ], }; const InfoPanelFactoryUtils = { createDiagnosticsContentElement() { return DomUtils.create('div', { className: 'dmz-panel-pane-content', innerHTML: InfoPanelDescriptors.DIAGNOSTICS_TEMPLATE, }); }, getAdFilterElements(root) { return { adFilter: root.querySelector(InfoPanelDescriptors.AD_FILTER_SELECTORS.adFilter), adAnalysisResult: root.querySelector(InfoPanelDescriptors.AD_FILTER_SELECTORS.adAnalysisResult), }; }, renderM3u8Health(manager, root, health) { InfoPanelDescriptors.M3U8_HEALTH_ITEMS.forEach((item) => { const section = health[item.statusKey]; const successText = section?.status === 'not_encrypted' && item.notEncryptedText ? item.notEncryptedText : item.successText; manager._setHtml( root, item.selector, manager._getStatusHtml( section?.status, successText, item.errorText, item.pendingText, item.codeKey ? section?.[item.codeKey] : '' ) ); }); }, }; class InfoPanelManager extends BaseModule { constructor(context) { super(context, 'InfoPanelManager'); } _getPanelRoot() { return this.context.unifiedPanelManager?.elements?.shadowRoot; } _createDiagnosticsContentElement() { return InfoPanelFactoryUtils.createDiagnosticsContentElement(); } _createCopyReportButton() { return DomUtils.create('button', { className: 'copy-report-btn', title: '复制完整诊断报告', innerHTML: '📋 复制报告', style: { flex: '1 1 0', margin: '0', }, }); } _createCopyJsonButton() { return DomUtils.create('button', { className: 'copy-report-btn', title: '复制结构化诊断 JSON', innerHTML: '🧬 复制 JSON', style: { flex: '1 1 0', margin: '0', }, }); } _toggleDeveloperLogs(devLogsContainer, detailsToggle) { const isVisible = devLogsContainer.style.display !== 'none'; devLogsContainer.style.display = isVisible ? 'none' : 'block'; detailsToggle.textContent = (!isVisible ? '▲ 收起' : '▼ 展开') + '开发者日志'; } _copyFullReport(copyReportBtn) { const reportText = this.generateFullReportText(); Utils.copyToClipboard(reportText, () => DomUtils.showButtonFeedback(copyReportBtn, { successText: '✔ 已复制!', duration: 2000, }) ); } _copyStructuredReportJson(copyJsonBtn) { const reportText = this.generateStructuredReportJson(); Utils.copyToClipboard(reportText, () => DomUtils.showButtonFeedback(copyJsonBtn, { successText: '✔ 已复制!', duration: 2000, }) ); } _bindDiagnosticsContentEvents(content, copyReportBtn, copyJsonBtn) { const devLogsContainer = content.querySelector('.developer-logs'); const detailsToggle = content.querySelector('.details-toggle'); detailsToggle.addEventListener('click', () => this._toggleDeveloperLogs(devLogsContainer, detailsToggle)); copyReportBtn.addEventListener('click', () => this._copyFullReport(copyReportBtn)); copyJsonBtn.addEventListener('click', () => this._copyStructuredReportJson(copyJsonBtn)); } generateDiagnosticsContent() { const copyReportBtn = this._createCopyReportButton(); const copyJsonBtn = this._createCopyJsonButton(); const buttonBar = DomUtils.create( 'div', { style: { display: 'flex', gap: '8px', margin: '0 0 10px', }, }, [copyReportBtn, copyJsonBtn] ); const content = this._createDiagnosticsContentElement(); content.prepend(buttonBar); this._bindDiagnosticsContentEvents(content, copyReportBtn, copyJsonBtn); return content; } _getRootText(root, selector) { return root.querySelector(selector)?.innerText.trim() ?? 'N/A'; } _getRootValue(root, selector) { return root.querySelector(selector)?.value ?? 'N/A'; } generateFullReportText() { const root = this._getPanelRoot(); if (!root) { return '无法生成报告:面板未初始化。'; } const structured = this.context.diagnosticsTool?.getStructuredReportData?.() ?? {}; const takeoverEvidence = structured.takeoverEvidence ?? {}; const initialWinner = takeoverEvidence.initialWinner?.sourceName || 'N/A'; const finalWinner = takeoverEvidence.finalWinner?.sourceName || this._getRootText(root, '[data-info="takeover"]'); const switchText = takeoverEvidence.switchCount > 0 ? `${takeoverEvidence.switchCount} 次 (${(takeoverEvidence.switchHistory || []).map((entry) => entry?.to?.sourceName || entry?.sourceName || '未知来源').join(' -> ')})` : '0 次'; const playerManager = this.context.playerManager; const videoFormat = playerManager?.currentVideoFormat ?? { name: '未知' }; const isM3U8Type = videoFormat.name === 'M3U8' || !!playerManager?.lastM3u8Content; const healthText = isM3U8Type ? `**播放健康度 - 播放列表:** ${this._getRootText(root, '[data-info="health-m3u8"]')} **播放健康度 - 内容解密:** ${this._getRootText(root, '[data-info="health-key"]')} **播放健康度 - 视频流:** ${this._getRootText(root, '[data-info="health-ts"]')}` : `**播放健康度 - 播放列表:** N/A (MP4直链,无播放列表) **播放健康度 - 内容解密:** N/A (无分片解密) **播放健康度 - 视频流:** ${this._getRootText(root, '[data-info="health-normal"]')}`; const nl = '\n'; const dbl = '\n\n'; const developerLogText = this.context.diagnosticsTool?.generateDeveloperReportText?.() ?? this._getRootText(root, '[data-info="dev-log"]'); return `### 📊 ${CONSTANTS.SCRIPT_NAME} 诊断报告 ###${dbl}**版本:** ${CONSTANTS.SCRIPT_VERSION}${nl}**页面URL:** ${window.location.href}${dbl}--- [广告过滤]净化报告 ---${dbl}**净化结果:** ${this._getRootText(root, '[data-info="ad-filter"]')}${nl}**分析摘要:**${nl}${this._getRootText(root, '[data-info="ad-analysis-result"]').replace(/•/g, '* ')}${nl}${dbl}--- 视频与播放诊断 ---${dbl}**接管方式:** ${finalWinner}${nl}**初始接管:** ${initialWinner}${nl}**最终接管:** ${finalWinner}${nl}**切换历史:** ${switchText}${nl}**视频来源:** ${playerManager?.currentVideoUrl ?? 'N/A'}${nl}${healthText}${dbl}--- M3U8 原始情报 ---${dbl}${this._getRootValue(root, '[data-info="m3u8-raw"]')}${dbl}--- M3U8 净化后情报 ---${dbl}${this._getRootValue(root, '[data-info="m3u8-processed"]')}${dbl}--- 开发者事件时间轴 ---${dbl}${developerLogText}`; } generateStructuredReportJson() { const rawData = this.context.diagnosticsTool?.getStructuredReportData?.(); const sanitize = (val, seen = new WeakSet()) => { if (val === null || typeof val !== 'object') return val; try { if (val.window === val || typeof val.postMessage === 'function') return '[Window Proxy]'; if (typeof val.appendChild === 'function') return '[DOM Element]'; } catch (e) { return '[Cross-Origin Object]'; } if (seen.has(val)) return '[Circular Reference]'; seen.add(val); if (Array.isArray(val)) { return val.map((item) => sanitize(item, seen)); } const result = {}; try { for (const key of Object.keys(val)) { result[key] = sanitize(val[key], seen); } } catch (e) { return '[Inaccessible Object]'; } return result; }; try { return JSON.stringify(sanitize(rawData) ?? { error: '暂无诊断数据。' }, null, 2); } catch (e) { return JSON.stringify({ error: '序列化彻底失败', details: String(e) }, null, 2); } } _getStatusHtml(status, successText, errorText, pendingText, errorCode = '') { switch (status) { case 'success': return `✅ ${successText}`; case 'not_encrypted': return `✅ ${successText}`; case 'not_applicable': return `➖ 不适用`; case 'error': return `❌ ${errorText} ${errorCode ? `(错误: ${errorCode})` : ''}`; default: return `⏳${pendingText}`; } } _setText(root, selector, text) { const el = root.querySelector(selector); if (el) { el.textContent = text; } } _setHtml(root, selector, html) { const el = root.querySelector(selector); if (el) { el.innerHTML = html; } } _escapeHtml(text) { return String(text ?? '') .replace(/&/g, '&') .replace(//g, '>'); } _getIdleStatusPresentation(domScanner, coreLogic) { if (coreLogic.isSystemHalted) { return { icon: '💤', text: '当前页面疑似非视频页,已自动休眠。' }; } if (domScanner && domScanner.isStopped) { return { icon: '💤', text: '扫描器暂停中...' }; } return { icon: '🕵️', text: '正在页面上巡逻,待命中...' }; } _getErrorStatusPresentation(health) { if (health.manifest.status === 'error') { return { icon: '❌', text: `视频目录加载失败(错误:${health.manifest.code}),无法播放`, }; } if (health.key.status === 'error') { return { icon: '❌', text: `视频密钥获取失败(错误:${health.key.code}),无法解密`, }; } if (health.media.status === 'error' && health.media.reason) { return { icon: '❌', text: `媒体解码失败(原因:${health.media.reason})`, }; } if (health.segments.status === 'error') { return { icon: '❌', text: `视频数据加载失败 (已达最大重试)` }; } return null; } _getActivePlaybackStatusPresentation(playerManager) { if (!playerManager.isPlayerActiveOrInitializing) { return null; } const v = playerManager.videoElement; if (v && (!v.videoWidth || !v.videoHeight)) { return { icon: '📏', text: '正在获取视频尺寸...' }; } if (v && v.readyState > 2 && !v.paused) { return { icon: '✅', text: '视频正在流畅播放中!' }; } return { icon: '⏳', text: '正在缓冲视频...' }; } _resolveStatusPresentation(playerManager, health, domScanner, coreLogic) { return ( this._getErrorStatusPresentation(health) || this._getActivePlaybackStatusPresentation(playerManager) || this._getIdleStatusPresentation(domScanner, coreLogic) ); } _applyStatusBanner(root, status) { this._setHtml(root, '[data-info="status-icon"]', `${status.icon}`); this._setText(root, '[data-info="status-text"]', status.text); } _resolveDisplayVideoFormat(playerManager) { let videoFormat = playerManager.currentVideoFormat ?? { name: '未知', type: '', }; if (playerManager.lastM3u8Content && videoFormat.name !== 'M3U8') { videoFormat = { name: 'M3U8', type: '流式传输' }; } return videoFormat; } _formatSlicedDuration(slicedDuration) { if (!(slicedDuration > 0)) { return ''; } let formattedDuration; if (slicedDuration < 60) { formattedDuration = `${slicedDuration.toFixed(1)}秒`; } else { const minutes = Math.floor(slicedDuration / 60); const remainingSeconds = Math.round(slicedDuration % 60); formattedDuration = `${minutes}分${remainingSeconds}秒`; } return `,总计时长约${formattedDuration}`; } _hasActivatedEngine(report, engineName) { const activated = report?.activatedEngines; if (activated instanceof Set) { return activated.has(engineName); } if (Array.isArray(activated)) { return activated.includes(engineName); } return false; } _getFoundFeatures(report, engineName) { const foundFeatures = report?.foundFeatures; if (!foundFeatures) { return []; } if (foundFeatures instanceof Map) { return Array.from(foundFeatures.get(engineName) ?? []); } if (Array.isArray(foundFeatures)) { const entry = foundFeatures.find(([key]) => key === engineName); return entry ? Array.from(entry[1] ?? []) : []; } const rawValue = foundFeatures[engineName]; if (Array.isArray(rawValue)) { return rawValue.slice(); } if (rawValue instanceof Set) { return Array.from(rawValue); } return rawValue ? [rawValue] : []; } _buildAdDetailBlock(title, body, extra = '') { const safeBody = body || '暂无可展示明细'; return `
${title}
${safeBody}
${extra}
`; } _buildUrlPatternAnalysis(report) { if (!this._hasActivatedEngine(report, 'URL_PATTERN')) { return ''; } const keywords = this._getFoundFeatures(report, 'URL_PATTERN') .map((k) => `${this._escapeHtml(k)}`) .join('、'); const keywordText = keywords || '已命中特征,但关键词缺失'; return this._buildAdDetailBlock( '「特征锁定」过滤', `命中 URL 特征关键词:${keywordText}`, `
已按特征精准切除广告分片。
` ); } _buildBehaviorModelAnalysis(report) { if (!this._hasActivatedEngine(report, 'BEHAVIOR_MODEL')) { return ''; } const details = this._getFoundFeatures(report, 'BEHAVIOR_MODEL'); const body = details.length > 0 ? details.map((d) => `• ${this._escapeHtml(d)}`).join('
') : '已启用行为清扫,但未记录到具体特征。'; return this._buildAdDetailBlock('「行为清扫」过滤', body); } _buildTimeRangeAnalysis(report) { if (!(report.slicedTimeRanges?.length > 0)) { return ''; } const ranges = report.slicedTimeRanges .slice() .sort((a, b) => a.start - b.start) .map((range) => `⏱️ ${Utils.formatTime(range.start)} → ${Utils.formatTime(range.end)}`) .join('
'); return this._buildAdDetailBlock('切除时间点', ranges); } _renderSmartSlicingDisabled(adFilter, adAnalysisResult) { adFilter.innerHTML = `“广告过滤”系统未启用`; adAnalysisResult.innerHTML = `已在设置中关闭,所有净化模块将不会运行。`; } _renderSlicedM3u8Report(adFilter, adAnalysisResult, report) { const slicedSegments = report?.slicedSegments ?? 0; const slicedGroups = report?.slicedGroups ?? 0; const durationText = this._formatSlicedDuration(report?.slicedDuration ?? 0); adFilter.innerHTML = `🛡️〖任务完成〗 已为您拦截 ${slicedSegments} 个广告片段${durationText}。`; const activatedEngines = Array.isArray(report?.activatedEngines) ? report.activatedEngines : Array.from(report?.activatedEngines ?? []); let analysisHTML = `📄〖广告过滤〗模块已激活,净化协议启动:
本次共切除 ${slicedGroups} 组广告、${slicedSegments} 个广告分片。
`; const detailBlocks = [ this._buildUrlPatternAnalysis(report), this._buildBehaviorModelAnalysis(report), this._buildTimeRangeAnalysis(report), ].filter(Boolean); if (detailBlocks.length > 0) { analysisHTML += detailBlocks.join(''); } else { const engineText = activatedEngines.length > 0 ? activatedEngines.map((name) => this._escapeHtml(name)).join('、') : '已执行净化,但暂无可展示特征'; analysisHTML += this._buildAdDetailBlock('净化明细', `命中引擎:${engineText}`); } analysisHTML += '[广告过滤]模块已全面接管,为您带来无干扰的沉浸式观看体验。'; adAnalysisResult.innerHTML = analysisHTML; } _renderCleanM3u8Report(adFilter, adAnalysisResult) { adFilter.innerHTML = `🛡️〖无需净化〗 视频流纯净。`; adAnalysisResult.innerHTML = `📑〖广告过滤〗模块已完成扫描,未在视频流中发现任何广告特征。确认为纯净内容,请放心观看。`; } _renderNormalFileReport(adFilter, adAnalysisResult, videoFormat) { adFilter.innerHTML = `🛡️( 常规文件〖无需净化〗)`; adAnalysisResult.innerHTML = `📑〖广告过滤〗模块仅适用于 M3U8 流媒体。当前 〖${videoFormat.name}〗 格式为单一文件,已为您直接播放。`; } _renderWaitingVideoReport(adFilter, adAnalysisResult) { adFilter.innerHTML = `⏳ 等待视频...`; adAnalysisResult.innerHTML = `捕获到视频后,〖广告过滤〗模块将自动启动。`; } _renderAdFilterSection(root, settingsManager, playerManager, videoFormat, report) { const refs = this._getAdFilterElements(root); const mode = this._resolveAdFilterMode(settingsManager, playerManager, videoFormat, report); this._renderAdFilterMode(mode, refs, videoFormat, report); } _getAdFilterElements(root) { return InfoPanelFactoryUtils.getAdFilterElements(root); } _resolveAdFilterMode(settingsManager, playerManager, videoFormat, report) { if (!settingsManager.config.isSmartSlicingEnabled) { return 'disabled'; } if (!playerManager.isPlayerActiveOrInitializing) { return 'waiting'; } if (videoFormat.name !== 'M3U8') { return 'normal'; } return report.slicedSegments > 0 ? 'sliced' : 'clean'; } _renderAdFilterMode(mode, refs, videoFormat, report) { if (mode === 'disabled') { this._renderSmartSlicingDisabled(refs.adFilter, refs.adAnalysisResult); return; } if (mode === 'waiting') { this._renderWaitingVideoReport(refs.adFilter, refs.adAnalysisResult); return; } if (mode === 'normal') { this._renderNormalFileReport(refs.adFilter, refs.adAnalysisResult, videoFormat); return; } if (mode === 'sliced') { this._renderSlicedM3u8Report(refs.adFilter, refs.adAnalysisResult, report); return; } this._renderCleanM3u8Report(refs.adFilter, refs.adAnalysisResult); } _renderTakeoverAndFormat(root, diagnosticsTool, playerManager) { const evidence = diagnosticsTool.takeoverEvidence ?? {}; const initialSource = evidence.initialWinner?.sourceName || ''; const finalSource = evidence.finalWinner?.sourceName || Array.from(evidence.sources ?? []).join(' + '); const takeoverText = evidence.switchCount > 0 && initialSource && finalSource && initialSource !== finalSource ? `${initialSource} → ${finalSource}` : finalSource || 'N/A'; this._setText(root, '[data-info="takeover"]', takeoverText); this._setText( root, '[data-info="format"]', playerManager.currentVideoFormat ? `${playerManager.currentVideoFormat.name} (${playerManager.currentVideoFormat.type})` : '未知' ); this._setText(root, '[data-info="resolution"]', playerManager.currentVideoResolution || '等待视频加载...'); } _renderUrlField(root, playerManager) { const urlEl = root.querySelector('[data-info="url"]'); const fullUrl = playerManager.currentVideoUrl ?? 'N/A'; urlEl.innerHTML = ''; urlEl.appendChild( document.createTextNode((fullUrl.length > 80 ? '...' + fullUrl.slice(-80) : fullUrl) + ' ') ); if (fullUrl !== 'N/A') { urlEl.appendChild(DomUtils.createCopyButtonWithFeedback(fullUrl, '复制')); } } _showM3u8HealthContainers(root) { const m3u8Container = root.querySelector('[data-info-container="health-m3u8"]'); const normalContainer = root.querySelector('[data-info-container="health-normal"]'); m3u8Container.style.display = 'flex'; m3u8Container.style.flexDirection = 'column'; m3u8Container.style.gap = '10px'; normalContainer.style.display = 'none'; } _renderM3u8Health(root, health) { InfoPanelFactoryUtils.renderM3u8Health(this, root, health); } _renderNormalHealth(root, health, videoFormat) { root.querySelector('[data-info-container="health-m3u8"]').style.display = 'none'; root.querySelector('[data-info-container="health-normal"]').style.display = 'flex'; this._setText(root, '[data-info-label="health-normal-label"]', `〖${videoFormat.name}〗 文件加载状态`); this._setHtml( root, '[data-info="health-normal"]', this._getStatusHtml(health.media.status, '媒体文件已加载', '媒体文件加载失败', '正在加载媒体文件...') ); } _renderIdleHealth(root) { this._setText(root, '[data-info="health-m3u8"]', '⏳ 待命中'); this._setText(root, '[data-info="health-key"]', '⏳ 待命中'); this._setText(root, '[data-info="health-ts"]', '⏳ 待命中'); } _renderHealthSection(root, playerManager, health, isM3U8Type, videoFormat) { this._showM3u8HealthContainers(root); if (!playerManager.isPlayerActiveOrInitializing) { this._renderIdleHealth(root); return; } if (isM3U8Type) { this._renderM3u8Health(root, health); return; } this._renderNormalHealth(root, health, videoFormat); } _renderM3u8Textarea(root, selector, value) { const textarea = root.querySelector(selector); textarea.value = value; return textarea; } _renderCopyControl(container, value, emptyKeyword) { container.innerHTML = ''; if (value && !value.includes(emptyKeyword)) { container.appendChild(DomUtils.createCopyButtonWithFeedback(value, '复制')); } } _renderM3u8Section(root, playerManager, diagnosticsTool, isM3U8Type) { const m3u8Raw = this._renderM3u8Textarea( root, '[data-info="m3u8-raw"]', playerManager.lastM3u8Content ?? '暂未捕获到M3U8情报。' ); root.querySelector('[data-info-container="m3u8-processed"]').style.display = isM3U8Type ? 'flex' : 'none'; const m3u8Processed = this._renderM3u8Textarea( root, '[data-info="m3u8-processed"]', diagnosticsTool.lastProcessedM3u8 ?? '暂无处理后内容。' ); this._renderCopyControl(root.querySelector('[data-info="m3u8-raw-controls"]'), m3u8Raw.value, '暂未捕获'); if (isM3U8Type) { this._renderCopyControl( root.querySelector('[data-info="m3u8-processed-controls"]'), m3u8Processed.value, '暂无' ); } else { root.querySelector('[data-info="m3u8-processed-controls"]').innerHTML = ''; } } update() { const panelContext = this._createPanelUpdateContext(); if (!panelContext) { return; } this._renderPanelStatus(panelContext); this._renderPanelMainSections(panelContext); this._renderPanelMediaSections(panelContext); } _createPanelUpdateContext() { const root = this._getPanelRoot(); if (!root || !this.context.playerManager || !this.context.diagnosticsTool) { return null; } const { playerManager, diagnosticsTool, settingsManager, domScanner, coreLogic } = this.context; const health = diagnosticsTool.playbackHealth; const report = diagnosticsTool.slicingReport ?? {}; const videoFormat = this._resolveDisplayVideoFormat(playerManager); return { root, playerManager, diagnosticsTool, settingsManager, domScanner, coreLogic, health, report, videoFormat, isM3U8Type: videoFormat.name === 'M3U8', }; } _renderPanelStatus(panelContext) { const { root, playerManager, health, domScanner, coreLogic } = panelContext; const status = this._resolveStatusPresentation(playerManager, health, domScanner, coreLogic); this._applyStatusBanner(root, status); } _renderPanelMainSections(panelContext) { const { root, settingsManager, playerManager, videoFormat, report, diagnosticsTool } = panelContext; this._renderAdFilterSection(root, settingsManager, playerManager, videoFormat, report); this._renderTakeoverAndFormat(root, diagnosticsTool, playerManager); this._renderUrlField(root, playerManager); } _renderPanelMediaSections(panelContext) { const { root, playerManager, health, isM3U8Type, videoFormat, diagnosticsTool } = panelContext; this._renderHealthSection(root, playerManager, health, isM3U8Type, videoFormat); this._renderM3u8Section(root, playerManager, diagnosticsTool, isM3U8Type); } _getCurrentLogSignature(timeline) { if (timeline.length === 0) { return null; } const lastLog = timeline[timeline.length - 1]; return `${timeline.length}_${lastLog.sequence}`; } _shouldSkipDeveloperLogRender(timeline) { const currentSignature = this._getCurrentLogSignature(timeline); if (!currentSignature) { return false; } if (this._lastLogSignature === currentSignature) { return true; } this._lastLogSignature = currentSignature; return false; } _renderDeveloperLogText(devLog, diagnosticsTool) { if (devLog) { devLog.innerHTML = diagnosticsTool.generateDeveloperReport(); } } _getChainStatusIcon(status) { return { success: '✅', warning: '⚠️', error: '❌', pending: '⏳', active: '🟢', switched: '🔁', }[status] || '⏳'; } _renderDeveloperLogControls(devLogControls, devLog) { if (!devLogControls) { return; } devLogControls.innerHTML = ''; const wrap = document.createElement('div'); wrap.style.display = 'flex'; wrap.style.gap = '8px'; const visibleButton = document.createElement('button'); visibleButton.className = 'copy-btn'; visibleButton.textContent = '复制日志'; visibleButton.addEventListener('click', () => { const visibleText = this.context.diagnosticsTool?.generateDeveloperReportText?.() ?? devLog.innerText; Utils.copyToClipboard(visibleText, () => DomUtils.showButtonFeedback(visibleButton, { successText: '✔ 已复制!' }) ); }); const jsonButton = document.createElement('button'); jsonButton.className = 'copy-btn'; jsonButton.textContent = '复制 JSON'; jsonButton.addEventListener('click', () => { const jsonText = this.generateStructuredReportJson(); Utils.copyToClipboard(jsonText, () => DomUtils.showButtonFeedback(jsonButton, { successText: '✔ 已复制!' }) ); }); wrap.append(visibleButton, jsonButton); devLogControls.appendChild(wrap); } renderDeveloperLog() { const root = this._getPanelRoot(); if (!root || !this.context.diagnosticsTool) { return; } const { diagnosticsTool } = this.context; const timeline = diagnosticsTool.eventTimeline; if (this._shouldSkipDeveloperLogRender(timeline)) { return; } const devLog = root.querySelector('[data-info="dev-log"]'); this._renderDeveloperLogText(devLog, diagnosticsTool); this._renderDeveloperLogControls(root.querySelector('[data-info="dev-log-controls"]'), devLog); } } class UnifiedPanelManager extends BaseModule { constructor(context) { super(context, 'UnifiedPanelManager'); this.hostElement = null; this.infoUpdateInterval = null; this.elements = {}; } _clearGestureTimer(timer) { if (timer) { clearTimeout(timer); } return null; } _bindGestureStart(clearState, setState) { document.addEventListener( 'touchstart', (e) => { if (e.touches.length === 2) { const timer = setTimeout(() => { this.show(); if (navigator.vibrate) { navigator.vibrate(50); } }, 1000); setState(timer, e.touches[0].clientX, e.touches[0].clientY); } else { clearState(); } }, { capture: true, passive: true } ); } _bindGestureMove(getState, clearState) { document.addEventListener( 'touchmove', (e) => { const state = getState(); if ( state.timer && (Math.abs(e.touches[0].clientX - state.startX) > 20 || Math.abs(e.touches[0].clientY - state.startY) > 20) ) { clearState(); } }, { capture: true, passive: true } ); } _bindGestureEnd(clearState) { ['touchend', 'touchcancel'].forEach((evt) => document.addEventListener(evt, clearState, { capture: true, passive: true, }) ); } initGesture() { let timer = null, startX, startY; const clearState = () => { timer = this._clearGestureTimer(timer); }; const setState = (nextTimer, x, y) => { timer = nextTimer; startX = x; startY = y; }; const getState = () => ({ timer, startX, startY }); this._bindGestureStart(clearState, setState); this._bindGestureMove(getState, clearState); this._bindGestureEnd(clearState); } _preventHostPenetration(e) { e.stopPropagation(); if (e.type === 'contextmenu') { e.preventDefault(); } } _bindHostPenetrationGuard() { [ 'touchstart', 'touchmove', 'touchend', 'mousedown', 'mousemove', 'mouseup', 'click', 'dblclick', 'contextmenu', 'wheel', ].forEach((evt) => { this.hostElement.addEventListener(evt, (e) => this._preventHostPenetration(e), { passive: false, }); }); } _mountHostElement() { if (document.body) { document.body.appendChild(this.hostElement); } else { window.addEventListener('DOMContentLoaded', () => document.body.appendChild(this.hostElement), { once: true, }); } } _createHeader() { return DomUtils.create('div', { className: 'panel-header' }, [ DomUtils.create('div', { className: 'title-bar' }, [ DomUtils.create('h3', { innerHTML: `💡 ${CONSTANTS.SCRIPT_NAME}` }), DomUtils.create('button', { className: 'close-btn', title: '关闭', textContent: '×', }), ]), ]); } _createNav() { return DomUtils.create( 'div', { className: 'dmz-panel-nav' }, UnifiedPanelFactoryUtils.createButtons(UnifiedPanelDescriptors.NAV_ITEMS) ); } _createContent() { const content = DomUtils.create('div', { className: 'dmz-unified-panel-content', }); const diagnosticsPane = DomUtils.create('div', { id: 'pane-diagnostics', className: 'dmz-panel-pane' }, [ this.context.infoPanelManager.generateDiagnosticsContent(), ]); const settingsPane = DomUtils.create('div', { id: 'pane-settings', className: 'dmz-panel-pane' }, [ this.context.settingsUI.generateSettingsContent(), ]); content.append(diagnosticsPane, settingsPane); return content; } _createFooter() { return DomUtils.create( 'div', { className: 'dmz-unified-panel-footer' }, UnifiedPanelFactoryUtils.createButtons(UnifiedPanelDescriptors.FOOTER_ITEMS) ); } _cacheElements(shadowRoot, wrapper, footer) { this.elements = { shadowRoot, wrapper, footer, navBtnDiagnostics: shadowRoot.querySelector('#nav-btn-diagnostics'), navBtnSettings: shadowRoot.querySelector('#nav-btn-settings'), paneDiagnostics: shadowRoot.querySelector('#pane-diagnostics'), paneSettings: shadowRoot.querySelector('#pane-settings'), saveBtn: shadowRoot.querySelector('#dmz-settings-save-btn'), resetBtn: shadowRoot.querySelector('#dmz-settings-reset-btn'), closeBtn: shadowRoot.querySelector('.close-btn'), addToBlacklistBtn: shadowRoot.querySelector('#add-to-blacklist-btn'), }; } create() { if (this.hostElement) { return; } this.hostElement = this._createUnifiedPanelHost(); const shadowRoot = this._createUnifiedPanelShadowRoot(); const wrapper = this._createUnifiedPanelWrapper(); shadowRoot.appendChild(wrapper); const footer = wrapper.querySelector('.dmz-unified-panel-footer'); this._cacheElements(shadowRoot, wrapper, footer); this.bindEvents(); } _createUnifiedPanelHost() { const host = DomUtils.create('div', { id: 'dmz-unified-panel-host' }); this.hostElement = host; this._bindHostPenetrationGuard(); this._mountHostElement(); return host; } _createUnifiedPanelShadowRoot() { const shadowRoot = this.hostElement.attachShadow({ mode: 'closed' }); shadowRoot.appendChild(DomUtils.create('style', { textContent: CSS.PANEL })); return shadowRoot; } _createUnifiedPanelWrapper() { const wrapper = DomUtils.create('div', { className: 'dmz-unified-panel-wrapper', }); wrapper.append( this._createHeader(), this._createNav(), this._createContent(), this._createFooter() ); return wrapper; } _bindPaneSwitchEvents() { this.elements.closeBtn.addEventListener('click', () => this.hide()); this.elements.navBtnDiagnostics.addEventListener('click', () => this.switchToPane('diagnostics')); this.elements.navBtnSettings.addEventListener('click', () => this.switchToPane('settings')); } _bindSaveEvent() { this.elements.saveBtn.addEventListener('click', async () => { const newConfig = this.context.settingsUI.saveDataFromUI(this.elements.shadowRoot); await this.context.settingsManager.save(newConfig); DomUtils.showButtonFeedback(this.elements.saveBtn, { successText: '✔ 已保存!', successBgColor: '#28a745', }); this.context.frameCommunicator.showNotification('设置已保存并应用。'); }); } _bindResetEvent() { this.elements.resetBtn.addEventListener('click', async () => { if (window.confirm('您确定要将所有设置恢复为默认值吗?此操作不可撤销。')) { const newConfig = await this.context.settingsManager.reset(); this.context.settingsUI.loadDataToUI(this.elements.shadowRoot, newConfig); } }); } _bindBlacklistEvent() { if (!this.elements.addToBlacklistBtn) { return; } this.elements.addToBlacklistBtn.addEventListener('click', () => { const blTextarea = this.elements.shadowRoot.querySelector('#site-blacklist-input'); const hostName = window.location.hostname; const list = blTextarea.value .split('\n') .map((s) => s.trim()) .filter(Boolean); if (list.includes(hostName)) { return this.context.frameCommunicator.showNotification(`"${hostName}" 已在黑名单中。`); } list.push(hostName); blTextarea.value = list.join('\n'); this.context.frameCommunicator.showNotification( `已将 "${hostName}" 添加到黑名单。保存后将在所有页面生效。` ); }); } _toggleCollapsibleLog(collapsibleHeader) { const content = collapsibleHeader.nextElementSibling; if (content && content.classList.contains('log-collapsed-content')) { const isVisible = content.style.display !== 'none'; content.style.display = isVisible ? 'none' : 'block'; const icon = collapsibleHeader.querySelector('strong'); if (isVisible) { icon.textContent = icon.textContent.replace('[-]', '[+]').replace('收起', '展开'); } else { icon.textContent = icon.textContent.replace('[+]', '[-]').replace('展开', '收起'); } } } _bindContentToggleEvent() { const contentContainer = this.elements.shadowRoot.querySelector('.dmz-unified-panel-content'); if (contentContainer) { contentContainer.addEventListener('click', (e) => { const collapsibleHeader = e.target.closest('.log-collapsible'); if (!collapsibleHeader) { return; } this._toggleCollapsibleLog(collapsibleHeader); }); } } bindEvents() { this._bindPaneSwitchEvents(); this._bindSaveEvent(); this._bindResetEvent(); this._bindBlacklistEvent(); this._bindContentToggleEvent(); } _togglePaneState(isDiagnostics) { this.elements.navBtnDiagnostics.classList.toggle('active', isDiagnostics); this.elements.paneDiagnostics.classList.toggle('active', isDiagnostics); this.elements.navBtnSettings.classList.toggle('active', !isDiagnostics); this.elements.paneSettings.classList.toggle('active', !isDiagnostics); this.elements.footer.classList.toggle('active', !isDiagnostics); } _clearInfoUpdateInterval() { if (this.infoUpdateInterval) { clearInterval(this.infoUpdateInterval); } } _startDiagnosticsRefresh() { this.context.infoPanelManager.renderDeveloperLog(); this.context.infoPanelManager.update(); this.infoUpdateInterval = Utils.startPollingTask( 1500, () => { this.context.infoPanelManager.update(); this.context.infoPanelManager.renderDeveloperLog(); return true; }, 'Diagnostics refresh failed:' ); } switchToPane(paneName) { const isDiagnostics = paneName === 'diagnostics'; this._togglePaneState(isDiagnostics); this._clearInfoUpdateInterval(); if (isDiagnostics) { this._startDiagnosticsRefresh(); } } _applyShowState() { this.hostElement.classList.add(CONSTANTS.CLASSES.VISIBLE); document.documentElement.style.overflow = 'hidden'; document.body.style.overflow = 'hidden'; } _applyHideState() { if (this.hostElement) { this.hostElement.classList.remove(CONSTANTS.CLASSES.VISIBLE); } document.documentElement.style.overflow = ''; document.body.style.overflow = ''; this._clearInfoUpdateInterval(); } show() { if (!CONSTANTS.IS_TOP_FRAME) { return; } if (!this.hostElement) { this.create(); } this._applyShowState(); this.context.settingsUI.loadDataToUI( this.elements.shadowRoot, this.context.settingsManager.config ); this.switchToPane('diagnostics'); } hide() { this._applyHideState(); } } class Draggable extends BaseModule { constructor(context, element, handles, playerManager) { super(context, 'Draggable'); this.element = element; this.handles = handles; this.playerManager = playerManager; this.isDragging = false; this.dragStartPos = { x: 0, y: 0 }; this.elementStartPos = { x: 0, y: 0 }; this.boundHandleDragStart = this.handleDragStart.bind(this); this.boundHandleDragMove = this.handleDragMove.bind(this); this.boundHandleDragEnd = this.handleDragEnd.bind(this); this.addListeners(); } addListeners() { this.handles.forEach((h) => h.addEventListener('pointerdown', this.boundHandleDragStart, { capture: true, }) ); } destroy() { this.removeDragListeners(); this.handles.forEach((h) => h.removeEventListener('pointerdown', this.boundHandleDragStart, { capture: true, }) ); this.element = null; } removeDragListeners() { document.removeEventListener('pointermove', this.boundHandleDragMove, { capture: true, }); document.removeEventListener('pointerup', this.boundHandleDragEnd, { capture: true, }); document.removeEventListener('pointercancel', this.boundHandleDragEnd, { capture: true, }); } handleDragStart(e) { if (!this.element || e.button !== 0) { return; } e.stopPropagation(); e.preventDefault(); this.isDragging = true; this.playerManager?.showIndicatorPermanently(true); this.element.style.transition = 'none'; const rect = this.element.getBoundingClientRect(); this.element.style.left = `0px`; this.element.style.top = `0px`; this.element.style.transform = `translate3d(${rect.left}px, ${rect.top}px, 0)`; this.elementStartPos = { x: rect.left, y: rect.top }; this.dragStartPos = { x: e.clientX, y: e.clientY }; document.addEventListener('pointermove', this.boundHandleDragMove, { capture: true, }); document.addEventListener('pointerup', this.boundHandleDragEnd, { capture: true, }); document.addEventListener('pointercancel', this.boundHandleDragEnd, { capture: true, }); } handleDragMove(e) { if (!this.isDragging || !this.element) { return; } e.stopPropagation(); e.preventDefault(); let newX = this.elementStartPos.x + (e.clientX - this.dragStartPos.x); let newY = this.elementStartPos.y + (e.clientY - this.dragStartPos.y); const viewportWidth = window.visualViewport?.width || window.innerWidth; const viewportHeight = window.visualViewport?.height || window.innerHeight; newX = Math.max(0, Math.min(newX, Math.max(0, viewportWidth - this.element.offsetWidth))); newY = Math.max(0, Math.min(newY, Math.max(0, viewportHeight - this.element.offsetHeight))); this.element.style.transform = `translate3d(${newX}px, ${newY}px, 0)`; } handleDragEnd(e) { if (!this.isDragging || !this.element) { return; } e.stopPropagation(); this.isDragging = false; this.removeDragListeners(); this.playerManager?.resetIndicatorTimeout(); this.element.style.transition = ''; const matrix = new DOMMatrix(window.getComputedStyle(this.element).transform); const finalPos = { top: `${matrix.m42}px`, left: `${matrix.m41}px` }; this.element.style.left = finalPos.left; this.element.style.top = finalPos.top; this.element.style.transform = ''; this.playerManager.isUsingSavedPlayerPosition = true; const isVideoVertical = this.playerManager.videoElement.videoHeight > this.playerManager.videoElement.videoWidth; const videoOrientationKey = isVideoVertical ? 'verticalVideo' : 'horizontalVideo'; const isPhoneLandscape = screen.orientation.type.includes('landscape'); const phoneOrientationKey = isPhoneLandscape ? 'landscape' : 'portrait'; this.context.settingsManager.savePlayerPosition( window.location.hostname, `${phoneOrientationKey}_${videoOrientationKey}`, finalPos ); } } class CustomControlsHandler extends BaseModule { constructor(context, container, video) { super(context, 'CustomControlsHandler'); this.container = container; this.video = video; this.eventManager = new EventManager(); this.hideTimeout = null; this.isSeeking = false; this.lastKnownVolumeBeforeMute = 1.0; this._bindControlHandlers(); this.controlActions = this._createControlActions(); } _bindControlHandlers() { this.boundHandleVolumeChange = this.handleVolumeChange.bind(this); this.boundHandleTimeUpdate = this.handleTimeUpdate.bind(this); this.boundHandleDurationChange = this.handleDurationChange.bind(this); this.boundHandleProgress = this.handleProgress.bind(this); this.boundOnPlay = this.onPlay.bind(this); this.boundOnPause = this.onPause.bind(this); this.boundHandleTap = this.handleTap.bind(this); this.boundResetHideTimeout = this.resetHideTimeout.bind(this); this.boundHideControls = this.hideControls.bind(this); } _createControlActions() { return { '.dmz-big-play-button': this.togglePlay.bind(this), '.dmz-fullscreen-btn': this.toggleFullscreen.bind(this), '.dmz-mute-btn': this.toggleMute.bind(this), '.dmz-more-btn': this.toggleMoreMenu.bind(this), '.dmz-pip-btn': this.togglePip.bind(this), '.dmz-playback-rate-btn': this.showPlaybackRates.bind(this), }; } init() { this._cacheElements(); this.lastKnownVolumeBeforeMute = this.context.settingsManager.config.lastVolume; this.addEventListeners(); this._syncInitialUiState(); this._applyPipAvailability(); } _cacheElements() { this.controlsBar = this.container.querySelector('.dmz-controls-bar'); this.bigPlayButton = this.container.querySelector('.dmz-big-play-button'); this.currentTimeEl = this.container.querySelector('.dmz-current-time'); this.totalTimeEl = this.container.querySelector('.dmz-total-time'); this.progressBar = this.container.querySelector('.dmz-progress-bar'); this.playedBar = this.container.querySelector('.dmz-progress-played'); this.bufferBar = this.container.querySelector('.dmz-progress-buffer'); this.handle = this.container.querySelector('.dmz-progress-handle'); this.fullscreenEnterIcon = this.container.querySelector('.dmz-fullscreen-btn .icon-fullscreen-enter'); this.fullscreenExitIcon = this.container.querySelector('.dmz-fullscreen-btn .icon-fullscreen-exit'); this.volumeOnIcon = this.container.querySelector('.dmz-mute-btn .icon-volume-on'); this.volumeOffIcon = this.container.querySelector('.dmz-mute-btn .icon-volume-off'); this.moreMenu = this.container.querySelector('.dmz-more-menu'); this.pipBtn = this.container.querySelector('.dmz-pip-btn'); this.playbackRateBtn = this.container.querySelector('.dmz-playback-rate-btn'); this.playbackRateValue = this.container.querySelector('.dmz-playback-rate-btn .dmz-menu-item-value'); this.playbackRatesContainer = this.container.querySelector('.dmz-playback-rates'); } _syncInitialUiState() { this.updatePlayButton(); this.updateFullscreenButton(); this.updateMuteButton(); } _applyPipAvailability() { if (!document.pictureInPictureEnabled && this.pipBtn) { this.pipBtn.style.display = 'none'; } } addEventListeners() { this.bindMediaEvents(); this.bindUiEvents(); } bindMediaEvents() { this.eventManager.add(this.video, 'play', this.boundOnPlay); this.eventManager.add(this.video, 'pause', this.boundOnPause); this.eventManager.add(this.video, 'volumechange', this.boundHandleVolumeChange); this.eventManager.add(this.video, 'timeupdate', this.boundHandleTimeUpdate); this.eventManager.add(this.video, 'durationchange', this.boundHandleDurationChange); this.eventManager.add(this.video, 'progress', this.boundHandleProgress); } bindUiEvents() { this.eventManager.add(this.container, 'mousemove', this.boundResetHideTimeout); const shield = (e) => e.stopPropagation(); if (this.controlsBar) { this.eventManager.add(this.controlsBar, 'pointerdown', shield, { capture: true, }); } if (this.controlsBar) { this.eventManager.add(this.controlsBar, 'click', this.boundHandleTap, { capture: true, }); } if (this.bigPlayButton) { this.eventManager.add(this.bigPlayButton, 'click', this.boundHandleTap, { capture: true }); } } destroy() { this.eventManager.removeAll(); clearTimeout(this.hideTimeout); } handleTap(e) { e.stopPropagation(); this.handleTapOnControl(e.target); } onPlay() { this.updatePlayButton(); this.syncDiagnosticsOnPlay(); this.syncNeutralizedMediaForCurrentSource(); } syncDiagnosticsOnPlay() { const { playerManager, diagnosticsTool } = this.context; if (playerManager.currentVideoType === 'normal') { diagnosticsTool.playbackHealth.media.status = 'success'; } else if (diagnosticsTool.playbackHealth.media.status === 'pending') { diagnosticsTool.playbackHealth.media.status = 'success'; } } resolveMediaUrlCore(url) { try { return new URL(url).pathname; } catch (e) { return url; } } syncNeutralizedMediaForCurrentSource() { const { playerManager, coreLogic } = this.context; const currentPlayingUrl = playerManager.currentVideoUrl; if (!currentPlayingUrl) { return; } const playingUrlCore = this.resolveMediaUrlCore(currentPlayingUrl); coreLogic.findAllVideosAndAudioInPage().forEach((media) => { if (media.id === CONSTANTS.IDS.PLAYER || !media.dataset.dmzNeutralized) { return; } const originalSrc = media.dataset.dmzOriginalSrc; if (originalSrc && this.resolveMediaUrlCore(originalSrc) === playingUrlCore) { coreLogic.neutralizedMedia.add(media); coreLogic.startPersistentEnforcer(); } }); } onPause() { this.updatePlayButton(); } handleTapOnControl(targetElement) { if (!targetElement) { return false; } const actionKey = Object.keys(this.controlActions).find((selector) => targetElement.closest(selector)); if (actionKey) { this.controlActions[actionKey](); return true; } const rateButton = targetElement.closest('[data-rate]'); if (rateButton) { this.setPlaybackRate(parseFloat(rateButton.dataset.rate)); return true; } return false; } toggleMoreMenu() { const isVisible = DomUtils.toggleVisibility(this.moreMenu); if (isVisible) { DomUtils.toggleVisibility(this.playbackRatesContainer, false); if (this.pipBtn) { this.pipBtn.style.display = document.pictureInPictureEnabled ? 'flex' : 'none'; } if (this.playbackRateBtn) { this.playbackRateBtn.style.display = 'flex'; } } } hideMoreMenu() { DomUtils.toggleVisibility(this.moreMenu, false); } togglePip() { this.hideMoreMenu(); if (!document.pictureInPictureElement) { this.video .requestPictureInPicture() .catch((e) => this.log(`请求画中画失败:${e.message}`, CONSTANTS.LOG_TYPES.ERROR)); } else { document.exitPictureInPicture(); } } showPlaybackRates() { if (!this.playbackRatesContainer || !this.playbackRateBtn) { return; } if (this.pipBtn) { this.pipBtn.style.display = 'none'; } if (this.playbackRateBtn) { this.playbackRateBtn.style.display = 'none'; } DomUtils.toggleVisibility(this.playbackRatesContainer, true); } setPlaybackRate(rate) { this.video.playbackRate = rate; if (this.playbackRateValue) { this.playbackRateValue.textContent = rate === 1.0 ? '正常' : `${rate}x`; } if (this.playbackRatesContainer) { this.playbackRatesContainer.querySelector('.active')?.classList.remove('active'); this.playbackRatesContainer.querySelector(`[data-rate="${rate}"]`)?.classList.add('active'); } this.hideMoreMenu(); } toggleControlsVisibility() { clearTimeout(this.hideTimeout); this.hideMoreMenu(); if (this.controlsBar?.classList.contains(CONSTANTS.CLASSES.HIDDEN)) { this.resetHideTimeout(); } else { this.hideControls(); } } updateSeekUI(clientX) { if (!this.progressBar || !this.video?.duration) { return; } const rect = this.progressBar.getBoundingClientRect(); if (rect.width === 0) { return; } let pos = (clientX - rect.left) / rect.width; pos = Math.max(0, Math.min(1, pos)); const newTime = pos * this.video.duration; if (isNaN(newTime)) { return; } const percentage = pos * 100; if (this.playedBar) { this.playedBar.style.width = `${percentage}%`; } if (this.handle) { this.handle.style.left = `${percentage}%`; } if (this.currentTimeEl) { this.currentTimeEl.textContent = Utils.formatTime(newTime); } return newTime; } togglePlay() { if (this.video) { if (this.video.paused) { const promise = this.video.play(); if (promise !== undefined) { promise.catch((e) => console.debug('Playback blocked', e)); } } else { this.video.pause(); } } } toggleFullscreen() { this.context.playerManager.fullscreenHandler?.toggleState(); } toggleMute() { if (!this.video) { return; } if (this.video.muted || this.video.volume === 0) { this.video.volume = this.lastKnownVolumeBeforeMute > 0.05 ? this.lastKnownVolumeBeforeMute : 0.5; this.video.muted = false; } else { this.video.muted = true; } this.context.settingsManager.save({ isMuted: this.video.muted }, false); } handleVolumeChange() { this.updateMuteButton(); const effectivelyMuted = this.video.muted || this.video.volume === 0; if (effectivelyMuted) { this.context.settingsManager.save({ isMuted: true }, false); } else { this.lastKnownVolumeBeforeMute = this.video.volume; this.context.settingsManager.save({ lastVolume: this.video.volume, isMuted: false }, false); } } updatePlayButton() { const bigPlayButton = this.container.querySelector('.dmz-big-play-button'); const bigPlayButtonContainer = this.container.querySelector('.dmz-big-play-button-container'); if (!bigPlayButton) { return; } if (this.video.paused) { bigPlayButton.innerHTML = ICONS.BIG_PLAY; if (bigPlayButtonContainer) { bigPlayButtonContainer.style.opacity = '1'; } } else { bigPlayButton.innerHTML = ICONS.BIG_PAUSE.replace( /^]+>/, '' ); if (bigPlayButtonContainer) { bigPlayButtonContainer.style.opacity = ''; } } } updateFullscreenButton() { const isFullscreen = !!document.fullscreenElement; DomUtils.toggleIcon(isFullscreen ? this.fullscreenExitIcon : this.fullscreenEnterIcon, [ isFullscreen ? this.fullscreenEnterIcon : this.fullscreenExitIcon, ]); } updateMuteButton() { const isMuted = this.video.muted || this.video.volume === 0; DomUtils.toggleIcon(isMuted ? this.volumeOffIcon : this.volumeOnIcon, [ isMuted ? this.volumeOnIcon : this.volumeOffIcon, ]); } handleTimeUpdate() { if (this.isSeeking || !isFinite(this.video.duration)) { return; } requestAnimationFrame(() => { if (this.video && this.playedBar && this.handle && this.currentTimeEl) { const percentage = (this.video.currentTime / this.video.duration) * 100; this.playedBar.style.width = `${percentage}%`; this.handle.style.left = `${percentage}%`; this.currentTimeEl.textContent = Utils.formatTime(this.video.currentTime); } }); } handleDurationChange() { if (this.totalTimeEl) { this.totalTimeEl.textContent = Utils.formatTime(this.video.duration); } } handleProgress() { if (this.video.duration > 0 && this.video.buffered.length > 0 && this.bufferBar) { this.bufferBar.style.width = `${(this.video.buffered.end(this.video.buffered.length - 1) / this.video.duration) * 100}%`; } } showControls() { this.controlsBar?.classList.remove(CONSTANTS.CLASSES.HIDDEN); this.container.classList.add('dmz-controls-visible'); } hideControls() { if (this.isSeeking) { return; } this.controlsBar?.classList.add(CONSTANTS.CLASSES.HIDDEN); this.container.classList.remove('dmz-controls-visible'); this.hideMoreMenu(); } resetHideTimeout() { this.showControls(); clearTimeout(this.hideTimeout); this.hideTimeout = setTimeout(this.boundHideControls, 1500); } } class FullscreenGestureHandler extends BaseModule { constructor(context, container, video) { super(context, 'FullscreenGestureHandler'); this.container = container; this.video = video; this.isEnabled = false; this._cacheGestureElements(container); this._resetGestureState(); } _cacheGestureElements(container) { this.indicator = container.querySelector('.dmz-gesture-indicator-wrapper'); this.indicatorIconContainer = this.indicator?.querySelector('.dmz-indicator-icon'); this.indicatorText = this.indicator?.querySelector('.dmz-indicator-text'); this.brightnessOverlay = container.querySelector('.dmz-brightness-overlay'); this.brightnessIndicator = container.querySelector('.dmz-side-indicator-container.left'); this.brightnessFill = this.brightnessIndicator?.querySelector('.dmz-side-indicator-fill'); this.volumeIndicator = container.querySelector('.dmz-side-indicator-container.right'); this.volumeFill = this.volumeIndicator?.querySelector('.dmz-side-indicator-fill'); } _resetGestureState() { this.isSwiping = false; this.swipeType = null; this.startX = 0; this.startY = 0; this.startCurrentTime = 0; this.startVolume = 0; this.startBrightness = 0; this.hideIndicatorTimeout = null; this.longPressTimer = null; this.isFastForwarding = false; this.originalPlaybackRate = 1.0; this.lastTapTime = 0; } deactivate() { clearTimeout(this.longPressTimer); clearTimeout(this.hideIndicatorTimeout); } enable() { this.isEnabled = true; } disable() { this.isEnabled = false; } onPointerDown(e) { this.isSwiping = false; this.swipeType = null; this.startX = e.clientX; this.startY = e.clientY; this.startCurrentTime = this.video.currentTime; this.startVolume = this.video.volume; this.startBrightness = this.brightnessOverlay ? parseFloat(this.brightnessOverlay.style.opacity) || 0 : 0; clearTimeout(this.longPressTimer); this.longPressTimer = setTimeout(() => { if (!this.isSwiping) { this.activateFastForward(); } }, CONSTANTS.LIMITS.LONG_PRESS_DURATION_MS); } onPointerMove(e) { const deltaX = e.clientX - this.startX; const deltaY = e.clientY - this.startY; this._startSwipeIfNeeded(deltaX, deltaY); if (!this.isSwiping) { return; } this._resolveSwipeType(deltaX, deltaY); this._applySwipeAction(deltaX, deltaY); } _startSwipeIfNeeded(deltaX, deltaY) { if (this.isSwiping || (Math.abs(deltaX) <= 10 && Math.abs(deltaY) <= 10)) { return; } clearTimeout(this.longPressTimer); this.isSwiping = true; this.volumeFill?.classList.remove('transition-active'); this.brightnessFill?.classList.remove('transition-active'); } _resolveSwipeType(deltaX, deltaY) { if (this.swipeType) { return; } const absDeltaX = Math.abs(deltaX); const absDeltaY = Math.abs(deltaY); if (absDeltaX > absDeltaY * 2.0) { this.swipeType = 'progress'; return; } if (absDeltaY > absDeltaX * 2.0) { const playerRect = this.container.getBoundingClientRect(); this.swipeType = this.startX < playerRect.left + playerRect.width / 2 ? 'brightness' : 'volume'; } } _applySwipeAction(deltaX, deltaY) { const yAdjustment = (deltaY * 3.0) / window.innerHeight; switch (this.swipeType) { case 'progress': this._handleProgressSwipe(deltaX); break; case 'volume': this._handleVolumeSwipe(yAdjustment); break; case 'brightness': this._handleBrightnessSwipe(yAdjustment); break; } } _handleProgressSwipe(deltaX) { if (!isFinite(this.video.duration)) { return; } const newTime = this.startCurrentTime + (deltaX / window.innerWidth) * (this.video.duration * 0.15); this.video.currentTime = Math.max(0, Math.min(newTime, this.video.duration)); this.context.playerManager.customControlsHandler?.handleTimeUpdate(); this.showIndicator( deltaX > 0 ? 'progress' : 'rewind', `${Utils.formatTime(this.video.currentTime)} (${Math.round((this.video.currentTime / this.video.duration) * 100)}%)` ); } _handleVolumeSwipe(yAdjustment) { const newVolume = Math.max(0, Math.min(this.startVolume - yAdjustment, 1)); if (newVolume > 0 && this.video.muted) { this.video.muted = false; } this.video.volume = newVolume; this.showIndicator('volume', null, this.video.volume * 100); } _handleBrightnessSwipe(yAdjustment) { if (!this.brightnessOverlay) { return; } const newBrightness = this.startBrightness + yAdjustment; this.brightnessOverlay.style.opacity = Math.max(0, Math.min(newBrightness, 0.8)); this.showIndicator('brightness', null, (1 - parseFloat(this.brightnessOverlay.style.opacity) / 0.8) * 100); } onPointerUp(e) { this._restoreSwipeTransitionState(); clearTimeout(this.longPressTimer); if (this.isFastForwarding) { this.deactivateFastForward(); } else if (this.isSwiping) { this.hideIndicator(); } else { this._handleTapRelease(e); } this.isSwiping = false; this.swipeType = null; } _restoreSwipeTransitionState() { this.volumeFill?.classList.add('transition-active'); this.brightnessFill?.classList.add('transition-active'); } _handleTapRelease(e) { const { customControlsHandler } = this.context.playerManager; if (e.target.closest('.dmz-big-play-button')) { customControlsHandler?.togglePlay(); return; } const now = Date.now(); if (now - this.lastTapTime < 300) { customControlsHandler?.toggleFullscreen(); this.lastTapTime = 0; return; } this.lastTapTime = now; this._restoreStrategicMute(customControlsHandler); customControlsHandler?.toggleControlsVisibility(); } _restoreStrategicMute(customControlsHandler) { if (this.video.dataset.dmzStrategicMute !== 'true') { return; } this.video.muted = false; this.log('首次交互,已自动恢复声音。', CONSTANTS.LOG_TYPES.PLAYER); customControlsHandler?.updateMuteButton(); delete this.video.dataset.dmzStrategicMute; } activateFastForward() { if (!this.video || this.isFastForwarding) { return; } const longPressRate = this.context.settingsManager.config.longPressRate || 2.0; this.isFastForwarding = true; this.originalPlaybackRate = this.video.playbackRate; this.video.playbackRate = longPressRate; this.showIndicator('progress', `${longPressRate}x 倍速播放`); this.context.playerManager.customControlsHandler?.hideControls(); this.context.playerManager.showIndicatorPermanently(false); this.log(`长按倍速已激活`, CONSTANTS.LOG_TYPES.PLAYER); } deactivateFastForward() { if (!this.video || !this.isFastForwarding) { return; } this.isFastForwarding = false; this.video.playbackRate = this.originalPlaybackRate; this._hideAllIndicators(); this.log(`长按倍速已结束,恢复原速`, CONSTANTS.LOG_TYPES.PLAYER); } _hideAllIndicators() { this.indicator?.classList.remove(CONSTANTS.CLASSES.VISIBLE); this.brightnessIndicator?.classList.remove(CONSTANTS.CLASSES.VISIBLE); this.volumeIndicator?.classList.remove(CONSTANTS.CLASSES.VISIBLE); } _updateSideIndicator(indicator, fill, value) { if (!indicator || !fill) { return; } indicator.classList.add(CONSTANTS.CLASSES.VISIBLE); fill.style.height = `${value}%`; } showIndicator(type, text, value) { clearTimeout(this.hideIndicatorTimeout); this._hideAllIndicators(); if (type === 'brightness') { this._updateSideIndicator(this.brightnessIndicator, this.brightnessFill, value); } else if (type === 'volume') { this._updateSideIndicator(this.volumeIndicator, this.volumeFill, value); } else if (this.indicator && this.indicatorIconContainer && this.indicatorText) { Array.from(this.indicatorIconContainer?.querySelectorAll('svg') || []).forEach( (svg) => (svg.style.display = 'none') ); const icon = this.indicatorIconContainer.querySelector( type === 'rewind' ? '.icon-rewind' : `.icon-${type}` ); if (icon) { icon.style.display = 'block'; } this.indicatorText.textContent = text; this.indicator.classList.add(CONSTANTS.CLASSES.VISIBLE); } } hideIndicator() { this.hideIndicatorTimeout = setTimeout(() => this._hideAllIndicators(), 800); } } class FullscreenHandler extends BaseModule { constructor(context, container) { super(context, 'FullscreenHandler'); this.containerElement = container; this.eventManager = new EventManager(); this.onFullscreenChange = this.onFullscreenChange.bind(this); this.eventManager.add(document, 'fullscreenchange', this.onFullscreenChange); } destroy() { this.eventManager.removeAll(); } onFullscreenChange() { this.context.playerManager.customControlsHandler?.updateFullscreenButton(); this.context.playerManager.customControlsHandler?.resetHideTimeout(); } async toggleState() { if (document.fullscreenElement) { await this.exitFullscreen(); } else { await this.enterFullscreen(); } } async enterFullscreen() { try { await this.containerElement?.requestFullscreen(); } catch (e) { this.log(`全屏失败:${e.message}`, CONSTANTS.LOG_TYPES.ERROR); } } async exitFullscreen() { if (document.fullscreenElement) { await document.exitFullscreen(); } } } const PLAYER_RENDERABLE_SIGNAL_EVENTS = [ 'loadedmetadata', 'loadeddata', 'durationchange', 'resize', 'canplay', 'playing', 'progress', ]; const PLAYER_REVEAL_TRIGGER_EVENTS = PLAYER_RENDERABLE_SIGNAL_EVENTS.filter( (eventName) => eventName !== 'progress' ); const PLAYER_MEDIA_HEALTH_SUCCESS_EVENTS = ['loadedmetadata', 'loadeddata', 'canplay']; const PLAYER_POINTER_EVENT_BINDINGS = [ ['pointermove', 'boundHandlePointerMove'], ['pointerup', 'boundHandlePointerUp'], ['pointercancel', 'boundHandlePointerUp'], ]; const PlayerBindingHelpers = { core: PlayerBindingCore, createCleanupBag() { return PlayerBindingRuntimeUtils.createCleanupBag(); }, clearTimer(timerId) { return TimingUtils.clear(timerId); }, cancelVideoFrameCallback(video, frameCallbackId) { return PlayerBindingRuntimeUtils.cancelVideoFrameCallback(video, frameCallbackId); }, bindVideoSignalListeners(video, eventNames, cleanupFns, onSignal) { this.core.bindVideoListeners(video, eventNames, cleanupFns, onSignal); }, bindVideoErrorOnce(video, cleanupFns, onError) { PlayerBindingRuntimeUtils.bindVideoErrorOnce(video, cleanupFns, onError); }, bindFirstMatchingVideoSignal(video, eventNames, cleanupFns, onSignal, listenerOptions = { once: true }) { PlayerBindingRuntimeUtils.bindFirstMatchingVideoSignal( video, eventNames, cleanupFns, onSignal, listenerOptions ); }, startVideoFrameLoop(video, onFrame, storeId, shouldContinue) { PlayerBindingRuntimeUtils.startVideoFrameLoop(video, onFrame, storeId, shouldContinue); }, toggleDocumentPointerEvents(manager, operation) { PlayerBindingRuntimeUtils.toggleDocumentPointerEvents(manager, operation, PLAYER_POINTER_EVENT_BINDINGS); }, }; class PlayerManager extends BaseModule { constructor(context) { super(context, 'PlayerManager'); this._initializePlayerRuntimeFields(); this._bindPlayerHandlers(); } _initializePlayerRuntimeFields() { this.hostElement = null; this.shadowRoot = null; this.videoElement = null; this.hlsInstance = null; this.draggableInstance = null; this.fullscreenGestureHandler = null; this.customControlsHandler = null; this.fullscreenHandler = null; this.isPlayerActiveOrInitializing = false; this.isInternalRequest = false; this.currentVideoType = null; this.currentVideoUrl = null; this.currentVideoFormat = null; this.currentPlaybackEngine = null; this.lastM3u8Content = ''; this.currentRequestUrl = null; this.originalPlayerStyle = null; this.dragHandles = []; this.indicatorHideTimeout = null; this.currentVideoResolution = null; this.viewportClampTaskToken = 0; this.isUsingSavedPlayerPosition = false; this.lastAlignedTop = undefined; } _bindPlayerHandlers() { this.boundHandleOrientationChange = this._handleOrientationChange.bind(this); this.boundHandleViewportChange = this._handleViewportChange.bind(this); this.boundHandlePointerDown = this.handlePointerDown.bind(this); this.boundHandlePointerMove = this.handlePointerMove.bind(this); this.boundHandlePointerUp = this.handlePointerUp.bind(this); } showIndicatorPermanently(visible) { clearTimeout(this.indicatorHideTimeout); this.dragHandles.forEach((h) => h.classList.toggle('dmz-indicator-visible', visible)); } resetIndicatorTimeout() { this.showIndicatorPermanently(true); this.indicatorHideTimeout = setTimeout(() => { this.dragHandles.forEach((h) => h.classList.remove('dmz-indicator-visible')); }, 1000); } async createBasePlayerContainer() { this.log('创建播放器基础容器...', CONSTANTS.LOG_TYPES.PLAYER); this.cleanup(true); await this._mountPlayerHost(); const { actualVideo, videoWrapper, container } = this._createPlayerDomParts(); this.videoElement = actualVideo; this.shadowRoot.appendChild(container); this.initializePlayerModules(videoWrapper, actualVideo); this._bindWrapperPointerdown(videoWrapper); this.log('所有模块初始化完成。', CONSTANTS.LOG_TYPES.PLAYER); return { video: actualVideo, videoWrapper }; } async _mountPlayerHost() { this.hostElement = this.createHostElement(); window.addEventListener('orientationchange', this.boundHandleOrientationChange); window.addEventListener('resize', this.boundHandleViewportChange); window.visualViewport?.addEventListener('resize', this.boundHandleViewportChange); await this.ensureBodyReady(); document.body.appendChild(this.hostElement); this.shadowRoot = this.hostElement.attachShadow({ mode: 'closed' }); this.context.styleManager.injectPlayerStyles(this.shadowRoot); } _createPlayerDomParts() { const closeButton = this.createCloseButton(); const actualVideo = this.createPlayerVideoElement(); const videoWrapper = this.createVideoWrapper(closeButton, actualVideo); const container = this.createPlayerShell(videoWrapper); return { actualVideo, videoWrapper, container }; } _bindWrapperPointerdown(videoWrapper) { videoWrapper.addEventListener('pointerdown', this.boundHandlePointerDown, { capture: true }); } createHostElement() { return PlayerDomFactory.createHostElement(); } async ensureBodyReady() { if (!document.body) { await new Promise((resolve) => window.addEventListener('DOMContentLoaded', resolve, { once: true })); } } createCloseButton() { return PlayerDomFactory.createCloseButton(this); } createPlayerVideoElement() { return PlayerDomFactory.createPlayerVideoElement(this); } applySavedMutePreference(video, savedConfig) { PlayerDomFactory.applySavedMutePreference(this, video, savedConfig); } createVideoWrapper(closeButton, actualVideo) { return PlayerDomFactory.createVideoWrapper(this, closeButton, actualVideo); } bindWrapperActivity(videoWrapper) { PlayerDomFactory.bindWrapperActivity(this, videoWrapper); } createPlayerShell(videoWrapper) { return PlayerDomFactory.createPlayerShell(this, videoWrapper); } initializePlayerModules(videoWrapper, actualVideo) { this.draggableInstance = new Draggable(this.context, this.hostElement, this.dragHandles, this); this.customControlsHandler = new CustomControlsHandler(this.context, videoWrapper, actualVideo); this.customControlsHandler.init(); this.fullscreenGestureHandler = new FullscreenGestureHandler(this.context, videoWrapper, actualVideo); this.fullscreenGestureHandler.enable(); this.fullscreenHandler = new FullscreenHandler(this.context, videoWrapper); this.activeInteraction = null; } async _applyOrientationLayout() { const layoutContext = this._getOrientationLayoutContext(); if (!layoutContext) { return; } const savedPos = await this.context.settingsManager.loadPlayerPosition( window.location.hostname, `${layoutContext.phoneOrientationKey}_${layoutContext.videoOrientationKey}` ); if (layoutContext.isPhoneLandscape) { this._applyLandscapePlayerLayout(layoutContext, savedPos); return; } this._applyPortraitPlayerLayout(layoutContext, savedPos); } _getOrientationLayoutContext() { if ( !this.hostElement || !this.videoElement || !this.isPlayerActiveOrInitializing || !this.videoElement.videoWidth ) { return null; } const orientationType = screen.orientation?.type ?? (Math.abs(window.orientation || 0) === 90 ? 'landscape' : 'portrait'); const isPhoneLandscape = orientationType.includes('landscape'); const isVideoVertical = this.videoElement.videoHeight > this.videoElement.videoWidth; return { isPhoneLandscape, isVideoVertical, videoOrientationKey: isVideoVertical ? 'verticalVideo' : 'horizontalVideo', phoneOrientationKey: isPhoneLandscape ? 'landscape' : 'portrait', }; } _applyPortraitPlayerLayout(layoutContext, savedPos) { let restored = false; if (this.originalPlayerStyle) { Object.assign(this.hostElement.style, this.originalPlayerStyle); this.originalPlayerStyle = null; restored = true; } this.hostElement.style.width = layoutContext.isVideoVertical ? '60vw' : '100vw'; this.hostElement.style.height = ''; if (restored) { if (layoutContext.isVideoVertical) { this.hostElement.style.left = '20vw'; this.hostElement.style.transform = ''; } this._scheduleClampPlayerIntoViewport(); return; } const usedSavedPosition = this._applyPortraitPlayerPosition(layoutContext.isVideoVertical, savedPos); if (!usedSavedPosition) { this._scheduleClampPlayerIntoViewport(); } } _applyPortraitPlayerPosition(isVideoVertical, savedPos) { const fallbackTop = isVideoVertical ? '192px' : '60px'; const posToUse = (savedPos && savedPos.top) ? savedPos : null; const usedSavedPosition = this._applySavedPlayerPosition(posToUse, () => { this._setPortraitPlayerFallbackPosition(fallbackTop, isVideoVertical); }, isVideoVertical); if (this._applyAlignedTopFromVisibleNativePlayer(isVideoVertical)) { return false; } return usedSavedPosition; } _setPortraitPlayerFallbackPosition(top, isVideoVertical = false) { this.hostElement.style.top = top; this.hostElement.style.left = isVideoVertical ? '50%' : '0px'; this.hostElement.style.transform = isVideoVertical ? 'translateX(-50%)' : ''; } _applyLandscapePlayerLayout(layoutContext, savedPos) { if (!this.originalPlayerStyle && this.hostElement.classList.contains('dmz-visible')) { this.originalPlayerStyle = { left: this.hostElement.style.left, top: this.hostElement.style.top, width: this.hostElement.style.width, height: this.hostElement.style.height, transform: this.hostElement.style.transform, }; } this.log('应用横屏布局。', CONSTANTS.LOG_TYPES.UI); this.hostElement.style.transform = ''; const videoAspectRatio = this.videoElement.videoWidth / this.videoElement.videoHeight; if (layoutContext.isVideoVertical) { this.hostElement.style.height = 'calc(100vh - 40px)'; this.hostElement.style.width = `calc((100vh - 40px) * ${videoAspectRatio})`; } else { const defaultWidth = '45vw'; this.hostElement.style.width = defaultWidth; this.hostElement.style.height = `calc(${defaultWidth} / ${videoAspectRatio})`; } const usedSavedPosition = this._applyLandscapePlayerPosition(savedPos); if (!usedSavedPosition) { this._scheduleClampPlayerIntoViewport(); } } _applyLandscapePlayerPosition(savedPos) { const usedSavedPosition = this._applySavedPlayerPosition(savedPos, () => { this._setLandscapePlayerFallbackPosition(); }); if (this._applyAlignedTopFromVisibleNativePlayer(this.videoElement.videoHeight > this.videoElement.videoWidth)) { return false; } return usedSavedPosition; } _applySavedPlayerPosition(savedPos, applyFallback, isVideoVertical = false) { if (savedPos) { const left = parseFloat(savedPos.left); const top = parseFloat(savedPos.top); if (this._isSavedPlayerPositionOutOfBounds(left, top)) { applyFallback(); this.isUsingSavedPlayerPosition = false; return false; } this.hostElement.style.top = savedPos.top; if (isVideoVertical) { this.hostElement.style.left = '50%'; this.hostElement.style.transform = 'translateX(-50%)'; } else { this.hostElement.style.left = savedPos.left; this.hostElement.style.transform = ''; } this.isUsingSavedPlayerPosition = true; return true; } applyFallback(); this.isUsingSavedPlayerPosition = false; return false; } _setLandscapePlayerFallbackPosition() { this.hostElement.style.top = '20px'; this.hostElement.style.left = '20px'; } _getAlignedTargetAttrText(el) { if (!el) { return ''; } return [ el.tagName || '', el.id || '', typeof el.className === 'string' ? el.className : '', el.getAttribute?.('title') || '', el.getAttribute?.('aria-label') || '', el.getAttribute?.('role') || '', el.getAttribute?.('data-role') || '', el.getAttribute?.('data-testid') || '', ].join(' ').toLowerCase(); } _isBadAlignedContainer(el) { if (!el || el.tagName === 'VIDEO' || el.tagName === 'IFRAME') { return false; } const attrText = this._getAlignedTargetAttrText(el); if (!attrText) { return false; } if (SHARED_PATTERNS.NEGATIVE_UI_KEYWORDS.test(attrText) || SHARED_PATTERNS.NEGATIVE_PLAYBACK_CONTROLS.test(attrText)) { return true; } return /(menu|links|toolbar|actions?|header|nav|tabs?|scroll|root|icon|button|btn|avatar|profile|comment|share|report|recommend|related|search)/i.test(attrText); } _getTrueVideoRect(target, rawRect) { let trueRect = { top: rawRect.top, left: rawRect.left, width: rawRect.width, height: rawRect.height, right: rawRect.right, bottom: rawRect.bottom }; let vw = this.videoElement?.videoWidth || 0; let vh = this.videoElement?.videoHeight || 0; if (!vw || !vh) { if (target.tagName === 'VIDEO' && target.videoWidth && target.videoHeight) { vw = target.videoWidth; vh = target.videoHeight; } else if (target.tagName === 'IFRAME') { if (target.dataset.dmzVideoWidth && target.dataset.dmzVideoHeight) { vw = parseFloat(target.dataset.dmzVideoWidth); vh = parseFloat(target.dataset.dmzVideoHeight); } else { try { const innerVideo = target.contentDocument?.querySelector('video'); if (innerVideo && innerVideo.videoWidth && innerVideo.videoHeight) { vw = innerVideo.videoWidth; vh = innerVideo.videoHeight; } } catch (e) {} } } } if (!vw || !vh) return trueRect; const cRatio = rawRect.width / rawRect.height; const vRatio = vw / vh; if (Math.abs(cRatio - vRatio) < 0.01) return trueRect; if (cRatio > vRatio) { const actualWidth = rawRect.height * vRatio; const offsetX = (rawRect.width - actualWidth) / 2; trueRect.width = actualWidth; trueRect.left = rawRect.left + offsetX; trueRect.right = trueRect.left + actualWidth; } else { const actualHeight = rawRect.width / vRatio; const offsetY = (rawRect.height - actualHeight) / 2; trueRect.height = actualHeight; trueRect.top = rawRect.top + offsetY; trueRect.bottom = trueRect.top + actualHeight; } return trueRect; } _resolveAlignedMediaCandidate(el, viewportWidth, viewportHeight) { if (!el || !MediaViewportUtils.isVisibleElement(el)) { return null; } if (el.closest(CONSTANTS.SELECTORS.DMZ_EXCLUSION)) { return null; } let targetEl = null; if (el.tagName === 'VIDEO' || el.tagName === 'IFRAME') { targetEl = el; } else { targetEl = el.querySelector('video, iframe'); if (!targetEl && this._isBadAlignedContainer(el)) { return null; } } const scoreEl = targetEl || el; if (!MediaViewportUtils.isVisibleElement(scoreEl)) { return null; } const rawRect = scoreEl.getBoundingClientRect(); const rect = this._getTrueVideoRect(scoreEl, rawRect); if (rect.width < 160 || rect.height < 90) { return null; } if ( rect.bottom <= 0 || rect.top >= viewportHeight || rect.right <= 0 || rect.left >= viewportWidth ) { return null; } let score = MediaViewportUtils.getPrimaryMediaCandidateScore(scoreEl, rect, viewportWidth, viewportHeight); if (scoreEl.tagName === 'VIDEO' || scoreEl.tagName === 'IFRAME') { score += 500000; } else { score -= 180000; } if (targetEl && targetEl !== el) { score += 220000; } return { rect, score }; } _getFullyVisibleNativePlayerRect() { const viewportWidth = window.visualViewport?.width || window.innerWidth; const viewportHeight = window.visualViewport?.height || window.innerHeight; let bestRect = null; let bestScore = -Infinity; for (const el of document.querySelectorAll(CONSTANTS.SELECTORS.PLAYER_ELEMENTS)) { const candidate = this._resolveAlignedMediaCandidate(el, viewportWidth, viewportHeight); if (!candidate) { continue; } if (candidate.score > bestScore) { bestScore = candidate.score; bestRect = candidate.rect; } } if (!bestRect) { const triggers = document.querySelectorAll('.dmz-switch-overlay, .dmz-iframe-remote-trigger'); for (const trigger of triggers) { if (trigger.offsetWidth > 0 && trigger.offsetHeight > 0) { const rect = trigger.getBoundingClientRect(); if (rect.width > 50 && rect.height > 50) { return { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom, width: rect.width, height: rect.height, }; } } } return null; } return { left: bestRect.left, top: bestRect.top, right: bestRect.right, bottom: bestRect.bottom, width: bestRect.width, height: bestRect.height, }; } _applyAlignedTopFromVisibleNativePlayer(isVideoVertical = false) { if (!this.hostElement) { return false; } let rect = this._getFullyVisibleNativePlayerRect(); if ( isVideoVertical && rect && rect.top > window.innerHeight / 2 && this.videoElement && this.videoElement.videoWidth > 0 ) { const hostWidth = window.innerWidth * 0.6; const videoAspectRatio = this.videoElement.videoHeight / this.videoElement.videoWidth; const predictedVideoHeight = hostWidth * videoAspectRatio; const dragHandleHeight = 18; const gap = 3; const predictedHostHeight = predictedVideoHeight + 2 * dragHandleHeight + gap; const shellTopOffset = dragHandleHeight; const potentialTop = Math.max(0, Math.round(rect.top - shellTopOffset)); if (potentialTop + predictedHostHeight > window.innerHeight) { return false; } } if (rect && rect.bottom > window.innerHeight) { rect = null; } if (!rect && this.lastAlignedTop === undefined) { return false; } if (rect) { this.lastAlignedTop = rect.top; } const targetTop = rect ? rect.top : this.lastAlignedTop; const shellTopOffset = Math.max( 0, Math.round( this.dragHandles?.[0]?.getBoundingClientRect?.().height || this.dragHandles?.[0]?.offsetHeight || 0 ) ); this.hostElement.style.top = `${Math.max(0, Math.round(targetTop - shellTopOffset))}px`; if (isVideoVertical) { this.hostElement.style.left = '50%'; this.hostElement.style.transform = 'translateX(-50%)'; } this.isUsingSavedPlayerPosition = false; return true; } _getEstimatedExpandedPlayerHeight(referenceWidth = 0) { if (!this.videoElement || !this.videoElement.videoWidth || !this.videoElement.videoHeight) { return 0; } const width = referenceWidth || this.hostElement?.getBoundingClientRect?.().width || this.hostElement?.offsetWidth || 0; if (!width) { return 0; } const dragHandleHeight = Math.max( 18, Math.round( this.dragHandles?.[0]?.getBoundingClientRect?.().height || this.dragHandles?.[0]?.offsetHeight || 18 ) ); const gap = Math.max(0, parseFloat(this.hostElement?.style?.gap || '3') || 3); const videoAspectRatio = this.videoElement.videoHeight / this.videoElement.videoWidth; const predictedVideoHeight = width * videoAspectRatio; const revealedVideoHeight = Math.min(predictedVideoHeight, window.innerHeight * 0.8); return revealedVideoHeight + dragHandleHeight * 2 + gap; } _getViewportClampRect() { const rect = this.hostElement.getBoundingClientRect(); let effectiveHeight = rect.height; if (effectiveHeight <= 48) { effectiveHeight = Math.max(effectiveHeight, this._getEstimatedExpandedPlayerHeight(rect.width)); } return { left: rect.left, top: rect.top, width: rect.width, height: effectiveHeight, }; } _isSavedPlayerPositionOutOfBounds(left, top) { if (!Number.isFinite(left) || !Number.isFinite(top)) { return true; } const viewportWidth = window.visualViewport?.width || window.innerWidth; const viewportHeight = window.visualViewport?.height || window.innerHeight; const rect = this.hostElement?.getBoundingClientRect?.(); const effectiveWidth = rect?.width || this.hostElement?.offsetWidth || 0; const effectiveHeight = rect ? Math.max(rect.height || 0, rect.height > 48 ? 0 : this._getEstimatedExpandedPlayerHeight(rect.width || 0)) : this._getEstimatedExpandedPlayerHeight(); return ( left < -50 || top < -50 || (effectiveWidth > 0 && left + effectiveWidth > viewportWidth + 50) || (effectiveHeight > 0 && top + effectiveHeight > viewportHeight + 50) ); } _handleViewportChange() { this._scheduleClampPlayerIntoViewport(); } _scheduleClampPlayerIntoViewport() { const token = ++this.viewportClampTaskToken; requestAnimationFrame(() => { requestAnimationFrame(() => { if (token !== this.viewportClampTaskToken) { return; } this._clampPlayerIntoViewport(); }); }); } _schedulePostRevealViewportClamp(videoWrapper) { this._scheduleClampPlayerIntoViewport(); setTimeout(() => this._scheduleClampPlayerIntoViewport(), 80); setTimeout(() => this._scheduleClampPlayerIntoViewport(), 420); setTimeout(() => this._scheduleClampPlayerIntoViewport(), 1100); if (videoWrapper) { const onceClamp = (event) => { if (event.propertyName && event.propertyName !== 'max-height') { return; } videoWrapper.removeEventListener('transitionend', onceClamp); this._scheduleClampPlayerIntoViewport(); }; videoWrapper.addEventListener('transitionend', onceClamp, { once: true }); } } _clampPlayerIntoViewport() { if (!this.hostElement || !this.isPlayerActiveOrInitializing) { return; } const rect = this._getViewportClampRect(); const viewportWidth = window.visualViewport?.width || window.innerWidth; const viewportHeight = window.visualViewport?.height || window.innerHeight; const maxLeft = Math.max(0, viewportWidth - rect.width); const maxTop = Math.max(0, viewportHeight - rect.height); const clampedLeft = Math.max(0, Math.min(rect.left, maxLeft)); const clampedTop = Math.max(0, Math.min(rect.top, maxTop)); this.hostElement.style.left = `${clampedLeft}px`; this.hostElement.style.top = `${clampedTop}px`; this.hostElement.style.transform = ''; } _handleOrientationChange() { if (!this.hostElement) { return; } this.hostElement.classList.add('dmz-no-transition'); this._applyOrientationLayout().finally(() => { this._scheduleClampPlayerIntoViewport(); requestAnimationFrame(() => this.hostElement?.classList.remove('dmz-no-transition')); }); } async _preparePlaybackSession(videoType) { const { settingsManager } = this.context; this.isPlayerActiveOrInitializing = true; this.currentVideoType = videoType; if (!CONSTANTS.IS_TOP_FRAME && settingsManager.config.crossFrameSupport) { this.isPlayerActiveOrInitializing = false; return null; } return await this.createBasePlayerContainer(); } async revealPlayer(video, videoWrapper) { const { coreLogic } = this.context; this.neutralizeVisibleMainIframe(); if (this._tryFastReveal(video, videoWrapper)) { return; } this.log(`视频尺寸未就绪,启动严格校验模式...`, CONSTANTS.LOG_TYPES.PLAYER_REVEAL); try { const { width, height } = await this.waitForRenderableDimensions(video); this._completeWaitedReveal(video, videoWrapper, width, height); } catch (error) { this.handleRevealFailure(error, coreLogic); } } _tryFastReveal(video, videoWrapper) { return this.tryRevealWithCurrentDimensions(video, videoWrapper); } _completeWaitedReveal(video, videoWrapper, width, height) { this.log(`轮询成功,视频尺寸已确认:(${width}x${height})。`, CONSTANTS.LOG_TYPES.PLAYER_REVEAL); this._finalizePlayerReveal(video, videoWrapper, width, height); } neutralizeVisibleMainIframe() { const { coreLogic, frameCommunicator } = this.context; const mainIframe = frameCommunicator.mainPlayerIframeElement; if (mainIframe && mainIframe.style.visibility !== 'hidden') { this.log(`渲染成功,执行中和:留空间并隐藏Iframe。`, CONSTANTS.LOG_TYPES.CORE_NEUTRALIZE); coreLogic.hiddenIframeElement = mainIframe; coreLogic.originalIframeVisibilityStyle = mainIframe.style.visibility; coreLogic.originalIframePointerEventsStyle = mainIframe.style.pointerEvents; coreLogic.originalIframeSrc = mainIframe.src; mainIframe.style.visibility = 'hidden'; mainIframe.style.pointerEvents = 'none'; } } tryRevealWithCurrentDimensions(video, videoWrapper) { if (video.videoWidth > 0 && video.videoHeight > 0) { this._finalizePlayerReveal(video, videoWrapper, video.videoWidth, video.videoHeight); return true; } return false; } async waitForRenderableDimensions(video) { await this._waitForRenderSizeReady(video); return this._getRenderableDimensions(video); } _waitForRenderSizeReady(video) { return new Promise((resolve, reject) => { let attempts = 0; let timerId = null; let frameCallbackId = null; let settled = false; const cleanupBag = PlayerBindingHelpers.createCleanupBag(); const cleanup = () => { timerId = PlayerBindingHelpers.clearTimer(timerId); frameCallbackId = PlayerBindingHelpers.cancelVideoFrameCallback(video, frameCallbackId); cleanupBag.run(); }; const settle = (handler, payload) => { if (settled) { return; } settled = true; cleanup(); handler(payload); }; const probe = () => { if (settled) { return; } if (timerId) { clearTimeout(timerId); timerId = null; } const abortError = this._getRenderWaitAbortError(video); if (abortError) { settle(reject, abortError); return; } if (this._hasRenderableDimensions(video)) { settle(resolve); return; } attempts += 1; this._tryWarmupVideoPlayback(video, attempts); if (attempts > this._getRenderableDimensionMaxAttempts()) { settle(reject, new Error(`等待视频尺寸超时`)); return; } timerId = setTimeout(probe, this._getRenderableDimensionProbeDelay(attempts)); }; this._bindRenderableDimensionSignals(video, cleanupBag.cleanupFns, probe); this._startRenderableVideoFrameProbe(video, probe, (nextId) => { frameCallbackId = nextId; }); probe(); }); } _bindRenderableDimensionSignals(video, cleanupFns, probe) { PlayerBindingHelpers.bindVideoSignalListeners(video, PLAYER_RENDERABLE_SIGNAL_EVENTS, cleanupFns, probe); } _startRenderableVideoFrameProbe(video, probe, storeId) { PlayerBindingHelpers.startVideoFrameLoop( video, probe, storeId, () => !this._hasRenderableDimensions(video) ); } _getRenderableDimensionProbeDelay(attempts) { if (attempts < 12) { return 16; } if (attempts < 40) { return 32; } if (attempts < 120) { return 60; } return 120; } _getRenderableDimensionMaxAttempts() { return (this.context.coreLogic.lastCandidatesBackup?.size ?? 0) > 2 ? 200 : 500; } _hasRenderableDimensions(video) { return !!(video.videoWidth > 0 && video.videoHeight > 0); } _tryWarmupVideoPlayback(video, attempts) { if (![2, 6, 14, 32].includes(attempts)) { return; } const p = video.play(); if (p) { p.catch(() => {}); } } _getRenderableDimensions(video) { return { width: video.videoWidth, height: video.videoHeight, }; } _getRenderWaitAbortError(video) { if (!this.hostElement) { return new Error('ABORT_TASK'); } if (video.error || video.networkState === 3) { return new Error('视频源已失效(NetworkState=3)或加载错误'); } return null; } handleRevealFailure(error, coreLogic) { if (error.message === 'ABORT_TASK') { this.log(`[渲染中止]任务被中断 (用户操作/页面切换)。`, CONSTANTS.LOG_TYPES.LIFECYCLE); this.cleanup(); } else { this.log(`[渲染失败]|${error.message}->正在尝试备用信号...`, CONSTANTS.LOG_TYPES.WARN); coreLogic.reportPlaybackFailure({ failedUrl: this.currentVideoUrl, reason: error.message, }); } } async _finalizePlayerReveal(video, videoWrapper, finalW, finalH) { const { settingsManager, coreLogic } = this.context; this.currentVideoResolution = `${finalW}x${finalH}`; this.predefinedResolution = null; this.log(`视频分辨率已确认:(${this.currentVideoResolution})。`, CONSTANTS.LOG_TYPES.PLAYER_REVEAL); coreLogic.sandboxManager.cleanup(); this.log(`视频元数据就绪,已满足显示前置条件,开始展示播放器(${finalW}x${finalH})。`, CONSTANTS.LOG_TYPES.PLAYER_REVEAL); this.broadcastPauseToFrames(); if (!this.ensureRevealHostAvailable()) { return; } await this.applyRevealLayout(video, videoWrapper, finalW, finalH); this.applyRevealPlaybackPreference(settingsManager, video); } broadcastPauseToFrames() { try { const frames = window.frames; for (let i = 0; i < frames.length; i++) { try { frames[i].postMessage({ type: CONSTANTS.MESSAGE_TYPES.FORCE_PAUSE_ALL }, '*'); } catch (e) { console.debug('Failed to pause iframe:', e); } } } catch (e) { console.debug('Failed to iterate frames:', e); } } ensureRevealHostAvailable() { if (!this.hostElement) { this.log('播放器显示失败:宿主元素不存在。', CONSTANTS.LOG_TYPES.WARN); this.cleanup(); return false; } return true; } async applyRevealLayout(video, videoWrapper, finalW, finalH) { this.hostElement.classList.add('dmz-no-transition'); await this._applyOrientationLayout(); this.hostElement.style.gap = '3px'; videoWrapper.style.flexGrow = '1'; video.style.opacity = '1'; void this.hostElement.offsetHeight; this.animateReveal(videoWrapper, finalW, finalH); } animateReveal(videoWrapper, finalW, finalH) { requestAnimationFrame(() => { if (this.hostElement) { this.hostElement.classList.remove('dmz-no-transition'); this.hostElement.classList.add('dmz-visible'); requestAnimationFrame(() => { if (videoWrapper && finalW > 0) { const w = this.hostElement.clientWidth; const h = w * (finalH / finalW); videoWrapper.style.maxHeight = `${Math.min(h, window.innerHeight * 0.8)}px`; } this._schedulePostRevealViewportClamp(videoWrapper); }); } }); } applyRevealPlaybackPreference(settingsManager, video) { if (settingsManager.config.autoPlay) { const p = video.play(); if (p !== undefined) { p.catch((e) => { this.log(`自动播放|⏸️|被阻断:${e.message}`, CONSTANTS.LOG_TYPES.WARN); }); } this.log('自动播放|▶️|已启动。', CONSTANTS.LOG_TYPES.PLAYER); this.customControlsHandler?.hideControls(); this.showIndicatorPermanently(false); } else { this.log('自动播放|🚫|已禁用,请手动点击播放。', CONSTANTS.LOG_TYPES.PLAYER); video.pause(); this.customControlsHandler?.resetHideTimeout(); } } async play(videoData) { const { coreLogic } = this.context; this.log('指令已接收,准备渲染视频内容。', CONSTANTS.LOG_TYPES.PLAYER); const playbackRequest = this._buildPlaybackRequest(videoData); this._applyPlaybackRequest(playbackRequest); const containerElements = await this._preparePlaybackSession(playbackRequest.videoType); if (!containerElements) { coreLogic.sendPlayCommand(playbackRequest.videoType, playbackRequest.videoData); return; } this._bindPlaybackReadyReveal(containerElements); this._startPlaybackRequest(playbackRequest, containerElements.video); } _buildPlaybackRequest(videoData) { const isM3U8 = typeof videoData === 'object'; return { videoData, isM3U8, videoType: isM3U8 ? 'm3u8' : 'normal', currentVideoUrl: isM3U8 ? videoData.finalUrl : videoData, lastM3u8Content: isM3U8 ? videoData.original : '', }; } _applyPlaybackRequest(playbackRequest) { this.currentVideoUrl = playbackRequest.currentVideoUrl; this.lastM3u8Content = playbackRequest.lastM3u8Content; this.currentRequestUrl = this.currentVideoUrl; } _bindPlaybackReadyReveal(containerElements) { const { video, videoWrapper } = containerElements; let isRevealed = false; let frameCallbackId = null; const cleanupBag = PlayerBindingHelpers.createCleanupBag(); const cleanup = () => { cleanupBag.run(); frameCallbackId = PlayerBindingHelpers.cancelVideoFrameCallback(video, frameCallbackId); }; const onReady = () => { if (isRevealed) { return; } const hasConfirmedSize = video.videoWidth > 0 || video.videoHeight > 0; if (!(hasConfirmedSize || video.readyState >= 1)) { return; } isRevealed = true; cleanup(); this.revealPlayer(video, videoWrapper); }; const metadataTimeoutMs = this._getAdaptiveMetadataLoadTimeoutMs(); const stallTimeoutId = setTimeout(() => { if (!isRevealed && this.isPlayerActiveOrInitializing && this.videoElement === video) { cleanup(); this.log(`视频元数据加载超时(${Math.round(metadataTimeoutMs / 1000)}s无响应),判定为网络堵塞或死链。`, CONSTANTS.LOG_TYPES.ERROR); this.context.coreLogic.reportPlaybackFailure({ reason: 'Metadata Load Timeout', }); } }, metadataTimeoutMs); cleanupBag.cleanupFns.push(() => clearTimeout(stallTimeoutId)); PlayerBindingHelpers.bindVideoSignalListeners( video, PLAYER_REVEAL_TRIGGER_EVENTS, cleanupBag.cleanupFns, onReady ); PlayerBindingHelpers.startVideoFrameLoop( video, onReady, (nextId) => { frameCallbackId = nextId; }, () => !isRevealed ); onReady(); } _getAdaptiveMetadataLoadTimeoutMs() { if (this._isVolatileSpaDirectCandidate()) { return 5000; } return 12000; } _isVolatileSpaDirectCandidate() { const coreLogic = this.context.coreLogic; if (!coreLogic) { return false; } const navAt = coreLogic.lastSpaNavigationAt || 0; if (!navAt || coreLogic.navigationRequestId <= 0) { return false; } const elapsed = Date.now() - navAt; if (elapsed < 0 || elapsed > 9000) { return false; } const sourceName = coreLogic.currentDecisionSourceName || ''; if (!/(页面元素扫描\(data-\*\)|iFrame信使 \(页面元素扫描\(data-\*\)\))/u.test(sourceName)) { return false; } const currentUrl = this.currentVideoUrl || ''; if (!currentUrl || /\.m3u8(?:$|[?#])/i.test(currentUrl)) { return false; } return /(?:^|[?&])(seasonId|hevc)=|[a-f0-9]{16,}/i.test(currentUrl); } _startPlaybackRequest(playbackRequest, videoElement) { if (playbackRequest.isM3U8) { this.playM3U8(playbackRequest.videoData, videoElement, true); return; } this.playNormalVideo(playbackRequest.videoData, videoElement); } _recordCurrentM3u8Quality(m3u8Data, coreLogic) { this.currentVideoFormat = { name: 'M3U8', type: '流式传输' }; const qualityMatch = m3u8Data.finalUrl.match(/\/(\d+p)\//); coreLogic.currentPlayingQuality = qualityMatch ? qualityMatch[1] : 'unknown'; if (coreLogic.currentPlayingQuality !== 'unknown') { this.log(`当前播放清晰度已记录为:${coreLogic.currentPlayingQuality}。`, CONSTANTS.LOG_TYPES.PLAYER); } } _syncM3u8KeyStatus(originalContent, diagnosticsTool) { if (originalContent.includes('#EXT-X-KEY') && !originalContent.includes('METHOD=NONE')) { diagnosticsTool.playbackHealth.key.status = 'pending'; } else { diagnosticsTool.playbackHealth.key.status = 'not_encrypted'; } } _refreshM3u8BlobSource(contentToPlay, diagnosticsTool) { if (this.m3u8BlobUrl) { URL.revokeObjectURL(this.m3u8BlobUrl); this.m3u8BlobUrl = null; } const blob = new Blob([contentToPlay], { type: 'application/vnd.apple.mpegurl', }); this.m3u8BlobUrl = URL.createObjectURL(blob); diagnosticsTool.lastProcessedM3u8 = contentToPlay; } _prewarmUrl(url) { if (!url) { return; } try { const origin = new URL(url, window.location.href).origin; if (!origin || origin === 'null') { return; } if (!this._prewarmedOrigins) { this._prewarmedOrigins = new Set(); } if (this._prewarmedOrigins.has(origin)) { return; } this._prewarmedOrigins.add(origin); if (!document.head) { return; } const dnsPrefetch = document.createElement('link'); dnsPrefetch.rel = 'dns-prefetch'; dnsPrefetch.href = origin; document.head.appendChild(dnsPrefetch); const preconnect = document.createElement('link'); preconnect.rel = 'preconnect'; preconnect.href = origin; preconnect.crossOrigin = 'anonymous'; document.head.appendChild(preconnect); } catch (e) { console.debug('Failed to prewarm url:', e); } } _prewarmM3u8Targets(m3u8Data) { this._prewarmUrl(m3u8Data?.finalUrl); const processed = String(m3u8Data?.processed || ''); if (!processed) { return; } let warmedCount = 0; for (const rawLine of processed.split('\n')) { const line = rawLine.trim(); if (!line || line.startsWith('#')) { continue; } this._prewarmUrl(line); warmedCount++; if (warmedCount >= 2) { break; } } } _attachHlsPlayback(videoElement, isFirstPlay) { PlayerHlsController.attachPlayback(this, videoElement, isFirstPlay); } _resetInternalRequestFlagLater() { setTimeout(() => { this.isInternalRequest = false; }, 500); } playM3U8(m3u8Data, videoElement, isFirstPlay) { const { diagnosticsTool, coreLogic } = this.context; this._recordCurrentM3u8Quality(m3u8Data, coreLogic); this._syncM3u8KeyStatus(m3u8Data.original ?? '', diagnosticsTool); this._prewarmM3u8Targets(m3u8Data); this._refreshM3u8BlobSource(m3u8Data.processed, diagnosticsTool); try { this.isInternalRequest = true; this._attachHlsPlayback(videoElement, isFirstPlay); } finally { this._resetInternalRequestFlagLater(); } } _bindHlsEvents(hls, videoElement, isFirstPlay) { PlayerHlsController.bindEvents(this, hls, videoElement, isFirstPlay); } _handleHlsMediaAttached(hls) { PlayerHlsController.handleMediaAttached(this, hls); } _capturePredefinedResolution(source, width, height) { PlayerHlsController.capturePredefinedResolution(this, source, width, height); } _handleHlsManifestParsed(hls, videoElement, isFirstPlay, data, hlsState) { PlayerHlsController.handleManifestParsed(this, hls, videoElement, isFirstPlay, data, hlsState); } _applyBestHlsManifestLevel(hls, levels) { PlayerHlsController.applyBestManifestLevel(this, hls, levels); } _extractHlsLevelInfo(bestLevel) { return PlayerHlsController.extractLevelInfo(bestLevel); } _captureManifestResolution(width, height) { PlayerHlsController.captureManifestResolution(this, width, height); } _formatHlsLevelInfo(levelInfo) { return PlayerHlsController.formatLevelInfo(levelInfo); } _autoplayAfterManifest(videoElement, isFirstPlay) { PlayerHlsController.autoplayAfterManifest(this, videoElement, isFirstPlay); } _getBestHlsLevelChoice(levels) { return PlayerHlsController.getBestLevelChoice(levels); } _handleHlsFragmentParsed(hls, data) { PlayerHlsController.handleFragmentParsed(this, hls, data); } _handleHlsLevelUpdated(hls, data) { PlayerHlsController.handleLevelUpdated(this, hls, data); } _handleHlsFragmentLoaded(data, hlsState) { PlayerHlsController.handleFragmentLoaded(this, data, hlsState); } _handleHlsError(hls, data, hlsState) { PlayerHlsController.handleError(this, hls, data, hlsState); } _handleHlsNetworkError(hls, data, hlsState) { return PlayerHlsController.handleNetworkError(this, hls, data, hlsState); } _handleHlsMediaError(hls, data) { return PlayerHlsController.handleMediaError(this, hls, data); } _handleHlsFatalError(hls, data) { PlayerHlsController.handleFatalError(this, hls, data); } _getHlsConfig() { return PlayerHlsController.getConfig(this); } _getHlsBufferConfig() { return PlayerHlsController.getBufferConfig(this); } _getHlsLoadingConfig() { return PlayerHlsController.getLoadingConfig(this); } _getHlsCodecConfig() { return PlayerHlsController.getCodecConfig(this); } playNormalVideo(videoUrl, videoElement) { this.currentPlaybackEngine = '浏览器原生'; this.currentVideoUrl = videoUrl; this._ensureNormalVideoFormat(videoUrl); this._prepareNormalVideoElement(videoElement); this._bindNormalVideoHealthEvents(videoElement); this._loadNormalVideoSource(videoElement, videoUrl); } _ensureNormalVideoFormat(videoUrl) { if (!this.currentVideoFormat) { this.currentVideoFormat = Utils.getVideoFormat(videoUrl); } } _prepareNormalVideoElement(videoElement) { videoElement.preload = 'auto'; videoElement.playsInline = true; videoElement.setAttribute('playsinline', ''); videoElement.setAttribute('webkit-playsinline', ''); videoElement.setAttribute('x5-playsinline', 'true'); } _loadNormalVideoSource(videoElement, videoUrl) { if (videoElement.src !== videoUrl) { videoElement.src = videoUrl; } videoElement.load(); } _bindNormalVideoHealthEvents(videoElement) { const { diagnosticsTool } = this.context; const cleanupBag = PlayerBindingHelpers.createCleanupBag(); PlayerBindingHelpers.bindFirstMatchingVideoSignal( videoElement, PLAYER_MEDIA_HEALTH_SUCCESS_EVENTS, cleanupBag.cleanupFns, () => { diagnosticsTool.playbackHealth.media.status = 'success'; } ); PlayerBindingHelpers.bindVideoErrorOnce(videoElement, cleanupBag.cleanupFns, () => { cleanupBag.run(); diagnosticsTool.playbackHealth.media.status = 'error'; this.context.coreLogic.reportPlaybackFailure(); }); } cleanup(isInternalCall = false) { PlayerCleanupManager.cleanup(this, isInternalCall); } handlePointerDown(e) { if (!e.isPrimary || this.activeInteraction) { return; } this.activeInteraction = this._resolvePointerInteraction(e.target); if (!this.activeInteraction) { return; } e.stopPropagation(); e.preventDefault(); this._startPointerInteraction(e); this._bindDocumentPointerEvents(); } _resolvePointerInteraction(target) { if (target.closest(`.${CONSTANTS.CLASSES.DRAG_HANDLE}`)) { return 'drag'; } if (target.closest('.dmz-progress-bar')) { return 'seek'; } if (!target.closest('.dmz-controls-bar, .dmz-close-button')) { return 'gesture'; } return null; } _startPointerInteraction(e) { switch (this.activeInteraction) { case 'drag': this.draggableInstance.handleDragStart(e); break; case 'seek': this.customControlsHandler.isSeeking = true; this.customControlsHandler.updateSeekUI(e.clientX); break; case 'gesture': this.fullscreenGestureHandler.onPointerDown(e); break; } } _bindDocumentPointerEvents() { PlayerBindingHelpers.toggleDocumentPointerEvents(this, 'addEventListener'); } handlePointerMove(e) { if (!e.isPrimary || !this.activeInteraction) { return; } e.stopPropagation(); e.preventDefault(); this._dispatchPointerMove(e); } _dispatchPointerMove(e) { switch (this.activeInteraction) { case 'drag': this.draggableInstance.handleDragMove(e); break; case 'seek': this.customControlsHandler.updateSeekUI(e.clientX); break; case 'gesture': this.fullscreenGestureHandler.onPointerMove(e); break; } } handlePointerUp(e) { if (!e.isPrimary || !this.activeInteraction) { return; } e.stopPropagation(); e.preventDefault(); this._unbindDocumentPointerEvents(); this._finishPointerInteraction(e); this.activeInteraction = null; } _unbindDocumentPointerEvents() { PlayerBindingHelpers.toggleDocumentPointerEvents(this, 'removeEventListener'); } _finishPointerInteraction(e) { switch (this.activeInteraction) { case 'drag': this.draggableInstance.handleDragEnd(e); break; case 'seek': this._completeSeekInteraction(e.clientX); break; case 'gesture': this.fullscreenGestureHandler.onPointerUp(e); break; } } _completeSeekInteraction(clientX) { const newTime = this.customControlsHandler.updateSeekUI(clientX); if (this.videoElement && !isNaN(newTime)) { this.videoElement.currentTime = newTime; } this.customControlsHandler.isSeeking = false; this.customControlsHandler.resetHideTimeout(); } } const M3U8FetchManager = { _logFetchRequest(processor, url, requestId) { processor.log(`[m3u8处理器]|[ID:${requestId}]原生获取M3U8清单文本:${url}`, CONSTANTS.LOG_TYPES.CORE); }, _shouldRetryFetch(processor, err, retriesLeft) { return ( retriesLeft > 0 && !err.message.includes('403') && !err.message.includes('404') && !err.message.includes('Cross frame fetch timeout') ); }, _resolveUrl(url) { return new URL(url, window.location.href).href; }, _buildDirectHlsUrl(processor, url) { try { const resolved = this._resolveUrl(url); const urlObj = new URL(resolved); if (!/bfvvs\.com$/i.test(urlObj.hostname)) { return null; } const match = urlObj.pathname.match(/^\/play\/([^/]+)\/index\.m3u8$/i); if (!match) { return null; } urlObj.pathname = `/play/hls/${match[1]}/index.m3u8`; return urlObj.href; } catch (e) { return null; } }, _buildFetchCandidateUrls(processor, url) { const candidates = []; const directHlsUrl = this._buildDirectHlsUrl(processor, url); if (directHlsUrl && directHlsUrl !== url) { candidates.push(directHlsUrl); } candidates.push(this._resolveUrl(url)); return [...new Set(candidates)]; }, _getCacheKey(processor, url) { const { coreLogic } = processor.context; const resolved = this._resolveUrl(url); if (coreLogic?._getNormalizationKey) { return coreLogic._getNormalizationKey(resolved); } return resolved; }, async _attemptSingleFetchText(processor, url, isValidSession, retriesLeft) { try { const response = await fetch(url); if (!isValidSession()) { throw new Error('Request outdated'); } if (!response.ok) { throw new Error(`Status ${response.status}`); } return await response.text(); } catch (err) { if (!isValidSession()) { throw new Error('Request outdated'); } const isCrossOrigin = new URL(url, window.location.href).hostname !== window.location.hostname; if (isCrossOrigin && CONSTANTS.IS_TOP_FRAME && err.message.includes('Failed to fetch')) { try { const text = await processor.context.frameCommunicator.requestCrossFrameFetch(url); if (!isValidSession()) { throw new Error('Request outdated'); } return text; } catch (crossErr) { err = crossErr; } } if (processor._shouldRetryFetch(err, retriesLeft)) { processor.log(`[m3u8处理器] 获取失败,重试中(剩余${retriesLeft}次)...`, CONSTANTS.LOG_TYPES.WARN); await new Promise((r) => setTimeout(r, 1000)); return this._attemptSingleFetchText(processor, url, isValidSession, retriesLeft - 1); } throw err; } }, async _attemptFetchText(processor, url, isValidSession, retriesLeft) { let lastError = null; const candidateUrls = this._buildFetchCandidateUrls(processor, url); for (const candidateUrl of candidateUrls) { try { if (candidateUrl !== this._resolveUrl(url)) { processor.log( `[m3u8处理器]|路径加速命中,优先尝试:${candidateUrl}`, CONSTANTS.LOG_TYPES.CORE_URL_RESOLVE ); } return await this._attemptSingleFetchText(processor, candidateUrl, isValidSession, retriesLeft); } catch (err) { if (err.message === 'Request outdated') { throw err; } lastError = err; } } throw lastError || new Error('Unknown fetch failure'); }, _applyFetchFailureToDiagnostics(processor, err) { const { diagnosticsTool } = processor.context; if (err.message !== 'Request outdated' && diagnosticsTool.playbackHealth.manifest.status === 'pending') { diagnosticsTool.playbackHealth.manifest = { status: 'error', code: err.message, }; } }, async fetchText(processor, url, isValidSession, requestId) { const { coreLogic } = processor.context; const cacheKey = this._getCacheKey(processor, url); if (cacheKey && coreLogic.m3u8SessionCache.has(cacheKey)) { return coreLogic.m3u8SessionCache.get(cacheKey); } if (cacheKey && coreLogic.m3u8FetchInflight.has(cacheKey)) { const text = await coreLogic.m3u8FetchInflight.get(cacheKey); if (!isValidSession()) { throw new Error('Request outdated'); } return text; } processor._logFetchRequest(url, requestId); const fetchPromise = (async () => { try { const text = await this._attemptFetchText(processor, url, () => true, 1); if (cacheKey) { coreLogic.m3u8SessionCache.set(cacheKey, text); } return text; } catch (err) { processor._applyFetchFailureToDiagnostics(err); throw new Error(`获取M3U8失败: ${err.message}`); } finally { if (cacheKey) { coreLogic.m3u8FetchInflight.delete(cacheKey); } } })(); if (cacheKey) { coreLogic.m3u8FetchInflight.set(cacheKey, fetchPromise); } const result = await fetchPromise; if (!isValidSession()) { throw new Error('Request outdated'); } return result; }, }; const HlsKeyPrefetchManager = { _escapeRegex(text) { return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }, _resolveKeyUrl(keyUrl, baseUrl) { try { return new URL(keyUrl, baseUrl || window.location.href).href; } catch (e) { return keyUrl; } }, _extractKeyDescriptors(manifestText, baseUrl) { const descriptors = []; const seen = new Set(); for (const rawLine of String(manifestText || '').split('\n')) { const line = rawLine.trim(); if (!/^#EXT-X-(?:KEY|SESSION-KEY):/i.test(line) || /METHOD=NONE/i.test(line)) { continue; } const uriMatch = line.match(/URI="([^"]+)"/i); const rawUri = uriMatch?.[1]; if (!rawUri) { continue; } const resolvedUri = this._resolveKeyUrl(rawUri, baseUrl); if (!resolvedUri || /^(?:data:|blob:)/i.test(resolvedUri)) { continue; } if (seen.has(resolvedUri)) { continue; } seen.add(resolvedUri); descriptors.push({ rawUri, resolvedUri }); } return descriptors; }, async _fetchArrayBufferWithGm(url) { return await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url, responseType: 'arraybuffer', timeout: 20000, headers: { Referer: window.location.href, Accept: '*/*', }, onload: (response) => { const status = Number(response.status) || 0; if (status >= 200 && status < 300 && response.response) { resolve(response.response); return; } reject(new Error(`Status ${status || 'GM_KEY_FETCH_FAILED'}`)); }, onerror: (err) => reject(new Error(err?.error || 'GM_KEY_FETCH_FAILED')), ontimeout: () => reject(new Error('GM_KEY_FETCH_TIMEOUT')), }); }); }, async _attemptFetchArrayBuffer(url) { try { const response = await fetch(url); if (!response.ok) { throw new Error(`Status ${response.status}`); } return await response.arrayBuffer(); } catch (err) { if (typeof GM_xmlhttpRequest === 'function') { return await this._fetchArrayBufferWithGm(url); } throw err; } }, async _getKeyBuffer(coreLogic, keyUrl, isValidSession) { if (!coreLogic.keyBinaryCache) { coreLogic.keyBinaryCache = new Map(); } if (!coreLogic.keyFetchInflight) { coreLogic.keyFetchInflight = new Map(); } const cacheKey = coreLogic._getNormalizationKey(keyUrl) || keyUrl; if (coreLogic.keyBinaryCache.has(cacheKey)) { return coreLogic.keyBinaryCache.get(cacheKey); } if (coreLogic.keyFetchInflight.has(cacheKey)) { const inflight = await coreLogic.keyFetchInflight.get(cacheKey); if (!isValidSession()) { throw new Error('Request outdated'); } return inflight; } const fetchPromise = (async () => { try { const buffer = await this._attemptFetchArrayBuffer(keyUrl); if (!buffer || !buffer.byteLength) { throw new Error('KEY_EMPTY'); } coreLogic.keyBinaryCache.set(cacheKey, buffer); return buffer; } finally { coreLogic.keyFetchInflight.delete(cacheKey); } })(); coreLogic.keyFetchInflight.set(cacheKey, fetchPromise); const result = await fetchPromise; if (!isValidSession()) { throw new Error('Request outdated'); } return result; }, _getBlobUrl(coreLogic, keyUrl, buffer) { if (!coreLogic.keyBlobUrlCache) { coreLogic.keyBlobUrlCache = new Map(); } const cacheKey = coreLogic._getNormalizationKey(keyUrl) || keyUrl; if (coreLogic.keyBlobUrlCache.has(cacheKey)) { return coreLogic.keyBlobUrlCache.get(cacheKey); } const blobUrl = URL.createObjectURL( new Blob([buffer], { type: 'application/octet-stream', }) ); coreLogic.keyBlobUrlCache.set(cacheKey, blobUrl); return blobUrl; }, async prefetchAndRewriteManifest(coreLogic, processedData, isValidSession, requestId) { const manifestText = String(processedData?.processed || ''); if (!manifestText) { return processedData; } const descriptors = this._extractKeyDescriptors(manifestText, processedData.finalUrl); if (descriptors.length === 0) { return processedData; } coreLogic.log( `[Key预取]|🔑|检测到${descriptors.length}个唯一密钥,开始预取并绑定到当前清单。`, CONSTANTS.LOG_TYPES.HLS ); let rewrittenManifest = manifestText; try { for (const descriptor of descriptors) { const buffer = await this._getKeyBuffer(coreLogic, descriptor.resolvedUri, isValidSession); if (!isValidSession()) { throw new Error('Request outdated'); } const blobUrl = this._getBlobUrl(coreLogic, descriptor.resolvedUri, buffer); const replaceTargets = Array.from(new Set([descriptor.rawUri, descriptor.resolvedUri])).filter(Boolean); for (const target of replaceTargets) { rewrittenManifest = rewrittenManifest.replace( new RegExp(`URI="${this._escapeRegex(target)}"`, 'g'), `URI="${blobUrl}"` ); } } } catch (err) { if (err.message !== 'Request outdated') { coreLogic.context.diagnosticsTool.playbackHealth.key = { status: 'error', code: err.message, }; throw new Error(`Key预取失败: ${err.message}`); } throw err; } processedData.processed = rewrittenManifest; processedData.prefetchedKeyCount = descriptors.length; coreLogic.log( `[Key预取]|🔑|已预取${descriptors.length}个密钥,并改写为本地引用。`, CONSTANTS.LOG_TYPES.HLS ); return processedData; }, }; const M3U8AdModel = { Shared: { PLAYABLE_MEDIA_PATTERN: /\.(ts|mp4|mkv|webm|flv|jpeg|jpg)(\?.*)?$/i, buildRebuildHeaderLines(originalLines, options = {}) { const { filterHeaderDiscontinuity = false } = options; const headerIndex = originalLines.findIndex((l) => l.startsWith('#EXTINF')); let headerLines = headerIndex > -1 ? originalLines.slice(0, headerIndex) : []; if (filterHeaderDiscontinuity) { headerLines = headerLines.filter((l) => !l.trim().startsWith('#EXT-X-DISCONTINUITY')); } return headerLines; }, appendRebuiltGroupLines(headerLines, finalGroups) { const finalLines = [...headerLines]; finalGroups.forEach((group, index) => { finalLines.push(...group); if (index < finalGroups.length - 1) { finalLines.push('#EXT-X-DISCONTINUITY'); } }); return finalLines; }, ensurePlaylistEndlist(originalLines, finalLines) { const endlistPresent = originalLines.some((l) => l.includes('#EXT-X-ENDLIST')); if (endlistPresent && !finalLines.some((l) => l.includes('#EXT-X-ENDLIST'))) { finalLines.push('#EXT-X-ENDLIST'); } }, splitDiscontinuityGroups(lines) { const segmentGroups = []; let currentGroup = []; lines.forEach((line) => { if (line.trim().startsWith('#EXT-X-DISCONTINUITY')) { if (currentGroup.length > 0) { segmentGroups.push(currentGroup); } currentGroup = []; } else { currentGroup.push(line); } }); if (currentGroup.length > 0) { segmentGroups.push(currentGroup); } return segmentGroups; }, calculateGroupDuration(group) { return group .filter((l) => l.startsWith('#EXTINF:')) .reduce((sum, line) => { const duration = parseFloat(line.split(':')[1]); return sum + (isNaN(duration) ? 0 : duration); }, 0); }, countAdSegments(group) { return group.filter((line) => { const trimmed = line.trim(); return trimmed.endsWith('.ts') || trimmed.includes('.jpeg'); }).length; }, createKeywordMatcher(keywords) { return new RegExp(keywords.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')); }, hasMediaSegments(lines) { return lines.some((line) => !line.startsWith('#') && this.PLAYABLE_MEDIA_PATTERN.test(line.trim())); }, createSegmentGroupingState() { return { headerTags: [ '#EXTM3U', '#EXT-X-VERSION', '#EXT-X-TARGETDURATION', '#EXT-X-PLAYLIST-TYPE', '#EXT-X-MEDIA-SEQUENCE', '#EXT-X-ALLOW-CACHE', '#EXT-X-KEY', ], uniqueHeaders: new Set(), headerLines: [], segmentGroups: [], currentGroup: [], }; }, collectSegmentHeaderLine(line, trimmed, state) { if (!state.headerTags.some((tag) => trimmed.startsWith(tag))) { return false; } const key = trimmed.split(':')[0]; if (!state.uniqueHeaders.has(key)) { state.uniqueHeaders.add(key); state.headerLines.push(line); } return true; }, consumeSegmentGroupingLine(line, state) { const trimmed = line.trim(); if (!trimmed) { return; } if (this.collectSegmentHeaderLine(line, trimmed, state)) { return; } if (trimmed.startsWith('#EXT-X-DISCONTINUITY')) { this.flushSegmentGroup(state); return; } state.currentGroup.push(line); }, flushSegmentGroup(state) { if (state.currentGroup.length <= 0) { state.currentGroup = []; return; } state.segmentGroups.push(state.currentGroup); state.currentGroup = []; }, getSegmentGroupBoundaryUrl(group, edge) { const urls = group .filter((line) => !line.startsWith('#') && line.trim().length > 0) .map((line) => line.trim()); if (urls.length === 0) { return null; } return edge === 'first' ? urls[0] : urls[urls.length - 1]; }, }, KeywordEngine: { rebuildFromGroups(processor, originalLines, finalGroups, options = {}) { const headerLines = this._buildGroupRebuildHeaderLines(processor, originalLines, options); const finalLines = this._appendRebuiltGroupLines(processor, headerLines, finalGroups); this._ensureRebuiltGroupsEndlist(processor, originalLines, finalLines); return finalLines.filter(Boolean); }, _buildGroupRebuildHeaderLines(processor, originalLines, options = {}) { return M3U8AdModel.Shared.buildRebuildHeaderLines(originalLines, options); }, _appendRebuiltGroupLines(processor, headerLines, finalGroups) { return M3U8AdModel.Shared.appendRebuiltGroupLines(headerLines, finalGroups); }, _ensureRebuiltGroupsEndlist(processor, originalLines, finalLines) { return M3U8AdModel.Shared.ensurePlaylistEndlist(originalLines, finalLines); }, _createKeywordMatcher(processor, keywords) { return M3U8AdModel.Shared.createKeywordMatcher(keywords); }, _splitDiscontinuityGroups(processor, lines) { return M3U8AdModel.Shared.splitDiscontinuityGroups(lines); }, _calculateGroupDuration(processor, group) { return M3U8AdModel.Shared.calculateGroupDuration(group); }, _handleKeywordMatchedGroup(processor, group, groupDuration, diagnosticsTool, state) { state.removedGroups++; state.removedSegments += M3U8AdModel.Shared.countAdSegments(group); diagnosticsTool.slicingReport.slicedDuration += groupDuration; if (groupDuration > 0) { diagnosticsTool.slicingReport.slicedTimeRanges.push({ start: state.originalTimelineDuration, end: state.originalTimelineDuration + groupDuration, }); } }, _collectKeywordSliceState(processor, segmentGroups, adMatcher, diagnosticsTool) { const state = { removedGroups: 0, removedSegments: 0, finalGroups: [], originalTimelineDuration: 0, }; segmentGroups.forEach((group) => { const isAd = group.some((line) => adMatcher.test(line)); const groupDuration = processor._calculateGroupDuration(group); if (isAd) { processor._handleKeywordMatchedGroup(group, groupDuration, diagnosticsTool, state); } else { state.finalGroups.push(group); } state.originalTimelineDuration += groupDuration; }); return state; }, _finalizeKeywordSlice(processor, lines, state, diagnosticsTool) { if (state.removedGroups === 0) { return lines; } diagnosticsTool.slicingReport.slicedGroups += state.removedGroups; diagnosticsTool.slicingReport.slicedSegments += state.removedSegments; processor.log( `[m3u8处理器]|「URL特征」净化完成,移除${state.removedGroups}组共${state.removedSegments} 个广告分片。`, CONSTANTS.LOG_TYPES.CORE_SLICE ); const finalLines = processor.rebuildFromGroups(lines, state.finalGroups, { filterHeaderDiscontinuity: true, }); if (processor.hasMediaSegments(finalLines)) { return finalLines; } processor.log( `[m3u8处理器]|[安全锁]“特征锁定”净化后无任何可播放内容!已还原为原始列表。`, CONSTANTS.LOG_TYPES.CORE_SLICE ); diagnosticsTool.slicingReport.slicedGroups -= state.removedGroups; diagnosticsTool.slicingReport.slicedSegments -= state.removedSegments; return lines; }, sliceAdSegmentsByKeywords(processor, lines, keywords) { const { diagnosticsTool } = processor.context; processor.log( `[m3u8处理器]|执行「URL特征」净化,关键词:[${keywords.join(', ')}]`, CONSTANTS.LOG_TYPES.CORE_SLICE ); const segmentGroups = processor._splitDiscontinuityGroups(lines); if (segmentGroups.length === 0) { return lines; } const adMatcher = processor._createKeywordMatcher(keywords); const state = processor._collectKeywordSliceState(segmentGroups, adMatcher, diagnosticsTool); return processor._finalizeKeywordSlice(lines, state, diagnosticsTool); }, }, BehaviorEngine: { runFalconV3Engine(processor, lines, baseUrl) { processor.log('[m3u8处理器]|「行为清扫」:启动,扫描典型短广告分组...', CONSTANTS.LOG_TYPES.CORE_SLICE); const { headerLines, segmentGroups } = processor._groupSegments(lines); if (segmentGroups.length < 2) { processor.log( `[m3u8处理器]|「行为清扫」视频分组不足(少于2个),不执行行为净化。`, CONSTANTS.LOG_TYPES.CORE_SLICE ); return { wasSliced: false, content: lines }; } const classifiedGroups = processor._analyzeGroupFeatures(segmentGroups, baseUrl); return processor._filterAdGroups(classifiedGroups, headerLines, lines); }, _groupSegments(processor, lines) { const state = processor._createSegmentGroupingState(); lines.forEach((line) => processor._consumeSegmentGroupingLine(line, state)); processor._flushSegmentGroup(state); return { headerLines: state.headerLines, segmentGroups: state.segmentGroups, }; }, _createSegmentGroupingState(processor) { return M3U8AdModel.Shared.createSegmentGroupingState(); }, _consumeSegmentGroupingLine(processor, line, state) { return M3U8AdModel.Shared.consumeSegmentGroupingLine(line, state); }, _collectSegmentHeaderLine(processor, line, trimmed, state) { return M3U8AdModel.Shared.collectSegmentHeaderLine(line, trimmed, state); }, _flushSegmentGroup(processor, state) { return M3U8AdModel.Shared.flushSegmentGroup(state); }, _analyzeGroupFeatures(processor, segmentGroups, baseUrl) { const classified = segmentGroups.map((groupLines, index) => { const duration = processor._calculateSegmentGroupDuration(groupLines); const sequenceRange = processor._getSegmentGroupSequenceRange(groupLines); return processor._buildClassifiedSegmentGroup(groupLines, index, duration, sequenceRange); }); processor._markBehavioralAdCandidates(classified); return classified; }, _calculateSegmentGroupDuration(processor, group) { return M3U8AdModel.Shared.calculateGroupDuration(group); }, _getSegmentGroupSequenceRange(processor, group) { const first = processor._getSegmentGroupBoundaryUrl(group, 'first'); const last = processor._getSegmentGroupBoundaryUrl(group, 'last'); return { start: first ? processor._extractSequenceNumber(first) : null, end: last ? processor._extractSequenceNumber(last) : null, }; }, _getSegmentGroupBoundaryUrl(processor, group, edge) { return M3U8AdModel.Shared.getSegmentGroupBoundaryUrl(group, edge); }, _extractSequenceNumber(processor, value) { try { const filename = value.split('?')[0].split('/').pop(); const dotIndex = filename.lastIndexOf('.'); const basename = dotIndex > 0 ? filename.substring(0, dotIndex) : filename; if ( /^[a-f0-9]{16}$/i.test(basename) || /^[a-f0-9]{32}$/i.test(basename) || /^[a-f0-9]{8}-[a-f0-9]{4}-/i.test(basename) ) { return null; } const match = filename.match(/(\d+)\.(ts|mp4|mkv|jpeg|jpg|png|image)$/i); if (match) { return parseInt(match[1], 10); } } catch (e) { console.debug('Sequence extraction failed:', e); } return null; }, _buildClassifiedSegmentGroup(processor, groupLines, index, duration, sequenceRange) { const segmentCount = M3U8AdModel.Shared.countAdSegments(groupLines); let score = 0; if (segmentCount > 0 && segmentCount <= 20 && duration <= 35) { score += 20; } return { lines: groupLines, isAdCandidate: false, segmentCount, totalDuration: duration, index, reason: '', score, seqStart: sequenceRange.start, seqEnd: sequenceRange.end, }; }, _markBehavioralAdCandidates(processor, classified) { for (let index = 1; index < classified.length - 1; index++) { const prev = classified[index - 1]; const curr = classified[index]; const next = classified[index + 1]; if (curr.totalDuration > 180) { continue; } if (processor._markSequenceBridgeAdCandidate(prev, curr, next)) { continue; } processor._markContextualShortAdCandidate(prev, curr, next); } }, _markSequenceBridgeAdCandidate(processor, prev, curr, next) { if (prev.seqEnd === null || next.seqStart === null || curr.seqStart === null) { return false; } const diffMain = next.seqStart - prev.seqEnd; if (diffMain < 1 || diffMain > 10) { return false; } const diffCurr = Math.abs(curr.seqStart - prev.seqEnd); if (diffCurr <= 100) { return false; } curr.isAdCandidate = true; curr.reason = `SequenceBridge(${diffMain}/${diffCurr})`; return true; }, _markContextualShortAdCandidate(processor, prev, curr, next) { if (curr.score < 20 || curr.isAdCandidate) { return; } const isSandwiched = prev.totalDuration > 60 && next.totalDuration > 60; const isMidRoll = prev.totalDuration > 120 && curr.totalDuration < 45; if (isSandwiched || isMidRoll) { curr.isAdCandidate = true; curr.reason = `ContextualShort(Dur:${curr.totalDuration.toFixed(1)}s)`; } }, _filterAdGroups(processor, classifiedGroups, headerLines, originalLines) { if (processor._shouldAbortBehavioralFiltering(classifiedGroups)) { return { wasSliced: false, content: originalLines }; } const filterState = processor._createAdGroupFilterState(headerLines); classifiedGroups.forEach((group) => processor._appendFilteredSegmentGroup(group, filterState)); processor._ensureFilteredPlaylistEndlist(originalLines, filterState.finalLines); if (filterState.removedGroups === 0) { return { wasSliced: false, content: originalLines }; } return processor._finalizeBehavioralFiltering(filterState, originalLines); }, _shouldAbortBehavioralFiltering(processor, classifiedGroups) { const adCandidateCount = classifiedGroups.filter((group) => group.isAdCandidate).length; const hardEvidenceCount = classifiedGroups.filter( (group) => group.isAdCandidate && group.reason.includes('SequenceBridge') ).length; if (hardEvidenceCount !== 0 || adCandidateCount === 0) { return false; } const adRatio = adCandidateCount / classifiedGroups.length; if (adRatio <= 0.8) { return false; } processor.log( `[m3u8处理器]|[安全锁] 疑似正片极其破碎,且无序列号硬证据,放弃过滤。`, CONSTANTS.LOG_TYPES.CORE_SLICE ); return true; }, _createAdGroupFilterState(processor, headerLines) { return { removedGroups: 0, removedSegments: 0, originalTimelineDuration: 0, hasAddedGroup: false, finalLines: [...headerLines], }; }, _appendFilteredSegmentGroup(processor, group, filterState) { if (group.isAdCandidate) { processor._appendAdFilteredSegmentGroup(group, filterState); } else { processor._appendPlayableFilteredSegmentGroup(group, filterState); } filterState.originalTimelineDuration += group.totalDuration; }, _appendAdFilteredSegmentGroup(processor, group, filterState) { const { diagnosticsTool } = processor.context; filterState.removedGroups++; filterState.removedSegments += group.segmentCount; diagnosticsTool.slicingReport.slicedDuration += group.totalDuration; processor._appendAdFilteredSegmentFeature(group, diagnosticsTool); processor._appendAdFilteredSegmentRange(group, filterState, diagnosticsTool); }, _appendAdFilteredSegmentFeature(processor, group, diagnosticsTool) { const feature = `<智能识别> (分片:${group.segmentCount}, 时长:${group.totalDuration.toFixed(1)}s, 原因:${group.reason})`; diagnosticsTool.slicingReport.foundFeatures.get('BEHAVIOR_MODEL').add(feature); processor.log( `[m3u8处理器]|「行为清扫」战果:移除广告组(${group.segmentCount}个分片, ${group.totalDuration.toFixed(1)}s) [${group.reason}]。`, CONSTANTS.LOG_TYPES.CORE_SLICE ); }, _appendAdFilteredSegmentRange(processor, group, filterState, diagnosticsTool) { if (group.totalDuration <= 0) { return; } diagnosticsTool.slicingReport.slicedTimeRanges.push({ start: filterState.originalTimelineDuration, end: filterState.originalTimelineDuration + group.totalDuration, }); }, _appendPlayableFilteredSegmentGroup(processor, group, filterState) { if (filterState.hasAddedGroup) { filterState.finalLines.push('#EXT-X-DISCONTINUITY'); } filterState.finalLines.push(...group.lines); filterState.hasAddedGroup = true; }, _ensureFilteredPlaylistEndlist(processor, originalLines, finalLines) { return M3U8AdModel.Shared.ensurePlaylistEndlist(originalLines, finalLines); }, _finalizeBehavioralFiltering(processor, filterState, originalLines) { const { diagnosticsTool } = processor.context; diagnosticsTool.slicingReport.activatedEngines.add('BEHAVIOR_MODEL'); diagnosticsTool.slicingReport.slicedGroups += filterState.removedGroups; diagnosticsTool.slicingReport.slicedSegments += filterState.removedSegments; processor.log( `[m3u8处理器]|净化报告:移除 ${filterState.removedGroups} 组广告 (含序列号异常识别)。`, CONSTANTS.LOG_TYPES.CORE_SLICE ); if (processor.hasMediaSegments(filterState.finalLines)) { return { wasSliced: true, content: filterState.finalLines }; } return { wasSliced: false, content: originalLines }; }, hasMediaSegments(processor, lines) { return M3U8AdModel.Shared.hasMediaSegments(lines); }, }, }; const M3U8KeywordSlicer = M3U8AdModel.KeywordEngine; const M3U8BehavioralAnalyzer = M3U8AdModel.BehaviorEngine; const M3U8PlaylistResolver = { resolveRelativeUrls(processor, lines, baseUrl) { if (baseUrl.startsWith('data:')) { processor.log(`检测到Data URI,跳过URL解析,保留相对路径。`, CONSTANTS.LOG_TYPES.CORE_URL_RESOLVE); return lines; } processor.log(`解析M3U8中的相对URL,基础URL:${baseUrl}`, CONSTANTS.LOG_TYPES.CORE_URL_RESOLVE); return lines.map((line) => processor._resolveRelativeM3u8Line(line, baseUrl)); }, _resolveRelativeM3u8Line(processor, line, baseUrl) { const trimmed = line.trim(); if (processor._shouldSkipRelativeM3u8Line(trimmed)) { return line; } try { if (!trimmed.startsWith('#')) { return processor._resolveRelativeMediaM3u8Line(line, trimmed, baseUrl); } return processor._resolveRelativeDirectiveM3u8Line(line, trimmed, baseUrl); } catch (e) { console.debug('Failed to resolve URL line in M3U8:', e); } return line; }, _resolveRelativeMediaM3u8Line(processor, line, trimmed, baseUrl) { if (processor._isResolvableMediaLine(trimmed) && !processor._isAbsoluteUrl(trimmed)) { return processor._resolveRelativeUrlAgainstBase(trimmed, baseUrl); } return line; }, _resolveRelativeDirectiveM3u8Line(processor, line, trimmed, baseUrl) { if (!processor._isResolvableDirectiveLine(trimmed)) { return line; } const uriMatch = trimmed.match(/URI="([^"]+)"/); const uri = uriMatch?.[1]; if (uri && !processor._isAbsoluteUrl(uri)) { return trimmed.replace(uri, processor._resolveRelativeUrlAgainstBase(uri, baseUrl)); } return line; }, _shouldSkipRelativeM3u8Line(processor, trimmed) { return ( trimmed.length === 0 || (!trimmed.includes('URI=') && trimmed.startsWith('#EXT') && !trimmed.startsWith('#EXT-X-KEY') && !trimmed.startsWith('#EXT-X-MEDIA')) ); }, _isResolvableMediaLine(processor, trimmed) { return /\.(ts|mp4|mkv|webm|flv|m3u8?|jpeg|jpg|png|image|m4s)(\?.*)?$/i.test(trimmed); }, _isResolvableDirectiveLine(processor, trimmed) { return ( trimmed.startsWith('#EXT-X-KEY') || trimmed.startsWith('#EXT-X-MEDIA') || trimmed.startsWith('#EXT-X-MAP') ); }, _isAbsoluteUrl(processor, url) { return /^(https?:)?\/\//.test(url); }, _resolveRelativeUrlAgainstBase(processor, relative, baseUrl) { try { if (/^(https?:)?\/\//i.test(relative)) { return relative; } const target = new URL(relative, baseUrl); processor._mergeBaseSearchParamsIntoTarget(target, baseUrl); return target.href; } catch (e) { return relative; } }, _mergeBaseSearchParamsIntoTarget(processor, target, baseUrl) { const base = new URL(baseUrl); if (!base.search) { return; } if (!target.search) { target.search = base.search; return; } const params = new URLSearchParams(target.search); new URLSearchParams(base.search).forEach((value, key) => { if (!params.has(key)) { params.set(key, value); } }); target.search = params.toString(); }, _resolveInitialM3u8Url(processor, initialUrl) { try { return new URL(initialUrl).href; } catch (e) { const baseUrl = new URL(initialUrl, window.location.href).href; processor.log( `[m3u8处理器]|初始URL为相对路径,已拼接为绝对路径:${baseUrl}`, CONSTANTS.LOG_TYPES.CORE_URL_RESOLVE ); return baseUrl; } }, _isMasterPlaylist(processor, lines) { return lines.some((l) => l.startsWith('#EXT-X-STREAM-INF')); }, _hasMediaEntries(processor, lines) { return lines.some((l) => l.startsWith('#EXTINF')); }, _extractQualityUrls(processor, lines) { return lines.filter((l) => l.trim() && !l.startsWith('#') && (l.endsWith('.m3u8') || l.endsWith('.m3u'))); }, _selectBestQualityUrl(processor, qualityUrls) { const qualityRanks = { '2160p': 6, '1080p': 5, '720p': 4, '480p': 3, '360p': 2, '240p': 1, }; let bestQuality = -1; let bestUrl = qualityUrls[qualityUrls.length - 1]; qualityUrls.forEach((url) => { for (const q in qualityRanks) { if (url.toLowerCase().includes(q) && qualityRanks[q] > bestQuality) { bestQuality = qualityRanks[q]; bestUrl = url; } } }); processor.log( `[m3u8处理器]|解析完成,共${qualityUrls.length}个流。自动选择最高清晰度:${Object.keys(qualityRanks).find((k) => qualityRanks[k] === bestQuality) || '未知'}`, CONSTANTS.LOG_TYPES.STREAM_SELECT ); return bestUrl; }, _resolveNextVariantUrl(processor, lines) { const isMaster = processor._isMasterPlaylist(lines); if (isMaster) { processor.log( `[m3u8处理器]|发现MasterPlaylist,开始解析可用清晰度...`, CONSTANTS.LOG_TYPES.STREAM_SELECT ); } const hasMedia = processor._hasMediaEntries(lines); if (!((isMaster && !hasMedia) || (lines.length < 5 && !hasMedia))) { return null; } const qualityUrls = processor._extractQualityUrls(lines); if (qualityUrls.length === 0) { return null; } return processor._selectBestQualityUrl(qualityUrls); }, async _resolvePlaylistChain(processor, initialText, baseUrl, requestId, isValidSession) { let currentText = initialText; let currentUrl = baseUrl; for (let i = 0; i < CONSTANTS.LIMITS.MAX_M3U8_REDIRECT_DEPTH; i++) { if (!isValidSession()) { throw new Error('Request outdated'); } const lines = currentText.split('\n'); const nextUrlLine = processor._resolveNextVariantUrl(lines); if (!nextUrlLine) { break; } currentUrl = new URL(nextUrlLine, currentUrl).href; currentText = await processor.fetchText(currentUrl, isValidSession, requestId); } return { currentText, currentUrl }; }, }; const M3U8ProcessingPipeline = { _applySmartSlicing(processor, processedLines, currentText, currentUrl, settingsManager, diagnosticsTool) { if (!settingsManager.config.isSmartSlicingEnabled) { processor.log('[m3u8处理器]|智能净化已禁用,跳过处理', CONSTANTS.LOG_TYPES.CORE_SLICE); return processedLines; } processedLines = processor._applyUrlFeatureSlicing( processedLines, currentText, currentUrl, diagnosticsTool ); return processor._applyFalconSmartSlicing(processedLines, currentUrl); }, _applyUrlFeatureSlicing(processor, processedLines, currentText, currentUrl, diagnosticsTool) { if (!currentText.includes('#EXT-X-DISCONTINUITY')) { processor.log( `[m3u8处理器]|决策:M3U8结构简单(无分组),跳过路径分析以防误判,直接进入行为分析。`, CONSTANTS.LOG_TYPES.CORE_SLICE ); return processedLines; } const urlKeywords = diagnosticsTool.analyzeUrlPatterns(currentText, currentUrl); if (urlKeywords?.length > 0) { processor.log( `[m3u8处理器]|决策:「URL特征」发现<高置信度>关键词,执行精准净化。`, CONSTANTS.LOG_TYPES.CORE_SLICE ); return processor.sliceAdSegmentsByKeywords(processedLines, urlKeywords); } processor.log(`[m3u8处理器]|决策:「URL特征」未发现,转交「行为清扫」。`, CONSTANTS.LOG_TYPES.CORE_SLICE); return processedLines; }, _applyFalconSmartSlicing(processor, processedLines, currentUrl) { const falconResult = processor.runFalconV3Engine(processedLines, currentUrl); if (falconResult.wasSliced) { return falconResult.content; } return processedLines; }, _finalizeProcessedM3u8(processor, finalLines) { let result = finalLines.join('\n'); if (!result.trim().startsWith('#EXTM3U')) { result = '#EXTM3U\n' + result; } return result; }, _handleProcessFailure(processor, e, requestId, initialUrl) { if (e.message.includes('Request outdated')) { processor.log( `[m3u8处理器]|[ID:${requestId}] 任务已由新请求接管,停止处理旧请求。`, CONSTANTS.LOG_TYPES.INFO ); } else { processor.log( `[m3u8处理器]|[ID:${requestId}]处理M3U8时出错:${initialUrl}, 错误:${e.message}`, CONSTANTS.LOG_TYPES.ERROR ); } }, async process(processor, initialText, initialUrl, requestId, isValidSession) { const { settingsManager, diagnosticsTool, playerManager } = processor.context; processor.log(`[m3u8处理器]|指令分发:将清单送往[广告过滤]模块进行净化。`, CONSTANTS.LOG_TYPES.CORE); let currentText = initialText; const baseUrl = processor._resolveInitialM3u8Url(initialUrl); try { const chain = await processor._resolvePlaylistChain(currentText, baseUrl, requestId, isValidSession); currentText = chain.currentText; const currentUrl = chain.currentUrl; if (!isValidSession()) { throw new Error('Request outdated'); } playerManager.lastM3u8Content = currentText; let processedLines = currentText.split('\n'); processedLines = processor._applySmartSlicing( processedLines, currentText, currentUrl, settingsManager, diagnosticsTool ); const finalLines = processor.resolveRelativeUrls(processedLines, currentUrl); const result = processor._finalizeProcessedM3u8(finalLines); processor.log( `[m3u8处理器]|[ID:${requestId}] M3U8处理完成: 最终URL为${currentUrl}`, CONSTANTS.LOG_TYPES.CORE ); return { original: currentText, processed: result, finalUrl: currentUrl, }; } catch (e) { processor._handleProcessFailure(e, requestId, initialUrl); throw e; } }, }; class M3U8Processor extends BaseModule { constructor(context) { super(context, 'M3u8'); } _logFetchRequest(url, requestId) { return M3U8FetchManager._logFetchRequest(this, url, requestId); } _shouldRetryFetch(err, retriesLeft) { return M3U8FetchManager._shouldRetryFetch(this, err, retriesLeft); } async _attemptFetchText(url, isValidSession, retriesLeft) { return await M3U8FetchManager._attemptFetchText(this, url, isValidSession, retriesLeft); } _applyFetchFailureToDiagnostics(err) { return M3U8FetchManager._applyFetchFailureToDiagnostics(this, err); } async fetchText(url, isValidSession, requestId) { return await M3U8FetchManager.fetchText(this, url, isValidSession, requestId); } rebuildFromGroups(originalLines, finalGroups, options = {}) { return M3U8KeywordSlicer.rebuildFromGroups(this, originalLines, finalGroups, options); } _buildGroupRebuildHeaderLines(originalLines, options = {}) { return M3U8KeywordSlicer._buildGroupRebuildHeaderLines(this, originalLines, options); } _appendRebuiltGroupLines(headerLines, finalGroups) { return M3U8KeywordSlicer._appendRebuiltGroupLines(this, headerLines, finalGroups); } _ensureRebuiltGroupsEndlist(originalLines, finalLines) { return M3U8KeywordSlicer._ensureRebuiltGroupsEndlist(this, originalLines, finalLines); } _createKeywordMatcher(keywords) { return M3U8KeywordSlicer._createKeywordMatcher(this, keywords); } _splitDiscontinuityGroups(lines) { return M3U8KeywordSlicer._splitDiscontinuityGroups(this, lines); } _calculateGroupDuration(group) { return M3U8KeywordSlicer._calculateGroupDuration(this, group); } _handleKeywordMatchedGroup(group, groupDuration, diagnosticsTool, state) { return M3U8KeywordSlicer._handleKeywordMatchedGroup(this, group, groupDuration, diagnosticsTool, state); } _collectKeywordSliceState(segmentGroups, adMatcher, diagnosticsTool) { return M3U8KeywordSlicer._collectKeywordSliceState(this, segmentGroups, adMatcher, diagnosticsTool); } _finalizeKeywordSlice(lines, state, diagnosticsTool) { return M3U8KeywordSlicer._finalizeKeywordSlice(this, lines, state, diagnosticsTool); } sliceAdSegmentsByKeywords(lines, keywords) { return M3U8KeywordSlicer.sliceAdSegmentsByKeywords(this, lines, keywords); } runFalconV3Engine(lines, baseUrl) { return M3U8BehavioralAnalyzer.runFalconV3Engine(this, lines, baseUrl); } _groupSegments(lines) { return M3U8BehavioralAnalyzer._groupSegments(this, lines); } _createSegmentGroupingState() { return M3U8BehavioralAnalyzer._createSegmentGroupingState(this); } _consumeSegmentGroupingLine(line, state) { return M3U8BehavioralAnalyzer._consumeSegmentGroupingLine(this, line, state); } _collectSegmentHeaderLine(line, trimmed, state) { return M3U8BehavioralAnalyzer._collectSegmentHeaderLine(this, line, trimmed, state); } _flushSegmentGroup(state) { return M3U8BehavioralAnalyzer._flushSegmentGroup(this, state); } _analyzeGroupFeatures(segmentGroups, baseUrl) { return M3U8BehavioralAnalyzer._analyzeGroupFeatures(this, segmentGroups, baseUrl); } _calculateSegmentGroupDuration(group) { return M3U8BehavioralAnalyzer._calculateSegmentGroupDuration(this, group); } _getSegmentGroupSequenceRange(group) { return M3U8BehavioralAnalyzer._getSegmentGroupSequenceRange(this, group); } _getSegmentGroupBoundaryUrl(group, edge) { return M3U8BehavioralAnalyzer._getSegmentGroupBoundaryUrl(this, group, edge); } _extractSequenceNumber(value) { return M3U8BehavioralAnalyzer._extractSequenceNumber(this, value); } _buildClassifiedSegmentGroup(groupLines, index, duration, sequenceRange) { return M3U8BehavioralAnalyzer._buildClassifiedSegmentGroup( this, groupLines, index, duration, sequenceRange ); } _markBehavioralAdCandidates(classified) { return M3U8BehavioralAnalyzer._markBehavioralAdCandidates(this, classified); } _markSequenceBridgeAdCandidate(prev, curr, next) { return M3U8BehavioralAnalyzer._markSequenceBridgeAdCandidate(this, prev, curr, next); } _markContextualShortAdCandidate(prev, curr, next) { return M3U8BehavioralAnalyzer._markContextualShortAdCandidate(this, prev, curr, next); } _filterAdGroups(classifiedGroups, headerLines, originalLines) { return M3U8BehavioralAnalyzer._filterAdGroups(this, classifiedGroups, headerLines, originalLines); } _shouldAbortBehavioralFiltering(classifiedGroups) { return M3U8BehavioralAnalyzer._shouldAbortBehavioralFiltering(this, classifiedGroups); } _createAdGroupFilterState(headerLines) { return M3U8BehavioralAnalyzer._createAdGroupFilterState(this, headerLines); } _appendFilteredSegmentGroup(group, filterState) { return M3U8BehavioralAnalyzer._appendFilteredSegmentGroup(this, group, filterState); } _appendAdFilteredSegmentGroup(group, filterState) { return M3U8BehavioralAnalyzer._appendAdFilteredSegmentGroup(this, group, filterState); } _appendAdFilteredSegmentFeature(group, diagnosticsTool) { return M3U8BehavioralAnalyzer._appendAdFilteredSegmentFeature(this, group, diagnosticsTool); } _appendAdFilteredSegmentRange(group, filterState, diagnosticsTool) { return M3U8BehavioralAnalyzer._appendAdFilteredSegmentRange(this, group, filterState, diagnosticsTool); } _appendPlayableFilteredSegmentGroup(group, filterState) { return M3U8BehavioralAnalyzer._appendPlayableFilteredSegmentGroup(this, group, filterState); } _ensureFilteredPlaylistEndlist(originalLines, finalLines) { return M3U8BehavioralAnalyzer._ensureFilteredPlaylistEndlist(this, originalLines, finalLines); } _finalizeBehavioralFiltering(filterState, originalLines) { return M3U8BehavioralAnalyzer._finalizeBehavioralFiltering(this, filterState, originalLines); } hasMediaSegments(lines) { return M3U8BehavioralAnalyzer.hasMediaSegments(this, lines); } resolveRelativeUrls(lines, baseUrl) { return M3U8PlaylistResolver.resolveRelativeUrls(this, lines, baseUrl); } _resolveRelativeM3u8Line(line, baseUrl) { return M3U8PlaylistResolver._resolveRelativeM3u8Line(this, line, baseUrl); } _resolveRelativeMediaM3u8Line(line, trimmed, baseUrl) { return M3U8PlaylistResolver._resolveRelativeMediaM3u8Line(this, line, trimmed, baseUrl); } _resolveRelativeDirectiveM3u8Line(line, trimmed, baseUrl) { return M3U8PlaylistResolver._resolveRelativeDirectiveM3u8Line(this, line, trimmed, baseUrl); } _shouldSkipRelativeM3u8Line(trimmed) { return M3U8PlaylistResolver._shouldSkipRelativeM3u8Line(this, trimmed); } _isResolvableMediaLine(trimmed) { return M3U8PlaylistResolver._isResolvableMediaLine(this, trimmed); } _isResolvableDirectiveLine(trimmed) { return M3U8PlaylistResolver._isResolvableDirectiveLine(this, trimmed); } _isAbsoluteUrl(url) { return M3U8PlaylistResolver._isAbsoluteUrl(this, url); } _resolveRelativeUrlAgainstBase(relative, baseUrl) { return M3U8PlaylistResolver._resolveRelativeUrlAgainstBase(this, relative, baseUrl); } _mergeBaseSearchParamsIntoTarget(target, baseUrl) { return M3U8PlaylistResolver._mergeBaseSearchParamsIntoTarget(this, target, baseUrl); } _resolveInitialM3u8Url(initialUrl) { return M3U8PlaylistResolver._resolveInitialM3u8Url(this, initialUrl); } _isMasterPlaylist(lines) { return M3U8PlaylistResolver._isMasterPlaylist(this, lines); } _hasMediaEntries(lines) { return M3U8PlaylistResolver._hasMediaEntries(this, lines); } _extractQualityUrls(lines) { return M3U8PlaylistResolver._extractQualityUrls(this, lines); } _selectBestQualityUrl(qualityUrls) { return M3U8PlaylistResolver._selectBestQualityUrl(this, qualityUrls); } _resolveNextVariantUrl(lines) { return M3U8PlaylistResolver._resolveNextVariantUrl(this, lines); } async _resolvePlaylistChain(initialText, baseUrl, requestId, isValidSession) { return await M3U8PlaylistResolver._resolvePlaylistChain( this, initialText, baseUrl, requestId, isValidSession ); } _applySmartSlicing(processedLines, currentText, currentUrl, settingsManager, diagnosticsTool) { return M3U8ProcessingPipeline._applySmartSlicing( this, processedLines, currentText, currentUrl, settingsManager, diagnosticsTool ); } _applyUrlFeatureSlicing(processedLines, currentText, currentUrl, diagnosticsTool) { return M3U8ProcessingPipeline._applyUrlFeatureSlicing( this, processedLines, currentText, currentUrl, diagnosticsTool ); } _applyFalconSmartSlicing(processedLines, currentUrl) { return M3U8ProcessingPipeline._applyFalconSmartSlicing(this, processedLines, currentUrl); } _finalizeProcessedM3u8(finalLines) { return M3U8ProcessingPipeline._finalizeProcessedM3u8(this, finalLines); } _handleProcessFailure(e, requestId, initialUrl) { return M3U8ProcessingPipeline._handleProcessFailure(this, e, requestId, initialUrl); } async process(initialText, initialUrl, requestId, isValidSession) { return await M3U8ProcessingPipeline.process(this, initialText, initialUrl, requestId, isValidSession); } } class SandboxManager extends BaseModule { constructor(context) { super(context, 'SandboxManager'); this.iframeSandbox = null; this.sandboxReloadTimer = null; this.sandboxCountdownTimer = null; this.pendingReloadDelayTimer = null; this.lastActualReloadAt = 0; this.lastBoostAt = 0; this.hasGrantedSlowSiteExtraWait = false; this.hasTriggeredSandbox = false; this.hasLockedHighValueTarget = false; this.lastBridgeAssistAt = 0; this.bridgeAttemptState = new Map(); this.bridgeCooldownState = new Map(); this.sandboxTargetCooldownState = new Map(); this.lastSandboxTargetKey = ''; this.lastSandboxTargetMode = ''; this.lastSandboxTargetStartedAt = 0; this._resetProgressTrackingState(); } _resetProgressTrackingState() { this.lastBridgeCandidateKey = ''; this.lastNestedPlayableSeenAt = 0; this.lastProgressAt = 0; this.lastProgressKind = ''; this.lastProgressMeta = ''; this.progressStage = 0; this.progressProtectUntil = 0; this.lastProgressLogAt = 0; } _isBridgeIframeCandidate(iframeOrSrc) { try { const src = typeof iframeOrSrc === 'string' ? iframeOrSrc : iframeOrSrc?.src || ''; if (!src) { return false; } const url = new URL(src, window.location.href); const rawPath = (url.pathname || '').toLowerCase(); const path = rawPath.replace(/\/+$/, ''); const lastSegment = path.split('/').filter(Boolean).pop() || ''; const queryValues = Array.from(url.searchParams.values()).filter(Boolean); const hasDirectMediaExt = /(?:\.m3u8|\.mp4|\.m3u|\.flv|\.mpd|\.webm|\.mov|\.mkv)(?:[?#]|$)/i.test(src); if (hasDirectMediaExt) { return false; } const looksLikePlayerPath = /(^|\/)(play|player|embed|watch|video|vod|stream)(\/|$)/i.test(path); const opaqueTail = /^[a-z0-9_-]{8,}$/i.test(lastSegment) && !/\.(?:html?|php|asp|aspx|jsp)$/i.test(lastSegment); const opaqueQuery = queryValues.some( (value) => /^[a-z0-9_%:/.-]{12,}$/i.test(String(value)) && !/\.(?:m3u8|mp4|m3u|flv|mpd)(?:[?#]|$)/i.test(String(value)) ); const wrapperLike = looksLikePlayerPath && (opaqueTail || opaqueQuery); if (!wrapperLike) { return false; } const iframe = typeof iframeOrSrc === 'string' ? null : iframeOrSrc; if (iframe && Number(iframe.dmzMediaEvidence || 0) > 0) { return false; } return true; } catch (e) { return false; } } _getBridgeTargetKey(iframeOrSrc) { try { const src = typeof iframeOrSrc === 'string' ? iframeOrSrc : iframeOrSrc?.src || ''; if (!src) { return ''; } const url = new URL(src, window.location.href); const keyBase = `${url.hostname.toLowerCase()}${url.pathname.toLowerCase()}`; const firstOpaqueQuery = Array.from(url.searchParams.entries()).find(([, value]) => /^[a-z0-9_%:/.-]{8,}$/i.test(String(value)) ) || null; return firstOpaqueQuery ? `${keyBase}?${firstOpaqueQuery[0]}=${String(firstOpaqueQuery[1]).toLowerCase()}` : keyBase; } catch (e) { return String( typeof iframeOrSrc === 'string' ? iframeOrSrc : iframeOrSrc?.src || '' || '' ).toLowerCase(); } } _recordSandboxProgress(kind, meta = '') { const stageMap = { bridge_detected: 10, significant_resize: 15, player_expand: 22, nested_iframe: 35, commander_lock: 45, soldier_started: 55, inner_scanner: 65, media_boot: 75, media_ready: 85, xhr_seen: 100, }; const protectMap = { bridge_detected: 2500, significant_resize: 3000, player_expand: 3500, nested_iframe: 4000, commander_lock: 4000, soldier_started: 4500, inner_scanner: 3500, media_boot: 3000, media_ready: 3000, xhr_seen: 5000, }; const now = Date.now(); const stage = Number(stageMap[kind] || 1); const shouldAdvance = stage > this.progressStage || kind !== this.lastProgressKind || !this.lastProgressAt || now - this.lastProgressAt > 1200; if (shouldAdvance) { this.progressStage = Math.max(this.progressStage, stage); this.lastProgressAt = now; this.lastProgressKind = kind; this.lastProgressMeta = String(meta || ''); } else { this.lastProgressAt = now; } const protectMs = Number(protectMap[kind] || 1800); this.progressProtectUntil = Math.max(Number(this.progressProtectUntil || 0), now + protectMs); } _getProgressExtensionMs() { if (this.progressStage >= 85) { return 2400; } if (this.progressStage >= 75) { return 2600; } if (this.progressStage >= 65) { return 3000; } if (this.progressStage >= 55) { return 3400; } if (this.progressStage >= 45) { return 4000; } if (this.progressStage >= 35) { return 3500; } if (this.progressStage >= 22) { return 3500; } if (this.progressStage >= 15) { return 3000; } if (this.progressStage >= 10) { return 2500; } return 0; } _shouldDeferFallbackOnProgress() { if (!this.lastProgressAt) { return false; } const now = Date.now(); if (this.progressProtectUntil && now < this.progressProtectUntil) { return true; } const elapsed = now - this.lastProgressAt; const extensionMs = this._getProgressExtensionMs(); return extensionMs > 0 && elapsed < extensionMs; } _logProgressDeferral(reason, extensionMs) { const now = Date.now(); if (this.lastProgressLogAt && now - this.lastProgressLogAt < 1200) { return; } this.lastProgressLogAt = now; const detail = this.lastProgressMeta ? `:${this.lastProgressMeta}` : ''; this.log( `[沙箱]|📦|检测到阶段推进(${this.lastProgressKind}${detail}),追加${Math.ceil(extensionMs / 1000)}秒观察,暂不执行${reason}。`, CONSTANTS.LOG_TYPES.CORE ); } _scheduleProgressExtension(reason, extensionMs, callback) { if (!(extensionMs > 0) || typeof callback !== 'function') { return false; } if (this.sandboxCountdownTimer) { clearTimeout(this.sandboxCountdownTimer); this.sandboxCountdownTimer = null; } this._logProgressDeferral(reason, extensionMs); this.sandboxCountdownTimer = setTimeout(() => { this.sandboxCountdownTimer = null; if ( this.iframeSandbox || this.context.playerManager.isPlayerActiveOrInitializing || this.context.coreLogic.decisionInProgress ) { return; } callback(); }, extensionMs); return true; } _scheduleSandboxTimeoutExtension(sandboxTarget, extensionMs) { if (!(extensionMs > 0)) { return false; } if (this.sandboxReloadTimer) { clearTimeout(this.sandboxReloadTimer); this.sandboxReloadTimer = null; } this._logProgressDeferral('策略切换', extensionMs); this.sandboxReloadTimer = setTimeout(() => { if (!this.iframeSandbox) { return; } this.log( `[沙箱]|📦|追加观察(${Math.round(extensionMs / 1000)}s)结束仍无有效信号。触发故障恢复...`, CONSTANTS.LOG_TYPES.WARN ); this.cleanup(); if (!this.context.playerManager.isPlayerActiveOrInitializing) { this.context.coreLogic.reportPlaybackFailure({ reason: 'Sandbox Timeout', }); } }, extensionMs); return true; } _getBridgeAttemptEntry(iframeOrSrc) { const key = this._getBridgeTargetKey(iframeOrSrc); if (!key) { return null; } let entry = this.bridgeAttemptState.get(key); if (!entry) { entry = { fullPageAttempts: 0, preciseAttempts: 0, nestedSeenAt: 0, lastMode: '', }; this.bridgeAttemptState.set(key, entry); } this.lastBridgeCandidateKey = key; return entry; } _markBridgeStrategyAttempt(iframeOrSrc, mode) { const entry = this._getBridgeAttemptEntry(iframeOrSrc); if (!entry) { return; } if (mode === 'bridge-fullpage') { entry.fullPageAttempts += 1; } else if (mode === 'bridge-precise') { entry.preciseAttempts += 1; } entry.lastMode = mode; } canRetryBridgeFlow() { const entry = this._getBridgeAttemptEntry(this.lastBridgeCandidateKey); if (!entry) { return false; } if (this.lastSandboxTargetMode !== 'bridge-fullpage') { return false; } if (this.progressStage < 15) { return false; } return entry.fullPageAttempts < 3; } _markBridgeNestedEvidence(iframeOrSrc = null) { const now = Date.now(); this.lastNestedPlayableSeenAt = now; const key = iframeOrSrc ? this._getBridgeTargetKey(iframeOrSrc) : this.lastBridgeCandidateKey; if (!key) { return; } const entry = this._getBridgeAttemptEntry(key); if (!entry) { return; } entry.nestedSeenAt = now; this.bridgeCooldownState.delete(key); this._recordSandboxProgress('nested_iframe', key); } _getBridgeCooldownEntry(iframeOrSrc) { const key = this._getBridgeTargetKey(iframeOrSrc); if (!key) { return null; } let entry = this.bridgeCooldownState.get(key); if (!entry) { entry = { until: 0, failures: 0, reason: '', }; this.bridgeCooldownState.set(key, entry); } return entry; } _getBridgeCooldownRemainingMs(iframeOrSrc) { const entry = this._getBridgeCooldownEntry(iframeOrSrc); if (!entry) { return 0; } const remainingMs = Number(entry.until || 0) - Date.now(); if (remainingMs <= 0) { entry.until = 0; entry.reason = ''; return 0; } return remainingMs; } _isBridgeCooldownActive(iframeOrSrc) { return this._getBridgeCooldownRemainingMs(iframeOrSrc) > 0; } _markBridgeCooldown(iframeOrSrc, reason = '桥接页超时空转', cooldownMs = 15000) { const key = this._getBridgeTargetKey(iframeOrSrc); if (!key) { return; } const entry = this._getBridgeCooldownEntry(key); if (!entry) { return; } const now = Date.now(); entry.failures = Number(entry.failures || 0) + 1; entry.reason = String(reason || '桥接页超时空转'); entry.until = Math.max(Number(entry.until || 0), now + Math.max(12000, Number(cooldownMs || 0))); this.log( `[桥接] 🧊同一桥接目标连续空转,进入${Math.ceil((entry.until - now) / 1000)}秒冷却,等待新的内层证据。`, CONSTANTS.LOG_TYPES.WARN ); } _normalizeSandboxTargetKey(targetUrl) { try { if (!targetUrl) { return ''; } const url = new URL(targetUrl, window.location.href); const keyBase = `${url.hostname.toLowerCase()}${url.pathname.toLowerCase()}`; const firstOpaqueQuery = Array.from(url.searchParams.entries()).find(([, value]) => /^[a-z0-9_%:/.-]{8,}$/i.test(String(value)) ) || null; return firstOpaqueQuery ? `${keyBase}?${firstOpaqueQuery[0]}=${String(firstOpaqueQuery[1]).toLowerCase()}` : keyBase; } catch (e) { return String(targetUrl || '').split('#')[0].toLowerCase(); } } _getSandboxTargetCooldownEntry(targetUrl) { const key = this._normalizeSandboxTargetKey(targetUrl); if (!key) { return null; } let entry = this.sandboxTargetCooldownState.get(key); if (!entry) { entry = { until: 0, failures: 0, reason: '', }; this.sandboxTargetCooldownState.set(key, entry); } return entry; } _getSandboxTargetCooldownRemainingMs(targetUrl) { const entry = this._getSandboxTargetCooldownEntry(targetUrl); if (!entry) { return 0; } const remainingMs = Number(entry.until || 0) - Date.now(); if (remainingMs <= 0) { entry.until = 0; entry.reason = ''; return 0; } return remainingMs; } _isSandboxTargetCooldownActive(targetUrl) { return this._getSandboxTargetCooldownRemainingMs(targetUrl) > 0; } _markSandboxTargetCooldown(targetUrl, reason = '同目标沙箱超时无信号', cooldownMs = 15000) { const key = this._normalizeSandboxTargetKey(targetUrl); if (!key) { return; } const entry = this._getSandboxTargetCooldownEntry(key); if (!entry) { return; } const now = Date.now(); entry.failures = Number(entry.failures || 0) + 1; entry.reason = String(reason || '同目标沙箱超时无信号'); entry.until = Math.max(Number(entry.until || 0), now + Math.max(15000, Number(cooldownMs || 0))); this.log( `[沙箱]|📦|同一目标已连续空转,进入${Math.ceil((entry.until - now) / 1000)}秒冷却,停止重复重载:${String(targetUrl).slice(-80)}`, CONSTANTS.LOG_TYPES.WARN ); } _noteSandboxTargetLaunch(sandboxTarget) { this.lastSandboxTargetKey = this._normalizeSandboxTargetKey(sandboxTarget?.targetUrl || ''); this.lastSandboxTargetMode = String(sandboxTarget?.mode || ''); this.lastSandboxTargetStartedAt = Date.now(); } _shouldSkipSandboxTargetReload(sandboxTarget) { const targetUrl = sandboxTarget?.targetUrl || ''; if (!targetUrl) { return false; } const cooldownLeftMs = this._getSandboxTargetCooldownRemainingMs(targetUrl); if (!(cooldownLeftMs > 0)) { return false; } this.log( `[沙箱]|📦|同一目标仍在冷却(${Math.ceil(cooldownLeftMs / 1000)}s),跳过重复重载:${String(targetUrl).slice(-80)}`, CONSTANTS.LOG_TYPES.WARN ); const bestIframe = this._getBestSandboxIframe(); if (bestIframe && this._isBridgeIframeCandidate(bestIframe)) { this.assistBridgeIframe(bestIframe); } return true; } _shouldEscalateBridgeToPrecise(bestIframe) { if (!bestIframe || !this._isBridgeIframeCandidate(bestIframe)) { return false; } if (this._isBridgeCooldownActive(bestIframe)) { return false; } const entry = this._getBridgeAttemptEntry(bestIframe); if (!entry) { return false; } const now = Date.now(); const relaxed = entry.fullPageAttempts >= 1 || this.progressStage >= 15; const hasLiveNestedIframe = !!this._findNestedPlayableIframe(bestIframe, relaxed); const recentNested = hasLiveNestedIframe || (entry.nestedSeenAt && now - entry.nestedSeenAt < 3000) || (this.lastNestedPlayableSeenAt && now - this.lastNestedPlayableSeenAt < 3000); if (recentNested) { return false; } const hasDirectCandidate = !!this._extractDirectSandboxCandidateUrl(bestIframe); if (hasDirectCandidate) { return entry.fullPageAttempts >= 1 && entry.preciseAttempts < 1; } return entry.fullPageAttempts >= 2 && this.progressStage >= 15 && entry.preciseAttempts < 1; } _findNestedPlayableIframe(excludeIframe = null, relaxed = false) { try { const minW = relaxed ? 48 : 120; const minH = relaxed ? 36 : 80; const minScore = relaxed ? 60 : 90; const candidates = Array.from(document.querySelectorAll('iframe')) .filter((f) => { if (!f || f.id === 'dmz_sandbox_frame' || f === excludeIframe) { return false; } const src = f.src || ''; if (!src || src === 'about:blank') { return false; } const rect = f.getBoundingClientRect?.() || { width: f.offsetWidth || 0, height: f.offsetHeight || 0, }; if ((rect.width || 0) < minW || (rect.height || 0) < minH) { return false; } if (this.context.frameCommunicator?._isDefinitelyNonVideoIframe?.(f, src)) { return false; } const semanticHit = /(^|\/)(play|player|embed|watch|video|vod|stream)(\/|$)|(?:\.m3u8|\.mp4|\.m3u|\.flv|\.mpd)(?:[?#]|$)/i.test( src ); const score = this.getIframeScore(f); return semanticHit || score >= minScore; }) .map((f) => ({ iframe: f, score: this.getIframeScore(f), area: Math.round((f.offsetWidth || 0) * (f.offsetHeight || 0)), })) .sort((a, b) => b.score - a.score || b.area - a.area); return candidates[0]?.iframe || null; } catch (e) { return null; } } _shouldPreferFullPageSandbox(bestIframe) { if (!bestIframe) { return false; } if (!this._isBridgeIframeCandidate(bestIframe)) { return false; } const nested = this._findNestedPlayableIframe(bestIframe); return !nested; } assistBridgeIframe(bestIframe) { if (!bestIframe) { return; } const now = Date.now(); if (now - this.lastBridgeAssistAt < 1800) { return; } this.lastBridgeAssistAt = now; this._getBridgeAttemptEntry(bestIframe); this.hasLockedHighValueTarget = true; this._recordSandboxProgress('bridge_detected', this._getBridgeTargetKey(bestIframe)); this.log( `[桥接] 🎯识别为桥接型播放器,原7秒等待已废除,进入极速破壁模式!`, CONSTANTS.LOG_TYPES.CORE_EXEC ); try { if (bestIframe.contentWindow) { this.context.coreLogic.overlayManager.attachIframeTrigger(bestIframe, bestIframe.contentWindow); } Utils.dispatchMouseEventSequence( bestIframe,['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'] ); } catch (e) {} this._promoteBestIframeDirectSignal(bestIframe, '桥接页预判(URL参数直连)'); this._startIframePoker(bestIframe); this.extendCountdown(1500, '桥接型播放器极速预热'); } reload(consumeQuota = true) { if (this._isSandboxReloadBlocked()) { return; } if (this._shouldDelayHostSensitiveSandboxReload(consumeQuota)) { return; } if (this._shouldAbortRepeatedSandboxReload()) { return; } const sandboxTarget = this._resolveSandboxReloadTarget(); if (!sandboxTarget) { return; } if (this._shouldSkipSandboxTargetReload(sandboxTarget)) { return; } this._markSandboxQuotaUsage(consumeQuota); this._noteSandboxReloadStarted(); this._launchSandboxReload(sandboxTarget); } _isHostSandboxRateLimited() { return false; } _getHostSandboxCooldownMs() { return 0; } _shouldDelayHostSensitiveSandboxReload(consumeQuota) { const cooldownMs = this._getHostSandboxCooldownMs(); if (!cooldownMs || !this.lastActualReloadAt) { return false; } const elapsed = Date.now() - this.lastActualReloadAt; if (elapsed >= cooldownMs) { return false; } if (this.pendingReloadDelayTimer) { return true; } const waitMs = cooldownMs - elapsed; this.log( `[沙箱]|📦|检测到站点节流风险,延迟${Math.ceil(waitMs / 1000)}秒后再尝试静默重载。`, CONSTANTS.LOG_TYPES.WARN ); this.pendingReloadDelayTimer = setTimeout(() => { this.pendingReloadDelayTimer = null; if ( this.context.playerManager.isPlayerActiveOrInitializing || this.context.coreLogic.decisionInProgress || this.context.coreLogic.isSystemHalted ) { return; } this.reload(consumeQuota); }, waitMs); return true; } _noteSandboxReloadStarted() { this.lastActualReloadAt = Date.now(); this._clearPendingReloadDelayTimer(); } _clearPendingReloadDelayTimer() { if (this.pendingReloadDelayTimer) { clearTimeout(this.pendingReloadDelayTimer); this.pendingReloadDelayTimer = null; } } _shouldAbortRepeatedSandboxReload() { if (!this.hasTriggeredSandbox) { return false; } this.log(`[沙箱]|📦|已执行过重载但仍未成功,停止尝试。`, CONSTANTS.LOG_TYPES.WARN); return true; } _resolveSandboxReloadTarget() { let targetUrl = window.location.href; let logMsg = '正在后台执行静默重载(全页模式)...'; let mode = 'fullpage'; try { const bestIframe = this._getBestSandboxIframe(); if (bestIframe) { this._logSandboxIframeTarget(bestIframe); this._promoteBestIframeDirectSignal(bestIframe, '沙箱预判(URL参数直连)'); this._startIframePoker(bestIframe); const isBridgeWrapper = this._isBridgeIframeCandidate(bestIframe); const bridgeEntry = isBridgeWrapper ? this._getBridgeAttemptEntry(bestIframe) : null; const relaxedNested = !!(bridgeEntry && (bridgeEntry.fullPageAttempts >= 1 || this.progressStage >= 15)); const nestedBridgeIframe = isBridgeWrapper ? this._findNestedPlayableIframe(bestIframe, relaxedNested) : null; if (isBridgeWrapper && !nestedBridgeIframe?.src && this._isBridgeCooldownActive(bestIframe)) { const cooldownLeftMs = this._getBridgeCooldownRemainingMs(bestIframe); this.log( `[桥接] 🧊同一桥接目标仍在冷却(${Math.ceil(cooldownLeftMs / 1000)}s),跳过重复沙箱重载,继续等待新证据。`, CONSTANTS.LOG_TYPES.WARN ); return null; } if (nestedBridgeIframe?.src) { targetUrl = nestedBridgeIframe.src; mode = 'nested-precise'; logMsg = `正在后台执行静默重载(内层精准模式),目标:${targetUrl.slice(-80)}`; this.log( `[桥接] 🧭检测到桥接页已展开内层播放器,直接切入内层目标。`, CONSTANTS.LOG_TYPES.CORE_EXEC ); this._markBridgeNestedEvidence(bestIframe); } else if (this._shouldPreferFullPageSandbox(bestIframe)) { if (this._shouldEscalateBridgeToPrecise(bestIframe)) { targetUrl = bestIframe.src; mode = 'bridge-precise'; logMsg = `正在后台执行静默重载(桥接型页面精准模式),目标:${targetUrl.slice(-80)}`; this.log( `[桥接] 🧭已拿到桥接页直连线索,改为精准切入。`, CONSTANTS.LOG_TYPES.CORE_EXEC ); this._markBridgeStrategyAttempt(bestIframe, mode); } else { mode = 'bridge-fullpage'; logMsg = '正在后台执行静默重载(桥接型页面全页模式)...'; this.log( `[桥接] 🧭当前高分目标更像桥接页,继续走全页模式等待其自行展开内层播放器。`, CONSTANTS.LOG_TYPES.CORE_EXEC ); this._markBridgeStrategyAttempt(bestIframe, mode); } } else if (isBridgeWrapper) { mode = 'bridge-fullpage'; logMsg = '正在后台执行静默重载(桥接型页面全页模式)...'; this.log( `[桥接] 🧭尚未发现可直击的内层播放器,保持全页观察,不对桥接页外壳做精准重载。`, CONSTANTS.LOG_TYPES.CORE_EXEC ); this._markBridgeStrategyAttempt(bestIframe, mode); } else { targetUrl = bestIframe.src; mode = 'precise'; logMsg = `正在后台执行静默重载(精准模式),目标:${targetUrl.slice(-80)}`; } } } catch (e) { console.debug('Failed to evaluate iframes for sandbox:', e); } return { targetUrl, logMsg, mode, _bridgeKey: mode.startsWith('bridge-') ? this.lastBridgeCandidateKey : '', }; } _getBestSandboxIframe() { return ( this._collectPotentialSandboxIframes().sort( (a, b) => b.dmzScore - a.dmzScore || b.dmzArea - a.dmzArea )[0] || null ); } _logSandboxIframeTarget(bestIframe) { this.hasLockedHighValueTarget = true; this.log( `[加速] 🎯锁定目标iFrame (Score:${bestIframe.dmzScore}, Src:${bestIframe.src.slice(-30)}),启动连发指令...`, CONSTANTS.LOG_TYPES.CORE_EXEC ); } _launchSandboxReload(sandboxTarget) { this._noteSandboxTargetLaunch(sandboxTarget); this.log(`[沙箱]|📦|${sandboxTarget.logMsg}`, CONSTANTS.LOG_TYPES.CORE_IFRAME); this.cleanup(); this.iframeSandbox = this._createSandboxFrame(); this._loadSandboxFrame(sandboxTarget.targetUrl); this._armSandboxTimeout(sandboxTarget); } _isSandboxReloadBlocked() { return new URLSearchParams(window.location.search).has('dmz_sandbox'); } _markSandboxQuotaUsage(consumeQuota) { if (consumeQuota) { this.hasTriggeredSandbox = true; } } _extractDirectSandboxCandidateUrl(iframeElement) { try { const iframeSrc = iframeElement?.src || ''; if (!iframeSrc) { return null; } const directUrl = this.context.coreLogic._extractUrlFromParameter(iframeSrc); if (!(directUrl && directUrl !== iframeSrc && /^https?:\/\//.test(directUrl))) { return null; } if ( !/(?:\.m3u8|\.mp4|\.m3u|\.flv|\.mpd)(?:[?#]|$)|\/(?:hls|play|vod|stream|video)\//i.test(directUrl) ) { return null; } return directUrl; } catch (e) { return null; } } _promoteBestIframeDirectSignal(bestIframe, sourceName = '沙箱预判(URL参数直连)') { const directUrl = this._extractDirectSandboxCandidateUrl(bestIframe); if (!directUrl && bestIframe && bestIframe.dmzScore >= 80 && bestIframe.src) { this.log( `[前置侦察] 对高分目标发起后台探测: ${bestIframe.src.slice(-40)}`, CONSTANTS.LOG_TYPES.CORE_EXEC ); Utils.probeIframeForM3u8(bestIframe.src, this.context, '沙箱预判(后台HTML直搜)'); } if (!directUrl) { return false; } if (directUrl === this.lastPromotedDirectUrl) { return true; } this.lastPromotedDirectUrl = directUrl; this.log( `[加速] 🎯从目标iFrame参数中提前提取直连地址:${directUrl.slice(-60)}`, CONSTANTS.LOG_TYPES.CORE_EXEC ); this.context.coreLogic.addTakeoverCandidate({ url: directUrl, sourceName, }); return true; } getIframeScore(f) { const r = f.getBoundingClientRect(); if (r.width < 50 || r.height < 50) { return 0; } if (this.context.frameCommunicator?._isDefinitelyNonVideoIframe?.(f, f?.src || '')) { f.dmzScore = 0; f.dmzArea = r.width * r.height; f.dmzHardRejected = true; return 0; } if ( this.context.frameCommunicator?._canPromoteIframeToTarget && !this.context.frameCommunicator._canPromoteIframeToTarget(f, f?.src || '', 'sandbox-score') ) { f.dmzScore = 0; f.dmzArea = r.width * r.height; return 0; } let score = 0; const baseScore = this.context.frameCommunicator ? this.context.frameCommunicator._scoreFrameContext(f, f?.src || '') : 0; score += Math.max(0, baseScore); const directUrl = this._extractDirectSandboxCandidateUrl(f); if (directUrl) { score += 35; } if ( directUrl && /(?:\.m3u8|\.mp4|\.m3u|\.flv|\.mpd)(?:[?#]|$)|\/(?:hls|play|vod|stream|video)\//i.test(directUrl) ) { score += 25; } f.dmzScore = score; f.dmzArea = r.width * r.height; return score; } _collectPotentialSandboxIframes() { return Array.from(document.querySelectorAll('iframe')).filter((f) => { if ( this.context.frameCommunicator?._canPromoteIframeToTarget && !this.context.frameCommunicator._canPromoteIframeToTarget(f, f?.src || '', 'collect-sandbox') ) { return false; } const score = this.getIframeScore(f); return score > 0 || f.dmzArea > 80000; }); } extendCountdown(duration, reason) { if (this.hasTriggeredSandbox || this.iframeSandbox) { return; } if (this.sandboxCountdownTimer && this.hasLockedHighValueTarget) { return; } const now = Date.now(); if (!this.lastExtendAt) { this.lastExtendAt = 0; } if (now - this.lastExtendAt < 500) { return; } this.lastExtendAt = now; const arm = () => { this.sandboxCountdownTimer = setTimeout(() => { this.sandboxCountdownTimer = null; if ( this.iframeSandbox || this.context.playerManager.isPlayerActiveOrInitializing || this.context.coreLogic.decisionInProgress ) { return; } if (this._shouldDeferFallbackOnProgress()) { const extensionMs = this._getProgressExtensionMs(); if ( this._scheduleProgressExtension('策略切换', extensionMs, () => { if ( this.iframeSandbox || this.context.playerManager.isPlayerActiveOrInitializing || this.context.coreLogic.decisionInProgress ) { return; } this.log( '[沙箱]|📦|阶段推进已停滞,触发重载(不消耗次数)。', CONSTANTS.LOG_TYPES.CORE_EXEC ); this.reload(false); }) ) { return; } } this.log('[沙箱]|📦|延长宽限期结束仍无信号,触发重载(不消耗次数)。', CONSTANTS.LOG_TYPES.CORE_EXEC); this.reload(false); }, duration); }; if (this.sandboxCountdownTimer) { clearTimeout(this.sandboxCountdownTimer); } arm(); this.log(`[沙箱]|📦|倒计时已更新(${duration}ms)「${reason}」。`, CONSTANTS.LOG_TYPES.CORE); } _startIframePoker(bestIframe) { let pokeCount = 0; Utils.startPollingTask( 400, () => { if (pokeCount++ > 40 || this.context.playerManager.isPlayerActiveOrInitializing) { return false; } try { bestIframe.contentWindow.postMessage({ type: CONSTANTS.MESSAGE_TYPES.ACTIVATE_AUTOPLAY }, '*'); } catch (e) { console.debug('Failed to poke iframe:', e); } return true; }, 'Iframe poker failed:' ); } _createSandboxFrame() { return DomUtils.create('iframe', { id: 'dmz_sandbox_frame', sandbox: 'allow-scripts allow-same-origin', allow: 'autoplay; fullscreen; encrypted-media', style: { display: 'block', width: '100%', height: '100%', border: 'none', position: 'fixed', top: '0', left: '0', opacity: '0.001', visibility: 'visible', zIndex: '-2147483648', pointerEvents: 'none', }, }); } _loadSandboxFrame(targetUrl) { try { const newUrl = new URL(targetUrl, window.location.href); newUrl.searchParams.set('dmz_sandbox', 'true'); this.iframeSandbox.src = newUrl.href; document.body.appendChild(this.iframeSandbox); } catch (e) { this.log(`[沙箱] URL构造失败: ${e.message}`, CONSTANTS.LOG_TYPES.ERROR); } } _armSandboxTimeout(sandboxTarget = null) { const mode = String(sandboxTarget?.mode || ''); let timeoutMs = Number(sandboxTarget?._timeoutOverride || 0); if (!(timeoutMs > 0)) { timeoutMs = 15000; if (mode === 'bridge-fullpage') { timeoutMs = 12000; } else if (mode === 'bridge-precise' || mode === 'nested-precise') { timeoutMs = 12000; } else { const bestIframe = this._getBestSandboxIframe(); const isBridgePath = this._shouldPreferFullPageSandbox(bestIframe); timeoutMs = isBridgePath ? 12000 : 15000; } } this.sandboxReloadTimer = setTimeout(() => { if (!this.iframeSandbox) { return; } if (this._shouldDeferFallbackOnProgress()) { const extensionMs = this._getProgressExtensionMs(); if (this._scheduleSandboxTimeoutExtension(sandboxTarget, extensionMs)) { return; } } this.log( `[沙箱]|📦|探测超时(${Math.round(timeoutMs / 1000)}s),未发现新信号。触发故障恢复...`, CONSTANTS.LOG_TYPES.WARN ); const isBridgeMode = mode === 'bridge-fullpage' || mode === 'bridge-precise'; const isOuterPageReload = sandboxTarget?.targetUrl && sandboxTarget.targetUrl.split('#')[0] === window.location.href.split('#')[0]; if (sandboxTarget?.targetUrl && !(mode === 'bridge-fullpage' && isOuterPageReload)) { this._markSandboxTargetCooldown( sandboxTarget.targetUrl, '同目标沙箱超时无有效信号', 15000 ); } if (isBridgeMode) { this._markBridgeCooldown( sandboxTarget?._bridgeKey || this.lastBridgeCandidateKey, '桥接沙箱超时无有效信号', 15000 ); } this.cleanup(); if (!this.context.playerManager.isPlayerActiveOrInitializing) { this.context.coreLogic.reportPlaybackFailure({ reason: 'Sandbox Timeout', }); } }, timeoutMs); } startCountdown() { const { playerManager, coreLogic } = this.context; if (this._shouldSkipSandboxCountdown(playerManager, coreLogic)) { return; } if (this.sandboxCountdownTimer) { clearTimeout(this.sandboxCountdownTimer); } this.lastBoostAt = 0; this.hasGrantedSlowSiteExtraWait = false; this.log('[沙箱]视频页启动|⏳0.8秒首轮倒计时...', CONSTANTS.LOG_TYPES.CORE); this.sandboxCountdownTimer = setTimeout(() => { this.sandboxCountdownTimer = null; this._handleSandboxCountdown(playerManager, coreLogic); }, 800); } _shouldSkipSandboxCountdown(playerManager, coreLogic) { return ( new URLSearchParams(window.location.search).has('dmz_sandbox') || playerManager.isPlayerActiveOrInitializing || coreLogic.isSystemHalted ); } _handleSandboxCountdown(playerManager, coreLogic) { if (playerManager.isPlayerActiveOrInitializing || coreLogic.decisionInProgress) { return; } this._boostSandboxSignal(); this._smartIdleWait(playerManager, coreLogic, 0); } _hasPendingHighConfidenceTargets() { const bestIframe = this._getBestSandboxIframe(); return bestIframe && bestIframe.dmzScore >= 60; } _smartIdleWait(playerManager, coreLogic, waitCount) { if (playerManager.isPlayerActiveOrInitializing || coreLogic.decisionInProgress) { return; } const hasTarget = this._hasPendingHighConfidenceTargets(); const isLoading = document.readyState !== 'complete'; const maxWait = hasTarget ? 10 : isLoading ? 8 : 4; if (waitCount >= maxWait) { this.log( `[沙箱]|📦|动态观察期结束(页面已静默/高潜目标耗尽),准备最终判决...`, CONSTANTS.LOG_TYPES.CORE ); this._extendSandboxCountdown(playerManager, coreLogic, '[沙箱]|📦|执行二次激活并继续等待...'); return; } if (waitCount === 0) { this.log( `[沙箱]|📦|${isLoading ? '页面加载中' : '页面已就绪'}${hasTarget ? ',但已锁定高潜目标' : ''},启动智能防抖等待...`, CONSTANTS.LOG_TYPES.CORE ); } this.sandboxCountdownTimer = setTimeout(() => { this.sandboxCountdownTimer = null; this._smartIdleWait(playerManager, coreLogic, waitCount + 1); }, 500); } _boostSandboxSignal() { try { const bestIframe = this._getBestSandboxIframe(); if (!bestIframe) { return; } this.lastBoostAt = Date.now(); this._logSandboxIframeTarget(bestIframe); if (bestIframe.contentWindow) { this.context.coreLogic.overlayManager.attachIframeTrigger(bestIframe, bestIframe.contentWindow); } this._promoteBestIframeDirectSignal(bestIframe, '沙箱加速(URL参数直连)'); if (!bestIframe.contentWindow) { return; } bestIframe.contentWindow.postMessage({ type: CONSTANTS.MESSAGE_TYPES.ACTIVATE_AUTOPLAY }, '*'); } catch (e) { console.debug('Failed to boost sandbox signal:', e); } } _shouldGrantSlowSiteExtraWait() { if (this.hasGrantedSlowSiteExtraWait) { return false; } try { const bestIframe = this._getBestSandboxIframe(); if (!bestIframe?.contentWindow) { return false; } const elapsed = this.lastBoostAt ? Date.now() - this.lastBoostAt : 0; if (elapsed > 3500) { return false; } const src = (bestIframe.src || '').toLowerCase(); return bestIframe.dmzScore >= 85 || /(play|video|vod|embed|stream|m3u8|dplayer|prism)/.test(src); } catch (e) { return false; } } _extendSandboxCountdown(playerManager, coreLogic, logMessage) { this.log(logMessage, CONSTANTS.LOG_TYPES.CORE); this.sandboxCountdownTimer = setTimeout(() => { this.sandboxCountdownTimer = null; if (this.iframeSandbox || playerManager.isPlayerActiveOrInitializing || coreLogic.decisionInProgress) { return; } if (this._shouldDeferFallbackOnProgress()) { const extensionMs = this._getProgressExtensionMs(); if ( this._scheduleProgressExtension('策略切换', extensionMs, () => { if ( this.iframeSandbox || playerManager.isPlayerActiveOrInitializing || coreLogic.decisionInProgress ) { return; } this.log('[沙箱]|📦|阶段推进已停滞,触发重载(不消耗次数)。', CONSTANTS.LOG_TYPES.CORE_EXEC); this.reload(false); }) ) { return; } } if (this._shouldGrantSlowSiteExtraWait()) { this.hasGrantedSlowSiteExtraWait = true; this._boostSandboxSignal(); this.log( '[沙箱]|📦|检测到站点链路仍在响应,判定为网站侧较慢,追加宽限等待...', CONSTANTS.LOG_TYPES.WARN ); this.sandboxCountdownTimer = setTimeout(() => { this.sandboxCountdownTimer = null; if ( this.iframeSandbox || playerManager.isPlayerActiveOrInitializing || coreLogic.decisionInProgress ) { return; } if (this._shouldDeferFallbackOnProgress()) { const extensionMs = this._getProgressExtensionMs(); if ( this._scheduleProgressExtension('策略切换', extensionMs, () => { if ( this.iframeSandbox || playerManager.isPlayerActiveOrInitializing || coreLogic.decisionInProgress ) { return; } this.log( '[沙箱]|📦|追加宽限结束且阶段停滞,触发重载(不消耗次数)。', CONSTANTS.LOG_TYPES.CORE_EXEC ); this.reload(false); }) ) { return; } } this.log( '[沙箱]|📦|追加宽限结束仍无信号,触发重载(不消耗次数)。', CONSTANTS.LOG_TYPES.CORE_EXEC ); this.reload(false); }, 6000); return; } this.log('[沙箱]|📦|宽限期结束仍无信号,触发重载(不消耗次数)。', CONSTANTS.LOG_TYPES.CORE_EXEC); this.reload(false); }, 1000); } cancelCountdown(reason) { if (this.sandboxCountdownTimer) { clearTimeout(this.sandboxCountdownTimer); this.sandboxCountdownTimer = null; this.log(`[沙箱]|📦|倒计时已取消「${reason}」。`, CONSTANTS.LOG_TYPES.CORE); } } cleanup() { if (this.sandboxReloadTimer) { clearTimeout(this.sandboxReloadTimer); this.sandboxReloadTimer = null; } if (this.sandboxCountdownTimer) { clearTimeout(this.sandboxCountdownTimer); this.sandboxCountdownTimer = null; } this._clearPendingReloadDelayTimer(); if (this.iframeSandbox) { this.iframeSandbox.remove(); this.iframeSandbox = null; this.log(`iFrame沙箱已清理。`, CONSTANTS.LOG_TYPES.CORE_IFRAME); } } reset() { this.hasTriggeredSandbox = false; this.lastBoostAt = 0; this.hasGrantedSlowSiteExtraWait = false; this.hasLockedHighValueTarget = false; this.lastBridgeAssistAt = 0; this.bridgeAttemptState.clear(); this.bridgeCooldownState.clear(); this.sandboxTargetCooldownState.clear(); this.lastSandboxTargetKey = ''; this.lastSandboxTargetMode = ''; this.lastSandboxTargetStartedAt = 0; this._resetProgressTrackingState(); this._clearPendingReloadDelayTimer(); this.cleanup(); } } class OverlayManager extends BaseModule { constructor(context) { super(context, 'OverlayManager'); } _getSmartAnchor(target) { let node = target.parentElement; while (node && node !== document.body) { const s = window.getComputedStyle(node); const isClipped = s.overflow === 'hidden' || s.overflowX === 'hidden' || s.overflowY === 'hidden'; const isSmall = node.offsetHeight < target.offsetHeight || node.offsetWidth < target.offsetWidth; if (isClipped && isSmall) { node = node.parentElement; continue; } if (s.position !== 'static' || s.transform !== 'none' || s.contain === 'paint') { return node; } node = node.parentElement; } return document.body; } _getSafeZIndex(anchor, target = null, targetRect = null) { let max = 10; try { Array.from(anchor.children).forEach((el) => { const z = this._getNumericZIndex(el); if (z > max && z < 2147483640) { max = z; } }); if (!target) { return String(max + 1); } const resolvedRect = targetRect || target.getBoundingClientRect(); const targetZ = this._getNumericZIndex(target); if (targetZ > max && targetZ < 2147483640) { max = targetZ; } let candidate = max + 1; let cap = 2147483639; this._collectOverlayBlockingElements(target, resolvedRect).forEach((el) => { const z = this._getNumericZIndex(el); if (z > 0) { cap = Math.min(cap, z - 1); } }); if (cap < 1) { cap = 1; } if (candidate > cap) { candidate = cap; } return String(candidate); } catch (e) { console.debug('Failed to get safe z-index:', e); } return '11'; } _getNumericZIndex(element) { if (!element || element === document.body || element === document.documentElement) { return 0; } try { const z = parseInt(window.getComputedStyle(element).zIndex, 10); return Number.isFinite(z) ? z : 0; } catch (e) { return 0; } } _rectsIntersect(a, b) { return ( !!a && !!b && a.width > 0 && a.height > 0 && b.width > 0 && b.height > 0 && !(a.right <= b.left || a.left >= b.right || a.bottom <= b.top || a.top >= b.bottom) ); } _getOverlaySamplingPoints(targetRect) { const clamp = (value, min, max) => Math.min(max, Math.max(min, value)); const insetX = Math.min(24, Math.max(8, targetRect.width * 0.1)); const insetY = Math.min(24, Math.max(8, targetRect.height * 0.1)); const maxX = Math.max(0, window.innerWidth - 1); const maxY = Math.max(0, window.innerHeight - 1); const left = clamp(targetRect.left + insetX, 0, maxX); const right = clamp(targetRect.right - insetX, 0, maxX); const centerX = clamp(targetRect.left + targetRect.width / 2, 0, maxX); const top = clamp(targetRect.top + insetY, 0, maxY); const bottom = clamp(targetRect.bottom - insetY, 0, maxY); const centerY = clamp(targetRect.top + targetRect.height / 2, 0, maxY); return [ [centerX, top], [centerX, centerY], [centerX, bottom], [left, top], [right, top], [left, centerY], [right, centerY], [left, bottom], [right, bottom], ]; } _isOverlayBlockingElement(element, target, targetRect) { if (!element || element === target || element === document.body || element === document.documentElement) { return false; } if (element.closest(CONSTANTS.SELECTORS.DMZ_EXCLUSION)) { return false; } if (element.contains(target) || target.contains(element)) { return false; } let style; try { style = window.getComputedStyle(element); } catch (e) { return false; } if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity || '1') === 0) { return false; } if (style.position !== 'fixed' && style.position !== 'sticky') { return false; } const rect = element.getBoundingClientRect(); if (!this._rectsIntersect(targetRect, rect)) { return false; } const nearTop = rect.top <= 40; const nearBottom = window.innerHeight - rect.bottom <= 40; const isWideBar = rect.width >= window.innerWidth * 0.35 && rect.height >= 28 && rect.height <= Math.min(220, window.innerHeight * 0.35) && (nearTop || nearBottom); const attrText = `${element.tagName || ''} ${element.id || ''} ${String(element.className || '')} ${element.getAttribute('role') || ''}`.toLowerCase(); const hasMediaChild = element.matches('video, iframe') || !!element.querySelector('video, iframe'); const isFloatingPlayer = rect.width >= 120 && rect.height >= 68 && rect.width <= window.innerWidth * 0.85 && rect.height <= window.innerHeight * 0.85 && (hasMediaChild || /(player|video|mini|float|pip|picture|dock)/.test(attrText)); return isWideBar || isFloatingPlayer; } _collectOverlayBlockingElements(target, targetRect) { const blockers = []; const seen = new Set(); this._getOverlaySamplingPoints(targetRect).forEach(([x, y]) => { let stack = []; try { stack = document.elementsFromPoint(x, y) || []; } catch (e) { stack = []; } stack.forEach((node) => { let current = node; while (current && current !== document.body && current !== document.documentElement) { if (!seen.has(current) && this._isOverlayBlockingElement(current, target, targetRect)) { seen.add(current); blockers.push(current); } current = current.parentElement; } }); }); return blockers; } _syncOverlayZIndex(overlay, anchor, target, targetRect) { const rectKey = `${Math.round(targetRect.top)}:${Math.round(targetRect.left)}:${Math.round(targetRect.width)}:${Math.round(targetRect.height)}`; const now = performance.now(); if ( overlay._dmzZIndexRectKey === rectKey && now - (overlay._dmzZIndexTimestamp || 0) < 250 && overlay.style.zIndex ) { return; } overlay._dmzZIndexRectKey = rectKey; overlay._dmzZIndexTimestamp = now; overlay.style.zIndex = this._getSafeZIndex(anchor, target, targetRect); } _createTriggerOverlayElement(isPassive) { const overlay = DomUtils.create('div', { className: isPassive ? 'dmz-switch-overlay' : 'dmz-iframe-remote-trigger', style: { position: 'absolute', top: '0', left: '0', width: '0', height: '0', background: 'rgba(0,0,0,0.5)', display: isPassive ? 'none' : 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', zIndex: 'auto', opacity: isPassive ? '1' : '0.8', visibility: 'hidden', pointerEvents: 'auto', transition: 'opacity 0.2s linear', userSelect: 'none', WebkitUserSelect: 'none', WebkitTouchCallout: 'none', }, }); overlay.innerHTML = `
`; return overlay; } _createTriggerOverlayCloseButton(onDismiss, overlay) { const closeBtn = DomUtils.create('div', { style: { position: 'absolute', top: '0', left: '0', width: '30px', height: '30px', background: 'rgba(0,0,0,0.6)', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', zIndex: '100', borderBottomRightRadius: '8px', fontSize: '20px', fontWeight: 'bold', }, textContent: '×', title: '关闭触发器', 'data-dmz-trigger-close': 'true', }); closeBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); onDismiss(overlay); }); return closeBtn; } _isTriggerOverlayCloseTarget(target) { return !!( target && typeof target.closest === 'function' && target.closest('[data-dmz-trigger-close="true"]') ); } _createLongPressHandlers() { let longPressTimer; return { start: (e) => { if ( (e.type === 'mousedown' && e.button !== 0) || (e.type === 'touchstart' && e.touches.length > 1) ) { return; } const playerHost = this.context.playerManager.hostElement; if (playerHost && playerHost.classList.contains('dmz-visible')) { return; } longPressTimer = setTimeout(() => { this.context.coreLogic.restorePageToOriginalState(); if (navigator.vibrate) { navigator.vibrate(50); } }, 1000); }, cancel: () => clearTimeout(longPressTimer), clear: () => clearTimeout(longPressTimer), }; } _bindOverlayContextMenu(overlay) { overlay.addEventListener('contextmenu', (e) => { e.preventDefault(); e.stopPropagation(); }); } _bindOverlayLongPressEvents(overlay, longPressHandlers) { overlay.addEventListener('mousedown', (e) => { if (this._isTriggerOverlayCloseTarget(e.target)) { longPressHandlers.clear(); return; } longPressHandlers.start(e); }); overlay.addEventListener( 'touchstart', (e) => { if (this._isTriggerOverlayCloseTarget(e.target)) { longPressHandlers.clear(); return; } longPressHandlers.start(e); }, { passive: true, } ); ['mouseup', 'touchend', 'touchcancel', 'mouseleave'].forEach((evt) => overlay.addEventListener(evt, longPressHandlers.cancel) ); } _bindOverlayActivationEvent(overlay, onActivate, longPressHandlers) { let startX, startY, isDragging, startTime; const cleanup = () => { document.removeEventListener('pointermove', onPointerMove, { capture: true }); document.removeEventListener('pointerup', onPointerUp, { capture: true }); document.removeEventListener('pointercancel', onPointerCancel, { capture: true }); }; const onPointerCancel = () => { isDragging = true; cleanup(); }; const onPointerUp = (e) => { cleanup(); if (this._isTriggerOverlayCloseTarget(e.target)) { isDragging = true; longPressHandlers.clear(); return; } if (isDragging) return; const duration = Date.now() - startTime; if (duration > 350) return; e.preventDefault(); e.stopPropagation(); longPressHandlers.clear(); onActivate(e, overlay); }; const onPointerMove = (e) => { if (isDragging) return; const deltaX = Math.abs(e.clientX - startX); const deltaY = Math.abs(e.clientY - startY); if (deltaX > 10 || deltaY > 10) { isDragging = true; cleanup(); } }; overlay.addEventListener('pointerdown', (e) => { if (e.button !== 0 || this._isTriggerOverlayCloseTarget(e.target)) return; isDragging = false; startX = e.clientX; startY = e.clientY; startTime = Date.now(); document.addEventListener('pointermove', onPointerMove, { capture: true }); document.addEventListener('pointerup', onPointerUp, { capture: true }); document.addEventListener('pointercancel', onPointerCancel, { capture: true }); }); } _appendOverlay(anchor, overlay) { overlay.style.zIndex = this._getSafeZIndex(anchor); anchor.appendChild(overlay); } _isCrossOriginFrameContext() { let isCross = false; try { isCross = window.self.location.origin !== window.top.location.origin; } catch (e) { isCross = true; } return !CONSTANTS.IS_TOP_FRAME && isCross; } _hideDetachedOverlay(target, overlay) { if (!target.isConnected) { overlay.remove(); return true; } return !overlay.isConnected; } _applyBodyOverlayPosition(overlay, targetRect) { const scrollX = window.scrollX || window.pageXOffset; const scrollY = window.scrollY || window.pageYOffset; overlay.style.top = `${targetRect.top + scrollY}px`; overlay.style.left = `${targetRect.left + scrollX}px`; } _applyAnchoredOverlayPosition(overlay, targetRect, anchor) { const anchorRect = anchor.getBoundingClientRect(); overlay.style.top = `${targetRect.top - anchorRect.top + anchor.scrollTop}px`; overlay.style.left = `${targetRect.left - anchorRect.left + anchor.scrollLeft}px`; } _applyOverlayRect(overlay, targetRect) { overlay.style.width = `${targetRect.width}px`; overlay.style.height = `${targetRect.height}px`; } _showOverlayIfAllowed(overlay, isPassive) { if (!isPassive) { overlay.style.display = 'flex'; } if (!isPassive && !overlay.dataset.userHidden) { overlay.style.opacity = '1'; } } _updateOverlayPosition(target, overlay, anchor, isPassive) { if (this._hideDetachedOverlay(target, overlay)) { return; } if (this._shouldDeferOverlayPositionInCrossFrame(overlay, target, anchor, isPassive)) { return; } this._applyOverlayPositionState(target, overlay, anchor, isPassive); requestAnimationFrame(() => this._updateOverlayPosition(target, overlay, anchor, isPassive)); } _shouldDeferOverlayPositionInCrossFrame(overlay, target, anchor, isPassive) { if (!this._isCrossOriginFrameContext()) { return false; } overlay.style.display = 'none'; requestAnimationFrame(() => this._updateOverlayPosition(target, overlay, anchor, isPassive)); return true; } _getTrueVideoRect(target, rawRect) { let trueRect = { top: rawRect.top, left: rawRect.left, width: rawRect.width, height: rawRect.height, right: rawRect.right, bottom: rawRect.bottom }; let vw = 0, vh = 0; if (target.tagName === 'VIDEO' && target.videoWidth && target.videoHeight) { vw = target.videoWidth; vh = target.videoHeight; } else if (target.tagName === 'IFRAME') { if (target.dataset.dmzVideoWidth && target.dataset.dmzVideoHeight) { vw = parseFloat(target.dataset.dmzVideoWidth); vh = parseFloat(target.dataset.dmzVideoHeight); } else { try { const innerVideo = target.contentDocument?.querySelector('video'); if (innerVideo && innerVideo.videoWidth && innerVideo.videoHeight) { vw = innerVideo.videoWidth; vh = innerVideo.videoHeight; } } catch (e) {} } } if (!vw || !vh) return trueRect; const cRatio = rawRect.width / rawRect.height; const vRatio = vw / vh; if (Math.abs(cRatio - vRatio) < 0.01) return trueRect; if (cRatio > vRatio) { const actualWidth = rawRect.height * vRatio; const offsetX = (rawRect.width - actualWidth) / 2; trueRect.width = actualWidth; trueRect.left = rawRect.left + offsetX; trueRect.right = trueRect.left + actualWidth; } else { const actualHeight = rawRect.width / vRatio; const offsetY = (rawRect.height - actualHeight) / 2; trueRect.height = actualHeight; trueRect.top = rawRect.top + offsetY; trueRect.bottom = trueRect.top + actualHeight; } return trueRect; } _applyOverlayPositionState(target, overlay, anchor, isPassive) { const rawRect = target.getBoundingClientRect(); if (this._isOverlayTargetOutOfViewport(rawRect)) { overlay.style.display = 'none'; return; } const targetRect = this._getTrueVideoRect(target, rawRect); this._showOverlayIfAllowed(overlay, isPassive); this._applyOverlayAnchorPosition(overlay, targetRect, anchor); this._applyOverlayRect(overlay, targetRect); this._syncOverlayZIndex(overlay, anchor, target, targetRect); } _isOverlayTargetOutOfViewport(targetRect) { return ( targetRect.width === 0 || targetRect.height === 0 || targetRect.bottom < 0 || targetRect.top > window.innerHeight ); } _applyOverlayAnchorPosition(overlay, targetRect, anchor) { if (anchor === document.body) { this._applyBodyOverlayPosition(overlay, targetRect); return; } this._applyAnchoredOverlayPosition(overlay, targetRect, anchor); } _primeOverlayPlacement(target, overlay, anchor) { const rawRect = target.getBoundingClientRect(); if (this._isOverlayTargetOutOfViewport(rawRect)) { overlay.style.display = 'none'; return; } const targetRect = this._getTrueVideoRect(target, rawRect); this._applyOverlayAnchorPosition(overlay, targetRect, anchor); this._applyOverlayRect(overlay, targetRect); this._syncOverlayZIndex(overlay, anchor, target, targetRect); } _revealPrimedOverlay(target, overlay, anchor, isPassive) { if (!overlay.isConnected) { return; } overlay.style.visibility = 'visible'; this._updateOverlayPosition(target, overlay, anchor, isPassive); } createTriggerOverlay(target, onActivate, onDismiss, isPassive = false) { const overlay = this._createTriggerOverlayElement(isPassive); const closeBtn = this._createTriggerOverlayCloseButton(onDismiss, overlay); const longPressHandlers = this._createLongPressHandlers(); overlay.appendChild(closeBtn); this._bindOverlayContextMenu(overlay); this._bindOverlayLongPressEvents(overlay, longPressHandlers); this._bindOverlayActivationEvent(overlay, onActivate, longPressHandlers); const anchor = this._getSmartAnchor(target); this._primeOverlayPlacement(target, overlay, anchor); this._appendOverlay(anchor, overlay); requestAnimationFrame(() => this._revealPrimedOverlay(target, overlay, anchor, isPassive)); return overlay; } _shouldSkipIframeTrigger(iframeElement) { const { coreLogic } = this.context; if (coreLogic.isSystemHalted || coreLogic.isRestoreInProgress) { return true; } return ( iframeElement.dataset.dmzHasTrigger === 'true' || iframeElement.dataset.dmzTriggerDismissed === 'true' ); } _markIframeTriggerAttached(iframeElement) { iframeElement.dataset.dmzHasTrigger = 'true'; this.log(`[跨域交互]|☎️|为Iframe添加智能层级触发器...`, CONSTANTS.LOG_TYPES.UI); } _createIframeDismissHandler(iframeElement) { return (overlay) => { iframeElement.dataset.dmzTriggerDismissed = 'true'; overlay.remove(); }; } _resetActivatedOverlay(overlay) { if (!overlay || !overlay.isConnected) { return; } overlay.style.opacity = '0.35'; overlay.style.pointerEvents = 'none'; overlay.dataset.userHidden = 'busy'; if (overlay.dmzRestoreTimer) { clearTimeout(overlay.dmzRestoreTimer); } overlay.dmzRestoreTimer = setTimeout(() => { if (!overlay.isConnected) { return; } if (overlay.dataset.userHidden === 'busy') { delete overlay.dataset.userHidden; } overlay.style.opacity = '1'; overlay.style.pointerEvents = 'auto'; }, 1800); } _rearmDomScanner(domScanner) { domScanner.isStopped = false; domScanner.init(); } _postIframeTriggerMessage(sourceWindow, payload, failureMessage, failureType = CONSTANTS.LOG_TYPES.ERROR) { try { sourceWindow.postMessage(payload, '*'); return true; } catch (err) { if (failureMessage) { this.log(`${failureMessage}:${err.message}`, failureType); } else { console.debug('Post message to iframe failed:', err); } return false; } } _tryReplayLastSuccessfulUrl(coreLogic, sourceWindow) { if (!(coreLogic.lastSuccessfulUrl && coreLogic.lastSuccessfulUrl.startsWith('http'))) { return false; } this.log( `[记忆回溯]|🌀|顶层直接复用历史地址进行重播:${coreLogic.lastSuccessfulUrl.slice(-50)}`, CONSTANTS.LOG_TYPES.CORE_EXEC ); coreLogic.addTakeoverCandidate({ url: coreLogic.lastSuccessfulUrl, sourceName: '跨域遥控触发器(顶层记忆重播)', }); this._postIframeTriggerMessage(sourceWindow, { type: CONSTANTS.MESSAGE_TYPES.IFRAME_TRIGGER_CLICK }, ''); return true; } _requestIframeSearch(sourceWindow) { this.log(`[跨域交互]|☎️|无历史记忆,向Iframe发送搜寻指令...`, CONSTANTS.LOG_TYPES.COMM); this._postIframeTriggerMessage( sourceWindow, { type: CONSTANTS.MESSAGE_TYPES.IFRAME_TRIGGER_CLICK }, `[跨域交互]|☎️|指令发送失败` ); } _createIframeActivateHandler(sourceWindow, domScanner, coreLogic) { return (e, overlay) => { if (coreLogic.isRestoreInProgress) { return; } this.log(`[跨域交互]|☎️|外部触发器被点击...`, CONSTANTS.LOG_TYPES.PLAYBACK_SWITCH); this._resetActivatedOverlay(overlay); coreLogic.isSystemHalted = false; coreLogic.setUserIntentToSwitch('replay'); this._rearmDomScanner(domScanner); if (this._tryReplayLastSuccessfulUrl(coreLogic, sourceWindow)) { return; } this._requestIframeSearch(sourceWindow); }; } _attachOverlayLifecycleHooks(overlay) { overlay.dmzUpdatePos = () => { if (overlay.dmzRestoreTimer) { clearTimeout(overlay.dmzRestoreTimer); overlay.dmzRestoreTimer = null; } delete overlay.dataset.userHidden; overlay.style.opacity = '1'; overlay.style.pointerEvents = 'auto'; }; overlay.cleanup = () => { if (overlay.dmzRestoreTimer) { clearTimeout(overlay.dmzRestoreTimer); overlay.dmzRestoreTimer = null; } overlay.remove(); }; } attachIframeTrigger(iframeElement, sourceWindow) { const { domScanner, coreLogic } = this.context; if (this._shouldSkipIframeTrigger(iframeElement)) { return; } this._markIframeTriggerAttached(iframeElement); const onDismiss = this._createIframeDismissHandler(iframeElement); const onActivate = this._createIframeActivateHandler(sourceWindow, domScanner, coreLogic); const overlay = this.createTriggerOverlay(iframeElement, onActivate, onDismiss, false); this._attachOverlayLifecycleHooks(overlay); } } class CoreLogic extends BaseModule { constructor(context) { super(context, 'CoreLogic'); this._initializeCoreState(); this._initializeCoreModules(context); } _initializeCoreState() { Object.assign(this, CoreStateFactory.createDefaults()); } _initializeCoreModules(context) { this.m3u8Processor = new M3U8Processor(context); this.sandboxManager = new SandboxManager(context); this.overlayManager = new OverlayManager(context); } startPersistentEnforcer() { CoreEnforcer.start(this); } stopPersistentEnforcer() { CoreEnforcer.stop(this); } handleRemoteTriggerClick() { CoreRemoteTrigger.handleClick(this); } restorePageToOriginalState() { CoreRestoreManager.restorePageToOriginalState(this); } initializeUserIntentObserver() { if (!CONSTANTS.IS_TOP_FRAME) { return; } document.addEventListener('click', (e) => this._handleUserIntentClick(e), true); this.log('用户意图监听器已部署。', CONSTANTS.LOG_TYPES.INIT); } _handleUserIntentClick(e) { if (this.isSystemHalted || !e.isTrusted) { return; } const target = this._getUserIntentTarget(e); if (!target || this._isIgnoredIntentTarget(e, target)) { return; } if (this._matchKeywordIntentTarget(target)) { return; } this._matchGenericIntentTarget(target); } _getUserIntentTarget(e) { const rawNode = e.target; const rawTarget = rawNode?.nodeType === Node.ELEMENT_NODE ? rawNode : rawNode?.parentElement; if (!rawTarget || typeof rawTarget.closest !== 'function') { return null; } const strongTarget = rawTarget.closest('a, button, [role="button"], [class*="episode"], [class*="item"],[class*="play"], [class*="line"]'); if (strongTarget) { return strongTarget; } const liTarget = rawTarget.closest('li'); if (!liTarget) { return rawTarget; } const text = (liTarget.textContent || '').trim(); const attr = DomUtils.getAttrSignature(liTarget); if ( liTarget.hasAttribute('role') || liTarget.hasAttribute('tabindex') || /play|player|route|line|source|episode|switch|tab|btn|option|播放|线路|路线|选集|切换|源/i.test(attr) || /第|集|话|季|EP\d+|E\d+|播放|线路|路线|源/i.test(text) ) { return liTarget; } return rawTarget; } _isIgnoredIntentTarget(e, target) { const { playerManager } = this.context; const path = e.composedPath(); if ( (playerManager.hostElement && path.includes(playerManager.hostElement)) || target.closest(`#${CONSTANTS.IDS.SETTINGS_PANEL}`) ) { return true; } const rect = target.getBoundingClientRect(); const screenArea = window.innerWidth * window.innerHeight; return rect.width * rect.height > screenArea * 0.25; } _matchKeywordIntentTarget(target) { const text = (target.textContent || '').trim(); if ( !( text.length > 0 && text.length < 12 && (/第|集|话|季|EP\d+|E\d+|下一|上一|重播|Play|播放|刷新|路线|线路|源|高清|极速|PV|CM|Prev|Next|^[<>«»◀▶‹›]$/i.test(text) || /^\d{1,4}$/.test(text)) ) ) { return false; } this.setUserIntentToSwitch(); this.log(`👆 [交互] 命中关键词[${text}],许可。`, CONSTANTS.LOG_TYPES.PLAYBACK_SWITCH); return true; } _matchGenericIntentTarget(target) { const tag = target?.tagName?.toUpperCase(); if (!tag || !['A', 'BUTTON', 'LI', 'SPAN', 'DIV', 'IMG', 'SVG'].includes(tag)) { return false; } const rect = target.getBoundingClientRect(); if (rect.width * rect.height >= 100000) { return false; } const attrStr = DomUtils.getAttrSignature(target); const isStrongSwitchIntent = /(episode|next|prev|switch|play|list|item|route|line|线路|路线|选集|切换|源)/i.test(attrStr); if (!isStrongSwitchIntent) { if ( SHARED_PATTERNS.NEGATIVE_UI_KEYWORDS.test(attrStr) || /(window-off|guanbi|close|detail-score|play-score|评分|评论|comment|reply|review|rating)/i.test(attrStr) ) { return false; } if ( !Utils.isElementInPrimaryMediaRegion(target) || !target.closest?.(CONSTANTS.SELECTORS.PLAYER_CONTEXT) ) { return false; } } const cls = (target.className || '').toString().toLowerCase(); this.setUserIntentToSwitch(); this.log( `👆 [交互] 泛点击捕获[${tag}.${cls.length > 50 ? cls.slice(0, 50) + '...' : cls}],解锁决策锁。`, CONSTANTS.LOG_TYPES.PLAYBACK_SWITCH ); return true; } setUserIntentToSwitch(mode = 'switch') { this.sandboxManager.cancelCountdown('用户交互'); const isReplayMode = mode === 'replay'; this.log( isReplayMode ? ' [用户操作]|👇|🔁检测到重播意图,重置决策锁但保留当前视频归属。' : ' [用户操作]|👇|🔐检测到点击,强制重置所有决策锁,为新视频让路。', CONSTANTS.LOG_TYPES.PLAYBACK_SWITCH ); if (!isReplayMode && this.lastSuccessfulUrl) { this.staleVideoUrl = this.lastSuccessfulUrl; } this.userIntendsToSwitch = true; this.decisionInProgress = false; this.decisionMade = false; this.failedCandidateKeys.clear(); if (this.userActionTimeout) { clearTimeout(this.userActionTimeout); } this.userActionTimeout = setTimeout(() => { this.userIntendsToSwitch = false; }, 10000); } _getNormalizationKey(url) { if (typeof url !== 'string') { return null; } if (url.startsWith('blob:')) { return url; } try { const urlObj = new URL(url); if (urlObj.hostname.includes('googlevideo.com')) { return url.split('?')[0]; } return urlObj.href; } catch (e) { return url; } } _extractUrlFromParameter(urlString) { try { const urlObj = new URL(urlString, window.location.href); for (const param of ['url', 'src', 'vid', 'play_url']) { if (urlObj.searchParams.has(param)) { const nestedUrl = urlObj.searchParams.get(param); if (nestedUrl && (nestedUrl.startsWith('http') || nestedUrl.startsWith('/'))) { this.log(`在URL参数'${param}'中提取到真实视频地址。`, CONSTANTS.LOG_TYPES.CORE_URL_RESOLVE); return nestedUrl; } } } } catch (e) { console.debug('Failed to extract URL from parameters:', e); } return urlString; } async addTakeoverCandidate(candidateInfo) { await CoreCandidateIntake.addTakeoverCandidate(this, candidateInfo); } _getQualityScore(url, isM3) { return CoreQualityManager.getQualityScore(url, isM3); } _getCandidateReliabilityScore(candidateInfo = {}) { return CoreQualityManager.getCandidateReliabilityScore(this, candidateInfo); } reportPlaybackFailure(failureContext = {}) { CoreFailureRecovery.reportPlaybackFailure(this, failureContext); } async sendPlayCommand(type, content) { await CorePlaybackDirector.sendPlayCommand(this, type, content); } _invalidateMediaQueryCache() { if (!this.mediaQueryCache) { this.mediaQueryCache = { stamp: 0, result: [], }; return; } this.mediaQueryCache.stamp = 0; this.mediaQueryCache.result = []; } collectMediaFromRoot(root) { const media = []; const visited = new Set(); this._collectMediaFromTree(root, media, visited); return media; } findAllVideosAndAudioInPage(forceRefresh = false) { const cache = this.mediaQueryCache || { stamp: 0, result: [], }; const now = performance.now(); if (!forceRefresh && now - Number(cache.stamp || 0) <= 180) { return cache.result; } const media = []; const visited = new Set(); if (document.body) { this._collectMediaFromTree(document.body, media, visited); } this.mediaQueryCache = { stamp: now, result: media, }; return media; } _collectMediaFromTree(root, media, visited) { if (!root || visited.has(root)) { return; } visited.add(root); try { const shadowRoots = []; if (root.nodeType === Node.ELEMENT_NODE) { if (/^(VIDEO|AUDIO)$/.test(root.tagName)) { media.push(root); } if (root.shadowRoot) { shadowRoots.push(root.shadowRoot); } } const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); let current = walker.nextNode(); while (current) { if (/^(VIDEO|AUDIO)$/.test(current.tagName)) { media.push(current); } if (current.shadowRoot) { shadowRoots.push(current.shadowRoot); } current = walker.nextNode(); } shadowRoots.forEach((shadowRoot) => this._collectMediaFromTree(shadowRoot, media, visited)); } catch (e) { console.debug('Failed to query media nodes:', e); } } neutralizeOriginalPlayer(media, initialState = 'active') { if (this._shouldSkipNeutralization(media)) { return null; } if (this._handleExistingNeutralization(media, initialState)) { return null; } const isPassive = this._isPassiveNeutralization(initialState); const parent = media.parentElement; this._prepareNeutralizationSession(media, isPassive); this._applyNeutralizationState(media, parent, isPassive); const overlay = this._createNeutralizationOverlay(media, parent, isPassive); this._attachOverlayObserver(media, overlay, isPassive); this._applyNeutralizationPlaybackOverrides(media, isPassive); return this._finalizeNeutralizationObserver(media, isPassive); } _isPassiveNeutralization(initialState) { return initialState === 'passive'; } _prepareNeutralizationSession(media, isPassive) { if (!isPassive) { this._enableManagedModules(); } this.context.actionLogger.log( `开始改造原生 ${media.tagName} 为切换触发器...`, CONSTANTS.LOG_TYPES.CORE_NEUTRALIZE ); this._prepareMediaNeutralization(media, isPassive); } _applyNeutralizationState(media, parent, isPassive) { if (!isPassive) { this._applyActiveMediaSuppression(media); } this._bindNeutralizedMediaEvents(media); this._prepareNeutralizedParent(parent); } _createNeutralizationOverlay(media, parent, isPassive) { const onDismiss = this._buildDismissHandler(media, parent); const onActivate = this._buildActivateHandler(media); return this.overlayManager.createTriggerOverlay(media, onActivate, onDismiss, isPassive); } _applyNeutralizationPlaybackOverrides(media, isPassive) { if (!isPassive) { this._overrideMediaPlaybackMethods(media); } } _finalizeNeutralizationObserver(media, isPassive) { const attributeObserver = this._createNeutralizedAttributeObserver(media); if (!isPassive) { this.startPersistentEnforcer(); this.activeNeutralizers.add(attributeObserver); } return attributeObserver; } _shouldSkipNeutralization(media) { if (this._isNeutralizationGloballyBlocked(media)) { return true; } if (this._isNeutralizationMediaUnavailable(media)) { return true; } if (this._isNeutralizationProtectedMedia(media)) { return true; } if (this._isSecondaryPreviewMedia(media)) { return true; } return this._isNeutralizationAdMedia(media); } _isSecondaryPreviewMedia(media) { if (!media || media.tagName !== 'VIDEO') { return false; } if (media.controls || (!media.muted && media.volume > 0)) { return false; } if (this.context.playerManager && this.context.playerManager.videoElement === media) { return false; } const mainPlayerSelectors = '.prism-player, .jwplayer, .dplayer, .plyr, .art-video-player, .video-js, .fp-ui, [class*="player" i]:not([class*="players" i]), [id*="player" i]'; if (media.closest(mainPlayerSelectors)) { return false; } if (media.closest('a')) { return true; } const previewSelectors = 'li, ul, [class*="item" i], [class*="list" i],[class*="thumb" i], [class*="card" i], [class*="preview" i],[class*="grid" i], [class*="feed" i], [class*="teaser" i],[class*="gallery" i]'; if (media.closest(previewSelectors)) { return true; } const rect = media.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { if (rect.width < window.innerWidth * 0.9 && rect.height < window.innerHeight * 0.5) { return true; } if (rect.width <= 450 && rect.height <= 300) { return true; } if (rect.top > window.innerHeight * 0.7 || rect.bottom < window.innerHeight * 0.2) { return true; } } const allVideos = document.querySelectorAll('video'); if (allVideos.length > 1) { let largest = null; let maxArea = 0; allVideos.forEach(v => { const r = v.getBoundingClientRect(); const area = r.width * r.height; if (area > maxArea) { maxArea = area; largest = v; } }); if (largest && largest !== media) { return true; } } return false; } _isNeutralizationGloballyBlocked(media) { return ( this.isSystemHalted || this.isRestoreInProgress || !media || media.id === CONSTANTS.IDS.PLAYER || media.dataset.dmzDismissed === 'true' ); } _isNeutralizationMediaUnavailable(media) { return !media.isConnected; } _isNeutralizationProtectedMedia(media) { if (this._isOffPageLinkMedia(media)) { return true; } if (this._isMiniAutoplayMedia(media)) { return true; } if (this._isLoopingDecorativeMedia(media)) { return true; } return this._isArtplayerProtectedMedia(media); } _isNeutralizationAdMedia(media) { const src = media.src || media.querySelector('source')?.src; return !!(src && PageScanner.isPreRollAdByPathFeatures(src, this.context)); } _isOffPageLinkMedia(media) { const parentLink = media.closest('a'); if ( !parentLink || !parentLink.href || parentLink.href.includes('#') || parentLink.href.includes('javascript:') ) { return false; } const currentUrl = window.location.href.split('#')[0].split('?')[0]; return !parentLink.href.includes(currentUrl); } _isMiniAutoplayMedia(media) { return media.muted && media.autoplay && !media.controls && media.videoWidth > 0 && media.videoWidth < 300; } _isLoopingDecorativeMedia(media) { return media.loop && !media.controls; } _isArtplayerProtectedMedia(media) { return ( media.closest('.artplayer-app, .artplayer-wrapper') && !this.decisionInProgress && this.takeoverCandidates.size === 0 ); } _handleExistingNeutralization(media, initialState) { if (media.dataset.dmzNeutralized && media.dmzOverlay && !media.dmzOverlay.isConnected) { delete media.dataset.dmzNeutralized; delete media.dataset.dmzSourceLocked; delete media.dataset.dmzDismissed; media.dmzOverlay = null; } if (media.dataset.dmzNeutralized === 'true') { if (!media.paused && media.dataset.dmzAllowSignalGeneration !== 'true') { media.pause(); } if (!media.muted) { media.muted = true; } return true; } if (media.dataset.dmzNeutralized === 'passive' && initialState === 'passive') { return true; } if (media.dataset.dmzNeutralized === 'passive' && initialState === 'active') { this._activatePassiveNeutralization(media); return true; } return false; } _activatePassiveNeutralization(media) { this.context.actionLogger.log( `[智能穿透] 监测到视频源转正,立即激活压制器!`, CONSTANTS.LOG_TYPES.CORE_NEUTRALIZE ); media.dataset.dmzNeutralized = 'true'; if (media.dmzOverlay) { media.dmzOverlay.style.display = 'flex'; media.dmzOverlay.style.opacity = '1'; media.dmzOverlay.style.pointerEvents = 'auto'; } media.pause(); media.muted = true; } _enableManagedModules() { ManagedModules.enableConfigured(this.context); } _prepareMediaNeutralization(media, isPassive) { const originalSrc = media.src || media.querySelector('source[src]')?.src; if (originalSrc) { media.dataset.dmzOriginalSrc = originalSrc; } media.dataset.dmzNeutralized = isPassive ? 'passive' : 'true'; media.dmzOverlay = null; media.style.removeProperty('visibility'); } _applyActiveMediaSuppression(media) { media.pause(); media.muted = true; media.volume = 0; media.autoplay = false; media.loop = false; media.removeAttribute('autoplay'); if (!media.dataset.dmzVolumeSaved) { media.dataset.dmzVolumeSaved = 'true'; media.dataset.dmzOriginalVolume = media.volume || 1.0; } } _createPreventPlayHandler(media) { return (e) => { if ( media.dataset.dmzNeutralized === 'passive' || media.dataset.dmzAllowSignalGeneration === 'true' || !e.isTrusted ) { return; } media.pause(); media.muted = true; e.stopPropagation(); e.stopImmediatePropagation(); }; } _bindNeutralizedMediaEvents(media) { media._dmzPreventPlay = this._createPreventPlayHandler(media); ['play', 'playing'].forEach((evt) => media.addEventListener(evt, media._dmzPreventPlay, true)); media.addEventListener( 'canplay', () => { if ( media.dataset.dmzNeutralized !== 'passive' && media.dataset.dmzAllowSignalGeneration !== 'true' ) { media.pause(); media.muted = true; } }, { once: false, passive: true } ); } _prepareNeutralizedParent(parent) { if (!parent) { return; } if (window.getComputedStyle(parent).position === 'static') { parent.style.position = 'relative'; } const r = parent.getBoundingClientRect(); if (r.width > 50 && r.height < 20) { parent.style.minHeight = (r.width * 9) / 16 + 'px'; parent.dataset.dmzFixHeight = 'true'; } } _buildDismissHandler(media, parent) { return (overlay) => { this._restoreNeutralizedParentLayout(parent); this._removeNeutralizedOverlay(overlay, media); this._clearNeutralizedMediaFlags(media); this._restoreNeutralizedMediaPlaybackMethods(media); this._restoreNeutralizedMediaPresentation(media); }; } _restoreNeutralizedParentLayout(parent) { if (!(parent && parent.dataset.dmzFixHeight)) { return; } parent.style.minHeight = ''; delete parent.dataset.dmzFixHeight; } _removeNeutralizedOverlay(overlay, media) { overlay.remove(); this.neutralizedMedia.delete(media); } _clearNeutralizedMediaFlags(media) { media.dataset.dmzDismissed = 'true'; delete media.dataset.dmzNeutralized; delete media.dataset.dmzSourceLocked; } _restoreNeutralizedMediaPlaybackMethods(media) { try { delete media.play; delete media.pause; } catch (e) { console.debug('Failed to delete media methods:', e); } } _restoreNeutralizedMediaPresentation(media) { media.style.visibility = ''; media.style.opacity = ''; media.style.pointerEvents = ''; media.muted = false; media.controls = true; } _buildActivateHandler(media) { return (e, overlay) => { if (this.isRestoreInProgress) { return; } this._prepareNeutralizedOverlayActivation(overlay); if (this._activateNeutralizedOverlayDirect(media)) { return; } if (this._activateNeutralizedOverlayPassive()) { return; } if (this._activateFromLastSuccessfulUrl()) { return; } this._requestActivationFallback(); }; } _prepareNeutralizedOverlayActivation(overlay) { this._prepareOverlayActivation(overlay); this._recordOverlayActivationIntent(); this._scanTopFrameActivationHints(); } _activateNeutralizedOverlayDirect(media) { const directUrl = this._getActivationDirectUrl(media); return this._activateFromDirectUrl(directUrl); } _activateNeutralizedOverlayPassive() { const bestMatch = this._getBestPassiveActivationCandidate(); return this._activateFromPassiveCandidate(bestMatch); } _prepareOverlayActivation(overlay) { if (!overlay.intersectionObserver) { return; } overlay.intersectionObserver.disconnect(); overlay.intersectionObserver = null; } _recordOverlayActivationIntent() { this.setUserIntentToSwitch('replay'); this.context.actionLogger.log( `播放器触发器被点击,系统已重置,准备捕获视频流...`, CONSTANTS.LOG_TYPES.PLAYBACK_SWITCH ); } _scanTopFrameActivationHints() { if (!CONSTANTS.IS_TOP_FRAME) { return; } PageScanner.scanForFlashvars(this.context); PageScanner.scanForEmbeddedData(this.context); } _getActivationDirectUrl(media) { return media.src || media.querySelector('source')?.src || media.dataset.dmzOriginalSrc; } _activateFromDirectUrl(directUrl) { if (!directUrl || !directUrl.startsWith('http')) { return false; } this.context.actionLogger.log( `[直接激活] 发现当前元素已有有效直链,立即播放: ${directUrl.slice(-30)}`, CONSTANTS.LOG_TYPES.CORE_EXEC ); this.addTakeoverCandidate({ url: directUrl, sourceName: '原生播放器触发器(直连)', }); return true; } _getBestPassiveActivationCandidate() { const passiveArr = Array.from(this.passiveCandidates.values()).reverse(); return passiveArr.find((c) => c.m3u8Text || Utils.isM3U8(c.url)) || passiveArr[0]; } _activateFromPassiveCandidate(bestMatch) { if (!bestMatch) { return false; } this.context.actionLogger.log( `[智能穿透] ⚡️命中后台缓存的潜伏信号(正片),立即接管: ${bestMatch.url.slice(-50)}`, CONSTANTS.LOG_TYPES.CORE_EXEC ); this.passiveCandidates.clear(); this.addTakeoverCandidate(bestMatch); return true; } _activateFromLastSuccessfulUrl() { if (!this.lastSuccessfulUrl || !this.lastSuccessfulUrl.startsWith('http')) { return false; } this.context.actionLogger.log( `[记忆回溯] 触发器点击:无新信号,尝试复用历史记录: ${this.lastSuccessfulUrl.slice(-50)}`, CONSTANTS.LOG_TYPES.CORE_EXEC ); this.addTakeoverCandidate({ url: this.lastSuccessfulUrl, sourceName: '原生播放器触发器(历史复用)', }); return true; } _requestActivationFallback() { if (!CONSTANTS.IS_TOP_FRAME) { this.context.actionLogger.log( `[交互] 触发器点击:源无效,请求顶层协助...`, CONSTANTS.LOG_TYPES.CORE_EXEC ); this.context.frameCommunicator.postToTopFrame({ type: CONSTANTS.MESSAGE_TYPES.IFRAME_TRIGGER_CLICK, }); return; } this.context.actionLogger.log( `[交互] 触发器点击:源无效(Blob/空),强制启动沙箱重载以获取新签名...`, CONSTANTS.LOG_TYPES.CORE_EXEC ); this.sandboxManager.hasTriggeredSandbox = false; if (this.sandboxManager.iframeSandbox) { this.sandboxManager.cleanup(); } this.sandboxManager.reload(); } _attachOverlayObserver(media, overlay, isPassive) { try { const parent = media.parentElement; if (parent) { parent.style.isolation = 'auto'; parent.style.zIndex = 'auto'; } } catch (e) { console.debug('Failed to set parent style:', e); } if (!media.dmzOverlay) { media.dmzOverlay = overlay; const observer = new IntersectionObserver( (entries) => { if (media.dataset.dmzNeutralized === 'passive') { return; } overlay.style.opacity = entries[0].isIntersecting ? '1' : '0'; overlay.style.pointerEvents = entries[0].isIntersecting ? 'auto' : 'none'; }, { root: null, rootMargin: '0px', threshold: 0.1 } ); observer.observe(media); overlay.intersectionObserver = observer; } } _overrideMediaPlaybackMethods(media) { try { Object.defineProperties(media, this._createMediaPlaybackOverrides(media)); } catch (e) { console.debug('Failed to override media play/pause:', e); } } _createMediaPlaybackOverrides(media) { return { play: { value: this._createPlayOverride(media), configurable: true, }, pause: { value: this._createPauseOverride(media), configurable: true, }, }; } _createPlayOverride(media) { return () => { if (media.dataset.dmzAllowSignalGeneration === 'true') { return HTMLMediaElement.prototype.play.call(media); } return Promise.resolve(); }; } _createPauseOverride(media) { return () => { if (media.dataset.dmzAllowSignalGeneration === 'true') { return HTMLMediaElement.prototype.pause.call(media); } }; } _createNeutralizedAttributeObserver(media) { const attributeObserver = new MutationObserver(() => { const newSrc = media.src || media.currentSrc; if (newSrc && newSrc !== media.dataset.dmzOriginalSrc) { media.dataset.dmzOriginalSrc = newSrc; if (media.dataset.dmzNeutralized === 'passive') { if (!PageScanner.isPreRollAdByPathFeatures(newSrc, this.context)) { this.neutralizeOriginalPlayer(media, 'active'); } return; } if (media.dataset.dmzAllowSignalGeneration !== 'true') { media.pause(); media.muted = true; } } }); attributeObserver.observe(media, { attributes: true, attributeFilter: ['src'], }); return attributeObserver; } _getBestCandidate(candidates) { return candidates.sort( (a, b) => this._getRecoveryCandidateScore(b) - this._getRecoveryCandidateScore(a) )[0]; } _getRecoveryCandidateScore(candidate) { const url = candidate?.url || ''; const isM3U8 = Utils.isM3U8(url) || (candidate?.m3u8Text && candidate.m3u8Text.includes('#EXTM3U')); return this._getQualityScore(url, isM3U8) + this._getCandidateReliabilityScore(candidate); } async _executeTakeover(dossier) { this._prepareTakeoverExecution(dossier); const { url, requestId } = this._createTakeoverRuntime(dossier); try { const { videoType, videoData } = await this._buildTakeoverPayload(dossier, requestId); if (this.currentRequestId !== requestId) { this.log( `[并发拦截]|🛑|发现更新的播放请求,旧任务(ID:${requestId})已终止。`, CONSTANTS.LOG_TYPES.INFO ); return; } await this.sendPlayCommand(videoType, videoData); } catch (error) { this._handleTakeoverExecutionError(error, requestId, url); } finally { if (this.currentRequestId === requestId) { this._scheduleIntentionalSwitchReset(); } } } _formatDecisionLockKey(url) { if (typeof url !== 'string' || !url) { return 'unknown'; } try { const urlObj = new URL(url, window.location.href); const segments = urlObj.pathname.split('/').filter(Boolean); const tail = segments.slice(-3).join('/'); if (tail) { return tail; } return `${urlObj.hostname}${urlObj.pathname}`.slice(-80); } catch (e) { return url.length > 80 ? url.slice(-80) : url; } } _prepareTakeoverExecution(dossier) { this.currentDecisionHadMediaElement = !!dossier.mediaElement; this.userIntendsToSwitch = false; this.findAllVideosAndAudioInPage().forEach((m) => { const obs = this.neutralizeOriginalPlayer(m); if (obs) { this.activeNeutralizers.add(obs); } }); if (!dossier.sources && dossier.sourceName) { dossier.sources = new Set([dossier.sourceName]); } this.lastCandidatesBackup = new Map(this.takeoverCandidates); this.currentDecisionSourceName = Array.from(dossier.sources).join(' + '); this.currentDecisionM3u8Text = dossier.m3u8Text || null; if (dossier.url && !dossier.url.startsWith('blob:')) { this.lastSuccessfulUrl = dossier.url; const videoKey = this._getNormalizationKey(dossier.url); if (videoKey) { this.globalSeenVideoUrls.add(videoKey); const currentPage = window.location.href.split('#')[0]; if (!this.pageVideoAssociations.has(currentPage)) { this.pageVideoAssociations.set(currentPage, new Set()); } this.pageVideoAssociations.get(currentPage).add(videoKey); } } this.log( `[裁决]|👨‍⚖️|🔒决策锁定:[${this._formatDecisionLockKey(dossier.url)}]|来源:${Array.from(dossier.sources).join(' + ')}。`, CONSTANTS.LOG_TYPES.TAKEOVER_SUCCESS ); } _createTakeoverRuntime(dossier) { const requestId = ++this.currentRequestId; this.context.diagnosticsTool.captureTakeoverEvidence(dossier); return { url: dossier.url, requestId }; } async _buildTakeoverPayload(dossier, requestId) { const { playerManager, diagnosticsTool } = this.context; let { url, m3u8Text } = dossier; diagnosticsTool.resetPlaybackHealth(); diagnosticsTool.resetSlicingReport(); const sourceMeta = this._resolveTakeoverSourceMeta(url, m3u8Text); m3u8Text = sourceMeta.m3u8Text; if (sourceMeta.videoType === 'm3u8') { const videoData = await this._buildM3u8TakeoverData(url, m3u8Text, requestId); const finalKey = this._getNormalizationKey(videoData.finalUrl); if (finalKey) { this.globalSeenVideoUrls.add(finalKey); const currentPage = window.location.href.split('#')[0]; if (!this.pageVideoAssociations.has(currentPage)) { this.pageVideoAssociations.set(currentPage, new Set()); } this.pageVideoAssociations.get(currentPage).add(finalKey); } return { videoType: sourceMeta.videoType, videoData }; } playerManager.currentVideoFormat = Utils.getVideoFormat(url); return { videoType: sourceMeta.videoType, videoData: url }; } _resolveTakeoverSourceMeta(url, m3u8Text) { const hasM3u8Header = m3u8Text && typeof m3u8Text === 'string' && (m3u8Text.includes('#EXTM3U') || m3u8Text.includes('#EXTINF')); const isM3U8 = hasM3u8Header ? true : /\.(mp4|webm|ogg|mov|avi|mkv)(\?|$)/i.test(url) ? false : Utils.isM3U8(url); if (!hasM3u8Header && !isM3U8) { m3u8Text = null; } return { m3u8Text, videoType: isM3U8 ? 'm3u8' : 'normal', }; } async _buildM3u8TakeoverData(url, m3u8Text, requestId) { const check = () => this.currentRequestId === requestId; const cacheKey = this._getNormalizationKey(url); if (!m3u8Text && cacheKey && this.m3u8ProcessedCache.has(cacheKey)) { return this.m3u8ProcessedCache.get(cacheKey); } const text = m3u8Text || (await this.m3u8Processor.fetchText(url, check, requestId)); if (!check()) { throw new Error('Request outdated'); } const textCacheKey = this._getNormalizationKey(url); if (textCacheKey) { this.m3u8SessionCache.set(textCacheKey, text); } const processed = await this.m3u8Processor.process(text, url, requestId, check); if (!check()) { throw new Error('Request outdated'); } if (!processed?.processed) { throw new Error('M3U8处理后内容为空'); } await HlsKeyPrefetchManager.prefetchAndRewriteManifest(this, processed, check, requestId); if (!check()) { throw new Error('Request outdated'); } if (cacheKey) { this.m3u8ProcessedCache.set(cacheKey, processed); } const finalCacheKey = this._getNormalizationKey(processed.finalUrl); if (finalCacheKey) { this.m3u8ProcessedCache.set(finalCacheKey, processed); } return processed; } _handleTakeoverExecutionError(error, requestId, url) { if (this.currentRequestId !== requestId) { this.log( `[并发拦截]|🛡️|[ID:${requestId}] 捕获到过期任务的报错(${error.message}),已拦截,保护当前播放状态。`, CONSTANTS.LOG_TYPES.INFO ); return; } if (!error.message.includes('Request outdated')) { this.log( `[裁决]|👨‍⚖️|[ID:${requestId}]处理视频源失败:${error.message}`, CONSTANTS.LOG_TYPES.TAKEOVER_FAIL ); this.reportPlaybackFailure({ failedUrl: url, reason: error.message }); } } _scheduleIntentionalSwitchReset() { setTimeout(() => { this.isIntentionalSwitch = false; }, 1000); } } class BehavioralFilter extends BaseModule { constructor(context) { super(context, 'BehavioralFilter'); } _isSameAsPageUrl(url) { if (!url || typeof url !== 'string') { return false; } try { const cleanPage = window.location.href.replace(/^(https?:\/\/)?(www\.)?/, '').replace(/\/$/, ''); const cleanUrl = url.replace(/^(https?:\/\/)?(www\.)?/, '').replace(/\/$/, ''); return cleanPage.includes('view_video.php') ? cleanPage === cleanUrl : cleanUrl.includes(cleanPage); } catch (e) { return false; } } _buildDecision(isLegitimate) { return { isLegitimate }; } _block(message) { this.log(message, CONSTANTS.LOG_TYPES.TAKEOVER_ATTEMPT); return this._buildDecision(false); } _allow(message) { this.log(message, CONSTANTS.LOG_TYPES.TAKEOVER_ATTEMPT); return this._buildDecision(true); } _analyzeIframeEmbedPage(sourceName, url) { if (!/JSON|DECRYPTION|ATTR/.test(sourceName)) { return null; } if (!/\/e\/|\/v\//.test(url)) { return null; } const hasVideoExtension = /\.(m3u8|mp4|flv|mkv|webm)(\?|$)/i.test(url); if (hasVideoExtension) { return null; } return this._block(`[门禁]|🚧|⛔️拦截|(确认是Iframe嵌入页)|来源:${sourceName}|URL:${url.slice(-50)}`); } _analyzeAdPathFeatures(candidate) { if (!PageScanner.isPreRollAdByPathFeatures(candidate.url, this.context, candidate)) { return null; } return this._block(`[门禁]|🚧|⛔️拦截|(广告特征)|URL:${candidate.url.slice(-50)}`); } _isTrustedRelativePath(sourceName, url, coreLogic) { return ( coreLogic.userIntendsToSwitch || coreLogic.sandboxManager.hasTriggeredSandbox || sourceName.includes('触发器') || sourceName.includes('记忆') || /\.m3u8/i.test(url) || /\.mp4/i.test(url) || /get_file/i.test(url) ); } _analyzeRelativeCrossOriginPath(sourceName, url, coreLogic) { if (!(sourceName.includes('iFrame') || sourceName.includes('跨域') || sourceName.includes('信使'))) { return null; } if (!(url.startsWith('/') && !url.startsWith('//'))) { return null; } const isTrusted = this._isTrustedRelativePath(sourceName, url, coreLogic); this.log( `[门禁]|🚧|${isTrusted ? '🟢放行' : '⛔️拦截'}|(跨域相对路径${isTrusted ? '-高置信度' : ''})|来源:${sourceName}。`, CONSTANTS.LOG_TYPES.TAKEOVER_ATTEMPT ); return this._buildDecision(isTrusted); } _isListingContext() { const { path, full } = Utils._getNormalizedPath(); const isListing = path.length < 2 || /^\/(index|home|default|main)(\.(html|php|jsp|asp|aspx))?\/?$/i.test(path) || /(^|\/)(s|search|query|results?|so)($|\/|\.|_)/i.test(path) || /(^|\/)(auth|login|signin|signup|register|account|dashboard|console|member|profile|settings|my|user)($|\/|\.|_)/i.test( path ) || /^(auth|login|account|sso|id)\./i.test(window.location.hostname) || /(^|\/)(type|vodtype|list|category|tag|channel|column)($|\/|\.|_)/i.test(path); return { path, full, isListing }; } _analyzeListingScene(sourceName) { if (!/JSON|DOM|ATTR|扫描|嵌入式|网络|Net/.test(sourceName)) { return null; } const { full, isListing } = this._isListingContext(); if (!isListing) { return null; } if (/(play|video|watch|detail|view)/i.test(full) || /[\?&](id|vid|v|f)=/.test(full)) { this.log( `[门禁]|🚧|疑似|💭列表页但包含播放特征 [${full}],豁免放行。`, CONSTANTS.LOG_TYPES.TAKEOVER_ATTEMPT ); return null; } return this._block(`[门禁]|🚧|⛔️拦截|(页面场景自动推荐,防止误扰)|来源:${sourceName}。`); } _analyzeHighPriorityM3U8(candidate) { if (!(Utils.isM3U8(candidate.url) || (candidate.m3u8Text && candidate.m3u8Text.includes('#EXTM3U')))) { return null; } return this._allow(`[门禁]|🚧|🟢放行|(高优先级M3U8信号)|来源:${candidate.sourceName}。`); } _analyzeInvalidUrl(url, sourceName) { if (url.startsWith('http') || url.startsWith('//') || url.startsWith('/') || url.startsWith('blob:')) { return null; } return this._block(`[门禁]|🚧|⛔️拦截|(无效的URL片段)|来源:${sourceName}|URL:${url.slice(0, 80)}。`); } _analyzeSamePageUrl(url, sourceName) { if (!this._isSameAsPageUrl(url)) { return null; } return this._block(`[门禁]|🚧|⛔️拦截|(无效的页面URL)|来源:${sourceName}。`); } _analyzeM3u8InterfacePath(url, sourceName) { if (!(url.includes('/m3u8/') && !Utils.isM3U8(url))) { return null; } if ( sourceName.includes('触发器') || sourceName.includes('记忆') || !/\.[a-zA-Z0-9]{2,4}$/.test(url.split('?')[0]) ) { this.log(`[门禁]|🚧|疑似|💭M3U8接口(无后缀/触发器),豁免放行。`, CONSTANTS.LOG_TYPES.TAKEOVER_ATTEMPT); return null; } return this._block(`[门禁]|🚧|⛔️拦截|(M3U8路径下的非M3U8文件,疑似诱饵)|来源:${sourceName}。`); } _analyzePreviewOrSample(url, sourceName) { if (/(freepv|litevideo|sample|preview|trailer|teaser)\//i.test(url)) { return this._block(`[门禁]|🚧|⛔️拦截|(预览或试看视频)|来源:${sourceName}。`); } return null; } _allowDefault(sourceName) { return this._allow(`[门禁]|🚧|🟢放行|来源:${sourceName}。`); } _evaluateRules(candidate, rules) { for (const rule of rules) { const result = rule(); if (result) { return result; } } return null; } async analyze(candidate) { const { coreLogic } = this.context; const { url, sourceName } = candidate; const rules = [ () => this._analyzeIframeEmbedPage(sourceName, url), () => this._analyzeAdPathFeatures(candidate), () => this._analyzeRelativeCrossOriginPath(sourceName, url, coreLogic), () => this._analyzeListingScene(sourceName), () => this._analyzeHighPriorityM3U8(candidate), () => this._analyzeInvalidUrl(url, sourceName), () => this._analyzeSamePageUrl(url, sourceName), () => this._analyzeM3u8InterfacePath(url, sourceName), () => this._analyzePreviewOrSample(url, sourceName), ]; return this._evaluateRules(candidate, rules) || this._allowDefault(sourceName); } } class SPANavigator extends BaseModule { constructor(context) { super(context, 'SPANavigator'); this.lastUrl = window.location.href; } init() { const pushState = history.pushState; const replaceState = history.replaceState; history.pushState = (...args) => { pushState.apply(history, args); this.onUrlChange(); }; history.replaceState = (...args) => { replaceState.apply(history, args); this.onUrlChange(); }; window.addEventListener('popstate', () => this.onUrlChange()); window.addEventListener('hashchange', () => this.onUrlChange()); } _shouldIgnoreHashOnlySpaChange() { const currentUrl = window.location.href; if (currentUrl === this.lastUrl) { return true; } const currentBase = currentUrl.split('#')[0]; const lastBase = this.lastUrl.split('#')[0]; if (currentBase !== lastBase) { return false; } const currentHash = window.location.hash; const lastHash = new URL(this.lastUrl).hash; const isSpaRoute = (h) => /^#[\/\!]/.test(h) || h.includes('?'); if (isSpaRoute(currentHash) || isSpaRoute(lastHash)) { return false; } return true; } _shouldIgnoreActiveNestedSpaChange(playerManager, coreLogic) { return ( window.location.href.startsWith(this.lastUrl) && playerManager.isPlayerActiveOrInitializing && !coreLogic.userIntendsToSwitch ); } _commitSpaUrlChangeLog(wasColdStart = false) { this.log(`URL 变化 (SPA):${this.lastUrl} ->${window.location.href}`, CONSTANTS.LOG_TYPES.NAV); if (this.context?.coreLogic) { this.context.coreLogic.lastSpaNavigationAt = Date.now(); this.context.coreLogic.lastSpaNavigationSignature = `${this.lastUrl.split('#')[0]} -> ${window.location.href.split('#')[0]}`; this.context.coreLogic.lastSpaNavigationWasColdStart = !!wasColdStart; } this.lastUrl = window.location.href; } _prepareSpaCoreState(playerManager, coreLogic) { if (coreLogic.lastSuccessfulUrl) { coreLogic.staleVideoUrl = coreLogic.lastSuccessfulUrl; } coreLogic.lastSuccessfulUrl = null; playerManager.cleanup(); coreLogic.sandboxManager.cleanup(); coreLogic.activeNeutralizers.forEach((obs) => obs.disconnect()); coreLogic.findAllVideosAndAudioInPage().forEach((media) => { delete media.dataset.dmzOriginalSrc; if (media.src) { media.removeAttribute('src'); } }); } _enterSpaSleepMode(domScanner, coreLogic, noiseReason) { this.log(`[NAV] 新页面评分过低[${noiseReason}],保持/进入休眠模式。`, CONSTANTS.LOG_TYPES.NAV); domScanner.isStopped = true; coreLogic.isSystemHalted = true; domScanner.stop(); } _keepFreshSpaCandidates(source, target, staleVideoUrl) { source.forEach((v, k) => { if (!staleVideoUrl || !v.url.includes(staleVideoUrl.split('?')[0])) { target.set(k, v); } }); } _collectSurvivedSpaCandidates(coreLogic) { const survivedCandidates = new Map(); const survivedPassive = new Map(); this._keepFreshSpaCandidates(coreLogic.takeoverCandidates, survivedCandidates, coreLogic.staleVideoUrl); this._keepFreshSpaCandidates(coreLogic.passiveCandidates, survivedPassive, coreLogic.staleVideoUrl); return { survivedCandidates, survivedPassive }; } _resetSpaCoreLogic(coreLogic, survivedCandidates, survivedPassive) { Object.assign(coreLogic, { decisionMade: false, decisionInProgress: false, takeoverCandidates: survivedCandidates, lastCandidatesBackup: new Map(), currentRequestId: 0, navigationRequestId: (coreLogic.navigationRequestId || 0) + 1, activeNeutralizers: new Set(), backupBufferTimer: (coreLogic.backupBufferTimer && clearTimeout(coreLogic.backupBufferTimer), null), isBufferingBackupCandidates: false, bufferedBackupCandidates: new Map(), passiveCandidates: survivedPassive, m3u8SessionCache: new Map(), m3u8FetchInflight: new Map(), m3u8ProcessedCache: new Map(), failedCandidateKeys: new Set(), failedRiskyM3u8Patterns: new Set(), pendingRecoveryTimer: (coreLogic.pendingRecoveryTimer && clearTimeout(coreLogic.pendingRecoveryTimer), null), }); if (this.context?.playerManager) { this.context.playerManager.isPlayerActiveOrInitializing = false; } if (this.context?.iframeMonitor?.reset) { this.context.iframeMonitor.reset(); } } _restartSpaRuntime(domScanner, coreLogic) { coreLogic.sandboxManager.hasTriggeredSandbox = false; domScanner.isStopped = false; domScanner.init(); ManagedModules.enableConfigured(this.context); setTimeout(() => { if (!coreLogic.isSystemHalted) { PageScanner.scanForFlashvars(this.context); PageScanner.scanForEmbeddedData(this.context); } }, 800); coreLogic.sandboxManager.startCountdown(); } onUrlChange() { this._handleUrlChangeFrame(); } _handleUrlChangeFrame() { if (this._shouldIgnoreHashOnlySpaChange()) { return; } const { playerManager, coreLogic, domScanner } = this.context; if (this._shouldIgnoreActiveNestedSpaChange(playerManager, coreLogic)) { this.lastUrl = window.location.href; return; } const wasColdStart = !!(coreLogic.isSystemHalted || domScanner.isStopped || !playerManager.isPlayerActiveOrInitializing); this._commitSpaUrlChangeLog(wasColdStart); this._prepareSpaCoreState(playerManager, coreLogic); const sleepEval = PageSleepModel.evaluate(this.context); if (sleepEval.isSleeping) { this._enterSpaSleepMode(domScanner, coreLogic, sleepEval.reason); return; } this._reactivateSpaRuntime(domScanner, coreLogic); } _reactivateSpaRuntime(domScanner, coreLogic) { this.log(`[NAV] 新页面评分达标,激活/重置系统!`, CONSTANTS.LOG_TYPES.NAV); coreLogic.isSystemHalted = false; const { survivedCandidates, survivedPassive } = this._collectSurvivedSpaCandidates(coreLogic); this._resetSpaCoreLogic(coreLogic, survivedCandidates, survivedPassive); this._restartSpaRuntime(domScanner, coreLogic); const passiveArr = Array.from(coreLogic.passiveCandidates.values()).reverse(); const bestPassive = passiveArr.find((c) => c.m3u8Text || Utils.isM3U8(c.url)) || passiveArr[0]; if (bestPassive) { this.log(`[NAV] 发现页面跳转前缓存的潜伏信号,自动激活!`, CONSTANTS.LOG_TYPES.NAV); coreLogic.passiveCandidates.clear(); setTimeout(() => coreLogic.addTakeoverCandidate(bestPassive), 200); } const pendingCandidates = SpaCandidateBuffer.drain(coreLogic); if (pendingCandidates.length > 0) { pendingCandidates.forEach((candidate, index) => { setTimeout(() => coreLogic.addTakeoverCandidate(candidate), 100 + index * 80); }); } } } class DOMScanner extends BaseModule { constructor(context) { super(context, 'DOMScanner'); this.observer = null; this.heartbeatTimer = null; this.isStopped = false; this.dirtyRoots = new Set(); this.pendingFullScan = true; this.boundScanPage = Utils.debounce(this.scanPage.bind(this), 200); this.boundHandleScroll = Utils.debounce(this.handleScroll.bind(this), 800); this.boundVisibilityChange = () => { if (document.hidden) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } else if (!this.heartbeatTimer && !this.isStopped) { this._startHeartbeatTimer(); this.requestScan(true); } }; } init() { if (this._recoverFromStoppedOrInitializedState()) { return; } if (this._shouldBlockScannerForCurrentPage()) { this.stop(); this.isStopped = true; return; } this._activateScanner(); } _recoverFromStoppedOrInitializedState() { if (this.observer) { this.isStopped = false; this.requestScan(true); return true; } if (this.isStopped) { this.stop(); this.isStopped = false; } return false; } _shouldBlockScannerForCurrentPage() { return /(captcha|auth|login|signin|widget|comment|plugin|share|button|tracker|analytics|pixel|sync|oauth|verify|challenge|static|assets|\/ads?\/|disqus)/i.test( window.location.href ); } _activateScanner() { const now = Date.now(); if (!this._lastActivateLogAt || now - this._lastActivateLogAt > 4500) { this.log('DOM扫描器已激活,开始搜索视频源。', CONSTANTS.LOG_TYPES.SCAN); this._lastActivateLogAt = now; } this.isStopped = false; this.requestScan(true); this._startMutationObserver(); this._bindScannerEvents(); this._startHeartbeatTimer(); } _startMutationObserver() { this.observer = new MutationObserver((mutations) => this._handleMutations(mutations)); this.observer.observe(document.documentElement, { childList: true, subtree: true, }); } _handleMutations(mutations) { if (this.isStopped || this.context.coreLogic.isSystemHalted) { return; } this.context.coreLogic._invalidateMediaQueryCache(); let addedNodeCount = 0; let shouldFallbackToFullScan = false; for (const mutation of mutations) { if (mutation.type !== 'childList') { shouldFallbackToFullScan = true; break; } if (!mutation.addedNodes || mutation.addedNodes.length === 0) { continue; } addedNodeCount += mutation.addedNodes.length; if (addedNodeCount > 36 || mutation.addedNodes.length > 12) { shouldFallbackToFullScan = true; break; } for (const node of mutation.addedNodes) { const dirtyRoot = this._resolveDirtyRoot(node); if (!dirtyRoot) { continue; } this.dirtyRoots.add(dirtyRoot); if (this.dirtyRoots.size > 18) { shouldFallbackToFullScan = true; break; } } if (shouldFallbackToFullScan) { break; } } if (shouldFallbackToFullScan) { this.pendingFullScan = true; this.dirtyRoots.clear(); } this.requestScan(); } _resolveDirtyRoot(node) { if (!node || node.nodeType !== Node.ELEMENT_NODE) { return null; } if (/^(VIDEO|AUDIO)$/.test(node.tagName)) { return node; } if (typeof node.querySelector === 'function' && node.querySelector('video, audio')) { return node; } const parent = node.parentElement; if (parent && /^(VIDEO|AUDIO)$/.test(parent.tagName)) { return parent; } return null; } _bindScannerEvents() { window.addEventListener('scroll', this.boundHandleScroll, { passive: true, }); document.addEventListener('visibilitychange', this.boundVisibilityChange); } _startHeartbeatTimer() { if (!this.heartbeatTimer) { this.heartbeatTimer = Utils.startPollingTask( 1500, () => { if (!this.isStopped) { this.pendingFullScan = true; this.requestScan(); } return true; }, 'DOM scanner heartbeat failed:' ); } } stop() { this.isStopped = true; if (this.observer) { this.observer.disconnect(); this.observer = null; } if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } window.removeEventListener('scroll', this.boundHandleScroll); if (this.boundVisibilityChange) { document.removeEventListener('visibilitychange', this.boundVisibilityChange); } } requestScan(forceImmediate = false) { if (this.isStopped || this.context.coreLogic.isSystemHalted) { return; } if (forceImmediate) { this.pendingFullScan = true; this.scanPage(); return; } this.boundScanPage(); } handleScroll() { if (!this.isStopped) { this.pendingFullScan = true; requestAnimationFrame(() => this.requestScan()); } } _collectPendingMedia() { const { coreLogic } = this.context; if (this.pendingFullScan || this.dirtyRoots.size === 0) { return coreLogic.findAllVideosAndAudioInPage(true); } const media = []; const seen = new Set(); for (const root of this.dirtyRoots) { coreLogic.collectMediaFromRoot(root).forEach((item) => { if (!seen.has(item)) { seen.add(item); media.push(item); } }); } return media; } scanPage() { if (this.isStopped || this.context.coreLogic.isSystemHalted) { return; } const pendingMedia = this._collectPendingMedia(); this.pendingFullScan = false; this.dirtyRoots.clear(); pendingMedia.forEach((media) => this.processMediaElement(media)); } async processMediaElement(media) { return DomScannerMediaPipeline.processMediaElement(this, media); } } class IframeMonitor extends BaseModule { constructor(context) { super(context, 'IframeMonitor'); this.observer = null; this.resizeObserver = null; this.processedIframes = new WeakMap(); this.iframeSizes = new WeakMap(); this.iframeHighScores = new WeakMap(); this.boundProcess = this._processIframe.bind(this); } init() { if (!CONSTANTS.IS_TOP_FRAME || this.observer) { return; } this._startObserver(); this._startResizeObserver(); document.querySelectorAll('iframe').forEach((iframe) => { this._processIframe(iframe, false); if (this.resizeObserver) { this.resizeObserver.observe(iframe); } }); } reset() { this.processedIframes = new WeakMap(); this.iframeSizes = new WeakMap(); this.iframeHighScores = new WeakMap(); } _startResizeObserver() { if (typeof ResizeObserver === 'undefined') { return; } this.resizeObserver = new ResizeObserver((entries) => { const { coreLogic, playerManager } = this.context; if (playerManager.isPlayerActiveOrInitializing || coreLogic.decisionInProgress) { return; } for (const entry of entries) { const iframe = entry.target; if (iframe.dmzHardRejected) { continue; } const newWidth = entry.contentRect.width; const newHeight = entry.contentRect.height; const newArea = newWidth * newHeight; const oldSize = this.iframeSizes.get(iframe) || { width: 0, height: 0, area: 0, }; if ( newWidth >= 150 && newHeight >= 80 && (newArea > oldSize.area * 1.3 || newWidth > oldSize.width + 50 || newHeight > oldSize.height + 50) ) { this.iframeSizes.set(iframe, { width: newWidth, height: newHeight, area: newArea, }); if (coreLogic?.sandboxManager) { const progressKind = newWidth >= 560 && newHeight >= 300 ? 'player_expand' : 'significant_resize'; coreLogic.sandboxManager._recordSandboxProgress( progressKind, `${Math.round(newWidth)}x${Math.round(newHeight)}` ); const looksLikeBridgeShell = coreLogic.sandboxManager._isBridgeIframeCandidate(iframe) && !coreLogic.sandboxManager._findNestedPlayableIframe( iframe, coreLogic.sandboxManager.progressStage >= 15 ) && !coreLogic.sandboxManager._extractDirectSandboxCandidateUrl(iframe); if (looksLikeBridgeShell) { this.log( `📈 发现桥接壳 iFrame 膨胀 (${Math.round(newWidth)}x${Math.round(newHeight)}),继续观察内层播放器,不立即升级重评估。`, CONSTANTS.LOG_TYPES.INFO ); coreLogic.sandboxManager.assistBridgeIframe(iframe); continue; } } this.log( `📈 发现 iFrame 尺寸显著膨胀 (${Math.round(newWidth)}x${Math.round(newHeight)}),触发重新评估...`, CONSTANTS.LOG_TYPES.INFO ); this._processIframe(iframe, true); } } }); } _buildIframeSignature(iframe) { const src = iframe?.src || ''; const width = Math.round(iframe?.clientWidth || 0); const height = Math.round(iframe?.clientHeight || 0); const allow = iframe?.getAttribute?.('allow') || ''; return `${src}|${width}x${height}|${allow}`; } _startObserver() { this.observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList') { for (const node of mutation.addedNodes) { if (node.tagName === 'IFRAME') { this._processIframe(node, false); if (this.resizeObserver) { this.resizeObserver.observe(node); } } else if (node.querySelectorAll) { const iframes = node.querySelectorAll('iframe'); if (iframes.length > 0) { iframes.forEach((iframe) => { this._processIframe(iframe, false); if (this.resizeObserver) { this.resizeObserver.observe(iframe); } }); } } } } else if (mutation.type === 'attributes') { if (mutation.target.tagName === 'IFRAME') { this._processIframe(mutation.target, true); } } } }); this.observer.observe(document.body || document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['src'], }); } _processIframe(iframe, force = false) { try { const src = iframe?.src || ''; if (!src || src === 'about:blank') { return; } if (this.context.frameCommunicator?._isDefinitelyNonVideoIframe?.(iframe, src)) { iframe.dmzHardRejected = true; return; } if ( this.context.frameCommunicator?._canPromoteIframeToTarget && !this.context.frameCommunicator._canPromoteIframeToTarget(iframe, src, 'iframe-monitor') ) { return; } if (iframe.id === 'dmz_sandbox_frame') { return; } const signature = this._buildIframeSignature(iframe); if (!force && this.processedIframes.get(iframe) === signature) { return; } this.processedIframes.set(iframe, signature); const { coreLogic, playerManager } = this.context; const sandboxManager = coreLogic?.sandboxManager; if (!coreLogic || !playerManager || !sandboxManager) { return; } if (playerManager.isPlayerActiveOrInitializing || coreLogic.decisionInProgress) { return; } let score = sandboxManager.getIframeScore(iframe); const prevHighScore = this.iframeHighScores.get(iframe) || 0; if (score < prevHighScore) { score = prevHighScore; } else if (score > prevHighScore) { this.iframeHighScores.set(iframe, score); } if (score > 0 && src) { this.log(`👀 评估 iFrame: ${src.slice(-50)} (最高得分:${score})`, CONSTANTS.LOG_TYPES.INFO); } if (score >= 110 && !iframe.dmzSandboxFastTracked) { iframe.dmzSandboxFastTracked = true; if (sandboxManager._isBridgeIframeCandidate(iframe)) { this.log( `🎯 目标得分极高,但识别为桥接型页面,优先激活等待内层播放器!`, CONSTANTS.LOG_TYPES.CORE_EXEC ); sandboxManager.cancelCountdown('桥接型页面优先预热'); sandboxManager.assistBridgeIframe(iframe); } else { this.log(`🎯 目标得分极高,跳过宽限期,并行启动沙箱侦查!`, CONSTANTS.LOG_TYPES.CORE_EXEC); sandboxManager.cancelCountdown('高分目标提权沙箱'); sandboxManager._launchSandboxReload({ targetUrl: iframe.src, logMsg: '高分目标提权沙箱', }); } } if (score >= 100 && force) { const bridgeWithoutEvidence = sandboxManager._isBridgeIframeCandidate(iframe) && !sandboxManager._findNestedPlayableIframe( iframe, sandboxManager.progressStage >= 15 ) && !sandboxManager._extractDirectSandboxCandidateUrl(iframe); if (bridgeWithoutEvidence && sandboxManager._isBridgeCooldownActive(iframe)) { this.log( `🎯 桥接目标仍在冷却且无新证据,跳过重复破壁指令。`, CONSTANTS.LOG_TYPES.INFO ); } else if (!iframe.dmzPokerStarted) { iframe.dmzPokerStarted = true; this.log( `🎯 目标 iFrame 得分突破(${score}),启动连续破壁指令!`, CONSTANTS.LOG_TYPES.CORE_EXEC ); let pokeCount = 0; Utils.startPollingTask( 400, () => { if ( pokeCount++ > 50 || this.context.playerManager.isPlayerActiveOrInitializing || this.context.coreLogic.decisionInProgress ) { return false; } try { iframe.contentWindow.postMessage( { type: CONSTANTS.MESSAGE_TYPES.ACTIVATE_AUTOPLAY }, '*' ); } catch (e) {} return true; }, 'Iframe poke failed:' ); } } const directUrl = sandboxManager._extractDirectSandboxCandidateUrl(iframe); if (directUrl) { this.log(`🎯 从动态 iFrame 提取直连: ${directUrl.slice(-50)}`, CONSTANTS.LOG_TYPES.CORE_EXEC); coreLogic.addTakeoverCandidate({ url: directUrl, sourceName: '动态iFrame(直连提取)', }); sandboxManager.extendCountdown(3000, '提取到直连参数,等待解析'); return; } if (score >= 30 && src.startsWith('http')) { if (!force) { this.log( `[前置侦察] 对新 iFrame 发起后台探测: ${src.slice(-40)}`, CONSTANTS.LOG_TYPES.CORE_EXEC ); } sandboxManager.extendCountdown(4000, '发现潜力iFrame,等待其就绪'); Utils.probeIframeForM3u8(src, this.context, '动态iFrame(后台HTML直搜)'); } } catch (error) { this.log(`[IframeMonitor] 处理iFrame失败: ${error.message}`, CONSTANTS.LOG_TYPES.ERROR); } } stop() { if (this.observer) { this.observer.disconnect(); this.observer = null; } if (this.resizeObserver) { this.resizeObserver.disconnect(); this.resizeObserver = null; } } } class IframeTerminator extends BaseModule { constructor(context) { super(context, 'IframeTerminator'); this.observer = null; this.processedIframes = new WeakSet(); } start() { if (this.observer || !CONSTANTS.IS_TOP_FRAME) { return; } const scan = (node) => { if (node.nodeType === 1) { (node.matches('iframe') ? [node] : node.querySelectorAll('iframe')).forEach((f) => this.terminate(f) ); } }; this.observer = new MutationObserver((ms) => ms.forEach((m) => m.addedNodes.forEach(scan))); this.observer.observe(document.documentElement, { childList: true, subtree: true, }); scan(document.documentElement); } stop() { this.observer?.disconnect(); this.observer = null; } terminate(iframe) { if (this.processedIframes.has(iframe)) { return; } try { if ( iframe.src && (/(^|[/.:-])(disqus|accounts\.google|googleapis|gstatic|recaptcha|facebook|instagram|twitter|x\.com|tiktok|reddit|pinterest|addthis|sharethis|taboola|outbrain|doubleclick|googlesyndication|criteo|hotjar|intercom|zendesk|onesignal)([/.:-]|$)/i.test(iframe.src) ||['/acs/phone/', '/ad/'].some((k) => iframe.src.includes(k))) ) { this.processedIframes.add(iframe); iframe.dataset.dmzOriginalSrc = iframe.src; iframe.src = 'about:blank'; Object.assign(iframe.style, { visibility: 'hidden', border: 'none', width: '0', height: '0', }); } } catch (e) { console.debug('Failed to terminate iframe:', e); } } } class BaseInterceptor extends BaseModule { constructor(name) { super(null, name); this.isActive = false; } enable(context) { this.context = context; if (!this.isActive) { this.activate(context); this.log(`已启用。`); } } disable(context) { if (this.isActive) { this.deactivate(context); this.log(`已禁用。`); } this.context = null; } log(msg, type = CONSTANTS.LOG_TYPES.MODULE) { this.context?.actionLogger.log(`[${this.name}] ${msg}`, type); } } const NetworkInterceptor = new (class extends BaseInterceptor { constructor() { super('NetworkInterceptor'); this.originalXhrOpen = null; this.originalFetch = null; } activate(context) { this.isActive = true; this.originalXhrOpen = XMLHttpRequest.prototype.open; this.originalFetch = window.fetch; this._installXhrHook(context); this._installFetchHook(context); } _isManifestText(text) { return ( typeof text === 'string' && text.length > 10 && text.trim().startsWith('#EXTM3U') && text.includes('#EXTINF') ); } _shouldInspectXhrUrl(urlStr, playerManager) { return typeof urlStr === 'string' && !(playerManager.isInternalRequest && urlStr.startsWith('blob:')); } _formatInterceptLogUrl(urlStr) { try { const url = new URL(urlStr, window.location.href); const parts = url.pathname.split('/').filter(Boolean); const tail = parts.slice(-2).join('/') || url.pathname || urlStr; const search = url.search || ''; if (!search) { return tail; } const compactSearch = search.length > 42 ? `${search.slice(0, 42)}...` : search; return `${tail}${compactSearch}`; } catch (e) { return urlStr.length > 96 ? `...${urlStr.slice(-93)}` : urlStr; } } _bindXhrManifestCapture(xhr, urlStr, context) { const { coreLogic } = context; xhr.addEventListener('readystatechange', () => { if (xhr.readyState === 2) { context.playerManager._prewarmUrl(urlStr); } }); xhr.addEventListener( 'load', () => { if ( xhr.status >= 200 && xhr.status < 400 && xhr.responseText && (Utils.isM3U8(urlStr) || this._isManifestText(xhr.responseText)) ) { context.actionLogger.log( `[NetworkInterceptor] [XHR]${this._formatInterceptLogUrl(urlStr)}`, CONSTANTS.LOG_TYPES.TAKEOVER_ATTEMPT ); coreLogic.addTakeoverCandidate({ url: urlStr, sourceName: CONSTANTS.TAKEOVER_SOURCES.NET_XHR, m3u8Text: xhr.responseText, }); } }, { once: true } ); } _installXhrHook(context) { const { playerManager } = context; const interceptor = this; XMLHttpRequest.prototype.open = function (method, url, ...args) { const urlStr = String(url); if (interceptor._shouldInspectXhrUrl(urlStr, playerManager)) { interceptor._bindXhrManifestCapture(this, urlStr, context); } return interceptor.originalXhrOpen.call(this, method, url, ...args); }; } _getFetchRequestUrl(args) { if (args[0] instanceof Request) { return args[0].url; } return String(args[0]); } _shouldBypassInternalBlob(requestUrl, playerManager) { return playerManager.isInternalRequest && requestUrl.startsWith('blob:'); } _isFetchManifestCandidate(requestUrl, response) { const cType = response.headers.get('content-type') || ''; return ( Utils.isM3U8(requestUrl) || cType.includes('mpegurl') || cType.includes('application/vnd.apple.mpegurl') ); } async _captureFetchManifest(requestUrl, response, context) { try { const clone = response.clone(); const text = await clone.text(); if (this._isManifestText(text)) { context.actionLogger.log( `[NetworkInterceptor] [Fetch]${this._formatInterceptLogUrl(requestUrl)}`, CONSTANTS.LOG_TYPES.TAKEOVER_ATTEMPT ); context.coreLogic.addTakeoverCandidate({ url: requestUrl, sourceName: CONSTANTS.TAKEOVER_SOURCES.NET_FETCH, m3u8Text: text, }); } } catch (e) { console.debug('Failed to read fetch response clone:', e); } } async _maybeCaptureFetchResponse(requestUrl, response, context) { if (!response.ok) { return; } if (!this._isFetchManifestCandidate(requestUrl, response)) { return; } await this._captureFetchManifest(requestUrl, response, context); } _installFetchHook(context) { const { playerManager } = context; const interceptor = this; window.fetch = async function (...args) { const requestUrl = interceptor._getFetchRequestUrl(args); if (interceptor._shouldBypassInternalBlob(requestUrl, playerManager)) { return interceptor.originalFetch.apply(this, args); } const response = await interceptor.originalFetch.apply(this, args); await interceptor._maybeCaptureFetchResponse(requestUrl, response, context); return response; }; } deactivate() { if (this.originalXhrOpen) { XMLHttpRequest.prototype.open = this.originalXhrOpen; } if (this.originalFetch) { window.fetch = this.originalFetch; } this.isActive = false; } })(); const PlayerHooker = new (class extends BaseInterceptor { constructor() { super('PlayerHooker'); this.targets = ['aliplayer', 'DPlayer', 'TCPlayer', 'xgplayer', 'Chimee', 'videojs', 'player', 'jwplayer']; this.originalPlayers = {}; } activate(context) { this.isActive = true; this.targets.forEach((name) => this._hookTarget(name, context)); } _cacheOriginalPlayer(name, player) { if (!this.originalPlayers[name]) { this.originalPlayers[name] = player; this.log(`监测到播放器库 ${name} 加载`, CONSTANTS.LOG_TYPES.HOOK); } } _resolveOriginalPlayer(name, internalValue) { return this.originalPlayers[name] || internalValue; } _buildNoopPlayerProxy() { return new Proxy({}, { get: (t, k) => (k in t ? t[k] : () => {}) }); } _buildHookedPlayerFactory(name, context, getOriginal) { return (...args) => { const src = this._extractSource(args[0]); if (src) { context.coreLogic.addTakeoverCandidate({ url: src, sourceName: CONSTANTS.TAKEOVER_SOURCES.PLAYER_HOOK(name), }); return this._buildNoopPlayerProxy(); } const original = getOriginal(); return original.constructor ? Reflect.construct(original, args) : original(...args); }; } _hookTarget(name, context) { let internalValue; try { Object.defineProperty(window, name, { configurable: true, enumerable: true, set: (player) => { this._cacheOriginalPlayer(name, player); internalValue = player; }, get: () => { const original = this._resolveOriginalPlayer(name, internalValue); if (!original) { return undefined; } return this._buildHookedPlayerFactory(name, context, () => this._resolveOriginalPlayer(name, internalValue) ); }, }); } catch (e) { console.debug(`Failed to hook player ${name}:`, e); } } deactivate() { this.isActive = false; } _extractSource(cfg) { if (!cfg) { return null; } const s = cfg.source || cfg.video?.url || cfg.url || cfg.src || cfg.file; if (typeof s === 'string') { return s; } if (Array.isArray(cfg.sources)) { return cfg.sources.find((o) => typeof o.src === 'string')?.src; } if (Array.isArray(cfg.playlist)) { return cfg.playlist[0]?.file; } return null; } })(); const DecryptionHooker = new (class extends BaseInterceptor { constructor() { super('DecryptionHooker'); this.originalAtob = null; this.originalDecode = null; } activate(context) { this.isActive = true; this.originalAtob = window.atob; window.atob = (...args) => { const res = this.originalAtob.apply(window, args); if (typeof res === 'string' && (Utils.isM3U8(res) || /\.(mp4|flv|webm)(\?|$)/.test(res))) { this.log(`atob 发现链接`, CONSTANTS.LOG_TYPES.DECRYPTION_HOOK); context.coreLogic.addTakeoverCandidate({ url: res, sourceName: CONSTANTS.TAKEOVER_SOURCES.DECRYPTION_HOOK_ATOB, }); } return res; }; if (typeof TextDecoder !== 'undefined') { this.originalDecode = TextDecoder.prototype.decode; TextDecoder.prototype.decode = function (input, options) { const res = DecryptionHooker.originalDecode.call(this, input, options); if (res && res.length > 20 && res.includes('#EXTM3U')) { DecryptionHooker.log(`TextDecoder 发现M3U8`, CONSTANTS.LOG_TYPES.DECRYPTION_HOOK); context.coreLogic.addTakeoverCandidate({ url: 'blob:text_decoder_hook', m3u8Text: res, sourceName: 'TextDecoder劫持', }); } return res; }; } } deactivate() { if (this.originalAtob) { window.atob = this.originalAtob; } if (this.originalDecode) { TextDecoder.prototype.decode = this.originalDecode; } this.isActive = false; } })(); const JsonInspector = new (class extends BaseInterceptor { constructor() { super('JsonInspector'); this.originalParse = null; this.processedUrls = new Set(); this.PATTERNS = { BLOCK: /googleads|doubleclick|syndication|adservice|criteo|taboola|favicon\.ico|blank\.mp4|empty\.mp4|\.gif|\.css|\.js|_TPL_|media=hls4A|analyt|pixel|beacon|stat|\.(jpg|jpeg|png|webp|svg|bmp|tiff|vtt|srt|ass|dfxp)(\?|$)/i, BAD_KEYS: /thumbnail|cover|poster|sprite|mask|icon|logo|avatar|ad_url|vast|vpaid|preroll|postroll|midroll|bumper|subtitle|caption|track|analytics|beacon|log|report|config|setting|preview|trailer|teaser|sample|snippet|intro/i, VIDEO_EXT: /\.(m3u8|mpd|mp4|flv|webm|mkv|avi|mov)(\?|$)/i, GOOD_KEYS: /^(src|url|play_?url|video_?url|stream|hls|dash|file|video|content|media|path|source)$/i, }; } activate(context) { this.isActive = true; this.originalParse = JSON.parse; JSON.parse = (text, reviver) => { const res = this.originalParse.call(JSON, text, reviver); if (!this._shouldInspectParsedText(text)) { return res; } setTimeout(() => this._scan(res, context), 0); return res; }; } _shouldInspectParsedText(text) { return !!( text && text.length >= 50 && /m3u8|mp4|flv|video|play|url|src|http/i.test(text) && !Utils.isHighNoisePage() ); } deactivate() { if (this.originalParse) { JSON.parse = this.originalParse; } this.isActive = false; } _walkArray(items, depth, processedObjects, candidates) { items.slice(0, 50).forEach((item) => this._walkObject(item, depth + 1, processedObjects, candidates)); } _pushJsonCandidate(candidates, key, value, owner) { const score = this._evaluate(key, value, owner); if (score >= 40) { candidates.push({ url: value.trim(), score, jsonContext: owner }); } } _walkObject(obj, depth, processedObjects, candidates) { if (depth > 6 || !obj || typeof obj !== 'object' || processedObjects.has(obj)) { return; } processedObjects.add(obj); if (Array.isArray(obj)) { this._walkArray(obj, depth, processedObjects, candidates); return; } for (const key in obj) { const value = obj[key]; if (typeof value === 'string') { this._pushJsonCandidate(candidates, key, value, obj); } else if (typeof value === 'object') { this._walkObject(value, depth + 1, processedObjects, candidates); } } } _publishCandidates(candidates, context) { candidates .sort((a, b) => b.score - a.score) .slice(0, 3) .forEach((candidate) => { if (!this.processedUrls.has(candidate.url)) { this.processedUrls.add(candidate.url); context.coreLogic.addTakeoverCandidate({ url: candidate.url, sourceName: `${CONSTANTS.TAKEOVER_SOURCES.DECRYPTION_HOOK_JSON} (评分:${candidate.score})`, jsonContext: candidate.jsonContext, }); } }); } _scan(obj, context) { const candidates = []; const processedObjects = new WeakSet(); try { this._walkObject(obj, 0, processedObjects, candidates); } catch (e) { console.debug('JSON walk failed:', e); } if (candidates.length) { this._publishCandidates(candidates, context); } } _evaluate(key, val, parent) { if (val.length < 10 || this.processedUrls.has(val)) { return 0; } if ( !/^(https?:|\/\/|blob:)/i.test(val) || this.PATTERNS.BLOCK.test(val) || this.PATTERNS.BAD_KEYS.test(key) ) { return 0; } let score = 0; const isVid = this.PATTERNS.VIDEO_EXT.test(val); if (isVid) { score += /\.m3u8|\.mpd/i.test(val) ? 80 : /\/m3u8\/|\/hls\//i.test(val) ? 0 : 30; } else { score -= 20; } if (this.PATTERNS.GOOD_KEYS.test(key)) { score += 30; } if (parent.type && /video|mpegURL/i.test(parent.type)) { score += 30; } if (val.includes('?') && isVid) { score += 10; } return score; } })(); const IntersectionObserverHooker = new (class extends BaseInterceptor { constructor() { super('IntersectionObserverHooker'); this.originalIO = null; } activate(context) { this.isActive = true; this.originalIO = window.IntersectionObserver; if (!this.originalIO) return; const OriginalIO = this.originalIO; window.IntersectionObserver = function (callback, options) { const hookedCallback = (entries, observer) => { try { entries.forEach((entry) => { if (!entry.isIntersecting) { Object.defineProperty(entry, 'isIntersecting', { value: true, configurable: true }); Object.defineProperty(entry, 'intersectionRatio', { value: 1, configurable: true }); } }); } catch (e) {} return callback(entries, observer); }; return new OriginalIO(hookedCallback, options); }; window.IntersectionObserver.prototype = OriginalIO.prototype; } deactivate() { if (this.originalIO) { window.IntersectionObserver = this.originalIO; } this.isActive = false; } })(); const SetAttributeHooker = new (class extends BaseInterceptor { constructor() { super('SetAttributeHooker'); this.originalSetAttribute = null; this.descriptors = new Map(); } activate(context) { this.isActive = true; this.originalSetAttribute = Element.prototype.setAttribute; this._installSetAttributeHook(context); this._hookSrcProperty(HTMLMediaElement.prototype, context); this._hookSrcProperty(HTMLSourceElement.prototype, context); } _isProcessableSrcValue(el, val) { return typeof val === 'string' && val.startsWith('http') && el._dmzLastSrc !== val; } _isLinkedAwayFromCurrentPage(el) { const parentLink = el.closest('a'); if (!(parentLink && parentLink.href && !parentLink.href.includes('#'))) { return false; } const currentUrl = window.location.href.split('#')[0].split('?')[0]; return !parentLink.href.includes(currentUrl); } _shouldIgnoreMediaElement(el, val, context) { if (!el.isConnected) { return true; } if (this._isLinkedAwayFromCurrentPage(el)) { return true; } if (el.muted && el.autoplay && !el.controls) { return true; } return PageScanner.isPreRollAdByPathFeatures(val, context); } _buildAttributeCandidate(el, val, type) { return { url: val, sourceName: `${CONSTANTS.TAKEOVER_SOURCES.DECRYPTION_HOOK_ATTR} (${type})`, mediaElement: el.tagName === 'SOURCE' ? el.parentElement : el, }; } _processSrcAssignment(el, val, type, context) { if (!this._isProcessableSrcValue(el, val)) { return; } el._dmzLastSrc = val; if (this._shouldIgnoreMediaElement(el, val, context)) { return; } this.log(`捕获〈${el.tagName}〉Src设置(${type})`, CONSTANTS.LOG_TYPES.DECRYPTION_HOOK); context.coreLogic.addTakeoverCandidate(this._buildAttributeCandidate(el, val, type)); } _isMediaSrcAttribute(el, name) { return /^(VIDEO|AUDIO|SOURCE)$/.test(el.tagName) && name.toLowerCase() === 'src'; } _installSetAttributeHook(context) { Element.prototype.setAttribute = function (name, value) { if (SetAttributeHooker._isMediaSrcAttribute(this, name)) { SetAttributeHooker._processSrcAssignment(this, value, 'Attribute', context); } return SetAttributeHooker.originalSetAttribute.apply(this, arguments); }; } _cacheSrcDescriptor(proto, desc) { this.descriptors.set(proto, desc); } _hookSrcProperty(proto, context) { const desc = Object.getOwnPropertyDescriptor(proto, 'src'); if (desc?.set) { this._cacheSrcDescriptor(proto, desc); Object.defineProperty(proto, 'src', { configurable: true, enumerable: true, get: desc.get, set: function (v) { SetAttributeHooker._processSrcAssignment(this, v, 'Prop', context); desc.set.call(this, v); }, }); } } deactivate() { if (this.originalSetAttribute) { Element.prototype.setAttribute = this.originalSetAttribute; } this.descriptors.forEach((d, p) => Object.defineProperty(p, 'src', d)); this.descriptors.clear(); this.isActive = false; } })(); const SCANNER_RULES = { KEYWORDS: [ { p: /(visual|preview|trailer|teaser|snippet|sample|intro|opening|ending|partner|advert|background|banner|cover|thumb|poster|sprite|animation|loop|web_preview|short_video|biometric|guide|instruction)/i, s: 50, }, { p: /(_|\-)(pv|ad|short)(_|\-|$|\.)/i, s: 40 }, { p: /(demo|try)=\d+/i, s: 40 }, { p: /(limit|start|end)=(?!1\b)(?!3\b)\d{1,7}(?!\d)/i, s: 40 }, { p: /\/ads?([\/\.&\?#]|$)/i, s: 50 }, { p: /advert|preroll|promo|sponsor|vast|vpaid/i, s: 40 }, { p: /\.gif|gif\./i, s: 60 }, { p: /(yugao|pianhua|zixun|huaxu|clip|featurette)/i, s: 80 }, { p: /\/ad(s|vert)?\//i, s: 70 }, { p: /(_|\-)(trailer|teaser|sample|preview)(_|\-|$|\.)/i, s: 60 }, ], PARAMS: [ { p: /^ad_|^utm_|^gclid$/i, s: 30 }, { p: /campaign|creative|impression/i, s: 20 }, { p: /watermark|logo/i, s: 15 }, ], LOW_RES: [ { p: /\/(\d+)x(\d+)\./, max: 230400, s: 30 }, { p: /_(\d{1,3})\.(mp4|ts|flv)/i, max: 240, s: 30 }, { p: /(_|\-)(360p|480p|540p|sd)(_|\-|\.|$)/i, s: 25 }, ], BLOCK_DOMAINS: [/doubleclick\.net|googlesyndication\.com|adservice\.google/i, /vp\.externulls\.com/i], }; class PageScanner { static get RULES() { return SCANNER_RULES; } static log(ctx, msg, type = CONSTANTS.LOG_TYPES.SCAN) { ctx.actionLogger.log(msg, type); } static _findFlashvarsScript() { return Array.from(document.querySelectorAll('script')).find((s) => s.textContent.includes('var flashvars_') ); } static _extractFlashvarsData(script) { const match = script && script.textContent.match(/var\s+flashvars_\d+\s*=\s*({[\s\S]*?});/); return match ? JSON.parse(match[1]) : null; } static _pickBestFlashvarsDefinition(mediaDefinitions) { let best = null; mediaDefinitions.forEach((d) => { if (d.videoUrl && parseInt(d.quality) > (best ? parseInt(best.quality) : -1)) { best = d; } }); return best; } static _publishFlashvarsCandidate(ctx, best) { this.log(ctx, `Flashvars 发现源: ${best.quality}p`, CONSTANTS.LOG_TYPES.TAKEOVER_SUCCESS); ctx.coreLogic.addTakeoverCandidate({ url: best.videoUrl, sourceName: '页面数据扫描(flashvars)', }); } static scanForFlashvars(ctx) { if (Utils.isHighNoisePage()) { return; } try { const data = this._extractFlashvarsData(this._findFlashvarsScript()); if (!data?.mediaDefinitions) { return; } const best = this._pickBestFlashvarsDefinition(data.mediaDefinitions); if (best) { this._publishFlashvarsCandidate(ctx, best); } } catch (e) { console.debug('Failed to scan flashvars:', e); } } static _getEmbeddedDataPatterns() { return [ /(?:'|")\w*(?:url|source|src)(?:'|")\s*:\s*(?:'|")([^'"]+\.(?:mp4|m3u8)(?:[\?#][^'"]*)?)(?:'|")/gi, /['"](https?:\/\/[^'"]+\.(?:mp4|m3u8)(?:[\?#][^'"]*)?)['"]/gi, ]; } static _decodeEmbeddedUrl(url) { return url.includes('\\u') ? url.replace(/\\u([0-9a-fA-F]{4})/g, (_, h) => String.fromCharCode(parseInt(h, 16))).replace(/\\/g, '') : url.replace(/\\/g, ''); } static _getEmbeddedScriptText(script) { return script.textContent.slice(0, 50000); } static _extractEmbeddedUrlFromPattern(pattern, text) { let match; while ((match = pattern.exec(text)) !== null) { return this._decodeEmbeddedUrl(match[1]); } return null; } static _scanScriptAgainstPatterns(script, ctx, patterns) { const text = this._getEmbeddedScriptText(script); if (text.length < 200) { return false; } let found = false; for (const pattern of patterns) { let url; while ((url = this._extractEmbeddedUrlFromPattern(pattern, text)) !== null) { if (!this.isPreRollAdByPathFeatures(url, ctx)) { this.log(ctx, `嵌入式扫描发现源`, CONSTANTS.LOG_TYPES.TAKEOVER_SUCCESS); ctx.coreLogic.addTakeoverCandidate({ url, sourceName: '页面数据扫描(嵌入式)', }); found = true; } } } return found; } static scanForEmbeddedData(ctx) { if (Utils.isHighNoisePage()) { return; } const patterns = this._getEmbeddedDataPatterns(); try { Array.from(document.querySelectorAll('script:not([src])')).forEach((script) => this._scanScriptAgainstPatterns(script, ctx, patterns) ); } catch (e) { console.debug('Failed to scan embedded data:', e); } } static _isDirectPlayableManifest(url) { return url.includes('.m3u8') && !/(preview|trailer|teaser|advert|promo|snippet)/i.test(url); } static _isImmediatePreviewAsset(url) { return /(freepv|litevideo|sample|preview|trailer|teaser|snippet|timeline|thumbnails?|sprite|vtt)/i.test( url ); } static _isAuthContextPath() { const { path } = Utils._getNormalizedPath(); return ( /(^|\/)(auth|login|signin|account|dashboard|member|profile|settings)($|\/|\.|_)/i.test(path) && !/(play|video|watch|detail|view)/i.test(path) ); } static _scoreKeywordRules(url) { let score = 0; SCANNER_RULES.KEYWORDS.forEach((rule) => { if (rule.p.test(url)) { score += rule.s; } }); return score; } static _scoreLongPathSegments(parsedUrl) { let score = 0; parsedUrl.pathname .split('/') .filter(Boolean) .forEach((seg) => { if (seg.length > 50 && /^[a-zA-Z0-9]+$/.test(seg)) { score += 5; } }); return score; } static _scoreTrackedQueryParams(parsedUrl) { let score = 0; parsedUrl.searchParams.forEach((value, key) => { SCANNER_RULES.PARAMS.forEach((rule) => { if (rule.p.test(key)) { score += rule.s; } }); }); return score; } static _scoreLowResolutionRules(url) { let score = 0; SCANNER_RULES.LOW_RES.forEach((rule) => { const match = url.match(rule.p); if (match) { const value = match.length > 2 ? parseInt(match[1]) * parseInt(match[2]) : parseInt(match[1]); if (value < rule.max) { score += rule.s; } } }); return score; } static _applyLowResolutionScore(url, parsedUrl, score) { let nextScore = score; nextScore += this._scoreLowResolutionRules(url); if (parsedUrl.searchParams.size > 8) { nextScore += 10; } nextScore += this._scoreLongPathSegments(parsedUrl); nextScore += this._scoreTrackedQueryParams(parsedUrl); return nextScore; } static _logKeywordAdDetection(ctx, score, url) { this.log( ctx, `URL判定为广告(关键词). Score:${score}. URL:${url.slice(-50)}`, CONSTANTS.LOG_TYPES.SCAN_WARN ); } static _logStructuralAdDetection(ctx, score, url) { this.log(ctx, `URL判定为广告(结构). Score:${score}. URL:${url.slice(-50)}`, CONSTANTS.LOG_TYPES.SCAN_WARN); } static _matchesBlockedDomain(url) { return SCANNER_RULES.BLOCK_DOMAINS.some((rule) => rule.test(url)); } static isPreRollAdByPathFeatures(url, ctx) { if (typeof url !== 'string' || !url) { return false; } if (this._isDirectPlayableManifest(url)) { return false; } if (this._isImmediatePreviewAsset(url)) { return true; } if (this._isAuthContextPath()) { return true; } let score = this._scoreKeywordRules(url); if (score >= 25) { this._logKeywordAdDetection(ctx, score, url); return true; } if (url.includes('ipa=') && /(rst|t)=\d+k/.test(url)) { return true; } if (this._matchesBlockedDomain(url)) { return true; } try { const parsedUrl = new URL(url, window.location.href); score = this._applyLowResolutionScore(url, parsedUrl, score); if (score >= 25) { this._logStructuralAdDetection(ctx, score, url); return true; } } catch (e) { console.debug('Failed to evaluate ad by features:', e); } return false; } } const MANAGED_MODULE_DESCRIPTORS = RefactorCollectionUtils.freezeCollection([ { module: PlayerHooker, configKey: 'enablePlayerTakeover' }, { module: NetworkInterceptor, configKey: 'enableNetworkInterceptor' }, { module: DecryptionHooker, configKey: 'enableDecryptionHook' }, { module: JsonInspector, configKey: 'enableJsonInspector' }, { module: SetAttributeHooker, configKey: 'enableSetAttributeHooker' }, { module: IntersectionObserverHooker, configKey: 'enablePlayerTakeover' }, ]); const ManagedModuleUtils = { forEach(descriptors, handler) { descriptors.forEach(handler); }, isEnabled(config, configKey) { return !!config?.[configKey]; }, applyConfig(descriptors, context, config) { this.forEach(descriptors, ({ module, configKey }) => { if (this.isEnabled(config, configKey)) { module.enable(context); } else { module.disable(context); } }); }, enableConfigured(descriptors, context) { const config = context.settingsManager?.config; this.forEach(descriptors, ({ module, configKey }) => { if (this.isEnabled(config, configKey)) { module.enable(context); } }); }, }; const ManagedModules = { applyConfig(context, config) { ManagedModuleUtils.applyConfig(MANAGED_MODULE_DESCRIPTORS, context, config); }, enableConfigured(context) { ManagedModuleUtils.enableConfigured(MANAGED_MODULE_DESCRIPTORS, context); }, }; const ServiceDescriptorUtils = { instantiate(context, descriptors) { return descriptors.reduce((services, { key, factory }) => { if (RefactorCollectionUtils.hasOwn(services, key)) { return services; } services[key] = factory(context); return services; }, {}); }, assign(context, descriptors) { const services = this.instantiate(context, descriptors); Object.keys(services).forEach((key) => { if (!RefactorCollectionUtils.hasOwn(context, key)) { context[key] = services[key]; } }); return context; }, }; const CORE_SERVICE_DESCRIPTORS = RefactorCollectionUtils.freezeCollection([ { key: 'diagnosticsTool', factory: (context) => new DiagnosticsTool(context) }, { key: 'settingsManager', factory: (context) => new SettingsManager(context) }, { key: 'frameCommunicator', factory: (context) => new FrameCommunicator(context) }, { key: 'styleManager', factory: (context) => new StyleManager(context) }, { key: 'playerManager', factory: (context) => new PlayerManager(context) }, { key: 'coreLogic', factory: (context) => new CoreLogic(context) }, { key: 'spaNavigator', factory: (context) => new SPANavigator(context) }, { key: 'domScanner', factory: (context) => new DOMScanner(context) }, { key: 'iframeTerminator', factory: (context) => new IframeTerminator(context) }, { key: 'iframeMonitor', factory: (context) => new IframeMonitor(context) }, ]); const PANEL_SERVICE_DESCRIPTORS = RefactorCollectionUtils.freezeCollection([ { key: 'settingsUI', factory: (context) => new SettingsUI(context) }, { key: 'infoPanelManager', factory: (context) => new InfoPanelManager(context) }, { key: 'unifiedPanelManager', factory: (context) => new UnifiedPanelManager(context) }, ]); const ContextFactory = { createServiceGroup(context, descriptors) { return ServiceDescriptorUtils.assign(context, descriptors); }, attachCoreServices(context) { return this.createServiceGroup(context, CORE_SERVICE_DESCRIPTORS); }, attachPanelServices(context) { return this.createServiceGroup(context, PANEL_SERVICE_DESCRIPTORS); }, attachAllServices(context) { this.attachCoreServices(context); this.attachPanelServices(context); return context; }, finalize(context) { context.actionLogger.setContext(context); window.diagnosticsTool = context.diagnosticsTool; return context; }, create() { const context = { actionLogger: new ActionLogger() }; this.attachAllServices(context); return this.finalize(context); }, }; const HeuristicBootstrap = { start(context) { let simClickCount = 0; SmartClickModel.ensurePlaybackObservers(document); Utils.startPollingTask( 1500, () => { if (context.coreLogic.isSystemHalted || context.domScanner.isStopped) { return true; } if (SmartClickModel.shouldStopAutomation(context)) { return false; } if (simClickCount++ > 300) { return false; } this.tick(context); return !SmartClickModel.shouldStopAutomation(context); }, 'Heuristic bootstrap failed:' ); }, tick(context) { if (SmartClickModel.shouldStopAutomation(context)) { return; } const isListingPage = this.detectListingPage(); if (this.shouldSkip(isListingPage)) { return; } if (!(context.playerManager.isPlayerActiveOrInitializing || context.coreLogic.decisionInProgress)) { this.attemptVideoAutoplay(isListingPage); } if (SmartClickModel.shouldStopAutomation(context)) { return; } this.triggerPlayTargets(this.findTargets(), context); }, detectListingPage() { const mainVideo = document.querySelector('video'); if (!mainVideo) { return document.querySelectorAll('a > img').length > 8; } const rect = mainVideo.getBoundingClientRect(); return rect.width < 150 || rect.height < 100; }, shouldSkip(isListingPage) { if (!isListingPage) { return false; } const pageSignal = window.location.href + document.title; return !/(movie|video|play|watch|vod|film|anime|drama|episode|scene|jav|porn)/i.test(pageSignal); }, attemptVideoAutoplay(isListingPage) { document.querySelectorAll('video').forEach((video) => { if (video.id === CONSTANTS.IDS.PLAYER) { return; } if (video.dataset.dmzNeutralized) { return; } if (video.paused && !isListingPage) { try { video.muted = true; video.play().catch(() => {}); } catch (e) { console.debug('Heuristic playback failed:', e); } } }); }, findTargets() { const staticTargets = Utils.queryFirstVisiblePerSelector([ '.fp-ui', '.jw-video', '.vjs-big-play-button', '.plyr__control--overlaid', '.art-control-play', '.dplayer-mobile-play', '.play-button', 'div[class*="play"]', 'button[class*="play"]', '.shadow-md.bg-black.rounded-md.overflow-hidden > div > div > div', ]); const dynamicTargets = Utils.discoverDynamicPlayButtons(); const fingerprintSeen = new Set(); return Array.from(new Set([...staticTargets, ...dynamicTargets])) .map((target) => SmartClickModel.resolveClickTarget(target)) .filter((target) => { if (!target) { return false; } const fp = SmartClickModel.getElementFingerprint(target); if (fp && fingerprintSeen.has(fp)) { return false; } if (fp) { fingerprintSeen.add(fp); } return true; }) .filter((target) => SmartClickModel.isHighConfidenceTarget(target, { allowVideoElement: false, }) ) .sort( (a, b) => SmartClickModel.getCandidateScore(b, { allowVideoElement: false }) - SmartClickModel.getCandidateScore(a, { allowVideoElement: false }) ); }, triggerPlayTargets(targets, context) { for (const target of targets) { if (this.shouldSkipTarget(target)) { continue; } if (SmartClickModel.shouldStopAutomation(context) || SmartClickModel.isInSettleWindow()) { break; } if (!SmartClickModel.canClickTarget(target)) { continue; } const clicked = this.dispatchClickSequence(target); if (clicked) { Utils.safeNativeClick(target, 'Heuristic native click failed:'); break; } } }, shouldSkipTarget(target) { if (target.closest(CONSTANTS.SELECTORS.DMZ_EXCLUSION)) { return true; } const interactiveNode = target.closest('a, button, [role="button"], li') || target; const attrStr = ( interactiveNode.id + ' ' + interactiveNode.className + ' ' + (interactiveNode.getAttribute('aria-label') || '') + ' ' + (interactiveNode.getAttribute('title') || '') + ' ' + target.className ).toLowerCase(); if (SHARED_PATTERNS.NEGATIVE_UI_KEYWORDS.test(attrStr)) { return true; } if (target.closest('[class*="prev"],[class*="next"]')) { return true; } const parentLink = target.closest('a'); if (parentLink && parentLink.href && !parentLink.href.includes('javascript')) { const containsVideo = parentLink.querySelector('video'); if (!containsVideo) { return true; } } return !SmartClickModel.isHighConfidenceTarget(target, { allowVideoElement: false, }); }, dispatchClickSequence(target) { const resolvedTarget = SmartClickModel.resolveClickTarget(target); if ( !SmartClickModel.isHighConfidenceTarget(resolvedTarget, { allowVideoElement: false, }) || !SmartClickModel.canClickTarget(resolvedTarget) ) { return false; } const rect = resolvedTarget.getBoundingClientRect(); SmartClickModel.recordClick(resolvedTarget); Utils.dispatchMouseEventSequence( resolvedTarget, ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'], { clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2, }, 'Heuristic click dispatch failed:' ); return true; }, }; const RuntimeCoordinator = { runOnDomReady(task) { if (document.readyState === 'loading') { window.addEventListener('DOMContentLoaded', task, { once: true }); } else { task(); } }, initAlwaysOnServices(context) { context.diagnosticsTool.init(); context.iframeTerminator.start(); }, isBlacklistedDomain(context) { const hostname = window.location.hostname; if ( !CONSTANTS.IS_TOP_FRAME && /(disqus\.com|recaptcha|doubleclick\.net|googlesyndication|adservice|taboola|outbrain|criteo|facebook\.com|twitter\.com|instagram\.com|scorecardresearch)/i.test( hostname ) ) { return true; } return context.settingsManager.config.blacklist.some((domain) => { try { return Utils.wildcardToRegex(domain.trim()).test(hostname); } catch (e) { return false; } }); }, handleBlacklistExit(context) { const hostname = window.location.hostname; context.actionLogger.log(`黑名单匹配,已在 ${hostname} 停用。`, CONSTANTS.LOG_TYPES.INIT); context.iframeTerminator.stop(); }, initTopFrameShell(context) { if (!CONSTANTS.IS_TOP_FRAME) { return; } context.unifiedPanelManager.initGesture(); if (typeof GM_registerMenuCommand === 'function') { GM_registerMenuCommand(`📊 ${CONSTANTS.SCRIPT_NAME} 诊断与设置`, () => context.unifiedPanelManager.show() ); } context.spaNavigator.init(); context.coreLogic.initializeUserIntentObserver(); }, resolveSleepState(context) { if (!CONSTANTS.IS_TOP_FRAME) { return false; } const sleepEval = PageSleepModel.evaluate(context); if (!sleepEval.isSleeping) { context.actionLogger.log(`页面评分达标(总分${sleepEval.score}),全速激活!`, CONSTANTS.LOG_TYPES.INIT); return false; } context.actionLogger.log(`页面评分低[${sleepEval.reason}],待机。`, CONSTANTS.LOG_TYPES.INIT); context.domScanner.isStopped = true; context.coreLogic.isSystemHalted = true; return true; }, logActivation() { console.log(`[${CONSTANTS.SCRIPT_NAME}] v${CONSTANTS.SCRIPT_VERSION} 激活 @ ${window.location.href}`); }, initFrameCommunicator(context) { context.frameCommunicator.init(); }, bindManagedModuleConfigSync(context) { EventBus.on('CONFIG_UPDATED', ({ newConfig }) => { ManagedModules.applyConfig(context, newConfig); }); }, createTopFramePageScanTask(context) { return () => { PageScanner.scanForFlashvars(context); PageScanner.scanForEmbeddedData(context); }; }, initTopFrameRuntime(context) { if (!CONSTANTS.IS_TOP_FRAME) { return; } try { context.iframeMonitor.init(); } catch (error) { context.actionLogger.log(`[IframeMonitor] 初始化失败: ${error.message}`, CONSTANTS.LOG_TYPES.ERROR); } context.coreLogic.sandboxManager.startCountdown(); this.runOnDomReady(this.createTopFramePageScanTask(context)); }, initActiveRuntime(context, isSleeping) { ManagedModules.enableConfigured(context); if (!isSleeping) { this.initTopFrameRuntime(context); } }, queryTopFrameIfNeeded(context) { if (CONSTANTS.IS_TOP_FRAME) { return; } if (window.__dmzMainPlayerQueryBootstrapped) { return; } window.__dmzMainPlayerQueryBootstrapped = true; let sentCount = 0; let lastSentAt = 0; let observer = null; const postQuery = () => { if (context.playerManager.isPlayerActiveOrInitializing || context.coreLogic.isSystemHalted) { return; } const now = Date.now(); if (sentCount >= 10 || now - lastSentAt < 300) { return; } lastSentAt = now; sentCount += 1; context.frameCommunicator.postToTopFrame({ type: CONSTANTS.MESSAGE_TYPES.QUERY_IS_MAIN_PLAYER, }); if (sentCount >= 10 && observer) { observer.disconnect(); observer = null; } }; [100, 450, 1100, 2200, 3600, 5500, 8000].forEach((delay) => setTimeout(postQuery, delay)); window.addEventListener('load', () => setTimeout(postQuery, 120), { once: true, }); window.addEventListener('pageshow', postQuery, { once: true }); document.addEventListener('readystatechange', () => { if (document.readyState === 'interactive' || document.readyState === 'complete') { postQuery(); } }); observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (sentCount >= 10) { observer.disconnect(); observer = null; return; } if (mutation.type === 'childList') { for (const node of mutation.addedNodes) { if (!(node instanceof Element)) { continue; } const playerSelectors = 'video, iframe, source, [class*="player" i],[id*="player" i], [class*="play" i], [id*="play" i],[aria-label*="play" i], [aria-label*="播放"], [title*="play" i],[title*="播放"], svg, .vjs-tech, .fp-ui'; if (node.matches(playerSelectors) || node.querySelector(playerSelectors)) { postQuery(); return; } } } if (mutation.type === 'attributes') { const target = mutation.target; if (target instanceof Element && target.matches('iframe, video, source')) { postQuery(); return; } } } }); observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['src', 'poster'], }); setTimeout(() => { if (observer) { observer.disconnect(); observer = null; } }, 8000); }, initDomScanner(context) { this.runOnDomReady(() => context.domScanner.init()); }, installWindowOpenGuard(context) { const origOpen = window.open; window.open = function (...args) { if ( context.playerManager.isPlayerActiveOrInitializing || (typeof args[0] === 'string' && /(ads?|tracker|analytics)/i.test(args[0])) ) { return null; } return origOpen.apply(window, args); }; }, installBlankTargetClickGuard() { document.addEventListener( 'click', (event) => { const target = event.target.closest('a'); if (target?.target === '_blank') { const media = document.querySelector('video, iframe'); if (media && (media.contains(target) || target.contains(media))) { event.preventDefault(); event.stopPropagation(); } } }, true ); }, installRateLimitToastGuard(context) { const shouldSuppressMessage = (value) => { const text = String(value || '').replace(/\s+/g, ''); if (!text || text.length > 40) { return false; } return /操作过于频繁/.test(text) && /(请)?稍[候后].{0,3}再试/.test(text); }; const shouldGuardNow = () => { try { return !!( context.playerManager?.isPlayerActiveOrInitializing || context.coreLogic?.decisionInProgress || context.coreLogic?.sandboxManager?.hasTriggeredSandbox || context.coreLogic?.sandboxManager?.iframeSandbox ); } catch (e) { return false; } }; const logSuppressedMessage = () => { if (context.__dmzRateLimitToastLogged) { return; } context.__dmzRateLimitToastLogged = true; context.actionLogger.log(`[通用兼容]|已启用“频繁操作提示”抑制保护。`, CONSTANTS.LOG_TYPES.INFO); }; const origAlert = window.alert; window.alert = function (...args) { if (shouldGuardNow() && shouldSuppressMessage(args[0])) { logSuppressedMessage(); return; } return origAlert.apply(this, args); }; const suppressElement = (element) => { if (!(element instanceof Element)) { return false; } if (!shouldGuardNow()) { return false; } const text = (element.textContent || '').replace(/\s+/g, ''); if (!shouldSuppressMessage(text)) { return false; } if (element === document.body || element === document.documentElement) { return false; } if (element.children.length > 12) { return false; } logSuppressedMessage(); element.style.setProperty('display', 'none', 'important'); element.style.setProperty('visibility', 'hidden', 'important'); element.style.setProperty('pointer-events', 'none', 'important'); if (typeof element.remove === 'function') { setTimeout(() => { try { element.remove(); } catch (e) {} }, 0); } return true; }; const inspectNode = (node) => { if (!(node instanceof Element)) { return; } if (suppressElement(node)) { return; } node.querySelectorAll?.('*').forEach(suppressElement); }; this.runOnDomReady(() => { inspectNode(document.body); const root = document.documentElement || document.body; if (!root) { return; } const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach(inspectNode); }); }); observer.observe(root, { childList: true, subtree: true, characterData: false, }); }); }, async bootstrap() { const context = ContextFactory.create(); this.initAlwaysOnServices(context); await context.settingsManager.load(); if (this.isBlacklistedDomain(context)) { this.handleBlacklistExit(context); return; } this.initTopFrameShell(context); const isSleeping = this.resolveSleepState(context); this.logActivation(); this.initFrameCommunicator(context); this.bindManagedModuleConfigSync(context); this.initActiveRuntime(context, isSleeping); this.queryTopFrameIfNeeded(context); this.initDomScanner(context); this.installWindowOpenGuard(context); this.installBlankTargetClickGuard(); this.installRateLimitToastGuard(context); HeuristicBootstrap.start(context); }, }; RuntimeCoordinator.bootstrap().catch((e) => console.error(`[${CONSTANTS.SCRIPT_NAME}] Fatal:`, e)); })();