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:
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user