Greasy Fork

来自缓存

Greasy Fork is available in English.

StripView 透视镜 (油猴版 - Debug调试版)

A draggable reveal lens for videos on any webpage (完美无损移植版)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         StripView 透视镜 (油猴版 - Debug调试版)
// @namespace    http://tampermonkey.net/
// @version      8.8.6
// @description  A draggable reveal lens for videos on any webpage (完美无损移植版)
// @author       You
// @match        *://*.youtube.com/*
// @match        *://*.bilibili.com/*
// @match        *://*.bilibili.tv/*
// @match        *://*.twitch.tv/*
// @match        *://*.vimeo.com/*
// @match        *://*.dailymotion.com/*
// @match        *://*.nicovideo.jp/*
// @match        *://*.acfun.cn/*
// @match        *://*.tiktok.com/*
// @match        *://*.douyin.com/*
// @match        *://*.kuaishou.com/*
// @match        *://*.ixigua.com/*
// @match        *://v.qq.com/*
// @match        *://*.iqiyi.com/*
// @match        *://*.youku.com/*
// @match        *://*.x.com/*
// @match        *://*.twitter.com/*
// @match        *://*.reddit.com/*
// @match        *://*.iwara.tv/*
// @match        *://*.spankbang.com/*
// @match        *://*.hanime.tv/*
// @match        *://*.tktube.com/*
// @grant        GM_addStyle
// @connect      sv.acreatorhub.com
// @run-at       document-idle
// ==/UserScript==


