livedesk 0.1.102 → 0.1.103

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.
@@ -1211,6 +1211,7 @@ export function createRemoteHub(options = {}) {
1211
1211
  const taskBatches = new Map();
1212
1212
  const sockets = new Map();
1213
1213
  const inputSockets = new Map();
1214
+ const frameSockets = new Map();
1214
1215
  const allSockets = new Set();
1215
1216
  const duplicateDeviceLogAt = new Map();
1216
1217
  let server = null;
@@ -1745,6 +1746,7 @@ export function createRemoteHub(options = {}) {
1745
1746
  function closeExistingDeviceSocket(deviceId, nextSessionId) {
1746
1747
  const existing = devices.get(deviceId);
1747
1748
  closeInputSocket(existing, 'replaced-by-new-session');
1749
+ closeFrameSocket(existing, 'replaced-by-new-session');
1748
1750
  if (!existing?.socket || existing.socket.destroyed) {
1749
1751
  return;
1750
1752
  }
@@ -1759,6 +1761,26 @@ export function createRemoteHub(options = {}) {
1759
1761
  existing.socket.destroy();
1760
1762
  }
1761
1763
 
1764
+ function closeFrameSocket(device, reason = 'frame-socket-closed') {
1765
+ const socket = device?.frameSocket;
1766
+ if (!socket) {
1767
+ return;
1768
+ }
1769
+
1770
+ device.frameSocket = null;
1771
+ frameSockets.delete(socket);
1772
+ try {
1773
+ writeJsonLine(socket, { type: 'disconnect', channel: 'frame', reason });
1774
+ } catch {
1775
+ // Best-effort notice before closing the side channel.
1776
+ }
1777
+ try {
1778
+ socket.destroy?.();
1779
+ } catch {
1780
+ // Ignore close failures.
1781
+ }
1782
+ }
1783
+
1762
1784
  function closeInputSocket(device, reason = 'input-socket-closed') {
1763
1785
  const socket = device?.inputSocket;
1764
1786
  if (!socket) {
@@ -1796,6 +1818,23 @@ export function createRemoteHub(options = {}) {
1796
1818
  emitRemoteEvent('RemoteInputSocketDisconnected', device, { reason });
1797
1819
  }
1798
1820
 
1821
+ function detachFrameSocket(socket, reason = 'frame-socket-closed') {
1822
+ const deviceId = frameSockets.get(socket);
1823
+ frameSockets.delete(socket);
1824
+ if (!deviceId) {
1825
+ return;
1826
+ }
1827
+
1828
+ const device = devices.get(deviceId);
1829
+ if (!device || device.frameSocket !== socket) {
1830
+ return;
1831
+ }
1832
+
1833
+ device.frameSocket = null;
1834
+ device.frameLastSeenAt = new Date().toISOString();
1835
+ emitRemoteEvent('RemoteFrameSocketDisconnected', device, { reason });
1836
+ }
1837
+
1799
1838
  function getDeviceActivityMs(device) {
1800
1839
  return Math.max(
1801
1840
  Date.parse(device?.lastSeenAt || '') || 0,
@@ -2181,6 +2220,7 @@ export function createRemoteHub(options = {}) {
2181
2220
  const device = {
2182
2221
  socket,
2183
2222
  inputSocket: null,
2223
+ frameSocket: null,
2184
2224
  deviceId,
2185
2225
  sessionId,
2186
2226
  deviceName: safeString(hello.deviceName || hello.hostname || deviceId, 120),
@@ -2199,6 +2239,8 @@ export function createRemoteHub(options = {}) {
2199
2239
  connectedAt: now,
2200
2240
  inputConnectedAt: '',
2201
2241
  inputLastSeenAt: '',
2242
+ frameConnectedAt: '',
2243
+ frameLastSeenAt: '',
2202
2244
  disconnectedAt: '',
2203
2245
  lastSeenAt: now,
2204
2246
  lastStatusAt: '',
@@ -2273,6 +2315,39 @@ export function createRemoteHub(options = {}) {
2273
2315
  return device;
2274
2316
  }
2275
2317
 
2318
+ function attachFrameSocket(socket, hello) {
2319
+ const deviceId = normalizeDeviceId(hello.deviceId);
2320
+ const device = devices.get(deviceId);
2321
+ if (!device || !device.connected || !device.socket || device.socket.destroyed) {
2322
+ writeJsonLine(socket, { type: 'error', error: 'device-not-connected' });
2323
+ socket.destroy();
2324
+ return null;
2325
+ }
2326
+
2327
+ closeFrameSocket(device, 'replaced-by-new-frame-socket');
2328
+
2329
+ const now = new Date().toISOString();
2330
+ device.frameSocket = socket;
2331
+ device.frameConnectedAt = now;
2332
+ device.frameLastSeenAt = now;
2333
+ frameSockets.set(socket, deviceId);
2334
+
2335
+ writeJsonLine(socket, {
2336
+ type: 'welcome',
2337
+ channel: 'frame',
2338
+ protocol: REMOTE_AGENT_PROTOCOL,
2339
+ protocolVersion: REMOTE_PROTOCOL_VERSION,
2340
+ sessionId: device.sessionId,
2341
+ deviceId,
2342
+ frameProtocol: buildRemoteFrameProtocolDescriptor(),
2343
+ frameModes: getSupportedRemoteFrameModeProfiles(),
2344
+ serverTime: now
2345
+ });
2346
+
2347
+ emitRemoteEvent('RemoteFrameSocketConnected', device);
2348
+ return device;
2349
+ }
2350
+
2276
2351
  function detachSocket(socket, reason = 'socket-closed') {
2277
2352
  const deviceId = sockets.get(socket);
2278
2353
  sockets.delete(socket);
@@ -2288,6 +2363,7 @@ export function createRemoteHub(options = {}) {
2288
2363
  device.connected = false;
2289
2364
  device.socket = null;
2290
2365
  closeInputSocket(device, reason);
2366
+ closeFrameSocket(device, reason);
2291
2367
  device.disconnectedAt = new Date().toISOString();
2292
2368
  device.lastDisconnectReason = reason;
2293
2369
  if (device.activeLiveStream) {
@@ -3060,6 +3136,9 @@ export function createRemoteHub(options = {}) {
3060
3136
 
3061
3137
  device.counters.messagesReceived += 1;
3062
3138
  device.lastSeenAt = new Date().toISOString();
3139
+ if (state.frameOnly) {
3140
+ device.frameLastSeenAt = device.lastSeenAt;
3141
+ }
3063
3142
 
3064
3143
  const frameKind = safeString(header.frameKind || header.kind || header.frameType || '', 40).toLowerCase();
3065
3144
  if (frameKind === 'thumbnail' || header.type === 'thumbnail.binary') {
@@ -3113,6 +3192,17 @@ export function createRemoteHub(options = {}) {
3113
3192
  state.inputOnly = true;
3114
3193
  return;
3115
3194
  }
3195
+ if (channel === 'frame') {
3196
+ const device = attachFrameSocket(socket, message);
3197
+ if (!device) {
3198
+ return;
3199
+ }
3200
+
3201
+ state.authenticated = true;
3202
+ state.device = device;
3203
+ state.frameOnly = true;
3204
+ return;
3205
+ }
3116
3206
 
3117
3207
  const device = attachDevice(socket, message);
3118
3208
  if (!device) {
@@ -3134,6 +3224,10 @@ export function createRemoteHub(options = {}) {
3134
3224
  device.inputLastSeenAt = new Date().toISOString();
3135
3225
  return;
3136
3226
  }
3227
+ if (state.frameOnly) {
3228
+ device.frameLastSeenAt = new Date().toISOString();
3229
+ return;
3230
+ }
3137
3231
 
3138
3232
  device.counters.messagesReceived += 1;
3139
3233
  device.lastSeenAt = new Date().toISOString();
@@ -3207,7 +3301,8 @@ export function createRemoteHub(options = {}) {
3207
3301
  const state = {
3208
3302
  authenticated: false,
3209
3303
  device: null,
3210
- inputOnly: false
3304
+ inputOnly: false,
3305
+ frameOnly: false
3211
3306
  };
3212
3307
 
3213
3308
  const helloTimer = setTimeout(() => {
@@ -3259,6 +3354,7 @@ export function createRemoteHub(options = {}) {
3259
3354
  allSockets.delete(socket);
3260
3355
  clearTimeout(helloTimer);
3261
3356
  detachInputSocket(socket, reason);
3357
+ detachFrameSocket(socket, reason);
3262
3358
  detachSocket(socket, reason);
3263
3359
  };
3264
3360
 
@@ -4009,6 +4105,21 @@ export function createRemoteHub(options = {}) {
4009
4105
  const normalized = normalizeLiveStreamStartOptions(options);
4010
4106
  const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer } = normalized;
4011
4107
  const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
4108
+ if (device.activeLiveStream?.streamId === streamId
4109
+ && options.forceRestart !== true
4110
+ && options.reuseExisting === true
4111
+ && liveStreamIsReusable(device.activeLiveStream)) {
4112
+ return {
4113
+ ok: true,
4114
+ commandId: device.activeLiveStream.commandId,
4115
+ streamId,
4116
+ fps: Number(device.activeLiveStream.fps || fps),
4117
+ mode: device.activeLiveStream.mode || transfer.mode,
4118
+ frameMode: device.activeLiveStream.frameMode || transfer.frameMode,
4119
+ monitorIndex: Number(device.activeLiveStream.monitorIndex || monitorIndex),
4120
+ reused: true
4121
+ };
4122
+ }
4012
4123
  if (device.activeLiveStream?.streamId === streamId
4013
4124
  && options.forceRestart !== true
4014
4125
  && liveStreamMatchesOptions(device.activeLiveStream, normalized)
package/hub/src/server.js CHANGED
@@ -27,7 +27,6 @@ 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;
31
30
 
32
31
  function handleRemoteHubEvent(type, event) {
33
32
  if (type !== 'RemoteDeviceConnected') {
@@ -221,6 +220,9 @@ function safeWebSocketSend(ws, payload, options = undefined) {
221
220
  }
222
221
 
223
222
  function updateFrameSubscription(ws, payload = {}) {
223
+ const previousDeviceIds = ws.liveDeskDeviceIds instanceof Set
224
+ ? new Set(ws.liveDeskDeviceIds)
225
+ : new Set();
224
226
  const deviceIds = normalizeDeviceIds(payload.deviceIds ?? payload.devices ?? payload.deviceId);
225
227
  ws.liveDeskDeviceIds = new Set(deviceIds);
226
228
  ws.liveDeskAutoStart = /^(1|true|yes|on|live)$/i.test(String(payload.autoStartLive ?? payload.startLive ?? ''));
@@ -232,7 +234,11 @@ function updateFrameSubscription(ws, payload = {}) {
232
234
  autoStartLive: ws.liveDeskAutoStart
233
235
  });
234
236
 
235
- startFrameSubscriptionLive(ws, 'subscribe');
237
+ const newlySubscribedIds = deviceIds.filter(deviceId => !previousDeviceIds.has(deviceId));
238
+ const targetIds = newlySubscribedIds.length > 0 ? newlySubscribedIds : deviceIds;
239
+ for (const deviceId of targetIds.slice(0, 80)) {
240
+ startFrameSubscriptionLive(ws, 'subscribe', deviceId, { reuseExisting: true });
241
+ }
236
242
  }
237
243
 
238
244
  function startFrameSubscriptionLive(ws, reason = 'subscribe', onlyDeviceId = '', overrideLiveOptions = null) {
@@ -271,6 +277,7 @@ function startFrameSubscriptionLive(ws, reason = 'subscribe', onlyDeviceId = '',
271
277
  const result = remoteHub.startLiveStream(deviceId, {
272
278
  ...liveOptions,
273
279
  monitorIndex,
280
+ reuseExisting: liveOptions.reuseExisting === true || reason === 'subscribe' || reason === 'watchdog',
274
281
  silentReuse: reason === 'watchdog' && liveOptions.forceRestart !== true
275
282
  });
276
283
  if (result?.ok) {
@@ -761,14 +768,7 @@ httpServer.on('upgrade', (req, socket, head) => {
761
768
  frameWss.on('connection', (ws, req) => {
762
769
  frameClients.add(ws);
763
770
  ws.liveDeskFrameClientId = `rfws-${++frameClientSeq}`;
764
- const liveWatchdog = setInterval(() => {
765
- if (ws.readyState === 1) {
766
- startFrameSubscriptionLive(ws, 'watchdog');
767
- }
768
- }, FRAME_LIVE_WATCHDOG_MS);
769
- liveWatchdog.unref?.();
770
771
  const cleanup = () => {
771
- clearInterval(liveWatchdog);
772
772
  frameClients.delete(ws);
773
773
  };
774
774
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.102",
3
+ "version": "0.1.103",
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.74",
33
+ "@livedesk/client": "0.1.75",
34
34
  "cors": "^2.8.5",
35
35
  "express": "^4.21.2",
36
36
  "ws": "^8.18.3"