livedesk 0.1.246 → 0.1.248

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.
@@ -24,6 +24,7 @@ const RECENT_LIVE_FRAME_CACHE_LIMIT = 1;
24
24
  const RECENT_THUMBNAIL_FRAME_CACHE_MAX_BYTES = 2 * 1024 * 1024;
25
25
  const RECENT_LIVE_FRAME_CACHE_MAX_BYTES = 8 * 1024 * 1024;
26
26
  const LIVE_STREAM_PENDING_REUSE_MS = 5000;
27
+ const LIVE_STREAM_REPLACEMENT_PENDING_MS = 7000;
27
28
  const LIVE_STREAM_MIN_FRESH_MS = 3000;
28
29
  const LIVE_STREAM_MAX_FRESH_MS = 12000;
29
30
  const REMOTE_PROTOCOL_VERSION = 2;
@@ -3342,21 +3343,30 @@ export function createRemoteHub(options = {}) {
3342
3343
 
3343
3344
  const commandId = safeString(message.commandId, 128);
3344
3345
  const activeCommandId = safeString(streamState.commandId, 128);
3345
- if (commandId && activeCommandId && commandId !== activeCommandId) {
3346
+ const pendingCommandId = safeString(streamState.pendingCommandId, 128);
3347
+ const isActiveGeneration = !commandId || !activeCommandId || commandId === activeCommandId;
3348
+ const isPendingGeneration = !!pendingCommandId && commandId === pendingCommandId;
3349
+ if (!isActiveGeneration && !isPendingGeneration) {
3346
3350
  emitRemoteEvent('RemoteLiveStreamOpenIgnored', device, {
3347
3351
  reason: 'stale-live-stream-generation',
3348
3352
  streamId,
3349
3353
  commandId,
3350
3354
  activeCommandId,
3355
+ pendingCommandId,
3351
3356
  transport
3352
3357
  });
3353
3358
  return false;
3354
3359
  }
3355
3360
 
3361
+ const activatingPendingGeneration = isPendingGeneration && commandId !== activeCommandId;
3356
3362
  const transfer = buildRemoteFrameTransferDescriptorFromMessage(message, streamState.mode || DEFAULT_REMOTE_FRAME_MODE);
3357
3363
  const openedAt = safeString(message.openedAt, 80) || device.lastSeenAt || new Date().toISOString();
3358
3364
  const nextStreamState = {
3359
3365
  ...streamState,
3366
+ commandId: activatingPendingGeneration ? commandId : streamState.commandId,
3367
+ pendingCommandId: activatingPendingGeneration ? '' : streamState.pendingCommandId,
3368
+ pendingStartedAt: activatingPendingGeneration ? '' : streamState.pendingStartedAt,
3369
+ pendingRestartToken: activatingPendingGeneration ? '' : streamState.pendingRestartToken,
3360
3370
  open: true,
3361
3371
  openedAt,
3362
3372
  openTransport: transport,
@@ -3380,6 +3390,9 @@ export function createRemoteHub(options = {}) {
3380
3390
  setDeviceLiveStream(device, nextStreamState);
3381
3391
  emitRemoteEvent('RemoteLiveStreamOpened', device, {
3382
3392
  streamId,
3393
+ commandId: nextStreamState.commandId,
3394
+ previousCommandId: activatingPendingGeneration ? activeCommandId : '',
3395
+ handoff: activatingPendingGeneration,
3383
3396
  mode: transfer.mode,
3384
3397
  frameMode: transfer.frameMode,
3385
3398
  frameProfile: transfer.frameProfile,
@@ -3409,13 +3422,16 @@ export function createRemoteHub(options = {}) {
3409
3422
  return false;
3410
3423
  }
3411
3424
  const activeCommandId = safeString(streamState.commandId, 128);
3412
- if (commandId && activeCommandId && commandId !== activeCommandId) {
3425
+ const pendingCommandId = safeString(streamState.pendingCommandId, 128);
3426
+ const isActiveGeneration = !commandId || !activeCommandId || commandId === activeCommandId;
3427
+ if (!isActiveGeneration) {
3413
3428
  device.counters.liveFramesDropped += 1;
3414
3429
  emitRemoteEvent('RemoteFrameDropped', device, {
3415
3430
  reason: 'stale-live-stream-generation-frame',
3416
3431
  streamId,
3417
3432
  commandId,
3418
3433
  activeCommandId,
3434
+ pendingCommandId,
3419
3435
  frameSeq: Number.isFinite(frameSeq) ? frameSeq : null,
3420
3436
  transport
3421
3437
  });
@@ -3965,7 +3981,11 @@ export function createRemoteHub(options = {}) {
3965
3981
  {
3966
3982
  const commandId = safeString(message.commandId, 128);
3967
3983
  const error = safeString(message.error, 500);
3968
- const task = applyTaskResult(device, commandId, message.result ?? null, error);
3984
+ const result = message.result ?? null;
3985
+ if (error || result?.ok === false || result?.status === 'failed') {
3986
+ failPendingLiveStream(device, commandId, error || result?.error || 'stream-start-failed');
3987
+ }
3988
+ const task = applyTaskResult(device, commandId, result, error);
3969
3989
  if (task) {
3970
3990
  emitRemoteEvent('RemoteTaskResult', device, {
3971
3991
  commandId,
@@ -4842,6 +4862,29 @@ export function createRemoteHub(options = {}) {
4842
4862
  return stream;
4843
4863
  }
4844
4864
 
4865
+ function failPendingLiveStream(device, commandId, error = 'stream-start-failed') {
4866
+ const key = safeString(commandId, 128);
4867
+ if (!device || !key) {
4868
+ return false;
4869
+ }
4870
+ const streams = ensureDeviceLiveStreams(device);
4871
+ for (const stream of streams.values()) {
4872
+ if (safeString(stream?.pendingCommandId, 128) !== key) {
4873
+ continue;
4874
+ }
4875
+ stream.pendingCommandId = '';
4876
+ stream.pendingStartedAt = '';
4877
+ stream.pendingRestartToken = '';
4878
+ emitRemoteEvent('RemoteLiveStreamRestartFailed', device, {
4879
+ streamId: stream.streamId,
4880
+ commandId: key,
4881
+ error: safeString(error, 500) || 'stream-start-failed'
4882
+ });
4883
+ return true;
4884
+ }
4885
+ return false;
4886
+ }
4887
+
4845
4888
  function deactivateDeviceLiveStreams(device, reason, stoppedAt = new Date().toISOString()) {
4846
4889
  const streams = ensureDeviceLiveStreams(device);
4847
4890
  for (const stream of streams.values()) {
@@ -4893,6 +4936,14 @@ export function createRemoteHub(options = {}) {
4893
4936
  return Number.isFinite(startedAt) && Date.now() - startedAt < LIVE_STREAM_PENDING_REUSE_MS;
4894
4937
  }
4895
4938
 
4939
+ function liveStreamReplacementStillPending(activeLiveStream) {
4940
+ if (!activeLiveStream?.active || !safeString(activeLiveStream.pendingCommandId, 128)) {
4941
+ return false;
4942
+ }
4943
+ const startedAt = Date.parse(activeLiveStream.pendingStartedAt || '');
4944
+ return Number.isFinite(startedAt) && Date.now() - startedAt < LIVE_STREAM_REPLACEMENT_PENDING_MS;
4945
+ }
4946
+
4896
4947
  function getLiveStreamFreshWindowMs(activeLiveStream) {
4897
4948
  const fps = Number(activeLiveStream?.fps || 0);
4898
4949
  const frameMs = Number.isFinite(fps) && fps > 0 ? 1000 / fps : 500;
@@ -5026,6 +5077,30 @@ export function createRemoteHub(options = {}) {
5026
5077
  const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
5027
5078
  const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
5028
5079
  const activeLiveStream = getDeviceLiveStream(device, streamId);
5080
+ if (activeLiveStream?.pendingCommandId) {
5081
+ if (liveStreamReplacementStillPending(activeLiveStream)) {
5082
+ emitRemoteEvent('RemoteLiveStreamRestartPending', device, {
5083
+ streamId,
5084
+ commandId: activeLiveStream.pendingCommandId,
5085
+ activeCommandId: activeLiveStream.commandId,
5086
+ reason: safeString(options.restartReason || 'duplicate-restart', 128)
5087
+ });
5088
+ return {
5089
+ ok: true,
5090
+ commandId: activeLiveStream.pendingCommandId,
5091
+ streamId,
5092
+ fps: Number(activeLiveStream.fps || fps),
5093
+ mode: activeLiveStream.mode || transfer.mode,
5094
+ frameMode: activeLiveStream.frameMode || transfer.frameMode,
5095
+ monitorIndex: Number(activeLiveStream.monitorIndex || monitorIndex),
5096
+ pending: true,
5097
+ reused: true
5098
+ };
5099
+ }
5100
+ activeLiveStream.pendingCommandId = '';
5101
+ activeLiveStream.pendingStartedAt = '';
5102
+ activeLiveStream.pendingRestartToken = '';
5103
+ }
5029
5104
  const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
5030
5105
  if (activeLiveStream
5031
5106
  && options.forceRestart !== true
@@ -5120,13 +5195,21 @@ export function createRemoteHub(options = {}) {
5120
5195
  return result;
5121
5196
  }
5122
5197
 
5198
+ const previousCommandId = safeString(activeLiveStream?.commandId, 128);
5199
+ const replacingActiveStream = activeLiveStream?.active === true
5200
+ && !!previousCommandId
5201
+ && previousCommandId !== commandId;
5123
5202
  setDeviceLiveStream(device, {
5203
+ ...(activeLiveStream || {}),
5124
5204
  streamId,
5125
- commandId,
5205
+ commandId: replacingActiveStream ? previousCommandId : commandId,
5206
+ pendingCommandId: replacingActiveStream ? commandId : '',
5207
+ pendingStartedAt: replacingActiveStream ? now : '',
5208
+ pendingRestartToken: replacingActiveStream ? safeString(options.restartToken, 128) : '',
5126
5209
  active: true,
5127
- open: false,
5128
- openedAt: '',
5129
- openTransport: '',
5210
+ open: replacingActiveStream ? activeLiveStream.open === true : false,
5211
+ openedAt: replacingActiveStream ? activeLiveStream.openedAt || '' : '',
5212
+ openTransport: replacingActiveStream ? activeLiveStream.openTransport || '' : '',
5130
5213
  mode: transfer.mode,
5131
5214
  frameMode: transfer.frameMode,
5132
5215
  frameProfile: transfer.frameProfile,
@@ -5142,7 +5225,7 @@ export function createRemoteHub(options = {}) {
5142
5225
  quality,
5143
5226
  monitorIndex,
5144
5227
  monitorCount: 1,
5145
- startedAt: now,
5228
+ startedAt: replacingActiveStream ? activeLiveStream.startedAt || now : now,
5146
5229
  stoppedAt: '',
5147
5230
  stopReason: '',
5148
5231
  lastFrameAt: '',
@@ -5154,6 +5237,8 @@ export function createRemoteHub(options = {}) {
5154
5237
  emitRemoteEvent('RemoteLiveStreamStarted', device, {
5155
5238
  streamId,
5156
5239
  commandId,
5240
+ previousCommandId,
5241
+ pending: replacingActiveStream,
5157
5242
  fps,
5158
5243
  mode: transfer.mode,
5159
5244
  frameMode: transfer.frameMode,
@@ -5183,6 +5268,9 @@ export function createRemoteHub(options = {}) {
5183
5268
  device.counters.commandsSent += 1;
5184
5269
  if (streamState) {
5185
5270
  streamState.active = false;
5271
+ streamState.pendingCommandId = '';
5272
+ streamState.pendingStartedAt = '';
5273
+ streamState.pendingRestartToken = '';
5186
5274
  streamState.stoppedAt = new Date().toISOString();
5187
5275
  streamState.stopReason = 'manager-request';
5188
5276
  }
@@ -5215,6 +5303,9 @@ export function createRemoteHub(options = {}) {
5215
5303
 
5216
5304
  if (streamState) {
5217
5305
  streamState.active = false;
5306
+ streamState.pendingCommandId = '';
5307
+ streamState.pendingStartedAt = '';
5308
+ streamState.pendingRestartToken = '';
5218
5309
  streamState.stoppedAt = new Date().toISOString();
5219
5310
  streamState.stopReason = 'manager-request';
5220
5311
  }
package/hub/src/server.js CHANGED
@@ -38,8 +38,9 @@ const webIndexPath = resolve(webDistPath, 'index.html');
38
38
  const packageInfo = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf8'));
39
39
  const httpHost = process.env.LIVEDESK_HUB_HTTP_HOST || '127.0.0.1';
40
40
  const httpPort = Number(process.env.LIVEDESK_HUB_HTTP_PORT || process.env.PORT || 5179);
41
- const frameBackpressureBytes = readPositiveIntegerEnv('LIVEDESK_FRAME_WS_BACKPRESSURE_BYTES', 64 * 1024 * 1024);
42
- const frameClientQueuePackets = readPositiveIntegerEnv('LIVEDESK_FRAME_WS_QUEUE_PACKETS', 256);
41
+ const frameBackpressureBytes = readPositiveIntegerEnv('LIVEDESK_FRAME_WS_BACKPRESSURE_BYTES', 64 * 1024 * 1024);
42
+ const frameClientQueuePackets = readPositiveIntegerEnv('LIVEDESK_FRAME_WS_QUEUE_PACKETS', 256);
43
+ const controlFrameClientQueuePackets = readPositiveIntegerEnv('LIVEDESK_CONTROL_FRAME_WS_QUEUE_PACKETS', 4);
43
44
  const frameClientDrainBudgetPackets = readPositiveIntegerEnv('LIVEDESK_FRAME_WS_DRAIN_BUDGET_PACKETS', 64);
44
45
  const frameStreamStopGraceMs = readPositiveIntegerEnv('LIVEDESK_FRAME_STREAM_STOP_GRACE_MS', 1500);
45
46
  const mode5FrameBackpressureBytes = readPositiveIntegerEnv('LIVEDESK_MODE5_WS_BACKPRESSURE_BYTES', 2 * 1024 * 1024);
@@ -1128,7 +1129,7 @@ function registerFrameClient(ws) {
1128
1129
  }
1129
1130
  }
1130
1131
 
1131
- function ensureFrameClientSendLane(ws) {
1132
+ function ensureFrameClientSendLane(ws) {
1132
1133
  if (!ws.liveDeskFrameSendLane) {
1133
1134
  ws.liveDeskFrameSendLane = {
1134
1135
  queue: [],
@@ -1137,8 +1138,14 @@ function ensureFrameClientSendLane(ws) {
1137
1138
  awaitingKeyFrames: new Set()
1138
1139
  };
1139
1140
  }
1140
- return ws.liveDeskFrameSendLane;
1141
- }
1141
+ return ws.liveDeskFrameSendLane;
1142
+ }
1143
+
1144
+ function frameClientQueueLimit(ws) {
1145
+ return ws?.liveDeskLiveOptions?.streamPurpose === 'control'
1146
+ ? controlFrameClientQueuePackets
1147
+ : frameClientQueuePackets;
1148
+ }
1142
1149
 
1143
1150
  function dropQueuedFrameForLane(lane) {
1144
1151
  const deltaIndex = lane.queue.findIndex(item => item?.isKeyFrame !== true);
@@ -1243,7 +1250,7 @@ function enqueueFramePacketForClient(ws, packet, meta) {
1243
1250
  ws.liveDeskFrameBackpressureDrops = Number(ws.liveDeskFrameBackpressureDrops || 0) + 1;
1244
1251
  }
1245
1252
  }
1246
- while (lane.queue.length >= frameClientQueuePackets) {
1253
+ while (lane.queue.length >= frameClientQueueLimit(ws)) {
1247
1254
  dropQueuedFrameForLane(lane);
1248
1255
  ws.liveDeskFrameBackpressureDrops = Number(ws.liveDeskFrameBackpressureDrops || 0) + 1;
1249
1256
  }
@@ -2866,9 +2873,14 @@ audioWss.on('connection', (ws, req) => {
2866
2873
  });
2867
2874
  });
2868
2875
 
2869
- inputWss.on('connection', ws => {
2870
- inputClients.add(ws);
2871
- ws.liveDeskInputClientId = `riws-${++inputClientSeq}`;
2876
+ inputWss.on('connection', ws => {
2877
+ inputClients.add(ws);
2878
+ ws.liveDeskInputClientId = `riws-${++inputClientSeq}`;
2879
+ try {
2880
+ ws._socket?.setNoDelay?.(true);
2881
+ } catch {
2882
+ // Best-effort latency hint for browser input sockets.
2883
+ }
2872
2884
  ws.on('message', data => {
2873
2885
  let payload;
2874
2886
  try {
@@ -2882,8 +2894,10 @@ inputWss.on('connection', ws => {
2882
2894
  ? { ...payload.input, hubReceivedAtEpochMs: Date.now() }
2883
2895
  : { ...payload, hubReceivedAtEpochMs: Date.now() };
2884
2896
  const result = remoteHub.sendInputControl(deviceId, input);
2885
- const fireAndForget = payload?.fireAndForget === true
2886
- || String(input?.type || '').toLowerCase() === 'pointermove';
2897
+ const inputType = String(input?.type || '').toLowerCase();
2898
+ const fireAndForget = payload?.fireAndForget === true
2899
+ || inputType === 'pointermove'
2900
+ || inputType === 'wheel';
2887
2901
  if (fireAndForget && result?.ok) {
2888
2902
  return;
2889
2903
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.246",
3
+ "version": "0.1.248",
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.133",
34
+ "@livedesk/client": "0.1.134",
35
35
  "@openai/codex-sdk": "0.144.5",
36
36
  "cors": "^2.8.5",
37
37
  "express": "^4.21.2",