@livedesk/hub 0.1.33 → 0.1.34

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/hub",
3
- "version": "0.1.33",
3
+ "version": "0.1.34",
4
4
  "description": "LiveDesk local Hub API and browser frame bridge",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
package/src/remote-hub.js CHANGED
@@ -47,7 +47,7 @@ const RECENT_TASK_LIMIT = 12;
47
47
  const RECENT_TASK_BATCH_LIMIT = 16;
48
48
  const RECENT_FRAME_CACHE_TTL_MS = 4000;
49
49
  const RECENT_THUMBNAIL_FRAME_CACHE_LIMIT = 2;
50
- const RECENT_LIVE_FRAME_CACHE_LIMIT = 1;
50
+ const RECENT_LIVE_FRAME_CACHE_LIMIT = 2;
51
51
  const RECENT_THUMBNAIL_FRAME_CACHE_MAX_BYTES = 2 * 1024 * 1024;
52
52
  const RECENT_LIVE_FRAME_CACHE_MAX_BYTES = 8 * 1024 * 1024;
53
53
  const LIVE_STREAM_PENDING_REUSE_MS = 5000;
@@ -1478,7 +1478,7 @@ function getRecentFrameCacheMaxBytes(frameKind) {
1478
1478
  : RECENT_LIVE_FRAME_CACHE_MAX_BYTES;
1479
1479
  }
1480
1480
 
