Reported right after trying the player rework: the action bar looked like two z-index layers with some elements duplicated, only in fullscreen. Root cause: video.controls = true (set for the host since this player's first version) has always rendered the browser's own native control bar - a separate piece of UI in its own always-on-top compositing layer, independent of this app's DOM/CSS. That was never visible before, since fullscreen used to show only the video; once it correctly started showing this app's own overlays too, the native bar and the custom one both became visible, and specifically duplicated - Chrome adds its own cast icon to a <video>'s native controls once it detects a Cast-capable device, entirely separate from #castBtn's own integration. Two fixes: - disableremoteplayback on <video> - the standard attribute telling the browser not to add its own cast icon, since this app has its own. Should have shipped with the original Chromecast feature. - Native <video controls> retired entirely, everywhere (not just fullscreen), replaced with a custom transport bar - asked for directly rather than just suppressing the symptom in fullscreen. PlaybackDriver (cast.ts) gained `duration` alongside currentTime/paused - the one thing a seek bar needs that the existing local/cast abstraction didn't yet expose. #bottomBar gained a seek row (progressBar/ progressFill) above the existing button row - host drags/clicks to seek by fraction of activeDriver.duration (pointer events, mouse and touch share one path); a follower's bar is the same element, read-only. A 500ms ticker keeps the fill/time label moving between discrete events. #playPauseBtn (host-only) and #muteBtn (everyone, local-only - never synced, hidden while casting since the local <video> is silent then) round out the replacement. One function, onDriverStateChanged, now threads through every discrete play/pause/seek event (the local <video>'s native events, and a cast session's RemotePlayerController events) so the button icon, the seek bar, and the party broadcast stay in lockstep regardless of which driver or gesture caused the change. Verified: full typecheck/build. Checked in a real browser against the compiled bundle - injected the new markup for both a playing/unmuted and a paused/muted state, confirmed icons/fill/time label/readOnly cursor all render as designed. Not verified: a real drag-to-seek gesture or real playback - no reachable Emby stream in this sandbox to attach a real <video> source to. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
123 lines
6.5 KiB
HTML
123 lines
6.5 KiB
HTML
<div class="playerWrap">
|
|
<!-- disableremoteplayback: without it, Chrome adds its OWN native cast
|
|
icon to this element's native controls (see becomeHost in
|
|
Player.ts) the moment it detects a Cast-capable device on the
|
|
network - entirely separate from, and visually duplicating,
|
|
#castBtn's own custom integration (cast.ts). Standard attribute
|
|
for exactly this - see docs/architecture.md. -->
|
|
<video id="video" playsinline disableremoteplayback></video>
|
|
|
|
<!-- Replaces the video content while casting - the local <video> is
|
|
paused/torn down (see Player.ts's startCasting/cast.ts), so
|
|
leaving its last frame frozen with no explanation would just look
|
|
stuck. See docs/architecture.md's "Casting to a Chromecast". -->
|
|
<div id="castingScreen" class="castingScreen hide">
|
|
<svg viewBox="0 0 24 24" width="40" height="40" aria-hidden="true">
|
|
<rect x="1" y="4" width="22" height="16" rx="2" fill="none" stroke="currentColor" stroke-width="1.6"></rect>
|
|
<path d="M4 15a5 5 0 0 1 5 5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"></path>
|
|
<path d="M4 11a9 9 0 0 1 9 9" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"></path>
|
|
<circle cx="4" cy="20" r="1.3" fill="currentColor"></circle>
|
|
</svg>
|
|
<strong>Diffusion sur le Chromecast</strong>
|
|
</div>
|
|
|
|
<p id="loadingNotice" class="loadingNotice"><span class="spinner"></span>Chargement du flux...</p>
|
|
<div id="playerError" class="playerError hide">
|
|
<p id="playerErrorText"></p>
|
|
<button id="playerErrorRetry" class="linklike">Réessayer</button>
|
|
</div>
|
|
|
|
<div class="topOverlay">
|
|
<div class="actionBar">
|
|
<span id="itemTitle" class="itemTitle"></span>
|
|
<span id="members" class="members"></span>
|
|
|
|
<button id="castBtn" class="iconBtn hide" title="Caster sur un Chromecast" aria-label="Caster sur un Chromecast" aria-pressed="false">
|
|
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
|
|
<rect x="1" y="4" width="22" height="16" rx="2" fill="none" stroke="currentColor" stroke-width="2"></rect>
|
|
<path d="M4 15a5 5 0 0 1 5 5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path>
|
|
<path d="M4 11a9 9 0 0 1 9 9" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path>
|
|
<circle cx="4" cy="20" r="1.5" fill="currentColor"></circle>
|
|
</svg>
|
|
</button>
|
|
|
|
<div id="settingsWrap" class="settingsWrap hide">
|
|
<button id="settingsBtn" class="iconBtn" title="Paramètres de lecture" aria-label="Paramètres de lecture">⚙</button>
|
|
<div id="settingsPanel" class="settingsPanel hide">
|
|
<label>Langue <select id="audioSelect"></select></label>
|
|
<label>Sous-titres <select id="subtitleSelect"><option value="">Aucun</option></select></label>
|
|
<label>Qualité <select id="qualitySelect"></select></label>
|
|
<p id="burnInHint" class="hint burnInHint hide">Flux incrusté actif pour toute la party - langue et qualité fixées tant qu'il l'est.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<button id="leaveBtn" class="linklike">Quitter</button>
|
|
</div>
|
|
|
|
<div id="presenceOverlay" class="presenceOverlay"></div>
|
|
</div>
|
|
|
|
<!-- Transient toast (see showRequestBanner in Player.ts) - positioned
|
|
independently of the bottom bar below, near the top, so it never
|
|
fights that persistent bar for space and reads as a pop-in/fade-out
|
|
notification rather than a fourth thing bolted onto a fixed layout. -->
|
|
<div id="requestBanner" class="requestBanner hide"></div>
|
|
|
|
<!-- One bar, bottom-anchored, replacing what used to be three separate
|
|
floating pieces (a centered banner/notice column, a request
|
|
toolbar cornered bottom-right, an "Épisode suivant" link up in the
|
|
top bar) AND the browser's own native <video controls> (see
|
|
becomeHost in Player.ts - never enabled any more, precisely
|
|
because that native bar used to render as its own always-on-top
|
|
layer fighting this one for the same space, especially visible
|
|
once plein écran started showing this bar too - see
|
|
docs/architecture.md). A seek bar on top, buttons below - staying
|
|
INSIDE .playerWrap, not a sibling positioned relative to it, is
|
|
what keeps all of this visible and clickable once plein écran
|
|
targets the whole wrap instead of just the <video>. -->
|
|
<div id="bottomBar" class="bottomBar">
|
|
<!-- Host: click/drag to seek (see Player.ts's pointerdown/move/up
|
|
handlers). Follower: purely a read-only position indicator,
|
|
ticked from activeDriver just like the fill. -->
|
|
<div id="progressBar" class="progressBar">
|
|
<div id="progressFill" class="progressFill"></div>
|
|
</div>
|
|
|
|
<div class="bottomBarButtons">
|
|
<div class="bottomBarZone left">
|
|
<button id="playPauseBtn" class="iconBtn hide" title="Lecture / pause" aria-label="Lecture / pause"></button>
|
|
<button id="muteBtn" class="iconBtn" title="Muet" aria-label="Muet"></button>
|
|
<span id="timeLabel" class="timeLabel"></span>
|
|
<button id="nextBtn" class="nextBtn hide">
|
|
<svg width="12" height="10" viewBox="0 0 14 10" aria-hidden="true">
|
|
<path d="M1 1 L6 5 L1 9 Z" fill="#fff"></path>
|
|
<path d="M7 1 L12 5 L7 9 Z" fill="#fff"></path>
|
|
<rect x="12.4" y="0.5" width="1.6" height="9" fill="#fff"></rect>
|
|
</svg>
|
|
Épisode suivant
|
|
</button>
|
|
<div id="requestToolbar" class="requestToolbar hide">
|
|
<button id="requestPauseBtn" class="requestBtn">
|
|
<svg width="9" height="9" viewBox="0 0 10 10" aria-hidden="true">
|
|
<rect x="1.5" y="1" width="2.4" height="8" fill="#fff"></rect>
|
|
<rect x="6.1" y="1" width="2.4" height="8" fill="#fff"></rect>
|
|
</svg>
|
|
Demander pause
|
|
</button>
|
|
<button id="requestResumeBtn" class="requestBtn">
|
|
<svg width="9" height="9" viewBox="0 0 10 10" aria-hidden="true">
|
|
<path d="M1.5 1 L8.5 5 L1.5 9 Z" fill="#fff"></path>
|
|
</svg>
|
|
Demander reprise
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="bottomBarZone center">
|
|
<span id="ambientNotice" class="ambientNotice hide"></span>
|
|
</div>
|
|
<div class="bottomBarZone right">
|
|
<button id="fullscreenBtn" class="fsBtn" title="Plein écran" aria-label="Plein écran"></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|