Greasy Fork

Greasy Fork is available in English.

YouTube 播放速度记忆

记住上次使用的播放速度,并改造YouTube的速度调整滑杆,最高支持8倍速。

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name                Youtube Remember Speed
// @name:zh-TW          YouTube 播放速度記憶
// @name:zh-CN          YouTube 播放速度记忆
// @name:ja             YouTube 再生速度メモリー
// @icon                https://www.youtube.com/img/favicon_48.png
// @author              ElectroKnight22
// @namespace           electroknight22_youtube_remember_playback_rate_namespace
// @version             2.0.0
// @match               *://www.youtube.com/*
// @match               *://www.youtube-nocookie.com/*
// @exclude             *://www.youtube.com/live_chat*
// @grant               GM.getValue
// @grant               GM.setValue
// @grant               GM.deleteValue
// @grant               GM.listValues
// @license             MIT
// @description         Remembers the speed that you last used. Now hijacks YouTube's custom speed slider and gives you up to 8x speed.
// @description:zh-TW   記住上次使用的播放速度,並改造YouTube的速度調整滑桿,最高支援8倍速。
// @description:zh-CN   记住上次使用的播放速度,并改造YouTube的速度调整滑杆,最高支持8倍速。
// @description:ja      最後に使った再生速度を覚えておき、YouTubeの速度スライダーを改造して最大8倍速まで対応させます。
// ==/UserScript==

/*jshint esversion: 11 */

