115 lines
4.0 KiB
HTML
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> · 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>
|