-
-
Save abec2304/2782f4fc47f9d010dfaab00f25e69c8a to your computer and use it in GitHub Desktop.
| // ==UserScript== | |
| // @name No YouTube Volume Normalization | |
| // @namespace https://gist.github.com/abec2304 | |
| // @match https://www.youtube.com/* | |
| // @match https://music.youtube.com/* | |
| // @grant GM_addElement | |
| // @version 2.80 | |
| // @author abec2304 | |
| // @description Enjoy YouTube videos at their true volume | |
| // @run-at document-start | |
| // @allFrames true | |
| // ==/UserScript== | |
| /* eslint-env browser, greasemonkey */ | |
| /* biome-ignore-all lint/style/useTemplate: legacy support */ | |
| /* biome-ignore-all lint/complexity/useArrowFunction: legacy support */ | |
| /* biome-ignore-all lint/suspicious/noRedundantUseStrict: quirky linters */ | |
| (function xvolnorm(pageScript, thisObj) { | |
| "use strict"; | |
| var scriptId = "ytvolfix2"; | |
| var logMessage = function (message) { | |
| console.debug(scriptId + "_injector: " + message); | |
| }; | |
| var digestMessage = function (message, callback) { | |
| var msgBytes = new TextEncoder().encode(message); | |
| logMessage("attempting to hash script"); | |
| window.crypto.subtle.digest("SHA-256", msgBytes).then(function (buffer) { | |
| var arr; | |
| var hex; | |
| if (typeof cloneInto !== typeof undefined) { | |
| // workaround for Firemonkey | |
| buffer = cloneInto(buffer, thisObj); | |
| } | |
| try { | |
| arr = Array.from(new Uint8Array(buffer)); | |
| hex = arr | |
| .map(function (b) { | |
| return b.toString(16).padStart(2, "0"); | |
| }) | |
| .join(""); | |
| logMessage("obtained hash"); | |
| callback(hex); | |
| } catch (_ignore) { | |
| logMessage("unable to convert hash data"); | |
| callback("unknown"); | |
| } | |
| }); | |
| }; | |
| var inject = function (hash) { | |
| var content = "(" + pageScript + ")('" + scriptId + "', '" + hash + "');"; | |
| logMessage("preparing page script"); | |
| if (document.head) { | |
| GM_addElement("script", { id: scriptId, textContent: content }); | |
| logMessage("injected page script"); | |
| return; | |
| } | |
| document.addEventListener("DOMContentLoaded", function () { | |
| GM_addElement("script", { id: scriptId, textContent: content }); | |
| logMessage("injected page script (delayed)"); | |
| }); | |
| }; | |
| if (typeof GM_addElement === typeof undefined) { | |
| window.GM_addElement = function (a, b) { | |
| var elem = document.createElement(a); | |
| Object.keys(b).forEach(function (key) { | |
| elem[key] = b[key]; | |
| }); | |
| document.head.appendChild(elem); | |
| return elem; | |
| }; | |
| logMessage("defined addElement polyfill"); | |
| } | |
| try { | |
| digestMessage(pageScript, inject); | |
| } catch (_ignore) { | |
| logMessage("unable to hash"); | |
| inject("unknown"); | |
| } | |
| })(function (scriptId, hash) { | |
| "use strict"; | |
| var logMessage = function (message) { | |
| console.debug(scriptId + ": " + message); | |
| }; | |
| var _ignore = logMessage("page script called"); | |
| var volumeColors = ["thistle", "plum", "orchid", "mediumorchid", "darkorchid", "darkviolet"]; | |
| var styleNum = 0; | |
| var addVolumeStyle = function (parent) { | |
| var offset = "position: absolute; left: 0 !important; transform: translate(-100%)"; | |
| var color = "background: " + volumeColors[styleNum % volumeColors.length] + " !important"; | |
| var about = "No YouTube Volume Normalization #" + hash.slice(0, 16); | |
| var curStyle = parent.querySelector("style." + scriptId + "_style"); | |
| if (curStyle) { | |
| logMessage("updating style"); | |
| } else { | |
| curStyle = document.createElement("style"); | |
| curStyle.className = scriptId + "_style"; | |
| parent.appendChild(curStyle); | |
| logMessage("added style element"); | |
| } | |
| curStyle.textContent = ".ytp-volume-slider-handle::after { " + color + "; " + offset + "; }"; | |
| curStyle.textContent += " .ytp-sfn-content::after { content: '" + about + "' }"; | |
| curStyle.textContent += " ytmusic-nerd-stats::after { content: '" + about + "' }"; | |
| styleNum += 1; | |
| }; | |
| var setVolume = function (panel, video, setter) { | |
| var newVolume = panel.getAttribute("aria-valuenow") / 100; | |
| if (newVolume === video.lastVolume) { | |
| return; | |
| } | |
| video.lastVolume = newVolume; | |
| setter.call(video, newVolume); | |
| }; | |
| var handleVideo = function (videoElem) { | |
| var parentL0; | |
| var parentL1; | |
| var desc; | |
| var setter; | |
| var volumePanel; | |
| parentL0 = videoElem.parentNode; | |
| if (!parentL0) { | |
| logMessage("video immediately detached from page " + videoElem.outerHTML); | |
| return; | |
| } | |
| parentL1 = parentL0.parentNode; | |
| if (!parentL1) { | |
| logMessage("video detached from page " + videoElem.outerHTML); | |
| return; | |
| } | |
| if (videoElem.volume === 42) { | |
| logMessage("shadowed value indicates we already handled this video"); | |
| return; | |
| } | |
| desc = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, "volume"); | |
| if (!desc) { | |
| logMessage("using archaic volume descriptor"); | |
| desc = Object.getOwnPropertyDescriptor(videoElem, "volume"); | |
| } | |
| setter = desc.set; | |
| volumePanel = parentL1.querySelector(".ytp-volume-panel"); | |
| if (!volumePanel) { | |
| volumePanel = document.querySelector("ytmusic-player-bar #volume-slider #sliderBar"); | |
| if (!volumePanel) { | |
| logMessage("abandoning shadow - no associated volume panel"); | |
| return; | |
| } else { | |
| logMessage("found music subdomain volume panel"); | |
| } | |
| } | |
| addVolumeStyle(parentL1); | |
| Object.defineProperty(videoElem, "volume", { | |
| get: function () { | |
| logMessage("read of shadowed volume value"); | |
| return 42; | |
| }, | |
| set: function (_ignore) { | |
| var toCall = function () { | |
| setVolume(volumePanel, videoElem, setter); | |
| }; | |
| // slight delay to allow volume panel to update | |
| window.setTimeout(toCall, 5); | |
| } | |
| }); | |
| logMessage("shadowed volume property"); | |
| setVolume(volumePanel, videoElem, setter); | |
| logMessage("initial volume set"); | |
| }; | |
| var videoObserver; | |
| var intervalId; | |
| var existingVideos = document.querySelectorAll("video"); | |
| logMessage("number of existing video elements = " + existingVideos.length); | |
| Array.prototype.forEach.call(existingVideos, handleVideo); | |
| videoObserver = new MutationObserver(function (records) { | |
| records.forEach(function (mutation) { | |
| Array.prototype.forEach.call(mutation.addedNodes, function (node) { | |
| var volumePanel; | |
| var allVideo; | |
| var processNext = function (index, videoLen) { | |
| if (index < videoLen) { | |
| logMessage("evaluating potential video element"); | |
| handleVideo(allVideo[index]); | |
| processNext(index + 1, videoLen); | |
| } | |
| }; | |
| if ("VIDEO" === node.tagName) { | |
| logMessage("observed a video element being added"); | |
| handleVideo(node); | |
| } else { | |
| volumePanel = node.querySelector ? node.querySelector(".ytp-volume-panel") : null; | |
| if (volumePanel) { | |
| logMessage("volume panel lazily added"); | |
| } else { | |
| return; | |
| } | |
| allVideo = document.querySelectorAll("video"); | |
| processNext(0, allVideo.length); | |
| } | |
| }); | |
| }); | |
| }); | |
| videoObserver.observe(document.documentElement, { childList: true, subtree: true }); | |
| intervalId = window.setInterval(function ytvolfix2cleanup() { | |
| var scriptElem = document.getElementById(scriptId); | |
| if (!scriptElem) { | |
| logMessage("nothing found to clean up"); | |
| } else { | |
| scriptElem.parentNode.removeChild(scriptElem); | |
| logMessage("cleaned up own script element"); | |
| } | |
| clearInterval(intervalId); | |
| }, 1500); | |
| }, this); |
Any chance on issuing an update to this script regarding YT's new player UI? Recently, YouTube is giving me this newer look of their player, therefore the script doesn't work anymore. This is usually my go-to for a while now since other scripts don't work for me for some reason. Looking forward to an update :)
WHAT IN THE HOLY BLASPHEMOUS DEMON is that UI? F*** right off with that shit, Youtube..
The new UI is terrible, but until this script is updated, YTM works, and I have no issues with ads with my UBlockPlus setup. I majorly use this for music, so.
Forked this to change variables for new layout, fix was insanely simple
Youtube updated its code, now this script works for me. When its decides to use new UI, because its like switching 4th time in a month
This works perfectly. But why does the stats for nerds still shows Normalised percentages ?
Volume / Normalised 100% / 59% (content loudness 5.9dB) ? Thought it sounds loud enough.
You can check what the script is doing by looking at the debug messages in your browser's console.
The Stats for nerds text isn't modified other than the script adding a simple hash to have a quick way to check it initialized.
It would be a moderate amount of effort for little reward to make other modifications to the Stats for nerds text.
Today, the script was updated to handle new weird behavior by YouTube that was preventing the script from working.
It does work impeccably! Thank you very much.
@abec2304 Hi can you please do a PR for TizenTube to include this loudness normalisation removal feature there ?
TizenTube is a YouTube TV mod built on Google's Cobalt web API, but without ads & stuff. So I'm sure this loudness normalisation thing can be disabled on it.
Here's the issue I opened
reisxd/TizenTubeCobalt#278
@abec2304 Hi can you please do a PR for TizenTube to include this loudness normalisation removal feature there ?
TizenTube is a YouTube TV mod built on Google's Cobalt web API, but without ads & stuff. So I'm sure this loudness normalisation thing can be disabled on it.
Here's the issue I opened reisxd/TizenTubeCobalt#278
I don't have a TV device to test with, so not really something I'd want to attempt.
You should probably file against the userscript repo https://github.com/reisxd/TizenTube, since that's what the repo you linked injects - https://github.com/reisxd/TizenTubeCobalt/blob/505aa031a9150d9319508c71371801c9670a2763/third_party/blink/renderer/core/dom/document.cc#L4026
As an alternative, you could try SmartTube and maybe disable this setting yuliskov/SmartTube#4434
Yes I'm infact using SmartTube right now. It directly offloads stream files into Android's local Exoplayer/Media3. Hence no need of loudness normalisation at all.
But I can't seem to use YouTube Music with it. TizenTube on the other hand, is exactly the same as YouTube supporting YTMusic too. But it lacks the original volume boost and instead lowers to -14 LUFS target.

Any chance on issuing an update to this script regarding YT's new player UI? Recently, YouTube is giving me this newer look of their player, therefore the script doesn't work anymore. This is usually my go-to for a while now since other scripts don't work for me for some reason. Looking forward to an update :)