feat: relay bridge mode — pairs two clients for real calls
When no --remote is configured, the relay now operates in bridge mode: - First client connects → echoes while waiting for a peer - Second client connects → both clients are bridged bidirectionally - A's packets go to B, B's packets go to A - Stats logged every 5 seconds (a_to_b / b_to_a packet counts) - Falls back to echo if only one client connects This enables the core use case: two clients on different networks calling each other through a single relay. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ use std::sync::atomic::{AtomicU64, Ordering};
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::{Mutex, watch};
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
use wzp_proto::MediaTransport;
|
use wzp_proto::MediaTransport;
|
||||||
@@ -192,6 +192,10 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Shared slot for bridge mode: first client waits here for a peer.
|
||||||
|
let waiting_peer: Arc<Mutex<Option<Arc<wzp_transport::QuinnTransport>>>> =
|
||||||
|
Arc::new(Mutex::new(None));
|
||||||
|
|
||||||
info!("Listening for connections...");
|
info!("Listening for connections...");
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@@ -205,6 +209,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let sessions = sessions.clone();
|
let sessions = sessions.clone();
|
||||||
let remote_transport = remote_transport.clone();
|
let remote_transport = remote_transport.clone();
|
||||||
|
let waiting_peer = waiting_peer.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let remote_addr = connection.remote_address();
|
let remote_addr = connection.remote_address();
|
||||||
@@ -290,15 +295,102 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
info!(%remote_addr, "session ended");
|
info!(%remote_addr, "session ended");
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
// No remote relay — echo mode: send packets back to the sender.
|
// No remote relay — bridge mode: pair two clients together.
|
||||||
// This lets two clients on the same relay talk to each other,
|
// First client waits, second client connects, then they're bridged.
|
||||||
// or a single client hear its own audio looped back.
|
// If only one client, fall back to echo mode.
|
||||||
info!("no remote relay configured, running in echo mode");
|
|
||||||
|
// Try to take a waiting peer, or register ourselves as waiting
|
||||||
|
let peer = {
|
||||||
|
let mut slot = waiting_peer.lock().await;
|
||||||
|
slot.take()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(peer_transport) = peer {
|
||||||
|
// We're the second client — bridge with the waiting peer
|
||||||
|
let peer_addr = peer_transport.connection().remote_address();
|
||||||
|
info!(%remote_addr, %peer_addr, "bridging two clients");
|
||||||
|
|
||||||
|
let stats = Arc::new(RelayStats {
|
||||||
|
upstream_packets: AtomicU64::new(0),
|
||||||
|
downstream_packets: AtomicU64::new(0),
|
||||||
|
});
|
||||||
|
|
||||||
|
let stats_log = stats.clone();
|
||||||
|
let log_a = remote_addr;
|
||||||
|
let log_b = peer_addr;
|
||||||
|
let stats_handle = tokio::spawn(async move {
|
||||||
|
let mut interval = tokio::time::interval(Duration::from_secs(5));
|
||||||
|
loop {
|
||||||
|
interval.tick().await;
|
||||||
|
let a_to_b = stats_log.upstream_packets.load(Ordering::Relaxed);
|
||||||
|
let b_to_a = stats_log.downstream_packets.load(Ordering::Relaxed);
|
||||||
|
info!(
|
||||||
|
a = %log_a, b = %log_b,
|
||||||
|
a_to_b, b_to_a,
|
||||||
|
"bridge stats"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// A→B: recv from us (new client), send to peer
|
||||||
|
let a_tx = client_transport.clone();
|
||||||
|
let b_tx = peer_transport.clone();
|
||||||
|
let s1 = stats.clone();
|
||||||
|
let a_to_b = tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
match a_tx.recv_media().await {
|
||||||
|
Ok(Some(pkt)) => {
|
||||||
|
if let Err(e) = b_tx.send_media(&pkt).await {
|
||||||
|
error!("A→B send error: {e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
s1.upstream_packets.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
Ok(None) => { info!("client A disconnected"); break; }
|
||||||
|
Err(e) => { error!("A recv error: {e}"); break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// B→A: recv from peer, send to us (new client)
|
||||||
|
let a_tx2 = client_transport.clone();
|
||||||
|
let b_tx2 = peer_transport.clone();
|
||||||
|
let s2 = stats.clone();
|
||||||
|
let b_to_a = tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
match b_tx2.recv_media().await {
|
||||||
|
Ok(Some(pkt)) => {
|
||||||
|
if let Err(e) = a_tx2.send_media(&pkt).await {
|
||||||
|
error!("B→A send error: {e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
s2.downstream_packets.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
Ok(None) => { info!("client B disconnected"); break; }
|
||||||
|
Err(e) => { error!("B recv error: {e}"); break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = a_to_b => {}
|
||||||
|
_ = b_to_a => {}
|
||||||
|
}
|
||||||
|
stats_handle.abort();
|
||||||
|
info!(%remote_addr, %peer_addr, "bridge ended");
|
||||||
|
} else {
|
||||||
|
// We're the first client — register and wait, or echo
|
||||||
|
{
|
||||||
|
let mut slot = waiting_peer.lock().await;
|
||||||
|
*slot = Some(client_transport.clone());
|
||||||
|
}
|
||||||
|
info!(%remote_addr, "waiting for second client to bridge (echo in meantime)");
|
||||||
|
|
||||||
|
// Echo while waiting for a peer to connect
|
||||||
let mut echo_count = 0u64;
|
let mut echo_count = 0u64;
|
||||||
loop {
|
loop {
|
||||||
match client_transport.recv_media().await {
|
match client_transport.recv_media().await {
|
||||||
Ok(Some(packet)) => {
|
Ok(Some(packet)) => {
|
||||||
// Echo the packet back to the sender
|
|
||||||
if let Err(e) = client_transport.send_media(&packet).await {
|
if let Err(e) = client_transport.send_media(&packet).await {
|
||||||
error!("echo send error: {e}");
|
error!("echo send error: {e}");
|
||||||
break;
|
break;
|
||||||
@@ -318,6 +410,10 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Clean up waiting slot if we disconnect
|
||||||
|
let mut slot = waiting_peer.lock().await;
|
||||||
|
*slot = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user