Spaces:
Running
Running
Switch from WebSocket to SSE+HTTP for browser compatibility
Browse files- index.html +81 -39
index.html
CHANGED
|
@@ -217,11 +217,11 @@
|
|
| 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 (
|
| 221 |
-
const SIGNALING_SERVER = '
|
| 222 |
|
| 223 |
// State
|
| 224 |
-
let
|
| 225 |
let peerConnection = null;
|
| 226 |
let dataChannel = null;
|
| 227 |
let selectedProducerId = null;
|
|
@@ -328,50 +328,83 @@
|
|
| 328 |
document.getElementById('logArea').innerHTML = '';
|
| 329 |
}
|
| 330 |
|
| 331 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 344 |
|
| 345 |
-
|
| 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 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
};
|
| 361 |
|
| 362 |
-
|
| 363 |
-
log(
|
| 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;
|
| 371 |
document.getElementById('startStreamBtn').disabled = true;
|
| 372 |
selectedProducerId = null;
|
| 373 |
myPeerId = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
};
|
|
|
|
| 375 |
} catch (e) {
|
| 376 |
log(`Failed to connect: ${e.message}`, 'error');
|
| 377 |
updateSignalingStatus('disconnected');
|
|
@@ -379,10 +412,13 @@
|
|
| 379 |
}
|
| 380 |
|
| 381 |
function disconnectSignaling() {
|
| 382 |
-
if (
|
| 383 |
-
|
| 384 |
-
|
| 385 |
}
|
|
|
|
|
|
|
|
|
|
| 386 |
stopStream();
|
| 387 |
}
|
| 388 |
|
|
@@ -393,19 +429,17 @@
|
|
| 393 |
el.textContent = status.charAt(0).toUpperCase() + status.slice(1);
|
| 394 |
}
|
| 395 |
|
| 396 |
-
function handleSignalingMessage(msg) {
|
| 397 |
switch (msg.type) {
|
| 398 |
case 'welcome':
|
| 399 |
myPeerId = msg.peerId;
|
| 400 |
log(`Connected as: ${myPeerId.substring(0, 8)}...`, 'success');
|
| 401 |
-
// Register as listener
|
| 402 |
-
|
| 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':
|
|
@@ -415,7 +449,10 @@
|
|
| 415 |
case 'peerStatusChanged':
|
| 416 |
log(`Robot ${msg.peerId.substring(0, 8)}... ${msg.roles?.length ? 'connected' : 'disconnected'}`);
|
| 417 |
// Refresh producer list
|
| 418 |
-
|
|
|
|
|
|
|
|
|
|
| 419 |
break;
|
| 420 |
|
| 421 |
case 'sessionStarted':
|
|
@@ -488,9 +525,9 @@
|
|
| 488 |
}
|
| 489 |
};
|
| 490 |
|
| 491 |
-
peerConnection.onicecandidate = (event) => {
|
| 492 |
if (event.candidate && currentSessionId) {
|
| 493 |
-
|
| 494 |
type: 'peer',
|
| 495 |
sessionId: currentSessionId,
|
| 496 |
ice: {
|
|
@@ -498,7 +535,7 @@
|
|
| 498 |
sdpMLineIndex: event.candidate.sdpMLineIndex,
|
| 499 |
sdpMid: event.candidate.sdpMid
|
| 500 |
}
|
| 501 |
-
})
|
| 502 |
}
|
| 503 |
};
|
| 504 |
|
|
@@ -525,10 +562,15 @@
|
|
| 525 |
};
|
| 526 |
|
| 527 |
log('Requesting session with robot...');
|
| 528 |
-
|
| 529 |
type: 'startSession',
|
| 530 |
peerId: selectedProducerId
|
| 531 |
-
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
}
|
| 533 |
|
| 534 |
async function handlePeerMessage(msg) {
|
|
@@ -542,11 +584,11 @@
|
|
| 542 |
if (msg.sdp.type === 'offer') {
|
| 543 |
const answer = await peerConnection.createAnswer();
|
| 544 |
await peerConnection.setLocalDescription(answer);
|
| 545 |
-
|
| 546 |
type: 'peer',
|
| 547 |
sessionId: currentSessionId,
|
| 548 |
sdp: { type: 'answer', sdp: answer.sdp }
|
| 549 |
-
})
|
| 550 |
log('Sent SDP answer');
|
| 551 |
}
|
| 552 |
}
|
|
|
|
| 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 (HTTP/SSE instead of WebSocket)
|
| 221 |
+
const SIGNALING_SERVER = 'https://cduss-reachy-mini-central.hf.space';
|
| 222 |
|
| 223 |
// State
|
| 224 |
+
let eventSource = null;
|
| 225 |
let peerConnection = null;
|
| 226 |
let dataChannel = null;
|
| 227 |
let selectedProducerId = null;
|
|
|
|
| 328 |
document.getElementById('logArea').innerHTML = '';
|
| 329 |
}
|
| 330 |
|
| 331 |
+
// Send message to server via HTTP POST
|
| 332 |
+
async function sendToServer(message) {
|
| 333 |
+
if (!userToken) {
|
| 334 |
+
log('Not authenticated', 'error');
|
| 335 |
+
return null;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
try {
|
| 339 |
+
const response = await fetch(`${SIGNALING_SERVER}/send?token=${encodeURIComponent(userToken)}`, {
|
| 340 |
+
method: 'POST',
|
| 341 |
+
headers: { 'Content-Type': 'application/json' },
|
| 342 |
+
body: JSON.stringify(message)
|
| 343 |
+
});
|
| 344 |
+
|
| 345 |
+
if (!response.ok) {
|
| 346 |
+
log(`Server error: ${response.status}`, 'error');
|
| 347 |
+
return null;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
return await response.json();
|
| 351 |
+
} catch (e) {
|
| 352 |
+
log(`Failed to send: ${e.message}`, 'error');
|
| 353 |
+
return null;
|
| 354 |
+
}
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
// Signaling via SSE
|
| 358 |
function connectSignaling() {
|
| 359 |
if (!userToken) {
|
| 360 |
log('Not authenticated', 'error');
|
| 361 |
return;
|
| 362 |
}
|
| 363 |
|
| 364 |
+
const url = `${SIGNALING_SERVER}/events?token=${encodeURIComponent(userToken)}`;
|
| 365 |
+
log('Connecting to signaling server (SSE)...');
|
| 366 |
updateSignalingStatus('connecting');
|
| 367 |
|
| 368 |
try {
|
| 369 |
+
eventSource = new EventSource(url);
|
| 370 |
|
| 371 |
+
eventSource.onopen = () => {
|
| 372 |
log('Connected to signaling server!', 'success');
|
| 373 |
updateSignalingStatus('connected');
|
| 374 |
document.getElementById('connectBtn').disabled = true;
|
| 375 |
document.getElementById('disconnectBtn').disabled = false;
|
| 376 |
};
|
| 377 |
|
| 378 |
+
eventSource.addEventListener('message', (event) => {
|
| 379 |
+
try {
|
| 380 |
+
const msg = JSON.parse(event.data);
|
| 381 |
+
log(`Received: ${msg.type}`);
|
| 382 |
+
handleSignalingMessage(msg);
|
| 383 |
+
} catch (e) {
|
| 384 |
+
console.error('Failed to parse message:', e);
|
| 385 |
+
}
|
| 386 |
+
});
|
| 387 |
|
| 388 |
+
eventSource.addEventListener('ping', () => {
|
| 389 |
+
// Keepalive, ignore
|
| 390 |
+
});
|
| 391 |
|
| 392 |
+
eventSource.onerror = (error) => {
|
| 393 |
+
log('Connection error', 'error');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
updateSignalingStatus('disconnected');
|
| 395 |
document.getElementById('connectBtn').disabled = false;
|
| 396 |
document.getElementById('disconnectBtn').disabled = true;
|
| 397 |
document.getElementById('startStreamBtn').disabled = true;
|
| 398 |
selectedProducerId = null;
|
| 399 |
myPeerId = null;
|
| 400 |
+
|
| 401 |
+
// Auto-reconnect after 5 seconds
|
| 402 |
+
if (eventSource) {
|
| 403 |
+
eventSource.close();
|
| 404 |
+
eventSource = null;
|
| 405 |
+
}
|
| 406 |
};
|
| 407 |
+
|
| 408 |
} catch (e) {
|
| 409 |
log(`Failed to connect: ${e.message}`, 'error');
|
| 410 |
updateSignalingStatus('disconnected');
|
|
|
|
| 412 |
}
|
| 413 |
|
| 414 |
function disconnectSignaling() {
|
| 415 |
+
if (eventSource) {
|
| 416 |
+
eventSource.close();
|
| 417 |
+
eventSource = null;
|
| 418 |
}
|
| 419 |
+
updateSignalingStatus('disconnected');
|
| 420 |
+
document.getElementById('connectBtn').disabled = false;
|
| 421 |
+
document.getElementById('disconnectBtn').disabled = true;
|
| 422 |
stopStream();
|
| 423 |
}
|
| 424 |
|
|
|
|
| 429 |
el.textContent = status.charAt(0).toUpperCase() + status.slice(1);
|
| 430 |
}
|
| 431 |
|
| 432 |
+
async function handleSignalingMessage(msg) {
|
| 433 |
switch (msg.type) {
|
| 434 |
case 'welcome':
|
| 435 |
myPeerId = msg.peerId;
|
| 436 |
log(`Connected as: ${myPeerId.substring(0, 8)}...`, 'success');
|
| 437 |
+
// Register as listener
|
| 438 |
+
await sendToServer({
|
| 439 |
type: 'setPeerStatus',
|
| 440 |
roles: ['listener'],
|
| 441 |
meta: { name: 'WebRTC Dashboard' }
|
| 442 |
+
});
|
|
|
|
|
|
|
| 443 |
break;
|
| 444 |
|
| 445 |
case 'list':
|
|
|
|
| 449 |
case 'peerStatusChanged':
|
| 450 |
log(`Robot ${msg.peerId.substring(0, 8)}... ${msg.roles?.length ? 'connected' : 'disconnected'}`);
|
| 451 |
// Refresh producer list
|
| 452 |
+
const listResponse = await sendToServer({ type: 'list' });
|
| 453 |
+
if (listResponse && listResponse.producers) {
|
| 454 |
+
displayProducers(listResponse.producers);
|
| 455 |
+
}
|
| 456 |
break;
|
| 457 |
|
| 458 |
case 'sessionStarted':
|
|
|
|
| 525 |
}
|
| 526 |
};
|
| 527 |
|
| 528 |
+
peerConnection.onicecandidate = async (event) => {
|
| 529 |
if (event.candidate && currentSessionId) {
|
| 530 |
+
await sendToServer({
|
| 531 |
type: 'peer',
|
| 532 |
sessionId: currentSessionId,
|
| 533 |
ice: {
|
|
|
|
| 535 |
sdpMLineIndex: event.candidate.sdpMLineIndex,
|
| 536 |
sdpMid: event.candidate.sdpMid
|
| 537 |
}
|
| 538 |
+
});
|
| 539 |
}
|
| 540 |
};
|
| 541 |
|
|
|
|
| 562 |
};
|
| 563 |
|
| 564 |
log('Requesting session with robot...');
|
| 565 |
+
const response = await sendToServer({
|
| 566 |
type: 'startSession',
|
| 567 |
peerId: selectedProducerId
|
| 568 |
+
});
|
| 569 |
+
|
| 570 |
+
if (response && response.sessionId) {
|
| 571 |
+
currentSessionId = response.sessionId;
|
| 572 |
+
log(`Session started: ${response.sessionId.substring(0, 8)}...`, 'success');
|
| 573 |
+
}
|
| 574 |
}
|
| 575 |
|
| 576 |
async function handlePeerMessage(msg) {
|
|
|
|
| 584 |
if (msg.sdp.type === 'offer') {
|
| 585 |
const answer = await peerConnection.createAnswer();
|
| 586 |
await peerConnection.setLocalDescription(answer);
|
| 587 |
+
await sendToServer({
|
| 588 |
type: 'peer',
|
| 589 |
sessionId: currentSessionId,
|
| 590 |
sdp: { type: 'answer', sdp: answer.sdp }
|
| 591 |
+
});
|
| 592 |
log('Sent SDP answer');
|
| 593 |
}
|
| 594 |
}
|