mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-01 13:33:03 +00:00
desktop: show call devices in one menu
This commit is contained in:
1 parent
c913626b69
commit
e957689eae
4 files changed
+32
-30
No files matched your search
@@ -1408,6 +1408,7 @@
|
||||
<string name="icon_descr_audio_call">audio call</string>
|
||||
<string name="call_desktop_engine_error">Calls are not available</string>
|
||||
<string name="call_screen_share_screen">Screen %d</string>
|
||||
<string name="call_devices">Devices</string>
|
||||
<string name="call_audio_device_system_default">System default</string>
|
||||
<string name="call_mic_connected">Microphone %s connected</string>
|
||||
<string name="call_mic_fallback">Microphone %s disconnected, using System default</string>
|
||||
|
||||
+27
-26
@@ -283,9 +283,7 @@ private fun CallControls(engine: NativeCallEngine, call: Call, modifier: Modifie
|
||||
if (cameraOn) MR.strings.icon_descr_video_off else MR.strings.icon_descr_video_on
|
||||
) { chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Camera, enable = !cameraOn)) }
|
||||
ScreenShareButton(engine, sources.screenVideo)
|
||||
CameraPickerButton(engine)
|
||||
AudioDevicePickerButton(engine, AudioDeviceKind.Mic, appPrefs.desktopCallMic, painterResource(MR.images.ic_keyboard_voice), MR.strings.permissions_record_audio)
|
||||
AudioDevicePickerButton(engine, AudioDeviceKind.Speaker, appPrefs.desktopCallSpeaker, painterResource(MR.images.ic_headphones), MR.strings.audio_device_speaker)
|
||||
DevicesButton(engine)
|
||||
ControlButton(painterResource(MR.images.ic_call_end_filled), MR.strings.icon_descr_hang_up, background = Color.Red) {
|
||||
withBGApi { chatModel.callManager.endCall(call) }
|
||||
}
|
||||
@@ -328,15 +326,22 @@ private fun ScreenShareButton(engine: NativeCallEngine, sharing: Boolean) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CameraPickerButton(engine: NativeCallEngine) {
|
||||
private fun DevicesButton(engine: NativeCallEngine) {
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
val mics = remember { mutableStateOf(emptyList<AudioDevice>()) }
|
||||
val speakers = remember { mutableStateOf(emptyList<AudioDevice>()) }
|
||||
val cameras = remember { mutableStateOf(emptyList<VideoDevice>()) }
|
||||
Box {
|
||||
ControlButton(painterResource(MR.images.ic_settings), MR.strings.permissions_camera) {
|
||||
ControlButton(painterResource(MR.images.ic_settings), MR.strings.call_devices) {
|
||||
mics.value = engine.audioDevices(AudioDeviceKind.Mic)
|
||||
speakers.value = engine.audioDevices(AudioDeviceKind.Speaker)
|
||||
cameras.value = NativeCallEngine.videoCaptureDevices()
|
||||
showMenu.value = true
|
||||
}
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
AudioDeviceItems(engine, AudioDeviceKind.Mic, appPrefs.desktopCallMic, mics.value, MR.strings.permissions_record_audio, showMenu)
|
||||
AudioDeviceItems(engine, AudioDeviceKind.Speaker, appPrefs.desktopCallSpeaker, speakers.value, MR.strings.audio_device_speaker, showMenu)
|
||||
DevicesSectionTitle(MR.strings.permissions_camera)
|
||||
if (cameras.value.isEmpty()) {
|
||||
Text(stringResource(MR.strings.camera_not_available), Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
@@ -351,31 +356,27 @@ private fun CameraPickerButton(engine: NativeCallEngine) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AudioDevicePickerButton(engine: NativeCallEngine, kind: AudioDeviceKind, pref: SharedPreference<String?>, icon: Painter, description: StringResource) {
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
val devices = remember { mutableStateOf(emptyList<AudioDevice>()) }
|
||||
Box {
|
||||
ControlButton(icon, description) {
|
||||
devices.value = engine.audioDevices(kind)
|
||||
showMenu.value = true
|
||||
}
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
val chosenId = storedAudioDevice(pref.state.value)?.id
|
||||
(listOf(null) + devices.value).forEach { device ->
|
||||
ItemAction(
|
||||
device?.name ?: stringResource(MR.strings.call_audio_device_system_default),
|
||||
{ if (device?.descriptor == chosenId) Icon(painterResource(MR.images.ic_check), null, tint = MenuTextColor) },
|
||||
onClick = {
|
||||
showMenu.value = false
|
||||
pref.set(device?.let { json.encodeToString(SavedAudioDevice.serializer(), savedAudioDevice(it)) })
|
||||
engine.selectAudioDevice(kind, device)
|
||||
}
|
||||
)
|
||||
private fun AudioDeviceItems(engine: NativeCallEngine, kind: AudioDeviceKind, pref: SharedPreference<String?>, devices: List<AudioDevice>, title: StringResource, showMenu: MutableState<Boolean>) {
|
||||
DevicesSectionTitle(title)
|
||||
val chosenId = storedAudioDevice(pref.state.value)?.id
|
||||
(listOf(null) + devices).forEach { device ->
|
||||
ItemAction(
|
||||
device?.name ?: stringResource(MR.strings.call_audio_device_system_default),
|
||||
{ if (device?.descriptor == chosenId) Icon(painterResource(MR.images.ic_check), null, tint = MenuTextColor) },
|
||||
onClick = {
|
||||
showMenu.value = false
|
||||
pref.set(device?.let { json.encodeToString(SavedAudioDevice.serializer(), savedAudioDevice(it)) })
|
||||
engine.selectAudioDevice(kind, device)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DevicesSectionTitle(title: StringResource) {
|
||||
Text(stringResource(title), Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
|
||||
private fun callAudioSettings() = CallAudioSettings(
|
||||
mic = storedAudioDevice(appPrefs.desktopCallMic.get()),
|
||||
speaker = storedAudioDevice(appPrefs.desktopCallSpeaker.get()),
|
||||
|
||||
@@ -21,7 +21,7 @@ Make and receive end-to-end encrypted audio and video calls over WebRTC. The imp
|
||||
| WebRTC host | `WebView` with `WebViewAssetLoader` serving local assets | `NativeCallEngine` (webrtc-java, native libwebrtc) |
|
||||
| Call activity | `CallActivity` (separate Android Activity) with lifecycle management | Full-window composable over the app, collapsible to the floating call avatar |
|
||||
| PiP support | Native Android PiP via `CallActivity` | Not supported |
|
||||
| Audio management | `CallAudioDeviceManager` with Android `AudioManager`, proximity wake lock | Microphone and speaker pickers (System default or a device), remembered across calls |
|
||||
| Audio management | `CallAudioDeviceManager` with Android `AudioManager`, proximity wake lock | Microphone and speaker in the devices menu (System default or a device), remembered across calls |
|
||||
| Screen sharing | Not supported | Screen or window picker |
|
||||
|
||||
## Page Sections
|
||||
@@ -61,7 +61,7 @@ Sound: `SoundPlayer.start()` plays ringtone while banner is visible (unless call
|
||||
| `NativeCallController` | Creates `NativeCallEngine` for the call and passes it commands from the `chatModel.callCommand` queue |
|
||||
| `NativeCallEngine` | Native WebRTC with `call.ts` semantics: transceivers, VP8, frame encryption, ICE batching, peer media detection |
|
||||
| `ActiveCallLayout` | Remote video or avatar, local camera preview, call state and encryption info |
|
||||
| Controls | Mic, peer audio, camera, screen share picker, camera, microphone and speaker pickers, hang up; back button collapses the view |
|
||||
| Controls | Mic, peer audio, camera, screen share picker, devices menu (microphone, speaker, camera), hang up; back button collapses the view |
|
||||
| Audio device notices | Toast when the chosen microphone or speaker disconnects (System default used, or unavailable when the fallback is disabled) and when it reconnects |
|
||||
|
||||
### WebRTC Signaling Flow
|
||||
|
||||
@@ -122,7 +122,7 @@ The `actual` platform implementation of `ActiveCallView()` and supporting compos
|
||||
|
||||
- **ActiveCallView** ([line 59](../../common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt#L59)): Handles `WCallResponse` messages with the same state machine as Android. A `Capabilities` error shows the engine error alert and ends the call; a call without a microphone proceeds without sending audio.
|
||||
- **NativeCallController** ([line 172](../../common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt#L172)): Creates the engine for the call, drains the `chatModel.callCommand` queue into it, and closes it on dispose.
|
||||
- **ActiveCallLayout** ([line 203](../../common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt#L203)): Full-window call view over the app: remote screen or camera video, local camera preview, state and encryption info, and controls for mic, peer audio, camera, screen share (screen or window picker), camera, microphone and speaker pickers and hang up. The back button collapses the view to the floating call avatar, which expands it again.
|
||||
- **ActiveCallLayout** ([line 203](../../common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt#L203)): Full-window call view over the app: remote screen or camera video, local camera preview, state and encryption info, and controls for mic, peer audio, camera, screen share (screen or window picker), a devices menu (microphone, speaker and camera) and hang up. The back button collapses the view to the floating call avatar, which expands it again.
|
||||
- **NativeCallEngine** ([line 29](../../common/src/desktopMain/kotlin/chat/simplex/common/views/call/NativeCallEngine.kt#L29)): Processes `WCallCommand` on a single thread and reports `WCallResponse` with `call.ts` semantics: four transceivers (mic, camera, screen audio, screen video), VP8 preferred, ICE candidates batched as in `call.ts`, connection state and peer media changes reported.
|
||||
- **Audio sending** ([line 330](../../common/src/desktopMain/kotlin/chat/simplex/common/views/call/NativeCallEngine.kt#L330)): Device-captured audio reaches every audio sender, so the mic is muted and the screen audio sender silenced by deactivating the sender's encoding, which stops RTP.
|
||||
- **Peer media** ([line 580](../../common/src/desktopMain/kotlin/chat/simplex/common/views/call/NativeCallEngine.kt#L580)): A received source is reported muted after 3 seconds without received bytes, as in `call.ts`.
|
||||
@@ -171,7 +171,7 @@ An in-app notification banner shown when a call invitation arrives while the app
|
||||
|---|---|---|---|
|
||||
| `CallView.kt` | [`common/src/commonMain/.../views/call/CallView.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/call/CallView.kt) | 28 | `expect fun ActiveCallView()`, delivery receipt waiting |
|
||||
| `CallView.android.kt` | [`common/src/androidMain/.../views/call/CallView.android.kt`](../../common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt) | 891 | Android WebView WebRTC, overlay, permissions |
|
||||
| `CallView.desktop.kt` | [`common/src/desktopMain/.../views/call/CallView.desktop.kt`](../../common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt) | 461 | Desktop call view and state machine |
|
||||
| `CallView.desktop.kt` | [`common/src/desktopMain/.../views/call/CallView.desktop.kt`](../../common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt) | 462 | Desktop call view and state machine |
|
||||
| `NativeCallEngine.kt` | [`common/src/desktopMain/.../views/call/NativeCallEngine.kt`](../../common/src/desktopMain/kotlin/chat/simplex/common/views/call/NativeCallEngine.kt) | 764 | Desktop native WebRTC engine (webrtc-java) |
|
||||
| `CallAudioDevices.kt` | [`common/src/desktopMain/.../views/call/CallAudioDevices.kt`](../../common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallAudioDevices.kt) | 62 | Desktop microphone and speaker choice and fallback notices |
|
||||
| `CallActivity.kt` | [`android/src/main/java/.../views/call/CallActivity.kt`](../../android/src/main/java/chat/simplex/app/views/call/CallActivity.kt) | 472 | Android call Activity, PiP, lock screen |
|
||||
|
||||
Reference in new issue
Block a user