Greasy Fork

Greasy Fork is available in English.

HTML5视频手势

集多家之长的手机浏览器触摸手势插件,让网页播放器像软件一样好用。

当前为 2026-04-20 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         HTML5视频手势
// @namespace    http://tampermonkey.net/
// @version      76.00
// @description  集多家之长的手机浏览器触摸手势插件,让网页播放器像软件一样好用。
// @author       Gemini & 仙
// @license      MIT
// @match        *://*/*
// @grant        GM_addStyle
// @grant        GM_setClipboard
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    const CFG = { 
        minDist: 10, longPress: 500, rateBase: 2.0, senseX: 0.25, senseY: 1.0, 
        progressBarColor: '#FF6699', uiTimeout: 2500, maxScale: 8.0, senseRate: 0.015 
    };

    let seekSec = GM_getValue('gt_seek_sec', 10);
    let seekMode = GM_getValue('gt_seek_mode', 'sec');
    let fpsMode = GM_getValue('gt_fps', 30);
    
    let state = { isScreenLocked: false, pinchMode: 'speed', scale: 1.0, panX: 0, panY: 0 };
    let startX, startY, initVol, initTime, initRate, targetV, targetP, isTouch = false, action = null, lpTimer = null, toastTimer = null, lastTapTime = 0, tapCount = 0, uiTimer = null;
    let activeSeekSide = null, seekAccumulator = 0, seekSessionTimer = null, wasPlayingBeforeSequence = false;
    let initPinchDist = 0, initScale = 1.0, initPanX = 0, initPanY = 0, initSpeed = 1.0, initCenterX = 0, initCenterY = 0, originDx = 0, originDy = 0;

    let blockGestureUntil = 0;
    let enforceStateUntil = 0;
    let enforceTarget = null; 

    window.addEventListener('message', (e) => {
        if (e.data && e.data.type === 'gt_lock_orientation') {
            if (screen.orientation && screen.orientation.lock) screen.orientation.lock(e.data.dir).catch(()=>{});
        } else if (e.data && e.data.type === 'gt_unlock_orientation') {
            if (screen.orientation && screen.orientation.unlock) screen.orientation.unlock();
        }
    });

    const VIP_SELECTORS = '.video-js, .vjs-custom-skin, .player-container, .art-video-player, .xgplayer, .tcplayer, .prism-player, .mui-player, [data-testid="videoComponent"], [data-testid="video-container"], .player-wrapper, .one-video-player_display-w, .one-video-player, vk-video-player, [aria-label="Видео плеер"], [aria-label="Video Player"], .plyr, #html5video, #movie_player, .html5-video-player, .bpx-player-container, .dplayer, .artplayer-app, .MacPlayer, .ckplayer, #playleft, iframe';
    
    const IGNORE_TOUCH_SELECTORS = '.gt-btn-base, .dplayer-controller, .dplayer-bar-wrap, .vjs-control-bar, .art-bottom, .art-controls, .bpx-player-control-wrap, .plyr__controls, .xgplayer-controls, .tcplayer-controls, .prism-controlbar, .mui-player-controls, .wrapper-bottom, [data-testid="player_controls"], [data-testid="progress_bar"], [data-testid="volume-slider"], input[type="range"], .buttons-bar, .progress-bar-container';

    const findUp = (el, selector) => { 
        while (el && el !== document.body && el !== document.documentElement) { 
            if (el.matches && el.matches(selector)) return el; 
            el = el.parentNode || el.host; 
        } 
        return null; 
    };

    const isExcludedZone = (e) => {
        const path = (typeof e.composedPath === 'function') ? e.composedPath() : [];
        if (path.length > 0) {
            for (let el of path) {
                if (el.matches && el.matches(IGNORE_TOUCH_SELECTORS)) return true;
            }
            return false;
        }
        return !!findUp(e.target, IGNORE_TOUCH_SELECTORS);
    };

    // [核心重构1] 绝对边界钳制:严格防止把整个网页或超级包裹壳认作视频容器
    const getValidPlayerRoot = (video) => {
        let current = video.parentNode || (video.getRootNode && video.getRootNode().host) || video;
        let bestMatch = null;
        
        let depth = 0;
        while (current && current !== document.body && current !== document.documentElement && depth < 12) {
            // 熔断机制:如果在向上爬的过程中遇到了整个网页的外壳,立刻终止并回退
            if (current.tagName === 'BODY' || current.tagName === 'HTML' || current.id === 'wrapper' || current.id === 'main') {
                break;
            }
            
            if (current.getBoundingClientRect) {
                const rect = current.getBoundingClientRect();
                // 必须有正常的物理尺寸,且不能大得离谱(比如高度占满了整个网页 > 屏幕高度的1.5倍)
                if (rect.width > 50 && rect.height > 50 && rect.height < (window.innerHeight * 1.5)) {
                    if (current.matches && current.matches(VIP_SELECTORS)) {
                        bestMatch = current; 
                    }
                }
            }
            current = current.parentNode || current.host;
            depth++;
        }
        return bestMatch;
    };

    const findDeepVid = (root) => {
        if (!root) return null;
        let v = root.querySelector ? root.querySelector('video') : null;
        if (v) return v;
        let els = root.querySelectorAll ? root.querySelectorAll('*') : [];
        for (let el of els) {
            if (el.shadowRoot) {
                v = findDeepVid(el.shadowRoot);
                if (v) return v;
            }
        }
        return null;
    };

    const hijackFullscreenAPI = () => {
        const fsMethods = ['requestFullscreen', 'webkitRequestFullscreen', 'mozRequestFullScreen', 'msRequestFullscreen'];
        fsMethods.forEach(method => {
            if (Element.prototype[method]) {
                const originalMethod = Element.prototype[method];
                Element.prototype[method] = function(...args) {
                    let target = this;
                    if (this.tagName === 'VIDEO') {
                        const isNaked = !this.parentNode.classList?.contains('gt-video-wrapper') && !findUp(this.parentNode, VIP_SELECTORS.replace(', iframe', ''));
                        if (isNaked) {
                            target = this.parentNode;
                            target.classList.add('gt-fullscreen-active');
                        }
                    }
                    
                    const promise = originalMethod.apply(target, args);
                    let v = target.tagName === 'VIDEO' ? target : findDeepVid(target);
                    if (!v) v = findDeepVid(document);
                    
                    if (v && screen.orientation && screen.orientation.lock) {
                        const dir = (v.videoWidth === 0 || v.videoWidth >= v.videoHeight) ? 'landscape' : 'portrait';
                        screen.orientation.lock(dir).catch(()=>{
                            try { window.top.postMessage({ type: 'gt_lock_orientation', dir: dir }, '*'); } catch(err){}
                        });
                    }
                    return promise;
                };
            }
        });
    };
    hijackFullscreenAPI();

    const toggleNativeFullscreen = (container, video) => {
        const isFS = !!(document.fullscreenElement || document.webkitFullscreenElement) || container.classList.contains('gt-fullscreen-active');
        const fsBtn = container.querySelector('.art-icon-fullscreenOn, .art-control-fullscreen, .dplayer-full-icon, .plyr__control[data-plyr="fullscreen"], .vjs-fullscreen-control, .xgplayer-fullscreen, .tcplayer-fullscreen-btn, .prism-fullscreen-btn, [aria-label*="全屏"], [title*="全屏"], .fullscreen-btn, .bilibili-player-video-btn-fullscreen');

        if (isFS) {
            if (fsBtn) { try { fsBtn.click(); } catch(e){} }
            
            if (document.exitFullscreen) document.exitFullscreen().catch(()=>{});
            else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
            container.classList.remove('gt-fullscreen-active');
            if (screen.orientation?.unlock) {
                screen.orientation.unlock();
                try { window.top.postMessage({ type: 'gt_unlock_orientation' }, '*'); } catch(e){}
            }
        } else {
            const forceLockLandscape = () => {
                let dir = 'landscape';
                if (video && video.videoWidth > 0 && video.videoHeight > 0) {
                    dir = (video.videoWidth < video.videoHeight) ? 'portrait' : 'landscape';
                }
                if (screen.orientation?.lock) {
                    screen.orientation.lock(dir).catch(()=>{
                        try { window.top.postMessage({ type: 'gt_lock_orientation', dir: dir }, '*'); } catch(err){}
                    });
                } else {
                     try { window.top.postMessage({ type: 'gt_lock_orientation', dir: dir }, '*'); } catch(err){}
                }
            };

            if (fsBtn) { try { fsBtn.click(); } catch(e){} }

            container.classList.add('gt-fullscreen-active');
            const reqFs = container.requestFullscreen || container.webkitRequestFullscreen || container.mozRequestFullScreen;
            
            if (reqFs) {
                const p = reqFs.call(container);
                if (p && p.then) {
                    p.then(() => setTimeout(forceLockLandscape, 150)).catch(()=>{ 
                        if (video.webkitEnterFullscreen) video.webkitEnterFullscreen(); 
                        setTimeout(forceLockLandscape, 150);
                    });
                } else {
                    setTimeout(forceLockLandscape, 150);
                }
            } else if (video.webkitEnterFullscreen) {
                video.webkitEnterFullscreen();
                setTimeout(forceLockLandscape, 150);
            }
        }
    };

    const showMsg = (txt) => {
        let uiLayer = null;
        if (targetP) {
            if (targetP.shadowRoot) uiLayer = targetP.shadowRoot.querySelector('.gt-ui-layer');
            if (!uiLayer) uiLayer = targetP.querySelector('.gt-ui-layer');
        }

        let t;
        if (uiLayer) {
            t = uiLayer.querySelector('.gt-toast');
            if (!t) {
                t = document.createElement('div');
                t.className = 'gt-toast';
                uiLayer.appendChild(t);
            }
            t.style.position = 'absolute';
        } else {
            t = document.getElementById('gt-toast-global');
            if (!t) {
                t = document.createElement('div');
                t.id = 'gt-toast-global';
                t.className = 'gt-toast';
                document.body.appendChild(t);
            }
            t.style.position = 'fixed';
            t.style.zIndex = '2147483647';
        }

        t.innerText = txt;
        t.classList.add('show');
        clearTimeout(toastTimer);
        toastTimer = setTimeout(() => t.classList.remove('show'), 800);
    };

    GM_registerMenuCommand('🔗 拷贝视频源(直链/页面)', () => {
        if (sniffedUrl) { GM_setClipboard(sniffedUrl); showMsg('已复制嗅探流媒体直链'); return; }
        let v = targetV || findDeepVid(document);
        if (!v) { showMsg('未找到视频元素'); return; }
        let src = v.src; if (!src || src.startsWith('blob:')) { const s = v.querySelector('source'); if (s && s.src) src = s.src; }
        if (src && !src.startsWith('blob:')) { GM_setClipboard(src); showMsg('已复制视频直链'); } else { GM_setClipboard(window.location.href); showMsg('已复制网页源地址'); }
    });

    let menuIdResume = null;
    const updateResumeMenu = () => {
        let resume = GM_getValue('gt_resume_playback', true);
        if (menuIdResume !== null) {
            GM_unregisterMenuCommand(menuIdResume);
        }
        const title = resume ? '🟢 [已开启] 从历史位置播放' : '⚪ [已关闭] 从历史位置播放';
        menuIdResume = GM_registerMenuCommand(title, () => {
            GM_setValue('gt_resume_playback', !resume);
            showMsg(`历史位置恢复已${!resume ? '开启' : '关闭'}`);
            updateResumeMenu();
        });
    };
    updateResumeMenu();

    const toggleAdBlockStyles = (enable) => {
        let styleEl = document.getElementById('gt-adblock-style');
        if (enable) {
            if (!styleEl) {
                styleEl = document.createElement('style');
                styleEl.id = 'gt-adblock-style';
                styleEl.textContent = `
                    .dplayer-pause-ad, .dplayer-notice, .dplayer-ad, 
                    .artplayer-plugin-ads, .art-ad, .art-notice, 
                    .play-ad, .pause-ad, .player-ad, .ad-box, .ad-mask, 
                    .pause-html, #pause-html, 
                    [class*="pause-html"], [id*="pause-html"], 
                    [class*="pause-ad"], [id*="pause-ad"], 
                    [class*="play-ad"], [id*="play-ad"], 
                    [class*="ad_box"], [id*="ad_box"], 
                    [class*="ad-wrap"], [id*="ad-wrap"], 
                    [class*="player_ad"], [id*="player_ad"], 
                    [id*="player_pause"], [class*="player_pause"], 
                    [title*="关闭广告"], [title*="广告"], 
                    a[href*="mms0.baidu.com"], img[src*="mms0.baidu.com"] { 
                        display: none !important; pointer-events: none !important; opacity: 0 !important; z-index: -2147483648 !important; width: 0 !important; height: 0 !important; 
                    }
                `;
                (document.head || document.documentElement).appendChild(styleEl);
            }
        } else {
            if (styleEl) styleEl.remove();
        }
    };

    let menuIdAdBlock = null;
    const updateAdBlockMenu = () => {
        let blockAds = GM_getValue('gt_block_ads', true);
        if (menuIdAdBlock !== null) {
            GM_unregisterMenuCommand(menuIdAdBlock);
        }
        const title = blockAds ? '🟢 [已开启] 拦截暂停广告' : '⚪ [已关闭] 拦截暂停广告';
        menuIdAdBlock = GM_registerMenuCommand(title, () => {
            GM_setValue('gt_block_ads', !blockAds);
            toggleAdBlockStyles(!blockAds);
            showMsg(`暂停广告拦截已${!blockAds ? '开启' : '关闭'}`);
            updateAdBlockMenu();
        });
    };
    toggleAdBlockStyles(GM_getValue('gt_block_ads', true));
    updateAdBlockMenu();

    const getVideoKey = (v) => {
        let route = window.location.host + window.location.pathname + window.location.search + window.location.hash;
        if (v.duration && !isNaN(v.duration) && v.duration !== Infinity) {
            route += '_' + Math.floor(v.duration);
        }
        return route;
    };

    const saveProgress = (v) => {
        if (!v || isNaN(v.duration) || v.duration <= 5 || v.currentTime < 2) return;
        const key = getVideoKey(v);
        let history = GM_getValue('gt_video_history', {});
        history[key] = { t: Math.floor(v.currentTime), ts: Date.now() };
        
        const keys = Object.keys(history);
        if (keys.length > 1000) {
            keys.sort((a, b) => history[b].ts - history[a].ts); 
            let newHistory = {};
            for (let i = 0; i < 1000; i++) {
                newHistory[keys[i]] = history[keys[i]];
            }
            history = newHistory;
        }
        GM_setValue('gt_video_history', history);
    };

    let sniffedUrl = '';
    const mediaReg = /\.(m3u8|mpd|mp4|webm|flv)(\?|$)/i;
    const sniff = (url) => { try { if (url && typeof url === 'string' && mediaReg.test(url) && url.startsWith('http')) sniffedUrl = url; } catch(e){} };
    
    const oFetch = window.fetch; 
    window.fetch = function(...args) { 
        try { let u = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].url ? args[0].url : ''); sniff(u); } catch(e){} 
        return oFetch.apply(window, args); 
    };
    const oOpen = XMLHttpRequest.prototype.open; 
    XMLHttpRequest.prototype.open = function(method, url, ...rest) { 
        try { sniff(url); } catch(e){} 
        return oOpen.call(this, method, url, ...rest); 
    };

    const toggleOrientation = () => { 
        if (!screen.orientation) return; 
        const dir = screen.orientation.type.startsWith('landscape') ? 'portrait' : 'landscape';
        if (screen.orientation.lock) {
            screen.orientation.lock(dir).catch(()=>{
                try { window.top.postMessage({ type: 'gt_lock_orientation', dir: dir }, '*'); } catch(e){}
            });
        }
    };

    const UI_COMPONENTS_CSS = `
        .gt-ui-layer { position: absolute !important; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none !important; z-index: 2147483647 !important; }
        .gt-mini-progress { position: absolute; bottom: 0; left: 0; width: 100%; height: 2px; background: rgba(255,255,255,0.2); z-index: 2147483640; pointer-events: none; overflow: hidden; opacity: 0.9; transition: height 0.2s, opacity 0.3s; box-shadow: 0 -1px 1px rgba(0,0,0,0.2); }
        .gt-mini-progress .gt-fill { height: 100%; width: 0%; background: ${CFG.progressBarColor}; transition: width 0.1s linear; box-shadow: 0 0 4px ${CFG.progressBarColor}; }
        :fullscreen .gt-mini-progress, .gt-fullscreen-active .gt-mini-progress { height: 3px !important; }
        .gt-lock-shield { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 2147483645; background: rgba(0,0,0,0); touch-action: none; display: none; pointer-events: auto; }
        :fullscreen .gt-lock-shield, .gt-fullscreen-active .gt-lock-shield { position: fixed !important; top: 0 !important; left: 0 !important; width: 100vw !important; height: 100vh !important; }
        .gt-btn-base { position: absolute; width: 26px; height: 26px; display: flex; align-items: center; justify-content: center; z-index: 2147483647; opacity: 0; pointer-events: none; transition: opacity 0.3s ease, transform 0.15s ease; border: none; background: transparent; color: rgba(255, 255, 255, 0.95); filter: drop-shadow(0px 2px 4px rgba(0,0,0,0.8)); }
        .gt-btn-base * { pointer-events: none !important; }
        .gt-btn-base svg { width: 15px; height: 15px; transition: all 0.2s ease; transform-origin: center center; fill: none !important; stroke: currentColor !important; stroke-width: 2 !important; stroke-linecap: round !important; stroke-linejoin: round !important; }
        .gt-btn-base svg * { fill: none !important; stroke: currentColor !important; stroke-width: 2 !important; stroke-linecap: round !important; stroke-linejoin: round !important; }
        .gt-btn-base span { font-size: 11px; font-weight: 800; font-family: system-ui; letter-spacing: 0.5px; transform-origin: center center; display: inline-block; }
        .gt-ui-visible .gt-btn-base { opacity: 0.65 !important; pointer-events: auto !important; }
        .gt-ui-visible .gt-btn-base.hidden-by-state { display: none !important; pointer-events: none !important; }
        .gt-btn-base:active { opacity: 0.9 !important; color: #fff; }
        
        .gt-rotate-btn { top: 10px; left: 10px; }
        .gt-seek-mode-btn { top: calc(10px + 32px); left: 10px; }
        .gt-seek-val-btn { top: calc(10px + 64px); left: 10px; }
        .gt-fit-btn { top: calc(10px + 96px); left: 10px; }
        .gt-shot-btn { top: calc(10px + 128px); left: 10px; }
        .gt-reset-speed-btn { top: calc(10px + 160px); left: 10px; }
        .gt-pip-btn { top: 10px; right: 10px; }
        .gt-lock-btn { top: calc(50% - 32px); right: 10px; transform: translateY(-50%); }
        .gt-mode-btn { top: calc(50% + 32px); right: 10px; transform: translateY(-50%); }
        .gt-reset-zoom-btn { top: calc(50% + 72px); right: 10px; transform: translateY(-50%); }
        
        :fullscreen .gt-btn-base, .gt-fullscreen-active .gt-btn-base { width: 38px; height: 38px; }
        :fullscreen .gt-ui-visible .gt-btn-base, .gt-fullscreen-active .gt-ui-visible .gt-btn-base { opacity: 0.5 !important; }
        :fullscreen .gt-btn-base svg, .gt-fullscreen-active .gt-btn-base svg { width: 22px; height: 22px; }
        :fullscreen .gt-btn-base span, .gt-fullscreen-active .gt-btn-base span { font-size: 14px; }
        
        :fullscreen .gt-rotate-btn, .gt-fullscreen-active .gt-rotate-btn { top: 20px; left: 20px; }
        :fullscreen .gt-seek-mode-btn, .gt-fullscreen-active .gt-seek-mode-btn { top: calc(20px + 60px); left: 20px; }
        :fullscreen .gt-seek-val-btn, .gt-fullscreen-active .gt-seek-val-btn { top: calc(20px + 120px); left: 20px; }
        :fullscreen .gt-fit-btn, .gt-fullscreen-active .gt-fit-btn { top: calc(20px + 180px); left: 20px; }
        :fullscreen .gt-shot-btn, .gt-fullscreen-active .gt-shot-btn { top: calc(20px + 240px); left: 20px; }
        :fullscreen .gt-reset-speed-btn, .gt-fullscreen-active .gt-reset-speed-btn { top: calc(20px + 300px); left: 20px; }
        :fullscreen .gt-pip-btn, .gt-fullscreen-active .gt-pip-btn { top: 20px; right: 20px; }
        :fullscreen .gt-lock-btn, .gt-fullscreen-active .gt-lock-btn { top: calc(50% - 35px); right: 20px; }
        :fullscreen .gt-mode-btn, .gt-fullscreen-active .gt-mode-btn { top: calc(50% + 35px); right: 20px; }
        :fullscreen .gt-reset-zoom-btn, .gt-fullscreen-active .gt-reset-zoom-btn { top: calc(50% + 85px); right: 20px; }
        
        .gt-seek-msg { position: absolute !important; top: 50% !important; color: rgba(255, 255, 255, 0.95) !important; z-index: 2147483647 !important; pointer-events: none !important; opacity: 0; transition: opacity 0.15s ease-out; display: flex !important; flex-direction: row !important; flex-wrap: nowrap !important; align-items: center !important; justify-content: center !important; gap: 6px !important; font-family: system-ui, -apple-system, sans-serif !important; white-space: nowrap !important; text-shadow: 0 0 10px rgba(0,0,0,0.8), 0 0 4px rgba(0,0,0,0.6), 0 2px 4px rgba(0,0,0,0.5) !important; }
        .gt-seek-msg.left { left: 15%; transform: translateY(-50%); }
        .gt-seek-msg.right { right: 15%; transform: translateY(-50%); }
        .gt-seek-msg.show { opacity: 1; }
        .gt-seek-text { display: block !important; font-size: 15px !important; font-weight: 500 !important; line-height: 1 !important; white-space: nowrap !important; transform-origin: center center !important; will-change: transform; }
        .gt-arrows { display: flex !important; flex-direction: row !important; flex-wrap: nowrap !important; align-items: center !important; justify-content: center !important; font-size: 22px !important; font-weight: 400 !important; line-height: 1 !important; }
        .gt-arrows span { display: block !important; line-height: 1 !important; white-space: nowrap !important; }
        .gt-pop-anim { animation: gt-pop 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
        @keyframes gt-pop { 0% { transform: scale(1); } 40% { transform: scale(1.35); } 100% { transform: scale(1); } }
        .gt-arrow-slide-r { animation: gt-slide-r 0.6s infinite; }
        @keyframes gt-slide-r { 0% { transform: translateX(-4px); opacity: 0; } 40% { opacity: 1; } 100% { transform: translateX(4px); opacity: 0; } }
        .gt-arrow-slide-l { animation: gt-slide-l 0.6s infinite; }
        @keyframes gt-slide-l { 0% { transform: translateX(4px); opacity: 0; } 40% { opacity: 1; } 100% { transform: translateX(-4px); opacity: 0; } }
        
        .gt-toast { position: absolute; top: 10%; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.15); color: #fff; padding: 4px 10px; border-radius: 4px; font: 700 14px system-ui; z-index: 2147483647; pointer-events: none; opacity: 0; transition: opacity 0.2s; text-shadow: 0 0 2px #000; border: 1px solid rgba(255,255,255,0.05); }
        .gt-toast.show { opacity: 1; }
    `;

    GM_addStyle(`
        :fullscreen { background-color: #000 !important; }
        #gt-toast-global { position: fixed; top: 10%; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.15); color: #fff; padding: 4px 10px; border-radius: 4px; font: 700 14px system-ui; z-index: 2147483647; pointer-events: none; opacity: 0; transition: opacity 0.2s; text-shadow: 0 0 2px #000; border: 1px solid rgba(255,255,255,0.05); }
        #gt-toast-global.show { opacity: 1; }
    `);

    const SVG_LOCK = `<svg viewBox="0 0 24 24" width="100%" height="100%"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>`;
    const SVG_UNLOCK = `<svg viewBox="0 0 24 24" width="100%" height="100%"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 9.9-1"></path></svg>`;
    const SVG_SPEED = `<svg viewBox="0 0 24 24" width="100%" height="100%"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>`;
    const SVG_ZOOM = `<svg viewBox="0 0 24 24" width="100%" height="100%"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>`;
    const SVG_RESET_ZOOM = `<svg viewBox="0 0 24 24" width="100%" height="100%"><rect x="3" y="3" width="18" height="18" rx="2" ry="2" stroke-dasharray="4 2"></rect></svg>`;
    const SVG_SEC = `<svg viewBox="0 0 24 24" width="100%" height="100%"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>`;
    const SVG_FRAME = `<svg viewBox="0 0 24 24" width="100%" height="100%"><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect><line x1="7" y1="2" x2="7" y2="22"></line><line x1="17" y1="2" x2="17" y2="22"></line><line x1="2" y1="12" x2="22" y2="12"></line><line x1="2" y1="7" x2="7" y2="7"></line><line x1="2" y1="17" x2="7" y2="17"></line><line x1="17" y1="17" x2="22" y2="17"></line><line x1="17" y1="7" x2="22" y2="7"></line></svg>`;
    const SVG_FIT = `<svg viewBox="0 0 24 24" width="100%" height="100%"><path d="M4 8V4h4m8 0h4v4m0 8v4h-4m-8 0H4v-4"></path></svg>`;
    const SVG_SHOT = `<svg viewBox="0 0 24 24" width="100%" height="100%"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle></svg>`;
    const SVG_PIP = `<svg viewBox="0 0 24 24" width="100%" height="100%"><path d="M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 1.98 2 1.98h18c1.1 0 2-.88 2-1.98V5c0-1.1-.9-2-2-2zm0 16.01H3V4.98h18v14.03z"></path></svg>`;

    const getFS = () => document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement;

    const findAllDeepVids = (root) => {
        let vids = Array.from(root.querySelectorAll ? root.querySelectorAll('video') : []);
        let els = root.querySelectorAll ? root.querySelectorAll('*') : [];
        for (let el of els) {
            if (el.shadowRoot) vids = vids.concat(findAllDeepVids(el.shadowRoot));
        }
        return vids;
    };

    // [核心重构2] 杜绝把整个大网页强行当作容器
    const identify = (e) => {
        let targetVideo = null;
        let rootContainer = null;
        
        let cx = e.clientX, cy = e.clientY;
        if (e.touches && e.touches.length > 0) { cx = e.touches[0].clientX; cy = e.touches[0].clientY; }
        else if (e.changedTouches && e.changedTouches.length > 0) { cx = e.changedTouches[0].clientX; cy = e.changedTouches[0].clientY; }
        
        if (cx === undefined || cy === undefined) return null;

        const allVids = findAllDeepVids(document);
        if (allVids.length === 0) return null;

        allVids.sort((a,b) => {
            const rA = a.getBoundingClientRect();
            const rB = b.getBoundingClientRect();
            return (rB.width * rB.height) - (rA.width * rA.height);
        });

        for (let v of allVids) {
            const r = v.getBoundingClientRect();
            // 必须存在物理宽高的正规尺寸,杜绝假视频盲挂载
            if (r.width > 50 && r.height > 50 && cx >= r.left - 20 && cx <= r.right + 20 && cy >= r.top - 20 && cy <= r.bottom + 20) {
                targetVideo = v;
                break;
            }
            
            let root = getValidPlayerRoot(v);
            if (root) {
                const rr = root.getBoundingClientRect();
                // 彻底拦截跨屏幕误触:拒绝整个网页 Body 以及超过全屏高度 1.5 倍的幽灵层
                if (rr.width > 50 && rr.height > 50 && rr.height < (window.innerHeight * 1.5) && 
                    cx >= rr.left - 20 && cx <= rr.right + 20 && cy >= rr.top - 20 && cy <= rr.bottom + 20) {
                    targetVideo = v;
                    break;
                }
            }
        }
        
        if (!targetVideo) return null;

        if (targetVideo.gtRoot && document.contains(targetVideo.gtRoot)) {
            rootContainer = targetVideo.gtRoot;
        } else {
            rootContainer = getValidPlayerRoot(targetVideo);
            if (rootContainer && rootContainer !== document.body && rootContainer !== document.documentElement) {
                targetVideo.gtRoot = rootContainer;
            }
        }

        if (!rootContainer || rootContainer === document.body || rootContainer === document.documentElement) return null;

        return { root: rootContainer, video: targetVideo, isNaked: (!rootContainer.classList?.contains('gt-video-wrapper') && !findUp(rootContainer, VIP_SELECTORS.replace(', iframe', ''))) };
    };

    let activeUIEl = null;
    const updateUIState = (root, video) => {
        if(!root) return;
        let uiLayer = null;
        if (root.shadowRoot) uiLayer = root.shadowRoot.querySelector('.gt-ui-layer');
        if (!uiLayer) uiLayer = root.querySelector('.gt-ui-layer');
        if(!uiLayer) return;

        const btnLock = uiLayer.querySelector('.gt-lock-btn'), btnMode = uiLayer.querySelector('.gt-mode-btn'), btnRot = uiLayer.querySelector('.gt-rotate-btn'), btnRst = uiLayer.querySelector('.gt-reset-speed-btn'), btnZoomRst = uiLayer.querySelector('.gt-reset-zoom-btn'), btnSeekMode = uiLayer.querySelector('.gt-seek-mode-btn'), btnSeekVal = uiLayer.querySelector('.gt-seek-val-btn'), btnFit = uiLayer.querySelector('.gt-fit-btn'), btnShot = uiLayer.querySelector('.gt-shot-btn'), btnPip = uiLayer.querySelector('.gt-pip-btn'), shield = uiLayer.querySelector('.gt-lock-shield');
        const isFS = !!getFS() || root.classList.contains('gt-fullscreen-active');

        if(btnLock) btnLock.innerHTML = state.isScreenLocked ? SVG_LOCK : SVG_UNLOCK;
        if(btnMode) btnMode.innerHTML = state.pinchMode === 'speed' ? SVG_SPEED : SVG_ZOOM;
        if(btnSeekMode) btnSeekMode.innerHTML = seekMode === 'sec' ? SVG_SEC : SVG_FRAME;
        if(btnSeekVal) btnSeekVal.innerHTML = `<span>${seekMode === 'sec' ? seekSec + 's' : fpsMode + 'f'}</span>`;
        if(btnFit) btnFit.innerHTML = SVG_FIT;
        if(btnShot) btnShot.innerHTML = SVG_SHOT;
        if(btnPip) btnPip.innerHTML = SVG_PIP;
        
        if (state.isScreenLocked) {
            if(shield) shield.style.display = 'block';
            [btnMode, btnRot, btnRst, btnZoomRst, btnSeekMode, btnSeekVal, btnFit, btnShot, btnPip].forEach(b => b?.classList.add('hidden-by-state'));
        } else {
            if(shield) shield.style.display = 'none';
            [btnMode, btnSeekMode, btnSeekVal, btnFit, btnShot, btnPip].forEach(b => b?.classList.remove('hidden-by-state'));
            if(btnRot) { if(isFS) btnRot.classList.remove('hidden-by-state'); else btnRot.classList.add('hidden-by-state'); }
            if(btnRst) { if(video && video.playbackRate !== 1.0) btnRst.classList.remove('hidden-by-state'); else btnRst.classList.add('hidden-by-state'); }
            if(btnZoomRst) { if(state.scale > 1.0) btnZoomRst.classList.remove('hidden-by-state'); else btnZoomRst.classList.add('hidden-by-state'); }
        }
    };

    const wakeUpUI = (root, video) => {
        if (!root) return; 
        let uiLayer = null;
        if (root.shadowRoot) uiLayer = root.shadowRoot.querySelector('.gt-ui-layer');
        if (!uiLayer) uiLayer = root.querySelector('.gt-ui-layer');
        if(!uiLayer) return;

        if (activeUIEl && activeUIEl !== uiLayer) activeUIEl.classList.remove('gt-ui-visible');
        activeUIEl = uiLayer; uiLayer.classList.add('gt-ui-visible'); updateUIState(root, video);
        if (uiTimer) clearTimeout(uiTimer); uiTimer = setTimeout(() => { if (activeUIEl) activeUIEl.classList.remove('gt-ui-visible'); activeUIEl = null; }, CFG.uiTimeout);
    };
    
    const hideUI = (root) => { 
        if (!root) return; 
        let uiLayer = null;
        if (root.shadowRoot) uiLayer = root.shadowRoot.querySelector('.gt-ui-layer');
        if (!uiLayer) uiLayer = root.querySelector('.gt-ui-layer');
        if(uiLayer) { uiLayer.classList.remove('gt-ui-visible'); if (activeUIEl === uiLayer) activeUIEl = null; } 
        if (uiTimer) clearTimeout(uiTimer); 
    };

    const applyTransform = () => {
        if(!targetV) return;
        if (state.scale <= 1.05) { 
            state.scale = 1.0; state.panX = 0; state.panY = 0; 
        } else {
            const mX = (targetV.clientWidth * state.scale - targetV.clientWidth) / 2, mY = (targetV.clientHeight * state.scale - targetV.clientHeight) / 2;
            state.panX = Math.max(-mX, Math.min(mX, state.panX)); state.panY = Math.max(-mY, Math.min(mY, state.panY));
        }
        targetV.style.setProperty('transition', 'none', 'important');
        targetV.style.setProperty('will-change', 'transform', 'important');
        targetV.style.transform = `translate(${state.panX}px, ${state.panY}px) scale(${state.scale})`;
    };

    let pendingRate = null; let lastRateUpdateTime = 0;
    const setRateThrottled = (v, rate) => { pendingRate = rate; const now = Date.now(); if (now - lastRateUpdateTime > 150) { v.playbackRate = pendingRate; lastRateUpdateTime = now; pendingRate = null; } };

    const bindTap = (btn, handler) => {
        let lastExec = 0;
        const wrap = (e) => {
            e.stopPropagation(); e.stopImmediatePropagation(); if (e.type === 'touchend' && e.cancelable) e.preventDefault();
            const now = Date.now(); if (now - lastExec < 300) return; lastExec = now; handler(e); 
            const icon = btn.querySelector('svg') || btn.querySelector('span');
            if (icon) { icon.classList.remove('gt-pop-anim'); void icon.offsetWidth; icon.classList.add('gt-pop-anim'); }
        };
        btn.addEventListener('touchend', wrap, {passive: false, capture: true}); btn.addEventListener('click', wrap, {capture: true});
        ['touchstart', 'mousedown', 'pointerdown', 'contextmenu', 'dblclick'].forEach(evt => { btn.addEventListener(evt, (e)=>{e.stopPropagation(); e.stopImmediatePropagation();}, { capture: true, passive: false }); });
    };

    const buildUI = (root, video) => {
        let uiLayer = document.createElement('div');
        uiLayer.className = 'gt-ui-layer';
        
        const styleEl = document.createElement('style');
        styleEl.textContent = UI_COMPONENTS_CSS;
        uiLayer.appendChild(styleEl);
        
        const bar = document.createElement('div'); bar.className = 'gt-mini-progress'; bar.innerHTML = '<div class="gt-fill"></div>'; uiLayer.appendChild(bar); 
        
        const shield = document.createElement('div'); shield.className = 'gt-lock-shield';
        const blk = (e) => { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); wakeUpUI(root, video); };
        ['click', 'mousedown', 'mouseup', 'pointerdown', 'pointerup', 'dblclick', 'touchstart', 'touchend'].forEach(evt => shield.addEventListener(evt, blk, {capture:true, passive:false}));
        uiLayer.appendChild(shield);

        const rBtn = document.createElement('div'); rBtn.className = 'gt-btn-base gt-rotate-btn'; rBtn.innerHTML = `<svg viewBox="0 0 24 24" width="100%" height="100%"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><polyline points="3 3 3 8 8 8"></polyline></svg>`;
        bindTap(rBtn, () => { toggleOrientation(); wakeUpUI(root, video); }); uiLayer.appendChild(rBtn);
        const smBtn = document.createElement('div'); smBtn.className = 'gt-btn-base gt-seek-mode-btn';
        bindTap(smBtn, () => { seekMode = seekMode === 'sec' ? 'frame' : 'sec'; GM_setValue('gt_seek_mode', seekMode); updateUIState(root, video); showMsg(`切换为: ${seekMode==='sec'?'按秒':'按帧'}`); wakeUpUI(root, video); }); uiLayer.appendChild(smBtn);
        const svBtn = document.createElement('div'); svBtn.className = 'gt-btn-base gt-seek-val-btn';
        bindTap(svBtn, () => { if(seekMode==='sec'){ const a=[10,15,30,1,5]; seekSec=a[(a.indexOf(seekSec)+1)%a.length]; GM_setValue('gt_seek_sec', seekSec); showMsg(`步进: ${seekSec}s`);} else { fpsMode=fpsMode===30?60:30; GM_setValue('gt_fps', fpsMode); showMsg(`帧率: ${fpsMode}`);} updateUIState(root, video); wakeUpUI(root, video); }); uiLayer.appendChild(svBtn);
        
        const fitBtn = document.createElement('div'); fitBtn.className = 'gt-btn-base gt-fit-btn'; fitBtn.innerHTML = SVG_FIT;
        bindTap(fitBtn, () => {
            const fits = ['contain', 'cover', 'fill'];
            const current = video.style.objectFit || 'contain';
            const next = fits[(fits.indexOf(current) + 1) % fits.length];
            video.style.objectFit = next;
            showMsg(next === 'cover' ? '画面: 铺满裁切' : (next === 'fill' ? '画面: 暴力拉伸' : '画面: 原始比例'));
            wakeUpUI(root, video);
        });
        uiLayer.appendChild(fitBtn);

        const shotBtn = document.createElement('div'); shotBtn.className = 'gt-btn-base gt-shot-btn'; shotBtn.innerHTML = SVG_SHOT;
        bindTap(shotBtn, () => {
            try {
                const canvas = document.createElement('canvas');
                canvas.width = video.videoWidth;
                canvas.height = video.videoHeight;
                canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
                const dataURL = canvas.toDataURL('image/png');
                const a = document.createElement('a');
                a.href = dataURL;
                a.download = `Screenshot_${Math.floor(Date.now()/1000)}.png`;
                a.click();
                showMsg('截图已保存');
            } catch (e) {
                showMsg('截图失败: 跨域限制');
            }
            wakeUpUI(root, video);
        });
        uiLayer.appendChild(shotBtn);

        if (document.pictureInPictureEnabled) {
            const pipBtn = document.createElement('div'); pipBtn.className = 'gt-btn-base gt-pip-btn'; pipBtn.innerHTML = SVG_PIP;
            bindTap(pipBtn, () => {
                if (document.pictureInPictureElement) document.exitPictureInPicture();
                else video.requestPictureInPicture().catch(() => showMsg('小窗启动失败'));
                wakeUpUI(root, video);
            });
            uiLayer.appendChild(pipBtn);
        }

        const lBtn = document.createElement('div'); lBtn.className = 'gt-btn-base gt-lock-btn';
        bindTap(lBtn, () => { state.isScreenLocked = !state.isScreenLocked; if(state.isScreenLocked){ const r=video.getBoundingClientRect(); const clk=new MouseEvent('click', {bubbles:true, cancelable:true, clientX:r.left+r.width/2, clientY:r.top+r.height/2}); video.dispatchEvent(clk); } wakeUpUI(root, video); }); uiLayer.appendChild(lBtn);
        const mBtn = document.createElement('div'); mBtn.className = 'gt-btn-base gt-mode-btn';
        bindTap(mBtn, () => { state.pinchMode = state.pinchMode==='speed'?'zoom':'speed'; wakeUpUI(root, video); showMsg(state.pinchMode==='speed'?'双指: 变速':'双指: 缩平移'); }); uiLayer.appendChild(mBtn);
        const rstBtn = document.createElement('div'); rstBtn.className = 'gt-btn-base gt-reset-speed-btn'; rstBtn.innerHTML = `<span>1.0x</span>`;
        bindTap(rstBtn, () => { video.playbackRate = 1.0; showMsg('恢复原速'); wakeUpUI(root, video); }); uiLayer.appendChild(rstBtn);
        const zoomRstBtn = document.createElement('div'); zoomRstBtn.className = 'gt-btn-base gt-reset-zoom-btn'; zoomRstBtn.innerHTML = SVG_RESET_ZOOM;
        bindTap(zoomRstBtn, () => { state.scale = 1.0; state.panX = 0; state.panY = 0; if (video.parentNode && video.parentNode.dataset.gtOverflow) { video.parentNode.style.overflow = video.parentNode.dataset.gtOverflow; } if (video) video.style.transform = `translate(0px, 0px) scale(1)`; showMsg('恢复大小'); wakeUpUI(root, video); }); uiLayer.appendChild(zoomRstBtn);
        
        if (root.shadowRoot) {
            root.shadowRoot.appendChild(uiLayer);
        } else {
            const style = window.getComputedStyle(root);
            if (style.position === 'static') root.style.position = 'relative';
            root.appendChild(uiLayer);
        }
        return uiLayer;
    };

    const checkAndBuildUI = (root, video) => {
        let uiLayer = root.shadowRoot ? root.shadowRoot.querySelector('.gt-ui-layer') : root.querySelector('.gt-ui-layer');
        if (!uiLayer) {
            uiLayer = buildUI(root, video);
        }
        return uiLayer;
    };

    // [核心装甲引擎]
    const setupPlayer = (video) => {
        if (!video) return false;

        // 强行要求包裹壳具备合理的展示面积,如果没有(早产的 0 尺寸层),直接打回,让探针继续等
        const root = getValidPlayerRoot(video);
        if (!root || root === document.body || root === document.documentElement) return false; 

        video.style.setProperty('touch-action', 'none', 'important');
        video.style.setProperty('overscroll-behavior', 'none', 'important');
        video.style.setProperty('transition', 'none', 'important');
        video.style.setProperty('will-change', 'transform', 'important');
        
        root.style.setProperty('touch-action', 'none', 'important');
        root.style.setProperty('overscroll-behavior', 'none', 'important');

        const isNaked = (!root.classList?.contains('gt-video-wrapper') && !findUp(root, VIP_SELECTORS.replace(', iframe', '')));
        if (isNaked) video.setAttribute('controlslist', 'nofullscreen');

        if (!video.dataset.gtSetupDone) {
            video.dataset.gtSetupDone = 'true';
            video.gtRoot = root; // 终身绑定正确的包围盒

            const tryRestore = () => {
                if (GM_getValue('gt_resume_playback', true)) {
                    const key = getVideoKey(video);
                    const history = GM_getValue('gt_video_history', {});
                    const targetT = history[key] ? history[key].t : 0;
                    if (targetT > 3 && targetT < video.duration - 5) {
                        video.currentTime = targetT;
                        showMsg(`已恢复至 ${Math.floor(targetT/60)}:${(targetT%60+'').padStart(2,'0')}`);
                        const guardReset = () => {
                            if (video.currentTime < 2) {
                                video.currentTime = targetT;
                                video.removeEventListener('timeupdate', guardReset);
                            }
                        };
                        video.addEventListener('timeupdate', guardReset);
                        setTimeout(() => video.removeEventListener('timeupdate', guardReset), 2500);
                    }
                }
            };
            
            if (video.readyState >= 1) { setTimeout(tryRestore, 200); } 
            else { video.addEventListener('loadedmetadata', () => setTimeout(tryRestore, 200), {once: true}); }

            let lastSaveTime = 0;
            video.addEventListener('timeupdate', () => {
                if (Math.abs(video.currentTime - lastSaveTime) > 5) {
                    saveProgress(video);
                    lastSaveTime = video.currentTime;
                }
                
                let uiLayer = checkAndBuildUI(root, video); 
                if (uiLayer) {
                    const fill = uiLayer.querySelector('.gt-mini-progress .gt-fill');
                    if (fill && video.duration) fill.style.width = `${(video.currentTime / video.duration) * 100}%`;
                }
            });

            const restoreVol = () => {
                let savedVol = GM_getValue('gt_volume', null);
                let savedMuted = GM_getValue('gt_is_muted', null);
                if (savedVol !== null && !isNaN(savedVol)) video.volume = savedVol;
                if (savedMuted === false && video.muted) video.muted = false;
                video.removeEventListener('play', restoreVol);
                video.removeEventListener('touchstart', restoreVol, {capture: true});
            };
            video.addEventListener('play', restoreVol);
            video.addEventListener('touchstart', restoreVol, {capture: true, passive: true});

            video.addEventListener('volumechange', () => {
                GM_setValue('gt_volume', video.volume);
                GM_setValue('gt_is_muted', video.muted);
            });

            video.addEventListener('pause', () => { if (Date.now() < enforceStateUntil && enforceTarget === 'playing') video.play().catch(()=>{}); });
            video.addEventListener('play', () => { if (Date.now() < enforceStateUntil && enforceTarget === 'paused') video.pause(); });
        }

        checkAndBuildUI(root, video);
        return true;
    };

    // [耐心等候系统] 指数退避重试 (Exponential Backoff) 处理 0x0 早产陷阱
    const trySetupPlayerAsync = (video) => {
        let attempts = 0;
        const maxAttempts = 15;
        const interval = 200; // 每 200ms 量一次尺码,最多等 3 秒

        const checkSizeAndSetup = () => {
            if (setupPlayer(video)) return; // 一旦成功包裹,立即功成身退
            
            attempts++;
            if (attempts < maxAttempts) {
                setTimeout(checkSizeAndSetup, interval);
            }
        };
        
        checkSizeAndSetup();
    };

    // 原生事件唤醒与探测网络
    ['loadstart', 'loadedmetadata', 'canplay', 'play'].forEach(evt => {
        document.addEventListener(evt, (e) => {
            const realTarget = (typeof e.composedPath === 'function' && e.composedPath().length > 0) ? e.composedPath()[0] : e.target;
            if (realTarget && realTarget.tagName === 'VIDEO') {
                if (!realTarget.dataset.gtObserved) {
                    realTarget.dataset.gtObserved = 'true';
                    trySetupPlayerAsync(realTarget);
                }
            }
        }, true);
    });

    const scanStaticVideos = () => {
        const processV = (v) => {
            if (!v.dataset.gtObserved) {
                v.dataset.gtObserved = 'true';
                trySetupPlayerAsync(v);
            }
        };
        document.querySelectorAll('video').forEach(processV);
        document.querySelectorAll(VIP_SELECTORS).forEach(el => {
            if (el.shadowRoot) el.shadowRoot.querySelectorAll('video').forEach(processV);
        });
    };
    document.addEventListener('DOMContentLoaded', scanStaticVideos);
    window.addEventListener('load', scanStaticVideos);

    const getPinchData = (touches) => {
        const dx = touches[0].clientX - touches[1].clientX, dy = touches[0].clientY - touches[1].clientY;
        return { dist: Math.hypot(dx, dy), cx: (touches[0].clientX + touches[1].clientX) / 2, cy: (touches[0].clientY + touches[1].clientY) / 2 };
    };

    const handleAccumulatedSeek = (dir, uiLayer, video) => {
        activeSeekSide = dir; const stepVal = seekMode === 'sec' ? seekSec : (1 / fpsMode); seekAccumulator += stepVal;
        video.currentTime = dir === 'left' ? Math.max(0, video.currentTime - stepVal) : Math.min(video.duration || 0, video.currentTime + stepVal);
        let t = uiLayer.querySelector('#gt-seek-' + dir); if (!t) { t = document.createElement('div'); t.id = 'gt-seek-' + dir; t.className = `gt-seek-msg ${dir}`; uiLayer.appendChild(t); }
        t.innerHTML = dir === 'left' ? `<div class="gt-arrows"><span>‹</span><span class="gt-arrow-slide-l">‹</span></div><span class="gt-seek-text gt-pop-anim">-${seekMode==='sec'?seekAccumulator:Math.round(seekAccumulator*fpsMode)}${seekMode==='sec'?'s':'帧'}</span>` : `<span class="gt-seek-text gt-pop-anim">+${seekMode==='sec'?seekAccumulator:Math.round(seekAccumulator*fpsMode)}${seekMode==='sec'?'s':'帧'}</span><div class="gt-arrows"><span class="gt-arrow-slide-r">›</span><span>›</span></div>`;
        t.classList.add('show'); clearTimeout(seekSessionTimer);
        seekSessionTimer = setTimeout(() => { t.classList.remove('show'); activeSeekSide = null; seekAccumulator = 0; setTimeout(() => { if (t && t.parentNode && !t.classList.contains('show')) t.innerHTML = ''; }, 200); }, 800); 
    };

    const onStart = (e) => {
        if (!getFS()) { document.querySelectorAll('.plyr--fullscreen-active, .jw-flag-fullscreen, .gt-fullscreen-active, .gt-ui-visible').forEach(el => { el.classList.remove('plyr--fullscreen-active', 'jw-flag-fullscreen', 'gt-fullscreen-active', 'gt-ui-visible'); el.style.cssText = ''; }); }
        
        const isEx = isExcludedZone(e);
        const isFS = !!getFS() || (targetP && targetP.classList.contains('gt-fullscreen-active'));
        
        if (state.isScreenLocked && !isEx) {
            if (isFS || (targetP && targetP.contains(e.target))) {
                if (e.cancelable) e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
                if (targetP && targetV) wakeUpUI(targetP, targetV); lastTapTime = Date.now(); return;
            }
        }
        
        if (isEx) { clearTimeout(lpTimer); return; }

        let hit = identify(e); if (!hit || !hit.video) return;
        
        setupPlayer(hit.video);
        
        targetP = hit.root; 
        targetV = hit.video;

        clearTimeout(lpTimer); const now = Date.now();

        const isRapid = (now - lastTapTime < 350);
        if (!isRapid) {
            tapCount = 1;
            wasPlayingBeforeSequence = targetV ? !targetV.paused : false;
            wakeUpUI(targetP, targetV);
        } else {
            tapCount++;
        }
        lastTapTime = now;

        if (e.touches && e.touches.length > 1) {
            if (e.cancelable) e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
            tapCount = 0;
            enforceTarget = wasPlayingBeforeSequence ? 'playing' : 'paused'; enforceStateUntil = now + 800;
            if (enforceTarget === 'playing' && targetV.paused) targetV.play().catch(()=>{});
            else if (enforceTarget === 'paused' && !targetV.paused) targetV.pause();
        }

        if (tapCount >= 2) {
            blockGestureUntil = now + 500; 
            if (e.cancelable) e.preventDefault(); 
            e.stopPropagation(); e.stopImmediatePropagation();
            
            const rRect = targetP.getBoundingClientRect();
            const rClientX = (e.touches ? e.touches[0].clientX : e.clientX);
            const rWidth = rRect.width || window.innerWidth;
            const rLeft = rRect.width ? rRect.left : 0;
            const r = (rClientX - rLeft) / rWidth;
            
            let uiLayer = null;
            if (targetP.shadowRoot) uiLayer = targetP.shadowRoot.querySelector('.gt-ui-layer');
            if (!uiLayer) uiLayer = targetP.querySelector('.gt-ui-layer');
            if (!uiLayer) uiLayer = targetP; 
            
            if (r < 0.3) handleAccumulatedSeek('left', uiLayer, targetV);
            else if (r > 0.7) handleAccumulatedSeek('right', uiLayer, targetV);
            else if (tapCount === 2) toggleNativeFullscreen(targetP, targetV);

            enforceTarget = wasPlayingBeforeSequence ? 'playing' : 'paused'; enforceStateUntil = now + 800;
            if (enforceTarget === 'playing' && targetV.paused) targetV.play().catch(()=>{});
            else if (enforceTarget === 'paused' && !targetV.paused) targetV.pause();

            isTouch = false;
            if (getFS()) hideUI(targetP); 
            return; 
        }

        isTouch = true; action = null; startX = e.touches[0].clientX; startY = e.touches[0].clientY;
        initVol = targetV.volume; initTime = targetV.currentTime; initRate = targetV.playbackRate;

        if (e.touches.length === 2) {
            const p = getPinchData(e.touches); initPinchDist = p.dist; initCenterX = p.cx; initCenterY = p.cy;
            initScale = state.scale; initPanX = state.panX; initPanY = state.panY; initSpeed = targetV.playbackRate;
            const rect = targetV.getBoundingClientRect(); originDx = initCenterX - (rect.left + rect.width/2 - initPanX); originDy = initCenterY - (rect.top + rect.height/2 - initPanY);
            action = 'pinch'; if (getFS()) hideUI(targetP);
        } else if (e.touches.length === 1 && state.scale === 1.0) {
            lpTimer = setTimeout(() => { 
                if (isTouch) { action = 'rate'; targetV.playbackRate = Math.max(0.1, initRate + CFG.rateBase - 1.0); showMsg(`${targetV.playbackRate.toFixed(1)}x`); if (getFS()) hideUI(targetP); } 
            }, CFG.longPress);
        }
    };

    const onMove = (e) => {
        if (state.isScreenLocked) {
            if (!isExcludedZone(e) && (!!getFS() || (targetP && targetP.contains(e.target)))) {
                if (e.cancelable) e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
            } return;
        }

        if (!isTouch || !targetV) return;
        
        if (action === 'pinch' || action === 'rate' || action === 'seek' || action === 'vol' || action === 'bri') {
            if (e.cancelable) e.preventDefault();
            e.stopPropagation(); e.stopImmediatePropagation();
        }

        if (action === 'pinch' && e.touches.length === 2) {
            const p = getPinchData(e.touches);
            if (state.pinchMode === 'zoom') {
                state.scale = Math.max(1.0, Math.min(CFG.maxScale, initScale * (p.dist/initPinchDist)));
                if (state.scale > 1.0) { const ds = state.scale/initScale; state.panX = (p.cx-initCenterX)+initPanX*ds+originDx*(1-ds); state.panY = (p.cy-initCenterY)+initPanY*ds+originDy*(1-ds); }
                else { state.panX = 0; state.panY = 0; } applyTransform();
            } else {
                let s = initSpeed + ((p.dist-initPinchDist) * 0.005);
                let finalSpeed = s;
                if (finalSpeed > 0.95 && finalSpeed < 1.05) finalSpeed = 1.0;
                finalSpeed = Math.max(0.1, Math.min(4.0, finalSpeed));
                setRateThrottled(targetV, finalSpeed); 
                showMsg(finalSpeed === 1.0 ? '1.0x' : `${finalSpeed.toFixed(2)}x`);
            } return;
        }

        if (action === 'pinch' || action === 'pinch_wait') return; 

        const dx = e.touches[0].clientX - startX, dy = startY - e.touches[0].clientY;

        if (action === 'rate') { targetV.playbackRate = Math.max(0.1, Math.min(4.0, initRate + (CFG.rateBase + dx * CFG.senseRate) - 1.0)); showMsg(`${targetV.playbackRate.toFixed(1)}x`); return; }
        
        if (!action) { 
            if (Math.abs(dx) > CFG.minDist || Math.abs(dy) > CFG.minDist) { 
                clearTimeout(lpTimer); action = Math.abs(dx) > Math.abs(dy) ? 'seek' : (startX < innerWidth/2 ? 'bri' : 'vol'); 
                
                enforceTarget = wasPlayingBeforeSequence ? 'playing' : 'paused'; enforceStateUntil = Date.now() + 800;
                if (enforceTarget === 'playing' && targetV.paused) targetV.play().catch(()=>{});
                else if (enforceTarget === 'paused' && !targetV.paused) targetV.pause();
                
                if (getFS()) hideUI(targetP);
            } else return; 
        }

        if (action === 'seek') { targetV.currentTime = Math.max(0, Math.min(targetV.duration||0, initTime + dx * CFG.senseX)); showMsg(`${Math.floor(targetV.currentTime/60)}:${(Math.floor(targetV.currentTime%60)+'').padStart(2,'0')}`); }
        else if (action === 'vol') { targetV.volume = Math.max(0, Math.min(1, initVol + dy/innerHeight * 2 * CFG.senseY)); showMsg(`Vol: ${Math.round(targetV.volume*100)}%`); }
        else if (action === 'bri') { let b = Math.max(0.1, Math.min(2.0, 1 + dy/innerHeight * 2 * CFG.senseY)); targetV.style.filter = `brightness(${b})`; showMsg(`Bri: ${Math.round(b*100)}%`); }
    };

    const onEnd = (e) => {
        const now = Date.now();
        const isEx = isExcludedZone(e);
        
        if (now < blockGestureUntil && !isEx) {
            e.stopPropagation(); e.stopImmediatePropagation();
            if (e.cancelable) e.preventDefault();
            isTouch = false;
            return;
        }

        if (state.isScreenLocked) {
            if (!isEx && (!!getFS() || (targetP && targetP.contains(e.target)))) { e.stopPropagation(); e.stopImmediatePropagation(); }
            isTouch = false; return;
        }
        if (!isTouch) return;
        if (e.touches.length > 0) { if (action === 'pinch') action = 'pinch_wait'; return; }
        
        if (pendingRate !== null && targetV) { targetV.playbackRate = pendingRate; pendingRate = null; }

        clearTimeout(lpTimer); 
        if (action === 'rate' && targetV) { targetV.playbackRate = initRate; showMsg(''); wakeUpUI(targetP, targetV); }
        if ((action === 'pinch' || action === 'pinch_wait' || action === 'pan') && targetV) { wakeUpUI(targetP, targetV); }
        
        if (!action) { if (!activeSeekSide) wakeUpUI(targetP, targetV); }
        else { blockGestureUntil = now + 500; }

        isTouch = false; targetV = null; action = null;
    };

    const pOpt = { passive: false, capture: true }; 
    document.addEventListener('touchstart', onStart, pOpt); document.addEventListener('touchmove', onMove, pOpt); document.addEventListener('touchend', onEnd, pOpt); document.addEventListener('touchcancel', onEnd, pOpt);
    
    ['pointerdown', 'pointerup', 'pointercancel', 'click', 'dblclick'].forEach(evt => {
        document.addEventListener(evt, (e) => {
            const isEx = isExcludedZone(e);

            if (evt === 'dblclick' && !isEx && identify(e)) {
                e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
                return;
            }

            if (Date.now() < blockGestureUntil) {
                if (!isEx) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); }
            } else if (evt === 'click') {
                const isL = state.isScreenLocked && !isEx && ((!!getFS()) || (targetP && targetP.contains(e.target)));
                if (activeSeekSide || isL) { e.stopPropagation(); e.stopImmediatePropagation(); e.preventDefault(); if (isL && targetP && targetV) wakeUpUI(targetP, targetV); }
            }
        }, { capture: true, passive: false });
    });

    ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange'].forEach(evt => {
        document.addEventListener(evt, () => { 
            let fsEl = getFS();
            if (!fsEl) { 
                hideUI(targetP); 
                if (screen.orientation?.unlock) {
                    screen.orientation.unlock();
                    try { window.top.postMessage({ type: 'gt_unlock_orientation' }, '*'); } catch(e){}
                }
            } else { 
                setTimeout(() => {
                    let v = targetV || document.querySelector('video');
                    let root = fsEl;
                    if (root && root.tagName === 'VIDEO') root = root.parentNode;
                    
                    let uiLayer = null;
                    if (targetP && targetP.shadowRoot) uiLayer = targetP.shadowRoot.querySelector('.gt-ui-layer');
                    if (!uiLayer && targetP) uiLayer = targetP.querySelector('.gt-ui-layer');
                    if (!uiLayer) uiLayer = document.querySelector('.gt-ui-layer');
                    
                    if (uiLayer && root && !root.contains(uiLayer)) {
                        const style = window.getComputedStyle(root);
                        if (style.position === 'static') root.style.position = 'relative';
                        root.appendChild(uiLayer);
                        targetP = root; 
                    }

                    wakeUpUI(root, v);
                    if (v) {
                        const dir = (v.videoWidth > 0 && v.videoWidth < v.videoHeight) ? 'portrait' : 'landscape';
                        if (screen.orientation?.lock) {
                            screen.orientation.lock(dir).catch(()=>{
                                try { window.top.postMessage({ type: 'gt_lock_orientation', dir: dir }, '*'); } catch(err){}
                            });
                        } else {
                            try { window.top.postMessage({ type: 'gt_lock_orientation', dir: dir }, '*'); } catch(err){}
                        }
                    }
                }, 200);
            } 
        });
    });
})();