Files
oxspeak_server/.junie/plans/replace-udp-with-rustrtc-3.md
2026-09-12 21:11:14 +02:00

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.