volume set per channel

allows you to set different volumes for different twitch channels!

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         volume set per channel
// @namespace    http://tampermonkey.net/
// @version      2025-08-02
// @description  allows you to set different volumes for different twitch channels!
// @author       trevrosa
// @run-at       document-body
// @match        https://www.twitch.tv/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=twitch.tv
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==

function log(msg) {
    console.log(`volumeset: ${msg}`)
}

// https://stackoverflow.com/a/61511955
function waitForElem(selector) {
    return new Promise(resolve => {
        if (document.querySelector(selector)) {
            return resolve(document.querySelector(selector));
        }

        const observer = new MutationObserver(mutations => {
            if (document.querySelector(selector)) {
                observer.disconnect();
                resolve(document.querySelector(selector));
            }
        });

        // If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    });
}

(function() {
    'use strict';

    async function getChannelName() {
        const page = window.location.pathname;
        // https://twitch.tv(/xdd)
        // ^ one `/`, two split regions
        if (page.split("/").length == 2) {
            return page;
        } else {
            log("in a vod: waiting for channel name");
            const name = (await waitForElem("h1[class*='ScTitleText']")).innerText;
            return `/${name.toLowerCase()}`;
        }
    }

    async function setVolume(slider) {
        if (!slider) {
            log("could not set volume: slider was null, retrying");
            setTimeout(() => { setVolume(document.querySelector("input[type='range']")) }, 1000);
            return;
        }

        const channel = await getChannelName();

        const volume = GM_getValue(channel, null);
        if (!volume) {
            log(`no saved volume for channel '${channel}'`);
            return;
        }

        // change the slider's value to what we want
        slider.value = volume;

        // invoke the react event handler to then change the volume (https://stackoverflow.com/a/77083516)
        const reactHandlerKey = Object.keys(slider).find(key => key.startsWith('__reactProps$')); // changed to reactProps
        const changeEvent = new Event('change', { bubbles: true });
        Object.defineProperty(changeEvent, 'currentTarget', {writable: false, value: slider}); // https://stackoverflow.com/a/49122553
        slider[reactHandlerKey].onChange(changeEvent);

        log(`set the volume to ${volume} (${channel})`)
    }

    let listenerSet = false;

    function setListener(slider) {
        if (!slider) {
            log("could not set listener: slider was null");
            return;
        }

        slider.onchange = async (e) => {
            const volume = parseFloat(e.target.value);
            const channel = await getChannelName();
            GM_setValue(channel, volume)
            log(`${channel} saved to ${volume} volume`);
        }

        log("set volume slider listener");
        listenerSet = true;
    }

    // skip the main page, if we switch to an actual channel, we set the listener then.
    if (window.location.pathname != "/") {
        log("waiting for volume slider")
        waitForElem("input[type='range']").then(async (slider) => {
            await setListener(slider);
            await setVolume(slider);
        });
    }

    let lastPage = window.location.pathname;
    setInterval(async () => {
        const curPage = window.location.pathname;

        // ignore main page
        if (curPage == "/") return;

        if (!listenerSet) {
            const slider = document.querySelector("input[type='range']");
            setListener(slider);
            setVolume(slider);
        }

        if (curPage != lastPage) {
            lastPage = curPage;
            await setVolume(document.querySelector("input[type='range']"));
        }
    }, 500);
})();