134 lines
8.0 KiB
Markdown
134 lines
8.0 KiB
Markdown
---
|
|
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. |