Files
backspace/docs/systems/voice.md
T
Jannis Braun a3a7527c9e chore: add system docs, specs, and misc updates from other sessions
- Add complete docs/systems/ reference (18 system docs)
- Add federation relay status doc and prior spec/plan docs
- Remove superseded docs/federation-dm-s2s.md (replaced by docs/systems/federation.md)
- CLAUDE.md updates
- Minor fixes in social.ts, types.ts, AddDmMemberModal, NewDmModal, UserSettings
2026-03-31 03:40:34 +02:00

6.1 KiB

Voice, Video & Calls System

Source files:

  • Server: routes/livekit.ts, ws/handler.ts, ws/events.ts
  • Client: hooks/useLiveKit.ts, stores/voiceStore.ts, utils/voice.ts, utils/voiceActions.ts, utils/screenShare.ts
  • Shared: packages/shared/src/constants.ts (bitrate matrix, resolutions)
  • Audio: audio/AudioManager.ts, audio/SpeakingDetector.ts

Voice Channel Join Flow

  1. Client sends voice_join { channelId } via WS
  2. Server checks CONNECT permission, enforces one-room-per-user
  3. Server loads voice restrictions from DB (space mute/deafen)
  4. Server broadcasts voice_state_update { action: 'join' } to space
  5. Client calls POST /api/livekit/token { channelId } → gets JWT + LiveKit URL
  6. Client connects to LiveKit room with token

Token grants (space channels):

  • SPEAK → can publish MICROPHONE + CAMERA
  • STREAM → can publish SCREEN_SHARE + SCREEN_SHARE_AUDIO
  • Missing permission → grant excludes those sources

Token grants (DM calls): Always full (canSpeak=true, canStream=true)

Identity format: {userId}:{username}, TTL: 1 hour, Room: {channelId} or dm-{dmChannelId}

Multi-tab: Each user has one voiceWs binding. New tab → old socket gets voice_disconnected { reason: 'displaced' }


DM Call State Machine

States: ringingactive → destroyed

Event Action State
dm_call_start Room created, caller bound, 60s timeout starts ringing
dm_call_incoming Broadcast to DM members (excludes caller) ringing
dm_call_accept First accept: ringing→active. Late joins welcome (group DM) active
dm_call_reject Room destroyed, caller unbound
dm_call_end All participants unbound, room destroyed
Timeout (60s) Auto-cleanup if still ringing, broadcast dm_call_ended

Edge cases:

  • Starting new call cancels any other ringing calls by same caller
  • Socket close during ringing → auto-cleanup
  • Participants drop to 0 in active state → room destroyed

Voice Moderation

Three independent muting mechanisms:

1. User Self-Mute/Deafen

  • Client toggles in voiceStore
  • Broadcasts via voice_status WS event
  • If also space-muted, remains effectively muted

2. Space Mute/Deafen (moderator, persisted)

  • Requires MUTE_MEMBERS / DEAFEN_MEMBERS permission
  • Stored in voice_restrictions table (survives reconnect)
  • In-memory: spaceMutedUsers / spaceDeafenedUsers sets ("spaceId:userId" keys)
  • On voice_join: restrictions loaded from DB into memory
  • Broadcasts voice_space_muted / voice_space_deafened to all space members

3. Permission Mute (automatic, ephemeral)

  • Triggered when user loses SPEAK permission (role update)
  • checkVoicePermissions(spaceId) re-evaluates all users in space voice
  • NOT persisted — derived from role permissions on demand
  • Broadcasts voice_permission_muted

Effective state: effectiveMuted = isMuted || spaceMuted || permissionMuted

Move & Disconnect

  • voice_move: Requires MOVE_MEMBERS. Same space only. Preserves voice status.
  • voice_disconnect: Requires DISCONNECT_MEMBERS. Full teardown.

Screen Sharing

Resolution & Framerate Options

Standard resolutions: 540, 720, 1080, 1440, 2160 (+ 'native')
Standard framerates: 30, 45, 60, 75, 90, 120
Width map: 540→960, 720→1280, 1080→1920, 1440→2560, 2160→3840

VP9 Bitrate Matrix (kbps)

       30    45    60    75    90    120
540:  1500  2000  2500  2800  3200  4000
720:  3000  3500  4000  4500  5000  6000
1080: 6000  7000  8000  9000  10000 12000
1440: 10000 12000 14000 16000 18000 22000
2160: 20000 24000 28000 32000 38000 45000

Config Object

ScreenShareConfig {
  height: number | 'native',       // Resolution or capture at display res
  fps: number,                     // 30-120
  mode: 'gaming' | 'text',         // Affects bitrate & content hint
  customBitrateKbps: number | null, // Admin override (if allowed)
  shareAudio: boolean               // System audio (disabled in Electron)
}

Build Pipeline (buildScreenShareOptions())

  1. Resolve bitrate from matrix (custom > override > default > native estimate)
  2. Clamp to instance limits (minBitrateKbps, maxBitrateKbps)
  3. Compute min bitrate = 25% of max
  4. Codec: VP9 (default) or H.264 (hardware overdrive)
  5. VP8 simulcast backup at reduced framerate/bitrate
  6. Content hint: 'detail' (text) or 'motion' (gaming)

Native Mode

  • Captures at display's full resolution
  • Snaps to nearest known tier for bitrate lookup
  • Scales proportionally: baseKbps * (capturedPixels / knownPixels) * (fps / knownFps)

Hardware Overdrive

  • Forces H.264 hardware encoder via SDP profile override
  • Applied 2s after stream starts (after WebRTC negotiation), re-applied at 5s
  • 4s: detects if using software fallback, warns user

Instance-Level Limits (admin-configured)

  • allowedResolutions, allowedFramerates (CSV in instance_settings)
  • maxResolution, maxFramerate, maxBitrateKbps, minBitrateKbps
  • allowCustomBitrate toggle
  • bitrateMatrixOverrides (JSON sparse overrides)

Audio Processing

Feature Default User Control Notes
Echo Cancellation on yes Stays on during screen share (Chrome AEC handles it)
Noise Suppression overridden Managed by RNNoise state
Auto Gain Control on yes
RNNoise (ML) on yes When enabled: browser NS forced off

Audio constraints applied to mic track:

{
  echoCancellation: userSetting,     // stays on during screen share
  noiseSuppression: rnnoiseEnabled ? false : userSetting,
  autoGainControl: userSetting,
}

Screen share audio (when enabled):

{
  restrictOwnAudio: true,    // Chrome 141+: exclude own tab audio
  echoCancellation: false,
  noiseSuppression: false,
  autoGainControl: false,
  channelCount: 2             // Stereo
}

Persistence: voiceStore with Zustand localStorage. Keys: echoCancellation, autoGainControl, rnnoiseEnabled, screenShareConfig.

Camera preset: 1280x720, 2Mbps, 30fps, H.264