@livedesk/hub 0.1.24 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/remote-hub.js +841 -40
- package/src/server.js +155 -5
- package/src/transport/udp-hub-transport.js +110 -8
- package/src/transport/udp-rendezvous.js +6 -2
package/src/remote-hub.js
CHANGED
|
@@ -31,6 +31,17 @@ const LIVE_STREAM_MIN_FRESH_MS = 3000;
|
|
|
31
31
|
const LIVE_STREAM_MAX_FRESH_MS = 12000;
|
|
32
32
|
const DEFAULT_REMOTE_INPUT_ACK_TIMEOUT_MS = 5000;
|
|
33
33
|
const DEFAULT_LIVE_CAPTURE_STOP_ACK_TIMEOUT_MS = 8000;
|
|
34
|
+
const TRANSPORT_DIAGNOSTIC_MIN_TTL_MS = 15_000;
|
|
35
|
+
const TRANSPORT_DIAGNOSTIC_MAX_TTL_MS = 180_000;
|
|
36
|
+
const TRANSPORT_DIAGNOSTIC_COMMAND_TIMEOUT_MS = 8_000;
|
|
37
|
+
const TRANSPORT_DIAGNOSTIC_RECORD_RETENTION_MS = 60_000;
|
|
38
|
+
const TRANSPORT_DIAGNOSTIC_MAX_RECORDS = 32;
|
|
39
|
+
const TRANSPORT_DIAGNOSTIC_PHASES = Object.freeze([
|
|
40
|
+
'direct',
|
|
41
|
+
'p2p',
|
|
42
|
+
'relay',
|
|
43
|
+
'recover-direct'
|
|
44
|
+
]);
|
|
34
45
|
const REMOTE_POLICY_BYPASS_COMMANDS = new Set([
|
|
35
46
|
'ping',
|
|
36
47
|
'stream.stop',
|
|
@@ -429,13 +440,23 @@ function isPublicIPv4(value) {
|
|
|
429
440
|
return false;
|
|
430
441
|
}
|
|
431
442
|
|
|
432
|
-
if (parts[0] === 192
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
443
|
+
if (parts[0] === 192
|
|
444
|
+
&& (parts[1] === 0
|
|
445
|
+
|| parts[1] === 168
|
|
446
|
+
|| (parts[1] === 88 && parts[2] === 99))) {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (parts[0] === 198
|
|
451
|
+
&& (parts[1] === 18
|
|
452
|
+
|| parts[1] === 19
|
|
453
|
+
|| (parts[1] === 51 && parts[2] === 100))) {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (parts[0] === 203 && parts[1] === 0 && parts[2] === 113) {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
439
460
|
|
|
440
461
|
return true;
|
|
441
462
|
}
|
|
@@ -690,21 +711,39 @@ const SUPPORTED_AGENT_OPERATIONS = new Set([
|
|
|
690
711
|
'logs.collect'
|
|
691
712
|
]);
|
|
692
713
|
|
|
693
|
-
function normalizeAgentOperation(value) {
|
|
694
|
-
const operation = safeString(value, 80).toLowerCase();
|
|
695
|
-
return SUPPORTED_AGENT_OPERATIONS.has(operation) ? operation : '';
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
714
|
+
function normalizeAgentOperation(value) {
|
|
715
|
+
const operation = safeString(value, 80).toLowerCase();
|
|
716
|
+
return SUPPORTED_AGENT_OPERATIONS.has(operation) ? operation : '';
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const REMOTE_INPUT_MONITOR_KEYS = Object.freeze([
|
|
720
|
+
'monitorIndex',
|
|
721
|
+
'MonitorIndex',
|
|
722
|
+
'screenIndex',
|
|
723
|
+
'ScreenIndex',
|
|
724
|
+
'displayIndex',
|
|
725
|
+
'DisplayIndex'
|
|
726
|
+
]);
|
|
727
|
+
|
|
728
|
+
function readRawRemoteInputMonitorIndex(input) {
|
|
729
|
+
for (const key of REMOTE_INPUT_MONITOR_KEYS) {
|
|
730
|
+
if (Object.prototype.hasOwnProperty.call(input, key)) {
|
|
731
|
+
return input[key];
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return undefined;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function normalizeRemoteInputEvent(value = {}) {
|
|
738
|
+
const input = value && typeof value === 'object' ? value : {};
|
|
739
|
+
const type = safeString(input.type || input.Type, 48);
|
|
701
740
|
const normalizedX = Number(input.normalizedX ?? input.NormalizedX);
|
|
702
741
|
const normalizedY = Number(input.normalizedY ?? input.NormalizedY);
|
|
703
742
|
const deltaX = Number(input.deltaX ?? input.DeltaX);
|
|
704
743
|
const deltaY = Number(input.deltaY ?? input.DeltaY);
|
|
705
744
|
const keyCode = Number(input.keyCode ?? input.KeyCode);
|
|
706
745
|
const location = Number(input.location ?? input.Location);
|
|
707
|
-
const monitorIndex =
|
|
746
|
+
const monitorIndex = parseExactLiveStreamMonitorIndex(readRawRemoteInputMonitorIndex(input));
|
|
708
747
|
const inputSeq = Number(input.inputSeq ?? input.InputSeq);
|
|
709
748
|
const issuedAtEpochMs = Number(input.issuedAtEpochMs ?? input.IssuedAtEpochMs);
|
|
710
749
|
const hubReceivedAtEpochMs = Number(input.hubReceivedAtEpochMs ?? input.HubReceivedAtEpochMs);
|
|
@@ -720,7 +759,7 @@ function normalizeRemoteInputEvent(value = {}) {
|
|
|
720
759
|
type,
|
|
721
760
|
// A missing monitor is not equivalent to the primary display. Control
|
|
722
761
|
// input must prove the exact monitor owned by its capture generation.
|
|
723
|
-
monitorIndex
|
|
762
|
+
monitorIndex,
|
|
724
763
|
normalizedX: Number.isFinite(normalizedX) ? Math.max(0, Math.min(1, normalizedX)) : undefined,
|
|
725
764
|
normalizedY: Number.isFinite(normalizedY) ? Math.max(0, Math.min(1, normalizedY)) : undefined,
|
|
726
765
|
button: safeString(input.button || input.Button, 24),
|
|
@@ -1510,6 +1549,11 @@ class RemoteHubWebSocketAgentSocket {
|
|
|
1510
1549
|
|
|
1511
1550
|
export function createRemoteHub(options = {}) {
|
|
1512
1551
|
const env = options.env || process.env;
|
|
1552
|
+
const transportDiagnosticCommandTimeoutMs = clampNumber(
|
|
1553
|
+
options.transportDiagnosticCommandTimeoutMs,
|
|
1554
|
+
25,
|
|
1555
|
+
TRANSPORT_DIAGNOSTIC_COMMAND_TIMEOUT_MS,
|
|
1556
|
+
TRANSPORT_DIAGNOSTIC_COMMAND_TIMEOUT_MS);
|
|
1513
1557
|
const rejectFrameChannelForTest = isEnabledValue(env.LIVEDESK_TEST_MODE, false)
|
|
1514
1558
|
&& isEnabledValue(env.LIVEDESK_TEST_REJECT_FRAME_CHANNEL, false);
|
|
1515
1559
|
const logEvent = options.logEvent || (() => {});
|
|
@@ -1613,7 +1657,9 @@ export function createRemoteHub(options = {}) {
|
|
|
1613
1657
|
const fileSockets = new Map();
|
|
1614
1658
|
const allSockets = new Set();
|
|
1615
1659
|
const pendingCommandResultWaiters = new Map();
|
|
1616
|
-
const
|
|
1660
|
+
const transportDiagnostics = new Map();
|
|
1661
|
+
const transportDiagnosticStartingDevices = new Set();
|
|
1662
|
+
const duplicateDeviceLogAt = new Map();
|
|
1617
1663
|
let server = null;
|
|
1618
1664
|
let started = false;
|
|
1619
1665
|
let boundPort = requestedPort;
|
|
@@ -2248,6 +2294,700 @@ export function createRemoteHub(options = {}) {
|
|
|
2248
2294
|
});
|
|
2249
2295
|
}
|
|
2250
2296
|
|
|
2297
|
+
function inspectAnnexBH264(payload) {
|
|
2298
|
+
if (!Buffer.isBuffer(payload) || payload.length < 5) {
|
|
2299
|
+
return {
|
|
2300
|
+
annexB: false,
|
|
2301
|
+
hasIdr: false,
|
|
2302
|
+
hasSps: false,
|
|
2303
|
+
hasPps: false,
|
|
2304
|
+
nalOrderValid: false
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
let annexB = false;
|
|
2308
|
+
let hasIdr = false;
|
|
2309
|
+
let hasSps = false;
|
|
2310
|
+
let hasPps = false;
|
|
2311
|
+
let firstSpsAt = -1;
|
|
2312
|
+
let firstPpsAfterSpsAt = -1;
|
|
2313
|
+
let firstIdrAfterPpsAt = -1;
|
|
2314
|
+
for (let index = 0; index + 4 < payload.length; index += 1) {
|
|
2315
|
+
let headerIndex = -1;
|
|
2316
|
+
if (payload[index] === 0 && payload[index + 1] === 0 && payload[index + 2] === 1) {
|
|
2317
|
+
headerIndex = index + 3;
|
|
2318
|
+
} else if (payload[index] === 0
|
|
2319
|
+
&& payload[index + 1] === 0
|
|
2320
|
+
&& payload[index + 2] === 0
|
|
2321
|
+
&& payload[index + 3] === 1) {
|
|
2322
|
+
headerIndex = index + 4;
|
|
2323
|
+
}
|
|
2324
|
+
if (headerIndex < 0 || headerIndex >= payload.length) continue;
|
|
2325
|
+
annexB = true;
|
|
2326
|
+
const nalType = payload[headerIndex] & 0x1f;
|
|
2327
|
+
if (nalType === 5) {
|
|
2328
|
+
hasIdr = true;
|
|
2329
|
+
if (firstPpsAfterSpsAt >= 0 && firstIdrAfterPpsAt < 0) {
|
|
2330
|
+
firstIdrAfterPpsAt = headerIndex;
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
if (nalType === 7) {
|
|
2334
|
+
hasSps = true;
|
|
2335
|
+
if (firstSpsAt < 0) firstSpsAt = headerIndex;
|
|
2336
|
+
}
|
|
2337
|
+
if (nalType === 8) {
|
|
2338
|
+
hasPps = true;
|
|
2339
|
+
if (firstSpsAt >= 0 && firstPpsAfterSpsAt < 0) {
|
|
2340
|
+
firstPpsAfterSpsAt = headerIndex;
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
index = headerIndex;
|
|
2344
|
+
}
|
|
2345
|
+
return {
|
|
2346
|
+
annexB,
|
|
2347
|
+
hasIdr,
|
|
2348
|
+
hasSps,
|
|
2349
|
+
hasPps,
|
|
2350
|
+
nalOrderValid: firstSpsAt >= 0
|
|
2351
|
+
&& firstPpsAfterSpsAt > firstSpsAt
|
|
2352
|
+
&& firstIdrAfterPpsAt > firstPpsAfterSpsAt
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
function expectedTransportForDiagnosticPhase(phase) {
|
|
2357
|
+
if (phase === 'p2p') return 'p2p';
|
|
2358
|
+
if (phase === 'relay') return 'relay';
|
|
2359
|
+
return 'direct';
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
function observedTransportPath(frame) {
|
|
2363
|
+
const transport = safeString(frame?.transport, 32).toLowerCase();
|
|
2364
|
+
if (transport === 'udp-p2p') return 'p2p';
|
|
2365
|
+
if (transport === 'relay-binary') return 'relay';
|
|
2366
|
+
if (transport === 'binary' || transport === 'ws-binary') return 'direct';
|
|
2367
|
+
return '';
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
function optionalFiniteNumber(value) {
|
|
2371
|
+
return value !== null && value !== undefined && value !== ''
|
|
2372
|
+
&& Number.isFinite(Number(value))
|
|
2373
|
+
? Number(value)
|
|
2374
|
+
: null;
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
function transportDiagnosticIdentifierHash(value, salt) {
|
|
2378
|
+
const normalized = safeString(value, 240);
|
|
2379
|
+
return normalized
|
|
2380
|
+
? crypto.createHash('sha256')
|
|
2381
|
+
.update(`${safeString(salt, 160)}:${normalized}`)
|
|
2382
|
+
.digest('hex')
|
|
2383
|
+
.slice(0, 16)
|
|
2384
|
+
: '';
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
function currentTransportDiagnosticOwner(device) {
|
|
2388
|
+
const frame = device?.latestLiveFrame;
|
|
2389
|
+
if (!device?.connected
|
|
2390
|
+
|| !device.sessionId
|
|
2391
|
+
|| !frame
|
|
2392
|
+
|| frame.currentGenerationVerified !== true
|
|
2393
|
+
|| safeString(frame.mimeType, 80).toLowerCase() !== 'video/h264'
|
|
2394
|
+
|| !safeString(frame.streamId, 128)
|
|
2395
|
+
|| !safeString(frame.commandId, 128)
|
|
2396
|
+
|| Number(frame.captureGeneration || 0) <= 0) {
|
|
2397
|
+
return null;
|
|
2398
|
+
}
|
|
2399
|
+
const monitorIndex = parseExactLiveStreamMonitorIndex(frame.monitorIndex);
|
|
2400
|
+
const streamState = getDeviceLiveStream(device, frame.streamId);
|
|
2401
|
+
if (monitorIndex === null
|
|
2402
|
+
|| !streamState?.active
|
|
2403
|
+
|| safeString(streamState.commandId, 128) !== safeString(frame.commandId, 128)
|
|
2404
|
+
|| Number(streamState.captureGeneration || 0) !== Number(frame.captureGeneration || 0)
|
|
2405
|
+
|| parseExactLiveStreamMonitorIndex(streamState.monitorIndex) !== monitorIndex) {
|
|
2406
|
+
return null;
|
|
2407
|
+
}
|
|
2408
|
+
return {
|
|
2409
|
+
deviceId: device.deviceId,
|
|
2410
|
+
sessionId: device.sessionId,
|
|
2411
|
+
streamId: safeString(frame.streamId, 128),
|
|
2412
|
+
streamCommandId: safeString(frame.commandId, 128),
|
|
2413
|
+
captureGeneration: Number(frame.captureGeneration),
|
|
2414
|
+
monitorIndex
|
|
2415
|
+
};
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
function transportDiagnosticOwnersEqual(left, right) {
|
|
2419
|
+
return !!left
|
|
2420
|
+
&& !!right
|
|
2421
|
+
&& left.deviceId === right.deviceId
|
|
2422
|
+
&& left.sessionId === right.sessionId
|
|
2423
|
+
&& left.streamId === right.streamId
|
|
2424
|
+
&& left.streamCommandId === right.streamCommandId
|
|
2425
|
+
&& left.captureGeneration === right.captureGeneration
|
|
2426
|
+
&& left.monitorIndex === right.monitorIndex;
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
function transportDiagnosticOwnerMatches(record, device) {
|
|
2430
|
+
const current = currentTransportDiagnosticOwner(device);
|
|
2431
|
+
return transportDiagnosticOwnersEqual(current, record.owner);
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
function serializeTransportDiagnostic(record) {
|
|
2435
|
+
const refreshedNetwork = udpTransport?.getDeviceDiagnosticNetwork?.(record.owner.deviceId, {
|
|
2436
|
+
salt: record.leaseId
|
|
2437
|
+
});
|
|
2438
|
+
if (refreshedNetwork?.evidence
|
|
2439
|
+
=== 'authenticated-udp-endpoint-rendezvous-observed'
|
|
2440
|
+
|| record.network?.evidence
|
|
2441
|
+
!== 'authenticated-udp-endpoint-rendezvous-observed') {
|
|
2442
|
+
record.network = refreshedNetwork || record.network;
|
|
2443
|
+
}
|
|
2444
|
+
return {
|
|
2445
|
+
ok: record.state !== 'failed',
|
|
2446
|
+
leaseId: record.leaseId,
|
|
2447
|
+
state: record.state,
|
|
2448
|
+
phase: record.phase,
|
|
2449
|
+
startedAt: record.startedAt,
|
|
2450
|
+
updatedAt: record.updatedAt,
|
|
2451
|
+
expiresAt: record.expiresAt,
|
|
2452
|
+
completedAt: record.completedAt || '',
|
|
2453
|
+
error: safeString(record.error, 240),
|
|
2454
|
+
owner: {
|
|
2455
|
+
deviceId: record.owner.deviceId,
|
|
2456
|
+
streamId: record.owner.streamId,
|
|
2457
|
+
streamCommandId: record.owner.streamCommandId,
|
|
2458
|
+
captureGeneration: record.owner.captureGeneration,
|
|
2459
|
+
monitorIndex: record.owner.monitorIndex
|
|
2460
|
+
},
|
|
2461
|
+
network: record.network,
|
|
2462
|
+
baseline: { ...record.baseline },
|
|
2463
|
+
frames: record.frames.map(frame => ({ ...frame })),
|
|
2464
|
+
cleanup: record.cleanup.map(entry => ({ ...entry }))
|
|
2465
|
+
};
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
function getTransportDiagnostic(leaseId, token) {
|
|
2469
|
+
const record = transportDiagnostics.get(safeString(leaseId, 128));
|
|
2470
|
+
if (!record || !diagnosticSecretEquals(record.token, token)) {
|
|
2471
|
+
return { ok: false, error: 'transport-diagnostic-not-found' };
|
|
2472
|
+
}
|
|
2473
|
+
return serializeTransportDiagnostic(record);
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
function diagnosticSecretEquals(expected, provided) {
|
|
2477
|
+
const expectedHash = crypto.createHash('sha256').update(String(expected || '')).digest();
|
|
2478
|
+
const providedHash = crypto.createHash('sha256').update(String(provided || '')).digest();
|
|
2479
|
+
return crypto.timingSafeEqual(expectedHash, providedHash);
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
async function sendTransportDiagnosticCommand(record, operation, phase = record.phase) {
|
|
2483
|
+
const device = devices.get(record.owner.deviceId);
|
|
2484
|
+
if (!device?.connected || device.sessionId !== record.owner.sessionId) {
|
|
2485
|
+
return { ok: false, error: 'transport-diagnostic-device-disconnected' };
|
|
2486
|
+
}
|
|
2487
|
+
if (operation !== 'release' && !transportDiagnosticOwnerMatches(record, device)) {
|
|
2488
|
+
return { ok: false, error: 'transport-diagnostic-owner-changed' };
|
|
2489
|
+
}
|
|
2490
|
+
const commandId = crypto.randomUUID();
|
|
2491
|
+
const waiter = waitForCommandResult(device, commandId, transportDiagnosticCommandTimeoutMs);
|
|
2492
|
+
const queued = sendCommand(device.deviceId, {
|
|
2493
|
+
commandId,
|
|
2494
|
+
command: 'transport.diagnostic',
|
|
2495
|
+
payload: {
|
|
2496
|
+
operation,
|
|
2497
|
+
leaseId: record.leaseId,
|
|
2498
|
+
token: record.token,
|
|
2499
|
+
streamId: record.owner.streamId,
|
|
2500
|
+
streamCommandId: record.owner.streamCommandId,
|
|
2501
|
+
captureGeneration: record.owner.captureGeneration,
|
|
2502
|
+
monitorIndex: record.owner.monitorIndex,
|
|
2503
|
+
phase,
|
|
2504
|
+
ttlMs: Math.max(0, Date.parse(record.expiresAt) - Date.now())
|
|
2505
|
+
}
|
|
2506
|
+
});
|
|
2507
|
+
if (!queued.ok) {
|
|
2508
|
+
failPendingCommandResultWaiter(commandId, queued.error);
|
|
2509
|
+
}
|
|
2510
|
+
const outcome = await waiter;
|
|
2511
|
+
if (!outcome.ok || outcome.result?.ok === false) {
|
|
2512
|
+
return {
|
|
2513
|
+
ok: false,
|
|
2514
|
+
error: safeString(outcome.error || outcome.result?.error, 240)
|
|
2515
|
+
|| 'transport-diagnostic-agent-rejected'
|
|
2516
|
+
};
|
|
2517
|
+
}
|
|
2518
|
+
return { ok: true };
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
function appendTransportDiagnosticCleanup(record, state, reason = '') {
|
|
2522
|
+
record.cleanup.push({
|
|
2523
|
+
at: new Date().toISOString(),
|
|
2524
|
+
state,
|
|
2525
|
+
reason: safeString(reason, 160)
|
|
2526
|
+
});
|
|
2527
|
+
if (record.cleanup.length > 16) record.cleanup.splice(0, record.cleanup.length - 16);
|
|
2528
|
+
record.updatedAt = new Date().toISOString();
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
function clearTransportDiagnosticTimers(record) {
|
|
2532
|
+
clearTimeout(record?.expiryTimer);
|
|
2533
|
+
clearTimeout(record?.removalTimer);
|
|
2534
|
+
if (record) {
|
|
2535
|
+
record.expiryTimer = null;
|
|
2536
|
+
record.removalTimer = null;
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
function removeTransportDiagnostic(record) {
|
|
2541
|
+
if (!record) return;
|
|
2542
|
+
clearTransportDiagnosticTimers(record);
|
|
2543
|
+
if (transportDiagnostics.get(record.leaseId) === record) {
|
|
2544
|
+
transportDiagnostics.delete(record.leaseId);
|
|
2545
|
+
}
|
|
2546
|
+
record.token = '';
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
function scheduleTransportDiagnosticRemoval(record) {
|
|
2550
|
+
if (!record) return;
|
|
2551
|
+
clearTimeout(record.expiryTimer);
|
|
2552
|
+
clearTimeout(record.removalTimer);
|
|
2553
|
+
record.expiryTimer = null;
|
|
2554
|
+
record.removalTimer = setTimeout(() => {
|
|
2555
|
+
removeTransportDiagnostic(record);
|
|
2556
|
+
}, TRANSPORT_DIAGNOSTIC_RECORD_RETENTION_MS);
|
|
2557
|
+
record.removalTimer.unref?.();
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
function pruneTransportDiagnosticRecords() {
|
|
2561
|
+
const finalized = [...transportDiagnostics.values()]
|
|
2562
|
+
.filter(record => record.state !== 'active'
|
|
2563
|
+
&& record.state !== 'acquiring'
|
|
2564
|
+
&& record.state !== 'expiring')
|
|
2565
|
+
.sort((left, right) => Date.parse(left.updatedAt || '') - Date.parse(right.updatedAt || ''));
|
|
2566
|
+
while (transportDiagnostics.size >= TRANSPORT_DIAGNOSTIC_MAX_RECORDS && finalized.length > 0) {
|
|
2567
|
+
removeTransportDiagnostic(finalized.shift());
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
async function expireTransportDiagnostic(record, reason = 'ttl-expired') {
|
|
2572
|
+
if (!record || record.state !== 'active') return;
|
|
2573
|
+
if (record.operationInFlight === true) {
|
|
2574
|
+
record.expiryTimer = setTimeout(() => {
|
|
2575
|
+
void expireTransportDiagnostic(record, reason);
|
|
2576
|
+
}, 250);
|
|
2577
|
+
record.expiryTimer.unref?.();
|
|
2578
|
+
return;
|
|
2579
|
+
}
|
|
2580
|
+
record.operationInFlight = true;
|
|
2581
|
+
record.state = 'expiring';
|
|
2582
|
+
appendTransportDiagnosticCleanup(record, 'release-requested', reason);
|
|
2583
|
+
try {
|
|
2584
|
+
const released = await sendTransportDiagnosticCommand(record, 'release');
|
|
2585
|
+
record.state = reason === 'ttl-expired' ? 'expired' : 'aborted';
|
|
2586
|
+
record.error = released.ok ? '' : released.error;
|
|
2587
|
+
record.completedAt = new Date().toISOString();
|
|
2588
|
+
appendTransportDiagnosticCleanup(
|
|
2589
|
+
record,
|
|
2590
|
+
released.ok ? 'agent-released' : 'agent-release-unconfirmed',
|
|
2591
|
+
released.error);
|
|
2592
|
+
} finally {
|
|
2593
|
+
record.operationInFlight = false;
|
|
2594
|
+
scheduleTransportDiagnosticRemoval(record);
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
function abortTransportDiagnosticsForDevice(device, reason) {
|
|
2599
|
+
for (const record of transportDiagnostics.values()) {
|
|
2600
|
+
if (!['active', 'acquiring', 'expiring'].includes(record.state)
|
|
2601
|
+
|| record.owner.deviceId !== device?.deviceId
|
|
2602
|
+
|| record.owner.sessionId !== device?.sessionId) {
|
|
2603
|
+
continue;
|
|
2604
|
+
}
|
|
2605
|
+
clearTimeout(record.expiryTimer);
|
|
2606
|
+
record.state = 'aborted';
|
|
2607
|
+
record.error = safeString(reason, 160) || 'device-disconnected';
|
|
2608
|
+
record.completedAt = new Date().toISOString();
|
|
2609
|
+
appendTransportDiagnosticCleanup(record, 'connection-closed', record.error);
|
|
2610
|
+
scheduleTransportDiagnosticRemoval(record);
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
async function startTransportDiagnostic(deviceId, ttlMs = 150_000, options = {}) {
|
|
2615
|
+
const requestSignal = options?.signal;
|
|
2616
|
+
const requestAborted = () => requestSignal?.aborted === true;
|
|
2617
|
+
const normalizedDeviceId = safeString(deviceId, 160);
|
|
2618
|
+
const device = devices.get(normalizedDeviceId);
|
|
2619
|
+
const owner = currentTransportDiagnosticOwner(device);
|
|
2620
|
+
if (!owner) {
|
|
2621
|
+
return { ok: false, error: 'transport-diagnostic-current-h264-stream-required' };
|
|
2622
|
+
}
|
|
2623
|
+
if (observedTransportPath(device.latestLiveFrame) !== 'direct'
|
|
2624
|
+
|| safeString(device.latestLiveFrame?.frameTransportActive, 20).toLowerCase() !== 'direct') {
|
|
2625
|
+
return { ok: false, error: 'transport-diagnostic-direct-start-required' };
|
|
2626
|
+
}
|
|
2627
|
+
if (transportDiagnosticStartingDevices.has(normalizedDeviceId)) {
|
|
2628
|
+
return { ok: false, error: 'transport-diagnostic-device-lease-active' };
|
|
2629
|
+
}
|
|
2630
|
+
const existingLease = [...transportDiagnostics.values()].some(existing =>
|
|
2631
|
+
['active', 'acquiring', 'expiring'].includes(existing.state)
|
|
2632
|
+
&& existing.owner.deviceId === owner.deviceId);
|
|
2633
|
+
if (existingLease) {
|
|
2634
|
+
return { ok: false, error: 'transport-diagnostic-device-lease-active' };
|
|
2635
|
+
}
|
|
2636
|
+
transportDiagnosticStartingDevices.add(normalizedDeviceId);
|
|
2637
|
+
try {
|
|
2638
|
+
const refreshedDiagnosticNetwork =
|
|
2639
|
+
await udpTransport?.refreshDeviceDiagnosticNetwork?.(
|
|
2640
|
+
owner.deviceId,
|
|
2641
|
+
{ timeoutMs: 3_000 });
|
|
2642
|
+
if (requestAborted()) {
|
|
2643
|
+
return { ok: false, error: 'transport-diagnostic-request-aborted' };
|
|
2644
|
+
}
|
|
2645
|
+
const physicalNetwork = udpTransport?.getDeviceDiagnosticNetwork?.(
|
|
2646
|
+
owner.deviceId,
|
|
2647
|
+
{ salt: 'transport-diagnostic-preflight' }) || {};
|
|
2648
|
+
if (refreshedDiagnosticNetwork !== true
|
|
2649
|
+
|| physicalNetwork.evidence
|
|
2650
|
+
!== 'authenticated-udp-endpoint-rendezvous-observed'
|
|
2651
|
+
|| physicalNetwork.endpointAuthenticated !== true
|
|
2652
|
+
|| physicalNetwork.clientAddressPublic !== true
|
|
2653
|
+
|| physicalNetwork.hubAddressPublic !== true) {
|
|
2654
|
+
return {
|
|
2655
|
+
ok: false,
|
|
2656
|
+
error: 'transport-diagnostic-fresh-rendezvous-egress-required'
|
|
2657
|
+
};
|
|
2658
|
+
}
|
|
2659
|
+
if (physicalNetwork.differentPublicEgress !== true) {
|
|
2660
|
+
return {
|
|
2661
|
+
ok: false,
|
|
2662
|
+
error: 'transport-diagnostic-distinct-public-egress-required'
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2665
|
+
const currentDevice = devices.get(normalizedDeviceId);
|
|
2666
|
+
const refreshedOwner = currentTransportDiagnosticOwner(currentDevice);
|
|
2667
|
+
if (!transportDiagnosticOwnersEqual(owner, refreshedOwner)
|
|
2668
|
+
|| observedTransportPath(currentDevice?.latestLiveFrame) !== 'direct'
|
|
2669
|
+
|| safeString(currentDevice?.latestLiveFrame?.frameTransportActive, 20).toLowerCase() !== 'direct') {
|
|
2670
|
+
return { ok: false, error: 'transport-diagnostic-owner-changed' };
|
|
2671
|
+
}
|
|
2672
|
+
const concurrentLease = [...transportDiagnostics.values()].some(existing =>
|
|
2673
|
+
['active', 'acquiring', 'expiring'].includes(existing.state)
|
|
2674
|
+
&& existing.owner.deviceId === owner.deviceId);
|
|
2675
|
+
if (concurrentLease) {
|
|
2676
|
+
return { ok: false, error: 'transport-diagnostic-device-lease-active' };
|
|
2677
|
+
}
|
|
2678
|
+
pruneTransportDiagnosticRecords();
|
|
2679
|
+
if (transportDiagnostics.size >= TRANSPORT_DIAGNOSTIC_MAX_RECORDS) {
|
|
2680
|
+
return { ok: false, error: 'transport-diagnostic-capacity-reached' };
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
const boundedTtlMs = clampNumber(
|
|
2684
|
+
ttlMs,
|
|
2685
|
+
TRANSPORT_DIAGNOSTIC_MIN_TTL_MS,
|
|
2686
|
+
TRANSPORT_DIAGNOSTIC_MAX_TTL_MS,
|
|
2687
|
+
150_000);
|
|
2688
|
+
const now = new Date();
|
|
2689
|
+
const leaseId = crypto.randomUUID();
|
|
2690
|
+
const token = crypto.randomBytes(32).toString('base64url');
|
|
2691
|
+
const currentFrame = currentDevice.latestLiveFrame;
|
|
2692
|
+
const udpStatus = udpTransport?.getDeviceStatus?.(owner.deviceId) || {};
|
|
2693
|
+
const hubAddressObservedAt = safeString(
|
|
2694
|
+
physicalNetwork.hubAddressObservedAt,
|
|
2695
|
+
80);
|
|
2696
|
+
const record = {
|
|
2697
|
+
leaseId,
|
|
2698
|
+
token,
|
|
2699
|
+
owner,
|
|
2700
|
+
phase: 'direct',
|
|
2701
|
+
phaseStartedAt: '',
|
|
2702
|
+
state: 'acquiring',
|
|
2703
|
+
operationInFlight: true,
|
|
2704
|
+
startedAt: now.toISOString(),
|
|
2705
|
+
updatedAt: now.toISOString(),
|
|
2706
|
+
expiresAt: new Date(now.getTime() + boundedTtlMs).toISOString(),
|
|
2707
|
+
completedAt: '',
|
|
2708
|
+
error: '',
|
|
2709
|
+
hubAddressObservedAt,
|
|
2710
|
+
baseline: {
|
|
2711
|
+
observedAt: safeString(currentFrame.receivedAt, 80),
|
|
2712
|
+
frameSeq: optionalFiniteNumber(currentFrame.frameSeq),
|
|
2713
|
+
switchCount: optionalFiniteNumber(currentFrame.frameTransportSwitchCount),
|
|
2714
|
+
agentQueueDropped: optionalFiniteNumber(currentFrame.nativeOutputQueueDroppedCount),
|
|
2715
|
+
hubIngressDropped: optionalFiniteNumber(currentFrame.hubIngressDropped),
|
|
2716
|
+
udpIncompleteFrames: optionalFiniteNumber(udpStatus.droppedFrames),
|
|
2717
|
+
hubIngressCounterEpochHash: transportDiagnosticIdentifierHash(
|
|
2718
|
+
currentFrame.hubIngressCounterEpoch,
|
|
2719
|
+
leaseId),
|
|
2720
|
+
udpCounterEpochHash: transportDiagnosticIdentifierHash(
|
|
2721
|
+
udpStatus.sessionId,
|
|
2722
|
+
leaseId),
|
|
2723
|
+
agentQueueTelemetryAvailable:
|
|
2724
|
+
currentFrame.nativeOutputQueueTelemetryAvailable === true,
|
|
2725
|
+
hubIngressTelemetryAvailable:
|
|
2726
|
+
currentFrame.hubIngressTelemetryAvailable === true
|
|
2727
|
+
&& !!currentFrame.hubIngressCounterEpoch,
|
|
2728
|
+
udpTelemetryAvailable:
|
|
2729
|
+
optionalFiniteNumber(udpStatus.droppedFrames) !== null
|
|
2730
|
+
&& !!udpStatus.sessionId
|
|
2731
|
+
},
|
|
2732
|
+
frames: [],
|
|
2733
|
+
cleanup: [],
|
|
2734
|
+
network: udpTransport?.getDeviceDiagnosticNetwork?.(owner.deviceId, {
|
|
2735
|
+
salt: leaseId
|
|
2736
|
+
}) || {
|
|
2737
|
+
evidence: 'unavailable',
|
|
2738
|
+
differentPublicEgress: null,
|
|
2739
|
+
hubAddressObservedAt
|
|
2740
|
+
},
|
|
2741
|
+
expiryTimer: null,
|
|
2742
|
+
removalTimer: null
|
|
2743
|
+
};
|
|
2744
|
+
transportDiagnostics.set(leaseId, record);
|
|
2745
|
+
const acquired = await sendTransportDiagnosticCommand(record, 'acquire', 'direct');
|
|
2746
|
+
record.operationInFlight = false;
|
|
2747
|
+
if (!acquired.ok || record.state !== 'acquiring') {
|
|
2748
|
+
const acquireError = acquired.ok
|
|
2749
|
+
? safeString(record.error, 240) || 'transport-diagnostic-acquire-interrupted'
|
|
2750
|
+
: acquired.error;
|
|
2751
|
+
record.error = acquireError;
|
|
2752
|
+
record.completedAt = new Date().toISOString();
|
|
2753
|
+
appendTransportDiagnosticCleanup(record, 'acquire-failed', record.error);
|
|
2754
|
+
appendTransportDiagnosticCleanup(
|
|
2755
|
+
record,
|
|
2756
|
+
'release-requested',
|
|
2757
|
+
'acquire-outcome-uncertain');
|
|
2758
|
+
record.operationInFlight = true;
|
|
2759
|
+
let released;
|
|
2760
|
+
try {
|
|
2761
|
+
released = await sendTransportDiagnosticCommand(record, 'release');
|
|
2762
|
+
} catch (error) {
|
|
2763
|
+
released = {
|
|
2764
|
+
ok: false,
|
|
2765
|
+
error: safeString(error instanceof Error ? error.message : error, 240)
|
|
2766
|
+
|| 'transport-diagnostic-agent-release-failed'
|
|
2767
|
+
};
|
|
2768
|
+
} finally {
|
|
2769
|
+
record.operationInFlight = false;
|
|
2770
|
+
}
|
|
2771
|
+
appendTransportDiagnosticCleanup(
|
|
2772
|
+
record,
|
|
2773
|
+
released.ok ? 'agent-released' : 'agent-release-unconfirmed',
|
|
2774
|
+
released.error);
|
|
2775
|
+
record.state = 'failed';
|
|
2776
|
+
record.completedAt = new Date().toISOString();
|
|
2777
|
+
const failed = { ...serializeTransportDiagnostic(record), token: '' };
|
|
2778
|
+
removeTransportDiagnostic(record);
|
|
2779
|
+
return failed;
|
|
2780
|
+
}
|
|
2781
|
+
record.state = 'active';
|
|
2782
|
+
record.phaseStartedAt = new Date().toISOString();
|
|
2783
|
+
record.updatedAt = record.phaseStartedAt;
|
|
2784
|
+
appendTransportDiagnosticCleanup(record, 'lease-acquired');
|
|
2785
|
+
if (requestAborted()) {
|
|
2786
|
+
await expireTransportDiagnostic(record, 'start-request-aborted');
|
|
2787
|
+
return { ok: false, error: 'transport-diagnostic-request-aborted' };
|
|
2788
|
+
}
|
|
2789
|
+
const remainingTtlMs = Math.max(1, Date.parse(record.expiresAt) - Date.now());
|
|
2790
|
+
record.expiryTimer = setTimeout(() => {
|
|
2791
|
+
void expireTransportDiagnostic(record, 'ttl-expired');
|
|
2792
|
+
}, remainingTtlMs);
|
|
2793
|
+
record.expiryTimer.unref?.();
|
|
2794
|
+
return { ...serializeTransportDiagnostic(record), token };
|
|
2795
|
+
} finally {
|
|
2796
|
+
transportDiagnosticStartingDevices.delete(normalizedDeviceId);
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
async function setTransportDiagnosticPhase(leaseId, token, phase) {
|
|
2801
|
+
const record = transportDiagnostics.get(safeString(leaseId, 128));
|
|
2802
|
+
const normalizedPhase = safeString(phase, 32).toLowerCase();
|
|
2803
|
+
if (!record || !diagnosticSecretEquals(record.token, token)) {
|
|
2804
|
+
return { ok: false, error: 'transport-diagnostic-not-found' };
|
|
2805
|
+
}
|
|
2806
|
+
if (record.state !== 'active') {
|
|
2807
|
+
return { ok: false, error: 'transport-diagnostic-not-active' };
|
|
2808
|
+
}
|
|
2809
|
+
if (record.operationInFlight === true) {
|
|
2810
|
+
return { ok: false, error: 'transport-diagnostic-operation-in-flight' };
|
|
2811
|
+
}
|
|
2812
|
+
if (!TRANSPORT_DIAGNOSTIC_PHASES.includes(normalizedPhase)) {
|
|
2813
|
+
return { ok: false, error: 'transport-diagnostic-phase-invalid' };
|
|
2814
|
+
}
|
|
2815
|
+
const currentIndex = TRANSPORT_DIAGNOSTIC_PHASES.indexOf(record.phase);
|
|
2816
|
+
const nextIndex = TRANSPORT_DIAGNOSTIC_PHASES.indexOf(normalizedPhase);
|
|
2817
|
+
const currentObserved = record.frames.some(frame => frame.phase === record.phase && frame.accepted === true);
|
|
2818
|
+
if (nextIndex !== currentIndex + 1 || !currentObserved) {
|
|
2819
|
+
return { ok: false, error: 'transport-diagnostic-phase-order-required' };
|
|
2820
|
+
}
|
|
2821
|
+
record.operationInFlight = true;
|
|
2822
|
+
try {
|
|
2823
|
+
const updated = await sendTransportDiagnosticCommand(record, 'set-phase', normalizedPhase);
|
|
2824
|
+
if (!updated.ok) {
|
|
2825
|
+
record.error = updated.error;
|
|
2826
|
+
record.updatedAt = new Date().toISOString();
|
|
2827
|
+
return { ok: false, error: updated.error };
|
|
2828
|
+
}
|
|
2829
|
+
if (record.state !== 'active') {
|
|
2830
|
+
return { ok: false, error: 'transport-diagnostic-interrupted' };
|
|
2831
|
+
}
|
|
2832
|
+
record.phase = normalizedPhase;
|
|
2833
|
+
record.phaseStartedAt = new Date().toISOString();
|
|
2834
|
+
record.updatedAt = record.phaseStartedAt;
|
|
2835
|
+
return serializeTransportDiagnostic(record);
|
|
2836
|
+
} finally {
|
|
2837
|
+
record.operationInFlight = false;
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
async function stopTransportDiagnostic(leaseId, token, reason = 'runner-complete') {
|
|
2842
|
+
const record = transportDiagnostics.get(safeString(leaseId, 128));
|
|
2843
|
+
if (!record || !diagnosticSecretEquals(record.token, token)) {
|
|
2844
|
+
return { ok: false, error: 'transport-diagnostic-not-found' };
|
|
2845
|
+
}
|
|
2846
|
+
if (record.state !== 'active') return serializeTransportDiagnostic(record);
|
|
2847
|
+
if (record.operationInFlight === true) {
|
|
2848
|
+
return { ok: false, error: 'transport-diagnostic-operation-in-flight' };
|
|
2849
|
+
}
|
|
2850
|
+
clearTimeout(record.expiryTimer);
|
|
2851
|
+
record.operationInFlight = true;
|
|
2852
|
+
try {
|
|
2853
|
+
appendTransportDiagnosticCleanup(record, 'release-requested', reason);
|
|
2854
|
+
const released = await sendTransportDiagnosticCommand(record, 'release');
|
|
2855
|
+
record.state = released.ok ? 'completed' : 'failed';
|
|
2856
|
+
record.error = released.ok ? '' : released.error;
|
|
2857
|
+
record.completedAt = new Date().toISOString();
|
|
2858
|
+
appendTransportDiagnosticCleanup(
|
|
2859
|
+
record,
|
|
2860
|
+
released.ok ? 'agent-released' : 'agent-release-unconfirmed',
|
|
2861
|
+
released.error);
|
|
2862
|
+
const serialized = serializeTransportDiagnostic(record);
|
|
2863
|
+
scheduleTransportDiagnosticRemoval(record);
|
|
2864
|
+
return serialized;
|
|
2865
|
+
} finally {
|
|
2866
|
+
record.operationInFlight = false;
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
function recordTransportDiagnosticFrame(device, message, payload, frame) {
|
|
2871
|
+
if (transportDiagnostics.size === 0 || message.transportDiagnosticApplied !== true) {
|
|
2872
|
+
return;
|
|
2873
|
+
}
|
|
2874
|
+
const leaseId = safeString(message.transportDiagnosticLeaseId, 128);
|
|
2875
|
+
const phase = safeString(message.transportDiagnosticPhase, 32).toLowerCase();
|
|
2876
|
+
if (!leaseId) return;
|
|
2877
|
+
const record = transportDiagnostics.get(leaseId);
|
|
2878
|
+
if (!record
|
|
2879
|
+
|| record.state !== 'active'
|
|
2880
|
+
|| record.phase !== phase
|
|
2881
|
+
|| !transportDiagnosticOwnerMatches(record, device)
|
|
2882
|
+
|| frame.streamId !== record.owner.streamId
|
|
2883
|
+
|| frame.commandId !== record.owner.streamCommandId
|
|
2884
|
+
|| frame.captureGeneration !== record.owner.captureGeneration
|
|
2885
|
+
|| frame.monitorIndex !== record.owner.monitorIndex
|
|
2886
|
+
|| safeString(frame.mimeType, 80).toLowerCase() !== 'video/h264') {
|
|
2887
|
+
return;
|
|
2888
|
+
}
|
|
2889
|
+
const expectedPath = expectedTransportForDiagnosticPhase(phase);
|
|
2890
|
+
const h264 = inspectAnnexBH264(payload);
|
|
2891
|
+
const agentPath = safeString(frame.frameTransportActive, 20).toLowerCase();
|
|
2892
|
+
const hubObservedPath = observedTransportPath(frame);
|
|
2893
|
+
const frameSeq = optionalFiniteNumber(frame.frameSeq);
|
|
2894
|
+
const switchCount = optionalFiniteNumber(message.frameTransportSwitchCount);
|
|
2895
|
+
const lastAcceptedFrame = [...record.frames]
|
|
2896
|
+
.reverse()
|
|
2897
|
+
.find(item => item.accepted === true);
|
|
2898
|
+
const previousFrameSeq = optionalFiniteNumber(
|
|
2899
|
+
lastAcceptedFrame?.frameSeq ?? record.baseline?.frameSeq);
|
|
2900
|
+
const previousSwitchCount = optionalFiniteNumber(
|
|
2901
|
+
lastAcceptedFrame?.switchCount ?? record.baseline?.switchCount);
|
|
2902
|
+
const sequenceAdvanced = frameSeq !== null
|
|
2903
|
+
&& (previousFrameSeq === null || frameSeq > previousFrameSeq);
|
|
2904
|
+
const switchAdvanced = switchCount !== null
|
|
2905
|
+
&& (lastAcceptedFrame
|
|
2906
|
+
? previousSwitchCount !== null && switchCount > previousSwitchCount
|
|
2907
|
+
: previousSwitchCount === null || switchCount >= previousSwitchCount);
|
|
2908
|
+
const agentQueueDepth = optionalFiniteNumber(message.nativeOutputQueueDepth);
|
|
2909
|
+
const agentQueueBytes = optionalFiniteNumber(message.nativeOutputQueueBytes);
|
|
2910
|
+
const agentQueueDropped = optionalFiniteNumber(message.nativeOutputQueueDroppedCount);
|
|
2911
|
+
const hubIngressDropped = optionalFiniteNumber(message.hubIngressDropped);
|
|
2912
|
+
const udpIncompleteFrames = optionalFiniteNumber(message.udpIncompleteFrames);
|
|
2913
|
+
const hubIngressCounterEpochHash = transportDiagnosticIdentifierHash(
|
|
2914
|
+
message.hubIngressCounterEpoch,
|
|
2915
|
+
record.leaseId);
|
|
2916
|
+
const agentQueueTelemetryAvailable = agentQueueDepth !== null
|
|
2917
|
+
&& agentQueueBytes !== null
|
|
2918
|
+
&& agentQueueDropped !== null;
|
|
2919
|
+
const hubIngressTelemetryAvailable = hubIngressDropped !== null
|
|
2920
|
+
&& !!hubIngressCounterEpochHash;
|
|
2921
|
+
const udpTelemetryAvailable = phase !== 'p2p' || udpIncompleteFrames !== null;
|
|
2922
|
+
const phaseLatencyMs = Math.max(
|
|
2923
|
+
0,
|
|
2924
|
+
Date.parse(frame.receivedAt || '') - Date.parse(record.phaseStartedAt || ''));
|
|
2925
|
+
const accepted = agentPath === expectedPath
|
|
2926
|
+
&& hubObservedPath === expectedPath
|
|
2927
|
+
&& frame.isKeyFrame === true
|
|
2928
|
+
&& h264.annexB
|
|
2929
|
+
&& h264.hasIdr
|
|
2930
|
+
&& h264.hasSps
|
|
2931
|
+
&& h264.hasPps
|
|
2932
|
+
&& h264.nalOrderValid
|
|
2933
|
+
&& sequenceAdvanced
|
|
2934
|
+
&& switchAdvanced
|
|
2935
|
+
&& agentQueueTelemetryAvailable
|
|
2936
|
+
&& hubIngressTelemetryAvailable
|
|
2937
|
+
&& udpTelemetryAvailable
|
|
2938
|
+
&& Number.isFinite(phaseLatencyMs);
|
|
2939
|
+
if (record.frames.some(item => item.phase === phase && item.accepted === true)) return;
|
|
2940
|
+
const observation = {
|
|
2941
|
+
phase,
|
|
2942
|
+
expectedPath,
|
|
2943
|
+
actualPath: agentPath,
|
|
2944
|
+
hubObservedPath,
|
|
2945
|
+
protocol: safeString(frame.frameTransportProtocol, 20),
|
|
2946
|
+
transport: safeString(frame.transport, 32),
|
|
2947
|
+
reason: safeString(frame.frameTransportReason, 160),
|
|
2948
|
+
receivedAt: frame.receivedAt,
|
|
2949
|
+
frameSeq,
|
|
2950
|
+
byteLength: frame.byteLength,
|
|
2951
|
+
contentHash: safeString(frame.contentHash, 128),
|
|
2952
|
+
isKeyFrame: frame.isKeyFrame === true,
|
|
2953
|
+
annexB: h264.annexB,
|
|
2954
|
+
hasIdr: h264.hasIdr,
|
|
2955
|
+
hasSps: h264.hasSps,
|
|
2956
|
+
hasPps: h264.hasPps,
|
|
2957
|
+
nalOrderValid: h264.nalOrderValid,
|
|
2958
|
+
switchCount,
|
|
2959
|
+
tcpFailureCount: frame.frameTransportTcpFailureCount,
|
|
2960
|
+
phaseLatencyMs,
|
|
2961
|
+
sequenceAdvanced,
|
|
2962
|
+
switchAdvanced,
|
|
2963
|
+
agentQueueDepth,
|
|
2964
|
+
agentQueueBytes,
|
|
2965
|
+
agentQueueDropped,
|
|
2966
|
+
hubIngressDropped,
|
|
2967
|
+
udpIncompleteFrames,
|
|
2968
|
+
hubIngressCounterEpochHash,
|
|
2969
|
+
agentQueueTelemetryAvailable,
|
|
2970
|
+
hubIngressTelemetryAvailable,
|
|
2971
|
+
udpTelemetryAvailable,
|
|
2972
|
+
accepted
|
|
2973
|
+
};
|
|
2974
|
+
if (accepted) {
|
|
2975
|
+
record.frames = record.frames.filter(item => item.phase !== phase);
|
|
2976
|
+
record.frames.push(observation);
|
|
2977
|
+
} else {
|
|
2978
|
+
const rejectedIndex = record.frames.findIndex(item =>
|
|
2979
|
+
item.phase === phase && item.accepted !== true);
|
|
2980
|
+
if (rejectedIndex >= 0) record.frames[rejectedIndex] = observation;
|
|
2981
|
+
else record.frames.push(observation);
|
|
2982
|
+
}
|
|
2983
|
+
if (record.frames.length > TRANSPORT_DIAGNOSTIC_PHASES.length * 2) {
|
|
2984
|
+
record.frames = record.frames.filter(item => item.accepted === true)
|
|
2985
|
+
.concat(record.frames.filter(item => item.accepted !== true)
|
|
2986
|
+
.slice(-TRANSPORT_DIAGNOSTIC_PHASES.length));
|
|
2987
|
+
}
|
|
2988
|
+
record.updatedAt = new Date().toISOString();
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2251
2991
|
function makeSyntheticFrame(device, streamId, mode = 'thumbnail', options = {}) {
|
|
2252
2992
|
const now = new Date().toISOString();
|
|
2253
2993
|
const descriptor = buildRemoteFrameTransferDescriptor(mode, mode === 'thumbnail' ? 'thumbnail' : DEFAULT_REMOTE_FRAME_MODE);
|
|
@@ -3117,9 +3857,10 @@ export function createRemoteHub(options = {}) {
|
|
|
3117
3857
|
return;
|
|
3118
3858
|
}
|
|
3119
3859
|
|
|
3120
|
-
device.connected = false;
|
|
3121
|
-
device.socket = null;
|
|
3122
|
-
|
|
3860
|
+
device.connected = false;
|
|
3861
|
+
device.socket = null;
|
|
3862
|
+
abortTransportDiagnosticsForDevice(device, reason);
|
|
3863
|
+
udpTransport?.unregisterDevice?.(deviceId, device.sessionId);
|
|
3123
3864
|
closeInputSocket(device, reason);
|
|
3124
3865
|
closeFrameSocket(device, reason);
|
|
3125
3866
|
device.disconnectedAt = new Date().toISOString();
|
|
@@ -3560,6 +4301,48 @@ export function createRemoteHub(options = {}) {
|
|
|
3560
4301
|
?? result.HubForwardedAtEpochMs
|
|
3561
4302
|
?? pending?.hubForwardedAtEpochMs
|
|
3562
4303
|
?? 0) || 0,
|
|
4304
|
+
duplicate: message.duplicate === true
|
|
4305
|
+
|| message.Duplicate === true
|
|
4306
|
+
|| result.duplicate === true
|
|
4307
|
+
|| result.Duplicate === true,
|
|
4308
|
+
nativeInputBackend: safeString(
|
|
4309
|
+
message.nativeInputBackend
|
|
4310
|
+
|| message.NativeInputBackend
|
|
4311
|
+
|| result.nativeInputBackend
|
|
4312
|
+
|| result.NativeInputBackend,
|
|
4313
|
+
80),
|
|
4314
|
+
nativeEventsRequested: Math.max(0, Number(
|
|
4315
|
+
message.nativeEventsRequested
|
|
4316
|
+
?? message.NativeEventsRequested
|
|
4317
|
+
?? result.nativeEventsRequested
|
|
4318
|
+
?? result.NativeEventsRequested
|
|
4319
|
+
?? 0) || 0),
|
|
4320
|
+
nativeEventsAccepted: Math.max(0, Number(
|
|
4321
|
+
message.nativeEventsAccepted
|
|
4322
|
+
?? message.NativeEventsAccepted
|
|
4323
|
+
?? result.nativeEventsAccepted
|
|
4324
|
+
?? result.NativeEventsAccepted
|
|
4325
|
+
?? 0) || 0),
|
|
4326
|
+
hookEventsObserved: Math.max(0, Number(
|
|
4327
|
+
message.hookEventsObserved
|
|
4328
|
+
?? message.HookEventsObserved
|
|
4329
|
+
?? result.hookEventsObserved
|
|
4330
|
+
?? result.HookEventsObserved
|
|
4331
|
+
?? 0) || 0),
|
|
4332
|
+
hookEventsBlocked: Math.max(0, Number(
|
|
4333
|
+
message.hookEventsBlocked
|
|
4334
|
+
?? message.HookEventsBlocked
|
|
4335
|
+
?? result.hookEventsBlocked
|
|
4336
|
+
?? result.HookEventsBlocked
|
|
4337
|
+
?? 0) || 0),
|
|
4338
|
+
focusChanged: message.focusChanged === true
|
|
4339
|
+
|| message.FocusChanged === true
|
|
4340
|
+
|| result.focusChanged === true
|
|
4341
|
+
|| result.FocusChanged === true,
|
|
4342
|
+
inputDiagnostic: message.inputDiagnostic === true
|
|
4343
|
+
|| message.InputDiagnostic === true
|
|
4344
|
+
|| result.inputDiagnostic === true
|
|
4345
|
+
|| result.InputDiagnostic === true,
|
|
3563
4346
|
fallback: pending?.fallback === true,
|
|
3564
4347
|
hubAcknowledgedAtEpochMs: Date.now()
|
|
3565
4348
|
};
|
|
@@ -4408,10 +5191,17 @@ export function createRemoteHub(options = {}) {
|
|
|
4408
5191
|
nativeOutputQueueHighWatermark: Number.isFinite(Number(message.nativeOutputQueueHighWatermark)) ? Number(message.nativeOutputQueueHighWatermark) : 0,
|
|
4409
5192
|
nativeOutputQueueDroppedCount: Number.isFinite(Number(message.nativeOutputQueueDroppedCount)) ? Number(message.nativeOutputQueueDroppedCount) : 0,
|
|
4410
5193
|
nativeOutputQueueAwaitingKeyFrame: message.nativeOutputQueueAwaitingKeyFrame === true,
|
|
5194
|
+
nativeOutputQueueTelemetryAvailable:
|
|
5195
|
+
optionalFiniteNumber(message.nativeOutputQueueDepth) !== null
|
|
5196
|
+
&& optionalFiniteNumber(message.nativeOutputQueueBytes) !== null
|
|
5197
|
+
&& optionalFiniteNumber(message.nativeOutputQueueDroppedCount) !== null,
|
|
4411
5198
|
nativeCaptureTelemetryAt: safeString(message.nativeCaptureTelemetryAt, 80),
|
|
4412
5199
|
droppedByAgent: Number.isFinite(Number(message.droppedByAgent)) ? Number(message.droppedByAgent) : 0,
|
|
4413
5200
|
hubIngressDropped: Number.isFinite(Number(message.hubIngressDropped)) ? Number(message.hubIngressDropped) : 0,
|
|
4414
5201
|
udpIncompleteFrames: Number.isFinite(Number(message.udpIncompleteFrames)) ? Number(message.udpIncompleteFrames) : 0,
|
|
5202
|
+
hubIngressTelemetryAvailable: optionalFiniteNumber(message.hubIngressDropped) !== null,
|
|
5203
|
+
udpIncompleteTelemetryAvailable: optionalFiniteNumber(message.udpIncompleteFrames) !== null,
|
|
5204
|
+
hubIngressCounterEpoch: safeString(message.hubIngressCounterEpoch, 160),
|
|
4415
5205
|
hardwareEncoder: safeString(message.hardwareEncoder, 80),
|
|
4416
5206
|
platformProfile: safeString(message.platformProfile, 80),
|
|
4417
5207
|
monitorIndex: activeMonitorIndex,
|
|
@@ -4462,9 +5252,10 @@ export function createRemoteHub(options = {}) {
|
|
|
4462
5252
|
frameTransportTcpFailureCount: Number.isFinite(Number(message.frameTransportTcpFailureCount)) ? Number(message.frameTransportTcpFailureCount) : 0,
|
|
4463
5253
|
sameContentStreak,
|
|
4464
5254
|
payload,
|
|
4465
|
-
accessToken: createFrameAccessToken()
|
|
4466
|
-
};
|
|
4467
|
-
|
|
5255
|
+
accessToken: createFrameAccessToken()
|
|
5256
|
+
};
|
|
5257
|
+
recordTransportDiagnosticFrame(device, message, payload, device.latestLiveFrame);
|
|
5258
|
+
streamState.latestFrame = device.latestLiveFrame;
|
|
4468
5259
|
rememberRecentFramePayload(device, 'live', device.latestLiveFrame);
|
|
4469
5260
|
emitFrame({
|
|
4470
5261
|
kind: 'live',
|
|
@@ -4659,7 +5450,8 @@ export function createRemoteHub(options = {}) {
|
|
|
4659
5450
|
if (frameKind === 'stream' || frameKind === 'live' || header.type === 'stream.binary') {
|
|
4660
5451
|
applyLiveFrame(device, {
|
|
4661
5452
|
...header,
|
|
4662
|
-
hubIngressDropped: Number(state.binaryQueueDrops || 0)
|
|
5453
|
+
hubIngressDropped: Number(state.binaryQueueDrops || 0),
|
|
5454
|
+
hubIngressCounterEpoch: state.binaryQueueEpoch
|
|
4663
5455
|
}, framePayload, binaryTransport);
|
|
4664
5456
|
return;
|
|
4665
5457
|
}
|
|
@@ -4700,10 +5492,11 @@ export function createRemoteHub(options = {}) {
|
|
|
4700
5492
|
lastFrameAt: new Date().toISOString(),
|
|
4701
5493
|
framesReceived: Number(device.udp?.framesReceived || 0) + 1
|
|
4702
5494
|
};
|
|
4703
|
-
return applyLiveFrame(device, {
|
|
4704
|
-
...header,
|
|
4705
|
-
transport: 'udp-p2p'
|
|
4706
|
-
|
|
5495
|
+
return applyLiveFrame(device, {
|
|
5496
|
+
...header,
|
|
5497
|
+
transport: 'udp-p2p',
|
|
5498
|
+
hubIngressCounterEpoch: sessionId
|
|
5499
|
+
}, payload, 'udp-p2p');
|
|
4707
5500
|
}
|
|
4708
5501
|
|
|
4709
5502
|
function dropQueuedAgentBinaryFrame(state) {
|
|
@@ -5033,9 +5826,10 @@ export function createRemoteHub(options = {}) {
|
|
|
5033
5826
|
binaryQueue: [],
|
|
5034
5827
|
binaryQueueDraining: false,
|
|
5035
5828
|
binaryQueueScheduled: false,
|
|
5036
|
-
binaryQueueSocket: null,
|
|
5037
|
-
binaryQueueDrops: 0,
|
|
5038
|
-
|
|
5829
|
+
binaryQueueSocket: null,
|
|
5830
|
+
binaryQueueDrops: 0,
|
|
5831
|
+
binaryQueueEpoch: crypto.randomUUID(),
|
|
5832
|
+
closed: false
|
|
5039
5833
|
};
|
|
5040
5834
|
|
|
5041
5835
|
const helloTimer = setTimeout(() => {
|
|
@@ -5193,9 +5987,10 @@ export function createRemoteHub(options = {}) {
|
|
|
5193
5987
|
binaryQueue: [],
|
|
5194
5988
|
binaryQueueDraining: false,
|
|
5195
5989
|
binaryQueueScheduled: false,
|
|
5196
|
-
binaryQueueSocket: null,
|
|
5197
|
-
binaryQueueDrops: 0,
|
|
5198
|
-
|
|
5990
|
+
binaryQueueSocket: null,
|
|
5991
|
+
binaryQueueDrops: 0,
|
|
5992
|
+
binaryQueueEpoch: crypto.randomUUID(),
|
|
5993
|
+
closed: false
|
|
5199
5994
|
};
|
|
5200
5995
|
|
|
5201
5996
|
const helloTimer = setTimeout(() => {
|
|
@@ -5419,8 +6214,9 @@ export function createRemoteHub(options = {}) {
|
|
|
5419
6214
|
return getStatus({ includeSecrets: false });
|
|
5420
6215
|
}
|
|
5421
6216
|
|
|
5422
|
-
async function close() {
|
|
6217
|
+
async function close() {
|
|
5423
6218
|
for (const device of devices.values()) {
|
|
6219
|
+
abortTransportDiagnosticsForDevice(device, 'hub-shutdown');
|
|
5424
6220
|
failAllPendingTasks(device, 'hub-shutdown');
|
|
5425
6221
|
failAllPendingInputFallbacks(device, 'hub-shutdown');
|
|
5426
6222
|
failPendingCommandResultWaiters(device, 'hub-shutdown');
|
|
@@ -5472,10 +6268,11 @@ export function createRemoteHub(options = {}) {
|
|
|
5472
6268
|
|
|
5473
6269
|
function disconnectDevice(deviceId, reason = 'manager-disconnect') {
|
|
5474
6270
|
const device = devices.get(String(deviceId || ''));
|
|
5475
|
-
if (device?.synthetic === true) {
|
|
5476
|
-
device.connected = false;
|
|
5477
|
-
device.disconnectedAt = new Date().toISOString();
|
|
5478
|
-
device.lastDisconnectReason = reason;
|
|
6271
|
+
if (device?.synthetic === true) {
|
|
6272
|
+
device.connected = false;
|
|
6273
|
+
device.disconnectedAt = new Date().toISOString();
|
|
6274
|
+
device.lastDisconnectReason = reason;
|
|
6275
|
+
abortTransportDiagnosticsForDevice(device, reason);
|
|
5479
6276
|
deactivateDeviceLiveStreams(device, reason, device.disconnectedAt);
|
|
5480
6277
|
failAllPendingTasks(device, 'device-disconnected');
|
|
5481
6278
|
failAllPendingInputFallbacks(device, 'device-disconnected');
|
|
@@ -7529,6 +8326,10 @@ export function createRemoteHub(options = {}) {
|
|
|
7529
8326
|
stopLiveStream,
|
|
7530
8327
|
pauseLiveCapture,
|
|
7531
8328
|
resumeLiveCapture,
|
|
8329
|
+
startTransportDiagnostic,
|
|
8330
|
+
getTransportDiagnostic,
|
|
8331
|
+
setTransportDiagnosticPhase,
|
|
8332
|
+
stopTransportDiagnostic,
|
|
7532
8333
|
startAudioStream,
|
|
7533
8334
|
stopAudioStream,
|
|
7534
8335
|
getDeviceLiveFrame,
|