feat: initial commit - Carinha Control Panel v1.0 com UI glassmorphism, watch-bar, emojis, braco robotico e conversa IA
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
<!doctype html><html><meta charset="utf-8"><body style="margin:0;background:black">
|
||||
<video id="fonte" autoplay muted playsinline style="width:1px;height:1px"></video>
|
||||
<script src="qualidade-auto.js"></script><script>
|
||||
'use strict';
|
||||
const video = document.getElementById('fonte');
|
||||
const sensor = document.createElement('canvas'); sensor.width = 176; sensor.height = 132;
|
||||
const ctx = sensor.getContext('2d', {willReadFrequently:true});
|
||||
let stream = null, pc = null, sender = null, sid = null, wanted = false;
|
||||
let previous = null, warmup = 0, icePending = [], tasks = Promise.resolve();
|
||||
let automatico = new QualidadeAuto(), lastStats = null, captureGeneration = 0;
|
||||
let config = {}, signature = '', movementEnabled = true, aviso = '', capacidades = {};
|
||||
let allowed = true;
|
||||
let lastMotionFrame = -1;
|
||||
function chave(c) { return JSON.stringify([c.cameraLado || 'user', c.videoResolucao || 'auto', c.videoFPS || 0]); }
|
||||
async function capturar(c) {
|
||||
const media = await navigator.mediaDevices.getUserMedia({audio:false,video:{facingMode:{ideal:c.cameraLado || 'user'},width:{ideal:640},height:{ideal:360},frameRate:{ideal:30}}});
|
||||
const track = media.getVideoTracks()[0];
|
||||
const facing = track.getSettings().facingMode;
|
||||
if (facing && facing !== (c.cameraLado || 'user')) {
|
||||
media.getTracks().forEach(t=>t.stop()); throw new Error('Câmera escolhida indisponível');
|
||||
}
|
||||
try { await ajustarTrack(track, c); } catch (e) { media.getTracks().forEach(t=>t.stop()); throw e; }
|
||||
return media;
|
||||
}
|
||||
async function ajustarTrack(track, c) {
|
||||
capacidades = track.getCapabilities ? track.getCapabilities() : {};
|
||||
aviso = '';
|
||||
const constraints = QualidadeAuto.constraints(c, capacidades); delete constraints.facingMode;
|
||||
try { await track.applyConstraints(constraints); }
|
||||
catch (e) {
|
||||
if (e.name !== 'OverconstrainedError') throw e;
|
||||
aviso = 'Combinação solicitada indisponível. Usando qualidade automática.';
|
||||
const fallback = QualidadeAuto.constraints({cameraLado:c.cameraLado}, capacidades); delete fallback.facingMode;
|
||||
await track.applyConstraints(fallback);
|
||||
}
|
||||
const s = track.getSettings();
|
||||
const bitrate = Math.max(2500000, Math.round((s.width || 640)*(s.height || 360)*(s.frameRate || 30)*.12));
|
||||
automatico = new QualidadeAuto(s.frameRate || 30, bitrate); lastStats = null;
|
||||
}
|
||||
async function configurar(c) {
|
||||
if (signature === chave(c) || !stream) { config = c; return; }
|
||||
const old = stream, oldCamera = config.cameraLado || 'user'; config = c;
|
||||
if (oldCamera !== (c.cameraLado || 'user')) {
|
||||
old.getTracks().forEach(t=>t.stop());
|
||||
const media = await capturar(c);
|
||||
if (!wanted) { media.getTracks().forEach(t=>t.stop()); return; }
|
||||
stream = media; await sender.replaceTrack(media.getVideoTracks()[0]); video.srcObject = stream; await video.play();
|
||||
} else await ajustarTrack(stream.getVideoTracks()[0], c);
|
||||
signature = chave(c); previous = null; warmup = 0;
|
||||
await aplicar(parametros());
|
||||
}
|
||||
function parametros() {
|
||||
if (aviso || ((config.videoResolucao || 'auto') === 'auto' && !config.videoFPS)) return automatico.parameters();
|
||||
return {maxFramerate:automatico.fps,maxBitrate:automatico.bitrate,scaleResolutionDownBy:1};
|
||||
}
|
||||
function sinal(msg) {
|
||||
if (sid) CarinhaRTC.enviar(JSON.stringify({id:sid, de:'app', mensagem:msg}));
|
||||
}
|
||||
function estado(msg) { CarinhaRTC.estado(JSON.stringify(msg)); }
|
||||
function parar() {
|
||||
wanted = false; captureGeneration++;
|
||||
if (pc) pc.close(); pc = null; sender = null;
|
||||
if (stream) stream.getTracks().forEach(t => t.stop());
|
||||
stream = null; video.srcObject = null; previous = null; icePending = [];
|
||||
estado({tipo:'webrtc', estado:'desligado'});
|
||||
}
|
||||
async function iniciar(id) {
|
||||
if (sid === id && wanted && stream) return;
|
||||
parar(); sid = id; wanted = true;
|
||||
const generation = captureGeneration;
|
||||
automatico = new QualidadeAuto(); lastStats = null; warmup = 0;
|
||||
await new Promise(r => setTimeout(r, 400));
|
||||
if (!wanted || captureGeneration !== generation) return;
|
||||
const media = await capturar(config); signature = chave(config);
|
||||
if (!wanted || captureGeneration !== generation) { media.getTracks().forEach(t => t.stop()); return; }
|
||||
stream = media; video.srcObject = stream; await video.play();
|
||||
pc = new RTCPeerConnection({iceServers:[]});
|
||||
sender = pc.addTrack(stream.getVideoTracks()[0], stream);
|
||||
const mine = pc;
|
||||
pc.ondatachannel = ({channel}) => {
|
||||
let lastSequence=-1;
|
||||
if (channel.label !== 'rastreamento') { channel.close(); return; }
|
||||
channel.onmessage = ({data}) => {
|
||||
if (pc!==mine || !wanted || typeof data!=='string' || data.length>2048 || !config.seguirRastreamento) return;
|
||||
try {
|
||||
const msg=JSON.parse(data), t=msg.alvo;
|
||||
if(msg.tipo!=='alvo' || !Number.isSafeInteger(msg.seq) || msg.seq<=lastSequence) return;
|
||||
if(t && (!['mao','rosto'].includes(t.tipo) || !Number.isFinite(t.x) || !Number.isFinite(t.y) || t.x<0 || t.x>1 || t.y<0 || t.y>1 ||
|
||||
(t.tipo==='mao' && !config.rastrearMaos) || (t.tipo==='rosto' && !config.rastrearRosto))) return;
|
||||
lastSequence=msg.seq; CarinhaRTC.rastrear(JSON.stringify(t || null));
|
||||
} catch(e) { console.log('Alvo inválido'); }
|
||||
};
|
||||
};
|
||||
pc.onicecandidate = e => { if (pc === mine && e.candidate) sinal({tipo:'candidate', candidate:e.candidate.toJSON()}); };
|
||||
pc.onconnectionstatechange = () => {
|
||||
if (pc !== mine) return;
|
||||
console.log('RTC estado=' + pc.connectionState);
|
||||
estado({tipo:'webrtc', estado:pc.connectionState, qualidade:'auto'});
|
||||
};
|
||||
console.log('RTC captura=' + JSON.stringify(stream.getVideoTracks()[0].getSettings()));
|
||||
}
|
||||
async function aplicar(p) {
|
||||
if (!sender) return;
|
||||
const params = sender.getParameters();
|
||||
if (!params.encodings || !params.encodings.length) return;
|
||||
Object.assign(params.encodings[0], p); params.degradationPreference = 'maintain-framerate';
|
||||
try { await sender.setParameters(params); }
|
||||
catch (e) {
|
||||
delete params.degradationPreference;
|
||||
try { await sender.setParameters(params); } catch (ignored) { console.log('RTC adaptação nativa: ' + ignored.name); }
|
||||
}
|
||||
}
|
||||
async function receber(packet) {
|
||||
if (!packet.id) { parar(); sid = null; return; }
|
||||
movementEnabled = packet.movimento !== false;
|
||||
if (packet.config) await configurar(packet.config);
|
||||
if (!wanted || sid !== packet.id) await iniciar(packet.id);
|
||||
if (!pc) return;
|
||||
for (const envelope of packet.mensagens || []) {
|
||||
const msg = envelope.mensagem;
|
||||
if (msg.tipo === 'offer') {
|
||||
await pc.setRemoteDescription({type:'offer',sdp:msg.sdp});
|
||||
for (const c of icePending) await pc.addIceCandidate(c);
|
||||
icePending = [];
|
||||
await pc.setLocalDescription(await pc.createAnswer());
|
||||
sinal({tipo:'answer',sdp:pc.localDescription.sdp});
|
||||
await aplicar(parametros());
|
||||
} else if (msg.tipo === 'candidate') {
|
||||
if (pc.remoteDescription) await pc.addIceCandidate(msg.candidate);
|
||||
else icePending.push(msg.candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
window.rtcReceber = packet => {
|
||||
allowed = true;
|
||||
tasks = tasks.then(() => allowed ? receber(packet) : undefined).catch(e => {
|
||||
console.log('RTC erro=' + e.name + ': ' + e.message);
|
||||
sinal({tipo:'erro',erro:e.name + ': ' + e.message});
|
||||
estado({tipo:'webrtc',estado:'erro',erro:e.message});
|
||||
parar();
|
||||
});
|
||||
};
|
||||
window.rtcParar = () => { allowed = false; parar(); sid = null; };
|
||||
setInterval(() => {
|
||||
if (!movementEnabled || !stream || video.readyState < 2 || video.currentTime===lastMotionFrame) return;
|
||||
lastMotionFrame=video.currentTime;
|
||||
ctx.drawImage(video,0,0,176,132);
|
||||
const d = ctx.getImageData(0,0,176,132).data;
|
||||
const current = new Uint8Array(176*132);
|
||||
let changed = 0, sx = 0, sy = 0;
|
||||
for (let i = 0; i < current.length; i++) {
|
||||
current[i] = (d[i*4]*77 + d[i*4+1]*150 + d[i*4+2]*29) >> 8;
|
||||
if (previous && Math.abs(current[i]-previous[i]) > 26) {changed++; sx += i%176; sy += Math.floor(i/176);}
|
||||
}
|
||||
previous = current;
|
||||
if (++warmup > 3) CarinhaRTC.movimento(Math.min(1,changed/(current.length*.25)), changed?sx/changed/176:.5, changed?sy/changed/132:.5);
|
||||
},33);
|
||||
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;
|
||||
let outbound, pair;
|
||||
report.forEach(s => {
|
||||
if (s.type === 'outbound-rtp' && (s.kind === 'video' || s.mediaType === 'video')) outbound = s;
|
||||
if (s.type === 'candidate-pair' && s.state === 'succeeded' && (s.nominated || s.selected)) pair = s;
|
||||
});
|
||||
if (!outbound) return;
|
||||
const fps = lastStats ? (outbound.framesEncoded-lastStats.framesEncoded)*1000/(outbound.timestamp-lastStats.timestamp) : outbound.framesPerSecond || 30;
|
||||
lastStats = outbound;
|
||||
const auto = !!aviso || ((config.videoResolucao || 'auto') === 'auto' && !config.videoFPS);
|
||||
if (auto) await aplicar(automatico.update({fps,bitrate:pair && pair.availableOutgoingBitrate || Infinity,rtt:pair && pair.currentRoundTripTime || 0}));
|
||||
const capture = stream.getVideoTracks()[0].getSettings();
|
||||
estado({tipo:'webrtc',estado:'connected',qualidade:auto?'auto':'manual',fps:Math.round(fps),largura:outbound.frameWidth,altura:outbound.frameHeight,nivel:automatico.level,aviso,captura:capture,capacidades});
|
||||
} catch (e) { console.log('RTC stats: ' + e.message); }
|
||||
finally { statsBusy = false; }
|
||||
},2000);
|
||||
</script></body></html>
|
||||
Reference in New Issue
Block a user