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

@@ -15,6 +15,7 @@ import {
lucideMicOff,
lucideChevronLeft,
lucideMonitor,
lucideVideo,
lucideHash,
lucideUsers,
lucidePlus,
@@ -40,10 +41,7 @@ import { VoiceActivityService, VoiceConnectionFacade } from '../../../domains/vo
import { VoiceSessionFacade, VoiceWorkspaceService } from '../../../domains/voice-session';
import { VoicePlaybackService } from '../../../domains/voice-connection/application/voice-playback.service';
import { VoiceControlsComponent } from '../../../domains/voice-session/feature/voice-controls/voice-controls.component';
import {
isChannelNameTaken,
normalizeChannelName
} from '../../../store/rooms/room-channels.rules';
import { isChannelNameTaken, normalizeChannelName } from '../../../store/rooms/room-channels.rules';
import {
ContextMenuComponent,
UserAvatarComponent,
@@ -81,6 +79,7 @@ type TabView = 'channels' | 'users';
lucideMicOff,
lucideChevronLeft,
lucideMonitor,
lucideVideo,
lucideHash,
lucideUsers,
lucidePlus,
@@ -274,6 +273,7 @@ export class RoomsSidePanelComponent {
input.focus();
input.select();
});
return;
}
@@ -334,7 +334,6 @@ export class RoomsSidePanelComponent {
confirmCreateChannel() {
const name = normalizeChannelName(this.newChannelName);
const validationError = this.getChannelNameError(name);
if (validationError) {
@@ -597,6 +596,13 @@ export class RoomsSidePanelComponent {
}
})
);
this.store.dispatch(
UsersActions.updateCameraState({
userId: current.id,
cameraState: { isEnabled: false }
})
);
}
this.voiceConnection.broadcastMessage({
@@ -620,11 +626,15 @@ export class RoomsSidePanelComponent {
}
viewShare(userId: string) {
this.voiceWorkspace.focusStream(userId, { connectRemoteShares: true });
this.voiceWorkspace.focusStream(`screen:${userId}`, { connectRemoteShares: true });
}
viewStream(userId: string) {
this.voiceWorkspace.focusStream(userId, { connectRemoteShares: true });
const focusTarget = this.isUserSharing(userId)
? `screen:${userId}`
: `camera:${userId}`;
this.voiceWorkspace.focusStream(focusTarget, { connectRemoteShares: true });
}
canMoveVoiceUsers(): boolean {
@@ -740,31 +750,65 @@ export class RoomsSidePanelComponent {
return this.voicePlayback.isUserMuted(peerId);
}
isUserSharing(userId: string): boolean {
const me = this.currentUser();
isUserOnCamera(userId: string): boolean {
const user = this.findKnownUser(userId);
if (me?.id === userId) {
if (!this.isUserInCurrentVoiceRoom(userId, user)) {
return false;
}
const current = this.currentUser();
if (current && (current.id === userId || current.oderId === userId)) {
return this.voiceConnection.isCameraEnabled();
}
if (user?.cameraState?.isEnabled === true) {
return true;
}
if (user?.cameraState?.isEnabled === false) {
return false;
}
return this.getPeerKeysForUser(user, userId)
.some((peerKey) => this.hasActiveVideoStream(this.voiceConnection.getRemoteCameraStream(peerKey)));
}
isUserSharing(userId: string): boolean {
const user = this.findKnownUser(userId);
if (!this.isUserInCurrentVoiceRoom(userId, user)) {
return false;
}
const current = this.currentUser();
if (current && (current.id === userId || current.oderId === userId)) {
return this.screenShare.isScreenSharing();
}
const user = this.onlineUsers().find((onlineUser) => onlineUser.id === userId || onlineUser.oderId === userId);
if (user?.screenShareState?.isSharing === true) {
return true;
}
if (user?.screenShareState?.isSharing === false) {
return false;
}
const peerKeys = [
user?.oderId,
user?.id,
userId
].filter(
(candidate): candidate is string => !!candidate
);
const stream = peerKeys
const stream = this.getPeerKeysForUser(user, userId)
.map((peerKey) => this.screenShare.getRemoteScreenShareStream(peerKey))
.find((candidate) => !!candidate && candidate.getVideoTracks().length > 0) || null;
.find((candidate) => this.hasActiveVideoStream(candidate)) || null;
return !!stream && stream.getVideoTracks().length > 0;
return this.hasActiveVideoStream(stream);
}
isUserStreaming(userId: string): boolean {
return this.isUserSharing(userId) || this.isUserOnCamera(userId);
}
getUserLiveIconName(userId: string): string {
return this.isUserSharing(userId) ? 'lucideMonitor' : 'lucideVideo';
}
voiceUsersInRoom(roomId: string) {
@@ -829,4 +873,45 @@ export class RoomsSidePanelComponent {
return 'bg-red-500';
}
private findKnownUser(userId: string): User | null {
const current = this.currentUser();
if (current && (current.id === userId || current.oderId === userId)) {
return current;
}
return this.onlineUsers().find((onlineUser) => onlineUser.id === userId || onlineUser.oderId === userId) ?? null;
}
private isUserInCurrentVoiceRoom(userId: string, user: User | null): boolean {
const currentVoiceState = this.currentUser()?.voiceState;
const current = this.currentUser();
if (!currentVoiceState?.isConnected || !currentVoiceState.roomId || !currentVoiceState.serverId) {
return false;
}
if (current && (current.id === userId || current.oderId === userId)) {
return true;
}
return !!user?.voiceState?.isConnected
&& user.voiceState.roomId === currentVoiceState.roomId
&& user.voiceState.serverId === currentVoiceState.serverId;
}
private getPeerKeysForUser(user: User | null, userId: string): string[] {
return [
user?.oderId,
user?.id,
userId
].filter(
(candidate): candidate is string => !!candidate
);
}
private hasActiveVideoStream(stream: MediaStream | null): boolean {
return !!stream && stream.getVideoTracks().some((track) => track.readyState === 'live');
}
}