视频播放增强:1. 长按左键临时加速 2. B站字幕开关记忆 3. 支持更多视频播放器
// ==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=
// ==/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 <span class="speed-arrow">▶▶</span>`;
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();
})();