feat: initial commit - Carinha Control Panel v1.0 com UI glassmorphism, watch-bar, emojis, braco robotico e conversa IA

This commit is contained in:
julio
2026-10-05 16:21:57 -03:00
commit 8217292893
114 changed files with 13145 additions and 0 deletions
+138
View File
@@ -0,0 +1,138 @@
(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);
});
})();