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

8.0 KiB

sessionId
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

// 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

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.