livedesk 0.1.378 → 0.1.380

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/hub/src/server.js CHANGED
@@ -59,14 +59,14 @@ const runtimeManager = createHubRuntime({
59
59
  runtimePid: process.pid
60
60
  });
61
61
  runtimeManager.emit('runtime.started', { role: runtimeRole, pid: process.pid });
62
- const frameBackpressureBytes = readPositiveIntegerEnv('LIVEDESK_FRAME_WS_BACKPRESSURE_BYTES', 64 * 1024 * 1024);
62
+ const frameBackpressureBytes = readPositiveIntegerEnv('LIVEDESK_FRAME_WS_BACKPRESSURE_BYTES', 8 * 1024 * 1024);
63
63
  const frameClientQueuePackets = readPositiveIntegerEnv('LIVEDESK_FRAME_WS_QUEUE_PACKETS', 4);
64
64
  const controlFrameClientQueuePackets = readPositiveIntegerEnv('LIVEDESK_CONTROL_FRAME_WS_QUEUE_PACKETS', 2);
65
- const frameClientDrainBudgetPackets = readPositiveIntegerEnv('LIVEDESK_FRAME_WS_DRAIN_BUDGET_PACKETS', 64);
66
- const frameStreamStopGraceMs = readPositiveIntegerEnv('LIVEDESK_FRAME_STREAM_STOP_GRACE_MS', 1500);
67
- const mode5FrameBackpressureBytes = readPositiveIntegerEnv('LIVEDESK_MODE5_WS_BACKPRESSURE_BYTES', 2 * 1024 * 1024);
68
- const mode5FrameQueuePackets = readPositiveIntegerEnv('LIVEDESK_MODE5_WS_QUEUE_PACKETS', 2);
69
- const audioBackpressureBytes = readPositiveIntegerEnv('LIVEDESK_AUDIO_WS_BACKPRESSURE_BYTES', 96 * 1024);
65
+ const frameStreamStopGraceMs = readPositiveIntegerEnv('LIVEDESK_FRAME_STREAM_STOP_GRACE_MS', 1500);
66
+ const mode5FrameBackpressureBytes = readPositiveIntegerEnv('LIVEDESK_MODE5_WS_BACKPRESSURE_BYTES', 2 * 1024 * 1024);
67
+ const mode5FrameQueuePackets = readPositiveIntegerEnv('LIVEDESK_MODE5_WS_QUEUE_PACKETS', 2);
68
+ const audioBackpressureBytes = readPositiveIntegerEnv('LIVEDESK_AUDIO_WS_BACKPRESSURE_BYTES', 96 * 1024);
69
+ const webSocketHeartbeatMs = readPositiveIntegerEnv('LIVEDESK_BROWSER_WS_HEARTBEAT_MS', 15_000);
70
70
  const frameClients = new Set();
71
71
  const frameClientsByDeviceId = new Map();
72
72
  const frameWildcardClients = new Set();
