// static/js/12-calls.js "use strict"; window.Glow = window.Glow || {}; Glow.calls = Glow.calls || {}; Glow.calls.iceServers = [ { urls: "stun:stun.l.google.com:19302" }, { urls: "stun:stun1.l.google.com:19302" } ]; Glow.getActiveDmPeer = () => { if (!Glow.state.activeDm || !Glow.state.currentUser) return null; return (Glow.state.activeDm.members || []).find( member => Number(member.id) !== Number(Glow.state.currentUser.id) ) || null; }; Glow.renderCallUi = () => { const call = Glow.state.calls.activeCall; const hasCall = Boolean(call); const incoming = Glow.state.calls.incomingCall; const peer = Glow.getActiveDmPeer(); if (!Glow.els.callStatus) return; Glow.els.callPeerLabel.textContent = peer?.display_name || peer?.username || "No active call"; Glow.els.callStatus.textContent = incoming ? "Incoming call" : hasCall ? call.active ? "In call" : "Ended" : "Idle"; Glow.els.startCallButton?.classList.toggle( "hidden", !Glow.state.activeDm || hasCall || Boolean(incoming) ); Glow.els.answerCallButton?.classList.toggle("hidden", !incoming); Glow.els.muteCallButton?.classList.toggle("hidden", !hasCall); Glow.els.hangupCallButton?.classList.toggle("hidden", !hasCall && !incoming); Glow.els.muteCallButton.textContent = Glow.state.calls.muted ? "Unmute" : "Mute"; }; Glow.openCallPanel = () => { Glow.els.callOverlay?.classList.remove("hidden"); Glow.renderCallUi(); }; Glow.endLocalCallUi = async () => { try { if (Glow.state.calls.peerConnection) { Glow.state.calls.peerConnection.onicecandidate = null; Glow.state.calls.peerConnection.ontrack = null; Glow.state.calls.peerConnection.close(); } } catch (_) {} try { if (Glow.state.calls.localStream) { for (const track of Glow.state.calls.localStream.getTracks()) { track.stop(); } } } catch (_) {} Glow.state.calls.peerConnection = null; Glow.state.calls.localStream = null; Glow.state.calls.activeCall = null; Glow.state.calls.incomingCall = null; Glow.state.calls.pendingOffer = null; Glow.state.calls.signalLastSeenId = 0; Glow.state.calls.muted = false; clearTimeout(Glow.state.calls.pollTimer); if (Glow.els.remoteAudio) { Glow.els.remoteAudio.srcObject = null; } Glow.renderCallUi(); }; Glow.ensurePeerConnection = async () => { if (Glow.state.calls.peerConnection) { return Glow.state.calls.peerConnection; } const pc = new RTCPeerConnection({ iceServers: Glow.calls.iceServers }); pc.onicecandidate = async event => { if (!event.candidate || !Glow.state.calls.activeCall) return; try { await Glow.apiPostCallSignal(Glow.state.calls.activeCall.id, { signal_type: "ice", payload: { candidate: event.candidate } }); } catch (_) {} }; pc.ontrack = event => { const stream = event.streams?.[0]; if (stream && Glow.els.remoteAudio) { Glow.els.remoteAudio.srcObject = stream; } }; Glow.state.calls.peerConnection = pc; return pc; }; Glow.ensureLocalAudio = async () => { if (Glow.state.calls.localStream) { return Glow.state.calls.localStream; } const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false }); Glow.state.calls.localStream = stream; return stream; }; Glow.attachLocalTracks = async () => { const pc = await Glow.ensurePeerConnection(); const stream = await Glow.ensureLocalAudio(); const existingSenders = pc.getSenders().map(sender => sender.track).filter(Boolean); for (const track of stream.getTracks()) { if (!existingSenders.includes(track)) { pc.addTrack(track, stream); } } }; Glow.startCall = async () => { if (!Glow.state.activeDm) { Glow.showToast("Open a DM to start a call.", "error"); return; } if (!navigator.mediaDevices?.getUserMedia || !window.RTCPeerConnection) { Glow.showToast("Voice calls are not supported in this browser.", "error"); return; } try { const data = await Glow.apiCreateCall(Glow.state.activeDm.id); Glow.state.calls.activeCall = data.call; Glow.state.calls.incomingCall = null; Glow.state.calls.pendingOffer = null; Glow.state.calls.signalLastSeenId = 0; Glow.openCallPanel(); await Glow.attachLocalTracks(); const pc = await Glow.ensurePeerConnection(); const offer = await pc.createOffer(); await pc.setLocalDescription(offer); await Glow.apiPostCallSignal(data.call.id, { signal_type: "offer", payload: { sdp: offer } }); await Glow.apiPostCallSignal(data.call.id, { signal_type: "ring", payload: {} }); Glow.renderCallUi(); Glow.scheduleCallPolling(); } catch (error) { Glow.showToast(error.message || "Could not start call.", "error"); await Glow.endLocalCallUi(); } }; Glow.answerIncomingCall = async () => { const incoming = Glow.state.calls.incomingCall; const pendingOffer = Glow.state.calls.pendingOffer; if (!incoming || !pendingOffer) return; try { Glow.state.calls.activeCall = incoming; Glow.state.calls.incomingCall = null; Glow.openCallPanel(); await Glow.attachLocalTracks(); const pc = await Glow.ensurePeerConnection(); await pc.setRemoteDescription(new RTCSessionDescription(pendingOffer)); const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); await Glow.apiPostCallSignal(incoming.id, { signal_type: "answer", payload: { sdp: answer } }); Glow.state.calls.pendingOffer = null; Glow.renderCallUi(); Glow.scheduleCallPolling(); } catch (error) { Glow.showToast(error.message || "Could not answer call.", "error"); await Glow.endLocalCallUi(); } }; Glow.handleCallSignal = async signal => { const activeCall = Glow.state.calls.activeCall; const incomingCall = Glow.state.calls.incomingCall; if (signal.signal_type === "ring") { if (!activeCall && !incomingCall) { try { const data = await Glow.apiGetCall(signal.call_session_id); Glow.state.calls.incomingCall = data.call; Glow.openCallPanel(); Glow.renderCallUi(); } catch (_) {} } return; } if (signal.signal_type === "offer") { try { const data = await Glow.apiGetCall(signal.call_session_id); Glow.state.calls.incomingCall = data.call; Glow.state.calls.pendingOffer = signal.payload?.sdp || signal.payload; Glow.openCallPanel(); Glow.renderCallUi(); } catch (_) {} return; } if (!Glow.state.calls.activeCall || Number(Glow.state.calls.activeCall.id) !== Number(signal.call_session_id)) { return; } const pc = await Glow.ensurePeerConnection(); if (signal.signal_type === "answer") { const sdp = signal.payload?.sdp || signal.payload; if (sdp) { await pc.setRemoteDescription(new RTCSessionDescription(sdp)); } return; } if (signal.signal_type === "ice") { const candidate = signal.payload?.candidate || signal.payload; if (candidate) { try { await pc.addIceCandidate(new RTCIceCandidate(candidate)); } catch (_) {} } return; } if (signal.signal_type === "hangup") { Glow.showToast("Call ended."); await Glow.endLocalCallUi(); return; } }; Glow.pollCallSignals = async () => { const activeCall = Glow.state.calls.activeCall; const incomingCall = Glow.state.calls.incomingCall; const call = activeCall || incomingCall; if (!call || document.hidden) return; try { const data = await Glow.apiGetCallSignals( call.id, Glow.state.calls.signalLastSeenId || 0 ); const signals = data.signals || []; for (const signal of signals) { Glow.state.calls.signalLastSeenId = Math.max( Glow.state.calls.signalLastSeenId || 0, Number(signal.id) || 0 ); if ( Glow.state.currentUser && Number(signal.user_id) === Number(Glow.state.currentUser.id) ) { continue; } await Glow.handleCallSignal(signal); } } catch (_) {} }; Glow.scheduleCallPolling = () => { clearTimeout(Glow.state.calls.pollTimer); if (!Glow.state.calls.activeCall && !Glow.state.calls.incomingCall) { return; } Glow.state.calls.pollTimer = setTimeout(async () => { await Glow.pollCallSignals(); Glow.scheduleCallPolling(); }, document.hidden ? 5000 : 2000); }; Glow.toggleMuteCall = () => { const stream = Glow.state.calls.localStream; if (!stream) return; Glow.state.calls.muted = !Glow.state.calls.muted; for (const track of stream.getAudioTracks()) { track.enabled = !Glow.state.calls.muted; } Glow.renderCallUi(); }; Glow.hangupCall = async () => { const call = Glow.state.calls.activeCall || Glow.state.calls.incomingCall; if (!call) return; try { await Glow.apiEndCall(call.id); } catch (_) { // ignore } Glow.showToast("Call ended."); await Glow.endLocalCallUi(); }; Glow.syncActiveCallForDm = async () => { if (!Glow.state.activeDm) return; try { const createResult = await Glow.apiCreateCall(Glow.state.activeDm.id); const call = createResult.call; if (call?.active) { Glow.state.calls.activeCall = call; Glow.renderCallUi(); Glow.scheduleCallPolling(); } } catch (_) { // ignore if no active call semantics vary } }; Glow.els.startCallButton?.addEventListener("click", () => { Glow.startCall?.(); }); Glow.els.answerCallButton?.addEventListener("click", () => { Glow.answerIncomingCall?.(); }); Glow.els.muteCallButton?.addEventListener("click", () => { Glow.toggleMuteCall?.(); }); Glow.els.hangupCallButton?.addEventListener("click", () => { Glow.hangupCall?.(); });