diff --git a/android/app/src/main/java/com/wzp/ui/call/InCallScreen.kt b/android/app/src/main/java/com/wzp/ui/call/InCallScreen.kt index aedf0f6..f3fe6f7 100644 --- a/android/app/src/main/java/com/wzp/ui/call/InCallScreen.kt +++ b/android/app/src/main/java/com/wzp/ui/call/InCallScreen.kt @@ -485,6 +485,7 @@ fun InCallScreen( onSelect = { idx -> viewModel.selectServer(idx) }, onDelete = { idx -> viewModel.removeServer(idx) }, onAdd = { addr, label -> viewModel.addServer(addr, label) }, + onRefresh = { viewModel.pingAllServers() }, onDismiss = { showManageRelays = false } ) } @@ -513,6 +514,7 @@ private fun ManageRelaysDialog( onSelect: (Int) -> Unit, onDelete: (Int) -> Unit, onAdd: (String, String) -> Unit, + onRefresh: () -> Unit, onDismiss: () -> Unit ) { var addName by remember { mutableStateOf("") } @@ -528,14 +530,26 @@ private fun ManageRelaysDialog( verticalAlignment = Alignment.CenterVertically ) { Text("Manage Relays", color = Color.White, fontWeight = FontWeight.Bold) - Surface( - onClick = onDismiss, - shape = RoundedCornerShape(8.dp), - color = DarkSurface2, - modifier = Modifier.size(32.dp) - ) { - Box(contentAlignment = Alignment.Center) { - Text("\u00D7", color = TextDim, fontSize = 18.sp) + Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + Surface( + onClick = onRefresh, + shape = RoundedCornerShape(8.dp), + color = DarkSurface2, + modifier = Modifier.size(32.dp) + ) { + Box(contentAlignment = Alignment.Center) { + Text("\u21BB", color = TextDim, fontSize = 16.sp) + } + } + Surface( + onClick = onDismiss, + shape = RoundedCornerShape(8.dp), + color = DarkSurface2, + modifier = Modifier.size(32.dp) + ) { + Box(contentAlignment = Alignment.Center) { + Text("\u00D7", color = TextDim, fontSize = 18.sp) + } } } } @@ -590,13 +604,17 @@ private fun ManageRelaysDialog( ) } } - Spacer(modifier = Modifier.width(8.dp)) - Text( - "\u00D7", - color = TextDim, - fontSize = 18.sp, - modifier = Modifier.clickable { onDelete(idx) } - ) + Spacer(modifier = Modifier.width(4.dp)) + Surface( + onClick = { onDelete(idx) }, + shape = RoundedCornerShape(4.dp), + color = Color.Transparent, + modifier = Modifier.size(32.dp) + ) { + Box(contentAlignment = Alignment.Center) { + Text("\u00D7", color = TextDim, fontSize = 18.sp) + } + } } } } 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 15d46ff..ca8f694 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 @@ -245,19 +245,19 @@ fun SettingsScreen( Spacer(modifier = Modifier.height(12.dp)) - // Quality selection — slider from best (studio 64k) to worst (codec2 1.2k) + // Quality selection — slider from best (studio 64k) to worst (codec2 1.2k) + auto val qualityLabels = listOf( - "Studio 64k", "Studio 48k", "Studio 32k", "Opus 24k", - "Opus 6k", "Codec2 1.2k", "Codec2 3.2k" + "Studio 64k", "Studio 48k", "Studio 32k", "Auto", + "Opus 24k", "Opus 6k", "Codec2 3.2k", "Codec2 1.2k" ) // Map slider position to JNI profile int: - // 0=Studio64k(6), 1=Studio48k(5), 2=Studio32k(4), 3=Opus24k(0), - // 4=Opus6k(1), 5=Codec2_1.2k(2), 6=Codec2_3.2k(3) - val sliderToProfile = intArrayOf(6, 5, 4, 0, 1, 2, 3) - val profileToSlider = mapOf(6 to 0, 5 to 1, 4 to 2, 0 to 3, 1 to 4, 2 to 5, 3 to 6) + // 0=Studio64k(6), 1=Studio48k(5), 2=Studio32k(4), 3=Auto(7), + // 4=Opus24k(0), 5=Opus6k(1), 6=Codec2_3.2k(3), 7=Codec2_1.2k(2) + val sliderToProfile = intArrayOf(6, 5, 4, 7, 0, 1, 3, 2) + val profileToSlider = mapOf(6 to 0, 5 to 1, 4 to 2, 7 to 3, 0 to 4, 1 to 5, 3 to 6, 2 to 7) val qualityColors = listOf( Color(0xFF22C55E), Color(0xFF4ADE80), Color(0xFF86EFAC), Color(0xFFA3E635), - Color(0xFFFACC15), Color(0xFF991B1B), Color(0xFFE97320) + Color(0xFFA3E635), Color(0xFFFACC15), Color(0xFFE97320), Color(0xFF991B1B) ) val currentCodec by viewModel.codecChoice.collectAsState() val sliderPos = profileToSlider[currentCodec] ?: 3 @@ -276,8 +276,8 @@ fun SettingsScreen( Slider( value = sliderPos.toFloat(), onValueChange = { viewModel.setCodecChoice(sliderToProfile[it.toInt()]) }, - valueRange = 0f..6f, - steps = 5, + valueRange = 0f..7f, + steps = 6, modifier = Modifier.fillMaxWidth() ) Row( diff --git a/crates/wzp-android/src/engine.rs b/crates/wzp-android/src/engine.rs index 8e84de4..325ea25 100644 --- a/crates/wzp-android/src/engine.rs +++ b/crates/wzp-android/src/engine.rs @@ -38,6 +38,8 @@ fn frame_samples_for(profile: &QualityProfile) -> usize { /// Configuration to start a call. pub struct CallStartConfig { pub profile: QualityProfile, + /// When true, use the relay's chosen_profile from CallAnswer instead of local profile. + pub auto_profile: bool, pub relay_addr: String, pub room: String, pub auth_token: Vec, @@ -49,6 +51,7 @@ impl Default for CallStartConfig { fn default() -> Self { Self { profile: QualityProfile::GOOD, + auto_profile: false, relay_addr: String::new(), room: String::new(), auth_token: Vec::new(), @@ -126,6 +129,7 @@ impl WzpEngine { let room = config.room.clone(); let identity_seed = config.identity_seed; let profile = config.profile; + let auto_profile = config.auto_profile; let alias = config.alias.clone(); let state = self.state.clone(); @@ -134,7 +138,7 @@ impl WzpEngine { let state_clone = state.clone(); runtime.block_on(async move { - if let Err(e) = run_call(relay_addr, &room, &identity_seed, profile, alias.as_deref(), state_clone).await + if let Err(e) = run_call(relay_addr, &room, &identity_seed, profile, auto_profile, alias.as_deref(), state_clone).await { error!("call failed: {e}"); } @@ -277,6 +281,7 @@ async fn run_call( room: &str, identity_seed: &[u8; 32], profile: QualityProfile, + auto_profile: bool, alias: Option<&str>, state: Arc, ) -> Result<(), anyhow::Error> { @@ -328,8 +333,8 @@ async fn run_call( .await? .ok_or_else(|| anyhow::anyhow!("connection closed before CallAnswer"))?; - let relay_ephemeral_pub = match answer { - SignalMessage::CallAnswer { ephemeral_pub, .. } => ephemeral_pub, + let (relay_ephemeral_pub, chosen_profile) = match answer { + SignalMessage::CallAnswer { ephemeral_pub, chosen_profile, .. } => (ephemeral_pub, chosen_profile), other => { return Err(anyhow::anyhow!( "expected CallAnswer, got {:?}", @@ -338,8 +343,16 @@ async fn run_call( } }; + // Auto mode: use the relay's chosen profile instead of the local preference + let profile = if auto_profile { + info!(chosen = ?chosen_profile.codec, "auto mode: using relay's chosen profile"); + chosen_profile + } else { + profile + }; + let _session = kx.derive_session(&relay_ephemeral_pub)?; - info!("handshake complete, call active"); + info!(codec = ?profile.codec, "handshake complete, call active"); { let mut stats = state.stats.lock().unwrap(); diff --git a/crates/wzp-android/src/jni_bridge.rs b/crates/wzp-android/src/jni_bridge.rs index b599115..61a28fd 100644 --- a/crates/wzp-android/src/jni_bridge.rs +++ b/crates/wzp-android/src/jni_bridge.rs @@ -21,6 +21,9 @@ unsafe fn handle_ref(handle: jlong) -> &'static mut EngineHandle { unsafe { &mut *(handle as *mut EngineHandle) } } +/// 7 = auto (use relay's chosen profile) +const PROFILE_AUTO: jint = 7; + fn profile_from_int(value: jint) -> QualityProfile { match value { 0 => QualityProfile::GOOD, // Opus 24k @@ -35,7 +38,7 @@ fn profile_from_int(value: jint) -> QualityProfile { 4 => QualityProfile::STUDIO_32K, // Opus 32k 5 => QualityProfile::STUDIO_48K, // Opus 48k 6 => QualityProfile::STUDIO_64K, // Opus 64k - _ => QualityProfile::GOOD, + _ => QualityProfile::GOOD, // auto falls back to GOOD } } @@ -122,6 +125,7 @@ pub unsafe extern "system" fn Java_com_wzp_engine_WzpEngine_nativeStartCall( let config = CallStartConfig { profile: profile_from_int(profile_j), + auto_profile: profile_j == PROFILE_AUTO, relay_addr, room, auth_token: if token.is_empty() { Vec::new() } else { token.into_bytes() },