From 6af83eb8969e1a3cf57ce4effb1c3d864701f303 Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Tue, 2 Jun 2026 18:35:31 -0400 Subject: [PATCH] zebra-spaces: request landscape camera + letterbox portrait frames Android Chrome / Firefox getUserMedia honour aspectRatio:{ideal:16/9} on the camera constraints to lock the sensor read-out to landscape regardless of the device's current screen orientation. Adding it stops the back camera from sending a portrait frame the receivers can't rotate. Belt-and-suspenders on the render side: camera tile video switches from object-fit:cover to object-fit:contain so any portrait stream that slips through (older browsers, manual override) letterboxes inside the 16:9 box instead of being middle-cropped into a square. Black background fills the bars. Thumbnails stay cover-cropped (visual consistency in the column); spotlight view always uses contain. --- web/zebra-spaces.html | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html index 2610fc7..1a453e1 100644 --- a/web/zebra-spaces.html +++ b/web/zebra-spaces.html @@ -326,8 +326,12 @@ } .camera-tile video { width: 100%; height: auto; display: block; - background: transparent; - aspect-ratio: 16 / 9; object-fit: cover; + background: #000; + /* contain (not cover) so a portrait stream from a mobile camera + * letterboxes inside the 16:9 box instead of being middle-cropped + * into a square. We still reserve a 16:9 slot for layout stability + * across mixed stream orientations. */ + aspect-ratio: 16 / 9; object-fit: contain; } .camera-tile .screen-meta { background: #f0f0f0; color: #333; font-size: 0.7rem; padding: 0.2rem 0.4rem; @@ -2182,7 +2186,16 @@ async function sfuUnpublishGame(){ async function sfuPublishCamera(){ if (sfuCameraPC || !myKeys || !roomID) return; let stream; - const videoConstraints = { width:{ideal:1280}, height:{ideal:720}, frameRate:{ideal:30} }; + /* aspectRatio:{ideal: 16/9} tells Android Chrome / Firefox to capture + * in landscape regardless of the device's current screen orientation. + * Without it the back camera in portrait delivers a portrait stream + * that the receivers then cover-crop into a square. With it the + * sensor reads out landscape and rotation no longer matters. */ + const videoConstraints = { + width: { ideal: 1280 }, height: { ideal: 720 }, + aspectRatio: { ideal: 16/9 }, + frameRate: { ideal: 30 }, + }; if (cameraDeviceId) videoConstraints.deviceId = { exact: cameraDeviceId }; try { /* camera only — mic comes through the separate sfuPublish path so a @@ -3696,8 +3709,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');