(function() { 'use strict'; const video = document.getElementById('cameraVideo'); const mensagem = document.getElementById('cameraMensagem'); const qualidade = document.getElementById('videoQualidade'); let pc = null, id = null, desde = 0, ice = [], busy = false, wanted = false; let opening = false, polling = false, generation = 0, retryAt = 0, last = null, outroPainel = false; let trackingChannel = null; let trackingSequence = 0; let effectiveQuality = ''; async function req(path, data) { const r = await fetch(path, {cache:'no-store',signal:AbortSignal.timeout(3500), ...(data === undefined?{}:{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)})}); const body = await r.json(); if (!r.ok) throw new Error(body.erro || 'HTTP ' + r.status); return body; } function texto(t) { mensagem.textContent = t; mensagem.hidden = false; video.hidden = true; } function parar(fechar = true) { generation++; const antiga = id; id = null; if (pc) pc.close(); pc = null; trackingChannel=null; ice = []; desde = 0; last = null; if (window.CarinhaRastreamento) window.CarinhaRastreamento.parar(); video.pause(); video.srcObject = null; video.hidden = true; if (antiga && fechar) req('/rtc/session',{acao:'fechar',id:antiga}).catch(() => {}); } async function enviar(msg, sessao) { await req('/rtc/send',{id:sessao || id,de:'pc',mensagem:msg}); } async function iniciar() { if (!wanted || pc || opening || outroPainel || Date.now() < retryAt) return; opening = true; const gen = generation; texto('Conectando vídeo ao vivo… O rosto continua no celular.'); try { const session = await req('/rtc/session',{acao:'abrir'}); if (gen !== generation || !wanted) { req('/rtc/session',{acao:'fechar',id:session.id}).catch(()=>{}); return; } id = session.id; const currentId = id; pc = new RTCPeerConnection({iceServers:[]}); const mine = pc; trackingChannel=pc.createDataChannel('rastreamento',{ordered:false,maxRetransmits:0}); pc.addTransceiver('video',{direction:'recvonly'}); pc.onicecandidate = event => { if (pc === mine && event.candidate) enviar({tipo:'candidate',candidate:event.candidate.toJSON()}, currentId).catch(()=>{}); }; pc.ontrack = event => { if (pc !== mine) return; video.srcObject = event.streams[0] || new MediaStream([event.track]); video.play().catch(e => texto('Clique em Ligar câmera para permitir o vídeo: ' + e.message)); }; pc.onconnectionstatechange = () => { if (pc !== mine) return; if (pc.connectionState === 'failed') { parar(); retryAt = Date.now()+2000; texto('Reconectando o vídeo…'); } }; await pc.setLocalDescription(await pc.createOffer()); if (pc !== mine) return; await enviar({tipo:'offer',sdp:pc.localDescription.sdp}, currentId); } catch (e) { parar(); retryAt = Date.now()+2500; texto('Falha ao iniciar vídeo: ' + e.message); } finally { opening = false; } } async function poll() { if (!id || !pc || polling) return; polling = true; const mine = pc, sid = id; try { const packet = await req('/rtc/poll?lado=pc&desde='+desde+'&id='+sid); if (pc !== mine) return; if (packet.id !== sid) { outroPainel = !!packet.id; parar(false); texto(outroPainel ? 'Vídeo aberto em outro painel. Recarregue para assumir o controle.' : 'Aguardando reconexão do vídeo…'); return; } for (const envelope of packet.mensagens) { const msg = envelope.mensagem; if (msg.tipo === 'answer') { await pc.setRemoteDescription({type:'answer',sdp:msg.sdp}); for (const c of ice) await pc.addIceCandidate(c); ice = []; } else if (msg.tipo === 'candidate') { if (pc.remoteDescription) await pc.addIceCandidate(msg.candidate); else ice.push(msg.candidate); } else if (msg.tipo === 'erro') { parar(); retryAt = Date.now()+3500; texto('Câmera no celular: ' + msg.erro); return; } desde = Math.max(desde,envelope.seq); } } catch (e) { if (pc === mine) { parar(); retryAt = Date.now()+2500; texto('Sinalização interrompida. Reconectando…'); } } finally { polling = false; } } video.onloadeddata = () => { video.hidden = false; mensagem.hidden = true; }; video.onplaying = () => { video.hidden = false; mensagem.hidden = true; }; let statsBusy = false; setInterval(async () => { if (!pc || pc.connectionState !== 'connected' || statsBusy) return; statsBusy = true; const mine = pc; try { const report = await pc.getStats(); if (pc !== mine) return; report.forEach(s => { if (s.type !== 'inbound-rtp' || (s.kind !== 'video' && s.mediaType !== 'video')) return; const fps = last ? (s.framesDecoded-last.framesDecoded)*1000/(s.timestamp-last.timestamp) : s.framesPerSecond || 0; const kbps = last ? (s.bytesReceived-last.bytesReceived)*8/(s.timestamp-last.timestamp) : 0; last = s; qualidade.textContent = (effectiveQuality==='auto' || (!effectiveQuality && appConfig?.videoResolucao === 'auto' && !appConfig?.videoFPS) ? 'Automática' : 'Manual') + ' · '+(s.frameWidth||video.videoWidth)+' × '+(s.frameHeight||video.videoHeight)+' · '+Math.round(fps)+' fps · '+Math.round(kbps)+' kb/s'; if (fps > 0) { video.hidden = false; mensagem.hidden = true; } }); } catch (e) { /* conexão em transição */ } finally { statsBusy = false; } },1000); setInterval(() => { iniciar(); poll(); },400); window.CarinhaVideo = { rastrear(alvo) { if (trackingChannel && trackingChannel.readyState==='open' && trackingChannel.bufferedAmount<4096) trackingChannel.send(JSON.stringify({tipo:'alvo',seq:++trackingSequence,alvo})); }, rastreamentoConectado() { return !!trackingChannel && trackingChannel.readyState==='open'; }, parar() { wanted = false; outroPainel = false; parar(); }, atualizar(status, config) { effectiveQuality=(status.estado || {}).video?.qualidade || ''; const liberada = config.sensorCamera !== false && (!(status.estado || {}).dormindo || config.cameraSono === true); const nova = !!config.preview && !!status.online && liberada; if (!nova) { if (wanted || pc) parar(); wanted = false; texto(!liberada ? 'Câmera desativada pelos ajustes de sensores.' : config.preview ? 'Celular sem conexão. Abra o Carinha.' : 'Transmissão desligada. O rosto e os sensores continuam funcionando.'); } else { wanted = true; iniciar(); } } }; window.addEventListener('pagehide', () => { if (id) navigator.sendBeacon('/rtc/session',new Blob([JSON.stringify({acao:'fechar',id})],{type:'application/json'})); wanted = false; parar(false); }); })();