livedesk 0.1.154 → 0.1.156

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.
@@ -551,6 +551,9 @@ function normalizeRemoteInputEvent(value = {}) {
551
551
  const keyCode = Number(input.keyCode ?? input.KeyCode);
552
552
  const location = Number(input.location ?? input.Location);
553
553
  const monitorIndex = Number(input.monitorIndex ?? input.MonitorIndex ?? input.screenIndex ?? input.ScreenIndex ?? input.displayIndex ?? input.DisplayIndex);
554
+ const inputSeq = Number(input.inputSeq ?? input.InputSeq);
555
+ const issuedAtEpochMs = Number(input.issuedAtEpochMs ?? input.IssuedAtEpochMs);
556
+ const hubReceivedAtEpochMs = Number(input.hubReceivedAtEpochMs ?? input.HubReceivedAtEpochMs);
554
557
  return {
555
558
  type,
556
559
  monitorIndex: Number.isFinite(monitorIndex) ? normalizeMonitorIndex(monitorIndex) : 0,
@@ -570,6 +573,9 @@ function normalizeRemoteInputEvent(value = {}) {
570
573
  altKey: input.altKey === true || input.AltKey === true,
571
574
  metaKey: input.metaKey === true || input.MetaKey === true || input.commandKey === true || input.CommandKey === true,
572
575
  controlLeaseId: safeString(input.controlLeaseId || input.ControlLeaseId, 128),
576
+ inputSeq: Number.isSafeInteger(inputSeq) && inputSeq > 0 ? inputSeq : 0,
577
+ issuedAtEpochMs: Number.isFinite(issuedAtEpochMs) ? issuedAtEpochMs : 0,
578
+ hubReceivedAtEpochMs: Number.isFinite(hubReceivedAtEpochMs) ? hubReceivedAtEpochMs : 0,
573
579
  issuedAt: safeString(input.issuedAt || input.IssuedAt, 80)
574
580
  };
575
581
  }
@@ -1849,17 +1855,18 @@ export function createRemoteHub(options = {}) {
1849
1855
  }
1850
1856
 
1851
1857
  function applySyntheticLiveFrame(device, streamId, options = {}) {
1852
- if (!device?.activeLiveStream?.active) {
1858
+ const streamState = getDeviceLiveStream(device, streamId);
1859
+ if (!streamState?.active) {
1853
1860
  return null;
1854
1861
  }
1855
1862
 
1856
- const frame = makeSyntheticFrame(device, streamId || device.activeLiveStream.streamId, device.activeLiveStream.mode || DEFAULT_REMOTE_FRAME_MODE, {
1857
- commandId: options.commandId || device.activeLiveStream.commandId,
1863
+ const frame = makeSyntheticFrame(device, streamId || streamState.streamId, streamState.mode || DEFAULT_REMOTE_FRAME_MODE, {
1864
+ commandId: options.commandId || streamState.commandId,
1858
1865
  width: options.maxWidth || 960,
1859
1866
  height: options.maxHeight || 540,
1860
- monitorIndex: options.monitorIndex ?? device.activeLiveStream.monitorIndex ?? 0,
1861
- monitorCount: options.monitorCount ?? device.activeLiveStream.monitorCount ?? 1,
1862
- fps: options.fps || device.activeLiveStream.fps
1867
+ monitorIndex: options.monitorIndex ?? streamState.monitorIndex ?? 0,
1868
+ monitorCount: options.monitorCount ?? streamState.monitorCount ?? 1,
1869
+ fps: options.fps || streamState.fps
1863
1870
  });
1864
1871
  device.latestLiveFrame = frame;
1865
1872
  rememberRecentFramePayload(device, 'live', frame);
@@ -1871,9 +1878,10 @@ export function createRemoteHub(options = {}) {
1871
1878
  mimeType: frame.mimeType,
1872
1879
  byteLength: frame.byteLength
1873
1880
  });
1874
- device.activeLiveStream.lastFrameAt = frame.receivedAt;
1875
- device.activeLiveStream.lastFrameSeq = frame.frameSeq;
1876
- device.activeLiveStream.framesReceived = (device.activeLiveStream.framesReceived || 0) + 1;
1881
+ streamState.latestFrame = frame;
1882
+ streamState.lastFrameAt = frame.receivedAt;
1883
+ streamState.lastFrameSeq = frame.frameSeq;
1884
+ streamState.framesReceived = (streamState.framesReceived || 0) + 1;
1877
1885
  device.lastSeenAt = frame.receivedAt;
1878
1886
  device.counters.liveFramesReceived += 1;
1879
1887
  emitRemoteEvent('RemoteFrameReceived', device, {
@@ -2645,11 +2653,7 @@ export function createRemoteHub(options = {}) {
2645
2653
  closeFrameSocket(device, reason);
2646
2654
  device.disconnectedAt = new Date().toISOString();
2647
2655
  device.lastDisconnectReason = reason;
2648
- if (device.activeLiveStream) {
2649
- device.activeLiveStream.active = false;
2650
- device.activeLiveStream.stoppedAt = device.disconnectedAt;
2651
- device.activeLiveStream.stopReason = reason;
2652
- }
2656
+ deactivateDeviceLiveStreams(device, reason, device.disconnectedAt);
2653
2657
  failAllPendingTasks(device, 'device-disconnected');
2654
2658
  logWarn('remote', `device disconnected ${device.deviceName} (${deviceId}): ${reason}`);
2655
2659
  emitRemoteEvent('RemoteDeviceDisconnected', device, { reason });
@@ -3127,7 +3131,8 @@ export function createRemoteHub(options = {}) {
3127
3131
 
3128
3132
  function applyLiveStreamOpen(device, message, transport = 'json') {
3129
3133
  const streamId = safeString(message.streamId, 128) || 'live';
3130
- if (!device.activeLiveStream?.active || device.activeLiveStream.streamId !== streamId) {
3134
+ const streamState = getDeviceLiveStream(device, streamId);
3135
+ if (!streamState?.active) {
3131
3136
  emitRemoteEvent('RemoteLiveStreamOpenIgnored', device, {
3132
3137
  reason: 'stale-live-stream-open',
3133
3138
  streamId,
@@ -3137,7 +3142,7 @@ export function createRemoteHub(options = {}) {
3137
3142
  }
3138
3143
 
3139
3144
  const commandId = safeString(message.commandId, 128);
3140
- const activeCommandId = safeString(device.activeLiveStream.commandId, 128);
3145
+ const activeCommandId = safeString(streamState.commandId, 128);
3141
3146
  if (commandId && activeCommandId && commandId !== activeCommandId) {
3142
3147
  emitRemoteEvent('RemoteLiveStreamOpenIgnored', device, {
3143
3148
  reason: 'stale-live-stream-generation',
@@ -3149,10 +3154,10 @@ export function createRemoteHub(options = {}) {
3149
3154
  return false;
3150
3155
  }
3151
3156
 
3152
- const transfer = buildRemoteFrameTransferDescriptorFromMessage(message, device.activeLiveStream.mode || DEFAULT_REMOTE_FRAME_MODE);
3157
+ const transfer = buildRemoteFrameTransferDescriptorFromMessage(message, streamState.mode || DEFAULT_REMOTE_FRAME_MODE);
3153
3158
  const openedAt = safeString(message.openedAt, 80) || device.lastSeenAt || new Date().toISOString();
3154
- device.activeLiveStream = {
3155
- ...device.activeLiveStream,
3159
+ const nextStreamState = {
3160
+ ...streamState,
3156
3161
  open: true,
3157
3162
  openedAt,
3158
3163
  openTransport: transport,
@@ -3165,14 +3170,15 @@ export function createRemoteHub(options = {}) {
3165
3170
  transferProtocol: transfer.transferProtocol,
3166
3171
  transferProtocolVersion: transfer.transferProtocolVersion,
3167
3172
  handshake: transfer.handshake,
3168
- width: Number.isFinite(Number(message.width)) ? Number(message.width) : Number(device.activeLiveStream.width || 0),
3169
- height: Number.isFinite(Number(message.height)) ? Number(message.height) : Number(device.activeLiveStream.height || 0),
3170
- sourceWidth: Number.isFinite(Number(message.sourceWidth)) ? Number(message.sourceWidth) : Number(device.activeLiveStream.sourceWidth || 0),
3171
- sourceHeight: Number.isFinite(Number(message.sourceHeight)) ? Number(message.sourceHeight) : Number(device.activeLiveStream.sourceHeight || 0),
3172
- monitorIndex: Number.isFinite(Number(message.monitorIndex)) ? normalizeMonitorIndex(message.monitorIndex) : Number(device.activeLiveStream.monitorIndex || 0),
3173
- monitorCount: Number.isFinite(Number(message.monitorCount)) ? clampNumber(message.monitorCount, 1, 64, 1) : Number(device.activeLiveStream.monitorCount || 1),
3174
- openedFrameSeq: Number.isFinite(Number(message.frameSeq)) ? Number(message.frameSeq) : Number(device.activeLiveStream.openedFrameSeq || 0)
3173
+ width: Number.isFinite(Number(message.width)) ? Number(message.width) : Number(streamState.width || 0),
3174
+ height: Number.isFinite(Number(message.height)) ? Number(message.height) : Number(streamState.height || 0),
3175
+ sourceWidth: Number.isFinite(Number(message.sourceWidth)) ? Number(message.sourceWidth) : Number(streamState.sourceWidth || 0),
3176
+ sourceHeight: Number.isFinite(Number(message.sourceHeight)) ? Number(message.sourceHeight) : Number(streamState.sourceHeight || 0),
3177
+ monitorIndex: Number.isFinite(Number(message.monitorIndex)) ? normalizeMonitorIndex(message.monitorIndex) : Number(streamState.monitorIndex || 0),
3178
+ monitorCount: Number.isFinite(Number(message.monitorCount)) ? clampNumber(message.monitorCount, 1, 64, 1) : Number(streamState.monitorCount || 1),
3179
+ openedFrameSeq: Number.isFinite(Number(message.frameSeq)) ? Number(message.frameSeq) : Number(streamState.openedFrameSeq || 0)
3175
3180
  };
3181
+ setDeviceLiveStream(device, nextStreamState);
3176
3182
  emitRemoteEvent('RemoteLiveStreamOpened', device, {
3177
3183
  streamId,
3178
3184
  mode: transfer.mode,
@@ -3192,7 +3198,8 @@ export function createRemoteHub(options = {}) {
3192
3198
  const frameSeq = Number(message.frameSeq);
3193
3199
  const streamId = safeString(message.streamId, 128) || 'live';
3194
3200
  const commandId = safeString(message.commandId, 128);
3195
- if (!device.activeLiveStream?.active || device.activeLiveStream.streamId !== streamId) {
3201
+ const streamState = getDeviceLiveStream(device, streamId);
3202
+ if (!streamState?.active) {
3196
3203
  device.counters.liveFramesDropped += 1;
3197
3204
  emitRemoteEvent('RemoteFrameDropped', device, {
3198
3205
  reason: 'stale-live-stream-frame',
@@ -3202,7 +3209,7 @@ export function createRemoteHub(options = {}) {
3202
3209
  });
3203
3210
  return false;
3204
3211
  }
3205
- const activeCommandId = safeString(device.activeLiveStream.commandId, 128);
3212
+ const activeCommandId = safeString(streamState.commandId, 128);
3206
3213
  if (commandId && activeCommandId && commandId !== activeCommandId) {
3207
3214
  device.counters.liveFramesDropped += 1;
3208
3215
  emitRemoteEvent('RemoteFrameDropped', device, {
@@ -3234,21 +3241,21 @@ export function createRemoteHub(options = {}) {
3234
3241
  const capturedAt = safeString(message.capturedAt, 80) || device.lastSeenAt;
3235
3242
  const payload = buildFramePayloadBuffer(framePayload, frameData);
3236
3243
  const contentHash = buildFrameContentHash(message, payload);
3237
- const sameContentStreak = computeSameContentStreak(device.latestLiveFrame, contentHash);
3238
- const transfer = buildRemoteFrameTransferDescriptorFromMessage(message, device.activeLiveStream.mode || DEFAULT_REMOTE_FRAME_MODE);
3239
- if (device.activeLiveStream.open !== true) {
3240
- device.activeLiveStream.open = true;
3241
- device.activeLiveStream.openedAt = device.lastSeenAt;
3242
- device.activeLiveStream.openTransport = `${transport}-implicit`;
3243
- device.activeLiveStream.mode = transfer.mode;
3244
- device.activeLiveStream.frameMode = transfer.frameMode;
3245
- device.activeLiveStream.frameProfile = transfer.frameProfile;
3246
- device.activeLiveStream.encoding = transfer.encoding;
3247
- device.activeLiveStream.codec = transfer.codec;
3248
- device.activeLiveStream.compression = transfer.compression;
3249
- device.activeLiveStream.transferProtocol = transfer.transferProtocol;
3250
- device.activeLiveStream.transferProtocolVersion = transfer.transferProtocolVersion;
3251
- device.activeLiveStream.handshake = transfer.handshake;
3244
+ const sameContentStreak = computeSameContentStreak(streamState.latestFrame, contentHash);
3245
+ const transfer = buildRemoteFrameTransferDescriptorFromMessage(message, streamState.mode || DEFAULT_REMOTE_FRAME_MODE);
3246
+ if (streamState.open !== true) {
3247
+ streamState.open = true;
3248
+ streamState.openedAt = device.lastSeenAt;
3249
+ streamState.openTransport = `${transport}-implicit`;
3250
+ streamState.mode = transfer.mode;
3251
+ streamState.frameMode = transfer.frameMode;
3252
+ streamState.frameProfile = transfer.frameProfile;
3253
+ streamState.encoding = transfer.encoding;
3254
+ streamState.codec = transfer.codec;
3255
+ streamState.compression = transfer.compression;
3256
+ streamState.transferProtocol = transfer.transferProtocol;
3257
+ streamState.transferProtocolVersion = transfer.transferProtocolVersion;
3258
+ streamState.handshake = transfer.handshake;
3252
3259
  }
3253
3260
  device.latestLiveFrame = {
3254
3261
  streamId,
@@ -3284,12 +3291,12 @@ export function createRemoteHub(options = {}) {
3284
3291
  hubIngressDropped: Number.isFinite(Number(message.hubIngressDropped)) ? Number(message.hubIngressDropped) : 0,
3285
3292
  hardwareEncoder: safeString(message.hardwareEncoder, 80),
3286
3293
  platformProfile: safeString(message.platformProfile, 80),
3287
- monitorIndex: Number.isFinite(Number(message.monitorIndex)) ? normalizeMonitorIndex(message.monitorIndex) : Number(device.activeLiveStream.monitorIndex || 0),
3288
- monitorCount: Number.isFinite(Number(message.monitorCount)) ? clampNumber(message.monitorCount, 1, 64, 1) : Number(device.activeLiveStream.monitorCount || 1),
3294
+ monitorIndex: Number.isFinite(Number(message.monitorIndex)) ? normalizeMonitorIndex(message.monitorIndex) : Number(streamState.monitorIndex || 0),
3295
+ monitorCount: Number.isFinite(Number(message.monitorCount)) ? clampNumber(message.monitorCount, 1, 64, 1) : Number(streamState.monitorCount || 1),
3289
3296
  transferProtocol: transfer.transferProtocol,
3290
3297
  transferProtocolVersion: transfer.transferProtocolVersion,
3291
3298
  handshake: transfer.handshake,
3292
- fps: Number.isFinite(Number(message.fps)) ? Number(message.fps) : device.activeLiveStream.fps,
3299
+ fps: Number.isFinite(Number(message.fps)) ? Number(message.fps) : streamState.fps,
3293
3300
  capturedAt,
3294
3301
  receivedAt: device.lastSeenAt,
3295
3302
  byteLength,
@@ -3303,6 +3310,7 @@ export function createRemoteHub(options = {}) {
3303
3310
  payload,
3304
3311
  accessToken: createFrameAccessToken()
3305
3312
  };
3313
+ streamState.latestFrame = device.latestLiveFrame;
3306
3314
  rememberRecentFramePayload(device, 'live', device.latestLiveFrame);
3307
3315
  emitFrame({
3308
3316
  kind: 'live',
@@ -3312,24 +3320,25 @@ export function createRemoteHub(options = {}) {
3312
3320
  mimeType: device.latestLiveFrame.mimeType,
3313
3321
  byteLength: device.latestLiveFrame.byteLength
3314
3322
  });
3315
- device.activeLiveStream.lastFrameAt = device.lastSeenAt;
3316
- device.activeLiveStream.lastFrameSeq = frameSeq;
3317
- device.activeLiveStream.framesReceived = (device.activeLiveStream.framesReceived || 0) + 1;
3318
- device.activeLiveStream.mode = transfer.mode;
3319
- device.activeLiveStream.frameMode = transfer.frameMode;
3320
- device.activeLiveStream.frameProfile = transfer.frameProfile;
3321
- device.activeLiveStream.encoding = transfer.encoding;
3322
- device.activeLiveStream.codec = transfer.codec;
3323
- device.activeLiveStream.compression = transfer.compression;
3324
- device.activeLiveStream.transferProtocol = transfer.transferProtocol;
3325
- device.activeLiveStream.transferProtocolVersion = transfer.transferProtocolVersion;
3326
- device.activeLiveStream.handshake = transfer.handshake;
3327
- device.activeLiveStream.width = device.latestLiveFrame.width;
3328
- device.activeLiveStream.height = device.latestLiveFrame.height;
3329
- device.activeLiveStream.sourceWidth = device.latestLiveFrame.sourceWidth;
3330
- device.activeLiveStream.sourceHeight = device.latestLiveFrame.sourceHeight;
3331
- device.activeLiveStream.monitorIndex = device.latestLiveFrame.monitorIndex;
3332
- device.activeLiveStream.monitorCount = device.latestLiveFrame.monitorCount;
3323
+ streamState.lastFrameAt = device.lastSeenAt;
3324
+ streamState.lastFrameSeq = frameSeq;
3325
+ streamState.framesReceived = (streamState.framesReceived || 0) + 1;
3326
+ streamState.mode = transfer.mode;
3327
+ streamState.frameMode = transfer.frameMode;
3328
+ streamState.frameProfile = transfer.frameProfile;
3329
+ streamState.encoding = transfer.encoding;
3330
+ streamState.codec = transfer.codec;
3331
+ streamState.compression = transfer.compression;
3332
+ streamState.transferProtocol = transfer.transferProtocol;
3333
+ streamState.transferProtocolVersion = transfer.transferProtocolVersion;
3334
+ streamState.handshake = transfer.handshake;
3335
+ streamState.width = device.latestLiveFrame.width;
3336
+ streamState.height = device.latestLiveFrame.height;
3337
+ streamState.sourceWidth = device.latestLiveFrame.sourceWidth;
3338
+ streamState.sourceHeight = device.latestLiveFrame.sourceHeight;
3339
+ streamState.monitorIndex = device.latestLiveFrame.monitorIndex;
3340
+ streamState.monitorCount = device.latestLiveFrame.monitorCount;
3341
+ ensureDeviceLiveStreams(device).set(streamId, streamState);
3333
3342
  device.counters.liveFramesReceived += 1;
3334
3343
  return true;
3335
3344
  }
@@ -3600,6 +3609,18 @@ export function createRemoteHub(options = {}) {
3600
3609
 
3601
3610
  if (state.inputOnly) {
3602
3611
  device.inputLastSeenAt = new Date().toISOString();
3612
+ if (message.type === 'input.applied') {
3613
+ emitEvent('RemoteInputApplied', {
3614
+ deviceId: device.deviceId,
3615
+ inputSeq: Number(message.inputSeq || 0) || 0,
3616
+ inputType: safeString(message.inputType, 48),
3617
+ issuedAtEpochMs: Number(message.issuedAtEpochMs || 0) || 0,
3618
+ hubReceivedAtEpochMs: Number(message.hubReceivedAtEpochMs || 0) || 0,
3619
+ hubForwardedAtEpochMs: Number(message.hubForwardedAtEpochMs || 0) || 0,
3620
+ agentApplyMs: Number(message.agentApplyMs || 0) || 0,
3621
+ hubAcknowledgedAtEpochMs: Date.now()
3622
+ });
3623
+ }
3603
3624
  return;
3604
3625
  }
3605
3626
  if (state.frameOnly) {
@@ -4105,11 +4126,7 @@ export function createRemoteHub(options = {}) {
4105
4126
  device.connected = false;
4106
4127
  device.disconnectedAt = new Date().toISOString();
4107
4128
  device.lastDisconnectReason = reason;
4108
- if (device.activeLiveStream) {
4109
- device.activeLiveStream.active = false;
4110
- device.activeLiveStream.stoppedAt = device.disconnectedAt;
4111
- device.activeLiveStream.stopReason = reason;
4112
- }
4129
+ deactivateDeviceLiveStreams(device, reason, device.disconnectedAt);
4113
4130
  failAllPendingTasks(device, 'device-disconnected');
4114
4131
  emitRemoteEvent('RemoteDeviceDisconnected', device, { reason, synthetic: true });
4115
4132
  return true;
@@ -4203,6 +4220,7 @@ export function createRemoteHub(options = {}) {
4203
4220
  type: 'input.control',
4204
4221
  payload: {
4205
4222
  ...normalized,
4223
+ hubForwardedAtEpochMs: Date.now(),
4206
4224
  issuedAt: normalized.issuedAt || new Date().toISOString()
4207
4225
  }
4208
4226
  });
@@ -4219,6 +4237,7 @@ export function createRemoteHub(options = {}) {
4219
4237
  command: 'input.control',
4220
4238
  payload: {
4221
4239
  ...normalized,
4240
+ hubForwardedAtEpochMs: Date.now(),
4222
4241
  issuedAt: normalized.issuedAt || new Date().toISOString()
4223
4242
  }
4224
4243
  });
@@ -4432,20 +4451,69 @@ export function createRemoteHub(options = {}) {
4432
4451
  });
4433
4452
  }
4434
4453
 
4435
- function makeStableLiveStreamId(deviceId) {
4454
+ function ensureDeviceLiveStreams(device) {
4455
+ if (!(device?.activeLiveStreams instanceof Map)) {
4456
+ Object.defineProperty(device, 'activeLiveStreams', {
4457
+ value: new Map(),
4458
+ enumerable: false,
4459
+ configurable: true,
4460
+ writable: true
4461
+ });
4462
+ if (device?.activeLiveStream?.streamId) {
4463
+ device.activeLiveStreams.set(device.activeLiveStream.streamId, device.activeLiveStream);
4464
+ }
4465
+ }
4466
+ return device.activeLiveStreams;
4467
+ }
4468
+
4469
+ function getDeviceLiveStream(device, streamId) {
4470
+ const id = safeString(streamId, 128);
4471
+ if (!id) {
4472
+ return device?.activeLiveStream || null;
4473
+ }
4474
+ return ensureDeviceLiveStreams(device).get(id)
4475
+ || (device?.activeLiveStream?.streamId === id ? device.activeLiveStream : null);
4476
+ }
4477
+
4478
+ function setDeviceLiveStream(device, stream) {
4479
+ if (!device || !stream?.streamId) {
4480
+ return stream;
4481
+ }
4482
+ ensureDeviceLiveStreams(device).set(stream.streamId, stream);
4483
+ device.activeLiveStream = stream;
4484
+ return stream;
4485
+ }
4486
+
4487
+ function deactivateDeviceLiveStreams(device, reason, stoppedAt = new Date().toISOString()) {
4488
+ const streams = ensureDeviceLiveStreams(device);
4489
+ for (const stream of streams.values()) {
4490
+ stream.active = false;
4491
+ stream.stoppedAt = stoppedAt;
4492
+ stream.stopReason = reason;
4493
+ }
4494
+ if (device?.activeLiveStream) {
4495
+ device.activeLiveStream.active = false;
4496
+ device.activeLiveStream.stoppedAt = stoppedAt;
4497
+ device.activeLiveStream.stopReason = reason;
4498
+ }
4499
+ }
4500
+
4501
+ function makeStableLiveStreamId(deviceId, purpose = 'wall') {
4436
4502
  const hash = crypto.createHash('sha1').update(String(deviceId || 'device')).digest('hex').slice(0, 16);
4437
- return `live-${hash}`;
4503
+ const role = safeString(purpose, 24).toLowerCase().replace(/[^a-z0-9_-]+/g, '-') || 'wall';
4504
+ return `${role}-${hash}`;
4438
4505
  }
4439
4506
 
4440
4507
  function normalizeLiveStreamStartOptions(options = {}) {
4441
4508
  const transfer = buildRemoteFrameTransferDescriptor(options.mode || options.frameMode || DEFAULT_REMOTE_FRAME_MODE);
4442
- const maxFps = transfer.frameMode === 'mode3-h264-hw' ? 20 : 30;
4509
+ const maxFps = 30;
4443
4510
  const fps = clampNumber(options.fps, 1, maxFps, 8);
4444
4511
  const maxWidth = clampNumber(options.maxWidth, 320, 3840, 640);
4445
4512
  const maxHeight = clampNumber(options.maxHeight, 180, 2160, 360);
4446
4513
  const quality = clampNumber(options.quality, 20, 95, 45);
4447
4514
  const monitorIndex = normalizeMonitorIndex(options.monitorIndex ?? options.screenIndex ?? options.displayIndex);
4448
- return { fps, maxWidth, maxHeight, quality, monitorIndex, transfer };
4515
+ const streamPurpose = safeString(options.streamPurpose || options.purpose || 'wall', 24) || 'wall';
4516
+ return { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose };
4449
4517
  }
4450
4518
 
4451
4519
  function liveStreamStartStillPending(activeLiveStream) {
@@ -4506,16 +4574,17 @@ export function createRemoteHub(options = {}) {
4506
4574
  }
4507
4575
 
4508
4576
  const now = new Date().toISOString();
4509
- const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId);
4510
4577
  const normalized = normalizeLiveStreamStartOptions({ fps: 2, ...options });
4511
- const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer } = normalized;
4578
+ const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
4579
+ const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
4580
+ const activeLiveStream = getDeviceLiveStream(device, streamId);
4512
4581
  const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
4513
- if (device.activeLiveStream?.streamId === streamId
4514
- && liveStreamMatchesOptions(device.activeLiveStream, normalized)
4515
- && (device.activeLiveStream.open === true || liveStreamStartStillPending(device.activeLiveStream))) {
4582
+ if (activeLiveStream
4583
+ && liveStreamMatchesOptions(activeLiveStream, normalized)
4584
+ && (activeLiveStream.open === true || liveStreamStartStillPending(activeLiveStream))) {
4516
4585
  return {
4517
4586
  ok: true,
4518
- commandId: device.activeLiveStream.commandId,
4587
+ commandId: activeLiveStream.commandId,
4519
4588
  streamId,
4520
4589
  fps,
4521
4590
  mode: transfer.mode,
@@ -4526,7 +4595,7 @@ export function createRemoteHub(options = {}) {
4526
4595
  };
4527
4596
  }
4528
4597
  device.counters.commandsSent += 1;
4529
- device.activeLiveStream = {
4598
+ setDeviceLiveStream(device, {
4530
4599
  streamId,
4531
4600
  commandId,
4532
4601
  active: true,
@@ -4553,8 +4622,9 @@ export function createRemoteHub(options = {}) {
4553
4622
  stopReason: '',
4554
4623
  lastFrameAt: '',
4555
4624
  lastFrameSeq: 0,
4556
- framesReceived: 0
4557
- };
4625
+ framesReceived: 0,
4626
+ streamPurpose
4627
+ });
4558
4628
  device.counters.liveStreamsStarted += 1;
4559
4629
  applySyntheticLiveFrame(device, streamId, {
4560
4630
  commandId,
@@ -4581,30 +4651,31 @@ export function createRemoteHub(options = {}) {
4581
4651
  }
4582
4652
 
4583
4653
  const now = new Date().toISOString();
4584
- const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId);
4585
4654
  const normalized = normalizeLiveStreamStartOptions(options);
4586
- const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer } = normalized;
4655
+ const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
4656
+ const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
4657
+ const activeLiveStream = getDeviceLiveStream(device, streamId);
4587
4658
  const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
4588
- if (device.activeLiveStream?.streamId === streamId
4659
+ if (activeLiveStream
4589
4660
  && options.forceRestart !== true
4590
4661
  && options.reuseExisting === true
4591
- && liveStreamMatchesOptions(device.activeLiveStream, normalized)
4592
- && liveStreamIsReusable(device.activeLiveStream)) {
4662
+ && liveStreamMatchesOptions(activeLiveStream, normalized)
4663
+ && liveStreamIsReusable(activeLiveStream)) {
4593
4664
  return {
4594
4665
  ok: true,
4595
- commandId: device.activeLiveStream.commandId,
4666
+ commandId: activeLiveStream.commandId,
4596
4667
  streamId,
4597
- fps: Number(device.activeLiveStream.fps || fps),
4598
- mode: device.activeLiveStream.mode || transfer.mode,
4599
- frameMode: device.activeLiveStream.frameMode || transfer.frameMode,
4600
- monitorIndex: Number(device.activeLiveStream.monitorIndex || monitorIndex),
4668
+ fps: Number(activeLiveStream.fps || fps),
4669
+ mode: activeLiveStream.mode || transfer.mode,
4670
+ frameMode: activeLiveStream.frameMode || transfer.frameMode,
4671
+ monitorIndex: Number(activeLiveStream.monitorIndex || monitorIndex),
4601
4672
  reused: true
4602
4673
  };
4603
4674
  }
4604
- if (device.activeLiveStream?.streamId === streamId
4675
+ if (activeLiveStream
4605
4676
  && options.forceRestart !== true
4606
- && liveStreamMatchesOptions(device.activeLiveStream, normalized)
4607
- && liveStreamIsReusable(device.activeLiveStream)) {
4677
+ && liveStreamMatchesOptions(activeLiveStream, normalized)
4678
+ && liveStreamIsReusable(activeLiveStream)) {
4608
4679
  if (options.silentReuse !== true) {
4609
4680
  emitRemoteEvent('RemoteLiveStreamStartReused', device, {
4610
4681
  streamId,
@@ -4616,7 +4687,7 @@ export function createRemoteHub(options = {}) {
4616
4687
  }
4617
4688
  return {
4618
4689
  ok: true,
4619
- commandId: device.activeLiveStream.commandId,
4690
+ commandId: activeLiveStream.commandId,
4620
4691
  streamId,
4621
4692
  fps,
4622
4693
  mode: transfer.mode,
@@ -4625,7 +4696,7 @@ export function createRemoteHub(options = {}) {
4625
4696
  reused: true
4626
4697
  };
4627
4698
  }
4628
- if (options.forceRestart === true && device.activeLiveStream?.streamId === streamId) {
4699
+ if (options.forceRestart === true && activeLiveStream) {
4629
4700
  emitRemoteEvent('RemoteLiveStreamForcedRestart', device, {
4630
4701
  streamId,
4631
4702
  fps,
@@ -4635,30 +4706,19 @@ export function createRemoteHub(options = {}) {
4635
4706
  reason: safeString(options.restartReason || 'browser-stale', 128)
4636
4707
  });
4637
4708
  }
4638
- if (device.activeLiveStream?.streamId === streamId
4639
- && liveStreamMatchesOptions(device.activeLiveStream, normalized)
4640
- && device.activeLiveStream.open === true
4641
- && !liveStreamIsReusable(device.activeLiveStream)) {
4709
+ if (activeLiveStream
4710
+ && liveStreamMatchesOptions(activeLiveStream, normalized)
4711
+ && activeLiveStream.open === true
4712
+ && !liveStreamIsReusable(activeLiveStream)) {
4642
4713
  emitRemoteEvent('RemoteLiveStreamStaleRestart', device, {
4643
4714
  streamId,
4644
4715
  fps,
4645
4716
  mode: transfer.mode,
4646
4717
  frameMode: transfer.frameMode,
4647
4718
  monitorIndex,
4648
- idleMs: getLiveStreamIdleMs(device.activeLiveStream),
4649
- staleAfterMs: getLiveStreamFreshWindowMs(device.activeLiveStream),
4650
- lastFrameAt: device.activeLiveStream.lastFrameAt || ''
4651
- });
4652
- }
4653
- if (device.activeLiveStream?.active && device.activeLiveStream.streamId && device.activeLiveStream.streamId !== streamId) {
4654
- sendCommand(deviceId, {
4655
- command: 'stream.stop',
4656
- commandId: crypto.randomUUID(),
4657
- payload: {
4658
- streamId: device.activeLiveStream.streamId,
4659
- reason: 'live-stream-replaced',
4660
- requestedAt: now
4661
- }
4719
+ idleMs: getLiveStreamIdleMs(activeLiveStream),
4720
+ staleAfterMs: getLiveStreamFreshWindowMs(activeLiveStream),
4721
+ lastFrameAt: activeLiveStream.lastFrameAt || ''
4662
4722
  });
4663
4723
  }
4664
4724
  const result = sendCommand(deviceId, {
@@ -4680,6 +4740,7 @@ export function createRemoteHub(options = {}) {
4680
4740
  maxHeight,
4681
4741
  quality,
4682
4742
  monitorIndex,
4743
+ streamPurpose,
4683
4744
  requestedAt: now
4684
4745
  }
4685
4746
  });
@@ -4688,7 +4749,7 @@ export function createRemoteHub(options = {}) {
4688
4749
  return result;
4689
4750
  }
4690
4751
 
4691
- device.activeLiveStream = {
4752
+ setDeviceLiveStream(device, {
4692
4753
  streamId,
4693
4754
  commandId,
4694
4755
  active: true,
@@ -4715,8 +4776,9 @@ export function createRemoteHub(options = {}) {
4715
4776
  stopReason: '',
4716
4777
  lastFrameAt: '',
4717
4778
  lastFrameSeq: 0,
4718
- framesReceived: 0
4719
- };
4779
+ framesReceived: 0,
4780
+ streamPurpose
4781
+ });
4720
4782
  device.counters.liveStreamsStarted += 1;
4721
4783
  emitRemoteEvent('RemoteLiveStreamStarted', device, {
4722
4784
  streamId,
@@ -4736,10 +4798,11 @@ export function createRemoteHub(options = {}) {
4736
4798
  const streamId = safeString(options.streamId, 128) || device.activeLiveStream?.streamId || '';
4737
4799
  const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
4738
4800
  device.counters.commandsSent += 1;
4739
- if (device.activeLiveStream && (!streamId || device.activeLiveStream.streamId === streamId)) {
4740
- device.activeLiveStream.active = false;
4741
- device.activeLiveStream.stoppedAt = new Date().toISOString();
4742
- device.activeLiveStream.stopReason = 'manager-request';
4801
+ const streamState = getDeviceLiveStream(device, streamId);
4802
+ if (streamState) {
4803
+ streamState.active = false;
4804
+ streamState.stoppedAt = new Date().toISOString();
4805
+ streamState.stopReason = 'manager-request';
4743
4806
  }
4744
4807
  device.counters.liveStreamsStopped += 1;
4745
4808
  emitRemoteEvent('RemoteLiveStreamStopped', device, {
@@ -4769,10 +4832,11 @@ export function createRemoteHub(options = {}) {
4769
4832
  return result;
4770
4833
  }
4771
4834
 
4772
- if (device.activeLiveStream && (!streamId || device.activeLiveStream.streamId === streamId)) {
4773
- device.activeLiveStream.active = false;
4774
- device.activeLiveStream.stoppedAt = new Date().toISOString();
4775
- device.activeLiveStream.stopReason = 'manager-request';
4835
+ const streamState = getDeviceLiveStream(device, streamId);
4836
+ if (streamState) {
4837
+ streamState.active = false;
4838
+ streamState.stoppedAt = new Date().toISOString();
4839
+ streamState.stopReason = 'manager-request';
4776
4840
  }
4777
4841
  device.counters.liveStreamsStopped += 1;
4778
4842
  emitRemoteEvent('RemoteLiveStreamStopped', device, {