MediaForge - Smooth, flowing control over media playback_播放增强器

A lightweight video enhancement script focusing on core features: speed, volume, picture, and playback control.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         MediaForge - Smooth, flowing control over media playback_播放增强器
// @namespace    https://github.com/aezizhu/video-enhancement-core
// @version      2.0.0
// @description  A lightweight video enhancement script focusing on core features: speed, volume, picture, and playback control.
// @author       aezi zhu
// @match        *://*/*
// @grant        unsafeWindow
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// @grant        GM_setClipboard
// @run-at       document-start
// @license      CC BY-NC-ND 4.0
// ==/UserScript==


/*
 * Copyright (c) 2025, aezi zhu (github.com/aezizhu)
 * This script is licensed under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.
 * You are free to use this script for personal, non-commercial purposes.
 * Any other use, including redistribution or commercial use, requires explicit permission from the author.
 * For full license details, see the LICENSE file or visit: https://creativecommons.org/licenses/by-nc-nd/4.0/
 */

(function () {
    'use strict';

    // --------------------------------------------------------------------------------
    // 0. Author Attribution Safeguard
    // --------------------------------------------------------------------------------
    // (Attribution removed for GreasyFork compliance - see original for details)

    // --------------------------------------------------------------------------------
    // 1. Configuration Management (Simplified)
    // --------------------------------------------------------------------------------

    const config = {
        prefix: '_h5player_core_',
        defaultSettings: {
            playbackRate: 1.0,
            volume: 1.0,
            enableHotkeys: true,
            // Filter initial values
            filters: {
                brightness: 1,
                contrast: 1,
                saturate: 1,
                hue: 0,
                blur: 0,
            },
            // Transform initial values
            transform: {
                rotate: 0,
                scaleX: 1,
                scaleY: 1,
                translateX: 0,
                translateY: 0,
            },
            // Hotkey definitions
            hotkeys: {
                // Playback Speed
                'c': { action: 'adjustPlaybackRate', value: 0.1, desc: 'Increase Speed' },
                'x': { action: 'adjustPlaybackRate', value: -0.1, desc: 'Decrease Speed' },
                'z': { action: 'setPlaybackRate', value: 1.0, desc: 'Reset Speed' },
                '1': { action: 'setPlaybackRate', value: 1.0, desc: 'Set Speed to 1x' },
                '2': { action: 'setPlaybackRate', value: 2.0, desc: 'Set Speed to 2x' },
                '3': { action: 'setPlaybackRate', value: 3.0, desc: 'Set Speed to 3x' },
                '4': { action: 'setPlaybackRate', value: 4.0, desc: 'Set Speed to 4x' },
                // Playback Control
                'space': { action: 'togglePlay', desc: 'Toggle Play/Pause' },
                'arrowright': { action: 'seek', value: 5, desc: 'Seek Forward 5s' },
                'arrowleft': { action: 'seek', value: -5, desc: 'Seek Backward 5s' },
                'ctrl+arrowright': { action: 'seek', value: 30, desc: 'Seek Forward 30s' },
                'ctrl+arrowleft': { action: 'seek', value: -30, desc: 'Seek Backward 30s' },
                'f': { action: 'frame', value: 1, desc: 'Next Frame' },
                'd': { action: 'frame', value: -1, desc: 'Previous Frame' },
                // Volume Control
                'arrowup': { action: 'adjustVolume', value: 0.05, desc: 'Volume Up 5%' },
                'arrowdown': { action: 'adjustVolume', value: -0.05, desc: 'Volume Down 5%' },
                'ctrl+arrowup': { action: 'adjustVolume', value: 0.2, desc: 'Volume Up 20%' },
                'ctrl+arrowdown': { action: 'adjustVolume', value: -0.2, desc: 'Volume Down 20%' },
                // Picture Enhancement
                'w': { action: 'adjustFilter', filter: 'brightness', value: 0.1, desc: 'Increase Brightness' },
                'e': { action: 'adjustFilter', filter: 'brightness', value: -0.1, desc: 'Decrease Brightness' },
                'r': { action: 'adjustFilter', filter: 'contrast', value: 0.1, desc: 'Increase Contrast' },
                't': { action: 'adjustFilter', filter: 'contrast', value: -0.1, desc: 'Decrease Contrast' },
                'y': { action: 'adjustFilter', filter: 'saturate', value: 0.1, desc: 'Increase Saturation' },
                'u': { action: 'adjustFilter', filter: 'saturate', value: -0.1, desc: 'Decrease Saturation' },
                'i': { action: 'adjustFilter', filter: 'hue', value: 15, desc: 'Increase Hue' },
                'o': { action: 'adjustFilter', filter: 'hue', value: -15, desc: 'Decrease Hue' },
                'j': { action: 'adjustFilter', filter: 'blur', value: 1, desc: 'Increase Blur' },
                'k': { action: 'adjustFilter', filter: 'blur', value: -1, desc: 'Decrease Blur' },
                's': { action: 'toggleRotation', desc: 'Rotate 90deg' },
                'm': { action: 'toggleMirror', axis: 'X', desc: 'Horizontal Mirror' },
                'shift+m': { action: 'toggleMirror', axis: 'Y', desc: 'Vertical Mirror' },
                'q': { action: 'resetFilterAndTransform', desc: 'Reset All Picture Adjustments' },
                // Display & Fullscreen
                'enter': { action: 'toggleFullScreen', desc: 'Toggle Browser Fullscreen' },
                'shift+enter': { action: 'toggleWebFullScreen', desc: 'Toggle Web Fullscreen' },
                'escape': { action: 'exitWebFullScreen', desc: 'Exit Web Fullscreen' },
                'shift+p': { action: 'togglePictureInPicture', desc: 'Toggle Picture-in-Picture' },
                'shift+s': { action: 'capture', desc: 'Screenshot' },
                // Media Download
                'shift+d': { action: 'download', desc: 'Download Media' },
            }
        },

        get(key) {
            if (typeof GM_getValue === 'undefined') return this.defaultSettings[key];
            return GM_getValue(this.prefix + key, this.defaultSettings[key]);
        },

        set(key, value) {
            if (typeof GM_setValue === 'undefined') return;
            GM_setValue(this.prefix + key, value);
        }
    };

    // --------------------------------------------------------------------------------
    // 2. Core Utilities
    // --------------------------------------------------------------------------------

    function isEditable(target) {
        // 1. Get the true active element, penetrating Shadow DOMs (Critical for Reddit & modern sites)
        let activeEl = document.activeElement;
        while (activeEl && activeEl.shadowRoot && activeEl.shadowRoot.activeElement) {
            activeEl = activeEl.shadowRoot.activeElement;
        }

        // Check the true active element
        if (activeEl && activeEl !== document.body && activeEl !== document.documentElement) {
            if (activeEl.isContentEditable) return true;
            if (activeEl.tagName && ['INPUT', 'TEXTAREA', 'SELECT'].includes(activeEl.tagName)) return true;
            const role = activeEl.getAttribute('role');
            if (role && ['textbox', 'searchbox', 'combobox'].includes(role)) return true;
        }
        
        // 2. Fallback: Check the event target
        if (!target || !target.nodeType || target.nodeType !== 1) return false;
        
        // Check if element itself is editable
        if (target.isContentEditable) return true;
        if (target.tagName && ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName)) return true;
        
        const targetRole = target.getAttribute('role');
        if (targetRole && ['textbox', 'searchbox', 'combobox'].includes(targetRole)) return true;
        
        // Check if target is inside an editable element
        let element = target.parentElement;
        while (element && element !== document.body) {
            if (element.isContentEditable) return true;
            if (element.tagName && ['INPUT', 'TEXTAREA', 'SELECT'].includes(element.tagName)) return true;
            const role = element.getAttribute('role');
            if (role && ['textbox', 'searchbox', 'combobox'].includes(role)) return true;
            element = element.parentElement;
        }
        
        return false;
    }

    function showToast(message, duration = 2000) {
        if (!document.body) return; // Ensure body exists
        let toast = document.querySelector('.enhancement-core-toast');
        if (!toast) {
            toast = document.createElement('div');
            toast.className = 'enhancement-core-toast';
            toast.style.cssText = `
                position: fixed;
                top: 20px;
                left: 50%;
                transform: translateX(-50%);
                background-color: rgba(0, 0, 0, 0.7);
                color: white;
                padding: 10px 20px;
                border-radius: 8px;
                z-index: 2147483647;
                font-family: sans-serif;
                font-size: 14px;
                transition: opacity 0.3s;
                opacity: 0;
            `;
            document.body.appendChild(toast);
        }
        toast.textContent = message;
        toast.style.opacity = '1';
        setTimeout(() => { toast.style.opacity = '0'; }, duration);
    }

    // --------------------------------------------------------------------------------
    // 3. Media Element Controller
    // --------------------------------------------------------------------------------

    class MediaController {
        constructor(mediaElement) {
            this.media = mediaElement;
            this.filters = { ...config.get('filters') };
            this.transform = { ...config.get('transform') };
            this._restoreTimeout = null;
            this._isRestoring = false;
            this.init();
        }

        init() {
            // Restore last known settings if available
            this.media.playbackRate = config.get('playbackRate');
            this.media.volume = config.get('volume');
            this.applyStyles();

            // Set up event listeners to restore playback rate when video loads
            this.setupPlaybackRateRestoration();
        }

        restorePlaybackRate() {
            // Skip if already restoring to prevent infinite loops
            if (this._isRestoring) return;

            const savedRate = config.get('playbackRate');
            const currentRate = this.media.playbackRate;

            // Only restore if rate differs from saved value
            if (Math.abs(currentRate - savedRate) > 0.01) {
                this._isRestoring = true;
                this.media.playbackRate = savedRate;

                if (window._debugHotkeys_) {
                    console.log('[restorePlaybackRate]', {
                        currentRate: currentRate,
                        savedRate: savedRate,
                        restored: true
                    });
                }

                // Reset flag after a short delay
                setTimeout(() => {
                    this._isRestoring = false;
                }, 100);
            }
        }

        setupPlaybackRateRestoration() {
            // Debounced restoration function
            const debouncedRestore = () => {
                if (this._restoreTimeout) {
                    clearTimeout(this._restoreTimeout);
                }
                this._restoreTimeout = setTimeout(() => {
                    this.restorePlaybackRate();
                }, 100);
            };

            // Listen to video lifecycle events
            this.media.addEventListener('loadedmetadata', debouncedRestore);
            this.media.addEventListener('canplay', debouncedRestore);
            this.media.addEventListener('play', debouncedRestore);

            // Optionally listen to ratechange to detect external modifications
            // But only restore if it wasn't changed by us
            this.media.addEventListener('ratechange', () => {
                if (!this._isRestoring) {
                    debouncedRestore();
                }
            });
        }

        applyStyles() {
            const filterStr = `brightness(${this.filters.brightness}) contrast(${this.filters.contrast}) saturate(${this.filters.saturate}) hue-rotate(${this.filters.hue}deg) blur(${this.filters.blur}px)`;
            const transformStr = `rotate(${this.transform.rotate}deg) scaleX(${this.transform.scaleX}) scaleY(${this.transform.scaleY}) translateX(${this.transform.translateX}px) translateY(${this.transform.translateY}px)`;
            
            // Only apply filter if any values are non-default
            const hasFilterChanges = this.filters.brightness !== 1 || this.filters.contrast !== 1 || 
                                    this.filters.saturate !== 1 || this.filters.hue !== 0 || this.filters.blur !== 0;
            if (hasFilterChanges) {
                this.media.style.filter = filterStr;
            } else {
                this.media.style.filter = '';
            }
            
            // Only apply transform if any values are non-default
            const hasTransformChanges = this.transform.rotate !== 0 || this.transform.scaleX !== 1 || 
                                       this.transform.scaleY !== 1 || this.transform.translateX !== 0 || 
                                       this.transform.translateY !== 0;
            if (hasTransformChanges) {
                this.media.style.transform = transformStr;
            } else {
                this.media.style.transform = '';
            }
        }

        // --- Actions ---
        togglePlay() {
            this.media.paused ? this.media.play() : this.media.pause();
        }

        seek(seconds) {
            this.media.currentTime += seconds;
            showToast(`Seek ${seconds > 0 ? '+' : ''}${seconds}s`);
        }

        frame(direction) {
            if (this.media.paused) {
                this.media.currentTime += direction * (1 / 60); // Assuming 60fps
            }
        }

        adjustVolume(delta) {
            let newVolume = Math.max(0, Math.min(2, this.media.volume + delta)); // Allow up to 200%
            this.media.volume = newVolume;
            config.set('volume', newVolume);
            showToast(`Volume: ${Math.round(newVolume * 100)}%`);
        }

        adjustPlaybackRate(delta) {
            if (window._debugHotkeys_) {
                console.log('[adjustPlaybackRate]', {
                    currentRate: this.media.playbackRate,
                    delta: delta,
                    expectedNewRate: this.media.playbackRate + delta
                });
            }
            let newRate = this.media.playbackRate + delta;
            newRate = Math.max(0.1, Math.min(16, newRate));
            // Set flag to prevent restoration during our own changes
            this._isRestoring = true;
            this.media.playbackRate = newRate;
            config.set('playbackRate', newRate);
            showToast(`Speed: ${newRate.toFixed(2)}x`);
            // Reset flag after a short delay
            setTimeout(() => {
                this._isRestoring = false;
            }, 100);
        }

        setPlaybackRate(rate) {
            // Set flag to prevent restoration during our own changes
            this._isRestoring = true;
            this.media.playbackRate = rate;
            config.set('playbackRate', rate);
            showToast(`Speed: ${rate.toFixed(2)}x`);
            // Reset flag after a short delay
            setTimeout(() => {
                this._isRestoring = false;
            }, 100);
        }

        adjustFilter(filter, delta) {
            this.filters[filter] += delta;
            if (filter === 'blur') this.filters[filter] = Math.max(0, this.filters[filter]);
            else if (filter !== 'hue') this.filters[filter] = Math.max(0, this.filters[filter]);
            this.applyStyles();
            config.set('filters', this.filters);
            showToast(`${filter.charAt(0).toUpperCase() + filter.slice(1)}: ${this.filters[filter].toFixed(1)}`);
        }
        
        toggleRotation() {
            this.transform.rotate = (this.transform.rotate + 90) % 360;
            this.applyStyles();
            config.set('transform', this.transform);
            showToast(`Rotation: ${this.transform.rotate}°`);
        }

        toggleMirror(axis) {
            if (axis === 'X') this.transform.scaleX *= -1;
            if (axis === 'Y') this.transform.scaleY *= -1;
            this.applyStyles();
            config.set('transform', this.transform);
            const direction = axis === 'X' ? 'Horizontal' : 'Vertical';
            const state = (axis === 'X' ? this.transform.scaleX : this.transform.scaleY) < 0 ? 'ON' : 'OFF';
            showToast(`${direction} Mirror: ${state}`);
        }

        resetFilterAndTransform() {
            // Reset all filters to default values
            this.filters = {
                brightness: 1,
                contrast: 1,
                saturate: 1,
                hue: 0,
                blur: 0,
            };
            // Reset all transforms to default values
            this.transform = {
                rotate: 0,
                scaleX: 1,
                scaleY: 1,
                translateX: 0,
                translateY: 0,
            };
            this.applyStyles();
            config.set('filters', this.filters);
            config.set('transform', this.transform);
            showToast('✨ All picture adjustments reset');
        }

        toggleFullScreen() {
            if (!document.fullscreenElement) {
                document.documentElement.requestFullscreen().catch(err => console.error(err));
            } else {
                document.exitFullscreen();
            }
        }

        toggleWebFullScreen() {
            // Site-specific web fullscreen selectors (preferred method)
            const siteSelectors = {
                'youtube.com': 'button.ytp-size-button',
                'bilibili.com': ['.bpx-player-ctrl-web-enter', '.bpx-player-ctrl-web-leave', '.squirtle-pagefullscreen-inactive', '.squirtle-pagefullscreen-active'],
                'live.bilibili.com': '.bilibili-live-player-video-controller-web-fullscreen-btn button',
                'douyin.com': '.xgplayer-page-full-screen',
                'live.douyin.com': '.xgplayer-page-full-screen'
            };

            // Try site-specific button first
            const hostname = window.location.hostname;
            for (const [domain, selector] of Object.entries(siteSelectors)) {
                if (hostname.includes(domain)) {
                    const selectors = Array.isArray(selector) ? selector : [selector];
                    for (const sel of selectors) {
                        const button = document.querySelector(sel);
                        if (button && getComputedStyle(button).display !== 'none') {
                            button.click();
                            showToast('🖥️ Web Fullscreen Toggled');
                            return;
                        }
                    }
                }
            }

            // Fallback: CSS-based web fullscreen
            if (!document.getElementById('web-fullscreen-style')) {
                const style = document.createElement('style');
                style.id = 'web-fullscreen-style';
                style.textContent = `
                    .web-fullscreen {
                        position: fixed !important;
                        top: 0 !important;
                        left: 0 !important;
                        width: 100vw !important;
                        height: 100vh !important;
                        max-width: none !important;
                        max-height: none !important;
                        z-index: 2147483646 !important;
                        object-fit: contain !important;
                        background: #000 !important;
                    }
                `;
                document.head.appendChild(style);
            }

            const isEntering = !this.media.classList.contains('web-fullscreen');

            if (isEntering) {
                // Remove web-fullscreen from other videos
                document.querySelectorAll('.web-fullscreen').forEach(el => {
                    if (el !== this.media) {
                        el.classList.remove('web-fullscreen');
                    }
                });

                this.media.classList.add('web-fullscreen');
                showToast('🖥️ Web Fullscreen: ON');
            } else {
                this.media.classList.remove('web-fullscreen');
                showToast('🖥️ Web Fullscreen: OFF');
            }
        }

        exitWebFullScreen() {
            // Exit web fullscreen for all videos
            const webFullscreenVideos = document.querySelectorAll('.web-fullscreen');
            if (webFullscreenVideos.length > 0) {
                webFullscreenVideos.forEach(el => el.classList.remove('web-fullscreen'));
                showToast('🖥️ Web Fullscreen: OFF');
                return true;
            }
            return false;
        }

        togglePictureInPicture() {
            if (document.pictureInPictureElement === this.media) {
                document.exitPictureInPicture().catch(err => console.error('PiP exit failed:', err));
            } else if (this.media.requestPictureInPicture) {
                this.media.requestPictureInPicture().catch(err => console.error('PiP request failed:', err));
            } else {
                showToast('Picture-in-Picture is not supported for this media.');
            }
        }

        capture() {
            if (!this.media || this.media.tagName.toLowerCase() !== 'video') {
                showToast('Capture is only available for video elements.');
                return;
            }

            const width = this.media.videoWidth;
            const height = this.media.videoHeight;

            if (!width || !height) {
                showToast('No video frame available to capture.');
                return;
            }

            const canvas = document.createElement('canvas');
            canvas.width = width;
            canvas.height = height;
            const ctx = canvas.getContext('2d');
            ctx.drawImage(this.media, 0, 0, canvas.width, canvas.height);
            canvas.toBlob(blob => {
                if (!blob) {
                    showToast('Failed to capture frame.');
                    return;
                }
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = `capture-${new Date().toISOString()}.png`;
                a.click();
                URL.revokeObjectURL(url);
                showToast('Screenshot saved!');
            });
        }

        download() {
            if (this.media.src) {
                showToast('Starting download...');
                const a = document.createElement('a');
                a.href = this.media.src;
                a.target = '_blank';
                // Try to add a download attribute, might not always work due to CORS
                try {
                    const url = new URL(this.media.src);
                    const filename = url.pathname.split('/').pop();
                    a.download = filename;
                } catch (e) {
                    a.download = 'media_file';
                }
                a.click();
            } else {
                showToast('No media source found to download.');
            }
        }
    }

    // --------------------------------------------------------------------------------
    // 4. Main Script Logic
    // --------------------------------------------------------------------------------

    let activeController = null;
    const controllers = new WeakMap();

    function initializeMedia(mediaElement) {
        if (controllers.has(mediaElement)) return;

        // Simple check to avoid enhancing tiny or ad-like videos (only for video elements)
        if (mediaElement.tagName === 'VIDEO') {
            if (mediaElement.videoWidth < 200 || mediaElement.videoHeight < 150) {
                if (mediaElement.duration < 30) return; // Ignore short ad clips
            }
        }

        console.log('Enhancement Core: Initializing new media element', mediaElement);
        const controller = new MediaController(mediaElement);
        controllers.set(mediaElement, controller);

        // Auto-set active controller if it's the first one found
        if (!activeController) {
            activeController = controller;
            console.log('[Controller] Auto-activated first detected media');
        }

        mediaElement.addEventListener('mouseenter', () => {
            activeController = controller;
            console.log('[Controller] Activated via mouseenter');
        });
        mediaElement.addEventListener('play', () => {
            activeController = controller;
            console.log('[Controller] Activated via play event');
        });
        mediaElement.addEventListener('focus', () => {
            activeController = controller;
            console.log('[Controller] Activated via focus');
        });
    }

    // --- Robust Detection (Prototype Hijacking) ---
    // Inspired by h5player, this ensures we catch videos even in Shadow DOM
    (function hijackPrototype() {
        const _play = HTMLMediaElement.prototype.play;
        const _pause = HTMLMediaElement.prototype.pause;
        const _load = HTMLMediaElement.prototype.load;

        HTMLMediaElement.prototype.play = function () {
            initializeMedia(this);
            return _play.apply(this, arguments);
        };

        HTMLMediaElement.prototype.pause = function () {
            initializeMedia(this);
            return _pause.apply(this, arguments);
        };

        HTMLMediaElement.prototype.load = function () {
            initializeMedia(this);
            return _load.apply(this, arguments);
        };
    })();

    // --- Hotkey Handler ---

    function keydownEvent(event) {
        // Check if hotkeys are enabled
        if (!config.get('enableHotkeys')) return;
        
        // CRITICAL: Always allow typing in editable fields
        if (isEditable(event.target)) return;

        // Auto-activate controller if none is active
        if (!activeController) {
            const mediaElements = document.querySelectorAll('video, audio');
            for (const mediaElement of mediaElements) {
                if (controllers.has(mediaElement)) {
                    activeController = controllers.get(mediaElement);
                    if (window._debugHotkeys_) {
                        console.log('[Hotkey] Auto-activated media element');
                    }
                    break;
                }
            }
        }

        if (!activeController) return;

        // Build hotkey string
        const modifiers = [];
        if (event.ctrlKey) modifiers.push('ctrl');
        if (event.shiftKey) modifiers.push('shift');
        if (event.altKey) modifiers.push('alt');
        if (event.metaKey) modifiers.push('meta');

        let key = event.key.toLowerCase();

        // Normalize key names
        if (event.code === 'Space' || event.keyCode === 32) key = 'space';
        if (key === ' ' || key === 'spacebar') key = 'space';

        const hotkeyStr = modifiers.length > 0 ? `${modifiers.join('+')}+${key}` : key;

        if (window._debugHotkeys_) {
            console.log(`[Hotkey] Detected: ${hotkeyStr}`);
        }

        // Look up action in config
        const actionDef = config.get('hotkeys')[hotkeyStr];

        if (actionDef) {
            const { action, value, filter, axis } = actionDef;

            if (typeof activeController[action] === 'function') {
                event.preventDefault();
                event.stopPropagation();
                event.stopImmediatePropagation();

                // Dispatch action with appropriate arguments
                if (filter !== undefined) {
                    activeController[action](filter, value);
                } else if (axis !== undefined) {
                    activeController[action](axis);
                } else if (value !== undefined) {
                    activeController[action](value);
                } else {
                    activeController[action]();
                }

                if (window._debugHotkeys_) {
                    console.log(`[Hotkey] Triggered action: ${action}`);
                }
            }
        }
    }

    document.addEventListener('keydown', keydownEvent, true);
    window.addEventListener('keydown', keydownEvent, true);

    function keyupEvent(event) {
        // Check if hotkeys are enabled
        if (!config.get('enableHotkeys')) return;
        
        // CRITICAL: Always allow typing in editable fields
        if (isEditable(event.target)) return;

        // Build hotkey string (same logic as keydown)
        const modifiers = [];
        if (event.ctrlKey) modifiers.push('ctrl');
        if (event.shiftKey) modifiers.push('shift');
        if (event.altKey) modifiers.push('alt');
        if (event.metaKey) modifiers.push('meta');

        let key = event.key.toLowerCase();

        // Normalize key names
        if (event.code === 'Space' || event.keyCode === 32) key = 'space';
        if (key === ' ' || key === 'spacebar') key = 'space';

        const hotkeyStr = modifiers.length > 0 ? `${modifiers.join('+')}+${key}` : key;

        // Look up action in config
        const actionDef = config.get('hotkeys')[hotkeyStr];

        if (actionDef) {
            // If it's a handled hotkey, suppress the keyup event
            // This prevents websites (like YouTube) from triggering their own action on keyup
            event.preventDefault();
            event.stopPropagation();
            event.stopImmediatePropagation();

            if (window._debugHotkeys_) {
                console.log(`[Hotkey] Suppressed keyup for: ${hotkeyStr}`);
            }
        }
    }

    document.addEventListener('keyup', keyupEvent, true);
    window.addEventListener('keyup', keyupEvent, true);

    // --- Media Detection ---
    function findMediaElements() {
        document.querySelectorAll('video, audio').forEach(initializeMedia);
    }

    // Use MutationObserver to detect dynamically added media
    const observer = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
            if (mutation.addedNodes.length) {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === 1) { // ELEMENT_NODE
                        if (node.matches('video, audio')) {
                            initializeMedia(node);
                        } else if (node.querySelector) {
                            node.querySelectorAll('video, audio').forEach(initializeMedia);
                        }
                    }
                });
            }
        }
    });

    // Start observing
    observer.observe(document.documentElement, {
        childList: true,
        subtree: true
    });

    // Initial scan
    findMediaElements();

    // Auto-activate the first video after a short delay
    setTimeout(() => {
        if (!activeController) {
            const firstVideo = document.querySelector('video');
            if (firstVideo) {
                const controller = controllers.get(firstVideo);
                if (controller) {
                    activeController = controller;
                    console.log('Auto-activated first video for hotkey control');
                }
            }
        }
    }, 1000);

    console.log('Video Enhancement Core loaded. Copyright (c) 2025, aezi zhu (github.com/aezizhu)');
    console.log('💡 Tip: Set window._debugHotkeys_ = true to enable hotkey debugging');
    console.log('🎬 Videos will auto-activate after 1 second, or hover/play to activate');
})();