// ==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);