12 KiB
sessionId
| sessionId |
|---|
| session-260912-132834-fuse |
Requirements
Overview & Goals
Replace the current placeholder raw-UDP voice relay (src/udp/*) with a real WebRTC media stack built on the rustrtc crate (already declared in Cargo.toml). The server becomes a centralized SFU (Selective Forwarding Unit): each connected client opens one PeerConnection with the server, the server decrypts/receives that client's audio (and later video) track and forwards it to every other member of the same voice Channel.
Scope
In Scope
- Remove
src/udp/server.rs,src/udp/router.rsraw-socket logic (metrics module is kept/adapted). - Add a
voicemodule hosting:PeerConnectionlifecycle management viarustrtc, SDP offer/answer exchange, ICE candidate exchange, and SFU-style track forwarding scoped bychannel_id. - Reuse the existing WebSocket gateway (
GatewayManager/RealtimeRouter/on_messageinsrc/routes/gateway/mod.rs) as the signaling transport — add aVoicenamespace to the existing JSON event envelope (GatewayEvent { namespace, action, content }) instead of introducing a new endpoint. - Reuse
channel::ChannelType::Voice,channel_user, and theJOIN_VOICE/SPEAKpermission bits already defined insrc/permissions.rsto authorize who may join/publish in a voice channel. - Update
config.toml/NetworkConfigso the singleudp_portcontinues to be the one UDP port used, bound throughrustrtc'sice_udp_mux(single-port ICE) instead of a rawUdpSocket. - Update
AppMetrics/core::mod::App::runwiring so the new voice service starts/stops alongside the HTTP server, replacingUdpServer.
Out of Scope
- TURN/STUN server configuration (kept minimal for LAN/local network scenarios for now).
- Video/screen-share specific handling (structure will allow it later, but only audio forwarding is implemented now).
- Client-side (frontend) implementation details beyond the signaling contract needed by the server.
User Stories
- As a user with
JOIN_VOICEpermission on a voice channel, I want to connect and have my microphone audio heard by other members of that same channel. - As a user already in a voice channel, I want to hear every other member's audio forwarded by the server without opening a direct connection to each peer.
- As a server operator, I want the previous ad-hoc UDP relay removed and replaced by a maintained WebRTC stack (rustrtc) so voice traffic is encrypted (SRTP) and NAT-traversal (ICE) works out of the box.
Functional Requirements
- A client sends an SDP offer through the gateway (
namespace: "Voice"), scoped to achannel_id; the server validates channel membership/permissions, creates arustrtc::PeerConnection, and returns an SDP answer through the same gateway connection. - ICE candidates generated by either side are exchanged as additional
Voicegateway events. - Once connected, the server subscribes the client's inbound audio track to every other
PeerConnectioncurrently joined to the same voice channel (SFU fan-out), and adds new joiners' tracks to previously-connected peers as they arrive. - When a user disconnects (gateway socket closes) or leaves the voice channel, their
PeerConnectionis closed and their track is removed from all other peers' forwarding sets. - Existing
UdpMetrics-style counters (packets/bytes in/out, errors) are preserved in spirit, adapted for voice metrics and reported by the periodicreporter.
Technical Design
Current Implementation
src/udp/server.rs: binds a rawtokio::net::UdpSocketonNetworkConfig.udp_port, loops onrecv_from, and blindly forwards datagrams to registered peers under a hard-coded"default"channel without protocol isolation or encryption.src/udp/router.rs:RoutingTablemapsChannelId -> Vec<SocketAddr>; dead placeholder API.src/udp/metrics.rs: atomic counters (packets_received,bytes_sent, etc.) +spawn_reporterlogging every interval, plugged intoAppMetrics.udp(src/metrics/mod.rs).src/core/mod.rs:App::build/App::runconstructsUdpServer::new(&config.network, udp_metrics)andtokio::spawn(udp_server.run()), joined into shutdown-broadcast pattern.src/routes/gateway/mod.rs:GatewayManagertracks client WebSocket connections;GatewayClient::on_messagehandles incoming messages and acts as the extension point for voice signaling.src/services/realtime_registry.rs: maintainschannel_id -> HashSet<user_id>membership computed fromcomputed_permission(READ_CHANNEL).src/permissions.rs:JOIN_VOICE,SPEAK,STREAM,MOVE_OTHERS,DISCONNECT_OTHERS,MANAGE_VOICE_CHANNELbits already defined.src/models/channel.rs:ChannelType::Voicevariant already exists.
Key Decisions
- Topology: centralized SFU — one
rustrtc::PeerConnectionper connected client per voice channel; the server forwards each publisher's RTP track to every other subscriber in that channel. No direct peer-to-peer mesh. - Signaling transport: existing WebSocket gateway — SDP offer/answer and ICE candidates are carried as
Voicenamespace events inside the currentGatewayEventenvelope, handled inGatewayClient::on_messageand dispatched toVoiceService. - Single UDP port reuse —
rustrtc'sRtcConfigurationis set up withice_udp_mux = trueand bound toNetworkConfig.udp_port, preserving existing firewall/network port configurations. - No TURN/STUN for now —
RtcConfiguration.ice_serversleft default/empty for local and direct network setups. - Voice presence tracked separately from
RealtimeRegistry— an in-memory registry (VoiceRoomperchannel_id) tracks active WebRTC connections independently from general WebSocket channel presence.
Proposed Changes
- Replace
src/udpmodule withsrc/voice:voice/mod.rs: public API, exportsVoiceService.voice/service.rs:VoiceServiceownsrustrtc::RtcConfiguration, a mapchannel_id -> VoiceRoom, and handler methods for offer, ICE candidates, and channel leaves.voice/room.rs:VoiceRoomholdsHashMap<user_id, Arc<PeerConnection>>for a channel and orchestrates track fan-out across peers.voice/metrics.rs: tracks voice counters (packets/bytes/errors), exposed throughAppMetrics.voiceand the existing reporter cadence.
- Extend
GatewayEventhandling: add"Voice"namespace with actionsoffer,answer,ice-candidate,leave. - Permission check on
offer: verifyChannelPermission::JOIN_VOICEandSPEAKbefore initializing aPeerConnection. - Update
src/core/mod.rs: replaceUdpServerinitialization withVoiceServiceheld insideAppState. - Update
src/config.rs: keepNetworkConfig.udp_portfeedingrustrtc'sice_udp_mux_port.
Data Models / Contracts
// Gateway JSON contract additions (namespace = "Voice")
{ "namespace": "Voice", "action": "offer", "content": { "channel_id": Uuid, "sdp": String } }
{ "namespace": "Voice", "action": "answer", "content": { "channel_id": Uuid, "sdp": String } }
{ "namespace": "Voice", "action": "ice-candidate", "content": { "channel_id": Uuid, "candidate": String } }
{ "namespace": "Voice", "action": "leave", "content": { "channel_id": Uuid } }
pub struct VoiceService {
config: rustrtc::RtcConfiguration,
rooms: RwLock<HashMap<Uuid, VoiceRoom>>,
}
impl VoiceService {
pub async fn handle_offer(&self, user_id: Uuid, channel_id: Uuid, sdp: String) -> anyhow::Result<String>;
pub async fn handle_ice_candidate(&self, user_id: Uuid, channel_id: Uuid, candidate: String) -> anyhow::Result<()>;
pub fn leave(&self, user_id: Uuid, channel_id: Uuid);
}
Architecture Diagram
graph TD
ClientA[Client A] -- WebSocket: Voice offer/answer/ICE --> Gateway[GatewayManager / on_message]
ClientB[Client B] -- WebSocket: Voice offer/answer/ICE --> Gateway
Gateway -- dispatch --> VoiceService
VoiceService -- creates/owns --> RoomA[VoiceRoom per channel_id]
RoomA -- PeerConnection A --> PCA[rustrtc PeerConnection A]
RoomA -- PeerConnection B --> PCB[rustrtc PeerConnection B]
ClientA == RTP/SRTP media on UDP mux port ==> PCA
ClientB == RTP/SRTP media on UDP mux port ==> PCB
PCA -- forward audio track --> PCB
PCB -- forward audio track --> PCA
Risks
rustrtcAPI details: verify track forwarding primitives againstrustrtc 0.3.133during implementation and encapsulate track re-subscription invoice/room.rs.- Permission enforcement: ensure permission checks take place before creating peer connections to prevent unauthorized audio relay.
- Signaling message volume: ensure gateway handling remains non-blocking for high frequency ICE candidates.
Delivery Steps
Step 1: Scaffold the voice module and RtcConfiguration from NetworkConfig
A new src/voice module exists with a VoiceService capable of building a rustrtc::RtcConfiguration from the app's network config, replacing the old src/udp module's role in core/mod.rs.
- Create
src/voice/mod.rs,src/voice/service.rs,src/voice/room.rs, andsrc/voice/metrics.rs. - Port
UdpMetrics-style counters intovoice/metrics.rs(packets/bytes in/out, errors), keeping theMetrics/MetricsSnapshottrait implementations used bycrate::metrics::reporter. - Build
RtcConfiguration/RtcConfigurationBuilderinVoiceService::new(&NetworkConfig, metrics)usingice_udp_mux = trueandice_udp_mux_port = network.udp_port, with no external ICE servers configured. - Remove
src/udp/server.rsandsrc/udp/router.rs; delete unusedRoutingTable. - Update
src/core/mod.rs(App::build/App::run) to constructVoiceServiceinstead ofUdpServer, store it onAppState, and drop unneededudp_shutdown_tx/udp_handletasks. - Update
src/metrics/mod.rs(AppMetrics) to reference the new voice metrics type.
Step 2: Implement per-channel PeerConnection lifecycle in VoiceRoom
VoiceService/VoiceRoom can accept an SDP offer for a given channel, create a rustrtc::PeerConnection, and return an SDP answer, tracking connections per channel_id.
- Implement
VoiceRoom(channel_id -> HashMap<user_id, Arc<PeerConnection>>). - Implement
VoiceService::handle_offer(user_id, channel_id, sdp): looks up or creates the room, creates aPeerConnectionviarustrtc, invokesset_remote_description/create_answer/set_local_description, and returns the answer SDP. - Implement
VoiceService::handle_ice_candidateandVoiceService::leave, closing and removing thePeerConnectionfrom its room. - Enforce
ChannelPermission::JOIN_VOICE(andSPEAKfor publishing) before creating aPeerConnection, reusing thecomputed_permissionlookup pattern fromsrc/services/realtime_registry.rs.
Step 3: Wire SFU track forwarding between peers in the same channel
Audio published by one connected client in a voice channel is forwarded by the server to every other client connected to the same channel.
- On
PeerConnection::on_trackfor a given user's connection, register the inbound track on the owningVoiceRoom. - For every other
PeerConnectionalready in thatVoiceRoom, add and forward the new track (SFU fan-out) usingrustrtc's track/MediaCapabilitiesAPIs. - When a new peer joins an existing room, subscribe it to all tracks already being forwarded by other room members.
- On
leaveor disconnect, remove the peer's published track from all other peers' forwarding sets and close itsPeerConnection.
Step 4: Expose Voice signaling through the existing WebSocket gateway
Clients can perform the full offer/answer/ICE handshake over the existing gateway WebSocket connection using a new Voice namespace, with no new HTTP/WS endpoint introduced.
- Extend
GatewayClient::on_messageinsrc/routes/gateway/mod.rsto parseGatewayEvent{namespace: "Voice", action, content}messages (offer,answer,ice-candidate,leave). - Dispatch parsed messages to
VoiceService(accessed viaAppState) and send the resulting answer/ICE-candidate events back through the client's existingmpsc::UnboundedSender<Message>. - On gateway disconnect (
GatewayClient::on_disconnect), callVoiceService::leavefor any channel the user was actively connected to in voice. - Update
config.toml'sDEFAULT_CONFIG_TOMLcomment forudp_portto reflect its new role as the rustrtc ICE/media mux port.