Greasy Fork

Greasy Fork is available in English.

YouTube 播放速度记忆

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

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

// ==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);
})();