cduss Claude Opus 4.5 commited on
Commit
b293436
·
1 Parent(s): 04560b1

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>

Files changed (2) hide show
  1. README.md +14 -7
  2. 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 directly to your Reachy Mini robot.
13
 
14
  ## Features
15
 
16
  - Video streaming from robot camera
17
  - Head control via data channel
18
- - Direct WebSocket connection to robot daemon
19
 
20
- ## Usage
21
 
22
- 1. Enter your robot's IP address (e.g., 192.168.1.95)
23
- 2. Click Connect to establish signaling connection
24
- 3. Select the robot stream and click Start Stream
25
- 4. Use the head controls to move the robot's head
 
 
 
 
 
 
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 directly to your robot</p>
108
-
109
- <div class="grid">
110
- <!-- Connection Panel -->
111
- <div class="card">
112
- <h2>1. Connection</h2>
 
 
 
 
 
 
113
 
114
- <label>Robot Address:</label>
115
- <input type="text" id="robotAddress" value="192.168.1.95" placeholder="e.g., 192.168.1.95 or reachy-mini.local">
 
 
 
 
 
 
 
 
 
 
116
 
117
- <div class="controls">
118
- <button id="connectBtn" onclick="connectSignaling()">Connect</button>
119
- <button id="disconnectBtn" onclick="disconnectSignaling()" disabled>Disconnect</button>
120
- </div>
121
 
122
- <div style="margin-top: 15px;">
123
- <span>Signaling: </span>
124
- <span id="signalingStatus" class="status disconnected">Disconnected</span>
125
- </div>
126
 
127
- <h3 style="margin-top: 20px; font-size: 1em;">Available Streams:</h3>
128
- <div id="producerList" class="producer-list">
129
- <em style="color: #666;">Connect to robot first</em>
 
130
  </div>
131
- </div>
132
 
133
- <!-- Video Panel -->
134
- <div class="card">
135
- <h2>2. Video Stream</h2>
136
- <video id="remoteVideo" autoplay playsinline muted></video>
137
- <div class="controls">
138
- <button id="startStreamBtn" onclick="startStream()" disabled>Start Stream</button>
139
- <button id="stopStreamBtn" onclick="stopStream()" disabled>Stop Stream</button>
140
- </div>
141
- <div style="margin-top: 15px;">
142
- <span>WebRTC: </span>
143
- <span id="webrtcStatus" class="status disconnected">Not Connected</span>
 
144
  </div>
145
- </div>
146
 
147
- <!-- Control Panel -->
148
- <div class="card">
149
- <h2>3. Head Control</h2>
150
- <p style="color: #888; font-size: 0.9em;">Send head pose commands via data channel</p>
151
- <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
152
- <div>
153
- <label>Yaw (deg):</label>
154
- <input type="number" id="yawInput" value="0" min="-45" max="45">
 
 
 
 
 
155
  </div>
156
- <div>
157
- <label>Pitch (deg):</label>
158
- <input type="number" id="pitchInput" value="0" min="-30" max="30">
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
- <!-- Log Panel -->
168
- <div class="card">
169
- <h2>Debug Log</h2>
170
- <div id="logArea" class="log"></div>
171
- <button onclick="clearLog()" style="margin-top: 10px; width: 100%;">Clear Log</button>
 
172
  </div>
173
  </div>
174
  </div>
175
 
176
- <script>
 
 
 
 
 
 
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 - connect to robot's daemon WebSocket proxy
202
  function connectSignaling() {
203
- const robotAddress = document.getElementById('robotAddress').value.trim();
204
- if (!robotAddress) {
205
- log('Please enter robot address', 'error');
206
  return;
207
  }
208
 
209
- // Connect to daemon's WebRTC signaling proxy endpoint
210
- const url = `ws://${robotAddress}:8000/webrtc/ws`;
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 - check robot address', '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(`My peer ID: ${myPeerId.substring(0, 8)}...`, 'success');
 
 
 
 
 
 
 
 
275
  break;
276
 
277
  case 'list':
@@ -279,11 +413,9 @@
279
  break;
280
 
281
  case 'peerStatusChanged':
282
- log(`Producer ${msg.peerId.substring(0, 8)}... ${msg.roles?.length ? 'available' : 'gone'}`);
283
- if (msg.roles && msg.roles.includes('producer')) {
284
- // Add/update producer
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 streams available</em>';
321
  return;
322
  }
323
 
324
  for (const producer of producers) {
325
- addProducerElement(producer.id, producer.meta);
 
 
 
 
 
326
  }
327
 
328
- log(`Found ${producers.length} stream(s)`, 'success');
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 stream selected', 'error');
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
- if (peerConnection.iceConnectionState === 'failed') {
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(`DC message: ${e.data}`);
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>