Better YouTube Video Controls

Enhanced YouTube playback controls with privacy-focused features. Hold right arrow for fast playback, left for slow-mo, and track your last 5 videos locally (no data sent to servers).

As of 2025-04-07. See the latest version.

// ==UserScript==
// @name         Better YouTube Video Controls
// @namespace    http://tampermonkey.net/
// @version      1.5.0
// @description  Enhanced YouTube playback controls with privacy-focused features. Hold right arrow for fast playback, left for slow-mo, and track your last 5 videos locally (no data sent to servers).
// @author       Henry Suen
// @match        *://*.youtube.com/*
// @license      MIT
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_deleteValue
// ==/UserScript==

(function() {
    'use strict';
    
    // --- USER CONFIGURABLE SETTINGS ---
    
    // Get user settings or use defaults
    let HOLD_RIGHT_PLAYBACK_SPEED = GM_getValue('holdRightPlaybackSpeed', 2.5);
    let HOLD_LEFT_PLAYBACK_SPEED = GM_getValue('holdLeftPlaybackSpeed', 0.2);
    let LONG_PRESS_THRESHOLD = GM_getValue('longPressThreshold', 250); // Default 250ms
    let SKIP_SECONDS = GM_getValue('skipSeconds', 5); // Default 5 seconds
    let RESUME_WATCHING_ENABLED = GM_getValue('resumeWatchingEnabled', true); // Default to on
    const MAX_HISTORY_SIZE = 5; // Number of videos to remember
    
    // Register menu commands for user configuration
    GM_registerMenuCommand('Set Right Arrow Hold Speed (Fast)', function() {
        const newSpeed = parseFloat(prompt('Enter playback speed when holding right arrow (e.g., 2.0, 3.0):', HOLD_RIGHT_PLAYBACK_SPEED));
        if (!isNaN(newSpeed) && newSpeed > 0) {
            HOLD_RIGHT_PLAYBACK_SPEED = newSpeed;
            GM_setValue('holdRightPlaybackSpeed', newSpeed);
            alert(`Right arrow hold speed set to ${newSpeed}x`);
        } else {
            alert('Invalid value. Please enter a positive number.');
        }
    });
    
    GM_registerMenuCommand('Set Left Arrow Hold Speed (Slow)', function() {
        const newSpeed = parseFloat(prompt('Enter playback speed when holding left arrow (e.g., 0.5, 0.75):', HOLD_LEFT_PLAYBACK_SPEED));
        if (!isNaN(newSpeed) && newSpeed > 0 && newSpeed <= 1) {
            HOLD_LEFT_PLAYBACK_SPEED = newSpeed;
            GM_setValue('holdLeftPlaybackSpeed', newSpeed);
            alert(`Left arrow hold speed set to ${newSpeed}x`);
        } else {
            alert('Invalid value. Please enter a number between 0 and 1.');
        }
    });
    
    GM_registerMenuCommand('Set Skip Seconds', function() {
        const newSkip = parseInt(prompt('Enter seconds to skip on right/left arrow tap:', SKIP_SECONDS));
        if (!isNaN(newSkip) && newSkip > 0) {
            SKIP_SECONDS = newSkip;
            GM_setValue('skipSeconds', newSkip);
            alert(`Skip seconds set to ${newSkip}`);
        } else {
            alert('Invalid value. Please enter a positive number.');
        }
    });
    
    GM_registerMenuCommand('Toggle Resume Watching: ' + (RESUME_WATCHING_ENABLED ? 'ON' : 'OFF'), function() {
        RESUME_WATCHING_ENABLED = !RESUME_WATCHING_ENABLED;
        GM_setValue('resumeWatchingEnabled', RESUME_WATCHING_ENABLED);
        alert(`Resume Watching: ${RESUME_WATCHING_ENABLED ? 'Enabled ✓' : 'Disabled ✗'}`);
        // Refresh menu command label
        GM_registerMenuCommand('Toggle Resume Watching: ' + (RESUME_WATCHING_ENABLED ? 'ON' : 'OFF'), arguments.callee);
    });
    
    GM_registerMenuCommand('Clear Saved Video History', function() {
        clearVideoHistory();
        alert('Video history has been cleared.');
    });
    
    // --- END OF USER CONFIGURABLE SETTINGS ---
    
    // Store the original playback rate
    let originalPlaybackRate = 1.0;
    // Flag to track if we're handling a long press
    let isLongPress = false;
    // Track when the key was pressed
    let keyDownTime = 0;
    // Flag to ensure we only process one keydown at a time
    let keyAlreadyDown = false;
    // Track which key is being held
    let activeKey = null;
    // Reference to our speed indicator element
    let speedIndicator = null;
    // Reference to our timeout for detecting long press
    let longPressTimeout = null;
    // Our own UI indicator for actions
    let actionIndicator = null;
    // Timeout for hiding the action indicator
    let hideActionTimeout = null;
    // Variables for tracking video position
    let currentVideoId = null;
    let positionSaveInterval = null;
    let lastSavedPosition = 0;
    // Store the element that had focus before we blurred the progress bar
    let lastActiveElement = null;
    // Flag to track if resume was skipped due to timestamp in URL
    let resumeSkippedDueToTimestamp = false;
    // Debug mode for troubleshooting
    const DEBUG = false;
    
    // --- Utility Functions ---
    
    // Debug logging function
    function debugLog(...args) {
        if (DEBUG) {
            console.log('[YT Controls]', ...args);
        }
    }
    
    // Get video history array
    function getVideoHistory() {
        const history = GM_getValue('videoHistory', []);
        return Array.isArray(history) ? history : [];
    }
    
    // Save video history array
    function saveVideoHistory(history) {
        GM_setValue('videoHistory', history);
    }
    
    // Add a video to the history
    function addToVideoHistory(videoId) {
        if (!videoId) return;
        
        let history = getVideoHistory();
        
        // Remove the video ID if it's already in the history to avoid duplicates
        history = history.filter(item => item !== videoId);
        
        // Add the video ID to the beginning of the array
        history.unshift(videoId);
        
        // Limit the history size
        if (history.length > MAX_HISTORY_SIZE) {
            history = history.slice(0, MAX_HISTORY_SIZE);
        }
        
        // Save the updated history
        saveVideoHistory(history);
        debugLog('Updated video history:', history);
    }
    
    // Clear all saved video history
    function clearVideoHistory() {
        GM_setValue('videoHistory', []);
        
        // Also clear all position values
        const allKeys = GM_listValues ? GM_listValues() : [];
        for (const key of allKeys) {
            if (key.startsWith('video_pos_')) {
                GM_deleteValue(key);
            }
        }
        
        debugLog('Video history cleared');
    }
    
    // Extract video ID from YouTube URL using multiple methods
    function getVideoId() {
        try {
            // Method 1: Using URLSearchParams (most reliable)
            const urlParams = new URLSearchParams(window.location.search);
            const vParam = urlParams.get('v');
            if (vParam) {
                debugLog('Video ID from URL params:', vParam);
                return vParam;
            }
            
            // Method 2: Try regex on full URL
            const url = window.location.href;
            const regex1 = /(?:v=|\/v\/|youtu\.be\/|\/embed\/)([a-zA-Z0-9_-]{11})/;
            const match1 = url.match(regex1);
            if (match1 && match1[1]) {
                debugLog('Video ID from URL regex:', match1[1]);
                return match1[1];
            }
            
            // Method 3: Look for it in page metadata
            const metaTag = document.querySelector('meta[property="og:video:url"], meta[itemprop="videoId"]');
            if (metaTag && metaTag.content) {
                const metaUrl = metaTag.content;
                const metaMatch = metaUrl.match(/([a-zA-Z0-9_-]{11})/);
                if (metaMatch && metaMatch[1]) {
                    debugLog('Video ID from meta tag:', metaMatch[1]);
                    return metaMatch[1];
                }
            }
            
            // Method 4: Look for video ID in the page content
            const pageContent = document.documentElement.innerHTML;
            const videoIdMatch = pageContent.match(/"videoId"\s*:\s*"([a-zA-Z0-9_-]{11})"/);
            if (videoIdMatch && videoIdMatch[1]) {
                debugLog('Video ID from page content:', videoIdMatch[1]);
                return videoIdMatch[1];
            }
            
            debugLog('Could not find video ID');
            return null;
        } catch (e) {
            console.error('Error in getVideoId:', e);
            return null;
        }
    }
    
    // Make video ID safe for storage as a key
    function getSafeVideoKey(videoId) {
        if (!videoId) return null;
        
        // Encode the video ID for safety as a key
        return 'video_pos_' + encodeURIComponent(videoId);
    }
    
    // Check if URL has a timestamp parameter
    function hasTimestampInUrl() {
        const url = window.location.href;
        return url.includes("&t=") || url.includes("?t=");
    }
    
    // Extract timestamp value from URL
    function getTimestampFromUrl() {
        const url = window.location.href;
        const regex = /[?&]t=([0-9hms]+)/;
        const match = url.match(regex);
        
        if (!match) return 0;
        
        const value = match[1];
        
        // Handle numeric seconds format (e.g., t=120)
        if (/^\d+$/.test(value)) {
            return parseInt(value);
        }
        
        // Handle YouTube's time format (e.g., 1h2m3s)
        let seconds = 0;
        const hours = value.match(/(\d+)h/);
        const minutes = value.match(/(\d+)m/);
        const secs = value.match(/(\d+)s/);
        
        if (hours) seconds += parseInt(hours[1]) * 3600;
        if (minutes) seconds += parseInt(minutes[1]) * 60;
        if (secs) seconds += parseInt(secs[1]);
        
        return seconds;
    }
    
    // Save current video position
    function saveVideoPosition() {
        // Skip if resume watching is disabled
        if (!RESUME_WATCHING_ENABLED) return;
        
        const video = findYouTubeVideo();
        if (!video) return;
        
        const videoId = getVideoId();
        if (!videoId) return;
        
        const safeKey = getSafeVideoKey(videoId);
        if (!safeKey) return;
        
        // Only update if position changed significantly (more than 1 second)
        if (Math.abs(video.currentTime - lastSavedPosition) > 1) {
            lastSavedPosition = video.currentTime;
            
            // Add to video history
            addToVideoHistory(videoId);
            
            // Save position
            GM_setValue(safeKey, video.currentTime);
            debugLog('Saved position', videoId, video.currentTime);
        }
    }
    
    // Start tracking video position
    function startPositionTracking() {
        // Skip if resume watching is disabled
        if (!RESUME_WATCHING_ENABLED) return;
        
        // Clear any existing interval
        if (positionSaveInterval) {
            clearInterval(positionSaveInterval);
        }
        
        // Set up the new interval
        positionSaveInterval = setInterval(saveVideoPosition, 5000);
        
        // Update the current video ID
        currentVideoId = getVideoId();
        
        // Add to history immediately
        if (currentVideoId) {
            addToVideoHistory(currentVideoId);
            debugLog('Started tracking', currentVideoId);
        }
    }
    
    // Restore video position
    function restoreVideoPosition() {
        // Skip if resume watching is disabled
        if (!RESUME_WATCHING_ENABLED) return;
        
        const videoId = getVideoId();
        if (!videoId) return;
        
        debugLog('Attempting to restore position for', videoId);
        
        // Check if URL has a timestamp
        if (hasTimestampInUrl()) {
            resumeSkippedDueToTimestamp = true;
            // Show notification that resume was skipped
            showActionIndicator("Resume skipped: Timestamp in URL", 3000);
            debugLog('Resume skipped due to timestamp in URL');
            return;
        }
        
        // Reset the flag since there's no timestamp
        resumeSkippedDueToTimestamp = false;
        
        const safeKey = getSafeVideoKey(videoId);
        if (!safeKey) return;
        
        // Get the saved position
        const savedPosition = GM_getValue(safeKey, 0);
        
        debugLog('Retrieved saved position:', savedPosition);
        
        if (savedPosition > 0) {
            const video = findYouTubeVideo();
            if (video) {
                // Don't resume if we're near the start or very close to where we left off
                if (video.currentTime < 3 && savedPosition > 5) {
                    video.currentTime = savedPosition;
                    
                    // Update YouTube's internal state to be aware of our time change
                    const ytplayer = findYouTubePlayer();
                    if (ytplayer && typeof ytplayer.seekTo === 'function') {
                        try {
                            ytplayer.seekTo(savedPosition, true);
                        } catch(e) {
                            debugLog('Error in ytplayer.seekTo', e);
                        }
                    }
                    
                    // Show a notification that we've resumed
                    showActionIndicator(`Resumed at ${formatTime(savedPosition)}`, 3000);
                    debugLog('Resumed to', savedPosition);
                }
            }
        }
    }
    
    // Format time in MM:SS format
    function formatTime(seconds) {
        const minutes = Math.floor(seconds / 60);
        const remainingSeconds = Math.floor(seconds % 60);
        return `${minutes}:${remainingSeconds < 10 ? '0' : ''}${remainingSeconds}`;
    }
    
    // Find video element on YouTube
    function findYouTubeVideo() {
        return document.querySelector('#movie_player video');
    }
    
    // Find YouTube player element
    function findYouTubePlayer() {
        return document.querySelector('#movie_player');
    }
    
    // Utility function to check if an element is an input field
    function isInputField(element) {
        if (!element) return false;
        const tagName = element.tagName.toLowerCase();
        const type = (element.type || '').toLowerCase();
        
        return (tagName === 'input' && 
                ['text', 'password', 'email', 'number', 'search', 'tel', 'url'].includes(type)) || 
               tagName === 'textarea' || 
               element.isContentEditable;
    }
    
    // Check if focus is on progress bar
    function isProgressBarFocused() {
        // There are multiple elements that make up the progress bar
        const progressBar = document.querySelector('.ytp-progress-bar');
        const scrubber = document.querySelector('.ytp-scrubber-container');
        const progressList = document.querySelectorAll('.ytp-progress-list');
        
        if (document.activeElement === progressBar || 
            document.activeElement === scrubber ||
            (progressList && Array.from(progressList).includes(document.activeElement))) {
            return true;
        }
        
        // Check for aria attributes that might indicate focus on progress controls
        const activeElement = document.activeElement;
        if (activeElement && (
            activeElement.getAttribute('aria-valuemin') !== null ||
            activeElement.getAttribute('aria-valuemax') !== null ||
            activeElement.getAttribute('role') === 'slider'
        )) {
            // Check if it's in the player controls
            const isInControls = activeElement.closest('.ytp-chrome-bottom');
            return isInControls !== null;
        }
        
        return false;
    }
    
    // Remove focus from progress bar and trigger volume control
    function handleVolumeKeyOnProgressBar(isVolumeUp) {
        if (isProgressBarFocused()) {
            // Store the active element so we can restore it later
            lastActiveElement = document.activeElement;
            
            // Blur the progress bar
            if (lastActiveElement && lastActiveElement.blur) {
                lastActiveElement.blur();
            }
            
            // Move focus to the player itself
            const player = findYouTubePlayer();
            if (player && player.focus) {
                player.focus();
            }
            
            // Create and dispatch a synthetic key event to trigger YouTube's volume control
            // We do this after ensuring focus is off the progress bar
            setTimeout(() => {
                const event = new KeyboardEvent('keydown', {
                    key: isVolumeUp ? 'ArrowUp' : 'ArrowDown',
                    code: isVolumeUp ? 'ArrowUp' : 'ArrowDown',
                    keyCode: isVolumeUp ? 38 : 40,
                    which: isVolumeUp ? 38 : 40,
                    bubbles: true,
                    cancelable: true,
                    composed: true
                });
                
                // Dispatch to player element to ensure YouTube's volume control is triggered
                if (player) {
                    player.dispatchEvent(event);
                } else {
                    // Fallback to document if player can't be found
                    document.dispatchEvent(event);
                }
            }, 10);
            
            return true;
        }
        return false;
    }
    
    // --- UI Elements ---
    
    // Create the speed indicator UI element
    function createSpeedIndicator() {
        // Remove any existing indicator first
        removeSpeedIndicator();
        
        // Create a new indicator
        speedIndicator = document.createElement('div');
        speedIndicator.id = 'speed-indicator';
        speedIndicator.textContent = '1x'; // Default text - will be updated when shown
        
        // Style the indicator with larger text
        const style = speedIndicator.style;
        style.position = 'absolute';
        style.right = '20px';
        style.top = '20px';
        style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
        style.color = 'white';
        style.padding = '8px 16px'; // Larger padding
        style.borderRadius = '6px';
        style.fontSize = '28px'; // Large font size
        style.fontWeight = 'bold';
        style.zIndex = '9999';
        style.display = 'none'; // Hidden by default
        style.opacity = '0';
        style.transition = 'opacity 0.3s ease';
        
        // Add it to the player
        const player = findYouTubePlayer();
        if (player) {
            player.appendChild(speedIndicator);
        } else {
            document.body.appendChild(speedIndicator);  // Fallback
        }
        
        return speedIndicator;
    }
    
    // Create action indicator for volume and skip
    function createActionIndicator() {
        if (actionIndicator) {
            return actionIndicator;
        }
        
        actionIndicator = document.createElement('div');
        actionIndicator.id = 'action-indicator';
        
        // Style the action indicator
        const style = actionIndicator.style;
        style.position = 'absolute';
        style.left = '50%';
        style.top = '50%';
        style.transform = 'translate(-50%, -50%)';
        style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
        style.color = 'white';
        style.padding = '12px 20px';
        style.borderRadius = '8px';
        style.fontSize = '24px';
        style.fontWeight = 'bold';
        style.zIndex = '10000';
        style.display = 'none';
        style.opacity = '0';
        style.transition = 'opacity 0.3s ease';
        
        // Add it to the player
        const player = findYouTubePlayer();
        if (player) {
            player.appendChild(actionIndicator);
        } else {
            document.body.appendChild(actionIndicator);
        }
        
        return actionIndicator;
    }
    
    // Show the speed indicator with a fade-in effect
    function showSpeedIndicator(speed) {
        if (!speedIndicator) {
            speedIndicator = createSpeedIndicator();
        }
        speedIndicator.textContent = speed + 'x'; // Update with current speed
        speedIndicator.style.display = 'block';
        setTimeout(() => {
            speedIndicator.style.opacity = '1';
        }, 10);  // Small delay to ensure the transition works
    }
    
    // Hide the speed indicator with a fade-out effect
    function hideSpeedIndicator() {
        if (speedIndicator) {
            speedIndicator.style.opacity = '0';
            setTimeout(() => {
                speedIndicator.style.display = 'none';
            }, 300);  // Wait for the transition to complete
        }
    }
    
    // Remove the speed indicator completely
    function removeSpeedIndicator() {
        if (speedIndicator && speedIndicator.parentNode) {
            speedIndicator.parentNode.removeChild(speedIndicator);
            speedIndicator = null;
        }
    }
    
    // Show action indicator
    function showActionIndicator(text, duration = 1000) {
        // Clear any existing hide timeout
        if (hideActionTimeout) {
            clearTimeout(hideActionTimeout);
            hideActionTimeout = null;
        }
        
        const indicator = createActionIndicator();
        
        // If indicator is already visible, just update text without the fade-out/fade-in
        if (indicator.style.opacity === '1') {
            indicator.textContent = text;
        } else {
            indicator.textContent = text;
            indicator.style.display = 'block';
            
            // Use a timeout to ensure the transition works
            setTimeout(() => {
                indicator.style.opacity = '1';
            }, 10);
        }
        
        // Set a new timeout to hide the indicator
        hideActionTimeout = setTimeout(() => {
            indicator.style.opacity = '0';
            setTimeout(() => {
                indicator.style.display = 'none';
            }, 300);
        }, duration);
    }
    
    // --- Action Functions ---
    
    // Function to perform a seek forward or backward
    function performSeek(forward = true) {
        const video = findYouTubeVideo();
        if (!video) return false;
        
        // Calculate new time
        const currentTime = video.currentTime;
        const newTime = currentTime + (forward ? SKIP_SECONDS : -SKIP_SECONDS);
        const finalTime = Math.max(0, newTime);
        
        // Update both the HTML5 video element and YouTube's internal state
        video.currentTime = finalTime;
        
        // Try to sync with YouTube's API
        const player = findYouTubePlayer();
        if (player && typeof player.seekTo === 'function') {
            try {
                player.seekTo(finalTime, true);
            } catch(e) {}
        }
        
        // Show our custom UI indicator
        showActionIndicator(`${forward ? 'Forward' : 'Backward'} ${SKIP_SECONDS}s`);
        return true;
    }
    
    // Function to change playback speed
    function changePlaybackSpeed(speed) {
        const video = findYouTubeVideo();
        if (!video) return false;
        
        // Store original rate on first change
        if (originalPlaybackRate === 1.0) {
            originalPlaybackRate = video.playbackRate;
        }
        
        // Set new speed
        video.playbackRate = speed;
        
        // Also try to set through YouTube's API if available
        const player = findYouTubePlayer();
        if (player && typeof player.setPlaybackRate === 'function') {
            try {
                player.setPlaybackRate(speed);
            } catch(e) {}
        }
        
        // Show indicator
        showSpeedIndicator(speed);
        
        return true;
    }
    
    // Function to reset playback speed
    function resetPlaybackSpeed() {
        const video = findYouTubeVideo();
        if (!video) return false;
        
        // Reset to original speed
        video.playbackRate = originalPlaybackRate;
        
        // Also try to reset through YouTube's API
        const player = findYouTubePlayer();
        if (player && typeof player.setPlaybackRate === 'function') {
            try {
                player.setPlaybackRate(originalPlaybackRate);
            } catch(e) {}
        }
        
        // Hide speed indicator
        hideSpeedIndicator();
        
        return true;
    }
    
    // --- Key Event Handlers ---
    
    // Main handler for key down events
    const handleKeyDown = function(event) {
        // Skip if we're in an input field
        if (isInputField(document.activeElement)) return;
        
        // Handle arrow keys
        if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
            // Prevent default immediately
            event.preventDefault();
            event.stopPropagation();
            
            // Store which key was pressed
            activeKey = event.key;
            
            // Only proceed if we found a video and it's the first keydown
            if (findYouTubeVideo() && !keyAlreadyDown) {
                keyAlreadyDown = true;
                keyDownTime = Date.now();
                
                // Set up a timeout to check for long press
                longPressTimeout = setTimeout(() => {
                    // If key is still being pressed after threshold
                    if (keyAlreadyDown) {
                        isLongPress = true;
                        
                        // Different behavior based on which arrow key
                        if (activeKey === 'ArrowRight') {
                            // Right arrow for fast playback
                            changePlaybackSpeed(HOLD_RIGHT_PLAYBACK_SPEED);
                        } else if (activeKey === 'ArrowLeft') {
                            // Left arrow for slow playback
                            changePlaybackSpeed(HOLD_LEFT_PLAYBACK_SPEED);
                        }
                    }
                }, LONG_PRESS_THRESHOLD);
            }
        }
        // Handle up/down arrow keys on progress bar
        else if ((event.key === 'ArrowUp' || event.key === 'ArrowDown') && isProgressBarFocused()) {
            // Prevent default to avoid any time change
            event.preventDefault();
            event.stopPropagation();
            
            // Handle volume control properly by fixing focus and dispatching a new event
            handleVolumeKeyOnProgressBar(event.key === 'ArrowUp');
        }
    };
    
    // Main handler for key up events
    const handleKeyUp = function(event) {
        // Skip if we're in an input field
        if (isInputField(document.activeElement)) return;
        
        if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
            // Prevent default
            event.preventDefault();
            event.stopPropagation();
            
            // Only process if this is the active key (prevents issues with multiple keys)
            if (event.key === activeKey) {
                // Clear the timeout to prevent speed change activation if key was released quickly
                if (longPressTimeout) {
                    clearTimeout(longPressTimeout);
                    longPressTimeout = null;
                }
                
                // If this was a long press, reset playback speed
                if (isLongPress) {
                    resetPlaybackSpeed();
                } else if (keyAlreadyDown) {
                    // This was a quick tap, perform a seek
                    performSeek(event.key === 'ArrowRight');
                }
                
                // Reset tracking variables
                isLongPress = false;
                keyDownTime = 0;
                keyAlreadyDown = false;
                activeKey = null;
            }
        }
    };
    
    // --- Setup Functions ---
    
    // More comprehensive event handling
    function setupGlobalEventHandlers() {
        // Capture all keyboard events at the window level
        window.addEventListener('keydown', (e) => {
            // Skip if we're in an input field to allow normal typing
            if (isInputField(document.activeElement)) {
                return;
            }
            
            if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
                // Always handle left/right arrows
                handleKeyDown(e);
                // Always prevent propagation
                e.stopPropagation();
                e.preventDefault();
            } else if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && isProgressBarFocused()) {
                // For up/down, handle and prevent default when progress bar is focused
                handleKeyDown(e);
                e.stopPropagation();
                e.preventDefault();
            }
        }, true);
        
        window.addEventListener('keyup', (e) => {
            // Skip if we're in an input field
            if (isInputField(document.activeElement)) {
                return;
            }
            
            if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
                // Call our handler first
                handleKeyUp(e);
                // Always prevent propagation to YouTube
                e.stopPropagation();
                e.preventDefault();
            }
        }, true);
    }
    
    // Additional handler for the YouTube player specifically
    function addYouTubePlayerHandlers() {
        const player = findYouTubePlayer();
        if (player) {
            // Create our indicators now that we have the player
            createSpeedIndicator();
            createActionIndicator();
            
            // Additional direct event listeners for the player
            player.addEventListener('keydown', function(e) {
                if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
                    e.preventDefault();
                    e.stopPropagation();
                } else if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && isProgressBarFocused()) {
                    // For up/down on progress bar, handle and prevent
                    e.preventDefault();
                    e.stopPropagation();
                    handleVolumeKeyOnProgressBar(e.key === 'ArrowUp');
                }
            }, true);
            
            // Try to restore video position
            setTimeout(() => {
                restoreVideoPosition();
                startPositionTracking();
            }, 1500);  // Give YouTube a moment to initialize the video
        }
    }
    
    // Function to handle YouTube video element being added or replaced
    function handleVideoElementChange() {
        const video = findYouTubeVideo();
        if (video) {
            // Try to restore video position
            setTimeout(() => {
                restoreVideoPosition();
                startPositionTracking();
            }, 1500);  // Give YouTube a moment to initialize the video
        }
    }
    
    // Setup functions that will need to be called once the page is loaded
    function setupOnLoad() {
        // Set up the global event handlers
        setupGlobalEventHandlers();
        
        // Add player-specific handlers
        addYouTubePlayerHandlers();
        
        // Also try to intercept YouTube's internal keyboard event handling
        const originalDocKeyDown = document.onkeydown;
        const originalDocKeyUp = document.onkeyup;
        
        document.onkeydown = function(e) {
            // Skip if we're in an input field
            if (isInputField(document.activeElement)) {
                return originalDocKeyDown ? originalDocKeyDown(e) : true;
            }
            
            if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
                handleKeyDown(e);
                return false; // Prevent default
            } else if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && isProgressBarFocused()) {
                // Completely prevent default when progress bar is focused
                handleKeyDown(e);
                return false; 
            }
            return originalDocKeyDown ? originalDocKeyDown(e) : true;
        };
        
        document.onkeyup = function(e) {
            // Skip if we're in an input field
            if (isInputField(document.activeElement)) {
                return originalDocKeyUp ? originalDocKeyUp(e) : true;
            }
            
            if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
                handleKeyUp(e);
                return false; // Prevent default
            }
            return originalDocKeyUp ? originalDocKeyUp(e) : true;
        };
    }
    
    // Watch for DOM changes to catch the player element if it loads after the script
    const observer = new MutationObserver(function(mutations) {
        // Look for the player
        if (!document.querySelector('#movie_player')) {
            return;
        }
        
        // Check if we need to set up the player
        if (!speedIndicator) {
            addYouTubePlayerHandlers();
        }
        
        // Also watch for video element changes
        const video = findYouTubeVideo();
        if (video && currentVideoId != getVideoId()) {
            handleVideoElementChange();
        }
    });
    
    // Start observing the document
    observer.observe(document, { childList: true, subtree: true });
    
    // Listen for navigation events (YouTube is a SPA)
    function handleNavigation() {
        // Check for video ID change
        const newVideoId = getVideoId();
        const currentId = currentVideoId;
        
        if (newVideoId && newVideoId !== currentId) {
            debugLog('Video ID changed from', currentId, 'to', newVideoId);
            currentVideoId = newVideoId;
            
            // Clear existing tracking
            if (positionSaveInterval) {
                clearInterval(positionSaveInterval);
                positionSaveInterval = null;
            }
            
            // Add to history
            addToVideoHistory(newVideoId);
            
            // Handle the video element for the new page
            handleVideoElementChange();
        }
    }
    
    // YouTube uses History API for navigation
    const originalPushState = history.pushState;
    history.pushState = function() {
        originalPushState.apply(this, arguments);
        handleNavigation();
    };
    
    window.addEventListener('popstate', handleNavigation);
    
    // Call setup once the page is fully loaded
    if (document.readyState === 'complete') {
        setupOnLoad();
    } else {
        window.addEventListener('load', setupOnLoad);
    }
    
    // Clean up when the page unloads
    window.addEventListener('unload', function() {
        // Save final position before unloading
        saveVideoPosition();
        
        removeSpeedIndicator();
        if (actionIndicator && actionIndicator.parentNode) {
            actionIndicator.parentNode.removeChild(actionIndicator);
        }
        observer.disconnect();
        if (longPressTimeout) {
            clearTimeout(longPressTimeout);
        }
        if (hideActionTimeout) {
            clearTimeout(hideActionTimeout);
        }
        if (positionSaveInterval) {
            clearInterval(positionSaveInterval);
        }
    });
})();