A camera that can be turned on after the call started
The camera was a decision made once, at the door: "Join with video" or not, and nothing afterwards. That is the moment nobody knows yet whether they want to be seen - the answer usually arrives a minute later, and there was nowhere to say it. So the toggle exists, beside the microphone and the screen, in the order every client puts them in. The plumbing was already there: livekit could enable a camera, the call already collects and shows remote video tracks, and screen share has worked for a while - what was missing was the one control. Offered only on a call held on a media server, which the call now says it is. A one-to-one Matrix call is genuinely voice-only here (#189), and a camera button that cannot turn a camera on is worse than no button at all. Not verified in a live call: it takes two people, and the working account's rooms all have strangers in them. What is verified is the state - a call joined with video reports its camera on, one joined without reports it off, and the toggle is only drawn where it can act. Refs #189. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -214,6 +214,19 @@ export function CallStage({ mode = 'inline' }: { mode?: 'inline' | 'pip' }): JSX
|
||||
className={call.muted ? 'calling' : undefined}
|
||||
onClick={() => store.toggleCallMute()}
|
||||
/>
|
||||
{/* Between the microphone and the screen, which is the order every
|
||||
client puts them in - and the order of how often they are
|
||||
pressed. Offered only where it can work: a one-to-one Matrix
|
||||
call is voice here, and a camera button that cannot turn a
|
||||
camera on is worse than no button. */}
|
||||
{call.onServer && (
|
||||
<IconButton
|
||||
name={call.cameraOn ? 'videocam' : 'videocam_off'}
|
||||
title={call.cameraOn ? 'Turn the camera off' : 'Turn the camera on'}
|
||||
className={call.cameraOn ? 'active' : undefined}
|
||||
onClick={() => void store.toggleCamera()}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
name={call.sharingScreen ? 'stop_screen_share' : 'screen_share'}
|
||||
title={call.sharingScreen ? 'Stop sharing' : 'Share a screen or window'}
|
||||
|
||||
@@ -213,6 +213,27 @@ export class SfuCall {
|
||||
return on
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns the camera on, or off. Returns whether it is on.
|
||||
*
|
||||
* A call could be *joined* with video and never changed after - the camera
|
||||
* was a decision made once, before anybody was on screen, which is the
|
||||
* moment nobody knows yet whether they want to be seen.
|
||||
*/
|
||||
async toggleCamera(): Promise<boolean> {
|
||||
const on = this.room.localParticipant.isCameraEnabled
|
||||
await this.room.localParticipant.setCameraEnabled(!on)
|
||||
// The local tile shows what this end is sending, so it has to be rebuilt
|
||||
// when that changes - the same reason toggleScreen does it.
|
||||
this.local = this.ownStream()
|
||||
return !on
|
||||
}
|
||||
|
||||
/** Whether this end is sending a picture at all. */
|
||||
get cameraOn(): boolean {
|
||||
return this.room.localParticipant.isCameraEnabled
|
||||
}
|
||||
|
||||
/** Shares a screen, or stops. Returns whether one is being shared. */
|
||||
async toggleScreen(): Promise<boolean> {
|
||||
const on = this.room.localParticipant.isScreenShareEnabled
|
||||
|
||||
@@ -515,6 +515,13 @@ export interface ChatState {
|
||||
video: boolean
|
||||
muted: boolean
|
||||
sharingScreen: boolean
|
||||
/** Whether this end is sending a picture. */
|
||||
cameraOn: boolean
|
||||
/**
|
||||
* Whether the call is held on a media server rather than between two
|
||||
* people. It decides what can be offered: a camera needs the server.
|
||||
*/
|
||||
onServer: boolean
|
||||
} | null
|
||||
/** Who each Matrix account has asked never to hear from. */
|
||||
ignoredByAccount: Record<string, string[]>
|
||||
@@ -900,7 +907,11 @@ export class ChatStore {
|
||||
phase,
|
||||
video: group.video,
|
||||
muted: this.state.activeCall?.muted ?? false,
|
||||
sharingScreen: false
|
||||
sharingScreen: false,
|
||||
// A call joined with video starts sending one; otherwise the
|
||||
// camera is off until somebody turns it on.
|
||||
cameraOn: group.video,
|
||||
onServer: true
|
||||
}
|
||||
})
|
||||
return
|
||||
@@ -917,7 +928,9 @@ export class ChatStore {
|
||||
phase,
|
||||
video: call.video,
|
||||
muted: this.state.activeCall?.muted ?? false,
|
||||
sharingScreen: call.call.sharingScreen
|
||||
sharingScreen: call.call.sharingScreen,
|
||||
cameraOn: false,
|
||||
onServer: false
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -2049,7 +2062,11 @@ export class ChatStore {
|
||||
phase: 'connecting',
|
||||
video,
|
||||
muted: false,
|
||||
sharingScreen: false
|
||||
sharingScreen: false,
|
||||
// Joined with video means the camera is already on; joined without
|
||||
// means it is off until somebody asks for it.
|
||||
cameraOn: video,
|
||||
onServer: true
|
||||
}
|
||||
})
|
||||
await this.matrixCalls.joinGroup(account.id, bufferId, video)
|
||||
@@ -2212,6 +2229,27 @@ export class ChatStore {
|
||||
this.set({ activeCall: { ...this.state.activeCall, muted } })
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns the camera on, or off, in a call already running.
|
||||
*
|
||||
* Only on a media server. A one-to-one Matrix call is voice here - see #189 -
|
||||
* and offering a camera button that cannot do anything would be worse than
|
||||
* not offering one.
|
||||
*/
|
||||
async toggleCamera(): Promise<void> {
|
||||
if (!this.state.activeCall) return
|
||||
const group = this.matrixCalls.currentGroup
|
||||
if (!group?.sfu) {
|
||||
this.toast('info', 'A camera needs a call held on a media server.')
|
||||
return
|
||||
}
|
||||
const on = await group.sfu.toggleCamera().catch((e: Error) => {
|
||||
this.toast('error', e.message)
|
||||
return false
|
||||
})
|
||||
this.set({ activeCall: { ...this.state.activeCall, cameraOn: on } })
|
||||
}
|
||||
|
||||
/**
|
||||
* Shares a screen or a window into the call.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user