Skip to content

Instantly share code, notes, and snippets.

@abec2304
Last active June 20, 2026 04:48
Show Gist options
  • Select an option

  • Save abec2304/2782f4fc47f9d010dfaab00f25e69c8a to your computer and use it in GitHub Desktop.

Select an option

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);
@abec2304

Copy link
Copy Markdown
Author

Also, would it be far too redundant and buggy to have both running at the same time? I can imagine so, I'm just curious.

Indeed, they won't get along.

Is there anything specific you're doing when you notice the script no longer working? It would be particularly insightful if you can manage to capture a video showing the exact point where it stops working.

@FrenGain

Copy link
Copy Markdown

Is there anything specific you're doing when you notice the script no longer working? It would be particularly insightful if you can manage to capture a video showing the exact point where it stops working.

No, there's nothing in specific. It usually happens after I boot my computer again, or when I mess with the plugin. Or seemingly at random. I've not noticed any particular pattern as to why it happens. Does there happen to be a log for the plugin or anything of the sort? I can likely link that if there is when I notice it not working again.

I'm also trying out the new version you posted here, and so far so good.

@narinishi

Copy link
Copy Markdown

Is there anything specific you're doing when you notice the script no longer working? It would be particularly insightful if you can manage to capture a video showing the exact point where it stops working.

No, there's nothing in specific. It usually happens after I boot my computer again, or when I mess with the plugin. Or seemingly at random. I've not noticed any particular pattern as to why it happens. Does there happen to be a log for the plugin or anything of the sort? I can likely link that if there is when I notice it not working again.

I'm also trying out the new version you posted here, and so far so good.

The only logging done is to check that the script initialized. You could edit the script to add more logging if you want to try identifying where things go wrong.

@FrenGain

Copy link
Copy Markdown

The only logging done is to check that the script initialized. You could edit the script to add more logging if you want to try identifying where things go wrong.

Hm. I don't know how I'd do that in this language.

@abec2304

Copy link
Copy Markdown
Author

Now updated to include extensive logging.

@FrenGain

Copy link
Copy Markdown

Now updated to include extensive logging.

No kidding! I can get to testing, likely now and tomorrow. How do I check the logs?

@abec2304

abec2304 commented Nov 15, 2024

Copy link
Copy Markdown
Author

Now updated to include extensive logging.

No kidding! I can get to testing, likely now and tomorrow. How do I check the logs?

It will output in the browser console. Open Developer Tools with CTRL SHIFT I then select Console.
If using Chrome, you may have to click Default levels at top-right and enable Verbose.
EDIT: then filter to ytvolfix2 to only see relevant log entries.

@FrenGain

FrenGain commented Nov 15, 2024

Copy link
Copy Markdown

No kidding! I can get to testing, likely now and tomorrow. How do I check the logs?

It will output in the browser console. Open Developer Tools with CTRL SHIFT I then select Console. If using Chrome, you may have to click Default levels at top-right and enable Verbose.

Tyvm. I'll get back to you once it doesn't work again. Only thing I've gotten so far is something about a piece of code being deprecated and ignored, instead recommended to use "renderer" over it for Firefox and several errors regarding something called "Cross-Origin", which I'm not sure about, but doesn't seem fairly important to this.

@FrenGain

Copy link
Copy Markdown

No kidding! I can get to testing, likely now and tomorrow. How do I check the logs?

It will output in the browser console. Open Developer Tools with CTRL SHIFT I then select Console. If using Chrome, you may have to click Default levels at top-right and enable Verbose. EDIT: then filter to ytvolfix2 to only see relevant log entries.

Had it happen again. Nothing in the logs at all - Disabling it and re-enabling it fixes it, as previously prescribed. It seemed Tampermonkey may've failed to use it, considering it didn't show it as an active script(though still enabled).

@abec2304

Copy link
Copy Markdown
Author

No kidding! I can get to testing, likely now and tomorrow. How do I check the logs?

It will output in the browser console. Open Developer Tools with CTRL SHIFT I then select Console. If using Chrome, you may have to click Default levels at top-right and enable Verbose.

Tyvm. I'll get back to you once it doesn't work again. Only thing I've gotten so far is something about a piece of code being deprecated and ignored, instead recommended to use "renderer" over it for Firefox and several errors regarding something called "Cross-Origin", which I'm not sure about, but doesn't seem fairly important to this.

Make sure you have the 'debug' level enabled in order to see the relevant messages.

@abec2304

Copy link
Copy Markdown
Author

Also if Tampermonkey is proving unreliable, it might be worth switching to Violentmonkey

