fix: close QUIC connection on hangup so relay removes participant immediately

stop_call() now calls close_now() on the stored transport handle before
killing the tokio runtime. This sends a QUIC CONNECTION_CLOSE frame so
the relay's recv loop breaks immediately, triggering leave() + RoomUpdate
broadcast. Previously the runtime was killed first, so transport.close()
never ran and the relay kept stale participants until idle timeout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude
2026-04-06 04:58:24 +00:00
parent 7eb136fcb3
commit a9c4260b4e
2 changed files with 18 additions and 0 deletions

View File

@@ -67,6 +67,9 @@ pub(crate) struct EngineState {
pub playout_ring: AudioRing, pub playout_ring: AudioRing,
/// Current audio level (RMS) for UI display, updated by capture path. /// Current audio level (RMS) for UI display, updated by capture path.
pub audio_level_rms: AtomicU32, pub audio_level_rms: AtomicU32,
/// QUIC transport handle — stored so stop_call() can close it immediately,
/// triggering relay-side leave + RoomUpdate broadcast.
pub quic_transport: Mutex<Option<Arc<wzp_transport::QuinnTransport>>>,
} }
pub struct WzpEngine { pub struct WzpEngine {
@@ -87,6 +90,7 @@ impl WzpEngine {
capture_ring: AudioRing::new(), capture_ring: AudioRing::new(),
playout_ring: AudioRing::new(), playout_ring: AudioRing::new(),
audio_level_rms: AtomicU32::new(0), audio_level_rms: AtomicU32::new(0),
quic_transport: Mutex::new(None),
}); });
Self { Self {
state, state,
@@ -145,6 +149,11 @@ impl WzpEngine {
pub fn stop_call(&mut self) { pub fn stop_call(&mut self) {
self.state.running.store(false, Ordering::Release); self.state.running.store(false, Ordering::Release);
// Close QUIC connection immediately so the relay detects disconnect
// and removes us from the room (broadcasts RoomUpdate to others).
if let Some(transport) = self.state.quic_transport.lock().unwrap().take() {
transport.close_now();
}
let _ = self.state.command_tx.send(EngineCommand::Stop); let _ = self.state.command_tx.send(EngineCommand::Stop);
if let Some(rt) = self.tokio_runtime.take() { if let Some(rt) = self.tokio_runtime.take() {
rt.shutdown_background(); rt.shutdown_background();
@@ -223,6 +232,9 @@ async fn run_call(
let transport = Arc::new(wzp_transport::QuinnTransport::new(conn)); let transport = Arc::new(wzp_transport::QuinnTransport::new(conn));
// Store transport handle so stop_call() can close the connection immediately
*state.quic_transport.lock().unwrap() = Some(transport.clone());
// Crypto handshake // Crypto handshake
let mut kx = WarzoneKeyExchange::from_identity_seed(identity_seed); let mut kx = WarzoneKeyExchange::from_identity_seed(identity_seed);
let ephemeral_pub = kx.generate_ephemeral(); let ephemeral_pub = kx.generate_ephemeral();

View File

@@ -33,6 +33,12 @@ impl QuinnTransport {
&self.connection &self.connection
} }
/// Close the QUIC connection immediately (synchronous, no async needed).
/// The relay will detect the close and remove this participant from the room.
pub fn close_now(&self) {
self.connection.close(quinn::VarInt::from_u32(0), b"hangup");
}
/// Feed an external RTT observation (e.g. from QUIC path stats) into the path monitor. /// Feed an external RTT observation (e.g. from QUIC path stats) into the path monitor.
pub fn feed_rtt(&self, rtt_ms: u32) { pub fn feed_rtt(&self, rtt_ms: u32) {
self.path_monitor.lock().unwrap().observe_rtt(rtt_ms); self.path_monitor.lock().unwrap().observe_rtt(rtt_ms);