livedesk 0.1.456 → 0.1.458

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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import assert from 'node:assert/strict';
4
- import { spawn, spawnSync } from 'node:child_process';
4
+ import { execFile, spawn, spawnSync } from 'node:child_process';
5
5
  import { createHash, randomBytes } from 'node:crypto';
6
6
  import { existsSync } from 'node:fs';
7
7
  import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
@@ -13,13 +13,15 @@ import { fileURLToPath } from 'node:url';
13
13
  import { setTimeout as delay } from 'node:timers/promises';
14
14
  import WebSocket from 'ws';
15
15
  import {
16
+ buildMacDiagnosticResourceSample,
16
17
  buildMacModifierProbeSteps,
17
18
  buildMacModifierReleaseSteps,
18
19
  classifyMacPhysicalGate,
20
+ createAsyncSingleFlightSampler,
19
21
  noteMacModifierAfterAppliedAck,
20
22
  noteMacModifierBeforeSend,
21
- parseMacHelperProcesses,
22
- parseMacLiveDeskRuntimeProcesses
23
+ parseMacLiveDeskRuntimeProcesses,
24
+ summarizeMacDiagnosticResourceSamples
23
25
  } from './livedesk-mac-physical-gate-core.mjs';
24
26
  import {
25
27
  acquireMacDiagnosticLease,
@@ -38,6 +40,7 @@ const publicMacPhysicalDiagnostic = String(rawArguments[0] || '').toLowerCase()
38
40
  const macPhysicalGate = process.argv.includes('--mac-physical-gate') || publicMacPhysicalDiagnostic;
39
41
  const isWindows = process.platform === 'win32';
40
42
  const isMacPhysicalGate = process.platform === 'darwin' && macPhysicalGate;
43
+ const MAC_PHYSICAL_RESOURCE_SAMPLE_INTERVAL_MS = 500;
41
44
  if (macPhysicalGate && !isMacPhysicalGate) {
42
45
  const message = 'Mac physical diagnostic requires macOS hardware; no LiveDesk process was started.';
43
46
  if (publicMacPhysicalDiagnostic) {
@@ -313,9 +316,28 @@ function readMacProcessSnapshot() {
313
316
  return String(result.stdout || '');
314
317
  }
315
318
 
316
- function readMacCaptureHelpers() {
317
- if (!isMacPhysicalGate) return [];
318
- return parseMacHelperProcesses(readMacProcessSnapshot());
319
+ function readMacProcessResourceSnapshot() {
320
+ return new Promise((resolveSnapshot, rejectSnapshot) => {
321
+ execFile(
322
+ '/bin/ps',
323
+ ['-axo', 'pid=,ppid=,comm=,%cpu=,rss=,args='],
324
+ {
325
+ encoding: 'utf8',
326
+ timeout: 2_000,
327
+ windowsHide: true,
328
+ maxBuffer: 4 * 1024 * 1024
329
+ },
330
+ (error, stdout, stderr) => {
331
+ if (error) {
332
+ rejectSnapshot(new Error(
333
+ `resource ps failed: ${String(stderr || error.message || error)}`
334
+ ));
335
+ return;
336
+ }
337
+ resolveSnapshot(String(stdout || ''));
338
+ }
339
+ );
340
+ });
319
341
  }
320
342
 
321
343
  function readConflictingMacLiveDeskRuntimes() {
@@ -469,6 +491,10 @@ async function decodeKeyFrame(accessUnit, label) {
469
491
  windowsHide: true
470
492
  });
471
493
  ownedDecoders.set(decoder, { directory, label });
494
+ if (isMacPhysicalGate && Number(decoder?.pid || 0) > 1) {
495
+ macOwnedDecoderPids.add(Number(decoder.pid));
496
+ requestMacResourceSample('steadyCapture');
497
+ }
472
498
  let errors = '';
473
499
  let settled = false;
474
500
  let unsubscribeAbort = () => {};
@@ -566,12 +592,20 @@ let hub;
566
592
  let agent;
567
593
  let frameSocket;
568
594
  const ownedDecoders = new Map();
595
+ const macOwnedDecoderPids = new Set();
569
596
  let hubLogs = [];
570
597
  let agentLogs = [];
571
598
  let helperSampleTimer;
572
599
  let helperMaxConcurrent = 0;
573
600
  const helperObservedPids = new Set();
574
601
  let helperFinalCount = 0;
602
+ let macResourcePhase = '';
603
+ const macResourceSamples = {
604
+ baseline: [],
605
+ steadyCapture: [],
606
+ afterCleanup: []
607
+ };
608
+ const macResourceSamplingErrors = new Set();
575
609
  let macModifierProbe = { expectedAcks: 8, appliedAcks: 0, applied: [], errors: [] };
576
610
  let macInitialPixelHash = '';
577
611
  let macNextPixelHash = '';
@@ -590,12 +624,62 @@ let macRunError = '';
590
624
  const macCleanupErrors = [];
591
625
  const macSignalHandlers = new Map();
592
626
 
593
- const sampleMacHelpers = () => {
627
+ const collectMacResourceSample = async phase => {
628
+ if (!isMacPhysicalGate) {
629
+ return {
630
+ totalLiveDeskProcessCount: 0,
631
+ captureHelperCount: 0,
632
+ unownedLiveDeskPids: [],
633
+ processes: []
634
+ };
635
+ }
636
+ let sample;
637
+ try {
638
+ sample = buildMacDiagnosticResourceSample(
639
+ await readMacProcessResourceSnapshot(),
640
+ {
641
+ diagnosticPid: process.pid,
642
+ hubPid: hub?.pid,
643
+ agentPid: agent?.pid,
644
+ decoderPids: [...ownedDecoders.keys()]
645
+ .filter(decoder => decoder?.exitCode === null && decoder?.signalCode === null)
646
+ .map(decoder => decoder?.pid)
647
+ }
648
+ );
649
+ } catch {
650
+ const safePhase = String(phase || 'unassigned').replace(/[^a-z-]/gi, '') || 'unassigned';
651
+ macResourceSamplingErrors.add(`${safePhase}:unavailable`);
652
+ throw new Error(`Mac ${safePhase} process resource telemetry is unavailable.`);
653
+ }
654
+ helperMaxConcurrent = Math.max(helperMaxConcurrent, sample.captureHelperCount);
655
+ for (const helper of sample.processes.filter(record => record.role === 'capture-helper')) {
656
+ helperObservedPids.add(helper.pid);
657
+ }
658
+ if (Object.prototype.hasOwnProperty.call(macResourceSamples, phase)) {
659
+ const phaseSamples = macResourceSamples[phase];
660
+ phaseSamples.push(sample);
661
+ while (phaseSamples.length > 240) phaseSamples.shift();
662
+ }
663
+ return sample;
664
+ };
665
+
666
+ const macResourceSampler = createAsyncSingleFlightSampler(collectMacResourceSample);
667
+
668
+ const sampleMacResources = phase => macResourceSampler.run(phase);
669
+
670
+ const requestMacResourceSample = phase => {
671
+ const pending = macResourceSampler.request(phase);
672
+ pending?.catch(() => {
673
+ // A later required sample reports persistent process telemetry failure.
674
+ });
675
+ };
676
+
677
+ const sampleMacHelpers = async () => {
594
678
  if (!isMacPhysicalGate) return [];
595
- const helpers = readMacCaptureHelpers();
596
- helperMaxConcurrent = Math.max(helperMaxConcurrent, helpers.length);
597
- for (const helper of helpers) helperObservedPids.add(helper.pid);
598
- return helpers;
679
+ const sample = await sampleMacResources(macResourcePhase);
680
+ return sample.processes
681
+ .filter(record => record.role === 'capture-helper')
682
+ .map(record => ({ pid: record.pid }));
599
683
  };
600
684
 
601
685
  async function stopMacAgentAndSampleDrain() {
@@ -614,7 +698,7 @@ async function stopMacAgentAndSampleDrain() {
614
698
  } finally {
615
699
  const drainDeadline = Date.now() + 5_000;
616
700
  do {
617
- const helpers = sampleMacHelpers();
701
+ const helpers = await sampleMacHelpers();
618
702
  helperFinalCount = helpers.length;
619
703
  if (helperFinalCount === 0) break;
620
704
  await delay(100);
@@ -641,8 +725,27 @@ async function writeMacPhysicalGateReport() {
641
725
  while (true) {
642
726
  const diagnostic = readDiagnosticState();
643
727
  const stateToken = diagnosticStateToken(diagnostic);
728
+ const expectedSteadyProcesses = [
729
+ { role: 'hub', pid: hub?.pid, ownedByDiagnostic: true },
730
+ { role: 'remote-fast', pid: agent?.pid, ownedByDiagnostic: true },
731
+ ...[...macOwnedDecoderPids].map(pid => ({
732
+ role: 'decoder',
733
+ pid,
734
+ ownedByDiagnostic: true
735
+ }))
736
+ ];
737
+ const resources = {
738
+ sampleIntervalMs: MAC_PHYSICAL_RESOURCE_SAMPLE_INTERVAL_MS,
739
+ samplingErrors: [...macResourceSamplingErrors],
740
+ baseline: summarizeMacDiagnosticResourceSamples(macResourceSamples.baseline),
741
+ steadyCapture: summarizeMacDiagnosticResourceSamples(
742
+ macResourceSamples.steadyCapture,
743
+ { expectedProcesses: expectedSteadyProcesses }
744
+ ),
745
+ afterCleanup: summarizeMacDiagnosticResourceSamples(macResourceSamples.afterCleanup)
746
+ };
644
747
  snapshot = {
645
- schemaVersion: 1,
748
+ schemaVersion: 2,
646
749
  generatedAt,
647
750
  runtimeId: clientRuntimeId,
648
751
  requestedFps: 30,
@@ -665,6 +768,7 @@ async function writeMacPhysicalGateReport() {
665
768
  },
666
769
  modifier: macModifierProbe,
667
770
  transport: macTransport,
771
+ resources,
668
772
  diagnostic,
669
773
  error: diagnostic.runError
670
774
  };
@@ -688,6 +792,13 @@ async function writeMacPhysicalGateReport() {
688
792
  `Mac physical gate helper: max=${helperMaxConcurrent}, final=${helperFinalCount}, `
689
793
  + `pids=${[...helperObservedPids].join(',') || '-'}.`
690
794
  );
795
+ const resourceEvidence = snapshot.resources;
796
+ console.log(
797
+ `Mac physical gate resources: baseline=${resourceEvidence.baseline.totalLiveDeskProcessCount}, `
798
+ + `steady-peak=${resourceEvidence.steadyCapture.totalLiveDeskProcessCountPeak}, `
799
+ + `after-cleanup=${resourceEvidence.afterCleanup.totalLiveDeskProcessCount}, `
800
+ + `samples=${resourceEvidence.steadyCapture.sampleCount}.`
801
+ );
691
802
  console.log(
692
803
  `Mac physical gate monitor: count=${macMonitorCount}, generation=${macInitialGeneration}->${macNextGeneration}, `
693
804
  + `pixels=${macInitialPixelHash.slice(0, 12) || '-'}->${macNextPixelHash.slice(0, 12) || '-'}.`
@@ -766,6 +877,12 @@ async function cleanupOwnedDiagnosticResources() {
766
877
  }
767
878
  }
768
879
  if (!isMacPhysicalGate) return;
880
+ macResourcePhase = 'afterCleanup';
881
+ try {
882
+ await sampleMacResources(macResourcePhase);
883
+ } catch (error) {
884
+ recordMacCleanupError('resource-snapshot-after-cleanup', error);
885
+ }
769
886
  const teardownPlan = buildMacDiagnosticTeardownPlan({
770
887
  agentStopped,
771
888
  hubStopped,
@@ -843,14 +960,12 @@ try {
843
960
  macIsolation = await prepareMacDiagnosticIsolation();
844
961
  if (isMacPhysicalGate) {
845
962
  macDiagnosticAbort.throwIfRequested();
846
- sampleMacHelpers();
963
+ macResourcePhase = 'baseline';
964
+ await sampleMacResources(macResourcePhase);
965
+ macResourcePhase = '';
847
966
  helperSampleTimer = setInterval(() => {
848
- try {
849
- sampleMacHelpers();
850
- } catch {
851
- // The final synchronous sample reports a persistent ps failure.
852
- }
853
- }, 100);
967
+ requestMacResourceSample(macResourcePhase);
968
+ }, MAC_PHYSICAL_RESOURCE_SAMPLE_INTERVAL_MS);
854
969
  helperSampleTimer.unref?.();
855
970
  }
856
971
  const isolatedHubEnvironment = isMacPhysicalGate
@@ -989,7 +1104,11 @@ try {
989
1104
  frameSocket.on('message', handleMessage);
990
1105
  });
991
1106
 
992
- frameSocket.send(JSON.stringify({
1107
+ if (isMacPhysicalGate) {
1108
+ macResourcePhase = 'steadyCapture';
1109
+ await sampleMacResources(macResourcePhase);
1110
+ }
1111
+ frameSocket.send(JSON.stringify({
993
1112
  type: 'subscribe',
994
1113
  deviceIds: [connectedDeviceId],
995
1114
  autoStartLive: true,
package/hub/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/hub",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "LiveDesk local Hub API and browser frame bridge",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
@@ -0,0 +1,38 @@
1
+ export const LIVE_STREAM_MONITOR_INDEX_MIN = 0;
2
+ export const LIVE_STREAM_MONITOR_INDEX_MAX = 63;
3
+
4
+ export function parseExactLiveStreamMonitorIndex(value) {
5
+ return typeof value === 'number'
6
+ && Number.isInteger(value)
7
+ && value >= LIVE_STREAM_MONITOR_INDEX_MIN
8
+ && value <= LIVE_STREAM_MONITOR_INDEX_MAX
9
+ ? value
10
+ : null;
11
+ }
12
+
13
+ export function isReusedLiveStreamFrameReady(result, activeStream, expectedBinding) {
14
+ const expectedMonitorIndex = parseExactLiveStreamMonitorIndex(expectedBinding?.monitorIndex);
15
+ const activeMonitorIndex = parseExactLiveStreamMonitorIndex(activeStream?.monitorIndex);
16
+ const latestFrame = activeStream?.latestFrame;
17
+ const frameMonitorIndex = parseExactLiveStreamMonitorIndex(latestFrame?.monitorIndex);
18
+ const expectedStreamId = String(expectedBinding?.streamId || '');
19
+ const expectedCommandId = String(expectedBinding?.commandId || '');
20
+ const expectedCaptureGeneration = Number(expectedBinding?.captureGeneration || 0);
21
+
22
+ return result?.reused === true
23
+ && result?.ready === true
24
+ && Number(activeStream?.framesReceived || 0) > 0
25
+ && !!expectedStreamId
26
+ && String(activeStream?.streamId || '') === expectedStreamId
27
+ && !!expectedCommandId
28
+ && String(activeStream?.commandId || '') === expectedCommandId
29
+ && Number.isInteger(expectedCaptureGeneration)
30
+ && expectedCaptureGeneration > 0
31
+ && Number(activeStream?.captureGeneration || 0) === expectedCaptureGeneration
32
+ && latestFrame?.currentGenerationVerified === true
33
+ && String(latestFrame?.commandId || '') === expectedCommandId
34
+ && Number(latestFrame?.captureGeneration || 0) === expectedCaptureGeneration
35
+ && expectedMonitorIndex !== null
36
+ && activeMonitorIndex === expectedMonitorIndex
37
+ && frameMonitorIndex === expectedMonitorIndex;
38
+ }
@@ -2,6 +2,7 @@ import net from 'net';
2
2
  import os from 'os';
3
3
  import crypto from 'crypto';
4
4
  import { createHubRelayControl } from './transport/relay-hub-control.js';
5
+ import { parseExactLiveStreamMonitorIndex } from './live-stream-monitor-contract.js';
5
6
 
6
7
  const DEFAULT_REMOTE_HUB_PORT = 5197;
7
8
  const DEFAULT_REMOTE_HUB_HOST = '0.0.0.0';
@@ -689,21 +690,39 @@ const SUPPORTED_AGENT_OPERATIONS = new Set([
689
690
  'logs.collect'
690
691
  ]);
691
692
 
692
- function normalizeAgentOperation(value) {
693
- const operation = safeString(value, 80).toLowerCase();
694
- return SUPPORTED_AGENT_OPERATIONS.has(operation) ? operation : '';
695
- }
696
-
697
- function normalizeRemoteInputEvent(value = {}) {
698
- const input = value && typeof value === 'object' ? value : {};
699
- const type = safeString(input.type || input.Type, 48);
693
+ function normalizeAgentOperation(value) {
694
+ const operation = safeString(value, 80).toLowerCase();
695
+ return SUPPORTED_AGENT_OPERATIONS.has(operation) ? operation : '';
696
+ }
697
+
698
+ const REMOTE_INPUT_MONITOR_KEYS = Object.freeze([
699
+ 'monitorIndex',
700
+ 'MonitorIndex',
701
+ 'screenIndex',
702
+ 'ScreenIndex',
703
+ 'displayIndex',
704
+ 'DisplayIndex'
705
+ ]);
706
+
707
+ function readRawRemoteInputMonitorIndex(input) {
708
+ for (const key of REMOTE_INPUT_MONITOR_KEYS) {
709
+ if (Object.prototype.hasOwnProperty.call(input, key)) {
710
+ return input[key];
711
+ }
712
+ }
713
+ return undefined;
714
+ }
715
+
716
+ function normalizeRemoteInputEvent(value = {}) {
717
+ const input = value && typeof value === 'object' ? value : {};
718
+ const type = safeString(input.type || input.Type, 48);
700
719
  const normalizedX = Number(input.normalizedX ?? input.NormalizedX);
701
720
  const normalizedY = Number(input.normalizedY ?? input.NormalizedY);
702
721
  const deltaX = Number(input.deltaX ?? input.DeltaX);
703
722
  const deltaY = Number(input.deltaY ?? input.DeltaY);
704
723
  const keyCode = Number(input.keyCode ?? input.KeyCode);
705
724
  const location = Number(input.location ?? input.Location);
706
- const monitorIndex = Number(input.monitorIndex ?? input.MonitorIndex ?? input.screenIndex ?? input.ScreenIndex ?? input.displayIndex ?? input.DisplayIndex);
725
+ const monitorIndex = parseExactLiveStreamMonitorIndex(readRawRemoteInputMonitorIndex(input));
707
726
  const inputSeq = Number(input.inputSeq ?? input.InputSeq);
708
727
  const issuedAtEpochMs = Number(input.issuedAtEpochMs ?? input.IssuedAtEpochMs);
709
728
  const hubReceivedAtEpochMs = Number(input.hubReceivedAtEpochMs ?? input.HubReceivedAtEpochMs);
@@ -719,7 +738,7 @@ function normalizeRemoteInputEvent(value = {}) {
719
738
  type,
720
739
  // A missing monitor is not equivalent to the primary display. Control
721
740
  // input must prove the exact monitor owned by its capture generation.
722
- monitorIndex: Number.isFinite(monitorIndex) ? normalizeMonitorIndex(monitorIndex) : null,
741
+ monitorIndex,
723
742
  normalizedX: Number.isFinite(normalizedX) ? Math.max(0, Math.min(1, normalizedX)) : undefined,
724
743
  normalizedY: Number.isFinite(normalizedY) ? Math.max(0, Math.min(1, normalizedY)) : undefined,
725
744
  button: safeString(input.button || input.Button, 24),
@@ -3983,6 +4002,34 @@ export function createRemoteHub(options = {}) {
3983
4002
  });
3984
4003
  return null;
3985
4004
  }
4005
+ const pendingMonitorIndex = parseExactLiveStreamMonitorIndex(pending.monitorIndex);
4006
+ const messageMonitorIndex = parseExactLiveStreamMonitorIndex(message.monitorIndex);
4007
+ if (pendingMonitorIndex === null || messageMonitorIndex === null) {
4008
+ emitRemoteEvent('RemoteLiveStreamOpenIgnored', device, {
4009
+ reason: 'invalid-pending-monitor-metadata',
4010
+ streamId: streamState.streamId,
4011
+ commandId,
4012
+ captureGeneration: messageCaptureGeneration,
4013
+ pendingCaptureGeneration,
4014
+ monitorIndex: messageMonitorIndex,
4015
+ pendingMonitorIndex,
4016
+ transport
4017
+ });
4018
+ return null;
4019
+ }
4020
+ if (messageMonitorIndex !== pendingMonitorIndex) {
4021
+ emitRemoteEvent('RemoteLiveStreamOpenIgnored', device, {
4022
+ reason: 'stale-pending-monitor',
4023
+ streamId: streamState.streamId,
4024
+ commandId,
4025
+ captureGeneration: messageCaptureGeneration,
4026
+ pendingCaptureGeneration,
4027
+ monitorIndex: messageMonitorIndex,
4028
+ pendingMonitorIndex,
4029
+ transport
4030
+ });
4031
+ return null;
4032
+ }
3986
4033
 
3987
4034
  const previousCommandId = safeString(streamState.commandId, 128);
3988
4035
  const transfer = buildRemoteFrameTransferDescriptorFromMessage(
@@ -4011,9 +4058,7 @@ export function createRemoteHub(options = {}) {
4011
4058
  height: Number.isFinite(Number(message.height)) ? Number(message.height) : Number(pending.height || 0),
4012
4059
  sourceWidth: Number.isFinite(Number(message.sourceWidth)) ? Number(message.sourceWidth) : Number(pending.sourceWidth || 0),
4013
4060
  sourceHeight: Number.isFinite(Number(message.sourceHeight)) ? Number(message.sourceHeight) : Number(pending.sourceHeight || 0),
4014
- monitorIndex: Number.isFinite(Number(message.monitorIndex))
4015
- ? normalizeMonitorIndex(message.monitorIndex)
4016
- : Number(pending.monitorIndex || 0),
4061
+ monitorIndex: pendingMonitorIndex,
4017
4062
  monitorCount: Number.isFinite(Number(message.monitorCount))
4018
4063
  ? clampNumber(message.monitorCount, 1, 64, 1)
4019
4064
  : Number(pending.monitorCount || 1),
@@ -4106,6 +4151,37 @@ export function createRemoteHub(options = {}) {
4106
4151
  return true;
4107
4152
  }
4108
4153
 
4154
+ const activeMonitorIndex = parseExactLiveStreamMonitorIndex(streamState.monitorIndex);
4155
+ const messageMonitorIndex = parseExactLiveStreamMonitorIndex(message.monitorIndex);
4156
+ if (activeMonitorIndex === null || messageMonitorIndex === null) {
4157
+ emitRemoteEvent('RemoteLiveStreamOpenIgnored', device, {
4158
+ reason: 'invalid-live-stream-monitor-metadata',
4159
+ streamId,
4160
+ commandId,
4161
+ activeCommandId,
4162
+ captureGeneration: Number(message.captureGeneration || 0),
4163
+ activeCaptureGeneration: Number(streamState.captureGeneration || 0),
4164
+ monitorIndex: messageMonitorIndex,
4165
+ activeMonitorIndex,
4166
+ transport
4167
+ });
4168
+ return false;
4169
+ }
4170
+ if (messageMonitorIndex !== activeMonitorIndex) {
4171
+ emitRemoteEvent('RemoteLiveStreamOpenIgnored', device, {
4172
+ reason: 'stale-live-stream-monitor',
4173
+ streamId,
4174
+ commandId,
4175
+ activeCommandId,
4176
+ captureGeneration: Number(message.captureGeneration || 0),
4177
+ activeCaptureGeneration: Number(streamState.captureGeneration || 0),
4178
+ monitorIndex: messageMonitorIndex,
4179
+ activeMonitorIndex,
4180
+ transport
4181
+ });
4182
+ return false;
4183
+ }
4184
+
4109
4185
  const transfer = buildRemoteFrameTransferDescriptorFromMessage(message, streamState.mode || DEFAULT_REMOTE_FRAME_MODE);
4110
4186
  const openedAt = safeString(message.openedAt, 80) || device.lastSeenAt || new Date().toISOString();
4111
4187
  const nextStreamState = {
@@ -4126,7 +4202,7 @@ export function createRemoteHub(options = {}) {
4126
4202
  height: Number.isFinite(Number(message.height)) ? Number(message.height) : Number(streamState.height || 0),
4127
4203
  sourceWidth: Number.isFinite(Number(message.sourceWidth)) ? Number(message.sourceWidth) : Number(streamState.sourceWidth || 0),
4128
4204
  sourceHeight: Number.isFinite(Number(message.sourceHeight)) ? Number(message.sourceHeight) : Number(streamState.sourceHeight || 0),
4129
- monitorIndex: Number.isFinite(Number(message.monitorIndex)) ? normalizeMonitorIndex(message.monitorIndex) : Number(streamState.monitorIndex || 0),
4205
+ monitorIndex: activeMonitorIndex,
4130
4206
  monitorCount: Number.isFinite(Number(message.monitorCount)) ? clampNumber(message.monitorCount, 1, 64, 1) : Number(streamState.monitorCount || 1),
4131
4207
  streamPurpose: safeString(message.streamPurpose || streamState.streamPurpose, 24) || 'wall',
4132
4208
  captureGeneration: Number.isSafeInteger(Number(message.captureGeneration)) && Number(message.captureGeneration) > 0
@@ -4204,8 +4280,40 @@ export function createRemoteHub(options = {}) {
4204
4280
  });
4205
4281
  return false;
4206
4282
  }
4207
-
4208
- const byteLength = normalizeFrameByteLength(framePayload, frameData);
4283
+ const activeMonitorIndex = parseExactLiveStreamMonitorIndex(streamState.monitorIndex);
4284
+ const frameMonitorIndex = parseExactLiveStreamMonitorIndex(message.monitorIndex);
4285
+ if (activeMonitorIndex === null || frameMonitorIndex === null) {
4286
+ device.counters.liveFramesDropped += 1;
4287
+ emitRemoteEvent('RemoteFrameDropped', device, {
4288
+ reason: 'invalid-live-stream-monitor-frame-metadata',
4289
+ streamId,
4290
+ commandId,
4291
+ captureGeneration: frameCaptureGeneration,
4292
+ activeCaptureGeneration,
4293
+ monitorIndex: frameMonitorIndex,
4294
+ activeMonitorIndex,
4295
+ frameSeq: Number.isFinite(frameSeq) ? frameSeq : null,
4296
+ transport
4297
+ });
4298
+ return false;
4299
+ }
4300
+ if (frameMonitorIndex !== activeMonitorIndex) {
4301
+ device.counters.liveFramesDropped += 1;
4302
+ emitRemoteEvent('RemoteFrameDropped', device, {
4303
+ reason: 'stale-live-stream-monitor-frame',
4304
+ streamId,
4305
+ commandId,
4306
+ captureGeneration: frameCaptureGeneration,
4307
+ activeCaptureGeneration,
4308
+ monitorIndex: frameMonitorIndex,
4309
+ activeMonitorIndex,
4310
+ frameSeq: Number.isFinite(frameSeq) ? frameSeq : null,
4311
+ transport
4312
+ });
4313
+ return false;
4314
+ }
4315
+
4316
+ const byteLength = normalizeFrameByteLength(framePayload, frameData);
4209
4317
  if ((!Buffer.isBuffer(framePayload) && !frameData)
4210
4318
  || byteLength > MAX_STREAM_BINARY_BYTES
4211
4319
  || !Number.isFinite(frameSeq)) {
@@ -4242,7 +4350,8 @@ export function createRemoteHub(options = {}) {
4242
4350
  const currentGenerationVerified = (!activeCommandId || commandId === activeCommandId)
4243
4351
  && (activeCaptureGeneration <= 0
4244
4352
  || (frameCaptureGeneration > 0
4245
- && frameCaptureGeneration === activeCaptureGeneration));
4353
+ && frameCaptureGeneration === activeCaptureGeneration))
4354
+ && frameMonitorIndex === activeMonitorIndex;
4246
4355
  const readyFrameReceivedBeforeFrame = streamState.readyFrameReceived === true;
4247
4356
  const captureTimingAvailable = hasFrameCaptureTiming(message);
4248
4357
  const videoTransport = transport === 'udp-p2p'
@@ -4323,7 +4432,7 @@ export function createRemoteHub(options = {}) {
4323
4432
  udpIncompleteFrames: Number.isFinite(Number(message.udpIncompleteFrames)) ? Number(message.udpIncompleteFrames) : 0,
4324
4433
  hardwareEncoder: safeString(message.hardwareEncoder, 80),
4325
4434
  platformProfile: safeString(message.platformProfile, 80),
4326
- monitorIndex: Number.isFinite(Number(message.monitorIndex)) ? normalizeMonitorIndex(message.monitorIndex) : Number(streamState.monitorIndex || 0),
4435
+ monitorIndex: activeMonitorIndex,
4327
4436
  monitorCount: Number.isFinite(Number(message.monitorCount)) ? clampNumber(message.monitorCount, 1, 64, 1) : Number(streamState.monitorCount || 1),
4328
4437
  transferProtocol: transfer.transferProtocol,
4329
4438
  transferProtocolVersion: transfer.transferProtocolVersion,
@@ -4402,7 +4511,6 @@ export function createRemoteHub(options = {}) {
4402
4511
  streamState.height = device.latestLiveFrame.height;
4403
4512
  streamState.sourceWidth = device.latestLiveFrame.sourceWidth;
4404
4513
  streamState.sourceHeight = device.latestLiveFrame.sourceHeight;
4405
- streamState.monitorIndex = device.latestLiveFrame.monitorIndex;
4406
4514
  streamState.monitorCount = device.latestLiveFrame.monitorCount;
4407
4515
  ensureDeviceLiveStreams(device).set(streamId, streamState);
4408
4516
  device.counters.liveFramesReceived += 1;
@@ -6457,8 +6565,12 @@ export function createRemoteHub(options = {}) {
6457
6565
  }
6458
6566
  const activeCaptureGeneration = Number(activeLiveStream.captureGeneration || 0);
6459
6567
  const frameCaptureGeneration = Number(latestFrame.captureGeneration || 0);
6460
- return activeCaptureGeneration <= 0
6461
- || (frameCaptureGeneration > 0 && frameCaptureGeneration === activeCaptureGeneration);
6568
+ const activeMonitorIndex = parseExactLiveStreamMonitorIndex(activeLiveStream.monitorIndex);
6569
+ const frameMonitorIndex = parseExactLiveStreamMonitorIndex(latestFrame.monitorIndex);
6570
+ return activeMonitorIndex !== null
6571
+ && frameMonitorIndex === activeMonitorIndex
6572
+ && (activeCaptureGeneration <= 0
6573
+ || (frameCaptureGeneration > 0 && frameCaptureGeneration === activeCaptureGeneration));
6462
6574
  }
6463
6575
 
6464
6576
  function getLiveStreamFreshWindowMs(activeLiveStream) {
package/hub/src/server.js CHANGED
@@ -7,8 +7,9 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, wri
7
7
  import { dirname, resolve } from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import os from 'node:os';
10
- import { WebSocketServer } from 'ws';
10
+ import { WebSocketServer } from 'ws';
11
11
  import { createRemoteHub } from './remote-hub.js';
12
+ import { isReusedLiveStreamFrameReady } from './live-stream-monitor-contract.js';
12
13
  import { buildMode4AtlasSessionKey, Mode4AtlasPool, planMode4AtlasInputTransitions } from './mode4-atlas-pool.js';
13
14
  import { resolveMode4AtlasTileSize } from './mode4-atlas-sizing.js';
14
15
  import { createHubFilesystem } from './filesystem/hub-filesystem.js';
@@ -1976,12 +1977,7 @@ function startFrameSubscriptionLive(ws, reason = 'subscribe', onlyDeviceId = '',
1976
1977
  readySent: false
1977
1978
  };
1978
1979
  const activeStream = device?.activeLiveStream;
1979
- const reusedFrameReady = result.reused === true
1980
- && result.ready === true
1981
- && Number(result.framesReceived ?? activeStream?.framesReceived ?? 0) > 0
1982
- && String(activeStream?.streamId || expectedBinding.streamId) === expectedBinding.streamId
1983
- && String(activeStream?.commandId || expectedBinding.commandId) === expectedBinding.commandId
1984
- && Number(activeStream?.captureGeneration || expectedBinding.captureGeneration) === expectedBinding.captureGeneration;
1980
+ const reusedFrameReady = isReusedLiveStreamFrameReady(result, activeStream, expectedBinding);
1985
1981
  expectedBinding.readySent = reusedFrameReady;
1986
1982
  ws.liveDeskStreamIdsByDeviceId.set(deviceId, result.streamId);
1987
1983
  ws.liveDeskExpectedStreamBindingsByDeviceId.set(deviceId, expectedBinding);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.456",
4
- "livedeskClientVersion": "0.1.211",
3
+ "version": "0.1.458",
4
+ "livedeskClientVersion": "0.1.212",
5
5
  "buildFlavor": "production",
6
6
  "description": "LiveDesk Hub and client launcher",
7
7
  "type": "module",
@@ -50,10 +50,10 @@
50
50
  "ws": "^8.18.3"
51
51
  },
52
52
  "optionalDependencies": {
53
- "@livedesk/fast-linux-x64": "0.1.415",
54
- "@livedesk/fast-osx-arm64": "0.1.415",
55
- "@livedesk/fast-osx-x64": "0.1.415",
56
- "@livedesk/fast-win-x64": "0.1.415"
53
+ "@livedesk/fast-linux-x64": "0.1.416",
54
+ "@livedesk/fast-osx-arm64": "0.1.416",
55
+ "@livedesk/fast-osx-x64": "0.1.416",
56
+ "@livedesk/fast-win-x64": "0.1.416"
57
57
  },
58
58
  "publishConfig": {
59
59
  "access": "public"