Skip to content

Commit 68f3b21

Browse files
._.
1 parent f82d86f commit 68f3b21

1 file changed

Lines changed: 8 additions & 58 deletions

File tree

src/core/voiceClient.js

Lines changed: 8 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,3 @@
1-
// core/voiceClient.js
2-
//
3-
// Persistent, app-wide voice/screen-share client built on PeerJS. Lives for
4-
// the lifetime of the app (not the component), so a call survives navigating
5-
// to other channels/servers.
6-
//
7-
// P2P layer: PeerJS. `peer_id` from voice_join/voice_user_joined is a PeerJS
8-
// broker id — offer/answer/ICE are handled entirely by PeerJS's own broker,
9-
// not relayed over the app's websocket. The chat websocket (`conn`) is only
10-
// used for the voice_* room-membership commands.
11-
//
12-
// Each pair of participants gets one bidirectional audio call, plus a
13-
// separate one-way call per active screen share (tagged via call metadata
14-
// so we can tell streams apart on the receiving end). Camera is left out
15-
// for now — same mechanism, add a "camera" kind alongside "screen" later.
161

172
import { createStore, produce } from "solid-js/store";
183
import { createSignal, createEffect, on } from "solid-js";
@@ -27,24 +12,22 @@ const RETRY_CAP_MS = 20_000;
2712
const JOIN_FALLBACK_MS = 1_500;
2813
const PEER_TIMEOUT_MS = 30_000;
2914
const MONITOR_INTERVAL_MS = 5_000;
30-
const SPEAKING_THRESHOLD = 6; // 0-100 scale from analyser average
15+
const SPEAKING_THRESHOLD = 6;
3116

3217
const STORAGE_VOLUMES = "voice_userVolumes";
3318
const STORAGE_MUTES = "voice_userMutes";
3419

3520
export const [voice, setVoice] = createStore({
36-
channel: null, // string | null
37-
server: null, // server src this call belongs to
21+
channel: null,
22+
server: null,
3823
joining: false,
3924
error: null,
4025
muted: false,
4126
deafened: false,
42-
speaking: false, // am I speaking
27+
speaking: false,
4328
myPeerId: null,
4429
isScreenSharing: false,
45-
// peer_id -> { username, peerId, muted, speaking, callState, locallyMuted, localVolume }
4630
participants: {},
47-
// peer_id -> MediaStream, for anyone currently sharing their screen
4831
screenStreams: {},
4932
});
5033

@@ -64,9 +47,8 @@ const audioEls = new Map();
6447
/** @type {Map<string, object>} */
6548
const peerEntries = new Map();
6649

67-
let localDetector = null; // { source, analyser, frameId }
68-
const remoteDetectors = new Map(); // peer_id -> { source, analyser, frameId }
69-
50+
let localDetector = null;
51+
const remoteDetectors = new Map();
7052
let monitorTimer = null;
7153

7254
let userVolumes = loadMap(STORAGE_VOLUMES);
@@ -83,7 +65,6 @@ function saveMap(key, data) {
8365
try {
8466
localStorage.setItem(key, JSON.stringify(data));
8567
} catch {
86-
/* ignore (private mode, etc.) */
8768
}
8869
}
8970

@@ -124,7 +105,6 @@ function setParticipant(peerId, patch) {
124105
);
125106
}
126107

127-
// ── local audio playback for remote mics ──────────────────────────────────
128108