@@ -1064,10 +1064,38 @@ const hubSharedFolders = createHubSharedFolders({
1064
1064
 
1065
1065
  const app = express();
1066
1066
  const httpServer = createServer(app);
1067
- const frameWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
1068
- const atlasWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
1069
- const inputWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
1070
- const audioWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
1067
+ const frameWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
1068
+ const atlasWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
1069
+ const inputWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
1070
+ const audioWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
1071
+ const browserWebSocketServers = [frameWss, atlasWss, inputWss, audioWss];
1072
+
1073
+ for (const wss of browserWebSocketServers) {
1074
+ wss.on('connection', ws => {
1075
+ ws.liveDeskHeartbeatAlive = true;
1076
+ ws.on('pong', () => {
1077
+ ws.liveDeskHeartbeatAlive = true;
1078
+ });
1079
+ });
1080
+ }
1081
+
1082
+ const browserWebSocketHeartbeatTimer = setInterval(() => {
1083
+ for (const wss of browserWebSocketServers) {
1084
+ for (const ws of wss.clients) {
1085
+ if (ws.liveDeskHeartbeatAlive === false) {
1086
+ ws.terminate();
1087
+ continue;
1088
+ }
1089
+ ws.liveDeskHeartbeatAlive = false;
1090
+ try {
1091
+ ws.ping();
1092
+ } catch {
1093
+ ws.terminate();
1094
+ }
1095
+ }
1096
+ }
1097
+ }, webSocketHeartbeatMs);
1098
+ browserWebSocketHeartbeatTimer.unref?.();
1071
1099
 
1072
1100
  function normalizeHostname(value) {
1073
1101
  return String(value || '').trim().toLowerCase().replace(/^\[|\]$/g, '');
@@ -1347,23 +1375,25 @@ function forgetWebSocketClient(ws) {
1347
1375
  }
1348
1376
  }
1349
1377
 
1350
- function safeWebSocketSend(ws, payload, options = undefined) {
1351
- if (!ws || ws.readyState !== 1) {
1352
- return false;
1353
- }
1354
-
1355
- try {
1356
- ws.send(payload, options, err => {
1357
- if (err) {
1358
- forgetWebSocketClient(ws);
1359
- }
1360
- });
1361
- return true;
1362
- } catch {
1363
- forgetWebSocketClient(ws);
1364
- return false;
1365
- }
1366
- }
1378
+ function safeWebSocketSend(ws, payload, options = undefined, onComplete = undefined) {
1379
+ if (!ws || ws.readyState !== 1) {
1380
+ return false;
1381
+ }
1382
+
1383
+ try {
1384
+ ws.send(payload, options, err => {
1385
+ if (err) {
1386
+ forgetWebSocketClient(ws);
1387
+ }
1388
+ onComplete?.(err || null);
1389
+ });
1390
+ return true;
1391
+ } catch (error) {
1392
+ forgetWebSocketClient(ws);
1393
+ onComplete?.(error);
1394
+ return false;
1395
+ }
1396
+ }
1367
1397
 
1368
1398
  function unregisterFrameClient(ws) {
1369
1399
  if (!ws) {
@@ -1383,7 +1413,12 @@ function unregisterFrameClient(ws) {
1383
1413
  }
1384
1414
  frameWildcardClients.delete(ws);
1385
1415
  ws.liveDeskExpectedStreamBindingsByDeviceId?.clear?.();
1386
- ws.liveDeskFrameSendLane?.queue?.splice?.(0);
1416
+ const lane = ws.liveDeskFrameSendLane;
1417
+ if (lane) {
1418
+ clearTimeout(lane.retryTimer);
1419
+ lane.retryTimer = null;
1420
+ lane.queue.splice(0);
1421
+ }
1387
1422
  }
1388
1423
 
1389
1424
  function hasOtherFrameStreamOwner(ws, deviceId, streamId, streamPurpose = '') {
@@ -1484,11 +1519,12 @@ function registerFrameClient(ws) {
1484
1519
  function ensureFrameClientSendLane(ws) {
1485
1520
  if (!ws.liveDeskFrameSendLane) {
1486
1521
  ws.liveDeskFrameSendLane = {
1487
- queue: [],
1488
- draining: false,
1489
- dropped: 0,
1490
- awaitingKeyFrames: new Set()
1491
- };
1522
+ queue: [],
1523
+ sending: false,
1524
+ retryTimer: null,
1525
+ dropped: 0,
1526
+ awaitingKeyFrames: new Set()
1527
+ };
1492
1528
  }
1493
1529
  return ws.liveDeskFrameSendLane;
1494
1530
  }
@@ -1536,39 +1572,48 @@ function dropQueuedMode5FrameForLane(lane, deviceId) {
1536
1572
  return true;
1537
1573
  }
1538
1574
 
1539
- function drainFrameClientSendLane(ws) {
1540
- const lane = ws.liveDeskFrameSendLane;
1541
- if (!lane || lane.draining) {
1542
- return;
1543
- }
1544
- lane.draining = true;
1545
- setImmediate(() => {
1546
- lane.draining = false;
1547
- if (!ws || ws.readyState !== ws.OPEN) {
1548
- lane.queue.length = 0;
1549
- return;
1550
- }
1551
-
1552
- let sent = 0;
1553
- while (lane.queue.length > 0 && sent < frameClientDrainBudgetPackets) {
1554
- if (ws.bufferedAmount > frameBackpressureBytes) {
1555
- ws.liveDeskFrameBackpressured = true;
1556
- break;
1557
- }
1558
- const item = lane.queue.shift();
1559
- if (!item) {
1560
- continue;
1561
- }
1562
- ws.liveDeskFrameBackpressured = false;
1563
- safeWebSocketSend(ws, item.packet, { binary: true });
1564
- sent += 1;
1565
- }
1566
-
1567
- if (lane.queue.length > 0 && ws.readyState === ws.OPEN) {
1568
- drainFrameClientSendLane(ws);
1569
- }
1570
- });
1571
- }
1575
+ function drainFrameClientSendLane(ws) {
1576
+ const lane = ws.liveDeskFrameSendLane;
1577
+ if (!lane || lane.sending) {
1578
+ return;
1579
+ }
1580
+ clearTimeout(lane.retryTimer);
1581
+ lane.retryTimer = null;
1582
+ if (!ws || ws.readyState !== ws.OPEN) {
1583
+ lane.queue.length = 0;
1584
+ return;
1585
+ }
1586
+ const item = lane.queue[0];
1587
+ if (!item) {
1588
+ return;
1589
+ }
1590
+ const backpressureBytes = item.isMode5 ? mode5FrameBackpressureBytes : frameBackpressureBytes;
1591
+ if (ws.bufferedAmount > backpressureBytes) {
1592
+ ws.liveDeskFrameBackpressured = true;
1593
+ lane.retryTimer = setTimeout(() => {
1594
+ lane.retryTimer = null;
1595
+ drainFrameClientSendLane(ws);
1596
+ }, 10);
1597
+ lane.retryTimer.unref?.();
1598
+ return;
1599
+ }
1600
+
1601
+ lane.queue.shift();
1602
+ lane.sending = true;
1603
+ ws.liveDeskFrameBackpressured = false;
1604
+ const accepted = safeWebSocketSend(ws, item.packet, { binary: true }, error => {
1605
+ lane.sending = false;
1606
+ if (error || ws.readyState !== ws.OPEN) {
1607
+ lane.queue.length = 0;
1608
+ return;
1609
+ }
1610
+ drainFrameClientSendLane(ws);
1611
+ });
1612
+ if (!accepted) {
1613
+ lane.sending = false;
1614
+ lane.queue.length = 0;
1615
+ }
1616
+ }
1572
1617
 
1573
1618
  function enqueueFramePacketForClient(ws, packet, meta) {
1574
1619
  if (!ws || ws.readyState !== ws.OPEN) {
@@ -2227,10 +2272,35 @@ function isCapabilityEnabled(device, key) {
2227
2272
  return /^(1|true|yes|on)$/i.test(String(value || '').trim());
2228
2273
  }
2229
2274
 
2230
- app.get('/api/health', (_req, res) => {
2231
- noStore(res);
2232
- res.json({ ok: true, product: 'LiveDesk', timestamp: new Date().toISOString() });
2233
- });
2275
+ app.get('/api/health', (_req, res) => {
2276
+ noStore(res);
2277
+ const memory = process.memoryUsage();
2278
+ const webSockets = {
2279
+ frame: frameWss.clients.size,
2280
+ atlas: atlasWss.clients.size,
2281
+ input: inputWss.clients.size,
2282
+ audio: audioWss.clients.size
2283
+ };
2284
+ res.json({
2285
+ ok: true,
2286
+ product: 'LiveDesk',
2287
+ timestamp: new Date().toISOString(),
2288
+ memory: {
2289
+ rssBytes: memory.rss,
2290
+ heapUsedBytes: memory.heapUsed,
2291
+ heapTotalBytes: memory.heapTotal,
2292
+ externalBytes: memory.external,
2293
+ arrayBuffersBytes: memory.arrayBuffers
2294
+ },
2295
+ webSockets: {
2296
+ ...webSockets,
2297
+ total: Object.values(webSockets).reduce((sum, count) => sum + count, 0),
2298
+ bufferedBytes: [...browserWebSocketServers]
2299
+ .flatMap(wss => [...wss.clients])
2300
+ .reduce((sum, ws) => sum + Math.max(0, Number(ws.bufferedAmount || 0)), 0)
2301
+ }
2302
+ });
2303
+ });
2234
2304
 
2235
2305
  app.get('/api/settings', async (_req, res) => {
2236
2306
  noStore(res);
@@ -3972,6 +4042,7 @@ for (const signal of ['SIGINT', 'SIGTERM']) {
3972
4042
  if (roleWatchTimer) {
3973
4043
  clearInterval(roleWatchTimer);
3974
4044
  }
4045
+ clearInterval(browserWebSocketHeartbeatTimer);
3975
4046
  liveDeskUpdateManager?.close();
3976
4047
  atlasPool.close();
3977
4048
  atlasClients.clear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.378",
3
+ "version": "0.1.380",
4
4
  "buildFlavor": "production",
5
5
  "description": "LiveDesk Hub and client launcher",
6
6
  "type": "module",