YouTube Speed Toggle

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         YouTube Speed Toggle
// @namespace    https://github.com/ywtaoo
// @version      1.0.0
// @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
// @supportURL   https://github.com/ywtaoo/youtube-speed-toggle/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;

    /**
     * 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() {
        const video = document.querySelector('video');
        if (!video) return;

        currentSpeedIndex = (currentSpeedIndex + 1) % SPEEDS.length;
        const newSpeed = SPEEDS[currentSpeedIndex];

        video.playbackRate = newSpeed;
        updateButtonDisplay(newSpeed);
    }

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

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

        if (video && button) {
            const currentRate = video.playbackRate;
            const speedIndex = SPEEDS.indexOf(currentRate);

            if (speedIndex !== -1) {
                currentSpeedIndex = speedIndex;
            } else {
                // If current speed is not in preset list, reset to 1x
                currentSpeedIndex = 0;
            }

            updateButtonDisplay(SPEEDS[currentSpeedIndex]);
        }
    }

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

        // Find right controls area
        const rightControls = document.querySelector('.ytp-right-controls');
        if (!rightControls) return;

        const button = createSpeedButton();

        // Insert at the first position of right controls (before fullscreen button)
        rightControls.insertBefore(button, rightControls.firstChild);

        // Sync current video speed
        syncButtonWithVideo();

        // Listen for video speed changes (user may change speed via other methods)
        const video = document.querySelector('video');
        if (video) {
            video.addEventListener('ratechange', () => {
                syncButtonWithVideo();
            });
        }

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

})();