Update static/js/12-calls.js
This commit is contained in:
parent
aa702bb2a9
commit
8a2ee3af05
@ -15,10 +15,24 @@ Glow.calls.ensureState = () => {
|
|||||||
state.selfPeerId ||= null;
|
state.selfPeerId ||= null;
|
||||||
state.peerConnections ||= new Map();
|
state.peerConnections ||= new Map();
|
||||||
state.remoteStreams ||= new Map();
|
state.remoteStreams ||= new Map();
|
||||||
|
state.participantAudioEls ||= new Map();
|
||||||
state.peers ||= [];
|
state.peers ||= [];
|
||||||
state.reconnectTimer ||= null;
|
state.reconnectTimer ||= null;
|
||||||
state.invites ||= [];
|
|
||||||
state.invitePollTimer ||= null;
|
state.invitePollTimer ||= null;
|
||||||
|
state.participantPollTimer ||= null;
|
||||||
|
state.heartbeatTimer ||= null;
|
||||||
|
state.invites ||= [];
|
||||||
|
state.reconnecting ||= false;
|
||||||
|
state.joining ||= false;
|
||||||
|
state.leaving ||= false;
|
||||||
|
state.permissions ||= {
|
||||||
|
can_manage: false,
|
||||||
|
can_end: false,
|
||||||
|
can_invite: false
|
||||||
|
};
|
||||||
|
state.backendParticipants ||= [];
|
||||||
|
state.scope ||= null;
|
||||||
|
state.statusText ||= "Idle";
|
||||||
};
|
};
|
||||||
|
|
||||||
Glow.calls.scopeLabel = invite => {
|
Glow.calls.scopeLabel = invite => {
|
||||||
@ -67,12 +81,54 @@ Glow.calls.sendSignal = payload => {
|
|||||||
Glow.calls.replaceHttpWs = url =>
|
Glow.calls.replaceHttpWs = url =>
|
||||||
url.replace(/^https:/, "wss:").replace(/^http:/, "ws:");
|
url.replace(/^https:/, "wss:").replace(/^http:/, "ws:");
|
||||||
|
|
||||||
Glow.calls.setRemoteAudioFromAnyPeer = () => {
|
Glow.calls.setStatusText = text => {
|
||||||
|
Glow.calls.ensureState();
|
||||||
|
Glow.calls.state().statusText = String(text || "");
|
||||||
|
Glow.renderCallUi();
|
||||||
|
};
|
||||||
|
|
||||||
|
Glow.calls.ensureRemoteAudioElement = peerId => {
|
||||||
Glow.calls.ensureState();
|
Glow.calls.ensureState();
|
||||||
const state = Glow.calls.state();
|
const state = Glow.calls.state();
|
||||||
const firstStream = [...state.remoteStreams.values()][0] || null;
|
if (!Glow.els.remoteAudioContainer) return null;
|
||||||
if (Glow.els.remoteAudio) {
|
|
||||||
Glow.els.remoteAudio.srcObject = firstStream;
|
let audio = state.participantAudioEls.get(peerId);
|
||||||
|
if (audio) return audio;
|
||||||
|
|
||||||
|
audio = document.createElement("audio");
|
||||||
|
audio.autoplay = true;
|
||||||
|
audio.playsInline = true;
|
||||||
|
audio.dataset.peerId = String(peerId);
|
||||||
|
Glow.els.remoteAudioContainer.append(audio);
|
||||||
|
state.participantAudioEls.set(peerId, audio);
|
||||||
|
return audio;
|
||||||
|
};
|
||||||
|
|
||||||
|
Glow.calls.removeRemoteAudioElement = peerId => {
|
||||||
|
Glow.calls.ensureState();
|
||||||
|
const state = Glow.calls.state();
|
||||||
|
const audio = state.participantAudioEls.get(peerId);
|
||||||
|
if (!audio) return;
|
||||||
|
audio.srcObject = null;
|
||||||
|
audio.remove();
|
||||||
|
state.participantAudioEls.delete(peerId);
|
||||||
|
};
|
||||||
|
|
||||||
|
Glow.calls.syncRemoteAudioElements = () => {
|
||||||
|
Glow.calls.ensureState();
|
||||||
|
const state = Glow.calls.state();
|
||||||
|
|
||||||
|
for (const [peerId, stream] of state.remoteStreams.entries()) {
|
||||||
|
const audio = Glow.calls.ensureRemoteAudioElement(peerId);
|
||||||
|
if (audio && audio.srcObject !== stream) {
|
||||||
|
audio.srcObject = stream;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const peerId of [...state.participantAudioEls.keys()]) {
|
||||||
|
if (!state.remoteStreams.has(peerId)) {
|
||||||
|
Glow.calls.removeRemoteAudioElement(peerId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -112,7 +168,8 @@ Glow.calls.removePeerConnection = peerId => {
|
|||||||
|
|
||||||
state.peerConnections.delete(peerId);
|
state.peerConnections.delete(peerId);
|
||||||
state.remoteStreams.delete(peerId);
|
state.remoteStreams.delete(peerId);
|
||||||
Glow.calls.setRemoteAudioFromAnyPeer();
|
Glow.calls.removeRemoteAudioElement(peerId);
|
||||||
|
Glow.calls.syncRemoteAudioElements();
|
||||||
};
|
};
|
||||||
|
|
||||||
Glow.calls.removePeer = peerId => {
|
Glow.calls.removePeer = peerId => {
|
||||||
@ -133,12 +190,28 @@ Glow.calls.ensureLocalAudio = async () => {
|
|||||||
const state = Glow.calls.state();
|
const state = Glow.calls.state();
|
||||||
if (state.localStream) return state.localStream;
|
if (state.localStream) return state.localStream;
|
||||||
|
|
||||||
|
try {
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
audio: true,
|
audio: true,
|
||||||
video: false
|
video: false
|
||||||
});
|
});
|
||||||
state.localStream = stream;
|
state.localStream = stream;
|
||||||
return stream;
|
return stream;
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.name === "NotAllowedError" || error?.name === "PermissionDeniedError") {
|
||||||
|
throw new Error("Microphone permission was denied.");
|
||||||
|
}
|
||||||
|
if (error?.name === "NotFoundError" || error?.name === "DevicesNotFoundError") {
|
||||||
|
throw new Error("No microphone was found on this device.");
|
||||||
|
}
|
||||||
|
if (error?.name === "NotReadableError" || error?.name === "TrackStartError") {
|
||||||
|
throw new Error("The microphone is already in use by another application.");
|
||||||
|
}
|
||||||
|
if (error?.name === "SecurityError") {
|
||||||
|
throw new Error("Microphone access is blocked by browser security settings.");
|
||||||
|
}
|
||||||
|
throw new Error("Could not access your microphone.");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Glow.calls.ensurePeerConnection = async peerId => {
|
Glow.calls.ensurePeerConnection = async peerId => {
|
||||||
@ -168,7 +241,7 @@ Glow.calls.ensurePeerConnection = async peerId => {
|
|||||||
const stream = event.streams?.[0];
|
const stream = event.streams?.[0];
|
||||||
if (!stream) return;
|
if (!stream) return;
|
||||||
state.remoteStreams.set(peerId, stream);
|
state.remoteStreams.set(peerId, stream);
|
||||||
Glow.calls.setRemoteAudioFromAnyPeer();
|
Glow.calls.syncRemoteAudioElements();
|
||||||
};
|
};
|
||||||
|
|
||||||
pc.onconnectionstatechange = () => {
|
pc.onconnectionstatechange = () => {
|
||||||
@ -243,9 +316,173 @@ Glow.calls.fetchGroupCallSession = async scope => {
|
|||||||
|
|
||||||
helperData.calls_base = data.calls_base;
|
helperData.calls_base = data.calls_base;
|
||||||
helperData.group_session = data.session;
|
helperData.group_session = data.session;
|
||||||
|
helperData.participants = data.participants || [];
|
||||||
|
helperData.permissions = data.permissions || {
|
||||||
|
can_manage: false,
|
||||||
|
can_end: false,
|
||||||
|
can_invite: false
|
||||||
|
};
|
||||||
|
helperData.scope = data.scope || null;
|
||||||
return helperData;
|
return helperData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Glow.calls.renderPendingInvites = () => {
|
||||||
|
const wrap = Glow.els.callPendingInvites;
|
||||||
|
if (!wrap) return;
|
||||||
|
|
||||||
|
wrap.replaceChildren();
|
||||||
|
|
||||||
|
const activeCallId = Glow.state.calls.activeCall?.groupSession?.id;
|
||||||
|
const relevant = (Glow.state.calls.invites || []).filter(
|
||||||
|
invite => Number(invite.group_call_session_id) === Number(activeCallId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!relevant.length) {
|
||||||
|
wrap.classList.add("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = document.createElement("div");
|
||||||
|
title.className = "call-pending-title";
|
||||||
|
title.textContent = "Pending invites";
|
||||||
|
wrap.append(title);
|
||||||
|
|
||||||
|
for (const invite of relevant) {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "call-pending-row";
|
||||||
|
|
||||||
|
const left = document.createElement("div");
|
||||||
|
left.className = "call-pending-name";
|
||||||
|
left.textContent = invite.inviter_username
|
||||||
|
? `from ${invite.inviter_username}`
|
||||||
|
: "pending invite";
|
||||||
|
|
||||||
|
const right = document.createElement("div");
|
||||||
|
right.className = "call-pending-status";
|
||||||
|
right.textContent = "pending";
|
||||||
|
|
||||||
|
row.append(left, right);
|
||||||
|
wrap.append(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
wrap.classList.remove("hidden");
|
||||||
|
};
|
||||||
|
|
||||||
|
Glow.calls.renderParticipants = () => {
|
||||||
|
if (!Glow.els.callParticipants) return;
|
||||||
|
|
||||||
|
Glow.calls.ensureState();
|
||||||
|
const state = Glow.calls.state();
|
||||||
|
const box = Glow.els.callParticipants;
|
||||||
|
box.replaceChildren();
|
||||||
|
|
||||||
|
const backendParticipants = Array.isArray(state.backendParticipants)
|
||||||
|
? state.backendParticipants
|
||||||
|
: [];
|
||||||
|
const peerMap = new Map(
|
||||||
|
(state.peers || []).map(peer => [String(peer.participantId), peer])
|
||||||
|
);
|
||||||
|
|
||||||
|
const selfRow = document.createElement("div");
|
||||||
|
selfRow.className = "call-participant self";
|
||||||
|
|
||||||
|
const selfLeft = document.createElement("div");
|
||||||
|
selfLeft.className = "call-participant-name";
|
||||||
|
selfLeft.textContent =
|
||||||
|
Glow.state.currentUser?.username
|
||||||
|
? `${Glow.state.currentUser.username} (you)`
|
||||||
|
: "you";
|
||||||
|
|
||||||
|
const selfRight = document.createElement("div");
|
||||||
|
selfRight.className = "call-participant-status";
|
||||||
|
selfRight.textContent = state.activeCall
|
||||||
|
? state.muted
|
||||||
|
? "connected · muted"
|
||||||
|
: "connected"
|
||||||
|
: "idle";
|
||||||
|
|
||||||
|
selfRow.append(selfLeft, selfRight);
|
||||||
|
box.append(selfRow);
|
||||||
|
|
||||||
|
const renderedUserIds = new Set();
|
||||||
|
if (Glow.state.currentUser?.id) {
|
||||||
|
renderedUserIds.add(String(Glow.state.currentUser.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const participant of backendParticipants) {
|
||||||
|
const userId = String(participant.user_id);
|
||||||
|
if (renderedUserIds.has(userId)) continue;
|
||||||
|
renderedUserIds.add(userId);
|
||||||
|
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "call-participant";
|
||||||
|
|
||||||
|
const left = document.createElement("div");
|
||||||
|
left.className = "call-participant-name";
|
||||||
|
left.textContent =
|
||||||
|
participant.display_name || participant.username || `user ${participant.user_id}`;
|
||||||
|
if (participant) {
|
||||||
|
Glow.applyUserColor(left, participant);
|
||||||
|
}
|
||||||
|
|
||||||
|
const right = document.createElement("div");
|
||||||
|
right.className = "call-participant-status";
|
||||||
|
|
||||||
|
const matchingPeer = [...peerMap.values()].find(peer => {
|
||||||
|
return (
|
||||||
|
String(peer.username || "").toLowerCase() ===
|
||||||
|
String(participant.username || "").toLowerCase()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
right.textContent = matchingPeer ? "connected" : "joined";
|
||||||
|
|
||||||
|
row.append(left, right);
|
||||||
|
box.append(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
const extraPeers = Glow.calls.listOtherPeers().filter(peer => {
|
||||||
|
return !backendParticipants.some(
|
||||||
|
participant =>
|
||||||
|
String(participant.username || "").toLowerCase() ===
|
||||||
|
String(peer.username || "").toLowerCase()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const peer of extraPeers) {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "call-participant";
|
||||||
|
|
||||||
|
const left = document.createElement("div");
|
||||||
|
left.className = "call-participant-name";
|
||||||
|
left.textContent = peer.username || peer.participantId;
|
||||||
|
|
||||||
|
const right = document.createElement("div");
|
||||||
|
right.className = "call-participant-status";
|
||||||
|
right.textContent = "connecting";
|
||||||
|
|
||||||
|
row.append(left, right);
|
||||||
|
box.append(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (box.children.length === 1 && state.activeCall) {
|
||||||
|
const empty = document.createElement("div");
|
||||||
|
empty.className = "call-participant";
|
||||||
|
const label = document.createElement("div");
|
||||||
|
label.className = "call-participant-name";
|
||||||
|
label.textContent = "waiting for others to join";
|
||||||
|
empty.append(label);
|
||||||
|
box.append(empty);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Glow.calls.renderHeaderCallChip = () => {
|
||||||
|
if (!Glow.els.callStatusChip) return;
|
||||||
|
const active = Boolean(Glow.state.calls.activeCall);
|
||||||
|
Glow.els.callStatusChip.classList.toggle("hidden", !active);
|
||||||
|
Glow.els.callStatusChip.textContent = active ? "Call active" : "Call";
|
||||||
|
};
|
||||||
|
|
||||||
Glow.calls.connectWebSocket = async () => {
|
Glow.calls.connectWebSocket = async () => {
|
||||||
Glow.calls.ensureState();
|
Glow.calls.ensureState();
|
||||||
const state = Glow.calls.state();
|
const state = Glow.calls.state();
|
||||||
@ -259,6 +496,9 @@ Glow.calls.connectWebSocket = async () => {
|
|||||||
state.ws = ws;
|
state.ws = ws;
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
|
state.reconnecting = false;
|
||||||
|
state.joining = false;
|
||||||
|
Glow.calls.setStatusText("Connected");
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -282,7 +522,9 @@ Glow.calls.connectWebSocket = async () => {
|
|||||||
|
|
||||||
if (signal.type === "joined") {
|
if (signal.type === "joined") {
|
||||||
state.selfPeerId = signal.participantId;
|
state.selfPeerId = signal.participantId;
|
||||||
|
Glow.calls.setStatusText("Call connected");
|
||||||
Glow.renderCallUi();
|
Glow.renderCallUi();
|
||||||
|
Glow.calls.renderParticipants();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,6 +534,7 @@ Glow.calls.connectWebSocket = async () => {
|
|||||||
roomId: signal.roomId
|
roomId: signal.roomId
|
||||||
});
|
});
|
||||||
Glow.renderCallUi();
|
Glow.renderCallUi();
|
||||||
|
Glow.calls.renderParticipants();
|
||||||
|
|
||||||
if (signal.participantId && signal.participantId !== state.selfPeerId) {
|
if (signal.participantId && signal.participantId !== state.selfPeerId) {
|
||||||
try {
|
try {
|
||||||
@ -304,6 +547,7 @@ Glow.calls.connectWebSocket = async () => {
|
|||||||
if (signal.type === "participant-left") {
|
if (signal.type === "participant-left") {
|
||||||
Glow.calls.removePeer(signal.participantId);
|
Glow.calls.removePeer(signal.participantId);
|
||||||
Glow.renderCallUi();
|
Glow.renderCallUi();
|
||||||
|
Glow.calls.renderParticipants();
|
||||||
Glow.showToast("Peer left call.");
|
Glow.showToast("Peer left call.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -326,6 +570,13 @@ Glow.calls.connectWebSocket = async () => {
|
|||||||
const from = signal.from;
|
const from = signal.from;
|
||||||
if (!from) return;
|
if (!from) return;
|
||||||
|
|
||||||
|
Glow.calls.upsertPeer({
|
||||||
|
participantId: from,
|
||||||
|
roomId: signal.roomId,
|
||||||
|
username: signal.username || from
|
||||||
|
});
|
||||||
|
Glow.calls.renderParticipants();
|
||||||
|
|
||||||
const pc = await Glow.calls.ensurePeerConnection(from);
|
const pc = await Glow.calls.ensurePeerConnection(from);
|
||||||
await pc.setRemoteDescription({ type: "offer", sdp: signal.sdp });
|
await pc.setRemoteDescription({ type: "offer", sdp: signal.sdp });
|
||||||
const answer = await pc.createAnswer();
|
const answer = await pc.createAnswer();
|
||||||
@ -388,18 +639,27 @@ Glow.calls.resetTransportOnly = () => {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const peerId of [...state.participantAudioEls.keys()]) {
|
||||||
|
Glow.calls.removeRemoteAudioElement(peerId);
|
||||||
|
}
|
||||||
|
|
||||||
state.ws = null;
|
state.ws = null;
|
||||||
state.peerConnections = new Map();
|
state.peerConnections = new Map();
|
||||||
state.remoteStreams = new Map();
|
state.remoteStreams = new Map();
|
||||||
|
state.participantAudioEls = new Map();
|
||||||
state.peers = [];
|
state.peers = [];
|
||||||
state.selfPeerId = null;
|
state.selfPeerId = null;
|
||||||
state.reconnectTimer = null;
|
state.reconnectTimer = null;
|
||||||
|
state.reconnecting = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
Glow.calls.resetAll = () => {
|
Glow.calls.resetAll = () => {
|
||||||
Glow.calls.ensureState();
|
Glow.calls.ensureState();
|
||||||
const state = Glow.calls.state();
|
const state = Glow.calls.state();
|
||||||
|
|
||||||
|
clearTimeout(state.participantPollTimer);
|
||||||
|
clearTimeout(state.heartbeatTimer);
|
||||||
|
|
||||||
Glow.calls.resetTransportOnly();
|
Glow.calls.resetTransportOnly();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -414,14 +674,34 @@ Glow.calls.resetAll = () => {
|
|||||||
state.activeCall = null;
|
state.activeCall = null;
|
||||||
state.session = null;
|
state.session = null;
|
||||||
state.muted = false;
|
state.muted = false;
|
||||||
|
state.joining = false;
|
||||||
|
state.leaving = false;
|
||||||
|
state.backendParticipants = [];
|
||||||
|
state.scope = null;
|
||||||
|
state.statusText = "Idle";
|
||||||
|
state.permissions = {
|
||||||
|
can_manage: false,
|
||||||
|
can_end: false,
|
||||||
|
can_invite: false
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
Glow.endLocalCallUi = async () => {
|
Glow.endLocalCallUi = async () => {
|
||||||
Glow.calls.resetAll();
|
Glow.calls.resetAll();
|
||||||
if (Glow.els.remoteAudio) {
|
|
||||||
Glow.els.remoteAudio.srcObject = null;
|
if (Glow.els.callParticipants) {
|
||||||
|
Glow.els.callParticipants.replaceChildren();
|
||||||
}
|
}
|
||||||
|
if (Glow.els.callPendingInvites) {
|
||||||
|
Glow.els.callPendingInvites.replaceChildren();
|
||||||
|
Glow.els.callPendingInvites.classList.add("hidden");
|
||||||
|
}
|
||||||
|
if (Glow.els.remoteAudioContainer) {
|
||||||
|
Glow.els.remoteAudioContainer.replaceChildren();
|
||||||
|
}
|
||||||
|
|
||||||
Glow.renderCallUi();
|
Glow.renderCallUi();
|
||||||
|
Glow.calls.renderHeaderCallChip();
|
||||||
};
|
};
|
||||||
|
|
||||||
Glow.calls.scheduleReconnect = () => {
|
Glow.calls.scheduleReconnect = () => {
|
||||||
@ -429,58 +709,151 @@ Glow.calls.scheduleReconnect = () => {
|
|||||||
const state = Glow.calls.state();
|
const state = Glow.calls.state();
|
||||||
|
|
||||||
clearTimeout(state.reconnectTimer);
|
clearTimeout(state.reconnectTimer);
|
||||||
|
state.reconnecting = true;
|
||||||
|
Glow.calls.setStatusText("Reconnecting…");
|
||||||
|
Glow.renderCallUi();
|
||||||
|
|
||||||
state.reconnectTimer = setTimeout(async () => {
|
state.reconnectTimer = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
if (!state.activeCall) return;
|
if (!state.activeCall) return;
|
||||||
const scope = state.activeCall.scope;
|
const scope = state.activeCall.scope || state.scope || Glow.calls.getCurrentScope();
|
||||||
if (!scope) return;
|
if (!scope) return;
|
||||||
|
|
||||||
Glow.calls.resetTransportOnly();
|
Glow.calls.resetTransportOnly();
|
||||||
|
Glow.calls.setStatusText("Reconnecting…");
|
||||||
state.session = await Glow.calls.fetchGroupCallSession(scope);
|
state.session = await Glow.calls.fetchGroupCallSession(scope);
|
||||||
|
state.permissions = state.session.permissions || state.permissions;
|
||||||
|
state.scope = state.session.scope || state.scope;
|
||||||
|
if (state.session.group_session?.id) {
|
||||||
|
await Glow.apiJoinGroupCall(state.session.group_session.id);
|
||||||
|
}
|
||||||
await Glow.calls.connectWebSocket();
|
await Glow.calls.connectWebSocket();
|
||||||
|
Glow.calls.startParticipantSync();
|
||||||
|
Glow.calls.startHeartbeat();
|
||||||
Glow.showToast("Call reconnected.");
|
Glow.showToast("Call reconnected.");
|
||||||
Glow.renderCallUi();
|
Glow.renderCallUi();
|
||||||
|
Glow.calls.refreshBackendParticipants();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
Glow.calls.scheduleReconnect();
|
Glow.calls.scheduleReconnect();
|
||||||
}
|
}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Glow.calls.refreshBackendParticipants = async () => {
|
||||||
|
Glow.calls.ensureState();
|
||||||
|
const sessionId = Glow.state.calls.activeCall?.groupSession?.id;
|
||||||
|
if (!sessionId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await Glow.apiGetGroupCallParticipants(sessionId);
|
||||||
|
Glow.state.calls.backendParticipants = data.participants || [];
|
||||||
|
Glow.state.calls.permissions = data.permissions || Glow.state.calls.permissions;
|
||||||
|
Glow.state.calls.scope = data.scope || Glow.state.calls.scope;
|
||||||
|
Glow.calls.renderParticipants();
|
||||||
|
Glow.renderCallUi();
|
||||||
|
} catch (_) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
Glow.calls.startParticipantSync = () => {
|
||||||
|
Glow.calls.ensureState();
|
||||||
|
clearTimeout(Glow.state.calls.participantPollTimer);
|
||||||
|
|
||||||
|
const run = async () => {
|
||||||
|
if (!Glow.state.calls.activeCall || document.hidden) {
|
||||||
|
Glow.calls.startParticipantSync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await Glow.calls.refreshBackendParticipants();
|
||||||
|
Glow.calls.startParticipantSync();
|
||||||
|
};
|
||||||
|
|
||||||
|
Glow.state.calls.participantPollTimer = setTimeout(run, 5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
Glow.calls.startHeartbeat = () => {
|
||||||
|
Glow.calls.ensureState();
|
||||||
|
clearTimeout(Glow.state.calls.heartbeatTimer);
|
||||||
|
|
||||||
|
const run = async () => {
|
||||||
|
const sessionId = Glow.state.calls.activeCall?.groupSession?.id;
|
||||||
|
if (!sessionId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Glow.apiHeartbeatGroupCall(sessionId);
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
Glow.calls.startHeartbeat();
|
||||||
|
};
|
||||||
|
|
||||||
|
Glow.state.calls.heartbeatTimer = setTimeout(run, 12000);
|
||||||
|
};
|
||||||
|
|
||||||
Glow.renderCallUi = () => {
|
Glow.renderCallUi = () => {
|
||||||
Glow.calls.ensureState();
|
Glow.calls.ensureState();
|
||||||
const state = Glow.calls.state();
|
const state = Glow.calls.state();
|
||||||
const active = Boolean(state.activeCall);
|
const active = Boolean(state.activeCall);
|
||||||
const scope = state.activeCall?.scope || Glow.calls.getCurrentScope();
|
const scope = state.activeCall?.scope || state.scope || Glow.calls.getCurrentScope();
|
||||||
const peerCount = Glow.calls.listOtherPeers().length;
|
const peerCount = Glow.calls.listOtherPeers().length;
|
||||||
|
|
||||||
if (!Glow.els.callStatus) return;
|
if (!Glow.els.callStatus) return;
|
||||||
|
|
||||||
Glow.els.callPeerLabel.textContent =
|
Glow.els.callPeerLabel.textContent =
|
||||||
scope?.label ||
|
scope?.label ||
|
||||||
|
state.scope?.scope_label ||
|
||||||
Glow.state.activeChannel?.name ||
|
Glow.state.activeChannel?.name ||
|
||||||
Glow.state.activeRoom?.name ||
|
Glow.state.activeRoom?.name ||
|
||||||
"No active call";
|
"No active call";
|
||||||
|
|
||||||
Glow.els.callStatus.textContent = active
|
let statusText = state.statusText || "Idle";
|
||||||
? peerCount > 0
|
|
||||||
? `Group call active · ${peerCount} peer(s)`
|
|
||||||
: "Call open"
|
|
||||||
: "Idle";
|
|
||||||
|
|
||||||
Glow.els.startCallButton?.classList.toggle("hidden", !Glow.calls.isGroupCallContext());
|
if (!active) {
|
||||||
Glow.els.answerCallButton?.classList.add("hidden");
|
statusText = "Idle";
|
||||||
|
} else if (state.joining) {
|
||||||
|
statusText = "Joining call…";
|
||||||
|
} else if (state.reconnecting) {
|
||||||
|
statusText = "Reconnecting…";
|
||||||
|
} else if (!Glow.calls.wsReady()) {
|
||||||
|
statusText = "Connecting…";
|
||||||
|
} else if (peerCount > 0) {
|
||||||
|
statusText = `Group call active · ${peerCount} peer(s)`;
|
||||||
|
} else {
|
||||||
|
statusText = "Call open · waiting for others";
|
||||||
|
}
|
||||||
|
|
||||||
|
Glow.els.callStatus.textContent = statusText;
|
||||||
|
|
||||||
|
Glow.els.startCallButton?.classList.toggle(
|
||||||
|
"hidden",
|
||||||
|
!Glow.calls.isGroupCallContext() || active
|
||||||
|
);
|
||||||
|
Glow.els.retryCallButton?.classList.toggle(
|
||||||
|
"hidden",
|
||||||
|
!active || !state.reconnecting
|
||||||
|
);
|
||||||
Glow.els.muteCallButton?.classList.toggle("hidden", !active);
|
Glow.els.muteCallButton?.classList.toggle("hidden", !active);
|
||||||
Glow.els.hangupCallButton?.classList.toggle("hidden", !active);
|
Glow.els.hangupCallButton?.classList.toggle("hidden", !active);
|
||||||
if (Glow.els.addToCallButton) {
|
Glow.els.addToCallButton?.classList.toggle(
|
||||||
Glow.els.addToCallButton.classList.toggle("hidden", !active);
|
"hidden",
|
||||||
}
|
!active || !state.permissions?.can_invite
|
||||||
|
);
|
||||||
|
Glow.els.endCallButton?.classList.toggle(
|
||||||
|
"hidden",
|
||||||
|
!active || !state.permissions?.can_end
|
||||||
|
);
|
||||||
|
|
||||||
Glow.els.muteCallButton.textContent =
|
Glow.els.muteCallButton.textContent =
|
||||||
state.muted ? "Unmute" : "Mute";
|
state.muted ? "Unmute" : "Mute";
|
||||||
|
|
||||||
|
Glow.calls.renderParticipants();
|
||||||
|
Glow.calls.renderPendingInvites();
|
||||||
|
Glow.calls.renderHeaderCallChip();
|
||||||
};
|
};
|
||||||
|
|
||||||
Glow.openCallPanel = () => {
|
Glow.openCallPanel = async () => {
|
||||||
Glow.els.callOverlay?.classList.remove("hidden");
|
Glow.els.callOverlay?.classList.remove("hidden");
|
||||||
|
if (Glow.state.calls.activeCall?.groupSession?.id) {
|
||||||
|
await Glow.calls.refreshBackendParticipants();
|
||||||
|
}
|
||||||
Glow.renderCallUi();
|
Glow.renderCallUi();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -499,6 +872,9 @@ Glow.startCall = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
Glow.state.calls.joining = true;
|
||||||
|
Glow.calls.setStatusText("Preparing call…");
|
||||||
|
|
||||||
const session = await Glow.calls.fetchGroupCallSession(scope);
|
const session = await Glow.calls.fetchGroupCallSession(scope);
|
||||||
Glow.state.calls.session = session;
|
Glow.state.calls.session = session;
|
||||||
Glow.state.calls.activeCall = {
|
Glow.state.calls.activeCall = {
|
||||||
@ -507,17 +883,28 @@ Glow.startCall = async () => {
|
|||||||
scope,
|
scope,
|
||||||
groupSession: session.group_session
|
groupSession: session.group_session
|
||||||
};
|
};
|
||||||
Glow.state.calls.selfPeerId = session.peer_id;
|
Glow.state.calls.scope = session.scope || scope;
|
||||||
|
Glow.state.calls.permissions = session.permissions || Glow.state.calls.permissions;
|
||||||
|
Glow.state.calls.selfPeerId = session.peer_id || null;
|
||||||
Glow.state.calls.peers = [];
|
Glow.state.calls.peers = [];
|
||||||
Glow.state.calls.peerConnections = new Map();
|
Glow.state.calls.peerConnections = new Map();
|
||||||
Glow.state.calls.remoteStreams = new Map();
|
Glow.state.calls.remoteStreams = new Map();
|
||||||
|
Glow.state.calls.participantAudioEls = new Map();
|
||||||
|
Glow.state.calls.backendParticipants = session.participants || [];
|
||||||
|
|
||||||
Glow.openCallPanel();
|
Glow.openCallPanel();
|
||||||
|
Glow.calls.setStatusText("Connecting microphone…");
|
||||||
await Glow.calls.ensureLocalAudio();
|
await Glow.calls.ensureLocalAudio();
|
||||||
|
Glow.calls.setStatusText("Joining call…");
|
||||||
|
await Glow.apiJoinGroupCall(session.group_session.id);
|
||||||
await Glow.calls.connectWebSocket();
|
await Glow.calls.connectWebSocket();
|
||||||
|
Glow.calls.startParticipantSync();
|
||||||
|
Glow.calls.startHeartbeat();
|
||||||
|
Glow.state.calls.joining = false;
|
||||||
Glow.renderCallUi();
|
Glow.renderCallUi();
|
||||||
Glow.showToast("Group call ready.");
|
Glow.showToast("Group call ready.");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
Glow.state.calls.joining = false;
|
||||||
Glow.showToast(error.message || "Could not start group call.", "error");
|
Glow.showToast(error.message || "Could not start group call.", "error");
|
||||||
await Glow.endLocalCallUi();
|
await Glow.endLocalCallUi();
|
||||||
}
|
}
|
||||||
@ -533,12 +920,56 @@ Glow.toggleMuteCall = () => {
|
|||||||
Glow.renderCallUi();
|
Glow.renderCallUi();
|
||||||
};
|
};
|
||||||
|
|
||||||
Glow.hangupCall = async () => {
|
Glow.leaveCall = async () => {
|
||||||
|
Glow.calls.ensureState();
|
||||||
|
const activeCall = Glow.state.calls.activeCall;
|
||||||
|
if (!activeCall) return;
|
||||||
|
|
||||||
|
Glow.state.calls.leaving = true;
|
||||||
|
Glow.calls.setStatusText("Leaving call…");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Glow.calls.sendSignal({ type: "hangup" });
|
Glow.calls.sendSignal({ type: "hangup" });
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (activeCall.groupSession?.id) {
|
||||||
|
await Glow.apiLeaveGroupCall(activeCall.groupSession.id);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
Glow.showToast("Left call.");
|
||||||
|
Glow.state.calls.leaving = false;
|
||||||
|
await Glow.endLocalCallUi();
|
||||||
|
};
|
||||||
|
|
||||||
|
Glow.endCallForEveryone = async () => {
|
||||||
|
Glow.calls.ensureState();
|
||||||
|
const activeCall = Glow.state.calls.activeCall;
|
||||||
|
if (!activeCall?.groupSession?.id) return;
|
||||||
|
|
||||||
|
const values = await Glow.openDialog({
|
||||||
|
title: "End call for everyone?",
|
||||||
|
body: "This will end the active call session for all participants.",
|
||||||
|
confirmText: "End call",
|
||||||
|
cancelText: "Cancel",
|
||||||
|
danger: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!values) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Glow.calls.setStatusText("Ending call…");
|
||||||
|
await Glow.apiEndGroupCall(activeCall.groupSession.id);
|
||||||
Glow.showToast("Call ended.");
|
Glow.showToast("Call ended.");
|
||||||
await Glow.endLocalCallUi();
|
await Glow.endLocalCallUi();
|
||||||
|
} catch (error) {
|
||||||
|
Glow.showToast(error.message || "Could not end call.", "error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Glow.hangupCall = async () => {
|
||||||
|
await Glow.leaveCall();
|
||||||
};
|
};
|
||||||
|
|
||||||
Glow.calls.requestInvite = async username => {
|
Glow.calls.requestInvite = async username => {
|
||||||
@ -548,13 +979,9 @@ Glow.calls.requestInvite = async username => {
|
|||||||
throw new Error("No active group call session.");
|
throw new Error("No active group call session.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Glow.apiRequest("/api/group-calls/invite", {
|
return Glow.apiInviteToGroupCall({
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
group_call_session_id: activeCall.groupSession.id,
|
group_call_session_id: activeCall.groupSession.id,
|
||||||
username
|
username
|
||||||
})
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -591,17 +1018,14 @@ Glow.openAddToCallDialog = async () => {
|
|||||||
Glow.calls.fetchInvites = async () => {
|
Glow.calls.fetchInvites = async () => {
|
||||||
Glow.calls.ensureState();
|
Glow.calls.ensureState();
|
||||||
if (!Glow.state.currentUser) return [];
|
if (!Glow.state.currentUser) return [];
|
||||||
const data = await Glow.apiRequest("/api/group-calls/invites");
|
const data = await Glow.apiListGroupCallInvites();
|
||||||
Glow.state.calls.invites = data.invites || [];
|
Glow.state.calls.invites = data.invites || [];
|
||||||
|
Glow.calls.renderPendingInvites();
|
||||||
return Glow.state.calls.invites;
|
return Glow.state.calls.invites;
|
||||||
};
|
};
|
||||||
|
|
||||||
Glow.calls.acceptInvite = async inviteId => {
|
Glow.calls.acceptInvite = async inviteId => {
|
||||||
const data = await Glow.apiRequest(`/api/group-calls/invites/${encodeURIComponent(inviteId)}/accept`, {
|
const data = await Glow.apiAcceptGroupCallInvite(inviteId);
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({})
|
|
||||||
});
|
|
||||||
|
|
||||||
const helperResp = await fetch(`${data.calls_base}/api/call/session`, {
|
const helperResp = await fetch(`${data.calls_base}/api/call/session`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -619,18 +1043,19 @@ Glow.calls.acceptInvite = async inviteId => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
helperData.calls_base = data.calls_base;
|
helperData.calls_base = data.calls_base;
|
||||||
helperData.group_session = {
|
helperData.group_session = data.session;
|
||||||
helper_call_id: data.call_id
|
helperData.participants = data.participants || [];
|
||||||
|
helperData.permissions = data.permissions || {
|
||||||
|
can_manage: false,
|
||||||
|
can_end: false,
|
||||||
|
can_invite: false
|
||||||
};
|
};
|
||||||
|
helperData.scope = data.scope || null;
|
||||||
return helperData;
|
return helperData;
|
||||||
};
|
};
|
||||||
|
|
||||||
Glow.calls.dismissInvite = async inviteId => {
|
Glow.calls.dismissInvite = async inviteId => {
|
||||||
await Glow.apiRequest(`/api/group-calls/invites/${encodeURIComponent(inviteId)}/dismiss`, {
|
await Glow.apiDismissGroupCallInvite(inviteId);
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({})
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Glow.calls.openIncomingInviteDialog = async invite => {
|
Glow.calls.openIncomingInviteDialog = async invite => {
|
||||||
@ -649,6 +1074,9 @@ Glow.calls.openIncomingInviteDialog = async invite => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
Glow.state.calls.joining = true;
|
||||||
|
Glow.calls.setStatusText("Joining call…");
|
||||||
|
|
||||||
const session = await Glow.calls.acceptInvite(invite.id);
|
const session = await Glow.calls.acceptInvite(invite.id);
|
||||||
|
|
||||||
Glow.state.calls.session = session;
|
Glow.state.calls.session = session;
|
||||||
@ -658,17 +1086,30 @@ Glow.calls.openIncomingInviteDialog = async invite => {
|
|||||||
scope: null,
|
scope: null,
|
||||||
groupSession: session.group_session
|
groupSession: session.group_session
|
||||||
};
|
};
|
||||||
Glow.state.calls.selfPeerId = session.peer_id;
|
Glow.state.calls.scope = session.scope || null;
|
||||||
|
Glow.state.calls.permissions = session.permissions || Glow.state.calls.permissions;
|
||||||
|
Glow.state.calls.selfPeerId = session.peer_id || null;
|
||||||
Glow.state.calls.peers = [];
|
Glow.state.calls.peers = [];
|
||||||
Glow.state.calls.peerConnections = new Map();
|
Glow.state.calls.peerConnections = new Map();
|
||||||
Glow.state.calls.remoteStreams = new Map();
|
Glow.state.calls.remoteStreams = new Map();
|
||||||
|
Glow.state.calls.participantAudioEls = new Map();
|
||||||
|
Glow.state.calls.backendParticipants = session.participants || [];
|
||||||
|
|
||||||
Glow.openCallPanel();
|
Glow.openCallPanel();
|
||||||
|
Glow.calls.setStatusText("Connecting microphone…");
|
||||||
await Glow.calls.ensureLocalAudio();
|
await Glow.calls.ensureLocalAudio();
|
||||||
|
if (session.group_session?.id) {
|
||||||
|
Glow.calls.setStatusText("Joining call…");
|
||||||
|
await Glow.apiJoinGroupCall(session.group_session.id);
|
||||||
|
}
|
||||||
await Glow.calls.connectWebSocket();
|
await Glow.calls.connectWebSocket();
|
||||||
|
Glow.calls.startParticipantSync();
|
||||||
|
Glow.calls.startHeartbeat();
|
||||||
|
Glow.state.calls.joining = false;
|
||||||
Glow.renderCallUi();
|
Glow.renderCallUi();
|
||||||
Glow.showToast("Joined group call.");
|
Glow.showToast("Joined group call.");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
Glow.state.calls.joining = false;
|
||||||
Glow.showToast(error.message || "Could not join group call.", "error");
|
Glow.showToast(error.message || "Could not join group call.", "error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -697,19 +1138,6 @@ Glow.calls.scheduleInvitePolling = () => {
|
|||||||
}, document.hidden ? 12000 : 6000);
|
}, document.hidden ? 12000 : 6000);
|
||||||
};
|
};
|
||||||
|
|
||||||
Glow.syncActiveCallForDm = async () => {
|
|
||||||
Glow.renderCallUi();
|
|
||||||
};
|
|
||||||
|
|
||||||
Glow.pollCallSignals = async () => {
|
|
||||||
};
|
|
||||||
|
|
||||||
Glow.scheduleCallPolling = () => {
|
|
||||||
};
|
|
||||||
|
|
||||||
Glow.answerIncomingCall = async () => {
|
|
||||||
};
|
|
||||||
|
|
||||||
Glow.els.startCallButton?.addEventListener("click", async () => {
|
Glow.els.startCallButton?.addEventListener("click", async () => {
|
||||||
if (!Glow.state.calls.activeCall) {
|
if (!Glow.state.calls.activeCall) {
|
||||||
await Glow.startCall();
|
await Glow.startCall();
|
||||||
@ -718,8 +1146,9 @@ Glow.els.startCallButton?.addEventListener("click", async () => {
|
|||||||
await Glow.calls.callAllPeers();
|
await Glow.calls.callAllPeers();
|
||||||
});
|
});
|
||||||
|
|
||||||
Glow.els.answerCallButton?.addEventListener("click", () => {
|
Glow.els.retryCallButton?.addEventListener("click", async () => {
|
||||||
Glow.answerIncomingCall?.();
|
clearTimeout(Glow.state.calls.reconnectTimer);
|
||||||
|
Glow.calls.scheduleReconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
Glow.els.muteCallButton?.addEventListener("click", () => {
|
Glow.els.muteCallButton?.addEventListener("click", () => {
|
||||||
@ -730,6 +1159,10 @@ Glow.els.hangupCallButton?.addEventListener("click", () => {
|
|||||||
Glow.hangupCall?.();
|
Glow.hangupCall?.();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Glow.els.endCallButton?.addEventListener("click", () => {
|
||||||
|
Glow.endCallForEveryone?.();
|
||||||
|
});
|
||||||
|
|
||||||
Glow.els.addToCallButton?.addEventListener("click", () => {
|
Glow.els.addToCallButton?.addEventListener("click", () => {
|
||||||
Glow.openAddToCallDialog?.();
|
Glow.openAddToCallDialog?.();
|
||||||
});
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user