(function() {
    'use strict';

    // 防止在各类嵌套广告 iframe 中运行,避免沙箱报错
    if (window.top !== window.self) return;

    // ============================================================
    // 1. 注入 CSS 样式
    // ============================================================
    try {
        GM_addStyle(`
.sv-overlay-video {
  position: absolute !important;
  top: 0 !important; left: 0 !important;
  width: 100% !important; height: 100% !important;
  z-index: 2147483639 !important;
  pointer-events: none !important;
  opacity: 0 !important;
  object-fit: contain !important;
}
.sv-lens-wrapper {
  position: fixed !important;
  z-index: 2147483646 !important;
  user-select: none !important;
  display: none;
}
.sv-lens {
  width: 100% !important;
  height: 100% !important;
  border-radius: 10px !important;
  background: transparent !important;
  overflow: hidden !important;
  cursor: grab !important;
  border: 1.5px solid rgba(255,255,255,0.5) !important;
  box-shadow:
    0 0 0 1px rgba(0,0,0,0.1),
    0 8px 30px rgba(0,0,0,0.2) !important;
  transition: box-shadow 0.2s !important;
  position: relative !important;
  backdrop-filter: blur(var(--sv-blur, 0px)) !important;
}
.sv-lens:hover {
  box-shadow:
    0 0 0 1px rgba(0,0,0,0.15),
    0 12px 40px rgba(0,0,0,0.3),
    0 0 15px rgba(100,180,255,0.08) !important;
}
.sv-lens.sv-grabbing { cursor: grabbing !important; }
.sv-lens-clip {
  position: absolute !important;
  overflow: hidden !important;
  pointer-events: none !important;
}
.sv-lens-clip canvas {
  display: block !important;
  filter: blur(var(--sv-blur, 0px)) !important;
}
.sv-lens-reflection {
  position: absolute !important;
  top: -40% !important; left: -20% !important;
  width: 140% !important; height: 50% !important;
  background: linear-gradient(165deg,
    rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.02) 35%, transparent 55%) !important;
  pointer-events: none !important; border-radius: 10px !important; z-index: 2 !important;
}
.sv-lens-ring {
  position: absolute !important;
  top: -1px !important; left: -1px !important; right: -1px !important; bottom: -1px !important;
  border-radius: 10px !important;
  border: 1px solid transparent !important;
  border-top-color: rgba(100,180,255,0.3) !important;
  animation: sv-scan 4s linear infinite !important;
  pointer-events: none !important; z-index: 3 !important;
}
@keyframes sv-scan { to { transform: rotate(360deg); } }
.sv-lens-corner {
  position: absolute !important; width: 14px !important; height: 14px !important;
  pointer-events: none !important;
  border-color: rgba(255,255,255,0.4) !important;
  border-style: solid !important; border-width: 0 !important; z-index: 4 !important;
}
.sv-tl { top: 5px !important; left: 5px !important; border-top-width: 1.5px !important; border-left-width: 1.5px !important; border-top-left-radius: 3px !important; }
.sv-tr { top: 5px !important; right: 5px !important; border-top-width: 1.5px !important; border-right-width: 1.5px !important; border-top-right-radius: 3px !important; }
.sv-bl { bottom: 5px !important; left: 5px !important; border-bottom-width: 1.5px !important; border-left-width: 1.5px !important; border-bottom-left-radius: 3px !important; }
.sv-br { bottom: 5px !important; right: 5px !important; border-bottom-width: 1.5px !important; border-right-width: 1.5px !important; border-bottom-right-radius: 3px !important; }
.sv-lens-label {
  position: absolute !important; bottom: 6px !important; left: 0 !important; width: 100% !important;
  text-align: center !important;
  font-family: 'Segoe UI', system-ui, sans-serif !important;
  font-size: 8px !important; letter-spacing: 2px !important; text-transform: uppercase !important;
  color: rgba(255,255,255,0.3) !important;
  pointer-events: none !important; text-shadow: 0 1px 3px rgba(0,0,0,0.4) !important;
  z-index: 5 !important;
}
.sv-resize-handle {
  position: absolute !important;
  bottom: -2px !important;
  right: -2px !important;
  width: 18px !important;
  height: 18px !important;
  cursor: nwse-resize !important;
  z-index: 20 !important;
  background: transparent !important;
  border: none !important;
}
.sv-resize-handle::before {
  content: '' !important;
  position: absolute !important;
  bottom: 3px !important;
  right: 3px !important;
  width: 10px !important;
  height: 10px !important;
  border-right: 2px solid rgba(255,255,255,0.35) !important;
  border-bottom: 2px solid rgba(255,255,255,0.35) !important;
  pointer-events: none !important;
}
.sv-panel {
  display: none; 
  position: fixed !important;
  top: 10px !important; right: 10px !important;
  z-index: 2147483647 !important;
  background: rgba(18,18,24,0.95) !important;
  border: 1px solid rgba(255,255,255,0.1) !important;
  border-radius: 10px !important;
  padding: 14px 16px !important;
  font-family: 'Segoe UI', system-ui, sans-serif !important;
  font-size: 12px !important; color: #ccc !important;
  width: 250px !important;
  box-shadow: 0 8px 30px rgba(0,0,0,0.4) !important;
  backdrop-filter: blur(12px) !important;
}
.sv-panel-header {
  font-size: 12px !important; font-weight: 600 !important;
  letter-spacing: 1px !important;
  color: rgba(100,180,255,0.9) !important; margin-bottom: 12px !important;
  display: flex !important; justify-content: space-between !important; align-items: center !important;
  cursor: move !important;
  user-select: none !important;
}
.sv-panel-header-actions {
  display: flex !important; align-items: center !important; gap: 8px !important;
}
.sv-lang-toggle {
  background: rgba(255,255,255,0.1) !important;
  border: 1px solid rgba(255,255,255,0.2) !important;
  border-radius: 4px !important;
  color: rgba(255,255,255,0.8) !important;
  font-size: 10px !important; padding: 2px 6px !important;
  cursor: pointer !important; transition: all 0.2s !important;
}
.sv-lang-toggle:hover { background: rgba(255,255,255,0.2) !important; color: #fff !important; }
.sv-panel-toggle {
  background: none !important; border: none !important;
  color: rgba(255,255,255,0.4) !important; cursor: pointer !important;
  font-size: 16px !important; padding: 0 4px !important; line-height: 1 !important;
}
.sv-panel-toggle:hover { color: #fff !important; }
.sv-panel-body { display: flex !important; flex-direction: column !important; gap: 8px !important; }
.sv-panel-body.sv-collapsed { display: none !important; }
.sv-control-group {
  display: flex !important; flex-direction: column !important; gap: 6px !important; margin-bottom: 4px !important;
}
.sv-control-group label {
  font-size: 10px !important; letter-spacing: 1px !important;
  color: rgba(255,255,255,0.5) !important; display: block !important;
}
.sv-control-group input[type="range"] {
  width: 100% !important; margin: 0 !important;
  accent-color: rgba(100,180,255,0.8) !important;
  cursor: pointer !important;
}
.sv-btn {
  background: rgba(100,180,255,0.15) !important;
  border: 1px solid rgba(100,180,255,0.25) !important;
  border-radius: 6px !important; padding: 6px 12px !important;
  color: rgba(100,180,255,0.9) !important; cursor: pointer !important;
  font-size: 11px !important; font-weight: 500 !important; transition: background 0.15s !important;
  flex: 1 !important; text-align: center !important;
}
.sv-btn:hover { background: rgba(100,180,255,0.25) !important; }
.sv-panel-status {
  margin-top: 4px !important;
  font-size: 11px !important; color: rgba(255,255,255,0.4) !important;
  font-family: 'Consolas','SF Mono',monospace !important; line-height: 1.4 !important;
}
.sv-ok { color: rgba(80,200,120,0.9) !important; }
.sv-err { color: rgba(255,100,100,0.9) !important; }
        `);
    } catch (e) {}

    // ============================================================
    // 2. 执行原始 JS 核心代码
    // ============================================================
    (function () {
      "use strict";
      if (window.__stripViewLoaded) return;
      window.__stripViewLoaded = true;

      const MIN_SIZE = 60, MAX_SIZE = 1200;
      const tracked = [];
      let overlayUrl = "";

      const i18n = {
        en: {
          title: "StripView Panel", blur: "Blur Strength", origVol: "Original Vol", overVol: "Overlay Vol",
          sync: "Force Sync", clear: "Clear", langBtn: "中", searching: "Searching...", found: "Overlay Loaded",
          notFound: "Not Found", cleared: "Overlay Cleared", syncedStart: "Synced to Start", syncedInPlace: "Synced seamlessly", loadFailed: "Load Failed"
        },
        zh: {
          title: "StripView 面板", blur: "毛玻璃强度", origVol: "原视频音量", overVol: "透视视频音量",
          sync: "强制重新对齐", clear: "清除", langBtn: "EN", searching: "等待解析...", found: "已解析并加载",
          notFound: "未找到透视视频", cleared: "已清除透视视频", syncedStart: "已重新对齐到开头", syncedInPlace: "已无缝同步", loadFailed: "视频加载失败"
        }
      };
      let currentLang = navigator.language.toLowerCase().startsWith('zh') ? 'zh' : 'en';

      function buildElement(tag, className, textContent) {
          const el = document.createElement(tag);
          if (className) el.className = className;
          if (textContent) el.textContent = textContent;
          return el;
      }

      function buildSlider(className, min, max, step, value) {
          const el = document.createElement("input");
          el.type = "range";
          el.className = className;
          el.min = min;
          el.max = max;
          if (step) el.step = step;
          el.value = value;
          return el;
      }

      const wrapper = document.createElement("div");
      wrapper.className = "sv-lens-wrapper";
      wrapper.style.cssText = "display:none;left:80px;top:80px;width:260px;height:260px;";

      const lens = document.createElement("div");
      lens.className = "sv-lens";
      
      lens.appendChild(buildElement("div", "sv-lens-reflection"));
      lens.appendChild(buildElement("div", "sv-lens-ring"));
      lens.appendChild(buildElement("div", "sv-lens-corner sv-tl"));
      lens.appendChild(buildElement("div", "sv-lens-corner sv-tr"));
      lens.appendChild(buildElement("div", "sv-lens-corner sv-bl"));
      lens.appendChild(buildElement("div", "sv-lens-corner sv-br"));
      lens.appendChild(buildElement("div", "sv-lens-label", "StripView"));
      
      wrapper.appendChild(lens);

      const resizeHandle = document.createElement("div");
      resizeHandle.className = "sv-resize-handle";
      wrapper.appendChild(resizeHandle);

      const panel = document.createElement("div");
      panel.className = "sv-panel";
      
      const pHeader = buildElement("div", "sv-panel-header");
      const pTitle = buildElement("span", "sv-i18n-title", "StripView 面板");
      const pActions = buildElement("div", "sv-panel-header-actions");
      const btnLang = buildElement("button", "sv-lang-toggle", "EN");
      const btnToggle = buildElement("button", "sv-panel-toggle", "−");
      pActions.appendChild(btnLang);
      pActions.appendChild(btnToggle);
      pHeader.appendChild(pTitle);
      pHeader.appendChild(pActions);

      const pBody = buildElement("div", "sv-panel-body");

      const cgBlur = buildElement("div", "sv-control-group");
      cgBlur.appendChild(buildElement("label", "sv-i18n-blur", "毛玻璃强度"));
      cgBlur.appendChild(buildSlider("sv-blur-slider", "0", "20", null, "7"));

      const cgOrig = buildElement("div", "sv-control-group");
      cgOrig.appendChild(buildElement("label", "sv-i18n-origVol", "原视频音量"));
      cgOrig.appendChild(buildSlider("sv-orig-vol", "0", "1", "0.05", "1"));

      const cgOver = buildElement("div", "sv-control-group");
      cgOver.appendChild(buildElement("label", "sv-i18n-overVol", "透视视频音量"));
      cgOver.appendChild(buildSlider("sv-over-vol", "0", "1", "0.05", "0"));

      const btnWrap = buildElement("div");
      btnWrap.style.cssText = "display:flex;gap:6px;margin-top:6px;";
      const btnSync = buildElement("button", "sv-btn sv-btn-sync sv-i18n-sync", "强制重新对齐");
      const btnClear = buildElement("button", "sv-btn sv-btn-clear sv-i18n-clear", "清除");
      btnWrap.appendChild(btnSync);
      btnWrap.appendChild(btnClear);

      const pStatus = buildElement("div", "sv-panel-status", "等待解析...");
      pStatus.dataset.state = "searching";

      pBody.appendChild(cgBlur);
      pBody.appendChild(cgOrig);
      pBody.appendChild(cgOver);
      pBody.appendChild(btnWrap);
      pBody.appendChild(pStatus);

      panel.appendChild(pHeader);
      panel.appendChild(pBody);

      try {
          document.documentElement.appendChild(wrapper);
          document.documentElement.appendChild(panel);
      } catch (e) {
      }

      const panelBody = panel.querySelector(".sv-panel-body");

      function setStatus(stateCode) {
        const statusEl = panel.querySelector('.sv-panel-status');
        statusEl.dataset.state = stateCode;
        const t = i18n[currentLang][stateCode];
        statusEl.textContent = ""; 
        if (stateCode === 'searching' || stateCode === 'cleared') {
           statusEl.textContent = t;
        } else if (stateCode.includes('Failed') || stateCode === 'notFound') {
           const icon = buildElement("span", "sv-err", "✗ ");
           statusEl.appendChild(icon);
           statusEl.appendChild(document.createTextNode(t));
        } else {
           const icon = buildElement("span", "sv-ok", "✓ ");
           statusEl.appendChild(icon);
           statusEl.appendChild(document.createTextNode(t));
        }
      }

      function updateLang() {
        const t = i18n[currentLang];
        panel.querySelector('.sv-i18n-title').textContent = t.title;
        panel.querySelector('.sv-i18n-blur').textContent = t.blur;
        panel.querySelector('.sv-i18n-origVol').textContent = t.origVol;
        panel.querySelector('.sv-i18n-overVol').textContent = t.overVol;
        panel.querySelector('.sv-i18n-sync').textContent = t.sync;
        panel.querySelector('.sv-i18n-clear').textContent = t.clear;
        panel.querySelector('.sv-lang-toggle').textContent = t.langBtn;
        setStatus(panel.querySelector('.sv-panel-status').dataset.state);
      }
      updateLang();

      panel.querySelector('.sv-lang-toggle').addEventListener('click', (e) => {
        e.stopPropagation();
        currentLang = currentLang === 'zh' ? 'en' : 'zh';
        updateLang();
      });

      let panelDrag = false;
      let px0, py0, pLeft, pTop;
      panel.querySelector('.sv-panel-header').addEventListener('mousedown', (e) => {
        if (e.target.tagName.toLowerCase() === 'button') return;
        panelDrag = true;
        px0 = e.clientX; py0 = e.clientY;
        const rect = panel.getBoundingClientRect();
        pLeft = rect.left; pTop = rect.top;
        panel.style.right = 'auto';
        panel.style.bottom = 'auto';
        panel.style.left = pLeft + 'px';
        panel.style.top = pTop + 'px';
        panel.style.margin = '0';
        e.preventDefault();
      });
      document.addEventListener('mousemove', (e) => {
        if (panelDrag) {
          panel.style.left = (pLeft + e.clientX - px0) + 'px';
          panel.style.top = (pTop + e.clientY - py0) + 'px';
        }
      });
      document.addEventListener('mouseup', () => { panelDrag = false; });

      panel.querySelector(".sv-panel-toggle").addEventListener("click", function (e) {
        e.stopPropagation();
        panelBody.classList.toggle("sv-collapsed");
        this.textContent = panelBody.classList.contains("sv-collapsed") ? "+" : "−";
      });

      const blurSlider = panel.querySelector('.sv-blur-slider');
      blurSlider.addEventListener('input', (e) => {
        lens.style.setProperty('--sv-blur', e.target.value + 'px');
      });
      lens.style.setProperty('--sv-blur', '7px');

      const origVolSlider = panel.querySelector('.sv-orig-vol');
      origVolSlider.addEventListener('input', (e) => {
        const vol = parseFloat(e.target.value);
        tracked.forEach(t => {
          if (t.video) {
            t.video.muted = vol === 0;
            t.video.volume = vol;
          }
        });
      });

      const overVolSlider = panel.querySelector('.sv-over-vol');
      overVolSlider.addEventListener('input', (e) => {
        const vol = parseFloat(e.target.value);
        tracked.forEach(t => {
          if (t.ov) {
            t.ov.muted = vol === 0;
            t.ov.volume = vol;
          }
        });
      });

      panel.querySelector(".sv-btn-sync").addEventListener("click", () => {
        tracked.forEach(entry => {
          if (entry.video && entry.ov && entry.ovReady) {
            entry.ov.currentTime = entry.video.currentTime;
            setStatus('syncedInPlace');
          }
        });
      });

      panel.querySelector(".sv-btn-clear").addEventListener("click", () => {
        overlayUrl = "";
        tracked.forEach((e) => {
          if (e.ov) { e.ov.pause(); e.ov.removeAttribute("src"); e.ov.load(); }
        });
        setStatus('cleared');
        panel.style.display = 'none';
        wrapper.style.display = 'none';
      });

      function setOverlaySource(entry, url) {
        if (entry.ov && entry.ov.parentNode) entry.ov.parentNode.removeChild(entry.ov);
        const ov = document.createElement("video");
        ov.className = "sv-overlay-video";
        ov.src = url;
        
        const overVol = parseFloat(document.querySelector('.sv-over-vol').value);
        ov.muted = overVol === 0;
        ov.volume = overVol;
        
        ov.playsInline = true;
        ov.preload = "auto";
        ov.crossOrigin = "anonymous";
        ov.loop = entry.video.loop;
        entry.container.appendChild(ov);
        entry.ov = ov;
        entry.ovReady = false;
        entry.initialSyncDone = false;

        ov.addEventListener("canplay", () => {
          if (!entry.initialSyncDone) {
            entry.initialSyncDone = true;
            
            ov.pause();
            ov.currentTime = entry.video.currentTime;
            
            ov.addEventListener("seeked", function onInitSeek() {
              ov.removeEventListener("seeked", onInitSeek);
              entry.ovReady = true;
              if (isTrulyPlaying(entry.video)) {
                ov.play().catch(() => {});
              }
              setStatus('syncedInPlace');
            });

            setTimeout(() => {
              if (!entry.ovReady) {
                entry.ovReady = true;
                if (isTrulyPlaying(entry.video)) {
                  ov.play().catch(()=>{});
                }
                setStatus('syncedInPlace');
              }
            }, 800);
          }
        });
        
        ov.addEventListener("error", (e) => {
          setStatus('loadFailed');
        });
      }

      function attachToVideo(video) {
        // 防止监听自身的覆盖视频
        if (video.classList.contains("sv-overlay-video")) return;
        
        if (tracked.find((t) => t.video === video)) return;
        if (video.clientWidth < 80 || video.clientHeight < 40) return;

        const container = video.parentElement;
        const pos = getComputedStyle(container).position;
        if (pos === "static" || pos === "") container.style.position = "relative";

        const entry = { video, ov: null, ovReady: false, container };
        tracked.push(entry);
        
        const origVol = parseFloat(document.querySelector('.sv-orig-vol').value);
        video.muted = origVol === 0;
        video.volume = origVol;

        video.addEventListener("pause", () => { 
          if (entry.ov && !entry.ov.paused) entry.ov.pause(); 
          if (entry.ov && Math.abs(entry.ov.currentTime - video.currentTime) > 0.05) {
            entry.ov.currentTime = video.currentTime; 
          }
        });
        video.addEventListener("seeked", () => { 
          if (entry.ov) entry.ov.currentTime = video.currentTime;
        });
        video.addEventListener("seeking", () => { if (entry.ov && !entry.ov.paused) entry.ov.pause(); });
        video.addEventListener("waiting", () => { if (entry.ov && !entry.ov.paused) entry.ov.pause(); });
        video.addEventListener("ratechange", () => { if (entry.ov) entry.ov.playbackRate = video.playbackRate; });
        video.addEventListener("playing", () => {
          if (entry.ov && entry.ovReady) {
            if (Math.abs(entry.ov.currentTime - video.currentTime) > 0.5) entry.ov.currentTime = video.currentTime;
            entry.ov.playbackRate = video.playbackRate;
            entry.ov.play().catch(() => {});
          }
        });

        if (overlayUrl) setOverlaySource(entry, overlayUrl);
      }

      function isTrulyPlaying(v) {
        return !v.paused && !v.ended && v.readyState >= 3 && !v.seeking;
      }

      setInterval(() => {
        // GC 垃圾回收已销毁节点
        for (let i = tracked.length - 1; i >= 0; i--) {
            if (!document.body.contains(tracked[i].video)) {
                const deadEntry = tracked[i];
                if (deadEntry.ov && deadEntry.ov.parentNode) {
                    deadEntry.ov.parentNode.removeChild(deadEntry.ov);
                }
                tracked.splice(i, 1);
            }
        }

        for (const entry of tracked) {
          const { video, ov, ovReady } = entry;
          if (!ov || !ov.src || !ovReady) continue;

          const origPlaying = isTrulyPlaying(video);
          const origPaused = video.paused;
          const origSeeking = video.seeking;
          const origTime = video.currentTime;
          const origRate = video.playbackRate;

          if (origPaused || video.ended) {
            if (!ov.paused) ov.pause();
            if (Math.abs(ov.currentTime - origTime) > 0.05) ov.currentTime = origTime;
            continue;
          }
          if (origSeeking) {
            if (!ov.paused) ov.pause();
            continue;
          }
          if (!origPlaying && !origPaused) {
            if (!ov.paused) ov.pause();
            continue;
          }
          if (ov.readyState < 3) {
            continue;
          }
          
          const diff = origTime - ov.currentTime; 
          const drift = Math.abs(diff);

          if (drift > 0.5) {
            ov.currentTime = origTime;
            continue;
          }

          if (drift > 0.1) {
            if (ov.paused) ov.play().catch(() => {});
            ov.playbackRate = origRate * (diff > 0 ? 1.25 : 0.85);
          } else if (drift > 0.04) {
            if (ov.paused) ov.play().catch(() => {});
            ov.playbackRate = origRate * (diff > 0 ? 1.05 : 0.95);
          } else {
            if (ov.paused) ov.play().catch(() => {});
            if (ov.playbackRate !== origRate) ov.playbackRate = origRate;
          }
        }
      }, 50);

      function renderLens() {
        const wl = parseFloat(wrapper.style.left) || 0;
        const wt = parseFloat(wrapper.style.top) || 0;
        const ww = parseInt(wrapper.style.width) || 260;
        const wh = parseInt(wrapper.style.height) || 260;

        lens.querySelectorAll(".sv-lens-clip").forEach((el) => el.remove());

        for (const { video, ov, ovReady } of tracked) {
          if (!ov || !ov.src || !ovReady || ov.readyState < 2) continue;

          const vr = video.getBoundingClientRect();
          const oL = Math.max(wl, vr.left);
          const oT = Math.max(wt, vr.top);
          const oR = Math.min(wl + ww, vr.right);
          const oB = Math.min(wt + wh, vr.bottom);
          if (oL >= oR || oT >= oB) continue;

          const clipW = oR - oL;
          const clipH = oB - oT;

          const clip = document.createElement("div");
          clip.className = "sv-lens-clip";
          clip.style.left = (oL - wl) + "px";
          clip.style.top = (oT - wt) + "px";
          clip.style.width = clipW + "px";
          clip.style.height = clipH + "px";

          const dpr = window.devicePixelRatio || 1;
          const canvas = document.createElement("canvas");
          canvas.width = Math.round(clipW * dpr);
          canvas.height = Math.round(clipH * dpr);
          canvas.style.cssText = `width:${clipW}px!important;height:${clipH}px!important;`;

          const ctx = canvas.getContext("2d");
          const scaleX = ov.videoWidth / vr.width;
          const scaleY = ov.videoHeight / vr.height;

          try {
            ctx.drawImage(ov,
              (oL - vr.left) * scaleX, (oT - vr.top) * scaleY,
              clipW * scaleX, clipH * scaleY,
              0, 0, canvas.width, canvas.height
            );
          } catch (e) {}

          clip.appendChild(canvas);
          lens.insertBefore(clip, lens.firstChild);
        }
        requestAnimationFrame(renderLens);
      }
      requestAnimationFrame(renderLens);

      let mode = null; 
      let mx0, my0, l0, t0, w0, h0;

      lens.addEventListener("mousedown", (e) => {
        mode = "drag";
        mx0 = e.clientX; my0 = e.clientY;
        l0 = parseFloat(wrapper.style.left) || 0;
        t0 = parseFloat(wrapper.style.top) || 0;
        lens.classList.add("sv-grabbing");
        e.preventDefault();
        document.addEventListener("mousemove", onMove, true);
        document.addEventListener("mouseup", onUp, true);
      });

      resizeHandle.addEventListener("mousedown", (e) => {
        mode = "resize";
        mx0 = e.clientX; my0 = e.clientY;
        w0 = parseInt(wrapper.style.width) || 260;
        h0 = parseInt(wrapper.style.height) || 260;
        e.preventDefault();
        e.stopPropagation();
        document.addEventListener("mousemove", onMove, true);
        document.addEventListener("mouseup", onUp, true);
      });

      function onMove(e) {
        const dx = e.clientX - mx0;
        const dy = e.clientY - my0;
        if (mode === "drag") {
          wrapper.style.left = (l0 + dx) + "px";
          wrapper.style.top = (t0 + dy) + "px";
        } else if (mode === "resize") {
          wrapper.style.width = Math.max(MIN_SIZE, Math.min(MAX_SIZE, w0 + dx)) + "px";
          wrapper.style.height = Math.max(MIN_SIZE, Math.min(MAX_SIZE, h0 + dy)) + "px";
        }
      }

      function onUp() {
        mode = null;
        lens.classList.remove("sv-grabbing");
        document.removeEventListener("mousemove", onMove, true);
        document.removeEventListener("mouseup", onUp, true);
      }

      function scan() {
        document.querySelectorAll("video:not(.sv-overlay-video)").forEach((v) => {
          if (v.currentSrc || v.src || v.querySelector("source")) {
            attachToVideo(v);
          } else {
            v.addEventListener("loadedmetadata", () => attachToVideo(v), { once: true });
          }
        });
      }
      setTimeout(scan, 800);
      setTimeout(scan, 2500);
      new MutationObserver(() => setTimeout(scan, 300))
        .observe(document.body, { childList: true, subtree: true });

      const WORKER_URL = "https://sv.acreatorhub.com";
      let lastCheckedUrl = "";
      let lastCheckedTag = "";

      async function tryResolveOverlay() {
        const pageUrl = window.location.href.split("#")[0];

        let textSource = document.title || "";
        const ytTitleNode = document.querySelector('h1.ytd-watch-metadata yt-formatted-string, h1.title yt-formatted-string');
        if (ytTitleNode) textSource += " " + ytTitleNode.textContent;

        const match = textSource.match(/\[(.*?)\]|【(.*?)】/);
        const tagWithBracket = match ? match[0] : null;                           

        if (pageUrl === lastCheckedUrl && tagWithBracket === lastCheckedTag) {
            return;
        }

        if (pageUrl !== lastCheckedUrl) {
            setStatus('searching');
            overlayUrl = ""; 
            panel.style.display = 'none';
            wrapper.style.display = 'none';
        }

        lastCheckedUrl = pageUrl;
        lastCheckedTag = tagWithBracket;

        // 【终极优化】:不再并发请求,组合成单条请求发送给服务端处理
        let reqParam = `url=${encodeURIComponent(pageUrl)}`;
        if (tagWithBracket) {
            reqParam += `&tag=${encodeURIComponent(tagWithBracket)}`;
        }

        try {
            const res = await fetch(`${WORKER_URL}/api/resolve?${reqParam}`);
            if (!res.ok) throw new Error(`HTTP Error ${res.status}`);
            
            const data = await res.json();
            if (data.url && data.url !== overlayUrl) {
                overlayUrl = data.url;
                tracked.forEach((e) => setOverlaySource(e, data.url));
                setStatus('found');
                panel.style.display = 'block';
                wrapper.style.display = 'block';
            }
        } catch (e) {
            // 失败时保持隐藏
        }
      }

      setInterval(tryResolveOverlay, 1000);

    })();

})();