livedesk 0.1.458 → 0.1.460
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/bin/livedesk.js +52 -25
- package/client/package.json +5 -5
- package/diagnostics/livedesk-mode3-capture-smoke.mjs +183 -0
- package/diagnostics/livedesk-transport-physical-core.mjs +503 -0
- package/diagnostics/livedesk-transport-physical.mjs +508 -0
- package/hub/package.json +1 -1
- package/hub/src/remote-hub.js +813 -30
- package/hub/src/server.js +155 -5
- package/hub/src/transport/udp-hub-transport.js +110 -8
- package/hub/src/transport/udp-rendezvous.js +6 -2
- package/package.json +7 -7
package/hub/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
|
}
|
|
@@ -1528,6 +1549,11 @@ class RemoteHubWebSocketAgentSocket {
|
|
|
1528
1549
|
|
|
1529
1550
|
export function createRemoteHub(options = {}) {
|
|
1530
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);
|
|
1531
1557
|
const rejectFrameChannelForTest = isEnabledValue(env.LIVEDESK_TEST_MODE, false)
|
|
1532
1558
|
&& isEnabledValue(env.LIVEDESK_TEST_REJECT_FRAME_CHANNEL, false);
|
|
1533
1559
|
const logEvent = options.logEvent || (() => {});
|
|
@@ -1631,7 +1657,9 @@ export function createRemoteHub(options = {}) {
|
|
|
1631
1657
|
const fileSockets = new Map();
|
|
1632
1658
|
const allSockets = new Set();
|
|
1633
1659
|
const pendingCommandResultWaiters = new Map();
|
|
1634
|
-
const
|
|
1660
|
+
const transportDiagnostics = new Map();
|
|
1661
|
+
const transportDiagnosticStartingDevices = new Set();
|
|
1662
|
+
const duplicateDeviceLogAt = new Map();
|
|
1635
1663
|
let server = null;
|
|
1636
1664
|
let started = false;
|
|
1637
1665
|
let boundPort = requestedPort;
|
|
@@ -2266,6 +2294,700 @@ export function createRemoteHub(options = {}) {
|
|
|
2266
2294
|
});
|
|
2267
2295
|
}
|
|
2268
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
|
+
|
|
2269
2991
|
function makeSyntheticFrame(device, streamId, mode = 'thumbnail', options = {}) {
|
|
2270
2992
|
const now = new Date().toISOString();
|
|
2271
2993
|
const descriptor = buildRemoteFrameTransferDescriptor(mode, mode === 'thumbnail' ? 'thumbnail' : DEFAULT_REMOTE_FRAME_MODE);
|
|
@@ -3135,9 +3857,10 @@ export function createRemoteHub(options = {}) {
|
|
|
3135
3857
|
return;
|
|
3136
3858
|
}
|
|
3137
3859
|
|
|
3138
|
-
device.connected = false;
|
|
3139
|
-
device.socket = null;
|
|
3140
|
-
|
|
3860
|
+
device.connected = false;
|
|
3861
|
+
device.socket = null;
|
|
3862
|
+
abortTransportDiagnosticsForDevice(device, reason);
|
|
3863
|
+
udpTransport?.unregisterDevice?.(deviceId, device.sessionId);
|
|
3141
3864
|
closeInputSocket(device, reason);
|
|
3142
3865
|
closeFrameSocket(device, reason);
|
|
3143
3866
|
device.disconnectedAt = new Date().toISOString();
|
|
@@ -3578,6 +4301,48 @@ export function createRemoteHub(options = {}) {
|
|
|
3578
4301
|
?? result.HubForwardedAtEpochMs
|
|
3579
4302
|
?? pending?.hubForwardedAtEpochMs
|
|
3580
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,
|
|
3581
4346
|
fallback: pending?.fallback === true,
|
|
3582
4347
|
hubAcknowledgedAtEpochMs: Date.now()
|
|
3583
4348
|
};
|
|
@@ -4426,10 +5191,17 @@ export function createRemoteHub(options = {}) {
|
|
|
4426
5191
|
nativeOutputQueueHighWatermark: Number.isFinite(Number(message.nativeOutputQueueHighWatermark)) ? Number(message.nativeOutputQueueHighWatermark) : 0,
|
|
4427
5192
|
nativeOutputQueueDroppedCount: Number.isFinite(Number(message.nativeOutputQueueDroppedCount)) ? Number(message.nativeOutputQueueDroppedCount) : 0,
|
|
4428
5193
|
nativeOutputQueueAwaitingKeyFrame: message.nativeOutputQueueAwaitingKeyFrame === true,
|
|
5194
|
+
nativeOutputQueueTelemetryAvailable:
|
|
5195
|
+
optionalFiniteNumber(message.nativeOutputQueueDepth) !== null
|
|
5196
|
+
&& optionalFiniteNumber(message.nativeOutputQueueBytes) !== null
|
|
5197
|
+
&& optionalFiniteNumber(message.nativeOutputQueueDroppedCount) !== null,
|
|
4429
5198
|
nativeCaptureTelemetryAt: safeString(message.nativeCaptureTelemetryAt, 80),
|
|
4430
5199
|
droppedByAgent: Number.isFinite(Number(message.droppedByAgent)) ? Number(message.droppedByAgent) : 0,
|
|
4431
5200
|
hubIngressDropped: Number.isFinite(Number(message.hubIngressDropped)) ? Number(message.hubIngressDropped) : 0,
|
|
4432
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),
|
|
4433
5205
|
hardwareEncoder: safeString(message.hardwareEncoder, 80),
|
|
4434
5206
|
platformProfile: safeString(message.platformProfile, 80),
|
|
4435
5207
|
monitorIndex: activeMonitorIndex,
|
|
@@ -4480,9 +5252,10 @@ export function createRemoteHub(options = {}) {
|
|
|
4480
5252
|
frameTransportTcpFailureCount: Number.isFinite(Number(message.frameTransportTcpFailureCount)) ? Number(message.frameTransportTcpFailureCount) : 0,
|
|
4481
5253
|
sameContentStreak,
|
|
4482
5254
|
payload,
|
|
4483
|
-
accessToken: createFrameAccessToken()
|
|
4484
|
-
};
|
|
4485
|
-
|
|
5255
|
+
accessToken: createFrameAccessToken()
|
|
5256
|
+
};
|
|
5257
|
+
recordTransportDiagnosticFrame(device, message, payload, device.latestLiveFrame);
|
|
5258
|
+
streamState.latestFrame = device.latestLiveFrame;
|
|
4486
5259
|
rememberRecentFramePayload(device, 'live', device.latestLiveFrame);
|
|
4487
5260
|
emitFrame({
|
|
4488
5261
|
kind: 'live',
|
|
@@ -4677,7 +5450,8 @@ export function createRemoteHub(options = {}) {
|
|
|
4677
5450
|
if (frameKind === 'stream' || frameKind === 'live' || header.type === 'stream.binary') {
|
|
4678
5451
|
applyLiveFrame(device, {
|
|
4679
5452
|
...header,
|
|
4680
|
-
hubIngressDropped: Number(state.binaryQueueDrops || 0)
|
|
5453
|
+
hubIngressDropped: Number(state.binaryQueueDrops || 0),
|
|
5454
|
+
hubIngressCounterEpoch: state.binaryQueueEpoch
|
|
4681
5455
|
}, framePayload, binaryTransport);
|
|
4682
5456
|
return;
|
|
4683
5457
|
}
|
|
@@ -4718,10 +5492,11 @@ export function createRemoteHub(options = {}) {
|
|
|
4718
5492
|
lastFrameAt: new Date().toISOString(),
|
|
4719
5493
|
framesReceived: Number(device.udp?.framesReceived || 0) + 1
|
|
4720
5494
|
};
|
|
4721
|
-
return applyLiveFrame(device, {
|
|
4722
|
-
...header,
|
|
4723
|
-
transport: 'udp-p2p'
|
|
4724
|
-
|
|
5495
|
+
return applyLiveFrame(device, {
|
|
5496
|
+
...header,
|
|
5497
|
+
transport: 'udp-p2p',
|
|
5498
|
+
hubIngressCounterEpoch: sessionId
|
|
5499
|
+
}, payload, 'udp-p2p');
|
|
4725
5500
|
}
|
|
4726
5501
|
|
|
4727
5502
|
function dropQueuedAgentBinaryFrame(state) {
|
|
@@ -5051,9 +5826,10 @@ export function createRemoteHub(options = {}) {
|
|
|
5051
5826
|
binaryQueue: [],
|
|
5052
5827
|
binaryQueueDraining: false,
|
|
5053
5828
|
binaryQueueScheduled: false,
|
|
5054
|
-
binaryQueueSocket: null,
|
|
5055
|
-
binaryQueueDrops: 0,
|
|
5056
|
-
|
|
5829
|
+
binaryQueueSocket: null,
|
|
5830
|
+
binaryQueueDrops: 0,
|
|
5831
|
+
binaryQueueEpoch: crypto.randomUUID(),
|
|
5832
|
+
closed: false
|
|
5057
5833
|
};
|
|
5058
5834
|
|
|
5059
5835
|
const helloTimer = setTimeout(() => {
|
|
@@ -5211,9 +5987,10 @@ export function createRemoteHub(options = {}) {
|
|
|
5211
5987
|
binaryQueue: [],
|
|
5212
5988
|
binaryQueueDraining: false,
|
|
5213
5989
|
binaryQueueScheduled: false,
|
|
5214
|
-
binaryQueueSocket: null,
|
|
5215
|
-
binaryQueueDrops: 0,
|
|
5216
|
-
|
|
5990
|
+
binaryQueueSocket: null,
|
|
5991
|
+
binaryQueueDrops: 0,
|
|
5992
|
+
binaryQueueEpoch: crypto.randomUUID(),
|
|
5993
|
+
closed: false
|
|
5217
5994
|
};
|
|
5218
5995
|
|
|
5219
5996
|
const helloTimer = setTimeout(() => {
|
|
@@ -5437,8 +6214,9 @@ export function createRemoteHub(options = {}) {
|
|
|
5437
6214
|
return getStatus({ includeSecrets: false });
|
|
5438
6215
|
}
|
|
5439
6216
|
|
|
5440
|
-
async function close() {
|
|
6217
|
+
async function close() {
|
|
5441
6218
|
for (const device of devices.values()) {
|
|
6219
|
+
abortTransportDiagnosticsForDevice(device, 'hub-shutdown');
|
|
5442
6220
|
failAllPendingTasks(device, 'hub-shutdown');
|
|
5443
6221
|
failAllPendingInputFallbacks(device, 'hub-shutdown');
|
|
5444
6222
|
failPendingCommandResultWaiters(device, 'hub-shutdown');
|
|
@@ -5490,10 +6268,11 @@ export function createRemoteHub(options = {}) {
|
|
|
5490
6268
|
|
|
5491
6269
|
function disconnectDevice(deviceId, reason = 'manager-disconnect') {
|
|
5492
6270
|
const device = devices.get(String(deviceId || ''));
|
|
5493
|
-
if (device?.synthetic === true) {
|
|
5494
|
-
device.connected = false;
|
|
5495
|
-
device.disconnectedAt = new Date().toISOString();
|
|
5496
|
-
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);
|
|
5497
6276
|
deactivateDeviceLiveStreams(device, reason, device.disconnectedAt);
|
|
5498
6277
|
failAllPendingTasks(device, 'device-disconnected');
|
|
5499
6278
|
failAllPendingInputFallbacks(device, 'device-disconnected');
|
|
@@ -7547,6 +8326,10 @@ export function createRemoteHub(options = {}) {
|
|
|
7547
8326
|
stopLiveStream,
|
|
7548
8327
|
pauseLiveCapture,
|
|
7549
8328
|
resumeLiveCapture,
|
|
8329
|
+
startTransportDiagnostic,
|
|
8330
|
+
getTransportDiagnostic,
|
|
8331
|
+
setTransportDiagnosticPhase,
|
|
8332
|
+
stopTransportDiagnostic,
|
|
7550
8333
|
startAudioStream,
|
|
7551
8334
|
stopAudioStream,
|
|
7552
8335
|
getDeviceLiveFrame,
|