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.1.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;
    }




    let finished = false;
    const onNavigateFinish = () => {
        if (finished) return;
        finished = true;
    document.removeEventListener('yt-navigate-finish', onNavigateFinish, true);

        document.head.appendChild(document.createElement('style')).textContent = `
        #player div.ytp-tooltip-text-wrapper[class] {
          pointer-events:none !important;

        }
        .volume-tip-offset{
        }
        .volume-tip-gain{
        opacity:0.52;
        }
        .volume-tip-normalized{
        opacity:0.4;
        }
        `;


        setTimeout(() => {

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

            let volumeTitle = volumeSlider.getAttribute('title');



            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)
            }

            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 volumeSpan = null;
            let lastContent = null;



            let video = document.querySelector('#player video[src]');
            let source = null;
            let gainNode = null;
            video.addEventListener('volumechange', changeVolumeText, false)


            let ktid = 0;
            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) {

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

                }
            }

            setInterval(() => {

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

                    if (elms[0]) {
                        volumeSpan = elms[0];
                        lastContent = volumeSpan.textContent;
                    }
                }

                if (volumeSpan && (!volumeSpan.isConnected || volumeSpan.textContent !== lastContent)) {
                    // volumeSpan.textContent = volumeTitle;
                    volumeSpan = null;
                    lastContent = null;
                }

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

                });

            }, 80)

        }, 300);


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


})();