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');