let currentVoiceWs = null;
let currentVoiceCallId = null;
let currentVoiceCallType = null;
let voiceWsHeartbeat = null;
let voiceActiveUsers = [];
let voiceAesKeyBytes = null;
let voiceAesCryptoKey = null;
let voiceUiContainer = null;
let micStream = null, audioContext = null, isAudioWorkletLoaded = false;
let micSourceNode = null, senderNode = null, receiverNode = null, audioEncoder = null;
let remoteAudio = {}, audioDecoders = {}, audioDecryptChains = {};
let knownVoiceUsers = new Set();
let isMicMuted = false;
let remoteVideos = {}, videoDecryptChains = {};
let camControl = null, screenControl = null;
const SPEAKING_HANGOVER_MS = 400;
let voiceDragState = false;
let voiceResizeAbort = null;
function clampVoiceWindowToViewport(popupEl) {
const r = popupEl.getBoundingClientRect();
const W = window.innerWidth, H = window.innerHeight;
const w = Math.min(r.width, W);
const h = Math.min(r.height, H);
const l = Math.min(Math.max(r.left, 0), W - w);
const t = Math.min(Math.max(r.top, 0), H - h);
if (l !== r.left || t !== r.top || w !== r.width || h !== r.height) {
popupEl.style.right = "auto";
popupEl.style.bottom = "auto";
popupEl.style.width = w + "px";
popupEl.style.height = h + "px";
popupEl.style.left = l + "px";
popupEl.style.top = t + "px";
}
}
let voiceVolumeState = {};
function getStoredVolume(type, clientId) {
let v = voiceVolumeState[`${type}:${clientId}`];
return v === undefined ? 100 : v;
}
function setVoiceStatus(state) {
let el = document.getElementById("voice-status");
if (!el) return;
el.classList.toggle("connected", state === "connected");
el.classList.toggle("connecting", state !== "connected");
let textEl = el.querySelector(".voice-status-text");
if (textEl) textEl.innerText = processBlah(state === "connected" ? "status.voice.connected" : "status.voice.connecting");
}
function setTileSpeaking(clientId, speaking) {
let tile = document.getElementById(`voice-tile-${clientId}`);
if (!tile) return;
if (tile._speakingTimer) {
clearTimeout(tile._speakingTimer);
tile._speakingTimer = null;
}
if (speaking) {
tile.classList.add("speaking");
tile._speakingTimer = setTimeout(() => {
tile._speakingTimer = null;
tile.classList.remove("speaking");
}, SPEAKING_HANGOVER_MS);
} else {
tile.classList.remove("speaking");
}
}
function clearAllSpeaking() {
for (const el of document.querySelectorAll(".voice-tile.speaking")) {
if (el._speakingTimer) { clearTimeout(el._speakingTimer); el._speakingTimer = null; }
el.classList.remove("speaking");
}
}
setInterval(() => {
const now = Date.now();
let changed = false;
for (const [decoderId, data] of Object.entries(remoteVideos)) {
if (now - data.lastUpdate > 3000) {
if (data.decoder) { try { data.decoder.close(); } catch (e) {} }
if (videoDecryptChains[decoderId]) videoDecryptChains[decoderId].clear();
delete remoteVideos[decoderId];
delete videoDecryptChains[decoderId];
changed = true;
}
}
if (changed) updateVoiceUIUsers();
}, 2000);
const voiceWorkletCode = `
class AudioProcessor extends AudioWorkletProcessor {
constructor() { super(); this.buffer = new Int16Array(1024); this.pointer = 0; }
process(inputs) {
const input = inputs[0][0];
if (!input) return true;
let sumSquares = 0;
for (let i = 0; i < input.length; i++) {
let s = Math.max(-1, Math.min(1, input[i]));
sumSquares += s * s;
this.buffer[this.pointer++] = s < 0 ? s * 0x8000 : s * 0x7FFF;
if (this.pointer >= 1024) {
const rms = Math.sqrt(sumSquares / 1024);
const db = 20 * Math.log10(rms || 0.0001);
this.port.postMessage({ audioData: this.buffer.buffer, db: db },[this.buffer.buffer]);
this.buffer = new Int16Array(1024);
this.pointer = 0;
sumSquares = 0;
}
} return true;
}
}
class AudioReceiver extends AudioWorkletProcessor {
constructor() {
super();
this.users = {};
this.volumes = {};
this.port.onmessage = (e) => {
if (e.data.action === 'clear') { this.users = {}; this.volumes = {}; return; }
if (e.data.action === 'setVolume') { this.volumes[e.data.clientId] = e.data.volume; return; }
if (e.data.action === 'remove') { delete this.users[e.data.clientId]; return; }
const { clientId, audioData } = e.data;
if (!this.users[clientId]) this.users[clientId] = { buffer: new Int16Array(65536), readPtr: 0, writePtr: 0, buffering: true };
const user = this.users[clientId];
const intData = new Int16Array(audioData);
for (let i = 0; i < intData.length; i++) {
user.buffer[user.writePtr] = intData[i];
user.writePtr = (user.writePtr + 1) % 65536;
}
};
}
process(inputs, outputs) {
const output = outputs[0][0];
if (!output) return true;
output.fill(0);
for (const id in this.users) {
const user = this.users[id];
let available = (user.writePtr - user.readPtr + 65536) % 65536;
if (user.buffering && available >= 4096) user.buffering = false;
if (!user.buffering) {
if (available < output.length) { user.buffering = true; }
else {
if (available > 24576) user.readPtr = (user.writePtr - 8192 + 65536) % 65536;
let vol = this.volumes[id] !== undefined ? this.volumes[id] : 1.0;
for (let i = 0; i < output.length; i++) {
output[i] += (user.buffer[user.readPtr] / 32768) * vol;
user.readPtr = (user.readPtr + 1) % 65536;
}
}
}
} return true;
}
}
registerProcessor('audio-sender', AudioProcessor);
registerProcessor('audio-receiver', AudioReceiver);`;
class TaskQueue {
constructor() { this.queue = []; this.isProcessing = false; }
enqueue(task) { this.queue.push(task); this.processNext(); }
async processNext() {
if (this.isProcessing || this.queue.length === 0) return;
this.isProcessing = true;
try { await this.queue.shift()(); } catch (e) { }
this.isProcessing = false;
this.processNext();
}
clear() { this.queue = []; }
}
async function initiateVoiceCall(roomId = null) {
if (currentVoiceWs) {
showBlahNotification("error:already.in.call");
return;
}
currentVoiceCallType = window.currentRoomType === "groups" ? "groups" : "dms";
currentVoiceCallId = roomId || window.currentDmId;
showAction("action.call.init", "callinit");
let res = await fetchEncrypted("room/voice/join", JSON.stringify({
string1: currentVoiceCallType,
string2: currentVoiceCallId
}));
clearAction("callinit");
if (res.startsWith("success:")) {
let bestIp = res.split("success:")[1];
if (!bestIp.startsWith("ws://") && !bestIp.startsWith("wss://")) {
bestIp = bestIp.includes("localhost") || bestIp.includes("127.0.0.1") ? "ws://" + bestIp : "wss://" + bestIp;
}
await startVoiceSession(bestIp);
} else {
showBlahNotification(res);
}
}
async function startVoiceSession(serverUrl) {
showVoiceUI();
updateVoiceUIUsers();
setVoiceStatus("connecting");
try {
const wsUrl = new URL(serverUrl);
if (!wsUrl.pathname.endsWith("/ws")) {
wsUrl.pathname = (wsUrl.pathname === "/" ? "" : wsUrl.pathname) + "/ws";
}
wsUrl.searchParams.set("room", currentVoiceCallId);
wsUrl.searchParams.set("userId", window.id);
wsUrl.searchParams.set("secret", "none");
currentVoiceWs = new WebSocket(wsUrl.toString());
currentVoiceWs.binaryType = 'arraybuffer';
currentVoiceWs.onopen = async () => {
setVoiceStatus("connected");
let btn = document.getElementById("voice-btn-mic");
if (btn) {
if (isMicMuted) btn.classList.add("voice-btn-danger");
else btn.classList.remove("voice-btn-danger");
}
const fetchUsers = async () => {
if (!currentVoiceWs || currentVoiceWs.readyState !== WebSocket.OPEN || !currentVoiceCallId) return;
try {
const baseUrl = currentVoiceWs.url.split('/ws')[0];
const httpUrl = baseUrl.replace('ws:', 'http:').replace('wss:', 'https:');
const res = await fetch(`${httpUrl}/room/users?room=${currentVoiceCallId}`);
const usersRaw = await res.json();
const users = usersRaw.map(u => String(u));
let changed = (users.length !== voiceActiveUsers.length);
for(let u of users) if(!voiceActiveUsers.includes(u)) changed = true;
if(changed) {
voiceActiveUsers = users;
updateVoiceUIUsers();
}
} catch (e) {}
};
fetchUsers();
voiceWsHeartbeat = setInterval(fetchUsers, 5000);
const micConstraints = {audio: {echoCancellation: false, noiseSuppression: false, autoGainControl: false}};
micStream = await navigator.mediaDevices.getUserMedia(micConstraints);
if (!audioContext) audioContext = new (window.AudioContext || window.webkitAudioContext)({sampleRate: 48000});
if (!isAudioWorkletLoaded) {
await audioContext.audioWorklet.addModule(URL.createObjectURL(new Blob([voiceWorkletCode], {type: 'application/javascript'})));
senderNode = new AudioWorkletNode(audioContext, 'audio-sender');
receiverNode = new AudioWorkletNode(audioContext, 'audio-receiver');
receiverNode.connect(audioContext.destination);
isAudioWorkletLoaded = true;
}
if (audioContext.state === 'suspended') await audioContext.resume();
micSourceNode = audioContext.createMediaStreamSource(micStream);
micSourceNode.connect(senderNode);
let vadHangover = 0;
senderNode.port.onmessage = async (e) => {
if (currentVoiceWs.readyState !== WebSocket.OPEN) return;
const {audioData, db} = e.data;
const sensitivity = -65;
const percent = Math.max(0, Math.min(100, (db + 80) / 80 * 100));
let micLevelBar = document.getElementById("voice-mic-level");
if (micLevelBar) micLevelBar.style.width = percent + '%';
if (db >= sensitivity) vadHangover = 75;
let isSpeaking = vadHangover > 0 && !isMicMuted;
setTileSpeaking(String(window.id), isSpeaking);
if (vadHangover > 0 && !isMicMuted) {
vadHangover--;
if (micLevelBar) micLevelBar.style.backgroundColor = 'var(--big-green)';
if (!voiceAesCryptoKey) return;
const iv = crypto.getRandomValues(new Uint8Array(12));
const cipher = await crypto.subtle.encrypt({name: "AES-GCM", iv}, voiceAesCryptoKey, audioData);
const payload = new Uint8Array(14 + cipher.byteLength);
payload[0] = 0; payload[1] = 0; payload.set(iv, 2); payload.set(new Uint8Array(cipher), 14);
currentVoiceWs.send(payload.buffer);
} else {
if (micLevelBar) micLevelBar.style.backgroundColor = 'rgba(255, 255, 255, 0.15)';
}
};
};
currentVoiceWs.onmessage = async (e) => {
const arrayBuffer = e.data;
const view = new DataView(arrayBuffer);
let clientId = view.getBigUint64(0, true).toString();
if (!voiceActiveUsers.includes(clientId) && clientId !== String(window.id)) {
const baseUrl = currentVoiceWs.url.split('/ws')[0];
const httpUrl = baseUrl.replace('ws:', 'http:').replace('wss:', 'https:');
fetch(`${httpUrl}/room/users?room=${currentVoiceCallId}`).then(res => res.json()).then(usersRaw => {
voiceActiveUsers = usersRaw.map(u => String(u));
updateVoiceUIUsers();
}).catch(()=>{});
}
const type = view.getUint8(8);
const isKeyFrame = view.getUint8(9) === 1;
const iv = new Uint8Array(arrayBuffer, 10, 12);
const ciphertext = new Uint8Array(arrayBuffer, 22);
if (!voiceAesCryptoKey) return;
if (type === 0 || type === 4) {
const isScreenAudio = (type === 4);
const audioId = isScreenAudio ? `${clientId}_screen` : clientId;
if (!remoteAudio[audioId]) remoteAudio[audioId] = { lastUpdate: Date.now() };
remoteAudio[audioId].lastUpdate = Date.now();
if (!audioDecryptChains[audioId]) audioDecryptChains[audioId] = new TaskQueue();
audioDecryptChains[audioId].enqueue(async () => {
try {
const decryptedBuffer = await crypto.subtle.decrypt({name: "AES-GCM", iv}, voiceAesCryptoKey, ciphertext);
if(receiverNode) receiverNode.port.postMessage({ clientId: audioId, audioData: decryptedBuffer }, [decryptedBuffer]);
setTileSpeaking(clientId, true);
} catch (err) {}
});
} else if (type === 1 || type === 2) {
const decoderId = `${clientId}-${type}`;
if (!videoDecryptChains[decoderId]) videoDecryptChains[decoderId] = new TaskQueue();
videoDecryptChains[decoderId].enqueue(async () => {
try {
const decryptedBuffer = await crypto.subtle.decrypt({name: "AES-GCM", iv}, voiceAesCryptoKey, ciphertext);
if (!remoteVideos[decoderId]) {
const canvas = document.createElement('canvas');
remoteVideos[decoderId] = {
lastUpdate: Date.now(),
hasKeyFrame: false,
decoder: null,
canvas: canvas
};
updateVoiceUIUsers();
}
const streamData = remoteVideos[decoderId];
streamData.lastUpdate = Date.now();
if (!streamData.decoder) {
if (!isKeyFrame) return;
const canvas = streamData.canvas;
const ctx = canvas.getContext('2d');
streamData.decoder = new VideoDecoder({
output: (frame) => {
canvas.width = frame.displayWidth;
canvas.height = frame.displayHeight;
ctx.drawImage(frame, 0, 0, canvas.width, canvas.height);
frame.close();
},
error: e => {
if (remoteVideos[decoderId]) {
try { remoteVideos[decoderId].decoder.close(); } catch(err) {}
remoteVideos[decoderId].decoder = null;
remoteVideos[decoderId].hasKeyFrame = false;
}
}
});
streamData.decoder.configure({codec: 'av01.0.04M.08'});
streamData.hasKeyFrame = true;
}
if (!streamData.hasKeyFrame) {
if (isKeyFrame) streamData.hasKeyFrame = true;
else return;
}
const chunk = new EncodedVideoChunk({
type: isKeyFrame ? 'key' : 'delta',
timestamp: Math.trunc(performance.now() * 1000),
data: decryptedBuffer
});
try {
streamData.decoder.decode(chunk);
} catch(err) {
try { streamData.decoder.close(); } catch(e){}
streamData.decoder = null;
streamData.hasKeyFrame = false;
}
} catch (err) {}
});
} else if (type === 3) {
crypto.subtle.decrypt({name: "AES-GCM", iv}, voiceAesCryptoKey, ciphertext).then(decrypted => {
const stoppedTypeId = new Uint8Array(decrypted)[0];
if (stoppedTypeId === 4) {
const audioId = `${clientId}_screen`;
if (remoteAudio[audioId]) {
if(audioDecryptChains[audioId]) audioDecryptChains[audioId].clear();
delete audioDecryptChains[audioId]; delete remoteAudio[audioId];
if(receiverNode) receiverNode.port.postMessage({ action: 'remove', clientId: audioId });
}
} else {
const decoderId = `${clientId}-${stoppedTypeId}`;
if (remoteVideos[decoderId]) {
try { if(remoteVideos[decoderId].decoder) remoteVideos[decoderId].decoder.close(); } catch (e) {}
if(videoDecryptChains[decoderId]) videoDecryptChains[decoderId].clear();
delete remoteVideos[decoderId]; delete videoDecryptChains[decoderId];
updateVoiceUIUsers();
}
}
}).catch(() => {});
}
};
currentVoiceWs.onclose = () => {
endVoiceSession();
};
} catch(err) {
console.error(err);
showBlahNotification("error:call.connect.failed");
endVoiceSession();
}
}
function handleCallUpdate(data) {
let parts = data.split(":");
let roomId = parts[1];
let state = [];
if(parts.slice(2).join(":") !== "") {
let stateRaw = JSON.parse(parts.slice(2).join(":"));
state = stateRaw.map(u => String(u));
}
voiceActiveUsers = state;
if (currentVoiceCallId === roomId) {
let myIdStr = String(window.id);
if (!voiceAesKeyBytes && state.length === 1 && state[0] === myIdStr) {
voiceAesKeyBytes = crypto.getRandomValues(new Uint8Array(32));
crypto.subtle.importKey("raw", voiceAesKeyBytes, {name: "AES-GCM"}, false, ["encrypt", "decrypt"])
.then(k => voiceAesCryptoKey = k);
}
if (voiceAesKeyBytes) {
for (let u of state) {
if (u !== myIdStr) sendVoiceSignalToUser(u, roomId);
}
}
updateVoiceUIUsers();
for (let u in remoteAudio) {
let pureU = u.replace("_screen", "");
if (!voiceActiveUsers.includes(pureU) && pureU !== myIdStr) {
if (audioDecoders[u]) try { audioDecoders[u].close(); } catch(e){}
delete audioDecoders[u];
delete remoteAudio[u];
if(receiverNode) receiverNode.port.postMessage({ action: 'remove', clientId: u });
}
}
for (let decoderId in remoteVideos) {
let pureU = decoderId.split('-')[0];
if (!voiceActiveUsers.includes(pureU) && pureU !== myIdStr) {
if (remoteVideos[decoderId].decoder) { try { remoteVideos[decoderId].decoder.close(); } catch(e){} }
if (videoDecryptChains[decoderId]) videoDecryptChains[decoderId].clear();
delete remoteVideos[decoderId];
delete videoDecryptChains[decoderId];
updateVoiceUIUsers();
}
}
} else if (state.length > 0 && !state.includes(String(window.id))) {
// Someone is in a call, and we are not. Show incoming call banner if it's the current room.
if (window.currentDmId === roomId) {
showIncomingCallUI(roomId);
}
} else if (state.length === 0) {
dismissIncomingCall();
}
}
async function resolveCallerInfo(roomId) {
let name = window.dmUsernameCache ? window.dmUsernameCache[roomId] : null;
if (name && name.includes(':')) name = name.split(':')[0];
if (!name && window.fetchAsync) {
try {
let fetched = await window.fetchAsync(`${window.url}/idtoname?id=${roomId}`);
if (fetched && !fetched.startsWith("error:")) {
name = fetched.includes(':') ? fetched.split(':')[0] : fetched;
if (window.dmUsernameCache) window.dmUsernameCache[roomId] = name;
}
} catch (e) {}
}
let pfpUrl = window.dmPfpCache ? window.dmPfpCache[roomId] : null;
if (!pfpUrl && window.getAvatarUrl) {
try { pfpUrl = await window.getAvatarUrl(roomId, name || "User"); } catch (e) {}
if (pfpUrl && window.dmPfpCache) window.dmPfpCache[roomId] = pfpUrl;
}
return { name: name || processBlah("title.unknown"), pfpUrl };
}
function showIncomingCallUI(roomId) {
if (document.getElementById("incoming-call-popup") || currentVoiceWs) return;
let cachedName = window.dmUsernameCache ? window.dmUsernameCache[roomId] : null;
if (cachedName && cachedName.includes(':')) cachedName = cachedName.split(':')[0];
let cachedPfp = window.dmPfpCache ? window.dmPfpCache[roomId] : null;
let incomingUi = document.createElement("div");
incomingUi.id = "incoming-call-popup";
incomingUi.innerHTML = `