Files

182 lines
9.1 KiB
HTML

<!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>