// ==UserScript==
// @name YouTube Theater Fill and volume control (Brave v2)
// @namespace https://greasyfork.org/users/1533208
// @version 2.2.1-brave
// @description Simple, robust baseline for Brave: auto-enable Theater and fill viewport height. Minimal SPA hooks. No volume OSD or extras.
// @author Martin (Left234) & Lina
// @license MIT
// @match https://*.youtube.com/*
// @exclude https://*.youtube.com/embed/*
// @exclude https://music.youtube.com/*
// @run-at document-start
// @grant none
// ==/UserScript==
(() => {
"use strict";
const VERSION = "2.2.1-brave";
const CLASS = "ytf-fill";
const COVER = "ytf-cover-header";
const STYLE_ID = "ytf-fill-style";
// --- OSD/Volume config ---
const OSD_FRAC = 0.382; // golden-ish position
const VOL_STEP = 0.05;
const SHIFT_MULT = 2;
const LS_THEATER_PREF_KEYS = [
"yt-player-theater-mode-preference",
"ytd-player-theater-mode-preference",
];
// ---------- utils ----------
function onReady(fn){
if (document.readyState === "complete" || document.readyState === "interactive") fn();
else document.addEventListener("DOMContentLoaded", fn, { once:true });
}
function $(sel, root=document){ return root.querySelector(sel); }
function initViewportUnit(){
let unit = "100vh";
try {
if (CSS.supports("height: 100dvh")) unit = "100dvh";
else if (CSS.supports("height: 100svh")) unit = "100svh";
} catch {}
document.documentElement.style.setProperty("--ytf-viewport", unit);
}
function injectStyle(){
if (document.getElementById(STYLE_ID)) return;
const css = `
body.${CLASS} { overflow-x: hidden !important; }
body.${CLASS} ytd-app { position: static !important; }
/* host containers sized to the usable viewport */
body.${CLASS} ytd-watch-flexy[theater] #player-theater-container.ytd-watch-flexy,
body.${CLASS} ytd-watch-flexy[theater] #player-full-bleed-container.ytd-watch-flexy,
body.${CLASS} ytd-watch-flexy[theater] #full-bleed-container.ytd-watch-flexy,
body.${CLASS} ytd-watch-grid[theater] #player-theater-container.ytd-watch-grid,
body.${CLASS} ytd-watch-grid[theater] #player-full-bleed-container.ytd-watch-grid,
body.${CLASS} ytd-watch-grid[theater] #full-bleed-container.ytd-watch-grid {
height: calc(var(--ytf-viewport, 100vh) - var(--ytf-offset, 56px)) !important;
max-height: calc(var(--ytf-viewport, 100vh) - var(--ytf-offset, 56px)) !important;
width: 100vw !important; max-width: 100vw !important;
background: #000 !important;
position: relative !important;
inset: auto !important;
left: 0 !important; right: 0 !important;
transform: none !important;
margin: 0 auto !important; padding: 0 !important;
}
body.${CLASS} ytd-watch-flexy[theater] #player-container-outer.ytd-watch-flexy,
body.${CLASS} ytd-watch-flexy[theater] #player-container-inner.ytd-watch-flexy,
body.${CLASS} ytd-watch-flexy[theater] #player-container.ytd-watch-flexy,
body.${CLASS} ytd-watch-grid[theater] #player-container-outer.ytd-watch-grid,
body.${CLASS} ytd-watch-grid[theater] #player-container-inner.ytd-watch-grid,
body.${CLASS} ytd-watch-grid[theater] #player-container.ytd-watch-grid {
height: 100% !important; width: 100% !important;
display: flex !important; justify-content: center !important; align-items: stretch !important;
left: 0 !important; right: 0 !important; transform: none !important;
margin: 0 !important; padding: 0 !important;
}
body.${CLASS} ytd-watch-flexy[theater] ytd-player#ytd-player,
body.${CLASS} ytd-watch-grid[theater] ytd-player#ytd-player { height: 100% !important; width: 100% !important; }
body.${CLASS} ytd-watch-flexy[theater] .html5-video-player,
body.${CLASS} ytd-watch-flexy[theater] .html5-video-container,
body.${CLASS} ytd-watch-flexy[theater] video.html5-main-video,
body.${CLASS} ytd-watch-grid[theater] .html5-video-player,
body.${CLASS} ytd-watch-grid[theater] .html5-video-container,
body.${CLASS} ytd-watch-grid[theater] video.html5-main-video {
width: 100% !important; height: 100% !important;
left: 0 !important; right: 0 !important;
transform: none !important;
object-fit: contain !important; object-position: center center !important;
}
/* reduce ambient fights */
body.${CLASS} #cinematics, body.${CLASS} #cinematics-container { display: none !important; }
/* header cover mode */
body.${CLASS}.${COVER} { --ytf-offset: 0px !important; }
body.${CLASS}.${COVER} ytd-app #masthead-container.ytd-app,
body.${CLASS}.${COVER} ytd-masthead {
position: absolute !important;
top: 0 !important; left: 0 !important; right: 0 !important;
z-index: 2020 !important; width: 100% !important;
}
/* --- Contain/Cover guard & absolute-fill chain --- */
.ytp-fit-cover-video .html5-main-video { object-fit: contain !important; }
#movie_player,
#movie_player .html5-video-player,
#movie_player .html5-video-container,
#movie_player video.html5-main-video {
position: absolute !important;
inset: 0 !important;
width: 100% !important;
height: 100% !important;
}
#movie_player video.html5-main-video {
object-fit: contain !important;
object-position: center center !important;
display: block !important;
}
/* Ensure clean bars on extra-wide videos (avoid page peeking) */
ytd-watch-flexy[theater] #player-theater-container.ytd-watch-flexy,
ytd-watch-grid[theater] #player-theater-container.ytd-watch-grid {
background: #000 !important;
}
`;
const s = document.createElement("style");
s.id = STYLE_ID; s.textContent = css;
(document.head || document.documentElement).appendChild(s);
}
function masthead(){ return document.getElementById("masthead-container") || $("ytd-masthead"); }
function updateOffset(){
if (!document.body) return;
if (document.body.classList.contains(COVER)) {
document.documentElement.style.setProperty("--ytf-offset", "0px"); return;
}
const h = masthead()?.offsetHeight || 56;
document.documentElement.style.setProperty("--ytf-offset", h + "px");
}
function isWatchUrl(){ return location.pathname === "/watch" || /[?&]v=/.test(location.search); }
const onWatch = () => isWatchUrl() || !!$("ytd-watch-flexy, ytd-watch-grid");
function setTheaterPref(){ try { LS_THEATER_PREF_KEYS.forEach(k => localStorage.setItem(k, "DEFAULT_ON")); } catch {} }
function ensureTheater(flexy){
if (!flexy) return;
setTheaterPref();
if (!flexy.hasAttribute("theater")) {
try { flexy.setAttribute("theater", ""); } catch {}
try { flexy.theater = true; } catch {}
queueMicrotask(() => {
const stillNot = !$("ytd-watch-flexy[theater], ytd-watch-grid[theater]");
if (stillNot) { const btn = $(".ytp-size-button"); if (btn) btn.click(); }
});
}
}
function whenFlexy(cb){
const tryNow = () => { const el = $("ytd-watch-flexy, ytd-watch-grid"); if (!el) return false; cb(el); return true; };
if (tryNow()) return;
const mo = new MutationObserver(() => { if (tryNow()) mo.disconnect(); });
mo.observe(document.documentElement, { childList:true, subtree:true });
}
function isFullscreen(){
return !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement);
}
function onFullscreenChange(){ document.body && document.body.classList.toggle(COVER, isFullscreen()); updateOffset(); }
// --- normalize player sizing to prevent "tiny corner" and overscale ---
function normalizePlayerSizing(){
const player = document.getElementById('movie_player');
const container = player?.querySelector('.html5-video-container');
const video = player?.querySelector('video.html5-main-video');
if (!player || !container || !video) return;
// Remove inline offsets/scales that can persist across SPA transitions
[player, container, video].forEach(el => {
['width','height','left','top','right','bottom','transform'].forEach(p => {
try { el.style.removeProperty(p); } catch {}
});
});
// Re-assert sane sizing
try {
container.style.position = 'absolute';
container.style.inset = '0';
video.style.width = '100%';
video.style.height = '100%';
video.style.objectFit = 'contain';
video.style.objectPosition = 'center center';
} catch {}
}
// -------------- Global Volume + OSD --------------
let ytfOsd, ytfOsdTimer;
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
const isEditable = (el) => el && (el.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/i.test(el.tagName) ||
!!el.closest('input, textarea, select, [contenteditable="true"]'));
const getVideo = () =>
document.querySelector("video.html5-main-video") ||
document.querySelector("#movie_player video") ||
document.querySelector("video");
function positionOSDToVideo() {
if (!ytfOsd) return;
const vid = getVideo();
if (!vid) return;
const r = vid.getBoundingClientRect();
const x = r.left + r.width / 2;
const y = r.top + r.height * OSD_FRAC;
ytfOsd.style.left = `${x}px`;
ytfOsd.style.top = `${y}px`;
}
function showVolumeOSD(value) {
if (!ytfOsd) {
ytfOsd = document.createElement("div");
ytfOsd.style.cssText = [
"position:fixed","left:50%","top:30%","transform:translate(-50%,-50%)",
"padding:0","background:transparent","color:#fff",
"font:600 40px/1.08 ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial, Apple Color Emoji, Segoe UI Emoji",
"letter-spacing:.2px","-webkit-font-smoothing:antialiased","text-rendering:optimizeLegibility",
"-webkit-text-stroke:.35px rgba(0,0,0,.30)","text-shadow:0 0 8px rgba(0,0,0,.28)",
"z-index:2147483647","pointer-events:none","opacity:0","transition:opacity .15s ease-in-out"
].join(";");
document.documentElement.appendChild(ytfOsd);
window.addEventListener("resize", positionOSDToVideo);
document.addEventListener("fullscreenchange", positionOSDToVideo);
document.addEventListener("webkitfullscreenchange", positionOSDToVideo);
document.addEventListener("mozfullscreenchange", positionOSDToVideo);
document.addEventListener("MSFullscreenChange", positionOSDToVideo);
}
ytfOsd.textContent = typeof value === "number" ? `${value}%` : String(value);
positionOSDToVideo();
ytfOsd.style.opacity = "1";
clearTimeout(ytfOsdTimer);
ytfOsdTimer = setTimeout(() => (ytfOsd.style.opacity = "0"), 900);
}
function onKeydown(e) {
if (!onWatch()) return;
if (isEditable(e.target)) return;
if (e.ctrlKey || e.metaKey || e.altKey) return;
if (e.key === "ArrowUp" || e.key === "ArrowDown") {
const v = getVideo(); if (!v) return;
const mult = e.shiftKey ? SHIFT_MULT : 1;
const delta = (e.key === "ArrowUp" ? 1 : -1) * VOL_STEP * mult;
const newVol = clamp(Math.round((v.volume + delta) * 100) / 100, 0, 1);
setVolumeUIAware(newVol);
showVolumeOSD(Math.round(newVol * 100));
e.preventDefault(); e.stopImmediatePropagation(); return;
}
if (e.key === "m" || e.key === "M") {
const muted = toggleMuteUIAware();
const mp = document.getElementById('movie_player');
const volPct = (muted ? 0 : Math.round(((mp?.getVolume?.() ?? getVideo()?.volume ?? 0) * (mp?.getVolume ? 1 : 100)) ));
showVolumeOSD(muted ? "Muted" : volPct);
e.preventDefault(); e.stopImmediatePropagation(); return;
}
}
// --- v2.1 beta: UI-aware volume so YT slider stays in sync ---
function setVolumeUIAware(v01) {
const mp = document.getElementById('movie_player');
const pct = Math.round(v01 * 100);
if (mp && typeof mp.setVolume === 'function') {
try { pct === 0 ? mp.mute?.() : mp.unMute?.(); } catch {}
try { mp.setVolume(pct); } catch {}
} else {
const v = getVideo(); if (v) { if (pct > 0) v.muted = false; v.volume = v01; }
}
try {
const v = getVideo(); v?.dispatchEvent(new Event('volumechange', { bubbles: true }));
} catch {}
}
function toggleMuteUIAware() {
const mp = document.getElementById('movie_player');
if (mp && typeof mp.isMuted === 'function') {
try { mp.isMuted() ? mp.unMute?.() : mp.mute?.(); } catch {}
return !!mp.isMuted?.();
} else {
const v = getVideo(); if (v) v.muted = !v.muted; return !!v?.muted;
}
}
// v2.2.1 beta: Guarded normalize when tab becomes visible / pageshow (race-safe)
function normalizeOnShow() {
if (!onWatch()) return;
const attempt = (tries = 0) => {
const v = getVideo();
const ready = v && v.readyState >= 1 && v.videoWidth > 0;
if (ready) {
normalizePlayerSizing();
} else if (tries < 5) {
setTimeout(() => attempt(tries + 1), 120);
}
};
setTimeout(() => attempt(0), 60);
}
function apply(){
if (!document.body) return;
if (!onWatch()) { document.body.classList.remove(CLASS, COVER); return; }
document.body.classList.add(CLASS);
whenFlexy(flexy => { ensureTheater(flexy); updateOffset(); window.scrollTo(0, 0); });
}
// ---------- init ----------
initViewportUnit();
injectStyle();
onReady(() => {
apply(); updateOffset(); setTimeout(normalizePlayerSizing, 50);
// light SPA hooks
document.addEventListener("yt-navigate-start", apply);
document.addEventListener("yt-navigate-finish", () => { apply(); normalizePlayerSizing(); });
window.addEventListener("load", () => { apply(); normalizePlayerSizing(); });
window.addEventListener("resize", () => { updateOffset(); normalizePlayerSizing(); });
document.addEventListener("fullscreenchange", onFullscreenChange);
document.addEventListener("webkitfullscreenchange", onFullscreenChange);
document.addEventListener("mozfullscreenchange", onFullscreenChange);
document.addEventListener("MSFullscreenChange", onFullscreenChange);
// capture phase to beat YT handlers
window.addEventListener("keydown", onKeydown, true);
// v2.2.1 beta guarded show hooks
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") normalizeOnShow();
}, { once: true });
window.addEventListener("pageshow", () => { normalizeOnShow(); }, { once: true });
});
})();