livedesk 0.1.367 → 0.1.369

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
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url';
9
9
  import os from 'node:os';
10
10
  import { WebSocketServer } from 'ws';
11
11
  import { createRemoteHub } from './remote-hub.js';
12
- import { buildMode4AtlasSessionKey, Mode4AtlasPool } from './mode4-atlas-pool.js';
12
+ import { buildMode4AtlasSessionKey, Mode4AtlasPool, planMode4AtlasInputTransitions } from './mode4-atlas-pool.js';
13
13
  import { createHubFilesystem } from './filesystem/hub-filesystem.js';
14
14
  import { createHubTransferJobs } from './filesystem/transfer-jobs.js';
15
15
  import { createHubSharedFolders } from './filesystem/shared-folders.js';
@@ -155,8 +155,8 @@ function readPositiveIntegerEnv(name, fallback) {
155
155
  return Number.isFinite(value) && value > 0 ? Math.round(value) : fallback;
156
156
  }
157
157
 
158
- function handleRemoteHubEvent(type, event) {
159
- liveDeskUpdateManager?.handleRemoteEvent(type, event);
158
+ function handleRemoteHubEvent(type, event) {
159
+ liveDeskUpdateManager?.handleRemoteEvent(type, event);
160
160
  if (type === 'RemoteInputApplied') {
161
161
  for (const ws of inputClients) {
162
162
  sendJson(ws, {
@@ -166,16 +166,16 @@ function handleRemoteHubEvent(type, event) {
166
166
  }
167
167
  return;
168
168
  }
169
- if (type === 'RemoteInputError') {
169
+ if (type === 'RemoteInputError') {
170
170
  for (const ws of inputClients) {
171
171
  sendJson(ws, {
172
172
  type: 'RemoteInputError',
173
173
  ...event
174
174
  });
175
- }
176
- return;
177
- }
178
- if (type === 'RemoteDeviceConnected' || type === 'RemoteDeviceDisconnected') {
175
+ }
176
+ return;
177
+ }
178
+ if (type === 'RemoteDeviceConnected' || type === 'RemoteDeviceDisconnected') {
179
179
  connectedDeviceCount = Number(remoteHub.getStatus({ includeSecrets: false }).connectedDeviceCount || 0);
180
180
  }
181
181
  if (type !== 'RemoteDeviceConnected') {
@@ -1365,7 +1365,7 @@ function safeWebSocketSend(ws, payload, options = undefined) {
1365
1365
  }
1366
1366
  }
1367
1367
 
1368
- function unregisterFrameClient(ws) {
1368
+ function unregisterFrameClient(ws) {
1369
1369
  if (!ws) {
1370
1370
  return;
1371
1371
  }
@@ -1380,10 +1380,11 @@ function unregisterFrameClient(ws) {
1380
1380
  if (clients.size === 0) {
1381
1381
  frameClientsByDeviceId.delete(deviceId);
1382
1382
  }
1383
- }
1384
- frameWildcardClients.delete(ws);
1385
- ws.liveDeskFrameSendLane?.queue?.splice?.(0);
1386
- }
1383
+ }
1384
+ frameWildcardClients.delete(ws);
1385
+ ws.liveDeskExpectedStreamBindingsByDeviceId?.clear?.();
1386
+ ws.liveDeskFrameSendLane?.queue?.splice?.(0);
1387
+ }
1387
1388
 
1388
1389
  function hasOtherFrameStreamOwner(ws, deviceId, streamId, streamPurpose = '') {
1389
1390
  if (!streamId) {
@@ -1617,7 +1618,7 @@ function enqueueFramePacketForClient(ws, packet, meta) {
1617
1618
  return true;
1618
1619
  }
1619
1620
 
1620
- function updateFrameSubscription(ws, payload = {}) {
1621
+ function updateFrameSubscription(ws, payload = {}) {
1621
1622
  const previousDeviceIds = ws.liveDeskDeviceIds instanceof Set
1622
1623
  ? new Set(ws.liveDeskDeviceIds)
1623
1624
  : new Set();
@@ -1627,14 +1628,18 @@ function updateFrameSubscription(ws, payload = {}) {
1627
1628
  registerFrameClient(ws);
1628
1629
  ws.liveDeskAutoStart = /^(1|true|yes|on|live)$/i.test(String(payload.autoStartLive ?? payload.startLive ?? ''));
1629
1630
  ws.liveDeskLiveOptions = normalizeLiveOptions(payload);
1630
- if (!(ws.liveDeskStreamIdsByDeviceId instanceof Map)) {
1631
- ws.liveDeskStreamIdsByDeviceId = new Map();
1632
- }
1633
- for (const subscribedDeviceId of ws.liveDeskStreamIdsByDeviceId.keys()) {
1634
- if (!ws.liveDeskDeviceIds.has(subscribedDeviceId)) {
1635
- ws.liveDeskStreamIdsByDeviceId.delete(subscribedDeviceId);
1636
- }
1637
- }
1631
+ if (!(ws.liveDeskStreamIdsByDeviceId instanceof Map)) {
1632
+ ws.liveDeskStreamIdsByDeviceId = new Map();
1633
+ }
1634
+ if (!(ws.liveDeskExpectedStreamBindingsByDeviceId instanceof Map)) {
1635
+ ws.liveDeskExpectedStreamBindingsByDeviceId = new Map();
1636
+ }
1637
+ for (const subscribedDeviceId of ws.liveDeskStreamIdsByDeviceId.keys()) {
1638
+ if (!ws.liveDeskDeviceIds.has(subscribedDeviceId)) {
1639
+ ws.liveDeskStreamIdsByDeviceId.delete(subscribedDeviceId);
1640
+ ws.liveDeskExpectedStreamBindingsByDeviceId.delete(subscribedDeviceId);
1641
+ }
1642
+ }
1638
1643
  sendJson(ws, {
1639
1644
  type: 'RemoteFrameSubscription',
1640
1645
  timestamp: new Date().toISOString(),
@@ -1688,15 +1693,46 @@ function startFrameSubscriptionLive(ws, reason = 'subscribe', onlyDeviceId = '',
1688
1693
  reuseExisting: liveOptions.forceRestart !== true
1689
1694
  && (liveOptions.reuseExisting === true || reason === 'subscribe' || reason === 'watchdog'),
1690
1695
  silentReuse: reason === 'watchdog' && liveOptions.forceRestart !== true
1691
- });
1692
- if (result?.ok) {
1693
- ws.liveDeskStreamIdsByDeviceId.set(deviceId, result.streamId);
1694
- cancelPendingFrameStreamStop(deviceId, result.streamId, String(liveOptions.streamPurpose || 'wall'));
1695
- started.push({ deviceId, streamId: result.streamId, fps: result.fps, reused: result.reused === true });
1696
- } else {
1697
- ws.liveDeskStreamIdsByDeviceId.delete(deviceId);
1698
- skipped.push({ deviceId, reason: result?.error || 'start-failed' });
1699
- }
1696
+ });
1697
+ if (result?.ok) {
1698
+ const expectedBinding = {
1699
+ deviceId,
1700
+ sessionId: String(result.sessionId || device.sessionId || ''),
1701
+ streamId: String(result.streamId || ''),
1702
+ streamPurpose: String(result.streamPurpose || liveOptions.streamPurpose || 'wall'),
1703
+ commandId: String(result.commandId || ''),
1704
+ captureGeneration: Number(result.captureGeneration || 0),
1705
+ monitorIndex: Number(result.monitorIndex || 0),
1706
+ readySent: false
1707
+ };
1708
+ const activeStream = device?.activeLiveStream;
1709
+ const reusedFrameReady = result.reused === true
1710
+ && result.ready === true
1711
+ && Number(result.framesReceived ?? activeStream?.framesReceived ?? 0) > 0
1712
+ && String(activeStream?.streamId || expectedBinding.streamId) === expectedBinding.streamId
1713
+ && String(activeStream?.commandId || expectedBinding.commandId) === expectedBinding.commandId
1714
+ && Number(activeStream?.captureGeneration || expectedBinding.captureGeneration) === expectedBinding.captureGeneration;
1715
+ expectedBinding.readySent = reusedFrameReady;
1716
+ ws.liveDeskStreamIdsByDeviceId.set(deviceId, result.streamId);
1717
+ ws.liveDeskExpectedStreamBindingsByDeviceId.set(deviceId, expectedBinding);
1718
+ cancelPendingFrameStreamStop(deviceId, result.streamId, String(liveOptions.streamPurpose || 'wall'));
1719
+ started.push({
1720
+ deviceId: expectedBinding.deviceId,
1721
+ sessionId: expectedBinding.sessionId,
1722
+ streamId: expectedBinding.streamId,
1723
+ streamPurpose: expectedBinding.streamPurpose,
1724
+ commandId: expectedBinding.commandId,
1725
+ captureGeneration: expectedBinding.captureGeneration,
1726
+ monitorIndex: expectedBinding.monitorIndex,
1727
+ ready: reusedFrameReady,
1728
+ fps: result.fps,
1729
+ reused: result.reused === true
1730
+ });
1731
+ } else {
1732
+ ws.liveDeskStreamIdsByDeviceId.delete(deviceId);
1733
+ ws.liveDeskExpectedStreamBindingsByDeviceId.delete(deviceId);
1734
+ skipped.push({ deviceId, reason: result?.error || 'start-failed' });
1735
+ }
1700
1736
  }
1701
1737
  if (reason === 'watchdog' && !started.some(item => item.reused !== true)) {
1702
1738
  return;
@@ -1895,15 +1931,36 @@ function broadcastRemoteBinaryFrame(frameEvent) {
1895
1931
  const frameSeq = Number(frame.frameSeq || 0) || 0;
1896
1932
  let sharedPacket = null;
1897
1933
  for (const client of targetClients) {
1898
- if (client.readyState !== client.OPEN) {
1899
- continue;
1900
- }
1901
- const expectedStreamId = client.liveDeskStreamIdsByDeviceId instanceof Map
1902
- ? client.liveDeskStreamIdsByDeviceId.get(deviceId)
1903
- : '';
1904
- if (expectedStreamId && String(frame.streamId || '') !== expectedStreamId) {
1905
- continue;
1906
- }
1934
+ if (client.readyState !== client.OPEN) {
1935
+ continue;
1936
+ }
1937
+ const expectedBinding = client.liveDeskExpectedStreamBindingsByDeviceId instanceof Map
1938
+ ? client.liveDeskExpectedStreamBindingsByDeviceId.get(deviceId)
1939
+ : null;
1940
+ if (!expectedBinding
1941
+ || String(frame.streamId || '') !== expectedBinding.streamId
1942
+ || String(frame.sessionId || '') !== expectedBinding.sessionId
1943
+ || String(frame.commandId || '') !== expectedBinding.commandId
1944
+ || Number(frame.captureGeneration || 0) !== expectedBinding.captureGeneration
1945
+ || String(frame.streamPurpose || '') !== expectedBinding.streamPurpose
1946
+ || Number(frame.monitorIndex || 0) !== expectedBinding.monitorIndex) {
1947
+ continue;
1948
+ }
1949
+ if (!expectedBinding.readySent) {
1950
+ expectedBinding.readySent = sendJson(client, {
1951
+ type: 'RemoteFrameStreamReady',
1952
+ deviceId,
1953
+ sessionId: expectedBinding.sessionId,
1954
+ streamId: expectedBinding.streamId,
1955
+ streamPurpose: expectedBinding.streamPurpose,
1956
+ commandId: expectedBinding.commandId,
1957
+ captureGeneration: expectedBinding.captureGeneration,
1958
+ monitorIndex: expectedBinding.monitorIndex
1959
+ });
1960
+ if (!expectedBinding.readySent) {
1961
+ continue;
1962
+ }
1963
+ }
1907
1964
  const lane = ensureFrameClientSendLane(client);
1908
1965
  if (client.liveDeskFrameBackpressured && isH264 && !isKeyFrame) {
1909
1966
  lane.dropped += 1;
@@ -2032,11 +2089,14 @@ function configureMode4Atlas(ws, payload = {}) {
2032
2089
  const tileHeight = sharpTiles ? 270 : 180;
2033
2090
  const previousInputDeviceIds = ws.liveDeskAtlasInputDeviceIds instanceof Set
2034
2091
  ? new Set(ws.liveDeskAtlasInputDeviceIds)
2035
- : new Set(deviceIds);
2036
- const previousMonitorSelections = ws.liveDeskAtlasInputOptions?.monitorSelections || {};
2092
+ : new Set();
2093
+ const previousInputOptions = ws.liveDeskAtlasInputOptions || {};
2037
2094
  const atlasConfig = {
2038
2095
  deviceIds,
2039
- inputDeviceIds: captureDeviceIds,
2096
+ // Composition ownership is independent from capture ownership. Keeping
2097
+ // every layout device ingestible lets a paused/reused input retain its
2098
+ // latest tile while only the changed device restarts capture.
2099
+ inputDeviceIds: deviceIds,
2040
2100
  width: clampNumber(payload.width, 640, 3840, 1920),
2041
2101
  height: clampNumber(payload.height, 360, 2160, 1080),
2042
2102
  tileWidth,
@@ -2061,22 +2121,32 @@ function configureMode4Atlas(ws, payload = {}) {
2061
2121
  ws.liveDeskAtlasDeviceIds = new Set(deviceIds);
2062
2122
  ws.liveDeskAtlasInputDeviceIds = new Set(captureDeviceIds);
2063
2123
  ws.liveDeskAtlasInputOptions = { tileWidth, tileHeight, monitorSelections };
2064
- for (const deviceId of previousInputDeviceIds) {
2065
- if (ws.liveDeskAtlasInputDeviceIds.has(deviceId)) {
2066
- continue;
2067
- }
2068
- stopMode4AtlasInput(ws, deviceId, 'atlas-input-paused');
2069
- }
2070
- for (const deviceId of captureDeviceIds) {
2071
- const previousMonitorIndex = Number(previousMonitorSelections[deviceId] || 0);
2072
- const nextMonitorIndex = Number(monitorSelections[deviceId] || 0);
2073
- startMode4AtlasInput(ws, deviceId, 'atlas-configure', {
2074
- forceRestart: previousInputDeviceIds.has(deviceId) && previousMonitorIndex !== nextMonitorIndex
2124
+ const inputTransitions = planMode4AtlasInputTransitions({
2125
+ inputDeviceIds: [...previousInputDeviceIds],
2126
+ tileWidth: previousInputOptions.tileWidth,
2127
+ tileHeight: previousInputOptions.tileHeight,
2128
+ monitorSelections: previousInputOptions.monitorSelections
2129
+ }, {
2130
+ inputDeviceIds: captureDeviceIds,
2131
+ tileWidth,
2132
+ tileHeight,
2133
+ monitorSelections
2134
+ });
2135
+ for (const deviceId of inputTransitions.stop) {
2136
+ stopMode4AtlasInput(ws, deviceId, 'atlas-input-paused');
2137
+ }
2138
+ for (const deviceId of inputTransitions.start) {
2139
+ startMode4AtlasInput(ws, deviceId, 'atlas-input-started');
2140
+ }
2141
+ for (const deviceId of inputTransitions.restart) {
2142
+ startMode4AtlasInput(ws, deviceId, 'atlas-input-changed', {
2143
+ forceRestart: true
2075
2144
  });
2076
2145
  }
2077
- sendJson(ws, {
2078
- type: 'Mode4AtlasSubscription',
2079
- deviceIds,
2146
+ sendJson(ws, {
2147
+ type: 'Mode4AtlasSubscription',
2148
+ streamId: ws.liveDeskAtlasStreamId,
2149
+ deviceIds,
2080
2150
  captureDeviceIds,
2081
2151
  inputMode: 'mode2-lzo',
2082
2152
  outputMode: 'mode4-h264-atlas',
@@ -3846,11 +3916,12 @@ inputWss.on('connection', ws => {
3846
3916
  } catch {
3847
3917
  sendJson(ws, { type: 'RemoteInputError', error: 'invalid-json' });
3848
3918
  return;
3849
- }
3919
+ }
3850
3920
  const deviceId = String(payload?.deviceId || '').trim();
3921
+ const inputEventId = String(payload?.requestId || '').trim() || crypto.randomUUID();
3851
3922
  const input = payload?.input && typeof payload.input === 'object'
3852
- ? { ...payload.input, hubConnectionId: ws.liveDeskInputClientId, hubReceivedAtEpochMs: Date.now() }
3853
- : { ...payload, hubConnectionId: ws.liveDeskInputClientId, hubReceivedAtEpochMs: Date.now() };
3923
+ ? { ...payload.input, inputEventId, hubConnectionId: ws.liveDeskInputClientId, hubReceivedAtEpochMs: Date.now() }
3924
+ : { ...payload, inputEventId, hubConnectionId: ws.liveDeskInputClientId, hubReceivedAtEpochMs: Date.now() };
3854
3925
  const result = remoteHub.sendInputControl(deviceId, input);
3855
3926
  const inputType = String(input?.type || '').toLowerCase();
3856
3927
  const fireAndForget = payload?.fireAndForget === true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.367",
3
+ "version": "0.1.369",
4
4
  "buildFlavor": "production",
5
5
  "description": "LiveDesk Hub and client launcher",
6
6
  "type": "module",
@@ -48,10 +48,10 @@
48
48
  "ws": "^8.18.3"
49
49
  },
50
50
  "optionalDependencies": {
51
- "@livedesk/fast-linux-x64": "0.1.367",
52
- "@livedesk/fast-osx-arm64": "0.1.367",
53
- "@livedesk/fast-osx-x64": "0.1.367",
54
- "@livedesk/fast-win-x64": "0.1.367"
51
+ "@livedesk/fast-linux-x64": "0.1.369",
52
+ "@livedesk/fast-osx-arm64": "0.1.369",
53
+ "@livedesk/fast-osx-x64": "0.1.369",
54
+ "@livedesk/fast-win-x64": "0.1.369"
55
55
  },
56
56
  "publishConfig": {
57
57
  "access": "public"