Spaces:
Running
Running
test latency
Browse files- index.html +67 -2
- style.css +16 -0
index.html
CHANGED
|
@@ -41,7 +41,7 @@
|
|
| 41 |
<div class="app-container">
|
| 42 |
<!-- Video -->
|
| 43 |
<div class="video-container">
|
| 44 |
-
<video id="remoteVideo" autoplay playsinline></video>
|
| 45 |
|
| 46 |
<div class="video-overlay-top">
|
| 47 |
<div class="connection-badge">
|
|
@@ -49,6 +49,9 @@
|
|
| 49 |
<span id="statusText">Disconnected</span>
|
| 50 |
</div>
|
| 51 |
<div class="robot-name" id="robotName"></div>
|
|
|
|
|
|
|
|
|
|
| 52 |
</div>
|
| 53 |
|
| 54 |
<div class="video-overlay-bottom">
|
|
@@ -159,13 +162,14 @@
|
|
| 159 |
</div>
|
| 160 |
|
| 161 |
<script type="module">
|
| 162 |
-
import { ReachyMini } from "https://cdn.jsdelivr.net/gh/pollen-robotics/reachy_mini@
|
| 163 |
|
| 164 |
const robot = new ReachyMini();
|
| 165 |
|
| 166 |
let selectedRobotId = null;
|
| 167 |
let headSlidersActive = false;
|
| 168 |
let detachVideo = null;
|
|
|
|
| 169 |
|
| 170 |
// Export functions for inline onclick handlers
|
| 171 |
window.loginToHuggingFace = () => robot.login();
|
|
@@ -214,6 +218,7 @@
|
|
| 214 |
updateStatus('connected', 'Connected');
|
| 215 |
enableControls(true);
|
| 216 |
document.getElementById('robotSelector').classList.add('hidden');
|
|
|
|
| 217 |
});
|
| 218 |
|
| 219 |
robot.addEventListener('sessionStopped', () => {
|
|
@@ -223,6 +228,7 @@
|
|
| 223 |
enableControls(false);
|
| 224 |
updateStatus('connected', 'Connected');
|
| 225 |
updateMicButton();
|
|
|
|
| 226 |
});
|
| 227 |
|
| 228 |
robot.addEventListener('state', (e) => updateStateDisplay(e.detail));
|
|
@@ -289,6 +295,65 @@
|
|
| 289 |
}
|
| 290 |
}
|
| 291 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
// ===================== Session =====================
|
| 293 |
async function startStream() {
|
| 294 |
if (!selectedRobotId) return;
|
|
|
|
| 41 |
<div class="app-container">
|
| 42 |
<!-- Video -->
|
| 43 |
<div class="video-container">
|
| 44 |
+
<video id="remoteVideo" autoplay playsinline muted></video>
|
| 45 |
|
| 46 |
<div class="video-overlay-top">
|
| 47 |
<div class="connection-badge">
|
|
|
|
| 49 |
<span id="statusText">Disconnected</span>
|
| 50 |
</div>
|
| 51 |
<div class="robot-name" id="robotName"></div>
|
| 52 |
+
<div class="latency-badge hidden" id="latencyBadge">
|
| 53 |
+
<span id="latencyValue">--</span>
|
| 54 |
+
</div>
|
| 55 |
</div>
|
| 56 |
|
| 57 |
<div class="video-overlay-bottom">
|
|
|
|
| 162 |
</div>
|
| 163 |
|
| 164 |
<script type="module">
|
| 165 |
+
import { ReachyMini } from "https://cdn.jsdelivr.net/gh/pollen-robotics/reachy_mini@invest-latency/js/reachy-mini.js";
|
| 166 |
|
| 167 |
const robot = new ReachyMini();
|
| 168 |
|
| 169 |
let selectedRobotId = null;
|
| 170 |
let headSlidersActive = false;
|
| 171 |
let detachVideo = null;
|
| 172 |
+
let latencyIntervalId = null;
|
| 173 |
|
| 174 |
// Export functions for inline onclick handlers
|
| 175 |
window.loginToHuggingFace = () => robot.login();
|
|
|
|
| 218 |
updateStatus('connected', 'Connected');
|
| 219 |
enableControls(true);
|
| 220 |
document.getElementById('robotSelector').classList.add('hidden');
|
| 221 |
+
startLatencyDisplay();
|
| 222 |
});
|
| 223 |
|
| 224 |
robot.addEventListener('sessionStopped', () => {
|
|
|
|
| 228 |
enableControls(false);
|
| 229 |
updateStatus('connected', 'Connected');
|
| 230 |
updateMicButton();
|
| 231 |
+
stopLatencyDisplay();
|
| 232 |
});
|
| 233 |
|
| 234 |
robot.addEventListener('state', (e) => updateStateDisplay(e.detail));
|
|
|
|
| 295 |
}
|
| 296 |
}
|
| 297 |
|
| 298 |
+
// ===================== Latency =====================
|
| 299 |
+
function startLatencyDisplay() {
|
| 300 |
+
const badge = document.getElementById('latencyBadge');
|
| 301 |
+
const label = document.getElementById('latencyValue');
|
| 302 |
+
badge.classList.remove('hidden');
|
| 303 |
+
|
| 304 |
+
latencyIntervalId = setInterval(async () => {
|
| 305 |
+
const video = document.getElementById('remoteVideo');
|
| 306 |
+
let bufLagMs = null;
|
| 307 |
+
let rttMs = null;
|
| 308 |
+
let jitterMs = null;
|
| 309 |
+
|
| 310 |
+
// Buffer lag (how far behind live edge)
|
| 311 |
+
if (video && video.buffered && video.buffered.length > 0) {
|
| 312 |
+
const end = video.buffered.end(video.buffered.length - 1);
|
| 313 |
+
bufLagMs = Math.round((end - video.currentTime) * 1000);
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
// WebRTC stats: RTT + jitter buffer delay
|
| 317 |
+
if (robot._pc) {
|
| 318 |
+
try {
|
| 319 |
+
const stats = await robot._pc.getStats();
|
| 320 |
+
stats.forEach(report => {
|
| 321 |
+
if (report.type === 'candidate-pair' && report.currentRoundTripTime != null) {
|
| 322 |
+
rttMs = Math.round(report.currentRoundTripTime * 1000);
|
| 323 |
+
}
|
| 324 |
+
if (report.type === 'inbound-rtp' && report.kind === 'video') {
|
| 325 |
+
if (report.jitterBufferDelay != null && report.jitterBufferEmittedCount > 0) {
|
| 326 |
+
jitterMs = Math.round((report.jitterBufferDelay / report.jitterBufferEmittedCount) * 1000);
|
| 327 |
+
}
|
| 328 |
+
}
|
| 329 |
+
});
|
| 330 |
+
} catch (_) { /* no stats yet */ }
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
// Display
|
| 334 |
+
const parts = [];
|
| 335 |
+
if (bufLagMs != null) parts.push(`buf ${bufLagMs}ms`);
|
| 336 |
+
if (rttMs != null) parts.push(`rtt ${rttMs}ms`);
|
| 337 |
+
if (jitterMs != null) parts.push(`jit ${jitterMs}ms`);
|
| 338 |
+
label.textContent = parts.length ? parts.join(' · ') : '--';
|
| 339 |
+
|
| 340 |
+
// Color based on buffer lag
|
| 341 |
+
badge.classList.remove('good', 'ok', 'bad');
|
| 342 |
+
if (bufLagMs != null) {
|
| 343 |
+
if (bufLagMs < 200) badge.classList.add('good');
|
| 344 |
+
else if (bufLagMs < 500) badge.classList.add('ok');
|
| 345 |
+
else badge.classList.add('bad');
|
| 346 |
+
}
|
| 347 |
+
}, 1000);
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
function stopLatencyDisplay() {
|
| 351 |
+
if (latencyIntervalId) { clearInterval(latencyIntervalId); latencyIntervalId = null; }
|
| 352 |
+
const badge = document.getElementById('latencyBadge');
|
| 353 |
+
badge.classList.add('hidden');
|
| 354 |
+
badge.classList.remove('good', 'ok', 'bad');
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
// ===================== Session =====================
|
| 358 |
async function startStream() {
|
| 359 |
if (!selectedRobotId) return;
|
style.css
CHANGED
|
@@ -132,6 +132,22 @@ video {
|
|
| 132 |
font-size: 0.8em;
|
| 133 |
}
|
| 134 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
.status-indicator {
|
| 136 |
width: 8px;
|
| 137 |
height: 8px;
|
|
|
|
| 132 |
font-size: 0.8em;
|
| 133 |
}
|
| 134 |
|
| 135 |
+
.latency-badge {
|
| 136 |
+
display: flex;
|
| 137 |
+
align-items: center;
|
| 138 |
+
background: rgba(0,0,0,0.5);
|
| 139 |
+
padding: 4px 10px;
|
| 140 |
+
border-radius: 12px;
|
| 141 |
+
font-size: 0.75em;
|
| 142 |
+
font-variant-numeric: tabular-nums;
|
| 143 |
+
color: var(--text-secondary);
|
| 144 |
+
font-family: 'Inter', monospace;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.latency-badge.good { color: var(--success); }
|
| 148 |
+
.latency-badge.ok { color: var(--warning); }
|
| 149 |
+
.latency-badge.bad { color: var(--danger); }
|
| 150 |
+
|
| 151 |
.status-indicator {
|
| 152 |
width: 8px;
|
| 153 |
height: 8px;
|