165 lines
6.1 KiB
JavaScript
165 lines
6.1 KiB
JavaScript
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 = `<span class="connected">Connected</span> · ${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: <strong>${selectedPeer.peerId.slice(0, 10)}…</strong>`
|
|
: 'Selected: <strong>—</strong>';
|
|
}
|
|
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 = `
|
|
<div class="comm-layout">
|
|
<aside class="comm-sidebar">
|
|
<div class="comm-sidebar-head">
|
|
<h2>Voice Call P2P</h2>
|
|
<p class="comm-sidebar-sub">BackOne core · same network as backone-cli</p>
|
|
</div>
|
|
<div class="comm-sidebar-body">
|
|
<div id="voice-picker"></div>
|
|
</div>
|
|
<div class="comm-sidebar-actions">
|
|
<div id="voice-selected-label" class="comm-selected-label">Selected: <strong>—</strong></div>
|
|
<button type="button" id="voice-call" class="btn-primary" disabled>Voice Call</button>
|
|
<button type="button" id="voice-hangup" class="btn-secondary">Hang Up</button>
|
|
</div>
|
|
</aside>
|
|
<div class="comm-main">
|
|
<div class="comm-main-head">
|
|
<div class="comm-breadcrumb">Voice Call P2P > <span id="voice-breadcrumb-state">Select peer</span> <strong id="voice-breadcrumb-peer">—</strong></div>
|
|
<div id="voice-head-status" class="comm-head-status"></div>
|
|
</div>
|
|
<div class="comm-stage">
|
|
<div class="voice-stage-inner">
|
|
<div class="avatar">🎙</div>
|
|
<div class="peer-id" id="voice-ph-peer">—</div>
|
|
<div id="voice-stage-label">Select a peer to call</div>
|
|
</div>
|
|
<div class="comm-call-controls">
|
|
<button type="button" id="voice-mic">Mic</button>
|
|
<button type="button" id="voice-hangup-stage" class="danger">Hang Up</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="voice-conn-bar" class="comm-connection-bar"></div>`;
|
|
|
|
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();
|
|
}
|
|
};
|
|
};
|