wireweave 0.1.1 → 0.1.3
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/README.md +46 -35
- package/package.json +14 -4
- package/src/bans.js +45 -0
- package/src/channels.js +107 -0
- package/src/chat.js +95 -0
- package/src/fsm.js +51 -0
- package/src/index.js +12 -0
- package/src/media.js +81 -0
- package/src/message.js +22 -0
- package/src/pages.js +77 -0
- package/src/roles.js +58 -0
- package/src/servers.js +103 -0
- package/src/settings.js +72 -0
- package/src/voice.js +348 -0
- package/src/wireweave.js +89 -0
- package/src/legacy/README.md +0 -71
- package/src/legacy/index.js +0 -49
- package/src/legacy/nostr-auth.js +0 -217
- package/src/legacy/nostr-bans.js +0 -59
- package/src/legacy/nostr-channels.js +0 -145
- package/src/legacy/nostr-chat.js +0 -185
- package/src/legacy/nostr-fsm.js +0 -5
- package/src/legacy/nostr-media.js +0 -97
- package/src/legacy/nostr-message.js +0 -12
- package/src/legacy/nostr-network.js +0 -182
- package/src/legacy/nostr-pages.js +0 -152
- package/src/legacy/nostr-roles.js +0 -74
- package/src/legacy/nostr-servers.js +0 -223
- package/src/legacy/nostr-settings.js +0 -97
- package/src/legacy/nostr-state-patch.js +0 -19
- package/src/legacy/nostr-voice-camera.js +0 -63
- package/src/legacy/nostr-voice-rtc.js +0 -199
- package/src/legacy/nostr-voice-sfu.js +0 -128
- package/src/legacy/nostr-voice.js +0 -257
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
window.__voiceRetrySchedule = window.__voiceRetrySchedule || {};
|
|
2
|
-
var nostrVoiceRtc = {
|
|
3
|
-
_iceServers:[
|
|
4
|
-
{urls:'stun:stun.l.google.com:19302'},
|
|
5
|
-
{urls:'stun:stun1.l.google.com:19302'},
|
|
6
|
-
{urls:'stun:stun.cloudflare.com:3478'},
|
|
7
|
-
{urls:'stun:stun.nextcloud.com:443'},
|
|
8
|
-
{url:'turn:openrelay.metered.ca:80',urls:'turn:openrelay.metered.ca:80',username:'openrelayproject',credential:'openrelayproject'},
|
|
9
|
-
{url:'turn:openrelay.metered.ca:443',urls:'turn:openrelay.metered.ca:443',username:'openrelayproject',credential:'openrelayproject'},
|
|
10
|
-
{url:'turn:openrelay.metered.ca:443?transport=tcp',urls:'turn:openrelay.metered.ca:443?transport=tcp',username:'openrelayproject',credential:'openrelayproject'},
|
|
11
|
-
{url:'turns:openrelay.metered.ca:443',urls:'turns:openrelay.metered.ca:443',username:'openrelayproject',credential:'openrelayproject'},
|
|
12
|
-
],
|
|
13
|
-
|
|
14
|
-
_applyAudioHints(pc) {
|
|
15
|
-
try {
|
|
16
|
-
pc.getSenders().forEach(function(sender) {
|
|
17
|
-
if(!sender.track||sender.track.kind!=='audio') return;
|
|
18
|
-
var params=sender.getParameters();
|
|
19
|
-
if(!params.encodings||!params.encodings.length) return;
|
|
20
|
-
params.encodings[0].networkPriority='high'; params.encodings[0].priority='high';
|
|
21
|
-
params.encodings[0].maxBitrate=48000;
|
|
22
|
-
sender.setParameters(params).catch(function(){});
|
|
23
|
-
});
|
|
24
|
-
pc.getReceivers().forEach(function(recv) {
|
|
25
|
-
if(!recv.track||recv.track.kind!=='audio') return;
|
|
26
|
-
try { recv.playoutDelayHint=0.02; } catch(e) {}
|
|
27
|
-
});
|
|
28
|
-
} catch(e) {}
|
|
29
|
-
},
|
|
30
|
-
|
|
31
|
-
maybeConnect(peerPubkey) {
|
|
32
|
-
var nv=nostrVoice;
|
|
33
|
-
if(!peerPubkey||peerPubkey===state.nostrPubkey||nv._peers.has(peerPubkey)) return;
|
|
34
|
-
if(window.nostrBans&&state.currentServerId&&(nostrBans.isBanned(state.currentServerId,peerPubkey)||nostrBans.isTimedOut(state.currentServerId,peerPubkey))) return;
|
|
35
|
-
nostrVoiceRtc.cancelReconnect(peerPubkey);
|
|
36
|
-
var fsm=XState.createActor(nostrFsm.peerMachine);
|
|
37
|
-
fsm.subscribe(function(snap){var p=nv._peers.get(peerPubkey);if(p)p.state=snap.value;});
|
|
38
|
-
fsm.start();
|
|
39
|
-
var peer={pc:null,audioEl:null,pendingCandidates:[],bufferedCandidates:[],iceTimer:null,disconnectTimer:null,failCount:0,state:'new',fsm:fsm};
|
|
40
|
-
nv._peers.set(peerPubkey,peer);
|
|
41
|
-
var pc=new RTCPeerConnection({iceServers:nostrVoiceRtc._iceServers,bundlePolicy:'max-bundle'}); peer.pc=pc;
|
|
42
|
-
var isOfferer=state.nostrPubkey>peerPubkey;
|
|
43
|
-
if(isOfferer){
|
|
44
|
-
if(nv._localStream)
|
|
45
|
-
nv._localStream.getTracks().forEach(t=>pc.addTransceiver(t,{direction:'sendrecv',streams:[nv._localStream]}));
|
|
46
|
-
else
|
|
47
|
-
pc.addTransceiver('audio',{direction:'recvonly'});
|
|
48
|
-
}
|
|
49
|
-
pc.ontrack=function(ev){
|
|
50
|
-
if(ev.track.kind==='video'){
|
|
51
|
-
var pKey='nostr-'+peerPubkey.slice(0,12);
|
|
52
|
-
var p=nv._participants.get(pKey);
|
|
53
|
-
if(p){p.hasVideo=true;p._videoStream=ev.streams[0];}
|
|
54
|
-
var el=document.getElementById('vtile-video-'+peerPubkey.slice(0,8));
|
|
55
|
-
if(!el){el=document.createElement('video');el.id='vtile-video-'+peerPubkey.slice(0,8);el.autoplay=true;el.playsinline=true;el.style.cssText='width:100%;height:100%;object-fit:cover;border-radius:8px';var wrap=document.getElementById('vtile-wrap-'+peerPubkey.slice(0,8));if(wrap){wrap.appendChild(el);}}
|
|
56
|
-
el.srcObject=ev.streams[0];
|
|
57
|
-
nv.updateParticipants();
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
if(!peer.audioEl){peer.audioEl=new Audio();peer.audioEl.autoplay=true;peer.audioEl.muted=state.voiceDeafened;document.body.appendChild(peer.audioEl);}
|
|
61
|
-
peer.audioEl.srcObject=ev.streams[0];
|
|
62
|
-
try { ev.receiver.playoutDelayHint=0.02; } catch(e) {}
|
|
63
|
-
ev.track.onended=function(){
|
|
64
|
-
if(!peer.fsm.getSnapshot().matches('connected')) return;
|
|
65
|
-
if(window.__debug) window.__debug['rtc-track-ended-'+peerPubkey.slice(0,8)]=Date.now();
|
|
66
|
-
doIceRestart();
|
|
67
|
-
};
|
|
68
|
-
if(!peer._stallInterval) peer._stallInterval=setInterval(function(){
|
|
69
|
-
if(!peer.audioEl||!peer.audioEl.srcObject) return;
|
|
70
|
-
if(!peer.fsm.getSnapshot().matches('connected')) return;
|
|
71
|
-
if(peer.trackEndedRestart) return;
|
|
72
|
-
var allEnded=peer.audioEl.srcObject.getTracks().every(function(t){return t.readyState==='ended';});
|
|
73
|
-
if(!allEnded) return;
|
|
74
|
-
peer.trackEndedRestart=true;
|
|
75
|
-
if(window.__debug) window.__debug['rtc-stall-'+peerPubkey.slice(0,8)]=Date.now();
|
|
76
|
-
doIceRestart();
|
|
77
|
-
},5000);
|
|
78
|
-
};
|
|
79
|
-
pc.onicecandidate=function(ev){
|
|
80
|
-
if(!ev.candidate) return;
|
|
81
|
-
peer.pendingCandidates.push(ev.candidate.toJSON());
|
|
82
|
-
if(peer.iceTimer) clearTimeout(peer.iceTimer);
|
|
83
|
-
peer.iceTimer=setTimeout(function(){
|
|
84
|
-
if(peer.pendingCandidates.length){nv._publishSignal(peerPubkey,'ice',peer.pendingCandidates.splice(0));peer.iceTimer=null;}
|
|
85
|
-
},500);
|
|
86
|
-
};
|
|
87
|
-
pc.onicegatheringstatechange=function(){
|
|
88
|
-
if(pc.iceGatheringState==='complete'){
|
|
89
|
-
if(peer.iceTimer){clearTimeout(peer.iceTimer);peer.iceTimer=null;}
|
|
90
|
-
if(peer.pendingCandidates.length){nv._publishSignal(peerPubkey,'ice',peer.pendingCandidates.splice(0));}
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
var doIceRestart=function(){
|
|
94
|
-
if(peer.disconnectTimer){clearTimeout(peer.disconnectTimer);peer.disconnectTimer=null;}
|
|
95
|
-
peer.failCount++;
|
|
96
|
-
if(peer.failCount<=1&&state.nostrPubkey>peerPubkey){
|
|
97
|
-
fsm.send({type:'restart'}); pc.restartIce();
|
|
98
|
-
pc.createOffer({iceRestart:true}).then(o=>pc.setLocalDescription(o).then(()=>nv._publishSignal(peerPubkey,'offer',o))).catch(()=>nv._closePeer(peerPubkey));
|
|
99
|
-
} else { nv._closePeer(peerPubkey); nostrVoiceRtc.scheduleReconnect(peerPubkey, peer.failCount); }
|
|
100
|
-
};
|
|
101
|
-
pc.onconnectionstatechange=function(){
|
|
102
|
-
if(pc.connectionState==='connected'){
|
|
103
|
-
peer.failCount=0; nostrVoiceRtc.cancelReconnect(peerPubkey);
|
|
104
|
-
if(fsm.getSnapshot().can({type:'recv_answer'})) fsm.send({type:'recv_answer'});
|
|
105
|
-
if(peer.disconnectTimer){clearTimeout(peer.disconnectTimer);peer.disconnectTimer=null;}
|
|
106
|
-
nostrVoiceRtc._applyAudioHints(pc);
|
|
107
|
-
var pKey='nostr-'+peerPubkey.slice(0,12);
|
|
108
|
-
var p=nv._participants.get(pKey);
|
|
109
|
-
if(!p){nv._participants.set(pKey,{identity:pKey,isSpeaking:false,isMuted:false,isLocal:false,hasVideo:false,connectionQuality:'good',_peerPubkey:peerPubkey});}
|
|
110
|
-
else{p.connectionQuality='good';p._peerPubkey=peerPubkey;}
|
|
111
|
-
nv.updateParticipants();
|
|
112
|
-
}
|
|
113
|
-
if(pc.connectionState==='disconnected'){
|
|
114
|
-
fsm.send({type:'disconnect'}); peer.disconnectTimer=setTimeout(doIceRestart,8000);
|
|
115
|
-
}
|
|
116
|
-
if(pc.connectionState==='failed') doIceRestart();
|
|
117
|
-
if(pc.connectionState==='closed'){nv._closePeer(peerPubkey);if(window.nostrVoiceSfu&&nostrVoiceSfu._hub===peerPubkey){nostrVoiceSfu._dissolve();setTimeout(nostrVoiceSfu._maybeElect,500);}}
|
|
118
|
-
};
|
|
119
|
-
if(isOfferer){
|
|
120
|
-
fsm.send({type:'offer'});
|
|
121
|
-
pc.createOffer().then(o=>pc.setLocalDescription(o).then(()=>nv._publishSignal(peerPubkey,'offer',o)))
|
|
122
|
-
.catch(e=>console.warn('[nostr-voice] offer:',e));
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
|
|
126
|
-
handleSignal(event) {
|
|
127
|
-
var nv=nostrVoice;
|
|
128
|
-
var from=event.pubkey; if(from===state.nostrPubkey) return;
|
|
129
|
-
var data; try{data=JSON.parse(event.content);}catch(e){return;} if(!data?.type) return;
|
|
130
|
-
if(!nv._peers.has(from)) nostrVoiceRtc.maybeConnect(from);
|
|
131
|
-
var peer=nv._peers.get(from); if(!peer) return;
|
|
132
|
-
var pc=peer.pc; var fsm=peer.fsm;
|
|
133
|
-
var addCands=function(cands){cands.forEach(c=>pc.addIceCandidate(new RTCIceCandidate(c)).catch(e=>console.error('[nostr-voice] ice:',e)));};
|
|
134
|
-
var drainBuf=function(){addCands(peer.bufferedCandidates);peer.bufferedCandidates=[];};
|
|
135
|
-
var doAnswer=function(){if(fsm.getSnapshot().can({type:'recv_offer'}))fsm.send({type:'recv_offer'});return pc.setRemoteDescription(new RTCSessionDescription(data.data)).then(function(){peer.remoteDescSet=true;drainBuf();var hasAudioTx=pc.getTransceivers().some(function(t){return t.receiver.track&&t.receiver.track.kind==='audio';});if(!hasAudioTx)pc.addTransceiver('audio',{direction:nv._localStream?'sendrecv':'recvonly'});if(nv._localStream){var hasSender=pc.getSenders().some(function(s){return s.track&&s.track.kind==='audio';});if(!hasSender)nv._localStream.getTracks().forEach(function(t){pc.addTrack(t,nv._localStream);});}return pc.createAnswer();}).then(function(a){return pc.setLocalDescription(a).then(function(){fsm.send({type:'sent_answer'});nv._publishSignal(from,'answer',a);});}).catch(function(e){console.warn('[nostr-voice] answer:',e);});};
|
|
136
|
-
if(data.type==='offer'){
|
|
137
|
-
var polite=state.nostrPubkey<from;
|
|
138
|
-
var collision=pc.signalingState!=='stable';
|
|
139
|
-
if(collision&&!polite) return;
|
|
140
|
-
if(collision&&polite){pc.setLocalDescription({type:'rollback'}).then(doAnswer).catch(function(e){console.warn('[nostr-voice] rollback:',e);});return;}
|
|
141
|
-
doAnswer();
|
|
142
|
-
} else if(data.type==='answer'&&pc.signalingState==='have-local-offer'){
|
|
143
|
-
pc.setRemoteDescription(new RTCSessionDescription(data.data)).then(()=>{peer.remoteDescSet=true;drainBuf();}).catch(e=>console.warn('[nostr-voice] set-answer:',e));
|
|
144
|
-
} else if(data.type==='ice'){
|
|
145
|
-
var cands=Array.isArray(data.data)?data.data:[data.data];
|
|
146
|
-
if(peer.remoteDescSet) addCands(cands);
|
|
147
|
-
else peer.bufferedCandidates.push(...cands);
|
|
148
|
-
}
|
|
149
|
-
},
|
|
150
|
-
|
|
151
|
-
subscribe(roomId,pubkey) {
|
|
152
|
-
nostrNet.subscribe('voice-signals-'+roomId,
|
|
153
|
-
[{kinds:[30078],'#p':[pubkey],'#r':[roomId]}],
|
|
154
|
-
nostrVoiceRtc.handleSignal,()=>{});
|
|
155
|
-
},
|
|
156
|
-
|
|
157
|
-
async publish(toPubkey,type,data,roomId) {
|
|
158
|
-
if(!state.nostrPubkey||!roomId) return;
|
|
159
|
-
var d='zellous-rtc:'+roomId+':'+state.nostrPubkey+':'+toPubkey+':'+type+':'+(type==='ice'?Date.now():'sdp');
|
|
160
|
-
nostrNet.publish(await auth.sign({kind:30078,created_at:Math.floor(Date.now()/1000),
|
|
161
|
-
tags:[['d',d],['p',toPubkey],['r',roomId]],
|
|
162
|
-
content:JSON.stringify({type,data})}));
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
window.__zellous.voiceRtc = nostrVoiceRtc;
|
|
166
|
-
window.nostrVoiceRtc = nostrVoiceRtc;
|
|
167
|
-
|
|
168
|
-
nostrVoiceRtc.cancelReconnect = function(pk) {
|
|
169
|
-
var e=window.__voiceRetrySchedule[pk]; if(e){clearTimeout(e.timer);delete window.__voiceRetrySchedule[pk];}
|
|
170
|
-
};
|
|
171
|
-
nostrVoiceRtc.scheduleReconnect = function(pk, attempt) {
|
|
172
|
-
var a=attempt||0; if(a>=6) return;
|
|
173
|
-
nostrVoiceRtc.cancelReconnect(pk);
|
|
174
|
-
var timer=setTimeout(function(){
|
|
175
|
-
delete window.__voiceRetrySchedule[pk];
|
|
176
|
-
var nv=nostrVoice; if(!nv._peers.has(pk)&&nv._roomId) nostrVoiceRtc.maybeConnect(pk);
|
|
177
|
-
}, Math.min(Math.pow(2,a)*2000,30000));
|
|
178
|
-
window.__voiceRetrySchedule[pk]={attempt:a,timer:timer};
|
|
179
|
-
if(window.__debug&&window.__debug.voice) window.__debug.voice.retrySchedule=window.__voiceRetrySchedule;
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
var _healDebounce=null,_pagehidden=false,_BAD={disconnected:1,failed:1,closed:1};
|
|
183
|
-
function _healPeers(){
|
|
184
|
-
if(_healDebounce){clearTimeout(_healDebounce);_healDebounce=null;}
|
|
185
|
-
_healDebounce=setTimeout(function(){
|
|
186
|
-
_healDebounce=null; var nv=nostrVoice; if(!nv._roomId) return;
|
|
187
|
-
nv._peers.forEach(function(peer,pk){if(_BAD[peer.pc&&peer.pc.connectionState]){nv._closePeer(pk);nostrVoiceRtc.scheduleReconnect(pk,0);}});
|
|
188
|
-
},500);
|
|
189
|
-
}
|
|
190
|
-
document.addEventListener('visibilitychange',function(){
|
|
191
|
-
if(document.visibilityState==='hidden'){_pagehidden=true;return;}
|
|
192
|
-
if(_pagehidden){_pagehidden=false;_healPeers();}
|
|
193
|
-
});
|
|
194
|
-
window.addEventListener('online',function(){
|
|
195
|
-
var nv=nostrVoice; if(!nv._roomId) return;
|
|
196
|
-
if(window.nostrNet&&nostrNet.reconnectAll) nostrNet.reconnectAll();
|
|
197
|
-
_healPeers();
|
|
198
|
-
});
|
|
199
|
-
window.addEventListener('pageshow',function(ev){if(ev.persisted||_pagehidden){_pagehidden=false;_healPeers();}});
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
var nostrVoiceSfu = {
|
|
2
|
-
_actor: null,
|
|
3
|
-
_hub: null,
|
|
4
|
-
_hubLostAt: null,
|
|
5
|
-
_rttMatrix: new Map(),
|
|
6
|
-
_electionTimer: null,
|
|
7
|
-
_statsInterval: null,
|
|
8
|
-
|
|
9
|
-
_initActor() {
|
|
10
|
-
var machine = XState.createMachine({initial:'mesh',states:{mesh:{on:{elect:'electing'}},electing:{on:{elected:'star',dissolve:'mesh'}},star:{on:{dissolve:'mesh'}}}});
|
|
11
|
-
nostrVoiceSfu._actor = XState.createActor(machine);
|
|
12
|
-
nostrVoiceSfu._actor.start();
|
|
13
|
-
},
|
|
14
|
-
|
|
15
|
-
start() {
|
|
16
|
-
nostrVoiceSfu._initActor();
|
|
17
|
-
nostrVoiceSfu._stopStats();
|
|
18
|
-
nostrVoiceSfu._statsInterval = setInterval(nostrVoiceSfu._poll, 5000);
|
|
19
|
-
},
|
|
20
|
-
|
|
21
|
-
stop() {
|
|
22
|
-
nostrVoiceSfu._stopStats();
|
|
23
|
-
if(nostrVoiceSfu._actor) nostrVoiceSfu._actor.send({type:'dissolve'});
|
|
24
|
-
nostrVoiceSfu._hub = null;
|
|
25
|
-
nostrVoiceSfu._rttMatrix.clear();
|
|
26
|
-
},
|
|
27
|
-
|
|
28
|
-
_stopStats() {
|
|
29
|
-
if(nostrVoiceSfu._statsInterval) { clearInterval(nostrVoiceSfu._statsInterval); nostrVoiceSfu._statsInterval = null; }
|
|
30
|
-
if(nostrVoiceSfu._electionTimer) { clearTimeout(nostrVoiceSfu._electionTimer); nostrVoiceSfu._electionTimer = null; }
|
|
31
|
-
},
|
|
32
|
-
|
|
33
|
-
async _poll() {
|
|
34
|
-
var nv = nostrVoice;
|
|
35
|
-
var scores = {};
|
|
36
|
-
var tasks = [];
|
|
37
|
-
nv._peers.forEach(function(peer, pk) {
|
|
38
|
-
if(!peer.pc || peer.pc.connectionState !== 'connected') return;
|
|
39
|
-
tasks.push(peer.pc.getStats().then(function(stats) {
|
|
40
|
-
stats.forEach(function(r) {
|
|
41
|
-
if(r.type === 'candidate-pair' && r.state === 'succeeded' && r.currentRoundTripTime != null) {
|
|
42
|
-
scores[pk] = Math.round(r.currentRoundTripTime * 1000);
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
}).catch(function(){}));
|
|
46
|
-
});
|
|
47
|
-
await Promise.all(tasks);
|
|
48
|
-
nostrVoiceSfu._rttMatrix.set(state.nostrPubkey, scores);
|
|
49
|
-
nostrVoiceSfu._maybeElect();
|
|
50
|
-
},
|
|
51
|
-
|
|
52
|
-
onPresenceRtt(pubkey, rttScores) {
|
|
53
|
-
if(rttScores && typeof rttScores === 'object') nostrVoiceSfu._rttMatrix.set(pubkey, rttScores);
|
|
54
|
-
nostrVoiceSfu._maybeElect();
|
|
55
|
-
},
|
|
56
|
-
|
|
57
|
-
_maybeElect() {
|
|
58
|
-
var nv = nostrVoice;
|
|
59
|
-
var peerCount = nv._peers.size;
|
|
60
|
-
if(peerCount < 3) { if(nostrVoiceSfu._actor && nostrVoiceSfu._actor.getSnapshot().value !== 'mesh') nostrVoiceSfu._dissolve(); return; }
|
|
61
|
-
if(nostrVoiceSfu._electionTimer) return;
|
|
62
|
-
nostrVoiceSfu._electionTimer = setTimeout(function() {
|
|
63
|
-
nostrVoiceSfu._electionTimer = null;
|
|
64
|
-
nostrVoiceSfu._elect();
|
|
65
|
-
}, 2000);
|
|
66
|
-
},
|
|
67
|
-
|
|
68
|
-
_elect() {
|
|
69
|
-
var nv = nostrVoice;
|
|
70
|
-
var allPeers = [state.nostrPubkey];
|
|
71
|
-
nv._peers.forEach(function(_, pk) { allPeers.push(pk); });
|
|
72
|
-
var best = null, bestAvg = Infinity;
|
|
73
|
-
allPeers.forEach(function(pk) {
|
|
74
|
-
var scores = nostrVoiceSfu._rttMatrix.get(pk);
|
|
75
|
-
if(!scores) return;
|
|
76
|
-
var vals = Object.values(scores).filter(function(v) { return typeof v === 'number'; });
|
|
77
|
-
if(!vals.length) return;
|
|
78
|
-
var avg = vals.reduce(function(a, b) { return a + b; }, 0) / vals.length;
|
|
79
|
-
if(avg < bestAvg || (avg === bestAvg && pk > best)) { bestAvg = avg; best = pk; }
|
|
80
|
-
});
|
|
81
|
-
if(!best) return;
|
|
82
|
-
if(best !== nostrVoiceSfu._hub) {
|
|
83
|
-
nostrVoiceSfu._hub = best;
|
|
84
|
-
if(nostrVoiceSfu._actor) nostrVoiceSfu._actor.send({type:'elected'});
|
|
85
|
-
if(best === state.nostrPubkey) nostrVoiceSfu._becomeHub();
|
|
86
|
-
else nostrVoiceSfu._routeToHub(best);
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
|
|
90
|
-
_becomeHub() {
|
|
91
|
-
var nv = nostrVoice;
|
|
92
|
-
nv._peers.forEach(function(srcPeer, srcPk) {
|
|
93
|
-
if(!srcPeer.pc || !srcPeer.pc.getReceivers) return;
|
|
94
|
-
srcPeer.pc.getReceivers().forEach(function(recv) {
|
|
95
|
-
if(!recv.track || recv.track.kind !== 'audio') return;
|
|
96
|
-
nv._peers.forEach(function(dstPeer, dstPk) {
|
|
97
|
-
if(dstPk === srcPk) return;
|
|
98
|
-
var sender = dstPeer.pc && dstPeer.pc.getSenders().find(function(s) { return s.track && s.track.kind === 'audio'; });
|
|
99
|
-
if(sender) sender.replaceTrack(recv.track).catch(function(){});
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
},
|
|
104
|
-
|
|
105
|
-
_routeToHub(hubPk) {
|
|
106
|
-
var nv = nostrVoice;
|
|
107
|
-
nv._peers.forEach(function(peer, pk) {
|
|
108
|
-
if(pk === hubPk) return;
|
|
109
|
-
nostrVoice._closePeer(pk);
|
|
110
|
-
nv._peers.delete(pk);
|
|
111
|
-
});
|
|
112
|
-
if(!nv._peers.has(hubPk)) nostrVoiceRtc.maybeConnect(hubPk);
|
|
113
|
-
},
|
|
114
|
-
|
|
115
|
-
_dissolve() {
|
|
116
|
-
if(nostrVoiceSfu._actor) nostrVoiceSfu._actor.send({type:'dissolve'});
|
|
117
|
-
nostrVoiceSfu._hubLostAt = Date.now();
|
|
118
|
-
nostrVoiceSfu._hub = null;
|
|
119
|
-
},
|
|
120
|
-
|
|
121
|
-
get __debug() {
|
|
122
|
-
var scores = {};
|
|
123
|
-
nostrVoiceSfu._rttMatrix.forEach(function(v, k) { scores[k.slice(0,12)] = v; });
|
|
124
|
-
return {mode: nostrVoiceSfu._actor ? nostrVoiceSfu._actor.getSnapshot().value : 'mesh', hub: nostrVoiceSfu._hub ? nostrVoiceSfu._hub.slice(0,12) : null, hubLostAt: nostrVoiceSfu._hubLostAt, rttMatrix: scores};
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
window.__zellous.voiceSfu = nostrVoiceSfu;
|
|
128
|
-
window.nostrVoiceSfu = nostrVoiceSfu;
|
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
var nostrVoice = {
|
|
2
|
-
_channelName: '', _roomId: '', _presenceInterval: null,
|
|
3
|
-
_participants: new Map(), _localStream: null, _peers: new Map(), _joinTs: 0,
|
|
4
|
-
_fsm: null,
|
|
5
|
-
|
|
6
|
-
_initFSM: function() {
|
|
7
|
-
var actor = XState.createActor(nostrFsm.voiceMachine);
|
|
8
|
-
actor.subscribe(function(snap) {
|
|
9
|
-
var next = snap.value;
|
|
10
|
-
state.voiceConnectionState = next === 'connected' ? 'connected' : next === 'idle' ? 'disconnected' : next;
|
|
11
|
-
state.voiceConnected = next === 'connected';
|
|
12
|
-
});
|
|
13
|
-
actor.start();
|
|
14
|
-
nostrVoice._fsm = actor;
|
|
15
|
-
},
|
|
16
|
-
|
|
17
|
-
_peerFSM: function(pubkey) {
|
|
18
|
-
var actor = XState.createActor(nostrFsm.peerMachine);
|
|
19
|
-
actor.subscribe(function(snap) {
|
|
20
|
-
var peer = nostrVoice._peers.get(pubkey);
|
|
21
|
-
if (peer) peer.state = snap.value;
|
|
22
|
-
});
|
|
23
|
-
actor.start();
|
|
24
|
-
return actor;
|
|
25
|
-
},
|
|
26
|
-
|
|
27
|
-
async _deriveRoomId(ch) {
|
|
28
|
-
var h = await crypto.subtle.digest('SHA-256', new TextEncoder().encode((state.currentServerId||'default')+':voice:'+ch));
|
|
29
|
-
return 'zellous'+Array.from(new Uint8Array(h)).map(b=>b.toString(16).padStart(2,'0')).join('').slice(0,16);
|
|
30
|
-
},
|
|
31
|
-
|
|
32
|
-
_displayName() { return state.nostrProfile?.name||(state.nostrPubkey?auth.npubShort(state.nostrPubkey):'Guest'); },
|
|
33
|
-
|
|
34
|
-
async _applyRnnoise(rawStream) {
|
|
35
|
-
try {
|
|
36
|
-
const worketUrl = new URL('../../vendor/rnnoise-worklet.js', document.baseURI).href;
|
|
37
|
-
if (!nostrVoice._rnnoiseCtx) {
|
|
38
|
-
nostrVoice._rnnoiseCtx = new AudioContext({ sampleRate: 48000 });
|
|
39
|
-
await nostrVoice._rnnoiseCtx.audioWorklet.addModule(worketUrl);
|
|
40
|
-
}
|
|
41
|
-
const ctx = nostrVoice._rnnoiseCtx;
|
|
42
|
-
if (ctx.state === 'suspended') await ctx.resume();
|
|
43
|
-
// tear down previous graph nodes if reconnecting
|
|
44
|
-
if (nostrVoice._rnnoiseSource) { try { nostrVoice._rnnoiseSource.disconnect(); } catch(e){} }
|
|
45
|
-
if (nostrVoice._rnnoiseNode) { try { nostrVoice._rnnoiseNode.disconnect(); } catch(e){} }
|
|
46
|
-
const source = ctx.createMediaStreamSource(rawStream);
|
|
47
|
-
const denoiseNode = new AudioWorkletNode(ctx, 'NoiseSuppressorWorklet');
|
|
48
|
-
const dest = ctx.createMediaStreamDestination();
|
|
49
|
-
source.connect(denoiseNode);
|
|
50
|
-
denoiseNode.connect(dest);
|
|
51
|
-
nostrVoice._rnnoiseSource = source;
|
|
52
|
-
nostrVoice._rnnoiseNode = denoiseNode;
|
|
53
|
-
nostrVoice._rnnoiseDest = dest;
|
|
54
|
-
return dest.stream;
|
|
55
|
-
} catch(e) {
|
|
56
|
-
console.warn('[nostr-voice] rnnoise unavailable, using raw stream:', e.message);
|
|
57
|
-
return rawStream;
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
|
|
61
|
-
_rnnoiseEnabled() {
|
|
62
|
-
const stored = localStorage.getItem('zellous_rnnoise');
|
|
63
|
-
return stored === null ? true : stored === 'true';
|
|
64
|
-
},
|
|
65
|
-
|
|
66
|
-
async connect(channelName) {
|
|
67
|
-
if (!nostrVoice._fsm) nostrVoice._initFSM();
|
|
68
|
-
if (!nostrVoice._fsm.getSnapshot().can({type:'connect'})) { await nostrVoice.disconnect(); }
|
|
69
|
-
nostrVoice._fsm.send({type:'connect'});
|
|
70
|
-
nostrVoice._channelName = channelName;
|
|
71
|
-
nostrVoice._joinTs = Math.floor(Date.now()/1000);
|
|
72
|
-
try {
|
|
73
|
-
nostrVoice._roomId = await nostrVoice._deriveRoomId(channelName);
|
|
74
|
-
let rawStream;
|
|
75
|
-
if (state.mediaStream && state.mediaStream.getAudioTracks().some(t=>t.readyState==='live')) {
|
|
76
|
-
rawStream = state.mediaStream;
|
|
77
|
-
} else {
|
|
78
|
-
rawStream = await navigator.mediaDevices.getUserMedia({audio:{echoCancellation:true,noiseSuppression:true,autoGainControl:true}});
|
|
79
|
-
state.mediaStream = rawStream;
|
|
80
|
-
}
|
|
81
|
-
if (nostrVoice._rnnoiseEnabled()) {
|
|
82
|
-
nostrVoice._localStream = await nostrVoice._applyRnnoise(rawStream);
|
|
83
|
-
} else {
|
|
84
|
-
nostrVoice._localStream = rawStream;
|
|
85
|
-
}
|
|
86
|
-
if (state.cameraEnabled && !nostrVoice._cameraStream) {
|
|
87
|
-
try {
|
|
88
|
-
const [w, h] = state.webcamResolution.split('x').map(Number);
|
|
89
|
-
nostrVoice._cameraStream = await navigator.mediaDevices.getUserMedia({video:{width:w,height:h,frameRate:state.webcamFps,facingMode:'user'}});
|
|
90
|
-
} catch(e) {
|
|
91
|
-
console.warn('[nostr-voice] camera access denied:', e.message);
|
|
92
|
-
state.cameraEnabled = false;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
nostrVoice._participants.clear();
|
|
96
|
-
nostrVoice._participants.set('local',{identity:nostrVoice._displayName(),isSpeaking:false,isMuted:false,isLocal:true,hasVideo:!!nostrVoice._cameraStream,connectionQuality:'good'});
|
|
97
|
-
nostrVoice._fsm.send({type:'connected'});
|
|
98
|
-
state.voiceConnectionQuality='good'; state.voiceChannelName=channelName;
|
|
99
|
-
state.voiceReconnectAttempts=0; state.dataChannelAvailable=false;
|
|
100
|
-
nostrVoiceRtc.subscribe(nostrVoice._roomId,state.nostrPubkey);
|
|
101
|
-
if(window.nostrBans) nostrBans.subscribe(state.currentServerId||'');
|
|
102
|
-
nostrVoice._subscribePresence();
|
|
103
|
-
nostrVoice._publishPresence('join'); nostrVoice._startHeartbeat(); nostrVoice.updateParticipants();
|
|
104
|
-
if(window.serverSettings) serverSettings.applyToEncoder();
|
|
105
|
-
if(window.nostrVoiceSfu) nostrVoiceSfu.start();
|
|
106
|
-
if(ui.voicePanel) ui.voicePanel.classList.add('visible');
|
|
107
|
-
if(ui.voicePanelChannel) ui.voicePanelChannel.textContent=channelName;
|
|
108
|
-
message.add('Voice connected');
|
|
109
|
-
} catch(e) {
|
|
110
|
-
console.warn('[nostr-voice] connect failed:',e);
|
|
111
|
-
nostrVoice._fsm.send({type:'fail'});
|
|
112
|
-
message.add('Voice connection failed: '+e.message);
|
|
113
|
-
}
|
|
114
|
-
},
|
|
115
|
-
|
|
116
|
-
async disconnect() {
|
|
117
|
-
if (!nostrVoice._fsm || nostrVoice._fsm.getSnapshot().matches('idle')) return;
|
|
118
|
-
if (!nostrVoice._fsm.getSnapshot().can({type:'disconnect'})) return;
|
|
119
|
-
nostrVoice._fsm.send({type:'disconnect'});
|
|
120
|
-
nostrVoice._publishPresence('leave'); nostrVoice._stopHeartbeat();
|
|
121
|
-
if(window.nostrVoiceSfu) nostrVoiceSfu.stop();
|
|
122
|
-
nostrVoice._peers.forEach((_,pk)=>nostrVoice._closePeer(pk)); nostrVoice._peers.clear();
|
|
123
|
-
if(nostrVoice._cameraStream){nostrVoice._cameraStream.getTracks().forEach(function(t){t.stop();});nostrVoice._cameraStream=null;}
|
|
124
|
-
state.cameraEnabled=false;
|
|
125
|
-
if(nostrVoice._localStream&&nostrVoice._localStream!==state.mediaStream){nostrVoice._localStream.getTracks().forEach(t=>t.stop());}
|
|
126
|
-
nostrVoice._localStream=null;
|
|
127
|
-
// tear down rnnoise graph (keep ctx alive for reuse, just disconnect nodes)
|
|
128
|
-
if(nostrVoice._rnnoiseSource){try{nostrVoice._rnnoiseSource.disconnect();}catch(e){} nostrVoice._rnnoiseSource=null;}
|
|
129
|
-
if(nostrVoice._rnnoiseNode){try{nostrVoice._rnnoiseNode.disconnect();}catch(e){} nostrVoice._rnnoiseNode=null;}
|
|
130
|
-
if(nostrVoice._roomId){nostrNet.unsubscribe('voice-presence-'+nostrVoice._roomId);nostrNet.unsubscribe('voice-signals-'+nostrVoice._roomId);}
|
|
131
|
-
nostrVoice._participants.clear(); nostrVoice._roomId=''; nostrVoice._channelName='';
|
|
132
|
-
state.voiceConnectionQuality='unknown'; state.voiceChannelName='';
|
|
133
|
-
state.voiceParticipants=[]; state.voiceReconnectAttempts=0;
|
|
134
|
-
state.micMuted=false; state.voiceDeafened=false; state.activeSpeakers=new Set();
|
|
135
|
-
if(ui.voicePanel) ui.voicePanel.classList.remove('visible');
|
|
136
|
-
nostrVoice._fsm.send({type:'done'});
|
|
137
|
-
nostrVoice.updateParticipants();
|
|
138
|
-
},
|
|
139
|
-
|
|
140
|
-
_closePeer(pubkey) {
|
|
141
|
-
var peer=nostrVoice._peers.get(pubkey); if(!peer) return;
|
|
142
|
-
if(peer.iceTimer) clearTimeout(peer.iceTimer);
|
|
143
|
-
if(peer.disconnectTimer) clearTimeout(peer.disconnectTimer);
|
|
144
|
-
if(peer._stallInterval) clearInterval(peer._stallInterval);
|
|
145
|
-
try{peer.pc?.close();}catch(e){}
|
|
146
|
-
if(peer.audioEl){peer.audioEl.srcObject=null;peer.audioEl.remove();}
|
|
147
|
-
nostrVoice._peers.delete(pubkey);
|
|
148
|
-
},
|
|
149
|
-
|
|
150
|
-
toggleMic() {
|
|
151
|
-
state.micMuted=!state.micMuted;
|
|
152
|
-
// mute the raw mic stream (affects both direct and rnnoise-processed path)
|
|
153
|
-
const rawStream = state.mediaStream;
|
|
154
|
-
if(rawStream) rawStream.getAudioTracks().forEach(t=>{t.enabled=!state.micMuted;});
|
|
155
|
-
// also mute processed stream output tracks if rnnoise is active
|
|
156
|
-
if(nostrVoice._localStream && nostrVoice._localStream !== rawStream) nostrVoice._localStream.getAudioTracks().forEach(t=>{t.enabled=!state.micMuted;});
|
|
157
|
-
var local=nostrVoice._participants.get('local'); if(local) local.isMuted=state.micMuted;
|
|
158
|
-
nostrVoice.updateParticipants();
|
|
159
|
-
document.getElementById('micToggleBtn')?.classList.toggle('muted',state.micMuted);
|
|
160
|
-
document.getElementById('voiceMicBtn')?.classList.toggle('active',!state.micMuted);
|
|
161
|
-
},
|
|
162
|
-
|
|
163
|
-
toggleDeafen() {
|
|
164
|
-
state.voiceDeafened=!state.voiceDeafened;
|
|
165
|
-
nostrVoice._peers.forEach(peer=>{if(peer.audioEl) peer.audioEl.muted=state.voiceDeafened;});
|
|
166
|
-
document.getElementById('deafenToggleBtn')?.classList.toggle('muted',state.voiceDeafened);
|
|
167
|
-
document.getElementById('voiceDeafenBtn')?.classList.toggle('active',state.voiceDeafened);
|
|
168
|
-
nostrVoice.updateParticipants();
|
|
169
|
-
},
|
|
170
|
-
|
|
171
|
-
toggleCamera() { return nostrVoiceCamera.toggle(); },
|
|
172
|
-
|
|
173
|
-
_publishSignal(toPubkey,type,data) { return nostrVoiceRtc.publish(toPubkey,type,data,nostrVoice._roomId); },
|
|
174
|
-
_maybeConnect(pk) { nostrVoiceRtc.maybeConnect(pk); },
|
|
175
|
-
_subscribeSignals() { nostrVoiceRtc.subscribe(nostrVoice._roomId,state.nostrPubkey); },
|
|
176
|
-
|
|
177
|
-
updateParticipants() {
|
|
178
|
-
var list=[]; nostrVoice._participants.forEach(p=>list.push(p)); state.voiceParticipants=list;
|
|
179
|
-
if(window.uiVoice){uiVoice.renderGrid();uiVoice.renderPanel();}
|
|
180
|
-
if(window.uiChannels) uiChannels.render();
|
|
181
|
-
},
|
|
182
|
-
|
|
183
|
-
_rttScores() {
|
|
184
|
-
var scores = {};
|
|
185
|
-
if(window.nostrVoiceSfu) {
|
|
186
|
-
var myScores = nostrVoiceSfu._rttMatrix.get(state.nostrPubkey);
|
|
187
|
-
if(myScores) Object.assign(scores, myScores);
|
|
188
|
-
}
|
|
189
|
-
return scores;
|
|
190
|
-
},
|
|
191
|
-
|
|
192
|
-
async _publishPresence(action) {
|
|
193
|
-
if(!auth.isLoggedIn()||!nostrVoice._roomId) return;
|
|
194
|
-
var rttScores = action === 'heartbeat' ? nostrVoice._rttScores() : {};
|
|
195
|
-
nostrNet.publish(await auth.sign({kind:30078,created_at:Math.floor(Date.now()/1000),
|
|
196
|
-
tags:[['d','zellous-voice:'+nostrVoice._roomId],['action',action],['channel',nostrVoice._channelName],['server',state.currentServerId||'']],
|
|
197
|
-
content:JSON.stringify({action,name:nostrVoice._displayName(),channel:nostrVoice._channelName,ts:Date.now(),rttScores})}));
|
|
198
|
-
},
|
|
199
|
-
|
|
200
|
-
_startHeartbeat() {
|
|
201
|
-
nostrVoice._stopHeartbeat();
|
|
202
|
-
nostrVoice._presenceInterval=setInterval(()=>{if(nostrVoice._fsm?.getSnapshot().matches('connected')) nostrVoice._publishPresence('heartbeat');},30000);
|
|
203
|
-
},
|
|
204
|
-
|
|
205
|
-
_stopHeartbeat() { if(nostrVoice._presenceInterval){clearInterval(nostrVoice._presenceInterval);nostrVoice._presenceInterval=null;} },
|
|
206
|
-
|
|
207
|
-
_subscribePresence() {
|
|
208
|
-
if(!nostrVoice._roomId) return;
|
|
209
|
-
nostrNet.subscribe('voice-presence-'+nostrVoice._roomId,
|
|
210
|
-
[{kinds:[30078],'#d':['zellous-voice:'+nostrVoice._roomId]}],
|
|
211
|
-
function(event) {
|
|
212
|
-
if(event.pubkey===state.nostrPubkey) return;
|
|
213
|
-
try {
|
|
214
|
-
var data=JSON.parse(event.content);
|
|
215
|
-
if(Date.now()-(data.ts||0)>300000) return;
|
|
216
|
-
var shortId='nostr-'+event.pubkey.slice(0,12);
|
|
217
|
-
if(data.rttScores && window.nostrVoiceSfu) nostrVoiceSfu.onPresenceRtt(event.pubkey, data.rttScores);
|
|
218
|
-
if(data.action==='leave'){nostrVoice._participants.delete(shortId);nostrVoice._closePeer(event.pubkey);}
|
|
219
|
-
else if(!nostrVoice._participants.has(shortId)){
|
|
220
|
-
nostrVoice._participants.set(shortId,{identity:data.name||auth.npubShort(event.pubkey),isSpeaking:false,isMuted:false,isLocal:false,hasVideo:false,connectionQuality:'connecting'});
|
|
221
|
-
nostrVoiceRtc.maybeConnect(event.pubkey);
|
|
222
|
-
} else if(!nostrVoice._peers.has(event.pubkey)){
|
|
223
|
-
nostrVoiceRtc.maybeConnect(event.pubkey);
|
|
224
|
-
} else {
|
|
225
|
-
var existingPeer=nostrVoice._peers.get(event.pubkey);
|
|
226
|
-
if(existingPeer&&existingPeer.pc&&existingPeer.pc.connectionState==='new'&&state.nostrPubkey>event.pubkey){
|
|
227
|
-
existingPeer.pc.createOffer().then(o=>existingPeer.pc.setLocalDescription(o).then(()=>nostrVoice._publishSignal(event.pubkey,'offer',o))).catch(()=>{});
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
nostrVoice.updateParticipants();
|
|
231
|
-
} catch(e){}
|
|
232
|
-
},()=>{});
|
|
233
|
-
},
|
|
234
|
-
|
|
235
|
-
isDataChannelReady:()=>false,
|
|
236
|
-
updateVoiceGrid(){nostrVoice.updateParticipants();},
|
|
237
|
-
|
|
238
|
-
get __debug() {
|
|
239
|
-
var peers=[];
|
|
240
|
-
nostrVoice._peers.forEach(function(peer,pk){
|
|
241
|
-
var audioState=null;
|
|
242
|
-
if(peer.audioEl){audioState=peer.audioEl.ended?'ended':peer.audioEl.paused?'paused':'playing';}
|
|
243
|
-
var trackState=null;
|
|
244
|
-
if(peer.audioEl&&peer.audioEl.srcObject){var tracks=peer.audioEl.srcObject.getAudioTracks();trackState=tracks.length?tracks[0].readyState:'none';}
|
|
245
|
-
peers.push({pubkey:pk.slice(0,12),fsmState:peer.fsm?.getSnapshot().value,iceState:peer.pc?.iceConnectionState,connState:peer.pc?.connectionState,audioState:audioState,trackState:trackState,retryAttempt:peer.retryAttempt||0,retryAt:peer.retryAt||null,candidates:peer.pendingCandidates?.length??0,buffered:peer.bufferedCandidates?.length??0});
|
|
246
|
-
});
|
|
247
|
-
var sfu=window.nostrVoiceSfu?nostrVoiceSfu.__debug:null;
|
|
248
|
-
var camera=window.nostrVoiceCamera?{fsm:nostrVoiceCamera._fsm?.getSnapshot().value,stream:!!nostrVoice._cameraStream,enabled:state.cameraEnabled}:null;
|
|
249
|
-
return {fsm:nostrVoice._fsm?.getSnapshot().value,camera:camera,peers:peers,participants:state.voiceParticipants,sfu:sfu,retrySchedule:window.__voiceRetrySchedule||{}};
|
|
250
|
-
}
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
window.__zellous.voice=nostrVoice;
|
|
254
|
-
window.lk=nostrVoice;
|
|
255
|
-
window.nostrVoice=nostrVoice;
|
|
256
|
-
if(!window.__debug) window.__debug={};
|
|
257
|
-
Object.defineProperty(window.__debug,'voice',{get:function(){return nostrVoice.__debug;},configurable:true});
|