Volume % badge
// ==UserScript==
// @name Show YouTube Volume % Badge
// @namespace yt.volbadge.icon
// @version 1.0
// @description Volume % badge
// @match *://*.youtube.com/*
// @match *://youtu.be/*
// @grant none
// @run-at document-idle
// @license MIT
// ==/UserScript==
(() => {
const BADGE_CLASS = 'ytp-volbadge';
const STYLE_ID = 'ytp-volbadge-style';
const stateByPlayer = new WeakMap();
function injectStyle() {
if (document.getElementById(STYLE_ID)) return;
const s = document.createElement('style');
s.id = STYLE_ID;
s.textContent = `
.ytp-volume-icon { position: relative !important; }
.${BADGE_CLASS}{
position: absolute; top: -2px; right: 2px;
height: 16px; line-height: 16px; padding: 0 6px;
border-radius: 10px; background: rgba(0,0,0,.65);
color: #fff; font-size: 10px; font-family: Roboto, Arial, Helvetica, sans-serif;
user-select: none; pointer-events: none; white-space: nowrap;
}`;
document.head.appendChild(s);
}
function getMainPlayer() {
const list = Array.from(document.querySelectorAll('.html5-video-player'));
return list.find(el => el.classList.contains('playing-mode') || el.classList.contains('paused-mode')) || list[0] || null;
}
function attachToPlayer(player) {
injectStyle();
let st = stateByPlayer.get(player);
if (!st) { st = {}; stateByPlayer.set(player, st); }
const video = player.querySelector('video');
const muteBtn = player.querySelector('.ytp-mute-button');
const volumePanel = player.querySelector('.ytp-volume-panel');
const ensureBadge = () => {
const icon = player.querySelector('.ytp-volume-icon');
if (!icon) return null;
let badge = icon.querySelector('.' + BADGE_CLASS);
if (!badge) {
badge = document.createElement('span');
badge.className = BADGE_CLASS;
badge.textContent = '--%';
icon.appendChild(badge);
}
st.icon = icon;
st.badge = badge;
return badge;
};
const getSlider = () =>
volumePanel?.querySelector('[role="slider"][aria-valuenow]') || null;
const compute = () => {
if (!st.badge) return;
const muted = (muteBtn?.getAttribute('aria-pressed') === 'true') || (video?.muted ?? false);
let val = 0;
if (st.slider && st.slider.hasAttribute('aria-valuenow')) {
const raw = parseInt(st.slider.getAttribute('aria-valuenow') || '0', 10);
val = isNaN(raw) ? 0 : raw;
} else if (video) {
val = Math.round((video.muted ? 0 : video.volume) * 100);
}
st.badge.textContent = `${muted ? 0 : val}`;
st.badge.title = muted ? 'Muted' : `Volume: ${val}%`;
};
const bindSliderObs = () => {
if (st.sliderObs) st.sliderObs.disconnect();
st.slider = getSlider();
if (!st.slider) return;
st.sliderObs = new MutationObserver(muts => {
if (muts.some(m => m.attributeName === 'aria-valuenow')) compute();
});
st.sliderObs.observe(st.slider, { attributes: true, attributeFilter: ['aria-valuenow'] });
compute();
};
// First ensure badge + slider
ensureBadge();
bindSliderObs();
compute();
// Bind once: mute + video events + lazy slider creation on hover/focus
if (!st.bound) {
if (muteBtn) {
const mo = new MutationObserver(muts => {
if (muts.some(m => m.attributeName === 'aria-pressed')) compute();
});
mo.observe(muteBtn, { attributes: true, attributeFilter: ['aria-pressed'] });
}
if (video) {
['volumechange', 'loadedmetadata', 'play'].forEach(ev =>
video.addEventListener(ev, compute, { passive: true })
);
}
if (volumePanel) {
volumePanel.addEventListener('mouseenter', bindSliderObs, { passive: true });
volumePanel.addEventListener('focusin', bindSliderObs, { passive: true });
}
st.bound = true;
}
// Tiny, throttled observer on the player's controls only
if (!st.controlsObs) {
const controls = player.querySelector('.ytp-chrome-bottom');
if (controls) {
let scheduled = false;
const scheduleEnsure = () => {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
// If icon or badge got replaced/removed, restore
ensureBadge();
// If slider node got swapped, rebind
bindSliderObs();
compute();
});
};
st.controlsObs = new MutationObserver(scheduleEnsure);
st.controlsObs.observe(controls, { childList: true, subtree: true });
}
}
}
function bootstrap() {
const p = getMainPlayer();
if (p) attachToPlayer(p);
}
// Start + keep alive across SPA navigations
bootstrap();
window.addEventListener('yt-navigate-finish', () => setTimeout(bootstrap, 150), { passive: true });
})();