@livedesk/hub 0.1.4 → 0.1.6
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 +2 -2
- package/src/agents/agent-tool-registry.js +19 -19
- package/src/agents/codex-agent-runtime.js +7 -3
- package/src/filesystem/shared-folders.js +1 -1
- package/src/filesystem/transfer-jobs.js +90 -5
- package/src/live-desk-update.js +585 -302
- package/src/mode4-atlas-pool.js +123 -0
- package/src/mode4-atlas-sizing.js +32 -0
- package/src/mode4-atlas-worker.js +58 -11
- package/src/mode4-atlas.js +9 -1
- package/src/remote-hub.js +1259 -423
- package/src/server.js +776 -275
package/src/remote-hub.js
CHANGED
|
@@ -24,9 +24,10 @@ const RECENT_LIVE_FRAME_CACHE_LIMIT = 1;
|
|
|
24
24
|
const RECENT_THUMBNAIL_FRAME_CACHE_MAX_BYTES = 2 * 1024 * 1024;
|
|
25
25
|
const RECENT_LIVE_FRAME_CACHE_MAX_BYTES = 8 * 1024 * 1024;
|
|
26
26
|
const LIVE_STREAM_PENDING_REUSE_MS = 5000;
|
|
27
|
-
const LIVE_STREAM_REPLACEMENT_PENDING_MS = 7000;
|
|
28
|
-
const LIVE_STREAM_MIN_FRESH_MS = 3000;
|
|
29
|
-
const LIVE_STREAM_MAX_FRESH_MS = 12000;
|
|
27
|
+
const LIVE_STREAM_REPLACEMENT_PENDING_MS = 7000;
|
|
28
|
+
const LIVE_STREAM_MIN_FRESH_MS = 3000;
|
|
29
|
+
const LIVE_STREAM_MAX_FRESH_MS = 12000;
|
|
30
|
+
const DEFAULT_REMOTE_INPUT_ACK_TIMEOUT_MS = 5000;
|
|
30
31
|
const REMOTE_PROTOCOL_VERSION = 2;
|
|
31
32
|
const REMOTE_AGENT_PROTOCOL = 'mindexec.remote.agent';
|
|
32
33
|
const REMOTE_FRAME_PROTOCOL = 'mindexec.remote.frame.v2';
|
|
@@ -58,14 +59,14 @@ const REMOTE_FRAME_MODE_PROFILES = Object.freeze({
|
|
|
58
59
|
modeVersion: 1,
|
|
59
60
|
implemented: true
|
|
60
61
|
}),
|
|
61
|
-
'mode2-lzo': Object.freeze({
|
|
62
|
-
mode: 'mode2-lzo',
|
|
63
|
-
label: '
|
|
62
|
+
'mode2-lzo': Object.freeze({
|
|
63
|
+
mode: 'mode2-lzo',
|
|
64
|
+
label: 'Internal Atlas Input - RGB565 LZO',
|
|
64
65
|
transport: 'ws-binary',
|
|
65
66
|
encoding: 'image-raw',
|
|
66
67
|
codec: 'rgb565',
|
|
67
68
|
compression: 'lzo1x',
|
|
68
|
-
profile: '
|
|
69
|
+
profile: 'atlas-input-rgb565-lzo-v1',
|
|
69
70
|
modeVersion: 2,
|
|
70
71
|
implemented: true
|
|
71
72
|
}),
|
|
@@ -151,7 +152,7 @@ const DEFAULT_PUBLIC_IP_TIMEOUT_MS = 1800;
|
|
|
151
152
|
const DUPLICATE_DEVICE_ACTIVE_REJECT_MS = 15000;
|
|
152
153
|
const DUPLICATE_DEVICE_LOG_THROTTLE_MS = 60000;
|
|
153
154
|
const DUPLICATE_DEVICE_RETRY_AFTER_MS = 30000;
|
|
154
|
-
const AGENT_BINARY_INGRESS_QUEUE_PACKETS =
|
|
155
|
+
const AGENT_BINARY_INGRESS_QUEUE_PACKETS = 8;
|
|
155
156
|
const MODE5_AGENT_BINARY_INGRESS_QUEUE_PACKETS = 2;
|
|
156
157
|
const AGENT_BINARY_INGRESS_DRAIN_BUDGET_PACKETS = 64;
|
|
157
158
|
const SYNTHETIC_FRAME_DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAADElEQVR42mP8z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC';
|
|
@@ -320,7 +321,7 @@ function isWildcardHost(value) {
|
|
|
320
321
|
|| host === '';
|
|
321
322
|
}
|
|
322
323
|
|
|
323
|
-
function isPrivateIPv4(value) {
|
|
324
|
+
function isPrivateIPv4(value) {
|
|
324
325
|
const parts = String(value || '').split('.').map(part => Number(part));
|
|
325
326
|
if (parts.length !== 4 || parts.some(part => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
326
327
|
return false;
|
|
@@ -329,9 +330,71 @@ function isPrivateIPv4(value) {
|
|
|
329
330
|
return parts[0] === 10
|
|
330
331
|
|| (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31)
|
|
331
332
|
|| (parts[0] === 192 && parts[1] === 168);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function isPrivateNetworkAddress(value) {
|
|
336
|
+
const address = String(value || '').replace(/^::ffff:/i, '').trim().toLowerCase();
|
|
337
|
+
return isLoopbackHost(address)
|
|
338
|
+
|| isPrivateIPv4(address)
|
|
339
|
+
|| address.startsWith('fe80:')
|
|
340
|
+
|| address.startsWith('fc')
|
|
341
|
+
|| address.startsWith('fd');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function readCpuTimes() {
|
|
345
|
+
let idle = 0;
|
|
346
|
+
let total = 0;
|
|
347
|
+
for (const cpu of os.cpus()) {
|
|
348
|
+
const times = Object.values(cpu.times || {}).map(Number);
|
|
349
|
+
idle += Number(cpu.times?.idle || 0);
|
|
350
|
+
total += times.reduce((sum, value) => sum + (Number.isFinite(value) ? value : 0), 0);
|
|
351
|
+
}
|
|
352
|
+
return { idle, total };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function createHubRuntimeDiagnosticsSampler() {
|
|
356
|
+
let previousCpu = readCpuTimes();
|
|
357
|
+
let previousProcessCpu = process.cpuUsage();
|
|
358
|
+
let previousAt = Date.now();
|
|
359
|
+
return () => {
|
|
360
|
+
const now = Date.now();
|
|
361
|
+
const cpu = readCpuTimes();
|
|
362
|
+
const processCpu = process.cpuUsage();
|
|
363
|
+
const elapsedMs = Math.max(1, now - previousAt);
|
|
364
|
+
const totalDelta = Math.max(0, cpu.total - previousCpu.total);
|
|
365
|
+
const idleDelta = Math.max(0, cpu.idle - previousCpu.idle);
|
|
366
|
+
const processMicros = Math.max(
|
|
367
|
+
0,
|
|
368
|
+
processCpu.user - previousProcessCpu.user + processCpu.system - previousProcessCpu.system
|
|
369
|
+
);
|
|
370
|
+
const cpuLogicalCores = Math.max(1, os.cpus().length || 1);
|
|
371
|
+
const totalMemoryBytes = Math.max(0, Number(os.totalmem() || 0));
|
|
372
|
+
const freeMemoryBytes = Math.max(0, Number(os.freemem() || 0));
|
|
373
|
+
const snapshot = {
|
|
374
|
+
collectedAt: new Date(now).toISOString(),
|
|
375
|
+
system: {
|
|
376
|
+
cpuLogicalCores,
|
|
377
|
+
cpuUsageRatio: totalDelta > 0
|
|
378
|
+
? Math.max(0, Math.min(1, 1 - idleDelta / totalDelta))
|
|
379
|
+
: null,
|
|
380
|
+
processCpuRatio: Math.max(0, Math.min(1, processMicros / (elapsedMs * 1000 * cpuLogicalCores))),
|
|
381
|
+
totalMemoryBytes,
|
|
382
|
+
freeMemoryBytes,
|
|
383
|
+
memoryUsedRatio: totalMemoryBytes > 0
|
|
384
|
+
? Math.max(0, Math.min(1, (totalMemoryBytes - freeMemoryBytes) / totalMemoryBytes))
|
|
385
|
+
: null,
|
|
386
|
+
processMemoryBytes: Math.max(0, Number(process.memoryUsage().rss || 0)),
|
|
387
|
+
uptimeSeconds: Math.max(0, Number(os.uptime() || 0))
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
previousCpu = cpu;
|
|
391
|
+
previousProcessCpu = processCpu;
|
|
392
|
+
previousAt = now;
|
|
393
|
+
return snapshot;
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function isPublicIPv4(value) {
|
|
335
398
|
const parts = String(value || '').split('.').map(part => Number(part));
|
|
336
399
|
if (parts.length !== 4 || parts.some(part => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
337
400
|
return false;
|
|
@@ -617,11 +680,12 @@ function normalizeRemoteInputEvent(value = {}) {
|
|
|
617
680
|
const location = Number(input.location ?? input.Location);
|
|
618
681
|
const monitorIndex = Number(input.monitorIndex ?? input.MonitorIndex ?? input.screenIndex ?? input.ScreenIndex ?? input.displayIndex ?? input.DisplayIndex);
|
|
619
682
|
const inputSeq = Number(input.inputSeq ?? input.InputSeq);
|
|
620
|
-
const issuedAtEpochMs = Number(input.issuedAtEpochMs ?? input.IssuedAtEpochMs);
|
|
621
|
-
const hubReceivedAtEpochMs = Number(input.hubReceivedAtEpochMs ?? input.HubReceivedAtEpochMs);
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
683
|
+
const issuedAtEpochMs = Number(input.issuedAtEpochMs ?? input.IssuedAtEpochMs);
|
|
684
|
+
const hubReceivedAtEpochMs = Number(input.hubReceivedAtEpochMs ?? input.HubReceivedAtEpochMs);
|
|
685
|
+
const captureGeneration = Number(input.captureGeneration ?? input.CaptureGeneration);
|
|
686
|
+
return {
|
|
687
|
+
type,
|
|
688
|
+
monitorIndex: Number.isFinite(monitorIndex) ? normalizeMonitorIndex(monitorIndex) : 0,
|
|
625
689
|
normalizedX: Number.isFinite(normalizedX) ? Math.max(0, Math.min(1, normalizedX)) : undefined,
|
|
626
690
|
normalizedY: Number.isFinite(normalizedY) ? Math.max(0, Math.min(1, normalizedY)) : undefined,
|
|
627
691
|
button: safeString(input.button || input.Button, 24),
|
|
@@ -636,9 +700,14 @@ function normalizeRemoteInputEvent(value = {}) {
|
|
|
636
700
|
shiftKey: input.shiftKey === true || input.ShiftKey === true,
|
|
637
701
|
ctrlKey: input.ctrlKey === true || input.ControlKey === true || input.CtrlKey === true,
|
|
638
702
|
altKey: input.altKey === true || input.AltKey === true,
|
|
639
|
-
metaKey: input.metaKey === true || input.MetaKey === true || input.commandKey === true || input.CommandKey === true,
|
|
640
|
-
controlLeaseId: safeString(input.controlLeaseId || input.ControlLeaseId, 128),
|
|
641
|
-
|
|
703
|
+
metaKey: input.metaKey === true || input.MetaKey === true || input.commandKey === true || input.CommandKey === true,
|
|
704
|
+
controlLeaseId: safeString(input.controlLeaseId || input.ControlLeaseId, 128),
|
|
705
|
+
controlSessionId: safeString(input.controlSessionId || input.sessionId || input.ControlSessionId || input.SessionId, 160),
|
|
706
|
+
controlCommandId: safeString(input.controlCommandId || input.commandId || input.ControlCommandId || input.CommandId, 128),
|
|
707
|
+
captureGeneration: Number.isSafeInteger(captureGeneration) && captureGeneration > 0 ? captureGeneration : 0,
|
|
708
|
+
hubConnectionId: safeString(input.hubConnectionId || input.HubConnectionId, 128),
|
|
709
|
+
inputEventId: safeString(input.inputEventId || input.InputEventId, 128) || crypto.randomUUID(),
|
|
710
|
+
inputSeq: Number.isSafeInteger(inputSeq) && inputSeq > 0 ? inputSeq : 0,
|
|
642
711
|
requestAck: input.requestAck !== false && input.RequestAck !== false,
|
|
643
712
|
issuedAtEpochMs: Number.isFinite(issuedAtEpochMs) ? issuedAtEpochMs : 0,
|
|
644
713
|
hubReceivedAtEpochMs: Number.isFinite(hubReceivedAtEpochMs) ? hubReceivedAtEpochMs : 0,
|
|
@@ -920,9 +989,10 @@ function serializeDevice(device, options = {}) {
|
|
|
920
989
|
platform: device.platform,
|
|
921
990
|
arch: device.arch,
|
|
922
991
|
slotNumber: device.slotNumber || 0,
|
|
923
|
-
pid: device.pid,
|
|
924
|
-
agentVersion: device.agentVersion,
|
|
925
|
-
|
|
992
|
+
pid: device.pid,
|
|
993
|
+
agentVersion: device.agentVersion,
|
|
994
|
+
productVersion: device.productVersion,
|
|
995
|
+
protocol: device.protocol,
|
|
926
996
|
protocolVersion: device.protocolVersion,
|
|
927
997
|
frameProtocol: device.frameProtocol ? { ...device.frameProtocol } : null,
|
|
928
998
|
frameModes: Array.isArray(device.frameModes)
|
|
@@ -1383,7 +1453,7 @@ class RemoteHubWebSocketAgentSocket {
|
|
|
1383
1453
|
}
|
|
1384
1454
|
}
|
|
1385
1455
|
|
|
1386
|
-
export function createRemoteHub(options = {}) {
|
|
1456
|
+
export function createRemoteHub(options = {}) {
|
|
1387
1457
|
const env = options.env || process.env;
|
|
1388
1458
|
const logEvent = options.logEvent || (() => {});
|
|
1389
1459
|
const logWarn = options.logWarn || (() => {});
|
|
@@ -1396,9 +1466,10 @@ export function createRemoteHub(options = {}) {
|
|
|
1396
1466
|
const getWelcomeDevicePolicy = typeof options.getWelcomeDevicePolicy === 'function'
|
|
1397
1467
|
? options.getWelcomeDevicePolicy
|
|
1398
1468
|
: getEffectiveDevicePolicy;
|
|
1399
|
-
const udpTransport = options.udpTransport && typeof options.udpTransport === 'object'
|
|
1400
|
-
? options.udpTransport
|
|
1401
|
-
: null;
|
|
1469
|
+
const udpTransport = options.udpTransport && typeof options.udpTransport === 'object'
|
|
1470
|
+
? options.udpTransport
|
|
1471
|
+
: null;
|
|
1472
|
+
const sampleHubRuntimeDiagnostics = createHubRuntimeDiagnosticsSampler();
|
|
1402
1473
|
|
|
1403
1474
|
function getDevicePolicy(device) {
|
|
1404
1475
|
try {
|
|
@@ -1425,11 +1496,16 @@ export function createRemoteHub(options = {}) {
|
|
|
1425
1496
|
const host = safeString(env.REMOTE_HUB_HOST || DEFAULT_REMOTE_HUB_HOST, 128);
|
|
1426
1497
|
const requestedPort = normalizePort(env.REMOTE_HUB_PORT || DEFAULT_REMOTE_HUB_PORT);
|
|
1427
1498
|
const heartbeatMs = clampNumber(env.REMOTE_HUB_HEARTBEAT_MS, 1000, 60000, DEFAULT_HEARTBEAT_MS);
|
|
1428
|
-
const taskTimeoutMs = clampNumber(
|
|
1429
|
-
options.taskTimeoutMs ?? env.REMOTE_HUB_TASK_TIMEOUT_MS,
|
|
1430
|
-
50,
|
|
1431
|
-
30 * 60 * 1000,
|
|
1432
|
-
DEFAULT_AGENT_TASK_TIMEOUT_MS);
|
|
1499
|
+
const taskTimeoutMs = clampNumber(
|
|
1500
|
+
options.taskTimeoutMs ?? env.REMOTE_HUB_TASK_TIMEOUT_MS,
|
|
1501
|
+
50,
|
|
1502
|
+
30 * 60 * 1000,
|
|
1503
|
+
DEFAULT_AGENT_TASK_TIMEOUT_MS);
|
|
1504
|
+
const inputAckTimeoutMs = clampNumber(
|
|
1505
|
+
options.inputAckTimeoutMs ?? env.REMOTE_HUB_INPUT_ACK_TIMEOUT_MS,
|
|
1506
|
+
250,
|
|
1507
|
+
30000,
|
|
1508
|
+
DEFAULT_REMOTE_INPUT_ACK_TIMEOUT_MS);
|
|
1433
1509
|
const managerPackage = safeString(options.managerPackage ?? env.LIVEDESK_MANAGER_PACKAGE ?? env.MINDEXEC_MANAGER_PACKAGE ?? '@livedesk/hub', 128);
|
|
1434
1510
|
const managerVersion = safeString(options.managerVersion ?? env.LIVEDESK_MANAGER_VERSION ?? env.MINDEXEC_MANAGER_VERSION ?? '', 64);
|
|
1435
1511
|
const hostInstanceId = safeString(options.hostInstanceId ?? env.LIVEDESK_HUB_INSTANCE_ID ?? env.MINDEXEC_BRIDGE_INSTANCE_ID ?? crypto.randomUUID(), 128) || crypto.randomUUID();
|
|
@@ -1750,11 +1826,22 @@ export function createRemoteHub(options = {}) {
|
|
|
1750
1826
|
};
|
|
1751
1827
|
}
|
|
1752
1828
|
|
|
1753
|
-
function getStatus({ includeSecrets = false } = {}) {
|
|
1754
|
-
const connectedDevices = [...devices.values()].filter(device => device.connected).length;
|
|
1755
|
-
const
|
|
1756
|
-
|
|
1757
|
-
|
|
1829
|
+
function getStatus({ includeSecrets = false } = {}) {
|
|
1830
|
+
const connectedDevices = [...devices.values()].filter(device => device.connected).length;
|
|
1831
|
+
const connectedRoutes = [...devices.values()]
|
|
1832
|
+
.filter(device => device.connected)
|
|
1833
|
+
.map(device => String(device.remoteAddress || '').replace(/^::ffff:/i, '').trim())
|
|
1834
|
+
.filter(Boolean);
|
|
1835
|
+
const externalDeviceCount = connectedRoutes.filter(address => !isPrivateNetworkAddress(address)).length;
|
|
1836
|
+
const activeHostTarget = serializeHostTarget();
|
|
1837
|
+
const routeInfo = getAgentEndpointRouteInfo();
|
|
1838
|
+
const diagnostics = sampleHubRuntimeDiagnostics();
|
|
1839
|
+
diagnostics.network = {
|
|
1840
|
+
lanDeviceCount: connectedRoutes.length - externalDeviceCount,
|
|
1841
|
+
externalDeviceCount,
|
|
1842
|
+
route: externalDeviceCount > 0 ? 'external' : connectedRoutes.length > 0 ? 'lan' : 'idle'
|
|
1843
|
+
};
|
|
1844
|
+
return {
|
|
1758
1845
|
enabled,
|
|
1759
1846
|
started,
|
|
1760
1847
|
host,
|
|
@@ -1804,9 +1891,10 @@ export function createRemoteHub(options = {}) {
|
|
|
1804
1891
|
sessionCount: 0,
|
|
1805
1892
|
readySessionCount: 0
|
|
1806
1893
|
},
|
|
1807
|
-
externalExposure: isWildcardHost(host) || !isLoopbackHost(host),
|
|
1808
|
-
|
|
1809
|
-
|
|
1894
|
+
externalExposure: isWildcardHost(host) || !isLoopbackHost(host),
|
|
1895
|
+
diagnostics,
|
|
1896
|
+
lastError
|
|
1897
|
+
};
|
|
1810
1898
|
}
|
|
1811
1899
|
|
|
1812
1900
|
function rotatePairingPin() {
|
|
@@ -1971,9 +2059,11 @@ export function createRemoteHub(options = {}) {
|
|
|
1971
2059
|
+ Number(device?.counters?.liveFramesReceived || 0)
|
|
1972
2060
|
+ 1;
|
|
1973
2061
|
return {
|
|
1974
|
-
streamId: safeString(streamId, 128) || `${mode}-${Date.now()}`,
|
|
1975
|
-
frameSeq,
|
|
1976
|
-
commandId: safeString(options.commandId, 128),
|
|
2062
|
+
streamId: safeString(streamId, 128) || `${mode}-${Date.now()}`,
|
|
2063
|
+
frameSeq,
|
|
2064
|
+
commandId: safeString(options.commandId, 128),
|
|
2065
|
+
captureGeneration: Number(options.captureGeneration || 0),
|
|
2066
|
+
currentGenerationVerified: true,
|
|
1977
2067
|
width: clampNumber(options.width, 2, 3840, mode === 'thumbnail' ? 360 : 640),
|
|
1978
2068
|
height: clampNumber(options.height, 1, 2160, mode === 'thumbnail' ? 220 : 360),
|
|
1979
2069
|
mimeType: 'image/png',
|
|
@@ -2038,10 +2128,11 @@ export function createRemoteHub(options = {}) {
|
|
|
2038
2128
|
commandId: options.commandId || streamState.commandId,
|
|
2039
2129
|
width: options.maxWidth || 960,
|
|
2040
2130
|
height: options.maxHeight || 540,
|
|
2041
|
-
monitorIndex: options.monitorIndex ?? streamState.monitorIndex ?? 0,
|
|
2042
|
-
monitorCount: options.monitorCount ?? streamState.monitorCount ?? 1,
|
|
2043
|
-
fps: options.fps || streamState.fps
|
|
2044
|
-
|
|
2131
|
+
monitorIndex: options.monitorIndex ?? streamState.monitorIndex ?? 0,
|
|
2132
|
+
monitorCount: options.monitorCount ?? streamState.monitorCount ?? 1,
|
|
2133
|
+
fps: options.fps || streamState.fps,
|
|
2134
|
+
captureGeneration: streamState.captureGeneration
|
|
2135
|
+
});
|
|
2045
2136
|
device.latestLiveFrame = frame;
|
|
2046
2137
|
rememberRecentFramePayload(device, 'live', frame);
|
|
2047
2138
|
emitFrame({
|
|
@@ -2076,9 +2167,10 @@ export function createRemoteHub(options = {}) {
|
|
|
2076
2167
|
return;
|
|
2077
2168
|
}
|
|
2078
2169
|
|
|
2079
|
-
existing.lastDisconnectReason = 'replaced-by-new-session';
|
|
2080
|
-
failAllPendingTasks(existing, 'replaced-by-new-session');
|
|
2081
|
-
|
|
2170
|
+
existing.lastDisconnectReason = 'replaced-by-new-session';
|
|
2171
|
+
failAllPendingTasks(existing, 'replaced-by-new-session');
|
|
2172
|
+
failAllPendingInputFallbacks(existing, 'replaced-by-new-session');
|
|
2173
|
+
writeJsonLine(existing.socket, {
|
|
2082
2174
|
type: 'disconnect',
|
|
2083
2175
|
reason: 'replaced-by-new-session',
|
|
2084
2176
|
nextSessionId
|
|
@@ -2313,7 +2405,7 @@ export function createRemoteHub(options = {}) {
|
|
|
2313
2405
|
const replace = options.replace !== false;
|
|
2314
2406
|
const now = new Date();
|
|
2315
2407
|
const platforms = ['win32', 'linux', 'darwin'];
|
|
2316
|
-
const machineKinds = ['Mac mini', 'Mini PC', '
|
|
2408
|
+
const machineKinds = ['Mac mini', 'Mini PC', 'Client', 'Render node', 'Dev box'];
|
|
2317
2409
|
const workloadRoles = ['LLM worker', 'Image worker', 'Video worker', 'Idle reserve', 'Build worker'];
|
|
2318
2410
|
const gpuNames = ['Apple M-series GPU', 'NVIDIA RTX local', 'Radeon Pro', 'Intel Arc', 'Integrated GPU'];
|
|
2319
2411
|
const npuNames = ['Apple Neural Engine', 'Ryzen AI NPU', 'Intel AI Boost', 'Qualcomm Hexagon', 'NPU not present'];
|
|
@@ -2630,9 +2722,10 @@ export function createRemoteHub(options = {}) {
|
|
|
2630
2722
|
platform: safeString(hello.platform, 80),
|
|
2631
2723
|
arch: safeString(hello.arch, 40),
|
|
2632
2724
|
slotNumber: normalizeSlotNumber(hello.slotNumber ?? hello.slot ?? hello.computerNumber),
|
|
2633
|
-
pid: Number.isFinite(Number(hello.pid)) ? Number(hello.pid) : 0,
|
|
2634
|
-
agentVersion: safeString(hello.agentVersion, 40),
|
|
2635
|
-
|
|
2725
|
+
pid: Number.isFinite(Number(hello.pid)) ? Number(hello.pid) : 0,
|
|
2726
|
+
agentVersion: safeString(hello.agentVersion, 40),
|
|
2727
|
+
productVersion: safeString(hello.productVersion, 40),
|
|
2728
|
+
protocol: safeString(hello.protocol, 80) || REMOTE_AGENT_PROTOCOL,
|
|
2636
2729
|
protocolVersion: Number.isFinite(Number(hello.protocolVersion)) ? Math.max(1, Math.floor(Number(hello.protocolVersion))) : 1,
|
|
2637
2730
|
frameProtocol: frameProtocol || buildRemoteFrameProtocolDescriptor(),
|
|
2638
2731
|
frameModes: frameModes.length > 0 ? frameModes : getSupportedRemoteFrameModeProfiles(),
|
|
@@ -2851,9 +2944,10 @@ export function createRemoteHub(options = {}) {
|
|
|
2851
2944
|
closeFrameSocket(device, reason);
|
|
2852
2945
|
device.disconnectedAt = new Date().toISOString();
|
|
2853
2946
|
device.lastDisconnectReason = reason;
|
|
2854
|
-
deactivateDeviceLiveStreams(device, reason, device.disconnectedAt);
|
|
2855
|
-
failAllPendingTasks(device, 'device-disconnected');
|
|
2856
|
-
|
|
2947
|
+
deactivateDeviceLiveStreams(device, reason, device.disconnectedAt);
|
|
2948
|
+
failAllPendingTasks(device, 'device-disconnected');
|
|
2949
|
+
failAllPendingInputFallbacks(device, 'device-disconnected');
|
|
2950
|
+
logWarn('remote', `device disconnected ${device.deviceName} (${deviceId}): ${reason}`);
|
|
2857
2951
|
emitRemoteEvent('RemoteDeviceDisconnected', device, { reason });
|
|
2858
2952
|
}
|
|
2859
2953
|
|
|
@@ -3111,10 +3205,10 @@ export function createRemoteHub(options = {}) {
|
|
|
3111
3205
|
return task;
|
|
3112
3206
|
}
|
|
3113
3207
|
|
|
3114
|
-
function failAllPendingTasks(device, error = 'device-disconnected') {
|
|
3115
|
-
if (!device?.pendingTaskCommands) {
|
|
3116
|
-
return 0;
|
|
3117
|
-
}
|
|
3208
|
+
function failAllPendingTasks(device, error = 'device-disconnected') {
|
|
3209
|
+
if (!device?.pendingTaskCommands) {
|
|
3210
|
+
return 0;
|
|
3211
|
+
}
|
|
3118
3212
|
|
|
3119
3213
|
const commandIds = [...device.pendingTaskCommands.keys()];
|
|
3120
3214
|
let failed = 0;
|
|
@@ -3122,11 +3216,263 @@ export function createRemoteHub(options = {}) {
|
|
|
3122
3216
|
if (failPendingTask(device, commandId, error)) {
|
|
3123
3217
|
failed += 1;
|
|
3124
3218
|
}
|
|
3125
|
-
}
|
|
3126
|
-
return failed;
|
|
3127
|
-
}
|
|
3128
|
-
|
|
3129
|
-
function
|
|
3219
|
+
}
|
|
3220
|
+
return failed;
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
function ensurePendingInputFallbacks(device) {
|
|
3224
|
+
if (!(device?.pendingInputFallbacks instanceof Map)) {
|
|
3225
|
+
Object.defineProperty(device, 'pendingInputFallbacks', {
|
|
3226
|
+
value: new Map(),
|
|
3227
|
+
enumerable: false,
|
|
3228
|
+
configurable: true,
|
|
3229
|
+
writable: true
|
|
3230
|
+
});
|
|
3231
|
+
}
|
|
3232
|
+
if (!(device?.completedInputFallbacks instanceof Map)) {
|
|
3233
|
+
Object.defineProperty(device, 'completedInputFallbacks', {
|
|
3234
|
+
value: new Map(),
|
|
3235
|
+
enumerable: false,
|
|
3236
|
+
configurable: true,
|
|
3237
|
+
writable: true
|
|
3238
|
+
});
|
|
3239
|
+
}
|
|
3240
|
+
return device.pendingInputFallbacks;
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
function pruneCompletedInputFallbacks(device, now = Date.now()) {
|
|
3244
|
+
ensurePendingInputFallbacks(device);
|
|
3245
|
+
for (const [key, expiresAt] of device.completedInputFallbacks.entries()) {
|
|
3246
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= now) {
|
|
3247
|
+
device.completedInputFallbacks.delete(key);
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
function getInputFallbackCompletionKeys(commandId, inputSeq) {
|
|
3253
|
+
const keys = [];
|
|
3254
|
+
const normalizedCommandId = safeString(commandId, 128);
|
|
3255
|
+
const normalizedInputSeq = Number(inputSeq || 0);
|
|
3256
|
+
if (normalizedCommandId) {
|
|
3257
|
+
keys.push(`command:${normalizedCommandId}`);
|
|
3258
|
+
}
|
|
3259
|
+
if (Number.isSafeInteger(normalizedInputSeq) && normalizedInputSeq > 0) {
|
|
3260
|
+
keys.push(`seq:${normalizedInputSeq}`);
|
|
3261
|
+
}
|
|
3262
|
+
return keys;
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3265
|
+
function rememberCompletedInputFallback(device, pending) {
|
|
3266
|
+
pruneCompletedInputFallbacks(device);
|
|
3267
|
+
const expiresAt = Date.now() + Math.max(inputAckTimeoutMs * 2, 10000);
|
|
3268
|
+
for (const key of getInputFallbackCompletionKeys(pending?.commandId, pending?.inputSeq)) {
|
|
3269
|
+
device.completedInputFallbacks.set(key, expiresAt);
|
|
3270
|
+
}
|
|
3271
|
+
while (device.completedInputFallbacks.size > 256) {
|
|
3272
|
+
const oldestKey = device.completedInputFallbacks.keys().next().value;
|
|
3273
|
+
if (!oldestKey) {
|
|
3274
|
+
break;
|
|
3275
|
+
}
|
|
3276
|
+
device.completedInputFallbacks.delete(oldestKey);
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
function wasInputFallbackCompleted(device, commandId, inputSeq) {
|
|
3281
|
+
pruneCompletedInputFallbacks(device);
|
|
3282
|
+
return getInputFallbackCompletionKeys(commandId, inputSeq)
|
|
3283
|
+
.some(key => device.completedInputFallbacks.has(key));
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3286
|
+
function findPendingInputFallback(device, commandId, inputSeq) {
|
|
3287
|
+
const pendingFallbacks = ensurePendingInputFallbacks(device);
|
|
3288
|
+
const key = safeString(commandId, 128);
|
|
3289
|
+
if (key && pendingFallbacks.has(key)) {
|
|
3290
|
+
return pendingFallbacks.get(key);
|
|
3291
|
+
}
|
|
3292
|
+
const sequence = Number(inputSeq || 0);
|
|
3293
|
+
if (!Number.isSafeInteger(sequence) || sequence <= 0) {
|
|
3294
|
+
return null;
|
|
3295
|
+
}
|
|
3296
|
+
for (const pending of pendingFallbacks.values()) {
|
|
3297
|
+
if (Number(pending?.inputSeq || 0) === sequence) {
|
|
3298
|
+
return pending;
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
return null;
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3304
|
+
function takePendingInputFallback(device, commandId, inputSeq) {
|
|
3305
|
+
const pending = findPendingInputFallback(device, commandId, inputSeq);
|
|
3306
|
+
if (!pending) {
|
|
3307
|
+
return null;
|
|
3308
|
+
}
|
|
3309
|
+
if (pending.timeoutTimer) {
|
|
3310
|
+
clearTimeout(pending.timeoutTimer);
|
|
3311
|
+
pending.timeoutTimer = null;
|
|
3312
|
+
}
|
|
3313
|
+
ensurePendingInputFallbacks(device).delete(pending.commandId);
|
|
3314
|
+
rememberCompletedInputFallback(device, pending);
|
|
3315
|
+
return pending;
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
function buildRemoteInputOutcome(device, message = {}, pending = null) {
|
|
3319
|
+
const result = message?.result && typeof message.result === 'object'
|
|
3320
|
+
? message.result
|
|
3321
|
+
: {};
|
|
3322
|
+
const inputSeq = Number(
|
|
3323
|
+
message.inputSeq
|
|
3324
|
+
?? message.InputSeq
|
|
3325
|
+
?? result.inputSeq
|
|
3326
|
+
?? result.InputSeq
|
|
3327
|
+
?? pending?.inputSeq
|
|
3328
|
+
?? 0) || 0;
|
|
3329
|
+
return {
|
|
3330
|
+
deviceId: device.deviceId,
|
|
3331
|
+
commandId: safeString(message.commandId || message.CommandId || pending?.commandId, 128),
|
|
3332
|
+
inputSeq,
|
|
3333
|
+
inputType: safeString(
|
|
3334
|
+
message.inputType
|
|
3335
|
+
|| message.InputType
|
|
3336
|
+
|| result.inputType
|
|
3337
|
+
|| result.InputType
|
|
3338
|
+
|| pending?.inputType,
|
|
3339
|
+
48),
|
|
3340
|
+
issuedAtEpochMs: Number(
|
|
3341
|
+
message.issuedAtEpochMs
|
|
3342
|
+
?? message.IssuedAtEpochMs
|
|
3343
|
+
?? result.issuedAtEpochMs
|
|
3344
|
+
?? result.IssuedAtEpochMs
|
|
3345
|
+
?? pending?.issuedAtEpochMs
|
|
3346
|
+
?? 0) || 0,
|
|
3347
|
+
hubReceivedAtEpochMs: Number(
|
|
3348
|
+
message.hubReceivedAtEpochMs
|
|
3349
|
+
?? message.HubReceivedAtEpochMs
|
|
3350
|
+
?? result.hubReceivedAtEpochMs
|
|
3351
|
+
?? result.HubReceivedAtEpochMs
|
|
3352
|
+
?? pending?.hubReceivedAtEpochMs
|
|
3353
|
+
?? 0) || 0,
|
|
3354
|
+
hubForwardedAtEpochMs: Number(
|
|
3355
|
+
message.hubForwardedAtEpochMs
|
|
3356
|
+
?? message.HubForwardedAtEpochMs
|
|
3357
|
+
?? result.hubForwardedAtEpochMs
|
|
3358
|
+
?? result.HubForwardedAtEpochMs
|
|
3359
|
+
?? pending?.hubForwardedAtEpochMs
|
|
3360
|
+
?? 0) || 0,
|
|
3361
|
+
fallback: pending?.fallback === true,
|
|
3362
|
+
hubAcknowledgedAtEpochMs: Date.now()
|
|
3363
|
+
};
|
|
3364
|
+
}
|
|
3365
|
+
|
|
3366
|
+
function emitRemoteInputApplied(device, message = {}, pending = null) {
|
|
3367
|
+
const outcome = buildRemoteInputOutcome(device, message, pending);
|
|
3368
|
+
emitEvent('RemoteInputApplied', {
|
|
3369
|
+
...outcome,
|
|
3370
|
+
agentApplyMs: Number(
|
|
3371
|
+
message.agentApplyMs
|
|
3372
|
+
?? message.AgentApplyMs
|
|
3373
|
+
?? message.result?.agentApplyMs
|
|
3374
|
+
?? message.result?.AgentApplyMs
|
|
3375
|
+
?? 0) || 0
|
|
3376
|
+
});
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3379
|
+
function emitRemoteInputError(device, message = {}, pending = null, fallbackError = 'remote-input-failed') {
|
|
3380
|
+
const outcome = buildRemoteInputOutcome(device, message, pending);
|
|
3381
|
+
emitEvent('RemoteInputError', {
|
|
3382
|
+
...outcome,
|
|
3383
|
+
error: safeString(
|
|
3384
|
+
message.error
|
|
3385
|
+
|| message.Error
|
|
3386
|
+
|| message.result?.error
|
|
3387
|
+
|| message.result?.Error
|
|
3388
|
+
|| fallbackError,
|
|
3389
|
+
600) || fallbackError,
|
|
3390
|
+
failedAtEpochMs: Number(
|
|
3391
|
+
message.failedAtEpochMs
|
|
3392
|
+
?? message.FailedAtEpochMs
|
|
3393
|
+
?? message.result?.failedAtEpochMs
|
|
3394
|
+
?? message.result?.FailedAtEpochMs
|
|
3395
|
+
?? 0) || 0
|
|
3396
|
+
});
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
function handleRemoteInputOutcome(device, message = {}, source = 'input') {
|
|
3400
|
+
const commandId = safeString(message.commandId || message.CommandId, 128);
|
|
3401
|
+
const inputSeq = Number(message.inputSeq ?? message.InputSeq ?? 0) || 0;
|
|
3402
|
+
const pending = takePendingInputFallback(device, commandId, inputSeq);
|
|
3403
|
+
if (!pending
|
|
3404
|
+
&& source === 'main'
|
|
3405
|
+
&& wasInputFallbackCompleted(device, commandId, inputSeq)) {
|
|
3406
|
+
return false;
|
|
3407
|
+
}
|
|
3408
|
+
// Main-channel input acknowledgements only exist for a tracked
|
|
3409
|
+
// compatibility fallback. Ignore stale/unmatched messages so an old
|
|
3410
|
+
// session cannot acknowledge input from the current control session.
|
|
3411
|
+
if (!pending && source === 'main') {
|
|
3412
|
+
return false;
|
|
3413
|
+
}
|
|
3414
|
+
if (message.type === 'input.error') {
|
|
3415
|
+
emitRemoteInputError(device, message, pending);
|
|
3416
|
+
} else {
|
|
3417
|
+
emitRemoteInputApplied(device, message, pending);
|
|
3418
|
+
}
|
|
3419
|
+
return true;
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3422
|
+
function handleInputFallbackCommandResult(device, message = {}) {
|
|
3423
|
+
const commandId = safeString(message.commandId || message.CommandId, 128);
|
|
3424
|
+
const pending = findPendingInputFallback(device, commandId, 0);
|
|
3425
|
+
if (!pending) {
|
|
3426
|
+
return false;
|
|
3427
|
+
}
|
|
3428
|
+
const completed = takePendingInputFallback(device, commandId, pending.inputSeq);
|
|
3429
|
+
const result = message.result && typeof message.result === 'object' ? message.result : {};
|
|
3430
|
+
const error = safeString(message.error || message.Error || result.error || result.Error, 600);
|
|
3431
|
+
const failed = message.type === 'command.error'
|
|
3432
|
+
|| !!error
|
|
3433
|
+
|| result.ok === false
|
|
3434
|
+
|| safeString(result.status, 40).toLowerCase() === 'failed';
|
|
3435
|
+
if (failed) {
|
|
3436
|
+
emitRemoteInputError(device, { ...message, error: error || 'remote-input-failed' }, completed);
|
|
3437
|
+
} else {
|
|
3438
|
+
emitRemoteInputApplied(device, message, completed);
|
|
3439
|
+
}
|
|
3440
|
+
return true;
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
function schedulePendingInputFallback(device, pending) {
|
|
3444
|
+
const pendingFallbacks = ensurePendingInputFallbacks(device);
|
|
3445
|
+
const existing = pendingFallbacks.get(pending.commandId);
|
|
3446
|
+
if (existing?.timeoutTimer) {
|
|
3447
|
+
clearTimeout(existing.timeoutTimer);
|
|
3448
|
+
}
|
|
3449
|
+
pendingFallbacks.set(pending.commandId, pending);
|
|
3450
|
+
pending.timeoutTimer = setTimeout(() => {
|
|
3451
|
+
const timedOut = takePendingInputFallback(device, pending.commandId, pending.inputSeq);
|
|
3452
|
+
if (timedOut) {
|
|
3453
|
+
emitRemoteInputError(device, { error: 'remote-input-timeout' }, timedOut, 'remote-input-timeout');
|
|
3454
|
+
}
|
|
3455
|
+
}, inputAckTimeoutMs);
|
|
3456
|
+
pending.timeoutTimer.unref?.();
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3459
|
+
function failAllPendingInputFallbacks(device, error = 'device-disconnected') {
|
|
3460
|
+
if (!device || typeof device !== 'object') {
|
|
3461
|
+
return 0;
|
|
3462
|
+
}
|
|
3463
|
+
const pendingFallbacks = ensurePendingInputFallbacks(device);
|
|
3464
|
+
const pending = [...pendingFallbacks.values()];
|
|
3465
|
+
for (const item of pending) {
|
|
3466
|
+
const failed = takePendingInputFallback(device, item.commandId, item.inputSeq);
|
|
3467
|
+
if (failed) {
|
|
3468
|
+
emitRemoteInputError(device, { error }, failed, error);
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
device.completedInputFallbacks.clear();
|
|
3472
|
+
return pending.length;
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
function scheduleTaskTimeout(device, task) {
|
|
3130
3476
|
const commandId = safeString(task?.commandId, 128);
|
|
3131
3477
|
if (!device?.pendingTaskTimers || !commandId) {
|
|
3132
3478
|
return;
|
|
@@ -3365,10 +3711,108 @@ export function createRemoteHub(options = {}) {
|
|
|
3365
3711
|
return true;
|
|
3366
3712
|
}
|
|
3367
3713
|
|
|
3368
|
-
function
|
|
3369
|
-
const
|
|
3370
|
-
const
|
|
3371
|
-
if (!
|
|
3714
|
+
function promotePendingLiveStream(device, streamState, message, transport) {
|
|
3715
|
+
const pending = getPendingLiveStreamDescriptor(streamState);
|
|
3716
|
+
const commandId = safeString(message.commandId, 128);
|
|
3717
|
+
if (!pending || !commandId || commandId !== safeString(pending.commandId, 128)) {
|
|
3718
|
+
return null;
|
|
3719
|
+
}
|
|
3720
|
+
const messageCaptureGeneration = Number(message.captureGeneration || 0);
|
|
3721
|
+
const pendingCaptureGeneration = Number(pending.captureGeneration || 0);
|
|
3722
|
+
if (messageCaptureGeneration > 0
|
|
3723
|
+
&& pendingCaptureGeneration > 0
|
|
3724
|
+
&& messageCaptureGeneration !== pendingCaptureGeneration) {
|
|
3725
|
+
emitRemoteEvent('RemoteLiveStreamOpenIgnored', device, {
|
|
3726
|
+
reason: 'stale-pending-capture-generation',
|
|
3727
|
+
streamId: streamState.streamId,
|
|
3728
|
+
commandId,
|
|
3729
|
+
captureGeneration: messageCaptureGeneration,
|
|
3730
|
+
pendingCaptureGeneration,
|
|
3731
|
+
transport
|
|
3732
|
+
});
|
|
3733
|
+
return null;
|
|
3734
|
+
}
|
|
3735
|
+
|
|
3736
|
+
const previousCommandId = safeString(streamState.commandId, 128);
|
|
3737
|
+
const transfer = buildRemoteFrameTransferDescriptorFromMessage(
|
|
3738
|
+
message,
|
|
3739
|
+
pending.mode || pending.frameMode || DEFAULT_REMOTE_FRAME_MODE);
|
|
3740
|
+
const openedAt = safeString(message.openedAt, 80) || device.lastSeenAt || new Date().toISOString();
|
|
3741
|
+
const nextStreamState = {
|
|
3742
|
+
...streamState,
|
|
3743
|
+
...pending,
|
|
3744
|
+
streamId: streamState.streamId,
|
|
3745
|
+
commandId,
|
|
3746
|
+
active: true,
|
|
3747
|
+
open: true,
|
|
3748
|
+
openedAt,
|
|
3749
|
+
openTransport: transport,
|
|
3750
|
+
mode: transfer.mode,
|
|
3751
|
+
frameMode: transfer.frameMode,
|
|
3752
|
+
frameProfile: transfer.frameProfile,
|
|
3753
|
+
encoding: transfer.encoding,
|
|
3754
|
+
codec: transfer.codec,
|
|
3755
|
+
compression: transfer.compression,
|
|
3756
|
+
transferProtocol: transfer.transferProtocol,
|
|
3757
|
+
transferProtocolVersion: transfer.transferProtocolVersion,
|
|
3758
|
+
handshake: transfer.handshake,
|
|
3759
|
+
width: Number.isFinite(Number(message.width)) ? Number(message.width) : Number(pending.width || 0),
|
|
3760
|
+
height: Number.isFinite(Number(message.height)) ? Number(message.height) : Number(pending.height || 0),
|
|
3761
|
+
sourceWidth: Number.isFinite(Number(message.sourceWidth)) ? Number(message.sourceWidth) : Number(pending.sourceWidth || 0),
|
|
3762
|
+
sourceHeight: Number.isFinite(Number(message.sourceHeight)) ? Number(message.sourceHeight) : Number(pending.sourceHeight || 0),
|
|
3763
|
+
monitorIndex: Number.isFinite(Number(message.monitorIndex))
|
|
3764
|
+
? normalizeMonitorIndex(message.monitorIndex)
|
|
3765
|
+
: Number(pending.monitorIndex || 0),
|
|
3766
|
+
monitorCount: Number.isFinite(Number(message.monitorCount))
|
|
3767
|
+
? clampNumber(message.monitorCount, 1, 64, 1)
|
|
3768
|
+
: Number(pending.monitorCount || 1),
|
|
3769
|
+
streamPurpose: safeString(message.streamPurpose || pending.streamPurpose, 24) || 'wall',
|
|
3770
|
+
captureGeneration: messageCaptureGeneration > 0
|
|
3771
|
+
? messageCaptureGeneration
|
|
3772
|
+
: pendingCaptureGeneration,
|
|
3773
|
+
openedFrameSeq: Number.isFinite(Number(message.frameSeq)) ? Number(message.frameSeq) : 0,
|
|
3774
|
+
lastFrameAt: '',
|
|
3775
|
+
lastFrameSeq: 0,
|
|
3776
|
+
framesReceived: 0,
|
|
3777
|
+
latestFrame: null,
|
|
3778
|
+
stoppedAt: '',
|
|
3779
|
+
stopReason: ''
|
|
3780
|
+
};
|
|
3781
|
+
clearPendingLiveStreamDescriptor(device, streamState);
|
|
3782
|
+
setPendingLiveStreamDescriptor(nextStreamState, null);
|
|
3783
|
+
if (device.latestLiveFrame?.streamId === streamState.streamId
|
|
3784
|
+
&& safeString(device.latestLiveFrame.commandId, 128) === previousCommandId) {
|
|
3785
|
+
device.latestLiveFrame = null;
|
|
3786
|
+
}
|
|
3787
|
+
setDeviceLiveStream(device, nextStreamState);
|
|
3788
|
+
return {
|
|
3789
|
+
streamState: nextStreamState,
|
|
3790
|
+
transfer,
|
|
3791
|
+
previousCommandId
|
|
3792
|
+
};
|
|
3793
|
+
}
|
|
3794
|
+
|
|
3795
|
+
function emitLiveStreamOpened(device, streamState, transfer, transport, previousCommandId = '') {
|
|
3796
|
+
emitRemoteEvent('RemoteLiveStreamOpened', device, {
|
|
3797
|
+
streamId: streamState.streamId,
|
|
3798
|
+
commandId: streamState.commandId,
|
|
3799
|
+
previousCommandId,
|
|
3800
|
+
handoff: !!previousCommandId,
|
|
3801
|
+
mode: transfer.mode,
|
|
3802
|
+
frameMode: transfer.frameMode,
|
|
3803
|
+
frameProfile: transfer.frameProfile,
|
|
3804
|
+
encoding: transfer.encoding,
|
|
3805
|
+
compression: transfer.compression,
|
|
3806
|
+
transport,
|
|
3807
|
+
captureGeneration: Number(streamState.captureGeneration || 0),
|
|
3808
|
+
streamPurpose: safeString(streamState.streamPurpose, 24) || 'wall'
|
|
3809
|
+
});
|
|
3810
|
+
}
|
|
3811
|
+
|
|
3812
|
+
function applyLiveStreamOpen(device, message, transport = 'json') {
|
|
3813
|
+
const streamId = safeString(message.streamId, 128) || 'live';
|
|
3814
|
+
const streamState = getDeviceLiveStream(device, streamId);
|
|
3815
|
+
if (!streamState?.active) {
|
|
3372
3816
|
emitRemoteEvent('RemoteLiveStreamOpenIgnored', device, {
|
|
3373
3817
|
reason: 'stale-live-stream-open',
|
|
3374
3818
|
streamId,
|
|
@@ -3376,12 +3820,13 @@ export function createRemoteHub(options = {}) {
|
|
|
3376
3820
|
});
|
|
3377
3821
|
return false;
|
|
3378
3822
|
}
|
|
3379
|
-
|
|
3380
|
-
const commandId = safeString(message.commandId, 128);
|
|
3381
|
-
const activeCommandId = safeString(streamState.commandId, 128);
|
|
3382
|
-
const
|
|
3383
|
-
const
|
|
3384
|
-
const
|
|
3823
|
+
|
|
3824
|
+
const commandId = safeString(message.commandId, 128);
|
|
3825
|
+
const activeCommandId = safeString(streamState.commandId, 128);
|
|
3826
|
+
const pendingDescriptor = getPendingLiveStreamDescriptor(streamState);
|
|
3827
|
+
const pendingCommandId = safeString(pendingDescriptor?.commandId, 128);
|
|
3828
|
+
const isActiveGeneration = !commandId || !activeCommandId || commandId === activeCommandId;
|
|
3829
|
+
const isPendingGeneration = !!pendingCommandId && commandId === pendingCommandId;
|
|
3385
3830
|
if (!isActiveGeneration && !isPendingGeneration) {
|
|
3386
3831
|
emitRemoteEvent('RemoteLiveStreamOpenIgnored', device, {
|
|
3387
3832
|
reason: 'stale-live-stream-generation',
|
|
@@ -3391,21 +3836,31 @@ export function createRemoteHub(options = {}) {
|
|
|
3391
3836
|
pendingCommandId,
|
|
3392
3837
|
transport
|
|
3393
3838
|
});
|
|
3394
|
-
return false;
|
|
3395
|
-
}
|
|
3396
|
-
|
|
3397
|
-
const activatingPendingGeneration = isPendingGeneration && commandId !== activeCommandId;
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3839
|
+
return false;
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
const activatingPendingGeneration = isPendingGeneration && commandId !== activeCommandId;
|
|
3843
|
+
if (activatingPendingGeneration) {
|
|
3844
|
+
const promoted = promotePendingLiveStream(device, streamState, message, transport);
|
|
3845
|
+
if (!promoted) {
|
|
3846
|
+
return false;
|
|
3847
|
+
}
|
|
3848
|
+
emitLiveStreamOpened(
|
|
3849
|
+
device,
|
|
3850
|
+
promoted.streamState,
|
|
3851
|
+
promoted.transfer,
|
|
3852
|
+
transport,
|
|
3853
|
+
promoted.previousCommandId);
|
|
3854
|
+
return true;
|
|
3855
|
+
}
|
|
3856
|
+
|
|
3857
|
+
const transfer = buildRemoteFrameTransferDescriptorFromMessage(message, streamState.mode || DEFAULT_REMOTE_FRAME_MODE);
|
|
3858
|
+
const openedAt = safeString(message.openedAt, 80) || device.lastSeenAt || new Date().toISOString();
|
|
3859
|
+
const nextStreamState = {
|
|
3860
|
+
...streamState,
|
|
3861
|
+
open: true,
|
|
3862
|
+
openedAt,
|
|
3863
|
+
openTransport: transport,
|
|
3409
3864
|
mode: transfer.mode,
|
|
3410
3865
|
frameMode: transfer.frameMode,
|
|
3411
3866
|
frameProfile: transfer.frameProfile,
|
|
@@ -3419,47 +3874,54 @@ export function createRemoteHub(options = {}) {
|
|
|
3419
3874
|
height: Number.isFinite(Number(message.height)) ? Number(message.height) : Number(streamState.height || 0),
|
|
3420
3875
|
sourceWidth: Number.isFinite(Number(message.sourceWidth)) ? Number(message.sourceWidth) : Number(streamState.sourceWidth || 0),
|
|
3421
3876
|
sourceHeight: Number.isFinite(Number(message.sourceHeight)) ? Number(message.sourceHeight) : Number(streamState.sourceHeight || 0),
|
|
3422
|
-
monitorIndex: Number.isFinite(Number(message.monitorIndex)) ? normalizeMonitorIndex(message.monitorIndex) : Number(streamState.monitorIndex || 0),
|
|
3423
|
-
monitorCount: Number.isFinite(Number(message.monitorCount)) ? clampNumber(message.monitorCount, 1, 64, 1) : Number(streamState.monitorCount || 1),
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
frameProfile: transfer.frameProfile,
|
|
3435
|
-
encoding: transfer.encoding,
|
|
3436
|
-
compression: transfer.compression,
|
|
3437
|
-
transport
|
|
3438
|
-
});
|
|
3439
|
-
return true;
|
|
3440
|
-
}
|
|
3877
|
+
monitorIndex: Number.isFinite(Number(message.monitorIndex)) ? normalizeMonitorIndex(message.monitorIndex) : Number(streamState.monitorIndex || 0),
|
|
3878
|
+
monitorCount: Number.isFinite(Number(message.monitorCount)) ? clampNumber(message.monitorCount, 1, 64, 1) : Number(streamState.monitorCount || 1),
|
|
3879
|
+
streamPurpose: safeString(message.streamPurpose || streamState.streamPurpose, 24) || 'wall',
|
|
3880
|
+
captureGeneration: Number.isSafeInteger(Number(message.captureGeneration)) && Number(message.captureGeneration) > 0
|
|
3881
|
+
? Number(message.captureGeneration)
|
|
3882
|
+
: Number(streamState.captureGeneration || 0),
|
|
3883
|
+
openedFrameSeq: Number.isFinite(Number(message.frameSeq)) ? Number(message.frameSeq) : Number(streamState.openedFrameSeq || 0)
|
|
3884
|
+
};
|
|
3885
|
+
setDeviceLiveStream(device, nextStreamState);
|
|
3886
|
+
emitLiveStreamOpened(device, nextStreamState, transfer, transport);
|
|
3887
|
+
return true;
|
|
3888
|
+
}
|
|
3441
3889
|
|
|
3442
3890
|
function applyLiveFrame(device, message, framePayload, transport = 'json-base64') {
|
|
3443
3891
|
const frameData = Buffer.isBuffer(framePayload)
|
|
3444
3892
|
? ''
|
|
3445
3893
|
: safeString(framePayload, MAX_STREAM_BASE64_CHARS + 1);
|
|
3446
|
-
const frameSeq = Number(message.frameSeq);
|
|
3447
|
-
const streamId = safeString(message.streamId, 128) || 'live';
|
|
3448
|
-
const commandId = safeString(message.commandId, 128);
|
|
3449
|
-
|
|
3450
|
-
if (!streamState?.active) {
|
|
3894
|
+
const frameSeq = Number(message.frameSeq);
|
|
3895
|
+
const streamId = safeString(message.streamId, 128) || 'live';
|
|
3896
|
+
const commandId = safeString(message.commandId, 128);
|
|
3897
|
+
let streamState = getDeviceLiveStream(device, streamId);
|
|
3898
|
+
if (!streamState?.active) {
|
|
3451
3899
|
device.counters.liveFramesDropped += 1;
|
|
3452
3900
|
emitRemoteEvent('RemoteFrameDropped', device, {
|
|
3453
3901
|
reason: 'stale-live-stream-frame',
|
|
3454
3902
|
streamId,
|
|
3455
3903
|
frameSeq: Number.isFinite(frameSeq) ? frameSeq : null,
|
|
3456
3904
|
transport
|
|
3457
|
-
});
|
|
3458
|
-
return false;
|
|
3459
|
-
}
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3905
|
+
});
|
|
3906
|
+
return false;
|
|
3907
|
+
}
|
|
3908
|
+
let activeCommandId = safeString(streamState.commandId, 128);
|
|
3909
|
+
let pendingCommandId = safeString(getPendingLiveStreamDescriptor(streamState)?.commandId, 128);
|
|
3910
|
+
if (pendingCommandId && commandId === pendingCommandId && commandId !== activeCommandId) {
|
|
3911
|
+
const promoted = promotePendingLiveStream(device, streamState, message, `${transport}-implicit`);
|
|
3912
|
+
if (promoted) {
|
|
3913
|
+
streamState = promoted.streamState;
|
|
3914
|
+
activeCommandId = safeString(streamState.commandId, 128);
|
|
3915
|
+
pendingCommandId = '';
|
|
3916
|
+
emitLiveStreamOpened(
|
|
3917
|
+
device,
|
|
3918
|
+
streamState,
|
|
3919
|
+
promoted.transfer,
|
|
3920
|
+
`${transport}-implicit`,
|
|
3921
|
+
promoted.previousCommandId);
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
const isActiveGeneration = !commandId || !activeCommandId || commandId === activeCommandId;
|
|
3463
3925
|
if (!isActiveGeneration) {
|
|
3464
3926
|
device.counters.liveFramesDropped += 1;
|
|
3465
3927
|
emitRemoteEvent('RemoteFrameDropped', device, {
|
|
@@ -3471,8 +3933,25 @@ export function createRemoteHub(options = {}) {
|
|
|
3471
3933
|
frameSeq: Number.isFinite(frameSeq) ? frameSeq : null,
|
|
3472
3934
|
transport
|
|
3473
3935
|
});
|
|
3474
|
-
return false;
|
|
3475
|
-
}
|
|
3936
|
+
return false;
|
|
3937
|
+
}
|
|
3938
|
+
const frameCaptureGeneration = Number(message.captureGeneration || streamState.captureGeneration || 0);
|
|
3939
|
+
const activeCaptureGeneration = Number(streamState.captureGeneration || 0);
|
|
3940
|
+
if (activeCaptureGeneration > 0
|
|
3941
|
+
&& frameCaptureGeneration > 0
|
|
3942
|
+
&& frameCaptureGeneration !== activeCaptureGeneration) {
|
|
3943
|
+
device.counters.liveFramesDropped += 1;
|
|
3944
|
+
emitRemoteEvent('RemoteFrameDropped', device, {
|
|
3945
|
+
reason: 'stale-capture-generation-frame',
|
|
3946
|
+
streamId,
|
|
3947
|
+
commandId,
|
|
3948
|
+
captureGeneration: frameCaptureGeneration,
|
|
3949
|
+
activeCaptureGeneration,
|
|
3950
|
+
frameSeq: Number.isFinite(frameSeq) ? frameSeq : null,
|
|
3951
|
+
transport
|
|
3952
|
+
});
|
|
3953
|
+
return false;
|
|
3954
|
+
}
|
|
3476
3955
|
|
|
3477
3956
|
const byteLength = normalizeFrameByteLength(framePayload, frameData);
|
|
3478
3957
|
if ((!Buffer.isBuffer(framePayload) && !frameData)
|
|
@@ -3494,7 +3973,7 @@ export function createRemoteHub(options = {}) {
|
|
|
3494
3973
|
const contentHash = buildFrameContentHash(message, payload);
|
|
3495
3974
|
const sameContentStreak = computeSameContentStreak(streamState.latestFrame, contentHash);
|
|
3496
3975
|
const transfer = buildRemoteFrameTransferDescriptorFromMessage(message, streamState.mode || DEFAULT_REMOTE_FRAME_MODE);
|
|
3497
|
-
if (streamState.open !== true) {
|
|
3976
|
+
if (streamState.open !== true) {
|
|
3498
3977
|
streamState.open = true;
|
|
3499
3978
|
streamState.openedAt = device.lastSeenAt;
|
|
3500
3979
|
streamState.openTransport = `${transport}-implicit`;
|
|
@@ -3506,14 +3985,22 @@ export function createRemoteHub(options = {}) {
|
|
|
3506
3985
|
streamState.compression = transfer.compression;
|
|
3507
3986
|
streamState.transferProtocol = transfer.transferProtocol;
|
|
3508
3987
|
streamState.transferProtocolVersion = transfer.transferProtocolVersion;
|
|
3509
|
-
streamState.handshake = transfer.handshake;
|
|
3510
|
-
}
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3988
|
+
streamState.handshake = transfer.handshake;
|
|
3989
|
+
}
|
|
3990
|
+
const currentGenerationVerified = (!activeCommandId || commandId === activeCommandId)
|
|
3991
|
+
&& (activeCaptureGeneration <= 0
|
|
3992
|
+
|| (frameCaptureGeneration > 0
|
|
3993
|
+
&& frameCaptureGeneration === activeCaptureGeneration));
|
|
3994
|
+
device.latestLiveFrame = {
|
|
3995
|
+
streamId,
|
|
3996
|
+
frameSeq,
|
|
3997
|
+
streamFrameSeq: Number.isFinite(Number(message.streamFrameSeq)) ? Number(message.streamFrameSeq) : frameSeq,
|
|
3998
|
+
commandId: commandId || activeCommandId,
|
|
3999
|
+
sessionId: device.sessionId,
|
|
4000
|
+
captureGeneration: activeCaptureGeneration || frameCaptureGeneration,
|
|
4001
|
+
currentGenerationVerified,
|
|
4002
|
+
streamPurpose: safeString(message.streamPurpose || streamState.streamPurpose, 24) || 'wall',
|
|
4003
|
+
width: Number.isFinite(Number(message.width)) ? Number(message.width) : 0,
|
|
3517
4004
|
height: Number.isFinite(Number(message.height)) ? Number(message.height) : 0,
|
|
3518
4005
|
sourceWidth: Number.isFinite(Number(message.sourceWidth)) ? Number(message.sourceWidth) : Number(message.width || 0),
|
|
3519
4006
|
sourceHeight: Number.isFinite(Number(message.sourceHeight)) ? Number(message.sourceHeight) : Number(message.height || 0),
|
|
@@ -3603,11 +4090,21 @@ export function createRemoteHub(options = {}) {
|
|
|
3603
4090
|
streamState.sourceWidth = device.latestLiveFrame.sourceWidth;
|
|
3604
4091
|
streamState.sourceHeight = device.latestLiveFrame.sourceHeight;
|
|
3605
4092
|
streamState.monitorIndex = device.latestLiveFrame.monitorIndex;
|
|
3606
|
-
streamState.monitorCount = device.latestLiveFrame.monitorCount;
|
|
3607
|
-
ensureDeviceLiveStreams(device).set(streamId, streamState);
|
|
3608
|
-
device.counters.liveFramesReceived += 1;
|
|
3609
|
-
|
|
3610
|
-
|
|
4093
|
+
streamState.monitorCount = device.latestLiveFrame.monitorCount;
|
|
4094
|
+
ensureDeviceLiveStreams(device).set(streamId, streamState);
|
|
4095
|
+
device.counters.liveFramesReceived += 1;
|
|
4096
|
+
if (streamState.framesReceived === 1 && liveStreamHasCurrentFrame(streamState)) {
|
|
4097
|
+
emitRemoteEvent('RemoteLiveStreamReady', device, {
|
|
4098
|
+
streamId,
|
|
4099
|
+
commandId: streamState.commandId,
|
|
4100
|
+
captureGeneration: Number(streamState.captureGeneration || 0),
|
|
4101
|
+
streamPurpose: safeString(streamState.streamPurpose, 24) || 'wall',
|
|
4102
|
+
monitorIndex: Number(streamState.monitorIndex || 0),
|
|
4103
|
+
frameSeq
|
|
4104
|
+
});
|
|
4105
|
+
}
|
|
4106
|
+
return true;
|
|
4107
|
+
}
|
|
3611
4108
|
|
|
3612
4109
|
function applyAudioFrame(device, message, framePayload, transport = 'json-base64') {
|
|
3613
4110
|
const audioData = Buffer.isBuffer(framePayload)
|
|
@@ -3906,8 +4403,21 @@ export function createRemoteHub(options = {}) {
|
|
|
3906
4403
|
return;
|
|
3907
4404
|
}
|
|
3908
4405
|
|
|
3909
|
-
if (!state.authenticated) {
|
|
3910
|
-
if (message.type
|
|
4406
|
+
if (!state.authenticated) {
|
|
4407
|
+
if (message.type === 'slot.assign') {
|
|
4408
|
+
if (!timingSafeStringEqual(message.pairToken, pairToken)) {
|
|
4409
|
+
writeJsonLine(socket, { type: 'slot.assignment', ok: false, error: 'invalid-pair-token' });
|
|
4410
|
+
socket.end();
|
|
4411
|
+
return;
|
|
4412
|
+
}
|
|
4413
|
+
const result = assignDeviceSlot(
|
|
4414
|
+
message.deviceId || message.DeviceId,
|
|
4415
|
+
message.slotNumber ?? message.slot ?? message.SlotNumber);
|
|
4416
|
+
writeJsonLine(socket, { type: 'slot.assignment', ...result });
|
|
4417
|
+
socket.end();
|
|
4418
|
+
return;
|
|
4419
|
+
}
|
|
4420
|
+
if (message.type !== 'hello') {
|
|
3911
4421
|
writeJsonLine(socket, { type: 'error', error: 'hello-required' });
|
|
3912
4422
|
socket.destroy();
|
|
3913
4423
|
return;
|
|
@@ -3981,32 +4491,27 @@ export function createRemoteHub(options = {}) {
|
|
|
3981
4491
|
return;
|
|
3982
4492
|
}
|
|
3983
4493
|
|
|
3984
|
-
if (state.inputOnly) {
|
|
3985
|
-
device.inputLastSeenAt = new Date().toISOString();
|
|
3986
|
-
if (message.type === 'input.applied') {
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
hubAcknowledgedAtEpochMs: Date.now()
|
|
4006
|
-
});
|
|
4007
|
-
}
|
|
4008
|
-
return;
|
|
4009
|
-
}
|
|
4494
|
+
if (state.inputOnly) {
|
|
4495
|
+
device.inputLastSeenAt = new Date().toISOString();
|
|
4496
|
+
if (message.type === 'input.applied') {
|
|
4497
|
+
handleRemoteInputOutcome(device, {
|
|
4498
|
+
...message,
|
|
4499
|
+
agentApplyMs: Number(message.agentApplyMs || 0) || 0
|
|
4500
|
+
}, 'input');
|
|
4501
|
+
}
|
|
4502
|
+
if (message.type === 'input.error') {
|
|
4503
|
+
const pending = takePendingInputFallback(
|
|
4504
|
+
device,
|
|
4505
|
+
message.commandId || message.CommandId,
|
|
4506
|
+
message.inputSeq ?? message.InputSeq);
|
|
4507
|
+
emitEvent('RemoteInputError', {
|
|
4508
|
+
...buildRemoteInputOutcome(device, message, pending),
|
|
4509
|
+
error: safeString(message.error || message.Error, 600) || 'remote-input-failed',
|
|
4510
|
+
failedAtEpochMs: Number(message.failedAtEpochMs ?? message.FailedAtEpochMs ?? 0) || 0
|
|
4511
|
+
});
|
|
4512
|
+
}
|
|
4513
|
+
return;
|
|
4514
|
+
}
|
|
4010
4515
|
if (state.frameOnly) {
|
|
4011
4516
|
device.frameLastSeenAt = new Date().toISOString();
|
|
4012
4517
|
return;
|
|
@@ -4028,25 +4533,30 @@ export function createRemoteHub(options = {}) {
|
|
|
4028
4533
|
device.status = typeof message.status === 'object' && message.status
|
|
4029
4534
|
? { ...message.status }
|
|
4030
4535
|
: {};
|
|
4031
|
-
{
|
|
4032
|
-
const nextSlotNumber = normalizeSlotNumber(device.status.slotNumber);
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4536
|
+
{
|
|
4537
|
+
const nextSlotNumber = normalizeSlotNumber(device.status.slotNumber);
|
|
4538
|
+
const assignedSlotNumber = normalizeSlotNumber(device.slotAssignment?.slotNumber);
|
|
4539
|
+
if (nextSlotNumber > 0 && (!assignedSlotNumber || nextSlotNumber === assignedSlotNumber)) {
|
|
4540
|
+
device.slotNumber = nextSlotNumber;
|
|
4541
|
+
}
|
|
4542
|
+
}
|
|
4037
4543
|
device.lastStatusAt = device.lastSeenAt;
|
|
4038
4544
|
device.counters.statusReceived += 1;
|
|
4039
4545
|
emitRemoteEvent('RemoteDeviceStatus', device);
|
|
4040
4546
|
break;
|
|
4041
|
-
case 'command.result':
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
const
|
|
4046
|
-
const
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4547
|
+
case 'command.result':
|
|
4548
|
+
case 'command.error':
|
|
4549
|
+
device.counters.commandResultsReceived += 1;
|
|
4550
|
+
{
|
|
4551
|
+
const commandId = safeString(message.commandId, 128);
|
|
4552
|
+
const error = safeString(
|
|
4553
|
+
message.error || (message.type === 'command.error' ? 'command-failed' : ''),
|
|
4554
|
+
500);
|
|
4555
|
+
const result = message.result ?? null;
|
|
4556
|
+
handleInputFallbackCommandResult(device, message);
|
|
4557
|
+
if (error || result?.ok === false || result?.status === 'failed') {
|
|
4558
|
+
failPendingLiveStream(device, commandId, error || result?.error || 'stream-start-failed');
|
|
4559
|
+
}
|
|
4050
4560
|
const task = applyTaskResult(device, commandId, result, error);
|
|
4051
4561
|
if (task) {
|
|
4052
4562
|
emitRemoteEvent('RemoteTaskResult', device, {
|
|
@@ -4061,8 +4571,12 @@ export function createRemoteHub(options = {}) {
|
|
|
4061
4571
|
commandId: safeString(message.commandId, 128),
|
|
4062
4572
|
result: message.result ?? null,
|
|
4063
4573
|
error: safeString(message.error, 500)
|
|
4064
|
-
});
|
|
4065
|
-
break;
|
|
4574
|
+
});
|
|
4575
|
+
break;
|
|
4576
|
+
case 'input.applied':
|
|
4577
|
+
case 'input.error':
|
|
4578
|
+
handleRemoteInputOutcome(device, message, 'main');
|
|
4579
|
+
break;
|
|
4066
4580
|
case 'thumbnail.frame': {
|
|
4067
4581
|
applyThumbnailFrame(device, message, message.data, 'json-base64');
|
|
4068
4582
|
break;
|
|
@@ -4483,9 +4997,10 @@ export function createRemoteHub(options = {}) {
|
|
|
4483
4997
|
}
|
|
4484
4998
|
|
|
4485
4999
|
async function close() {
|
|
4486
|
-
for (const device of devices.values()) {
|
|
4487
|
-
failAllPendingTasks(device, 'hub-shutdown');
|
|
4488
|
-
|
|
5000
|
+
for (const device of devices.values()) {
|
|
5001
|
+
failAllPendingTasks(device, 'hub-shutdown');
|
|
5002
|
+
failAllPendingInputFallbacks(device, 'hub-shutdown');
|
|
5003
|
+
if (device.socket && !device.socket.destroyed) {
|
|
4489
5004
|
writeJsonLine(device.socket, { type: 'disconnect', reason: 'hub-shutdown' });
|
|
4490
5005
|
device.socket.destroy();
|
|
4491
5006
|
}
|
|
@@ -4530,15 +5045,16 @@ export function createRemoteHub(options = {}) {
|
|
|
4530
5045
|
started = false;
|
|
4531
5046
|
}
|
|
4532
5047
|
|
|
4533
|
-
function disconnectDevice(deviceId, reason = 'manager-disconnect') {
|
|
5048
|
+
function disconnectDevice(deviceId, reason = 'manager-disconnect') {
|
|
4534
5049
|
const device = devices.get(String(deviceId || ''));
|
|
4535
5050
|
if (device?.synthetic === true) {
|
|
4536
5051
|
device.connected = false;
|
|
4537
5052
|
device.disconnectedAt = new Date().toISOString();
|
|
4538
5053
|
device.lastDisconnectReason = reason;
|
|
4539
|
-
deactivateDeviceLiveStreams(device, reason, device.disconnectedAt);
|
|
4540
|
-
failAllPendingTasks(device, 'device-disconnected');
|
|
4541
|
-
|
|
5054
|
+
deactivateDeviceLiveStreams(device, reason, device.disconnectedAt);
|
|
5055
|
+
failAllPendingTasks(device, 'device-disconnected');
|
|
5056
|
+
failAllPendingInputFallbacks(device, 'device-disconnected');
|
|
5057
|
+
emitRemoteEvent('RemoteDeviceDisconnected', device, { reason, synthetic: true });
|
|
4542
5058
|
return true;
|
|
4543
5059
|
}
|
|
4544
5060
|
|
|
@@ -4547,11 +5063,67 @@ export function createRemoteHub(options = {}) {
|
|
|
4547
5063
|
}
|
|
4548
5064
|
|
|
4549
5065
|
writeJsonLine(device.socket, { type: 'disconnect', reason });
|
|
4550
|
-
device.socket.destroy();
|
|
4551
|
-
return true;
|
|
4552
|
-
}
|
|
4553
|
-
|
|
4554
|
-
function
|
|
5066
|
+
device.socket.destroy();
|
|
5067
|
+
return true;
|
|
5068
|
+
}
|
|
5069
|
+
|
|
5070
|
+
function assignDeviceSlot(deviceId, value) {
|
|
5071
|
+
const normalizedDeviceId = safeString(deviceId, 160);
|
|
5072
|
+
const slotNumber = normalizeSlotNumber(value);
|
|
5073
|
+
if (!normalizedDeviceId) {
|
|
5074
|
+
return { ok: false, error: 'device-id-required' };
|
|
5075
|
+
}
|
|
5076
|
+
if (slotNumber <= 0) {
|
|
5077
|
+
return { ok: false, error: 'invalid-slot-number' };
|
|
5078
|
+
}
|
|
5079
|
+
|
|
5080
|
+
const device = devices.get(normalizedDeviceId);
|
|
5081
|
+
if (!device?.connected) {
|
|
5082
|
+
return { ok: false, error: 'device-not-connected' };
|
|
5083
|
+
}
|
|
5084
|
+
|
|
5085
|
+
const conflict = [...devices.values()].find(candidate =>
|
|
5086
|
+
candidate?.connected
|
|
5087
|
+
&& candidate.deviceId !== normalizedDeviceId
|
|
5088
|
+
&& normalizeSlotNumber(candidate.slotNumber) === slotNumber);
|
|
5089
|
+
if (conflict) {
|
|
5090
|
+
return {
|
|
5091
|
+
ok: false,
|
|
5092
|
+
error: 'slot-taken',
|
|
5093
|
+
slotNumber,
|
|
5094
|
+
conflict: {
|
|
5095
|
+
deviceId: conflict.deviceId,
|
|
5096
|
+
deviceName: conflict.deviceName || conflict.hostname || conflict.deviceId,
|
|
5097
|
+
hostname: conflict.hostname || '',
|
|
5098
|
+
slotNumber
|
|
5099
|
+
}
|
|
5100
|
+
};
|
|
5101
|
+
}
|
|
5102
|
+
|
|
5103
|
+
const previousSlotNumber = normalizeSlotNumber(device.slotNumber);
|
|
5104
|
+
device.slotNumber = slotNumber;
|
|
5105
|
+
device.status = {
|
|
5106
|
+
...(device.status && typeof device.status === 'object' ? device.status : {}),
|
|
5107
|
+
slotNumber
|
|
5108
|
+
};
|
|
5109
|
+
device.slotAssignment = {
|
|
5110
|
+
slotNumber,
|
|
5111
|
+
assignedAt: new Date().toISOString()
|
|
5112
|
+
};
|
|
5113
|
+
emitRemoteEvent('RemoteDeviceSlotAssigned', device, {
|
|
5114
|
+
previousSlotNumber,
|
|
5115
|
+
slotNumber
|
|
5116
|
+
});
|
|
5117
|
+
return {
|
|
5118
|
+
ok: true,
|
|
5119
|
+
deviceId: normalizedDeviceId,
|
|
5120
|
+
deviceName: device.deviceName || device.hostname || normalizedDeviceId,
|
|
5121
|
+
previousSlotNumber,
|
|
5122
|
+
slotNumber
|
|
5123
|
+
};
|
|
5124
|
+
}
|
|
5125
|
+
|
|
5126
|
+
function sendCommand(deviceId, command) {
|
|
4555
5127
|
const device = devices.get(String(deviceId || ''));
|
|
4556
5128
|
const commandName = safeString(command?.command || 'ping', 80);
|
|
4557
5129
|
const requiredPermission = commandName === 'input.control'
|
|
@@ -4629,20 +5201,28 @@ export function createRemoteHub(options = {}) {
|
|
|
4629
5201
|
return { ok: false, error: 'device-not-connected' };
|
|
4630
5202
|
}
|
|
4631
5203
|
|
|
4632
|
-
const command = safeString(options.command, 20000);
|
|
4633
|
-
if (!command) return { ok: false, error: 'client-update-command-missing' };
|
|
4634
|
-
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
4635
|
-
const
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
5204
|
+
const command = safeString(options.command, 20000);
|
|
5205
|
+
if (!command) return { ok: false, error: 'client-update-command-missing' };
|
|
5206
|
+
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
5207
|
+
const timeoutMs = clampNumber(options.timeoutMs, 1000, 120000, 30000);
|
|
5208
|
+
const sent = writeJsonLine(device.socket, {
|
|
5209
|
+
type: 'command',
|
|
5210
|
+
commandId,
|
|
5211
|
+
command: 'command.run',
|
|
5212
|
+
payload: {
|
|
5213
|
+
command,
|
|
5214
|
+
timeoutMs,
|
|
5215
|
+
permissionMode: 'full-access',
|
|
5216
|
+
// RemoteFast releases before 0.1.170 read command.run inputs
|
|
5217
|
+
// from toolArguments, while Node and current Agents accept the
|
|
5218
|
+
// direct payload fields. Carry both during the update bridge.
|
|
5219
|
+
toolArguments: {
|
|
5220
|
+
command,
|
|
5221
|
+
timeoutMs
|
|
5222
|
+
}
|
|
5223
|
+
},
|
|
5224
|
+
issuedAt: new Date().toISOString()
|
|
5225
|
+
});
|
|
4646
5226
|
if (!sent) return { ok: false, error: 'device-not-connected' };
|
|
4647
5227
|
device.counters.commandsSent += 1;
|
|
4648
5228
|
emitRemoteEvent('RemoteCommandQueued', device, {
|
|
@@ -4664,20 +5244,57 @@ export function createRemoteHub(options = {}) {
|
|
|
4664
5244
|
return { ok: false, error: 'device-not-connected' };
|
|
4665
5245
|
}
|
|
4666
5246
|
|
|
4667
|
-
const denied = policyError(device, 'allowControl');
|
|
4668
|
-
if (denied) return { ok: false, error: denied };
|
|
4669
|
-
|
|
4670
|
-
if (
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
5247
|
+
const denied = policyError(device, 'allowControl');
|
|
5248
|
+
if (denied) return { ok: false, error: denied };
|
|
5249
|
+
|
|
5250
|
+
if (device.capabilities?.controlPermission === false
|
|
5251
|
+
|| device.capabilities?.accessibilityPermission === false) {
|
|
5252
|
+
return { ok: false, error: 'INPUT_PERMISSION_DENIED' };
|
|
5253
|
+
}
|
|
5254
|
+
if (!readCapabilityFlag(device.capabilities, 'control')) {
|
|
5255
|
+
return { ok: false, error: 'device-input-control-unavailable' };
|
|
5256
|
+
}
|
|
5257
|
+
|
|
5258
|
+
const normalized = normalizeRemoteInputEvent(input);
|
|
5259
|
+
if (!normalized.type) {
|
|
5260
|
+
return { ok: false, error: 'missing-input-type' };
|
|
5261
|
+
}
|
|
5262
|
+
const controlStream = getActiveControlStream(device);
|
|
5263
|
+
if (!controlStream
|
|
5264
|
+
|| !liveStreamHasCurrentFrame(controlStream)
|
|
5265
|
+
|| !safeString(controlStream.commandId, 128)
|
|
5266
|
+
|| Number(controlStream.captureGeneration || 0) <= 0) {
|
|
5267
|
+
return { ok: false, error: 'CONTROL_NOT_READY' };
|
|
5268
|
+
}
|
|
5269
|
+
if (getPendingLiveStreamDescriptor(controlStream)) {
|
|
5270
|
+
return { ok: false, error: 'CAPTURE_TRANSITION_IN_PROGRESS' };
|
|
5271
|
+
}
|
|
5272
|
+
if (!normalized.controlSessionId
|
|
5273
|
+
|| normalized.controlSessionId !== safeString(device.sessionId, 160)) {
|
|
5274
|
+
return {
|
|
5275
|
+
ok: false,
|
|
5276
|
+
error: 'STALE_CONTROL_SESSION',
|
|
5277
|
+
activeSessionId: safeString(device.sessionId, 160)
|
|
5278
|
+
};
|
|
5279
|
+
}
|
|
5280
|
+
if (!normalized.controlCommandId
|
|
5281
|
+
|| normalized.controlCommandId !== safeString(controlStream.commandId, 128)) {
|
|
5282
|
+
return {
|
|
5283
|
+
ok: false,
|
|
5284
|
+
error: 'STALE_CONTROL_SESSION',
|
|
5285
|
+
activeCommandId: safeString(controlStream.commandId, 128)
|
|
5286
|
+
};
|
|
5287
|
+
}
|
|
5288
|
+
if (normalized.captureGeneration !== Number(controlStream.captureGeneration || 0)) {
|
|
5289
|
+
return {
|
|
5290
|
+
ok: false,
|
|
5291
|
+
error: 'STALE_CAPTURE_GENERATION',
|
|
5292
|
+
activeCaptureGeneration: Number(controlStream.captureGeneration || 0)
|
|
5293
|
+
};
|
|
5294
|
+
}
|
|
5295
|
+
|
|
5296
|
+
const inputSocket = device.inputSocket;
|
|
5297
|
+
if (inputSocket && !inputSocket.destroyed) {
|
|
4681
5298
|
const sent = writeJsonLine(inputSocket, {
|
|
4682
5299
|
type: 'input.control',
|
|
4683
5300
|
payload: {
|
|
@@ -4687,22 +5304,57 @@ export function createRemoteHub(options = {}) {
|
|
|
4687
5304
|
}
|
|
4688
5305
|
});
|
|
4689
5306
|
if (sent) {
|
|
4690
|
-
device.counters.commandsSent += 1;
|
|
4691
|
-
device.inputLastSeenAt = new Date().toISOString();
|
|
4692
|
-
return {
|
|
4693
|
-
|
|
5307
|
+
device.counters.commandsSent += 1;
|
|
5308
|
+
device.inputLastSeenAt = new Date().toISOString();
|
|
5309
|
+
return {
|
|
5310
|
+
ok: true,
|
|
5311
|
+
inputSocket: true,
|
|
5312
|
+
sessionId: device.sessionId,
|
|
5313
|
+
commandId: controlStream.commandId,
|
|
5314
|
+
captureGeneration: controlStream.captureGeneration
|
|
5315
|
+
};
|
|
5316
|
+
}
|
|
4694
5317
|
|
|
4695
5318
|
detachInputSocket(inputSocket, 'input-socket-write-failed');
|
|
4696
5319
|
}
|
|
4697
5320
|
|
|
4698
|
-
|
|
5321
|
+
const hubForwardedAtEpochMs = Date.now();
|
|
5322
|
+
const fallback = sendCommand(deviceId, {
|
|
4699
5323
|
command: 'input.control',
|
|
4700
|
-
payload: {
|
|
4701
|
-
...normalized,
|
|
4702
|
-
hubForwardedAtEpochMs
|
|
4703
|
-
issuedAt: normalized.issuedAt || new Date().toISOString()
|
|
4704
|
-
}
|
|
5324
|
+
payload: {
|
|
5325
|
+
...normalized,
|
|
5326
|
+
hubForwardedAtEpochMs,
|
|
5327
|
+
issuedAt: normalized.issuedAt || new Date().toISOString()
|
|
5328
|
+
}
|
|
4705
5329
|
});
|
|
5330
|
+
if (fallback.ok && normalized.requestAck && normalized.inputSeq > 0) {
|
|
5331
|
+
schedulePendingInputFallback(device, {
|
|
5332
|
+
commandId: fallback.commandId,
|
|
5333
|
+
inputSeq: normalized.inputSeq,
|
|
5334
|
+
inputType: normalized.type,
|
|
5335
|
+
issuedAtEpochMs: normalized.issuedAtEpochMs,
|
|
5336
|
+
hubReceivedAtEpochMs: normalized.hubReceivedAtEpochMs,
|
|
5337
|
+
hubForwardedAtEpochMs,
|
|
5338
|
+
fallback: true,
|
|
5339
|
+
timeoutTimer: null
|
|
5340
|
+
});
|
|
5341
|
+
}
|
|
5342
|
+
return fallback.ok
|
|
5343
|
+
? {
|
|
5344
|
+
...fallback,
|
|
5345
|
+
inputSocket: false,
|
|
5346
|
+
fallback: true,
|
|
5347
|
+
sessionId: device.sessionId,
|
|
5348
|
+
deliveryCommandId: fallback.commandId,
|
|
5349
|
+
commandId: controlStream.commandId,
|
|
5350
|
+
captureGeneration: controlStream.captureGeneration,
|
|
5351
|
+
inputSeq: normalized.inputSeq
|
|
5352
|
+
}
|
|
5353
|
+
: {
|
|
5354
|
+
...fallback,
|
|
5355
|
+
error: fallback.error || 'CONTROL_NOT_READY',
|
|
5356
|
+
detail: 'input-channel-not-connected'
|
|
5357
|
+
};
|
|
4706
5358
|
}
|
|
4707
5359
|
|
|
4708
5360
|
function notifyAgentProgress(deviceIds, progress = {}) {
|
|
@@ -5015,31 +5667,100 @@ export function createRemoteHub(options = {}) {
|
|
|
5015
5667
|
|| (device?.activeLiveStream?.streamId === id ? device.activeLiveStream : null);
|
|
5016
5668
|
}
|
|
5017
5669
|
|
|
5018
|
-
function setDeviceLiveStream(device, stream) {
|
|
5019
|
-
if (!device || !stream?.streamId) {
|
|
5020
|
-
return stream;
|
|
5021
|
-
}
|
|
5022
|
-
ensureDeviceLiveStreams(device).set(stream.streamId, stream);
|
|
5023
|
-
device.activeLiveStream = stream;
|
|
5024
|
-
return stream;
|
|
5025
|
-
}
|
|
5026
|
-
|
|
5027
|
-
function
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5670
|
+
function setDeviceLiveStream(device, stream) {
|
|
5671
|
+
if (!device || !stream?.streamId) {
|
|
5672
|
+
return stream;
|
|
5673
|
+
}
|
|
5674
|
+
ensureDeviceLiveStreams(device).set(stream.streamId, stream);
|
|
5675
|
+
device.activeLiveStream = stream;
|
|
5676
|
+
return stream;
|
|
5677
|
+
}
|
|
5678
|
+
|
|
5679
|
+
function ensureLiveStreamReplacementTimers(device) {
|
|
5680
|
+
if (!(device?.liveStreamReplacementTimers instanceof Map)) {
|
|
5681
|
+
Object.defineProperty(device, 'liveStreamReplacementTimers', {
|
|
5682
|
+
value: new Map(),
|
|
5683
|
+
enumerable: false,
|
|
5684
|
+
configurable: true,
|
|
5685
|
+
writable: true
|
|
5686
|
+
});
|
|
5687
|
+
}
|
|
5688
|
+
return device.liveStreamReplacementTimers;
|
|
5689
|
+
}
|
|
5690
|
+
|
|
5691
|
+
function getPendingLiveStreamDescriptor(stream) {
|
|
5692
|
+
return stream?.pendingDescriptor && typeof stream.pendingDescriptor === 'object'
|
|
5693
|
+
? stream.pendingDescriptor
|
|
5694
|
+
: null;
|
|
5695
|
+
}
|
|
5696
|
+
|
|
5697
|
+
function setPendingLiveStreamDescriptor(stream, descriptor) {
|
|
5698
|
+
const pending = descriptor && typeof descriptor === 'object'
|
|
5699
|
+
? { ...descriptor }
|
|
5700
|
+
: null;
|
|
5701
|
+
stream.pendingDescriptor = pending;
|
|
5702
|
+
// Keep the scalar fields for older status consumers while the
|
|
5703
|
+
// descriptor remains the canonical pending-generation state.
|
|
5704
|
+
stream.pendingCommandId = safeString(pending?.commandId, 128);
|
|
5705
|
+
stream.pendingCaptureGeneration = Number(pending?.captureGeneration || 0);
|
|
5706
|
+
stream.pendingStartedAt = safeString(pending?.startedAt, 80);
|
|
5707
|
+
stream.pendingRestartToken = safeString(pending?.restartToken, 128);
|
|
5708
|
+
return pending;
|
|
5709
|
+
}
|
|
5710
|
+
|
|
5711
|
+
function clearLiveStreamReplacementTimer(device, streamId) {
|
|
5712
|
+
const timers = ensureLiveStreamReplacementTimers(device);
|
|
5713
|
+
const key = safeString(streamId, 128);
|
|
5714
|
+
const timer = timers.get(key);
|
|
5715
|
+
if (timer) {
|
|
5716
|
+
clearTimeout(timer);
|
|
5717
|
+
}
|
|
5718
|
+
timers.delete(key);
|
|
5719
|
+
}
|
|
5720
|
+
|
|
5721
|
+
function clearPendingLiveStreamDescriptor(device, stream) {
|
|
5722
|
+
if (!stream) {
|
|
5723
|
+
return null;
|
|
5724
|
+
}
|
|
5725
|
+
const pending = getPendingLiveStreamDescriptor(stream);
|
|
5726
|
+
clearLiveStreamReplacementTimer(device, stream.streamId);
|
|
5727
|
+
setPendingLiveStreamDescriptor(stream, null);
|
|
5728
|
+
return pending;
|
|
5729
|
+
}
|
|
5730
|
+
|
|
5731
|
+
function scheduleLiveStreamReplacementTimeout(device, stream) {
|
|
5732
|
+
const pending = getPendingLiveStreamDescriptor(stream);
|
|
5733
|
+
if (!pending?.commandId) {
|
|
5734
|
+
clearLiveStreamReplacementTimer(device, stream?.streamId);
|
|
5735
|
+
return;
|
|
5736
|
+
}
|
|
5737
|
+
clearLiveStreamReplacementTimer(device, stream.streamId);
|
|
5738
|
+
const timer = setTimeout(() => {
|
|
5739
|
+
const current = getDeviceLiveStream(device, stream.streamId);
|
|
5740
|
+
const currentPending = getPendingLiveStreamDescriptor(current);
|
|
5741
|
+
if (currentPending?.commandId === pending.commandId) {
|
|
5742
|
+
failPendingLiveStream(device, pending.commandId, 'stream-start-timeout');
|
|
5743
|
+
}
|
|
5744
|
+
}, LIVE_STREAM_REPLACEMENT_PENDING_MS);
|
|
5745
|
+
timer.unref?.();
|
|
5746
|
+
ensureLiveStreamReplacementTimers(device).set(stream.streamId, timer);
|
|
5747
|
+
}
|
|
5748
|
+
|
|
5749
|
+
function failPendingLiveStream(device, commandId, error = 'stream-start-failed') {
|
|
5750
|
+
const key = safeString(commandId, 128);
|
|
5751
|
+
if (!device || !key) {
|
|
5752
|
+
return false;
|
|
5753
|
+
}
|
|
5754
|
+
const streams = ensureDeviceLiveStreams(device);
|
|
5755
|
+
for (const stream of streams.values()) {
|
|
5756
|
+
const pending = getPendingLiveStreamDescriptor(stream);
|
|
5757
|
+
if (safeString(pending?.commandId, 128) !== key) {
|
|
5758
|
+
continue;
|
|
5759
|
+
}
|
|
5760
|
+
clearPendingLiveStreamDescriptor(device, stream);
|
|
5761
|
+
emitRemoteEvent('RemoteLiveStreamRestartFailed', device, {
|
|
5762
|
+
streamId: stream.streamId,
|
|
5763
|
+
commandId: key,
|
|
5043
5764
|
error: safeString(error, 500) || 'stream-start-failed'
|
|
5044
5765
|
});
|
|
5045
5766
|
return true;
|
|
@@ -5047,12 +5768,13 @@ export function createRemoteHub(options = {}) {
|
|
|
5047
5768
|
return false;
|
|
5048
5769
|
}
|
|
5049
5770
|
|
|
5050
|
-
function deactivateDeviceLiveStreams(device, reason, stoppedAt = new Date().toISOString()) {
|
|
5051
|
-
const streams = ensureDeviceLiveStreams(device);
|
|
5052
|
-
for (const stream of streams.values()) {
|
|
5053
|
-
stream
|
|
5054
|
-
stream.
|
|
5055
|
-
stream.
|
|
5771
|
+
function deactivateDeviceLiveStreams(device, reason, stoppedAt = new Date().toISOString()) {
|
|
5772
|
+
const streams = ensureDeviceLiveStreams(device);
|
|
5773
|
+
for (const stream of streams.values()) {
|
|
5774
|
+
clearPendingLiveStreamDescriptor(device, stream);
|
|
5775
|
+
stream.active = false;
|
|
5776
|
+
stream.stoppedAt = stoppedAt;
|
|
5777
|
+
stream.stopReason = reason;
|
|
5056
5778
|
}
|
|
5057
5779
|
if (device?.activeLiveStream) {
|
|
5058
5780
|
device.activeLiveStream.active = false;
|
|
@@ -5061,11 +5783,30 @@ export function createRemoteHub(options = {}) {
|
|
|
5061
5783
|
}
|
|
5062
5784
|
}
|
|
5063
5785
|
|
|
5064
|
-
function makeStableLiveStreamId(deviceId, purpose = 'wall') {
|
|
5065
|
-
const hash = crypto.createHash('sha1').update(String(deviceId || 'device')).digest('hex').slice(0, 16);
|
|
5066
|
-
const role = safeString(purpose, 24).toLowerCase().replace(/[^a-z0-9_-]+/g, '-') || 'wall';
|
|
5067
|
-
return `${role}-${hash}`;
|
|
5068
|
-
}
|
|
5786
|
+
function makeStableLiveStreamId(deviceId, purpose = 'wall') {
|
|
5787
|
+
const hash = crypto.createHash('sha1').update(String(deviceId || 'device')).digest('hex').slice(0, 16);
|
|
5788
|
+
const role = safeString(purpose, 24).toLowerCase().replace(/[^a-z0-9_-]+/g, '-') || 'wall';
|
|
5789
|
+
return `${role}-${hash}`;
|
|
5790
|
+
}
|
|
5791
|
+
|
|
5792
|
+
function getActiveLiveStreams(device) {
|
|
5793
|
+
if (!device || typeof device !== 'object') {
|
|
5794
|
+
return [];
|
|
5795
|
+
}
|
|
5796
|
+
return [...ensureDeviceLiveStreams(device).values()].filter(stream => stream?.active === true);
|
|
5797
|
+
}
|
|
5798
|
+
|
|
5799
|
+
function getActiveControlStream(device) {
|
|
5800
|
+
return getActiveLiveStreams(device).find(stream =>
|
|
5801
|
+
safeString(stream?.streamPurpose, 24).toLowerCase() === 'control') || null;
|
|
5802
|
+
}
|
|
5803
|
+
|
|
5804
|
+
function nextCaptureGeneration(device) {
|
|
5805
|
+
const current = Number(device?.captureGenerationCounter || 0);
|
|
5806
|
+
const next = Number.isSafeInteger(current) && current > 0 ? current + 1 : 1;
|
|
5807
|
+
device.captureGenerationCounter = next;
|
|
5808
|
+
return next;
|
|
5809
|
+
}
|
|
5069
5810
|
|
|
5070
5811
|
function normalizeLivePlatform(value) {
|
|
5071
5812
|
const normalized = safeString(value, 32).toLowerCase();
|
|
@@ -5075,20 +5816,27 @@ export function createRemoteHub(options = {}) {
|
|
|
5075
5816
|
return normalized || 'unknown';
|
|
5076
5817
|
}
|
|
5077
5818
|
|
|
5078
|
-
function normalizeLiveStreamStartOptions(options = {}) {
|
|
5079
|
-
const transfer = buildRemoteFrameTransferDescriptor(options.mode || options.frameMode || DEFAULT_REMOTE_FRAME_MODE);
|
|
5080
|
-
const streamPurpose = safeString(options.streamPurpose || options.purpose || 'wall', 24) || 'wall';
|
|
5081
|
-
const platform = normalizeLivePlatform(options.platform);
|
|
5082
|
-
const
|
|
5083
|
-
const
|
|
5084
|
-
const
|
|
5085
|
-
const
|
|
5086
|
-
const
|
|
5087
|
-
const
|
|
5088
|
-
const
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5819
|
+
function normalizeLiveStreamStartOptions(options = {}) {
|
|
5820
|
+
const transfer = buildRemoteFrameTransferDescriptor(options.mode || options.frameMode || DEFAULT_REMOTE_FRAME_MODE);
|
|
5821
|
+
const streamPurpose = safeString(options.streamPurpose || options.purpose || 'wall', 24) || 'wall';
|
|
5822
|
+
const platform = normalizeLivePlatform(options.platform);
|
|
5823
|
+
const maxFps = streamPurpose === 'control' ? 60 : 30;
|
|
5824
|
+
const defaultFps = 8;
|
|
5825
|
+
const maxWidth = clampNumber(options.maxWidth, 320, 3840, 640);
|
|
5826
|
+
const maxHeight = clampNumber(options.maxHeight, 180, 2160, 360);
|
|
5827
|
+
const fps = clampNumber(options.fps, 1, maxFps, defaultFps);
|
|
5828
|
+
const quality = clampNumber(options.quality, 20, 95, 45);
|
|
5829
|
+
const monitorIndex = normalizeMonitorIndex(options.monitorIndex ?? options.screenIndex ?? options.displayIndex);
|
|
5830
|
+
return { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose, platform };
|
|
5831
|
+
}
|
|
5832
|
+
|
|
5833
|
+
function liveStreamModePolicyError(normalized) {
|
|
5834
|
+
if (normalized?.transfer?.frameMode === 'mode2-lzo'
|
|
5835
|
+
&& normalized?.streamPurpose !== 'atlas') {
|
|
5836
|
+
return 'MODE2_ATLAS_ONLY: Mode 2 is reserved for internal Mode 4 Atlas input and cannot be used for Wall or Control.';
|
|
5837
|
+
}
|
|
5838
|
+
return '';
|
|
5839
|
+
}
|
|
5092
5840
|
|
|
5093
5841
|
function liveStreamStartStillPending(activeLiveStream) {
|
|
5094
5842
|
if (!activeLiveStream?.active || activeLiveStream.open === true) {
|
|
@@ -5098,13 +5846,35 @@ export function createRemoteHub(options = {}) {
|
|
|
5098
5846
|
return Number.isFinite(startedAt) && Date.now() - startedAt < LIVE_STREAM_PENDING_REUSE_MS;
|
|
5099
5847
|
}
|
|
5100
5848
|
|
|
5101
|
-
function liveStreamReplacementStillPending(activeLiveStream) {
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5849
|
+
function liveStreamReplacementStillPending(activeLiveStream) {
|
|
5850
|
+
const pending = getPendingLiveStreamDescriptor(activeLiveStream);
|
|
5851
|
+
if (!activeLiveStream?.active || !safeString(pending?.commandId, 128)) {
|
|
5852
|
+
return false;
|
|
5853
|
+
}
|
|
5854
|
+
const startedAt = Date.parse(pending.startedAt || '');
|
|
5855
|
+
return Number.isFinite(startedAt) && Date.now() - startedAt < LIVE_STREAM_REPLACEMENT_PENDING_MS;
|
|
5856
|
+
}
|
|
5857
|
+
|
|
5858
|
+
function liveStreamHasCurrentFrame(activeLiveStream) {
|
|
5859
|
+
if (!activeLiveStream?.active
|
|
5860
|
+
|| activeLiveStream.open !== true
|
|
5861
|
+
|| Number(activeLiveStream.framesReceived || 0) < 1) {
|
|
5862
|
+
return false;
|
|
5863
|
+
}
|
|
5864
|
+
const latestFrame = activeLiveStream.latestFrame;
|
|
5865
|
+
if (!latestFrame || latestFrame.currentGenerationVerified !== true) {
|
|
5866
|
+
return false;
|
|
5867
|
+
}
|
|
5868
|
+
const activeCommandId = safeString(activeLiveStream.commandId, 128);
|
|
5869
|
+
const frameCommandId = safeString(latestFrame.commandId, 128);
|
|
5870
|
+
if (activeCommandId && frameCommandId !== activeCommandId) {
|
|
5871
|
+
return false;
|
|
5872
|
+
}
|
|
5873
|
+
const activeCaptureGeneration = Number(activeLiveStream.captureGeneration || 0);
|
|
5874
|
+
const frameCaptureGeneration = Number(latestFrame.captureGeneration || 0);
|
|
5875
|
+
return activeCaptureGeneration <= 0
|
|
5876
|
+
|| (frameCaptureGeneration > 0 && frameCaptureGeneration === activeCaptureGeneration);
|
|
5877
|
+
}
|
|
5108
5878
|
|
|
5109
5879
|
function getLiveStreamFreshWindowMs(activeLiveStream) {
|
|
5110
5880
|
const fps = Number(activeLiveStream?.fps || 0);
|
|
@@ -5157,29 +5927,36 @@ export function createRemoteHub(options = {}) {
|
|
|
5157
5927
|
return { ok: false, error: 'device-live-stream-unavailable' };
|
|
5158
5928
|
}
|
|
5159
5929
|
|
|
5160
|
-
const now = new Date().toISOString();
|
|
5161
|
-
const normalized = normalizeLiveStreamStartOptions({ fps: 2, ...options, platform: device.platform });
|
|
5162
|
-
const
|
|
5163
|
-
|
|
5164
|
-
const
|
|
5165
|
-
const
|
|
5930
|
+
const now = new Date().toISOString();
|
|
5931
|
+
const normalized = normalizeLiveStreamStartOptions({ fps: 2, ...options, platform: device.platform });
|
|
5932
|
+
const modePolicyError = liveStreamModePolicyError(normalized);
|
|
5933
|
+
if (modePolicyError) return { ok: false, error: modePolicyError };
|
|
5934
|
+
const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
|
|
5935
|
+
const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
|
|
5936
|
+
const activeLiveStream = getDeviceLiveStream(device, streamId);
|
|
5937
|
+
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
5166
5938
|
if (activeLiveStream
|
|
5167
|
-
&& liveStreamMatchesOptions(activeLiveStream, normalized)
|
|
5168
|
-
&& (activeLiveStream.open === true || liveStreamStartStillPending(activeLiveStream))) {
|
|
5169
|
-
return {
|
|
5170
|
-
ok: true,
|
|
5171
|
-
commandId: activeLiveStream.commandId,
|
|
5172
|
-
|
|
5173
|
-
|
|
5939
|
+
&& liveStreamMatchesOptions(activeLiveStream, normalized)
|
|
5940
|
+
&& (activeLiveStream.open === true || liveStreamStartStillPending(activeLiveStream))) {
|
|
5941
|
+
return {
|
|
5942
|
+
ok: true,
|
|
5943
|
+
commandId: activeLiveStream.commandId,
|
|
5944
|
+
sessionId: device.sessionId,
|
|
5945
|
+
streamId,
|
|
5946
|
+
streamPurpose,
|
|
5947
|
+
fps,
|
|
5174
5948
|
mode: transfer.mode,
|
|
5175
|
-
frameMode: transfer.frameMode,
|
|
5176
|
-
monitorIndex,
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5949
|
+
frameMode: transfer.frameMode,
|
|
5950
|
+
monitorIndex,
|
|
5951
|
+
captureGeneration: Number(activeLiveStream.captureGeneration || 0),
|
|
5952
|
+
ready: liveStreamHasCurrentFrame(activeLiveStream),
|
|
5953
|
+
synthetic: true,
|
|
5954
|
+
reused: true
|
|
5955
|
+
};
|
|
5956
|
+
}
|
|
5957
|
+
const captureGeneration = nextCaptureGeneration(device);
|
|
5958
|
+
device.counters.commandsSent += 1;
|
|
5959
|
+
setDeviceLiveStream(device, {
|
|
5183
5960
|
streamId,
|
|
5184
5961
|
commandId,
|
|
5185
5962
|
active: true,
|
|
@@ -5204,11 +5981,12 @@ export function createRemoteHub(options = {}) {
|
|
|
5204
5981
|
startedAt: now,
|
|
5205
5982
|
stoppedAt: '',
|
|
5206
5983
|
stopReason: '',
|
|
5207
|
-
lastFrameAt: '',
|
|
5208
|
-
lastFrameSeq: 0,
|
|
5209
|
-
framesReceived: 0,
|
|
5210
|
-
streamPurpose
|
|
5211
|
-
|
|
5984
|
+
lastFrameAt: '',
|
|
5985
|
+
lastFrameSeq: 0,
|
|
5986
|
+
framesReceived: 0,
|
|
5987
|
+
streamPurpose,
|
|
5988
|
+
captureGeneration
|
|
5989
|
+
});
|
|
5212
5990
|
device.counters.liveStreamsStarted += 1;
|
|
5213
5991
|
applySyntheticLiveFrame(device, streamId, {
|
|
5214
5992
|
commandId,
|
|
@@ -5224,7 +6002,20 @@ export function createRemoteHub(options = {}) {
|
|
|
5224
6002
|
mode: transfer.mode,
|
|
5225
6003
|
synthetic: true
|
|
5226
6004
|
});
|
|
5227
|
-
return {
|
|
6005
|
+
return {
|
|
6006
|
+
ok: true,
|
|
6007
|
+
commandId,
|
|
6008
|
+
sessionId: device.sessionId,
|
|
6009
|
+
streamId,
|
|
6010
|
+
streamPurpose,
|
|
6011
|
+
fps,
|
|
6012
|
+
mode: transfer.mode,
|
|
6013
|
+
frameMode: transfer.frameMode,
|
|
6014
|
+
monitorIndex,
|
|
6015
|
+
captureGeneration,
|
|
6016
|
+
ready: true,
|
|
6017
|
+
synthetic: true
|
|
6018
|
+
};
|
|
5228
6019
|
}
|
|
5229
6020
|
|
|
5230
6021
|
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
@@ -5234,51 +6025,66 @@ export function createRemoteHub(options = {}) {
|
|
|
5234
6025
|
return { ok: false, error: 'device-live-stream-unavailable' };
|
|
5235
6026
|
}
|
|
5236
6027
|
|
|
5237
|
-
const now = new Date().toISOString();
|
|
5238
|
-
const normalized = normalizeLiveStreamStartOptions({ ...options, platform: device.platform });
|
|
5239
|
-
const
|
|
5240
|
-
|
|
5241
|
-
const
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
6028
|
+
const now = new Date().toISOString();
|
|
6029
|
+
const normalized = normalizeLiveStreamStartOptions({ ...options, platform: device.platform });
|
|
6030
|
+
const modePolicyError = liveStreamModePolicyError(normalized);
|
|
6031
|
+
if (modePolicyError) return { ok: false, error: modePolicyError };
|
|
6032
|
+
const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
|
|
6033
|
+
const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
|
|
6034
|
+
const activeLiveStream = getDeviceLiveStream(device, streamId);
|
|
6035
|
+
const pendingDescriptor = getPendingLiveStreamDescriptor(activeLiveStream);
|
|
6036
|
+
if (pendingDescriptor?.commandId) {
|
|
6037
|
+
const pendingMatchesRequest = liveStreamMatchesOptions(pendingDescriptor, normalized);
|
|
6038
|
+
if (pendingMatchesRequest && liveStreamReplacementStillPending(activeLiveStream)) {
|
|
6039
|
+
emitRemoteEvent('RemoteLiveStreamRestartPending', device, {
|
|
6040
|
+
streamId,
|
|
6041
|
+
commandId: pendingDescriptor.commandId,
|
|
6042
|
+
activeCommandId: activeLiveStream.commandId,
|
|
6043
|
+
reason: safeString(options.restartReason || 'duplicate-restart', 128)
|
|
6044
|
+
});
|
|
6045
|
+
return {
|
|
6046
|
+
ok: true,
|
|
6047
|
+
commandId: pendingDescriptor.commandId,
|
|
6048
|
+
sessionId: device.sessionId,
|
|
6049
|
+
streamId,
|
|
6050
|
+
streamPurpose: safeString(pendingDescriptor.streamPurpose, 24) || streamPurpose,
|
|
6051
|
+
fps: Number(pendingDescriptor.fps || fps),
|
|
6052
|
+
mode: pendingDescriptor.mode || transfer.mode,
|
|
6053
|
+
frameMode: pendingDescriptor.frameMode || transfer.frameMode,
|
|
6054
|
+
monitorIndex: Number(pendingDescriptor.monitorIndex ?? monitorIndex),
|
|
6055
|
+
captureGeneration: Number(pendingDescriptor.captureGeneration || 0),
|
|
6056
|
+
ready: false,
|
|
6057
|
+
pending: true,
|
|
6058
|
+
reused: true
|
|
6059
|
+
};
|
|
6060
|
+
}
|
|
6061
|
+
failPendingLiveStream(
|
|
6062
|
+
device,
|
|
6063
|
+
pendingDescriptor.commandId,
|
|
6064
|
+
liveStreamReplacementStillPending(activeLiveStream)
|
|
6065
|
+
? 'stream-restart-superseded'
|
|
6066
|
+
: 'stream-start-timeout');
|
|
6067
|
+
}
|
|
5266
6068
|
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
5267
6069
|
if (activeLiveStream
|
|
5268
6070
|
&& options.forceRestart !== true
|
|
5269
6071
|
&& options.reuseExisting === true
|
|
5270
6072
|
&& liveStreamMatchesOptions(activeLiveStream, normalized)
|
|
5271
6073
|
&& liveStreamIsReusable(activeLiveStream)) {
|
|
5272
|
-
return {
|
|
5273
|
-
ok: true,
|
|
5274
|
-
commandId: activeLiveStream.commandId,
|
|
5275
|
-
|
|
6074
|
+
return {
|
|
6075
|
+
ok: true,
|
|
6076
|
+
commandId: activeLiveStream.commandId,
|
|
6077
|
+
sessionId: device.sessionId,
|
|
6078
|
+
streamId,
|
|
6079
|
+
streamPurpose,
|
|
5276
6080
|
fps: Number(activeLiveStream.fps || fps),
|
|
5277
6081
|
mode: activeLiveStream.mode || transfer.mode,
|
|
5278
|
-
frameMode: activeLiveStream.frameMode || transfer.frameMode,
|
|
5279
|
-
monitorIndex: Number(activeLiveStream.monitorIndex || monitorIndex),
|
|
5280
|
-
|
|
5281
|
-
|
|
6082
|
+
frameMode: activeLiveStream.frameMode || transfer.frameMode,
|
|
6083
|
+
monitorIndex: Number(activeLiveStream.monitorIndex || monitorIndex),
|
|
6084
|
+
captureGeneration: Number(activeLiveStream.captureGeneration || 0),
|
|
6085
|
+
ready: liveStreamHasCurrentFrame(activeLiveStream),
|
|
6086
|
+
reused: true
|
|
6087
|
+
};
|
|
5282
6088
|
}
|
|
5283
6089
|
if (activeLiveStream
|
|
5284
6090
|
&& options.forceRestart !== true
|
|
@@ -5293,16 +6099,20 @@ export function createRemoteHub(options = {}) {
|
|
|
5293
6099
|
monitorIndex
|
|
5294
6100
|
});
|
|
5295
6101
|
}
|
|
5296
|
-
return {
|
|
5297
|
-
ok: true,
|
|
5298
|
-
commandId: activeLiveStream.commandId,
|
|
5299
|
-
|
|
6102
|
+
return {
|
|
6103
|
+
ok: true,
|
|
6104
|
+
commandId: activeLiveStream.commandId,
|
|
6105
|
+
sessionId: device.sessionId,
|
|
6106
|
+
streamId,
|
|
6107
|
+
streamPurpose,
|
|
5300
6108
|
fps,
|
|
5301
6109
|
mode: transfer.mode,
|
|
5302
|
-
frameMode: transfer.frameMode,
|
|
5303
|
-
monitorIndex,
|
|
5304
|
-
|
|
5305
|
-
|
|
6110
|
+
frameMode: transfer.frameMode,
|
|
6111
|
+
monitorIndex,
|
|
6112
|
+
captureGeneration: Number(activeLiveStream.captureGeneration || 0),
|
|
6113
|
+
ready: liveStreamHasCurrentFrame(activeLiveStream),
|
|
6114
|
+
reused: true
|
|
6115
|
+
};
|
|
5306
6116
|
}
|
|
5307
6117
|
if (options.forceRestart === true && activeLiveStream) {
|
|
5308
6118
|
emitRemoteEvent('RemoteLiveStreamForcedRestart', device, {
|
|
@@ -5329,10 +6139,11 @@ export function createRemoteHub(options = {}) {
|
|
|
5329
6139
|
lastFrameAt: activeLiveStream.lastFrameAt || ''
|
|
5330
6140
|
});
|
|
5331
6141
|
}
|
|
5332
|
-
const
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
6142
|
+
const captureGeneration = nextCaptureGeneration(device);
|
|
6143
|
+
const result = sendCommand(deviceId, {
|
|
6144
|
+
command: 'stream.start',
|
|
6145
|
+
commandId,
|
|
6146
|
+
payload: {
|
|
5336
6147
|
streamId,
|
|
5337
6148
|
mode: transfer.mode,
|
|
5338
6149
|
frameMode: transfer.frameMode,
|
|
@@ -5347,34 +6158,32 @@ export function createRemoteHub(options = {}) {
|
|
|
5347
6158
|
maxWidth,
|
|
5348
6159
|
maxHeight,
|
|
5349
6160
|
quality,
|
|
5350
|
-
monitorIndex,
|
|
5351
|
-
streamPurpose,
|
|
5352
|
-
|
|
5353
|
-
|
|
6161
|
+
monitorIndex,
|
|
6162
|
+
streamPurpose,
|
|
6163
|
+
captureGeneration,
|
|
6164
|
+
sessionId: device.sessionId,
|
|
6165
|
+
requestedAt: now
|
|
6166
|
+
}
|
|
5354
6167
|
});
|
|
5355
6168
|
|
|
5356
6169
|
if (!result.ok) {
|
|
5357
6170
|
return result;
|
|
5358
6171
|
}
|
|
5359
6172
|
|
|
5360
|
-
const previousCommandId = safeString(activeLiveStream?.commandId, 128);
|
|
5361
|
-
const replacingActiveStream = activeLiveStream?.active === true
|
|
5362
|
-
&& !!previousCommandId
|
|
5363
|
-
&& previousCommandId !== commandId;
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
openTransport: replacingActiveStream ? activeLiveStream.openTransport || '' : '',
|
|
5375
|
-
mode: transfer.mode,
|
|
5376
|
-
frameMode: transfer.frameMode,
|
|
5377
|
-
frameProfile: transfer.frameProfile,
|
|
6173
|
+
const previousCommandId = safeString(activeLiveStream?.commandId, 128);
|
|
6174
|
+
const replacingActiveStream = activeLiveStream?.active === true
|
|
6175
|
+
&& !!previousCommandId
|
|
6176
|
+
&& previousCommandId !== commandId;
|
|
6177
|
+
const nextDescriptor = {
|
|
6178
|
+
streamId,
|
|
6179
|
+
commandId,
|
|
6180
|
+
active: true,
|
|
6181
|
+
open: false,
|
|
6182
|
+
openedAt: '',
|
|
6183
|
+
openTransport: '',
|
|
6184
|
+
mode: transfer.mode,
|
|
6185
|
+
frameMode: transfer.frameMode,
|
|
6186
|
+
frameProfile: transfer.frameProfile,
|
|
5378
6187
|
encoding: transfer.encoding,
|
|
5379
6188
|
codec: transfer.codec,
|
|
5380
6189
|
compression: transfer.compression,
|
|
@@ -5385,16 +6194,32 @@ export function createRemoteHub(options = {}) {
|
|
|
5385
6194
|
maxWidth,
|
|
5386
6195
|
maxHeight,
|
|
5387
6196
|
quality,
|
|
5388
|
-
monitorIndex,
|
|
5389
|
-
monitorCount: 1,
|
|
5390
|
-
startedAt:
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
6197
|
+
monitorIndex,
|
|
6198
|
+
monitorCount: 1,
|
|
6199
|
+
startedAt: now,
|
|
6200
|
+
restartToken: safeString(options.restartToken, 128),
|
|
6201
|
+
stoppedAt: '',
|
|
6202
|
+
stopReason: '',
|
|
6203
|
+
lastFrameAt: '',
|
|
5394
6204
|
lastFrameSeq: 0,
|
|
5395
|
-
framesReceived: 0,
|
|
5396
|
-
streamPurpose
|
|
5397
|
-
|
|
6205
|
+
framesReceived: 0,
|
|
6206
|
+
streamPurpose,
|
|
6207
|
+
captureGeneration,
|
|
6208
|
+
latestFrame: null,
|
|
6209
|
+
width: 0,
|
|
6210
|
+
height: 0,
|
|
6211
|
+
sourceWidth: 0,
|
|
6212
|
+
sourceHeight: 0
|
|
6213
|
+
};
|
|
6214
|
+
if (replacingActiveStream) {
|
|
6215
|
+
setPendingLiveStreamDescriptor(activeLiveStream, nextDescriptor);
|
|
6216
|
+
scheduleLiveStreamReplacementTimeout(device, activeLiveStream);
|
|
6217
|
+
setDeviceLiveStream(device, activeLiveStream);
|
|
6218
|
+
} else {
|
|
6219
|
+
const nextStreamState = { ...nextDescriptor };
|
|
6220
|
+
setPendingLiveStreamDescriptor(nextStreamState, null);
|
|
6221
|
+
setDeviceLiveStream(device, nextStreamState);
|
|
6222
|
+
}
|
|
5398
6223
|
device.counters.liveStreamsStarted += 1;
|
|
5399
6224
|
emitRemoteEvent('RemoteLiveStreamStarted', device, {
|
|
5400
6225
|
streamId,
|
|
@@ -5407,7 +6232,19 @@ export function createRemoteHub(options = {}) {
|
|
|
5407
6232
|
monitorIndex
|
|
5408
6233
|
});
|
|
5409
6234
|
|
|
5410
|
-
return {
|
|
6235
|
+
return {
|
|
6236
|
+
ok: true,
|
|
6237
|
+
commandId,
|
|
6238
|
+
sessionId: device.sessionId,
|
|
6239
|
+
streamId,
|
|
6240
|
+
streamPurpose,
|
|
6241
|
+
fps,
|
|
6242
|
+
mode: transfer.mode,
|
|
6243
|
+
frameMode: transfer.frameMode,
|
|
6244
|
+
monitorIndex,
|
|
6245
|
+
captureGeneration,
|
|
6246
|
+
ready: false
|
|
6247
|
+
};
|
|
5411
6248
|
}
|
|
5412
6249
|
|
|
5413
6250
|
function stopLiveStream(deviceId, options = {}) {
|
|
@@ -5417,24 +6254,25 @@ export function createRemoteHub(options = {}) {
|
|
|
5417
6254
|
const purposeStreamId = streamPurpose ? makeStableLiveStreamId(deviceId, streamPurpose) : '';
|
|
5418
6255
|
const streamId = requestedStreamId || purposeStreamId || device?.activeLiveStream?.streamId || '';
|
|
5419
6256
|
const streamState = getDeviceLiveStream(device, streamId);
|
|
5420
|
-
if (streamState && streamState.active !== true) {
|
|
5421
|
-
|
|
6257
|
+
if (streamState && streamState.active !== true) {
|
|
6258
|
+
clearPendingLiveStreamDescriptor(device, streamState);
|
|
6259
|
+
return {
|
|
5422
6260
|
ok: true,
|
|
5423
6261
|
commandId: safeString(options.commandId, 128),
|
|
5424
6262
|
streamId,
|
|
5425
|
-
alreadyStopped: true
|
|
5426
|
-
};
|
|
5427
|
-
}
|
|
5428
|
-
if (
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
streamState.
|
|
5436
|
-
streamState.stoppedAt = new Date().toISOString();
|
|
5437
|
-
streamState.stopReason = 'manager-request';
|
|
6263
|
+
alreadyStopped: true
|
|
6264
|
+
};
|
|
6265
|
+
}
|
|
6266
|
+
if (streamState) {
|
|
6267
|
+
clearPendingLiveStreamDescriptor(device, streamState);
|
|
6268
|
+
}
|
|
6269
|
+
if (device?.synthetic === true && device.connected) {
|
|
6270
|
+
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
6271
|
+
device.counters.commandsSent += 1;
|
|
6272
|
+
if (streamState) {
|
|
6273
|
+
streamState.active = false;
|
|
6274
|
+
streamState.stoppedAt = new Date().toISOString();
|
|
6275
|
+
streamState.stopReason = 'manager-request';
|
|
5438
6276
|
}
|
|
5439
6277
|
device.counters.liveStreamsStopped += 1;
|
|
5440
6278
|
emitRemoteEvent('RemoteLiveStreamStopped', device, {
|
|
@@ -5462,14 +6300,11 @@ export function createRemoteHub(options = {}) {
|
|
|
5462
6300
|
if (!result.ok) {
|
|
5463
6301
|
return result;
|
|
5464
6302
|
}
|
|
5465
|
-
|
|
5466
|
-
if (streamState) {
|
|
5467
|
-
streamState.active = false;
|
|
5468
|
-
streamState.
|
|
5469
|
-
streamState.
|
|
5470
|
-
streamState.pendingRestartToken = '';
|
|
5471
|
-
streamState.stoppedAt = new Date().toISOString();
|
|
5472
|
-
streamState.stopReason = 'manager-request';
|
|
6303
|
+
|
|
6304
|
+
if (streamState) {
|
|
6305
|
+
streamState.active = false;
|
|
6306
|
+
streamState.stoppedAt = new Date().toISOString();
|
|
6307
|
+
streamState.stopReason = 'manager-request';
|
|
5473
6308
|
}
|
|
5474
6309
|
device.counters.liveStreamsStopped += 1;
|
|
5475
6310
|
emitRemoteEvent('RemoteLiveStreamStopped', device, {
|
|
@@ -5634,9 +6469,10 @@ export function createRemoteHub(options = {}) {
|
|
|
5634
6469
|
getTaskBatch,
|
|
5635
6470
|
cancelTaskBatch,
|
|
5636
6471
|
retryTaskBatch,
|
|
5637
|
-
listDeviceFrames,
|
|
5638
|
-
disconnectDevice,
|
|
5639
|
-
|
|
6472
|
+
listDeviceFrames,
|
|
6473
|
+
disconnectDevice,
|
|
6474
|
+
assignDeviceSlot,
|
|
6475
|
+
sendCommand,
|
|
5640
6476
|
sendLegacyClientUpdate,
|
|
5641
6477
|
sendInputControl,
|
|
5642
6478
|
notifyAgentProgress,
|