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/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} +
Status: {status}
Peers: {peers.size}
+ + {/* Remote peers list */} + {Array.from(peers.entries()).map(([id, peer]) => ( +Peers: {room.peerCount}
{#each Array.from(room.peers.entries()) as [id, peer]} - (
Peer {id.slice(0, 6)} - Level: {peer.audioLevel}
+ {peer.remoteMuted && (Self-Muted)}
+
))}
diff --git a/bun.lock b/bun.lock
index b180146..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,9 +39,9 @@
},
"packages/voca-react": {
"name": "@treyorr/voca-react",
- "version": "0.3.0",
+ "version": "0.4.0",
"dependencies": {
- "@treyorr/voca-client": "*",
+ "@treyorr/voca-client": "^0.3.0",
},
"devDependencies": {
"@types/react": "^19.2.7",
@@ -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-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-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/package.json b/packages/voca-react/package.json
index 0e93144..95b7cd9 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",
@@ -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-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/package.json b/packages/voca-svelte/package.json
index 65b0213..a283955 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",
@@ -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/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/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/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"
diff --git a/services/signaling/src/handlers.rs b/services/signaling/src/handlers.rs
index ce05c1d..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(),
},
};
@@ -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 },