Spaces:
Running
Running
Add OAuth and central signaling server support
Browse files- Update index.html with HuggingFace OAuth authentication
- Connect to central signaling server (wss://cduss-reachy-mini-central.hf.space/ws)
- Enable hf_oauth in README.md configuration
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- README.md +14 -7
- index.html +220 -118
README.md
CHANGED
|
@@ -5,21 +5,28 @@ colorFrom: blue
|
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
# Reachy Mini WebRTC Demo
|
| 11 |
|
| 12 |
-
WebRTC dashboard to connect
|
| 13 |
|
| 14 |
## Features
|
| 15 |
|
| 16 |
- Video streaming from robot camera
|
| 17 |
- Head control via data channel
|
| 18 |
-
-
|
| 19 |
|
| 20 |
-
##
|
| 21 |
|
| 22 |
-
1.
|
| 23 |
-
2.
|
| 24 |
-
3. Select
|
| 25 |
-
4.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
+
hf_oauth: true
|
| 9 |
+
hf_oauth_expiration_minutes: 480
|
| 10 |
---
|
| 11 |
|
| 12 |
# Reachy Mini WebRTC Demo
|
| 13 |
|
| 14 |
+
WebRTC dashboard to connect to your Reachy Mini robot via the central signaling server.
|
| 15 |
|
| 16 |
## Features
|
| 17 |
|
| 18 |
- Video streaming from robot camera
|
| 19 |
- Head control via data channel
|
| 20 |
+
- HuggingFace OAuth authentication
|
| 21 |
|
| 22 |
+
## How it works
|
| 23 |
|
| 24 |
+
1. Sign in with your HuggingFace account
|
| 25 |
+
2. Connect to the central signaling server
|
| 26 |
+
3. Select your robot from the list
|
| 27 |
+
4. Start the video stream
|
| 28 |
+
|
| 29 |
+
## Requirements
|
| 30 |
+
|
| 31 |
+
- Robot must be running with WebRTC enabled and connected to the central signaling server
|
| 32 |
+
- Robot must have a valid HuggingFace token configured
|
index.html
CHANGED
|
@@ -62,6 +62,15 @@
|
|
| 62 |
button:hover { background: #00a8cc; }
|
| 63 |
button:disabled { background: #555; cursor: not-allowed; }
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
.log {
|
| 66 |
background: #0a0a1a;
|
| 67 |
border-radius: 8px;
|
|
@@ -99,81 +108,118 @@
|
|
| 99 |
}
|
| 100 |
.producer-item:hover { background: #1a4a80; }
|
| 101 |
.producer-item.selected { border: 2px solid #00d4ff; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
</style>
|
| 103 |
</head>
|
| 104 |
<body>
|
| 105 |
<div class="container">
|
| 106 |
<h1>Reachy Mini WebRTC Dashboard</h1>
|
| 107 |
-
<p class="subtitle">Connect
|
| 108 |
-
|
| 109 |
-
<
|
| 110 |
-
|
| 111 |
-
<div class="card">
|
| 112 |
-
<h2>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
-
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
|
|
|
| 130 |
</div>
|
| 131 |
-
</div>
|
| 132 |
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
|
|
|
| 144 |
</div>
|
| 145 |
-
</div>
|
| 146 |
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
</div>
|
| 156 |
-
<div>
|
| 157 |
-
<
|
| 158 |
-
<
|
| 159 |
</div>
|
| 160 |
</div>
|
| 161 |
-
<div class="controls">
|
| 162 |
-
<button id="sendPoseBtn" onclick="sendHeadPose()" disabled>Send Pose</button>
|
| 163 |
-
<button id="centerBtn" onclick="centerHead()" disabled>Center</button>
|
| 164 |
-
</div>
|
| 165 |
-
</div>
|
| 166 |
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
|
|
|
| 172 |
</div>
|
| 173 |
</div>
|
| 174 |
</div>
|
| 175 |
|
| 176 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
// State
|
| 178 |
let signalingWs = null;
|
| 179 |
let peerConnection = null;
|
|
@@ -181,6 +227,90 @@
|
|
| 181 |
let selectedProducerId = null;
|
| 182 |
let myPeerId = null;
|
| 183 |
let currentSessionId = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
|
| 185 |
// Logging
|
| 186 |
function log(message, type = 'info') {
|
|
@@ -198,33 +328,25 @@
|
|
| 198 |
document.getElementById('logArea').innerHTML = '';
|
| 199 |
}
|
| 200 |
|
| 201 |
-
// Signaling
|
| 202 |
function connectSignaling() {
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
log('Please enter robot address', 'error');
|
| 206 |
return;
|
| 207 |
}
|
| 208 |
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
log(`Connecting to ${url}...`);
|
| 212 |
updateSignalingStatus('connecting');
|
| 213 |
|
| 214 |
try {
|
| 215 |
signalingWs = new WebSocket(url);
|
| 216 |
|
| 217 |
signalingWs.onopen = () => {
|
| 218 |
-
log('Connected to signaling!', 'success');
|
| 219 |
updateSignalingStatus('connected');
|
| 220 |
document.getElementById('connectBtn').disabled = true;
|
| 221 |
document.getElementById('disconnectBtn').disabled = false;
|
| 222 |
-
// Request listener role to get producer list
|
| 223 |
-
signalingWs.send(JSON.stringify({
|
| 224 |
-
type: 'setPeerStatus',
|
| 225 |
-
roles: ['listener'],
|
| 226 |
-
meta: { name: 'WebRTC Dashboard' }
|
| 227 |
-
}));
|
| 228 |
};
|
| 229 |
|
| 230 |
signalingWs.onmessage = (event) => {
|
|
@@ -234,11 +356,15 @@
|
|
| 234 |
};
|
| 235 |
|
| 236 |
signalingWs.onerror = (error) => {
|
| 237 |
-
log('WebSocket error
|
| 238 |
};
|
| 239 |
|
| 240 |
signalingWs.onclose = (event) => {
|
| 241 |
log(`Disconnected: code=${event.code}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
updateSignalingStatus('disconnected');
|
| 243 |
document.getElementById('connectBtn').disabled = false;
|
| 244 |
document.getElementById('disconnectBtn').disabled = true;
|
|
@@ -271,7 +397,15 @@
|
|
| 271 |
switch (msg.type) {
|
| 272 |
case 'welcome':
|
| 273 |
myPeerId = msg.peerId;
|
| 274 |
-
log(`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
break;
|
| 276 |
|
| 277 |
case 'list':
|
|
@@ -279,11 +413,9 @@
|
|
| 279 |
break;
|
| 280 |
|
| 281 |
case 'peerStatusChanged':
|
| 282 |
-
log(`
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
addProducer(msg.peerId, msg.meta);
|
| 286 |
-
}
|
| 287 |
break;
|
| 288 |
|
| 289 |
case 'sessionStarted':
|
|
@@ -291,12 +423,6 @@
|
|
| 291 |
log(`Session started: ${msg.sessionId.substring(0, 8)}...`, 'success');
|
| 292 |
break;
|
| 293 |
|
| 294 |
-
case 'startSession':
|
| 295 |
-
// Robot is starting session with us (we're consumer)
|
| 296 |
-
currentSessionId = msg.sessionId;
|
| 297 |
-
log(`Session from robot: ${msg.sessionId.substring(0, 8)}...`, 'success');
|
| 298 |
-
break;
|
| 299 |
-
|
| 300 |
case 'peer':
|
| 301 |
handlePeerMessage(msg);
|
| 302 |
break;
|
|
@@ -317,38 +443,20 @@
|
|
| 317 |
container.innerHTML = '';
|
| 318 |
|
| 319 |
if (!producers || !Array.isArray(producers) || producers.length === 0) {
|
| 320 |
-
container.innerHTML = '<em style="color: #666;">No
|
| 321 |
return;
|
| 322 |
}
|
| 323 |
|
| 324 |
for (const producer of producers) {
|
| 325 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
}
|
| 327 |
|
| 328 |
-
log(`Found ${producers.length}
|
| 329 |
-
}
|
| 330 |
-
|
| 331 |
-
function addProducer(peerId, meta) {
|
| 332 |
-
const container = document.getElementById('producerList');
|
| 333 |
-
// Check if already exists
|
| 334 |
-
if (document.getElementById(`producer-${peerId}`)) return;
|
| 335 |
-
|
| 336 |
-
// Clear "no streams" message if present
|
| 337 |
-
if (container.querySelector('em')) {
|
| 338 |
-
container.innerHTML = '';
|
| 339 |
-
}
|
| 340 |
-
addProducerElement(peerId, meta);
|
| 341 |
-
}
|
| 342 |
-
|
| 343 |
-
function addProducerElement(peerId, meta) {
|
| 344 |
-
const container = document.getElementById('producerList');
|
| 345 |
-
const div = document.createElement('div');
|
| 346 |
-
div.id = `producer-${peerId}`;
|
| 347 |
-
div.className = 'producer-item';
|
| 348 |
-
const name = meta?.name || 'Reachy Mini';
|
| 349 |
-
div.innerHTML = `<strong>${name}</strong><br><small style="color: #888;">${peerId.substring(0, 8)}...</small>`;
|
| 350 |
-
div.onclick = () => selectProducer(peerId, div);
|
| 351 |
-
container.appendChild(div);
|
| 352 |
}
|
| 353 |
|
| 354 |
function selectProducer(peerId, element) {
|
|
@@ -362,7 +470,7 @@
|
|
| 362 |
// WebRTC
|
| 363 |
async function startStream() {
|
| 364 |
if (!selectedProducerId) {
|
| 365 |
-
log('No
|
| 366 |
return;
|
| 367 |
}
|
| 368 |
|
|
@@ -402,12 +510,9 @@
|
|
| 402 |
document.getElementById('stopStreamBtn').disabled = false;
|
| 403 |
document.getElementById('sendPoseBtn').disabled = false;
|
| 404 |
document.getElementById('centerBtn').disabled = false;
|
| 405 |
-
} else if (peerConnection.iceConnectionState === 'failed'
|
| 406 |
-
peerConnection.iceConnectionState === 'disconnected') {
|
| 407 |
updateWebrtcStatus('disconnected');
|
| 408 |
-
|
| 409 |
-
log('Connection failed', 'error');
|
| 410 |
-
}
|
| 411 |
}
|
| 412 |
};
|
| 413 |
|
|
@@ -416,10 +521,10 @@
|
|
| 416 |
dataChannel = event.channel;
|
| 417 |
dataChannel.onopen = () => log('Data channel open', 'success');
|
| 418 |
dataChannel.onclose = () => log('Data channel closed');
|
| 419 |
-
dataChannel.onmessage = (e) => log(`
|
| 420 |
};
|
| 421 |
|
| 422 |
-
log('Requesting session...');
|
| 423 |
signalingWs.send(JSON.stringify({
|
| 424 |
type: 'startSession',
|
| 425 |
peerId: selectedProducerId
|
|
@@ -510,9 +615,6 @@
|
|
| 510 |
document.getElementById('pitchInput').value = 0;
|
| 511 |
sendHeadPose();
|
| 512 |
}
|
| 513 |
-
|
| 514 |
-
// Initialize
|
| 515 |
-
log('Ready. Enter robot address and click Connect.', 'info');
|
| 516 |
</script>
|
| 517 |
</body>
|
| 518 |
</html>
|
|
|
|
| 62 |
button:hover { background: #00a8cc; }
|
| 63 |
button:disabled { background: #555; cursor: not-allowed; }
|
| 64 |
|
| 65 |
+
.btn-hf {
|
| 66 |
+
background: #ff9d00;
|
| 67 |
+
color: #000;
|
| 68 |
+
width: 100%;
|
| 69 |
+
padding: 12px 20px;
|
| 70 |
+
font-size: 1.1em;
|
| 71 |
+
}
|
| 72 |
+
.btn-hf:hover { background: #ffb340; }
|
| 73 |
+
|
| 74 |
.log {
|
| 75 |
background: #0a0a1a;
|
| 76 |
border-radius: 8px;
|
|
|
|
| 108 |
}
|
| 109 |
.producer-item:hover { background: #1a4a80; }
|
| 110 |
.producer-item.selected { border: 2px solid #00d4ff; }
|
| 111 |
+
|
| 112 |
+
.user-info {
|
| 113 |
+
display: flex;
|
| 114 |
+
align-items: center;
|
| 115 |
+
gap: 10px;
|
| 116 |
+
padding: 10px;
|
| 117 |
+
background: #0f3460;
|
| 118 |
+
border-radius: 8px;
|
| 119 |
+
margin-bottom: 15px;
|
| 120 |
+
}
|
| 121 |
+
.user-info .username { flex: 1; font-weight: 500; }
|
| 122 |
+
.user-info button { padding: 6px 12px; font-size: 0.9em; }
|
| 123 |
+
|
| 124 |
+
#login-view, #main-view { display: none; }
|
| 125 |
</style>
|
| 126 |
</head>
|
| 127 |
<body>
|
| 128 |
<div class="container">
|
| 129 |
<h1>Reachy Mini WebRTC Dashboard</h1>
|
| 130 |
+
<p class="subtitle">Connect to your robot via central signaling server</p>
|
| 131 |
+
|
| 132 |
+
<!-- Login View -->
|
| 133 |
+
<div id="login-view">
|
| 134 |
+
<div class="card" style="max-width: 400px; margin: 50px auto; text-align: center;">
|
| 135 |
+
<h2>Sign In Required</h2>
|
| 136 |
+
<p style="color: #888; margin-bottom: 20px;">Sign in with your HuggingFace account to connect to your robot.</p>
|
| 137 |
+
<button class="btn-hf" onclick="loginToHuggingFace()">
|
| 138 |
+
Sign in with Hugging Face
|
| 139 |
+
</button>
|
| 140 |
+
</div>
|
| 141 |
+
</div>
|
| 142 |
|
| 143 |
+
<!-- Main View (after login) -->
|
| 144 |
+
<div id="main-view">
|
| 145 |
+
<div class="grid">
|
| 146 |
+
<!-- Connection Panel -->
|
| 147 |
+
<div class="card">
|
| 148 |
+
<h2>1. Connection</h2>
|
| 149 |
+
|
| 150 |
+
<div class="user-info">
|
| 151 |
+
<span>Signed in as</span>
|
| 152 |
+
<span class="username" id="username">@user</span>
|
| 153 |
+
<button onclick="logout()">Sign out</button>
|
| 154 |
+
</div>
|
| 155 |
|
| 156 |
+
<div class="controls">
|
| 157 |
+
<button id="connectBtn" onclick="connectSignaling()">Connect to Server</button>
|
| 158 |
+
<button id="disconnectBtn" onclick="disconnectSignaling()" disabled>Disconnect</button>
|
| 159 |
+
</div>
|
| 160 |
|
| 161 |
+
<div style="margin-top: 15px;">
|
| 162 |
+
<span>Status: </span>
|
| 163 |
+
<span id="signalingStatus" class="status disconnected">Disconnected</span>
|
| 164 |
+
</div>
|
| 165 |
|
| 166 |
+
<h3 style="margin-top: 20px; font-size: 1em;">Available Robots:</h3>
|
| 167 |
+
<div id="producerList" class="producer-list">
|
| 168 |
+
<em style="color: #666;">Connect to signaling server first</em>
|
| 169 |
+
</div>
|
| 170 |
</div>
|
|
|
|
| 171 |
|
| 172 |
+
<!-- Video Panel -->
|
| 173 |
+
<div class="card">
|
| 174 |
+
<h2>2. Video Stream</h2>
|
| 175 |
+
<video id="remoteVideo" autoplay playsinline muted></video>
|
| 176 |
+
<div class="controls">
|
| 177 |
+
<button id="startStreamBtn" onclick="startStream()" disabled>Start Stream</button>
|
| 178 |
+
<button id="stopStreamBtn" onclick="stopStream()" disabled>Stop Stream</button>
|
| 179 |
+
</div>
|
| 180 |
+
<div style="margin-top: 15px;">
|
| 181 |
+
<span>WebRTC: </span>
|
| 182 |
+
<span id="webrtcStatus" class="status disconnected">Not Connected</span>
|
| 183 |
+
</div>
|
| 184 |
</div>
|
|
|
|
| 185 |
|
| 186 |
+
<!-- Control Panel -->
|
| 187 |
+
<div class="card">
|
| 188 |
+
<h2>3. Head Control</h2>
|
| 189 |
+
<p style="color: #888; font-size: 0.9em;">Send head pose commands via data channel</p>
|
| 190 |
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
| 191 |
+
<div>
|
| 192 |
+
<label>Yaw (deg):</label>
|
| 193 |
+
<input type="number" id="yawInput" value="0" min="-45" max="45">
|
| 194 |
+
</div>
|
| 195 |
+
<div>
|
| 196 |
+
<label>Pitch (deg):</label>
|
| 197 |
+
<input type="number" id="pitchInput" value="0" min="-30" max="30">
|
| 198 |
+
</div>
|
| 199 |
</div>
|
| 200 |
+
<div class="controls">
|
| 201 |
+
<button id="sendPoseBtn" onclick="sendHeadPose()" disabled>Send Pose</button>
|
| 202 |
+
<button id="centerBtn" onclick="centerHead()" disabled>Center</button>
|
| 203 |
</div>
|
| 204 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
|
| 206 |
+
<!-- Log Panel -->
|
| 207 |
+
<div class="card">
|
| 208 |
+
<h2>Debug Log</h2>
|
| 209 |
+
<div id="logArea" class="log"></div>
|
| 210 |
+
<button onclick="clearLog()" style="margin-top: 10px; width: 100%;">Clear Log</button>
|
| 211 |
+
</div>
|
| 212 |
</div>
|
| 213 |
</div>
|
| 214 |
</div>
|
| 215 |
|
| 216 |
+
<!-- HuggingFace Hub library for OAuth -->
|
| 217 |
+
<script type="module">
|
| 218 |
+
import { oauthLoginUrl, oauthHandleRedirectIfPresent } from "https://cdn.jsdelivr.net/npm/@huggingface/hub@0.15.2/+esm";
|
| 219 |
+
|
| 220 |
+
// Central signaling server (same HF space owner)
|
| 221 |
+
const SIGNALING_SERVER = 'wss://cduss-reachy-mini-central.hf.space/ws';
|
| 222 |
+
|
| 223 |
// State
|
| 224 |
let signalingWs = null;
|
| 225 |
let peerConnection = null;
|
|
|
|
| 227 |
let selectedProducerId = null;
|
| 228 |
let myPeerId = null;
|
| 229 |
let currentSessionId = null;
|
| 230 |
+
let userToken = null;
|
| 231 |
+
let currentUser = null;
|
| 232 |
+
|
| 233 |
+
// Make functions available globally
|
| 234 |
+
window.loginToHuggingFace = loginToHuggingFace;
|
| 235 |
+
window.logout = logout;
|
| 236 |
+
window.connectSignaling = connectSignaling;
|
| 237 |
+
window.disconnectSignaling = disconnectSignaling;
|
| 238 |
+
window.startStream = startStream;
|
| 239 |
+
window.stopStream = stopStream;
|
| 240 |
+
window.sendHeadPose = sendHeadPose;
|
| 241 |
+
window.centerHead = centerHead;
|
| 242 |
+
window.clearLog = clearLog;
|
| 243 |
+
|
| 244 |
+
// Initialize on page load
|
| 245 |
+
document.addEventListener('DOMContentLoaded', initAuth);
|
| 246 |
+
|
| 247 |
+
async function initAuth() {
|
| 248 |
+
try {
|
| 249 |
+
// Check if returning from OAuth redirect
|
| 250 |
+
const oauthResult = await oauthHandleRedirectIfPresent();
|
| 251 |
+
|
| 252 |
+
if (oauthResult) {
|
| 253 |
+
currentUser = oauthResult.userInfo.name || oauthResult.userInfo.fullname || oauthResult.userInfo.preferred_username;
|
| 254 |
+
userToken = oauthResult.accessToken;
|
| 255 |
+
|
| 256 |
+
sessionStorage.setItem('hf_token', userToken);
|
| 257 |
+
sessionStorage.setItem('hf_username', currentUser);
|
| 258 |
+
sessionStorage.setItem('hf_token_expires', oauthResult.accessTokenExpiresAt);
|
| 259 |
+
|
| 260 |
+
log('OAuth login successful: ' + currentUser, 'success');
|
| 261 |
+
showMainView();
|
| 262 |
+
} else {
|
| 263 |
+
// Check stored session
|
| 264 |
+
const storedToken = sessionStorage.getItem('hf_token');
|
| 265 |
+
const storedUser = sessionStorage.getItem('hf_username');
|
| 266 |
+
const tokenExpires = sessionStorage.getItem('hf_token_expires');
|
| 267 |
+
|
| 268 |
+
if (storedToken && storedUser && tokenExpires && new Date(tokenExpires) > new Date()) {
|
| 269 |
+
userToken = storedToken;
|
| 270 |
+
currentUser = storedUser;
|
| 271 |
+
showMainView();
|
| 272 |
+
} else {
|
| 273 |
+
showLoginView();
|
| 274 |
+
}
|
| 275 |
+
}
|
| 276 |
+
} catch (error) {
|
| 277 |
+
console.error('Auth error:', error);
|
| 278 |
+
log('Auth error: ' + error.message, 'error');
|
| 279 |
+
showLoginView();
|
| 280 |
+
}
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
async function loginToHuggingFace() {
|
| 284 |
+
try {
|
| 285 |
+
const loginUrl = await oauthLoginUrl();
|
| 286 |
+
window.location.href = loginUrl;
|
| 287 |
+
} catch (error) {
|
| 288 |
+
console.error('Login error:', error);
|
| 289 |
+
alert('Failed to initiate login: ' + error.message);
|
| 290 |
+
}
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
function logout() {
|
| 294 |
+
sessionStorage.removeItem('hf_token');
|
| 295 |
+
sessionStorage.removeItem('hf_username');
|
| 296 |
+
sessionStorage.removeItem('hf_token_expires');
|
| 297 |
+
userToken = null;
|
| 298 |
+
currentUser = null;
|
| 299 |
+
disconnectSignaling();
|
| 300 |
+
showLoginView();
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
function showLoginView() {
|
| 304 |
+
document.getElementById('login-view').style.display = 'block';
|
| 305 |
+
document.getElementById('main-view').style.display = 'none';
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
function showMainView() {
|
| 309 |
+
document.getElementById('login-view').style.display = 'none';
|
| 310 |
+
document.getElementById('main-view').style.display = 'block';
|
| 311 |
+
document.getElementById('username').textContent = '@' + currentUser;
|
| 312 |
+
log('Ready. Click "Connect to Server" to find your robot.', 'info');
|
| 313 |
+
}
|
| 314 |
|
| 315 |
// Logging
|
| 316 |
function log(message, type = 'info') {
|
|
|
|
| 328 |
document.getElementById('logArea').innerHTML = '';
|
| 329 |
}
|
| 330 |
|
| 331 |
+
// Signaling
|
| 332 |
function connectSignaling() {
|
| 333 |
+
if (!userToken) {
|
| 334 |
+
log('Not authenticated', 'error');
|
|
|
|
| 335 |
return;
|
| 336 |
}
|
| 337 |
|
| 338 |
+
const url = `${SIGNALING_SERVER}?token=${encodeURIComponent(userToken)}`;
|
| 339 |
+
log('Connecting to signaling server...');
|
|
|
|
| 340 |
updateSignalingStatus('connecting');
|
| 341 |
|
| 342 |
try {
|
| 343 |
signalingWs = new WebSocket(url);
|
| 344 |
|
| 345 |
signalingWs.onopen = () => {
|
| 346 |
+
log('Connected to signaling server!', 'success');
|
| 347 |
updateSignalingStatus('connected');
|
| 348 |
document.getElementById('connectBtn').disabled = true;
|
| 349 |
document.getElementById('disconnectBtn').disabled = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
};
|
| 351 |
|
| 352 |
signalingWs.onmessage = (event) => {
|
|
|
|
| 356 |
};
|
| 357 |
|
| 358 |
signalingWs.onerror = (error) => {
|
| 359 |
+
log('WebSocket error', 'error');
|
| 360 |
};
|
| 361 |
|
| 362 |
signalingWs.onclose = (event) => {
|
| 363 |
log(`Disconnected: code=${event.code}`);
|
| 364 |
+
if (event.code === 4001) {
|
| 365 |
+
log('Authentication failed - please sign in again', 'error');
|
| 366 |
+
logout();
|
| 367 |
+
}
|
| 368 |
updateSignalingStatus('disconnected');
|
| 369 |
document.getElementById('connectBtn').disabled = false;
|
| 370 |
document.getElementById('disconnectBtn').disabled = true;
|
|
|
|
| 397 |
switch (msg.type) {
|
| 398 |
case 'welcome':
|
| 399 |
myPeerId = msg.peerId;
|
| 400 |
+
log(`Connected as: ${myPeerId.substring(0, 8)}...`, 'success');
|
| 401 |
+
// Register as listener to get producer announcements
|
| 402 |
+
signalingWs.send(JSON.stringify({
|
| 403 |
+
type: 'setPeerStatus',
|
| 404 |
+
roles: ['listener'],
|
| 405 |
+
meta: { name: 'WebRTC Dashboard' }
|
| 406 |
+
}));
|
| 407 |
+
// Request list of producers
|
| 408 |
+
signalingWs.send(JSON.stringify({ type: 'list' }));
|
| 409 |
break;
|
| 410 |
|
| 411 |
case 'list':
|
|
|
|
| 413 |
break;
|
| 414 |
|
| 415 |
case 'peerStatusChanged':
|
| 416 |
+
log(`Robot ${msg.peerId.substring(0, 8)}... ${msg.roles?.length ? 'connected' : 'disconnected'}`);
|
| 417 |
+
// Refresh producer list
|
| 418 |
+
signalingWs.send(JSON.stringify({ type: 'list' }));
|
|
|
|
|
|
|
| 419 |
break;
|
| 420 |
|
| 421 |
case 'sessionStarted':
|
|
|
|
| 423 |
log(`Session started: ${msg.sessionId.substring(0, 8)}...`, 'success');
|
| 424 |
break;
|
| 425 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 426 |
case 'peer':
|
| 427 |
handlePeerMessage(msg);
|
| 428 |
break;
|
|
|
|
| 443 |
container.innerHTML = '';
|
| 444 |
|
| 445 |
if (!producers || !Array.isArray(producers) || producers.length === 0) {
|
| 446 |
+
container.innerHTML = '<em style="color: #666;">No robots connected. Make sure your robot is online and connected to central server.</em>';
|
| 447 |
return;
|
| 448 |
}
|
| 449 |
|
| 450 |
for (const producer of producers) {
|
| 451 |
+
const div = document.createElement('div');
|
| 452 |
+
div.className = 'producer-item';
|
| 453 |
+
const name = producer.meta?.name || 'Reachy Mini';
|
| 454 |
+
div.innerHTML = `<strong>${name}</strong><br><small style="color: #888;">${producer.id.substring(0, 8)}...</small>`;
|
| 455 |
+
div.onclick = () => selectProducer(producer.id, div);
|
| 456 |
+
container.appendChild(div);
|
| 457 |
}
|
| 458 |
|
| 459 |
+
log(`Found ${producers.length} robot(s)`, 'success');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 460 |
}
|
| 461 |
|
| 462 |
function selectProducer(peerId, element) {
|
|
|
|
| 470 |
// WebRTC
|
| 471 |
async function startStream() {
|
| 472 |
if (!selectedProducerId) {
|
| 473 |
+
log('No robot selected', 'error');
|
| 474 |
return;
|
| 475 |
}
|
| 476 |
|
|
|
|
| 510 |
document.getElementById('stopStreamBtn').disabled = false;
|
| 511 |
document.getElementById('sendPoseBtn').disabled = false;
|
| 512 |
document.getElementById('centerBtn').disabled = false;
|
| 513 |
+
} else if (peerConnection.iceConnectionState === 'failed') {
|
|
|
|
| 514 |
updateWebrtcStatus('disconnected');
|
| 515 |
+
log('Connection failed', 'error');
|
|
|
|
|
|
|
| 516 |
}
|
| 517 |
};
|
| 518 |
|
|
|
|
| 521 |
dataChannel = event.channel;
|
| 522 |
dataChannel.onopen = () => log('Data channel open', 'success');
|
| 523 |
dataChannel.onclose = () => log('Data channel closed');
|
| 524 |
+
dataChannel.onmessage = (e) => log(`Received: ${e.data}`);
|
| 525 |
};
|
| 526 |
|
| 527 |
+
log('Requesting session with robot...');
|
| 528 |
signalingWs.send(JSON.stringify({
|
| 529 |
type: 'startSession',
|
| 530 |
peerId: selectedProducerId
|
|
|
|
| 615 |
document.getElementById('pitchInput').value = 0;
|
| 616 |
sendHeadPose();
|
| 617 |
}
|
|
|
|
|
|
|
|
|
|
| 618 |
</script>
|
| 619 |
</body>
|
| 620 |
</html>
|