HTML5 音量增强器

Adds Up/Down arrow key volume control with boost capability (>100%) to HTML5 videos using Web Audio API and displays a tip. Based on HTML5视频播放工具.

// ==UserScript==
// @name         HTML5 音量增强器
// @match        *://*/*
// @grant        none
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js
// @version      1.0
// @author       AI
// @license MIT
// @description  Adds Up/Down arrow key volume control with boost capability (>100%) to HTML5 videos using Web Audio API and displays a tip. Based on HTML5视频播放工具.
// @namespace https://greasyfork.org/users/1186846
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration ---
    const VOLUME_STEP = 0.5; // How much to change gain per key press (original script used 0.5 for gain)
    const MAX_GAIN = 6;      // Maximum volume gain (5 = 500%)
    const VIDEO_SELECTOR = 'video'; // How to find the video element. 'video' finds the first one. Adjust if needed.

    // --- Globals ---
    let v = null; // The currently controlled video element
    let $msg = null; // Tip message element (jQuery object)
    const by = document.body; // Reference to the body element

    // --- Tip Display Function (Requires jQuery) ---
    // Creates and shows a message centered on the video element
    const tip = (msg, videoEl = v) => {
        if (!videoEl || !msg) { // Don't show tip if no video or message
            if ($msg) $msg.css('opacity', 0);
            return;
        }

        // Initialize the message element if it doesn't exist
        if (!$msg) {
            $msg = $('<div/>').css({
                'position': 'fixed', // Use fixed positioning
                'z-index': 2147483647, // Max z-index to be on top
                'background': 'rgba(0,0,0,0.8)',
                'color': '#FFF',
                'font-size': '24px',
                'font-weight': 'bold',
                'padding': '12px 24px',
                'border-radius': '8px',
                'box-shadow': '0 4px 12px rgba(0,0,0,0.25)',
                'pointer-events': 'none', // Don't intercept mouse events
                'opacity': 0, // Start invisible
                'transform': 'translate(-50%, -50%)', // Center align trick
                'transition': 'opacity 0.3s', // Smooth fade
                'white-space': 'nowrap' // Prevent line breaks
            }).appendTo(by); // Add to the body
        }

        // Calculate video center position
        const rect = videoEl.getBoundingClientRect();
        const centerX = rect.left + rect.width / 2;
        const centerY = rect.top + rect.height / 2;

        // Update message text and position, make visible
        $msg.text(msg)
            .css({
                'left': centerX + 'px',
                'top': centerY + 'px',
                'opacity': 1
            });

        // Set timer to fade out the message
        clearTimeout($msg.timer); // Clear previous timer
        $msg.timer = setTimeout(() => {
            if ($msg) $msg.css('opacity', 0);
        }, 2000); // Fade out after 2 seconds
    };

    // --- Volume Adjustment Function (Uses Web Audio API for boosting) ---
    const adjustVolume = (n) => {
        // Attempt to find the video element if not already set or if it became invalid
        if (!v || !document.contains(v)) {
            v = document.querySelector(VIDEO_SELECTOR);
        }
        if (!v) {
           // console.log("AdjustVolume: No video element found.");
           return; // Exit if no video found
        }

        // Initialize AudioContext and GainNode if they don't exist on the video element
        if (!v.audioGainNode) {
            try {
                // Create AudioContext
                const audioContext = new (window.AudioContext || window.webkitAudioContext)();
                // Create a source node from the video element
                const source = audioContext.createMediaElementSource(v);
                // Create a gain node (volume control)
                const gainNode = audioContext.createGain();
                // Connect the source to the gain node
                source.connect(gainNode);
                // Connect the gain node to the audio output (speakers)
                gainNode.connect(audioContext.destination);
                // Store the gain node on the video element for later access
                v.audioGainNode = gainNode;
                // Set the video's native volume to 1 (max) so gainNode controls the actual volume
                v.volume = 1;
                console.log("AudioContext and GainNode initialized for video.");
            } catch (e) {
                // If Web Audio API fails (e.g., protected content, browser limitations)
                console.error('Failed to initialize AudioContext/GainNode:', e);

                // Fallback to standard HTML5 volume control (max 100%)
                const currentVolume = v.volume || 0;
                 // Use smaller steps (0.1) for standard volume control as 0.5 is too coarse
                const volumeChange = n > 0 ? 0.1 : -0.1;
                const newVolume = Math.min(Math.max(currentVolume + volumeChange, 0), 1);
                v.volume = +newVolume.toFixed(2);
                tip(`Volume: ${Math.round(v.volume * 100)}%`); // Show standard volume percentage
                return; // Stop here after fallback
            }
        }

        // If AudioContext/GainNode exists, adjust the gain
        let currentGain = v.audioGainNode.gain.value;
        currentGain += n; // Apply the change (+/- VOLUME_STEP)
        // Clamp the gain between 0 and the configured MAX_GAIN
        currentGain = Math.max(0, Math.min(currentGain, MAX_GAIN));
        // Apply the new gain value
        v.audioGainNode.gain.value = +currentGain.toFixed(2);
        // Display the gain multiplier (e.g., "🔊 1.5x")
        tip(`🔊 ${currentGain.toFixed(1)}x`);
    };

    // --- Keydown Event Handler ---
    function handleVolumeKeys(e) {
        const t = e.target;
        // Ignore keys if typing in inputs, focused on editable content, or if modifier keys are pressed
        if (e.ctrlKey || e.metaKey || e.altKey || t.isContentEditable || /INPUT|TEXTAREA|SELECT/i.test(t.nodeName)) {
            return;
        }

        let action = null;
        // Check for Up Arrow (increase volume) or Down Arrow (decrease volume)
        switch (e.keyCode) {
            case 38: // Up Arrow
                action = () => adjustVolume(VOLUME_STEP);
                break;
            case 40: // Down Arrow
                action = () => adjustVolume(-VOLUME_STEP);
                break;
        }

        if (action) {
            // Try to find the video element (most relevant one might be the focused one or the first one)
            const currentVideo = document.querySelector(VIDEO_SELECTOR); // Re-check for video
            if (currentVideo) {
                v = currentVideo; // Update the global 'v' if found
                // Heuristic: Apply if the body or the video itself has focus, to avoid hijacking keys unintentionally.
                 const activeElement = document.activeElement;
                 const isVideoRelated = v.contains(activeElement) || v === activeElement;
                 const isBodyFocused = activeElement === by || activeElement === null;

                 if (isVideoRelated || isBodyFocused) {
                     e.preventDefault(); // Prevent default arrow key behavior (scrolling)
                     e.stopPropagation(); // Stop event bubbling
                     action(); // Perform the volume adjustment
                 }
            }
        }
    }

    // --- Initialization ---
    function init() {
        console.log("Initializing Standalone Volume Booster...");
        // Find the initial video element
        v = document.querySelector(VIDEO_SELECTOR);
        if (v) {
            console.log("Initial video element found:", v);
        } else {
            console.log("No video element found on initial load. Will check during key events.");
        }
        // Add the keydown listener to the document body
        by.addEventListener('keydown', handleVolumeKeys);
        console.log("Volume key listener attached to body.");
    }

    // --- Run Initialization ---
    // Wait for jQuery to be ready (if using @require) and DOM to be loaded
    if (typeof $ === 'undefined') {
        console.error("Volume Booster: jQuery is required for the volume tip display.");
        // Fallback or error handling if jQuery isn't present
    } else {
        // Run init() slightly delayed after DOM is ready
        if (document.readyState === 'complete' || document.readyState === 'interactive') {
            setTimeout(init, 500);
        } else {
            window.addEventListener('DOMContentLoaded', () => setTimeout(init, 500));
        }
    }

})();