@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 +1 -1
- package/src/remote-hub.js +54 -6
- package/src/server.js +52 -1
package/package.json
CHANGED
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 =
|
|
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
|
-
|
|
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:
|
|
3330
|
+
ready: expectedBinding.readySent === true,
|
|
3331
|
+
replayedKeyFrame,
|
|
3281
3332
|
fps: result.fps,
|
|
3282
3333
|
reused: result.reused === true
|
|
3283
3334
|
});
|