const STUN_CONFIG = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] };
let ws = null;
let myId = null;
let myName = '';
let localStream = null;
const peers = new Map(); // peerId -> RTCPeerConnection
// --- DOM ---
const joinScreen = document.getElementById('join-screen');
const talkieScreen = document.getElementById('talkie-screen');
const inputName = document.getElementById('input-name');
const inputRoom = document.getElementById('input-room');
const btnJoin = document.getElementById('btn-join');
const btnLeave = document.getElementById('btn-leave');
const roomLabel = document.getElementById('room-label');
const peersList = document.getElementById('peers-list');
const pttBtn = document.getElementById('ptt-btn');
const btnLockMic = document.getElementById('btn-lock-mic');
const statusText = document.getElementById('status-text');
let micLocked = false;
// --- JOIN ---
btnJoin.addEventListener('click', joinRoom);
inputRoom.addEventListener('keydown', e => { if (e.key === 'Enter') joinRoom(); });
async function joinRoom() {
const name = inputName.value.trim() || 'Anonimo';
const room = inputRoom.value.trim().toUpperCase();
if (!room) { inputRoom.focus(); return; }
myName = name;
try {
localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
} catch {
alert('Impossibile accedere al microfono. Controlla i permessi del browser.');
return;
}
// Mute microphone until PTT pressed
setMic(false);
connectWS(room, name);
}
// --- WEBSOCKET ---
function connectWS(room, name) {
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
ws = new WebSocket(`${proto}://${location.host}`);
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'join', room, name }));
showTalkieScreen(room);
};
ws.onmessage = ({ data }) => {
const msg = JSON.parse(data);
handleSignal(msg);
};
ws.onclose = () => {
leaveRoom();
};
}
async function handleSignal(msg) {
switch (msg.type) {
case 'id':
myId = msg.id;
break;
case 'peers':
// Existing peers: I (new joiner) initiate offers to each
for (const peer of msg.peers) {
addPeerToUI(peer.id, peer.name);
await createOffer(peer.id);
}
break;
case 'peer-joined':
addPeerToUI(msg.id, msg.name);
// They will send us an offer; just prep the connection
getOrCreatePC(msg.id);
break;
case 'offer':
await handleOffer(msg.from, msg.sdp);
break;
case 'answer':
await peers.get(msg.from)?.setRemoteDescription(new RTCSessionDescription(msg.sdp));
break;
case 'ice':
try {
await peers.get(msg.from)?.addIceCandidate(new RTCIceCandidate(msg.candidate));
} catch {}
break;
case 'peer-left':
peers.get(msg.id)?.close();
peers.delete(msg.id);
removePeerFromUI(msg.id);
break;
case 'talking':
setPeerTalking(msg.id, msg.active);
break;
}
}
// --- WEBRTC ---
function getOrCreatePC(peerId) {
if (peers.has(peerId)) return peers.get(peerId);
const pc = new RTCPeerConnection(STUN_CONFIG);
peers.set(peerId, pc);
// Add local tracks
localStream.getTracks().forEach(track => pc.addTrack(track, localStream));
// Remote audio
pc.ontrack = ({ streams }) => {
attachAudio(peerId, streams[0]);
};
// ICE candidates
pc.onicecandidate = ({ candidate }) => {
if (candidate) {
ws.send(JSON.stringify({ type: 'ice', to: peerId, candidate }));
}
};
pc.onconnectionstatechange = () => {
if (['failed', 'disconnected', 'closed'].includes(pc.connectionState)) {
peers.delete(peerId);
}
};
return pc;
}
async function createOffer(peerId) {
const pc = getOrCreatePC(peerId);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
ws.send(JSON.stringify({ type: 'offer', to: peerId, sdp: pc.localDescription }));
}
async function handleOffer(peerId, sdp) {
const pc = getOrCreatePC(peerId);
await pc.setRemoteDescription(new RTCSessionDescription(sdp));
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
ws.send(JSON.stringify({ type: 'answer', to: peerId, sdp: pc.localDescription }));
}
function attachAudio(peerId, stream) {
let audio = document.getElementById(`audio-${peerId}`);
if (!audio) {
audio = document.createElement('audio');
audio.id = `audio-${peerId}`;
audio.autoplay = true;
audio.style.display = 'none';
document.body.appendChild(audio);
}
audio.srcObject = stream;
}
// --- PTT ---
function setMic(enabled) {
if (!localStream) return;
localStream.getAudioTracks().forEach(t => t.enabled = enabled);
}
function startTalking() {
if (micLocked) return;
setMic(true);
pttBtn.classList.add('active');
statusText.textContent = 'Stai trasmettendo...';
statusText.style.color = '#22c55e';
ws?.send(JSON.stringify({ type: 'talking', active: true }));
}
function stopTalking() {
if (micLocked) return;
setMic(false);
pttBtn.classList.remove('active');
statusText.textContent = 'Rilascia per ascoltare';
statusText.style.color = '';
ws?.send(JSON.stringify({ type: 'talking', active: false }));
}
function toggleMicLock() {
micLocked = !micLocked;
if (micLocked) {
setMic(true);
pttBtn.classList.add('active');
btnLockMic.classList.add('locked');
btnLockMic.querySelector('svg').innerHTML = `