fix: dedup participants in UI, wait for QUIC close ack before exiting
UI: deduplicate room participants by fingerprint so ghost entries from stale relay state don't show duplicates. Engine: after select! ends, call close_now() + connection.closed() with 500ms timeout to wait for the relay to acknowledge the CONNECTION_CLOSE. Previously the close frame was queued but the runtime died before quinn could retransmit if the first packet was lost. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -239,13 +239,17 @@ fun InCallScreen(
|
|||||||
QualityIndicator(qualityTier, stats.qualityLabel)
|
QualityIndicator(qualityTier, stats.qualityLabel)
|
||||||
|
|
||||||
if (stats.roomParticipantCount > 0) {
|
if (stats.roomParticipantCount > 0) {
|
||||||
|
// Dedup by fingerprint — same key = same person, even if
|
||||||
|
// relay hasn't cleaned up stale entries yet.
|
||||||
|
val unique = stats.roomParticipants
|
||||||
|
.distinctBy { it.fingerprint.ifEmpty { it.displayName } }
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "${stats.roomParticipantCount} in room",
|
text = "${unique.size} in room",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
stats.roomParticipants.forEach { member ->
|
unique.forEach { member ->
|
||||||
Text(
|
Text(
|
||||||
text = member.displayName,
|
text = member.displayName,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
|||||||
@@ -148,21 +148,25 @@ impl WzpEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn stop_call(&mut self) {
|
pub fn stop_call(&mut self) {
|
||||||
|
info!("stop_call: setting running=false");
|
||||||
self.state.running.store(false, Ordering::Release);
|
self.state.running.store(false, Ordering::Release);
|
||||||
// Close QUIC connection first — queues a CONNECTION_CLOSE frame.
|
// Close QUIC connection — this wakes up all blocked recv/send futures
|
||||||
// Quinn needs the tokio runtime alive to actually send it on the wire,
|
// inside block_on(run_call(...)) on the JNI thread. run_call will then
|
||||||
// so we use shutdown_timeout() to give it time to flush.
|
// wait up to 500ms for the peer to acknowledge the close before returning.
|
||||||
if let Some(transport) = self.state.quic_transport.lock().unwrap().take() {
|
if let Some(transport) = self.state.quic_transport.lock().unwrap().take() {
|
||||||
|
info!("stop_call: closing QUIC connection");
|
||||||
transport.close_now();
|
transport.close_now();
|
||||||
}
|
}
|
||||||
let _ = self.state.command_tx.send(EngineCommand::Stop);
|
let _ = self.state.command_tx.send(EngineCommand::Stop);
|
||||||
|
// Note: the runtime is still blocked in block_on(run_call(...)) on the
|
||||||
|
// start_call thread. Once run_call exits (triggered by running=false +
|
||||||
|
// connection close above), block_on returns and stores the runtime in
|
||||||
|
// self.tokio_runtime. We don't need to shut it down here.
|
||||||
if let Some(rt) = self.tokio_runtime.take() {
|
if let Some(rt) = self.tokio_runtime.take() {
|
||||||
// Give quinn up to 500ms to send the CONNECTION_CLOSE frame.
|
rt.shutdown_timeout(std::time::Duration::from_millis(100));
|
||||||
// The desktop client uses 2s, but we keep it short on Android
|
|
||||||
// to avoid blocking the UI thread.
|
|
||||||
rt.shutdown_timeout(std::time::Duration::from_millis(500));
|
|
||||||
}
|
}
|
||||||
self.call_start = None;
|
self.call_start = None;
|
||||||
|
info!("stop_call: done");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_mute(&self, muted: bool) {
|
pub fn set_mute(&self, muted: bool) {
|
||||||
@@ -585,12 +589,22 @@ async fn run_call(
|
|||||||
};
|
};
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = send_task => {}
|
_ = send_task => info!("send task ended"),
|
||||||
_ = recv_task => {}
|
_ = recv_task => info!("recv task ended"),
|
||||||
_ = stats_task => {}
|
_ = stats_task => info!("stats task ended"),
|
||||||
_ = signal_task => {}
|
_ = signal_task => info!("signal task ended"),
|
||||||
}
|
}
|
||||||
|
|
||||||
transport.close().await.ok();
|
// Send CONNECTION_CLOSE and wait up to 500ms for the peer to acknowledge.
|
||||||
|
// This ensures the relay sees the close even if the first packet is lost.
|
||||||
|
info!("closing QUIC connection...");
|
||||||
|
transport.close_now();
|
||||||
|
match tokio::time::timeout(
|
||||||
|
std::time::Duration::from_millis(500),
|
||||||
|
transport.connection().closed(),
|
||||||
|
).await {
|
||||||
|
Ok(_) => info!("QUIC connection closed cleanly"),
|
||||||
|
Err(_) => info!("QUIC close timed out (relay may not have ack'd)"),
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user