livedesk 0.1.154 → 0.1.155

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.
@@ -1849,17 +1849,18 @@ export function createRemoteHub(options = {}) {
1849
1849
  }
1850
1850
 
1851
1851
  function applySyntheticLiveFrame(device, streamId, options = {}) {
1852
- if (!device?.activeLiveStream?.active) {
1852
+ const streamState = getDeviceLiveStream(device, streamId);
1853
+ if (!streamState?.active) {
1853
1854
  return null;
1854
1855
  }
1855
1856
 
1856
- const frame = makeSyntheticFrame(device, streamId || device.activeLiveStream.streamId, device.activeLiveStream.mode || DEFAULT_REMOTE_FRAME_MODE, {
1857
- commandId: options.commandId || device.activeLiveStream.commandId,
1857
+ const frame = makeSyntheticFrame(device, streamId || streamState.streamId, streamState.mode || DEFAULT_REMOTE_FRAME_MODE, {
1858
+ commandId: options.commandId || streamState.commandId,
1858
1859
  width: options.maxWidth || 960,
1859
1860
  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
1861
+ monitorIndex: options.monitorIndex ?? streamState.monitorIndex ?? 0,
1862
+ monitorCount: options.monitorCount ?? streamState.monitorCount ?? 1,
1863
+ fps: options.fps || streamState.fps
1863
1864
  });
1864
1865
  device.latestLiveFrame = frame;
1865
1866
  rememberRecentFramePayload(device, 'live', frame);
@@ -1871,9 +1872,10 @@ export function createRemoteHub(options = {}) {
1871
1872
  mimeType: frame.mimeType,
1872
1873
  byteLength: frame.byteLength
1873
1874
  });
1874
- device.activeLiveStream.lastFrameAt = frame.receivedAt;
1875
- device.activeLiveStream.lastFrameSeq = frame.frameSeq;
1876
- device.activeLiveStream.framesReceived = (device.activeLiveStream.framesReceived || 0) + 1;
1875
+ streamState.latestFrame = frame;
1876
+ streamState.lastFrameAt = frame.receivedAt;
1877
+ streamState.lastFrameSeq = frame.frameSeq;
1878
+ streamState.framesReceived = (streamState.framesReceived || 0) + 1;
1877
1879
  device.lastSeenAt = frame.receivedAt;
1878
1880
  device.counters.liveFramesReceived += 1;
1879
1881
  emitRemoteEvent('RemoteFrameReceived', device, {
@@ -2645,11 +2647,7 @@ export function createRemoteHub(options = {}) {
2645
2647
  closeFrameSocket(device, reason);
2646
2648
  device.disconnectedAt = new Date().toISOString();
2647
2649
  device.lastDisconnectReason = reason;
2648
- if (device.activeLiveStream) {
2649
- device.activeLiveStream.active = false;
2650
- device.activeLiveStream.stoppedAt = device.disconnectedAt;
2651
- device.activeLiveStream.stopReason = reason;
2652
- }
2650
+ deactivateDeviceLiveStreams(device, reason, device.disconnectedAt);
2653
2651
  failAllPendingTasks(device, 'device-disconnected');
2654
2652
  logWarn('remote', `device disconnected ${device.deviceName} (${deviceId}): ${reason}`);
2655
2653
  emitRemoteEvent('RemoteDeviceDisconnected', device, { reason });
@@ -3127,7 +3125,8 @@ export function createRemoteHub(options = {}) {
3127
3125
 
3128
3126
  function applyLiveStreamOpen(device, message, transport = 'json') {
3129
3127
  const streamId = safeString(message.streamId, 128) || 'live';
3130
- if (!device.activeLiveStream?.active || device.activeLiveStream.streamId !== streamId) {
3128
+ const streamState = getDeviceLiveStream(device, streamId);
3129
+ if (!streamState?.active) {
3131
3130
  emitRemoteEvent('RemoteLiveStreamOpenIgnored', device, {
3132
3131
  reason: 'stale-live-stream-open',
3133
3132
  streamId,
@@ -3137,7 +3136,7 @@ export function createRemoteHub(options = {}) {
3137
3136
  }
3138
3137
 
3139
3138
  const commandId = safeString(message.commandId, 128);
3140
- const activeCommandId = safeString(device.activeLiveStream.commandId, 128);
3139
+ const activeCommandId = safeString(streamState.commandId, 128);
3141
3140
  if (commandId && activeCommandId && commandId !== activeCommandId) {
3142
3141
  emitRemoteEvent('RemoteLiveStreamOpenIgnored', device, {
3143
3142
  reason: 'stale-live-stream-generation',
@@ -3149,10 +3148,10 @@ export function createRemoteHub(options = {}) {
3149
3148
  return false;
3150
3149
  }
3151
3150
 
3152
- const transfer = buildRemoteFrameTransferDescriptorFromMessage(message, device.activeLiveStream.mode || DEFAULT_REMOTE_FRAME_MODE);
3151
+ const transfer = buildRemoteFrameTransferDescriptorFromMessage(message, streamState.mode || DEFAULT_REMOTE_FRAME_MODE);
3153
3152
  const openedAt = safeString(message.openedAt, 80) || device.lastSeenAt || new Date().toISOString();
3154
- device.activeLiveStream = {
3155
- ...device.activeLiveStream,
3153
+ const nextStreamState = {
3154
+ ...streamState,
3156
3155
  open: true,
3157
3156
  openedAt,
3158
3157
  openTransport: transport,
@@ -3165,14 +3164,15 @@ export function createRemoteHub(options = {}) {
3165
3164
  transferProtocol: transfer.transferProtocol,
3166
3165
  transferProtocolVersion: transfer.transferProtocolVersion,
3167
3166
  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)
3167
+ width: Number.isFinite(Number(message.width)) ? Number(message.width) : Number(streamState.width || 0),
3168
+ height: Number.isFinite(Number(message.height)) ? Number(message.height) : Number(streamState.height || 0),
3169
+ sourceWidth: Number.isFinite(Number(message.sourceWidth)) ? Number(message.sourceWidth) : Number(streamState.sourceWidth || 0),
3170
+ sourceHeight: Number.isFinite(Number(message.sourceHeight)) ? Number(message.sourceHeight) : Number(streamState.sourceHeight || 0),
3171
+ monitorIndex: Number.isFinite(Number(message.monitorIndex)) ? normalizeMonitorIndex(message.monitorIndex) : Number(streamState.monitorIndex || 0),
3172
+ monitorCount: Number.isFinite(Number(message.monitorCount)) ? clampNumber(message.monitorCount, 1, 64, 1) : Number(streamState.monitorCount || 1),
3173
+ openedFrameSeq: Number.isFinite(Number(message.frameSeq)) ? Number(message.frameSeq) : Number(streamState.openedFrameSeq || 0)
3175
3174
  };
3175
+ setDeviceLiveStream(device, nextStreamState);
3176
3176
  emitRemoteEvent('RemoteLiveStreamOpened', device, {
3177
3177
  streamId,
3178
3178
  mode: transfer.mode,
@@ -3192,7 +3192,8 @@ export function createRemoteHub(options = {}) {
3192
3192
  const frameSeq = Number(message.frameSeq);
3193
3193
  const streamId = safeString(message.streamId, 128) || 'live';
3194
3194
  const commandId = safeString(message.commandId, 128);
3195
- if (!device.activeLiveStream?.active || device.activeLiveStream.streamId !== streamId) {
3195
+ const streamState = getDeviceLiveStream(device, streamId);
3196
+ if (!streamState?.active) {
3196
3197
  device.counters.liveFramesDropped += 1;
3197
3198
  emitRemoteEvent('RemoteFrameDropped', device, {
3198
3199
  reason: 'stale-live-stream-frame',
@@ -3202,7 +3203,7 @@ export function createRemoteHub(options = {}) {
3202
3203
  });
3203
3204
  return false;
3204
3205
  }
3205
- const activeCommandId = safeString(device.activeLiveStream.commandId, 128);
3206
+ const activeCommandId = safeString(streamState.commandId, 128);
3206
3207
  if (commandId && activeCommandId && commandId !== activeCommandId) {
3207
3208
  device.counters.liveFramesDropped += 1;
3208
3209
  emitRemoteEvent('RemoteFrameDropped', device, {
@@ -3234,21 +3235,21 @@ export function createRemoteHub(options = {}) {
3234
3235
  const capturedAt = safeString(message.capturedAt, 80) || device.lastSeenAt;
3235
3236
  const payload = buildFramePayloadBuffer(framePayload, frameData);
3236
3237
  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;
3238
+ const sameContentStreak = computeSameContentStreak(streamState.latestFrame, contentHash);
3239
+ const transfer = buildRemoteFrameTransferDescriptorFromMessage(message, streamState.mode || DEFAULT_REMOTE_FRAME_MODE);
3240
+ if (streamState.open !== true) {
3241
+ streamState.open = true;
3242
+ streamState.openedAt = device.lastSeenAt;
3243
+ streamState.openTransport = `${transport}-implicit`;
3244
+ streamState.mode = transfer.mode;
3245
+ streamState.frameMode = transfer.frameMode;
3246
+ streamState.frameProfile = transfer.frameProfile;
3247
+ streamState.encoding = transfer.encoding;
3248
+ streamState.codec = transfer.codec;
3249
+ streamState.compression = transfer.compression;
3250
+ streamState.transferProtocol = transfer.transferProtocol;
3251
+ streamState.transferProtocolVersion = transfer.transferProtocolVersion;
3252
+ streamState.handshake = transfer.handshake;
3252
3253
  }
3253
3254
  device.latestLiveFrame = {
3254
3255
  streamId,
@@ -3284,12 +3285,12 @@ export function createRemoteHub(options = {}) {
3284
3285
  hubIngressDropped: Number.isFinite(Number(message.hubIngressDropped)) ? Number(message.hubIngressDropped) : 0,
3285
3286
  hardwareEncoder: safeString(message.hardwareEncoder, 80),
3286
3287
  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),
3288
+ monitorIndex: Number.isFinite(Number(message.monitorIndex)) ? normalizeMonitorIndex(message.monitorIndex) : Number(streamState.monitorIndex || 0),
3289
+ monitorCount: Number.isFinite(Number(message.monitorCount)) ? clampNumber(message.monitorCount, 1, 64, 1) : Number(streamState.monitorCount || 1),
3289
3290
  transferProtocol: transfer.transferProtocol,
3290
3291
  transferProtocolVersion: transfer.transferProtocolVersion,
3291
3292
  handshake: transfer.handshake,
3292
- fps: Number.isFinite(Number(message.fps)) ? Number(message.fps) : device.activeLiveStream.fps,
3293
+ fps: Number.isFinite(Number(message.fps)) ? Number(message.fps) : streamState.fps,
3293
3294
  capturedAt,
3294
3295
  receivedAt: device.lastSeenAt,
3295
3296
  byteLength,
@@ -3303,6 +3304,7 @@ export function createRemoteHub(options = {}) {
3303
3304
  payload,
3304
3305
  accessToken: createFrameAccessToken()
3305
3306
  };
3307
+ streamState.latestFrame = device.latestLiveFrame;
3306
3308
  rememberRecentFramePayload(device, 'live', device.latestLiveFrame);
3307
3309
  emitFrame({
3308
3310
  kind: 'live',
@@ -3312,24 +3314,25 @@ export function createRemoteHub(options = {}) {
3312
3314
  mimeType: device.latestLiveFrame.mimeType,
3313
3315
  byteLength: device.latestLiveFrame.byteLength
3314
3316
  });
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;
3317
+ streamState.lastFrameAt = device.lastSeenAt;
3318
+ streamState.lastFrameSeq = frameSeq;
3319
+ streamState.framesReceived = (streamState.framesReceived || 0) + 1;
3320
+ streamState.mode = transfer.mode;
3321
+ streamState.frameMode = transfer.frameMode;
3322
+ streamState.frameProfile = transfer.frameProfile;
3323
+ streamState.encoding = transfer.encoding;
3324
+ streamState.codec = transfer.codec;
3325
+ streamState.compression = transfer.compression;
3326
+ streamState.transferProtocol = transfer.transferProtocol;
3327
+ streamState.transferProtocolVersion = transfer.transferProtocolVersion;
3328
+ streamState.handshake = transfer.handshake;
3329
+ streamState.width = device.latestLiveFrame.width;
3330
+ streamState.height = device.latestLiveFrame.height;
3331
+ streamState.sourceWidth = device.latestLiveFrame.sourceWidth;
3332
+ streamState.sourceHeight = device.latestLiveFrame.sourceHeight;
3333
+ streamState.monitorIndex = device.latestLiveFrame.monitorIndex;
3334
+ streamState.monitorCount = device.latestLiveFrame.monitorCount;
3335
+ ensureDeviceLiveStreams(device).set(streamId, streamState);
3333
3336
  device.counters.liveFramesReceived += 1;
3334
3337
  return true;
3335
3338
  }
@@ -4105,11 +4108,7 @@ export function createRemoteHub(options = {}) {
4105
4108
  device.connected = false;
4106
4109
  device.disconnectedAt = new Date().toISOString();
4107
4110
  device.lastDisconnectReason = reason;
4108
- if (device.activeLiveStream) {
4109
- device.activeLiveStream.active = false;
4110
- device.activeLiveStream.stoppedAt = device.disconnectedAt;
4111
- device.activeLiveStream.stopReason = reason;
4112
- }
4111
+ deactivateDeviceLiveStreams(device, reason, device.disconnectedAt);
4113
4112
  failAllPendingTasks(device, 'device-disconnected');
4114
4113
  emitRemoteEvent('RemoteDeviceDisconnected', device, { reason, synthetic: true });
4115
4114
  return true;
@@ -4432,20 +4431,69 @@ export function createRemoteHub(options = {}) {
4432
4431
  });
4433
4432
  }
4434
4433
 
4435
- function makeStableLiveStreamId(deviceId) {
4434
+ function ensureDeviceLiveStreams(device) {
4435
+ if (!(device?.activeLiveStreams instanceof Map)) {
4436
+ Object.defineProperty(device, 'activeLiveStreams', {
4437
+ value: new Map(),
4438
+ enumerable: false,
4439
+ configurable: true,
4440
+ writable: true
4441
+ });
4442
+ if (device?.activeLiveStream?.streamId) {
4443
+ device.activeLiveStreams.set(device.activeLiveStream.streamId, device.activeLiveStream);
4444
+ }
4445
+ }
4446
+ return device.activeLiveStreams;
4447
+ }
4448
+
4449
+ function getDeviceLiveStream(device, streamId) {
4450
+ const id = safeString(streamId, 128);
4451
+ if (!id) {
4452
+ return device?.activeLiveStream || null;
4453
+ }
4454
+ return ensureDeviceLiveStreams(device).get(id)
4455
+ || (device?.activeLiveStream?.streamId === id ? device.activeLiveStream : null);
4456
+ }
4457
+
4458
+ function setDeviceLiveStream(device, stream) {
4459
+ if (!device || !stream?.streamId) {
4460
+ return stream;
4461
+ }
4462
+ ensureDeviceLiveStreams(device).set(stream.streamId, stream);
4463
+ device.activeLiveStream = stream;
4464
+ return stream;
4465
+ }
4466
+
4467
+ function deactivateDeviceLiveStreams(device, reason, stoppedAt = new Date().toISOString()) {
4468
+ const streams = ensureDeviceLiveStreams(device);
4469
+ for (const stream of streams.values()) {
4470
+ stream.active = false;
4471
+ stream.stoppedAt = stoppedAt;
4472
+ stream.stopReason = reason;
4473
+ }
4474
+ if (device?.activeLiveStream) {
4475
+ device.activeLiveStream.active = false;
4476
+ device.activeLiveStream.stoppedAt = stoppedAt;
4477
+ device.activeLiveStream.stopReason = reason;
4478
+ }
4479
+ }
4480
+
4481
+ function makeStableLiveStreamId(deviceId, purpose = 'wall') {
4436
4482
  const hash = crypto.createHash('sha1').update(String(deviceId || 'device')).digest('hex').slice(0, 16);
4437
- return `live-${hash}`;
4483
+ const role = safeString(purpose, 24).toLowerCase().replace(/[^a-z0-9_-]+/g, '-') || 'wall';
4484
+ return `${role}-${hash}`;
4438
4485
  }
4439
4486
 
4440
4487
  function normalizeLiveStreamStartOptions(options = {}) {
4441
4488
  const transfer = buildRemoteFrameTransferDescriptor(options.mode || options.frameMode || DEFAULT_REMOTE_FRAME_MODE);
4442
- const maxFps = transfer.frameMode === 'mode3-h264-hw' ? 20 : 30;
4489
+ const maxFps = 30;
4443
4490
  const fps = clampNumber(options.fps, 1, maxFps, 8);
4444
4491
  const maxWidth = clampNumber(options.maxWidth, 320, 3840, 640);
4445
4492
  const maxHeight = clampNumber(options.maxHeight, 180, 2160, 360);
4446
4493
  const quality = clampNumber(options.quality, 20, 95, 45);
4447
4494
  const monitorIndex = normalizeMonitorIndex(options.monitorIndex ?? options.screenIndex ?? options.displayIndex);
4448
- return { fps, maxWidth, maxHeight, quality, monitorIndex, transfer };
4495
+ const streamPurpose = safeString(options.streamPurpose || options.purpose || 'wall', 24) || 'wall';
4496
+ return { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose };
4449
4497
  }
4450
4498
 
4451
4499
  function liveStreamStartStillPending(activeLiveStream) {
@@ -4506,16 +4554,17 @@ export function createRemoteHub(options = {}) {
4506
4554
  }
4507
4555
 
4508
4556
  const now = new Date().toISOString();
4509
- const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId);
4510
4557
  const normalized = normalizeLiveStreamStartOptions({ fps: 2, ...options });
4511
- const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer } = normalized;
4558
+ const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
4559
+ const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
4560
+ const activeLiveStream = getDeviceLiveStream(device, streamId);
4512
4561
  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))) {
4562
+ if (activeLiveStream
4563
+ && liveStreamMatchesOptions(activeLiveStream, normalized)
4564
+ && (activeLiveStream.open === true || liveStreamStartStillPending(activeLiveStream))) {
4516
4565
  return {
4517
4566
  ok: true,
4518
- commandId: device.activeLiveStream.commandId,
4567
+ commandId: activeLiveStream.commandId,
4519
4568
  streamId,
4520
4569
  fps,
4521
4570
  mode: transfer.mode,
@@ -4526,7 +4575,7 @@ export function createRemoteHub(options = {}) {
4526
4575
  };
4527
4576
  }
4528
4577
  device.counters.commandsSent += 1;
4529
- device.activeLiveStream = {
4578
+ setDeviceLiveStream(device, {
4530
4579
  streamId,
4531
4580
  commandId,
4532
4581
  active: true,
@@ -4553,8 +4602,9 @@ export function createRemoteHub(options = {}) {
4553
4602
  stopReason: '',
4554
4603
  lastFrameAt: '',
4555
4604
  lastFrameSeq: 0,
4556
- framesReceived: 0
4557
- };
4605
+ framesReceived: 0,
4606
+ streamPurpose
4607
+ });
4558
4608
  device.counters.liveStreamsStarted += 1;
4559
4609
  applySyntheticLiveFrame(device, streamId, {
4560
4610
  commandId,
@@ -4581,30 +4631,31 @@ export function createRemoteHub(options = {}) {
4581
4631
  }
4582
4632
 
4583
4633
  const now = new Date().toISOString();
4584
- const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId);
4585
4634
  const normalized = normalizeLiveStreamStartOptions(options);
4586
- const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer } = normalized;
4635
+ const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
4636
+ const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
4637
+ const activeLiveStream = getDeviceLiveStream(device, streamId);
4587
4638
  const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
4588
- if (device.activeLiveStream?.streamId === streamId
4639
+ if (activeLiveStream
4589
4640
  && options.forceRestart !== true
4590
4641
  && options.reuseExisting === true
4591
- && liveStreamMatchesOptions(device.activeLiveStream, normalized)
4592
- && liveStreamIsReusable(device.activeLiveStream)) {
4642
+ && liveStreamMatchesOptions(activeLiveStream, normalized)
4643
+ && liveStreamIsReusable(activeLiveStream)) {
4593
4644
  return {
4594
4645
  ok: true,
4595
- commandId: device.activeLiveStream.commandId,
4646
+ commandId: activeLiveStream.commandId,
4596
4647
  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),
4648
+ fps: Number(activeLiveStream.fps || fps),
4649
+ mode: activeLiveStream.mode || transfer.mode,
4650
+ frameMode: activeLiveStream.frameMode || transfer.frameMode,
4651
+ monitorIndex: Number(activeLiveStream.monitorIndex || monitorIndex),
4601
4652
  reused: true
4602
4653
  };
4603
4654
  }
4604
- if (device.activeLiveStream?.streamId === streamId
4655
+ if (activeLiveStream
4605
4656
  && options.forceRestart !== true
4606
- && liveStreamMatchesOptions(device.activeLiveStream, normalized)
4607
- && liveStreamIsReusable(device.activeLiveStream)) {
4657
+ && liveStreamMatchesOptions(activeLiveStream, normalized)
4658
+ && liveStreamIsReusable(activeLiveStream)) {
4608
4659
  if (options.silentReuse !== true) {
4609
4660
  emitRemoteEvent('RemoteLiveStreamStartReused', device, {
4610
4661
  streamId,
@@ -4616,7 +4667,7 @@ export function createRemoteHub(options = {}) {
4616
4667
  }
4617
4668
  return {
4618
4669
  ok: true,
4619
- commandId: device.activeLiveStream.commandId,
4670
+ commandId: activeLiveStream.commandId,
4620
4671
  streamId,
4621
4672
  fps,
4622
4673
  mode: transfer.mode,
@@ -4625,7 +4676,7 @@ export function createRemoteHub(options = {}) {
4625
4676
  reused: true
4626
4677
  };
4627
4678
  }
4628
- if (options.forceRestart === true && device.activeLiveStream?.streamId === streamId) {
4679
+ if (options.forceRestart === true && activeLiveStream) {
4629
4680
  emitRemoteEvent('RemoteLiveStreamForcedRestart', device, {
4630
4681
  streamId,
4631
4682
  fps,
@@ -4635,30 +4686,19 @@ export function createRemoteHub(options = {}) {
4635
4686
  reason: safeString(options.restartReason || 'browser-stale', 128)
4636
4687
  });
4637
4688
  }
4638
- if (device.activeLiveStream?.streamId === streamId
4639
- && liveStreamMatchesOptions(device.activeLiveStream, normalized)
4640
- && device.activeLiveStream.open === true
4641
- && !liveStreamIsReusable(device.activeLiveStream)) {
4689
+ if (activeLiveStream
4690
+ && liveStreamMatchesOptions(activeLiveStream, normalized)
4691
+ && activeLiveStream.open === true
4692
+ && !liveStreamIsReusable(activeLiveStream)) {
4642
4693
  emitRemoteEvent('RemoteLiveStreamStaleRestart', device, {
4643
4694
  streamId,
4644
4695
  fps,
4645
4696
  mode: transfer.mode,
4646
4697
  frameMode: transfer.frameMode,
4647
4698
  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
- }
4699
+ idleMs: getLiveStreamIdleMs(activeLiveStream),
4700
+ staleAfterMs: getLiveStreamFreshWindowMs(activeLiveStream),
4701
+ lastFrameAt: activeLiveStream.lastFrameAt || ''
4662
4702
  });
4663
4703
  }
4664
4704
  const result = sendCommand(deviceId, {
@@ -4680,6 +4720,7 @@ export function createRemoteHub(options = {}) {
4680
4720
  maxHeight,
4681
4721
  quality,
4682
4722
  monitorIndex,
4723
+ streamPurpose,
4683
4724
  requestedAt: now
4684
4725
  }
4685
4726
  });
@@ -4688,7 +4729,7 @@ export function createRemoteHub(options = {}) {
4688
4729
  return result;
4689
4730
  }
4690
4731
 
4691
- device.activeLiveStream = {
4732
+ setDeviceLiveStream(device, {
4692
4733
  streamId,
4693
4734
  commandId,
4694
4735
  active: true,
@@ -4715,8 +4756,9 @@ export function createRemoteHub(options = {}) {
4715
4756
  stopReason: '',
4716
4757
  lastFrameAt: '',
4717
4758
  lastFrameSeq: 0,
4718
- framesReceived: 0
4719
- };
4759
+ framesReceived: 0,
4760
+ streamPurpose
4761
+ });
4720
4762
  device.counters.liveStreamsStarted += 1;
4721
4763
  emitRemoteEvent('RemoteLiveStreamStarted', device, {
4722
4764
  streamId,
@@ -4736,10 +4778,11 @@ export function createRemoteHub(options = {}) {
4736
4778
  const streamId = safeString(options.streamId, 128) || device.activeLiveStream?.streamId || '';
4737
4779
  const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
4738
4780
  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';
4781
+ const streamState = getDeviceLiveStream(device, streamId);
4782
+ if (streamState) {
4783
+ streamState.active = false;
4784
+ streamState.stoppedAt = new Date().toISOString();
4785
+ streamState.stopReason = 'manager-request';
4743
4786
  }
4744
4787
  device.counters.liveStreamsStopped += 1;
4745
4788
  emitRemoteEvent('RemoteLiveStreamStopped', device, {
@@ -4769,10 +4812,11 @@ export function createRemoteHub(options = {}) {
4769
4812
  return result;
4770
4813
  }
4771
4814
 
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';
4815
+ const streamState = getDeviceLiveStream(device, streamId);
4816
+ if (streamState) {
4817
+ streamState.active = false;
4818
+ streamState.stoppedAt = new Date().toISOString();
4819
+ streamState.stopReason = 'manager-request';
4776
4820
  }
4777
4821
  device.counters.liveStreamsStopped += 1;
4778
4822
  emitRemoteEvent('RemoteLiveStreamStopped', device, {
package/hub/src/server.js CHANGED
@@ -398,7 +398,7 @@ function normalizeTransferChunk(body = {}) {
398
398
 
399
399
  function normalizeLiveOptions(payload = {}) {
400
400
  const mode = String(payload.frameMode || payload.mode || 'mode3-h264-hw').trim() || 'mode3-h264-hw';
401
- const maxFps = mode === 'mode3-h264-hw' ? 20 : 30;
401
+ const maxFps = 30;
402
402
  return {
403
403
  fps: clampNumber(payload.fps, 1, maxFps, 2),
404
404
  maxWidth: clampNumber(payload.maxWidth, 320, 3840, 640),
@@ -408,6 +408,7 @@ function normalizeLiveOptions(payload = {}) {
408
408
  monitorSelections: normalizeMonitorSelections(payload.monitorSelections),
409
409
  forceRestart: /^(1|true|yes|on)$/i.test(String(payload.forceRestart ?? '')),
410
410
  reuseExisting: /^(1|true|yes|on)$/i.test(String(payload.reuseExisting ?? '')),
411
+ streamPurpose: String(payload.streamPurpose || payload.purpose || 'wall').trim().slice(0, 24) || 'wall',
411
412
  mode,
412
413
  frameMode: mode
413
414
  };
@@ -607,6 +608,14 @@ function updateFrameSubscription(ws, payload = {}) {
607
608
  registerFrameClient(ws);
608
609
  ws.liveDeskAutoStart = /^(1|true|yes|on|live)$/i.test(String(payload.autoStartLive ?? payload.startLive ?? ''));
609
610
  ws.liveDeskLiveOptions = normalizeLiveOptions(payload);
611
+ if (!(ws.liveDeskStreamIdsByDeviceId instanceof Map)) {
612
+ ws.liveDeskStreamIdsByDeviceId = new Map();
613
+ }
614
+ for (const subscribedDeviceId of ws.liveDeskStreamIdsByDeviceId.keys()) {
615
+ if (!ws.liveDeskDeviceIds.has(subscribedDeviceId)) {
616
+ ws.liveDeskStreamIdsByDeviceId.delete(subscribedDeviceId);
617
+ }
618
+ }
610
619
  sendJson(ws, {
611
620
  type: 'RemoteFrameSubscription',
612
621
  timestamp: new Date().toISOString(),
@@ -666,8 +675,10 @@ function startFrameSubscriptionLive(ws, reason = 'subscribe', onlyDeviceId = '',
666
675
  silentReuse: reason === 'watchdog' && liveOptions.forceRestart !== true
667
676
  });
668
677
  if (result?.ok) {
678
+ ws.liveDeskStreamIdsByDeviceId.set(deviceId, result.streamId);
669
679
  started.push({ deviceId, streamId: result.streamId, fps: result.fps, reused: result.reused === true });
670
680
  } else {
681
+ ws.liveDeskStreamIdsByDeviceId.delete(deviceId);
671
682
  skipped.push({ deviceId, reason: result?.error || 'start-failed' });
672
683
  }
673
684
  }
@@ -836,6 +847,12 @@ function broadcastRemoteBinaryFrame(frameEvent) {
836
847
  if (client.readyState !== client.OPEN) {
837
848
  continue;
838
849
  }
850
+ const expectedStreamId = client.liveDeskStreamIdsByDeviceId instanceof Map
851
+ ? client.liveDeskStreamIdsByDeviceId.get(deviceId)
852
+ : '';
853
+ if (expectedStreamId && String(frame.streamId || '') !== expectedStreamId) {
854
+ continue;
855
+ }
839
856
  const lane = ensureFrameClientSendLane(client);
840
857
  if (client.liveDeskFrameBackpressured && isH264 && !isKeyFrame) {
841
858
  lane.dropped += 1;
@@ -964,6 +981,7 @@ function configureMode4Atlas(ws, payload = {}) {
964
981
  quality: 60,
965
982
  mode: 'mode2-lzo',
966
983
  frameMode: 'mode2-lzo',
984
+ streamPurpose: 'atlas',
967
985
  monitorIndex: Number(monitorSelections[deviceId] || 0),
968
986
  reuseExisting: false,
969
987
  silentReuse: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.154",
3
+ "version": "0.1.155",
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.103",
34
+ "@livedesk/client": "0.1.104",
35
35
  "cors": "^2.8.5",
36
36
  "express": "^4.21.2",
37
37
  "ffmpeg-static": "^5.3.0",