feat(ui): full-screen video stage with PiP local preview
Some checks failed
Mirror to GitHub / mirror (push) Failing after 28s
Build Release Binaries / build-amd64 (push) Failing after 3m5s

Move video out of the voice drawer into a fixed-position stage that
covers the lobby above the drawer. Remote canvas fills the stage with
object-fit: contain; local preview is a 200x112 PiP in the bottom-right.
Placeholder shows "Waiting for remote video" with a frame counter until
the first frame arrives. Counter logs first remote frame to console for
debugging.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-05-25 17:53:10 +04:00
parent 1329abbeba
commit cbc3a8d37e
3 changed files with 61 additions and 16 deletions

View File

@@ -105,11 +105,16 @@
</div> </div>
</div> </div>
<div id="vd-stats" class="vd-stats"></div> <div id="vd-stats" class="vd-stats"></div>
<!-- Video strip: remote (canvas) + local preview (video element) --> </div>
<div id="vd-video-strip" class="vd-video-strip hidden">
<canvas id="vd-remote-video" class="vd-video-tile" width="320" height="180"></canvas> <!-- ═════ Video stage — full-screen overlay above drawer ═════ -->
<video id="vd-local-video" class="vd-video-tile" autoplay muted playsinline></video> <div id="vd-video-strip" class="vd-video-stage hidden">
<canvas id="vd-remote-video" class="vd-remote-stage" width="1280" height="720"></canvas>
<div id="vd-remote-placeholder" class="vd-remote-placeholder">
<div class="vd-placeholder-text">Waiting for remote video…</div>
<div id="vd-remote-counter" class="vd-placeholder-sub">0 frames received</div>
</div> </div>
<video id="vd-local-video" class="vd-local-pip" autoplay muted playsinline></video>
</div> </div>
</div> </div>

View File

@@ -465,6 +465,9 @@ function leaveVoice() {
if (statusInterval) { clearInterval(statusInterval); statusInterval = null; } if (statusInterval) { clearInterval(statusInterval); statusInterval = null; }
stopCamera(); stopCamera();
remoteVideoActive = false; remoteVideoActive = false;
remoteFrameCount = 0;
vdRemoteCounter.textContent = "0 frames received";
vdRemotePlaceholder.classList.remove("hidden");
vdVideoStrip.classList.add("hidden"); vdVideoStrip.classList.add("hidden");
remoteCtx.clearRect(0, 0, vdRemoteVideo.width, vdRemoteVideo.height); remoteCtx.clearRect(0, 0, vdRemoteVideo.width, vdRemoteVideo.height);
} }
@@ -535,6 +538,9 @@ vdCamBtn.addEventListener("click", () => {
// ── Remote video display (Blocker 5) ───────────────────────────── // ── Remote video display (Blocker 5) ─────────────────────────────
const remoteCtx = vdRemoteVideo.getContext("2d")!; const remoteCtx = vdRemoteVideo.getContext("2d")!;
const vdRemotePlaceholder = document.getElementById("vd-remote-placeholder")!;
const vdRemoteCounter = document.getElementById("vd-remote-counter")!;
let remoteFrameCount = 0;
listen("video:frame", (event: any) => { listen("video:frame", (event: any) => {
const { width, height, jpeg_b64 } = event.payload; const { width, height, jpeg_b64 } = event.payload;
@@ -542,8 +548,11 @@ listen("video:frame", (event: any) => {
remoteVideoActive = true; remoteVideoActive = true;
vdVideoStrip.classList.remove("hidden"); vdVideoStrip.classList.remove("hidden");
vdRemotePlaceholder.classList.add("hidden");
vdRemoteVideo.width = width ?? vdRemoteVideo.width; vdRemoteVideo.width = width ?? vdRemoteVideo.width;
vdRemoteVideo.height = height ?? vdRemoteVideo.height; vdRemoteVideo.height = height ?? vdRemoteVideo.height;
remoteFrameCount++;
if (remoteFrameCount === 1) console.log("first remote video frame:", width, "x", height);
const img = new Image(); const img = new Image();
img.onload = () => { img.onload = () => {

View File

@@ -316,20 +316,51 @@ body {
padding: 2px 0 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding: 2px 0 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
} }
/* Video strip in voice drawer */ /* Full-screen video stage — overlays lobby/main when video is active */
.vd-video-strip { .vd-video-stage {
display: flex; position: fixed;
gap: 4px; top: 0;
padding: 4px 0 2px; left: 0;
overflow-x: auto; right: 0;
} bottom: 96px; /* leave room for voice drawer */
.vd-video-tile {
width: 160px;
height: 90px;
border-radius: 6px;
background: #000; background: #000;
z-index: 50;
overflow: hidden;
}
.vd-remote-stage {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
background: #000;
}
.vd-remote-placeholder {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #888;
pointer-events: none;
z-index: 1;
}
.vd-remote-placeholder.hidden { display: none; }
.vd-placeholder-text { font-size: 18px; margin-bottom: 8px; }
.vd-placeholder-sub { font-size: 12px; opacity: 0.7; }
.vd-local-pip {
position: absolute;
right: 16px;
bottom: 16px;
width: 200px;
height: 112px;
border-radius: 8px;
background: #111;
border: 2px solid rgba(255, 255, 255, 0.2);
object-fit: cover; object-fit: cover;
flex-shrink: 0; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
z-index: 2;
} }
/* Incoming call banner */ /* Incoming call banner */