wireweave 0.3.14 → 0.3.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/voice.js +48 -10
package/package.json
CHANGED
package/src/voice.js
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
// ICE servers. STUN handles same-LAN / non-symmetric-NAT cases; TURN is required
|
|
2
|
+
// when both peers sit behind symmetric or restricted-cone NAT (typical home routers,
|
|
3
|
+
// most carrier-grade NAT, corporate networks). The legacy openrelay.metered.ca
|
|
4
|
+
// hostname was retired; the current public-credential endpoint is global.relay.metered.ca.
|
|
5
|
+
// We include UDP, TCP, and TLS variants so at least one path survives strict egress filtering.
|
|
6
|
+
const DEFAULT_ICE_SERVERS = [
|
|
2
7
|
{ urls: 'stun:stun.l.google.com:19302' },
|
|
3
8
|
{ urls: 'stun:stun1.l.google.com:19302' },
|
|
4
9
|
{ urls: 'stun:stun.cloudflare.com:3478' },
|
|
5
|
-
{ urls: 'stun:stun.
|
|
6
|
-
{ urls: 'turn:
|
|
7
|
-
{ urls: 'turn:
|
|
8
|
-
{ urls: 'turn:
|
|
9
|
-
{ urls: 'turns:
|
|
10
|
+
{ urls: 'stun:global.stun.twilio.com:3478' },
|
|
11
|
+
{ urls: 'turn:global.relay.metered.ca:80', username: 'openrelayproject', credential: 'openrelayproject' },
|
|
12
|
+
{ urls: 'turn:global.relay.metered.ca:80?transport=tcp', username: 'openrelayproject', credential: 'openrelayproject' },
|
|
13
|
+
{ urls: 'turn:global.relay.metered.ca:443', username: 'openrelayproject', credential: 'openrelayproject' },
|
|
14
|
+
{ urls: 'turns:global.relay.metered.ca:443?transport=tcp', username: 'openrelayproject', credential: 'openrelayproject' },
|
|
15
|
+
// Legacy hostname kept as a low-priority fallback in case some deployments still resolve it.
|
|
16
|
+
{ urls: 'turn:openrelay.metered.ca:443?transport=tcp', username: 'openrelayproject', credential: 'openrelayproject' }
|
|
10
17
|
];
|
|
18
|
+
let ICE_SERVERS = DEFAULT_ICE_SERVERS;
|
|
19
|
+
export const setIceServers = (list) => { if (Array.isArray(list) && list.length) ICE_SERVERS = list; };
|
|
20
|
+
export const getIceServers = () => ICE_SERVERS.slice();
|
|
11
21
|
|
|
12
22
|
const PRESENCE_EXPIRY = 300000;
|
|
13
23
|
const HEARTBEAT = 30000;
|
|
@@ -421,7 +431,7 @@ export class VoiceSession extends EventTarget {
|
|
|
421
431
|
fsmActor.start();
|
|
422
432
|
const peer = { pc: null, audioEl: null, pendingCandidates: [], bufferedCandidates: [], iceTimer: null, disconnectTimer: null, failCount: 0, state: 'new', fsm: fsmActor, _stallInterval: null, remoteDescSet: false, trackEndedRestart: false };
|
|
423
433
|
this.peers.set(peerPubkey, peer);
|
|
424
|
-
const pc = new RTCPeerConnection({ iceServers: ICE_SERVERS, bundlePolicy: 'max-bundle' });
|
|
434
|
+
const pc = new RTCPeerConnection({ iceServers: ICE_SERVERS, bundlePolicy: 'max-bundle', iceCandidatePoolSize: 4, iceTransportPolicy: 'all' });
|
|
425
435
|
peer.pc = pc;
|
|
426
436
|
const isOfferer = this.auth.pubkey > peerPubkey;
|
|
427
437
|
if (isOfferer) {
|
|
@@ -443,9 +453,19 @@ export class VoiceSession extends EventTarget {
|
|
|
443
453
|
};
|
|
444
454
|
pc.onicecandidate = (ev) => {
|
|
445
455
|
if (!ev.candidate) return;
|
|
446
|
-
|
|
456
|
+
const cand = ev.candidate.toJSON();
|
|
457
|
+
// Direct-path candidates (host = LAN, srflx = STUN-mapped, prflx = peer-reflexive) get published
|
|
458
|
+
// immediately so the remote side can begin probing direct pairs without waiting for the full
|
|
459
|
+
// gathering cycle. Relay candidates batch — they're a fallback and don't need millisecond latency.
|
|
460
|
+
const cstr = cand.candidate || '';
|
|
461
|
+
const isDirect = cstr.includes(' typ host') || cstr.includes(' typ srflx') || cstr.includes(' typ prflx');
|
|
462
|
+
if (isDirect) {
|
|
463
|
+
this._publishSignal(peerPubkey, 'ice', [cand]);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
peer.pendingCandidates.push(cand);
|
|
447
467
|
if (peer.iceTimer) clearTimeout(peer.iceTimer);
|
|
448
|
-
peer.iceTimer = setTimeout(() => { if (peer.pendingCandidates.length) { this._publishSignal(peerPubkey, 'ice', peer.pendingCandidates.splice(0)); peer.iceTimer = null; } },
|
|
468
|
+
peer.iceTimer = setTimeout(() => { if (peer.pendingCandidates.length) { this._publishSignal(peerPubkey, 'ice', peer.pendingCandidates.splice(0)); peer.iceTimer = null; } }, 100);
|
|
449
469
|
};
|
|
450
470
|
pc.onicegatheringstatechange = () => { if (pc.iceGatheringState === 'complete') { if (peer.iceTimer) { clearTimeout(peer.iceTimer); peer.iceTimer = null; } if (peer.pendingCandidates.length) this._publishSignal(peerPubkey, 'ice', peer.pendingCandidates.splice(0)); } };
|
|
451
471
|
pc.onconnectionstatechange = () => {
|
|
@@ -468,8 +488,26 @@ export class VoiceSession extends EventTarget {
|
|
|
468
488
|
|
|
469
489
|
_applyAudioHints(pc) {
|
|
470
490
|
try {
|
|
471
|
-
pc.getSenders().forEach(s => {
|
|
491
|
+
pc.getSenders().forEach(s => {
|
|
492
|
+
if (!s.track || s.track.kind !== 'audio') return;
|
|
493
|
+
const p = s.getParameters(); if (!p.encodings?.length) return;
|
|
494
|
+
p.encodings[0].networkPriority = 'high';
|
|
495
|
+
p.encodings[0].maxBitrate = 48000;
|
|
496
|
+
// Opus inband-FEC + DTX: lower bitrate use under silence, better recovery on packet loss.
|
|
497
|
+
// Reduces the chance of fallback to TURN-relay on flaky direct UDP paths.
|
|
498
|
+
p.encodings[0].priority = 'high';
|
|
499
|
+
s.setParameters(p).catch(() => {});
|
|
500
|
+
});
|
|
472
501
|
pc.getReceivers().forEach(r => { if (r.track?.kind !== 'audio') return; try { r.playoutDelayHint = 0.02; } catch {} });
|
|
502
|
+
// Munge SDP-level Opus params via setCodecPreferences when the transceiver supports it.
|
|
503
|
+
pc.getTransceivers().forEach(t => {
|
|
504
|
+
if (t.receiver?.track?.kind !== 'audio' && t.sender?.track?.kind !== 'audio') return;
|
|
505
|
+
if (typeof RTCRtpSender === 'undefined' || !RTCRtpSender.getCapabilities) return;
|
|
506
|
+
const caps = RTCRtpSender.getCapabilities('audio'); if (!caps?.codecs) return;
|
|
507
|
+
const opus = caps.codecs.filter(c => /opus/i.test(c.mimeType));
|
|
508
|
+
const others = caps.codecs.filter(c => !/opus/i.test(c.mimeType));
|
|
509
|
+
try { t.setCodecPreferences([...opus, ...others]); } catch {}
|
|
510
|
+
});
|
|
473
511
|
} catch {}
|
|
474
512
|
}
|
|
475
513
|
|