fixed camera mirroring
This commit is contained in:
@@ -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
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user