Greasy Fork

Greasy Fork is available in English.

通用视频倍速控制器

1-9设置对应倍速,0设为0.5倍,+-调整0.5倍;在视频播放器内显示倍速和变速时间;

当前为 2025-08-16 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         通用视频倍速控制器
// @namespace    http://tampermonkey.net/
// @version      20250816.1
// @description  1-9设置对应倍速,0设为0.5倍,+-调整0.5倍;在视频播放器内显示倍速和变速时间;
// @author       atakhalo
// @match        *://*/*
// @grant        none
// @license MIT
// ==/UserScript==

(function () {
	'use strict';

	// 当前控制的视频元素
	let currentVideo = null;
	// 倍速显示元素
	let speedDisplay = null;
	// 时间显示元素
	let timeDisplay = null;
	// 倍速提示的计时器
	let speedDisplayTimer = null;

	// 初始化函数
	function init() {
		// 查找当前视频元素
		findCurrentVideo();
		if (!currentVideo) return;

		// 确保视频容器有定位上下文
		const videoContainer = getVideoContainer(currentVideo);
		if (!videoContainer) return;

		// 创建倍速显示元素
		speedDisplay = document.createElement('div');
		speedDisplay.id = 'custom-speed-display';
		speedDisplay.style.cssText = `
            position: absolute;
            bottom: 60px;
            right: 20px;
            background: rgba(0,0,0,0.7);
            color: #ffcc00;
            font-size: 28px;
            font-weight: bold;
            padding: 10px 20px;
            border-radius: 8px;
            z-index: 1000;
            display: none;
            pointer-events: none;
            box-shadow: 0 0 15px rgba(255, 204, 0, 0.5);
            transition: opacity 0.3s;
        `;
		videoContainer.appendChild(speedDisplay);

		// 创建时间显示元素
		timeDisplay = document.createElement('div');
		timeDisplay.id = 'custom-time-display';
		timeDisplay.style.cssText = `
            position: absolute;
            top: 20px;
            right: 20px;
            background: rgba(0,0,0,0.7);
            color: white;
            font-size: 16px;
            padding: 8px 15px;
            border-radius: 6px;
            z-index: 1000;
            pointer-events: none;
            box-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
        `;
		videoContainer.appendChild(timeDisplay);

		// 监听键盘事件
		document.addEventListener('keydown', handleKeyPress, true); // true会吞掉点击,避免触发网站的快捷键

		// 监听页面变化,以便在动态加载视频时也能工作
		const observer = new MutationObserver(() => {
			if (!currentVideo || !document.contains(currentVideo)) {
				findCurrentVideo();
				if (currentVideo) {
					moveDisplaysToVideoContainer();
				}
			}
		});
		observer.observe(document, { childList: true, subtree: true });

		// 每秒更新一次时间显示
		setInterval(updateTimeDisplay, 1000);

		// 初始更新时间显示
		updateTimeDisplay();
	}

	// 获取视频容器
	function getVideoContainer(video) {
		// 尝试找到合适的容器
		let container = video.parentElement;
		while (container) {
			if (container.tagName === 'BODY') break;
			const style = window.getComputedStyle(container);
			if (style.position !== 'static') {
				return container;
			}
			container = container.parentElement;
		}
		// 如果没有定位容器,使用body
		return document.body;
	}

	// 移动显示元素到视频容器
	function moveDisplaysToVideoContainer() {
		if (!currentVideo) return;

		const videoContainer = getVideoContainer(currentVideo);
		if (!videoContainer) return;

		if (speedDisplay && speedDisplay.parentNode !== videoContainer) {
			if (speedDisplay.parentNode) {
				speedDisplay.parentNode.removeChild(speedDisplay);
			}
			videoContainer.appendChild(speedDisplay);
		}

		if (timeDisplay && timeDisplay.parentNode !== videoContainer) {
			if (timeDisplay.parentNode) {
				timeDisplay.parentNode.removeChild(timeDisplay);
			}
			videoContainer.appendChild(timeDisplay);
		}
	}

	// 查找当前视频元素
	function findCurrentVideo() {
		// 优先选择正在播放的视频
		const playingVideos = Array.from(document.querySelectorAll('video')).filter(v => !v.paused);
		if (playingVideos.length > 0) {
			currentVideo = playingVideos[0];
			return;
		}

		// 其次选择有焦点的视频
		const focusedVideo = document.querySelector('video:focus');
		if (focusedVideo) {
			currentVideo = focusedVideo;
			return;
		}

		// 最后选择页面上第一个视频
		const firstVideo = document.querySelector('video');
		if (firstVideo) {
			currentVideo = firstVideo;
		}
	}

	// 处理按键事件
	function handleKeyPress(e) {
		if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)
			return;

		// 如果焦点在可输入元素上,则不处理
		if (['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) {
			return;
		}

		// 查找当前视频
		findCurrentVideo();
		if (!currentVideo) return;

		// 确保显示元素在正确的容器中
		moveDisplaysToVideoContainer();

		let newSpeed = currentVideo.playbackRate;
		let speedUpdate = true;

		// 数字键1-9: 设置对应倍速
		if (e.key >= '1' && e.key <= '9') {
			newSpeed = parseInt(e.key);
		}
		// 数字0: 设置为0.5倍速
		else if (e.key === '0') {
			newSpeed = 0.5;
		}
		// +键: 增加0.5倍速
		else if (e.key === '+' || e.key === '=') {
			newSpeed = Math.min(currentVideo.playbackRate + 0.5, 16);
		}
		// -键: 减少0.5倍速
		else if (e.key === '-' || e.key === '_') {
			newSpeed = Math.max(currentVideo.playbackRate - 0.5, 0.1);
		}
		// *键: x2倍速
		else if (e.key === '*' || e.key === ']') {
			console.log(currentVideo.playbackRate * 2)
			newSpeed = Math.min(Math.ceil(currentVideo.playbackRate * 2), 16);
		}
		// /键: /2倍速
		else if (e.key === '/' || e.key === '[') {
			newSpeed = Math.max(currentVideo.playbackRate / 2, 0.1);
		}
		// 其他按键忽略
		else {
			return;
		}

		// 应用新速度
		currentVideo.playbackRate = newSpeed;

		// 显示倍速提示
		showSpeedDisplay(newSpeed);

		// 更新右上角时间显示
		updateTimeDisplay();

		// 快捷键生效时不再传播
		// 1. 阻止默认行为
		e.preventDefault();
		// 2. 停止事件传播
		e.stopImmediatePropagation();
		// 3. 停止事件在DOM树中进一步传播
		e.stopPropagation();
	}

	// 显示倍速提示
	function showSpeedDisplay(speed) {
		if (!speedDisplay) return;

		speedDisplay.textContent = speed.toFixed(1) + 'x';
		speedDisplay.style.display = 'block';
		speedDisplay.style.opacity = '1';

		// 清除之前的计时器
		if (speedDisplayTimer) clearTimeout(speedDisplayTimer);

		// 1秒后淡出
		speedDisplayTimer = setTimeout(() => {
			speedDisplay.style.opacity = '0';
			setTimeout(() => {
				speedDisplay.style.display = 'none';
			}, 300);
		}, 1000);
	}

	// 更新右上角时间显示(显示变速后时间)
	function updateTimeDisplay() {
		if (!currentVideo || !timeDisplay || isNaN(currentVideo.duration)) {
			if (timeDisplay) timeDisplay.style.display = 'none';
			return;
		}

		// 计算变速后的时间
		const speed = currentVideo.playbackRate;
		const actualCurrentTime = currentVideo.currentTime;
		const actualDuration = currentVideo.duration;

		// 变速后时间 = 原时间 / 速度
		const adjustedCurrentTime = actualCurrentTime / speed;
		const adjustedDuration = actualDuration / speed;

		// 格式化时间 (秒 -> mm:ss)
		function formatTime(seconds) {
			const min = Math.floor(seconds / 60);
			const sec = Math.floor(seconds % 60);
			return `${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`;
		}

		const current = formatTime(adjustedCurrentTime);
		const total = formatTime(adjustedDuration);

		timeDisplay.textContent = `${current} / ${total} (${speed.toFixed(1)}x)`;
		timeDisplay.style.display = 'block';
	}

	// 页面加载完成后初始化
	if (document.readyState === 'complete') {
		init();
	} else {
		window.addEventListener('load', init);
	}
})();