livedesk 0.1.455 → 0.1.456
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/README.md +31 -4
- package/bin/livedesk.js +98 -18
- package/client/bin/livedesk-client.js +2 -2
- package/client/package.json +22 -22
- package/diagnostics/livedesk-mac-physical-gate-core.mjs +365 -0
- package/diagnostics/livedesk-mac-physical-isolation.mjs +311 -0
- package/diagnostics/livedesk-mode3-capture-smoke.mjs +1393 -0
- package/hub/package.json +1 -1
- package/hub/src/server.js +6 -5
- package/package.json +12 -11
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ Pass `--no-clean` to disable the startup cleanup explicitly. If a child Hub
|
|
|
51
51
|
still loses a startup race with `EADDRINUSE`, the launcher performs one cleanup
|
|
52
52
|
and startup retry, then exits without looping.
|
|
53
53
|
|
|
54
|
-
## Client compatibility
|
|
54
|
+
## Client compatibility
|
|
55
55
|
|
|
56
56
|
```powershell
|
|
57
57
|
npx -y --prefer-online livedesk@latest client 3
|
|
@@ -61,9 +61,36 @@ The `client` form remains as a legacy compatibility alias. It records the
|
|
|
61
61
|
Client role and uses the same unified runtime lock. The client signs in with
|
|
62
62
|
Google, discovers the active Hub, and connects to the wall. On Windows, enable
|
|
63
63
|
**Start with Windows** on the connection page to reconnect automatically after
|
|
64
|
-
reboot; the generated startup command uses the unified launcher.
|
|
65
|
-
|
|
66
|
-
##
|
|
64
|
+
reboot; the generated startup command uses the unified launcher.
|
|
65
|
+
|
|
66
|
+
## macOS physical diagnostic
|
|
67
|
+
|
|
68
|
+
On a Mac with two connected displays, stop every running LiveDesk runtime,
|
|
69
|
+
show visibly different content on the two displays, grant Screen Recording and
|
|
70
|
+
Accessibility permissions, then run:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npx -y --prefer-online livedesk@latest diagnose mac-physical
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
This command branches before role resolution, state migration, and the normal
|
|
77
|
+
runtime lock. It refuses to overlap an existing LiveDesk runtime or capture
|
|
78
|
+
helper, and normal macOS startup reciprocally refuses an active diagnostic
|
|
79
|
+
lease. It starts only an isolated loopback Hub and temporary RemoteFast Client
|
|
80
|
+
and stops only the exact children it created. Ctrl+C and termination signals
|
|
81
|
+
join the same owned cleanup before exit. The default JSON report is written to
|
|
82
|
+
one final path after cleanup, and run/cleanup errors always produce a failed
|
|
83
|
+
result. It is written under `~/.livedesk/diagnostics`. To select a path relative
|
|
84
|
+
to the directory where the command was invoked:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npx -y --prefer-online livedesk@latest diagnose mac-physical --report ./mac-result.json
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Use `diagnose mac-physical --help` to inspect requirements without starting a
|
|
91
|
+
runtime or changing cached role/account state.
|
|
92
|
+
|
|
93
|
+
## Frame modes
|
|
67
94
|
|
|
68
95
|
- Mode 2: independent 320x180 RGB565+LZO tiles for large, stable walls.
|
|
69
96
|
- Mode 3: direct hardware H.264 for focused remote control up to 4K.
|
package/bin/livedesk.js
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import { runLegacyClientUpdateSupervisorCli } from '../bootstrap/legacy-client-update.js';
|
|
35
35
|
import { transferSavedClientSessionToHub } from '../bootstrap/hub-auth-handoff.js';
|
|
36
36
|
import { readWindowsOwnedProcessManifest } from '../client/src/runtime/windows-owned-process-manifest.js';
|
|
37
|
+
import { inspectMacDiagnosticLease } from '../diagnostics/livedesk-mac-physical-isolation.mjs';
|
|
37
38
|
|
|
38
39
|
const require = createRequire(import.meta.url);
|
|
39
40
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -143,6 +144,22 @@ function isPidAlive(pid) {
|
|
|
143
144
|
}
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
async function assertNoActiveMacPhysicalDiagnosticLease() {
|
|
148
|
+
if (os.platform() !== 'darwin') return;
|
|
149
|
+
const lease = await inspectMacDiagnosticLease();
|
|
150
|
+
if (lease.status === 'missing' || lease.status === 'stale') return;
|
|
151
|
+
if (lease.status === 'active') {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Mac physical diagnostic is running (pid ${lease.pid}); normal LiveDesk startup was not started. `
|
|
154
|
+
+ 'Wait for its owned cleanup to finish or stop the diagnostic with Ctrl+C.'
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
throw new Error(
|
|
158
|
+
`Mac physical diagnostic ownership is unreadable at ${lease.path}; normal LiveDesk startup was not started `
|
|
159
|
+
+ 'to avoid overlapping a capture runtime.'
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
146
163
|
function canClaimHubUpdateResult(current, next) {
|
|
147
164
|
const currentOperationId = String(current?.operationId || '');
|
|
148
165
|
const nextOperationId = String(next?.operationId || '');
|
|
@@ -312,15 +329,18 @@ function printHelp() {
|
|
|
312
329
|
process.stdout.write(`
|
|
313
330
|
LiveDesk
|
|
314
331
|
|
|
315
|
-
Usage:
|
|
316
|
-
npx -y --prefer-online livedesk@latest
|
|
317
|
-
npx -y --prefer-online livedesk@latest --force-role hub
|
|
318
|
-
npx -y --prefer-online livedesk@latest --force-role client
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
332
|
+
Usage:
|
|
333
|
+
npx -y --prefer-online livedesk@latest
|
|
334
|
+
npx -y --prefer-online livedesk@latest --force-role hub
|
|
335
|
+
npx -y --prefer-online livedesk@latest --force-role client
|
|
336
|
+
npx -y --prefer-online livedesk@latest diagnose mac-physical
|
|
337
|
+
|
|
338
|
+
Commands:
|
|
339
|
+
No command Resolve the signed-in device role and start the matching runtime.
|
|
340
|
+
hub Legacy alias for --force-role hub.
|
|
341
|
+
client Legacy alias for --force-role client.
|
|
342
|
+
diagnose mac-physical
|
|
343
|
+
Run the isolated two-display macOS capture/control physical gate.
|
|
324
344
|
|
|
325
345
|
Hub options:
|
|
326
346
|
--no-open Do not open the browser automatically.
|
|
@@ -342,10 +362,63 @@ Common:
|
|
|
342
362
|
Do not ask for a role on first run; fail if no role is known.
|
|
343
363
|
--help Show this help.
|
|
344
364
|
--version Show version.
|
|
345
|
-
`.trimStart());
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function
|
|
365
|
+
`.trimStart());
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function printMacPhysicalDiagnosticHelp() {
|
|
369
|
+
process.stdout.write(`
|
|
370
|
+
LiveDesk macOS physical diagnostic
|
|
371
|
+
|
|
372
|
+
Usage:
|
|
373
|
+
npx -y --prefer-online livedesk@latest diagnose mac-physical
|
|
374
|
+
npx -y --prefer-online livedesk@latest diagnose mac-physical --report ./mac-result.json
|
|
375
|
+
|
|
376
|
+
Requirements:
|
|
377
|
+
macOS with two connected displays showing visibly different content.
|
|
378
|
+
Screen Recording permission for the terminal/LiveDesk capture helper.
|
|
379
|
+
Accessibility permission for the terminal/LiveDesk input helper.
|
|
380
|
+
Stop every existing LiveDesk Hub, Client, and desktop runtime first.
|
|
381
|
+
|
|
382
|
+
Behavior:
|
|
383
|
+
Starts one isolated loopback Hub and one temporary RemoteFast Client.
|
|
384
|
+
Refuses to overlap a running LiveDesk runtime or capture helper.
|
|
385
|
+
Stops only the exact child processes created by this diagnostic.
|
|
386
|
+
Writes the default report under ~/.livedesk/diagnostics.
|
|
387
|
+
|
|
388
|
+
Options:
|
|
389
|
+
--report <path> Write the JSON report relative to the invocation directory.
|
|
390
|
+
--monitor <n> Initial zero-based display index. Default: 0.
|
|
391
|
+
--width <px> Requested capture width. Default: 1920.
|
|
392
|
+
--height <px> Requested capture height. Default: 1080.
|
|
393
|
+
--help Show this help without resolving a role or acquiring a runtime lock.
|
|
394
|
+
`.trimStart());
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function runDiagnosticCommand(args) {
|
|
398
|
+
const name = String(args[0] || '').trim().toLowerCase();
|
|
399
|
+
if (!name || name === '--help' || name === '-h' || name === 'help') {
|
|
400
|
+
printMacPhysicalDiagnosticHelp();
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (name !== 'mac-physical') {
|
|
404
|
+
throw new Error(`Unknown LiveDesk diagnostic: ${name}. Supported diagnostic: mac-physical.`);
|
|
405
|
+
}
|
|
406
|
+
if (args.slice(1).some(argument => ['--help', '-h', 'help'].includes(String(argument).toLowerCase()))) {
|
|
407
|
+
printMacPhysicalDiagnosticHelp();
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const diagnosticEntry = resolve(
|
|
411
|
+
packageRoot,
|
|
412
|
+
'diagnostics',
|
|
413
|
+
'livedesk-mode3-capture-smoke.mjs'
|
|
414
|
+
);
|
|
415
|
+
if (!existsSync(diagnosticEntry)) {
|
|
416
|
+
throw new Error(`LiveDesk mac-physical diagnostic is missing from this package: ${diagnosticEntry}`);
|
|
417
|
+
}
|
|
418
|
+
await import(pathToFileURL(diagnosticEntry).href);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function printClientHelp() {
|
|
349
422
|
process.stdout.write(`
|
|
350
423
|
LiveDesk Client
|
|
351
424
|
|
|
@@ -2847,8 +2920,14 @@ async function runDesktopRoleBootstrap() {
|
|
|
2847
2920
|
|
|
2848
2921
|
async function main() {
|
|
2849
2922
|
const rawArgs = process.argv.slice(2);
|
|
2923
|
+
const rawCommand = String(rawArgs[0] || '').trim().toLowerCase();
|
|
2924
|
+
if (rawCommand === 'diagnose') {
|
|
2925
|
+
await runDiagnosticCommand(rawArgs.slice(1));
|
|
2926
|
+
return;
|
|
2927
|
+
}
|
|
2850
2928
|
restoreUpdateOriginalCwd();
|
|
2851
2929
|
if (rawArgs.includes('--internal-legacy-client-update')) {
|
|
2930
|
+
await assertNoActiveMacPhysicalDiagnosticLease();
|
|
2852
2931
|
await runLegacyClientUpdateSupervisorCli();
|
|
2853
2932
|
return;
|
|
2854
2933
|
}
|
|
@@ -2870,11 +2949,12 @@ async function main() {
|
|
|
2870
2949
|
printHelp();
|
|
2871
2950
|
return;
|
|
2872
2951
|
}
|
|
2873
|
-
if (command === '--version' || command === '-v' || command === 'version') {
|
|
2874
|
-
console.log(readVersion());
|
|
2875
|
-
return;
|
|
2876
|
-
}
|
|
2877
|
-
|
|
2952
|
+
if (command === '--version' || command === '-v' || command === 'version') {
|
|
2953
|
+
console.log(readVersion());
|
|
2954
|
+
return;
|
|
2955
|
+
}
|
|
2956
|
+
await assertNoActiveMacPhysicalDiagnosticLease();
|
|
2957
|
+
const explicitRoleCommand = command === 'hub' || command === 'manager' || command === 'client';
|
|
2878
2958
|
let resolvedRole;
|
|
2879
2959
|
let runtimeArgs = args;
|
|
2880
2960
|
if (explicitRoleCommand) {
|
|
@@ -4795,8 +4795,8 @@ export async function runClientRuntime(argv = process.argv.slice(2)) {
|
|
|
4795
4795
|
const parsed = parseLauncherArgs(argv);
|
|
4796
4796
|
if (parsed.retiredModelOptionsDiscarded > 0) {
|
|
4797
4797
|
console.warn(
|
|
4798
|
-
'[LiveDesk Client] Ignored retired Client
|
|
4799
|
-
+ '
|
|
4798
|
+
'[LiveDesk Client] Ignored retired Client options. '
|
|
4799
|
+
+ 'Computer commands are handled by the Hub Codex Agent.'
|
|
4800
4800
|
);
|
|
4801
4801
|
}
|
|
4802
4802
|
if (parsed.updateWaitPid) {
|
package/client/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livedesk/client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.211",
|
|
4
4
|
"description": "LiveDesk local remote client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
"livedesk-client-node": "bin/livedesk-client-node.js",
|
|
10
10
|
"livedesk-client-fast": "bin/livedesk-client-fast.js"
|
|
11
11
|
},
|
|
12
|
-
"files": [
|
|
13
|
-
"bin/",
|
|
14
|
-
"src/",
|
|
15
|
-
"README.md",
|
|
16
|
-
"THIRD_PARTY_NOTICES.md"
|
|
17
|
-
],
|
|
12
|
+
"files": [
|
|
13
|
+
"bin/",
|
|
14
|
+
"src/",
|
|
15
|
+
"README.md",
|
|
16
|
+
"THIRD_PARTY_NOTICES.md"
|
|
17
|
+
],
|
|
18
18
|
"scripts": {
|
|
19
19
|
"check": "node --check bin/client-version.js && node --check bin/livedesk-client.js && node --check bin/livedesk-client-node.js && node --check bin/livedesk-client-fast.js",
|
|
20
20
|
"test:version": "node --test tests/client-version.test.mjs",
|
|
@@ -31,21 +31,21 @@
|
|
|
31
31
|
"engines": {
|
|
32
32
|
"node": ">=20"
|
|
33
33
|
},
|
|
34
|
-
"dependencies": {
|
|
35
|
-
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
|
36
|
-
"ffmpeg-static": "^5.3.0",
|
|
37
|
-
"@livedesk/runtime-core": "0.1.0",
|
|
38
|
-
"@supabase/supabase-js": "^2.110.0",
|
|
39
|
-
"node-screenshots": "^0.2.8",
|
|
40
|
-
"ws": "^8.18.3"
|
|
41
|
-
},
|
|
42
|
-
"optionalDependencies": {
|
|
43
|
-
"@livedesk/fast-linux-x64": "0.1.415",
|
|
44
|
-
"@livedesk/fast-osx-arm64": "0.1.415",
|
|
45
|
-
"@livedesk/fast-osx-x64": "0.1.415",
|
|
46
|
-
"@livedesk/fast-win-x64": "0.1.415"
|
|
47
|
-
},
|
|
48
|
-
"publishConfig": {
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
|
36
|
+
"ffmpeg-static": "^5.3.0",
|
|
37
|
+
"@livedesk/runtime-core": "0.1.0",
|
|
38
|
+
"@supabase/supabase-js": "^2.110.0",
|
|
39
|
+
"node-screenshots": "^0.2.8",
|
|
40
|
+
"ws": "^8.18.3"
|
|
41
|
+
},
|
|
42
|
+
"optionalDependencies": {
|
|
43
|
+
"@livedesk/fast-linux-x64": "0.1.415",
|
|
44
|
+
"@livedesk/fast-osx-arm64": "0.1.415",
|
|
45
|
+
"@livedesk/fast-osx-x64": "0.1.415",
|
|
46
|
+
"@livedesk/fast-win-x64": "0.1.415"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
49
|
"access": "public"
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
const MAC_VIDEO_HELPER_NAME = 'livedesk-sck-h264';
|
|
4
|
+
const MAC_AUDIO_HELPER_NAME = 'livedesk-sck-audio';
|
|
5
|
+
|
|
6
|
+
function normalizeProcessText(value) {
|
|
7
|
+
return String(value || '').trim().replaceAll('\\', '/');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function parseMacProcessRecords(psOutput) {
|
|
11
|
+
const records = [];
|
|
12
|
+
for (const rawLine of String(psOutput || '').split(/\r?\n/)) {
|
|
13
|
+
const match = rawLine.match(/^\s*(\d+)\s+(\d+)\s+(\S+)\s*(.*)$/);
|
|
14
|
+
if (!match) continue;
|
|
15
|
+
const pid = Number(match[1]);
|
|
16
|
+
const parentPid = Number(match[2]);
|
|
17
|
+
if (!Number.isInteger(pid) || pid <= 1 || !Number.isInteger(parentPid)) continue;
|
|
18
|
+
records.push({
|
|
19
|
+
pid,
|
|
20
|
+
parentPid,
|
|
21
|
+
commandName: basenameWithoutQuotes(match[3]),
|
|
22
|
+
commandLine: String(match[4] || '').trim()
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return records;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isMacCaptureHelperRecord(record) {
|
|
29
|
+
const commandName = String(record?.commandName || '');
|
|
30
|
+
const commandLine = normalizeProcessText(record?.commandLine);
|
|
31
|
+
const executable = basenameWithoutQuotes(commandLine.split(/\s+/)[0]);
|
|
32
|
+
const videoPath = /(?:^|\/)\.livedesk\/helpers\/macos-sck-h264\/livedesk-sck-h264(?:\s|$)/i;
|
|
33
|
+
const audioPath = /(?:^|\/)\.livedesk\/helpers\/macos-sck-audio\/livedesk-sck-audio(?:\s|$)/i;
|
|
34
|
+
return commandName === MAC_VIDEO_HELPER_NAME
|
|
35
|
+
|| commandName === 'livedesk-sck-h2'
|
|
36
|
+
|| commandName === MAC_AUDIO_HELPER_NAME
|
|
37
|
+
|| commandName === 'livedesk-sck-aud'
|
|
38
|
+
|| executable === MAC_VIDEO_HELPER_NAME
|
|
39
|
+
|| executable === MAC_AUDIO_HELPER_NAME
|
|
40
|
+
|| videoPath.test(commandLine)
|
|
41
|
+
|| audioPath.test(commandLine);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function finite(value, fallback = 0) {
|
|
45
|
+
const parsed = Number(value);
|
|
46
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function basenameWithoutQuotes(value) {
|
|
50
|
+
return path.basename(String(value || '').trim().replace(/^["']|["']$/g, ''));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function parseMacHelperProcesses(psOutput) {
|
|
54
|
+
return parseMacProcessRecords(psOutput).filter(isMacCaptureHelperRecord);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function parseMacLiveDeskRuntimeProcesses(psOutput, { ignorePids = [] } = {}) {
|
|
58
|
+
const ignored = new Set(
|
|
59
|
+
(Array.isArray(ignorePids) ? ignorePids : [])
|
|
60
|
+
.map(Number)
|
|
61
|
+
.filter(pid => Number.isInteger(pid) && pid > 1)
|
|
62
|
+
);
|
|
63
|
+
const results = [];
|
|
64
|
+
for (const record of parseMacProcessRecords(psOutput)) {
|
|
65
|
+
if (ignored.has(record.pid)) continue;
|
|
66
|
+
const commandName = String(record.commandName || '');
|
|
67
|
+
const commandLine = normalizeProcessText(record.commandLine);
|
|
68
|
+
let kind = '';
|
|
69
|
+
if (isMacCaptureHelperRecord(record)) {
|
|
70
|
+
kind = 'capture-helper';
|
|
71
|
+
} else if (
|
|
72
|
+
/^livedesk-client-fast$/i.test(commandName)
|
|
73
|
+
|| /(?:^|\/|[\s"'])livedesk-client-fast(?:\.dll)?(?:\s|$|["'])/i.test(commandLine)
|
|
74
|
+
) {
|
|
75
|
+
kind = 'client-agent';
|
|
76
|
+
} else if (
|
|
77
|
+
/(?:node_modules\/(?:livedesk\/client|@livedesk\/client)|packages\/(?:livedesk\/client|client))\/bin\/livedesk-client(?:-node)?\.js(?:\s|$|["'])/i
|
|
78
|
+
.test(commandLine)
|
|
79
|
+
) {
|
|
80
|
+
kind = 'client-runtime';
|
|
81
|
+
} else if (
|
|
82
|
+
/(?:node_modules\/livedesk|packages\/livedesk)\/bin\/livedesk\.js(?:\s|$|["'])/i
|
|
83
|
+
.test(commandLine)
|
|
84
|
+
|| /(?:^|[\s"'])[^"'\s]*node_modules\/\.bin\/livedesk(?:\s|$|["'])/i
|
|
85
|
+
.test(commandLine)
|
|
86
|
+
) {
|
|
87
|
+
kind = 'unified-runtime';
|
|
88
|
+
} else if (
|
|
89
|
+
/(?:node_modules\/(?:livedesk\/hub|@livedesk\/hub)|packages\/(?:livedesk\/hub|hub))\/src\/server\.js(?:\s|$|["'])/i
|
|
90
|
+
.test(commandLine)
|
|
91
|
+
) {
|
|
92
|
+
kind = 'hub-runtime';
|
|
93
|
+
} else if (
|
|
94
|
+
/(?:^|\/)LiveDesk\.app\/Contents\/MacOS\/LiveDesk(?:\s|$|["'])/i.test(commandLine)
|
|
95
|
+
) {
|
|
96
|
+
kind = 'desktop-runtime';
|
|
97
|
+
}
|
|
98
|
+
if (!kind) continue;
|
|
99
|
+
results.push({
|
|
100
|
+
...record,
|
|
101
|
+
kind
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return results;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const MODIFIERS = Object.freeze([
|
|
108
|
+
{ code: 'ShiftLeft', key: 'Shift', keyCode: 16, location: 1, flag: 'shiftKey' },
|
|
109
|
+
{ code: 'ControlLeft', key: 'Control', keyCode: 17, location: 1, flag: 'ctrlKey' },
|
|
110
|
+
{ code: 'AltLeft', key: 'Alt', keyCode: 18, location: 1, flag: 'altKey' },
|
|
111
|
+
{ code: 'MetaLeft', key: 'Meta', keyCode: 91, location: 1, flag: 'metaKey' }
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
export function buildMacModifierProbeSteps(startInputSeq = 1) {
|
|
115
|
+
const pressed = [];
|
|
116
|
+
const steps = [];
|
|
117
|
+
let inputSeq = Math.max(1, Math.floor(finite(startInputSeq, 1)));
|
|
118
|
+
const snapshot = () => ({
|
|
119
|
+
pressedCodes: [...pressed],
|
|
120
|
+
shiftKey: pressed.includes('ShiftLeft'),
|
|
121
|
+
ctrlKey: pressed.includes('ControlLeft'),
|
|
122
|
+
altKey: pressed.includes('AltLeft'),
|
|
123
|
+
metaKey: pressed.includes('MetaLeft')
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
for (const modifier of MODIFIERS) {
|
|
127
|
+
pressed.push(modifier.code);
|
|
128
|
+
steps.push({
|
|
129
|
+
type: 'keydown',
|
|
130
|
+
key: modifier.key,
|
|
131
|
+
code: modifier.code,
|
|
132
|
+
keyCode: modifier.keyCode,
|
|
133
|
+
location: modifier.location,
|
|
134
|
+
repeat: false,
|
|
135
|
+
inputSeq: inputSeq++,
|
|
136
|
+
...snapshot()
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
for (const modifier of [...MODIFIERS].reverse()) {
|
|
140
|
+
pressed.splice(pressed.indexOf(modifier.code), 1);
|
|
141
|
+
steps.push({
|
|
142
|
+
type: 'keyup',
|
|
143
|
+
key: modifier.key,
|
|
144
|
+
code: modifier.code,
|
|
145
|
+
keyCode: modifier.keyCode,
|
|
146
|
+
location: modifier.location,
|
|
147
|
+
repeat: false,
|
|
148
|
+
inputSeq: inputSeq++,
|
|
149
|
+
...snapshot()
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return steps;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function buildMacModifierReleaseSteps(pressedCodes = [], startInputSeq = 1) {
|
|
156
|
+
const pressed = MODIFIERS
|
|
157
|
+
.map(modifier => modifier.code)
|
|
158
|
+
.filter(code => Array.isArray(pressedCodes) && pressedCodes.includes(code));
|
|
159
|
+
const steps = [];
|
|
160
|
+
let inputSeq = Math.max(1, Math.floor(finite(startInputSeq, 1)));
|
|
161
|
+
for (const modifier of [...MODIFIERS].reverse()) {
|
|
162
|
+
const pressedIndex = pressed.indexOf(modifier.code);
|
|
163
|
+
if (pressedIndex < 0) continue;
|
|
164
|
+
pressed.splice(pressedIndex, 1);
|
|
165
|
+
steps.push({
|
|
166
|
+
type: 'keyup',
|
|
167
|
+
key: modifier.key,
|
|
168
|
+
code: modifier.code,
|
|
169
|
+
keyCode: modifier.keyCode,
|
|
170
|
+
location: modifier.location,
|
|
171
|
+
repeat: false,
|
|
172
|
+
inputSeq: inputSeq++,
|
|
173
|
+
pressedCodes: [...pressed],
|
|
174
|
+
shiftKey: pressed.includes('ShiftLeft'),
|
|
175
|
+
ctrlKey: pressed.includes('ControlLeft'),
|
|
176
|
+
altKey: pressed.includes('AltLeft'),
|
|
177
|
+
metaKey: pressed.includes('MetaLeft')
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return steps;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function noteMacModifierBeforeSend(pressedCodes, step = {}) {
|
|
184
|
+
if (!(pressedCodes instanceof Set)) {
|
|
185
|
+
throw new TypeError('Mac modifier pressed state must be a Set.');
|
|
186
|
+
}
|
|
187
|
+
const code = String(step.code || '').trim();
|
|
188
|
+
if (step.type === 'keydown' && code) {
|
|
189
|
+
// A timed-out ACK is ambiguous: the native keydown may already have been
|
|
190
|
+
// applied. Track it before the send boundary so cleanup remains fail-safe.
|
|
191
|
+
pressedCodes.add(code);
|
|
192
|
+
}
|
|
193
|
+
return pressedCodes;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function noteMacModifierAfterAppliedAck(pressedCodes, step = {}) {
|
|
197
|
+
if (!(pressedCodes instanceof Set)) {
|
|
198
|
+
throw new TypeError('Mac modifier pressed state must be a Set.');
|
|
199
|
+
}
|
|
200
|
+
const code = String(step.code || '').trim();
|
|
201
|
+
if (step.type === 'keyup' && code) {
|
|
202
|
+
// Only a native applied ACK proves that releasing this key succeeded.
|
|
203
|
+
pressedCodes.delete(code);
|
|
204
|
+
}
|
|
205
|
+
return pressedCodes;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function check(id, ok, detail) {
|
|
209
|
+
return { id, ok: ok === true, detail: String(detail || '') };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function classifyMacPhysicalGate(snapshot = {}) {
|
|
213
|
+
const requestedFps = Math.max(1, finite(snapshot.requestedFps, 30));
|
|
214
|
+
const minimumFps = requestedFps * 0.8;
|
|
215
|
+
const telemetry = snapshot.telemetry || {};
|
|
216
|
+
const helper = snapshot.helper || {};
|
|
217
|
+
const monitor = snapshot.monitor || {};
|
|
218
|
+
const modifier = snapshot.modifier || {};
|
|
219
|
+
const transport = snapshot.transport || {};
|
|
220
|
+
const diagnostic = snapshot.diagnostic || {};
|
|
221
|
+
const abortedSignal = String(diagnostic.abortedSignal || '').trim();
|
|
222
|
+
const runError = String(
|
|
223
|
+
diagnostic.runError
|
|
224
|
+
|| snapshot.error
|
|
225
|
+
|| (abortedSignal ? `interrupted by ${abortedSignal}` : '')
|
|
226
|
+
|| ''
|
|
227
|
+
).trim();
|
|
228
|
+
const cleanupErrors = Array.isArray(diagnostic.cleanupErrors)
|
|
229
|
+
? diagnostic.cleanupErrors.map(value => String(value || '').trim()).filter(Boolean)
|
|
230
|
+
: [];
|
|
231
|
+
const captureFps = Math.max(0, finite(telemetry.captureFps));
|
|
232
|
+
const compressionFps = Math.max(0, finite(telemetry.compressionFps));
|
|
233
|
+
const stdoutFps = Math.max(0, finite(telemetry.stdoutFps));
|
|
234
|
+
const idleOptimized = finite(telemetry.idleSkipped) > 0
|
|
235
|
+
&& finite(telemetry.backpressureSkipped) === 0;
|
|
236
|
+
const captureAtTarget = captureFps >= minimumFps;
|
|
237
|
+
const compressionKeepsUp = idleOptimized
|
|
238
|
+
|| (captureFps > 0 && compressionFps >= captureFps * 0.8);
|
|
239
|
+
const stdoutKeepsUp = (idleOptimized && compressionFps <= 0)
|
|
240
|
+
|| (compressionFps > 0 && stdoutFps >= compressionFps * 0.8);
|
|
241
|
+
const deliveredAtTarget = idleOptimized
|
|
242
|
+
|| finite(snapshot.deliveredFps) >= minimumFps;
|
|
243
|
+
const helperPids = Array.isArray(helper.observedPids)
|
|
244
|
+
? helper.observedPids.map(Number).filter(pid => Number.isInteger(pid) && pid > 1)
|
|
245
|
+
: [];
|
|
246
|
+
const expectedModifierAcks = Math.max(1, finite(modifier.expectedAcks, 8));
|
|
247
|
+
const checks = [
|
|
248
|
+
check(
|
|
249
|
+
'diagnostic-run',
|
|
250
|
+
!runError,
|
|
251
|
+
runError || 'completed'
|
|
252
|
+
),
|
|
253
|
+
check(
|
|
254
|
+
'diagnostic-cleanup',
|
|
255
|
+
cleanupErrors.length === 0,
|
|
256
|
+
cleanupErrors.length === 0 ? 'completed' : cleanupErrors.join('; ')
|
|
257
|
+
),
|
|
258
|
+
check(
|
|
259
|
+
'first-frame',
|
|
260
|
+
finite(snapshot.firstFrameMs) > 0 && finite(snapshot.firstFrameMs) < 8_000,
|
|
261
|
+
`${Math.round(finite(snapshot.firstFrameMs))}ms`
|
|
262
|
+
),
|
|
263
|
+
check(
|
|
264
|
+
'monitor-first-frame',
|
|
265
|
+
finite(snapshot.monitorSwitchFirstFrameMs) > 0
|
|
266
|
+
&& finite(snapshot.monitorSwitchFirstFrameMs) < 8_000,
|
|
267
|
+
`${Math.round(finite(snapshot.monitorSwitchFirstFrameMs))}ms`
|
|
268
|
+
),
|
|
269
|
+
check(
|
|
270
|
+
'native-profile',
|
|
271
|
+
snapshot.platformProfile === 'macos-screencapturekit-nv12-videotoolbox',
|
|
272
|
+
snapshot.platformProfile || 'missing'
|
|
273
|
+
),
|
|
274
|
+
check(
|
|
275
|
+
'telemetry-current',
|
|
276
|
+
finite(telemetry.version) >= 2
|
|
277
|
+
&& Number.isFinite(Number(telemetry.ageMs))
|
|
278
|
+
&& finite(telemetry.ageMs) <= 3_000,
|
|
279
|
+
`v${finite(telemetry.version)} age=${Number.isFinite(Number(telemetry.ageMs)) ? Math.round(finite(telemetry.ageMs)) : '-'}ms`
|
|
280
|
+
),
|
|
281
|
+
check(
|
|
282
|
+
'hardware-videotoolbox',
|
|
283
|
+
telemetry.hardwareAccelerated === true,
|
|
284
|
+
telemetry.hardwareAccelerated === true ? 'hardware' : 'not-confirmed'
|
|
285
|
+
),
|
|
286
|
+
check('screencapturekit-fps', captureAtTarget, `${captureFps.toFixed(1)}/${requestedFps}`),
|
|
287
|
+
check(
|
|
288
|
+
'videotoolbox-fps',
|
|
289
|
+
compressionKeepsUp,
|
|
290
|
+
`${compressionFps.toFixed(1)} after ${captureFps.toFixed(1)}${idleOptimized ? ' idle-optimized' : ''}`
|
|
291
|
+
),
|
|
292
|
+
check(
|
|
293
|
+
'stdout-fps',
|
|
294
|
+
stdoutKeepsUp,
|
|
295
|
+
`${stdoutFps.toFixed(1)} after ${compressionFps.toFixed(1)}${idleOptimized ? ' idle-optimized' : ''}`
|
|
296
|
+
),
|
|
297
|
+
check(
|
|
298
|
+
'delivered-fps',
|
|
299
|
+
deliveredAtTarget,
|
|
300
|
+
`${finite(snapshot.deliveredFps).toFixed(1)}/${requestedFps}${idleOptimized ? ' idle-optimized' : ''}`
|
|
301
|
+
),
|
|
302
|
+
check(
|
|
303
|
+
'single-helper',
|
|
304
|
+
finite(helper.maxConcurrent) <= 1,
|
|
305
|
+
`max=${finite(helper.maxConcurrent)} pids=${helperPids.join(',') || '-'}`
|
|
306
|
+
),
|
|
307
|
+
check('helper-drained', finite(helper.finalCount) === 0, `final=${finite(helper.finalCount)}`),
|
|
308
|
+
check('two-monitors', finite(monitor.count) >= 2, `count=${finite(monitor.count)}`),
|
|
309
|
+
check(
|
|
310
|
+
'monitor-generation',
|
|
311
|
+
finite(monitor.nextGeneration) > finite(monitor.initialGeneration),
|
|
312
|
+
`${finite(monitor.initialGeneration)} -> ${finite(monitor.nextGeneration)}`
|
|
313
|
+
),
|
|
314
|
+
check(
|
|
315
|
+
'monitor-pixels',
|
|
316
|
+
Boolean(monitor.initialHash)
|
|
317
|
+
&& Boolean(monitor.nextHash)
|
|
318
|
+
&& monitor.initialHash !== monitor.nextHash,
|
|
319
|
+
`${String(monitor.initialHash || '-').slice(0, 12)} -> ${String(monitor.nextHash || '-').slice(0, 12)}`
|
|
320
|
+
),
|
|
321
|
+
check(
|
|
322
|
+
'modifier-native-ack',
|
|
323
|
+
finite(modifier.appliedAcks) === expectedModifierAcks
|
|
324
|
+
&& (!Array.isArray(modifier.errors) || modifier.errors.length === 0),
|
|
325
|
+
`${finite(modifier.appliedAcks)}/${expectedModifierAcks} native ACK`
|
|
326
|
+
),
|
|
327
|
+
check(
|
|
328
|
+
'active-transport',
|
|
329
|
+
Boolean(String(transport.active || '').trim()),
|
|
330
|
+
[
|
|
331
|
+
String(transport.active || '-'),
|
|
332
|
+
String(transport.protocol || '-'),
|
|
333
|
+
String(transport.state || '-')
|
|
334
|
+
].join('/')
|
|
335
|
+
)
|
|
336
|
+
];
|
|
337
|
+
|
|
338
|
+
const failed = checks.filter(item => !item.ok);
|
|
339
|
+
const bottleneckByCheck = {
|
|
340
|
+
'diagnostic-run': 'diagnostic-run',
|
|
341
|
+
'diagnostic-cleanup': 'diagnostic-cleanup',
|
|
342
|
+
'first-frame': 'capture-startup',
|
|
343
|
+
'monitor-first-frame': 'monitor-restart',
|
|
344
|
+
'native-profile': 'capture-fallback',
|
|
345
|
+
'telemetry-current': 'native-telemetry-stale',
|
|
346
|
+
'hardware-videotoolbox': 'videotoolbox-software',
|
|
347
|
+
'screencapturekit-fps': 'screencapturekit',
|
|
348
|
+
'videotoolbox-fps': 'videotoolbox',
|
|
349
|
+
'stdout-fps': 'native-stdout',
|
|
350
|
+
'delivered-fps': 'agent-or-transport',
|
|
351
|
+
'single-helper': 'duplicate-helper',
|
|
352
|
+
'helper-drained': 'stale-helper',
|
|
353
|
+
'two-monitors': 'monitor-topology',
|
|
354
|
+
'monitor-generation': 'monitor-generation',
|
|
355
|
+
'monitor-pixels': 'monitor-binding',
|
|
356
|
+
'modifier-native-ack': 'remote-input',
|
|
357
|
+
'active-transport': 'transport-telemetry'
|
|
358
|
+
};
|
|
359
|
+
return {
|
|
360
|
+
ok: failed.length === 0,
|
|
361
|
+
idleOptimized,
|
|
362
|
+
bottleneck: failed.length > 0 ? bottleneckByCheck[failed[0].id] || failed[0].id : 'none',
|
|
363
|
+
checks
|
|
364
|
+
};
|
|
365
|
+
}
|