# Voice Session Domain Tracks voice session metadata across client-side navigation and manages the voice workspace UI state (expanded, minimized, hidden). This domain does not touch WebRTC directly; actual connections live in `voice-connection` and `infrastructure/realtime`. The actual mixed live-stream workspace UI lives in `features/room/voice-workspace` and consumes `VoiceWorkspaceService` from this domain. ## Module map ``` voice-session/ ├── application/ │ ├── facades/ │ │ └── voice-session.facade.ts Tracks active voice session, drives floating controls │ └── services/ │ └── voice-workspace.service.ts Workspace mode (hidden/expanded/minimized), focused stream, mini-window position │ ├── domain/ │ ├── logic/ │ │ └── voice-session.logic.ts isViewingVoiceSessionServer, buildVoiceSessionRoom │ └── models/ │ └── voice-session.model.ts VoiceSessionInfo interface │ ├── infrastructure/ │ └── util/ │ └── voice-settings-storage.util.ts Persists audio device IDs, volumes, bitrate, latency, noise reduction to localStorage │ ├── feature/ │ ├── voice-controls/ Full voice control panel (mic, camera, deafen, devices, screen share, settings) │ └── floating-voice-controls/ Minimal overlay when user navigates away from the voice server │ └── index.ts Barrel exports ``` ## How the pieces connect The facade manages session bookkeeping. The workspace service owns view state. Settings storage provides persistence for user preferences. Neither service opens any WebRTC connections. ```mermaid graph TD VSF[VoiceSessionFacade] VWS[VoiceWorkspaceService] VSS[voiceSettingsStorage] Logic[voice-session.logic] VC[VoiceControlsComponent] FC[FloatingVoiceControlsComponent] Store[NgRx Store] VC --> VSF VC --> VWS VC --> VSS FC --> VSF FC --> VWS VSF --> Logic VSF --> Store VWS --> VSF click VSF "application/facades/voice-session.facade.ts" "Tracks active voice session" _blank click VWS "application/services/voice-workspace.service.ts" "Workspace mode and focused stream" _blank click VSS "infrastructure/util/voice-settings-storage.util.ts" "localStorage persistence for audio settings" _blank click Logic "domain/logic/voice-session.logic.ts" "Pure helper functions" _blank click VC "feature/voice-controls/" "Full voice control panel" _blank click FC "feature/floating-voice-controls/" "Minimal floating overlay" _blank ``` ## Session lifecycle ```mermaid stateDiagram-v2 [*] --> NoSession NoSession --> Active: startSession(info) Active --> Active: checkCurrentRoute(serverId) Active --> NoSession: endSession() state Active { [*] --> ViewingServer ViewingServer --> AwayFromServer: navigated to different server AwayFromServer --> ViewingServer: navigated back / navigateToVoiceServer() } ``` When a voice session is active and the user navigates away from the voice-connected server, `showFloatingControls` becomes `true` and the floating overlay appears. Clicking the overlay dispatches `RoomsActions.viewServer` to navigate back. Joining a new voice target is exclusive: entering another voice channel or private call first disconnects the current call/channel, clears local voice state, and broadcasts the leave for the previous target. Users never need to manually leave one voice target before joining another. Remote voice playback is scoped to the active voice channel, not the whole server. Users stay connected to the shared peer mesh for text, presence, and screen-share control, but voice transport and playback only stay active for peers whose `voiceState.roomId` and `voiceState.serverId` match the local user's current voice session. Owners and admins can also move connected users between voice channels from the room sidebar by dragging a user onto a different voice channel. The moved client updates its local heartbeat and voice-session metadata to the new channel, so routing, floating controls, and occupancy stay in sync after the move. ## Workspace modes `VoiceWorkspaceService` controls the voice workspace panel state. The workspace is only visible when the user is viewing the voice-connected server. ```mermaid stateDiagram-v2 [*] --> Hidden Hidden --> Expanded: open() Expanded --> Minimized: minimize() Expanded --> Hidden: close() / showChat() Minimized --> Expanded: restore() Minimized --> Hidden: close() Expanded --> Hidden: voice session ends Minimized --> Hidden: voice session ends ``` The minimized mode renders a draggable mini-window. Its position is tracked in `miniWindowPosition` and clamped to viewport bounds on resize. `focusedStreamId` controls which live stream gets the widescreen treatment in expanded mode, using feature-level stream IDs such as `screen:` or `camera:`. ## Voice settings Settings are stored in localStorage under a single JSON key. All values are validated and clamped on load to defend against corrupt storage. | Setting | Default | Range | |---|---|---| | inputDevice | `""` | device ID string | | outputDevice | `""` | device ID string | | inputVolume | 100 | 0 -- 100 | | outputVolume | 100 | 0 -- 100 | | audioBitrate | 96 kbps | 32 -- 256 | | latencyProfile | `"balanced"` | low / balanced / high | | noiseReduction | `true` | boolean | | screenShareQuality | `"balanced"` | low / balanced / high | | askScreenShareQuality | `true` | boolean | | includeSystemAudio | `false` | boolean | `loadVoiceSettingsFromStorage()` and `saveVoiceSettingsToStorage(patch)` are the only entry points. The save function merges the patch with the current stored value so callers only need to pass changed fields.