// ==UserScript== // @name 视频临时倍速+B站字幕开关记忆 // @namespace http://tampermonkey.net/ // @version 2.6 // @description 视频播放增强:1. 长按左键临时加速 2. B站字幕开关记忆 3. 支持更多视频播放器 // @author Alonewinds // @match *://*/* // @exclude *://*/iframe/* // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @run-at document-start // @license MIT // @icon https://s1.aigei.com/src/img/png/a6/a6c975c4efb84ebea1126c902f7daf1f.png?e=2051020800&token=P7S2Xpzfz11vAkASLTkfHN7Fw-oOZBecqeJaxypL:t5hcie9Hw5PjZfuwchVYoN5lrlo= // @downloadURL none // ==/UserScript== (function() { 'use strict'; if (window.location.hostname.includes('bilibili.com') && window.self !== window.top && window.location.hostname !== 'player.bilibili.com') { return; } // 默认配置 const config = { speedRate: GM_getValue('speedRate', 2.0), minPressTime: 200, selectors: { 'www.bilibili.com': '.bpx-player-video-area', 'www.youtube.com': '.html5-video-player', 'default': '.video-controls, .progress-bar, [role="slider"]' }, debug: false }; // 字幕相关常量选择器 const SUBTITLE_SELECTORS = { VIDEO_WRAP: '.bpx-player-video-wrap', VIDEO: 'video', SUBTITLE_BUTTON: '.bpx-player-ctrl-subtitle-result', SUBTITLE_TOGGLE: '.bpx-player-ctrl-btn.bpx-player-ctrl-subtitle', CHINESE_LANGUAGE_OPTION: '.bpx-player-ctrl-subtitle-language-item[data-lan="ai-zh"]', ACTIVE_CHINESE_LANGUAGE: '.bpx-player-ctrl-subtitle-language-item.bpx-state-active[data-lan="ai-zh"]', OFF_SUBTITLE_OPTION: '.bpx-player-ctrl-subtitle-language-item[data-lan="off"]', MAX_RETRIES: 5 }; const TIMING = { INITIAL_SUBTITLE_DELAY: 2000, SUBTITLE_CHECK_INTERVAL: 500, LANGUAGE_CLICK_DELAY: 100 }; // 状态变量 let pressStartTime = 0; let originalSpeed = 1.0; let isPressed = false; let activeVideo = null; let isLongPress = false; let preventNextClick = false; // B站字幕相关变量 let subtitleCheckTimer = null; let animationFrameId = null; let urlObserver = null; let isAutoSetting = false; // 标记是否正在自动设置字幕 // 调试日志函数 function debugLog(...args) { if (config.debug) { console.log(...args); } } // ================ 字幕功能 ================ // 获取全局字幕状态 function getGlobalSubtitleState() { return GM_getValue('globalSubtitleState', true); // 默认开启字幕 } // 保存全局字幕状态 function saveGlobalSubtitleState(isOpen) { GM_setValue('globalSubtitleState', isOpen); debugLog('保存字幕状态:', isOpen); } // 检测字幕是否开启 function isSubtitleOn() { // 方法1:检查激活的中文字幕选项 const activeLanguageItem = document.querySelector(SUBTITLE_SELECTORS.ACTIVE_CHINESE_LANGUAGE); if (activeLanguageItem) { return true; } // 方法2:检查字幕按钮状态 const subtitleBtn = document.querySelector(SUBTITLE_SELECTORS.SUBTITLE_TOGGLE); if (subtitleBtn && subtitleBtn.classList.contains('bpx-state-active')) { return true; } // 方法3:检查是否有可用的中文字幕选项 const chineseOption = document.querySelector(SUBTITLE_SELECTORS.CHINESE_LANGUAGE_OPTION); return !chineseOption; // 如果没有中文字幕选项,说明字幕已开启 } // 设置字幕状态 function setSubtitleState(desiredState) { if (isAutoSetting) return; isAutoSetting = true; let retryCount = 0; const intervalId = setInterval(() => { if (retryCount >= SUBTITLE_SELECTORS.MAX_RETRIES) { clearInterval(intervalId); isAutoSetting = false; return; } retryCount++; const subtitleToggle = document.querySelector(SUBTITLE_SELECTORS.SUBTITLE_TOGGLE); if (!subtitleToggle) return; clearInterval(intervalId); // 检查当前状态 const currentState = isSubtitleOn(); if (currentState === desiredState) { isAutoSetting = false; return; } // 打开字幕面板 subtitleToggle.click(); setTimeout(() => { if (desiredState) { // 开启字幕 const chineseOption = document.querySelector(SUBTITLE_SELECTORS.CHINESE_LANGUAGE_OPTION); if (chineseOption) { chineseOption.click(); debugLog('自动开启字幕'); } } else { // 关闭字幕 const offOption = document.querySelector(SUBTITLE_SELECTORS.OFF_SUBTITLE_OPTION); if (offOption) { offOption.click(); debugLog('自动关闭字幕'); } } setTimeout(() => { isAutoSetting = false; }, 500); }, TIMING.LANGUAGE_CLICK_DELAY); }, TIMING.SUBTITLE_CHECK_INTERVAL); } // 初始化字幕功能 function initSubtitleAutoOpen() { // 初始检查视频元素 checkAndInitVideoListener(); // 监听页面变化,处理视频切换场景 const observer = new MutationObserver(() => { checkAndInitVideoListener(); }); observer.observe(document.body, { childList: true, subtree: true }); // 设置字幕按钮点击监听 setupSubtitleButtonListener(); } // 检查并初始化视频监听器 function checkAndInitVideoListener() { const videoWrapElement = document.querySelector(SUBTITLE_SELECTORS.VIDEO_WRAP); if (!videoWrapElement) return; const videoElement = videoWrapElement.querySelector(SUBTITLE_SELECTORS.VIDEO); if (!videoElement) return; // 移除已存在的事件监听,避免重复绑定 videoElement.removeEventListener('loadeddata', onVideoLoaded); videoElement.addEventListener('loadeddata', onVideoLoaded); } // 视频加载完成处理函数 function onVideoLoaded() { setTimeout(applySubtitleMemory, TIMING.INITIAL_SUBTITLE_DELAY); } // 应用记忆的字幕状态 function applySubtitleMemory() { const rememberedState = getGlobalSubtitleState(); setSubtitleState(rememberedState); } // 设置字幕按钮点击监听 function setupSubtitleButtonListener() { // 使用MutationObserver监听字幕相关元素的变化 const subtitleObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'childList') { mutation.addedNodes.forEach((node) => { if (node.nodeType === 1) { // Element node // 检查是否是字幕面板 if (node.classList && ( node.classList.contains('bpx-player-ctrl-subtitle-panel') || node.querySelector('.bpx-player-ctrl-subtitle-panel') )) { // 字幕面板出现,设置选项点击监听 setTimeout(() => { setupSubtitleOptionListeners(); }, 100); } } }); } }); }); subtitleObserver.observe(document.body, { childList: true, subtree: true }); // 直接监听字幕按钮点击 document.addEventListener('click', (e) => { const subtitleToggle = e.target.closest(SUBTITLE_SELECTORS.SUBTITLE_TOGGLE); if (subtitleToggle && !isAutoSetting) { // 用户点击了字幕按钮,延迟检查状态变化 setTimeout(() => { const currentState = isSubtitleOn(); saveGlobalSubtitleState(currentState); debugLog('用户点击字幕按钮,保存状态:', currentState); }, 1000); } }, true); } // 设置字幕选项点击监听 function setupSubtitleOptionListeners() { const subtitleOptions = document.querySelectorAll([ SUBTITLE_SELECTORS.CHINESE_LANGUAGE_OPTION, SUBTITLE_SELECTORS.OFF_SUBTITLE_OPTION ].join(',')); subtitleOptions.forEach(option => { if (!option._hasListener) { option._hasListener = true; option.addEventListener('click', () => { if (isAutoSetting) return; // 用户点击了字幕选项,保存状态 setTimeout(() => { const currentState = isSubtitleOn(); saveGlobalSubtitleState(currentState); debugLog('用户选择字幕选项,保存状态:', currentState); }, 500); }); } }); } // ================ 倍速控制功能 ================ function findVideoElement(element) { if (!element) return null; if (element instanceof HTMLVideoElement) return element; const domain = window.location.hostname; if (domain === 'www.bilibili.com') { const playerArea = document.querySelector('.bpx-player-video-area'); if (!playerArea?.contains(element)) return null; } else if (domain === 'www.youtube.com') { const ytPlayer = element.closest('.html5-video-player'); if (!ytPlayer?.contains(element)) return null; const video = ytPlayer.querySelector('video'); if (video) return video; } const controlSelector = config.selectors.default; if (element.closest(controlSelector)) return null; const container = element.closest('*:has(video)'); const video = container?.querySelector('video'); return video && window.getComputedStyle(video).display !== 'none' ? video : null; } function setYouTubeSpeed(video, speed) { if (window.location.hostname === 'www.youtube.com') { const player = video.closest('.html5-video-player'); if (player) { try { if (player._speedInterval) { clearInterval(player._speedInterval); player._speedInterval = null; } video.playbackRate = speed; if (speed !== 1.0) { player._speedInterval = setInterval(() => { if (video.playbackRate !== speed) { video.playbackRate = speed; } }, 100); setTimeout(() => { if (player._speedInterval) { clearInterval(player._speedInterval); player._speedInterval = null; } }, 5000); } video.dispatchEvent(new Event('ratechange')); } catch (e) { console.error('设置 YouTube 播放速度失败:', e); } } } else { video.playbackRate = speed; } } function startPressDetection() { if (!animationFrameId) { function checkPress() { handlePressDetection(); animationFrameId = requestAnimationFrame(checkPress); } checkPress(); } } function stopPressDetection() { if (animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId = null; } } function handleMouseDown(e) { if (e.button !== 0) return; const domain = window.location.hostname; let video = null; let playerArea = null; if (domain === 'www.bilibili.com' || domain === 'www.youtube.com') { const selector = config.selectors[domain]; playerArea = document.querySelector(selector); if (!playerArea?.contains(e.target)) return; video = findVideoElement(e.target); } else { video = findVideoElement(e.target); if (video) { playerArea = video.closest('*:has(video)') || video.parentElement; if (!playerArea?.contains(e.target)) return; } } if (!video || video.paused) { hideSpeedIndicator(); return; } pressStartTime = Date.now(); activeVideo = video; originalSpeed = video.playbackRate; isPressed = true; isLongPress = false; preventNextClick = false; startPressDetection(); } function handleMouseUp(e) { if (!isPressed || !activeVideo) return; const pressDuration = Date.now() - pressStartTime; if (pressDuration >= config.minPressTime) { preventNextClick = true; setYouTubeSpeed(activeVideo, originalSpeed); hideSpeedIndicator(); } isPressed = false; isLongPress = false; activeVideo = null; stopPressDetection(); } function handlePressDetection() { if (!isPressed || !activeVideo) return; const pressDuration = Date.now() - pressStartTime; if (pressDuration >= config.minPressTime) { const currentSpeedRate = GM_getValue('speedRate', config.speedRate); if (activeVideo.playbackRate !== currentSpeedRate) { setYouTubeSpeed(activeVideo, currentSpeedRate); } if (!isLongPress) { isLongPress = true; const playerArea = activeVideo.closest('*:has(video)') || activeVideo.parentElement; let indicator = document.querySelector('.speed-indicator'); if (!indicator) { indicator = document.createElement('div'); indicator.className = 'speed-indicator'; indicator.style.pointerEvents = 'none'; playerArea.appendChild(indicator); } indicator.innerHTML = `当前加速 ${currentSpeedRate}x ▶▶`; indicator.style.display = 'block'; } } } function handleClick(e) { if (preventNextClick) { e.stopPropagation(); preventNextClick = false; } } function hideSpeedIndicator() { const indicator = document.querySelector('.speed-indicator'); if (indicator) { indicator.style.display = 'none'; } } function addSpeedIndicatorStyle() { if (document.querySelector('.speed-indicator-style')) return; const style = document.createElement('style'); style.className = 'speed-indicator-style'; style.textContent = ` .speed-indicator { position: absolute; top: 15%; left: 50%; transform: translateX(-50%); background: rgba(0, 0, 0, 0.7); color: white; padding: 5px 10px; border-radius: 4px; z-index: 999999; display: none; font-size: 14px; pointer-events: none; } .speed-arrow { color: #00a1d6; margin-left: 2px; }`; document.head.appendChild(style); } function initializeEvents() { addSpeedIndicatorStyle(); document.addEventListener('mousedown', handleMouseDown, true); document.addEventListener('mouseup', handleMouseUp, true); document.addEventListener('click', handleClick, true); document.addEventListener('mouseleave', handleMouseUp, true); document.addEventListener('fullscreenchange', hideSpeedIndicator); document.addEventListener('webkitfullscreenchange', hideSpeedIndicator); document.addEventListener('mozfullscreenchange', hideSpeedIndicator); document.addEventListener('MSFullscreenChange', hideSpeedIndicator); document.addEventListener('pause', (e) => { if (e.target instanceof HTMLVideoElement) { hideSpeedIndicator(); } }, true); if (window.location.hostname === 'www.bilibili.com') { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { setTimeout(initSubtitleAutoOpen, 1000); }); } else { setTimeout(initSubtitleAutoOpen, 1000); } let lastUrl = location.href; urlObserver = new MutationObserver(() => { const url = location.href; if (url !== lastUrl) { lastUrl = url; setTimeout(() => { checkAndInitVideoListener(); }, 500); } }); urlObserver.observe(document, {subtree: true, childList: true}); } } function cleanup() { if (animationFrameId) cancelAnimationFrame(animationFrameId); if (subtitleCheckTimer) clearTimeout(subtitleCheckTimer); if (urlObserver) urlObserver.disconnect(); } window.addEventListener('unload', cleanup); // 菜单命令 GM_registerMenuCommand('设置倍速值', () => { if (window.self !== window.top && window.location.hostname !== 'player.bilibili.com') return; const newSpeed = prompt('请输入新的倍速值(建议范围:1.1-4):', config.speedRate); if (newSpeed && !isNaN(newSpeed)) { config.speedRate = parseFloat(newSpeed); GM_setValue('speedRate', config.speedRate); } }); initializeEvents(); })();