diff --git a/android/app/src/main/java/com/wzp/data/SettingsRepository.kt b/android/app/src/main/java/com/wzp/data/SettingsRepository.kt index 79fe9ba..f8e397e 100644 --- a/android/app/src/main/java/com/wzp/data/SettingsRepository.kt +++ b/android/app/src/main/java/com/wzp/data/SettingsRepository.kt @@ -126,6 +126,11 @@ class SettingsRepository(context: Context) { fun saveDebugRecording(enabled: Boolean) { prefs.edit().putBoolean(KEY_DEBUG_RECORDING, enabled).apply() } fun loadDebugRecording(): Boolean = prefs.getBoolean(KEY_DEBUG_RECORDING, false) + // --- Codec choice --- + // 0 = Opus (GOOD), 1 = Opus Low (DEGRADED), 2 = Codec2 (CATASTROPHIC) + fun saveCodecChoice(choice: Int) { prefs.edit().putInt("codec_choice", choice).apply() } + fun loadCodecChoice(): Int = prefs.getInt("codec_choice", 0) + // --- Identity seed --- /** diff --git a/android/app/src/main/java/com/wzp/engine/WzpEngine.kt b/android/app/src/main/java/com/wzp/engine/WzpEngine.kt index 8693c7d..64c37ae 100644 --- a/android/app/src/main/java/com/wzp/engine/WzpEngine.kt +++ b/android/app/src/main/java/com/wzp/engine/WzpEngine.kt @@ -38,9 +38,12 @@ class WzpEngine(private val callback: WzpCallback) { * @param alias display name sent to relay for room participant list * @return 0 on success, negative error code on failure */ - fun startCall(relayAddr: String, room: String, seedHex: String = "", token: String = "", alias: String = ""): Int { + /** + * @param profile 0 = Opus GOOD, 1 = Opus DEGRADED, 2 = Codec2 CATASTROPHIC + */ + fun startCall(relayAddr: String, room: String, seedHex: String = "", token: String = "", alias: String = "", profile: Int = 0): Int { check(nativeHandle != 0L) { "Engine not initialized" } - val result = nativeStartCall(nativeHandle, relayAddr, room, seedHex, token, alias) + val result = nativeStartCall(nativeHandle, relayAddr, room, seedHex, token, alias, profile) if (result == 0) { callback.onCallStateChanged(CallStateConstants.CONNECTING) } else { @@ -141,7 +144,7 @@ class WzpEngine(private val callback: WzpCallback) { private external fun nativeInit(): Long private external fun nativeStartCall( - handle: Long, relay: String, room: String, seed: String, token: String, alias: String + handle: Long, relay: String, room: String, seed: String, token: String, alias: String, profile: Int ): Int private external fun nativeStopCall(handle: Long) private external fun nativeSetMute(handle: Long, muted: Boolean) diff --git a/android/app/src/main/java/com/wzp/ui/call/CallViewModel.kt b/android/app/src/main/java/com/wzp/ui/call/CallViewModel.kt index d7ae6b6..4caa5e9 100644 --- a/android/app/src/main/java/com/wzp/ui/call/CallViewModel.kt +++ b/android/app/src/main/java/com/wzp/ui/call/CallViewModel.kt @@ -109,6 +109,10 @@ class CallViewModel : ViewModel(), WzpCallback { private val _debugRecording = MutableStateFlow(false) val debugRecording: StateFlow = _debugRecording.asStateFlow() + // 0 = Opus (GOOD), 1 = Opus Low (DEGRADED), 2 = Codec2 (CATASTROPHIC) + private val _codecChoice = MutableStateFlow(0) + val codecChoice: StateFlow = _codecChoice.asStateFlow() + /** True when a call just ended and debug report can be sent. */ private val _debugReportAvailable = MutableStateFlow(false) val debugReportAvailable: StateFlow = _debugReportAvailable.asStateFlow() @@ -164,6 +168,7 @@ class CallViewModel : ViewModel(), WzpCallback { _seedHex.value = s.getOrCreateSeedHex() _aecEnabled.value = s.loadAecEnabled() _debugRecording.value = s.loadDebugRecording() + _codecChoice.value = s.loadCodecChoice() _recentRooms.value = s.loadRecentRooms() } @@ -309,6 +314,11 @@ class CallViewModel : ViewModel(), WzpCallback { settings?.saveDebugRecording(enabled) } + fun setCodecChoice(choice: Int) { + _codecChoice.value = choice + settings?.saveCodecChoice(choice) + } + /** * Resolve DNS hostname to IP address on the Kotlin/Android side, * since Rust's DNS resolution may not work on Android. @@ -406,7 +416,7 @@ class CallViewModel : ViewModel(), WzpCallback { val seed = _seedHex.value val name = _alias.value Log.i(TAG, "startCall: resolved=$relay, alias=$name, calling engine.startCall") - val result = engine?.startCall(relay, room, seedHex = seed, alias = name) ?: -1 + val result = engine?.startCall(relay, room, seedHex = seed, alias = name, profile = _codecChoice.value) ?: -1 Log.i(TAG, "startCall: engine returned $result") // Only wire up notification callback after engine is running CallService.onStopFromNotification = { stopCall() } diff --git a/android/app/src/main/java/com/wzp/ui/settings/SettingsScreen.kt b/android/app/src/main/java/com/wzp/ui/settings/SettingsScreen.kt index 5b3fdf3..ce5d32f 100644 --- a/android/app/src/main/java/com/wzp/ui/settings/SettingsScreen.kt +++ b/android/app/src/main/java/com/wzp/ui/settings/SettingsScreen.kt @@ -1,5 +1,6 @@ package com.wzp.ui.settings +import androidx.compose.foundation.clickable import android.content.ClipData import android.content.ClipboardManager import android.content.Context @@ -22,6 +23,7 @@ import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Divider +import androidx.compose.material3.RadioButton import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.IconButtonDefaults @@ -241,6 +243,35 @@ fun SettingsScreen( ) } + Spacer(modifier = Modifier.height(12.dp)) + + // Codec selection + val codecNames = listOf("Opus 24k (Best)", "Opus 6k (Low BW)", "Codec2 1.2k (Minimal)") + val currentCodec by viewModel.codecChoice.collectAsState() + Text("Encode Codec", style = MaterialTheme.typography.bodyMedium) + Text( + text = "Decode always accepts all codecs", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(4.dp)) + codecNames.forEachIndexed { idx, name -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { viewModel.setCodecChoice(idx) } + .padding(vertical = 4.dp) + ) { + RadioButton( + selected = currentCodec == idx, + onClick = { viewModel.setCodecChoice(idx) } + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(name, style = MaterialTheme.typography.bodyMedium) + } + } + Spacer(modifier = Modifier.height(24.dp)) Divider() Spacer(modifier = Modifier.height(16.dp)) diff --git a/crates/wzp-android/src/jni_bridge.rs b/crates/wzp-android/src/jni_bridge.rs index ca27d52..1f9848d 100644 --- a/crates/wzp-android/src/jni_bridge.rs +++ b/crates/wzp-android/src/jni_bridge.rs @@ -85,6 +85,7 @@ pub unsafe extern "system" fn Java_com_wzp_engine_WzpEngine_nativeStartCall( seed_hex_j: JString, token_j: JString, alias_j: JString, + profile_j: jint, ) -> jint { let result = panic::catch_unwind(panic::AssertUnwindSafe(|| { let relay_addr: String = env.get_string(&relay_addr_j).map(|s| s.into()).unwrap_or_default(); @@ -110,7 +111,7 @@ pub unsafe extern "system" fn Java_com_wzp_engine_WzpEngine_nativeStartCall( } let config = CallStartConfig { - profile: QualityProfile::GOOD, + profile: profile_from_int(profile_j), relay_addr, room, auth_token: if token.is_empty() { Vec::new() } else { token.into_bytes() },