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.
@@ -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});