From 0714bb0db7967de637c644ebbbb2ef97a602438e Mon Sep 17 00:00:00 2001 From: Trey Orr Date: Thu, 19 Feb 2026 23:18:46 -0500 Subject: [PATCH 1/3] muting added --- apps/web/src/routes/[room]/+page.svelte | 26 +++++++- apps/web/src/routes/docs/api/+page.svelte | 17 +++++- .../routes/docs/getting-started/+page.svelte | 27 ++++++++- bun.lock | 2 +- packages/voca-client/src/index.ts | 59 +++++++++++++++++-- packages/voca-react/src/index.ts | 19 ++++++ packages/voca-svelte/src/lib/index.svelte.ts | 13 ++++ services/signaling/src/handlers.rs | 1 + services/signaling/src/types.rs | 1 + 9 files changed, 152 insertions(+), 13 deletions(-) diff --git a/apps/web/src/routes/[room]/+page.svelte b/apps/web/src/routes/[room]/+page.svelte index 9fa3c30..e7d24c0 100644 --- a/apps/web/src/routes/[room]/+page.svelte +++ b/apps/web/src/routes/[room]/+page.svelte @@ -241,10 +241,30 @@ {#each Array.from(room?.peers.entries() ?? []) as [peerId, peer]}
-

PEER

-

{peerId.slice(0, 6)}

+
+

PEER

+ + {peerId.slice(0, 6)} + +
+

+ {#if peer.remoteMuted} + [SELF-MUTED] + {:else} + [LIVE] + {/if} +

-
[CONNECTED]
+
{/each} diff --git a/apps/web/src/routes/docs/api/+page.svelte b/apps/web/src/routes/docs/api/+page.svelte index 7430846..f74a11c 100644 --- a/apps/web/src/routes/docs/api/+page.svelte +++ b/apps/web/src/routes/docs/api/+page.svelte @@ -85,7 +85,14 @@ console.log(client.roomId); // e.g. "abc123"`} toggleMute() boolean - Toggle mute, returns new state + Toggle local microphone mute, returns new state + + + togglePeerMute(peerId) + boolean + Toggle local playback mute for a specific peer, returns new state on(event, callback) @@ -155,6 +162,14 @@ console.log(client.roomId); // e.g. "abc123"`} local-audio-level level: number (0-1) + + peer-mute + (peerId: string, isMuted: boolean) + + + peer-local-mute + (peerId: string, isMuted: boolean) + diff --git a/apps/web/src/routes/docs/getting-started/+page.svelte b/apps/web/src/routes/docs/getting-started/+page.svelte index da429da..6bf950c 100644 --- a/apps/web/src/routes/docs/getting-started/+page.svelte +++ b/apps/web/src/routes/docs/getting-started/+page.svelte @@ -135,8 +135,18 @@ function VoiceRoom({ roomId }: { roomId: string }) {

Status: {status}

Peers: {peers.size}

+ + {/* Remote peers list */} + {Array.from(peers.entries()).map(([id, peer]) => ( +
+ Peer {id} + +
+ ))} ); }`} @@ -184,7 +194,13 @@ client.on('peer-left', (peerId) => console.log('Peer left:', peerId));`}

Peers: {room.peerCount}

