feat: Add webcam basic support

This commit is contained in:
2026-03-30 03:10:44 +02:00
parent 727059fb52
commit b7d4bf20e3
40 changed files with 1042 additions and 296 deletions

View File

@@ -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:<peerKey>` or `camera:<peerKey>`.
## Voice settings

View File

@@ -86,6 +86,25 @@
/>
</button>
<!-- Camera Toggle -->
<button
type="button"
(click)="toggleCamera()"
[class]="getCameraButtonClass()"
>
@if (isCameraEnabled()) {
<ng-icon
name="lucideVideoOff"
class="w-5 h-5"
/>
} @else {
<ng-icon
name="lucideVideo"
class="w-5 h-5"
/>
}
</button>
<!-- Screen Share Toggle -->
<button
type="button"

View File

@@ -84,6 +84,7 @@ export class VoiceControlsComponent implements OnInit, OnDestroy {
connectionErrorMessage = computed(() => this.webrtcService.connectionErrorMessage());
isMuted = signal(false);
isDeafened = signal(false);
isCameraEnabled = computed(() => this.webrtcService.isCameraEnabled());
isScreenSharing = this.screenShareService.isScreenSharing;
showSettings = signal(false);
@@ -281,6 +282,12 @@ export class VoiceControlsComponent implements OnInit, OnDestroy {
}
})
);
this.store.dispatch(
UsersActions.updateCameraState({
userId: user.id,
cameraState: { isEnabled: false }
})
);
}
// End voice session for floating controls
@@ -364,6 +371,42 @@ export class VoiceControlsComponent implements OnInit, OnDestroy {
}
}
async toggleCamera(): Promise<void> {
if (!this.isConnected()) {
return;
}
const user = this.currentUser();
if (this.isCameraEnabled()) {
this.webrtcService.disableCamera();
if (user?.id) {
this.store.dispatch(
UsersActions.updateCameraState({
userId: user.id,
cameraState: { isEnabled: false }
})
);
}
return;
}
try {
await this.webrtcService.enableCamera();
if (user?.id) {
this.store.dispatch(
UsersActions.updateCameraState({
userId: user.id,
cameraState: { isEnabled: true }
})
);
}
} catch (_error) {}
}
async toggleScreenShare(): Promise<void> {
if (this.isScreenSharing()) {
this.screenShareService.stopScreenShare();
@@ -562,6 +605,17 @@ export class VoiceControlsComponent implements OnInit, OnDestroy {
return `${base} bg-secondary text-foreground hover:bg-secondary/80`;
}
getCameraButtonClass(): string {
const base =
'w-10 h-10 inline-flex items-center justify-center rounded-full transition-colors disabled:opacity-50 disabled:cursor-not-allowed';
if (this.isCameraEnabled()) {
return `${base} bg-primary/20 text-primary hover:bg-primary/30`;
}
return `${base} bg-secondary text-foreground hover:bg-secondary/80`;
}
getScreenShareButtonClass(): string {
const base =
'w-10 h-10 inline-flex items-center justify-center rounded-full transition-colors disabled:opacity-50 disabled:cursor-not-allowed';