Last active
April 11, 2025 11:02
-
-
Save abec2304/2782f4fc47f9d010dfaab00f25e69c8a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name No YouTube Volume Normalization | |
// @namespace https://gist.github.com/abec2304 | |
// @match https://www.youtube.com/* | |
// @grant GM_addElement | |
// @version 2.72 | |
// @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 + "' }"; | |
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 associated volume panel"); | |
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)); |
It kind of fixed itself throughout the week... and I'm not sure what caused this. Probably youtube changed something. Anyway, really thank you for this script. It is an absolute must-have on any firefox based browser when using Linux's audio stack such as pulseaudio or pipewire due to weird bugs, because reported volume levels to system are all out of wack, but you probably know this already.
Can you add support for YT Music?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.