// ==UserScript==
// @name 视频倍速播放增强版
// @name:en Enhanced Video Speed Controller
// @namespace http://tampermonkey.net/
// @version 1.4.0
// @description 长按右方向键倍速播放,松开恢复原速。按+/-键调整倍速,按]/[键快速调整倍速,按P键恢复默认速度。上/下方向键调节音量,回车键切换全屏。左/右方向键快退/快进5秒。支持YouTube、Bilibili等大多数视频网站。如遇兼容性问题,可在启用脚本后,通过油猴菜单执行“重新扫描以查找视频”。
// @description:en Hold right arrow key for speed playback, release to restore. Press +/- to adjust speed, press ]/[ for quick speed adjustment, press P to restore default speed. Up/Down arrows control volume, Enter toggles fullscreen. Left/Right arrows for 5s rewind/forward. Supports most sites. For compatibility issues, use "Rescan for Videos" from the Tampermonkey menu after enabling the script.
// @author ternece
// @license MIT
// @match *://*.youtube.com/*
// @match *://*.bilibili.com/video/*
// @match *://*/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=greasyforks.org
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_notification
// ==/UserScript==
(function () {
"use strict";
// 默认设置
const DEFAULT_SETTINGS = {
defaultRate: 1.0, // 默认播放速度
targetRate: 2.5, // 长按右键时的倍速
quickRateStep: 0.5, // 按[]键调整速度的步长
targetRateStep: 0.5 // 按 +/- 键调整目标倍速的步长
};
// 通用配置
const CONFIG = {
SEEK_STEP_SECONDS: 5, // 快进/快退的秒数
VOLUME_STEP: 0.1, // 音量调整步长
DEFAULT_FPS: 30, // 默认视频帧率 (用于逐帧操作)
SHORT_PRESS_MAX_COUNT: 3, // 短按判断的按键计数阈值
// 超时与延迟
INIT_RETRY_DELAY: 5000, // 初始化重试延迟
URL_CHANGE_INIT_DELAY: 1000, // URL 变化后初始化延迟
WAIT_FOR_VIDEO_TIMEOUT: 10000, // 等待视频元素超时时间
// 数值限制
MAX_RATE: 16, // 最大允许的播放速度
MAX_QUICK_RATE_STEP: 3 // “快速调速步长”的最大值
};
// 特定网站的配置
const SITE_SPECIFIC_CONFIG = {
'youtube.com': {
mainVideoSelector: '.html5-main-video',
fullscreenButtonSelector: '.ytp-fullscreen-button'
},
'bilibili.com': {
// 宽度大于400px通常是主播放器
mainVideoPredicate: video => video.getBoundingClientRect().width > 400,
// 新版 '.bpx-player-ctrl-full', 旧版 '.bilibili-player-video-btn-fullscreen'
fullscreenButtonSelector: '.bpx-player-ctrl-full, .bilibili-player-video-btn-fullscreen'
}
};
// 显示通知 (保留在外部,因为它依赖 GM_notification)
function showNotification(message) {
if (typeof GM_notification !== 'undefined') {
GM_notification({
text: message,
title: '视频倍速控制器',
timeout: 3000
});
} else {
// 如果 GM_notification 不可用,则使用浮动消息作为备用
showFloatingMessage(message);
}
}
// 显示浮动提示 (保留在外部,因为它是一个独立的UI工具函数)
function showFloatingMessage(message) {
const messageElement = document.createElement("div");
messageElement.textContent = message;
messageElement.style.position = "fixed";
messageElement.style.top = "10px";
messageElement.style.left = "50%";
messageElement.style.transform = "translateX(-50%)";
messageElement.style.backgroundColor = "rgba(0, 0, 0, 0.8)";
messageElement.style.color = "white";
messageElement.style.padding = "8px 16px";
messageElement.style.borderRadius = "4px";
messageElement.style.zIndex = "10000";
messageElement.style.fontFamily = "Arial, sans-serif";
messageElement.style.fontSize = "14px";
messageElement.style.transition = "opacity 0.5s ease-out";
document.body.appendChild(messageElement);
setTimeout(() => {
messageElement.style.opacity = "0";
setTimeout(() => {
document.body.removeChild(messageElement);
}, 500);
}, 2000);
}
// 通用防抖函数
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func.apply(this, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
class VideoController {
constructor() {
// 调试开关
this.DEBUG = false;
// 长按判定时间(毫秒)
this.LONG_PRESS_DELAY = 200; // 长按判定时间(毫秒)
// 从全局加载配置
this.config = CONFIG;
// 获取当前网站的特定配置
this.siteConfig = {};
for (const domain in SITE_SPECIFIC_CONFIG) {
if (window.location.hostname.includes(domain)) {
this.siteConfig = SITE_SPECIFIC_CONFIG[domain];
break;
}
}
// 1. 状态 (State)
this.settings = {
defaultRate: GM_getValue('defaultRate', DEFAULT_SETTINGS.defaultRate),
targetRate: GM_getValue('targetRate', DEFAULT_SETTINGS.targetRate),
quickRateStep: GM_getValue('quickRateStep', DEFAULT_SETTINGS.quickRateStep),
targetRateStep: GM_getValue('targetRateStep', DEFAULT_SETTINGS.targetRateStep)
};
this.tempEnabledDomains = GM_getValue('tempEnabledDomains', []);
this.currentDomain = window.location.hostname;
this.currentUrl = location.href;
this.lastManualRateChangeTime = 0;
this.activeVideo = null;
this.videoControlButtons = new Map();
this.rightKeyTimer = null;
this.downCount = 0;
this.originalRate = 1.0;
this.targetRate = this.settings.targetRate;
this.currentQuickRate = 1.0;
this.keyHandlers = {};
// 监听器和观察器引用
this.keydownListener = null;
this.keyupListener = null;
this.mainObserver = null; // 合并后的主观察器
this.videoChangeObserver = null;
this.activeObservers = new Set();
// 创建防抖版的视频检测函数
this.debouncedDetectAndSetupVideos = debounce(this.detectAndSetupVideos.bind(this), 500);
this._initializeKeyHandlers();
}
// 2. 核心启动与检查逻辑
start() {
// 核心菜单命令应该总是可用,无论脚本是否已在此网站启用
this.registerCoreMenuCommands();
if (!this.shouldEnableScript()) {
// 如果未启用,则只注册(不可用)“启用”命令(已在核心中完成),然后返回
return;
}
// 如果已启用,则注册(不可用)其余的动态菜单命令,并开始初始化
this.registerDynamicMenuCommands();
this.initialize();
}
shouldEnableScript() {
if (this.currentDomain.includes('youtube.com') ||
(this.currentDomain.includes('bilibili.com') && window.location.pathname.includes('/video/'))) {
return true;
}
return this.tempEnabledDomains.includes(this.currentDomain);
}
// 3. 菜单命令注册(不可用)
registerEnableCommand() {
GM_registerMenuCommand('在当前网站启用视频倍速控制', () => {
if (!this.tempEnabledDomains.includes(this.currentDomain)) {
this.tempEnabledDomains.push(this.currentDomain);
GM_setValue('tempEnabledDomains', this.tempEnabledDomains);
showNotification(`已在 ${this.currentDomain} 启用。请刷新页面,若视频仍无法控制,请使用菜单中的“重新扫描”功能。`);
} else {
showNotification(`${this.currentDomain} 已经在启用列表中`);
}
});
}
// 核心菜单命令,应无条件注册(不可用)
registerCoreMenuCommands() {
// 仅在脚本未启用时,才显示“启用”命令
if (!this.shouldEnableScript()) {
this.registerEnableCommand();
}
GM_registerMenuCommand('查看所有临时启用的网站', () => {
if (this.tempEnabledDomains.length === 0) {
showFloatingMessage('当前没有临时启用的网站');
} else {
console.log('--- 视频倍速控制器:临时启用的网站列表 ---');
console.log(this.tempEnabledDomains.join('\n'));
console.log('-------------------------------------------');
showFloatingMessage('临时启用的网站列表已打印到控制台 (F12)');
}
});
}
// 动态菜单命令,仅在脚本启用后注册(不可用)
registerDynamicMenuCommands() {
GM_registerMenuCommand('重新扫描以查找视频', () => {
console.log("执行重新扫描...");
showFloatingMessage('正在重新扫描以查找视频...');
const videos = this.deepFindVideoElements();
if (videos.length > 0) {
this.setupVideos(videos);
showFloatingMessage(`扫描发现 ${videos.length} 个视频!`);
} else {
showFloatingMessage('扫描未发现任何视频。');
}
});
GM_registerMenuCommand('设置默认播放速度', () => this.updateSetting('defaultRate', `请输入默认播放速度 (0.1-${this.config.MAX_RATE})`));
GM_registerMenuCommand('设置长按右键倍速', () => this.updateSetting('targetRate', `请输入长按右键时的倍速 (0.1-${this.config.MAX_RATE})`));
GM_registerMenuCommand('设置快速调速步长', () => this.updateSetting('quickRateStep', `请输入按 [ 或 ] 键调整速度的步长 (0.1-${this.config.MAX_QUICK_RATE_STEP})`, this.config.MAX_QUICK_RATE_STEP));
GM_registerMenuCommand('设置目标倍速调整步长', () => this.updateSetting('targetRateStep', `请输入按 +/- 键调整目标倍速的步长 (0.1-${this.config.MAX_RATE})`));
// 如果当前网站是临时启用的,则提供“移除”选项
if (this.tempEnabledDomains.includes(this.currentDomain)) {
GM_registerMenuCommand('从临时启用列表中移除当前网站', () => {
const index = this.tempEnabledDomains.indexOf(this.currentDomain);
if (index !== -1) {
this.tempEnabledDomains.splice(index, 1);
GM_setValue('tempEnabledDomains', this.tempEnabledDomains);
showNotification(`已从临时启用列表中移除 ${this.currentDomain},请刷新页面`);
}
});
}
}
updateSetting(key, promptMessage, max = this.config.MAX_RATE) {
const newValue = prompt(promptMessage, this.settings[key]);
if (newValue !== null) {
const value = parseFloat(newValue);
if (!isNaN(value) && value >= 0.1 && value <= max) {
this.settings[key] = value;
GM_setValue(key, value);
showFloatingMessage(`设置已更新: ${value}`);
if (key === 'defaultRate' && this.activeVideo) {
this.activeVideo.playbackRate = value;
}
} else {
// 使用浮动消息替代 alert
showFloatingMessage(`设置失败: 请输入有效的值 (0.1-${max})`);
}
}
}
// 4. 初始化流程
async initialize(isRetry = false) {
this.cleanup();
try {
this.activeVideo = await this._findInitialVideo();
console.log("初始化成功, 找到视频:", this.activeVideo);
this._setupPersistentObservers();
this.setupEventListeners();
this.watchUrlChange();
} catch (error) {
console.warn("初始化尝试失败:", error.message);
// 仅在首次尝试时启动重试逻辑
if (!isRetry) {
// 如果是特定错误类型,比如找不到视频,则在一段时间后重试
if (error.type === "no_video" || error.type === "timeout") {
setTimeout(() => this.initialize(true).catch(console.error), this.config.INIT_RETRY_DELAY);
}
}
// 如果是重试失败,则不再继续,避免无限循环
}
}
async _findInitialVideo() {
try {
// 尝试用快速方法找到视频
const video = await this.waitForVideoElement();
if (video) {
this.detectAndSetupVideos(); // 确保视频设置完成
return this.activeVideo || video;
}
} catch (error) {
// 如果快速方法超时或找不到,则尝试深度查找
console.log("快速查找失败,尝试深度查找...");
const deepVideos = this.deepFindVideoElements();
if (deepVideos.length > 0) {
this.setupVideos(deepVideos);
showFloatingMessage(`通过深度查找发现了 ${deepVideos.length} 个视频`);
return deepVideos[0];
}
}
// 如果所有方法都找不到视频,则抛出错误
throw { type: "no_video", message: "在页面上找不到任何视频元素。" };
}
_setupPersistentObservers() {
// 1. 合并 videoObserver 和 urlObserver, 并优化回调
this.mainObserver = new MutationObserver((mutations) => {
// 优先检查 URL 变化,因为它更轻量
if (location.href !== this.currentUrl) {
this.handleUrlChange();
// URL 变化通常意味着页面重载或切换,此时可以先返回,等待 initialize
return;
}
// 检查 DOM 变动
mutations.forEach(mutation => {
// 垃圾回收:检查是否有被管理的视频被移除了
mutation.removedNodes.forEach(removedNode => {
// 检查被移除的节点本身或者其子节点是否是我们正在管理的视频
const videosToRemove = [];
if (this.videoControlButtons.has(removedNode)) {
videosToRemove.push(removedNode);
} else if (removedNode.querySelectorAll) {
removedNode.querySelectorAll('video').forEach(video => {
if (this.videoControlButtons.has(video)) {
videosToRemove.push(video);
}
});
}
videosToRemove.forEach(video => {
console.log("垃圾回收:清理被移除的视频", video);
const button = this.videoControlButtons.get(video);
if (button) button.remove();
this.videoControlButtons.delete(video);
if (this.activeVideo === video) {
this.activeVideo = null;
}
});
});
// 检查是否有新视频被添加
const hasNewVideos = Array.from(mutation.addedNodes).some(n => n.tagName === 'VIDEO' || (n.querySelector && n.querySelector('video')));
if (hasNewVideos) {
console.log("侦测到新视频相关的DOM变动,调用防抖版检测...");
this.debouncedDetectAndSetupVideos();
}
});
});
this.mainObserver.observe(document.body, { childList: true, subtree: true });
this.activeObservers.add(this.mainObserver);
// 2. 观察当前视频的父节点,以便在视频被替换时重新初始化 (保留)
if (this.activeVideo && this.activeVideo.parentElement) {
this.videoChangeObserver = new MutationObserver((mutations) => {
const videoWasRemoved = mutations.some(m => Array.from(m.removedNodes).some(n => n === this.activeVideo));
if (videoWasRemoved) {
console.log("侦测到当前活动视频节点被移除,将重新初始化...");
this.initialize().catch(console.error);
}
});
this.videoChangeObserver.observe(this.activeVideo.parentElement, { childList: true });
this.activeObservers.add(this.videoChangeObserver);
}
}
// 5. 清理与监听
cleanup() {
if (this.keydownListener) {
window.removeEventListener("keydown", this.keydownListener, true);
this.keydownListener = null;
}
if (this.keyupListener) {
window.removeEventListener("keyup", this.keyupListener, true);
this.keyupListener = null;
}
this.activeObservers.forEach(observer => observer.disconnect());
this.activeObservers.clear();
this.videoControlButtons.forEach(button => button.remove());
this.videoControlButtons.clear();
this.activeVideo = null;
}
handleUrlChange() {
this.currentUrl = location.href;
console.log("URL发生变化,重新初始化...");
// 使用 setTimeout 延迟执行,确保新页面的 DOM 元素已加载
setTimeout(() => this.initialize().catch(console.error), this.config.URL_CHANGE_INIT_DELAY);
}
watchUrlChange() {
// MutationObserver 的部分已合并到 mainObserver 中
// 这里只处理 History API 的监听
const handleStateChange = this.handleUrlChange.bind(this);
// 使用 History API 监听
const originalPushState = history.pushState;
const self = this;
history.pushState = function() {
originalPushState.apply(this, arguments);
handleStateChange();
};
const originalReplaceState = history.replaceState;
history.replaceState = function() {
originalReplaceState.apply(this, arguments);
handleStateChange();
};
window.addEventListener('popstate', handleStateChange);
}
// 6. 事件监听器设置
setupEventListeners() {
this.keydownListener = this.handleKeyDown.bind(this);
this.keyupListener = this.handleKeyUp.bind(this);
window.addEventListener("keydown", this.keydownListener, true);
window.addEventListener("keyup", this.keyupListener, true);
}
// 7. 视频查找与设置
waitForVideoElement() {
return new Promise((resolve, reject) => {
const maxAttempts = 20;
let attempts = 0;
const check = () => {
const video = this.detectAndSetupVideos();
if (video) {
observer.disconnect();
resolve(video);
} else if (++attempts >= maxAttempts) {
observer.disconnect();
reject({ type: "no_video" });
}
};
const observer = new MutationObserver(check);
observer.observe(document.body, { childList: true, subtree: true });
this.activeObservers.add(observer);
check(); // 立即检查
setTimeout(() => {
observer.disconnect();
reject({ type: "timeout" });
}, this.config.WAIT_FOR_VIDEO_TIMEOUT);
});
}
deepFindVideoElements() {
console.log('开始深度查找视频元素...');
const foundVideos = new Set();
const find = (element, depth = 0) => {
if (depth > 10) return;
if (element.tagName === 'VIDEO') foundVideos.add(element);
if (element.shadowRoot) find(element.shadowRoot, depth + 1);
if (element.contentDocument) find(element.contentDocument, depth + 1);
Array.from(element.children || []).forEach(child => find(child, depth + 1));
};
find(document.body);
console.log(`深度查找完成,共找到 ${foundVideos.size} 个视频元素`);
return Array.from(foundVideos);
}
detectAndSetupVideos() {
const videos = this.findAllVideos();
if (videos.length === 0) return null;
this.setupVideos(videos);
return this.activeVideo || videos[0];
}
findAllVideos() {
const allVideos = new Set(document.querySelectorAll('video'));
const findIn = (root) => {
try {
root.querySelectorAll('video').forEach(v => allVideos.add(v));
root.querySelectorAll('iframe').forEach(f => {
try {
if (f.contentDocument) findIn(f.contentDocument);
} catch(e) {/* cross-origin */}
});
root.querySelectorAll('*').forEach(el => {
if (el.shadowRoot) findIn(el.shadowRoot);
});
} catch(e) {/* ignore */}
};
findIn(document);
return Array.from(allVideos);
}
setupVideos(videos) {
if (videos.length === 1) {
const video = videos[0];
if (video.readyState >= 1 && !this.activeVideo) {
this.activeVideo = video;
this.setDefaultRate(video);
}
} else if (videos.length > 1) {
// 对于配置了特定规则的网站,进行主视频判断
if (this.siteConfig.mainVideoSelector || this.siteConfig.mainVideoPredicate) {
if (!this.activeVideo || !videos.includes(this.activeVideo)) {
let mainVideo;
// 优先使用 predicate 函数判断
if (this.siteConfig.mainVideoPredicate) {
mainVideo = videos.find(this.siteConfig.mainVideoPredicate);
}
// 如果没有找到,再使用选择器判断
if (!mainVideo && this.siteConfig.mainVideoSelector) {
mainVideo = videos.find(v => v.matches(this.siteConfig.mainVideoSelector));
}
// 如果还是没有,则找一个未暂停的作为补充
if (!mainVideo) {
mainVideo = videos.find(v => !v.paused);
}
this.activeVideo = mainVideo || videos[0];
this.setDefaultRate(this.activeVideo);
}
} else {
// 其他网站,创建控制按钮
videos.forEach((video, index) => {
if (!this.videoControlButtons.has(video) && video.readyState >= 1) {
this.createVideoControlButton(video, index + 1);
this.setDefaultRate(video);
if (!this.activeVideo) this.activeVideo = video;
}
});
}
}
}
setDefaultRate(video) {
if (Date.now() - this.lastManualRateChangeTime > 5000) {
video.playbackRate = this.settings.defaultRate;
}
}
createVideoControlButton(video, index) {
const button = document.createElement('div');
Object.assign(button.style, {
position: 'absolute', top: '10px', left: '10px',
backgroundColor: 'rgba(0, 0, 0, 0.6)', color: 'white',
padding: '5px 10px', borderRadius: '4px', fontSize: '12px',
fontFamily: 'Arial, sans-serif', cursor: 'pointer', zIndex: '9999',
transition: 'background-color 0.3s', userSelect: 'none'
});
// 安全加固:使用 textContent 替代 innerHTML
const textSpan = document.createElement('span');
textSpan.textContent = `视频 ${index}`;
button.appendChild(textSpan);
if (!this.activeVideo) {
this.activeVideo = video;
button.style.backgroundColor = 'rgba(0, 128, 255, 0.8)';
}
button.addEventListener('click', () => {
this.videoControlButtons.forEach(btn => btn.style.backgroundColor = 'rgba(0, 0, 0, 0.6)');
this.activeVideo = video;
button.style.backgroundColor = 'rgba(0, 128, 255, 0.8)';
showFloatingMessage(`已切换到视频 ${index} 控制`);
});
const container = video.parentElement || document.body;
const style = window.getComputedStyle(container);
if (style.position === 'static') container.style.position = 'relative';
container.appendChild(button);
this.videoControlButtons.set(video, button);
}
// 8. 按键事件处理
handleKeyDown(e) {
// 忽略因长按而重复触发的 keydown 事件 (除了右箭头,它有自己的长按逻辑)
if (e.repeat && e.code !== 'ArrowRight') {
return;
}
const path = e.composedPath();
const isInputFocused = path.some(el => el.isContentEditable || ['INPUT', 'TEXTAREA'].includes(el.tagName));
if (isInputFocused || !this.activeVideo) {
return;
}
const handler = this.keyHandlers[e.code];
if (handler) {
e.preventDefault();
e.stopImmediatePropagation();
handler();
}
}
handleKeyUp(e) {
// 拦截空格键的 keyup 事件,防止冲突
if (e.code === 'Space' && this.currentDomain.includes('youtube.com')) {
e.preventDefault();
e.stopImmediatePropagation();
}
if (e.code === 'ArrowRight') {
clearTimeout(this.rightKeyTimer);
this.rightKeyTimer = null;
if (this.downCount < this.config.SHORT_PRESS_MAX_COUNT) { //判定为短按
this.seek(this.config.SEEK_STEP_SECONDS);
} else { //判定为长按
if(this.activeVideo) {
this.activeVideo.playbackRate = this.originalRate;
showFloatingMessage(`恢复播放速度: ${this.originalRate.toFixed(1)}x`);
}
}
this.downCount = 0;
}
}
// 9. 按键处理器和具体功能实现
_initializeKeyHandlers() {
this.keyHandlers = {
// 直接使用 .bind 将函数与参数关联,代码更紧凑
'ArrowUp': this.adjustVolume.bind(this, this.config.VOLUME_STEP),
'ArrowDown': this.adjustVolume.bind(this, -this.config.VOLUME_STEP),
'Enter': this.toggleFullScreen.bind(this),
'Space': this.togglePlayPause.bind(this),
'ArrowLeft': this.seek.bind(this, -this.config.SEEK_STEP_SECONDS),
'ArrowRight': this.handleRightArrowPress.bind(this), // 此函数逻辑复杂,保留原样
'Equal': this.adjustTargetRate.bind(this, this.settings.targetRateStep),
'Minus': this.adjustTargetRate.bind(this, -this.settings.targetRateStep),
'BracketRight': this.adjustPlaybackRate.bind(this, this.settings.quickRateStep),
'BracketLeft': this.adjustPlaybackRate.bind(this, -this.settings.quickRateStep),
'KeyP': this.resetPlaybackRate.bind(this),
'Comma': this.frameStep.bind(this, -1),
'Period': this.frameStep.bind(this, 1),
};
}
// 移除了 _handle... 系列的中间函数,因为它们已被 .bind 替代
adjustVolume(delta) {
this.activeVideo.volume = Math.max(0, Math.min(1, this.activeVideo.volume + delta));
showFloatingMessage(`音量:${Math.round(this.activeVideo.volume * 100)}%`);
}
toggleFullScreen() {
// 优先使用网站特定选择器
if (this.siteConfig.fullscreenButtonSelector) {
const fsButton = document.querySelector(this.siteConfig.fullscreenButtonSelector);
if (fsButton) {
fsButton.click();
return;
}
}
// 通用备用方案:使用原生API
console.log('未找到特定网站的全屏按钮,使用原生API。');
if (!document.fullscreenElement) {
if (this.activeVideo.requestFullscreen) {
this.activeVideo.requestFullscreen();
} else if (this.activeVideo.webkitRequestFullscreen) {
this.activeVideo.webkitRequestFullscreen();
} else if (this.activeVideo.msRequestFullscreen) {
this.activeVideo.msRequestFullscreen();
}
showFloatingMessage('进入全屏');
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
showFloatingMessage('退出全屏');
}
}
togglePlayPause() {
if (this.activeVideo.paused) {
this.activeVideo.play();
showFloatingMessage('播放');
} else {
this.activeVideo.pause();
showFloatingMessage('暂停');
}
}
seek(delta) {
if (this.activeVideo.paused) this.activeVideo.play();
this.activeVideo.currentTime = Math.max(0, this.activeVideo.currentTime + delta);
showFloatingMessage(`快${delta > 0 ? '进' : '退'} ${this.config.SEEK_STEP_SECONDS} 秒`);
}
// 此方法逻辑复杂,保留原名,仅在 handler 中调用
handleRightArrowPress() {
if (this.activeVideo.paused) this.activeVideo.play();
if (this.downCount === 0) {
this.originalRate = this.activeVideo.playbackRate;
this.rightKeyTimer = setTimeout(() => {
this.activeVideo.playbackRate = this.targetRate;
showFloatingMessage(`倍速播放: ${this.targetRate.toFixed(2)}x`);
this.downCount = this.config.SHORT_PRESS_MAX_COUNT; // 设置为长按状态
}, this.LONG_PRESS_DELAY);
}
this.downCount++;
}
adjustTargetRate(delta) {
this.targetRate = Math.max(0.1, Math.min(this.config.MAX_RATE, this.targetRate + delta));
this.lastManualRateChangeTime = Date.now();
showFloatingMessage(`目标倍速设置为: ${this.targetRate.toFixed(2)}x`);
}
adjustPlaybackRate(delta) {
const newRate = Math.max(0.1, Math.min(this.config.MAX_RATE, this.activeVideo.playbackRate + delta));
this.activeVideo.playbackRate = newRate;
this.lastManualRateChangeTime = Date.now();
showFloatingMessage(`播放速度: ${newRate.toFixed(2)}x`);
}
resetPlaybackRate() {
this.activeVideo.playbackRate = this.settings.defaultRate;
this.lastManualRateChangeTime = Date.now();
showFloatingMessage(`播放速度重置为: ${this.settings.defaultRate.toFixed(2)}x`);
}
frameStep(direction) {
if (this.activeVideo.paused) {
this.activeVideo.currentTime += (direction / this.config.DEFAULT_FPS);
showFloatingMessage(direction > 0 ? `下一帧` : `上一帧`);
}
}
}
// 启动脚本
const controller = new VideoController();
controller.start();
})();