fix webrtc

This commit is contained in:
2026-04-11 15:33:55 -04:00
parent eacd25a6ef
commit b02f7b292c
4 changed files with 90 additions and 21 deletions
@@ -41,10 +41,10 @@
return `${bps.toFixed(0)} bps`; return `${bps.toFixed(0)} bps`;
}; };
function handleContextMenu(e: MouseEvent, vsIdentity: any) { function handleContextMenu(e: MouseEvent, targetIdentity: any) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const user = chat.users.find((u) => u.identity.isEqual(vsIdentity)); const user = chat.users.find((u) => u.identity.isEqual(targetIdentity));
if (user) { if (user) {
chat.userContextMenu = { x: e.clientX, y: e.clientY, user }; chat.userContextMenu = { x: e.clientX, y: e.clientY, user };
} }
@@ -144,9 +144,10 @@
</span> </span>
{:else if s.isMuted} {:else if s.isMuted}
<span class="voice-indicator-icon" title="Muted"><i class="fas fa-microphone-slash"></i></span> <span class="voice-indicator-icon" title="Muted"><i class="fas fa-microphone-slash"></i></span>
{#if isLocalUserInThisChannel}
<div class="status-dot {voiceStatusColor}"></div>
{/if} {/if}
{#if isLocalUserInThisChannel && (s.isDeafened || s.isMuted)}
<div class="status-dot {voiceStatusColor}"></div>
{/if} {/if}
</div> </div>
</div> </div>
@@ -87,9 +87,13 @@ export class ChannelAudioWebRTCService {
}); });
// Lifecycle Mesh Management // Lifecycle Mesh Management
let lastChannelId: bigint | undefined = undefined;
$effect(() => { $effect(() => {
if (!this.connectedChannelId || !this.identity) { const currentChannelId = this.connectedChannelId;
console.log(`[WebRTC][voice] Cleaning up channel state`); 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.peers.forEach((_, id) =>
this.peerManager.closePeer(id), this.peerManager.closePeer(id),
); );
@@ -97,15 +101,27 @@ export class ChannelAudioWebRTCService {
this.makingOffer.clear(); this.makingOffer.clear();
this.ignoreOffer.clear(); this.ignoreOffer.clear();
this.signalingQueue.clear(); this.signalingQueue.clear();
lastChannelId = undefined;
return; 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( const peersToConnect = new Set(
this.userStates this.userStates
.filter( .filter(
(s) => (s) =>
s.channelId === this.connectedChannelId && s.channelId === currentChannelId &&
!s.identity.isEqual(this.identity!), !s.identity.isEqual(currentIdentity),
) )
.map((s) => s.identity.toHexString()), .map((s) => s.identity.toHexString()),
); );
@@ -132,19 +148,26 @@ export class ChannelAudioWebRTCService {
// Signaling Processors // Signaling Processors
$effect(() => { $effect(() => {
if (!this.connectedChannelId || !this.identity) return; const currentChannelId = this.connectedChannelId;
const currentIdentity = this.identity;
if (!currentChannelId || !currentIdentity) return;
const mySignals = this.signals.filter( const mySignals = this.signals.filter(
(s) => (s) =>
s.channelId === this.connectedChannelId && s.channelId === currentChannelId &&
s.receiver.isEqual(this.identity!) && s.receiver.isEqual(currentIdentity) &&
s.mediaType.tag === "Voice" s.mediaType.tag === "Voice"
); );
if (mySignals.length > 0) {
console.log(`[WebRTC][voice] Found ${mySignals.length} signals for channel ${currentChannelId}`);
}
for (const signal of mySignals) { for (const signal of mySignals) {
if (this.processedSignals.has(signal.id)) continue; if (this.processedSignals.has(signal.id)) continue;
this.processedSignals.add(signal.id); 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) { switch (signal.signalKind.tag) {
case "Offer": case "Offer":
this.handleOffer(signal); this.handleOffer(signal);
@@ -203,6 +226,7 @@ export class ChannelAudioWebRTCService {
try { try {
this.makingOffer.set(peerIdHex, true); this.makingOffer.set(peerIdHex, true);
await pc.setLocalDescription(); await pc.setLocalDescription();
console.log(`[WebRTC][voice] Sending Offer to ${peerIdHex.substring(0,8)}`);
this.#sendSignal({ this.#sendSignal({
receiver: Identity.fromString(peerIdHex), receiver: Identity.fromString(peerIdHex),
signalKind: { tag: "Offer" }, signalKind: { tag: "Offer" },
@@ -218,6 +242,7 @@ export class ChannelAudioWebRTCService {
onIceCandidate(peerIdHex: string, candidate: RTCIceCandidate) { onIceCandidate(peerIdHex: string, candidate: RTCIceCandidate) {
if (this.connectedChannelId) { if (this.connectedChannelId) {
console.log(`[WebRTC][voice] Sending ICE candidate to ${peerIdHex.substring(0,8)}`);
this.#sendSignal({ this.#sendSignal({
receiver: Identity.fromString(peerIdHex), receiver: Identity.fromString(peerIdHex),
signalKind: { tag: "IceCandidate" }, signalKind: { tag: "IceCandidate" },
@@ -247,6 +272,7 @@ export class ChannelAudioWebRTCService {
); );
const answer = await pc.createAnswer(); const answer = await pc.createAnswer();
await pc.setLocalDescription(answer); await pc.setLocalDescription(answer);
console.log(`[WebRTC][voice] Sending Answer to ${peerIdHex.substring(0,8)}`);
this.#sendSignal({ this.#sendSignal({
receiver: signal.sender, receiver: signal.sender,
signalKind: { tag: "Answer" }, signalKind: { tag: "Answer" },
@@ -266,6 +292,7 @@ export class ChannelAudioWebRTCService {
handleAnswer(signal: Types.WebRtcSignal) { handleAnswer(signal: Types.WebRtcSignal) {
const peerIdHex = signal.sender.toHexString(); const peerIdHex = signal.sender.toHexString();
console.log(`[WebRTC][voice] Received Answer from ${peerIdHex.substring(0,8)}`);
this.enqueueSignalingTask(peerIdHex, async () => { this.enqueueSignalingTask(peerIdHex, async () => {
const peer = this.peerManager.getPeer(peerIdHex); const peer = this.peerManager.getPeer(peerIdHex);
if (!peer) return; if (!peer) return;
@@ -285,6 +312,7 @@ export class ChannelAudioWebRTCService {
handleIceCandidate(signal: Types.WebRtcSignal) { handleIceCandidate(signal: Types.WebRtcSignal) {
const peerIdHex = signal.sender.toHexString(); const peerIdHex = signal.sender.toHexString();
console.log(`[WebRTC][voice] Received ICE candidate from ${peerIdHex.substring(0,8)}`);
this.enqueueSignalingTask(peerIdHex, async () => { this.enqueueSignalingTask(peerIdHex, async () => {
const pc = this.peerManager.createPeerConnection(peerIdHex); const pc = this.peerManager.createPeerConnection(peerIdHex);
if (!pc) return; if (!pc) return;
@@ -82,9 +82,13 @@ export class ScreenSharingWebRTCService {
}); });
// Lifecycle Mesh Management // Lifecycle Mesh Management
let lastChannelId: bigint | undefined = undefined;
$effect(() => { $effect(() => {
if (!this.connectedChannelId || !this.identity) { const currentChannelId = this.connectedChannelId;
console.log(`[WebRTC][screen] Cleaning up screen state`); 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.peers.forEach((_, id) =>
this.peerManager.closePeer(id), this.peerManager.closePeer(id),
); );
@@ -92,18 +96,30 @@ export class ScreenSharingWebRTCService {
this.makingOffer.clear(); this.makingOffer.clear();
this.ignoreOffer.clear(); this.ignoreOffer.clear();
this.signalingQueue.clear(); this.signalingQueue.clear();
lastChannelId = undefined;
return; 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<string>(); const screenPeersToConnect = new Set<string>();
this.userStates.forEach((s) => { this.userStates.forEach((s) => {
if (s.channelId === this.connectedChannelId) { if (s.channelId === currentChannelId) {
// If I am watching this user // 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 (s.watching) screenPeersToConnect.add(s.watching.toHexString());
} }
// If this user is watching me // If this user is watching me
if (s.watching?.isEqual(this.identity!)) { if (s.watching?.isEqual(currentIdentity)) {
screenPeersToConnect.add(s.identity.toHexString()); screenPeersToConnect.add(s.identity.toHexString());
} }
} }
@@ -111,7 +127,7 @@ export class ScreenSharingWebRTCService {
screenPeersToConnect.forEach((id) => { screenPeersToConnect.forEach((id) => {
if (!this.peerManager.peers.has(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.peerManager.createPeerConnection(id, [
this.localScreenStream?.getVideoTracks()[0] || null, this.localScreenStream?.getVideoTracks()[0] || null,
this.localScreenStream?.getAudioTracks()[0] || null, this.localScreenStream?.getAudioTracks()[0] || null,
@@ -121,7 +137,7 @@ export class ScreenSharingWebRTCService {
this.peerManager.peers.forEach((_, id) => { this.peerManager.peers.forEach((_, id) => {
if (!screenPeersToConnect.has(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.peerManager.closePeer(id);
this.makingOffer.delete(id); this.makingOffer.delete(id);
this.ignoreOffer.delete(id); this.ignoreOffer.delete(id);
@@ -132,19 +148,26 @@ export class ScreenSharingWebRTCService {
// Signaling Processors // Signaling Processors
$effect(() => { $effect(() => {
if (!this.connectedChannelId || !this.identity) return; const currentChannelId = this.connectedChannelId;
const currentIdentity = this.identity;
if (!currentChannelId || !currentIdentity) return;
const mySignals = this.signals.filter( const mySignals = this.signals.filter(
(s) => (s) =>
s.channelId === this.connectedChannelId && s.channelId === currentChannelId &&
s.receiver.isEqual(this.identity!) && s.receiver.isEqual(currentIdentity) &&
s.mediaType.tag === "Screen" s.mediaType.tag === "Screen"
); );
if (mySignals.length > 0) {
console.log(`[WebRTC][screen] Found ${mySignals.length} signals for channel ${currentChannelId}`);
}
for (const signal of mySignals) { for (const signal of mySignals) {
if (this.processedSignals.has(signal.id)) continue; if (this.processedSignals.has(signal.id)) continue;
this.processedSignals.add(signal.id); 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) { switch (signal.signalKind.tag) {
case "Offer": case "Offer":
this.handleOffer(signal); this.handleOffer(signal);
@@ -203,6 +226,7 @@ export class ScreenSharingWebRTCService {
try { try {
this.makingOffer.set(peerIdHex, true); this.makingOffer.set(peerIdHex, true);
await pc.setLocalDescription(); await pc.setLocalDescription();
console.log(`[WebRTC][screen] Sending Offer to ${peerIdHex.substring(0,8)}`);
this.#sendSignal({ this.#sendSignal({
receiver: Identity.fromString(peerIdHex), receiver: Identity.fromString(peerIdHex),
signalKind: { tag: "Offer" }, signalKind: { tag: "Offer" },
@@ -218,6 +242,7 @@ export class ScreenSharingWebRTCService {
onIceCandidate(peerIdHex: string, candidate: RTCIceCandidate) { onIceCandidate(peerIdHex: string, candidate: RTCIceCandidate) {
if (this.connectedChannelId) { if (this.connectedChannelId) {
console.log(`[WebRTC][screen] Sending ICE candidate to ${peerIdHex.substring(0,8)}`);
this.#sendSignal({ this.#sendSignal({
receiver: Identity.fromString(peerIdHex), receiver: Identity.fromString(peerIdHex),
signalKind: { tag: "IceCandidate" }, signalKind: { tag: "IceCandidate" },
@@ -230,6 +255,7 @@ export class ScreenSharingWebRTCService {
handleOffer(signal: Types.WebRtcSignal) { handleOffer(signal: Types.WebRtcSignal) {
const peerIdHex = signal.sender.toHexString(); const peerIdHex = signal.sender.toHexString();
console.log(`[WebRTC][screen] Received Offer from ${peerIdHex.substring(0,8)}`);
this.enqueueSignalingTask(peerIdHex, async () => { this.enqueueSignalingTask(peerIdHex, async () => {
const pc = this.peerManager.createPeerConnection(peerIdHex); const pc = this.peerManager.createPeerConnection(peerIdHex);
if (!pc) return; if (!pc) return;
@@ -247,6 +273,7 @@ export class ScreenSharingWebRTCService {
); );
const answer = await pc.createAnswer(); const answer = await pc.createAnswer();
await pc.setLocalDescription(answer); await pc.setLocalDescription(answer);
console.log(`[WebRTC][screen] Sending Answer to ${peerIdHex.substring(0,8)}`);
this.#sendSignal({ this.#sendSignal({
receiver: signal.sender, receiver: signal.sender,
signalKind: { tag: "Answer" }, signalKind: { tag: "Answer" },
@@ -266,6 +293,7 @@ export class ScreenSharingWebRTCService {
handleAnswer(signal: Types.WebRtcSignal) { handleAnswer(signal: Types.WebRtcSignal) {
const peerIdHex = signal.sender.toHexString(); const peerIdHex = signal.sender.toHexString();
console.log(`[WebRTC][screen] Received Answer from ${peerIdHex.substring(0,8)}`);
this.enqueueSignalingTask(peerIdHex, async () => { this.enqueueSignalingTask(peerIdHex, async () => {
const peer = this.peerManager.getPeer(peerIdHex); const peer = this.peerManager.getPeer(peerIdHex);
if (!peer) return; if (!peer) return;
@@ -285,6 +313,7 @@ export class ScreenSharingWebRTCService {
handleIceCandidate(signal: Types.WebRtcSignal) { handleIceCandidate(signal: Types.WebRtcSignal) {
const peerIdHex = signal.sender.toHexString(); const peerIdHex = signal.sender.toHexString();
console.log(`[WebRTC][screen] Received ICE candidate from ${peerIdHex.substring(0,8)}`);
this.enqueueSignalingTask(peerIdHex, async () => { this.enqueueSignalingTask(peerIdHex, async () => {
const pc = this.peerManager.createPeerConnection(peerIdHex); const pc = this.peerManager.createPeerConnection(peerIdHex);
if (!pc) return; if (!pc) return;
+11
View File
@@ -67,6 +67,10 @@ export class WebRTCService {
this.localMedia = new LocalMediaService(connectedChannelId); this.localMedia = new LocalMediaService(connectedChannelId);
$effect(() => {
this.localMedia.connectedChannelId = this.connectedChannelId;
});
// Pass localMedia's stream and deafen state to voice // Pass localMedia's stream and deafen state to voice
this.voice = new ChannelAudioWebRTCService( this.voice = new ChannelAudioWebRTCService(
identity, identity,
@@ -81,6 +85,13 @@ export class WebRTCService {
this.localMedia.localScreenStream, 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 // Sound for streams I am watching
let lastWatched = new Set<string>(); let lastWatched = new Set<string>();
$effect(() => { $effect(() => {