diff --git a/STATUS.md b/STATUS.md index 17b4e4d..1174040 100644 --- a/STATUS.md +++ b/STATUS.md @@ -70,7 +70,7 @@ Source of truth: this repo, `techy/Media-Server-Setup` on the stack's own Gitea ## Unfinished, in rough priority -0. **Band removal**: deployed 2026-11-01; `vgdeband.sh` catch-up ran on the Vanguard-Training recordings (check `journalctl -u vgdeband` for the done lines). Live portal: band still shown on the live picture → canvas positioning + overflow hide, later. +0. **Band removal**: deployed 2026-11-01; `vgdeband.sh` catch-up ran on the Vanguard-Training recordings (check `journalctl -u vgdeband` for the done lines). Live portal: viewer now clips the band with a measured game-aspect frame (refits on every dockview resize/move) — committed 2026-11-01, deploy with `./deployLive.sh`. 1. Roles→groups policy: bound and updated (revokes only its own grants); will be proven by the next natural Discord logins (`/whoami` should show `bot-users`). Nested groups **verified 2026-11-01**: `git-admins` now reaches the header via the `Vanguard profile (nested groups)` scope mapping (README 5e). `akadmin` remains the recovery login if a policy misfires. 2. Confirm plugin 0.1.3 under Jellyfin → Plugins and the Gitea runner registered. diff --git a/live/src/Player.jsx b/live/src/Player.jsx index dfe3288..98c3464 100644 --- a/live/src/Player.jsx +++ b/live/src/Player.jsx @@ -9,7 +9,9 @@ export default function Player({ params, api }) { const videoRef = useRef(null) const [status, setStatus] = useState('connecting…') const [viewers, setViewers] = useState(params.viewers ?? 0) - const [aspect, setAspect] = useState(null) // game aspect once we know the video size (crops the timecode band) + const wrapRef = useRef(null) + const [dims, setDims] = useState(null) // {w, h, gameH}: video size + game height (h minus the timecode band) + const [box, setBox] = useState(null) // {W, H}: game-aspect frame fitted into the panel; video overflows it at the bottom const state = audio.use() const muted = effectiveMuted(state, id) const volume = state.streams[id]?.volume ?? 0.8 @@ -17,6 +19,26 @@ export default function Player({ params, api }) { const focused = state.focused === id useEffect(() => { audio.ensure(id) }, [id]) + // size the frame: fit a box of the GAME aspect (video width / height-without-band) into the panel, letterboxed. + // The video is laid across the full box width, so its band rows fall below the box and are clipped. + useEffect(() => { + const el = wrapRef.current; if (!el) return + const fit = () => { + if (!dims) { setBox(null); return } + const cw = el.clientWidth, ch = el.clientHeight; if (!cw || !ch) return + const a = dims.w / dims.gameH + const W = cw / ch > a ? Math.floor(ch * a) : cw + setBox({ W, H: Math.floor(W / a) }) + } + fit(); const ro = new ResizeObserver(fit); ro.observe(el); return () => ro.disconnect() + }, [dims]) + const onSize = e => { + const v = e.currentTarget; const w = v.videoWidth, h = v.videoHeight; if (!w || !h) return + let band = Math.round(w / 160); band += band % 2 + // only treat the extra rows as a band if what remains is a standard game height + const gameH = [720, 1080, 1440, 2160].includes(h - band) ? h - band : h + setDims(d => (d && d.w === w && d.h === h) ? d : { w, h, gameH }) + } useEffect(() => { const d = api.onDidActiveChange?.(() => {}) ; return () => d?.dispose?.() }, [api]) useEffect(() => { const t = setInterval(() => setViewers(params.viewers ?? 0), 2000); return () => clearInterval(t) }, [params.viewers]) @@ -73,12 +95,10 @@ export default function Player({ params, api }) { const icon = muted ? '🔇' : volume === 0 ? '🔈' : volume < 0.5 ? '🔉' : '🔊' return (