zebra-spaces: re-acquire mobile camera on device orientation change
The aspectRatio constraint hint at getUserMedia time is unreliable on Android Chrome / Firefox — the sensor's natural read-out orientation gets locked at acquire time and rotating the phone does not update the encoded frames. Listeners stay seeing whatever orientation the camera was first opened in. screen.orientation 'change' (with the orientationchange legacy event as fallback) now triggers a re-acquire of the camera with the same constraints. The fresh video track is swapped into the existing publish PC's sender via replaceTrack — no SDP renegotiation, no SFU-side supplant, no subscriber renegotiation. Subscribers just start receiving frames in the new orientation within a few hundred ms. Debounced 350ms so a fast rotation flick doesn't double-fire. Old stream's tracks are stopped only after the swap so the encoder has the new source ready before the old one ends.
This commit is contained in:
parent
6af83eb896
commit
fc11aee569
1 changed files with 64 additions and 2 deletions
|
|
@ -2252,6 +2252,68 @@ async function sfuUnpublishCamera(){
|
|||
try { sessionStorage.removeItem(ACTIVE_CAM_KEY); } catch(_){}
|
||||
}
|
||||
|
||||
/* On mobile the camera sensor's natural read-out orientation is locked
|
||||
* at getUserMedia() time. Rotating the phone does NOT update the encoded
|
||||
* frames' orientation — listeners see whatever was captured initially,
|
||||
* regardless of how the publisher is holding the device now.
|
||||
*
|
||||
* Fix: when the device orientation changes while we're publishing, re-
|
||||
* acquire the camera so the sensor reads out in the new orientation.
|
||||
* We swap the track in the existing publisher's RTCRtpSender via
|
||||
* replaceTrack — no SDP renegotiation, no SFU-side supplant, no
|
||||
* subscriber renegotiation. Subscribers just start receiving frames
|
||||
* in the new orientation a few hundred ms later.
|
||||
*
|
||||
* Debounced 350ms so a fast orientation flick doesn't trigger two
|
||||
* back-to-back captures. */
|
||||
let cameraOrientationDebounce = null;
|
||||
async function reacquireCameraForOrientation(){
|
||||
if (!sfuCameraPC || !sfuCameraStream || !myKeys) return;
|
||||
const constraints = {
|
||||
width: { ideal: 1280 }, height: { ideal: 720 },
|
||||
aspectRatio: { ideal: 16/9 },
|
||||
frameRate: { ideal: 30 },
|
||||
};
|
||||
if (cameraDeviceId) constraints.deviceId = { exact: cameraDeviceId };
|
||||
let fresh;
|
||||
try { fresh = await navigator.mediaDevices.getUserMedia({ video: constraints, audio: false }); }
|
||||
catch(e){ logLine('err','camera re-acquire failed: '+e.message); return; }
|
||||
const newVideo = fresh.getVideoTracks()[0];
|
||||
if (!newVideo){ fresh.getTracks().forEach(t=>t.stop()); return; }
|
||||
newVideo.contentHint = 'motion';
|
||||
newVideo.addEventListener('ended', () => { sfuUnpublishCamera(); });
|
||||
/* swap the sender's track */
|
||||
let swapped = false;
|
||||
for (const s of sfuCameraPC.getSenders()){
|
||||
if (s.track && s.track.kind === 'video'){
|
||||
try { await s.replaceTrack(newVideo); swapped = true; break; }
|
||||
catch(e){ logLine('err','replaceTrack: '+e.message); }
|
||||
}
|
||||
}
|
||||
if (!swapped){ fresh.getTracks().forEach(t=>t.stop()); return; }
|
||||
/* stop the old stream's tracks AFTER swap so the encoder has the
|
||||
* new source ready before the old one ends */
|
||||
const old = sfuCameraStream;
|
||||
sfuCameraStream = fresh;
|
||||
if (old) old.getTracks().forEach(t=>t.stop());
|
||||
renderCameraTile(myKeys.pubHex, fresh, { local: true });
|
||||
const w = newVideo.getSettings ? (newVideo.getSettings().width||'?') : '?';
|
||||
const h = newVideo.getSettings ? (newVideo.getSettings().height||'?') : '?';
|
||||
logLine('', 'camera re-captured for new orientation ('+w+'x'+h+')');
|
||||
}
|
||||
function onCameraOrientationChange(){
|
||||
if (cameraOrientationDebounce) clearTimeout(cameraOrientationDebounce);
|
||||
cameraOrientationDebounce = setTimeout(() => {
|
||||
cameraOrientationDebounce = null;
|
||||
reacquireCameraForOrientation();
|
||||
}, 350);
|
||||
}
|
||||
if (window.screen && window.screen.orientation && window.screen.orientation.addEventListener){
|
||||
window.screen.orientation.addEventListener('change', onCameraOrientationChange);
|
||||
} else {
|
||||
window.addEventListener('orientationchange', onCameraOrientationChange);
|
||||
}
|
||||
|
||||
async function sfuUnpublish(){
|
||||
if (!sfuPubPC) return;
|
||||
const pid = sfuPubPeerID;
|
||||
|
|
@ -3709,8 +3771,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> · built <span class="stamp-date">2026-06-02</span><br>
|
||||
md5 <span class="stamp-md5">ac20654825a05d94fc8840fc14a7ccec</span><br>
|
||||
sha256 <span class="stamp-sha">4d7d6235f401636cb12b323236d4a0381a0d82a1af7e3c1bb2854a8d725a4572</span><br>
|
||||
md5 <span class="stamp-md5">d6220b4e18f5dbe18bcce34b3762e67b</span><br>
|
||||
sha256 <span class="stamp-sha">eae5efabeda7df668ae9b10cd962d49e2bc3dfb370fa38041a373591afcf3e4e</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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue