Skip to content

Instantly share code, notes, and snippets.

@abec2304
Last active August 6, 2025 08:14
Show Gist options
  • Save abec2304/2782f4fc47f9d010dfaab00f25e69c8a to your computer and use it in GitHub Desktop.
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.73beta
// @author abec2304
// @description Enjoy YouTube videos at their true volume
// @run-at document-start
// @allFrames true
// ==/UserScript==
/* eslint-env browser, greasemonkey */
(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 color = volumeColors[styleNum % volumeColors.length];
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::before { background: " + color + "; z-index: -1; }";
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;
}
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) {
logMessage("no regular associated volume panel");
volumePanel = document.querySelector("ytmusic-player-bar #volume-slider #sliderBar")
if(!volumePanel) {
logMessage("no associated music volume panel either");
return;
}
}
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) {
if("VIDEO" === node.tagName) {
logMessage("observed a video element being added");
handleVideo(node);
}
});
});
});
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));
@FrenGain
Copy link

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.

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