feat(p2p): Phase 6 — ICE-style path negotiation
Before Phase 6, each side's dual-path race ran independently and
committed to whichever transport completed first. When one side
picked Direct and the other picked Relay, they sent media to
different places — TX > 0 RX: 0 on both, completely silent call.
Phase 6 adds a negotiation step: after the local race completes,
each side sends a MediaPathReport { call_id, direct_ok, winner }
to the peer through the relay. Both wait for the other's report
before committing a transport to the CallEngine. The decision
rule is simple: if BOTH report direct_ok = true, use direct; if
EITHER reports false, BOTH use relay.
## Wire protocol
New `SignalMessage::MediaPathReport { call_id, direct_ok,
race_winner }`. The relay forwards it to the call peer via the
same signal_hub routing used for DirectCallOffer/Answer. The
cross-relay dispatcher also forwards it.
## dual_path::race restructured
Returns `RaceResult` instead of `(Arc<QuinnTransport>, WinningPath)`:
- `direct_transport: Option<Arc<QuinnTransport>>`
- `relay_transport: Option<Arc<QuinnTransport>>`
- `local_winner: WinningPath`
Both paths are run as spawned tasks. After the first completes,
a 1s grace period lets the loser also finish. The connect
command gets BOTH transports (when available) and picks the
right one based on the negotiation outcome. The unused transport
is dropped.
## connect command flow (revised)
1. Run race() → RaceResult with both transports
2. Send MediaPathReport to relay with our direct_ok
3. Install oneshot; wait for peer's report (3s timeout)
4. Decision: both direct_ok → use direct; else → use relay
5. Start CallEngine with the agreed transport
If the peer never responds (old build, timeout), falls back to
relay — backward compatible.
## Relay forwarding
MediaPathReport is forwarded like DirectCallOffer/Answer: via
signal_hub.send_to(peer_fp) for same-relay calls, and via
cross-relay dispatcher for federated calls.
## Debug log events
- `connect:dual_path_race_done` — local race result
- `connect:path_report_sent` — our report to the peer
- `connect:peer_report_received` — peer's report
- `connect:peer_report_timeout` — peer didn't respond (3s)
- `connect:path_negotiated` — final agreed path with reasons
Full workspace test: 423 passing (no regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -687,6 +687,23 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 6: MediaPathReport forwarded across
|
||||
// federation — deliver to the local participant
|
||||
// of the matching call.
|
||||
SignalMessage::MediaPathReport { ref call_id, .. } => {
|
||||
// Deliver to the local caller (the cross-relay
|
||||
// dispatcher only handles calls where the caller
|
||||
// is local and the callee is remote, or vice versa)
|
||||
let caller_fp = {
|
||||
let reg = call_registry_d.lock().await;
|
||||
reg.get(call_id).map(|c| c.caller_fingerprint.clone())
|
||||
};
|
||||
if let Some(fp) = caller_fp {
|
||||
let hub = signal_hub_d.lock().await;
|
||||
let _ = hub.send_to(&fp, &inner).await;
|
||||
}
|
||||
}
|
||||
|
||||
SignalMessage::Hangup { .. } => {
|
||||
// Best-effort: broadcast the hangup to every
|
||||
// local participant of any call that currently
|
||||
@@ -1294,6 +1311,21 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 6: forward MediaPathReport to the
|
||||
// call peer so both sides can negotiate
|
||||
// the media path before committing.
|
||||
SignalMessage::MediaPathReport { ref call_id, .. } => {
|
||||
let peer_fp = {
|
||||
let reg = call_registry.lock().await;
|
||||
reg.peer_fingerprint(call_id, &client_fp)
|
||||
.map(|s| s.to_string())
|
||||
};
|
||||
if let Some(fp) = peer_fp {
|
||||
let hub = signal_hub.lock().await;
|
||||
let _ = hub.send_to(&fp, &msg).await;
|
||||
}
|
||||
}
|
||||
|
||||
SignalMessage::Ping { timestamp_ms } => {
|
||||
let _ = transport.send_signal(&SignalMessage::Pong { timestamp_ms }).await;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user