window.BackOneAddons = window.BackOneAddons || {}; window.BackOneAddons.voice = function voiceAddon() { let pc = null; let picker = null; let selectedPeer = null; let inCall = false; let localStream = null; let micEnabled = true; let root = null; function updateConnectionBar() { const bar = root?.querySelector('#voice-conn-bar'); if (!bar || !window.BackOneCommLayout) return; bar.innerHTML = BackOneCommLayout.connectionBarHtml( selectedPeer, BackOneNetworkSession.getActiveNetwork() ); } function updateHeadStatus() { const statusEl = root?.querySelector('#voice-head-status'); const breadcrumb = root?.querySelector('#voice-breadcrumb-peer'); if (!statusEl) return; if (inCall && selectedPeer) { statusEl.innerHTML = `Connected · ${selectedPeer.latency != null ? selectedPeer.latency + 'ms' : '—'}`; if (breadcrumb) breadcrumb.textContent = selectedPeer.peerId; } else if (selectedPeer) { statusEl.textContent = 'Ready'; if (breadcrumb) breadcrumb.textContent = selectedPeer.peerId; } else { statusEl.textContent = ''; if (breadcrumb) breadcrumb.textContent = '—'; } } function updateCallButtons() { const callBtn = root?.querySelector('#voice-call'); if (callBtn) callBtn.disabled = !selectedPeer || !BackOneNetworkSession.getNetworkId() || inCall; const selectedLabel = root?.querySelector('#voice-selected-label'); if (selectedLabel) { selectedLabel.innerHTML = selectedPeer ? `Selected: ${selectedPeer.peerId.slice(0, 10)}…` : 'Selected: —'; } updateHeadStatus(); updateConnectionBar(); } function hangUp() { if (pc) { pc.close(); pc = null; } if (localStream) { localStream.getTracks().forEach(t => t.stop()); localStream = null; } inCall = false; micEnabled = true; updateCallButtons(); window.BackOneRingingPopup?.dismiss(); } return { mount(container) { root = container; container.innerHTML = `
Voice Call P2P > Select peer —
🎙
—
Select a peer to call
`; const callBtn = container.querySelector('#voice-call'); const stageLabel = container.querySelector('#voice-stage-label'); picker = BackOnePeerPicker(container.querySelector('#voice-picker'), { layout: 'sidebar', showActionButton: false, onSelect(peer) { selectedPeer = peer; root.querySelector('#voice-ph-peer').textContent = peer.peerId; if (!inCall) stageLabel.textContent = `Ready to call ${peer.displayName || peer.peerId}`; updateCallButtons(); }, onNetworkChange() { if (!inCall) selectedPeer = null; updateCallButtons(); } }); BackOneNetworkSession.subscribe(() => updateCallButtons()); callBtn.onclick = async () => { const peer = selectedPeer || picker.getSelectedPeer(); const networkId = BackOneNetworkSession.getNetworkId(); if (!peer || !networkId) return; try { pc = new RTCPeerConnection(); localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false }); localStream.getTracks().forEach(t => pc.addTrack(t, localStream)); const offer = await pc.createOffer(); await pc.setLocalDescription(offer); await BackOneAuth.fetchWithAuth('/controller/comms/call', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'voice', networkId, targetId: peer.peerId, sdp: offer.sdp }) }); inCall = true; root.querySelector('#voice-breadcrumb-state').textContent = 'Connected to'; stageLabel.textContent = `In call with ${peer.displayName || peer.peerId}`; updateCallButtons(); } catch (err) { hangUp(); alert(err.message || 'Call failed'); } }; container.querySelector('#voice-hangup').onclick = hangUp; container.querySelector('#voice-hangup-stage').onclick = hangUp; container.querySelector('#voice-mic').onclick = () => { if (!localStream) return; micEnabled = !micEnabled; localStream.getAudioTracks().forEach(t => { t.enabled = micEnabled; }); container.querySelector('#voice-mic').textContent = micEnabled ? 'Mic' : 'Mic off'; }; updateCallButtons(); } }; };