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.
- package/client/package.json +5 -5
- package/diagnostics/livedesk-mac-physical-gate-core.mjs +389 -36
- package/diagnostics/livedesk-mode3-capture-smoke.mjs +140 -21
- package/hub/package.json +1 -1
- package/hub/src/live-stream-monitor-contract.js +38 -0
- package/hub/src/remote-hub.js +133 -21
- package/hub/src/server.js +3 -7
- package/package.json +6 -6
- package/web/dist/assets/index-JqqQ5DKN.js +25 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/index-Bv9NFUjC.js +0 -25
|
@@ -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
|
-
|
|
22
|
-
|
|
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
|
|
317
|
-
|
|
318
|
-
|
|
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
|
|
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
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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:
|
|
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
|
-
|
|
963
|
+
macResourcePhase = 'baseline';
|
|
964
|
+
await sampleMacResources(macResourcePhase);
|
|
965
|
+
macResourcePhase = '';
|
|
847
966
|
helperSampleTimer = setInterval(() => {
|
|
848
|
-
|
|
849
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
+
}
|
package/hub/src/remote-hub.js
CHANGED
|
@@ -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
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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 =
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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
|
-
|
|
6461
|
-
|
|
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
|
|
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.
|
|
4
|
-
"livedeskClientVersion": "0.1.
|
|
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.
|
|
54
|
-
"@livedesk/fast-osx-arm64": "0.1.
|
|
55
|
-
"@livedesk/fast-osx-x64": "0.1.
|
|
56
|
-
"@livedesk/fast-win-x64": "0.1.
|
|
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"
|