{#each Array.from(room.peers.entries()) as [id, peer]} -
Peer {id.slice(0, 6)} - Level: {peer.audioLevel}
+
+ Peer {id.slice(0, 6)} - Level: {peer.audioLevel} + {#if peer.remoteMuted} (Self-Muted) {/if} + +
{/each}`} {:else}
 (
         
Peer {id.slice(0, 6)} - Level: {peer.audioLevel} + {peer.remoteMuted && (Self-Muted)} +
))} diff --git a/bun.lock b/bun.lock index b180146..a130485 100644 --- a/bun.lock +++ b/bun.lock @@ -41,7 +41,7 @@ "name": "@treyorr/voca-react", "version": "0.3.0", "dependencies": { - "@treyorr/voca-client": "*", + "@treyorr/voca-client": "^0.3.0", }, "devDependencies": { "@types/react": "^19.2.7", diff --git a/packages/voca-client/src/index.ts b/packages/voca-client/src/index.ts index 6d36bee..c9af0e0 100644 --- a/packages/voca-client/src/index.ts +++ b/packages/voca-client/src/index.ts @@ -28,17 +28,20 @@ export interface Peer { connection: RTCPeerConnection; audioLevel: number; stream?: MediaStream; + remoteMuted?: boolean; + localMuted?: boolean; } type SignalMessage = { from: string; - type: 'hello' | 'welcome' | 'join' | 'leave' | 'offer' | 'answer' | 'ice' | 'ping' | 'pong' | 'error'; + type: 'hello' | 'welcome' | 'join' | 'leave' | 'offer' | 'answer' | 'ice' | 'ping' | 'pong' | 'error' | 'mute'; peer_id?: string; to?: string; sdp?: string; candidate?: string; code?: string; message?: string; + muted?: boolean; // Protocol versioning version?: string; client?: string; @@ -53,6 +56,8 @@ interface VocaEvents { 'peer-audio-level': (peerId: string, level: number) => void; 'local-audio-level': (level: number) => void; 'track': (peerId: string, track: MediaStreamTrack, stream: MediaStream) => void; + 'peer-mute': (peerId: string, isMuted: boolean) => void; + 'peer-local-mute': (peerId: string, isMuted: boolean) => void; } /** @@ -99,7 +104,7 @@ export class VocaClient { private shouldReconnect = true; // Audio analysis nodes per peer (for cleanup) - private peerAnalysers: Map = new Map(); + private peerAnalysers: Map = new Map(); /** * Create a new room and return a VocaClient connected to it. @@ -235,10 +240,31 @@ export class VocaClient { if (track) { track.enabled = !track.enabled; this.isMuted = !track.enabled; + // Broadcast our mute state to everyone else + this.send({ type: 'mute', muted: this.isMuted }); } return this.isMuted; } + public togglePeerMute(peerId: string) { + const peer = this.peers.get(peerId); + if (!peer) return false; + + const isCurrentlyMuted = peer.localMuted ?? false; + const newMutedState = !isCurrentlyMuted; + peer.localMuted = newMutedState; + + // Update the gain node + const audioNodes = this.peerAnalysers.get(peerId); + if (audioNodes) { + // Mute by dropping gain to 0, unmute by setting back to 1 + audioNodes.gainNode.gain.value = newMutedState ? 0 : 1; + } + + this.events.emit('peer-local-mute', peerId, newMutedState); + return newMutedState; + } + private async setupMediaAndAudio() { @@ -380,6 +406,10 @@ export class VocaClient { break; case 'join': await this.createPeer(msg.from, true); + if (this.isMuted) { + // Send our mute state specifically to the joined peer + this.send({ type: 'mute', to: msg.from, muted: true }); + } break; case 'offer': await this.createPeer(msg.from, false, msg.sdp); @@ -406,6 +436,13 @@ export class VocaClient { case 'leave': this.removePeer(msg.from); break; + case 'mute': + const mutePeer = this.peers.get(msg.from); + if (mutePeer) { + mutePeer.remoteMuted = msg.muted ?? false; + this.events.emit('peer-mute', msg.from, mutePeer.remoteMuted); + } + break; case 'error': this.handleError(msg.code ?? 'unknown', msg.message ?? 'Unknown error'); break; @@ -430,7 +467,7 @@ export class VocaClient { // Add local tracks this.localStream?.getTracks().forEach((track) => pc.addTrack(track, this.localStream!)); - this.peers.set(peerId, { id: peerId, connection: pc, audioLevel: 0 }); + this.peers.set(peerId, { id: peerId, connection: pc, audioLevel: 0, remoteMuted: false, localMuted: false }); this.events.emit('peer-joined', peerId); if (isInitiator) { @@ -455,6 +492,7 @@ export class VocaClient { if (audio) { audio.source.disconnect(); audio.analyser.disconnect(); + audio.gainNode.disconnect(); this.peerAnalysers.delete(peerId); } @@ -470,13 +508,24 @@ export class VocaClient { const source = this.audioContext.createMediaStreamSource(stream); const analyser = this.audioContext.createAnalyser(); analyser.fftSize = 256; + + // Create a GainNode for local muting + const gainNode = this.audioContext.createGain(); + + // Check if the peer is already locally muted (in case they reconnect) + const peer = this.peers.get(peerId); + if (peer?.localMuted) { + gainNode.gain.value = 0; + } + source.connect(analyser); + analyser.connect(gainNode); // CRITICAL: Connect to speakers so audio is actually played! - source.connect(this.audioContext.destination); + gainNode.connect(this.audioContext.destination); // Store for cleanup when peer leaves - this.peerAnalysers.set(peerId, { source, analyser }); + this.peerAnalysers.set(peerId, { source, analyser, gainNode }); const data = new Uint8Array(analyser.frequencyBinCount); const update = () => { diff --git a/packages/voca-react/src/index.ts b/packages/voca-react/src/index.ts index 4b6595d..d22e42e 100644 --- a/packages/voca-react/src/index.ts +++ b/packages/voca-react/src/index.ts @@ -22,6 +22,8 @@ export interface UseVocaRoomResult { localAudioLevel: number; /** Toggle local audio mute */ toggleMute: () => void; + /** Toggle local playback mute for a specific peer */ + togglePeerMute: (peerId: string) => void; /** Disconnect from room */ disconnect: () => void; } @@ -43,6 +45,14 @@ export interface UseVocaRoomResult { *

Status: {status}

*

Peers: {peers.size}

* + * {Array.from(peers.entries()).map(([id, peer]) => ( + *
+ * Peer {id} + * + *
+ * ))} * * ); * } @@ -65,6 +75,8 @@ export function useVocaRoom(roomId: string, config?: VocaConfig): UseVocaRoomRes const unsubPeerLeft = client.on('peer-left', () => setPeers(new Map(client.peers))); const unsubPeerAudio = client.on('peer-audio-level', () => setPeers(new Map(client.peers))); const unsubLocalAudio = client.on('local-audio-level', setLocalAudioLevel); + const unsubPeerMute = client.on('peer-mute', () => setPeers(new Map(client.peers))); + const unsubPeerLocalMute = client.on('peer-local-mute', () => setPeers(new Map(client.peers))); // Connect to room client.connect() @@ -78,6 +90,8 @@ export function useVocaRoom(roomId: string, config?: VocaConfig): UseVocaRoomRes unsubPeerLeft(); unsubPeerAudio(); unsubLocalAudio(); + unsubPeerMute(); + unsubPeerLocalMute(); client.disconnect(); }; }, [client]); @@ -86,6 +100,10 @@ export function useVocaRoom(roomId: string, config?: VocaConfig): UseVocaRoomRes setIsMuted(client.toggleMute()); }, [client]); + const togglePeerMute = useCallback((peerId: string) => { + client.togglePeerMute(peerId); + }, [client]); + const disconnect = useCallback(() => { client.disconnect(); }, [client]); @@ -97,6 +115,7 @@ export function useVocaRoom(roomId: string, config?: VocaConfig): UseVocaRoomRes isMuted, localAudioLevel, toggleMute, + togglePeerMute, disconnect, }; } diff --git a/packages/voca-svelte/src/lib/index.svelte.ts b/packages/voca-svelte/src/lib/index.svelte.ts index b26f326..bd6e546 100644 --- a/packages/voca-svelte/src/lib/index.svelte.ts +++ b/packages/voca-svelte/src/lib/index.svelte.ts @@ -55,6 +55,14 @@ export class VocaRoom { this.client.on('local-audio-level', (level: number) => { this.localAudioLevel = level; }); + + this.client.on('peer-mute', () => { + this.peers = new Map(this.client.peers); + }); + + this.client.on('peer-local-mute', () => { + this.peers = new Map(this.client.peers); + }); } private updatePeers() { @@ -83,6 +91,11 @@ export class VocaRoom { this.isMuted = this.client.toggleMute(); } + togglePeerMute(peerId: string) { + this.client.togglePeerMute(peerId); + // Reactivity is handled by the 'peer-local-mute' listener + } + getPulseStyle(level: number) { return `--pulse: ${1 + Math.round(level * 7)}px`; } diff --git a/services/signaling/src/handlers.rs b/services/signaling/src/handlers.rs index ce05c1d..9aa8cd3 100644 --- a/services/signaling/src/handlers.rs +++ b/services/signaling/src/handlers.rs @@ -585,6 +585,7 @@ async fn run_forward_task( if to != &peer_id { continue; } } SignalPayload::Ping | SignalPayload::Pong => continue, + SignalPayload::Mute { .. } => {} // Broadcast to everyone else _ => {} } diff --git a/services/signaling/src/types.rs b/services/signaling/src/types.rs index 5b0ab0c..1937fd5 100644 --- a/services/signaling/src/types.rs +++ b/services/signaling/src/types.rs @@ -19,6 +19,7 @@ pub enum SignalPayload { Offer { to: String, sdp: String }, Answer { to: String, sdp: String }, Ice { to: String, candidate: String }, + Mute { muted: bool }, Ping, Pong, Error { code: String, message: String }, From 1c34766eb7a0f4cab00f9d3ea30d5bc85bcfa1c2 Mon Sep 17 00:00:00 2001 From: Trey Orr Date: Thu, 19 Feb 2026 23:21:56 -0500 Subject: [PATCH 2/3] chore: bump version to 0.4.0 --- packages/voca-client/package.json | 4 ++-- packages/voca-react/package.json | 2 +- packages/voca-svelte/package.json | 2 +- services/signaling/Cargo.toml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/voca-client/package.json b/packages/voca-client/package.json index dce75be..abaea24 100644 --- a/packages/voca-client/package.json +++ b/packages/voca-client/package.json @@ -1,7 +1,7 @@ { "name": "@treyorr/voca-client", - "version": "0.3.0", - "description": "Core TypeScript SDK for Voca Signaling", + "version": "0.4.0", + "description": "Voca WebRTC Client SDK", "main": "dist/index.js", "module": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/voca-react/package.json b/packages/voca-react/package.json index 0e93144..7d2681e 100644 --- a/packages/voca-react/package.json +++ b/packages/voca-react/package.json @@ -1,6 +1,6 @@ { "name": "@treyorr/voca-react", - "version": "0.3.0", + "version": "0.4.0", "description": "React hooks for Voca WebRTC voice chat", "main": "dist/index.js", "module": "dist/index.js", diff --git a/packages/voca-svelte/package.json b/packages/voca-svelte/package.json index 65b0213..96e1cfd 100644 --- a/packages/voca-svelte/package.json +++ b/packages/voca-svelte/package.json @@ -1,6 +1,6 @@ { "name": "@treyorr/voca-svelte", - "version": "0.3.0", + "version": "0.4.0", "description": "Svelte 5 Runes wrapper for Voca Client SDK", "svelte": "./dist/index.svelte.js", "types": "./dist/index.svelte.d.ts", diff --git a/services/signaling/Cargo.toml b/services/signaling/Cargo.toml index 812c3d0..219222b 100644 --- a/services/signaling/Cargo.toml +++ b/services/signaling/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "signaling" -version = "0.3.0" +version = "0.4.0" authors = ["Trey Orr"] edition = "2024" From c814b79f4236012c778399a7efce22e2904d7df2 Mon Sep 17 00:00:00 2001 From: Trey Orr Date: Thu, 19 Feb 2026 23:25:04 -0500 Subject: [PATCH 3/3] fix: bump internal workspace dependencies --- apps/web/package.json | 2 +- bun.lock | 10 +++++----- packages/voca-react/package.json | 2 +- packages/voca-svelte/package.json | 2 +- services/signaling/Cargo.lock | 2 +- services/signaling/src/handlers.rs | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index c0c4358..9236ffa 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -23,7 +23,7 @@ "vite": "^7.3.0" }, "dependencies": { - "@treyorr/voca-svelte": "^0.3.0", + "@treyorr/voca-svelte": "^0.4.0", "@lucide/svelte": "^0.574.0" } } \ No newline at end of file diff --git a/bun.lock b/bun.lock index a130485..fbd0f51 100644 --- a/bun.lock +++ b/bun.lock @@ -13,7 +13,7 @@ "version": "0.0.1", "dependencies": { "@lucide/svelte": "^0.574.0", - "@treyorr/voca-svelte": "^0.3.0", + "@treyorr/voca-svelte": "^0.4.0", }, "devDependencies": { "@sveltejs/kit": "^2.49.2", @@ -29,7 +29,7 @@ }, "packages/voca-client": { "name": "@treyorr/voca-client", - "version": "0.3.0", + "version": "0.4.0", "dependencies": { "nanoevents": "^9.1.0", }, @@ -39,7 +39,7 @@ }, "packages/voca-react": { "name": "@treyorr/voca-react", - "version": "0.3.0", + "version": "0.4.0", "dependencies": { "@treyorr/voca-client": "^0.3.0", }, @@ -54,9 +54,9 @@ }, "packages/voca-svelte": { "name": "@treyorr/voca-svelte", - "version": "0.3.0", + "version": "0.4.0", "dependencies": { - "@treyorr/voca-client": "^0.3.0", + "@treyorr/voca-client": "^0.4.0", }, "devDependencies": { "@sveltejs/package": "^2.3.7", diff --git a/packages/voca-react/package.json b/packages/voca-react/package.json index 7d2681e..95b7cd9 100644 --- a/packages/voca-react/package.json +++ b/packages/voca-react/package.json @@ -39,7 +39,7 @@ "react": ">=18.0.0" }, "dependencies": { - "@treyorr/voca-client": "^0.3.0" + "@treyorr/voca-client": "^0.4.0" }, "devDependencies": { "@types/react": "^19.2.7", diff --git a/packages/voca-svelte/package.json b/packages/voca-svelte/package.json index 96e1cfd..a283955 100644 --- a/packages/voca-svelte/package.json +++ b/packages/voca-svelte/package.json @@ -37,7 +37,7 @@ "access": "public" }, "dependencies": { - "@treyorr/voca-client": "^0.3.0" + "@treyorr/voca-client": "^0.4.0" }, "devDependencies": { "@sveltejs/package": "^2.3.7", diff --git a/services/signaling/Cargo.lock b/services/signaling/Cargo.lock index f85320d..d2ddac7 100644 --- a/services/signaling/Cargo.lock +++ b/services/signaling/Cargo.lock @@ -1050,7 +1050,7 @@ dependencies = [ [[package]] name = "signaling" -version = "0.3.0" +version = "0.4.0" dependencies = [ "axum", "axum-extra", diff --git a/services/signaling/src/handlers.rs b/services/signaling/src/handlers.rs index 9aa8cd3..fbe7558 100644 --- a/services/signaling/src/handlers.rs +++ b/services/signaling/src/handlers.rs @@ -432,7 +432,7 @@ async fn handle_socket(socket: WebSocket, key: RoomKey, state: AppState) { let welcome = SignalMessage { from: "server".to_string(), payload: SignalPayload::Welcome { - version: "0.3.0".to_string(), + version: "0.4.0".to_string(), peer_id: peer_id.clone(), }, };