1481
- function pruneRecentFrameCache(cache, frameKind, nowMs = Date.now()) {
1481
+ export function pruneRecentFrameCache(cache, frameKind, nowMs = Date.now()) {
1482
1482
  if (!Array.isArray(cache)) {
1483
1483
  return;
1484
1484
  }
@@ -1499,6 +1499,20 @@ function pruneRecentFrameCache(cache, frameKind, nowMs = Date.now()) {
1499
1499
  }
1500
1500
  }
1501
1501
 
1502
+ if (frameKind !== 'thumbnail' && cache.length > 1) {
1503
+ // Keep the newest frame plus the newest independently decodable key
1504
+ // frame. A browser can subscribe after capture has already started;
1505
+ // retaining only the latest 30 fps delta makes that browser wait for a
1506
+ // future GOP even though the Hub accepted a current key frame moments
1507
+ // earlier. Exact stream-owner matching below prevents stale replay.
1508
+ const latest = cache[0];
1509
+ const keyFrame = cache.find(entry =>
1510
+ entry !== latest
1511
+ && (entry?.frame?.isKeyFrame === true
1512
+ || safeString(entry?.frame?.chunkType, 20).toLowerCase() === 'key'));
1513
+ cache.splice(0, cache.length, ...[latest, keyFrame].filter(Boolean));
1514
+ }
1515
+
1502
1516
  for (let index = 0; index < cache.length; index += 1) {
1503
1517
  const entry = cache[index];
1504
1518
  const byteLength = Number(entry?.byteLength || entry?.frame?.payload?.length || 0) || 0;
@@ -1510,7 +1524,7 @@ function pruneRecentFrameCache(cache, frameKind, nowMs = Date.now()) {
1510
1524
  }
1511
1525
  }
1512
1526
 
1513
- function rememberRecentFramePayload(device, frameKind, frame) {
1527
+ export function rememberRecentFramePayload(device, frameKind, frame) {
1514
1528
  if (!device || !frame || !Buffer.isBuffer(frame.payload)) {
1515
1529
  return;
1516
1530
  }
@@ -1546,7 +1560,7 @@ function rememberRecentFramePayload(device, frameKind, frame) {
1546
1560
  pruneRecentFrameCache(cache, kind, nowMs);
1547
1561
  }
1548
1562
 
1549
- function isFramePayloadRequestMatch(frame, options = {}) {
1563
+ export function isFramePayloadRequestMatch(frame, options = {}) {
1550
1564
  if (!frame || !Buffer.isBuffer(frame.payload)) {
1551
1565
  return false;
1552
1566
  }
@@ -1565,10 +1579,37 @@ function isFramePayloadRequestMatch(frame, options = {}) {
1565
1579
  return false;
1566
1580
  }
1567
1581
 
1582
+ if (options.requireKeyFrame === true
1583
+ && frame.isKeyFrame !== true
1584
+ && safeString(frame.chunkType, 20).toLowerCase() !== 'key') {
1585
+ return false;
1586
+ }
1587
+
1588
+ for (const key of ['streamId', 'sessionId', 'commandId', 'streamPurpose']) {
1589
+ const expected = safeString(options[key], 160);
1590
+ if (expected && safeString(frame[key], 160) !== expected) {
1591
+ return false;
1592
+ }
1593
+ }
1594
+
1595
+ const requestedCaptureGeneration = Number(options.captureGeneration);
1596
+ if (Number.isFinite(requestedCaptureGeneration)
1597
+ && requestedCaptureGeneration > 0
1598
+ && Number(frame.captureGeneration || 0) !== requestedCaptureGeneration) {
1599
+ return false;
1600
+ }
1601
+
1602
+ const requestedMonitorIndex = Number(options.monitorIndex);
1603
+ if (Number.isFinite(requestedMonitorIndex)
1604
+ && requestedMonitorIndex >= 0
1605
+ && Number(frame.monitorIndex ?? -1) !== requestedMonitorIndex) {
1606
+ return false;
1607
+ }
1608
+
1568
1609
  return true;
1569
1610
  }
1570
1611
 
1571
- function findRecentFramePayload(device, frameKind, options = {}) {
1612
+ export function findRecentFramePayload(device, frameKind, options = {}) {
1572
1613
  const kind = frameKind === 'thumbnail' ? 'thumbnail' : 'live';
1573
1614
  const latest = kind === 'thumbnail'
1574
1615
  ? device?.latestThumbnail
@@ -1579,7 +1620,14 @@ function findRecentFramePayload(device, frameKind, options = {}) {
1579
1620
 
1580
1621
  const token = safeString(options.token, 128);
1581
1622
  const requestedSeq = Number(options.frameSeq);
1582
- if (!token && !Number.isFinite(requestedSeq)) {
1623
+ const ownerQuery = options.requireKeyFrame === true
1624
+ || ['streamId', 'sessionId', 'commandId', 'streamPurpose']
1625
+ .some(key => safeString(options[key], 160))
1626
+ || (Number.isFinite(Number(options.captureGeneration))
1627
+ && Number(options.captureGeneration) > 0)
1628
+ || (Number.isFinite(Number(options.monitorIndex))
1629
+ && Number(options.monitorIndex) >= 0);
1630
+ if (!token && !Number.isFinite(requestedSeq) && !ownerQuery) {
1583
1631
  return null;
1584
1632
  }
1585
1633
 
package/src/server.js CHANGED
@@ -2738,6 +2738,11 @@ function replaceExpectedFrameBindingForClient(ws, deviceId, expectedBinding = nu
2738
2738
  && bindingIdentity === frameLaneBindingIdentity(currentExpectedBinding)) {
2739
2739
  expectedBinding.readySent = currentExpectedBinding?.readySent === true
2740
2740
  || expectedBinding.readySent === true;
2741
+ expectedBinding.replayedKeyFrameSeq = Math.max(
2742
+ 0,
2743
+ Number(currentExpectedBinding?.replayedKeyFrameSeq || 0),
2744
+ Number(expectedBinding.replayedKeyFrameSeq || 0)
2745
+ );
2741
2746
  ws.liveDeskExpectedStreamBindingsByDeviceId.set(normalizedDeviceId, expectedBinding);
2742
2747
  if (!lane.bindingEpochsByDeviceId.has(normalizedDeviceId)) {
2743
2748
  lane.bindingEpochsByDeviceId.set(normalizedDeviceId, 1);
@@ -2776,6 +2781,50 @@ function replaceExpectedFrameBindingForClient(ws, deviceId, expectedBinding = nu
2776
2781
  return bindingIdentity;
2777
2782
  }
2778
2783
 
2784
+ function replayExpectedBindingKeyFrame(ws, expectedBinding) {
2785
+ if (!ws || ws.readyState !== ws.OPEN || !expectedBinding) {
2786
+ return false;
2787
+ }
2788
+ const deviceId = String(expectedBinding.deviceId || '').trim();
2789
+ const currentBinding = ws.liveDeskExpectedStreamBindingsByDeviceId instanceof Map
2790
+ ? ws.liveDeskExpectedStreamBindingsByDeviceId.get(deviceId)
2791
+ : null;
2792
+ const bindingIdentity = frameLaneBindingIdentity(expectedBinding);
2793
+ if (!deviceId
2794
+ || !bindingIdentity
2795
+ || bindingIdentity !== frameLaneBindingIdentity(currentBinding)) {
2796
+ return false;
2797
+ }
2798
+ const cached = remoteHub.getFramePayload(deviceId, 'live', {
2799
+ requireKeyFrame: true,
2800
+ streamId: expectedBinding.streamId,
2801
+ sessionId: expectedBinding.sessionId,
2802
+ commandId: expectedBinding.commandId,
2803
+ captureGeneration: expectedBinding.captureGeneration,
2804
+ streamPurpose: expectedBinding.streamPurpose,
2805
+ monitorIndex: expectedBinding.monitorIndex
2806
+ });
2807
+ const frameSeq = Number(cached?.frame?.frameSeq || 0);
2808
+ if (!cached
2809
+ || cached.frame?.isKeyFrame !== true
2810
+ || !Number.isSafeInteger(frameSeq)
2811
+ || frameSeq <= 0
2812
+ || Number(currentBinding?.replayedKeyFrameSeq || 0) === frameSeq) {
2813
+ return false;
2814
+ }
2815
+ currentBinding.replayedKeyFrameSeq = frameSeq;
2816
+ broadcastRemoteBinaryFrame({
2817
+ kind: 'live',
2818
+ deviceId,
2819
+ frame: cached.frame,
2820
+ payload: cached.payload,
2821
+ mimeType: cached.mimeType,
2822
+ byteLength: cached.byteLength,
2823
+ replayedForBinding: true
2824
+ });
2825
+ return true;
2826
+ }
2827
+
2779
2828
  function dropQueuedFrameAtForLane(lane, dropIndex) {
2780
2829
  const droppedBefore = lane.dropped;
2781
2830
  const droppedItem = removeFrameLaneQueueItem(lane, dropIndex);
@@ -3269,6 +3318,7 @@ function startFrameSubscriptionLive(
3269
3318
  continue;
3270
3319
  }
3271
3320
  cancelPendingFrameStreamStop(deviceId, result.streamId, String(liveOptions.streamPurpose || 'wall'));
3321
+ const replayedKeyFrame = replayExpectedBindingKeyFrame(ws, expectedBinding);
3272
3322
  started.push({
3273
3323
  deviceId: expectedBinding.deviceId,
3274
3324
  sessionId: expectedBinding.sessionId,
@@ -3277,7 +3327,8 @@ function startFrameSubscriptionLive(
3277
3327
  commandId: expectedBinding.commandId,
3278
3328
  captureGeneration: expectedBinding.captureGeneration,
3279
3329
  monitorIndex: expectedBinding.monitorIndex,
3280
- ready: reusedFrameReady,
3330
+ ready: expectedBinding.readySent === true,
3331
+ replayedKeyFrame,
3281
3332
  fps: result.fps,
3282
3333
  reused: result.reused === true
3283
3334
  });