Auto-hide the bottom bar down to just its seek line
Reported right after the native-controls replacement shipped: the bottom bar, now always visible, sits exactly where a native <track> subtitle renders and was masking it. Same fix every real video player already uses: collapse to a minimal state after a stretch of no activity, expand again on the slightest interaction. #bottomBar gets a .minimal class (3s of no activity) that transitions away its padding/background/border and the buttons row's height/ opacity - #progressBar itself (and its own generous invisible seek hit-area) is untouched, so a thin line is always there, just without the box around it once minimal. Expands on any pointer activity anywhere in the player, or while the pointer sits on the bar itself (hovering pauses the countdown entirely rather than racing it). Stays expanded outright while genuinely paused, and every discrete playback event re-arms the countdown too - not just raw pointer movement - so resuming from pause restarts it instead of leaving the bar stuck open. Applies to host and follower alike. Verified: full typecheck/build. Confirmed in a real browser against the compiled bundle, side by side with an actual subtitle-shaped overlay: the expanded bar visibly overlaps it, the minimal state leaves it fully readable. Not driven through a real wall-clock wait for the timer itself - the class-toggling/transition logic was checked directly instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
a7944538bc
commit
d3e2811bd0
4 changed files with 128 additions and 3 deletions
|
|
@ -309,7 +309,9 @@ Then put it behind your existing reverse proxy like everything else
|
|||
- [x] Reworked the video player's bottom bar (episode/request actions/
|
||||
fullscreen, one bar instead of three floating pieces) and added a
|
||||
real fullscreen toggle — fullscreen now targets the whole player,
|
||||
not just the `<video>`, so the request buttons stay visible — see
|
||||
not just the `<video>`, so the request buttons stay visible; the
|
||||
bar now also auto-hides down to just its seek line after a
|
||||
stretch of no activity, so it stops masking subtitles — see
|
||||
docs/architecture.md
|
||||
- [x] Retired the browser's native `<video controls>` (it was rendering
|
||||
as its own always-on-top bar, duplicating the cast icon and
|
||||
|
|
|
|||
|
|
@ -180,14 +180,43 @@
|
|||
z-index: 6;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
background: rgba(10, 8, 11, .8);
|
||||
border: 1px solid rgba(255, 255, 255, .08);
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
transition: padding .25s ease, background-color .25s ease, border-color .25s ease;
|
||||
}
|
||||
|
||||
.bottomBarButtons { display: flex; align-items: center; gap: 10px; }
|
||||
/* Collapsed down to just the seek bar after a stretch of no activity
|
||||
(see Player.ts's scheduleControlsHide) - asked for directly: the
|
||||
full bar, always visible, sits right where a native <track> subtitle
|
||||
renders (applySubtitleTrack) and was masking it. Only the padding/
|
||||
background/border and the buttons row transition - #progressBar
|
||||
itself (and its own generous invisible hit-area, see below) stays
|
||||
exactly where and how big it already was either way. */
|
||||
.bottomBar.minimal {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.bottomBarButtons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 8px; /* not .bottomBar's own gap - so .minimal can collapse just this row's own spacing, not the seek bar's position */
|
||||
max-height: 40px;
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
transition: max-height .25s ease, opacity .2s ease, margin-top .25s ease;
|
||||
}
|
||||
|
||||
.bottomBar.minimal .bottomBarButtons {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
margin-top: 0;
|
||||
pointer-events: none; /* never clickable while invisible - see Player.ts's own comment on why this needs no extra guarding at the click handlers themselves */
|
||||
}
|
||||
.bottomBarZone { display: flex; align-items: center; gap: 8px; }
|
||||
.bottomBarZone.left { flex-shrink: 0; }
|
||||
.bottomBarZone.center { flex: 1; justify-content: center; min-width: 0; }
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const TICKS_PER_SECOND = 10000000; // Emby/.NET convention: 1 tick = 100ns
|
|||
const DRIFT_TOLERANCE_SECONDS = 1.5;
|
||||
const REQUEST_COOLDOWN_MS = 5000; // matches the server's own throttle in SocketGateway - shared by both request types, each throttled independently
|
||||
const REQUEST_HIGHLIGHT_MS = 4000; // how long the banner + the orange badge stay up
|
||||
const CONTROLS_HIDE_DELAY_MS = 3000; // how long #bottomBar stays fully expanded after the last bit of activity, before collapsing to just its seek bar
|
||||
|
||||
// hls.js's own defaults (10s manifest/level/frag load timeouts) assume a
|
||||
// file server or a source already being direct-played/remuxed - too tight
|
||||
|
|
@ -196,6 +197,7 @@ function renderVideoPlayer(root: HTMLElement, partyId: string, party: PartyPubli
|
|||
const burnInHint = root.querySelector('#burnInHint')!;
|
||||
const castBtn = root.querySelector('#castBtn') as HTMLButtonElement;
|
||||
const fullscreenBtn = root.querySelector('#fullscreenBtn') as HTMLButtonElement;
|
||||
const bottomBar = root.querySelector('#bottomBar') as HTMLElement;
|
||||
const progressBar = root.querySelector('#progressBar') as HTMLElement;
|
||||
const progressFill = root.querySelector('#progressFill') as HTMLElement;
|
||||
const playPauseBtn = root.querySelector('#playPauseBtn') as HTMLButtonElement;
|
||||
|
|
@ -231,6 +233,8 @@ function renderVideoPlayer(root: HTMLElement, partyId: string, party: PartyPubli
|
|||
let pauseRequestCooldownUntil = 0;
|
||||
let resumeRequestCooldownUntil = 0;
|
||||
let requestBannerTimer: number | null = null;
|
||||
let controlsHideTimer: number | null = null;
|
||||
let isPointerOverBottomBar = false;
|
||||
// The party's shared self-hosted-ffmpeg burn-in choice (see
|
||||
// TranscodeService) - null means "no burn-in, everyone streams their
|
||||
// own normal per-viewer Emby-proxied copy" - initialized from the
|
||||
|
|
@ -433,6 +437,7 @@ function renderVideoPlayer(root: HTMLElement, partyId: string, party: PartyPubli
|
|||
updatePlayPauseBtn();
|
||||
updateProgressBar();
|
||||
sendLocalState();
|
||||
scheduleControlsHide(); // e.g. resuming from pause must re-arm the countdown, not leave the bar stuck open forever
|
||||
}
|
||||
|
||||
let isDraggingProgress = false;
|
||||
|
|
@ -487,6 +492,55 @@ function renderVideoPlayer(root: HTMLElement, partyId: string, party: PartyPubli
|
|||
updatePlayPauseBtn();
|
||||
updateMuteBtn();
|
||||
|
||||
/**
|
||||
* Auto-hides #bottomBar down to just its seek bar (see .bottomBar
|
||||
* .minimal in Player.css) after a stretch of no activity - asked for
|
||||
* directly: the full bar, always visible, sits right where a native
|
||||
* <track> subtitle renders (see applySubtitleTrack) and was masking
|
||||
* it. Applies to everyone, host or follower - the bar exists (and
|
||||
* would mask subtitles) for both. Stays expanded while genuinely
|
||||
* paused (nothing playing to hide from) or while the pointer is
|
||||
* sitting on the bar itself, and re-arms on every discrete playback
|
||||
* event too (onDriverStateChanged above), not just raw pointer
|
||||
* activity - resuming from pause has to restart the countdown, not
|
||||
* leave the bar stuck open forever.
|
||||
*/
|
||||
function clearControlsHideTimer() {
|
||||
if (controlsHideTimer) {
|
||||
clearTimeout(controlsHideTimer);
|
||||
controlsHideTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleControlsHide() {
|
||||
clearControlsHideTimer();
|
||||
controlsHideTimer = window.setTimeout(() => {
|
||||
if (activeDriver.paused || isPointerOverBottomBar) return;
|
||||
bottomBar.classList.add('minimal');
|
||||
}, CONTROLS_HIDE_DELAY_MS);
|
||||
}
|
||||
|
||||
function showControls() {
|
||||
bottomBar.classList.remove('minimal');
|
||||
scheduleControlsHide();
|
||||
}
|
||||
|
||||
function onBottomBarPointerEnter() {
|
||||
isPointerOverBottomBar = true;
|
||||
showControls();
|
||||
}
|
||||
|
||||
function onBottomBarPointerLeave() {
|
||||
isPointerOverBottomBar = false;
|
||||
scheduleControlsHide();
|
||||
}
|
||||
|
||||
playerWrap.addEventListener('pointermove', showControls);
|
||||
playerWrap.addEventListener('pointerdown', showControls);
|
||||
bottomBar.addEventListener('mouseenter', onBottomBarPointerEnter);
|
||||
bottomBar.addEventListener('mouseleave', onBottomBarPointerLeave);
|
||||
scheduleControlsHide();
|
||||
|
||||
/**
|
||||
* Promotes this client to host - called either at init (if we already
|
||||
* were) or on a live "hostChanged" event (the previous host left and the
|
||||
|
|
@ -1048,9 +1102,14 @@ function renderVideoPlayer(root: HTMLElement, partyId: string, party: PartyPubli
|
|||
if (heartbeat) clearInterval(heartbeat);
|
||||
if (progressTicker) clearInterval(progressTicker);
|
||||
if (requestBannerTimer) clearTimeout(requestBannerTimer);
|
||||
clearControlsHideTimer();
|
||||
if (hls) hls.destroy();
|
||||
document.removeEventListener('click', onDocumentClick);
|
||||
document.removeEventListener('fullscreenchange', onFullscreenChange);
|
||||
playerWrap.removeEventListener('pointermove', showControls);
|
||||
playerWrap.removeEventListener('pointerdown', showControls);
|
||||
bottomBar.removeEventListener('mouseenter', onBottomBarPointerEnter);
|
||||
bottomBar.removeEventListener('mouseleave', onBottomBarPointerLeave);
|
||||
unsubscribeCastAvailability();
|
||||
unsubscribeCastSessionEnded();
|
||||
// Deliberately NOT stopping an active cast here - leaving this page
|
||||
|
|
|
|||
|
|
@ -2734,6 +2734,41 @@ this sandbox has no reachable Emby stream to attach one to, so the
|
|||
event wiring is confirmed by reading, not by dragging a real seek bar.
|
||||
Worth exercising for real once deployed.
|
||||
|
||||
## The bottom bar auto-hides down to just the seek bar
|
||||
|
||||
Reported right after the previous section shipped: the bottom bar,
|
||||
now always visible, sits exactly where a native `<track>` subtitle
|
||||
renders (`applySubtitleTrack`) - and was masking it. The standard fix
|
||||
every real video player already uses: collapse to a minimal state
|
||||
after a stretch of no activity, expand again on the slightest
|
||||
interaction.
|
||||
|
||||
`#bottomBar` gets a `.minimal` class (`scheduleControlsHide`,
|
||||
`CONTROLS_HIDE_DELAY_MS` = 3s of no activity) that transitions away its
|
||||
padding/background/border and the buttons row's height/opacity -
|
||||
`#progressBar` itself is untouched by any of this (including its own
|
||||
generous invisible hit-area for seeking), so a thin seek line is always
|
||||
there, just without the box around it once minimal. Expands again on
|
||||
any `pointermove`/`pointerdown` anywhere in `.playerWrap`, or while the
|
||||
pointer is sitting on the bar itself (`mouseenter`/`mouseleave` pause
|
||||
the countdown entirely rather than just resetting it, so hovering the
|
||||
bar can never lose a race with its own timer). Stays expanded outright
|
||||
while genuinely paused - nothing playing to hide the bar *from* - and
|
||||
every discrete playback event (`onDriverStateChanged`) re-arms the
|
||||
countdown too, not just raw pointer activity, so resuming from a pause
|
||||
correctly restarts it rather than leaving the bar stuck open forever.
|
||||
Applies to everyone, host or follower alike - the bar (and the
|
||||
subtitle-masking problem) exists for both.
|
||||
|
||||
Verified: full `typecheck`/`build`. Confirmed in a real browser against
|
||||
the compiled bundle, side by side with an actual subtitle-shaped
|
||||
overlay in the same position: the expanded bar visibly overlaps it,
|
||||
the `.minimal` state (just the thin progress line) leaves it fully
|
||||
readable. The *timer* itself (does it actually fire after 3s of real
|
||||
inactivity, does hovering genuinely prevent it) wasn't driven through
|
||||
real wall-clock waits here - the CSS transition and class-toggling
|
||||
logic were checked directly instead.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Playback always goes through `hls.js` against whatever Emby's
|
||||
|
|
|
|||
Loading…
Reference in a new issue