livedesk 0.1.114 → 0.1.116

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
@@ -22,7 +22,11 @@ const packageInfo = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.js
22
22
  const httpHost = process.env.LIVEDESK_HUB_HTTP_HOST || '0.0.0.0';
23
23
  const httpPort = Number(process.env.LIVEDESK_HUB_HTTP_PORT || process.env.PORT || 5179);
24
24
  const frameBackpressureBytes = readPositiveIntegerEnv('LIVEDESK_FRAME_WS_BACKPRESSURE_BYTES', 512 * 1024);
25
+ const frameClientQueuePackets = readPositiveIntegerEnv('LIVEDESK_FRAME_WS_QUEUE_PACKETS', 3);
26
+ const frameClientDrainBudgetPackets = readPositiveIntegerEnv('LIVEDESK_FRAME_WS_DRAIN_BUDGET_PACKETS', 2);
25
27
  const frameClients = new Set();
28
+ const frameClientsByDeviceId = new Map();
29
+ const frameWildcardClients = new Set();
26
30
  const inputClients = new Set();
27
31
  const audioClients = new Set();
28
32
  let frameClientSeq = 0;
@@ -42,10 +46,14 @@ function handleRemoteHubEvent(type, event) {
42
46
  if (!deviceId) {
43
47
  return;
44
48
  }
45
- for (const ws of frameClients) {
49
+ const clients = new Set([
50
+ ...(frameClientsByDeviceId.get(deviceId) || []),
51
+ ...frameWildcardClients
52
+ ]);
53
+ for (const ws of clients) {
46
54
  if (ws.readyState === 1
47
55
  && ws.liveDeskAutoStart
48
- && ws.liveDeskDeviceIds?.has(deviceId)) {
56
+ && (!ws.liveDeskDeviceIds?.size || ws.liveDeskDeviceIds.has(deviceId))) {
49
57
  startFrameSubscriptionLive(ws, 'device-reconnected', deviceId);
50
58
  }
51
59
  }
@@ -68,9 +76,9 @@ const remoteHub = createRemoteHub({
68
76
 
69
77
  const app = express();
70
78
  const httpServer = createServer(app);
71
- const frameWss = new WebSocketServer({ noServer: true });
72
- const inputWss = new WebSocketServer({ noServer: true });
73
- const audioWss = new WebSocketServer({ noServer: true });
79
+ const frameWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
80
+ const inputWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
81
+ const audioWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
74
82
 
75
83
  app.use((req, res, next) => {
76
84
  if (req.headers.origin) {
@@ -200,6 +208,7 @@ function sendJson(ws, payload) {
200
208
  }
201
209
 
202
210
  function forgetWebSocketClient(ws) {
211
+ unregisterFrameClient(ws);
203
212
  frameClients.delete(ws);
204
213
  audioClients.delete(ws);
205
214
  inputClients.delete(ws);
@@ -228,12 +237,129 @@ function safeWebSocketSend(ws, payload, options = undefined) {
228
237
  }
229
238
  }
230
239
 
240
+ function unregisterFrameClient(ws) {
241
+ if (!ws) {
242
+ return;
243
+ }
244
+ const ids = ws.liveDeskDeviceIds instanceof Set ? ws.liveDeskDeviceIds : new Set();
245
+ for (const deviceId of ids) {
246
+ const clients = frameClientsByDeviceId.get(deviceId);
247
+ if (!clients) {
248
+ continue;
249
+ }
250
+ clients.delete(ws);
251
+ if (clients.size === 0) {
252
+ frameClientsByDeviceId.delete(deviceId);
253
+ }
254
+ }
255
+ frameWildcardClients.delete(ws);
256
+ ws.liveDeskFrameSendLane?.queue?.splice?.(0);
257
+ }
258
+
259
+ function registerFrameClient(ws) {
260
+ if (!ws) {
261
+ return;
262
+ }
263
+ const ids = ws.liveDeskDeviceIds instanceof Set ? ws.liveDeskDeviceIds : new Set();
264
+ if (ids.size === 0) {
265
+ frameWildcardClients.add(ws);
266
+ return;
267
+ }
268
+ for (const deviceId of ids) {
269
+ let clients = frameClientsByDeviceId.get(deviceId);
270
+ if (!clients) {
271
+ clients = new Set();
272
+ frameClientsByDeviceId.set(deviceId, clients);
273
+ }
274
+ clients.add(ws);
275
+ }
276
+ }
277
+
278
+ function ensureFrameClientSendLane(ws) {
279
+ if (!ws.liveDeskFrameSendLane) {
280
+ ws.liveDeskFrameSendLane = {
281
+ queue: [],
282
+ draining: false,
283
+ dropped: 0
284
+ };
285
+ }
286
+ return ws.liveDeskFrameSendLane;
287
+ }
288
+
289
+ function dropQueuedFrameForLane(lane) {
290
+ const deltaIndex = lane.queue.findIndex(item => item?.isKeyFrame !== true);
291
+ const dropIndex = deltaIndex >= 0 ? deltaIndex : 0;
292
+ const dropped = lane.queue.splice(dropIndex, 1);
293
+ lane.dropped += dropped.length;
294
+ }
295
+
296
+ function drainFrameClientSendLane(ws) {
297
+ const lane = ws.liveDeskFrameSendLane;
298
+ if (!lane || lane.draining) {
299
+ return;
300
+ }
301
+ lane.draining = true;
302
+ setImmediate(() => {
303
+ lane.draining = false;
304
+ if (!ws || ws.readyState !== ws.OPEN) {
305
+ lane.queue.length = 0;
306
+ return;
307
+ }
308
+
309
+ let sent = 0;
310
+ while (lane.queue.length > 0 && sent < frameClientDrainBudgetPackets) {
311
+ if (ws.bufferedAmount > frameBackpressureBytes) {
312
+ ws.liveDeskFrameBackpressured = true;
313
+ break;
314
+ }
315
+ const item = lane.queue.shift();
316
+ if (!item) {
317
+ continue;
318
+ }
319
+ ws.liveDeskFrameBackpressured = false;
320
+ safeWebSocketSend(ws, item.packet, { binary: true });
321
+ sent += 1;
322
+ }
323
+
324
+ if (lane.queue.length > 0 && ws.readyState === ws.OPEN) {
325
+ drainFrameClientSendLane(ws);
326
+ }
327
+ });
328
+ }
329
+
330
+ function enqueueFramePacketForClient(ws, packet, meta) {
331
+ if (!ws || ws.readyState !== ws.OPEN) {
332
+ return false;
333
+ }
334
+ const lane = ensureFrameClientSendLane(ws);
335
+ if (ws.bufferedAmount > frameBackpressureBytes) {
336
+ ws.liveDeskFrameBackpressured = true;
337
+ ws.liveDeskFrameBackpressureDrops = Number(ws.liveDeskFrameBackpressureDrops || 0) + 1;
338
+ return false;
339
+ }
340
+ while (lane.queue.length >= frameClientQueuePackets) {
341
+ dropQueuedFrameForLane(lane);
342
+ ws.liveDeskFrameBackpressureDrops = Number(ws.liveDeskFrameBackpressureDrops || 0) + 1;
343
+ }
344
+ lane.queue.push({
345
+ packet,
346
+ deviceId: meta.deviceId,
347
+ frameSeq: meta.frameSeq,
348
+ isH264: meta.isH264,
349
+ isKeyFrame: meta.isKeyFrame
350
+ });
351
+ drainFrameClientSendLane(ws);
352
+ return true;
353
+ }
354
+
231
355
  function updateFrameSubscription(ws, payload = {}) {
232
356
  const previousDeviceIds = ws.liveDeskDeviceIds instanceof Set
233
357
  ? new Set(ws.liveDeskDeviceIds)
234
358
  : new Set();
359
+ unregisterFrameClient(ws);
235
360
  const deviceIds = normalizeDeviceIds(payload.deviceIds ?? payload.devices ?? payload.deviceId);
236
361
  ws.liveDeskDeviceIds = new Set(deviceIds);
362
+ registerFrameClient(ws);
237
363
  ws.liveDeskAutoStart = /^(1|true|yes|on|live)$/i.test(String(payload.autoStartLive ?? payload.startLive ?? ''));
238
364
  ws.liveDeskLiveOptions = normalizeLiveOptions(payload);
239
365
  sendJson(ws, {
@@ -347,6 +473,8 @@ function buildRemoteFrameBinaryPacket(frameEvent) {
347
473
  return null;
348
474
  }
349
475
  const frame = frameEvent.frame || {};
476
+ const hubPacketEpochMs = Date.now();
477
+ const hubPacketAt = new Date(hubPacketEpochMs).toISOString();
350
478
  const metadata = {
351
479
  type: 'remote.frame.binary',
352
480
  kind: frameEvent.kind || 'live',
@@ -380,6 +508,8 @@ function buildRemoteFrameBinaryPacket(frameEvent) {
380
508
  fps: Number(frame.fps || 0) || 0,
381
509
  capturedAt: frame.capturedAt || '',
382
510
  receivedAt: frame.receivedAt || '',
511
+ hubPacketAt,
512
+ hubPacketEpochMs,
383
513
  byteLength: Number(frameEvent.byteLength || payload.length) || payload.length,
384
514
  contentHash: frame.contentHash || '',
385
515
  captureMs: Number(frame.captureMs || 0) || 0,
@@ -420,6 +550,13 @@ function broadcastRemoteBinaryFrame(frameEvent) {
420
550
  if (!deviceId || frameClients.size === 0) {
421
551
  return;
422
552
  }
553
+ const targetClients = new Set([
554
+ ...(frameClientsByDeviceId.get(deviceId) || []),
555
+ ...frameWildcardClients
556
+ ]);
557
+ if (targetClients.size === 0) {
558
+ return;
559
+ }
423
560
  const frame = frameEvent.frame || {};
424
561
  const isH264 = String(frame.codec || frame.mimeType || frame.format || '').toLowerCase().includes('h264')
425
562
  || String(frame.frameMode || frame.mode || '').toLowerCase() === 'mode3-h264-hw';
@@ -428,24 +565,16 @@ function broadcastRemoteBinaryFrame(frameEvent) {
428
565
  if (!packet) {
429
566
  return;
430
567
  }
431
- for (const client of frameClients) {
568
+ const frameSeq = Number(frame.frameSeq || 0) || 0;
569
+ for (const client of targetClients) {
432
570
  if (client.readyState !== client.OPEN) {
433
571
  continue;
434
572
  }
435
- if (client.liveDeskDeviceIds?.size > 0 && !client.liveDeskDeviceIds.has(deviceId)) {
436
- continue;
437
- }
438
- if (client.bufferedAmount > frameBackpressureBytes) {
439
- client.liveDeskFrameBackpressured = true;
440
- client.liveDeskFrameBackpressureDrops = Number(client.liveDeskFrameBackpressureDrops || 0) + 1;
441
- continue;
442
- }
443
573
  if (client.liveDeskFrameBackpressured && isH264 && !isKeyFrame) {
444
574
  client.liveDeskFrameBackpressureDrops = Number(client.liveDeskFrameBackpressureDrops || 0) + 1;
445
575
  continue;
446
576
  }
447
- client.liveDeskFrameBackpressured = false;
448
- safeWebSocketSend(client, packet, { binary: true });
577
+ enqueueFramePacketForClient(client, packet, { deviceId, frameSeq, isH264, isKeyFrame });
449
578
  }
450
579
  }
451
580
 
@@ -795,6 +924,7 @@ frameWss.on('connection', (ws, req) => {
795
924
  // Best-effort latency hint for browser frame sockets.
796
925
  }
797
926
  const cleanup = () => {
927
+ unregisterFrameClient(ws);
798
928
  frameClients.delete(ws);
799
929
  };
800
930
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.114",
3
+ "version": "0.1.116",
4
4
  "description": "LiveDesk Hub and client launcher",
5
5
  "type": "module",
6
6
  "bin": {