feat: direct 1:1 calling via relay signaling (Phase 1)
New feature: call someone directly by fingerprint through the relay.
- Client connects with SNI "_signal" for persistent signaling
- RegisterPresence/RegisterPresenceAck for relay registration
- DirectCallOffer routed to target by fingerprint
- DirectCallAnswer with AcceptGeneric/AcceptTrusted/Reject modes
- Relay creates private room (call-{id}), sends CallSetup to both
- Both clients connect to private room for media (existing SFU path)
- Hangup forwarding + cleanup on disconnect
- Desktop CLI: --signal + --call <fingerprint> for testing
- CallRegistry tracks call state (Pending/Ringing/Active/Ended)
- SignalHub manages persistent signaling connections
Tested: Alice calls Bob by fingerprint, relay routes offer, Bob
auto-accepts, both join private room, media flows bidirectionally.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25,8 +25,9 @@ pub mod traits;
|
||||
pub use codec_id::{CodecId, QualityProfile};
|
||||
pub use error::*;
|
||||
pub use packet::{
|
||||
HangupReason, MediaHeader, MediaPacket, MiniFrameContext, MiniHeader, QualityReport,
|
||||
RoomParticipant, SignalMessage, TrunkEntry, TrunkFrame, FRAME_TYPE_FULL, FRAME_TYPE_MINI,
|
||||
CallAcceptMode, HangupReason, MediaHeader, MediaPacket, MiniFrameContext, MiniHeader,
|
||||
QualityReport, RoomParticipant, SignalMessage, TrunkEntry, TrunkFrame, FRAME_TYPE_FULL,
|
||||
FRAME_TYPE_MINI,
|
||||
};
|
||||
pub use bandwidth::{BandwidthEstimator, CongestionState};
|
||||
pub use quality::{AdaptiveQualityController, NetworkContext, Tier};
|
||||
|
||||
@@ -677,6 +677,91 @@ pub enum SignalMessage {
|
||||
GlobalRoomInactive {
|
||||
room: String,
|
||||
},
|
||||
|
||||
// ── Direct calling signals (client ↔ relay signaling) ──
|
||||
|
||||
/// Register on relay for direct calls. Sent on `_signal` connections
|
||||
/// after optional AuthToken.
|
||||
RegisterPresence {
|
||||
/// Client's Ed25519 identity public key.
|
||||
identity_pub: [u8; 32],
|
||||
/// Signature over ("register-presence" || identity_pub).
|
||||
signature: Vec<u8>,
|
||||
/// Optional display name.
|
||||
alias: Option<String>,
|
||||
},
|
||||
|
||||
/// Relay confirms presence registration.
|
||||
RegisterPresenceAck {
|
||||
success: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
error: Option<String>,
|
||||
},
|
||||
|
||||
/// Direct call offer routed through the relay to a specific peer.
|
||||
DirectCallOffer {
|
||||
/// Caller's fingerprint.
|
||||
caller_fingerprint: String,
|
||||
/// Caller's display name.
|
||||
caller_alias: Option<String>,
|
||||
/// Target's fingerprint.
|
||||
target_fingerprint: String,
|
||||
/// Unique call session ID (UUID).
|
||||
call_id: String,
|
||||
/// Caller's Ed25519 identity pub.
|
||||
identity_pub: [u8; 32],
|
||||
/// Caller's ephemeral X25519 pub (for key exchange on media connect).
|
||||
ephemeral_pub: [u8; 32],
|
||||
/// Signature over (ephemeral_pub || target_fingerprint || call_id).
|
||||
signature: Vec<u8>,
|
||||
/// Supported quality profiles.
|
||||
supported_profiles: Vec<crate::QualityProfile>,
|
||||
},
|
||||
|
||||
/// Callee's response to a direct call.
|
||||
DirectCallAnswer {
|
||||
call_id: String,
|
||||
/// How the callee accepts (or rejects).
|
||||
accept_mode: CallAcceptMode,
|
||||
/// Callee's identity pub (present when accepting).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
identity_pub: Option<[u8; 32]>,
|
||||
/// Callee's ephemeral pub (present when accepting).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
ephemeral_pub: Option<[u8; 32]>,
|
||||
/// Signature (present when accepting).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
signature: Option<Vec<u8>>,
|
||||
/// Chosen quality profile (present when accepting).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
chosen_profile: Option<crate::QualityProfile>,
|
||||
},
|
||||
|
||||
/// Relay tells both parties: media room is ready.
|
||||
CallSetup {
|
||||
call_id: String,
|
||||
/// Room name on the relay for the media session (e.g., "_call:a1b2c3d4").
|
||||
room: String,
|
||||
/// Relay address for the QUIC media connection.
|
||||
relay_addr: String,
|
||||
},
|
||||
|
||||
/// Ringing notification (relay → caller, callee received the offer).
|
||||
CallRinging {
|
||||
call_id: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// How the callee responds to a direct call.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum CallAcceptMode {
|
||||
/// Reject the call.
|
||||
Reject,
|
||||
/// Accept with trust — in Phase 2, this enables P2P (reveals IP).
|
||||
/// In Phase 1, behaves the same as AcceptGeneric.
|
||||
AcceptTrusted,
|
||||
/// Accept with privacy — relay always mediates media.
|
||||
AcceptGeneric,
|
||||
}
|
||||
|
||||
/// A participant entry in a RoomUpdate message.
|
||||
|
||||
Reference in New Issue
Block a user