refactor(camera): unify toggle paths through handleCameraAction
- Voice-bar and mobile camera buttons now use the canonical handler (fixes mobile no-op and voice-bar wrong-preset bugs) - Remove dead useLiveKit.toggleCamera - Add _intentionalCameraOff flag with mark/consume helpers
This commit is contained in:
@@ -8,6 +8,7 @@ import { useContextMenuStore, type ContextMenuItem } from '../../stores/contextM
|
|||||||
import { buildVoiceModMenuItems, VolumeSliderItem } from '../voice/voiceMenuItems';
|
import { buildVoiceModMenuItems, VolumeSliderItem } from '../voice/voiceMenuItems';
|
||||||
import { wsSend } from '../../hooks/useWebSocket';
|
import { wsSend } from '../../hooks/useWebSocket';
|
||||||
import { getChannelOrigin } from '../../stores/spaceStore';
|
import { getChannelOrigin } from '../../stores/spaceStore';
|
||||||
|
import { handleCameraAction } from '../../utils/voiceActions';
|
||||||
|
|
||||||
export function MobileVoiceFullScreen() {
|
export function MobileVoiceFullScreen() {
|
||||||
const popMobileScreen = useUIStore((s) => s.popMobileScreen);
|
const popMobileScreen = useUIStore((s) => s.popMobileScreen);
|
||||||
@@ -20,7 +21,6 @@ export function MobileVoiceFullScreen() {
|
|||||||
const isScreenSharing = useVoiceStore((s) => s.isScreenSharing);
|
const isScreenSharing = useVoiceStore((s) => s.isScreenSharing);
|
||||||
const toggleMute = useVoiceStore((s) => s.toggleMic);
|
const toggleMute = useVoiceStore((s) => s.toggleMic);
|
||||||
const toggleDeafen = useVoiceStore((s) => s.toggleDeafen);
|
const toggleDeafen = useVoiceStore((s) => s.toggleDeafen);
|
||||||
const toggleCamera = useVoiceStore((s) => s.toggleCamera);
|
|
||||||
const toggleScreenShare = useVoiceStore((s) => s.toggleScreenShare);
|
const toggleScreenShare = useVoiceStore((s) => s.toggleScreenShare);
|
||||||
const leaveVoice = useVoiceStore((s) => s.leaveVoice);
|
const leaveVoice = useVoiceStore((s) => s.leaveVoice);
|
||||||
const voiceUsers = useVoiceStore((s) => s.voiceUsers);
|
const voiceUsers = useVoiceStore((s) => s.voiceUsers);
|
||||||
@@ -283,7 +283,7 @@ export function MobileVoiceFullScreen() {
|
|||||||
|
|
||||||
{/* Camera */}
|
{/* Camera */}
|
||||||
<button
|
<button
|
||||||
onClick={toggleCamera}
|
onClick={handleCameraAction}
|
||||||
className={`w-12 h-12 rounded-full flex items-center justify-center transition-colors ${
|
className={`w-12 h-12 rounded-full flex items-center justify-center transition-colors ${
|
||||||
isCameraOn ? 'bg-accent-mint/20 text-accent-mint' : 'bg-surface-elevated text-txt-primary hover:bg-interactive-hover'
|
isCameraOn ? 'bg-accent-mint/20 text-accent-mint' : 'bg-surface-elevated text-txt-primary hover:bg-interactive-hover'
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { ConnectionInfoPopover } from './ConnectionInfoPopover';
|
|||||||
import { startScreenShare, stopScreenShare } from '../../utils/screenShare';
|
import { startScreenShare, stopScreenShare } from '../../utils/screenShare';
|
||||||
import { hasPermissionBit, PermissionBits } from '../../utils/permissions';
|
import { hasPermissionBit, PermissionBits } from '../../utils/permissions';
|
||||||
import { broadcastVoiceStatus } from '../../utils/voice';
|
import { broadcastVoiceStatus } from '../../utils/voice';
|
||||||
|
import { handleCameraAction } from '../../utils/voiceActions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* VoiceControls renders the voice status + button rows.
|
* VoiceControls renders the voice status + button rows.
|
||||||
@@ -17,7 +18,6 @@ export function VoiceControls() {
|
|||||||
const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId);
|
const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId);
|
||||||
const isCameraOn = useVoiceStore((s) => s.isCameraOn);
|
const isCameraOn = useVoiceStore((s) => s.isCameraOn);
|
||||||
const isScreenSharing = useVoiceStore((s) => s.isScreenSharing);
|
const isScreenSharing = useVoiceStore((s) => s.isScreenSharing);
|
||||||
const toggleCamera = useVoiceStore((s) => s.toggleCamera);
|
|
||||||
const rnnoiseEnabled = useVoiceStore((s) => s.rnnoiseEnabled);
|
const rnnoiseEnabled = useVoiceStore((s) => s.rnnoiseEnabled);
|
||||||
const setRnnoiseEnabled = useVoiceStore((s) => s.setRnnoiseEnabled);
|
const setRnnoiseEnabled = useVoiceStore((s) => s.setRnnoiseEnabled);
|
||||||
const connectionError = useVoiceStore((s) => s.connectionError);
|
const connectionError = useVoiceStore((s) => s.connectionError);
|
||||||
@@ -44,19 +44,6 @@ export function VoiceControls() {
|
|||||||
const channel = channels.find(c => c.id === currentVoiceChannelId);
|
const channel = channels.find(c => c.id === currentVoiceChannelId);
|
||||||
const channelName = channel?.name ?? (activeDmCall ? 'DM Call' : 'Voice Channel');
|
const channelName = channel?.name ?? (activeDmCall ? 'DM Call' : 'Voice Channel');
|
||||||
|
|
||||||
const handleCamera = async () => {
|
|
||||||
const room = getActiveRoom();
|
|
||||||
if (!room) return;
|
|
||||||
try {
|
|
||||||
const willEnable = !isCameraOn;
|
|
||||||
await room.localParticipant.setCameraEnabled(willEnable);
|
|
||||||
toggleCamera();
|
|
||||||
broadcastVoiceStatus();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[VoiceControls] Failed to toggle camera:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleScreenShare = async () => {
|
const handleScreenShare = async () => {
|
||||||
const room = getActiveRoom();
|
const room = getActiveRoom();
|
||||||
console.log('[SS] handleScreenShare clicked, room:', !!room, 'isScreenSharing:', isScreenSharing);
|
console.log('[SS] handleScreenShare clicked, room:', !!room, 'isScreenSharing:', isScreenSharing);
|
||||||
@@ -162,7 +149,7 @@ export function VoiceControls() {
|
|||||||
<div className="relative flex items-center gap-1 px-3 pb-2 pt-1">
|
<div className="relative flex items-center gap-1 px-3 pb-2 pt-1">
|
||||||
{canSpeak && (
|
{canSpeak && (
|
||||||
<button
|
<button
|
||||||
onClick={handleCamera}
|
onClick={handleCameraAction}
|
||||||
className={`${btnBase} ${
|
className={`${btnBase} ${
|
||||||
isCameraOn
|
isCameraOn
|
||||||
? 'bg-surface-base text-status-online hover:bg-surface-channel'
|
? 'bg-surface-base text-status-online hover:bg-surface-channel'
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import { broadcastVoiceStatus } from '../utils/voice';
|
|||||||
import { AudioManager } from '../audio/AudioManager';
|
import { AudioManager } from '../audio/AudioManager';
|
||||||
import { SpeakingDetector } from '../audio/SpeakingDetector';
|
import { SpeakingDetector } from '../audio/SpeakingDetector';
|
||||||
import {
|
import {
|
||||||
CAMERA_PRESET,
|
|
||||||
CAMERA_OVERDRIVE,
|
CAMERA_OVERDRIVE,
|
||||||
buildScreenShareOptions,
|
buildScreenShareOptions,
|
||||||
applyOverdrive,
|
applyOverdrive,
|
||||||
@@ -644,16 +643,6 @@ export function useLiveKit() {
|
|||||||
useVoiceStore.getState().toggleMic();
|
useVoiceStore.getState().toggleMic();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toggleCamera = useCallback(async () => {
|
|
||||||
if (roomRef.current) {
|
|
||||||
if (!isCameraOn) {
|
|
||||||
await roomRef.current.localParticipant.setCameraEnabled(true, { resolution: CAMERA_PRESET.resolution, frameRate: CAMERA_PRESET.encoding.maxFramerate }, { videoCodec: CAMERA_PRESET.codec, videoEncoding: CAMERA_PRESET.encoding, simulcast: true });
|
|
||||||
setTimeout(() => { if (roomRef.current) applyOverdrive(roomRef.current, Track.Source.Camera, CAMERA_OVERDRIVE); }, 2000);
|
|
||||||
} else { await roomRef.current.localParticipant.setCameraEnabled(false); }
|
|
||||||
updateParticipants();
|
|
||||||
}
|
|
||||||
}, [isCameraOn, updateParticipants]);
|
|
||||||
|
|
||||||
const toggleScreenShare = useCallback(async () => {
|
const toggleScreenShare = useCallback(async () => {
|
||||||
if (!roomRef.current) return;
|
if (!roomRef.current) return;
|
||||||
if (!useVoiceStore.getState().isScreenSharing) {
|
if (!useVoiceStore.getState().isScreenSharing) {
|
||||||
@@ -721,5 +710,5 @@ export function useLiveKit() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
return { room, isConnected, isConnecting, connectionState, connectedChannelId, connectionError, connect, disconnect, toggleMic, toggleCamera, toggleScreenShare };
|
return { room, isConnected, isConnecting, connectionState, connectedChannelId, connectionError, connect, disconnect, toggleMic, toggleScreenShare };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,25 @@ import { getChannelOrigin } from '../stores/spaceStore';
|
|||||||
import { broadcastVoiceStatus, broadcastDeafenViaLiveKit } from './voice';
|
import { broadcastVoiceStatus, broadcastDeafenViaLiveKit } from './voice';
|
||||||
import { CAMERA_PRESET, startScreenShare, stopScreenShare } from './screenShare';
|
import { CAMERA_PRESET, startScreenShare, stopScreenShare } from './screenShare';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One-shot flag used to distinguish user-initiated camera-off from unexpected
|
||||||
|
* track-end events (hardware unplug, OS permission revoke). Set right before
|
||||||
|
* `setCameraEnabled(false)` in any deliberate disable path; consumed-and-cleared
|
||||||
|
* by the camera-track `ended` handler in useLiveKit. Module-level by design:
|
||||||
|
* never persisted, never on the store, single producer + single consumer.
|
||||||
|
*/
|
||||||
|
let _intentionalCameraOff = false;
|
||||||
|
|
||||||
|
export function markIntentionalCameraOff(): void {
|
||||||
|
_intentionalCameraOff = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function consumeIntentionalCameraOff(): boolean {
|
||||||
|
const v = _intentionalCameraOff;
|
||||||
|
_intentionalCameraOff = false;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle mute. Respects space-mute/deafen guards.
|
* Toggle mute. Respects space-mute/deafen guards.
|
||||||
* Extracted from VoiceControlBar so keybinds and buttons share the same logic.
|
* Extracted from VoiceControlBar so keybinds and buttons share the same logic.
|
||||||
@@ -31,7 +50,8 @@ export function handleDeafenAction(isSpaceDeafened: boolean): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle camera. Requires LiveKit room.
|
* Toggle camera. Requires LiveKit room. Sole canonical camera-toggle path —
|
||||||
|
* the voice-bar button, mobile button, and keybind all funnel through here.
|
||||||
*/
|
*/
|
||||||
export async function handleCameraAction(): Promise<void> {
|
export async function handleCameraAction(): Promise<void> {
|
||||||
const room = getActiveRoom();
|
const room = getActiveRoom();
|
||||||
@@ -40,8 +60,9 @@ export async function handleCameraAction(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
const willEnable = !isCameraOn;
|
const willEnable = !isCameraOn;
|
||||||
if (willEnable) {
|
if (willEnable) {
|
||||||
await room.localParticipant.setCameraEnabled(true,
|
await room.localParticipant.setCameraEnabled(
|
||||||
{ resolution: CAMERA_PRESET.resolution },
|
true,
|
||||||
|
{ resolution: CAMERA_PRESET.resolution, frameRate: CAMERA_PRESET.encoding.maxFramerate },
|
||||||
{
|
{
|
||||||
videoCodec: CAMERA_PRESET.codec,
|
videoCodec: CAMERA_PRESET.codec,
|
||||||
videoEncoding: CAMERA_PRESET.encoding,
|
videoEncoding: CAMERA_PRESET.encoding,
|
||||||
@@ -49,6 +70,9 @@ export async function handleCameraAction(): Promise<void> {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
// Mark this disable as intentional so the track-`ended` handler skips
|
||||||
|
// its unplug/permission-revoke probe + toast.
|
||||||
|
markIntentionalCameraOff();
|
||||||
await room.localParticipant.setCameraEnabled(false);
|
await room.localParticipant.setCameraEnabled(false);
|
||||||
}
|
}
|
||||||
useVoiceStore.getState().toggleCamera();
|
useVoiceStore.getState().toggleCamera();
|
||||||
|
|||||||
Reference in New Issue
Block a user