@livedesk/hub 0.1.25 → 0.1.27
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 +1176 -78
- package/src/server.js +159 -5
- package/src/transport/udp-hub-transport.js +139 -14
- 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
|
}
|
|
@@ -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
|
};
|
|
@@ -3924,6 +4689,10 @@ export function createRemoteHub(options = {}) {
|
|
|
3924
4689
|
? message.nativeEncoderHardwareAccelerated
|
|
3925
4690
|
: null,
|
|
3926
4691
|
nativeEncoderHardwareQueryStatus: safeString(message.nativeEncoderHardwareQueryStatus, 80),
|
|
4692
|
+
nativeCaptureDisplayId: Number.isSafeInteger(Number(message.nativeCaptureDisplayId))
|
|
4693
|
+
&& Number(message.nativeCaptureDisplayId) > 0
|
|
4694
|
+
? Number(message.nativeCaptureDisplayId)
|
|
4695
|
+
: 0,
|
|
3927
4696
|
nativeCaptureFps: Number.isFinite(Number(message.nativeCaptureFps)) ? Number(message.nativeCaptureFps) : 0,
|
|
3928
4697
|
nativeCompressionFps: Number.isFinite(Number(message.nativeCompressionFps)) ? Number(message.nativeCompressionFps) : 0,
|
|
3929
4698
|
nativeEncodedFps: Number.isFinite(Number(message.nativeEncodedFps)) ? Number(message.nativeEncodedFps) : 0,
|
|
@@ -3981,7 +4750,7 @@ export function createRemoteHub(options = {}) {
|
|
|
3981
4750
|
return true;
|
|
3982
4751
|
}
|
|
3983
4752
|
|
|
3984
|
-
function
|
|
4753
|
+
function matchPendingLiveStreamOwner(device, streamState, message, transport) {
|
|
3985
4754
|
const pending = getPendingLiveStreamDescriptor(streamState);
|
|
3986
4755
|
const commandId = safeString(message.commandId, 128);
|
|
3987
4756
|
if (!pending || !commandId || commandId !== safeString(pending.commandId, 128)) {
|
|
@@ -3989,9 +4758,11 @@ export function createRemoteHub(options = {}) {
|
|
|
3989
4758
|
}
|
|
3990
4759
|
const messageCaptureGeneration = Number(message.captureGeneration || 0);
|
|
3991
4760
|
const pendingCaptureGeneration = Number(pending.captureGeneration || 0);
|
|
3992
|
-
if (messageCaptureGeneration
|
|
3993
|
-
|
|
3994
|
-
|
|
4761
|
+
if (!Number.isSafeInteger(messageCaptureGeneration)
|
|
4762
|
+
|| messageCaptureGeneration <= 0
|
|
4763
|
+
|| !Number.isSafeInteger(pendingCaptureGeneration)
|
|
4764
|
+
|| pendingCaptureGeneration <= 0
|
|
4765
|
+
|| messageCaptureGeneration !== pendingCaptureGeneration) {
|
|
3995
4766
|
emitRemoteEvent('RemoteLiveStreamOpenIgnored', device, {
|
|
3996
4767
|
reason: 'stale-pending-capture-generation',
|
|
3997
4768
|
streamId: streamState.streamId,
|
|
@@ -4030,6 +4801,27 @@ export function createRemoteHub(options = {}) {
|
|
|
4030
4801
|
});
|
|
4031
4802
|
return null;
|
|
4032
4803
|
}
|
|
4804
|
+
return {
|
|
4805
|
+
pending,
|
|
4806
|
+
commandId,
|
|
4807
|
+
messageCaptureGeneration,
|
|
4808
|
+
pendingCaptureGeneration,
|
|
4809
|
+
pendingMonitorIndex
|
|
4810
|
+
};
|
|
4811
|
+
}
|
|
4812
|
+
|
|
4813
|
+
function promotePendingLiveStream(device, streamState, message, transport) {
|
|
4814
|
+
const owner = matchPendingLiveStreamOwner(device, streamState, message, transport);
|
|
4815
|
+
if (!owner) {
|
|
4816
|
+
return null;
|
|
4817
|
+
}
|
|
4818
|
+
const {
|
|
4819
|
+
pending,
|
|
4820
|
+
commandId,
|
|
4821
|
+
messageCaptureGeneration,
|
|
4822
|
+
pendingCaptureGeneration,
|
|
4823
|
+
pendingMonitorIndex
|
|
4824
|
+
} = owner;
|
|
4033
4825
|
|
|
4034
4826
|
const previousCommandId = safeString(streamState.commandId, 128);
|
|
4035
4827
|
const transfer = buildRemoteFrameTransferDescriptorFromMessage(
|
|
@@ -4138,16 +4930,22 @@ export function createRemoteHub(options = {}) {
|
|
|
4138
4930
|
|
|
4139
4931
|
const activatingPendingGeneration = isPendingGeneration && commandId !== activeCommandId;
|
|
4140
4932
|
if (activatingPendingGeneration) {
|
|
4141
|
-
const
|
|
4142
|
-
if (!
|
|
4933
|
+
const owner = matchPendingLiveStreamOwner(device, streamState, message, transport);
|
|
4934
|
+
if (!owner) {
|
|
4143
4935
|
return false;
|
|
4144
4936
|
}
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4937
|
+
// stream.open travels on the manager connection while the binary
|
|
4938
|
+
// first frame travels on a separate transport. Their arrival order
|
|
4939
|
+
// is not defined. Keep the current generation and its last frame
|
|
4940
|
+
// until the matching first real frame validates and commits the
|
|
4941
|
+
// pending owner atomically.
|
|
4942
|
+
emitRemoteEvent('RemoteLiveStreamOpenDeferred', device, {
|
|
4943
|
+
streamId,
|
|
4944
|
+
commandId,
|
|
4945
|
+
captureGeneration: owner.messageCaptureGeneration,
|
|
4946
|
+
monitorIndex: owner.pendingMonitorIndex,
|
|
4947
|
+
transport
|
|
4948
|
+
});
|
|
4151
4949
|
return true;
|
|
4152
4950
|
}
|
|
4153
4951
|
|
|
@@ -4224,8 +5022,8 @@ export function createRemoteHub(options = {}) {
|
|
|
4224
5022
|
const commandId = safeString(message.commandId, 128);
|
|
4225
5023
|
let streamState = getDeviceLiveStream(device, streamId);
|
|
4226
5024
|
if (!streamState?.active) {
|
|
4227
|
-
device.counters.liveFramesDropped += 1;
|
|
4228
|
-
emitRemoteEvent('RemoteFrameDropped', device, {
|
|
5025
|
+
device.counters.liveFramesDropped += 1;
|
|
5026
|
+
emitRemoteEvent('RemoteFrameDropped', device, {
|
|
4229
5027
|
reason: 'stale-live-stream-frame',
|
|
4230
5028
|
streamId,
|
|
4231
5029
|
frameSeq: Number.isFinite(frameSeq) ? frameSeq : null,
|
|
@@ -4233,9 +5031,91 @@ export function createRemoteHub(options = {}) {
|
|
|
4233
5031
|
});
|
|
4234
5032
|
return false;
|
|
4235
5033
|
}
|
|
5034
|
+
const byteLength = normalizeFrameByteLength(framePayload, frameData);
|
|
5035
|
+
const streamFrameSeq = Number.isFinite(Number(message.streamFrameSeq))
|
|
5036
|
+
? Number(message.streamFrameSeq)
|
|
5037
|
+
: frameSeq;
|
|
5038
|
+
if (byteLength <= 0
|
|
5039
|
+
|| byteLength > MAX_STREAM_BINARY_BYTES
|
|
5040
|
+
|| !Number.isSafeInteger(frameSeq)
|
|
5041
|
+
|| frameSeq <= 0
|
|
5042
|
+
|| !Number.isSafeInteger(streamFrameSeq)
|
|
5043
|
+
|| streamFrameSeq <= 0) {
|
|
5044
|
+
device.counters.liveFramesDropped += 1;
|
|
5045
|
+
emitRemoteEvent('RemoteFrameDropped', device, {
|
|
5046
|
+
reason: 'invalid-live-frame',
|
|
5047
|
+
streamId,
|
|
5048
|
+
commandId,
|
|
5049
|
+
captureGeneration: Number(message.captureGeneration || 0),
|
|
5050
|
+
monitorIndex: parseExactLiveStreamMonitorIndex(message.monitorIndex),
|
|
5051
|
+
frameSeq: Number.isFinite(frameSeq) ? frameSeq : null,
|
|
5052
|
+
transport
|
|
5053
|
+
});
|
|
5054
|
+
return false;
|
|
5055
|
+
}
|
|
5056
|
+
const payload = buildFramePayloadBuffer(framePayload, frameData);
|
|
5057
|
+
if (payload.length <= 0 || payload.length > MAX_STREAM_BINARY_BYTES) {
|
|
5058
|
+
device.counters.liveFramesDropped += 1;
|
|
5059
|
+
emitRemoteEvent('RemoteFrameDropped', device, {
|
|
5060
|
+
reason: 'invalid-live-frame-payload',
|
|
5061
|
+
streamId,
|
|
5062
|
+
commandId,
|
|
5063
|
+
captureGeneration: Number(message.captureGeneration || 0),
|
|
5064
|
+
monitorIndex: parseExactLiveStreamMonitorIndex(message.monitorIndex),
|
|
5065
|
+
frameSeq,
|
|
5066
|
+
transport
|
|
5067
|
+
});
|
|
5068
|
+
return false;
|
|
5069
|
+
}
|
|
4236
5070
|
let activeCommandId = safeString(streamState.commandId, 128);
|
|
4237
|
-
|
|
5071
|
+
const pendingDescriptor = getPendingLiveStreamDescriptor(streamState);
|
|
5072
|
+
let pendingCommandId = safeString(pendingDescriptor?.commandId, 128);
|
|
4238
5073
|
if (pendingCommandId && commandId === pendingCommandId && commandId !== activeCommandId) {
|
|
5074
|
+
if (!matchPendingLiveStreamOwner(device, streamState, message, `${transport}-implicit`)) {
|
|
5075
|
+
return false;
|
|
5076
|
+
}
|
|
5077
|
+
const pendingFrameMode = safeString(
|
|
5078
|
+
message.frameMode
|
|
5079
|
+
|| message.mode
|
|
5080
|
+
|| pendingDescriptor?.frameMode
|
|
5081
|
+
|| pendingDescriptor?.mode,
|
|
5082
|
+
40).toLowerCase();
|
|
5083
|
+
const pendingMimeType = safeString(
|
|
5084
|
+
message.mimeType
|
|
5085
|
+
|| message.format
|
|
5086
|
+
|| pendingDescriptor?.compression,
|
|
5087
|
+
80).toLowerCase();
|
|
5088
|
+
const pendingCodec = safeString(
|
|
5089
|
+
message.codec
|
|
5090
|
+
|| pendingDescriptor?.codec
|
|
5091
|
+
|| pendingDescriptor?.encoding,
|
|
5092
|
+
40).toLowerCase();
|
|
5093
|
+
const requiresH264KeyFrame = pendingFrameMode === 'mode3-h264-hw'
|
|
5094
|
+
|| pendingMimeType.includes('h264')
|
|
5095
|
+
|| pendingMimeType.includes('avc')
|
|
5096
|
+
|| pendingCodec.includes('h264')
|
|
5097
|
+
|| pendingCodec.includes('avc');
|
|
5098
|
+
const isH264KeyFrame = message.isKeyFrame === true
|
|
5099
|
+
|| safeString(message.chunkType, 20).toLowerCase() === 'key';
|
|
5100
|
+
const h264 = requiresH264KeyFrame ? inspectAnnexBH264(payload) : null;
|
|
5101
|
+
const independentlyDecodableH264KeyFrame = isH264KeyFrame
|
|
5102
|
+
&& h264?.annexB === true
|
|
5103
|
+
&& h264.hasSps === true
|
|
5104
|
+
&& h264.hasPps === true
|
|
5105
|
+
&& h264.hasIdr === true
|
|
5106
|
+
&& h264.nalOrderValid === true;
|
|
5107
|
+
if (requiresH264KeyFrame && !independentlyDecodableH264KeyFrame) {
|
|
5108
|
+
device.counters.liveFramesDropped += 1;
|
|
5109
|
+
emitRemoteEvent('RemoteFrameDropped', device, {
|
|
5110
|
+
reason: 'pending-live-stream-decodable-keyframe-required',
|
|
5111
|
+
streamId,
|
|
5112
|
+
commandId,
|
|
5113
|
+
frameSeq,
|
|
5114
|
+
streamFrameSeq,
|
|
5115
|
+
transport
|
|
5116
|
+
});
|
|
5117
|
+
return false;
|
|
5118
|
+
}
|
|
4239
5119
|
const promoted = promotePendingLiveStream(device, streamState, message, `${transport}-implicit`);
|
|
4240
5120
|
if (promoted) {
|
|
4241
5121
|
streamState = promoted.streamState;
|
|
@@ -4313,24 +5193,38 @@ export function createRemoteHub(options = {}) {
|
|
|
4313
5193
|
return false;
|
|
4314
5194
|
}
|
|
4315
5195
|
|
|
4316
|
-
const
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
5196
|
+
const previousFrame = streamState.latestFrame;
|
|
5197
|
+
const currentOwnerCommandId = commandId || activeCommandId;
|
|
5198
|
+
const currentOwnerCaptureGeneration = activeCaptureGeneration || frameCaptureGeneration;
|
|
5199
|
+
const previousStreamFrameSeq = Number(previousFrame?.streamFrameSeq ?? previousFrame?.frameSeq);
|
|
5200
|
+
const sameFrameOwner = previousFrame
|
|
5201
|
+
&& safeString(previousFrame.streamId, 128) === streamId
|
|
5202
|
+
&& safeString(previousFrame.commandId, 128) === currentOwnerCommandId
|
|
5203
|
+
&& Number(previousFrame.captureGeneration || 0) === currentOwnerCaptureGeneration
|
|
5204
|
+
&& parseExactLiveStreamMonitorIndex(previousFrame.monitorIndex) === frameMonitorIndex;
|
|
5205
|
+
if (sameFrameOwner
|
|
5206
|
+
&& Number.isSafeInteger(previousStreamFrameSeq)
|
|
5207
|
+
&& streamFrameSeq <= previousStreamFrameSeq) {
|
|
5208
|
+
device.counters.liveFramesDropped += 1;
|
|
5209
|
+
emitRemoteEvent('RemoteFrameDropped', device, {
|
|
5210
|
+
reason: streamFrameSeq === previousStreamFrameSeq
|
|
5211
|
+
? 'duplicate-live-stream-frame'
|
|
5212
|
+
: 'out-of-order-live-stream-frame',
|
|
5213
|
+
streamId,
|
|
5214
|
+
commandId: currentOwnerCommandId,
|
|
5215
|
+
captureGeneration: currentOwnerCaptureGeneration,
|
|
5216
|
+
monitorIndex: frameMonitorIndex,
|
|
5217
|
+
frameSeq,
|
|
5218
|
+
streamFrameSeq,
|
|
5219
|
+
previousStreamFrameSeq,
|
|
5220
|
+
transport
|
|
5221
|
+
});
|
|
5222
|
+
return false;
|
|
5223
|
+
}
|
|
5224
|
+
|
|
5225
|
+
const mimeType = safeString(message.mimeType || message.format || 'image/jpeg', 80) || 'image/jpeg';
|
|
5226
|
+
const capturedAt = safeString(message.capturedAt, 80) || device.lastSeenAt;
|
|
5227
|
+
const contentHash = buildFrameContentHash(message, payload);
|
|
4334
5228
|
const sameContentStreak = computeSameContentStreak(streamState.latestFrame, contentHash);
|
|
4335
5229
|
const transfer = buildRemoteFrameTransferDescriptorFromMessage(message, streamState.mode || DEFAULT_REMOTE_FRAME_MODE);
|
|
4336
5230
|
if (streamState.open !== true) {
|
|
@@ -4364,7 +5258,7 @@ export function createRemoteHub(options = {}) {
|
|
|
4364
5258
|
device.latestLiveFrame = {
|
|
4365
5259
|
streamId,
|
|
4366
5260
|
frameSeq,
|
|
4367
|
-
streamFrameSeq
|
|
5261
|
+
streamFrameSeq,
|
|
4368
5262
|
commandId: commandId || activeCommandId,
|
|
4369
5263
|
sessionId: device.sessionId,
|
|
4370
5264
|
captureGeneration: activeCaptureGeneration || frameCaptureGeneration,
|
|
@@ -4410,6 +5304,10 @@ export function createRemoteHub(options = {}) {
|
|
|
4410
5304
|
? message.nativeEncoderHardwareAccelerated
|
|
4411
5305
|
: null,
|
|
4412
5306
|
nativeEncoderHardwareQueryStatus: safeString(message.nativeEncoderHardwareQueryStatus, 80),
|
|
5307
|
+
nativeCaptureDisplayId: Number.isSafeInteger(Number(message.nativeCaptureDisplayId))
|
|
5308
|
+
&& Number(message.nativeCaptureDisplayId) > 0
|
|
5309
|
+
? Number(message.nativeCaptureDisplayId)
|
|
5310
|
+
: 0,
|
|
4413
5311
|
nativeCaptureFps: Number.isFinite(Number(message.nativeCaptureFps)) ? Number(message.nativeCaptureFps) : 0,
|
|
4414
5312
|
nativeCompressionFps: Number.isFinite(Number(message.nativeCompressionFps)) ? Number(message.nativeCompressionFps) : 0,
|
|
4415
5313
|
nativeEncodedFps: Number.isFinite(Number(message.nativeEncodedFps)) ? Number(message.nativeEncodedFps) : 0,
|
|
@@ -4426,10 +5324,17 @@ export function createRemoteHub(options = {}) {
|
|
|
4426
5324
|
nativeOutputQueueHighWatermark: Number.isFinite(Number(message.nativeOutputQueueHighWatermark)) ? Number(message.nativeOutputQueueHighWatermark) : 0,
|
|
4427
5325
|
nativeOutputQueueDroppedCount: Number.isFinite(Number(message.nativeOutputQueueDroppedCount)) ? Number(message.nativeOutputQueueDroppedCount) : 0,
|
|
4428
5326
|
nativeOutputQueueAwaitingKeyFrame: message.nativeOutputQueueAwaitingKeyFrame === true,
|
|
5327
|
+
nativeOutputQueueTelemetryAvailable:
|
|
5328
|
+
optionalFiniteNumber(message.nativeOutputQueueDepth) !== null
|
|
5329
|
+
&& optionalFiniteNumber(message.nativeOutputQueueBytes) !== null
|
|
5330
|
+
&& optionalFiniteNumber(message.nativeOutputQueueDroppedCount) !== null,
|
|
4429
5331
|
nativeCaptureTelemetryAt: safeString(message.nativeCaptureTelemetryAt, 80),
|
|
4430
5332
|
droppedByAgent: Number.isFinite(Number(message.droppedByAgent)) ? Number(message.droppedByAgent) : 0,
|
|
4431
5333
|
hubIngressDropped: Number.isFinite(Number(message.hubIngressDropped)) ? Number(message.hubIngressDropped) : 0,
|
|
4432
5334
|
udpIncompleteFrames: Number.isFinite(Number(message.udpIncompleteFrames)) ? Number(message.udpIncompleteFrames) : 0,
|
|
5335
|
+
hubIngressTelemetryAvailable: optionalFiniteNumber(message.hubIngressDropped) !== null,
|
|
5336
|
+
udpIncompleteTelemetryAvailable: optionalFiniteNumber(message.udpIncompleteFrames) !== null,
|
|
5337
|
+
hubIngressCounterEpoch: safeString(message.hubIngressCounterEpoch, 160),
|
|
4433
5338
|
hardwareEncoder: safeString(message.hardwareEncoder, 80),
|
|
4434
5339
|
platformProfile: safeString(message.platformProfile, 80),
|
|
4435
5340
|
monitorIndex: activeMonitorIndex,
|
|
@@ -4480,9 +5385,10 @@ export function createRemoteHub(options = {}) {
|
|
|
4480
5385
|
frameTransportTcpFailureCount: Number.isFinite(Number(message.frameTransportTcpFailureCount)) ? Number(message.frameTransportTcpFailureCount) : 0,
|
|
4481
5386
|
sameContentStreak,
|
|
4482
5387
|
payload,
|
|
4483
|
-
accessToken: createFrameAccessToken()
|
|
4484
|
-
};
|
|
4485
|
-
|
|
5388
|
+
accessToken: createFrameAccessToken()
|
|
5389
|
+
};
|
|
5390
|
+
recordTransportDiagnosticFrame(device, message, payload, device.latestLiveFrame);
|
|
5391
|
+
streamState.latestFrame = device.latestLiveFrame;
|
|
4486
5392
|
rememberRecentFramePayload(device, 'live', device.latestLiveFrame);
|
|
4487
5393
|
emitFrame({
|
|
4488
5394
|
kind: 'live',
|
|
@@ -4596,7 +5502,7 @@ export function createRemoteHub(options = {}) {
|
|
|
4596
5502
|
return true;
|
|
4597
5503
|
}
|
|
4598
5504
|
|
|
4599
|
-
function applyAudioStatus(device, message, framePayload, transport = 'binary') {
|
|
5505
|
+
function applyAudioStatus(device, message, framePayload, transport = 'binary') {
|
|
4600
5506
|
if (!Buffer.isBuffer(framePayload) || framePayload.length < 1) {
|
|
4601
5507
|
device.counters.audioFramesDropped += 1;
|
|
4602
5508
|
return false;
|
|
@@ -4637,9 +5543,184 @@ export function createRemoteHub(options = {}) {
|
|
|
4637
5543
|
payload: framePayload,
|
|
4638
5544
|
byteLength: framePayload.length
|
|
4639
5545
|
});
|
|
4640
|
-
return true;
|
|
4641
|
-
}
|
|
4642
|
-
|
|
5546
|
+
return true;
|
|
5547
|
+
}
|
|
5548
|
+
|
|
5549
|
+
function readFrameTransportProof(message = {}) {
|
|
5550
|
+
if (message.frameTransportProofId === undefined
|
|
5551
|
+
&& message.frameTransportProofVersion === undefined) {
|
|
5552
|
+
return null;
|
|
5553
|
+
}
|
|
5554
|
+
const rawProofId = typeof message.frameTransportProofId === 'string'
|
|
5555
|
+
? message.frameTransportProofId.trim()
|
|
5556
|
+
: '';
|
|
5557
|
+
const captureGeneration = Number(message.captureGeneration);
|
|
5558
|
+
const monitorIndex = parseExactLiveStreamMonitorIndex(message.monitorIndex);
|
|
5559
|
+
const proof = {
|
|
5560
|
+
proofId: rawProofId,
|
|
5561
|
+
streamId: safeString(message.streamId, 128),
|
|
5562
|
+
commandId: safeString(message.commandId, 128),
|
|
5563
|
+
captureGeneration,
|
|
5564
|
+
monitorIndex
|
|
5565
|
+
};
|
|
5566
|
+
const valid = Number(message.frameTransportProofVersion) === 1
|
|
5567
|
+
&& /^[a-f0-9]{32}$/.test(rawProofId)
|
|
5568
|
+
&& !!proof.streamId
|
|
5569
|
+
&& !!proof.commandId
|
|
5570
|
+
&& Number.isSafeInteger(captureGeneration)
|
|
5571
|
+
&& captureGeneration > 0
|
|
5572
|
+
&& monitorIndex !== null;
|
|
5573
|
+
return {
|
|
5574
|
+
...proof,
|
|
5575
|
+
valid,
|
|
5576
|
+
error: valid ? '' : 'frame-proof-owner-invalid'
|
|
5577
|
+
};
|
|
5578
|
+
}
|
|
5579
|
+
|
|
5580
|
+
function applyLiveFrameWithTransportProof(
|
|
5581
|
+
device,
|
|
5582
|
+
message,
|
|
5583
|
+
framePayload,
|
|
5584
|
+
transport) {
|
|
5585
|
+
const proof = readFrameTransportProof(message);
|
|
5586
|
+
if (!proof) {
|
|
5587
|
+
return {
|
|
5588
|
+
accepted: applyLiveFrame(device, message, framePayload, transport),
|
|
5589
|
+
proofAccepted: false,
|
|
5590
|
+
proof: null,
|
|
5591
|
+
error: ''
|
|
5592
|
+
};
|
|
5593
|
+
}
|
|
5594
|
+
const frameMode = safeString(message.frameMode || message.mode, 40).toLowerCase();
|
|
5595
|
+
const mimeType = safeString(message.mimeType || message.format, 80).toLowerCase();
|
|
5596
|
+
const chunkType = safeString(message.chunkType, 20).toLowerCase();
|
|
5597
|
+
const proofFrameSeq = Number(message.frameSeq);
|
|
5598
|
+
const proofStreamFrameSeq = message.streamFrameSeq === undefined
|
|
5599
|
+
|| message.streamFrameSeq === null
|
|
5600
|
+
|| message.streamFrameSeq === ''
|
|
5601
|
+
? proofFrameSeq
|
|
5602
|
+
: Number(message.streamFrameSeq);
|
|
5603
|
+
const proofOnly = message.frameTransportProofOnly === true;
|
|
5604
|
+
const h264 = inspectAnnexBH264(framePayload);
|
|
5605
|
+
const proofPayloadValid = proof.valid
|
|
5606
|
+
&& (transport === 'udp-p2p' || transport === 'relay-binary')
|
|
5607
|
+
&& Buffer.isBuffer(framePayload)
|
|
5608
|
+
&& framePayload.length > 0
|
|
5609
|
+
&& framePayload.length <= MAX_STREAM_BINARY_BYTES
|
|
5610
|
+
&& Number.isSafeInteger(proofFrameSeq)
|
|
5611
|
+
&& proofFrameSeq > 0
|
|
5612
|
+
&& Number.isSafeInteger(proofStreamFrameSeq)
|
|
5613
|
+
&& proofStreamFrameSeq > 0
|
|
5614
|
+
&& frameMode === 'mode3-h264-hw'
|
|
5615
|
+
&& mimeType === 'video/h264'
|
|
5616
|
+
&& (message.isKeyFrame === true || chunkType === 'key')
|
|
5617
|
+
&& safeString(message.frameTransportActive, 20).toLowerCase() === 'none'
|
|
5618
|
+
&& safeString(message.frameTransportState, 20).toLowerCase() === 'probing'
|
|
5619
|
+
&& h264.annexB
|
|
5620
|
+
&& h264.hasSps
|
|
5621
|
+
&& h264.hasPps
|
|
5622
|
+
&& h264.hasIdr
|
|
5623
|
+
&& h264.nalOrderValid;
|
|
5624
|
+
if (!proofPayloadValid) {
|
|
5625
|
+
device.counters.liveFramesDropped += 1;
|
|
5626
|
+
emitRemoteEvent('RemoteFrameDropped', device, {
|
|
5627
|
+
reason: proof.error || 'frame-proof-h264-invalid',
|
|
5628
|
+
streamId: proof.streamId,
|
|
5629
|
+
commandId: proof.commandId,
|
|
5630
|
+
captureGeneration: proof.captureGeneration,
|
|
5631
|
+
monitorIndex: proof.monitorIndex,
|
|
5632
|
+
transport
|
|
5633
|
+
});
|
|
5634
|
+
return {
|
|
5635
|
+
accepted: false,
|
|
5636
|
+
proofAccepted: false,
|
|
5637
|
+
proof,
|
|
5638
|
+
error: proof.error || 'frame-proof-h264-invalid'
|
|
5639
|
+
};
|
|
5640
|
+
}
|
|
5641
|
+
if (proofOnly) {
|
|
5642
|
+
const streamState = getDeviceLiveStream(device, proof.streamId);
|
|
5643
|
+
// A proof-only frame is an out-of-band path challenge. It must
|
|
5644
|
+
// prove the exact current stream owner and carry a complete
|
|
5645
|
+
// independently decodable H.264 key frame, but it must not race
|
|
5646
|
+
// the presentation lane's latest delta frame. WAN fragmentation
|
|
5647
|
+
// can legitimately deliver this challenge several relay frames
|
|
5648
|
+
// after the same source key was presented.
|
|
5649
|
+
const exactActiveStreamOwner = transport === 'udp-p2p'
|
|
5650
|
+
&& streamState?.active === true
|
|
5651
|
+
&& safeString(streamState.commandId, 128) === proof.commandId
|
|
5652
|
+
&& Number(streamState.captureGeneration || 0) === proof.captureGeneration
|
|
5653
|
+
&& parseExactLiveStreamMonitorIndex(streamState.monitorIndex) === proof.monitorIndex;
|
|
5654
|
+
if (exactActiveStreamOwner) {
|
|
5655
|
+
emitRemoteEvent('RemoteFrameTransportProofAccepted', device, {
|
|
5656
|
+
proofOnly: true,
|
|
5657
|
+
proofId: proof.proofId,
|
|
5658
|
+
streamId: proof.streamId,
|
|
5659
|
+
commandId: proof.commandId,
|
|
5660
|
+
captureGeneration: proof.captureGeneration,
|
|
5661
|
+
monitorIndex: proof.monitorIndex,
|
|
5662
|
+
frameSeq: proofFrameSeq,
|
|
5663
|
+
streamFrameSeq: proofStreamFrameSeq,
|
|
5664
|
+
transport
|
|
5665
|
+
});
|
|
5666
|
+
}
|
|
5667
|
+
return {
|
|
5668
|
+
accepted: exactActiveStreamOwner,
|
|
5669
|
+
proofAccepted: exactActiveStreamOwner,
|
|
5670
|
+
proofOnly: true,
|
|
5671
|
+
proof,
|
|
5672
|
+
error: exactActiveStreamOwner
|
|
5673
|
+
? ''
|
|
5674
|
+
: 'frame-proof-active-owner-mismatch'
|
|
5675
|
+
};
|
|
5676
|
+
}
|
|
5677
|
+
const accepted = applyLiveFrame(device, message, framePayload, transport);
|
|
5678
|
+
const frame = device.latestLiveFrame;
|
|
5679
|
+
const exactAcceptedFrame = accepted
|
|
5680
|
+
&& frame?.currentGenerationVerified === true
|
|
5681
|
+
&& safeString(frame.streamId, 128) === proof.streamId
|
|
5682
|
+
&& safeString(frame.commandId, 128) === proof.commandId
|
|
5683
|
+
&& Number(frame.captureGeneration) === proof.captureGeneration
|
|
5684
|
+
&& parseExactLiveStreamMonitorIndex(frame.monitorIndex) === proof.monitorIndex;
|
|
5685
|
+
if (exactAcceptedFrame) {
|
|
5686
|
+
emitRemoteEvent('RemoteFrameTransportProofAccepted', device, {
|
|
5687
|
+
proofOnly: false,
|
|
5688
|
+
proofId: proof.proofId,
|
|
5689
|
+
streamId: proof.streamId,
|
|
5690
|
+
commandId: proof.commandId,
|
|
5691
|
+
captureGeneration: proof.captureGeneration,
|
|
5692
|
+
monitorIndex: proof.monitorIndex,
|
|
5693
|
+
frameSeq: proofFrameSeq,
|
|
5694
|
+
streamFrameSeq: proofStreamFrameSeq,
|
|
5695
|
+
transport
|
|
5696
|
+
});
|
|
5697
|
+
}
|
|
5698
|
+
return {
|
|
5699
|
+
accepted: exactAcceptedFrame,
|
|
5700
|
+
proofAccepted: exactAcceptedFrame,
|
|
5701
|
+
proof,
|
|
5702
|
+
error: exactAcceptedFrame ? '' : 'frame-proof-current-generation-rejected'
|
|
5703
|
+
};
|
|
5704
|
+
}
|
|
5705
|
+
|
|
5706
|
+
function writeFrameTransportProofResponse(socket, result) {
|
|
5707
|
+
const proof = result?.proof;
|
|
5708
|
+
if (!proof?.proofId) return;
|
|
5709
|
+
const accepted = result.proofAccepted === true;
|
|
5710
|
+
writeJsonLine(socket, {
|
|
5711
|
+
type: accepted
|
|
5712
|
+
? 'frame.transport.proof-accepted'
|
|
5713
|
+
: 'frame.transport.proof-rejected',
|
|
5714
|
+
protocol: 'livedesk.frame-transport-proof.v1',
|
|
5715
|
+
proofId: proof.proofId,
|
|
5716
|
+
streamId: proof.streamId,
|
|
5717
|
+
commandId: proof.commandId,
|
|
5718
|
+
captureGeneration: proof.captureGeneration,
|
|
5719
|
+
monitorIndex: proof.monitorIndex,
|
|
5720
|
+
...(accepted ? {} : { error: safeString(result.error || 'frame-proof-rejected', 160) })
|
|
5721
|
+
});
|
|
5722
|
+
}
|
|
5723
|
+
|
|
4643
5724
|
function handleAgentBinaryFrame(socket, state, header, framePayload) {
|
|
4644
5725
|
if (!state.authenticated || !state.device) {
|
|
4645
5726
|
writeJsonLine(socket, { type: 'error', error: 'hello-required' });
|
|
@@ -4675,10 +5756,12 @@ export function createRemoteHub(options = {}) {
|
|
|
4675
5756
|
}
|
|
4676
5757
|
|
|
4677
5758
|
if (frameKind === 'stream' || frameKind === 'live' || header.type === 'stream.binary') {
|
|
4678
|
-
|
|
5759
|
+
const result = applyLiveFrameWithTransportProof(device, {
|
|
4679
5760
|
...header,
|
|
4680
|
-
hubIngressDropped: Number(state.binaryQueueDrops || 0)
|
|
5761
|
+
hubIngressDropped: Number(state.binaryQueueDrops || 0),
|
|
5762
|
+
hubIngressCounterEpoch: state.binaryQueueEpoch
|
|
4681
5763
|
}, framePayload, binaryTransport);
|
|
5764
|
+
writeFrameTransportProofResponse(socket, result);
|
|
4682
5765
|
return;
|
|
4683
5766
|
}
|
|
4684
5767
|
|
|
@@ -4698,7 +5781,7 @@ export function createRemoteHub(options = {}) {
|
|
|
4698
5781
|
});
|
|
4699
5782
|
}
|
|
4700
5783
|
|
|
4701
|
-
function handleUdpFrame({ deviceId, sessionId, header, payload } = {}) {
|
|
5784
|
+
function handleUdpFrame({ deviceId, sessionId, header, payload } = {}) {
|
|
4702
5785
|
const device = devices.get(safeString(deviceId, 160));
|
|
4703
5786
|
if (!device || !device.connected) {
|
|
4704
5787
|
return false;
|
|
@@ -4711,18 +5794,25 @@ export function createRemoteHub(options = {}) {
|
|
|
4711
5794
|
emitRemoteEvent('RemoteFrameDropped', device, { reason: 'udp-frame-mode-not-supported', transport: 'udp-p2p', frameMode });
|
|
4712
5795
|
return false;
|
|
4713
5796
|
}
|
|
4714
|
-
|
|
4715
|
-
...
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
5797
|
+
const result = applyLiveFrameWithTransportProof(device, {
|
|
5798
|
+
...header,
|
|
5799
|
+
transport: 'udp-p2p',
|
|
5800
|
+
hubIngressCounterEpoch: sessionId
|
|
5801
|
+
}, payload, 'udp-p2p');
|
|
5802
|
+
if (result.accepted) {
|
|
5803
|
+
device.udp = {
|
|
5804
|
+
...device.udp,
|
|
5805
|
+
state: result.proof ? 'udp-proof-accepted' : 'udp-active',
|
|
5806
|
+
ready: true,
|
|
5807
|
+
lastFrameAt: new Date().toISOString(),
|
|
5808
|
+
framesReceived: Number(device.udp?.framesReceived || 0)
|
|
5809
|
+
+ (result.proofOnly === true ? 0 : 1)
|
|
5810
|
+
};
|
|
5811
|
+
}
|
|
5812
|
+
return result.proof
|
|
5813
|
+
? result
|
|
5814
|
+
: result.accepted;
|
|
5815
|
+
}
|
|
4726
5816
|
|
|
4727
5817
|
function dropQueuedAgentBinaryFrame(state) {
|
|
4728
5818
|
if (!Array.isArray(state.binaryQueue) || state.binaryQueue.length === 0) {
|
|
@@ -5051,9 +6141,10 @@ export function createRemoteHub(options = {}) {
|
|
|
5051
6141
|
binaryQueue: [],
|
|
5052
6142
|
binaryQueueDraining: false,
|
|
5053
6143
|
binaryQueueScheduled: false,
|
|
5054
|
-
binaryQueueSocket: null,
|
|
5055
|
-
binaryQueueDrops: 0,
|
|
5056
|
-
|
|
6144
|
+
binaryQueueSocket: null,
|
|
6145
|
+
binaryQueueDrops: 0,
|
|
6146
|
+
binaryQueueEpoch: crypto.randomUUID(),
|
|
6147
|
+
closed: false
|
|
5057
6148
|
};
|
|
5058
6149
|
|
|
5059
6150
|
const helloTimer = setTimeout(() => {
|
|
@@ -5211,9 +6302,10 @@ export function createRemoteHub(options = {}) {
|
|
|
5211
6302
|
binaryQueue: [],
|
|
5212
6303
|
binaryQueueDraining: false,
|
|
5213
6304
|
binaryQueueScheduled: false,
|
|
5214
|
-
binaryQueueSocket: null,
|
|
5215
|
-
binaryQueueDrops: 0,
|
|
5216
|
-
|
|
6305
|
+
binaryQueueSocket: null,
|
|
6306
|
+
binaryQueueDrops: 0,
|
|
6307
|
+
binaryQueueEpoch: crypto.randomUUID(),
|
|
6308
|
+
closed: false
|
|
5217
6309
|
};
|
|
5218
6310
|
|
|
5219
6311
|
const helloTimer = setTimeout(() => {
|
|
@@ -5437,8 +6529,9 @@ export function createRemoteHub(options = {}) {
|
|
|
5437
6529
|
return getStatus({ includeSecrets: false });
|
|
5438
6530
|
}
|
|
5439
6531
|
|
|
5440
|
-
async function close() {
|
|
6532
|
+
async function close() {
|
|
5441
6533
|
for (const device of devices.values()) {
|
|
6534
|
+
abortTransportDiagnosticsForDevice(device, 'hub-shutdown');
|
|
5442
6535
|
failAllPendingTasks(device, 'hub-shutdown');
|
|
5443
6536
|
failAllPendingInputFallbacks(device, 'hub-shutdown');
|
|
5444
6537
|
failPendingCommandResultWaiters(device, 'hub-shutdown');
|
|
@@ -5490,10 +6583,11 @@ export function createRemoteHub(options = {}) {
|
|
|
5490
6583
|
|
|
5491
6584
|
function disconnectDevice(deviceId, reason = 'manager-disconnect') {
|
|
5492
6585
|
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;
|
|
6586
|
+
if (device?.synthetic === true) {
|
|
6587
|
+
device.connected = false;
|
|
6588
|
+
device.disconnectedAt = new Date().toISOString();
|
|
6589
|
+
device.lastDisconnectReason = reason;
|
|
6590
|
+
abortTransportDiagnosticsForDevice(device, reason);
|
|
5497
6591
|
deactivateDeviceLiveStreams(device, reason, device.disconnectedAt);
|
|
5498
6592
|
failAllPendingTasks(device, 'device-disconnected');
|
|
5499
6593
|
failAllPendingInputFallbacks(device, 'device-disconnected');
|
|
@@ -7547,6 +8641,10 @@ export function createRemoteHub(options = {}) {
|
|
|
7547
8641
|
stopLiveStream,
|
|
7548
8642
|
pauseLiveCapture,
|
|
7549
8643
|
resumeLiveCapture,
|
|
8644
|
+
startTransportDiagnostic,
|
|
8645
|
+
getTransportDiagnostic,
|
|
8646
|
+
setTransportDiagnosticPhase,
|
|
8647
|
+
stopTransportDiagnostic,
|
|
7550
8648
|
startAudioStream,
|
|
7551
8649
|
stopAudioStream,
|
|
7552
8650
|
getDeviceLiveFrame,
|