diff --git a/toju-app/src/app/core/services/debugging/debugging-network-snapshot.builder.ts b/toju-app/src/app/core/services/debugging/debugging-network-snapshot.builder.ts index bf1b6c3..308cdc1 100644 --- a/toju-app/src/app/core/services/debugging/debugging-network-snapshot.builder.ts +++ b/toju-app/src/app/core/services/debugging/debugging-network-snapshot.builder.ts @@ -433,7 +433,7 @@ class DebugNetworkSnapshotBuilder { } } - if (type === 'screen-state') { + if (type === 'screen-state' || type === 'camera-state') { const subjectNode = direction === 'outbound' ? this.ensureLocalNetworkNode( state, @@ -442,12 +442,14 @@ class DebugNetworkSnapshotBuilder { this.getPayloadString(payload, 'displayName') ) : peerNode; - const isScreenSharing = this.getPayloadBoolean(payload, 'isScreenSharing'); + const isStreaming = type === 'screen-state' + ? this.getPayloadBoolean(payload, 'isScreenSharing') + : this.getPayloadBoolean(payload, 'isCameraEnabled'); - if (isScreenSharing !== null) { - subjectNode.isStreaming = isScreenSharing; + if (isStreaming !== null) { + subjectNode.isStreaming = isStreaming; - if (!isScreenSharing) + if (!isStreaming) subjectNode.streams.video = 0; } } diff --git a/toju-app/src/app/domains/README.md b/toju-app/src/app/domains/README.md index 72ed0db..a41ef31 100644 --- a/toju-app/src/app/domains/README.md +++ b/toju-app/src/app/domains/README.md @@ -13,7 +13,7 @@ infrastructure adapters and UI. | **chat** | Messaging rules, sync logic, GIF/Klipy integration, chat UI | `KlipyService`, `canEditMessage()`, `ChatMessagesComponent` | | **screen-share** | Source picker, quality presets | `ScreenShareFacade` | | **server-directory** | Multi-server endpoint management, health checks, invites, server search UI | `ServerDirectoryFacade` | -| **voice-connection** | Voice activity detection, bitrate profiles | `VoiceConnectionFacade` | +| **voice-connection** | Voice activity detection, bitrate profiles, in-channel camera transport | `VoiceConnectionFacade` | | **voice-session** | Join/leave orchestration, voice settings persistence | `VoiceSessionFacade` | ## Folder convention diff --git a/toju-app/src/app/domains/screen-share/README.md b/toju-app/src/app/domains/screen-share/README.md index 0dbfcc3..5fd4554 100644 --- a/toju-app/src/app/domains/screen-share/README.md +++ b/toju-app/src/app/domains/screen-share/README.md @@ -1,6 +1,8 @@ # Screen Share Domain -Manages screen sharing sessions, source selection (Electron), quality presets, and the viewer/workspace UI. Like `voice-connection`, the actual WebRTC track distribution lives in `infrastructure/realtime`; this domain provides the application-facing API and UI components. +Manages screen sharing sessions, source selection (Electron), quality presets, and screen-share-specific UI. Like `voice-connection`, the actual WebRTC track distribution lives in `infrastructure/realtime`; this domain provides the application-facing API for display-media capture and playback. + +The mixed live-stream workspace is intentionally not part of this domain. It lives in `features/room/voice-workspace` because it composes screen share, voice-session, voice-connection, and camera state in one shell. ## Module map @@ -14,12 +16,9 @@ screen-share/ │ └── screen-share.config.ts Quality presets and types (re-exported from shared-kernel) │ ├── feature/ -│ ├── screen-share-viewer/ Single-stream video player with fullscreen + volume -│ └── screen-share-workspace/ Multi-stream grid workspace -│ ├── screen-share-workspace.component.ts Grid layout, featured/thumbnail streams, mini-window mode -│ ├── screen-share-stream-tile.component.ts Individual stream tile with fullscreen/volume controls -│ ├── screen-share-playback.service.ts Per-user mute/volume state for screen share audio -│ └── screen-share-workspace.models.ts ScreenShareWorkspaceStreamItem +│ ├── screen-share-quality-dialog/ Quality preset picker before capture +│ ├── screen-share-source-picker/ Electron source selection dialog +│ └── screen-share-viewer/ Single-stream video player with fullscreen + volume │ └── index.ts Barrel exports ``` @@ -33,24 +32,18 @@ graph TD RSF[RealtimeSessionFacade] Config[screen-share.config] Viewer[ScreenShareViewerComponent] - Workspace[ScreenShareWorkspaceComponent] - Tile[ScreenShareStreamTileComponent] - Playback[ScreenSharePlaybackService] + Workspace[VoiceWorkspaceComponent] SSF --> RSF Viewer --> SSF Workspace --> SSF - Workspace --> Playback - Workspace --> Tile Picker --> Config click SSF "application/screen-share.facade.ts" "Proxy to RealtimeSessionFacade" _blank click Picker "application/screen-share-source-picker.service.ts" "Electron source picker" _blank click RSF "../../infrastructure/realtime/realtime-session.service.ts" "Low-level WebRTC composition root" _blank click Viewer "feature/screen-share-viewer/screen-share-viewer.component.ts" "Single-stream player" _blank - click Workspace "feature/screen-share-workspace/screen-share-workspace.component.ts" "Multi-stream workspace" _blank - click Tile "feature/screen-share-workspace/screen-share-stream-tile.component.ts" "Stream tile" _blank - click Playback "feature/screen-share-workspace/screen-share-playback.service.ts" "Per-user volume state" _blank + click Workspace "../../features/room/voice-workspace/voice-workspace.component.ts" "Room-level live stream workspace" _blank click Config "domain/screen-share.config.ts" "Quality presets" _blank ``` @@ -110,28 +103,6 @@ The quality dialog can be shown before each share (`askScreenShareQuality` setti - Focus events from other components via a `viewer:focus` custom DOM event - Auto-stop when the watched user stops sharing or the stream's video tracks end -## Workspace component +## Voice workspace integration -`ScreenShareWorkspaceComponent` is the multi-stream grid view inside the voice workspace panel. It handles: - -- Listing all active screen shares (local + remote) sorted with remote first -- Featured/widescreen mode for a single focused stream with thumbnail sidebar -- Mini-window mode (draggable, position-clamped to viewport) -- Auto-hide header chrome in widescreen mode (2.2 s timeout, revealed on pointer move) -- On-demand remote stream requests via `syncRemoteScreenShareRequests` -- Per-stream volume and mute via `ScreenSharePlaybackService` -- Voice controls (mute, deafen, disconnect, share toggle) integrated into the workspace header - -```mermaid -stateDiagram-v2 - [*] --> Hidden - Hidden --> Expanded: open() - Expanded --> GridView: multiple shares, no focus - Expanded --> WidescreenView: single share or focused stream - WidescreenView --> GridView: showAllStreams() - GridView --> WidescreenView: focusShare(peerKey) - Expanded --> Minimized: minimize() - Minimized --> Expanded: restore() - Expanded --> Hidden: close() - Minimized --> Hidden: close() -``` +`VoiceWorkspaceComponent` in `features/room/voice-workspace` is the multi-stream grid view inside the room shell. It consumes `ScreenShareFacade` for display-media capture and on-demand remote screen-share requests, but it is not part of this domain because it also owns camera presentation and voice-session controls. diff --git a/toju-app/src/app/domains/screen-share/feature/screen-share-workspace/screen-share-workspace.models.ts b/toju-app/src/app/domains/screen-share/feature/screen-share-workspace/screen-share-workspace.models.ts deleted file mode 100644 index e8a530c..0000000 --- a/toju-app/src/app/domains/screen-share/feature/screen-share-workspace/screen-share-workspace.models.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { User } from '../../../../shared-kernel'; - -export interface ScreenShareWorkspaceStreamItem { - id: string; - peerKey: string; - user: User; - stream: MediaStream; - isLocal: boolean; -} diff --git a/toju-app/src/app/domains/screen-share/index.ts b/toju-app/src/app/domains/screen-share/index.ts index 41d0d48..bb43f94 100644 --- a/toju-app/src/app/domains/screen-share/index.ts +++ b/toju-app/src/app/domains/screen-share/index.ts @@ -4,5 +4,3 @@ export * from './domain/screen-share.config'; // Feature components export { ScreenShareViewerComponent } from './feature/screen-share-viewer/screen-share-viewer.component'; -export { ScreenShareWorkspaceComponent } from './feature/screen-share-workspace/screen-share-workspace.component'; -export { ScreenShareStreamTileComponent } from './feature/screen-share-workspace/screen-share-stream-tile.component'; diff --git a/toju-app/src/app/domains/voice-connection/README.md b/toju-app/src/app/domains/voice-connection/README.md index 9940776..5a3197a 100644 --- a/toju-app/src/app/domains/voice-connection/README.md +++ b/toju-app/src/app/domains/voice-connection/README.md @@ -1,13 +1,13 @@ # Voice Connection Domain -Bridges the application layer to the low-level realtime infrastructure for voice calls. Provides speaking detection via Web Audio analysis and per-peer volume control for playback. The actual WebRTC plumbing lives in `infrastructure/realtime`; this domain wraps it with a clean facade. +Bridges the application layer to the low-level realtime infrastructure for voice calls and in-channel camera transport. Provides speaking detection via Web Audio analysis and per-peer volume control for playback. The actual WebRTC plumbing lives in `infrastructure/realtime`; this domain wraps it with a clean facade. ## Module map ``` voice-connection/ ├── application/ -│ ├── voice-connection.facade.ts Proxy to RealtimeSessionFacade for voice signals and methods +│ ├── voice-connection.facade.ts Proxy to RealtimeSessionFacade for voice and camera signals/methods │ ├── voice-activity.service.ts RMS-based speaking detection via AnalyserNode (per-user signals) │ └── voice-playback.service.ts Per-peer GainNode chain, 0-200% volume, deafen support │ @@ -42,13 +42,17 @@ graph TD `VoiceConnectionFacade` exposes signals and methods from `RealtimeSessionFacade` without leaking infrastructure details into feature components. It covers: -- Connection state: `isVoiceConnected`, `isMuted`, `isDeafened`, `hasConnectionError` -- Stream access: `getRemoteVoiceStream`, `getLocalStream`, `getRawMicStream` -- Controls: `enableVoice`, `disableVoice`, `toggleMute`, `toggleDeafen`, `toggleNoiseReduction` +- Connection state: `isVoiceConnected`, `isMuted`, `isDeafened`, `isCameraEnabled`, `hasConnectionError` +- Stream access: `getRemoteVoiceStream`, `getRemoteCameraStream`, `getLocalStream`, `getLocalCameraStream`, `getRawMicStream` +- Controls: `enableVoice`, `disableVoice`, `enableCamera`, `disableCamera`, `toggleMute`, `toggleDeafen`, `toggleNoiseReduction` - Audio tuning: `setOutputVolume`, `setInputVolume`, `setAudioBitrate`, `setLatencyProfile` - Peer events: `onRemoteStream`, `onPeerConnected`, `onPeerDisconnected` - Heartbeat: `startVoiceHeartbeat`, `stopVoiceHeartbeat` +## Camera transport + +Camera capture is treated as voice-adjacent transport, not screen share. The underlying realtime layer routes webcam video only to peers in the same active voice channel, exposes remote camera streams through `getRemoteCameraStream(peerId)`, and keeps webcam senders separate from screen-share senders so both features can run at the same time. + ## Speaking detection `VoiceActivityService` monitors audio levels for local and remote streams using the Web Audio API. Each tracked stream gets its own `AudioContext` with an `AnalyserNode`. A single `requestAnimationFrame` loop polls all analysers. diff --git a/toju-app/src/app/domains/voice-connection/application/voice-connection.facade.ts b/toju-app/src/app/domains/voice-connection/application/voice-connection.facade.ts index 4e6de13..23ff9de 100644 --- a/toju-app/src/app/domains/voice-connection/application/voice-connection.facade.ts +++ b/toju-app/src/app/domains/voice-connection/application/voice-connection.facade.ts @@ -8,6 +8,7 @@ export class VoiceConnectionFacade { readonly isVoiceConnected = inject(RealtimeSessionFacade).isVoiceConnected; readonly isMuted = inject(RealtimeSessionFacade).isMuted; readonly isDeafened = inject(RealtimeSessionFacade).isDeafened; + readonly isCameraEnabled = inject(RealtimeSessionFacade).isCameraEnabled; readonly isNoiseReductionEnabled = inject(RealtimeSessionFacade).isNoiseReductionEnabled; readonly hasConnectionError = inject(RealtimeSessionFacade).hasConnectionError; readonly connectionErrorMessage = inject(RealtimeSessionFacade).connectionErrorMessage; @@ -36,10 +37,18 @@ export class VoiceConnectionFacade { return this.realtime.getRemoteVoiceStream(peerId); } + getRemoteCameraStream(peerId: string): MediaStream | null { + return this.realtime.getRemoteCameraStream(peerId); + } + getLocalStream(): MediaStream | null { return this.realtime.getLocalStream(); } + getLocalCameraStream(): MediaStream | null { + return this.realtime.getLocalCameraStream(); + } + getRawMicStream(): MediaStream | null { return this.realtime.getRawMicStream(); } @@ -52,6 +61,14 @@ export class VoiceConnectionFacade { this.realtime.disableVoice(); } + async enableCamera(): Promise { + return await this.realtime.enableCamera(); + } + + disableCamera(): void { + this.realtime.disableCamera(); + } + async setLocalStream(stream: MediaStream): Promise { await this.realtime.setLocalStream(stream); } diff --git a/toju-app/src/app/domains/voice-session/README.md b/toju-app/src/app/domains/voice-session/README.md index fd57d30..ea0bebb 100644 --- a/toju-app/src/app/domains/voice-session/README.md +++ b/toju-app/src/app/domains/voice-session/README.md @@ -2,6 +2,8 @@ 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 ``` @@ -18,7 +20,7 @@ voice-session/ │ └── voice-settings.storage.ts Persists audio device IDs, volumes, bitrate, latency, noise reduction to localStorage │ ├── feature/ -│ ├── voice-controls/ Full voice control panel (mic, deafen, devices, screen share, settings) +│ ├── 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 @@ -93,7 +95,7 @@ stateDiagram-v2 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 screen-share stream gets the widescreen treatment in expanded mode. +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 diff --git a/toju-app/src/app/domains/voice-session/feature/voice-controls/voice-controls.component.html b/toju-app/src/app/domains/voice-session/feature/voice-controls/voice-controls.component.html index 9d2ca43..1fa5ccb 100644 --- a/toju-app/src/app/domains/voice-session/feature/voice-controls/voice-controls.component.html +++ b/toju-app/src/app/domains/voice-session/feature/voice-controls/voice-controls.component.html @@ -86,6 +86,25 @@ /> + + + } @@ -261,13 +265,13 @@ In voice

} - @if (currentUser()?.screenShareState?.isSharing || (currentUser()?.id && isUserSharing(currentUser()!.id))) { + @if (currentUser() && isUserStreaming(currentUser()!.oderId || currentUser()!.id)) { - @if (!item().isLocal) { + @if (!item().isLocal && item().hasAudio) {