129109
function playRemoteAudio(peerId, stream) {
130110
let el = audioEls.get(peerId);
@@ -151,7 +131,7 @@ export function setUserVolume(peerId, volume) {
151131
userVolumes[peerId] = clamped;
152132
saveMap(STORAGE_VOLUMES, userVolumes);
153133
const el = audioEls.get(peerId);
154-
if (el) el.volume = Math.min(1, clamped); // <audio>.volume caps at 1; swap in a GainNode if you need boost above unity
134+
if (el) el.volume = Math.min(1, clamped);
155135
setParticipant(peerId, { localVolume: clamped });
156136
}
157137
export function getUserVolume(peerId) {
@@ -168,7 +148,6 @@ export function getUserMuted(peerId) {
168148
return userMutes[peerId] ?? false;
169149
}
170150

171-
// ── speaking detection ─────────────────────────────────────────────────────
172151

173152
function startLocalSpeakingDetection() {
174153
if (!localAudioStream) return;
@@ -241,7 +220,6 @@ function stopRemoteSpeakingDetection(peerId) {
241220
}
242221
}
243222

244-
// ── retry helpers ──────────────────────────────────────────────────────────
245223

246224
function scheduleRetry(retry, fn, max = AUDIO_RETRY_MAX) {
247225
if (retry.count >= max) {
@@ -265,9 +243,6 @@ function clearRetry(retry) {
265243
}
266244
}
267245

268-
// ── audio mesh ──────────────────────────────────────────────────────────────
269-
270-
/** Lexicographically-smaller peer id initiates, to avoid both sides calling at once. */
271246
function isAudioInitiator(remotePeerId) {
272247
return !!peer?.id && peer.id < remotePeerId;
273248
}
@@ -280,8 +255,6 @@ function ensureAudio(remotePeerId) {
280255
callPeerAudio(remotePeerId);
281256
return;
282257
}
283-
// Otherwise wait for their inbound call; fall back to initiating ourselves
284-
// if it never arrives (handles their initial attempt getting lost).
285258
if (entry.joinFallbackTimer !== null) clearTimeout(entry.joinFallbackTimer);
286259
entry.joinFallbackTimer = setTimeout(() => {
287260
entry.joinFallbackTimer = null;
@@ -332,8 +305,6 @@ function callPeerAudio(remotePeerId) {
332305
});
333306
}
334307

335-
// ── screen share mesh ───────────────────────────────────────────────────────
336-
337308
function callPeerScreen(remotePeerId, stream) {
338309
if (!peer || peer.destroyed) return;
339310
const tracks = stream.getVideoTracks();
@@ -394,7 +365,6 @@ function closeScreenCalls() {
394365
}
395366
}
396367

397-
/** Start/stop sharing your screen with everyone currently in the call. */
398368
export async function toggleScreenShare() {
399369
if (localScreenStreamRaw) {
400370
stopScreenShare();
@@ -406,7 +376,6 @@ export async function toggleScreenShare() {
406376
audio: true,
407377
});
408378
} catch {
409-
// cancelled or denied by the browser's picker — not worth surfacing as an error
410379
return;
411380
}
412381
const track = localScreenStreamRaw.getVideoTracks()[0];
@@ -425,8 +394,6 @@ function stopScreenShare() {
425394
closeScreenCalls();
426395
}
427396

428-
// ── inbound calls ───────────────────────────────────────────────────────────
429-
430397
function handleInboundCall(call) {
431398
if (!voice.channel) return;
432399
const kind = call.metadata?.kind === "screen" ? "screen" : "audio";
@@ -473,8 +440,6 @@ function handleInboundCall(call) {
473440
});
474441
}
475442

476-
// ── connection state watcher + monitor ──────────────────────────────────────
477-
478443
function watchConnectionState(call, remotePeerId, entry, attempt = 0) {
479444
const pc = call.peerConnection;
480445
if (!pc) {
@@ -558,15 +523,13 @@ function detachPeer(peerId) {
558523
);
559524
}
560525

561-
// ── peer lifecycle ──────────────────────────────────────────────────────────
562-
563526
function initPeer() {
564527
if (peer && !peer.destroyed && peerReady) return peerReady;
565528
peerReady = new Promise((resolve, reject) => {
566529
const p = new Peer({
567530
debug: 0,
568531
config: { iceServers: ICE_SERVERS, iceCandidatePoolSize: 4 },
569-
}); // pass {host, port, path, secure} here for a self-hosted PeerServer
532+
});
570533
p.on("open", () => { peer = p; resolve(p); });
571534
p.on("error", (err) => { if (!peer) reject(err); else console.error("[voice] peer error", err); });
572535
p.on("disconnected", () => {
@@ -577,7 +540,6 @@ function initPeer() {
577540
return peerReady;
578541
}
579542

580-
// ── public API ───────────────────────────────────────────────────────────────
581543

582544
export async function joinVoiceChannel(conn, channel, serverSrc) {
583545
if (voice.channel) leaveVoiceChannel();
@@ -645,11 +607,9 @@ export function toggleVoiceDeafen() {
645607
for (const [peerId, el] of audioEls) {
646608
el.muted = nextDeafened || !!userMutes[peerId];
647609
}
648-
// Deafening implies muting yourself too, same convention as Discord.
649610
if (nextDeafened && !voice.muted) toggleVoiceMute();
650611
}
651612

652-
// ── incoming websocket events ────────────────────────────────────────────────
653613

654614
function handleVoiceEvent(event) {
655615
if (!event?.cmd?.startsWith("voice_")) return;
@@ -710,8 +670,6 @@ function handleVoiceEvent(event) {
710670
break;
711671
}
712672
case "voice_mute": {
713-
// Assumed shape: { cmd: "voice_mute", channel, peer_id, muted }.
714-
// Adjust if your voice_mute.md doc specifies something different.
715673
if (event.channel !== voice.channel) break;
716674
setParticipant(event.peer_id, { muted: !!event.muted });
717675
break;
@@ -721,17 +679,9 @@ function handleVoiceEvent(event) {
721679
}
722680
}
723681

724-
/**
725-
* Call once per active `conn` — e.g. right after `useServerConnection()` in
726-
* App.jsx — so voice broadcasts get processed regardless of which
727-
* channel/server is currently on screen. Must be called inside a component
728-
* or other reactive root (uses createEffect).
729-
*/
730682
export function bindVoiceEvents(conn) {
731683
createEffect(
732684
on(conn.lastEvent, (event) => {
733-
// Only react to the connection our active call belongs to (or any
734-
// connection before we've joined, so we can catch the join response).
735685
if (voice.channel && activeConn && conn !== activeConn) return;
736686
handleVoiceEvent(event);
737687
})

0 commit comments

Comments
 (0)