livedesk 0.1.81 → 0.1.83
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/remote-hub.js +172 -3
- package/hub/src/server.js +16 -2
- package/package.json +2 -2
package/hub/src/remote-hub.js
CHANGED
|
@@ -22,6 +22,9 @@ const RECENT_THUMBNAIL_FRAME_CACHE_LIMIT = 4;
|
|
|
22
22
|
const RECENT_LIVE_FRAME_CACHE_LIMIT = 80;
|
|
23
23
|
const RECENT_THUMBNAIL_FRAME_CACHE_MAX_BYTES = 3 * 1024 * 1024;
|
|
24
24
|
const RECENT_LIVE_FRAME_CACHE_MAX_BYTES = 24 * 1024 * 1024;
|
|
25
|
+
const LIVE_STREAM_PENDING_REUSE_MS = 5000;
|
|
26
|
+
const LIVE_STREAM_MIN_FRESH_MS = 3000;
|
|
27
|
+
const LIVE_STREAM_MAX_FRESH_MS = 12000;
|
|
25
28
|
const REMOTE_PROTOCOL_VERSION = 2;
|
|
26
29
|
const REMOTE_AGENT_PROTOCOL = 'mindexec.remote.agent';
|
|
27
30
|
const REMOTE_FRAME_PROTOCOL = 'mindexec.remote.frame.v2';
|
|
@@ -1205,6 +1208,7 @@ export function createRemoteHub(options = {}) {
|
|
|
1205
1208
|
const devices = new Map();
|
|
1206
1209
|
const taskBatches = new Map();
|
|
1207
1210
|
const sockets = new Map();
|
|
1211
|
+
const inputSockets = new Map();
|
|
1208
1212
|
const allSockets = new Set();
|
|
1209
1213
|
const duplicateDeviceLogAt = new Map();
|
|
1210
1214
|
let server = null;
|
|
@@ -1738,6 +1742,7 @@ export function createRemoteHub(options = {}) {
|
|
|
1738
1742
|
|
|
1739
1743
|
function closeExistingDeviceSocket(deviceId, nextSessionId) {
|
|
1740
1744
|
const existing = devices.get(deviceId);
|
|
1745
|
+
closeInputSocket(existing, 'replaced-by-new-session');
|
|
1741
1746
|
if (!existing?.socket || existing.socket.destroyed) {
|
|
1742
1747
|
return;
|
|
1743
1748
|
}
|
|
@@ -1752,6 +1757,43 @@ export function createRemoteHub(options = {}) {
|
|
|
1752
1757
|
existing.socket.destroy();
|
|
1753
1758
|
}
|
|
1754
1759
|
|
|
1760
|
+
function closeInputSocket(device, reason = 'input-socket-closed') {
|
|
1761
|
+
const socket = device?.inputSocket;
|
|
1762
|
+
if (!socket) {
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
device.inputSocket = null;
|
|
1767
|
+
inputSockets.delete(socket);
|
|
1768
|
+
try {
|
|
1769
|
+
writeJsonLine(socket, { type: 'disconnect', reason });
|
|
1770
|
+
} catch {
|
|
1771
|
+
// Best-effort notice before closing the side channel.
|
|
1772
|
+
}
|
|
1773
|
+
try {
|
|
1774
|
+
socket.destroy?.();
|
|
1775
|
+
} catch {
|
|
1776
|
+
// Ignore close failures.
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
function detachInputSocket(socket, reason = 'input-socket-closed') {
|
|
1781
|
+
const deviceId = inputSockets.get(socket);
|
|
1782
|
+
inputSockets.delete(socket);
|
|
1783
|
+
if (!deviceId) {
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
const device = devices.get(deviceId);
|
|
1788
|
+
if (!device || device.inputSocket !== socket) {
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
device.inputSocket = null;
|
|
1793
|
+
device.inputLastSeenAt = new Date().toISOString();
|
|
1794
|
+
emitRemoteEvent('RemoteInputSocketDisconnected', device, { reason });
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1755
1797
|
function getDeviceActivityMs(device) {
|
|
1756
1798
|
return Math.max(
|
|
1757
1799
|
Date.parse(device?.lastSeenAt || '') || 0,
|
|
@@ -2136,6 +2178,7 @@ export function createRemoteHub(options = {}) {
|
|
|
2136
2178
|
|
|
2137
2179
|
const device = {
|
|
2138
2180
|
socket,
|
|
2181
|
+
inputSocket: null,
|
|
2139
2182
|
deviceId,
|
|
2140
2183
|
sessionId,
|
|
2141
2184
|
deviceName: safeString(hello.deviceName || hello.hostname || deviceId, 120),
|
|
@@ -2152,6 +2195,8 @@ export function createRemoteHub(options = {}) {
|
|
|
2152
2195
|
capabilities,
|
|
2153
2196
|
connected: true,
|
|
2154
2197
|
connectedAt: now,
|
|
2198
|
+
inputConnectedAt: '',
|
|
2199
|
+
inputLastSeenAt: '',
|
|
2155
2200
|
disconnectedAt: '',
|
|
2156
2201
|
lastSeenAt: now,
|
|
2157
2202
|
lastStatusAt: '',
|
|
@@ -2195,6 +2240,37 @@ export function createRemoteHub(options = {}) {
|
|
|
2195
2240
|
return device;
|
|
2196
2241
|
}
|
|
2197
2242
|
|
|
2243
|
+
function attachInputSocket(socket, hello) {
|
|
2244
|
+
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
2245
|
+
const device = devices.get(deviceId);
|
|
2246
|
+
if (!device || !device.connected || !device.socket || device.socket.destroyed) {
|
|
2247
|
+
writeJsonLine(socket, { type: 'error', error: 'device-not-connected' });
|
|
2248
|
+
socket.destroy();
|
|
2249
|
+
return null;
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
closeInputSocket(device, 'replaced-by-new-input-socket');
|
|
2253
|
+
|
|
2254
|
+
const now = new Date().toISOString();
|
|
2255
|
+
device.inputSocket = socket;
|
|
2256
|
+
device.inputConnectedAt = now;
|
|
2257
|
+
device.inputLastSeenAt = now;
|
|
2258
|
+
inputSockets.set(socket, deviceId);
|
|
2259
|
+
|
|
2260
|
+
writeJsonLine(socket, {
|
|
2261
|
+
type: 'welcome',
|
|
2262
|
+
channel: 'input',
|
|
2263
|
+
protocol: REMOTE_AGENT_PROTOCOL,
|
|
2264
|
+
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
2265
|
+
sessionId: device.sessionId,
|
|
2266
|
+
deviceId,
|
|
2267
|
+
serverTime: now
|
|
2268
|
+
});
|
|
2269
|
+
|
|
2270
|
+
emitRemoteEvent('RemoteInputSocketConnected', device);
|
|
2271
|
+
return device;
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2198
2274
|
function detachSocket(socket, reason = 'socket-closed') {
|
|
2199
2275
|
const deviceId = sockets.get(socket);
|
|
2200
2276
|
sockets.delete(socket);
|
|
@@ -2209,6 +2285,7 @@ export function createRemoteHub(options = {}) {
|
|
|
2209
2285
|
|
|
2210
2286
|
device.connected = false;
|
|
2211
2287
|
device.socket = null;
|
|
2288
|
+
closeInputSocket(device, reason);
|
|
2212
2289
|
device.disconnectedAt = new Date().toISOString();
|
|
2213
2290
|
device.lastDisconnectReason = reason;
|
|
2214
2291
|
if (device.activeLiveStream) {
|
|
@@ -2973,6 +3050,12 @@ export function createRemoteHub(options = {}) {
|
|
|
2973
3050
|
}
|
|
2974
3051
|
|
|
2975
3052
|
const device = state.device;
|
|
3053
|
+
if (state.inputOnly) {
|
|
3054
|
+
writeJsonLine(socket, { type: 'error', error: 'input-channel-binary-not-supported' });
|
|
3055
|
+
socket.destroy();
|
|
3056
|
+
return;
|
|
3057
|
+
}
|
|
3058
|
+
|
|
2976
3059
|
device.counters.messagesReceived += 1;
|
|
2977
3060
|
device.lastSeenAt = new Date().toISOString();
|
|
2978
3061
|
|
|
@@ -3016,6 +3099,19 @@ export function createRemoteHub(options = {}) {
|
|
|
3016
3099
|
return;
|
|
3017
3100
|
}
|
|
3018
3101
|
|
|
3102
|
+
const channel = safeString(message.channel || message.Channel, 40).toLowerCase();
|
|
3103
|
+
if (channel === 'input') {
|
|
3104
|
+
const device = attachInputSocket(socket, message);
|
|
3105
|
+
if (!device) {
|
|
3106
|
+
return;
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
state.authenticated = true;
|
|
3110
|
+
state.device = device;
|
|
3111
|
+
state.inputOnly = true;
|
|
3112
|
+
return;
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3019
3115
|
const device = attachDevice(socket, message);
|
|
3020
3116
|
if (!device) {
|
|
3021
3117
|
return;
|
|
@@ -3032,6 +3128,11 @@ export function createRemoteHub(options = {}) {
|
|
|
3032
3128
|
return;
|
|
3033
3129
|
}
|
|
3034
3130
|
|
|
3131
|
+
if (state.inputOnly) {
|
|
3132
|
+
device.inputLastSeenAt = new Date().toISOString();
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3035
3136
|
device.counters.messagesReceived += 1;
|
|
3036
3137
|
device.lastSeenAt = new Date().toISOString();
|
|
3037
3138
|
|
|
@@ -3103,7 +3204,8 @@ export function createRemoteHub(options = {}) {
|
|
|
3103
3204
|
|
|
3104
3205
|
const state = {
|
|
3105
3206
|
authenticated: false,
|
|
3106
|
-
device: null
|
|
3207
|
+
device: null,
|
|
3208
|
+
inputOnly: false
|
|
3107
3209
|
};
|
|
3108
3210
|
|
|
3109
3211
|
const helloTimer = setTimeout(() => {
|
|
@@ -3154,6 +3256,7 @@ export function createRemoteHub(options = {}) {
|
|
|
3154
3256
|
const detach = reason => {
|
|
3155
3257
|
allSockets.delete(socket);
|
|
3156
3258
|
clearTimeout(helloTimer);
|
|
3259
|
+
detachInputSocket(socket, reason);
|
|
3157
3260
|
detachSocket(socket, reason);
|
|
3158
3261
|
};
|
|
3159
3262
|
|
|
@@ -3243,6 +3346,7 @@ export function createRemoteHub(options = {}) {
|
|
|
3243
3346
|
const state = {
|
|
3244
3347
|
authenticated: false,
|
|
3245
3348
|
device: null,
|
|
3349
|
+
inputOnly: false,
|
|
3246
3350
|
buffer: Buffer.alloc(0),
|
|
3247
3351
|
pendingBinaryFrame: null
|
|
3248
3352
|
};
|
|
@@ -3323,11 +3427,13 @@ export function createRemoteHub(options = {}) {
|
|
|
3323
3427
|
socket.on('close', () => {
|
|
3324
3428
|
allSockets.delete(socket);
|
|
3325
3429
|
clearTimeout(helloTimer);
|
|
3430
|
+
detachInputSocket(socket);
|
|
3326
3431
|
detachSocket(socket);
|
|
3327
3432
|
});
|
|
3328
3433
|
socket.on('error', err => {
|
|
3329
3434
|
allSockets.delete(socket);
|
|
3330
3435
|
clearTimeout(helloTimer);
|
|
3436
|
+
detachInputSocket(socket, err?.message || 'socket-error');
|
|
3331
3437
|
detachSocket(socket, err?.message || 'socket-error');
|
|
3332
3438
|
});
|
|
3333
3439
|
}
|
|
@@ -3514,6 +3620,24 @@ export function createRemoteHub(options = {}) {
|
|
|
3514
3620
|
return { ok: false, error: 'missing-input-type' };
|
|
3515
3621
|
}
|
|
3516
3622
|
|
|
3623
|
+
const inputSocket = device.inputSocket;
|
|
3624
|
+
if (inputSocket && !inputSocket.destroyed) {
|
|
3625
|
+
const sent = writeJsonLine(inputSocket, {
|
|
3626
|
+
type: 'input.control',
|
|
3627
|
+
payload: {
|
|
3628
|
+
...normalized,
|
|
3629
|
+
issuedAt: normalized.issuedAt || new Date().toISOString()
|
|
3630
|
+
}
|
|
3631
|
+
});
|
|
3632
|
+
if (sent) {
|
|
3633
|
+
device.counters.commandsSent += 1;
|
|
3634
|
+
device.inputLastSeenAt = new Date().toISOString();
|
|
3635
|
+
return { ok: true, inputSocket: true };
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
detachInputSocket(inputSocket, 'input-socket-write-failed');
|
|
3639
|
+
}
|
|
3640
|
+
|
|
3517
3641
|
return sendCommand(deviceId, {
|
|
3518
3642
|
command: 'input.control',
|
|
3519
3643
|
payload: {
|
|
@@ -3751,7 +3875,37 @@ export function createRemoteHub(options = {}) {
|
|
|
3751
3875
|
return false;
|
|
3752
3876
|
}
|
|
3753
3877
|
const startedAt = Date.parse(activeLiveStream.startedAt || '');
|
|
3754
|
-
return Number.isFinite(startedAt) && Date.now() - startedAt <
|
|
3878
|
+
return Number.isFinite(startedAt) && Date.now() - startedAt < LIVE_STREAM_PENDING_REUSE_MS;
|
|
3879
|
+
}
|
|
3880
|
+
|
|
3881
|
+
function getLiveStreamFreshWindowMs(activeLiveStream) {
|
|
3882
|
+
const fps = Number(activeLiveStream?.fps || 0);
|
|
3883
|
+
const frameMs = Number.isFinite(fps) && fps > 0 ? 1000 / fps : 500;
|
|
3884
|
+
return Math.max(
|
|
3885
|
+
LIVE_STREAM_MIN_FRESH_MS,
|
|
3886
|
+
Math.min(LIVE_STREAM_MAX_FRESH_MS, Math.round(frameMs * 20))
|
|
3887
|
+
);
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3890
|
+
function getLiveStreamIdleMs(activeLiveStream) {
|
|
3891
|
+
const lastFrameAt = Date.parse(activeLiveStream?.lastFrameAt || activeLiveStream?.openedAt || activeLiveStream?.startedAt || '');
|
|
3892
|
+
if (!Number.isFinite(lastFrameAt)) {
|
|
3893
|
+
return Infinity;
|
|
3894
|
+
}
|
|
3895
|
+
return Math.max(0, Date.now() - lastFrameAt);
|
|
3896
|
+
}
|
|
3897
|
+
|
|
3898
|
+
function liveStreamIsReusable(activeLiveStream) {
|
|
3899
|
+
if (!activeLiveStream?.active) {
|
|
3900
|
+
return false;
|
|
3901
|
+
}
|
|
3902
|
+
if (liveStreamStartStillPending(activeLiveStream)) {
|
|
3903
|
+
return true;
|
|
3904
|
+
}
|
|
3905
|
+
if (activeLiveStream.open !== true) {
|
|
3906
|
+
return false;
|
|
3907
|
+
}
|
|
3908
|
+
return getLiveStreamIdleMs(activeLiveStream) <= getLiveStreamFreshWindowMs(activeLiveStream);
|
|
3755
3909
|
}
|
|
3756
3910
|
|
|
3757
3911
|
function liveStreamMatchesOptions(activeLiveStream, normalized) {
|
|
@@ -3855,7 +4009,7 @@ export function createRemoteHub(options = {}) {
|
|
|
3855
4009
|
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
3856
4010
|
if (device.activeLiveStream?.streamId === streamId
|
|
3857
4011
|
&& liveStreamMatchesOptions(device.activeLiveStream, normalized)
|
|
3858
|
-
&& (device.activeLiveStream
|
|
4012
|
+
&& liveStreamIsReusable(device.activeLiveStream)) {
|
|
3859
4013
|
emitRemoteEvent('RemoteLiveStreamStartReused', device, {
|
|
3860
4014
|
streamId,
|
|
3861
4015
|
fps,
|
|
@@ -3874,6 +4028,21 @@ export function createRemoteHub(options = {}) {
|
|
|
3874
4028
|
reused: true
|
|
3875
4029
|
};
|
|
3876
4030
|
}
|
|
4031
|
+
if (device.activeLiveStream?.streamId === streamId
|
|
4032
|
+
&& liveStreamMatchesOptions(device.activeLiveStream, normalized)
|
|
4033
|
+
&& device.activeLiveStream.open === true
|
|
4034
|
+
&& !liveStreamIsReusable(device.activeLiveStream)) {
|
|
4035
|
+
emitRemoteEvent('RemoteLiveStreamStaleRestart', device, {
|
|
4036
|
+
streamId,
|
|
4037
|
+
fps,
|
|
4038
|
+
mode: transfer.mode,
|
|
4039
|
+
frameMode: transfer.frameMode,
|
|
4040
|
+
monitorIndex,
|
|
4041
|
+
idleMs: getLiveStreamIdleMs(device.activeLiveStream),
|
|
4042
|
+
staleAfterMs: getLiveStreamFreshWindowMs(device.activeLiveStream),
|
|
4043
|
+
lastFrameAt: device.activeLiveStream.lastFrameAt || ''
|
|
4044
|
+
});
|
|
4045
|
+
}
|
|
3877
4046
|
if (device.activeLiveStream?.active && device.activeLiveStream.streamId && device.activeLiveStream.streamId !== streamId) {
|
|
3878
4047
|
sendCommand(deviceId, {
|
|
3879
4048
|
command: 'stream.stop',
|
package/hub/src/server.js
CHANGED
|
@@ -27,6 +27,7 @@ const audioClients = new Set();
|
|
|
27
27
|
let frameClientSeq = 0;
|
|
28
28
|
let inputClientSeq = 0;
|
|
29
29
|
let audioClientSeq = 0;
|
|
30
|
+
const FRAME_LIVE_WATCHDOG_MS = 2500;
|
|
30
31
|
|
|
31
32
|
function handleRemoteHubEvent(type, event) {
|
|
32
33
|
if (type !== 'RemoteDeviceConnected') {
|
|
@@ -246,6 +247,9 @@ function startFrameSubscriptionLive(ws, reason = 'subscribe', onlyDeviceId = '')
|
|
|
246
247
|
skipped.push({ deviceId, reason: result?.error || 'start-failed' });
|
|
247
248
|
}
|
|
248
249
|
}
|
|
250
|
+
if (reason === 'watchdog' && !started.some(item => item.reused !== true)) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
249
253
|
sendJson(ws, {
|
|
250
254
|
type: 'RemoteFrameLiveAutoStart',
|
|
251
255
|
timestamp: new Date().toISOString(),
|
|
@@ -702,6 +706,16 @@ httpServer.on('upgrade', (req, socket, head) => {
|
|
|
702
706
|
frameWss.on('connection', (ws, req) => {
|
|
703
707
|
frameClients.add(ws);
|
|
704
708
|
ws.liveDeskFrameClientId = `rfws-${++frameClientSeq}`;
|
|
709
|
+
const liveWatchdog = setInterval(() => {
|
|
710
|
+
if (ws.readyState === 1) {
|
|
711
|
+
startFrameSubscriptionLive(ws, 'watchdog');
|
|
712
|
+
}
|
|
713
|
+
}, FRAME_LIVE_WATCHDOG_MS);
|
|
714
|
+
liveWatchdog.unref?.();
|
|
715
|
+
const cleanup = () => {
|
|
716
|
+
clearInterval(liveWatchdog);
|
|
717
|
+
frameClients.delete(ws);
|
|
718
|
+
};
|
|
705
719
|
try {
|
|
706
720
|
const parsed = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
|
|
707
721
|
updateFrameSubscription(ws, { deviceIds: parsed.searchParams.get('devices') || '' });
|
|
@@ -718,8 +732,8 @@ frameWss.on('connection', (ws, req) => {
|
|
|
718
732
|
sendJson(ws, { type: 'RemoteFrameSubscriptionError', error: 'invalid-message' });
|
|
719
733
|
}
|
|
720
734
|
});
|
|
721
|
-
ws.on('close',
|
|
722
|
-
ws.on('error',
|
|
735
|
+
ws.on('close', cleanup);
|
|
736
|
+
ws.on('error', cleanup);
|
|
723
737
|
sendJson(ws, {
|
|
724
738
|
type: 'RemoteFrameSocketReady',
|
|
725
739
|
protocol: 'livedesk.remote.frames.binary.v1',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livedesk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.83",
|
|
4
4
|
"description": "LiveDesk Hub and client launcher",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"node": ">=20"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@livedesk/client": "0.1.
|
|
33
|
+
"@livedesk/client": "0.1.62",
|
|
34
34
|
"cors": "^2.8.5",
|
|
35
35
|
"express": "^4.21.2",
|
|
36
36
|
"ws": "^8.18.3"
|