// ==UserScript== // @name 精简HTML5播放器增强脚本 // @name:en Simple HTML5 Video Player Enhancer // @namespace https://github.com/tunecc/h5player // @version 2.36 // @description 极简 HTML5 视频增强脚本,支持倍速、快进快退、智能全屏(保留控制栏)。 // @description:en Minimalist HTML5 video enhancer. Supports speed control, fast forward/rewind, and smart fullscreen (with controls). // @author Tune // @homepage https://github.com/tunecc/h5player // @source https://github.com/tunecc/h5player // @downloadURL https://raw.githubusercontent.com/tunecc/h5player/master/h5player.user.js // @updateURL https://raw.githubusercontent.com/tunecc/h5player/master/h5player.user.js // @icon https://raw.githubusercontent.com/tunecc/h5player/master/logo.png // @match *://*/* // @grant none // @run-at document-end // @license MIT // ==/UserScript== (function() { 'use strict'; // --- 配置区域 --- const STEP_XC = 0.1; const MAX_SPEED = 16.0; const MIN_SPEED = 0.1; const DEFAULT_RESTORE_SPEED = 1.5; const CLICK_THRESHOLD = 500; const SEEK_STEP_FORWARD = 5; const SEEK_STEP_REWIND = 3; // ---------------- const plusInfo = {}; let lastPlaybackRate = DEFAULT_RESTORE_SPEED; let seekAccumulator = 0; let seekResetTimer = null; // 【核心变量】用于缓存当前正在活跃的视频元素 let currentVideo = null; // UI配置 const tipsDiv = document.createElement('div'); tipsDiv.style.cssText = ` position: fixed; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 12px; font-weight: 700; line-height: 1; padding: 0px 2px; background: rgba(0, 0, 0, 0.5); color: #fff; top: 0; left: 0; transition: opacity 0.3s ease; opacity: 0; border-bottom-right-radius: 4px; display: none; pointer-events: none; text-shadow: none; `; let timer = null; function showTips(text, targetVideo) { if (!targetVideo) return; const fullscreenEl = document.fullscreenElement || document.webkitFullscreenElement; const container = fullscreenEl || document.body; if (tipsDiv.parentNode !== container) { container.appendChild(tipsDiv); } const rect = targetVideo.getBoundingClientRect(); const top = Math.max(0, rect.top); const left = Math.max(0, rect.left); tipsDiv.style.top = `${top}px`; tipsDiv.style.left = `${left}px`; tipsDiv.innerText = text; tipsDiv.style.display = 'block'; requestAnimationFrame(() => { tipsDiv.style.opacity = '1'; }); if (timer) clearTimeout(timer); timer = setTimeout(() => { tipsDiv.style.opacity = '0'; setTimeout(() => { tipsDiv.style.display = 'none'; }, 400); }, 800); } /** * 视频捕获器 */ function captureMedia(e) { const target = e.target; if (target && target.tagName === 'VIDEO') { currentVideo = target; } } document.addEventListener('play', captureMedia, true); document.addEventListener('playing', captureMedia, true); document.addEventListener('pause', captureMedia, true); document.addEventListener('timeupdate', captureMedia, true); document.addEventListener('canplay', captureMedia, true); function getTargetPlayer() { if (currentVideo && currentVideo.isConnected) { return currentVideo; } const medias = Array.from(document.querySelectorAll('video')); const visibleMedias = medias.filter(m => { const rect = m.getBoundingClientRect(); return rect.width > 10 && rect.height > 10; }); if (visibleMedias.length > 0) { currentVideo = visibleMedias[visibleMedias.length - 1]; return currentVideo; } return null; } function setSpeed(rate) { const player = getTargetPlayer(); if (!player) return false; rate = Math.max(MIN_SPEED, Math.min(MAX_SPEED, Number(rate.toFixed(2)))); player.playbackRate = rate; showTips(`${rate}x`, player); return true; } function toggleResetSpeed() { const player = getTargetPlayer(); if (!player) return false; if (player.playbackRate === 1.0) { setSpeed(lastPlaybackRate); } else { lastPlaybackRate = player.playbackRate; setSpeed(1.0); } return true; } function changeSpeed(delta) { const player = getTargetPlayer(); if (!player) return false; setSpeed(player.playbackRate + delta); return true; } function changeTime(seconds) { const player = getTargetPlayer(); if (!player) return false; player.currentTime += seconds; seekAccumulator += seconds; if (seekResetTimer) clearTimeout(seekResetTimer); seekResetTimer = setTimeout(() => { seekAccumulator = 0; }, 1000); const text = seekAccumulator > 0 ? `+${seekAccumulator}s` : `${seekAccumulator}s`; showTips(text, player); return true; } function setPlaybackRatePlus(num, isRepeat) { const player = getTargetPlayer(); if (!player) return false; num = Number(num); const now = Date.now(); if (!plusInfo[num]) { plusInfo[num] = { time: 0, value: num }; } if (isRepeat || (now - plusInfo[num].time < CLICK_THRESHOLD)) { plusInfo[num].value += num; } else { plusInfo[num].value = num; } plusInfo[num].time = now; setSpeed(plusInfo[num].value); return true; } function tccFullScreen(player) { const host = window.location.host; let selector = ''; if (host.includes('bilibili.com')) selector = '.bpx-player-ctrl-full, .squirtle-video-fullscreen'; else if (host.includes('youtube.com')) selector = '.ytp-fullscreen-button'; else if (host.includes('iqiyi.com')) selector = '.iqp-btn-fullscreen'; else if (host.includes('youku.com')) selector = '.control-fullscreen-icon'; else if (host.includes('qq.com')) selector = 'txpdiv[data-report="window-fullscreen"]'; if (selector) { const btn = document.querySelector(selector); if (btn) { btn.click(); return true; } } return false; } function getContainer(player) { let container = player; let parent = player.parentNode; for (let i = 0; i < 3; i++) { if (!parent || parent.tagName === 'BODY') break; const pRect = parent.getBoundingClientRect(); const vRect = player.getBoundingClientRect(); if (pRect.width <= vRect.width * 1.5 && pRect.height <= vRect.height * 1.5) { container = parent; } parent = parent.parentNode; } return container; } function toggleFullScreen() { const player = getTargetPlayer(); if (!player) return false; if (tccFullScreen(player)) return true; if (document.fullscreenElement) { document.exitFullscreen(); } else { const container = getContainer(player); if (container.requestFullscreen) { container.requestFullscreen(); } else if (player.webkitEnterFullscreen) { player.webkitEnterFullscreen(); } } return true; } function isInteractiveElement(element) { if (!element) return false; const tag = element.tagName ? element.tagName.toUpperCase() : ''; // 移除了 'BUTTON' // 这样当你点击播放器控制栏(它们通常是 BUTTON)时,脚本不会停止工作 if (['INPUT', 'TEXTAREA', 'SELECT'].includes(tag)) return true; if (element.isContentEditable || element.getAttribute('contenteditable') === 'true') return true; const className = (element.className || '').toString().toLowerCase(); if (className.includes('bpx-player-dm-input') || className.includes('reply-box-textarea')) return true; return false; } document.addEventListener('keydown', (e) => { const path = e.composedPath ? e.composedPath() : []; const realTarget = path.length > 0 ? path[0] : e.target; const activeEl = document.activeElement; // 使用优化后的防误触检查 if (isInteractiveElement(realTarget) || isInteractiveElement(activeEl)) { return; } if (e.ctrlKey || e.altKey || e.metaKey) return; const key = e.key.toLowerCase(); let handled = false; switch (key) { case '1': handled = setPlaybackRatePlus(1, e.repeat); break; case '2': handled = setPlaybackRatePlus(2, e.repeat); break; case '3': handled = setPlaybackRatePlus(3, e.repeat); break; case '4': handled = setPlaybackRatePlus(4, e.repeat); break; case 'z': handled = toggleResetSpeed(); break; case 'x': handled = changeSpeed(-STEP_XC); break; case 'c': handled = changeSpeed(STEP_XC); break; case 'enter': handled = toggleFullScreen(); break; case 'arrowright': handled = changeTime(SEEK_STEP_FORWARD); break; case 'arrowleft': handled = changeTime(-SEEK_STEP_REWIND); break; default: handled = false; } if (handled) { e.stopPropagation(); e.preventDefault(); } }, true); })();