feat(ui): full-screen video stage with PiP local preview
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:
@@ -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 id="vd-video-strip" class="vd-video-strip hidden">
|
|
||||||
<canvas id="vd-remote-video" class="vd-video-tile" width="320" height="180"></canvas>
|
|
||||||
<video id="vd-local-video" class="vd-video-tile" autoplay muted playsinline></video>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ═════ Video stage — full-screen overlay above drawer ═════ -->
|
||||||
|
<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>
|
||||||
|
<video id="vd-local-video" class="vd-local-pip" autoplay muted playsinline></video>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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 = () => {
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
Reference in New Issue
Block a user