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.
This commit is contained in:
Russell Ballestrini 2026-06-02 18:35:31 -04:00
parent 666ba8e88a
commit 6af83eb896
No known key found for this signature in database

View file

@ -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');
<footer style="margin:2.2rem auto 0;font-size:0.65rem;color:#999;line-height:1.7;word-break:break-all;font-family:monospace">
<span id="pi-seal" style="color:#777;cursor:default;user-select:none" title="">page integrity</span> &nbsp;·&nbsp; built <span class="stamp-date">2026-06-02</span><br>
md5 <span class="stamp-md5">aca11b254d306fc675e74a2b7b76c3c9</span><br>
sha256 <span class="stamp-sha">bf43d6fa4bec117e6e77ebe4b096ab69c0df5ed5cb76f01fc515666209ad800a</span><br>
md5 <span class="stamp-md5">ac20654825a05d94fc8840fc14a7ccec</span><br>
sha256 <span class="stamp-sha">4d7d6235f401636cb12b323236d4a0381a0d82a1af7e3c1bb2854a8d725a4572</span><br>
<span style="color:#bbb">hashes are of this page with these two fields zeroed — to verify, blank them and re-hash</span><br>
<span style="color:#bbb">one self-contained file — <strong>save a copy</strong> and verify against these hashes; point at your own servers with ?signal= and ?turncred=, or <a href="host-your-own.html" style="color:#999">host your own community</a></span>
</footer>