139 lines
6.8 KiB
JavaScript
139 lines
6.8 KiB
JavaScript
(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);
|
||
});
|
||
})();
|