feat: redesign audio networking for real-network resilience

Replaces the fire-and-forget datagram path with sequenced packets, a
per-peer jitter buffer, and persistent per-peer send tasks. Together these
fix three intertwined weaknesses that only showed up off localhost.

Packet format: every audio frame now carries a 4-byte little-endian
sequence number header ([seq][opus payload]), the basis for reordering and
loss detection.

Jitter buffer (core/jitter.rs): incoming packets are reordered by sequence
behind a fixed ~60ms playout delay. Missing sequences with later packets
already buffered are concealed via Opus PLC (decode(None)) -- a path the
decoder supported but nothing ever invoked. Underruns go idle and re-buffer
rather than concealing indefinitely. Covered by unit tests using real
encoded frames (reorder, gap-conceal, prime, late-drop).

Transport (network/iroh_impl.rs): each peer gets one long-lived send task
fed by a shallow bounded channel (drop-oldest on backpressure), instead of
spawning a throwaway task per peer per 20ms frame. Connections are now
established reactively on peer-join and torn down on peer-leave; the
lexicographically-lower EndpointId dials so a full-mesh pair forms exactly
one shared bidirectional connection instead of two racing ones. This also
removes the previous lock-held-across-connect().await serialization.

Opus decoder: PLC output is now sized to one 20ms frame, so concealment
synthesizes 20ms instead of a 120ms burst from the oversized max buffer.

Known follow-up (Tier 2): no reconnect on transient connection loss; a
send error currently retires the peer until they rejoin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 15:48:43 -04:00
co-authored by Claude Opus 4.8
parent 7af0235736
commit 875e6e124c
5 changed files with 437 additions and 176 deletions
+11 -2
View File
@@ -69,8 +69,17 @@ impl FromStr for PeerSpeakTicket {
#[async_trait]
pub trait NetworkTransport: Send + Sync {
/// Send a low-latency unreliable datagram to a specific peer (for audio).
async fn send_datagram(&self, peer_id: EndpointId, data: Bytes) -> Result<(), NetError>;
/// Establish (or ensure) a connection to a peer and set up its send path.
/// Idempotent; safe to call again for an already-connected peer.
async fn connect_peer(&self, peer_id: EndpointId);
/// Tear down the connection and send path for a peer that has left.
async fn disconnect_peer(&self, peer_id: EndpointId);
/// Fan a single audio datagram out to every connected peer. Non-blocking:
/// per-peer queues drop the oldest-pending frame when full, so a slow link
/// can never stall the capture/encode thread. Callable from any thread.
fn broadcast(&self, data: Bytes);
/// Subscribes to incoming datagrams from any peer.
async fn receive_datagrams(&self) -> Result<Receiver<(EndpointId, Bytes)>, NetError>;