--- sessionId: session-260912-132945-1get --- # Requirements ### Overview & Goals Separate WebRTC voice signaling from the main real-time chat gateway by introducing a **dedicated Voice WebSocket endpoint** (`/ws/voice`). The voice signaling protocol (SDP offer/answer exchange, ICE candidates, room joins/leaves) is isolated from chat events, mirroring the architecture of production communication platforms (e.g. Discord's separate Gateway and Voice Gateway). ### Scope **In Scope** - Implement a dedicated WebSocket route `GET /ws/voice` (nested alongside `/ws/gateway` under `ws_routes`). - Authenticate incoming voice WebSocket connections using the existing `CurrentUser` extractor (supporting Authorization headers, cookies, and `?token=` query parameters). - Define and implement a lightweight, dedicated JSON signaling protocol for voice sessions (`offer`, `answer`, `ice-candidate`, `leave`, `error`). - Bind the voice connection lifecycle directly to the WebSocket lifecycle: connecting/joining initiates the session; closing the socket cleanly tears down the `PeerConnection` and removes the participant from the `VoiceRoom`. - Remove voice signaling handlers (`namespace: "Voice"`) from the general chat gateway (`src/routes/gateway/mod.rs`), restoring clean separation of concerns. - Maintain full compatibility with `VoiceService` and `rustrtc` SFU track fan-out. **Out of Scope** - Changing the underlying `rustrtc` SFU forwarding engine or UDP multiplexing port (`udp_port`). - Implementing TURN/STUN relay configurations. - Video/screen-sharing UI controls (handled in future iterations). ### User Stories - As a client joining a voice channel, I want to open a dedicated WebSocket connection for voice so that media signaling traffic (large SDP payloads, ICE candidates) does not delay or congest my chat message stream. - As a client leaving a voice channel, I want closing my voice WebSocket connection to immediately and cleanly release all server WebRTC resources without affecting my general gateway connection. - As a backend developer, I want voice signaling isolated in its own module/handler so the main gateway router remains focused strictly on application domain events. ### Functional Requirements - **Dedicated Route**: `GET /ws/voice` upgrades to a WebSocket connection authenticated via `CurrentUser`. - **Signaling Exchange**: - Client sends `offer` with `{ "channel_id": Uuid, "sdp": String }`. The server validates permissions (`JOIN_VOICE`), creates/updates the `rustrtc::PeerConnection`, and returns an `answer` event with the server SDP. - Client and server exchange `ice-candidate` events with `{ "channel_id": Uuid, "candidate": String }`. - Client sends `leave` or closes the socket; server removes peer from `VoiceRoom` and unsubscribes tracks. - **Error Handling**: Server returns `{ "action": "error", "message": String }` on permission denial, invalid payload, or WebRTC negotiation failure. - **Gateway Decoupling**: `/ws/gateway` no longer processes `"Voice"` namespace events. ### Non-Functional Requirements - **Performance**: High throughput and low latency signaling with asynchronous message handling via Tokio tasks. - **Reliability**: Deterministic cleanup on socket disconnection preventing orphaned WebRTC sessions or memory leaks. # Technical Design ### Current Implementation - `src/routes/mod.rs`: Mounts `ws_routes` containing `/ws/gateway`. - `src/routes/gateway/mod.rs`: `GatewayClient::on_message` inspects JSON events; if `namespace == "Voice"`, it dispatches `offer`, `ice-candidate`, and `leave` to `AppState.voice`. - `src/voice/service.rs`: Manages `RtcConfiguration`, `VoiceRoom` instances, SDP answers, ICE candidates, and permissions. - `src/http/middleware.rs` & `src/http/context.rs`: `auth_middleware` extracts JWTs from Bearer headers, cookies, or `?token=` query parameters and populates `CurrentUser`. ### Key Decisions 1. **Dedicated WebSocket Route (`/ws/voice`)**: Voice signaling is completely extracted into its own endpoint and handler module (`src/routes/voice/` or `src/voice/ws.rs`), eliminating protocol coupling in `src/routes/gateway/`. 2. **Connection-bound Voice Lifecycle**: The dedicated WebSocket lifetime directly reflects voice room occupancy. When the WebSocket connection drops, `VoiceService::leave` is triggered automatically. 3. **Simplified Signaling Protocol**: Elimination of the generic `GatewayEvent` envelope wrapper (`{ namespace, action, content }`) in favor of direct, typed voice signaling messages (`{ action, ... }`). 4. **Standardized Authentication**: Reuse of `CurrentUser` extractor ensures uniform JWT validation across REST, Gateway, and Voice WebSocket endpoints. ### Proposed Changes - **New Route & Handler**: - Create `src/routes/voice/` with `mod.rs`, `handlers.rs`, and `routes.rs` (or integrate under `src/voice/` and expose via `src/routes/mod.rs`). - Add `GET /voice` route to `ws_routes` in `src/routes/mod.rs` (resulting in `/ws/voice`). - Implement `voice_ws_handler` accepting `WebSocketUpgrade`, `State(AppState)`, and `CurrentUser(user)`. - **Voice WebSocket Protocol**: - Define incoming message types: `Offer { channel_id, sdp }`, `IceCandidate { channel_id, candidate }`, `Leave { channel_id }`. - Define outgoing message types: `Answer { channel_id, sdp }`, `IceCandidate { channel_id, candidate }`, `Error { message }`. - **Gateway Cleanup**: - Remove `namespace == "Voice"` handling from `GatewayClient::on_message`. - Remove voice-specific imports from `src/routes/gateway/mod.rs`. - **VoiceService Integration**: - Update `VoiceService::handle_offer` to take the dedicated voice WebSocket sender channel for emitting server ICE candidates. ### Data Models / Contracts ```rust // Voice WebSocket Client -> Server messages #[derive(Debug, Deserialize)] #[serde(tag = "action", rename_all = "kebab-case")] pub enum VoiceClientMessage { Offer { channel_id: Uuid, sdp: String, }, IceCandidate { channel_id: Uuid, candidate: String, }, Leave { channel_id: Uuid, }, } // Voice WebSocket Server -> Client messages #[derive(Debug, Serialize)] #[serde(tag = "action", rename_all = "kebab-case")] pub enum VoiceServerMessage { Answer { channel_id: Uuid, sdp: String, }, IceCandidate { channel_id: Uuid, candidate: String, }, Error { message: String, }, } ``` ### Components - `VoiceWsHandler` (`src/routes/voice/handlers.rs`): Handles WebSocket upgrades, authentication, event loop, message serialization/deserialization, and lifecycle teardown. - `VoiceRoutes` (`src/routes/voice/routes.rs`): Defines Axum router for `/voice`. - `VoiceService` (`src/voice/service.rs`): Coordinates `rustrtc::PeerConnection` instances, SFU track forwarding, and room management. - `GatewayManager` (`src/routes/gateway/mod.rs`): Retains responsibility solely for domain event broadcasting (chat, server, channel updates). ### File Structure - `src/routes/mod.rs`: Register `voice::routes::router()` under `/ws`. - `src/routes/voice/mod.rs`: Public module definition. - `src/routes/voice/routes.rs`: Voice WebSocket route definition. - `src/routes/voice/handlers.rs`: Voice WebSocket connection handler and message loop. - `src/routes/voice/messages.rs`: Strongly typed signaling message definitions. - `src/routes/gateway/mod.rs`: Cleaned up to remove voice signaling logic. ### Architecture Diagram ```mermaid graph TD Client[Client App] Client -- Chat events / presence --> GW["/ws/gateway (GatewayManager)"] Client -- Voice signaling (SDP/ICE) --> VWS["/ws/voice (VoiceWsHandler)"] VWS -- Manage connections & rooms --> VS[VoiceService] VS -- SFU media fan-out --> RR[rustrtc / VoiceRoom] Client == SRTP media over single UDP port ==> RR ``` ### Risks - **Socket Disconnect Race Conditions**: Ensure that client disconnection promptly removes the user from `VoiceRoom` without deadlocking `RwLock` guards. - **Message Deserialization Errors**: Malformed payloads must result in an `Error` response over the WebSocket rather than terminating the connection loop prematurely.