@FrenGain

Copy link
Copy Markdown

Make sure you have the 'debug' level enabled in order to see the relevant messages.

I believe that is enabled, though I'm not sure what the Verbose thing refers to.

Also if Tampermonkey is proving unreliable, it might be worth switching to Violentmonkey

I'd never heard of that, admittedly, nor did I know other script-adjacent addons existed. Why do you say it'd be worth a switch?

@abec2304

Copy link
Copy Markdown
Author

Make sure you have the 'debug' level enabled in order to see the relevant messages.

I believe that is enabled, though I'm not sure what the Verbose thing refers to.

image
Sorry, Verbose is what it's called in Chrome. In Firefox, it's Debug.

Also if Tampermonkey is proving unreliable, it might be worth switching to Violentmonkey

I'd never heard of that, admittedly, nor did I know other script-adjacent addons existed. Why do you say it'd be worth a switch?

I primarily test using Violentmonkey and haven't had any issues.

@FrenGain

Copy link
Copy Markdown

Sorry, Verbose is what it's called in Chrome. In Firefox, it's Debug.

No worries - Already had that on, as well.

I primarily test using Violentmonkey and haven't had any issues.

I'll give it a go, then. Not sure what difference there is between the two, but I primarily, if only use them for this script, so what's the harm?

@CartridgeGen

Copy link
Copy Markdown

Just wanted to give a thank you for this. I hate how websites limit volume. If only this was possible for all media sources on the browser. Not only youtube.

@abec2304

Copy link
Copy Markdown
Author

Just wanted to give a thank you for this. I hate how websites limit volume. If only this was possible for all media sources on the browser. Not only youtube.

Any specific sites you have in mind?

@abec2304

Copy link
Copy Markdown
Author

Hi, I have a problem with this script, I've developed my own slider that sets the document.querySelector('video').playbackRate from 0.05x-3.00x, the problem is that the range 2.00x-3.00x resets this normalization script which in turn noticeably reloads the video player as it flickers then:

ytvolfix2: observed a video element being added
ytvolfix2: updating style
ytvolfix2: shadowed volume property
ytvolfix2: initial volume set

Those 4 lines are spammed whenever I'm moving in that range and I can notice that the color of the background of the volume slider is being changed gradually to stronger purple shade whenever this happens. Any guess? I'm using latest Librewolf on Linux with only this script, uBlock Origin and I'm just only trying to modify document.querySelector('video').playbackRate beyond 2.00x. I haven't looked at your code just yet, I thought about asking about clues first.

The volume bar turning more purple is part of my script, I added that as a visual indicator that unexpected behavior is occurring.

I tried adjusting playbackRate beyond 2x with LibreWolf on Windows and wasn't able to reproduce your issue.

Perhaps LibreWolf on Linux doesn't properly support higher playback rates and causes the video element to be re-initialized.

@Choomai

Choomai commented Apr 11, 2025

Copy link
Copy Markdown

Can you add support for YT Music?

@abec2304

Copy link
Copy Markdown
Author

Can you add support for YT Music?

Sure. The latest version should now work with the Music site. Try it and let me know if you have any issues.

@Choomai

Choomai commented May 27, 2025

Copy link
Copy Markdown

Can you add support for YT Music?

Sure. The latest version should now work with the Music site. Try it and let me know if you have any issues.

It is working now, thanks!

@FatheredPuma81

Copy link
Copy Markdown

Thank you so much man Blocktube's implementation suddenly broke and only applied if I manually reloaded the page. Now I can go back to listening to music at it's normal volume again.

@yusanee-341

Copy link
Copy Markdown

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

@Eisys

Eisys commented Jul 20, 2025

Copy link
Copy Markdown

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..

@FrenGain

Copy link
Copy Markdown

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.

@fixator10

fixator10 commented Aug 18, 2025

Copy link
Copy Markdown

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

@abec2304

abec2304 commented Oct 1, 2025

Copy link
Copy Markdown
Author

As the above commenter mentioned, since the frontend code for the new UI was revised, there's no need for me to update the script at present.

Although there is a slight cosmetic issue with the display of the volume bar which I'll look at fixing.
-- cosmetic issue now fixed!

image

@veganomy

Copy link
Copy Markdown

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.

@abec2304

Copy link
Copy Markdown
Author

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.

@vintprox

vintprox commented Jun 6, 2026

Copy link
Copy Markdown

It does work impeccably! Thank you very much.

@veganomy

veganomy commented Jun 18, 2026

Copy link
Copy Markdown

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment