// ==UserScript==
// @name 视频临时倍速+B站字幕开关记忆
// @namespace http://tampermonkey.net/
// @version 2.4
// @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
// ==/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', // 添加 YouTube 选择器
'default': '.video-controls, .progress-bar, [role="slider"]'
},
// 调试模式开关
debug: false
};
// 状态变量
let pressStartTime = 0;
let originalSpeed = 1.0;
let isPressed = false;
let activeVideo = null;
let isLongPress = false;
let preventNextClick = false;
// B站字幕相关变量
let currentVideoId = '';
let want_open = false;
let subtitleCheckTimer = null;
let debounceTimer = null;
let lastSubtitleState = null;
let lastSubtitleCheckTime = 0;
// 添加动画帧ID跟踪
let animationFrameId = null;
let urlObserver = null;
// 调试日志函数
function debugLog(...args) {
if (config.debug) {
console.log(...args);
}
}
// 添加开始检测函数
function startPressDetection() {
if (!animationFrameId) {
function checkPress() {
handlePressDetection();
animationFrameId = requestAnimationFrame(checkPress);
}
checkPress();
}
}
// 添加停止检测函数
function stopPressDetection() {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
}
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);
const indicator = document.querySelector('.speed-indicator');
if (indicator) {
indicator.innerHTML = `当前加速 ${config.speedRate}x <span class="speed-arrow">▶▶</span>`;
}
}
});
// ================ 倍速控制功能 ================
function findVideoElement(element) {
if (!element) return null;
if (element instanceof HTMLVideoElement) {
return element;
}
const domain = window.location.hostname;
// B站和YouTube使用区域限制
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) { // 只在加速时监控
// 增加检查间隔到 100ms
player._speedInterval = setInterval(() => {
if (video.playbackRate !== speed) {
video.playbackRate = speed;
}
}, 100);
// 添加超时清理
setTimeout(() => {
if (player._speedInterval) {
clearInterval(player._speedInterval);
player._speedInterval = null;
}
}, 5000); // 5秒后自动清理
}
video.dispatchEvent(new Event('ratechange'));
} catch (e) {
console.error('设置 YouTube 播放速度失败:', e);
}
}
} else {
video.playbackRate = speed;
}
}
// ================ B站字幕功能 ================
function getVideoId() {
const match = location.pathname.match(/\/video\/(.*?)\//);
return match ? match[1] : '';
}
const browserMode = (function() {
const mode_data = navigator.userAgent;
if (mode_data.includes('Firefox')) {
return 'Firefox';
} else if (mode_data.includes('Chrome')) {
return 'Chrome';
} else return 'Chrome';
})();
function isAiSubtitle() {
let sub = document.querySelector('.bpx-player-ctrl-subtitle-major-inner > .bpx-state-active');
if (sub && sub.innerText.includes("自动")) return true;
return false;
}
function isSubtitleOpen() {
const now = Date.now();
if (lastSubtitleState !== null && now - lastSubtitleCheckTime < 500) {
return lastSubtitleState;
}
let max_length = 3;
if (browserMode === 'Firefox') max_length = 2;
let sub = document.querySelectorAll('svg[preserveAspectRatio="xMidYMid meet"] > defs > filter');
lastSubtitleCheckTime = now;
lastSubtitleState = (sub.length !== max_length);
return lastSubtitleState;
}
function isRememberOpen() {
return GM_getValue('subtitleOpen', false);
}
// 开启字幕 - 添加防重复执行
function openSubtitle() {
// 清除之前的定时器
if (subtitleCheckTimer) {
clearTimeout(subtitleCheckTimer);
subtitleCheckTimer = null;
}
let sub = document.querySelector('[aria-label="字幕"] [class="bpx-common-svg-icon"]');
if (!sub) {
sub = document.querySelector('.bpx-player-ctrl-btn.bpx-player-ctrl-subtitle .bpx-player-ctrl-btn-icon');
}
if (!sub) {
// 如果没找到字幕按钮,延迟重试
subtitleCheckTimer = setTimeout(openSubtitle, 2000);
return;
}
debugLog('尝试开启字幕', isRememberOpen());
const currentState = isSubtitleOpen();
const desiredState = isRememberOpen();
if (currentState !== desiredState && !want_open) {
want_open = true;
setTimeout(() => {
if (sub) sub.click();
want_open = false;
debugLog('已点击字幕按钮');
}, 300);
}
rememberSwitch();
}
// 记忆开关状态回调函数 - 添加防抖
function rememberSwitchCallback(e) {
if (!e.isTrusted) return;
if (debounceTimer) {
clearTimeout(debounceTimer);
}
debounceTimer = setTimeout(() => {
const isOpen = isSubtitleOpen();
GM_setValue('subtitleOpen', isOpen);
debugLog('储存字幕开关状态', isOpen);
debounceTimer = null;
}, 300);
}
// 记忆开关状态 - 优化事件监听
function rememberSwitch() {
let sub = document.querySelector('div[aria-label="字幕"]');
if (!sub) {
sub = document.querySelector('.bpx-player-ctrl-btn.bpx-player-ctrl-subtitle');
}
if (sub && !sub._hasSubtitleListener) {
// 使用属性标记已添加监听器,避免重复添加
sub._hasSubtitleListener = true;
sub.addEventListener('click', rememberSwitchCallback);
debugLog('已添加字幕按钮点击监听');
}
}
function handleMouseDown(e) {
if (e.button !== 0) return;
const domain = window.location.hostname;
let video = null;
let playerArea = null;
// B站和YouTube使用严格区域限制
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 (!video) return;
if (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';
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;
return;
}
}
// 优化视频加载处理 - 合并事件监听
function onVideoLoad() {
if (window.location.hostname !== 'www.bilibili.com') return;
const video = document.querySelector('video');
if (!video) {
setTimeout(onVideoLoad, 1000); // 增加延迟,减少检查频率
return;
}
const newVideoId = getVideoId();
if (newVideoId !== currentVideoId) {
currentVideoId = newVideoId;
// 重置字幕状态缓存
lastSubtitleState = null;
lastSubtitleCheckTime = 0;
// 视频ID变化时,初始化字幕功能
setTimeout(openSubtitle, 1500);
}
if (!video._hasEvents) {
video._hasEvents = true;
const handleVideoEvent = () => {
// 清除之前的定时器
if (subtitleCheckTimer) {
clearTimeout(subtitleCheckTimer);
}
// 延迟检查字幕,避免频繁调用
subtitleCheckTimer = setTimeout(openSubtitle, 1500);
};
// 视频加载和播放时,检查字幕状态
video.addEventListener('loadeddata', handleVideoEvent);
video.addEventListener('play', handleVideoEvent);
}
}
// ================ 初始化 ================
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', onVideoLoad);
} else {
onVideoLoad();
}
// 优化URL变化监听 - 添加节流
let lastUrl = location.href;
let urlChangeTimer = null;
// 清理之前的观察器
if (urlObserver) {
urlObserver.disconnect();
urlObserver = null;
}
urlObserver = new MutationObserver(() => {
const url = location.href;
if (url !== lastUrl) {
// 清除之前的定时器
if (urlChangeTimer) {
clearTimeout(urlChangeTimer);
}
// 延迟处理URL变化,避免频繁触发
urlChangeTimer = setTimeout(() => {
lastUrl = url;
onVideoLoad();
urlChangeTimer = null;
}, 500);
}
});
urlObserver.observe(document, {subtree: true, childList: true});
// 初始化字幕功能 - 优化检测逻辑
let initAttempts = 0;
let initTimer = setInterval(() => {
let k = document.querySelector('div[aria-label="宽屏"]');
initAttempts++;
if (k || initAttempts > 20) {
clearInterval(initTimer);
if (k) openSubtitle();
}
}, 200); // 增加间隔,减少检查频率
}
}
function addSpeedIndicatorStyle() {
const style = document.createElement('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;
}
.speed-arrow {
color: #00a1d6;
margin-left: 2px;
}`;
document.head.appendChild(style);
}
function hideSpeedIndicator() {
const indicator = document.querySelector('.speed-indicator');
if (indicator) {
indicator.style.display = 'none';
}
}
// 清理函数 - 在页面卸载时清理资源
function cleanup() {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
if (subtitleCheckTimer) {
clearTimeout(subtitleCheckTimer);
}
if (debounceTimer) {
clearTimeout(debounceTimer);
}
if (urlObserver) {
urlObserver.disconnect();
}
}
// 注册页面卸载事件
window.addEventListener('unload', cleanup);
initializeEvents();
})();