// ==UserScript== // @name 视频控制器 // @namespace video-controller // @description 80KB的极简视频控制器,适配HTML5播放器。支持倍速(0.25x–16x)、音量增强(最高5x)、亮度增强(最高3x)。常规快捷键操作:倍速/快进/音量/逐帧/亮度/画面缩放。此外,支持屏幕全屏/网页全屏/旋转90°/水平翻转/画面拖动/截图/画中画/纯净模式,支持自动记忆网站设置/全局自动设置/色彩模式更改/区间循环播放。 // @version 1.1.2 // @license MIT // @author Qiu Zongman // @homepageURL https://gitee.com/qiuzongman/video-controller // @icon https://gitee.com/qiuzongman/video-controller/raw/master/icon.png // @match *://*/* // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @run-at document-start // ==/UserScript== // (function () { 'use strict'; // 仅顶层页面运行,避免 iframe 内重复执行 if (window.top !== window.self) return; const DEFAULT_SETTINGS = { togglePlay: ' ', speedUp: 'w', speedDown: 's', forward: 'ArrowRight', backward: 'ArrowLeft', frameForward: '', frameBackward: '', volumeUp: 'ArrowUp', volumeDown: 'ArrowDown', brightnessUp: '+', brightnessDown: '-', fullscreen: '', screenshot: '', rotateKey: '', flipKey: '', screenFullKey: '', pipKey: '', cleanKey: '', zoomUpKey: '', zoomDownKey: '', zoomStep: 0.1, panKey: '', speedStep: 0.5, volumeStep: 0.1, brightnessStep: 0.1, minSpeed: 0.25, maxSpeed: 16, maxVolume: 5.0, skipSeconds: 5, quickSpeed1Key: '1', quickSpeed1Val: 1.0, quickSpeed2Key: '2', quickSpeed2Val: 2.0, quickSpeed3Key: '3', quickSpeed3Val: 3.0, quickSpeed4Key: '4', quickSpeed4Val: 4.0, autoSpeedEnabled: false, autoSpeed: 1.0, autoVolumeEnabled: false, autoVolume: 1.0, autoBrightness: 1.0, autoBrightnessEnabled: false, autoPlayEnabled: false, siteMemoryEnabled: true, noMemorySites: '', toastDuration: 3000, lastTab: 0, hideMenuEntry: false, openSettingsKey: '', }; const COLOR_PRESETS = { '默认':'brightness(1) contrast(1) saturate(1) hue-rotate(0deg)', '明亮':'brightness(1.2) contrast(1) saturate(1.1) hue-rotate(0deg)', '鲜艳':'brightness(1.1) contrast(1.15) saturate(1.6) hue-rotate(0deg)', '柔和':'brightness(1.05) contrast(0.85) saturate(0.75) hue-rotate(0deg)', '高对比':'brightness(1) contrast(1.5) saturate(1) hue-rotate(0deg)', '黑白':'brightness(1) contrast(1.1) saturate(0) hue-rotate(0deg)', '暖色':'brightness(1) contrast(1) saturate(1.15) hue-rotate(30deg)', '冷色':'brightness(1) contrast(1) saturate(1) hue-rotate(200deg)', '复古':'brightness(0.95) contrast(1.1) saturate(0.5) hue-rotate(340deg)', '反转':'brightness(1) contrast(1) saturate(1) hue-rotate(180deg)', '护眼':'brightness(0.7) contrast(0.85) saturate(0.85) hue-rotate(0deg)', }; const STORAGE_KEY = 'vc_settings'; let settings = {}; function loadSettings() { try { const raw = typeof GM_getValue === 'function' ? GM_getValue(STORAGE_KEY, null) : null; settings = raw ? { ...DEFAULT_SETTINGS, ...JSON.parse(raw) } : { ...DEFAULT_SETTINGS }; } catch (e) { settings = { ...DEFAULT_SETTINGS }; } if (!settings.openSettingsKey) settings.hideMenuEntry = false; } function saveSettings() { try { if (typeof GM_setValue === 'function') { GM_setValue(STORAGE_KEY, JSON.stringify(settings)); } } catch (e) {} } const SITE_STORAGE_KEY = 'vc_site_settings'; let siteSettings = {}; function loadSiteSettings() { try { const raw = typeof GM_getValue === 'function' ? GM_getValue(SITE_STORAGE_KEY, null) : null; siteSettings = raw ? JSON.parse(raw) : {}; } catch (e) { siteSettings = {}; } } function saveSiteSettings() { try { if (typeof GM_setValue === 'function') { GM_setValue(SITE_STORAGE_KEY, JSON.stringify(siteSettings)); } } catch (e) {} } function getCurrentSite() { return location.hostname; } function getSiteAuto(site) { return siteSettings[site] || null; } function setSiteAuto(site, auto) { siteSettings[site] = auto; saveSiteSettings(); } function removeSiteAuto(site) { delete siteSettings[site]; saveSiteSettings(); } function hackAttachShadow() { if (window._vcHasHackAttachShadow_) return; try { window._vcShadowDomList_ = window._vcShadowDomList_ || []; const origAttach = window.Element.prototype.attachShadow; window.Element.prototype.attachShadow = function (init) { if (init && init.mode) { init.mode = 'open'; } const shadowRoot = origAttach.call(this, init); window._vcShadowDomList_.push(shadowRoot); document.dispatchEvent(new CustomEvent('vcAddShadowRoot', { detail: { shadowRoot } })); return shadowRoot; }; window._vcHasHackAttachShadow_ = true; } catch (e) { console.warn('[视频控制器] hackAttachShadow 失败:', e); } } let _toastEl = null; let _toastTimer = null; function Toast(msg) { if (settings.toastDuration === 0) return; if (!_toastEl) { _toastEl = document.createElement('div'); _toastEl.style.cssText = [ 'font-family: Arial, "Microsoft YaHei", sans-serif;', 'max-width: 60%; min-width: 150px; padding: 0 14px;', 'height: 40px; color: #fff; line-height: 40px;', 'text-align: center; border-radius: 8px;', 'position: fixed; top: 50%; left: 50%;', 'transform: translate(-50%, -50%);', 'z-index: 2147483647;', 'background: rgba(0,0,0,0.78);', 'pointer-events: none;', 'transition: opacity 0.3s ease;' ].join(''); document.body.appendChild(_toastEl); } // toast 位置由 fullscreenchange 事件控制,此处不动 _toastEl.textContent = msg; _toastEl.style.opacity = '1'; _toastEl.style.display = ''; if (_toastTimer) clearTimeout(_toastTimer); _toastTimer = setTimeout(() => { _toastEl.style.opacity = '0'; _toastTimer = setTimeout(() => { _toastEl.style.display = 'none'; }, 300); }, settings.toastDuration); } const audioCtxMap = new WeakMap(); function getAudioBoost(video) { var record = audioCtxMap.get(video); if (record) return record; try { var AC = window.AudioContext || window.webkitAudioContext; if (!AC) return null; var ctx = new AC(); ctx.resume(); var source = ctx.createMediaElementSource(video); var gain = ctx.createGain(); source.connect(gain); gain.connect(ctx.destination); record = { ctx, source, gain }; audioCtxMap.set(video, record); return record; } catch (e) { audioCtxMap.delete(video); return null; } } function setVideoVolume(video, vol) { var clamped = Math.max(0, Math.min(settings.maxVolume, vol)); var record = audioCtxMap.get(video); var actual; if (record) { video.volume = 1.0; record.gain.gain.value = clamped; actual = clamped; } else if (clamped > 1.0) { record = getAudioBoost(video); if (record) { video.volume = 1.0; record.gain.gain.value = clamped; actual = clamped; } else { actual = Math.min(1, clamped); video.volume = actual; } } else { actual = clamped; video.volume = actual; } return actual; } function getVideoVolume(video) { var record = audioCtxMap.get(video); if (record) return record.gain.gain.value; return video.volume; } const VIDEO_SEL = 'video, bwp-video'; function findAllVideos() { const videos = []; document.querySelectorAll(VIDEO_SEL).forEach(function(v) { videos.push(v); }); try { document.querySelectorAll('*').forEach(function(el) { if (el.shadowRoot) { el.shadowRoot.querySelectorAll(VIDEO_SEL).forEach(function(v) { videos.push(v); }); } }); } catch (e) {} if (window._vcShadowDomList_) { window._vcShadowDomList_.forEach(function(sr) { try { if (sr && sr.querySelectorAll) { sr.querySelectorAll(VIDEO_SEL).forEach(function(v) { if (!videos.includes(v)) videos.push(v); }); } } catch (e) {} }); } return videos; } function getActiveVideo() { const videos = findAllVideos(); if (videos.length === 0) return null; for (let i = 0; i < videos.length; i++) { if (!videos[i].paused) return videos[i]; } let best = null; let bestArea = 0; for (let i = 0; i < videos.length; i++) { const r = videos[i].getBoundingClientRect(); if (r.width > 0 && r.height > 0) { const area = r.width * r.height; if (area > bestArea) { bestArea = area; best = videos[i]; } } } if (best) return best; return videos[0]; } var _sessionSpeed, _sessionVolume, _sessionBrightness; function hijackPlaybackRate() { if (window._vcHijackPR) return; window._vcHijackPR = true; try { var desc = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'playbackRate'); if (!desc || !desc.set) return; var origSet = desc.set; Object.defineProperty(HTMLMediaElement.prototype, 'playbackRate', { get: function() { return desc.get.call(this); }, set: function(v) { if (this._vcApplying) { origSet.call(this, v); return; } origSet.call(this, v); if (_sessionSpeed !== undefined && Math.abs(v - _sessionSpeed) > 0.001) { this._vcApplying = true; origSet.call(this, _sessionSpeed); this._vcApplying = false; } }, configurable: true }); } catch(e) {} } function changeSpeed(video, delta) { if (!video) return; var r = (video.playbackRate / settings.speedStep).toFixed(2); var newRate = (Math.round(r) + delta / settings.speedStep) * settings.speedStep; newRate = Math.max(settings.minSpeed, Math.min(settings.maxSpeed, newRate)); newRate = Math.round(newRate * 100) / 100; _sessionSpeed = newRate; video.playbackRate = newRate; saveSiteMem('speed', newRate); Toast('倍速 ' + newRate.toFixed(2) + 'x'); } function skipTime(video, seconds) { if (!video || isNaN(video.duration)) return; video.currentTime = Math.max(0, Math.min(video.duration, video.currentTime + seconds)); } function changeVolume(video, delta) { if (!video) return; var curVol = getVideoVolume(video); var newVol = Math.round(curVol / settings.volumeStep) * settings.volumeStep + delta; newVol = Math.max(0, Math.min(settings.maxVolume, newVol)); var actual = setVideoVolume(video, newVol); _sessionVolume = actual; saveSiteMem('volume', actual); Toast('音量 ' + Math.round(actual * 100) + '%'); } function changeBrightness(video, delta) { if (!video) return; var val = (video._vcBrightness || 1.0) + delta; val = Math.round(val / settings.brightnessStep) * settings.brightnessStep; val = Math.max(0, Math.min(3, val)); _sessionBrightness = val; video._vcBrightness = val; video.style.filter = 'brightness(' + val + ')'; saveSiteMem('brightness', val); Toast('亮度 ' + Math.round(val * 100) + '%'); } function setBrightness(video, val) { if (!video) return; video._vcBrightness = val; video.style.filter = val === 1 ? '' : 'brightness(' + val + ')'; } function checkLoop() { var v = this; var loop = v._vcLoop; if (!loop) return; if (v.currentTime >= loop.end) { if (loop.count > 0 && loop.cur >= loop.count - 1) return; if (loop.count > 0) loop.cur++; v.currentTime = loop.start; } } function togglePip(video) { if (document.pictureInPictureElement) { document.exitPictureInPicture().catch(function(){}); if (video._vcPipHidden) { for (var i = 0; i < video._vcPipHidden.length; i++) { video._vcPipHidden[i].style.display = video._vcPipHidden[i]._vcOrigDisplay || ''; } video._vcPipHidden = null; } Toast('画中画:关闭'); } else { video.requestPictureInPicture().then(function() { setTimeout(function() { var hidden = []; var el = video.parentElement; for (var i = 0; i < 10 && el && el !== document.body; i++) { var cs = window.getComputedStyle(el); if ((cs.position === 'fixed' || cs.position === 'absolute') && cs.display !== 'none' && el.offsetWidth > 150) { el._vcOrigDisplay = cs.display; el.style.display = 'none'; hidden.push(el); } el = el.parentElement; } video._vcPipHidden = hidden; }, 300); }).catch(function(){}); Toast('画中画:开启'); } } function toggleScreenFull(video) { var btn = document.querySelector('.bpx-player-ctrl-web,.dplayer-full-icon[data-name="web"],.vjs-remaining-time,.plyr__control[data-plyr="fullscreen"][data-size="small"],[aria-label="网页全屏"],[title="网页全屏"]'); if (btn) { btn.click(); return; } // fallback: 包裹式网页全屏 if (video._vcSFParent) { // 退出:拆包裹 var wrap = video._vcSFParent; var inner = wrap.firstChild; if (inner) { wrap.parentElement.insertBefore(inner, wrap); inner.style.cssText = video._vcSFOrigCss || ''; } wrap.remove(); video._vcSFParent = null; video._vcSFOrigCss = null; Toast('退出网页全屏'); } else { // 进入:用包裹元素实现全屏,不改变容器本身的样式 var el = video.parentElement; for (var i = 0; i < 5 && el; i++) { if (el.querySelectorAll('video').length >= 1 && el.offsetWidth > 200) break; el = el.parentElement; } if (!el || el === document.body) el = video; video._vcSFOrigCss = el.style.cssText; var wrap = document.createElement('div'); wrap.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:2147483646;background:#000'; el.parentElement.insertBefore(wrap, el); wrap.appendChild(el); video._vcSFParent = wrap; Toast('进入网页全屏'); } } function toggleFlip(video) { video._vcFlipped = !video._vcFlipped; applyVideoTransform(video); Toast(video._vcFlipped ? '水平翻转:开启' : '水平翻转:关闭'); } function applyVideoTransform(video) { var t = ''; if (video.style.position === 'absolute') t += ' translate(-50%,-50%)'; var px = video._vcPanX || 0; var py = video._vcPanY || 0; if (px || py) t += ' translate(' + px + 'px,' + py + 'px)'; var z = video._vcZoom || 1; if (z !== 1) t += ' scale(' + z + ')'; var r = video._vcRotate || 0; if (r !== 0) t += ' rotate(' + r + 'deg)'; if (video._vcFlipped) t += ' scaleX(-1)'; video.style.transform = t || ''; } function changeZoom(video, delta) { if (!video) return; var z = (video._vcZoom || 1) + delta; z = Math.round(z / settings.zoomStep) * settings.zoomStep; z = Math.max(0.1, Math.min(5, z)); video._vcZoom = z; applyVideoTransform(video); Toast('缩放 ' + Math.round(z * 100) + '%'); } var _panVideo = null; var _panX = 0, _panY = 0, _panOX = 0, _panOY = 0; function togglePanMode() { if (_panVideo) { _panVideo.style.cursor = ''; _panVideo._vcPanX = 0; _panVideo._vcPanY = 0; applyVideoTransform(_panVideo); _panVideo = null; Toast('画面拖动:关闭'); return; } var v = getActiveVideo(); if (!v) return; _panVideo = v; v.style.cursor = 'grab'; Toast('画面拖动:开启(拖拽鼠标移动画面)'); } function autoRotate(video) { var deg = ((video._vcRotate || 0) + 90) % 360; video._vcRotate = deg; if (deg % 180 !== 0) { var x1 = video.clientWidth || video.offsetWidth || 640; var y1 = video.clientHeight || video.offsetHeight || 360; if (x1 && y1 && x1 !== y1) { video.style.width = (x1 > y1 ? y1 : x1 * x1 / y1) + 'px'; video.style.height = (x1 > y1 ? y1 * y1 / x1 : x1) + 'px'; } video.style.position = 'absolute'; video.style.top = '50%'; video.style.left = '50%'; } else { video.style.position = ''; video.style.top = ''; video.style.left = ''; video.style.width = ''; video.style.height = ''; } applyVideoTransform(video); Toast('旋转 ' + deg + '°'); } var _cleanMode = false; var _cleanEls = []; var _cleanSheet = null; function toggleCleanMode() { _cleanMode = !_cleanMode; if (!_cleanMode) { for (var i = 0; i < _cleanEls.length; i++) { _cleanEls[i].classList.remove('vc-clean-lock'); } _cleanEls = []; if (_cleanSheet) { _cleanSheet.remove(); _cleanSheet = null; } Toast('纯净模式:关闭'); return; } if (!_cleanSheet) { _cleanSheet = document.createElement('style'); _cleanSheet.id = 'vc-clean-sheet'; _cleanSheet.textContent = '.vc-clean-lock{display:none!important}'; document.head.appendChild(_cleanSheet); } var v = getActiveVideo(); if (!v) { _cleanMode = false; return; } // 隐藏视频的兄弟元素(覆盖层),但不隐藏包含视频的容器 var p = v.parentElement; for (var d = 0; d < 4 && p && p !== document.body; d++) { var kids = p.children; for (var k = 0; k < kids.length; k++) { var el = kids[k]; if (el === v || el.contains(v)) continue; if (el.tagName === 'VIDEO' || el.tagName === 'SOURCE') continue; el.classList.add('vc-clean-lock'); _cleanEls.push(el); } v = p; // 上一层的视频元素视为容器本身 p = p.parentElement; } Toast('纯净模式:开启'); } function screenshot(video) { if (!video || video.videoWidth === 0 || video.videoHeight === 0) return; try { var c = document.createElement('canvas'); c.width = video.videoWidth; c.height = video.videoHeight; var ctx = c.getContext('2d'); ctx.filter = video.style.filter || 'none'; ctx.drawImage(video, 0, 0, c.width, c.height); c.toBlob(function(blob) { if (!blob) return; var item = new ClipboardItem({ 'image/png': blob }); navigator.clipboard.write([item]).then(function() { Toast('已截图并复制到剪贴板'); }).catch(function() { Toast('复制失败(需 HTTPS 或 localhost)'); }); }); } catch (e) { Toast('截图失败'); } } function onVideoPlay(e) { const video = e.target; if (!video || video.tagName !== 'VIDEO') return; if (settings.autoSpeedEnabled) { var rate = parseFloat(settings.autoSpeed); if (!isNaN(rate) && rate >= settings.minSpeed && rate <= settings.maxSpeed) { _sessionSpeed = Math.round(rate * 100) / 100; video.playbackRate = rate; } } else if (_sessionSpeed !== undefined) { video.playbackRate = _sessionSpeed; } if (settings.autoVolumeEnabled) { var vv = parseFloat(settings.autoVolume); if (!isNaN(vv) && vv >= 0 && vv <= settings.maxVolume) { _sessionVolume = vv; setVideoVolume(video, vv); } } else if (_sessionVolume !== undefined) { setVideoVolume(video, _sessionVolume); } if (settings.autoBrightnessEnabled) { var bb = parseFloat(settings.autoBrightness); if (!isNaN(bb) && bb >= 0 && bb <= 3) { _sessionBrightness = bb; setBrightness(video, bb); } } else if (_sessionBrightness !== undefined) { setBrightness(video, _sessionBrightness); } var site = getSiteAuto(getCurrentSite()); if (settings.siteMemoryEnabled && site && !isNoMemory(getCurrentSite())) { if (site.speed !== undefined) { _sessionSpeed = Math.round(site.speed * 100) / 100; video.playbackRate = _sessionSpeed; } if (site.volume !== undefined) { _sessionVolume = site.volume; setVideoVolume(video, site.volume); } if (site.brightness !== undefined) { _sessionBrightness = site.brightness; setBrightness(video, site.brightness); } } } function bindVideoEvents(video) { if (video._vcEventsBound) return; video._vcEventsBound = true; video.addEventListener('playing', onVideoPlay); if (settings.autoPlayEnabled) { video.play().catch(function(){}); } } function isNoMemory(host) { var list = (settings.noMemorySites || '').split('\n'); for (var i = 0; i < list.length; i++) { if (list[i].trim() === host) return true; } return false; } function saveSiteMem(key, val) { if (!settings.siteMemoryEnabled || isNoMemory(getCurrentSite())) return; var s = getSiteAuto(getCurrentSite()) || {}; s[key] = (key === 'speed') ? Math.round(val * 100) / 100 : val; setSiteAuto(getCurrentSite(), s); } function bindAllVideos() { const videos = findAllVideos(); for (let i = 0; i < videos.length; i++) { bindVideoEvents(videos[i]); } } function onKeyDown(e) { const tag = (e.target && e.target.tagName) || ''; if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || (e.target && e.target.isContentEditable)) { return; } if (e.metaKey) return; if (onKeyDown._lk === e.key && Date.now() - onKeyDown._lt < 150) return; onKeyDown._lk = e.key; onKeyDown._lt = Date.now(); var combo = ''; if (e.ctrlKey) combo += 'Ctrl+'; if (e.altKey) combo += 'Alt+'; combo += e.key; if (settings.openSettingsKey && combo === settings.openSettingsKey) { e.preventDefault(); e.stopPropagation(); openSettings(); return; } const video = getActiveVideo(); if (!video) return; let handled = false; function m(k) { return k && k !== '' && combo === k; } if (m(settings.togglePlay)) { video.paused ? video.play() : video.pause(); Toast(video.paused ? '已暂停' : '已播放'); handled = true; } if (m(settings.speedUp)) { changeSpeed(video, settings.speedStep); handled = true; } if (m(settings.speedDown)) { changeSpeed(video, -settings.speedStep); handled = true; } if (m(settings.forward)) { skipTime(video, settings.skipSeconds); Toast('快进 ' + settings.skipSeconds + 's'); handled = true; } if (m(settings.backward)) { skipTime(video, -settings.skipSeconds); Toast('快退 ' + settings.skipSeconds + 's'); handled = true; } if (m(settings.frameForward)) { skipTime(video, 1 / 30); Toast('逐帧+'); handled = true; } if (m(settings.frameBackward)) { skipTime(video, -1 / 30); Toast('逐帧-'); handled = true; } if (m(settings.volumeUp)) { changeVolume(video, settings.volumeStep); handled = true; } if (m(settings.volumeDown)) { changeVolume(video, -settings.volumeStep); handled = true; } if (m(settings.brightnessUp)) { changeBrightness(video, settings.brightnessStep); handled = true; } if (m(settings.brightnessDown)) { changeBrightness(video, -settings.brightnessStep); handled = true; } if (m(settings.fullscreen)) { var fsBtn = document.querySelector('.bpx-player-ctrl-full,.dplayer-full-icon,.vjs-fullscreen-control,.jw-icon-fullscreen,.plyr__control[data-plyr="fullscreen"],.mejs-fullscreen-button,.video-js .vjs-fullscreen-control,[aria-label="全屏"],[aria-label="Fullscreen"],[title="全屏"],[title="Fullscreen"]'); if (fsBtn) { fsBtn.click(); handled = true; } else { var wasFull = !!document.fullscreenElement; if (wasFull) { document.exitFullscreen().catch(function(){}); } else { video.requestFullscreen().catch(function(){}); } Toast(wasFull ? '退出屏幕全屏' : '屏幕全屏'); handled = true; } } if (m(settings.screenshot)) { screenshot(video); handled = true; } if (m(settings.rotateKey)) { autoRotate(video); handled = true; } if (m(settings.flipKey)) { toggleFlip(video); handled = true; } if (m(settings.screenFullKey)) { toggleScreenFull(video); handled = true; } if (m(settings.pipKey)) { togglePip(video); handled = true; } if (m(settings.cleanKey)) { toggleCleanMode(); handled = true; } if (m(settings.zoomUpKey)) { changeZoom(video, settings.zoomStep); handled = true; } if (m(settings.zoomDownKey)) { changeZoom(video, -settings.zoomStep); handled = true; } if (m(settings.panKey)) { togglePanMode(); handled = true; } for (let i = 1; i <= 4; i++) { if (m(settings['quickSpeed' + i + 'Key'])) { _sessionSpeed = Math.round(settings['quickSpeed' + i + 'Val'] * 100) / 100; video.playbackRate = _sessionSpeed; saveSiteMem('speed', settings['quickSpeed' + i + 'Val']); Toast('倍速 ' + settings['quickSpeed' + i + 'Val'].toFixed(1) + 'x'); handled = true; break; } } if (handled) { e.preventDefault(); e.stopPropagation(); } } function openSettings() { const existing = document.getElementById('vc-settings-panel'); if (existing) { existing.remove(); return; } const panel = document.createElement('div'); panel.id = 'vc-settings-panel'; panel.style.cssText = [ 'position: fixed; top: 50%; left: 50%;', 'transform: translate(-50%, -50%);', 'background: #fff; padding: 20px;', 'border: 2px solid #555; border-radius: 8px;', 'z-index: 2147483647; width: 560px;', 'max-height: 85vh;', 'display: flex; flex-direction: column;', '--vc-fs: 13px; font-size: 13px;', 'pointer-events: auto;' ].join(''); panel.innerHTML = buildSettingsHTML(); var host = document.fullscreenElement || document.body; // 视频元素全屏时不显示子节点,改用其父容器 if (host && host.tagName === 'VIDEO') host = host.parentElement; host.appendChild(panel); bindSettingsEvents(panel); } function buildSettingsHTML() { const s = settings; const esc = (v) => String(v).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); const displayKey = (v) => { if (v === '') return ''; if (v === ' ') return 'Space'; return v; }; return `