Files
live-reconstruction/static/index.html
2026-04-20 14:03:20 +00:00

115 lines
4.0 KiB
HTML

<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Live 3D Reconstruction — lingbot-map</title>
<style>
html, body { margin:0; padding:0; background:#111; color:#eee; font-family:ui-monospace,monospace; }
header { padding: 8px 12px; background:#1c1c1c; display:flex; justify-content:space-between; align-items:center; }
header h1 { font-size: 14px; margin: 0; font-weight: 500; }
#status { font-size: 12px; color:#9cf; }
#video-wrap { position:relative; width:100%; background:#000; }
video { width:100%; display:block; }
canvas { display:none; }
#controls { padding: 12px; display:flex; gap:8px; flex-wrap:wrap; }
button { background:#2b5; color:#000; border:0; padding:10px 16px; font-size:14px; border-radius:4px; cursor:pointer; }
button:disabled { background:#555; color:#888; cursor:not-allowed; }
#stats { padding: 8px 12px; font-size: 12px; color:#aaa; }
#link { padding: 8px 12px; font-size: 13px; }
#link a { color:#9cf; }
</style>
</head>
<body>
<header>
<h1>LIVE RECONSTRUCTION</h1>
<span id="status">idle</span>
</header>
<div id="video-wrap">
<video id="v" autoplay muted playsinline></video>
<canvas id="c" width="518" height="294"></canvas>
</div>
<div id="controls">
<button id="start">Start camera</button>
<button id="stop" disabled>Stop</button>
<label style="display:flex;align-items:center;gap:6px;font-size:13px;">
FPS <input id="fps" type="number" value="2" min="1" max="10" style="width:3em">
</label>
</div>
<div id="stats">frames sent: <span id="sent">0</span> &middot; last RTT: <span id="rtt">-</span> ms</div>
<div id="link">3D viewer → <a id="viser" target="_blank">open viser</a></div>
<script>
const video = document.getElementById("v");
const canvas = document.getElementById("c");
const ctx = canvas.getContext("2d");
const statusEl = document.getElementById("status");
const sentEl = document.getElementById("sent");
const rttEl = document.getElementById("rtt");
const btnStart = document.getElementById("start");
const btnStop = document.getElementById("stop");
const fpsInput = document.getElementById("fps");
document.getElementById("viser").href = `http://${location.hostname}:8081/`;
let ws = null;
let stream = null;
let timer = null;
let sent = 0;
function setStatus(s, ok=true){ statusEl.textContent = s; statusEl.style.color = ok? "#9cf" : "#f88"; }
btnStart.onclick = async () => {
try {
stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: { ideal: "environment" }, width: { ideal: 1280 }, height: { ideal: 720 } },
audio: false,
});
} catch (e) {
setStatus("camera denied: " + e.message, false);
return;
}
video.srcObject = stream;
await new Promise(r => video.onloadedmetadata = r);
ws = new WebSocket(`ws://${location.host}/ws`);
ws.binaryType = "arraybuffer";
ws.onopen = () => { setStatus("connected"); startLoop(); btnStart.disabled = true; btnStop.disabled = false; };
ws.onclose = () => { setStatus("disconnected", false); stopLoop(); btnStart.disabled = false; btnStop.disabled = true; };
ws.onerror = () => setStatus("ws error", false);
ws.onmessage = (ev) => {
try { const m = JSON.parse(ev.data); rttEl.textContent = m.ms; } catch (_){}
};
};
btnStop.onclick = () => {
if (ws) ws.close();
if (stream) stream.getTracks().forEach(t => t.stop());
};
function startLoop(){
const fps = Math.max(1, Math.min(10, Number(fpsInput.value) || 2));
const interval = Math.round(1000 / fps);
timer = setInterval(() => {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
if (video.videoWidth === 0) return;
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
canvas.toBlob(blob => {
if (!blob) return;
blob.arrayBuffer().then(buf => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(buf);
sent += 1;
sentEl.textContent = sent;
}
});
}, "image/jpeg", 0.7);
}, interval);
}
function stopLoop(){
if (timer) clearInterval(timer);
timer = null;
}
</script>
</body>
</html>