Files
BackOne/addons/video/frontend/index.js
T
2026-07-02 09:12:57 +07:00

227 lines
8.8 KiB
JavaScript

window.BackOneAddons = window.BackOneAddons || {};
window.BackOneAddons.video = function videoAddon() {
let picker = null;
let selectedPeer = null;
let inCall = false;
let micEnabled = true;
let camEnabled = true;
let root = null;
function updateConnectionBar() {
const bar = root?.querySelector('#video-conn-bar');
if (!bar || !window.BackOneCommLayout) return;
bar.innerHTML = BackOneCommLayout.connectionBarHtml(
selectedPeer,
BackOneNetworkSession.getActiveNetwork()
);
}
function updateHeadStatus() {
const statusEl = root?.querySelector('#video-head-status');
const breadcrumb = root?.querySelector('#video-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('#video-call');
if (callBtn) callBtn.disabled = !selectedPeer || !BackOneNetworkSession.getNetworkId() || inCall;
const selectedLabel = root?.querySelector('#video-selected-label');
if (selectedLabel) {
selectedLabel.innerHTML = selectedPeer
? `Selected: <strong>${selectedPeer.peerId.slice(0, 10)}…</strong>`
: 'Selected: <strong>—</strong>';
}
updateHeadStatus();
updateConnectionBar();
}
function updateCallLayout() {
const layout = root?.querySelector('.comm-layout');
if (layout) layout.classList.toggle('video-in-call', inCall);
}
function showPlaceholder(show) {
const stage = root?.querySelector('.video-stage');
const remote = root?.querySelector('#video-remote');
const phLabel = root?.querySelector('#video-ph-label');
if (stage) stage.classList.toggle('has-remote', !show);
if (remote) remote.classList.toggle('active', !show);
if (phLabel && show) {
phLabel.textContent = inCall
? 'Waiting for remote peer…'
: (selectedPeer ? 'Ready to call' : 'Select a peer to start a video call');
}
updateCallLayout();
}
function bindStreams() {
const local = root?.querySelector('#video-local');
const remote = root?.querySelector('#video-remote');
const localStream = BackOneWebRtcSession.getLocalStream();
const remoteStream = BackOneWebRtcSession.getRemoteStream();
if (local && localStream) local.srcObject = localStream;
if (remote && remoteStream && remoteStream.getTracks().length) {
remote.srcObject = remoteStream;
showPlaceholder(false);
root?.querySelector('#video-breadcrumb-state')?.textContent = 'Connected to';
}
}
async function hangUp() {
await BackOneWebRtcSession.hangUp();
const local = root?.querySelector('#video-local');
const remote = root?.querySelector('#video-remote');
if (local) local.srcObject = null;
if (remote) remote.srcObject = null;
inCall = false;
micEnabled = true;
camEnabled = true;
const breadcrumbState = root?.querySelector('#video-breadcrumb-state');
if (breadcrumbState) breadcrumbState.textContent = 'Select peer';
showPlaceholder(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>Video Call P2P</h2>
<p class="comm-sidebar-sub">BackOne core · same network as backone-cli</p>
</div>
<div class="comm-sidebar-body">
<div id="video-picker"></div>
</div>
<div class="comm-sidebar-actions">
<div id="video-selected-label" class="comm-selected-label">Selected: <strong>—</strong></div>
<button type="button" id="video-call" class="btn-primary" disabled>Video Call</button>
<button type="button" id="video-hangup" class="btn-secondary">Hang Up</button>
</div>
</aside>
<div class="comm-main">
<div class="comm-main-head">
<div class="comm-breadcrumb">Video Call P2P &gt; <span id="video-breadcrumb-state">Select peer</span> <strong id="video-breadcrumb-peer">—</strong></div>
<div id="video-head-status" class="comm-head-status"></div>
</div>
<div class="comm-stage video-stage">
<div class="video-remote-main">
<div id="video-placeholder" class="comm-remote-placeholder">
<div class="avatar">👤</div>
<div class="peer-id" id="video-ph-peer">—</div>
<div id="video-ph-label">Select a peer to start a video call</div>
</div>
<video id="video-remote" class="video-remote-stream" autoplay playsinline></video>
</div>
<div class="video-local-pip">
<span class="video-pip-label">You</span>
<video id="video-local" autoplay muted playsinline></video>
</div>
<div class="comm-remote-tag"><span class="dot"></span> Remote</div>
<div class="comm-call-controls">
<button type="button" id="video-mic">Mic</button>
<button type="button" id="video-cam">Camera</button>
<button type="button" id="video-fs">Fullscreen</button>
<button type="button" id="video-hangup-stage" class="danger">Hang Up</button>
</div>
</div>
</div>
</div>
<div id="video-conn-bar" class="comm-connection-bar"></div>`;
const callBtn = container.querySelector('#video-call');
picker = BackOnePeerPicker(container.querySelector('#video-picker'), {
layout: 'sidebar',
showActionButton: false,
onSelect(peer) {
selectedPeer = peer;
const phPeer = root.querySelector('#video-ph-peer');
if (phPeer) phPeer.textContent = peer.peerId;
if (!inCall) showPlaceholder(true);
updateCallButtons();
},
onNetworkChange() {
if (!inCall) selectedPeer = null;
updateCallButtons();
}
});
BackOneNetworkSession.subscribe(() => updateCallButtons());
BackOneWebRtcSession.subscribe((event) => {
if (event === 'call-started') {
inCall = true;
updateCallButtons();
bindStreams();
}
if (event === 'remote-track' || event === 'call-accepted') bindStreams();
if (event === 'call-ended') {
inCall = false;
showPlaceholder(true);
updateCallButtons();
}
});
callBtn.onclick = async () => {
const peer = selectedPeer || picker.getSelectedPeer();
if (!peer || !BackOneNetworkSession.getNetworkId()) return;
try {
inCall = true;
root.querySelector('#video-breadcrumb-state').textContent = 'Connecting to';
updateCallLayout();
updateCallButtons();
await BackOneWebRtcSession.startOutgoingCall(peer, 'video');
bindStreams();
} catch (err) {
await hangUp();
alert(err.message || 'Call failed');
}
};
container.querySelector('#video-hangup').onclick = () => hangUp();
container.querySelector('#video-hangup-stage').onclick = () => hangUp();
container.querySelector('#video-mic').onclick = () => {
const localStream = BackOneWebRtcSession.getLocalStream();
if (!localStream) return;
micEnabled = !micEnabled;
localStream.getAudioTracks().forEach(t => { t.enabled = micEnabled; });
container.querySelector('#video-mic').textContent = micEnabled ? 'Mic' : 'Mic off';
};
container.querySelector('#video-cam').onclick = () => {
const localStream = BackOneWebRtcSession.getLocalStream();
if (!localStream) return;
camEnabled = !camEnabled;
localStream.getVideoTracks().forEach(t => { t.enabled = camEnabled; });
container.querySelector('#video-cam').textContent = camEnabled ? 'Camera' : 'Camera off';
};
container.querySelector('#video-fs').onclick = () => {
const stage = container.querySelector('.comm-stage');
if (stage.requestFullscreen) stage.requestFullscreen();
};
const active = BackOneWebRtcSession.getActiveCall();
inCall = !!active && active.callType === 'video';
showPlaceholder(true);
if (inCall) bindStreams();
updateCallButtons();
}
};
};