(function() {
    "use strict";

    const DEFAULT_SETTINGS = {
        targetSpeed: 1
    };

    let userSettings = { ...DEFAULT_SETTINGS };
    let shouldInitialize = false;
    const maxSpeed = 8;

    function setSpeed(targetSpeed) {
        try {
            let video = document.querySelector('video');
            video.playbackRate = targetSpeed;
        } catch (error) {
            console.error("Error when trying to set speed. Error: " + error);
        }
    }

    function overrideSpeedElements() {
        if (!window.location.pathname.startsWith('/watch')) return;
        const parentSelector = '.ytp-popup.ytp-settings-menu';
        const sliderSelector = 'input.ytp-input-slider.ytp-speedslider';

        let speedTextElement = null;
        let speedLabel = null

        function setSpeedText(targetString) {
            try {
                const text = speedTextElement.textContent;
                const newValue = targetString;
                speedTextElement.textContent = /\(.*?\)/.test(text)
                    ? text.replace(/\(.*?\)/, `(${newValue})`)
                    : newValue;
            } catch (error) {
                console.error("Error when trying to set speed text. Error: " + error);
            }
        }

        function overrideSpeedLabel(sliderElement) {
            try {
                console.log('overrideSpeedLabel');
                if (!speedLabel || !speedLabel.isConnected) {
                    speedLabel = sliderElement.closest('.ytp-menuitem-with-footer').querySelector('.ytp-menuitem-label');
                }

                if (speedLabel?.textContent) {
                    speedLabel.textContent = speedLabel.textContent.replace(/\(.*?\)/, `(${sliderElement.value})`);
                }
            } catch (error) {
                console.error("Error when trying to override speed label. Error: " + error);
            }
        }

        function overrideSliderStyle(sliderElement) {

            if (!sliderElement) return;

            const speedMenuItems = sliderElement.closest('.ytp-panel-menu').children;
            if (speedMenuItems[0].classList.contains('ytp-menuitem-with-footer')) {
                Array.from(speedMenuItems).forEach(item => { item.setAttribute('aria-checked', 'false'); });
                speedMenuItems[0].setAttribute('aria-checked', 'true');
            }

            overrideSpeedLabel(sliderElement);

            sliderElement.style = `--yt-slider-shape-gradient-percent: ${sliderElement.value / maxSpeed * 100}%;`;
            document.querySelector('.ytp-speedslider-text').textContent = sliderElement.value + 'x';
        }

        function overrideSliderFunction() {
            const sliderElement = parentMenu.querySelector(sliderSelector);
            if (!sliderElement || !shouldInitialize) return;

            overrideSpeedLabel(sliderElement);
            shouldInitialize = false;
            if (sliderElement.initialized) return;

            sliderElement.max = maxSpeed.toString();
            sliderElement.setAttribute('value', userSettings.targetSpeed.toString());
            setSpeed(userSettings.targetSpeed);
            overrideSliderStyle(sliderElement);

            sliderElement.addEventListener('input', () => {
                const newSpeed = parseFloat(sliderElement.value);
                updateSavedSpeed(newSpeed);
                setSpeed(newSpeed);
                overrideSliderStyle(sliderElement)
            });

            // Since we are using the html speed control we should suppress youtube's own speed control to prevent unwanted updates.
            sliderElement.addEventListener('change', (event) => {
                setSpeedText(sliderElement.value);
                event.stopImmediatePropagation();
            }, true);
            sliderElement.initialized = true;
        }

        async function findSpeedTextElement() {
            const youtubeApi = document.querySelector('#movie_player');
            await youtubeApi.setPlaybackRate(1.05);
            const settingItems = document.querySelectorAll('.ytp-menuitem');
            const matchingItem = Array.from(settingItems).find(item =>
                item.textContent.includes('1.05')
            );
            speedTextElement = matchingItem?.querySelector('.ytp-menuitem-content');
        }

        const speedTextObserver = new MutationObserver(initializeSpeedTextElement);
        async function initializeSpeedTextElement() {
            try {
                if (speedTextElement) {
                    if (speedTextElement.initialized) return;
                    const youtubeApi = document.querySelector('#movie_player');
                    youtubeApi?.addEventListener('onPlaybackRateChange', updateSavedSpeed, true);
                    speedTextElement.initialized = true;
                    setSpeedText(userSettings.targetSpeed);
                    speedTextObserver.disconnect();
                } else {
                    const requestedSpeed = userSettings.targetSpeed;
                    await findSpeedTextElement();
                    setSpeed(requestedSpeed);
                    updateSavedSpeed(requestedSpeed);
                    setSpeedText(requestedSpeed);
                }
            } catch (error) {
                console.error("Error when trying to initialize speed text element. Error: " + error);
            }
        }

        const parentMenu = document.querySelector(parentSelector);
        if (parentMenu) {
            const sliderObserver = new MutationObserver(overrideSliderFunction);
            sliderObserver.observe(parentMenu, {
                childList: true,
                subtree: true,
            });
            speedTextObserver.observe(parentMenu, {
                childList: true,
                subtree: true,
                attributes: true,
            });
        } else {
            console.error('Whoops! The parent menu was not found. The script can\'t run. 😥');
        }
    }

    // syncs the user's settings on load
    async function applySettings() {
        try {
            const storedValues = await GM.listValues();

            await Promise.all(Object.entries(DEFAULT_SETTINGS).map(async ([key, value]) => {
                if (!storedValues.includes(key)) {
                    await GM.setValue(key, value);
                }
            }));

            await Promise.all(storedValues.map(async key => {
                if (!(key in DEFAULT_SETTINGS)) {
                    await GM.deleteValue(key);
                }
            }));

            await Promise.all(
                storedValues.map(key => GM.getValue(key).then(value => [key, value]))
            ).then(keyValuePairs => keyValuePairs.forEach(([newKey, newValue]) => {
                userSettings[newKey] = newValue;
            }));

            console.log(Object.entries(userSettings).map(([key, value]) => key + ": " + value).join(", "));
        } catch (error) {
            console.error("Error when applying settings: " + error.message);
        }
    }
    function updateSavedSpeed(speed) {
        userSettings.targetSpeed = speed;
        GM.setValue('targetSpeed', userSettings.targetSpeed);
    }

    function handleNewVideoLoad() {
        shouldInitialize = true;
        setSpeed(userSettings.targetSpeed);
        overrideSpeedElements();
    }

    function main() {
        window.addEventListener("pageshow", () => {
            handleNewVideoLoad();
            window.addEventListener('yt-player-updated', () => {
                handleNewVideoLoad();
            }, true);
        }, true);
    }

    applySettings().then(main);
})();