YouTube Volume Assistant

Enhances the volume control on YouTube by providing additional information and features.

目前為 2023-05-21 提交的版本,檢視 最新版本

// ==UserScript==
// @name         YouTube Volume Assistant
// @namespace    http://tampermonkey.net/
// @version      0.2.0
// @description  Enhances the volume control on YouTube by providing additional information and features.
// @author       CY Fung
// @license      MIT License
// @match        https://www.youtube.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant        none
// @run-at       document-start
// @unwrap
// @allFrames
// @inject-into page
// ==/UserScript==

(function () {
    'use strict';

    //    AudioContext.prototype._createGain = AudioContext.prototype.createGain;


    let wm = new WeakMap();
    /*
        AudioContext.prototype.createGain = function(...args){
            return this.createdGain || (this.createdGain = this._createGain(...args));
        }
    */

    function getMediaElementSource() {
        return wm.get(this) || null;
    }
    function getGainNode() {
        return wm.get(this) || null;
    }

    AudioContext.prototype._createMediaElementSource = AudioContext.prototype.createMediaElementSource;

    AudioContext.prototype.createMediaElementSource = function (video, ...args) {
        let createdMediaElementSource = wm.get(video);
        if (createdMediaElementSource) return createdMediaElementSource;
        wm.set(video, createdMediaElementSource = this._createMediaElementSource(video, ...args));
        video.getMediaElementSource = getMediaElementSource;
        return createdMediaElementSource;
    }


    MediaElementAudioSourceNode.prototype._connect = MediaElementAudioSourceNode.prototype.connect;

    MediaElementAudioSourceNode.prototype.connect = function (gainNode, ...args) {

        this._connect(gainNode, ...args);
        wm.set(this, gainNode);

        this.getGainNode = getGainNode;
    }



    function addDblTap(element, doubleClick) {
        // https://stackoverflow.com/questions/45804917/dblclick-doesnt-work-on-touch-devices

        let expired


        let doubleTouch = function (e) {
            if (e.touches.length === 1) {
                if (!expired) {
                    expired = e.timeStamp + 400
                } else if (e.timeStamp <= expired) {
                    // remove the default of this event ( Zoom )
                    e.preventDefault()
                    doubleClick(e)
                    // then reset the variable for other "double Touches" event
                    expired = null
                } else {
                    // if the second touch was expired, make it as it's the first
                    expired = e.timeStamp + 400
                }
            }
        }

        element.addEventListener('touchstart', doubleTouch)
        element.addEventListener('dblclick', doubleClick)
    }


    function createCSS() {

        if (document.querySelector('#iTFoh')) return;
        let style = document.createElement('style');
        style.id = 'iTFoh';
        style.textContent = `
        .video-tip-offseted {
        margin-top:-1em;
        }
        .volume-tip-gain{
        opacity:0.52;
        }
        .volume-tip-normalized{
        opacity:0.4;
        }
        `;

        document.head.appendChild(style)

    }

    let volumeSlider = null;
    let volumeTitle = '';


    let volumeSpan = null;
    let lastContent = null;
    let source = null;
    let gainNode = null;



    function refreshDOM() {

        volumeSlider = document.querySelector('.ytp-volume-panel[role="slider"][title]');
        if (volumeSlider) {

            volumeTitle = volumeSlider.getAttribute('title');
        } else {
            volumeTitle = '';
        }

    }
    function setDblTap() {
        if (!volumeSlider) return;
        if (volumeSlider.hasAttribute('pKRyA')) return;
        volumeSlider.setAttribute('pKRyA', '');

        addDblTap(volumeSlider, (e) => {

            let target = null;
            try {
                target = e.target.closest('.ytp-volume-area').querySelector('.ytp-mute-button');
            } catch (e) { }
            console.log(target)
            const e2 = new MouseEvent('contextmenu', {
                bubbles: true,
                cancelable: true,
                view: window
            });

            if (target) target.dispatchEvent(e2);


        });
    }

    let template = document.createElement('template');


    function changeVolumeText() {

        let video = document.querySelector('#player video[src]');
        if (!video) return;

        if (gainNode === null) {

            source = video.getMediaElementSource ? video.getMediaElementSource() : null;
            if (source) {
                gainNode = source.getGainNode ? source.getGainNode() : null;
            }
        }

        let gainValue = (((gainNode || 0).gain || 0).value || 0);
        let m = gainValue || 1.0;

        let actualVolume = document.querySelector('ytd-player').player_.getVolume();
        let normalized = video.volume * 100;

        let gainText = gainValue ? `<span class="volume-tip-gain">Gain = ${+(gainValue.toFixed(2))}</span><br>` : '';

        template.innerHTML = `
                <span class="volume-tip-offset">
    ${gainText}
    <span class="volume-tip-volume">Volume: ${(m * actualVolume).toFixed(1)}% </span><br>
    <span class="volume-tip-normalized"> Normalized: ${(m * normalized).toFixed(1)}%</span>
    </span>`.trim();
        if (volumeSpan.textContent !== template.content.textContent && lastContent === volumeSpan.textContent) {

            volumeSpan.innerHTML = template.innerHTML;
            lastContent = volumeSpan.textContent;

        }
    }

    function addVideoEvents() {



        let video = document.querySelector('#player video[src]');
        if (!video) return;
        if (video.hasAttribute('zHbT0')) return;
        video.setAttribute('zHbT0', '');
        video.addEventListener('volumechange', changeVolumeText, false)



    }


    let ktid = 0;
    let goChecking = false;

    const asyncNavigateFinish = async () => {

        goChecking = false;
        createCSS();
        let cid = 0;

        const f = () => {
            if (!cid) return;
            refreshDOM();
            if (!volumeSlider) return;
            setDblTap();
            addVideoEvents();
            goChecking = true;
            clearTimeout(cid);
            cid = 0;
        };

        cid = setTimeout(f, 300);
        f();
    }

    const onNavigateFinish = () => {
        asyncNavigateFinish();

    };
    document.addEventListener('yt-navigate-finish', onNavigateFinish, true);


    setInterval(() => {

        if (!goChecking) return;

        if (!volumeSpan) {
            let elms = [...document.querySelectorAll('#player .ytp-tooltip div.ytp-tooltip-text-wrapper span.ytp-tooltip-text')];
            elms = elms.filter(t => t.textContent === volumeTitle);

            if (elms[0]) {
                HTMLElement.prototype.closest.call(elms[0], '#player .ytp-tooltip').classList.add('video-tip-offseted');
                volumeSpan = elms[0];
                lastContent = volumeSpan.textContent;
            }
        }

        if (volumeSpan && (!volumeSpan.isConnected || volumeSpan.textContent !== lastContent)) {
            // volumeSpan.textContent = volumeTitle;
            let p = document.querySelector('.video-tip-offseted');
            if (p) p.classList.remove('video-tip-offseted');
            let m = document.querySelector('.volume-tip-offset');
            if (m) m.remove();
            volumeSpan = null;
            lastContent = null;
        }

        if (!volumeSpan) return;
        let tid = Date.now();
        ktid = tid;
        requestAnimationFrame(() => {
            if (ktid !== tid) return;
            changeVolumeText();

        });

    }, 80)


})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址