4 Commits

Author SHA1 Message Date
Myx
1c7e535057 Possibly screensharing fix for windows where they get deafened when screensharing with audio
All checks were successful
Queue Release Build / prepare (push) Successful in 14s
Deploy Web Apps / deploy (push) Successful in 9m24s
Queue Release Build / build-linux (push) Successful in 24m57s
Queue Release Build / build-windows (push) Successful in 25m21s
Queue Release Build / finalize (push) Successful in 1m43s
2026-03-19 04:15:59 +01:00
Myx
8f960be1e9 Resync username instead of using Anonymous 2026-03-19 03:57:51 +01:00
Myx
9a173792a4 Fix users list to only show server users 2026-03-19 03:48:41 +01:00
Myx
cb2c0495b9 hotfix handshake issue
All checks were successful
Queue Release Build / prepare (push) Successful in 16s
Deploy Web Apps / deploy (push) Successful in 10m15s
Queue Release Build / build-linux (push) Successful in 26m14s
Queue Release Build / build-windows (push) Successful in 25m41s
Queue Release Build / finalize (push) Successful in 1m51s
2026-03-19 03:34:26 +01:00
10 changed files with 157 additions and 56 deletions

View File

@@ -8,18 +8,24 @@ interface WsMessage {
type: string; type: string;
} }
function normalizeDisplayName(value: unknown, fallback = 'User'): string {
const normalized = typeof value === 'string' ? value.trim() : '';
return normalized || fallback;
}
/** Sends the current user list for a given server to a single connected user. */ /** Sends the current user list for a given server to a single connected user. */
function sendServerUsers(user: ConnectedUser, serverId: string): void { function sendServerUsers(user: ConnectedUser, serverId: string): void {
const users = Array.from(connectedUsers.values()) const users = Array.from(connectedUsers.values())
.filter(cu => cu.serverIds.has(serverId) && cu.oderId !== user.oderId) .filter(cu => cu.serverIds.has(serverId) && cu.oderId !== user.oderId)
.map(cu => ({ oderId: cu.oderId, displayName: cu.displayName ?? 'Anonymous' })); .map(cu => ({ oderId: cu.oderId, displayName: normalizeDisplayName(cu.displayName) }));
user.ws.send(JSON.stringify({ type: 'server_users', serverId, users })); user.ws.send(JSON.stringify({ type: 'server_users', serverId, users }));
} }
function handleIdentify(user: ConnectedUser, message: WsMessage, connectionId: string): void { function handleIdentify(user: ConnectedUser, message: WsMessage, connectionId: string): void {
user.oderId = String(message['oderId'] || connectionId); user.oderId = String(message['oderId'] || connectionId);
user.displayName = String(message['displayName'] || 'Anonymous'); user.displayName = normalizeDisplayName(message['displayName'], normalizeDisplayName(user.displayName));
connectedUsers.set(connectionId, user); connectedUsers.set(connectionId, user);
console.log(`User identified: ${user.displayName} (${user.oderId})`); console.log(`User identified: ${user.displayName} (${user.oderId})`);
} }
@@ -47,7 +53,7 @@ async function handleJoinServer(user: ConnectedUser, message: WsMessage, connect
user.serverIds.add(sid); user.serverIds.add(sid);
user.viewedServerId = sid; user.viewedServerId = sid;
connectedUsers.set(connectionId, user); connectedUsers.set(connectionId, user);
console.log(`User ${user.displayName ?? 'Anonymous'} (${user.oderId}) joined server ${sid} (new=${isNew})`); console.log(`User ${normalizeDisplayName(user.displayName)} (${user.oderId}) joined server ${sid} (new=${isNew})`);
sendServerUsers(user, sid); sendServerUsers(user, sid);
@@ -55,7 +61,7 @@ async function handleJoinServer(user: ConnectedUser, message: WsMessage, connect
broadcastToServer(sid, { broadcastToServer(sid, {
type: 'user_joined', type: 'user_joined',
oderId: user.oderId, oderId: user.oderId,
displayName: user.displayName ?? 'Anonymous', displayName: normalizeDisplayName(user.displayName),
serverId: sid serverId: sid
}, user.oderId); }, user.oderId);
} }
@@ -66,7 +72,7 @@ function handleViewServer(user: ConnectedUser, message: WsMessage, connectionId:
user.viewedServerId = viewSid; user.viewedServerId = viewSid;
connectedUsers.set(connectionId, user); connectedUsers.set(connectionId, user);
console.log(`User ${user.displayName ?? 'Anonymous'} (${user.oderId}) viewing server ${viewSid}`); console.log(`User ${normalizeDisplayName(user.displayName)} (${user.oderId}) viewing server ${viewSid}`);
sendServerUsers(user, viewSid); sendServerUsers(user, viewSid);
} }
@@ -87,7 +93,7 @@ function handleLeaveServer(user: ConnectedUser, message: WsMessage, connectionId
broadcastToServer(leaveSid, { broadcastToServer(leaveSid, {
type: 'user_left', type: 'user_left',
oderId: user.oderId, oderId: user.oderId,
displayName: user.displayName ?? 'Anonymous', displayName: normalizeDisplayName(user.displayName),
serverId: leaveSid, serverId: leaveSid,
serverIds: Array.from(user.serverIds) serverIds: Array.from(user.serverIds)
}, user.oderId); }, user.oderId);

View File

@@ -464,23 +464,37 @@ export class WebRTCService implements OnDestroy {
} }
const existing = this.peerManager.activePeerConnections.get(user.oderId); const existing = this.peerManager.activePeerConnections.get(user.oderId);
const healthy = this.isPeerHealthy(existing);
if (existing && !healthy) { if (this.canReusePeerConnection(existing)) {
this.logger.info('Removing stale peer before recreate', { oderId: user.oderId }); this.logger.info('Reusing active peer connection', {
connectionState: existing?.connection.connectionState ?? 'unknown',
dataChannelState: existing?.dataChannel?.readyState ?? 'missing',
oderId: user.oderId,
serverId: message.serverId,
signalUrl
});
continue;
}
if (existing) {
this.logger.info('Removing failed peer before recreate', {
connectionState: existing.connection.connectionState,
dataChannelState: existing.dataChannel?.readyState ?? 'missing',
oderId: user.oderId,
serverId: message.serverId,
signalUrl
});
this.peerManager.removePeer(user.oderId); this.peerManager.removePeer(user.oderId);
} }
if (healthy)
continue;
this.logger.info('Create peer connection to existing user', { this.logger.info('Create peer connection to existing user', {
oderId: user.oderId, oderId: user.oderId,
serverId: message.serverId serverId: message.serverId,
signalUrl
}); });
this.peerManager.createPeerConnection(user.oderId, true); this.peerManager.createPeerConnection(user.oderId, true);
this.peerManager.createAndSendOffer(user.oderId); void this.peerManager.createAndSendOffer(user.oderId);
} }
} }
@@ -811,13 +825,15 @@ export class WebRTCService implements OnDestroy {
* @param displayName - The user's display name. * @param displayName - The user's display name.
*/ */
identify(oderId: string, displayName: string, signalUrl?: string): void { identify(oderId: string, displayName: string, signalUrl?: string): void {
const normalizedDisplayName = displayName.trim() || DEFAULT_DISPLAY_NAME;
this.lastIdentifyCredentials = { oderId, this.lastIdentifyCredentials = { oderId,
displayName }; displayName: normalizedDisplayName };
const identifyMessage = { const identifyMessage = {
type: SIGNALING_TYPE_IDENTIFY, type: SIGNALING_TYPE_IDENTIFY,
oderId, oderId,
displayName displayName: normalizedDisplayName
}; };
if (signalUrl) { if (signalUrl) {
@@ -825,7 +841,15 @@ export class WebRTCService implements OnDestroy {
return; return;
} }
this.sendRawMessage(identifyMessage); const connectedManagers = this.getConnectedSignalingManagers();
if (connectedManagers.length === 0) {
return;
}
for (const { manager } of connectedManagers) {
manager.sendRawMessage(identifyMessage);
}
} }
/** /**
@@ -1282,15 +1306,14 @@ export class WebRTCService implements OnDestroy {
this._isDeafened.set(this.mediaManager.getIsSelfDeafened()); this._isDeafened.set(this.mediaManager.getIsSelfDeafened());
} }
/** Returns true if a peer connection exists and its data channel is open. */ /** Returns true if a peer connection is still alive enough to finish negotiating. */
private isPeerHealthy(peer: import('./webrtc').PeerData | undefined): boolean { private canReusePeerConnection(peer: import('./webrtc').PeerData | undefined): boolean {
if (!peer) if (!peer)
return false; return false;
const connState = peer.connection?.connectionState; const connState = peer.connection?.connectionState;
const dcState = peer.dataChannel?.readyState;
return connState === 'connected' && dcState === 'open'; return connState !== 'closed' && connState !== 'failed';
} }
private handlePeerControlMessage(event: ChatEvent): void { private handlePeerControlMessage(event: ChatEvent): void {

View File

@@ -27,8 +27,14 @@ export class DesktopElectronScreenShareCapture {
return !!this.dependencies.getElectronApi()?.getSources && !this.isLinuxElectron(); return !!this.dependencies.getElectronApi()?.getSources && !this.isLinuxElectron();
} }
shouldSuppressRemotePlaybackDuringShare(includeSystemAudio: boolean): boolean { shouldSuppressRemotePlaybackDuringShare(
return includeSystemAudio && this.isWindowsElectron(); includeSystemAudio: boolean,
usingElectronDesktopCapture: boolean
): boolean {
// Chromium display-media capture can use own-audio suppression on modern
// builds. The Electron desktop-capturer fallback cannot, so keep the old
// Windows mute behavior only for that fallback path.
return includeSystemAudio && usingElectronDesktopCapture && this.isWindowsElectron();
} }
async startCapture( async startCapture(

View File

@@ -340,7 +340,10 @@ export class ScreenShareManager {
includeSystemAudio: this.isScreenActive ? includeSystemAudio : false, includeSystemAudio: this.isScreenActive ? includeSystemAudio : false,
stream: this.isScreenActive ? this.activeScreenStream : null, stream: this.isScreenActive ? this.activeScreenStream : null,
suppressRemotePlayback: this.isScreenActive suppressRemotePlayback: this.isScreenActive
&& this.desktopElectronScreenShareCapture.shouldSuppressRemotePlaybackDuringShare(includeSystemAudio), && this.desktopElectronScreenShareCapture.shouldSuppressRemotePlaybackDuringShare(
includeSystemAudio,
captureMethod === 'electron-desktop'
),
forceDefaultRemotePlaybackOutput: this.isScreenActive forceDefaultRemotePlaybackOutput: this.isScreenActive
&& includeSystemAudio && includeSystemAudio
&& captureMethod === 'linux-electron' && captureMethod === 'linux-electron'

View File

@@ -261,11 +261,11 @@
} }
<!-- Other Online Users --> <!-- Other Online Users -->
@if (onlineUsersFiltered().length > 0) { @if (onlineRoomUsers().length > 0) {
<div class="mb-4"> <div class="mb-4">
<h4 class="text-xs uppercase tracking-wide text-muted-foreground font-medium mb-2 px-1">Online - {{ onlineUsersFiltered().length }}</h4> <h4 class="text-xs uppercase tracking-wide text-muted-foreground font-medium mb-2 px-1">Online - {{ onlineRoomUsers().length }}</h4>
<div class="space-y-1"> <div class="space-y-1">
@for (user of onlineUsersFiltered(); track user.id) { @for (user of onlineRoomUsers(); track user.id) {
<div <div
class="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-secondary/40 group/user" class="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-secondary/40 group/user"
(contextmenu)="openUserContextMenu($event, user)" (contextmenu)="openUserContextMenu($event, user)"
@@ -354,7 +354,7 @@
} }
<!-- No other users message --> <!-- No other users message -->
@if (onlineUsersFiltered().length === 0 && offlineRoomMembers().length === 0) { @if (onlineRoomUsers().length === 0 && offlineRoomMembers().length === 0) {
<div class="text-center py-4 text-muted-foreground"> <div class="text-center py-4 text-muted-foreground">
<p class="text-sm">No other users in this server</p> <p class="text-sm">No other users in this server</p>
</div> </div>

View File

@@ -104,15 +104,30 @@ export class RoomsSidePanelComponent {
textChannels = this.store.selectSignal(selectTextChannels); textChannels = this.store.selectSignal(selectTextChannels);
voiceChannels = this.store.selectSignal(selectVoiceChannels); voiceChannels = this.store.selectSignal(selectVoiceChannels);
roomMembers = computed(() => this.currentRoom()?.members ?? []); roomMembers = computed(() => this.currentRoom()?.members ?? []);
offlineRoomMembers = computed(() => { roomMemberIdentifiers = computed(() => {
const current = this.currentUser(); const identifiers = new Set<string>();
const onlineIds = new Set(this.onlineUsers().map((user) => user.oderId || user.id));
if (current) { for (const member of this.roomMembers()) {
onlineIds.add(current.oderId || current.id); this.addIdentifiers(identifiers, member);
} }
return this.roomMembers().filter((member) => !onlineIds.has(this.roomMemberKey(member))); return identifiers;
});
onlineRoomUsers = computed(() => {
const memberIdentifiers = this.roomMemberIdentifiers();
return this.onlineUsers().filter((user) => !this.isCurrentUserIdentity(user) && this.matchesIdentifiers(memberIdentifiers, user));
});
offlineRoomMembers = computed(() => {
const onlineIdentifiers = new Set<string>();
for (const user of this.onlineRoomUsers()) {
this.addIdentifiers(onlineIdentifiers, user);
}
this.addIdentifiers(onlineIdentifiers, this.currentUser());
return this.roomMembers().filter((member) => !this.matchesIdentifiers(onlineIdentifiers, member));
}); });
knownUserCount = computed(() => { knownUserCount = computed(() => {
const memberIds = new Set( const memberIds = new Set(
@@ -151,18 +166,36 @@ export class RoomsSidePanelComponent {
volumeMenuPeerId = signal(''); volumeMenuPeerId = signal('');
volumeMenuDisplayName = signal(''); volumeMenuDisplayName = signal('');
onlineUsersFiltered() {
const current = this.currentUser();
const currentId = current?.id;
const currentOderId = current?.oderId;
return this.onlineUsers().filter((user) => user.id !== currentId && user.oderId !== currentOderId);
}
private roomMemberKey(member: RoomMember): string { private roomMemberKey(member: RoomMember): string {
return member.oderId || member.id; return member.oderId || member.id;
} }
private addIdentifiers(identifiers: Set<string>, entity: { id?: string; oderId?: string } | null | undefined): void {
if (!entity)
return;
if (entity.id) {
identifiers.add(entity.id);
}
if (entity.oderId) {
identifiers.add(entity.oderId);
}
}
private matchesIdentifiers(identifiers: Set<string>, entity: { id?: string; oderId?: string }): boolean {
return !!((entity.id && identifiers.has(entity.id)) || (entity.oderId && identifiers.has(entity.oderId)));
}
private isCurrentUserIdentity(entity: { id?: string; oderId?: string }): boolean {
const current = this.currentUser();
return !!current && (
(typeof entity.id === 'string' && entity.id === current.id)
|| (typeof entity.oderId === 'string' && entity.oderId === current.oderId)
);
}
canManageChannels(): boolean { canManageChannels(): boolean {
const room = this.currentRoom(); const room = this.currentRoom();
const user = this.currentUser(); const user = this.currentUser();

View File

@@ -69,18 +69,17 @@ export class FloatingVoiceControlsComponent implements OnInit {
isConnected = computed(() => this.webrtcService.isVoiceConnected()); isConnected = computed(() => this.webrtcService.isVoiceConnected());
isMuted = signal(false); isMuted = signal(false);
isDeafened = signal(false); isDeafened = signal(false);
isScreenSharing = signal(false); isScreenSharing = this.webrtcService.isScreenSharing;
includeSystemAudio = signal(false); includeSystemAudio = signal(false);
screenShareQuality = signal<ScreenShareQuality>('balanced'); screenShareQuality = signal<ScreenShareQuality>('balanced');
askScreenShareQuality = signal(true); askScreenShareQuality = signal(true);
showScreenShareQualityDialog = signal(false); showScreenShareQualityDialog = signal(false);
/** Sync local mute/deafen/screen-share state from the WebRTC service on init. */ /** Sync local mute/deafen state from the WebRTC service on init. */
ngOnInit(): void { ngOnInit(): void {
// Sync mute/deafen state from webrtc service // Sync mute/deafen state from webrtc service
this.isMuted.set(this.webrtcService.isMuted()); this.isMuted.set(this.webrtcService.isMuted());
this.isDeafened.set(this.webrtcService.isDeafened()); this.isDeafened.set(this.webrtcService.isDeafened());
this.isScreenSharing.set(this.webrtcService.isScreenSharing());
this.syncScreenShareSettings(); this.syncScreenShareSettings();
const settings = loadVoiceSettingsFromStorage(); const settings = loadVoiceSettingsFromStorage();
@@ -145,7 +144,6 @@ export class FloatingVoiceControlsComponent implements OnInit {
async toggleScreenShare(): Promise<void> { async toggleScreenShare(): Promise<void> {
if (this.isScreenSharing()) { if (this.isScreenSharing()) {
this.webrtcService.stopScreenShare(); this.webrtcService.stopScreenShare();
this.isScreenSharing.set(false);
} else { } else {
this.syncScreenShareSettings(); this.syncScreenShareSettings();
@@ -214,7 +212,6 @@ export class FloatingVoiceControlsComponent implements OnInit {
this.voiceSessionService.endSession(); this.voiceSessionService.endSession();
// Reset local state // Reset local state
this.isScreenSharing.set(false);
this.isMuted.set(false); this.isMuted.set(false);
this.isDeafened.set(false); this.isDeafened.set(false);
} }
@@ -288,8 +285,6 @@ export class FloatingVoiceControlsComponent implements OnInit {
includeSystemAudio: this.includeSystemAudio(), includeSystemAudio: this.includeSystemAudio(),
quality quality
}); });
this.isScreenSharing.set(true);
} catch (_error) { } catch (_error) {
// Screen share request was denied or failed // Screen share request was denied or failed
} }

View File

@@ -84,7 +84,7 @@ export class VoiceControlsComponent implements OnInit, OnDestroy {
connectionErrorMessage = computed(() => this.webrtcService.connectionErrorMessage()); connectionErrorMessage = computed(() => this.webrtcService.connectionErrorMessage());
isMuted = signal(false); isMuted = signal(false);
isDeafened = signal(false); isDeafened = signal(false);
isScreenSharing = signal(false); isScreenSharing = this.webrtcService.isScreenSharing;
showSettings = signal(false); showSettings = signal(false);
inputDevices = signal<AudioDevice[]>([]); inputDevices = signal<AudioDevice[]>([]);
@@ -286,7 +286,6 @@ export class VoiceControlsComponent implements OnInit, OnDestroy {
// End voice session for floating controls // End voice session for floating controls
this.voiceSessionService.endSession(); this.voiceSessionService.endSession();
this.isScreenSharing.set(false);
this.isMuted.set(false); this.isMuted.set(false);
this.isDeafened.set(false); this.isDeafened.set(false);
} }
@@ -368,7 +367,6 @@ export class VoiceControlsComponent implements OnInit, OnDestroy {
async toggleScreenShare(): Promise<void> { async toggleScreenShare(): Promise<void> {
if (this.isScreenSharing()) { if (this.isScreenSharing()) {
this.webrtcService.stopScreenShare(); this.webrtcService.stopScreenShare();
this.isScreenSharing.set(false);
} else { } else {
this.syncScreenShareSettings(); this.syncScreenShareSettings();
@@ -539,8 +537,6 @@ export class VoiceControlsComponent implements OnInit, OnDestroy {
includeSystemAudio: this.includeSystemAudio(), includeSystemAudio: this.includeSystemAudio(),
quality quality
}); });
this.isScreenSharing.set(true);
} catch (_error) {} } catch (_error) {}
} }

View File

@@ -60,7 +60,7 @@ function buildSignalingUser(
data: { oderId: string; displayName?: string }, data: { oderId: string; displayName?: string },
extras: Record<string, unknown> = {} extras: Record<string, unknown> = {}
) { ) {
const displayName = data.displayName || 'User'; const displayName = data.displayName?.trim() || 'User';
return { return {
oderId: data.oderId, oderId: data.oderId,
@@ -98,6 +98,16 @@ function isWrongServer(
return !!(msgServerId && viewedServerId && msgServerId !== viewedServerId); return !!(msgServerId && viewedServerId && msgServerId !== viewedServerId);
} }
function resolveUserDisplayName(user: Pick<User, 'displayName' | 'username'> | null | undefined): string {
const displayName = user?.displayName?.trim();
if (displayName) {
return displayName;
}
return user?.username?.trim() || 'User';
}
interface RoomPresenceSignalingMessage { interface RoomPresenceSignalingMessage {
type: string; type: string;
reason?: string; reason?: string;
@@ -1379,7 +1389,7 @@ export class RoomsEffects {
sourceUrl: room.sourceUrl sourceUrl: room.sourceUrl
}); });
const oderId = resolvedOderId || user?.oderId || this.webrtc.peerId(); const oderId = resolvedOderId || user?.oderId || this.webrtc.peerId();
const displayName = user?.displayName || 'Anonymous'; const displayName = resolveUserDisplayName(user);
const sameSignalRooms = this.getRoomsForSignalingUrl(this.includeRoom(savedRooms, room), wsUrl); const sameSignalRooms = this.getRoomsForSignalingUrl(this.includeRoom(savedRooms, room), wsUrl);
const backgroundRooms = sameSignalRooms.filter((candidate) => candidate.id !== room.id); const backgroundRooms = sameSignalRooms.filter((candidate) => candidate.id !== room.id);
const joinCurrentEndpointRooms = () => { const joinCurrentEndpointRooms = () => {

View File

@@ -438,6 +438,25 @@ export class UsersEffects {
{ dispatch: false } { dispatch: false }
); );
/** Keep signaling identity aligned with the current profile to avoid stale fallback names. */
syncSignalingIdentity$ = createEffect(
() =>
this.actions$.pipe(
ofType(
UsersActions.setCurrentUser,
UsersActions.loadCurrentUserSuccess
),
withLatestFrom(this.store.select(selectCurrentUser)),
tap(([, user]) => {
if (!user)
return;
this.webrtc.identify(user.oderId || user.id, this.resolveDisplayName(user));
})
),
{ dispatch: false }
);
private resolveRoom(roomId: string | undefined, currentRoom: Room | null, savedRooms: Room[]): Room | null { private resolveRoom(roomId: string | undefined, currentRoom: Room | null, savedRooms: Room[]): Room | null {
if (!roomId) if (!roomId)
return currentRoom; return currentRoom;
@@ -448,6 +467,16 @@ export class UsersEffects {
return savedRooms.find((room) => room.id === roomId) ?? null; return savedRooms.find((room) => room.id === roomId) ?? null;
} }
private resolveDisplayName(user: Pick<User, 'displayName' | 'username'>): string {
const displayName = user.displayName?.trim();
if (displayName) {
return displayName;
}
return user.username?.trim() || 'User';
}
private toSourceSelector(room: Room): { sourceId?: string; sourceUrl?: string } { private toSourceSelector(room: Room): { sourceId?: string; sourceUrl?: string } {
return { return {
sourceId: room.sourceId, sourceId: room.sourceId,