diff --git a/src/chat/components/channels/VoiceChannelGroup.svelte b/src/chat/components/channels/VoiceChannelGroup.svelte index b5de357..ec7710d 100644 --- a/src/chat/components/channels/VoiceChannelGroup.svelte +++ b/src/chat/components/channels/VoiceChannelGroup.svelte @@ -41,10 +41,10 @@ return `${bps.toFixed(0)} bps`; }; - function handleContextMenu(e: MouseEvent, vsIdentity: any) { + function handleContextMenu(e: MouseEvent, targetIdentity: any) { e.preventDefault(); e.stopPropagation(); - const user = chat.users.find((u) => u.identity.isEqual(vsIdentity)); + const user = chat.users.find((u) => u.identity.isEqual(targetIdentity)); if (user) { chat.userContextMenu = { x: e.clientX, y: e.clientY, user }; } @@ -144,9 +144,10 @@ {:else if s.isMuted} - {#if isLocalUserInThisChannel} -
{/if} + + {#if isLocalUserInThisChannel && (s.isDeafened || s.isMuted)} +
{/if} diff --git a/src/chat/services/webrtc/channel-audio-webrtc.svelte.ts b/src/chat/services/webrtc/channel-audio-webrtc.svelte.ts index 689c585..3a84f1f 100644 --- a/src/chat/services/webrtc/channel-audio-webrtc.svelte.ts +++ b/src/chat/services/webrtc/channel-audio-webrtc.svelte.ts @@ -87,9 +87,13 @@ export class ChannelAudioWebRTCService { }); // Lifecycle Mesh Management + let lastChannelId: bigint | undefined = undefined; $effect(() => { - if (!this.connectedChannelId || !this.identity) { - console.log(`[WebRTC][voice] Cleaning up channel state`); + const currentChannelId = this.connectedChannelId; + const currentIdentity = this.identity; + + if (!currentChannelId || !currentIdentity) { + console.log(`[WebRTC][voice] Cleaning up channel state (Channel: ${currentChannelId}, Identity: ${currentIdentity?.toHexString().substring(0,8)})`); this.peerManager.peers.forEach((_, id) => this.peerManager.closePeer(id), ); @@ -97,15 +101,27 @@ export class ChannelAudioWebRTCService { this.makingOffer.clear(); this.ignoreOffer.clear(); this.signalingQueue.clear(); + lastChannelId = undefined; return; } + // If we switched channels, clear state for the new connection mesh + if (lastChannelId !== undefined && lastChannelId !== currentChannelId) { + console.log(`[WebRTC][voice] Channel switched ${lastChannelId} -> ${currentChannelId}, resetting mesh state`); + this.peerManager.peers.forEach((_, id) => this.peerManager.closePeer(id)); + this.processedSignals.clear(); + this.makingOffer.clear(); + this.ignoreOffer.clear(); + this.signalingQueue.clear(); + } + lastChannelId = currentChannelId; + const peersToConnect = new Set( this.userStates .filter( (s) => - s.channelId === this.connectedChannelId && - !s.identity.isEqual(this.identity!), + s.channelId === currentChannelId && + !s.identity.isEqual(currentIdentity), ) .map((s) => s.identity.toHexString()), ); @@ -132,19 +148,26 @@ export class ChannelAudioWebRTCService { // Signaling Processors $effect(() => { - if (!this.connectedChannelId || !this.identity) return; + const currentChannelId = this.connectedChannelId; + const currentIdentity = this.identity; + if (!currentChannelId || !currentIdentity) return; const mySignals = this.signals.filter( (s) => - s.channelId === this.connectedChannelId && - s.receiver.isEqual(this.identity!) && + s.channelId === currentChannelId && + s.receiver.isEqual(currentIdentity) && s.mediaType.tag === "Voice" ); + if (mySignals.length > 0) { + console.log(`[WebRTC][voice] Found ${mySignals.length} signals for channel ${currentChannelId}`); + } + for (const signal of mySignals) { if (this.processedSignals.has(signal.id)) continue; this.processedSignals.add(signal.id); + console.log(`[WebRTC][voice] Processing ${signal.signalKind.tag} from ${signal.sender.toHexString().substring(0,8)} (Signal ID: ${signal.id})`); switch (signal.signalKind.tag) { case "Offer": this.handleOffer(signal); @@ -203,6 +226,7 @@ export class ChannelAudioWebRTCService { try { this.makingOffer.set(peerIdHex, true); await pc.setLocalDescription(); + console.log(`[WebRTC][voice] Sending Offer to ${peerIdHex.substring(0,8)}`); this.#sendSignal({ receiver: Identity.fromString(peerIdHex), signalKind: { tag: "Offer" }, @@ -218,6 +242,7 @@ export class ChannelAudioWebRTCService { onIceCandidate(peerIdHex: string, candidate: RTCIceCandidate) { if (this.connectedChannelId) { + console.log(`[WebRTC][voice] Sending ICE candidate to ${peerIdHex.substring(0,8)}`); this.#sendSignal({ receiver: Identity.fromString(peerIdHex), signalKind: { tag: "IceCandidate" }, @@ -247,6 +272,7 @@ export class ChannelAudioWebRTCService { ); const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); + console.log(`[WebRTC][voice] Sending Answer to ${peerIdHex.substring(0,8)}`); this.#sendSignal({ receiver: signal.sender, signalKind: { tag: "Answer" }, @@ -266,6 +292,7 @@ export class ChannelAudioWebRTCService { handleAnswer(signal: Types.WebRtcSignal) { const peerIdHex = signal.sender.toHexString(); + console.log(`[WebRTC][voice] Received Answer from ${peerIdHex.substring(0,8)}`); this.enqueueSignalingTask(peerIdHex, async () => { const peer = this.peerManager.getPeer(peerIdHex); if (!peer) return; @@ -285,6 +312,7 @@ export class ChannelAudioWebRTCService { handleIceCandidate(signal: Types.WebRtcSignal) { const peerIdHex = signal.sender.toHexString(); + console.log(`[WebRTC][voice] Received ICE candidate from ${peerIdHex.substring(0,8)}`); this.enqueueSignalingTask(peerIdHex, async () => { const pc = this.peerManager.createPeerConnection(peerIdHex); if (!pc) return; diff --git a/src/chat/services/webrtc/screen-sharing-webrtc.svelte.ts b/src/chat/services/webrtc/screen-sharing-webrtc.svelte.ts index 2bad553..92ccf6c 100644 --- a/src/chat/services/webrtc/screen-sharing-webrtc.svelte.ts +++ b/src/chat/services/webrtc/screen-sharing-webrtc.svelte.ts @@ -82,9 +82,13 @@ export class ScreenSharingWebRTCService { }); // Lifecycle Mesh Management + let lastChannelId: bigint | undefined = undefined; $effect(() => { - if (!this.connectedChannelId || !this.identity) { - console.log(`[WebRTC][screen] Cleaning up screen state`); + const currentChannelId = this.connectedChannelId; + const currentIdentity = this.identity; + + if (!currentChannelId || !currentIdentity) { + console.log(`[WebRTC][screen] Cleaning up screen state (Channel: ${currentChannelId}, Identity: ${currentIdentity?.toHexString().substring(0,8)})`); this.peerManager.peers.forEach((_, id) => this.peerManager.closePeer(id), ); @@ -92,18 +96,30 @@ export class ScreenSharingWebRTCService { this.makingOffer.clear(); this.ignoreOffer.clear(); this.signalingQueue.clear(); + lastChannelId = undefined; return; } + // If we switched channels, clear state for the new connection mesh + if (lastChannelId !== undefined && lastChannelId !== currentChannelId) { + console.log(`[WebRTC][screen] Channel switched ${lastChannelId} -> ${currentChannelId}, resetting mesh state`); + this.peerManager.peers.forEach((_, id) => this.peerManager.closePeer(id)); + this.processedSignals.clear(); + this.makingOffer.clear(); + this.ignoreOffer.clear(); + this.signalingQueue.clear(); + } + lastChannelId = currentChannelId; + const screenPeersToConnect = new Set(); this.userStates.forEach((s) => { - if (s.channelId === this.connectedChannelId) { + if (s.channelId === currentChannelId) { // If I am watching this user - if (s.identity.isEqual(this.identity!)) { + if (s.identity.isEqual(currentIdentity)) { if (s.watching) screenPeersToConnect.add(s.watching.toHexString()); } // If this user is watching me - if (s.watching?.isEqual(this.identity!)) { + if (s.watching?.isEqual(currentIdentity)) { screenPeersToConnect.add(s.identity.toHexString()); } } @@ -111,7 +127,7 @@ export class ScreenSharingWebRTCService { screenPeersToConnect.forEach((id) => { if (!this.peerManager.peers.has(id)) { - console.log(`[WebRTC][screen] Connecting to watched peer ${id}`); + console.log(`[WebRTC][screen] Connecting to watched peer ${id.substring(0,8)}`); this.peerManager.createPeerConnection(id, [ this.localScreenStream?.getVideoTracks()[0] || null, this.localScreenStream?.getAudioTracks()[0] || null, @@ -121,7 +137,7 @@ export class ScreenSharingWebRTCService { this.peerManager.peers.forEach((_, id) => { if (!screenPeersToConnect.has(id)) { - console.log(`[WebRTC][screen] Peer ${id} no longer watched, closing`); + console.log(`[WebRTC][screen] Peer ${id.substring(0,8)} no longer watched, closing`); this.peerManager.closePeer(id); this.makingOffer.delete(id); this.ignoreOffer.delete(id); @@ -132,19 +148,26 @@ export class ScreenSharingWebRTCService { // Signaling Processors $effect(() => { - if (!this.connectedChannelId || !this.identity) return; + const currentChannelId = this.connectedChannelId; + const currentIdentity = this.identity; + if (!currentChannelId || !currentIdentity) return; const mySignals = this.signals.filter( (s) => - s.channelId === this.connectedChannelId && - s.receiver.isEqual(this.identity!) && + s.channelId === currentChannelId && + s.receiver.isEqual(currentIdentity) && s.mediaType.tag === "Screen" ); + if (mySignals.length > 0) { + console.log(`[WebRTC][screen] Found ${mySignals.length} signals for channel ${currentChannelId}`); + } + for (const signal of mySignals) { if (this.processedSignals.has(signal.id)) continue; this.processedSignals.add(signal.id); + console.log(`[WebRTC][screen] Processing ${signal.signalKind.tag} from ${signal.sender.toHexString().substring(0,8)} (Signal ID: ${signal.id})`); switch (signal.signalKind.tag) { case "Offer": this.handleOffer(signal); @@ -203,6 +226,7 @@ export class ScreenSharingWebRTCService { try { this.makingOffer.set(peerIdHex, true); await pc.setLocalDescription(); + console.log(`[WebRTC][screen] Sending Offer to ${peerIdHex.substring(0,8)}`); this.#sendSignal({ receiver: Identity.fromString(peerIdHex), signalKind: { tag: "Offer" }, @@ -218,6 +242,7 @@ export class ScreenSharingWebRTCService { onIceCandidate(peerIdHex: string, candidate: RTCIceCandidate) { if (this.connectedChannelId) { + console.log(`[WebRTC][screen] Sending ICE candidate to ${peerIdHex.substring(0,8)}`); this.#sendSignal({ receiver: Identity.fromString(peerIdHex), signalKind: { tag: "IceCandidate" }, @@ -230,6 +255,7 @@ export class ScreenSharingWebRTCService { handleOffer(signal: Types.WebRtcSignal) { const peerIdHex = signal.sender.toHexString(); + console.log(`[WebRTC][screen] Received Offer from ${peerIdHex.substring(0,8)}`); this.enqueueSignalingTask(peerIdHex, async () => { const pc = this.peerManager.createPeerConnection(peerIdHex); if (!pc) return; @@ -247,6 +273,7 @@ export class ScreenSharingWebRTCService { ); const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); + console.log(`[WebRTC][screen] Sending Answer to ${peerIdHex.substring(0,8)}`); this.#sendSignal({ receiver: signal.sender, signalKind: { tag: "Answer" }, @@ -266,6 +293,7 @@ export class ScreenSharingWebRTCService { handleAnswer(signal: Types.WebRtcSignal) { const peerIdHex = signal.sender.toHexString(); + console.log(`[WebRTC][screen] Received Answer from ${peerIdHex.substring(0,8)}`); this.enqueueSignalingTask(peerIdHex, async () => { const peer = this.peerManager.getPeer(peerIdHex); if (!peer) return; @@ -285,6 +313,7 @@ export class ScreenSharingWebRTCService { handleIceCandidate(signal: Types.WebRtcSignal) { const peerIdHex = signal.sender.toHexString(); + console.log(`[WebRTC][screen] Received ICE candidate from ${peerIdHex.substring(0,8)}`); this.enqueueSignalingTask(peerIdHex, async () => { const pc = this.peerManager.createPeerConnection(peerIdHex); if (!pc) return; diff --git a/src/chat/services/webrtc/webrtc.svelte.ts b/src/chat/services/webrtc/webrtc.svelte.ts index dd2dbf8..58f8c46 100644 --- a/src/chat/services/webrtc/webrtc.svelte.ts +++ b/src/chat/services/webrtc/webrtc.svelte.ts @@ -67,6 +67,10 @@ export class WebRTCService { this.localMedia = new LocalMediaService(connectedChannelId); + $effect(() => { + this.localMedia.connectedChannelId = this.connectedChannelId; + }); + // Pass localMedia's stream and deafen state to voice this.voice = new ChannelAudioWebRTCService( identity, @@ -81,6 +85,13 @@ export class WebRTCService { this.localMedia.localScreenStream, ); + $effect(() => { + this.voice.connectedChannelId = this.connectedChannelId; + this.screen.connectedChannelId = this.connectedChannelId; + this.voice.localStream = this.localMedia.localStream; + this.screen.localScreenStream = this.localMedia.localScreenStream; + }); + // Sound for streams I am watching let lastWatched = new Set(); $effect(() => {