initial prototype: aiohttp + WebSocket + viser live reconstruction
This commit is contained in:
114
static/index.html
Normal file
114
static/index.html
Normal file
@@ -0,0 +1,114 @@
|
||||
<!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> · 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>
|
||||
Reference in New Issue
Block a user