livedesk 0.1.460 → 0.1.461

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.
@@ -4689,6 +4689,10 @@ export function createRemoteHub(options = {}) {
4689
4689
  ? message.nativeEncoderHardwareAccelerated
4690
4690
  : null,
4691
4691
  nativeEncoderHardwareQueryStatus: safeString(message.nativeEncoderHardwareQueryStatus, 80),
4692
+ nativeCaptureDisplayId: Number.isSafeInteger(Number(message.nativeCaptureDisplayId))
4693
+ && Number(message.nativeCaptureDisplayId) > 0
4694
+ ? Number(message.nativeCaptureDisplayId)
4695
+ : 0,
4692
4696
  nativeCaptureFps: Number.isFinite(Number(message.nativeCaptureFps)) ? Number(message.nativeCaptureFps) : 0,
4693
4697
  nativeCompressionFps: Number.isFinite(Number(message.nativeCompressionFps)) ? Number(message.nativeCompressionFps) : 0,
4694
4698
  nativeEncodedFps: Number.isFinite(Number(message.nativeEncodedFps)) ? Number(message.nativeEncodedFps) : 0,
@@ -4746,7 +4750,7 @@ export function createRemoteHub(options = {}) {
4746
4750
  return true;
4747
4751
  }
4748
4752
 
4749
- function promotePendingLiveStream(device, streamState, message, transport) {
4753
+ function matchPendingLiveStreamOwner(device, streamState, message, transport) {
4750
4754
  const pending = getPendingLiveStreamDescriptor(streamState);
4751
4755
  const commandId = safeString(message.commandId, 128);
4752
4756
  if (!pending || !commandId || commandId !== safeString(pending.commandId, 128)) {
@@ -4754,9 +4758,11 @@ export function createRemoteHub(options = {}) {
4754
4758
  }
4755
4759
  const messageCaptureGeneration = Number(message.captureGeneration || 0);
4756
4760
  const pendingCaptureGeneration = Number(pending.captureGeneration || 0);
4757
- if (messageCaptureGeneration > 0
4758
- && pendingCaptureGeneration > 0
4759
- && messageCaptureGeneration !== pendingCaptureGeneration) {
4761
+ if (!Number.isSafeInteger(messageCaptureGeneration)
4762
+ || messageCaptureGeneration <= 0
4763
+ || !Number.isSafeInteger(pendingCaptureGeneration)
4764
+ || pendingCaptureGeneration <= 0
4765
+ || messageCaptureGeneration !== pendingCaptureGeneration) {
4760
4766
  emitRemoteEvent('RemoteLiveStreamOpenIgnored', device, {
4761
4767
  reason: 'stale-pending-capture-generation',
4762
4768
  streamId: streamState.streamId,
@@ -4795,6 +4801,27 @@ export function createRemoteHub(options = {}) {
4795
4801
  });
4796
4802
  return null;
4797
4803
  }
4804
+ return {
4805
+ pending,
4806
+ commandId,
4807
+ messageCaptureGeneration,
4808
+ pendingCaptureGeneration,
4809
+ pendingMonitorIndex
4810
+ };
4811
+ }
4812
+
4813
+ function promotePendingLiveStream(device, streamState, message, transport) {
4814
+ const owner = matchPendingLiveStreamOwner(device, streamState, message, transport);
4815
+ if (!owner) {
4816
+ return null;
4817
+ }
4818
+ const {
4819
+ pending,
4820
+ commandId,
4821
+ messageCaptureGeneration,
4822
+ pendingCaptureGeneration,
4823
+ pendingMonitorIndex
4824
+ } = owner;
4798
4825
 
4799
4826
  const previousCommandId = safeString(streamState.commandId, 128);
4800
4827
  const transfer = buildRemoteFrameTransferDescriptorFromMessage(
@@ -4903,16 +4930,22 @@ export function createRemoteHub(options = {}) {
4903
4930
 
4904
4931
  const activatingPendingGeneration = isPendingGeneration && commandId !== activeCommandId;
4905
4932
  if (activatingPendingGeneration) {
4906
- const promoted = promotePendingLiveStream(device, streamState, message, transport);
4907
- if (!promoted) {
4933
+ const owner = matchPendingLiveStreamOwner(device, streamState, message, transport);
4934
+ if (!owner) {
4908
4935
  return false;
4909
4936
  }
4910
- emitLiveStreamOpened(
4911
- device,
4912
- promoted.streamState,
4913
- promoted.transfer,
4914
- transport,
4915
- promoted.previousCommandId);
4937
+ // stream.open travels on the manager connection while the binary
4938
+ // first frame travels on a separate transport. Their arrival order
4939
+ // is not defined. Keep the current generation and its last frame
4940
+ // until the matching first real frame validates and commits the
4941
+ // pending owner atomically.
4942
+ emitRemoteEvent('RemoteLiveStreamOpenDeferred', device, {
4943
+ streamId,
4944
+ commandId,
4945
+ captureGeneration: owner.messageCaptureGeneration,
4946
+ monitorIndex: owner.pendingMonitorIndex,
4947
+ transport
4948
+ });
4916
4949
  return true;
4917
4950
  }
4918
4951
 
@@ -4989,8 +5022,8 @@ export function createRemoteHub(options = {}) {
4989
5022
  const commandId = safeString(message.commandId, 128);
4990
5023
  let streamState = getDeviceLiveStream(device, streamId);
4991
5024
  if (!streamState?.active) {
4992
- device.counters.liveFramesDropped += 1;
4993
- emitRemoteEvent('RemoteFrameDropped', device, {
5025
+ device.counters.liveFramesDropped += 1;
5026
+ emitRemoteEvent('RemoteFrameDropped', device, {
4994
5027
  reason: 'stale-live-stream-frame',
4995
5028
  streamId,
4996
5029
  frameSeq: Number.isFinite(frameSeq) ? frameSeq : null,
@@ -4998,9 +5031,91 @@ export function createRemoteHub(options = {}) {
4998
5031
  });
4999
5032
  return false;
5000
5033
  }
5034
+ const byteLength = normalizeFrameByteLength(framePayload, frameData);
5035
+ const streamFrameSeq = Number.isFinite(Number(message.streamFrameSeq))
5036
+ ? Number(message.streamFrameSeq)
5037
+ : frameSeq;
5038
+ if (byteLength <= 0
5039
+ || byteLength > MAX_STREAM_BINARY_BYTES
5040
+ || !Number.isSafeInteger(frameSeq)
5041
+ || frameSeq <= 0
5042
+ || !Number.isSafeInteger(streamFrameSeq)
5043
+ || streamFrameSeq <= 0) {
5044
+ device.counters.liveFramesDropped += 1;
5045
+ emitRemoteEvent('RemoteFrameDropped', device, {
5046
+ reason: 'invalid-live-frame',
5047
+ streamId,
5048
+ commandId,
5049
+ captureGeneration: Number(message.captureGeneration || 0),
5050
+ monitorIndex: parseExactLiveStreamMonitorIndex(message.monitorIndex),
5051
+ frameSeq: Number.isFinite(frameSeq) ? frameSeq : null,
5052
+ transport
5053
+ });
5054
+ return false;
5055
+ }
5056
+ const payload = buildFramePayloadBuffer(framePayload, frameData);
5057
+ if (payload.length <= 0 || payload.length > MAX_STREAM_BINARY_BYTES) {
5058
+ device.counters.liveFramesDropped += 1;
5059
+ emitRemoteEvent('RemoteFrameDropped', device, {
5060
+ reason: 'invalid-live-frame-payload',
5061
+ streamId,
5062
+ commandId,
5063
+ captureGeneration: Number(message.captureGeneration || 0),
5064
+ monitorIndex: parseExactLiveStreamMonitorIndex(message.monitorIndex),
5065
+ frameSeq,
5066
+ transport
5067
+ });
5068
+ return false;
5069
+ }
5001
5070
  let activeCommandId = safeString(streamState.commandId, 128);
5002
- let pendingCommandId = safeString(getPendingLiveStreamDescriptor(streamState)?.commandId, 128);
5071
+ const pendingDescriptor = getPendingLiveStreamDescriptor(streamState);
5072
+ let pendingCommandId = safeString(pendingDescriptor?.commandId, 128);
5003
5073
  if (pendingCommandId && commandId === pendingCommandId && commandId !== activeCommandId) {
5074
+ if (!matchPendingLiveStreamOwner(device, streamState, message, `${transport}-implicit`)) {
5075
+ return false;
5076
+ }
5077
+ const pendingFrameMode = safeString(
5078
+ message.frameMode
5079
+ || message.mode
5080
+ || pendingDescriptor?.frameMode
5081
+ || pendingDescriptor?.mode,
5082
+ 40).toLowerCase();
5083
+ const pendingMimeType = safeString(
5084
+ message.mimeType
5085
+ || message.format
5086
+ || pendingDescriptor?.compression,
5087
+ 80).toLowerCase();
5088
+ const pendingCodec = safeString(
5089
+ message.codec
5090
+ || pendingDescriptor?.codec
5091
+ || pendingDescriptor?.encoding,
5092
+ 40).toLowerCase();
5093
+ const requiresH264KeyFrame = pendingFrameMode === 'mode3-h264-hw'
5094
+ || pendingMimeType.includes('h264')
5095
+ || pendingMimeType.includes('avc')
5096
+ || pendingCodec.includes('h264')
5097
+ || pendingCodec.includes('avc');
5098
+ const isH264KeyFrame = message.isKeyFrame === true
5099
+ || safeString(message.chunkType, 20).toLowerCase() === 'key';
5100
+ const h264 = requiresH264KeyFrame ? inspectAnnexBH264(payload) : null;
5101
+ const independentlyDecodableH264KeyFrame = isH264KeyFrame
5102
+ && h264?.annexB === true
5103
+ && h264.hasSps === true
5104
+ && h264.hasPps === true
5105
+ && h264.hasIdr === true
5106
+ && h264.nalOrderValid === true;
5107
+ if (requiresH264KeyFrame && !independentlyDecodableH264KeyFrame) {
5108
+ device.counters.liveFramesDropped += 1;
5109
+ emitRemoteEvent('RemoteFrameDropped', device, {
5110
+ reason: 'pending-live-stream-decodable-keyframe-required',
5111
+ streamId,
5112
+ commandId,
5113
+ frameSeq,
5114
+ streamFrameSeq,
5115
+ transport
5116
+ });
5117
+ return false;
5118
+ }
5004
5119
  const promoted = promotePendingLiveStream(device, streamState, message, `${transport}-implicit`);
5005
5120
  if (promoted) {
5006
5121
  streamState = promoted.streamState;
@@ -5078,24 +5193,38 @@ export function createRemoteHub(options = {}) {
5078
5193
  return false;
5079
5194
  }
5080
5195
 
5081
- const byteLength = normalizeFrameByteLength(framePayload, frameData);
5082
- if ((!Buffer.isBuffer(framePayload) && !frameData)
5083
- || byteLength > MAX_STREAM_BINARY_BYTES
5084
- || !Number.isFinite(frameSeq)) {
5085
- device.counters.liveFramesDropped += 1;
5086
- emitRemoteEvent('RemoteFrameDropped', device, {
5087
- reason: 'invalid-live-frame',
5088
- streamId,
5089
- frameSeq: Number.isFinite(frameSeq) ? frameSeq : null,
5090
- transport
5091
- });
5092
- return false;
5093
- }
5094
-
5095
- const mimeType = safeString(message.mimeType || message.format || 'image/jpeg', 80) || 'image/jpeg';
5096
- const capturedAt = safeString(message.capturedAt, 80) || device.lastSeenAt;
5097
- const payload = buildFramePayloadBuffer(framePayload, frameData);
5098
- const contentHash = buildFrameContentHash(message, payload);
5196
+ const previousFrame = streamState.latestFrame;
5197
+ const currentOwnerCommandId = commandId || activeCommandId;
5198
+ const currentOwnerCaptureGeneration = activeCaptureGeneration || frameCaptureGeneration;
5199
+ const previousStreamFrameSeq = Number(previousFrame?.streamFrameSeq ?? previousFrame?.frameSeq);
5200
+ const sameFrameOwner = previousFrame
5201
+ && safeString(previousFrame.streamId, 128) === streamId
5202
+ && safeString(previousFrame.commandId, 128) === currentOwnerCommandId
5203
+ && Number(previousFrame.captureGeneration || 0) === currentOwnerCaptureGeneration
5204
+ && parseExactLiveStreamMonitorIndex(previousFrame.monitorIndex) === frameMonitorIndex;
5205
+ if (sameFrameOwner
5206
+ && Number.isSafeInteger(previousStreamFrameSeq)
5207
+ && streamFrameSeq <= previousStreamFrameSeq) {
5208
+ device.counters.liveFramesDropped += 1;
5209
+ emitRemoteEvent('RemoteFrameDropped', device, {
5210
+ reason: streamFrameSeq === previousStreamFrameSeq
5211
+ ? 'duplicate-live-stream-frame'
5212
+ : 'out-of-order-live-stream-frame',
5213
+ streamId,
5214
+ commandId: currentOwnerCommandId,
5215
+ captureGeneration: currentOwnerCaptureGeneration,
5216
+ monitorIndex: frameMonitorIndex,
5217
+ frameSeq,
5218
+ streamFrameSeq,
5219
+ previousStreamFrameSeq,
5220
+ transport
5221
+ });
5222
+ return false;
5223
+ }
5224
+
5225
+ const mimeType = safeString(message.mimeType || message.format || 'image/jpeg', 80) || 'image/jpeg';
5226
+ const capturedAt = safeString(message.capturedAt, 80) || device.lastSeenAt;
5227
+ const contentHash = buildFrameContentHash(message, payload);
5099
5228
  const sameContentStreak = computeSameContentStreak(streamState.latestFrame, contentHash);
5100
5229
  const transfer = buildRemoteFrameTransferDescriptorFromMessage(message, streamState.mode || DEFAULT_REMOTE_FRAME_MODE);
5101
5230
  if (streamState.open !== true) {
@@ -5129,7 +5258,7 @@ export function createRemoteHub(options = {}) {
5129
5258
  device.latestLiveFrame = {
5130
5259
  streamId,
5131
5260
  frameSeq,
5132
- streamFrameSeq: Number.isFinite(Number(message.streamFrameSeq)) ? Number(message.streamFrameSeq) : frameSeq,
5261
+ streamFrameSeq,
5133
5262
  commandId: commandId || activeCommandId,
5134
5263
  sessionId: device.sessionId,
5135
5264
  captureGeneration: activeCaptureGeneration || frameCaptureGeneration,
@@ -5175,6 +5304,10 @@ export function createRemoteHub(options = {}) {
5175
5304
  ? message.nativeEncoderHardwareAccelerated
5176
5305
  : null,
5177
5306
  nativeEncoderHardwareQueryStatus: safeString(message.nativeEncoderHardwareQueryStatus, 80),
5307
+ nativeCaptureDisplayId: Number.isSafeInteger(Number(message.nativeCaptureDisplayId))
5308
+ && Number(message.nativeCaptureDisplayId) > 0
5309
+ ? Number(message.nativeCaptureDisplayId)
5310
+ : 0,
5178
5311
  nativeCaptureFps: Number.isFinite(Number(message.nativeCaptureFps)) ? Number(message.nativeCaptureFps) : 0,
5179
5312
  nativeCompressionFps: Number.isFinite(Number(message.nativeCompressionFps)) ? Number(message.nativeCompressionFps) : 0,
5180
5313
  nativeEncodedFps: Number.isFinite(Number(message.nativeEncodedFps)) ? Number(message.nativeEncodedFps) : 0,
@@ -5369,7 +5502,7 @@ export function createRemoteHub(options = {}) {
5369
5502
  return true;
5370
5503
  }
5371
5504
 
5372
- function applyAudioStatus(device, message, framePayload, transport = 'binary') {
5505
+ function applyAudioStatus(device, message, framePayload, transport = 'binary') {
5373
5506
  if (!Buffer.isBuffer(framePayload) || framePayload.length < 1) {
5374
5507
  device.counters.audioFramesDropped += 1;
5375
5508
  return false;
@@ -5410,9 +5543,184 @@ export function createRemoteHub(options = {}) {
5410
5543
  payload: framePayload,
5411
5544
  byteLength: framePayload.length
5412
5545
  });
5413
- return true;
5414
- }
5415
-
5546
+ return true;
5547
+ }
5548
+
5549
+ function readFrameTransportProof(message = {}) {
5550
+ if (message.frameTransportProofId === undefined
5551
+ && message.frameTransportProofVersion === undefined) {
5552
+ return null;
5553
+ }
5554
+ const rawProofId = typeof message.frameTransportProofId === 'string'
5555
+ ? message.frameTransportProofId.trim()
5556
+ : '';
5557
+ const captureGeneration = Number(message.captureGeneration);
5558
+ const monitorIndex = parseExactLiveStreamMonitorIndex(message.monitorIndex);
5559
+ const proof = {
5560
+ proofId: rawProofId,
5561
+ streamId: safeString(message.streamId, 128),
5562
+ commandId: safeString(message.commandId, 128),
5563
+ captureGeneration,
5564
+ monitorIndex
5565
+ };
5566
+ const valid = Number(message.frameTransportProofVersion) === 1
5567
+ && /^[a-f0-9]{32}$/.test(rawProofId)
5568
+ && !!proof.streamId
5569
+ && !!proof.commandId
5570
+ && Number.isSafeInteger(captureGeneration)
5571
+ && captureGeneration > 0
5572
+ && monitorIndex !== null;
5573
+ return {
5574
+ ...proof,
5575
+ valid,
5576
+ error: valid ? '' : 'frame-proof-owner-invalid'
5577
+ };
5578
+ }
5579
+
5580
+ function applyLiveFrameWithTransportProof(
5581
+ device,
5582
+ message,
5583
+ framePayload,
5584
+ transport) {
5585
+ const proof = readFrameTransportProof(message);
5586
+ if (!proof) {
5587
+ return {
5588
+ accepted: applyLiveFrame(device, message, framePayload, transport),
5589
+ proofAccepted: false,
5590
+ proof: null,
5591
+ error: ''
5592
+ };
5593
+ }
5594
+ const frameMode = safeString(message.frameMode || message.mode, 40).toLowerCase();
5595
+ const mimeType = safeString(message.mimeType || message.format, 80).toLowerCase();
5596
+ const chunkType = safeString(message.chunkType, 20).toLowerCase();
5597
+ const proofFrameSeq = Number(message.frameSeq);
5598
+ const proofStreamFrameSeq = message.streamFrameSeq === undefined
5599
+ || message.streamFrameSeq === null
5600
+ || message.streamFrameSeq === ''
5601
+ ? proofFrameSeq
5602
+ : Number(message.streamFrameSeq);
5603
+ const proofOnly = message.frameTransportProofOnly === true;
5604
+ const h264 = inspectAnnexBH264(framePayload);
5605
+ const proofPayloadValid = proof.valid
5606
+ && (transport === 'udp-p2p' || transport === 'relay-binary')
5607
+ && Buffer.isBuffer(framePayload)
5608
+ && framePayload.length > 0
5609
+ && framePayload.length <= MAX_STREAM_BINARY_BYTES
5610
+ && Number.isSafeInteger(proofFrameSeq)
5611
+ && proofFrameSeq > 0
5612
+ && Number.isSafeInteger(proofStreamFrameSeq)
5613
+ && proofStreamFrameSeq > 0
5614
+ && frameMode === 'mode3-h264-hw'
5615
+ && mimeType === 'video/h264'
5616
+ && (message.isKeyFrame === true || chunkType === 'key')
5617
+ && safeString(message.frameTransportActive, 20).toLowerCase() === 'none'
5618
+ && safeString(message.frameTransportState, 20).toLowerCase() === 'probing'
5619
+ && h264.annexB
5620
+ && h264.hasSps
5621
+ && h264.hasPps
5622
+ && h264.hasIdr
5623
+ && h264.nalOrderValid;
5624
+ if (!proofPayloadValid) {
5625
+ device.counters.liveFramesDropped += 1;
5626
+ emitRemoteEvent('RemoteFrameDropped', device, {
5627
+ reason: proof.error || 'frame-proof-h264-invalid',
5628
+ streamId: proof.streamId,
5629
+ commandId: proof.commandId,
5630
+ captureGeneration: proof.captureGeneration,
5631
+ monitorIndex: proof.monitorIndex,
5632
+ transport
5633
+ });
5634
+ return {
5635
+ accepted: false,
5636
+ proofAccepted: false,
5637
+ proof,
5638
+ error: proof.error || 'frame-proof-h264-invalid'
5639
+ };
5640
+ }
5641
+ if (proofOnly) {
5642
+ const streamState = getDeviceLiveStream(device, proof.streamId);
5643
+ // A proof-only frame is an out-of-band path challenge. It must
5644
+ // prove the exact current stream owner and carry a complete
5645
+ // independently decodable H.264 key frame, but it must not race
5646
+ // the presentation lane's latest delta frame. WAN fragmentation
5647
+ // can legitimately deliver this challenge several relay frames
5648
+ // after the same source key was presented.
5649
+ const exactActiveStreamOwner = transport === 'udp-p2p'
5650
+ && streamState?.active === true
5651
+ && safeString(streamState.commandId, 128) === proof.commandId
5652
+ && Number(streamState.captureGeneration || 0) === proof.captureGeneration
5653
+ && parseExactLiveStreamMonitorIndex(streamState.monitorIndex) === proof.monitorIndex;
5654
+ if (exactActiveStreamOwner) {
5655
+ emitRemoteEvent('RemoteFrameTransportProofAccepted', device, {
5656
+ proofOnly: true,
5657
+ proofId: proof.proofId,
5658
+ streamId: proof.streamId,
5659
+ commandId: proof.commandId,
5660
+ captureGeneration: proof.captureGeneration,
5661
+ monitorIndex: proof.monitorIndex,
5662
+ frameSeq: proofFrameSeq,
5663
+ streamFrameSeq: proofStreamFrameSeq,
5664
+ transport
5665
+ });
5666
+ }
5667
+ return {
5668
+ accepted: exactActiveStreamOwner,
5669
+ proofAccepted: exactActiveStreamOwner,
5670
+ proofOnly: true,
5671
+ proof,
5672
+ error: exactActiveStreamOwner
5673
+ ? ''
5674
+ : 'frame-proof-active-owner-mismatch'
5675
+ };
5676
+ }
5677
+ const accepted = applyLiveFrame(device, message, framePayload, transport);
5678
+ const frame = device.latestLiveFrame;
5679
+ const exactAcceptedFrame = accepted
5680
+ && frame?.currentGenerationVerified === true
5681
+ && safeString(frame.streamId, 128) === proof.streamId
5682
+ && safeString(frame.commandId, 128) === proof.commandId
5683
+ && Number(frame.captureGeneration) === proof.captureGeneration
5684
+ && parseExactLiveStreamMonitorIndex(frame.monitorIndex) === proof.monitorIndex;
5685
+ if (exactAcceptedFrame) {
5686
+ emitRemoteEvent('RemoteFrameTransportProofAccepted', device, {
5687
+ proofOnly: false,
5688
+ proofId: proof.proofId,
5689
+ streamId: proof.streamId,
5690
+ commandId: proof.commandId,
5691
+ captureGeneration: proof.captureGeneration,
5692
+ monitorIndex: proof.monitorIndex,
5693
+ frameSeq: proofFrameSeq,
5694
+ streamFrameSeq: proofStreamFrameSeq,
5695
+ transport
5696
+ });
5697
+ }
5698
+ return {
5699
+ accepted: exactAcceptedFrame,
5700
+ proofAccepted: exactAcceptedFrame,
5701
+ proof,
5702
+ error: exactAcceptedFrame ? '' : 'frame-proof-current-generation-rejected'
5703
+ };
5704
+ }
5705
+
5706
+ function writeFrameTransportProofResponse(socket, result) {
5707
+ const proof = result?.proof;
5708
+ if (!proof?.proofId) return;
5709
+ const accepted = result.proofAccepted === true;
5710
+ writeJsonLine(socket, {
5711
+ type: accepted
5712
+ ? 'frame.transport.proof-accepted'
5713
+ : 'frame.transport.proof-rejected',
5714
+ protocol: 'livedesk.frame-transport-proof.v1',
5715
+ proofId: proof.proofId,
5716
+ streamId: proof.streamId,
5717
+ commandId: proof.commandId,
5718
+ captureGeneration: proof.captureGeneration,
5719
+ monitorIndex: proof.monitorIndex,
5720
+ ...(accepted ? {} : { error: safeString(result.error || 'frame-proof-rejected', 160) })
5721
+ });
5722
+ }
5723
+
5416
5724
  function handleAgentBinaryFrame(socket, state, header, framePayload) {
5417
5725
  if (!state.authenticated || !state.device) {
5418
5726
  writeJsonLine(socket, { type: 'error', error: 'hello-required' });
@@ -5448,11 +5756,12 @@ export function createRemoteHub(options = {}) {
5448
5756
  }
5449
5757
 
5450
5758
  if (frameKind === 'stream' || frameKind === 'live' || header.type === 'stream.binary') {
5451
- applyLiveFrame(device, {
5759
+ const result = applyLiveFrameWithTransportProof(device, {
5452
5760
  ...header,
5453
5761
  hubIngressDropped: Number(state.binaryQueueDrops || 0),
5454
5762
  hubIngressCounterEpoch: state.binaryQueueEpoch
5455
5763
  }, framePayload, binaryTransport);
5764
+ writeFrameTransportProofResponse(socket, result);
5456
5765
  return;
5457
5766
  }
5458
5767
 
@@ -5472,7 +5781,7 @@ export function createRemoteHub(options = {}) {
5472
5781
  });
5473
5782
  }
5474
5783
 
5475
- function handleUdpFrame({ deviceId, sessionId, header, payload } = {}) {
5784
+ function handleUdpFrame({ deviceId, sessionId, header, payload } = {}) {
5476
5785
  const device = devices.get(safeString(deviceId, 160));
5477
5786
  if (!device || !device.connected) {
5478
5787
  return false;
@@ -5485,19 +5794,25 @@ export function createRemoteHub(options = {}) {
5485
5794
  emitRemoteEvent('RemoteFrameDropped', device, { reason: 'udp-frame-mode-not-supported', transport: 'udp-p2p', frameMode });
5486
5795
  return false;
5487
5796
  }
5488
- device.udp = {
5489
- ...device.udp,
5490
- state: 'udp-active',
5491
- ready: true,
5492
- lastFrameAt: new Date().toISOString(),
5493
- framesReceived: Number(device.udp?.framesReceived || 0) + 1
5494
- };
5495
- return applyLiveFrame(device, {
5797
+ const result = applyLiveFrameWithTransportProof(device, {
5496
5798
  ...header,
5497
5799
  transport: 'udp-p2p',
5498
5800
  hubIngressCounterEpoch: sessionId
5499
5801
  }, payload, 'udp-p2p');
5500
- }
5802
+ if (result.accepted) {
5803
+ device.udp = {
5804
+ ...device.udp,
5805
+ state: result.proof ? 'udp-proof-accepted' : 'udp-active',
5806
+ ready: true,
5807
+ lastFrameAt: new Date().toISOString(),
5808
+ framesReceived: Number(device.udp?.framesReceived || 0)
5809
+ + (result.proofOnly === true ? 0 : 1)
5810
+ };
5811
+ }
5812
+ return result.proof
5813
+ ? result
5814
+ : result.accepted;
5815
+ }
5501
5816
 
5502
5817
  function dropQueuedAgentBinaryFrame(state) {
5503
5818
  if (!Array.isArray(state.binaryQueue) || state.binaryQueue.length === 0) {
package/hub/src/server.js CHANGED
@@ -2137,6 +2137,10 @@ function buildRemoteFrameBinaryPacket(frameEvent, diagnostics = {}) {
2137
2137
  ? frame.nativeEncoderHardwareAccelerated
2138
2138
  : null,
2139
2139
  nativeEncoderHardwareQueryStatus: frame.nativeEncoderHardwareQueryStatus || '',
2140
+ nativeCaptureDisplayId: Number.isSafeInteger(Number(frame.nativeCaptureDisplayId))
2141
+ && Number(frame.nativeCaptureDisplayId) > 0
2142
+ ? Number(frame.nativeCaptureDisplayId)
2143
+ : 0,
2140
2144
  nativeCaptureFps: Number(frame.nativeCaptureFps || 0) || 0,
2141
2145
  nativeCompressionFps: Number(frame.nativeCompressionFps || 0) || 0,
2142
2146
  nativeEncodedFps: Number(frame.nativeEncodedFps || 0) || 0,
@@ -263,7 +263,7 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
263
263
  if (!complete) return;
264
264
  session.framesReceived += 1;
265
265
  const reassembly = session.reassembler.getStats();
266
- frameHandler({
266
+ const handled = frameHandler({
267
267
  deviceId: session.deviceId,
268
268
  sessionId: session.sessionId,
269
269
  header: {
@@ -276,11 +276,34 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
276
276
  },
277
277
  payload: complete.payload,
278
278
  transport: 'udp-p2p'
279
- });
280
- sendPacket(session, 'pong', {
281
- state: 'frame-ack',
282
- frameSeq: Number(complete.header?.frameSeq || 0) || 0
283
- });
279
+ });
280
+ const accepted = handled === true || handled?.accepted === true;
281
+ const proofId = safeText(complete.header?.frameTransportProofId, 96);
282
+ if (proofId) {
283
+ if (handled?.suppressProofResponse === true) return;
284
+ const proofAccepted = accepted && handled?.proofAccepted === true;
285
+ sendPacket(session, 'pong', {
286
+ state: proofAccepted ? 'frame-proof-ack' : 'frame-proof-reject',
287
+ proofId,
288
+ protocol: 'livedesk.frame-transport-proof.v1',
289
+ streamId: safeText(complete.header?.streamId, 128),
290
+ commandId: safeText(complete.header?.commandId, 128),
291
+ captureGeneration: Number(complete.header?.captureGeneration || 0) || 0,
292
+ monitorIndex: Number(complete.header?.monitorIndex ?? -1),
293
+ error: proofAccepted ? '' : safeText(handled?.error || 'frame-proof-rejected', 160)
294
+ });
295
+ return;
296
+ }
297
+ if (accepted) {
298
+ sendPacket(session, 'pong', {
299
+ state: 'frame-ack',
300
+ frameSeq: Number(complete.header?.frameSeq || 0) || 0,
301
+ streamId: safeText(complete.header?.streamId, 128),
302
+ commandId: safeText(complete.header?.commandId, 128),
303
+ captureGeneration: Number(complete.header?.captureGeneration || 0) || 0,
304
+ monitorIndex: Number(complete.header?.monitorIndex ?? -1)
305
+ });
306
+ }
284
307
  });
285
308
  socket.on('error', error => logWarn('udp', `UDP socket error: ${error?.message || error}`));
286
309
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.460",
4
- "livedeskClientVersion": "0.1.213",
3
+ "version": "0.1.461",
4
+ "livedeskClientVersion": "0.1.214",
5
5
  "buildFlavor": "production",
6
6
  "description": "LiveDesk Hub and client launcher",
7
7
  "type": "module",
@@ -15,9 +15,10 @@
15
15
  "diagnostics/",
16
16
  "hub/",
17
17
  "runtime-core/",
18
- "client/",
19
- "electron/",
20
- "web/",
18
+ "client/",
19
+ "electron/",
20
+ "scripts/",
21
+ "web/",
21
22
  "README.md"
22
23
  ],
23
24
  "scripts": {
@@ -50,10 +51,10 @@
50
51
  "ws": "^8.18.3"
51
52
  },
52
53
  "optionalDependencies": {
53
- "@livedesk/fast-linux-x64": "0.1.417",
54
- "@livedesk/fast-osx-arm64": "0.1.417",
55
- "@livedesk/fast-osx-x64": "0.1.417",
56
- "@livedesk/fast-win-x64": "0.1.417"
54
+ "@livedesk/fast-linux-x64": "0.1.419",
55
+ "@livedesk/fast-osx-arm64": "0.1.419",
56
+ "@livedesk/fast-osx-x64": "0.1.419",
57
+ "@livedesk/fast-win-x64": "0.1.419"
57
58
  },
58
59
  "publishConfig": {
59
60
  "access": "public"