YouTube Speed Toggle

Add a speed toggle button to YouTube player controls, switch between 1x and 2x with one click

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

// ==UserScript==
// @name         YouTube Speed Toggle
// @namespace    https://github.com/ywtaoo
// @version      1.0.2
// @description  Add a speed toggle button to YouTube player controls, switch between 1x and 2x with one click
// @author       ywtaoo
// @license      MIT
// @match        https://www.youtube.com/*
// @icon         https://www.youtube.com/favicon.ico
// @homepageURL  https://github.com/ywtaoo/youtube_speed_toggle_shortcut
// @supportURL   https://github.com/ywtaoo/youtube_speed_toggle_shortcut/issues
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const BUTTON_ID = 'yt-speed-toggle-btn';
    const SPEEDS = [1, 2];
    let currentSpeedIndex = 0;
    let observedVideo = null;

    /**
     * Format playback speed for display
     */
    function formatSpeed(speed) {
        return parseFloat(speed.toFixed(2)) + 'x';
    }

    /**
     * Get YouTube player instance
     */
    function getPlayer() {
        return document.querySelector('#movie_player');
    }

    /**
     * Get current playback speed
     */
    function getPlaybackSpeed() {
        const player = getPlayer();
        if (player && typeof player.getPlaybackRate === 'function') {
            const playerSpeed = Number(player.getPlaybackRate());
            if (!Number.isNaN(playerSpeed)) {
                return playerSpeed;
            }
        }

        const video = document.querySelector('video');
        if (video) {
            return video.playbackRate;
        }

        return null;
    }

    /**
     * Set playback speed
     */
    function setPlaybackSpeed(speed) {
        const player = getPlayer();
        const video = document.querySelector('video');

        if (!player && !video) return false;

        if (player && typeof player.setPlaybackRate === 'function') {
            player.setPlaybackRate(speed);
        }

        if (video && video.playbackRate !== speed) {
            video.playbackRate = speed;
        }

        return true;
    }

    /**
     * Listen for speed changes on the current video element
     */
    function observeVideoRateChanges() {
        const video = document.querySelector('video');
        if (!video || video === observedVideo) return;

        if (observedVideo) {
            observedVideo.removeEventListener('ratechange', syncButtonWithVideo);
        }

        observedVideo = video;
        observedVideo.addEventListener('ratechange', syncButtonWithVideo);
    }

    /**
     * Create speed toggle button
     */
    function createSpeedButton() {
        const button = document.createElement('button');
        button.id = BUTTON_ID;
        button.className = 'ytp-button';
        button.title = 'Toggle playback speed';
        button.style.cssText = `
            display: flex;
            align-items: center;
            justify-content: center;
            width: auto;
            min-width: 40px;
            height: 100%;
            padding: 0 8px;
            font-size: 14px;
            font-weight: 500;
            color: #fff;
            opacity: 0.9;
            cursor: pointer;
            transition: opacity 0.1s ease;
            line-height: 1;
        `;
        button.textContent = '1x';

        button.addEventListener('mouseenter', () => {
            button.style.opacity = '1';
        });

        button.addEventListener('mouseleave', () => {
            button.style.opacity = '0.9';
        });

        button.addEventListener('click', (e) => {
            e.stopPropagation();
            toggleSpeed();
        });

        return button;
    }

    /**
     * Toggle playback speed
     */
    function toggleSpeed() {
        currentSpeedIndex = (currentSpeedIndex + 1) % SPEEDS.length;
        const newSpeed = SPEEDS[currentSpeedIndex];

        if (!setPlaybackSpeed(newSpeed)) return;

        updateButtonDisplay(newSpeed);
    }

    /**
     * Update button display
     */
    function updateButtonDisplay(speed) {
        const button = document.getElementById(BUTTON_ID);
        if (button) {
            const speedText = formatSpeed(speed);
            button.textContent = speedText;
            button.title = `Current speed: ${speedText} (click to toggle)`;
        }
    }

    /**
     * Sync button state with current video speed
     */
    function syncButtonWithVideo() {
        const button = document.getElementById(BUTTON_ID);

        if (button) {
            const currentRate = getPlaybackSpeed();
            if (currentRate === null) return;

            const speedIndex = SPEEDS.indexOf(currentRate);

            if (speedIndex !== -1) {
                currentSpeedIndex = speedIndex;
            } else {
                // If current speed is not in preset list, make the next click switch to 2x
                currentSpeedIndex = 0;
            }

            updateButtonDisplay(currentRate);
        }

        observeVideoRateChanges();
    }

    /**
     * Inject button into player controls
     */
    function injectButton() {
        // Check if button already exists
        if (document.getElementById(BUTTON_ID)) {
            syncButtonWithVideo();
            return;
        }

        // Find right controls area (Delhi player uses sub-containers)
        const targetContainer = document.querySelector('.ytp-right-controls-left')
                             || document.querySelector('.ytp-right-controls');
        if (!targetContainer) return;

        const button = createSpeedButton();

        // Insert at the first position of the controls container
        targetContainer.insertBefore(button, targetContainer.firstChild);

        // Sync current video speed
        syncButtonWithVideo();

        // Listen for video speed changes (user may change speed via other methods)
        observeVideoRateChanges();

        console.log('[YouTube Speed Toggle] Button injected');
    }

    /**
     * Initialize script
     */
    function init() {
        // Try to inject immediately
        injectButton();

        // Use MutationObserver to watch for DOM changes
        // YouTube is a SPA, content loads dynamically
        const observer = new MutationObserver((mutations) => {
            // Check if player exists but button doesn't
            const player = document.querySelector('#movie_player');
            const button = document.getElementById(BUTTON_ID);

            if (player && !button) {
                injectButton();
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        // Listen for YouTube SPA navigation
        // YouTube uses History API for page navigation
        const originalPushState = history.pushState;
        history.pushState = function(...args) {
            originalPushState.apply(this, args);
            // Delay execution to wait for new page content to load
            setTimeout(injectButton, 1000);
        };

        const originalReplaceState = history.replaceState;
        history.replaceState = function(...args) {
            originalReplaceState.apply(this, args);
            setTimeout(injectButton, 1000);
        };

        window.addEventListener('popstate', () => {
            setTimeout(injectButton, 1000);
        });

        // Listen for YouTube's yt-navigate-finish event (more reliable)
        window.addEventListener('yt-navigate-finish', () => {
            setTimeout(injectButton, 500);
        });
    }

    // Start script
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();