fix: auto codec, force-ping button, relay delete button
Some checks failed
Mirror to GitHub / mirror (push) Failing after 36s
Build Release Binaries / build-amd64 (push) Failing after 1m57s

1. Auto codec: new "Auto" position on quality slider (JNI index 7).
   When selected, the engine uses the relay's chosen_profile from
   CallAnswer instead of the local preference. Slider now has 8
   positions: Studio 64k → Auto → Codec2 1.2k.

2. Force ping: added refresh button (↻) in Manage Relays dialog
   header. Calls pingAllServers() to re-check all relays on demand.

3. Delete relay fix: the X button was inside a Surface(onClick=...)
   which swallowed the touch event. Replaced with a separate Surface
   that properly intercepts the click.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-04-07 21:22:24 +04:00
parent c8bcc5c974
commit d06cf66538
4 changed files with 65 additions and 30 deletions

View File

@@ -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)
}
}
}
}
}

View File

@@ -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(

View File

@@ -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<u8>,
@@ -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<EngineState>,
) -> 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();

View File

@@ -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() },