From 40955bd11c0e630bd2fc6f91d89e4db2d1a8a51d Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Sun, 12 Apr 2026 13:29:58 +0400 Subject: [PATCH] debug(media): add connection diagnostics for direct P2P drops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When direct P2P calls show 100% datagram drops, we need to know WHY send_media() fails. This commit adds: - Remote address + stable_id logging on A-role accept and D-role dial success (dual_path.rs) — tells us which candidate won - Remote address + max_datagram_size on engine transport init — verifies datagrams are negotiated - last_send_err in send heartbeat — captures the actual error from send_datagram() failures - QuinnTransport::remote_address() helper Also fixes UI badge: was looking for wrong event name ("dual_path_race_won" → "path_negotiated"). Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/wzp-client/src/dual_path.rs | 19 +++++++++++++++++-- crates/wzp-transport/src/quic.rs | 5 +++++ desktop/src-tauri/src/engine.rs | 17 +++++++++++++++-- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/crates/wzp-client/src/dual_path.rs b/crates/wzp-client/src/dual_path.rs index 9fe7274..e8478ac 100644 --- a/crates/wzp-client/src/dual_path.rs +++ b/crates/wzp-client/src/dual_path.rs @@ -202,12 +202,20 @@ pub async fn race( tokio::select! { v4 = wzp_transport::accept(&ep_for_fut) => { let conn = v4.map_err(|e| anyhow::anyhow!("v4 accept: {e}"))?; - tracing::info!("dual_path: A-role accepted on IPv4 endpoint"); + tracing::info!( + remote = %conn.remote_address(), + stable_id = conn.stable_id(), + "dual_path: A-role accepted on IPv4 endpoint" + ); Ok(QuinnTransport::new(conn)) } v6 = wzp_transport::accept(&v6_ep) => { let conn = v6.map_err(|e| anyhow::anyhow!("v6 accept: {e}"))?; - tracing::info!("dual_path: A-role accepted on IPv6 endpoint"); + tracing::info!( + remote = %conn.remote_address(), + stable_id = conn.stable_id(), + "dual_path: A-role accepted on IPv6 endpoint" + ); Ok(QuinnTransport::new(conn)) } } @@ -216,6 +224,11 @@ pub async fn race( let conn = wzp_transport::accept(&ep_for_fut) .await .map_err(|e| anyhow::anyhow!("direct accept: {e}"))?; + tracing::info!( + remote = %conn.remote_address(), + stable_id = conn.stable_id(), + "dual_path: A-role accepted (v4-only)" + ); Ok(QuinnTransport::new(conn)) } } @@ -316,6 +329,8 @@ pub async fn race( tracing::info!( %candidate, candidate_idx = idx, + remote = %conn.remote_address(), + stable_id = conn.stable_id(), "dual_path: direct dial succeeded on candidate" ); // Abort the remaining in-flight diff --git a/crates/wzp-transport/src/quic.rs b/crates/wzp-transport/src/quic.rs index caf7a02..8200498 100644 --- a/crates/wzp-transport/src/quic.rs +++ b/crates/wzp-transport/src/quic.rs @@ -33,6 +33,11 @@ impl QuinnTransport { &self.connection } + /// Remote address of the peer on this connection. + pub fn remote_address(&self) -> std::net::SocketAddr { + self.connection.remote_address() + } + /// Send raw bytes as a QUIC datagram (no MediaPacket framing). pub fn send_raw_datagram(&self, data: &[u8]) -> Result<(), TransportError> { self.connection diff --git a/desktop/src-tauri/src/engine.rs b/desktop/src-tauri/src/engine.rs index de2be2f..33712a7 100644 --- a/desktop/src-tauri/src/engine.rs +++ b/desktop/src-tauri/src/engine.rs @@ -477,6 +477,7 @@ impl CallEngine { let send_fs = frames_sent.clone(); let send_level = audio_level.clone(); let send_drops = Arc::new(AtomicU64::new(0)); + let send_last_err: Arc>> = Arc::new(Mutex::new(None)); let send_quality = quality.clone(); let send_tx_codec = tx_codec.clone(); let send_t0 = call_t0; @@ -561,9 +562,14 @@ impl CallEngine { last_pkt_bytes = pkt.payload.len(); if let Err(e) = send_t.send_media(pkt).await { send_drops.fetch_add(1, Ordering::Relaxed); - if send_drops.load(Ordering::Relaxed) <= 3 { + let count = send_drops.load(Ordering::Relaxed); + if count <= 3 { tracing::warn!("send_media error (dropping packet): {e}"); } + // Latch last error for heartbeat + if count == 1 { + *send_last_err.lock().await = Some(format!("{e}")); + } } } let before = send_fs.fetch_add(1, Ordering::Relaxed); @@ -605,6 +611,7 @@ impl CallEngine { // you capture/mic is broken; a live one with // no peer recv tells you outbound is being // dropped somewhere in the media path. + let err_str = send_last_err.lock().await.clone(); crate::emit_call_debug( &send_app, "media:send_heartbeat", @@ -614,6 +621,7 @@ impl CallEngine { "last_pkt_bytes": last_pkt_bytes, "short_reads": short_reads, "drops": drops, + "last_send_err": err_str, }), ); heartbeat = std::time::Instant::now(); @@ -1105,7 +1113,12 @@ impl CallEngine { // Transport source: either pre-connected or fresh. let transport = if let Some(t) = pre_connected_transport { - info!(is_direct_p2p, "using pre-connected transport"); + info!( + is_direct_p2p, + remote = %t.remote_address(), + max_datagram = ?t.max_datagram_size(), + "using pre-connected transport" + ); t } else { // Connect — reuse the signal endpoint if the direct-call path gave