# 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/
│   ├── voice-session.facade.ts              Tracks active voice session, drives floating controls
│   └── voice-workspace.service.ts           Workspace mode (hidden/expanded/minimized), focused stream, mini-window position
│
├── domain/
│   ├── voice-session.logic.ts               isViewingVoiceSessionServer, buildVoiceSessionRoom
│   └── voice-session.models.ts              VoiceSessionInfo interface
│
├── infrastructure/
│   └── voice-settings.storage.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/voice-session.facade.ts" "Tracks active voice session" _blank
    click VWS "application/voice-workspace.service.ts" "Workspace mode and focused stream" _blank
    click VSS "infrastructure/voice-settings.storage.ts" "localStorage persistence for audio settings" _blank
    click Logic "domain/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.

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:<peerKey>` or `camera:<peerKey>`.

## 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.
