livedesk 0.1.225 → 0.1.227

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.
@@ -10,6 +10,8 @@ export class Mode4AtlasSession {
10
10
  this.onStatus = typeof options.onStatus === 'function' ? options.onStatus : () => {};
11
11
  this.deviceIds = [];
12
12
  this.deviceSet = new Set();
13
+ this.inputDeviceIds = [];
14
+ this.inputDeviceSet = new Set();
13
15
  this.config = null;
14
16
  this.closed = false;
15
17
  this.worker = null;
@@ -65,7 +67,10 @@ export class Mode4AtlasSession {
65
67
  this.deviceIds = [...new Set((Array.isArray(options.deviceIds) ? options.deviceIds : [])
66
68
  .map(value => String(value || '').trim()).filter(Boolean))].slice(0, 100);
67
69
  this.deviceSet = new Set(this.deviceIds);
68
- this.pendingFrames = new Map([...this.pendingFrames].filter(([deviceId]) => this.deviceSet.has(deviceId)));
70
+ this.inputDeviceIds = [...new Set((Array.isArray(options.inputDeviceIds) ? options.inputDeviceIds : this.deviceIds)
71
+ .map(value => String(value || '').trim()).filter(value => this.deviceSet.has(value)))].slice(0, 100);
72
+ this.inputDeviceSet = new Set(this.inputDeviceIds);
73
+ this.pendingFrames = new Map([...this.pendingFrames].filter(([deviceId]) => this.inputDeviceSet.has(deviceId)));
69
74
  this.config = {
70
75
  deviceIds: this.deviceIds,
71
76
  width: Number(options.width || 1920),
@@ -102,7 +107,7 @@ export class Mode4AtlasSession {
102
107
  const frame = frameEvent?.frame || {};
103
108
  const deviceId = String(frameEvent?.deviceId || frame.deviceId || '').trim();
104
109
  const mode = String(frame.frameMode || frame.mode || '').toLowerCase();
105
- if (!this.deviceSet.has(deviceId) || mode !== 'mode2-lzo' || !Buffer.isBuffer(frameEvent?.payload)) return;
110
+ if (!this.inputDeviceSet.has(deviceId) || mode !== 'mode2-lzo' || !Buffer.isBuffer(frameEvent?.payload)) return;
106
111
  this.pendingFrames.set(deviceId, {
107
112
  type: 'frame',
108
113
  deviceId,
@@ -3474,6 +3474,12 @@ export function createRemoteHub(options = {}) {
3474
3474
  transferProtocolVersion: transfer.transferProtocolVersion,
3475
3475
  handshake: transfer.handshake,
3476
3476
  fps: Number.isFinite(Number(message.fps)) ? Number(message.fps) : streamState.fps,
3477
+ requestedFps: Number.isFinite(Number(message.requestedFps)) ? Number(message.requestedFps) : Number(streamState.fps || 0),
3478
+ effectiveFps: Number.isFinite(Number(message.effectiveFps)) ? Number(message.effectiveFps) : Number(message.fps || streamState.fps || 0),
3479
+ captureAverageMs: readFrameStageMs(message, 'captureAverageMs'),
3480
+ captureP95Ms: readFrameStageMs(message, 'captureP95Ms'),
3481
+ slowFrameCount: Number.isFinite(Number(message.slowFrameCount)) ? Number(message.slowFrameCount) : 0,
3482
+ captureHelperRestarts: Number.isFinite(Number(message.captureHelperRestarts)) ? Number(message.captureHelperRestarts) : 0,
3477
3483
  capturedAt,
3478
3484
  receivedAt: device.lastSeenAt,
3479
3485
  byteLength,
@@ -4809,16 +4815,27 @@ export function createRemoteHub(options = {}) {
4809
4815
  return `${role}-${hash}`;
4810
4816
  }
4811
4817
 
4818
+ function normalizeLivePlatform(value) {
4819
+ const normalized = safeString(value, 32).toLowerCase();
4820
+ if (['macos', 'darwin', 'mac', 'macosx'].includes(normalized)) return 'macos';
4821
+ if (['windows', 'win32', 'win32nt'].includes(normalized)) return 'windows';
4822
+ if (['linux', 'unix'].includes(normalized)) return 'linux';
4823
+ return normalized || 'unknown';
4824
+ }
4825
+
4812
4826
  function normalizeLiveStreamStartOptions(options = {}) {
4813
4827
  const transfer = buildRemoteFrameTransferDescriptor(options.mode || options.frameMode || DEFAULT_REMOTE_FRAME_MODE);
4814
4828
  const streamPurpose = safeString(options.streamPurpose || options.purpose || 'wall', 24) || 'wall';
4815
- const maxFps = streamPurpose === 'control' ? 60 : 30;
4816
- const fps = clampNumber(options.fps, 1, maxFps, 8);
4817
- const maxWidth = clampNumber(options.maxWidth, 320, 3840, 640);
4818
- const maxHeight = clampNumber(options.maxHeight, 180, 2160, 360);
4829
+ const platform = normalizeLivePlatform(options.platform);
4830
+ const macWallProfile = platform === 'macos' && streamPurpose !== 'control';
4831
+ const maxFps = streamPurpose === 'control' ? 60 : macWallProfile ? 8 : 30;
4832
+ const defaultFps = macWallProfile && options.fps === undefined ? 4 : 8;
4833
+ const maxWidth = clampNumber(options.maxWidth, 320, macWallProfile ? 432 : 3840, macWallProfile ? 432 : 640);
4834
+ const maxHeight = clampNumber(options.maxHeight, 180, macWallProfile ? 243 : 2160, macWallProfile ? 243 : 360);
4835
+ const fps = clampNumber(options.fps, 1, maxFps, defaultFps);
4819
4836
  const quality = clampNumber(options.quality, 20, 95, 45);
4820
4837
  const monitorIndex = normalizeMonitorIndex(options.monitorIndex ?? options.screenIndex ?? options.displayIndex);
4821
- return { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose };
4838
+ return { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose, platform, macWallProfile };
4822
4839
  }
4823
4840
 
4824
4841
  function liveStreamStartStillPending(activeLiveStream) {
@@ -4879,7 +4896,7 @@ export function createRemoteHub(options = {}) {
4879
4896
  }
4880
4897
 
4881
4898
  const now = new Date().toISOString();
4882
- const normalized = normalizeLiveStreamStartOptions({ fps: 2, ...options });
4899
+ const normalized = normalizeLiveStreamStartOptions({ fps: 2, ...options, platform: device.platform });
4883
4900
  const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
4884
4901
  const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
4885
4902
  const activeLiveStream = getDeviceLiveStream(device, streamId);
@@ -4956,7 +4973,7 @@ export function createRemoteHub(options = {}) {
4956
4973
  }
4957
4974
 
4958
4975
  const now = new Date().toISOString();
4959
- const normalized = normalizeLiveStreamStartOptions(options);
4976
+ const normalized = normalizeLiveStreamStartOptions({ ...options, platform: device.platform });
4960
4977
  const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
4961
4978
  const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
4962
4979
  const activeLiveStream = getDeviceLiveStream(device, streamId);
@@ -5099,11 +5116,22 @@ export function createRemoteHub(options = {}) {
5099
5116
 
5100
5117
  function stopLiveStream(deviceId, options = {}) {
5101
5118
  const device = devices.get(String(deviceId || ''));
5119
+ const streamPurpose = safeString(options.streamPurpose || options.purpose, 24);
5120
+ const requestedStreamId = safeString(options.streamId, 128);
5121
+ const purposeStreamId = streamPurpose ? makeStableLiveStreamId(deviceId, streamPurpose) : '';
5122
+ const streamId = requestedStreamId || purposeStreamId || device?.activeLiveStream?.streamId || '';
5123
+ const streamState = getDeviceLiveStream(device, streamId);
5124
+ if (streamState && streamState.active !== true) {
5125
+ return {
5126
+ ok: true,
5127
+ commandId: safeString(options.commandId, 128),
5128
+ streamId,
5129
+ alreadyStopped: true
5130
+ };
5131
+ }
5102
5132
  if (device?.synthetic === true && device.connected) {
5103
- const streamId = safeString(options.streamId, 128) || device.activeLiveStream?.streamId || '';
5104
5133
  const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
5105
5134
  device.counters.commandsSent += 1;
5106
- const streamState = getDeviceLiveStream(device, streamId);
5107
5135
  if (streamState) {
5108
5136
  streamState.active = false;
5109
5137
  streamState.stoppedAt = new Date().toISOString();
@@ -5122,7 +5150,6 @@ export function createRemoteHub(options = {}) {
5122
5150
  return { ok: false, error: 'device-not-connected' };
5123
5151
  }
5124
5152
 
5125
- const streamId = safeString(options.streamId, 128) || device.activeLiveStream?.streamId || '';
5126
5153
  const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
5127
5154
  const result = sendCommand(deviceId, {
5128
5155
  command: 'stream.stop',
@@ -5137,7 +5164,6 @@ export function createRemoteHub(options = {}) {
5137
5164
  return result;
5138
5165
  }
5139
5166
 
5140
- const streamState = getDeviceLiveStream(device, streamId);
5141
5167
  if (streamState) {
5142
5168
  streamState.active = false;
5143
5169
  streamState.stoppedAt = new Date().toISOString();
package/hub/src/server.js CHANGED
@@ -101,11 +101,11 @@ function handleRemoteHubEvent(type, event) {
101
101
  if (!deviceId) {
102
102
  return;
103
103
  }
104
- for (const ws of atlasClients.keys()) {
105
- if (ws.readyState === 1 && ws.liveDeskAtlasDeviceIds?.has(deviceId)) {
106
- startMode4AtlasInput(ws, deviceId, 'device-connected');
107
- }
108
- }
104
+ for (const ws of atlasClients.keys()) {
105
+ if (ws.readyState === 1 && ws.liveDeskAtlasInputDeviceIds?.has(deviceId)) {
106
+ startMode4AtlasInput(ws, deviceId, 'device-connected');
107
+ }
108
+ }
109
109
  const clients = new Set([
110
110
  ...(frameClientsByDeviceId.get(deviceId) || []),
111
111
  ...frameWildcardClients
@@ -976,11 +976,12 @@ function safeWebSocketSend(ws, payload, options = undefined) {
976
976
  }
977
977
  }
978
978
 
979
- function unregisterFrameClient(ws) {
980
- if (!ws) {
981
- return;
982
- }
983
- const ids = ws.liveDeskDeviceIds instanceof Set ? ws.liveDeskDeviceIds : new Set();
979
+ function unregisterFrameClient(ws) {
980
+ if (!ws) {
981
+ return;
982
+ }
983
+ stopFrameClientStreams(ws);
984
+ const ids = ws.liveDeskDeviceIds instanceof Set ? ws.liveDeskDeviceIds : new Set();
984
985
  for (const deviceId of ids) {
985
986
  const clients = frameClientsByDeviceId.get(deviceId);
986
987
  if (!clients) {
@@ -991,9 +992,49 @@ function unregisterFrameClient(ws) {
991
992
  frameClientsByDeviceId.delete(deviceId);
992
993
  }
993
994
  }
994
- frameWildcardClients.delete(ws);
995
- ws.liveDeskFrameSendLane?.queue?.splice?.(0);
996
- }
995
+ frameWildcardClients.delete(ws);
996
+ ws.liveDeskFrameSendLane?.queue?.splice?.(0);
997
+ }
998
+
999
+ function hasOtherFrameStreamOwner(ws, deviceId, streamId) {
1000
+ if (!streamId) {
1001
+ return false;
1002
+ }
1003
+ for (const candidate of frameClients) {
1004
+ if (candidate === ws || candidate.readyState !== candidate.OPEN) {
1005
+ continue;
1006
+ }
1007
+ if (candidate.liveDeskStreamIdsByDeviceId instanceof Map
1008
+ && candidate.liveDeskStreamIdsByDeviceId.get(deviceId) === streamId) {
1009
+ return true;
1010
+ }
1011
+ }
1012
+ return false;
1013
+ }
1014
+
1015
+ function stopFrameClientStreams(ws) {
1016
+ const streams = ws.liveDeskStreamIdsByDeviceId instanceof Map
1017
+ ? ws.liveDeskStreamIdsByDeviceId
1018
+ : null;
1019
+ if (!streams) {
1020
+ return;
1021
+ }
1022
+ const streamPurpose = String(ws.liveDeskLiveOptions?.streamPurpose || 'wall');
1023
+ for (const [deviceId, streamId] of streams.entries()) {
1024
+ if (!streamId || hasOtherFrameStreamOwner(ws, deviceId, streamId)) {
1025
+ continue;
1026
+ }
1027
+ const result = remoteHub.stopLiveStream(deviceId, {
1028
+ streamId,
1029
+ streamPurpose,
1030
+ reason: 'frame-client-closed'
1031
+ });
1032
+ if (result?.ok && !result.alreadyStopped) {
1033
+ console.log(`[LiveDesk Hub] stopped ${streamPurpose} stream after frame client closed device=${deviceId} stream=${streamId}`);
1034
+ }
1035
+ }
1036
+ streams.clear();
1037
+ }
997
1038
 
998
1039
  function registerFrameClient(ws) {
999
1040
  if (!ws) {
@@ -1322,9 +1363,15 @@ function buildRemoteFrameBinaryPacket(frameEvent, diagnostics = {}) {
1322
1363
  hardwareEncoder: frame.hardwareEncoder || '',
1323
1364
  platformProfile: frame.platformProfile || '',
1324
1365
  monitorIndex: Number.isFinite(Number(frame.monitorIndex)) ? Number(frame.monitorIndex) : 0,
1325
- monitorCount: Number.isFinite(Number(frame.monitorCount)) ? Number(frame.monitorCount) : 1,
1326
- fps: Number(frame.fps || 0) || 0,
1327
- capturedAt: frame.capturedAt || '',
1366
+ monitorCount: Number.isFinite(Number(frame.monitorCount)) ? Number(frame.monitorCount) : 1,
1367
+ fps: Number(frame.fps || 0) || 0,
1368
+ requestedFps: Number(frame.requestedFps || 0) || 0,
1369
+ effectiveFps: Number(frame.effectiveFps || 0) || 0,
1370
+ captureAverageMs: Number(frame.captureAverageMs || 0) || 0,
1371
+ captureP95Ms: Number(frame.captureP95Ms || 0) || 0,
1372
+ slowFrameCount: Number(frame.slowFrameCount || 0) || 0,
1373
+ captureHelperRestarts: Number(frame.captureHelperRestarts || 0) || 0,
1374
+ capturedAt: frame.capturedAt || '',
1328
1375
  receivedAt: frame.receivedAt || '',
1329
1376
  hubPacketAt,
1330
1377
  hubPacketEpochMs,
@@ -1537,28 +1584,42 @@ function sendMode4AtlasFrame(ws, output) {
1537
1584
  }
1538
1585
  }
1539
1586
 
1540
- function configureMode4Atlas(ws, payload = {}) {
1541
- const deviceIds = normalizeDeviceIds(payload.deviceIds ?? payload.devices ?? payload.deviceId).slice(0, 100);
1542
- const monitorSelections = normalizeMonitorSelections(payload.monitorSelections);
1587
+ function configureMode4Atlas(ws, payload = {}) {
1588
+ const deviceIds = normalizeDeviceIds(payload.deviceIds ?? payload.devices ?? payload.deviceId).slice(0, 100);
1589
+ const captureDeviceIds = normalizeDeviceIds(payload.captureDeviceIds ?? payload.inputDeviceIds ?? deviceIds)
1590
+ .filter(deviceId => deviceIds.includes(deviceId));
1591
+ const monitorSelections = normalizeMonitorSelections(payload.monitorSelections);
1543
1592
  const sharpTiles = Number(payload.tileWidth) >= 480 || Number(payload.tileHeight) >= 270;
1544
1593
  const tileWidth = sharpTiles ? 480 : 320;
1545
1594
  const tileHeight = sharpTiles ? 270 : 180;
1546
- ws.liveDeskAtlasDeviceIds = new Set(deviceIds);
1547
- ws.liveDeskAtlasInputOptions = { tileWidth, tileHeight, monitorSelections };
1548
- ws.liveDeskAtlasSession.configure({
1549
- deviceIds,
1595
+ const previousInputDeviceIds = ws.liveDeskAtlasInputDeviceIds instanceof Set
1596
+ ? new Set(ws.liveDeskAtlasInputDeviceIds)
1597
+ : new Set(deviceIds);
1598
+ ws.liveDeskAtlasDeviceIds = new Set(deviceIds);
1599
+ ws.liveDeskAtlasInputDeviceIds = new Set(captureDeviceIds);
1600
+ ws.liveDeskAtlasInputOptions = { tileWidth, tileHeight, monitorSelections };
1601
+ ws.liveDeskAtlasSession.configure({
1602
+ deviceIds,
1603
+ inputDeviceIds: captureDeviceIds,
1550
1604
  width: clampNumber(payload.width, 640, 3840, 1920),
1551
1605
  height: clampNumber(payload.height, 360, 2160, 1080),
1552
1606
  tileWidth,
1553
1607
  tileHeight,
1554
1608
  fps: clampNumber(payload.fps, 1, 30, 20)
1555
1609
  });
1556
- for (const deviceId of deviceIds) {
1557
- startMode4AtlasInput(ws, deviceId, 'atlas-configure');
1558
- }
1610
+ for (const deviceId of previousInputDeviceIds) {
1611
+ if (ws.liveDeskAtlasInputDeviceIds.has(deviceId)) {
1612
+ continue;
1613
+ }
1614
+ stopMode4AtlasInput(ws, deviceId, 'atlas-input-paused');
1615
+ }
1616
+ for (const deviceId of captureDeviceIds) {
1617
+ startMode4AtlasInput(ws, deviceId, 'atlas-configure');
1618
+ }
1559
1619
  sendJson(ws, {
1560
- type: 'Mode4AtlasSubscription',
1561
- deviceIds,
1620
+ type: 'Mode4AtlasSubscription',
1621
+ deviceIds,
1622
+ captureDeviceIds,
1562
1623
  inputMode: 'mode2-lzo',
1563
1624
  outputMode: 'mode4-h264-atlas',
1564
1625
  tileWidth,
@@ -1567,10 +1628,10 @@ function configureMode4Atlas(ws, payload = {}) {
1567
1628
  });
1568
1629
  }
1569
1630
 
1570
- function startMode4AtlasInput(ws, deviceId, reason = 'atlas-configure') {
1571
- const options = ws?.liveDeskAtlasInputOptions;
1572
- if (!options || !ws.liveDeskAtlasDeviceIds?.has(deviceId)) return { ok: false, error: 'atlas-device-not-configured' };
1573
- return remoteHub.startLiveStream(deviceId, {
1631
+ function startMode4AtlasInput(ws, deviceId, reason = 'atlas-configure') {
1632
+ const options = ws?.liveDeskAtlasInputOptions;
1633
+ if (!options || !ws.liveDeskAtlasInputDeviceIds?.has(deviceId)) return { ok: false, error: 'atlas-device-not-configured' };
1634
+ return remoteHub.startLiveStream(deviceId, {
1574
1635
  fps: 8,
1575
1636
  maxWidth: options.tileWidth,
1576
1637
  maxHeight: options.tileHeight,
@@ -1582,8 +1643,30 @@ function startMode4AtlasInput(ws, deviceId, reason = 'atlas-configure') {
1582
1643
  reuseExisting: false,
1583
1644
  silentReuse: true,
1584
1645
  restartReason: reason
1585
- });
1586
- }
1646
+ });
1647
+ }
1648
+
1649
+ function hasOtherAtlasInputOwner(ws, deviceId) {
1650
+ for (const candidate of atlasClients.keys()) {
1651
+ if (candidate === ws || candidate.readyState !== candidate.OPEN) {
1652
+ continue;
1653
+ }
1654
+ if (candidate.liveDeskAtlasInputDeviceIds?.has(deviceId)) {
1655
+ return true;
1656
+ }
1657
+ }
1658
+ return false;
1659
+ }
1660
+
1661
+ function stopMode4AtlasInput(ws, deviceId, reason = 'atlas-input-stopped') {
1662
+ if (!deviceId || hasOtherAtlasInputOwner(ws, deviceId)) {
1663
+ return { ok: true, alreadyOwned: true };
1664
+ }
1665
+ return remoteHub.stopLiveStream(deviceId, {
1666
+ streamPurpose: 'atlas',
1667
+ reason
1668
+ });
1669
+ }
1587
1670
 
1588
1671
  function wantsBinaryFrame(req) {
1589
1672
  return /^(1|true|yes|on|binary|raw)$/i.test(String(req.query?.binary ?? req.query?.raw ?? ''))
@@ -2457,9 +2540,13 @@ atlasWss.on('connection', ws => {
2457
2540
  });
2458
2541
  ws.liveDeskAtlasSession = session;
2459
2542
  atlasClients.set(ws, session);
2460
- const cleanup = () => {
2461
- atlasClients.delete(ws);
2462
- session.close();
2543
+ const cleanup = () => {
2544
+ atlasClients.delete(ws);
2545
+ for (const deviceId of ws.liveDeskAtlasInputDeviceIds || []) {
2546
+ stopMode4AtlasInput(ws, deviceId, 'atlas-client-closed');
2547
+ }
2548
+ ws.liveDeskAtlasInputDeviceIds = new Set();
2549
+ session.close();
2463
2550
  const lane = ws.liveDeskFrameSendLane;
2464
2551
  if (lane) {
2465
2552
  lane.queue.length = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.225",
3
+ "version": "0.1.227",
4
4
  "description": "LiveDesk Hub and client launcher",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@ffmpeg-installer/ffmpeg": "^1.1.0",
34
- "@livedesk/client": "0.1.128",
34
+ "@livedesk/client": "0.1.130",
35
35
  "@openai/codex-sdk": "0.144.5",
36
36
  "cors": "^2.8.5",
37
37
  "express": "^4.21.2",