fix: auto codec, force-ping button, relay delete button
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:
@@ -485,6 +485,7 @@ fun InCallScreen(
|
|||||||
onSelect = { idx -> viewModel.selectServer(idx) },
|
onSelect = { idx -> viewModel.selectServer(idx) },
|
||||||
onDelete = { idx -> viewModel.removeServer(idx) },
|
onDelete = { idx -> viewModel.removeServer(idx) },
|
||||||
onAdd = { addr, label -> viewModel.addServer(addr, label) },
|
onAdd = { addr, label -> viewModel.addServer(addr, label) },
|
||||||
|
onRefresh = { viewModel.pingAllServers() },
|
||||||
onDismiss = { showManageRelays = false }
|
onDismiss = { showManageRelays = false }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -513,6 +514,7 @@ private fun ManageRelaysDialog(
|
|||||||
onSelect: (Int) -> Unit,
|
onSelect: (Int) -> Unit,
|
||||||
onDelete: (Int) -> Unit,
|
onDelete: (Int) -> Unit,
|
||||||
onAdd: (String, String) -> Unit,
|
onAdd: (String, String) -> Unit,
|
||||||
|
onRefresh: () -> Unit,
|
||||||
onDismiss: () -> Unit
|
onDismiss: () -> Unit
|
||||||
) {
|
) {
|
||||||
var addName by remember { mutableStateOf("") }
|
var addName by remember { mutableStateOf("") }
|
||||||
@@ -528,6 +530,17 @@ private fun ManageRelaysDialog(
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text("Manage Relays", color = Color.White, fontWeight = FontWeight.Bold)
|
Text("Manage Relays", color = Color.White, fontWeight = FontWeight.Bold)
|
||||||
|
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(
|
Surface(
|
||||||
onClick = onDismiss,
|
onClick = onDismiss,
|
||||||
shape = RoundedCornerShape(8.dp),
|
shape = RoundedCornerShape(8.dp),
|
||||||
@@ -539,6 +552,7 @@ private fun ManageRelaysDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
Column {
|
Column {
|
||||||
@@ -590,13 +604,17 @@ private fun ManageRelaysDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text(
|
Surface(
|
||||||
"\u00D7",
|
onClick = { onDelete(idx) },
|
||||||
color = TextDim,
|
shape = RoundedCornerShape(4.dp),
|
||||||
fontSize = 18.sp,
|
color = Color.Transparent,
|
||||||
modifier = Modifier.clickable { onDelete(idx) }
|
modifier = Modifier.size(32.dp)
|
||||||
)
|
) {
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
Text("\u00D7", color = TextDim, fontSize = 18.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -245,19 +245,19 @@ fun SettingsScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
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(
|
val qualityLabels = listOf(
|
||||||
"Studio 64k", "Studio 48k", "Studio 32k", "Opus 24k",
|
"Studio 64k", "Studio 48k", "Studio 32k", "Auto",
|
||||||
"Opus 6k", "Codec2 1.2k", "Codec2 3.2k"
|
"Opus 24k", "Opus 6k", "Codec2 3.2k", "Codec2 1.2k"
|
||||||
)
|
)
|
||||||
// Map slider position to JNI profile int:
|
// Map slider position to JNI profile int:
|
||||||
// 0=Studio64k(6), 1=Studio48k(5), 2=Studio32k(4), 3=Opus24k(0),
|
// 0=Studio64k(6), 1=Studio48k(5), 2=Studio32k(4), 3=Auto(7),
|
||||||
// 4=Opus6k(1), 5=Codec2_1.2k(2), 6=Codec2_3.2k(3)
|
// 4=Opus24k(0), 5=Opus6k(1), 6=Codec2_3.2k(3), 7=Codec2_1.2k(2)
|
||||||
val sliderToProfile = intArrayOf(6, 5, 4, 0, 1, 2, 3)
|
val sliderToProfile = intArrayOf(6, 5, 4, 7, 0, 1, 3, 2)
|
||||||
val profileToSlider = mapOf(6 to 0, 5 to 1, 4 to 2, 0 to 3, 1 to 4, 2 to 5, 3 to 6)
|
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(
|
val qualityColors = listOf(
|
||||||
Color(0xFF22C55E), Color(0xFF4ADE80), Color(0xFF86EFAC), Color(0xFFA3E635),
|
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 currentCodec by viewModel.codecChoice.collectAsState()
|
||||||
val sliderPos = profileToSlider[currentCodec] ?: 3
|
val sliderPos = profileToSlider[currentCodec] ?: 3
|
||||||
@@ -276,8 +276,8 @@ fun SettingsScreen(
|
|||||||
Slider(
|
Slider(
|
||||||
value = sliderPos.toFloat(),
|
value = sliderPos.toFloat(),
|
||||||
onValueChange = { viewModel.setCodecChoice(sliderToProfile[it.toInt()]) },
|
onValueChange = { viewModel.setCodecChoice(sliderToProfile[it.toInt()]) },
|
||||||
valueRange = 0f..6f,
|
valueRange = 0f..7f,
|
||||||
steps = 5,
|
steps = 6,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ fn frame_samples_for(profile: &QualityProfile) -> usize {
|
|||||||
/// Configuration to start a call.
|
/// Configuration to start a call.
|
||||||
pub struct CallStartConfig {
|
pub struct CallStartConfig {
|
||||||
pub profile: QualityProfile,
|
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 relay_addr: String,
|
||||||
pub room: String,
|
pub room: String,
|
||||||
pub auth_token: Vec<u8>,
|
pub auth_token: Vec<u8>,
|
||||||
@@ -49,6 +51,7 @@ impl Default for CallStartConfig {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
profile: QualityProfile::GOOD,
|
profile: QualityProfile::GOOD,
|
||||||
|
auto_profile: false,
|
||||||
relay_addr: String::new(),
|
relay_addr: String::new(),
|
||||||
room: String::new(),
|
room: String::new(),
|
||||||
auth_token: Vec::new(),
|
auth_token: Vec::new(),
|
||||||
@@ -126,6 +129,7 @@ impl WzpEngine {
|
|||||||
let room = config.room.clone();
|
let room = config.room.clone();
|
||||||
let identity_seed = config.identity_seed;
|
let identity_seed = config.identity_seed;
|
||||||
let profile = config.profile;
|
let profile = config.profile;
|
||||||
|
let auto_profile = config.auto_profile;
|
||||||
let alias = config.alias.clone();
|
let alias = config.alias.clone();
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
|
|
||||||
@@ -134,7 +138,7 @@ impl WzpEngine {
|
|||||||
|
|
||||||
let state_clone = state.clone();
|
let state_clone = state.clone();
|
||||||
runtime.block_on(async move {
|
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}");
|
error!("call failed: {e}");
|
||||||
}
|
}
|
||||||
@@ -277,6 +281,7 @@ async fn run_call(
|
|||||||
room: &str,
|
room: &str,
|
||||||
identity_seed: &[u8; 32],
|
identity_seed: &[u8; 32],
|
||||||
profile: QualityProfile,
|
profile: QualityProfile,
|
||||||
|
auto_profile: bool,
|
||||||
alias: Option<&str>,
|
alias: Option<&str>,
|
||||||
state: Arc<EngineState>,
|
state: Arc<EngineState>,
|
||||||
) -> Result<(), anyhow::Error> {
|
) -> Result<(), anyhow::Error> {
|
||||||
@@ -328,8 +333,8 @@ async fn run_call(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| anyhow::anyhow!("connection closed before CallAnswer"))?;
|
.ok_or_else(|| anyhow::anyhow!("connection closed before CallAnswer"))?;
|
||||||
|
|
||||||
let relay_ephemeral_pub = match answer {
|
let (relay_ephemeral_pub, chosen_profile) = match answer {
|
||||||
SignalMessage::CallAnswer { ephemeral_pub, .. } => ephemeral_pub,
|
SignalMessage::CallAnswer { ephemeral_pub, chosen_profile, .. } => (ephemeral_pub, chosen_profile),
|
||||||
other => {
|
other => {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
"expected CallAnswer, got {:?}",
|
"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)?;
|
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();
|
let mut stats = state.stats.lock().unwrap();
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ unsafe fn handle_ref(handle: jlong) -> &'static mut EngineHandle {
|
|||||||
unsafe { &mut *(handle as *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 {
|
fn profile_from_int(value: jint) -> QualityProfile {
|
||||||
match value {
|
match value {
|
||||||
0 => QualityProfile::GOOD, // Opus 24k
|
0 => QualityProfile::GOOD, // Opus 24k
|
||||||
@@ -35,7 +38,7 @@ fn profile_from_int(value: jint) -> QualityProfile {
|
|||||||
4 => QualityProfile::STUDIO_32K, // Opus 32k
|
4 => QualityProfile::STUDIO_32K, // Opus 32k
|
||||||
5 => QualityProfile::STUDIO_48K, // Opus 48k
|
5 => QualityProfile::STUDIO_48K, // Opus 48k
|
||||||
6 => QualityProfile::STUDIO_64K, // Opus 64k
|
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 {
|
let config = CallStartConfig {
|
||||||
profile: profile_from_int(profile_j),
|
profile: profile_from_int(profile_j),
|
||||||
|
auto_profile: profile_j == PROFILE_AUTO,
|
||||||
relay_addr,
|
relay_addr,
|
||||||
room,
|
room,
|
||||||
auth_token: if token.is_empty() { Vec::new() } else { token.into_bytes() },
|
auth_token: if token.is_empty() { Vec::new() } else { token.into_bytes() },
|
||||||
|
|||||||
Reference in New Issue
Block a user