@mindexec/cli 0.2.477 → 0.2.478
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 +323 -323
- package/codex-model-catalog.js +202 -202
- package/codex-runtime.js +1608 -1608
- package/desktop-workspace-state.cjs +120 -120
- package/electron/main.cjs +517 -517
- package/electron/preload.cjs +17 -17
- package/electron/recurring-update-owner-smoke.mjs +55 -55
- package/electron/recurring-update-owner.cjs +97 -97
- package/electron/source-smoke.mjs +108 -108
- package/electron/update-feed-resolver.cjs +79 -79
- package/electron/update-feed.json +10 -10
- package/electron/update-manager-smoke.mjs +146 -146
- package/electron/update-manager.cjs +297 -297
- package/electron/windows-package-smoke.mjs +156 -156
- package/launch-bridge.cjs +236 -236
- package/lzo1x.js +110 -110
- package/mode4-atlas-worker.js +440 -440
- package/mode4-atlas.js +130 -130
- package/package.json +1 -1
- package/port-guard.cjs +285 -285
- package/remote-fast/osx-arm64/MacScreenCaptureKitH264.swift +339 -339
- package/remote-fast/osx-arm64/MacSystemAudioCapture.swift +214 -214
- package/remote-fast/osx-arm64/UnixRawCaptureHelper.mjs +80 -80
- package/remote-fast/osx-arm64/livedesk-raw-capture-resampler.mjs +73 -73
- package/remote-fast/osx-arm64/mindexec-remote-fast.dll +0 -0
- package/remote-fast/osx-x64/MacScreenCaptureKitH264.swift +339 -339
- package/remote-fast/osx-x64/MacSystemAudioCapture.swift +214 -214
- package/remote-fast/osx-x64/UnixRawCaptureHelper.mjs +80 -80
- package/remote-fast/osx-x64/livedesk-raw-capture-resampler.mjs +73 -73
- package/remote-fast/osx-x64/mindexec-remote-fast.dll +0 -0
- package/remote-fast/win-x64/mindexec-remote-fast.dll +0 -0
- package/remote-hub.js +3698 -3698
- package/scripts/auth-session-smoke.mjs +262 -262
- package/scripts/board-render-ownership-smoke.mjs +85 -85
- package/scripts/codex-model-catalog-smoke.mjs +150 -150
- package/scripts/codex-runtime-home-smoke.mjs +58 -58
- package/scripts/codex-sdk-image-live-smoke.mjs +43 -43
- package/scripts/codex-sdk-image-runtime-smoke.mjs +122 -122
- package/scripts/codex-sdk-runtime-smoke.mjs +89 -89
- package/scripts/desktop-update-publisher-smoke.mjs +89 -89
- package/scripts/desktop-update-worker-smoke.mjs +72 -72
- package/scripts/desktop-workspace-state-smoke.mjs +81 -81
- package/scripts/drive-installed-update-smoke.mjs +67 -67
- package/scripts/npm-fresh-install-smoke.mjs +216 -216
- package/scripts/publish-mindexec-desktop-updates.mjs +224 -224
- package/scripts/remote-agent-managed-smoke.mjs +318 -318
- package/scripts/remote-agent-package-smoke.mjs +64 -64
- package/scripts/remote-agent-ws-smoke.mjs +269 -269
- package/scripts/remote-fast-live-rate-smoke.mjs +343 -343
- package/scripts/remote-fast-mdm-browser-smoke.mjs +453 -453
- package/scripts/remote-fleet-render-smoke.mjs +1350 -1350
- package/scripts/remote-frame-ws-smoke.mjs +234 -234
- package/scripts/remote-http-smoke.mjs +592 -592
- package/scripts/remote-hub-identity-smoke.mjs +146 -146
- package/scripts/remote-hub-scale-smoke.mjs +124 -124
- package/scripts/remote-hub-smoke.mjs +699 -699
- package/scripts/remote-input-ws-smoke.mjs +263 -263
- package/scripts/remote-registry-follower-smoke.mjs +815 -815
- package/scripts/setup-tree-sitter-grammars.mjs +59 -59
- package/server.js +7690 -7690
- package/start-bridge.bat +32 -32
- package/start-bridge.sh +81 -81
- package/wwwroot/_content/MindExecution.Plugins.Admin/css/admin-dashboard.css +546 -546
- package/wwwroot/_content/MindExecution.Plugins.YouTube/css/youtube-search.css +251 -251
- package/wwwroot/_content/MindExecution.Shared/css/app.css +1 -1
- package/wwwroot/_content/MindExecution.Shared/css/mind-map-overrides.css +3789 -3789
- package/wwwroot/_content/MindExecution.Shared/js/agent-visualization.js +27 -27
- package/wwwroot/_content/MindExecution.Shared/js/background-themes.js +1449 -1449
- package/wwwroot/_content/MindExecution.Shared/js/code-master.js +8316 -8316
- package/wwwroot/_content/MindExecution.Shared/js/file-system-helper.js +338 -338
- package/wwwroot/_content/MindExecution.Shared/js/marked.min.js +69 -69
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-animated-image-preview.js +270 -270
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-automation-edge-motion.js +170 -170
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-board-render-ownership.js +81 -81
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +10506 -10506
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-hotpath.js +902 -902
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +24637 -24637
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-dev-guards.js +325 -325
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js +1491 -1491
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-glow-shader.js +70 -70
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-image-policy.js +218 -218
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-interactions.js +7065 -7065
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-plan-worker.js +160 -160
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-renderer.js +9956 -9956
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-video-placeholder.js +107 -107
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-logic-workers.js +1004 -1004
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-menu-manager.js +1351 -1351
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-multi-select.js +865 -865
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-node-search-worker.js +554 -554
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-nodes.js +4744 -4744
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-normal-boundary-optimizer.js +624 -624
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js +241 -241
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-pipeline.js +2290 -2290
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-remote-codecs.js +202 -202
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-render-plan.js +1971 -1971
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-text-lod-system.js +93 -93
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js +2391 -2391
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-video-playback-controls.js +95 -95
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-visibility-worker.js +895 -895
- package/wwwroot/_content/MindExecution.Shared/js/mindmap-toolbar.js +636 -636
- package/wwwroot/_content/MindExecution.Shared/js/native-drop-handler.js +45 -45
- package/wwwroot/_content/MindExecution.Shared/js/plan-master.js +117 -117
- package/wwwroot/_content/MindExecution.Shared/js/renderers/CSS3DRenderer.js +114 -114
- package/wwwroot/_content/MindExecution.Shared/js/texture-worker-manager.js +10 -10
- package/wwwroot/_content/MindExecution.Shared/js/three.min.js +6 -6
- package/wwwroot/_content/MindExecution.Shared/js/token-manager.js +7 -7
- package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js +6 -6
- package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js.map +2 -2
- package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/css/all.min.css +8 -8
- package/wwwroot/_content/MindExecution.Shared/models/vocab.txt +30522 -30522
- package/wwwroot/_framework/{Google.Protobuf.9h59ukbel7.dll → Google.Protobuf.33g2gb5is0.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Core.ty1xewqp36.dll → MindExecution.Core.z100xo4mg0.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Kernel.h7rqjoo69o.dll → MindExecution.Kernel.i9cldrx8yc.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Admin.wrfrcvhjde.dll → MindExecution.Plugins.Admin.48lzqddb0a.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Business.csbcyjurdh.dll → MindExecution.Plugins.Business.7m8u1q142p.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Concept.buomiysqjn.dll → MindExecution.Plugins.Concept.cl5wmr53od.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Directory.7k00i2v769.dll → MindExecution.Plugins.Directory.slipi39041.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.85i5ocraro.dll → MindExecution.Plugins.PlanMaster.qfge05l671.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.YouTube.nbolrfakte.dll → MindExecution.Plugins.YouTube.tn7v29ut77.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Shared.nqh4porv7x.dll → MindExecution.Shared.rjqlf0umjn.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Web.211q3bf4mr.dll → MindExecution.Web.rwawrxxk38.dll} +0 -0
- package/wwwroot/_framework/blazor.boot.json +23 -23
- package/wwwroot/_headers +40 -40
- package/wwwroot/_redirects +1 -1
- package/wwwroot/index.html +772 -772
- package/wwwroot/js/marketing-tool.js +180 -180
- package/wwwroot/manifest.webmanifest +23 -23
- package/wwwroot/service-worker-assets.js +78 -106
- package/wwwroot/service-worker.js +33 -33
- package/tree-sitter-grammars/README.md +0 -18
- package/tree-sitter-grammars/tree-sitter-c_sharp.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-go.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-java.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-javascript.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-python.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-rust.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-tsx.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-typescript.wasm +0 -0
- package/wwwroot/_content/MindExecution.Plugins.Directory/background.png +0 -0
- package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Bold.ttf +0 -0
- package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Regular.ttf +0 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js.backup +0 -1059
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js.bak +0 -434
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js.backup +0 -372
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js.backup +0 -1258
|
@@ -1,343 +1,343 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import assert from 'node:assert/strict';
|
|
4
|
-
import { spawn } from 'node:child_process';
|
|
5
|
-
import { mkdtemp, rm } from 'node:fs/promises';
|
|
6
|
-
import net from 'node:net';
|
|
7
|
-
import os from 'node:os';
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
import { fileURLToPath } from 'node:url';
|
|
10
|
-
import { WebSocket } from 'ws';
|
|
11
|
-
|
|
12
|
-
const BRIDGE_TOKEN = 'remote-fast-live-rate-smoke-token';
|
|
13
|
-
const PAIR_TOKEN = 'remote-fast-live-rate-pair-token';
|
|
14
|
-
const LEASE_ID = 'remote-fast-live-rate-lease';
|
|
15
|
-
const LOCAL_BRIDGE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
16
|
-
const REQUESTED_FPS = Number(process.env.MINDEXEC_REMOTE_LIVE_RATE_REQUEST_FPS || 12);
|
|
17
|
-
const SAMPLE_MS = Number(process.env.MINDEXEC_REMOTE_LIVE_RATE_SAMPLE_MS || 3500);
|
|
18
|
-
const MIN_EFFECTIVE_FPS = Number(process.env.MINDEXEC_REMOTE_LIVE_RATE_MIN_FPS || 10);
|
|
19
|
-
|
|
20
|
-
function wait(ms) {
|
|
21
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function findFreePort() {
|
|
25
|
-
return await new Promise((resolve, reject) => {
|
|
26
|
-
const server = net.createServer();
|
|
27
|
-
server.unref();
|
|
28
|
-
server.once('error', reject);
|
|
29
|
-
server.listen(0, '127.0.0.1', () => {
|
|
30
|
-
const address = server.address();
|
|
31
|
-
const port = typeof address === 'object' && address ? address.port : 0;
|
|
32
|
-
server.close(() => resolve(port));
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function fetchJson(url, options = {}) {
|
|
38
|
-
const response = await fetch(url, {
|
|
39
|
-
...options,
|
|
40
|
-
headers: {
|
|
41
|
-
...(options.body ? { 'Content-Type': 'application/json' } : {}),
|
|
42
|
-
'X-Bridge-Token': BRIDGE_TOKEN,
|
|
43
|
-
...(options.headers || {})
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
let payload = null;
|
|
48
|
-
try {
|
|
49
|
-
payload = await response.json();
|
|
50
|
-
} catch {
|
|
51
|
-
// Some failure responses may be empty.
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return { status: response.status, ok: response.ok, payload };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function waitFor(predicate, timeoutMs = 30000, label = 'condition') {
|
|
58
|
-
const startedAt = Date.now();
|
|
59
|
-
let lastError = null;
|
|
60
|
-
while (Date.now() - startedAt < timeoutMs) {
|
|
61
|
-
try {
|
|
62
|
-
const value = await predicate();
|
|
63
|
-
if (value) {
|
|
64
|
-
return value;
|
|
65
|
-
}
|
|
66
|
-
} catch (error) {
|
|
67
|
-
lastError = error;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
await wait(100);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
throw new Error(`Timed out waiting for ${label}${lastError ? `: ${lastError.message}` : ''}.`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function spawnBridge({ bridgePort, remoteHubPort, workspacePath, label, traceFrames = false }) {
|
|
77
|
-
const child = spawn(process.execPath, ['server.js'], {
|
|
78
|
-
cwd: LOCAL_BRIDGE_DIR,
|
|
79
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
80
|
-
windowsHide: true,
|
|
81
|
-
env: {
|
|
82
|
-
...process.env,
|
|
83
|
-
BRIDGE_PORT: String(bridgePort),
|
|
84
|
-
BRIDGE_TOKEN,
|
|
85
|
-
BRIDGE_REQUIRE_TOKEN: '1',
|
|
86
|
-
MINDEXEC_REMOTE_HUB: '1',
|
|
87
|
-
MINDEXEC_REMOTE_TRACE_FRAMES: traceFrames ? '1' : '',
|
|
88
|
-
MINDEXEC_REMOTE_REGISTRY_FOLLOWER: '0',
|
|
89
|
-
MINDEXEC_REMOTE_REGISTRY_REALTIME: '0',
|
|
90
|
-
MINDEXEC_REMOTE_HOST_TARGET_AUTO_RENEW: '0',
|
|
91
|
-
REMOTE_HUB_HOST: '127.0.0.1',
|
|
92
|
-
REMOTE_HUB_PORT: String(remoteHubPort),
|
|
93
|
-
REMOTE_HUB_PAIR_TOKEN: PAIR_TOKEN,
|
|
94
|
-
WORKSPACE_PATH: workspacePath,
|
|
95
|
-
NO_COLOR: '1'
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
let stdout = '';
|
|
100
|
-
let stderr = '';
|
|
101
|
-
child.stdout.on('data', chunk => {
|
|
102
|
-
stdout += chunk.toString();
|
|
103
|
-
});
|
|
104
|
-
child.stderr.on('data', chunk => {
|
|
105
|
-
stderr += chunk.toString();
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
const exitPromise = new Promise(resolve => child.once('exit', resolve));
|
|
109
|
-
const details = () => `${label} stdout=${stdout}\n${label} stderr=${stderr}`;
|
|
110
|
-
const stop = async () => {
|
|
111
|
-
if (child.exitCode === null && !child.killed) {
|
|
112
|
-
child.kill('SIGTERM');
|
|
113
|
-
await Promise.race([exitPromise, wait(5000)]);
|
|
114
|
-
if (child.exitCode === null && !child.killed) {
|
|
115
|
-
child.kill('SIGKILL');
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
baseUrl: `http://127.0.0.1:${bridgePort}`,
|
|
122
|
-
child,
|
|
123
|
-
details,
|
|
124
|
-
stop
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async function waitForBridge(bridge) {
|
|
129
|
-
return await waitFor(async () => {
|
|
130
|
-
const result = await fetchJson(`${bridge.baseUrl}/api/status`);
|
|
131
|
-
return result.ok && result.payload?.status === 'ok' ? result.payload : null;
|
|
132
|
-
}, 30000, `bridge startup\n${bridge.details()}`);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function parseFramePacket(data) {
|
|
136
|
-
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
137
|
-
if (buffer.length <= 8 || buffer[0] === 0x7b || buffer[0] === 0x5b) {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const metaLength = buffer.readUInt32BE(0);
|
|
142
|
-
if (metaLength <= 0 || metaLength > 64 * 1024 || 4 + metaLength >= buffer.length) {
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const metadata = JSON.parse(buffer.subarray(4, 4 + metaLength).toString('utf8'));
|
|
147
|
-
const payload = buffer.subarray(4 + metaLength);
|
|
148
|
-
if (String(metadata?.type || '').toLowerCase() !== 'remote.frame.binary') {
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return { metadata, payload, receivedAtMs: Date.now() };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
async function openFrameSocket(baseUrl, deviceId) {
|
|
156
|
-
const ws = new WebSocket(`${baseUrl.replace(/^http:/, 'ws:')}/api/remote/frames/ws?token=${encodeURIComponent(BRIDGE_TOKEN)}`);
|
|
157
|
-
await new Promise((resolve, reject) => {
|
|
158
|
-
ws.once('open', resolve);
|
|
159
|
-
ws.once('error', reject);
|
|
160
|
-
});
|
|
161
|
-
ws.send(JSON.stringify({ type: 'subscribe', deviceIds: [deviceId] }));
|
|
162
|
-
return ws;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
async function collectLiveFrames(ws, deviceId, sampleMs) {
|
|
166
|
-
const frames = [];
|
|
167
|
-
let firstFrameAt = 0;
|
|
168
|
-
let settle = null;
|
|
169
|
-
let settled = false;
|
|
170
|
-
|
|
171
|
-
const finish = () => {
|
|
172
|
-
if (settled) {
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
settled = true;
|
|
176
|
-
settle?.();
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
const done = new Promise((resolve, reject) => {
|
|
180
|
-
settle = resolve;
|
|
181
|
-
const timeout = setTimeout(() => reject(new Error('timed out waiting for RemoteFast binary frames')), 25000);
|
|
182
|
-
ws.on('message', data => {
|
|
183
|
-
const frame = parseFramePacket(data);
|
|
184
|
-
if (!frame
|
|
185
|
-
|| String(frame.metadata.kind || '').toLowerCase() !== 'live'
|
|
186
|
-
|| String(frame.metadata.deviceId || '') !== deviceId) {
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
frames.push(frame);
|
|
191
|
-
if (!firstFrameAt) {
|
|
192
|
-
firstFrameAt = frame.receivedAtMs;
|
|
193
|
-
}
|
|
194
|
-
if (frame.receivedAtMs - firstFrameAt >= sampleMs) {
|
|
195
|
-
clearTimeout(timeout);
|
|
196
|
-
finish();
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
ws.once('close', () => {
|
|
200
|
-
clearTimeout(timeout);
|
|
201
|
-
if (!settled) {
|
|
202
|
-
reject(new Error('frame websocket closed during live-rate sample'));
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
ws.once('error', error => {
|
|
206
|
-
clearTimeout(timeout);
|
|
207
|
-
reject(error);
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
await done;
|
|
212
|
-
return frames;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function summarizeFrames(frames) {
|
|
216
|
-
const first = frames[0];
|
|
217
|
-
const last = frames[frames.length - 1];
|
|
218
|
-
const durationMs = Math.max(1, Number(last?.receivedAtMs || 0) - Number(first?.receivedAtMs || 0));
|
|
219
|
-
const fps = frames.length > 1 ? ((frames.length - 1) * 1000) / durationMs : 0;
|
|
220
|
-
const seqs = frames.map(frame => Number(frame.metadata.frameSeq || 0)).filter(Number.isFinite);
|
|
221
|
-
const uniqueSeqs = new Set(seqs);
|
|
222
|
-
const byteLengths = frames.map(frame => Number(frame.payload?.length || 0)).filter(value => value > 0);
|
|
223
|
-
return {
|
|
224
|
-
frames: frames.length,
|
|
225
|
-
uniqueSeqs: uniqueSeqs.size,
|
|
226
|
-
durationMs,
|
|
227
|
-
fps,
|
|
228
|
-
firstSeq: seqs[0] || 0,
|
|
229
|
-
lastSeq: seqs[seqs.length - 1] || 0,
|
|
230
|
-
minBytes: byteLengths.length > 0 ? Math.min(...byteLengths) : 0,
|
|
231
|
-
maxBytes: byteLengths.length > 0 ? Math.max(...byteLengths) : 0,
|
|
232
|
-
metadataFps: Number(last?.metadata?.fps || 0) || 0,
|
|
233
|
-
mode: String(last?.metadata?.mode || '')
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
async function main() {
|
|
238
|
-
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'mindexec-remote-fast-live-rate-'));
|
|
239
|
-
const hostWorkspace = path.join(tempRoot, 'host');
|
|
240
|
-
const clientWorkspace = path.join(tempRoot, 'client');
|
|
241
|
-
const hostBridgePort = await findFreePort();
|
|
242
|
-
const hostRemotePort = await findFreePort();
|
|
243
|
-
const clientBridgePort = await findFreePort();
|
|
244
|
-
const clientRemotePort = await findFreePort();
|
|
245
|
-
const managerEndpoint = `127.0.0.1:${hostRemotePort}`;
|
|
246
|
-
let hostBridge = null;
|
|
247
|
-
let clientBridge = null;
|
|
248
|
-
let ws = null;
|
|
249
|
-
|
|
250
|
-
try {
|
|
251
|
-
hostBridge = spawnBridge({
|
|
252
|
-
bridgePort: hostBridgePort,
|
|
253
|
-
remoteHubPort: hostRemotePort,
|
|
254
|
-
workspacePath: hostWorkspace,
|
|
255
|
-
label: 'host'
|
|
256
|
-
});
|
|
257
|
-
await waitForBridge(hostBridge);
|
|
258
|
-
|
|
259
|
-
clientBridge = spawnBridge({
|
|
260
|
-
bridgePort: clientBridgePort,
|
|
261
|
-
remoteHubPort: clientRemotePort,
|
|
262
|
-
workspacePath: clientWorkspace,
|
|
263
|
-
label: 'client',
|
|
264
|
-
traceFrames: true
|
|
265
|
-
});
|
|
266
|
-
await waitForBridge(clientBridge);
|
|
267
|
-
|
|
268
|
-
const connectResult = await fetchJson(`${clientBridge.baseUrl}/api/remote/agent/connect`, {
|
|
269
|
-
method: 'POST',
|
|
270
|
-
body: JSON.stringify({
|
|
271
|
-
managerCandidates: [managerEndpoint],
|
|
272
|
-
pairToken: PAIR_TOKEN,
|
|
273
|
-
leaseId: LEASE_ID,
|
|
274
|
-
nodeId: 'remote-fast-live-rate-node',
|
|
275
|
-
engine: 'auto',
|
|
276
|
-
source: 'live-rate-smoke'
|
|
277
|
-
})
|
|
278
|
-
});
|
|
279
|
-
assert.equal(connectResult.ok, true, JSON.stringify(connectResult.payload));
|
|
280
|
-
assert.equal(connectResult.payload?.ok, true, JSON.stringify(connectResult.payload));
|
|
281
|
-
assert.equal(connectResult.payload?.agent?.usingNpx, false, JSON.stringify(connectResult.payload?.agent));
|
|
282
|
-
assert.match(String(connectResult.payload?.agent?.launcher || ''), /mindexec-remote-fast/i);
|
|
283
|
-
|
|
284
|
-
const device = await waitFor(async () => {
|
|
285
|
-
const result = await fetchJson(`${hostBridge.baseUrl}/api/remote/devices`);
|
|
286
|
-
return result.payload?.devices?.find(item =>
|
|
287
|
-
item.connected === true
|
|
288
|
-
&& item.capabilities?.liveStream === true
|
|
289
|
-
&& item.capabilities?.binaryFrames === true) || null;
|
|
290
|
-
}, 15000, `RemoteFast device registration\n${hostBridge.details()}\n${clientBridge.details()}`);
|
|
291
|
-
|
|
292
|
-
ws = await openFrameSocket(hostBridge.baseUrl, device.deviceId);
|
|
293
|
-
const live = await fetchJson(`${hostBridge.baseUrl}/api/remote/devices/${encodeURIComponent(device.deviceId)}/live/start`, {
|
|
294
|
-
method: 'POST',
|
|
295
|
-
body: JSON.stringify({
|
|
296
|
-
mode: 'remote-fast',
|
|
297
|
-
fps: REQUESTED_FPS,
|
|
298
|
-
maxWidth: 960,
|
|
299
|
-
maxHeight: 540,
|
|
300
|
-
quality: 60
|
|
301
|
-
})
|
|
302
|
-
});
|
|
303
|
-
assert.equal(live.ok, true, JSON.stringify(live.payload));
|
|
304
|
-
assert.equal(live.payload?.ok, true, JSON.stringify(live.payload));
|
|
305
|
-
|
|
306
|
-
let frames = [];
|
|
307
|
-
try {
|
|
308
|
-
frames = await collectLiveFrames(ws, device.deviceId, SAMPLE_MS);
|
|
309
|
-
} catch (error) {
|
|
310
|
-
const hostStatus = await fetchJson(`${hostBridge.baseUrl}/api/remote/status`).catch(() => null);
|
|
311
|
-
const hostDevices = await fetchJson(`${hostBridge.baseUrl}/api/remote/devices`).catch(() => null);
|
|
312
|
-
const clientAgent = await fetchJson(`${clientBridge.baseUrl}/api/remote/agent/status`).catch(() => null);
|
|
313
|
-
error.message = `${error.message}\n` +
|
|
314
|
-
`hostStatus=${JSON.stringify(hostStatus?.payload || null)}\n` +
|
|
315
|
-
`hostDevices=${JSON.stringify(hostDevices?.payload || null)}\n` +
|
|
316
|
-
`clientAgent=${JSON.stringify(clientAgent?.payload || null)}\n` +
|
|
317
|
-
`${hostBridge.details()}\n${clientBridge.details()}`;
|
|
318
|
-
throw error;
|
|
319
|
-
}
|
|
320
|
-
const summary = summarizeFrames(frames);
|
|
321
|
-
assert.ok(summary.frames >= 2, JSON.stringify(summary));
|
|
322
|
-
assert.equal(summary.uniqueSeqs, summary.frames, `frameSeq should advance for each binary frame: ${JSON.stringify(summary)}`);
|
|
323
|
-
assert.ok(summary.minBytes > 0, `binary payload should be non-empty: ${JSON.stringify(summary)}`);
|
|
324
|
-
assert.equal(summary.metadataFps, REQUESTED_FPS, JSON.stringify(summary));
|
|
325
|
-
assert.equal(summary.mode, 'remote-fast', JSON.stringify(summary));
|
|
326
|
-
assert.ok(summary.fps >= MIN_EFFECTIVE_FPS, `RemoteFast live-rate too low: ${JSON.stringify(summary)}`);
|
|
327
|
-
|
|
328
|
-
console.log(`RemoteFast live-rate smoke OK (${summary.frames} frames, ${summary.fps.toFixed(1)} fps, bytes ${summary.minBytes}-${summary.maxBytes})`);
|
|
329
|
-
} finally {
|
|
330
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
331
|
-
ws.close();
|
|
332
|
-
}
|
|
333
|
-
if (clientBridge) {
|
|
334
|
-
await clientBridge.stop();
|
|
335
|
-
}
|
|
336
|
-
if (hostBridge) {
|
|
337
|
-
await hostBridge.stop();
|
|
338
|
-
}
|
|
339
|
-
await rm(tempRoot, { recursive: true, force: true });
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
await main();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
6
|
+
import net from 'node:net';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { WebSocket } from 'ws';
|
|
11
|
+
|
|
12
|
+
const BRIDGE_TOKEN = 'remote-fast-live-rate-smoke-token';
|
|
13
|
+
const PAIR_TOKEN = 'remote-fast-live-rate-pair-token';
|
|
14
|
+
const LEASE_ID = 'remote-fast-live-rate-lease';
|
|
15
|
+
const LOCAL_BRIDGE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
16
|
+
const REQUESTED_FPS = Number(process.env.MINDEXEC_REMOTE_LIVE_RATE_REQUEST_FPS || 12);
|
|
17
|
+
const SAMPLE_MS = Number(process.env.MINDEXEC_REMOTE_LIVE_RATE_SAMPLE_MS || 3500);
|
|
18
|
+
const MIN_EFFECTIVE_FPS = Number(process.env.MINDEXEC_REMOTE_LIVE_RATE_MIN_FPS || 10);
|
|
19
|
+
|
|
20
|
+
function wait(ms) {
|
|
21
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function findFreePort() {
|
|
25
|
+
return await new Promise((resolve, reject) => {
|
|
26
|
+
const server = net.createServer();
|
|
27
|
+
server.unref();
|
|
28
|
+
server.once('error', reject);
|
|
29
|
+
server.listen(0, '127.0.0.1', () => {
|
|
30
|
+
const address = server.address();
|
|
31
|
+
const port = typeof address === 'object' && address ? address.port : 0;
|
|
32
|
+
server.close(() => resolve(port));
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function fetchJson(url, options = {}) {
|
|
38
|
+
const response = await fetch(url, {
|
|
39
|
+
...options,
|
|
40
|
+
headers: {
|
|
41
|
+
...(options.body ? { 'Content-Type': 'application/json' } : {}),
|
|
42
|
+
'X-Bridge-Token': BRIDGE_TOKEN,
|
|
43
|
+
...(options.headers || {})
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
let payload = null;
|
|
48
|
+
try {
|
|
49
|
+
payload = await response.json();
|
|
50
|
+
} catch {
|
|
51
|
+
// Some failure responses may be empty.
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { status: response.status, ok: response.ok, payload };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function waitFor(predicate, timeoutMs = 30000, label = 'condition') {
|
|
58
|
+
const startedAt = Date.now();
|
|
59
|
+
let lastError = null;
|
|
60
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
61
|
+
try {
|
|
62
|
+
const value = await predicate();
|
|
63
|
+
if (value) {
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
lastError = error;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await wait(100);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
throw new Error(`Timed out waiting for ${label}${lastError ? `: ${lastError.message}` : ''}.`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function spawnBridge({ bridgePort, remoteHubPort, workspacePath, label, traceFrames = false }) {
|
|
77
|
+
const child = spawn(process.execPath, ['server.js'], {
|
|
78
|
+
cwd: LOCAL_BRIDGE_DIR,
|
|
79
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
80
|
+
windowsHide: true,
|
|
81
|
+
env: {
|
|
82
|
+
...process.env,
|
|
83
|
+
BRIDGE_PORT: String(bridgePort),
|
|
84
|
+
BRIDGE_TOKEN,
|
|
85
|
+
BRIDGE_REQUIRE_TOKEN: '1',
|
|
86
|
+
MINDEXEC_REMOTE_HUB: '1',
|
|
87
|
+
MINDEXEC_REMOTE_TRACE_FRAMES: traceFrames ? '1' : '',
|
|
88
|
+
MINDEXEC_REMOTE_REGISTRY_FOLLOWER: '0',
|
|
89
|
+
MINDEXEC_REMOTE_REGISTRY_REALTIME: '0',
|
|
90
|
+
MINDEXEC_REMOTE_HOST_TARGET_AUTO_RENEW: '0',
|
|
91
|
+
REMOTE_HUB_HOST: '127.0.0.1',
|
|
92
|
+
REMOTE_HUB_PORT: String(remoteHubPort),
|
|
93
|
+
REMOTE_HUB_PAIR_TOKEN: PAIR_TOKEN,
|
|
94
|
+
WORKSPACE_PATH: workspacePath,
|
|
95
|
+
NO_COLOR: '1'
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
let stdout = '';
|
|
100
|
+
let stderr = '';
|
|
101
|
+
child.stdout.on('data', chunk => {
|
|
102
|
+
stdout += chunk.toString();
|
|
103
|
+
});
|
|
104
|
+
child.stderr.on('data', chunk => {
|
|
105
|
+
stderr += chunk.toString();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const exitPromise = new Promise(resolve => child.once('exit', resolve));
|
|
109
|
+
const details = () => `${label} stdout=${stdout}\n${label} stderr=${stderr}`;
|
|
110
|
+
const stop = async () => {
|
|
111
|
+
if (child.exitCode === null && !child.killed) {
|
|
112
|
+
child.kill('SIGTERM');
|
|
113
|
+
await Promise.race([exitPromise, wait(5000)]);
|
|
114
|
+
if (child.exitCode === null && !child.killed) {
|
|
115
|
+
child.kill('SIGKILL');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
baseUrl: `http://127.0.0.1:${bridgePort}`,
|
|
122
|
+
child,
|
|
123
|
+
details,
|
|
124
|
+
stop
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function waitForBridge(bridge) {
|
|
129
|
+
return await waitFor(async () => {
|
|
130
|
+
const result = await fetchJson(`${bridge.baseUrl}/api/status`);
|
|
131
|
+
return result.ok && result.payload?.status === 'ok' ? result.payload : null;
|
|
132
|
+
}, 30000, `bridge startup\n${bridge.details()}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseFramePacket(data) {
|
|
136
|
+
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
137
|
+
if (buffer.length <= 8 || buffer[0] === 0x7b || buffer[0] === 0x5b) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const metaLength = buffer.readUInt32BE(0);
|
|
142
|
+
if (metaLength <= 0 || metaLength > 64 * 1024 || 4 + metaLength >= buffer.length) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const metadata = JSON.parse(buffer.subarray(4, 4 + metaLength).toString('utf8'));
|
|
147
|
+
const payload = buffer.subarray(4 + metaLength);
|
|
148
|
+
if (String(metadata?.type || '').toLowerCase() !== 'remote.frame.binary') {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { metadata, payload, receivedAtMs: Date.now() };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function openFrameSocket(baseUrl, deviceId) {
|
|
156
|
+
const ws = new WebSocket(`${baseUrl.replace(/^http:/, 'ws:')}/api/remote/frames/ws?token=${encodeURIComponent(BRIDGE_TOKEN)}`);
|
|
157
|
+
await new Promise((resolve, reject) => {
|
|
158
|
+
ws.once('open', resolve);
|
|
159
|
+
ws.once('error', reject);
|
|
160
|
+
});
|
|
161
|
+
ws.send(JSON.stringify({ type: 'subscribe', deviceIds: [deviceId] }));
|
|
162
|
+
return ws;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function collectLiveFrames(ws, deviceId, sampleMs) {
|
|
166
|
+
const frames = [];
|
|
167
|
+
let firstFrameAt = 0;
|
|
168
|
+
let settle = null;
|
|
169
|
+
let settled = false;
|
|
170
|
+
|
|
171
|
+
const finish = () => {
|
|
172
|
+
if (settled) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
settled = true;
|
|
176
|
+
settle?.();
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const done = new Promise((resolve, reject) => {
|
|
180
|
+
settle = resolve;
|
|
181
|
+
const timeout = setTimeout(() => reject(new Error('timed out waiting for RemoteFast binary frames')), 25000);
|
|
182
|
+
ws.on('message', data => {
|
|
183
|
+
const frame = parseFramePacket(data);
|
|
184
|
+
if (!frame
|
|
185
|
+
|| String(frame.metadata.kind || '').toLowerCase() !== 'live'
|
|
186
|
+
|| String(frame.metadata.deviceId || '') !== deviceId) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
frames.push(frame);
|
|
191
|
+
if (!firstFrameAt) {
|
|
192
|
+
firstFrameAt = frame.receivedAtMs;
|
|
193
|
+
}
|
|
194
|
+
if (frame.receivedAtMs - firstFrameAt >= sampleMs) {
|
|
195
|
+
clearTimeout(timeout);
|
|
196
|
+
finish();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
ws.once('close', () => {
|
|
200
|
+
clearTimeout(timeout);
|
|
201
|
+
if (!settled) {
|
|
202
|
+
reject(new Error('frame websocket closed during live-rate sample'));
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
ws.once('error', error => {
|
|
206
|
+
clearTimeout(timeout);
|
|
207
|
+
reject(error);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
await done;
|
|
212
|
+
return frames;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function summarizeFrames(frames) {
|
|
216
|
+
const first = frames[0];
|
|
217
|
+
const last = frames[frames.length - 1];
|
|
218
|
+
const durationMs = Math.max(1, Number(last?.receivedAtMs || 0) - Number(first?.receivedAtMs || 0));
|
|
219
|
+
const fps = frames.length > 1 ? ((frames.length - 1) * 1000) / durationMs : 0;
|
|
220
|
+
const seqs = frames.map(frame => Number(frame.metadata.frameSeq || 0)).filter(Number.isFinite);
|
|
221
|
+
const uniqueSeqs = new Set(seqs);
|
|
222
|
+
const byteLengths = frames.map(frame => Number(frame.payload?.length || 0)).filter(value => value > 0);
|
|
223
|
+
return {
|
|
224
|
+
frames: frames.length,
|
|
225
|
+
uniqueSeqs: uniqueSeqs.size,
|
|
226
|
+
durationMs,
|
|
227
|
+
fps,
|
|
228
|
+
firstSeq: seqs[0] || 0,
|
|
229
|
+
lastSeq: seqs[seqs.length - 1] || 0,
|
|
230
|
+
minBytes: byteLengths.length > 0 ? Math.min(...byteLengths) : 0,
|
|
231
|
+
maxBytes: byteLengths.length > 0 ? Math.max(...byteLengths) : 0,
|
|
232
|
+
metadataFps: Number(last?.metadata?.fps || 0) || 0,
|
|
233
|
+
mode: String(last?.metadata?.mode || '')
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function main() {
|
|
238
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'mindexec-remote-fast-live-rate-'));
|
|
239
|
+
const hostWorkspace = path.join(tempRoot, 'host');
|
|
240
|
+
const clientWorkspace = path.join(tempRoot, 'client');
|
|
241
|
+
const hostBridgePort = await findFreePort();
|
|
242
|
+
const hostRemotePort = await findFreePort();
|
|
243
|
+
const clientBridgePort = await findFreePort();
|
|
244
|
+
const clientRemotePort = await findFreePort();
|
|
245
|
+
const managerEndpoint = `127.0.0.1:${hostRemotePort}`;
|
|
246
|
+
let hostBridge = null;
|
|
247
|
+
let clientBridge = null;
|
|
248
|
+
let ws = null;
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
hostBridge = spawnBridge({
|
|
252
|
+
bridgePort: hostBridgePort,
|
|
253
|
+
remoteHubPort: hostRemotePort,
|
|
254
|
+
workspacePath: hostWorkspace,
|
|
255
|
+
label: 'host'
|
|
256
|
+
});
|
|
257
|
+
await waitForBridge(hostBridge);
|
|
258
|
+
|
|
259
|
+
clientBridge = spawnBridge({
|
|
260
|
+
bridgePort: clientBridgePort,
|
|
261
|
+
remoteHubPort: clientRemotePort,
|
|
262
|
+
workspacePath: clientWorkspace,
|
|
263
|
+
label: 'client',
|
|
264
|
+
traceFrames: true
|
|
265
|
+
});
|
|
266
|
+
await waitForBridge(clientBridge);
|
|
267
|
+
|
|
268
|
+
const connectResult = await fetchJson(`${clientBridge.baseUrl}/api/remote/agent/connect`, {
|
|
269
|
+
method: 'POST',
|
|
270
|
+
body: JSON.stringify({
|
|
271
|
+
managerCandidates: [managerEndpoint],
|
|
272
|
+
pairToken: PAIR_TOKEN,
|
|
273
|
+
leaseId: LEASE_ID,
|
|
274
|
+
nodeId: 'remote-fast-live-rate-node',
|
|
275
|
+
engine: 'auto',
|
|
276
|
+
source: 'live-rate-smoke'
|
|
277
|
+
})
|
|
278
|
+
});
|
|
279
|
+
assert.equal(connectResult.ok, true, JSON.stringify(connectResult.payload));
|
|
280
|
+
assert.equal(connectResult.payload?.ok, true, JSON.stringify(connectResult.payload));
|
|
281
|
+
assert.equal(connectResult.payload?.agent?.usingNpx, false, JSON.stringify(connectResult.payload?.agent));
|
|
282
|
+
assert.match(String(connectResult.payload?.agent?.launcher || ''), /mindexec-remote-fast/i);
|
|
283
|
+
|
|
284
|
+
const device = await waitFor(async () => {
|
|
285
|
+
const result = await fetchJson(`${hostBridge.baseUrl}/api/remote/devices`);
|
|
286
|
+
return result.payload?.devices?.find(item =>
|
|
287
|
+
item.connected === true
|
|
288
|
+
&& item.capabilities?.liveStream === true
|
|
289
|
+
&& item.capabilities?.binaryFrames === true) || null;
|
|
290
|
+
}, 15000, `RemoteFast device registration\n${hostBridge.details()}\n${clientBridge.details()}`);
|
|
291
|
+
|
|
292
|
+
ws = await openFrameSocket(hostBridge.baseUrl, device.deviceId);
|
|
293
|
+
const live = await fetchJson(`${hostBridge.baseUrl}/api/remote/devices/${encodeURIComponent(device.deviceId)}/live/start`, {
|
|
294
|
+
method: 'POST',
|
|
295
|
+
body: JSON.stringify({
|
|
296
|
+
mode: 'remote-fast',
|
|
297
|
+
fps: REQUESTED_FPS,
|
|
298
|
+
maxWidth: 960,
|
|
299
|
+
maxHeight: 540,
|
|
300
|
+
quality: 60
|
|
301
|
+
})
|
|
302
|
+
});
|
|
303
|
+
assert.equal(live.ok, true, JSON.stringify(live.payload));
|
|
304
|
+
assert.equal(live.payload?.ok, true, JSON.stringify(live.payload));
|
|
305
|
+
|
|
306
|
+
let frames = [];
|
|
307
|
+
try {
|
|
308
|
+
frames = await collectLiveFrames(ws, device.deviceId, SAMPLE_MS);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
const hostStatus = await fetchJson(`${hostBridge.baseUrl}/api/remote/status`).catch(() => null);
|
|
311
|
+
const hostDevices = await fetchJson(`${hostBridge.baseUrl}/api/remote/devices`).catch(() => null);
|
|
312
|
+
const clientAgent = await fetchJson(`${clientBridge.baseUrl}/api/remote/agent/status`).catch(() => null);
|
|
313
|
+
error.message = `${error.message}\n` +
|
|
314
|
+
`hostStatus=${JSON.stringify(hostStatus?.payload || null)}\n` +
|
|
315
|
+
`hostDevices=${JSON.stringify(hostDevices?.payload || null)}\n` +
|
|
316
|
+
`clientAgent=${JSON.stringify(clientAgent?.payload || null)}\n` +
|
|
317
|
+
`${hostBridge.details()}\n${clientBridge.details()}`;
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
const summary = summarizeFrames(frames);
|
|
321
|
+
assert.ok(summary.frames >= 2, JSON.stringify(summary));
|
|
322
|
+
assert.equal(summary.uniqueSeqs, summary.frames, `frameSeq should advance for each binary frame: ${JSON.stringify(summary)}`);
|
|
323
|
+
assert.ok(summary.minBytes > 0, `binary payload should be non-empty: ${JSON.stringify(summary)}`);
|
|
324
|
+
assert.equal(summary.metadataFps, REQUESTED_FPS, JSON.stringify(summary));
|
|
325
|
+
assert.equal(summary.mode, 'remote-fast', JSON.stringify(summary));
|
|
326
|
+
assert.ok(summary.fps >= MIN_EFFECTIVE_FPS, `RemoteFast live-rate too low: ${JSON.stringify(summary)}`);
|
|
327
|
+
|
|
328
|
+
console.log(`RemoteFast live-rate smoke OK (${summary.frames} frames, ${summary.fps.toFixed(1)} fps, bytes ${summary.minBytes}-${summary.maxBytes})`);
|
|
329
|
+
} finally {
|
|
330
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
331
|
+
ws.close();
|
|
332
|
+
}
|
|
333
|
+
if (clientBridge) {
|
|
334
|
+
await clientBridge.stop();
|
|
335
|
+
}
|
|
336
|
+
if (hostBridge) {
|
|
337
|
+
await hostBridge.stop();
|
|
338
|
+
}
|
|
339
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
await main();
|