YouTube Theater Fill (Brave) — v3.2 clean + OSD

Minimal, stability-first build for Brave: auto-enable Theater and fill viewport height with clean letterboxing.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         YouTube Theater Fill (Brave) — v3.2 clean + OSD
// @namespace    https://greasyfork.org/users/1533208
// @version      3.2-stable
// @description  Minimal, stability-first build for Brave: auto-enable Theater and fill viewport height with clean letterboxing. 
// @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  = "3.0-stripped-beta";
  const CLASS    = "ytf-fill";
  const COVER    = "ytf-cover-header";
  const STYLE_ID = "ytf-fill-style";

  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 });
  }
  const $ = (sel, root=document) => 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 = `
/* Base */
body.${CLASS} { overflow-x: hidden !important; }
body.${CLASS} ytd-app { position: static !important; }

/* Size the main host containers 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;
}

/* These inner containers must stretch to 100% */
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;
  position: relative !important; /* anchor for absolute fill below */
}

/* Ensure player + video absolutely fill the anchored box */
#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 {
  display: block !important;
  object-fit: contain !important;
  object-position: center center !important;
}

/* Avoid ambient cinematics fighting our sizing */
body.${CLASS} #cinematics,
body.${CLASS} #cinematics-container { display: none !important; }

/* Header cover when fullscreen (prevents stray bars) */
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;
}

/* If YouTube tries "cover", force contain to preserve full frame visibility */
.ytp-fit-cover-video .html5-main-video { object-fit: contain !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");
    } else {
      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 = document.querySelector(".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(){ if (document.body) { document.body.classList.toggle(COVER, isFullscreen()); updateOffset(); } }

  // Strip inline styles that can persist across SPA transitions and cause corner/tiny/offset issues.
  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;

    [player, container, video].forEach(el => {
      ['width','height','left','top','right','bottom','transform'].forEach(p => { try { el.style.removeProperty(p); } catch {} });
    });

    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 {}
  }

  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(); normalizePlayerSizing(); });
  }

  // ---------- init ----------
  initViewportUnit();
  injectStyle();

  onReady(() => {
    apply();
    updateOffset();

    // Minimal, stability-first hooks only
    document.addEventListener("yt-navigate-finish", apply);
    window.addEventListener("load", apply);
    window.addEventListener("resize", updateOffset);
    document.addEventListener("fullscreenchange", onFullscreenChange);
    document.addEventListener("webkitfullscreenchange", onFullscreenChange);
    document.addEventListener("mozfullscreenchange", onFullscreenChange);
    document.addEventListener("MSFullscreenChange", onFullscreenChange);
  });
  // ---- Minimal Volume OSD + Arrow Up/Down hotkeys (no layout changes) ----
  (function ytfMinimalVolumeOSD(){
    const VOL_STEP = 0.05;
    const SHIFT_MULT = 2;
    let osd, timer;

    function isEditable(el){
      if (!el) return false;
      if (el.isContentEditable) return true;
      const t = el.tagName;
      if (!t) return false;
      const edit = /^(INPUT|TEXTAREA|SELECT)$/i.test(t);
      return edit || !!el.closest('input, textarea, select, [contenteditable="true"]');
    }

    function getVideo(){
      return document.querySelector("video.html5-main-video") ||
             document.querySelector("#movie_player video") ||
             document.querySelector("ytd-player video") ||
             document.querySelector("video");
    }

    function ensureOSD(){
      if (osd) return osd;
      osd = document.createElement("div");
      osd.style.cssText = [
        "position:fixed","left:50%","top:35%","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 .12s ease"
      ].join(";");
      document.documentElement.appendChild(osd);
      return osd;
    }

    function positionToVideo(){
      const v = getVideo(); if (!v || !osd) return;
      const r = v.getBoundingClientRect();
      if (!r.width || !r.height) return;
      const x = r.left + r.width / 2;
      const y = r.top + r.height * 0.382; // golden-ish
      osd.style.left = x + "px";
      osd.style.top  = y + "px";
    }

    function show(val){
      ensureOSD();
      osd.textContent = typeof val === "number" ? `${val}%` : String(val);
      positionToVideo();
      osd.style.opacity = "1";
      clearTimeout(timer);
      timer = setTimeout(() => { osd.style.opacity = "0"; }, 900);
    }

    function clamp(v, lo, hi){ return Math.min(hi, Math.max(lo, v)); }

    function onKey(e){
      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);
        if (newVol !== v.volume){
          if (v.muted && newVol > 0) v.muted = false;
          v.volume = newVol;
          // let YouTube react naturally; do not touch layout/containers
          v.dispatchEvent(new Event('volumechange'));
          show(Math.round(newVol * 100));
        }
        e.preventDefault(); e.stopImmediatePropagation();
      } else if (e.key === "m" || e.key === "M"){
        const v = getVideo(); if (!v) return;
        v.muted = !v.muted;
        v.dispatchEvent(new Event('volumechange'));
        show(v.muted ? "Muted" : Math.round(v.volume * 100));
        e.preventDefault(); e.stopImmediatePropagation();
      }
    }

    // Bind once DOM is interactive; capture phase to win over page handlers
    const bind = () => { window.addEventListener("keydown", onKey, true); };
    if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", bind, { once: true });
    else bind();

    window.addEventListener("resize", positionToVideo);
    document.addEventListener("fullscreenchange", positionToVideo);
    document.addEventListener("webkitfullscreenchange", positionToVideo);
    document.addEventListener("mozfullscreenchange", positionToVideo);
    document.addEventListener("MSFullscreenChange", positionToVideo);
  })();

})();


/* === YTF Experimental: Idle Boot + Black-Frame Nudge (v3.2) === */
(function ytfIdleBootNudge(){
  "use strict";
  let armed = false;
  let kicked = false;

  function getVideo(){
    return document.querySelector("video.html5-main-video") ||
           document.querySelector("#movie_player video") ||
           document.querySelector("ytd-player video") ||
           document.querySelector("video");
  }
  function isPaintLikely(v){
    try {
      if (!v) return false;
      const r = v.getBoundingClientRect();
      return r && r.width > 40 && r.height > 40 && v.readyState >= 2 && v.videoWidth > 0;
    } catch { return false; }
  }
  function displayFlip(v){
    const prev = v.style.display || "";
    v.style.display = "none";
    window.dispatchEvent(new Event("resize"));
    requestAnimationFrame(() => {
      v.style.display = prev || "block";
      window.dispatchEvent(new Event("resize"));
    });
  }
  function tinySeekKick(v){
    try { v.currentTime = Math.max(0, v.currentTime + 0.001); } catch {}
  }

  function lateKick(){
    if (kicked) return;
    const v = getVideo();
    if (!v) return;
    if (isPaintLikely(v)) return;
    displayFlip(v);
    setTimeout(() => {
      if (!isPaintLikely(v)) tinySeekKick(v);
    }, 120);
    kicked = true;
  }

  function bindVideoOnce(v){
    if (!v) return;
    const start = () => {
      setTimeout(lateKick, 150);
      setTimeout(lateKick, 600);
      setTimeout(lateKick, 1400);
    };
    ["loadedmetadata","loadeddata","playing","resize"].forEach(ev => {
      try { v.addEventListener(ev, start, { once: true }); } catch {}
    });
    setTimeout(start, 800);
  }

  function run(){
    if (armed) return;
    if (document.visibilityState !== "visible"){
      const onVis = () => {
        if (document.visibilityState === "visible"){
          document.removeEventListener("visibilitychange", onVis);
          run();
        }
      };
      document.addEventListener("visibilitychange", onVis);
      return;
    }
    const v = getVideo();
    if (!v){
      setTimeout(run, 300);
      return;
    }
    armed = true;
    bindVideoOnce(v);
  }

  window.addEventListener("pageshow", () => { kicked = false; armed = false; run(); });
  if ("requestIdleCallback" in window){
    requestIdleCallback(run, { timeout: 1200 });
  } else {
    setTimeout(run, 400);
  }
})();