fixed camera mirroring

This commit is contained in:
2026-07-23 07:06:06 +00:00
parent e19fc103b9
commit 52acba0dfc
4 changed files with 52 additions and 37 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ if os.path.exists('.env'):
# Version
VERSION = '1.0.2'
VERSION = '1.0.3'
# Server settings
SERVER_HOST = '0.0.0.0'
+1 -1
View File
@@ -97,7 +97,7 @@
<path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/>
</svg>
</button>
<button id="flip-cam-btn" class="control-btn mobile-only" title="Flip Camera">
<button id="flip-cam-btn" class="control-btn mobile-only hidden" title="Flip Camera">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M20 4h-3.17L15 2H9L7.17 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-8 3c.98 0 1.87.31 2.6.84l-1.1 1.1c-.44-.28-.95-.44-1.5-.44-1.65 0-3 1.35-3 3h1.75L8.5 16.75 6.25 14.5H8c0-2.21 1.79-4 4-4zm0 10c-.98 0-1.87-.31-2.6-.84l1.1-1.1c.44.28.95.44 1.5.44 1.65 0 3-1.35 3-3h-1.75l2.25-2.25 2.25 2.25H16c0 2.21-1.79 4-4 4z"/>
</svg>
+47 -35
View File
@@ -106,50 +106,62 @@ async function toggleCam() {
}
$('cam-btn').classList.toggle('active', state.camEnabled);
// Flip-camera button is only useful when the camera is on (mobile shows it via CSS).
$('flip-cam-btn')?.classList.toggle('hidden', !state.camEnabled);
updateUI();
}
// Mobile flip-camera button: switch between front ('user') and back ('environment').
// If the camera is off, we just remember the choice for next time it's turned on.
async function flipCamera() {
state.facingMode = (state.facingMode === 'environment') ? 'user' : 'environment';
// Acquire a camera track for the given facing and wire it into the local stream + every
// peer's camera sender. Removes any leftover camera track first (screen-share lives on a
// separate stream, so it's untouched).
async function swapCameraTrack(facing) {
const stream = await navigator.mediaDevices.getUserMedia({
video: { ...getVideoConstraints(), facingMode: { ideal: facing } }
});
const track = stream.getVideoTracks()[0];
if (!state.camEnabled) {
updateUI();
return;
state.localStream.getVideoTracks().forEach(t => {
t.stop();
state.localStream.removeTrack(t);
});
state.localStream.addTrack(track);
for (const [peerId, peer] of Object.entries(state.peers)) {
const sender = peer.pc.getSenders().find(s => s.track && s.track.kind === 'video' && s !== peer.screenSender);
if (sender) await sender.replaceTrack(track);
}
}
// Mobile flip-camera button: switch between front ('user') and back ('environment').
// The button is only shown while the camera is on (see toggleCam), so there's always a
// camera to flip.
async function flipCamera() {
if (!state.camEnabled) return;
const prev = state.facingMode;
const next = (prev === 'environment') ? 'user' : 'environment';
// Release the current camera BEFORE opening the other one - many phones can't hold
// both cameras at once, which would make getUserMedia throw and the flip silently fail.
state.localStream.getVideoTracks().forEach(t => {
t.stop();
state.localStream.removeTrack(t);
});
try {
const newStream = await navigator.mediaDevices.getUserMedia({
video: { ...getVideoConstraints(), facingMode: { ideal: state.facingMode } }
});
const newTrack = newStream.getVideoTracks()[0];
// Swap the camera track in the local stream (leave any screen-share track alone).
const oldTrack = state.localStream.getVideoTracks()[0];
if (oldTrack) {
oldTrack.stop();
state.localStream.removeTrack(oldTrack);
}
state.localStream.addTrack(newTrack);
// Replace on each peer's CAMERA video sender (skip the screen-share sender).
for (const [peerId, peer] of Object.entries(state.peers)) {
const sender = peer.pc.getSenders().find(s => s.track && s.track.kind === 'video' && s !== peer.screenSender);
if (sender) await sender.replaceTrack(newTrack);
}
// Re-apply outgoing shaping to the fresh sender track.
if (typeof applyVideoBitrateCap === 'function') applyVideoBitrateCap();
if (typeof applyAudioOnlyGatingAll === 'function') applyAudioOnlyGatingAll();
updateVideoGrid();
console.log('[Camera] Flipped to', state.facingMode);
state.facingMode = next;
await swapCameraTrack(next);
console.log('[Camera] Flipped to', next);
} catch (e) {
console.error('[Camera] Flip failed:', e);
// Revert the desired facing so the button state stays truthful.
state.facingMode = (state.facingMode === 'environment') ? 'user' : 'environment';
console.error('[Camera] Flip failed, restoring previous camera:', e);
state.facingMode = prev;
try { await swapCameraTrack(prev); } catch (e2) { console.error('[Camera] Restore failed:', e2); }
}
// Re-apply outgoing shaping to the fresh sender track.
if (typeof applyVideoBitrateCap === 'function') applyVideoBitrateCap();
if (typeof applyAudioOnlyGatingAll === 'function') applyAudioOnlyGatingAll();
updateVideoGrid();
}
async function toggleScreen() {
+3
View File
@@ -1498,6 +1498,9 @@ body {
Display-only - the stream sent to other people is unaffected. Never mirror a screen share. */
.video-tile.local:not(.screen-share) video { transform: scaleX(-1); }
/* Flip-camera button reads as an enabled action (brighter than the default grey icon). */
#flip-cam-btn { color: var(--text-primary); }
@media (max-width: 480px) {
.video-tile { border-radius: 8px; }
.video-tile .username { font-size: 0.75rem; padding: 0.5rem 0.75rem; }