Amplificateur de volume Twitch

Amplifie le volume Twitch en combinant volume natif et gain WebAudio, avec UI native améliorée.

// ==UserScript==
// @name         Amplificateur de volume Twitch
// @namespace    https://linktr.ee/yakenofficielle
// @version      5.4
// @description  Amplifie le volume Twitch en combinant volume natif et gain WebAudio, avec UI native améliorée.
// @author       NatioLaurin
// @match        https://www.twitch.tv/*
// @grant        none
// @license      Yaken
// ==/UserScript==

(function () {
    'use strict';

    let audioCtx = null;
    let gainNode = null;
    let sourceNode = null;
    let video = null;
    let lastVideoSrc = null;

    let amplificationGain = 1; // amplification multipliée au volume natif
    let nativeVolume = 1; // volume natif (slider Twitch)

    // Slider amplification custom (petit et discret)
    let ampSliderContainer = null;
    let ampSlider = null;
    let dragHandle = null;
    let isDragging = false;
    let offset = { x: 0, y: 0 };

    function createAmplificationSlider() {
        if (ampSliderContainer) return;

        ampSliderContainer = document.createElement("div");
        Object.assign(ampSliderContainer.style, {
            position: "fixed",
            top: "20px",
            right: "20px",
            width: "140px",
            backgroundColor: "rgba(24, 24, 27, 0.90)",
            borderRadius: "8px",
            boxShadow: "0 4px 12px rgba(0,0,0,0.7)",
            padding: "8px",
            display: "flex",
            flexDirection: "column",
            alignItems: "center",
            zIndex: "99999",
            userSelect: "none",
            color: "white",
            fontFamily: "Arial, sans-serif",
            fontSize: "13px",
        });

        const label = document.createElement("div");
        label.textContent = "🔊Amplification";
        label.style.marginBottom = "6px";
        label.style.fontWeight = "600";
        label.style.paddingRight = "18px";
        ampSliderContainer.appendChild(label);

        ampSlider = document.createElement("input");
        ampSlider.type = "range";
        ampSlider.min = "1";
        ampSlider.max = "5";
        ampSlider.step = "0.1";
        ampSlider.value = amplificationGain.toString();
        Object.assign(ampSlider.style, {
            width: "100%",
            cursor: "pointer",
            accentColor: "#9147FF",
        });
        ampSliderContainer.appendChild(ampSlider);

        dragHandle = document.createElement("div");
        Object.assign(dragHandle.style, {
            width: "16px",
            height: "16px",
            backgroundColor: "#9147FF",
            borderRadius: "4px",
            cursor: "grab",
            position: "absolute",
            top: "8px",
            right: "8px",
            marginTop: "0",
            alignSelf: "auto",
        });
        dragHandle.title = "Déplacer";
        ampSliderContainer.appendChild(dragHandle);

        document.body.appendChild(ampSliderContainer);

        dragHandle.addEventListener("mousedown", e => {
            isDragging = true;
            const rect = ampSliderContainer.getBoundingClientRect();
            offset.x = e.clientX - rect.left;
            offset.y = e.clientY - rect.top;
            e.preventDefault();
        });
        document.addEventListener("mousemove", e => {
            if (isDragging) {
                ampSliderContainer.style.left = `${e.clientX - offset.x}px`;
                ampSliderContainer.style.top = `${e.clientY - offset.y}px`;
                ampSliderContainer.style.right = "auto";
                ampSliderContainer.style.bottom = "auto";
            }
        });
        document.addEventListener("mouseup", () => {
            isDragging = false;
        });

        ampSlider.addEventListener("input", () => {
            amplificationGain = parseFloat(ampSlider.value);
            updateGain();
            console.log(`[Booster] Amplification réglée sur ${amplificationGain}x`);
        });
    }

    function updateGain() {
        if (gainNode) {
            let newGain = nativeVolume * amplificationGain;
            if (newGain > 10) newGain = 10;
            gainNode.gain.value = newGain;
        }
    }

    async function attachToVideo() {
        const candidate = document.querySelector('video');
        if (!candidate || candidate.readyState < 1) return;

        if (candidate.src === lastVideoSrc && audioCtx) {
            if (audioCtx.state === 'suspended') {
                await audioCtx.resume();
                console.log("[Booster] AudioContext repris");
            }
            return;
        }

        if (audioCtx) {
            try {
                await audioCtx.close();
                console.log("[Booster] AudioContext fermé");
            } catch (e) {
                console.warn("[Booster] Erreur en fermant AudioContext", e);
            }
            audioCtx = null;
            gainNode = null;
            sourceNode = null;
        }

        try {
            video = candidate;
            lastVideoSrc = video.src;

            audioCtx = new (window.AudioContext || window.webkitAudioContext)();
            sourceNode = audioCtx.createMediaElementSource(video);
            gainNode = audioCtx.createGain();
            gainNode.gain.value = nativeVolume * amplificationGain;

            sourceNode.connect(gainNode).connect(audioCtx.destination);

            if (audioCtx.state === 'suspended') {
                await audioCtx.resume();
                console.log("[Booster] AudioContext démarré");
            }

            console.log("[Booster] Audio connecté à la vidéo Twitch.");

            watchNativeVolumeSlider();

        } catch (err) {
            console.error("[Booster] Impossible de connecter l'audio :", err);
        }
    }

    function watchNativeVolumeSlider() {
        const nativeSlider = document.querySelector('input[data-test-selector="volume-slider"]');
        if (!nativeSlider) return;

        nativeVolume = parseFloat(nativeSlider.value) || 1;

        nativeSlider.oninput = () => {
            nativeVolume = parseFloat(nativeSlider.value);
            updateGain();
            console.log(`[Booster] Volume natif réglé sur ${nativeVolume}`);
        };
    }

    setInterval(() => {
        createAmplificationSlider();
        attachToVideo();
    }, 1000);
})();