@mindexec/cli 0.2.393 → 0.2.395
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/package.json +1 -1
- package/remote-fast/osx-arm64/mindexec-remote-fast.dll +0 -0
- 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 +438 -15
- package/scripts/remote-agent-ws-smoke.mjs +69 -2
- package/scripts/remote-fast-live-rate-smoke.mjs +3 -0
- package/scripts/remote-hub-smoke.mjs +111 -20
- package/server.js +11 -1
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/remote-hub.js
CHANGED
|
@@ -20,9 +20,77 @@ const RECENT_THUMBNAIL_FRAME_CACHE_LIMIT = 4;
|
|
|
20
20
|
const RECENT_LIVE_FRAME_CACHE_LIMIT = 80;
|
|
21
21
|
const RECENT_THUMBNAIL_FRAME_CACHE_MAX_BYTES = 3 * 1024 * 1024;
|
|
22
22
|
const RECENT_LIVE_FRAME_CACHE_MAX_BYTES = 24 * 1024 * 1024;
|
|
23
|
-
const REMOTE_PROTOCOL_VERSION =
|
|
23
|
+
const REMOTE_PROTOCOL_VERSION = 2;
|
|
24
|
+
const REMOTE_AGENT_PROTOCOL = 'mindexec.remote.agent';
|
|
25
|
+
const REMOTE_FRAME_PROTOCOL = 'mindexec.remote.frame.v2';
|
|
26
|
+
const REMOTE_FRAME_PROTOCOL_VERSION = 2;
|
|
27
|
+
const REMOTE_FRAME_HANDSHAKE = 'smartview-request-open-frame';
|
|
28
|
+
const DEFAULT_REMOTE_FRAME_MODE = 'remote-fast';
|
|
29
|
+
const REMOTE_FRAME_MODE_PROFILES = Object.freeze({
|
|
30
|
+
thumbnail: Object.freeze({
|
|
31
|
+
mode: 'thumbnail',
|
|
32
|
+
label: 'Thumbnail PNG/JPEG Binary',
|
|
33
|
+
transport: 'ws-binary',
|
|
34
|
+
encoding: 'image',
|
|
35
|
+
codec: 'image',
|
|
36
|
+
compression: 'image/*',
|
|
37
|
+
profile: 'thumbnail-binary-v1',
|
|
38
|
+
modeVersion: 1,
|
|
39
|
+
implemented: true
|
|
40
|
+
}),
|
|
41
|
+
'remote-fast': Object.freeze({
|
|
42
|
+
mode: 'remote-fast',
|
|
43
|
+
label: 'RemoteFast JPEG Binary',
|
|
44
|
+
transport: 'ws-binary',
|
|
45
|
+
encoding: 'image',
|
|
46
|
+
codec: 'jpeg',
|
|
47
|
+
compression: 'image/jpeg',
|
|
48
|
+
profile: 'jpeg-binary-v1',
|
|
49
|
+
modeVersion: 1,
|
|
50
|
+
implemented: true
|
|
51
|
+
}),
|
|
52
|
+
'remote-quality': Object.freeze({
|
|
53
|
+
mode: 'remote-quality',
|
|
54
|
+
label: 'RemoteQuality JPEG Binary',
|
|
55
|
+
transport: 'ws-binary',
|
|
56
|
+
encoding: 'image',
|
|
57
|
+
codec: 'jpeg',
|
|
58
|
+
compression: 'image/jpeg',
|
|
59
|
+
profile: 'jpeg-quality-binary-v1',
|
|
60
|
+
modeVersion: 1,
|
|
61
|
+
implemented: true
|
|
62
|
+
}),
|
|
63
|
+
'video-codec': Object.freeze({
|
|
64
|
+
mode: 'video-codec',
|
|
65
|
+
label: 'Video Codec Stream',
|
|
66
|
+
transport: 'ws-binary',
|
|
67
|
+
encoding: 'video',
|
|
68
|
+
codec: 'video',
|
|
69
|
+
compression: 'video/*',
|
|
70
|
+
profile: 'future-video-codec',
|
|
71
|
+
modeVersion: 0,
|
|
72
|
+
implemented: false
|
|
73
|
+
})
|
|
74
|
+
});
|
|
75
|
+
const REMOTE_FRAME_MODE_ALIASES = new Map([
|
|
76
|
+
['fast', 'remote-fast'],
|
|
77
|
+
['remotefast', 'remote-fast'],
|
|
78
|
+
['remote_fast', 'remote-fast'],
|
|
79
|
+
['remote-fast', 'remote-fast'],
|
|
80
|
+
['quality', 'remote-quality'],
|
|
81
|
+
['remotequality', 'remote-quality'],
|
|
82
|
+
['remote_quality', 'remote-quality'],
|
|
83
|
+
['remote-quality', 'remote-quality'],
|
|
84
|
+
['thumb', 'thumbnail'],
|
|
85
|
+
['thumbnail', 'thumbnail'],
|
|
86
|
+
['video', 'video-codec'],
|
|
87
|
+
['codec', 'video-codec'],
|
|
88
|
+
['video-codec', 'video-codec']
|
|
89
|
+
]);
|
|
24
90
|
const MAX_SYNTHETIC_DEVICES = 1000;
|
|
25
91
|
const DEFAULT_HOST_TARGET_LEASE_MS = 30000;
|
|
92
|
+
const DUPLICATE_DEVICE_ACTIVE_REJECT_MS = 15000;
|
|
93
|
+
const DUPLICATE_DEVICE_LOG_THROTTLE_MS = 5000;
|
|
26
94
|
const SYNTHETIC_FRAME_DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAADElEQVR42mP8z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC';
|
|
27
95
|
const SYNTHETIC_FRAME_PAYLOAD = Buffer.from(SYNTHETIC_FRAME_DATA_URL.split(',')[1], 'base64');
|
|
28
96
|
const SYNTHETIC_FRAME_HASH = crypto.createHash('sha256').update(SYNTHETIC_FRAME_PAYLOAD).digest('hex').slice(0, 16);
|
|
@@ -56,6 +124,115 @@ function safeString(value, maxLength = 200) {
|
|
|
56
124
|
return String(value ?? '').replace(/[\r\n\t]/g, ' ').trim().slice(0, maxLength);
|
|
57
125
|
}
|
|
58
126
|
|
|
127
|
+
function normalizeRemoteFrameMode(value, fallback = DEFAULT_REMOTE_FRAME_MODE) {
|
|
128
|
+
const requested = safeString(value, 80).toLowerCase();
|
|
129
|
+
const fallbackMode = REMOTE_FRAME_MODE_PROFILES[fallback] ? fallback : DEFAULT_REMOTE_FRAME_MODE;
|
|
130
|
+
if (!requested) {
|
|
131
|
+
return fallbackMode;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const normalized = REMOTE_FRAME_MODE_ALIASES.get(requested) || requested;
|
|
135
|
+
const profile = REMOTE_FRAME_MODE_PROFILES[normalized];
|
|
136
|
+
if (!profile || profile.implemented !== true) {
|
|
137
|
+
return fallbackMode;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return normalized;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function serializeRemoteFrameModeProfile(profile) {
|
|
144
|
+
return {
|
|
145
|
+
mode: profile.mode,
|
|
146
|
+
label: profile.label,
|
|
147
|
+
transport: profile.transport,
|
|
148
|
+
encoding: profile.encoding,
|
|
149
|
+
codec: profile.codec,
|
|
150
|
+
compression: profile.compression,
|
|
151
|
+
profile: profile.profile,
|
|
152
|
+
modeVersion: profile.modeVersion,
|
|
153
|
+
implemented: profile.implemented === true
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function getRemoteFrameModeProfile(value, fallback = DEFAULT_REMOTE_FRAME_MODE) {
|
|
158
|
+
const mode = normalizeRemoteFrameMode(value, fallback);
|
|
159
|
+
return serializeRemoteFrameModeProfile(REMOTE_FRAME_MODE_PROFILES[mode]);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function getSupportedRemoteFrameModeProfiles() {
|
|
163
|
+
return Object.values(REMOTE_FRAME_MODE_PROFILES)
|
|
164
|
+
.filter(profile => profile.implemented === true)
|
|
165
|
+
.map(serializeRemoteFrameModeProfile);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function buildRemoteFrameProtocolDescriptor() {
|
|
169
|
+
return {
|
|
170
|
+
protocol: REMOTE_FRAME_PROTOCOL,
|
|
171
|
+
version: REMOTE_FRAME_PROTOCOL_VERSION,
|
|
172
|
+
handshake: REMOTE_FRAME_HANDSHAKE,
|
|
173
|
+
defaultMode: DEFAULT_REMOTE_FRAME_MODE,
|
|
174
|
+
modes: getSupportedRemoteFrameModeProfiles()
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildRemoteFrameTransferDescriptor(value, fallback = DEFAULT_REMOTE_FRAME_MODE) {
|
|
179
|
+
const profile = getRemoteFrameModeProfile(value, fallback);
|
|
180
|
+
return {
|
|
181
|
+
mode: profile.mode,
|
|
182
|
+
frameMode: profile.mode,
|
|
183
|
+
frameProfile: profile.profile,
|
|
184
|
+
encoding: profile.encoding,
|
|
185
|
+
codec: profile.codec,
|
|
186
|
+
compression: profile.compression,
|
|
187
|
+
transferProtocol: REMOTE_FRAME_PROTOCOL,
|
|
188
|
+
transferProtocolVersion: REMOTE_FRAME_PROTOCOL_VERSION,
|
|
189
|
+
handshake: REMOTE_FRAME_HANDSHAKE
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function buildRemoteFrameTransferDescriptorFromMessage(message = {}, fallback = DEFAULT_REMOTE_FRAME_MODE) {
|
|
194
|
+
const descriptor = buildRemoteFrameTransferDescriptor(message.frameMode || message.mode, fallback);
|
|
195
|
+
return {
|
|
196
|
+
...descriptor,
|
|
197
|
+
frameProfile: safeString(message.frameProfile, 80) || descriptor.frameProfile,
|
|
198
|
+
encoding: safeString(message.encoding, 40) || descriptor.encoding,
|
|
199
|
+
codec: safeString(message.codec, 40) || descriptor.codec,
|
|
200
|
+
compression: safeString(message.compression || message.mimeType || message.format, 80) || descriptor.compression,
|
|
201
|
+
transferProtocol: safeString(message.transferProtocol, 80) || descriptor.transferProtocol,
|
|
202
|
+
transferProtocolVersion: Number.isFinite(Number(message.transferProtocolVersion))
|
|
203
|
+
? Math.max(1, Math.floor(Number(message.transferProtocolVersion)))
|
|
204
|
+
: descriptor.transferProtocolVersion,
|
|
205
|
+
handshake: safeString(message.handshake, 80) || descriptor.handshake
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function normalizeIncomingFrameModeProfile(value) {
|
|
210
|
+
if (!value) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (typeof value === 'string') {
|
|
215
|
+
return getRemoteFrameModeProfile(value);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (typeof value !== 'object') {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const profile = buildRemoteFrameTransferDescriptor(value.mode || value.frameMode || value.profile);
|
|
223
|
+
return {
|
|
224
|
+
mode: profile.mode,
|
|
225
|
+
label: safeString(value.label, 80) || profile.mode,
|
|
226
|
+
transport: safeString(value.transport, 40) || 'ws-binary',
|
|
227
|
+
encoding: safeString(value.encoding, 40) || profile.encoding,
|
|
228
|
+
codec: safeString(value.codec, 40) || profile.codec,
|
|
229
|
+
compression: safeString(value.compression, 80) || profile.compression,
|
|
230
|
+
profile: safeString(value.profile || value.frameProfile, 80) || profile.frameProfile,
|
|
231
|
+
modeVersion: Number.isFinite(Number(value.modeVersion)) ? Math.max(0, Math.floor(Number(value.modeVersion))) : 1,
|
|
232
|
+
implemented: value.implemented !== false
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
59
236
|
function isLoopbackHost(value) {
|
|
60
237
|
const host = String(value || '').trim().toLowerCase();
|
|
61
238
|
return host === 'localhost'
|
|
@@ -540,6 +717,12 @@ function serializeDevice(device, options = {}) {
|
|
|
540
717
|
arch: device.arch,
|
|
541
718
|
pid: device.pid,
|
|
542
719
|
agentVersion: device.agentVersion,
|
|
720
|
+
protocol: device.protocol,
|
|
721
|
+
protocolVersion: device.protocolVersion,
|
|
722
|
+
frameProtocol: device.frameProtocol ? { ...device.frameProtocol } : null,
|
|
723
|
+
frameModes: Array.isArray(device.frameModes)
|
|
724
|
+
? device.frameModes.map(profile => ({ ...profile }))
|
|
725
|
+
: [],
|
|
543
726
|
capabilities: { ...device.capabilities },
|
|
544
727
|
connected: device.connected,
|
|
545
728
|
connectedAt: device.connectedAt,
|
|
@@ -882,6 +1065,7 @@ export function createRemoteHub(options = {}) {
|
|
|
882
1065
|
const taskBatches = new Map();
|
|
883
1066
|
const sockets = new Map();
|
|
884
1067
|
const allSockets = new Set();
|
|
1068
|
+
const duplicateDeviceLogAt = new Map();
|
|
885
1069
|
let server = null;
|
|
886
1070
|
let started = false;
|
|
887
1071
|
let boundPort = requestedPort;
|
|
@@ -1083,6 +1267,9 @@ export function createRemoteHub(options = {}) {
|
|
|
1083
1267
|
port: boundPort || requestedPort,
|
|
1084
1268
|
protocol: 'tcp-jsonl',
|
|
1085
1269
|
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
1270
|
+
agentProtocol: REMOTE_AGENT_PROTOCOL,
|
|
1271
|
+
frameProtocol: buildRemoteFrameProtocolDescriptor(),
|
|
1272
|
+
frameModes: getSupportedRemoteFrameModeProfiles(),
|
|
1086
1273
|
heartbeatMs,
|
|
1087
1274
|
taskTimeoutMs,
|
|
1088
1275
|
managerPackage,
|
|
@@ -1180,6 +1367,7 @@ export function createRemoteHub(options = {}) {
|
|
|
1180
1367
|
|
|
1181
1368
|
function makeSyntheticFrame(device, streamId, mode = 'thumbnail', options = {}) {
|
|
1182
1369
|
const now = new Date().toISOString();
|
|
1370
|
+
const descriptor = buildRemoteFrameTransferDescriptor(mode, mode === 'thumbnail' ? 'thumbnail' : DEFAULT_REMOTE_FRAME_MODE);
|
|
1183
1371
|
const frameSeq = Number(device?.counters?.thumbnailFramesReceived || 0)
|
|
1184
1372
|
+ Number(device?.counters?.liveFramesReceived || 0)
|
|
1185
1373
|
+ 1;
|
|
@@ -1192,6 +1380,14 @@ export function createRemoteHub(options = {}) {
|
|
|
1192
1380
|
mimeType: 'image/png',
|
|
1193
1381
|
format: 'image/png',
|
|
1194
1382
|
mode,
|
|
1383
|
+
frameMode: descriptor.frameMode,
|
|
1384
|
+
frameProfile: descriptor.frameProfile,
|
|
1385
|
+
encoding: descriptor.encoding,
|
|
1386
|
+
codec: 'png',
|
|
1387
|
+
compression: 'image/png',
|
|
1388
|
+
transferProtocol: descriptor.transferProtocol,
|
|
1389
|
+
transferProtocolVersion: descriptor.transferProtocolVersion,
|
|
1390
|
+
handshake: descriptor.handshake,
|
|
1195
1391
|
fps: clampNumber(options.fps, 1, 24, mode === 'remote-fast' ? 20 : 1),
|
|
1196
1392
|
capturedAt: now,
|
|
1197
1393
|
receivedAt: now,
|
|
@@ -1236,7 +1432,7 @@ export function createRemoteHub(options = {}) {
|
|
|
1236
1432
|
return null;
|
|
1237
1433
|
}
|
|
1238
1434
|
|
|
1239
|
-
const frame = makeSyntheticFrame(device, streamId || device.activeLiveStream.streamId,
|
|
1435
|
+
const frame = makeSyntheticFrame(device, streamId || device.activeLiveStream.streamId, device.activeLiveStream.mode || DEFAULT_REMOTE_FRAME_MODE, {
|
|
1240
1436
|
commandId: options.commandId || device.activeLiveStream.commandId,
|
|
1241
1437
|
width: options.maxWidth || 960,
|
|
1242
1438
|
height: options.maxHeight || 540,
|
|
@@ -1281,6 +1477,63 @@ export function createRemoteHub(options = {}) {
|
|
|
1281
1477
|
existing.socket.destroy();
|
|
1282
1478
|
}
|
|
1283
1479
|
|
|
1480
|
+
function getDeviceActivityMs(device) {
|
|
1481
|
+
return Math.max(
|
|
1482
|
+
Date.parse(device?.lastSeenAt || '') || 0,
|
|
1483
|
+
Date.parse(device?.lastStatusAt || '') || 0,
|
|
1484
|
+
Date.parse(device?.connectedAt || '') || 0
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function isDeviceConnectionFresh(device, nowMs = Date.now()) {
|
|
1489
|
+
if (!device?.connected || !device.socket || device.socket.destroyed) {
|
|
1490
|
+
return false;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
const lastActivityMs = getDeviceActivityMs(device);
|
|
1494
|
+
if (!lastActivityMs) {
|
|
1495
|
+
return true;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
const freshnessMs = Math.max(DUPLICATE_DEVICE_ACTIVE_REJECT_MS, heartbeatMs * 3);
|
|
1499
|
+
return nowMs - lastActivityMs <= freshnessMs;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function rejectDuplicateActiveDevice(socket, existing, attemptedSessionId) {
|
|
1503
|
+
const retryAfterMs = Math.max(DUPLICATE_DEVICE_LOG_THROTTLE_MS, Math.min(30000, heartbeatMs));
|
|
1504
|
+
writeJsonLine(socket, {
|
|
1505
|
+
type: 'disconnect',
|
|
1506
|
+
reason: 'duplicate-device-active',
|
|
1507
|
+
retryAfterMs,
|
|
1508
|
+
activeSessionId: existing.sessionId,
|
|
1509
|
+
attemptedSessionId
|
|
1510
|
+
});
|
|
1511
|
+
|
|
1512
|
+
existing.counters.duplicateConnectionsRejected = (existing.counters.duplicateConnectionsRejected || 0) + 1;
|
|
1513
|
+
const nowMs = Date.now();
|
|
1514
|
+
const lastLoggedAt = duplicateDeviceLogAt.get(existing.deviceId) || 0;
|
|
1515
|
+
if (nowMs - lastLoggedAt >= DUPLICATE_DEVICE_LOG_THROTTLE_MS) {
|
|
1516
|
+
duplicateDeviceLogAt.set(existing.deviceId, nowMs);
|
|
1517
|
+
logWarn(
|
|
1518
|
+
'remote',
|
|
1519
|
+
`duplicate device connection suppressed ${existing.deviceName} (${existing.deviceId}); keeping active session ${existing.sessionId}`);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
try {
|
|
1523
|
+
socket.end?.();
|
|
1524
|
+
} catch {
|
|
1525
|
+
// Best-effort graceful close before hard destroy.
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
setTimeout(() => {
|
|
1529
|
+
try {
|
|
1530
|
+
socket.destroy?.();
|
|
1531
|
+
} catch {
|
|
1532
|
+
// Ignore destroy failures.
|
|
1533
|
+
}
|
|
1534
|
+
}, 25).unref?.();
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1284
1537
|
function clearSyntheticFleet() {
|
|
1285
1538
|
let removed = 0;
|
|
1286
1539
|
for (const [deviceId, device] of devices.entries()) {
|
|
@@ -1422,11 +1675,23 @@ export function createRemoteHub(options = {}) {
|
|
|
1422
1675
|
}
|
|
1423
1676
|
|
|
1424
1677
|
if (isLive) {
|
|
1678
|
+
const transfer = buildRemoteFrameTransferDescriptor(DEFAULT_REMOTE_FRAME_MODE);
|
|
1425
1679
|
device.activeLiveStream = {
|
|
1426
1680
|
streamId: `synthetic-live-${ordinal}`,
|
|
1427
1681
|
commandId: `synthetic-live-command-${ordinal}`,
|
|
1428
1682
|
active: true,
|
|
1429
|
-
|
|
1683
|
+
open: true,
|
|
1684
|
+
openedAt: seenAt,
|
|
1685
|
+
openTransport: 'synthetic',
|
|
1686
|
+
mode: transfer.mode,
|
|
1687
|
+
frameMode: transfer.frameMode,
|
|
1688
|
+
frameProfile: transfer.frameProfile,
|
|
1689
|
+
encoding: transfer.encoding,
|
|
1690
|
+
codec: transfer.codec,
|
|
1691
|
+
compression: transfer.compression,
|
|
1692
|
+
transferProtocol: transfer.transferProtocol,
|
|
1693
|
+
transferProtocolVersion: transfer.transferProtocolVersion,
|
|
1694
|
+
handshake: transfer.handshake,
|
|
1430
1695
|
fps: 20,
|
|
1431
1696
|
startedAt: seenAt,
|
|
1432
1697
|
stoppedAt: '',
|
|
@@ -1470,12 +1735,37 @@ export function createRemoteHub(options = {}) {
|
|
|
1470
1735
|
}
|
|
1471
1736
|
|
|
1472
1737
|
function attachDevice(socket, hello) {
|
|
1473
|
-
const
|
|
1738
|
+
const nowMs = Date.now();
|
|
1739
|
+
const now = new Date(nowMs).toISOString();
|
|
1474
1740
|
const sessionId = crypto.randomUUID();
|
|
1475
1741
|
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
1742
|
+
const existing = devices.get(deviceId);
|
|
1743
|
+
|
|
1744
|
+
if (isDeviceConnectionFresh(existing, nowMs)) {
|
|
1745
|
+
rejectDuplicateActiveDevice(socket, existing, sessionId);
|
|
1746
|
+
return null;
|
|
1747
|
+
}
|
|
1476
1748
|
|
|
1477
1749
|
closeExistingDeviceSocket(deviceId, sessionId);
|
|
1478
1750
|
|
|
1751
|
+
const frameProtocol = typeof hello.frameProtocol === 'object' && hello.frameProtocol
|
|
1752
|
+
? { ...hello.frameProtocol }
|
|
1753
|
+
: null;
|
|
1754
|
+
const frameModes = Array.isArray(hello.frameModes)
|
|
1755
|
+
? hello.frameModes.map(normalizeIncomingFrameModeProfile).filter(Boolean)
|
|
1756
|
+
: [];
|
|
1757
|
+
const capabilities = typeof hello.capabilities === 'object' && hello.capabilities
|
|
1758
|
+
? { ...hello.capabilities }
|
|
1759
|
+
: {};
|
|
1760
|
+
if (!capabilities.frameProtocol) {
|
|
1761
|
+
capabilities.frameProtocol = frameProtocol || buildRemoteFrameProtocolDescriptor();
|
|
1762
|
+
}
|
|
1763
|
+
if (!capabilities.frameModes) {
|
|
1764
|
+
capabilities.frameModes = frameModes.length > 0
|
|
1765
|
+
? frameModes.map(profile => ({ ...profile }))
|
|
1766
|
+
: getSupportedRemoteFrameModeProfiles();
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1479
1769
|
const device = {
|
|
1480
1770
|
socket,
|
|
1481
1771
|
deviceId,
|
|
@@ -1486,9 +1776,11 @@ export function createRemoteHub(options = {}) {
|
|
|
1486
1776
|
arch: safeString(hello.arch, 40),
|
|
1487
1777
|
pid: Number.isFinite(Number(hello.pid)) ? Number(hello.pid) : 0,
|
|
1488
1778
|
agentVersion: safeString(hello.agentVersion, 40),
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1779
|
+
protocol: safeString(hello.protocol, 80) || REMOTE_AGENT_PROTOCOL,
|
|
1780
|
+
protocolVersion: Number.isFinite(Number(hello.protocolVersion)) ? Math.max(1, Math.floor(Number(hello.protocolVersion))) : 1,
|
|
1781
|
+
frameProtocol: frameProtocol || buildRemoteFrameProtocolDescriptor(),
|
|
1782
|
+
frameModes: frameModes.length > 0 ? frameModes : getSupportedRemoteFrameModeProfiles(),
|
|
1783
|
+
capabilities,
|
|
1492
1784
|
connected: true,
|
|
1493
1785
|
connectedAt: now,
|
|
1494
1786
|
disconnectedAt: '',
|
|
@@ -1517,10 +1809,13 @@ export function createRemoteHub(options = {}) {
|
|
|
1517
1809
|
sockets.set(socket, deviceId);
|
|
1518
1810
|
writeJsonLine(socket, {
|
|
1519
1811
|
type: 'welcome',
|
|
1812
|
+
protocol: REMOTE_AGENT_PROTOCOL,
|
|
1520
1813
|
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
1521
1814
|
sessionId,
|
|
1522
1815
|
deviceId,
|
|
1523
1816
|
heartbeatMs,
|
|
1817
|
+
frameProtocol: buildRemoteFrameProtocolDescriptor(),
|
|
1818
|
+
frameModes: getSupportedRemoteFrameModeProfiles(),
|
|
1524
1819
|
serverTime: now
|
|
1525
1820
|
});
|
|
1526
1821
|
|
|
@@ -1956,6 +2251,7 @@ export function createRemoteHub(options = {}) {
|
|
|
1956
2251
|
const payload = buildFramePayloadBuffer(framePayload, frameData);
|
|
1957
2252
|
const contentHash = buildFrameContentHash(message, payload);
|
|
1958
2253
|
const sameContentStreak = computeSameContentStreak(device.latestThumbnail, contentHash);
|
|
2254
|
+
const transfer = buildRemoteFrameTransferDescriptorFromMessage(message, 'thumbnail');
|
|
1959
2255
|
device.latestThumbnail = {
|
|
1960
2256
|
streamId: safeString(message.streamId, 128) || 'thumbnail',
|
|
1961
2257
|
frameSeq,
|
|
@@ -1964,6 +2260,14 @@ export function createRemoteHub(options = {}) {
|
|
|
1964
2260
|
height: Number.isFinite(Number(message.height)) ? Number(message.height) : 0,
|
|
1965
2261
|
mimeType,
|
|
1966
2262
|
format: mimeType,
|
|
2263
|
+
frameMode: transfer.frameMode,
|
|
2264
|
+
frameProfile: transfer.frameProfile,
|
|
2265
|
+
encoding: transfer.encoding,
|
|
2266
|
+
codec: transfer.codec,
|
|
2267
|
+
compression: transfer.compression || mimeType,
|
|
2268
|
+
transferProtocol: transfer.transferProtocol,
|
|
2269
|
+
transferProtocolVersion: transfer.transferProtocolVersion,
|
|
2270
|
+
handshake: transfer.handshake,
|
|
1967
2271
|
capturedAt,
|
|
1968
2272
|
receivedAt: device.lastSeenAt,
|
|
1969
2273
|
byteLength,
|
|
@@ -1988,6 +2292,49 @@ export function createRemoteHub(options = {}) {
|
|
|
1988
2292
|
return true;
|
|
1989
2293
|
}
|
|
1990
2294
|
|
|
2295
|
+
function applyLiveStreamOpen(device, message, transport = 'json') {
|
|
2296
|
+
const streamId = safeString(message.streamId, 128) || 'live';
|
|
2297
|
+
if (!device.activeLiveStream?.active || device.activeLiveStream.streamId !== streamId) {
|
|
2298
|
+
emitRemoteEvent('RemoteLiveStreamOpenIgnored', device, {
|
|
2299
|
+
reason: 'stale-live-stream-open',
|
|
2300
|
+
streamId,
|
|
2301
|
+
transport
|
|
2302
|
+
});
|
|
2303
|
+
return false;
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
const transfer = buildRemoteFrameTransferDescriptorFromMessage(message, device.activeLiveStream.mode || DEFAULT_REMOTE_FRAME_MODE);
|
|
2307
|
+
const openedAt = safeString(message.openedAt, 80) || device.lastSeenAt || new Date().toISOString();
|
|
2308
|
+
device.activeLiveStream = {
|
|
2309
|
+
...device.activeLiveStream,
|
|
2310
|
+
open: true,
|
|
2311
|
+
openedAt,
|
|
2312
|
+
openTransport: transport,
|
|
2313
|
+
mode: transfer.mode,
|
|
2314
|
+
frameMode: transfer.frameMode,
|
|
2315
|
+
frameProfile: transfer.frameProfile,
|
|
2316
|
+
encoding: transfer.encoding,
|
|
2317
|
+
codec: transfer.codec,
|
|
2318
|
+
compression: transfer.compression,
|
|
2319
|
+
transferProtocol: transfer.transferProtocol,
|
|
2320
|
+
transferProtocolVersion: transfer.transferProtocolVersion,
|
|
2321
|
+
handshake: transfer.handshake,
|
|
2322
|
+
width: Number.isFinite(Number(message.width)) ? Number(message.width) : Number(device.activeLiveStream.width || 0),
|
|
2323
|
+
height: Number.isFinite(Number(message.height)) ? Number(message.height) : Number(device.activeLiveStream.height || 0),
|
|
2324
|
+
openedFrameSeq: Number.isFinite(Number(message.frameSeq)) ? Number(message.frameSeq) : Number(device.activeLiveStream.openedFrameSeq || 0)
|
|
2325
|
+
};
|
|
2326
|
+
emitRemoteEvent('RemoteLiveStreamOpened', device, {
|
|
2327
|
+
streamId,
|
|
2328
|
+
mode: transfer.mode,
|
|
2329
|
+
frameMode: transfer.frameMode,
|
|
2330
|
+
frameProfile: transfer.frameProfile,
|
|
2331
|
+
encoding: transfer.encoding,
|
|
2332
|
+
compression: transfer.compression,
|
|
2333
|
+
transport
|
|
2334
|
+
});
|
|
2335
|
+
return true;
|
|
2336
|
+
}
|
|
2337
|
+
|
|
1991
2338
|
function applyLiveFrame(device, message, framePayload, transport = 'json-base64') {
|
|
1992
2339
|
const frameData = Buffer.isBuffer(framePayload)
|
|
1993
2340
|
? ''
|
|
@@ -2024,6 +2371,21 @@ export function createRemoteHub(options = {}) {
|
|
|
2024
2371
|
const payload = buildFramePayloadBuffer(framePayload, frameData);
|
|
2025
2372
|
const contentHash = buildFrameContentHash(message, payload);
|
|
2026
2373
|
const sameContentStreak = computeSameContentStreak(device.latestLiveFrame, contentHash);
|
|
2374
|
+
const transfer = buildRemoteFrameTransferDescriptorFromMessage(message, device.activeLiveStream.mode || DEFAULT_REMOTE_FRAME_MODE);
|
|
2375
|
+
if (device.activeLiveStream.open !== true) {
|
|
2376
|
+
device.activeLiveStream.open = true;
|
|
2377
|
+
device.activeLiveStream.openedAt = device.lastSeenAt;
|
|
2378
|
+
device.activeLiveStream.openTransport = `${transport}-implicit`;
|
|
2379
|
+
device.activeLiveStream.mode = transfer.mode;
|
|
2380
|
+
device.activeLiveStream.frameMode = transfer.frameMode;
|
|
2381
|
+
device.activeLiveStream.frameProfile = transfer.frameProfile;
|
|
2382
|
+
device.activeLiveStream.encoding = transfer.encoding;
|
|
2383
|
+
device.activeLiveStream.codec = transfer.codec;
|
|
2384
|
+
device.activeLiveStream.compression = transfer.compression;
|
|
2385
|
+
device.activeLiveStream.transferProtocol = transfer.transferProtocol;
|
|
2386
|
+
device.activeLiveStream.transferProtocolVersion = transfer.transferProtocolVersion;
|
|
2387
|
+
device.activeLiveStream.handshake = transfer.handshake;
|
|
2388
|
+
}
|
|
2027
2389
|
device.latestLiveFrame = {
|
|
2028
2390
|
streamId,
|
|
2029
2391
|
frameSeq,
|
|
@@ -2032,7 +2394,15 @@ export function createRemoteHub(options = {}) {
|
|
|
2032
2394
|
height: Number.isFinite(Number(message.height)) ? Number(message.height) : 0,
|
|
2033
2395
|
mimeType,
|
|
2034
2396
|
format: mimeType,
|
|
2035
|
-
mode:
|
|
2397
|
+
mode: transfer.mode,
|
|
2398
|
+
frameMode: transfer.frameMode,
|
|
2399
|
+
frameProfile: transfer.frameProfile,
|
|
2400
|
+
encoding: transfer.encoding,
|
|
2401
|
+
codec: transfer.codec,
|
|
2402
|
+
compression: transfer.compression || mimeType,
|
|
2403
|
+
transferProtocol: transfer.transferProtocol,
|
|
2404
|
+
transferProtocolVersion: transfer.transferProtocolVersion,
|
|
2405
|
+
handshake: transfer.handshake,
|
|
2036
2406
|
fps: Number.isFinite(Number(message.fps)) ? Number(message.fps) : device.activeLiveStream.fps,
|
|
2037
2407
|
capturedAt,
|
|
2038
2408
|
receivedAt: device.lastSeenAt,
|
|
@@ -2056,6 +2426,15 @@ export function createRemoteHub(options = {}) {
|
|
|
2056
2426
|
device.activeLiveStream.lastFrameAt = device.lastSeenAt;
|
|
2057
2427
|
device.activeLiveStream.lastFrameSeq = frameSeq;
|
|
2058
2428
|
device.activeLiveStream.framesReceived = (device.activeLiveStream.framesReceived || 0) + 1;
|
|
2429
|
+
device.activeLiveStream.mode = transfer.mode;
|
|
2430
|
+
device.activeLiveStream.frameMode = transfer.frameMode;
|
|
2431
|
+
device.activeLiveStream.frameProfile = transfer.frameProfile;
|
|
2432
|
+
device.activeLiveStream.encoding = transfer.encoding;
|
|
2433
|
+
device.activeLiveStream.codec = transfer.codec;
|
|
2434
|
+
device.activeLiveStream.compression = transfer.compression;
|
|
2435
|
+
device.activeLiveStream.transferProtocol = transfer.transferProtocol;
|
|
2436
|
+
device.activeLiveStream.transferProtocolVersion = transfer.transferProtocolVersion;
|
|
2437
|
+
device.activeLiveStream.handshake = transfer.handshake;
|
|
2059
2438
|
device.counters.liveFramesReceived += 1;
|
|
2060
2439
|
emitRemoteEvent('RemoteFrameReceived', device, {
|
|
2061
2440
|
streamId,
|
|
@@ -2116,8 +2495,13 @@ export function createRemoteHub(options = {}) {
|
|
|
2116
2495
|
return;
|
|
2117
2496
|
}
|
|
2118
2497
|
|
|
2498
|
+
const device = attachDevice(socket, message);
|
|
2499
|
+
if (!device) {
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2119
2503
|
state.authenticated = true;
|
|
2120
|
-
state.device =
|
|
2504
|
+
state.device = device;
|
|
2121
2505
|
return;
|
|
2122
2506
|
}
|
|
2123
2507
|
|
|
@@ -2165,6 +2549,10 @@ export function createRemoteHub(options = {}) {
|
|
|
2165
2549
|
applyThumbnailFrame(device, message, message.data, 'json-base64');
|
|
2166
2550
|
break;
|
|
2167
2551
|
}
|
|
2552
|
+
case 'stream.open': {
|
|
2553
|
+
applyLiveStreamOpen(device, message, 'json');
|
|
2554
|
+
break;
|
|
2555
|
+
}
|
|
2168
2556
|
case 'stream.frame': {
|
|
2169
2557
|
applyLiveFrame(device, message, message.data, 'json-base64');
|
|
2170
2558
|
break;
|
|
@@ -2823,12 +3211,24 @@ export function createRemoteHub(options = {}) {
|
|
|
2823
3211
|
const streamId = safeString(options.streamId, 128) || `live-${Date.now()}`;
|
|
2824
3212
|
const fps = clampNumber(options.fps, 1, 24, 20);
|
|
2825
3213
|
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
3214
|
+
const transfer = buildRemoteFrameTransferDescriptor(options.mode || DEFAULT_REMOTE_FRAME_MODE);
|
|
2826
3215
|
device.counters.commandsSent += 1;
|
|
2827
3216
|
device.activeLiveStream = {
|
|
2828
3217
|
streamId,
|
|
2829
3218
|
commandId,
|
|
2830
3219
|
active: true,
|
|
2831
|
-
|
|
3220
|
+
open: true,
|
|
3221
|
+
openedAt: now,
|
|
3222
|
+
openTransport: 'synthetic',
|
|
3223
|
+
mode: transfer.mode,
|
|
3224
|
+
frameMode: transfer.frameMode,
|
|
3225
|
+
frameProfile: transfer.frameProfile,
|
|
3226
|
+
encoding: transfer.encoding,
|
|
3227
|
+
codec: transfer.codec,
|
|
3228
|
+
compression: transfer.compression,
|
|
3229
|
+
transferProtocol: transfer.transferProtocol,
|
|
3230
|
+
transferProtocolVersion: transfer.transferProtocolVersion,
|
|
3231
|
+
handshake: transfer.handshake,
|
|
2832
3232
|
fps,
|
|
2833
3233
|
startedAt: now,
|
|
2834
3234
|
stoppedAt: '',
|
|
@@ -2848,9 +3248,10 @@ export function createRemoteHub(options = {}) {
|
|
|
2848
3248
|
streamId,
|
|
2849
3249
|
commandId,
|
|
2850
3250
|
fps,
|
|
3251
|
+
mode: transfer.mode,
|
|
2851
3252
|
synthetic: true
|
|
2852
3253
|
});
|
|
2853
|
-
return { ok: true, commandId, streamId, fps, synthetic: true };
|
|
3254
|
+
return { ok: true, commandId, streamId, fps, mode: transfer.mode, frameMode: transfer.frameMode, synthetic: true };
|
|
2854
3255
|
}
|
|
2855
3256
|
|
|
2856
3257
|
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
@@ -2864,12 +3265,21 @@ export function createRemoteHub(options = {}) {
|
|
|
2864
3265
|
const streamId = safeString(options.streamId, 128) || `live-${Date.now()}`;
|
|
2865
3266
|
const fps = clampNumber(options.fps, 1, 24, 12);
|
|
2866
3267
|
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
3268
|
+
const transfer = buildRemoteFrameTransferDescriptor(options.mode || DEFAULT_REMOTE_FRAME_MODE);
|
|
2867
3269
|
const result = sendCommand(deviceId, {
|
|
2868
3270
|
command: 'stream.start',
|
|
2869
3271
|
commandId,
|
|
2870
3272
|
payload: {
|
|
2871
3273
|
streamId,
|
|
2872
|
-
mode:
|
|
3274
|
+
mode: transfer.mode,
|
|
3275
|
+
frameMode: transfer.frameMode,
|
|
3276
|
+
frameProfile: transfer.frameProfile,
|
|
3277
|
+
encoding: transfer.encoding,
|
|
3278
|
+
codec: transfer.codec,
|
|
3279
|
+
compression: transfer.compression,
|
|
3280
|
+
transferProtocol: transfer.transferProtocol,
|
|
3281
|
+
transferProtocolVersion: transfer.transferProtocolVersion,
|
|
3282
|
+
handshake: transfer.handshake,
|
|
2873
3283
|
fps,
|
|
2874
3284
|
maxWidth: clampNumber(options.maxWidth, 320, 2560, 960),
|
|
2875
3285
|
maxHeight: clampNumber(options.maxHeight, 180, 1440, 540),
|
|
@@ -2886,7 +3296,18 @@ export function createRemoteHub(options = {}) {
|
|
|
2886
3296
|
streamId,
|
|
2887
3297
|
commandId,
|
|
2888
3298
|
active: true,
|
|
2889
|
-
|
|
3299
|
+
open: false,
|
|
3300
|
+
openedAt: '',
|
|
3301
|
+
openTransport: '',
|
|
3302
|
+
mode: transfer.mode,
|
|
3303
|
+
frameMode: transfer.frameMode,
|
|
3304
|
+
frameProfile: transfer.frameProfile,
|
|
3305
|
+
encoding: transfer.encoding,
|
|
3306
|
+
codec: transfer.codec,
|
|
3307
|
+
compression: transfer.compression,
|
|
3308
|
+
transferProtocol: transfer.transferProtocol,
|
|
3309
|
+
transferProtocolVersion: transfer.transferProtocolVersion,
|
|
3310
|
+
handshake: transfer.handshake,
|
|
2890
3311
|
fps,
|
|
2891
3312
|
startedAt: now,
|
|
2892
3313
|
stoppedAt: '',
|
|
@@ -2899,10 +3320,12 @@ export function createRemoteHub(options = {}) {
|
|
|
2899
3320
|
emitRemoteEvent('RemoteLiveStreamStarted', device, {
|
|
2900
3321
|
streamId,
|
|
2901
3322
|
commandId,
|
|
2902
|
-
fps
|
|
3323
|
+
fps,
|
|
3324
|
+
mode: transfer.mode,
|
|
3325
|
+
frameMode: transfer.frameMode
|
|
2903
3326
|
});
|
|
2904
3327
|
|
|
2905
|
-
return { ok: true, commandId, streamId, fps };
|
|
3328
|
+
return { ok: true, commandId, streamId, fps, mode: transfer.mode, frameMode: transfer.frameMode };
|
|
2906
3329
|
}
|
|
2907
3330
|
|
|
2908
3331
|
function stopLiveStream(deviceId, options = {}) {
|
|
@@ -88,6 +88,25 @@ try {
|
|
|
88
88
|
pid: process.pid,
|
|
89
89
|
agentVersion: '0.0.0-ws-smoke',
|
|
90
90
|
runtime: 'node-smoke',
|
|
91
|
+
protocol: 'mindexec.remote.agent',
|
|
92
|
+
protocolVersion: 2,
|
|
93
|
+
frameProtocol: {
|
|
94
|
+
protocol: 'mindexec.remote.frame.v2',
|
|
95
|
+
version: 2,
|
|
96
|
+
handshake: 'smartview-request-open-frame',
|
|
97
|
+
defaultMode: 'remote-fast'
|
|
98
|
+
},
|
|
99
|
+
frameModes: [{
|
|
100
|
+
mode: 'remote-fast',
|
|
101
|
+
label: 'RemoteFast JPEG Binary',
|
|
102
|
+
transport: 'ws-binary',
|
|
103
|
+
encoding: 'image',
|
|
104
|
+
codec: 'jpeg',
|
|
105
|
+
compression: 'image/jpeg',
|
|
106
|
+
profile: 'jpeg-binary-v1',
|
|
107
|
+
modeVersion: 1,
|
|
108
|
+
implemented: true
|
|
109
|
+
}],
|
|
91
110
|
capabilities: {
|
|
92
111
|
status: true,
|
|
93
112
|
thumbnail: true,
|
|
@@ -109,13 +128,20 @@ try {
|
|
|
109
128
|
}
|
|
110
129
|
}));
|
|
111
130
|
|
|
112
|
-
await waitFor(() => received.find(item => item.type === 'welcome'), 5000, 'welcome');
|
|
131
|
+
const welcome = await waitFor(() => received.find(item => item.type === 'welcome'), 5000, 'welcome');
|
|
132
|
+
assert.equal(welcome.protocol, 'mindexec.remote.agent');
|
|
133
|
+
assert.equal(welcome.frameProtocol?.handshake, 'smartview-request-open-frame');
|
|
134
|
+
assert.equal(welcome.frameProtocol?.defaultMode, 'remote-fast');
|
|
135
|
+
assert.ok(welcome.frameModes?.some(item => item.mode === 'remote-fast'));
|
|
113
136
|
const device = await waitFor(() => {
|
|
114
137
|
const current = hub.listDevices();
|
|
115
138
|
return current.length === 1 && current[0].deviceId === DEVICE_ID ? current[0] : null;
|
|
116
139
|
}, 5000, 'device registration');
|
|
117
140
|
assert.equal(device.connected, true);
|
|
118
141
|
assert.equal(device.capabilities.binaryFrames, true);
|
|
142
|
+
assert.equal(device.protocol, 'mindexec.remote.agent');
|
|
143
|
+
assert.equal(device.frameProtocol?.handshake, 'smartview-request-open-frame');
|
|
144
|
+
assert.ok(device.frameModes?.some(item => item.mode === 'remote-fast'));
|
|
119
145
|
|
|
120
146
|
const inputCommand = hub.sendInputControl(DEVICE_ID, { type: 'noop' });
|
|
121
147
|
assert.equal(inputCommand.ok, true);
|
|
@@ -170,10 +196,36 @@ try {
|
|
|
170
196
|
quality: 50
|
|
171
197
|
});
|
|
172
198
|
assert.equal(liveCommand.ok, true);
|
|
173
|
-
await waitFor(
|
|
199
|
+
const liveMessage = await waitFor(
|
|
174
200
|
() => received.find(item => item.commandId === liveCommand.commandId),
|
|
175
201
|
5000,
|
|
176
202
|
'live command');
|
|
203
|
+
assert.equal(liveMessage.command, 'stream.start');
|
|
204
|
+
assert.equal(liveMessage.payload?.handshake, 'smartview-request-open-frame');
|
|
205
|
+
assert.equal(liveMessage.payload?.mode, 'remote-fast');
|
|
206
|
+
assert.equal(liveMessage.payload?.frameMode, 'remote-fast');
|
|
207
|
+
assert.equal(liveMessage.payload?.frameProfile, 'jpeg-binary-v1');
|
|
208
|
+
assert.equal(liveMessage.payload?.compression, 'image/jpeg');
|
|
209
|
+
assert.equal(liveMessage.payload?.transferProtocol, 'mindexec.remote.frame.v2');
|
|
210
|
+
ws.send(JSON.stringify({
|
|
211
|
+
type: 'stream.open',
|
|
212
|
+
commandId: liveCommand.commandId,
|
|
213
|
+
streamId: 'remote-agent-ws-live',
|
|
214
|
+
mode: 'remote-fast',
|
|
215
|
+
frameMode: 'remote-fast',
|
|
216
|
+
frameProfile: 'jpeg-binary-v1',
|
|
217
|
+
encoding: 'image',
|
|
218
|
+
codec: 'png',
|
|
219
|
+
compression: 'image/png',
|
|
220
|
+
transferProtocol: 'mindexec.remote.frame.v2',
|
|
221
|
+
transferProtocolVersion: 2,
|
|
222
|
+
handshake: 'smartview-request-open-frame',
|
|
223
|
+
fps: 10,
|
|
224
|
+
width: 2,
|
|
225
|
+
height: 1,
|
|
226
|
+
openedAt: new Date().toISOString()
|
|
227
|
+
}));
|
|
228
|
+
await waitFor(() => hub.listDevices()[0]?.activeLiveStream?.open === true, 5000, 'live stream open');
|
|
177
229
|
writeAgentBinaryFrame(ws, {
|
|
178
230
|
frameKind: 'stream',
|
|
179
231
|
commandId: liveCommand.commandId,
|
|
@@ -182,6 +234,15 @@ try {
|
|
|
182
234
|
width: 2,
|
|
183
235
|
height: 1,
|
|
184
236
|
mimeType: 'image/png',
|
|
237
|
+
mode: 'remote-fast',
|
|
238
|
+
frameMode: 'remote-fast',
|
|
239
|
+
frameProfile: 'jpeg-binary-v1',
|
|
240
|
+
encoding: 'image',
|
|
241
|
+
codec: 'png',
|
|
242
|
+
compression: 'image/png',
|
|
243
|
+
transferProtocol: 'mindexec.remote.frame.v2',
|
|
244
|
+
transferProtocolVersion: 2,
|
|
245
|
+
handshake: 'smartview-request-open-frame',
|
|
185
246
|
fps: 10,
|
|
186
247
|
capturedAt: new Date().toISOString()
|
|
187
248
|
}, SMOKE_PNG);
|
|
@@ -191,6 +252,12 @@ try {
|
|
|
191
252
|
}, 5000, 'websocket binary live frame');
|
|
192
253
|
assert.equal(liveDevice.latestLiveFrame.transport, 'binary');
|
|
193
254
|
assert.equal(liveDevice.latestLiveFrame.streamId, 'remote-agent-ws-live');
|
|
255
|
+
assert.equal(liveDevice.activeLiveStream.open, true);
|
|
256
|
+
assert.equal(liveDevice.activeLiveStream.frameMode, 'remote-fast');
|
|
257
|
+
assert.equal(liveDevice.latestLiveFrame.frameMode, 'remote-fast');
|
|
258
|
+
assert.equal(liveDevice.latestLiveFrame.frameProfile, 'jpeg-binary-v1');
|
|
259
|
+
assert.equal(liveDevice.latestLiveFrame.compression, 'image/png');
|
|
260
|
+
assert.equal(liveDevice.latestLiveFrame.transferProtocol, 'mindexec.remote.frame.v2');
|
|
194
261
|
|
|
195
262
|
ws.close();
|
|
196
263
|
console.log('RemoteAgent WebSocket smoke OK');
|
|
@@ -85,6 +85,9 @@ function spawnBridge({ bridgePort, remoteHubPort, workspacePath, label, traceFra
|
|
|
85
85
|
BRIDGE_REQUIRE_TOKEN: '1',
|
|
86
86
|
MINDEXEC_REMOTE_HUB: '1',
|
|
87
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',
|
|
88
91
|
REMOTE_HUB_HOST: '127.0.0.1',
|
|
89
92
|
REMOTE_HUB_PORT: String(remoteHubPort),
|
|
90
93
|
REMOTE_HUB_PAIR_TOKEN: PAIR_TOKEN,
|
|
@@ -214,6 +214,23 @@ try {
|
|
|
214
214
|
quality: 50
|
|
215
215
|
});
|
|
216
216
|
assert.equal(liveCommand.ok, true);
|
|
217
|
+
assert.equal(liveCommand.frameMode, 'remote-fast');
|
|
218
|
+
writeJsonLine(socket, {
|
|
219
|
+
type: 'stream.open',
|
|
220
|
+
commandId: liveCommand.commandId,
|
|
221
|
+
streamId: 'smoke-live',
|
|
222
|
+
mode: 'remote-fast',
|
|
223
|
+
frameMode: 'remote-fast',
|
|
224
|
+
frameProfile: 'jpeg-binary-v1',
|
|
225
|
+
encoding: 'image',
|
|
226
|
+
codec: 'png',
|
|
227
|
+
compression: 'image/png',
|
|
228
|
+
transferProtocol: 'mindexec.remote.frame.v2',
|
|
229
|
+
transferProtocolVersion: 2,
|
|
230
|
+
handshake: 'smartview-request-open-frame',
|
|
231
|
+
fps: 5,
|
|
232
|
+
openedAt: new Date().toISOString()
|
|
233
|
+
});
|
|
217
234
|
writeJsonLine(socket, {
|
|
218
235
|
type: 'stream.frame',
|
|
219
236
|
commandId: liveCommand.commandId,
|
|
@@ -223,6 +240,14 @@ try {
|
|
|
223
240
|
height: 1,
|
|
224
241
|
mimeType: 'image/png',
|
|
225
242
|
mode: 'remote-fast',
|
|
243
|
+
frameMode: 'remote-fast',
|
|
244
|
+
frameProfile: 'jpeg-binary-v1',
|
|
245
|
+
encoding: 'image',
|
|
246
|
+
codec: 'png',
|
|
247
|
+
compression: 'image/png',
|
|
248
|
+
transferProtocol: 'mindexec.remote.frame.v2',
|
|
249
|
+
transferProtocolVersion: 2,
|
|
250
|
+
handshake: 'smartview-request-open-frame',
|
|
226
251
|
fps: 5,
|
|
227
252
|
capturedAt: new Date().toISOString(),
|
|
228
253
|
data: 'iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAADElEQVR42mP8z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC'
|
|
@@ -233,7 +258,11 @@ try {
|
|
|
233
258
|
return current[0]?.latestLiveFrame?.streamId === 'smoke-live' ? current[0] : null;
|
|
234
259
|
});
|
|
235
260
|
assert.equal(liveDevice.activeLiveStream.active, true);
|
|
261
|
+
assert.equal(liveDevice.activeLiveStream.open, true);
|
|
262
|
+
assert.equal(liveDevice.activeLiveStream.handshake, 'smartview-request-open-frame');
|
|
236
263
|
assert.equal(liveDevice.latestLiveFrame.mode, 'remote-fast');
|
|
264
|
+
assert.equal(liveDevice.latestLiveFrame.frameMode, 'remote-fast');
|
|
265
|
+
assert.equal(liveDevice.latestLiveFrame.compression, 'image/png');
|
|
237
266
|
assert.equal(liveDevice.counters.liveFramesReceived, 1);
|
|
238
267
|
const serializedFirstLiveFrame = hub.getDeviceLiveFrame('smoke-device', { includeDataUrl: false });
|
|
239
268
|
const serializedFirstLiveUrl = new URL(serializedFirstLiveFrame.framePath, 'http://127.0.0.1');
|
|
@@ -247,6 +276,14 @@ try {
|
|
|
247
276
|
height: 1,
|
|
248
277
|
mimeType: 'image/png',
|
|
249
278
|
mode: 'remote-fast',
|
|
279
|
+
frameMode: 'remote-fast',
|
|
280
|
+
frameProfile: 'jpeg-binary-v1',
|
|
281
|
+
encoding: 'image',
|
|
282
|
+
codec: 'png',
|
|
283
|
+
compression: 'image/png',
|
|
284
|
+
transferProtocol: 'mindexec.remote.frame.v2',
|
|
285
|
+
transferProtocolVersion: 2,
|
|
286
|
+
handshake: 'smartview-request-open-frame',
|
|
250
287
|
fps: 5,
|
|
251
288
|
capturedAt: new Date().toISOString()
|
|
252
289
|
}, smokePngFrame);
|
|
@@ -265,6 +302,8 @@ try {
|
|
|
265
302
|
const serializedBinaryLiveFrame = hub.getDeviceLiveFrame('smoke-device', { includeDataUrl: false });
|
|
266
303
|
assert.equal(serializedBinaryLiveFrame.streamId, 'smoke-live');
|
|
267
304
|
assert.equal(serializedBinaryLiveFrame.frameSeq, 5);
|
|
305
|
+
assert.equal(serializedBinaryLiveFrame.frameMode, 'remote-fast');
|
|
306
|
+
assert.equal(serializedBinaryLiveFrame.compression, 'image/png');
|
|
268
307
|
assert.equal(serializedBinaryLiveFrame.contentHash, binaryLiveDevice.latestLiveFrame.contentHash);
|
|
269
308
|
assert.ok(serializedBinaryLiveFrame.framePath.includes('/api/remote/devices/smoke-device/live/frame?'));
|
|
270
309
|
assert.ok(!('dataUrl' in serializedBinaryLiveFrame));
|
|
@@ -515,6 +554,78 @@ try {
|
|
|
515
554
|
});
|
|
516
555
|
assert.equal(staleSessionTask.ok, true);
|
|
517
556
|
const oldSessionId = lateTimedOutDevice.sessionId;
|
|
557
|
+
|
|
558
|
+
const duplicateSocket = net.createConnection({ host: '127.0.0.1', port: status.port });
|
|
559
|
+
duplicateSocket.setEncoding('utf8');
|
|
560
|
+
const duplicateMessages = [];
|
|
561
|
+
let duplicateBuffer = '';
|
|
562
|
+
await new Promise((resolve, reject) => {
|
|
563
|
+
duplicateSocket.once('connect', resolve);
|
|
564
|
+
duplicateSocket.once('error', reject);
|
|
565
|
+
});
|
|
566
|
+
duplicateSocket.on('error', () => { });
|
|
567
|
+
duplicateSocket.on('data', chunk => {
|
|
568
|
+
duplicateBuffer += chunk;
|
|
569
|
+
let lineBreakIndex = duplicateBuffer.indexOf('\n');
|
|
570
|
+
while (lineBreakIndex >= 0) {
|
|
571
|
+
const line = duplicateBuffer.slice(0, lineBreakIndex).trim();
|
|
572
|
+
duplicateBuffer = duplicateBuffer.slice(lineBreakIndex + 1);
|
|
573
|
+
if (line) {
|
|
574
|
+
duplicateMessages.push(JSON.parse(line));
|
|
575
|
+
}
|
|
576
|
+
lineBreakIndex = duplicateBuffer.indexOf('\n');
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
writeJsonLine(duplicateSocket, {
|
|
580
|
+
type: 'hello',
|
|
581
|
+
pairToken: 'smoke-token',
|
|
582
|
+
deviceId: 'smoke-device',
|
|
583
|
+
deviceName: 'Smoke Device Duplicate',
|
|
584
|
+
hostname: 'smoke-host-duplicate',
|
|
585
|
+
platform: process.platform,
|
|
586
|
+
arch: process.arch,
|
|
587
|
+
pid: process.pid,
|
|
588
|
+
agentVersion: '0.0.0-smoke-duplicate',
|
|
589
|
+
capabilities: {
|
|
590
|
+
status: true,
|
|
591
|
+
thumbnail: false,
|
|
592
|
+
control: false,
|
|
593
|
+
liveStream: true,
|
|
594
|
+
computerAgent: true,
|
|
595
|
+
taskDispatch: true,
|
|
596
|
+
aiAssist: false
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
const duplicateDisconnect = await waitFor(() => (
|
|
601
|
+
duplicateMessages.find(message => message?.type === 'disconnect')
|
|
602
|
+
), 1000);
|
|
603
|
+
assert.equal(duplicateDisconnect.reason, 'duplicate-device-active');
|
|
604
|
+
assert.equal(duplicateDisconnect.activeSessionId, oldSessionId);
|
|
605
|
+
assert.equal(duplicateDisconnect.retryAfterMs >= 1000, true);
|
|
606
|
+
await wait(100);
|
|
607
|
+
const duplicateRejectedDevice = hub.listDevices()[0];
|
|
608
|
+
assert.equal(duplicateRejectedDevice.sessionId, oldSessionId);
|
|
609
|
+
assert.equal(duplicateRejectedDevice.deviceName, 'Smoke Device');
|
|
610
|
+
assert.equal(duplicateRejectedDevice.connected, true);
|
|
611
|
+
assert.equal(duplicateRejectedDevice.counters.duplicateConnectionsRejected, 1);
|
|
612
|
+
assert.equal(duplicateRejectedDevice.latestTask.taskId, staleSessionTask.taskId);
|
|
613
|
+
duplicateSocket.destroy();
|
|
614
|
+
|
|
615
|
+
socket.destroy();
|
|
616
|
+
const disconnectedDevice = await waitFor(() => {
|
|
617
|
+
const current = hub.listDevices();
|
|
618
|
+
return current.length === 1
|
|
619
|
+
&& current[0]?.sessionId === oldSessionId
|
|
620
|
+
&& current[0]?.connected === false
|
|
621
|
+
? current[0]
|
|
622
|
+
: null;
|
|
623
|
+
});
|
|
624
|
+
assert.equal(disconnectedDevice.latestTask.status, 'failed');
|
|
625
|
+
assert.equal(
|
|
626
|
+
['device-disconnected', 'task-timeout'].includes(disconnectedDevice.latestTask.error),
|
|
627
|
+
true);
|
|
628
|
+
|
|
518
629
|
const replacementSocket = net.createConnection({ host: '127.0.0.1', port: status.port });
|
|
519
630
|
replacementSocket.setEncoding('utf8');
|
|
520
631
|
await new Promise((resolve, reject) => {
|
|
@@ -565,25 +676,6 @@ try {
|
|
|
565
676
|
assert.equal(reconnectedDevice.latestTask, null);
|
|
566
677
|
assert.equal(reconnectedDevice.counters.taskResultsReceived, 0);
|
|
567
678
|
|
|
568
|
-
writeJsonLine(socket, {
|
|
569
|
-
type: 'status',
|
|
570
|
-
status: {
|
|
571
|
-
uptimeSec: 999,
|
|
572
|
-
totalMem: 999,
|
|
573
|
-
freeMem: 0
|
|
574
|
-
}
|
|
575
|
-
});
|
|
576
|
-
writeJsonLine(socket, {
|
|
577
|
-
type: 'command.result',
|
|
578
|
-
commandId: staleSessionTask.commandId,
|
|
579
|
-
result: {
|
|
580
|
-
kind: 'agent.task',
|
|
581
|
-
taskId: staleSessionTask.taskId,
|
|
582
|
-
status: 'completed',
|
|
583
|
-
summary: 'This old-session result must not mutate the reconnected device.',
|
|
584
|
-
completedAt: new Date().toISOString()
|
|
585
|
-
}
|
|
586
|
-
});
|
|
587
679
|
await wait(100);
|
|
588
680
|
const staleSessionIgnoredDevice = hub.listDevices()[0];
|
|
589
681
|
assert.equal(staleSessionIgnoredDevice.sessionId, reconnectedDevice.sessionId);
|
|
@@ -592,7 +684,6 @@ try {
|
|
|
592
684
|
assert.equal(staleSessionIgnoredDevice.latestTask, null);
|
|
593
685
|
assert.equal(staleSessionIgnoredDevice.counters.taskResultsReceived, 0);
|
|
594
686
|
|
|
595
|
-
socket.destroy();
|
|
596
687
|
replacementSocket.destroy();
|
|
597
688
|
await waitFor(() => hub.listDevices()[0]?.connected === false);
|
|
598
689
|
console.log('RemoteHub smoke OK');
|
package/server.js
CHANGED
|
@@ -2550,12 +2550,14 @@ function clampRemoteFrameWsNumber(value, min, max, fallback) {
|
|
|
2550
2550
|
}
|
|
2551
2551
|
|
|
2552
2552
|
function normalizeRemoteFrameWsLiveOptions(payload = {}) {
|
|
2553
|
+
const mode = String(payload.frameMode || payload.mode || 'remote-fast').trim() || 'remote-fast';
|
|
2553
2554
|
return {
|
|
2554
2555
|
fps: clampRemoteFrameWsNumber(payload.fps, 1, 24, REMOTE_FRAME_WS_DEFAULT_FPS),
|
|
2555
2556
|
maxWidth: clampRemoteFrameWsNumber(payload.maxWidth, 320, 2560, REMOTE_FRAME_WS_DEFAULT_MAX_WIDTH),
|
|
2556
2557
|
maxHeight: clampRemoteFrameWsNumber(payload.maxHeight, 180, 1440, REMOTE_FRAME_WS_DEFAULT_MAX_HEIGHT),
|
|
2557
2558
|
quality: clampRemoteFrameWsNumber(payload.quality, 20, 95, REMOTE_FRAME_WS_DEFAULT_QUALITY),
|
|
2558
|
-
mode
|
|
2559
|
+
mode,
|
|
2560
|
+
frameMode: mode
|
|
2559
2561
|
};
|
|
2560
2562
|
}
|
|
2561
2563
|
|
|
@@ -2671,6 +2673,14 @@ function buildRemoteFrameBinaryPacket(frameEvent) {
|
|
|
2671
2673
|
height: Number(frame.height || 0) || 0,
|
|
2672
2674
|
mimeType: frameEvent.mimeType || frame.mimeType || frame.format || 'image/jpeg',
|
|
2673
2675
|
mode: frame.mode || '',
|
|
2676
|
+
frameMode: frame.frameMode || frame.mode || '',
|
|
2677
|
+
frameProfile: frame.frameProfile || '',
|
|
2678
|
+
encoding: frame.encoding || '',
|
|
2679
|
+
codec: frame.codec || '',
|
|
2680
|
+
compression: frame.compression || frame.mimeType || frame.format || '',
|
|
2681
|
+
transferProtocol: frame.transferProtocol || '',
|
|
2682
|
+
transferProtocolVersion: Number(frame.transferProtocolVersion || 0) || 0,
|
|
2683
|
+
handshake: frame.handshake || '',
|
|
2674
2684
|
fps: Number(frame.fps || 0) || 0,
|
|
2675
2685
|
capturedAt: frame.capturedAt || '',
|
|
2676
2686
|
receivedAt: frame.receivedAt || '',
|