livedesk 0.1.445 → 0.1.446
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 +86 -6
- package/bootstrap/client-process-identity.js +20 -6
- package/bootstrap/runtime-lock.js +145 -33
- package/client/bin/livedesk-client.js +295 -100
- package/client/package.json +5 -5
- package/client/src/runtime/agent-process-lifecycle.js +81 -18
- package/hub/package.json +1 -1
- package/hub/src/remote-hub.js +284 -88
- package/hub/src/server.js +173 -28
- package/hub/src/transport/relay-hub-control.js +1004 -0
- package/hub/src/transport/udp-rendezvous.js +2 -0
- package/package.json +6 -6
- package/web/dist/assets/index-5opXQY8X.js +25 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/index-D3VhnrP6.js +0 -25
|
@@ -384,6 +384,55 @@ export async function drainOwnedWindowsAgentTree(child, {
|
|
|
384
384
|
return child.livedeskWindowsDrainPromise;
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Fail-closed wrapper for lifecycle replacement and terminal shutdown.
|
|
389
|
+
* A bounded drain attempt can fail because CIM/taskkill is temporarily busy;
|
|
390
|
+
* that must never let the launcher exit and orphan an owned FFmpeg descendant.
|
|
391
|
+
*/
|
|
392
|
+
export async function drainOwnedWindowsAgentTreeUntilStopped(child, {
|
|
393
|
+
platform = process.platform,
|
|
394
|
+
retryForeverMs = 1000,
|
|
395
|
+
waitImpl = waitMilliseconds,
|
|
396
|
+
reportTerminationFailure = message => console.error(message),
|
|
397
|
+
...drainOptions
|
|
398
|
+
} = {}) {
|
|
399
|
+
if (platform !== 'win32') return { ok: true, remaining: [] };
|
|
400
|
+
if (child?.livedeskWindowsDrainUntilStoppedPromise) {
|
|
401
|
+
return child.livedeskWindowsDrainUntilStoppedPromise;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
child.livedeskWindowsDrainUntilStoppedPromise = (async () => {
|
|
405
|
+
let attempt = 0;
|
|
406
|
+
while (true) {
|
|
407
|
+
attempt += 1;
|
|
408
|
+
// drainOwnedWindowsAgentTree memoizes one bounded proof attempt.
|
|
409
|
+
// Clear only that completed attempt; the immutable tracker remains
|
|
410
|
+
// attached to the exact child and is refreshed on every retry.
|
|
411
|
+
child.livedeskWindowsDrainPromise = null;
|
|
412
|
+
let result;
|
|
413
|
+
try {
|
|
414
|
+
result = await drainOwnedWindowsAgentTree(child, {
|
|
415
|
+
platform,
|
|
416
|
+
waitImpl,
|
|
417
|
+
reportTerminationFailure,
|
|
418
|
+
...drainOptions
|
|
419
|
+
});
|
|
420
|
+
} catch (error) {
|
|
421
|
+
result = { ok: false, error, remaining: [] };
|
|
422
|
+
}
|
|
423
|
+
if (result?.ok !== false) {
|
|
424
|
+
return result;
|
|
425
|
+
}
|
|
426
|
+
reportTerminationFailure(
|
|
427
|
+
`[LiveDesk Client] Exact Windows Agent tree is not drained yet `
|
|
428
|
+
+ `(attempt ${attempt}); the launcher will remain alive and retry.`
|
|
429
|
+
);
|
|
430
|
+
await waitImpl(Math.max(100, Number(retryForeverMs) || 1000));
|
|
431
|
+
}
|
|
432
|
+
})();
|
|
433
|
+
return child.livedeskWindowsDrainUntilStoppedPromise;
|
|
434
|
+
}
|
|
435
|
+
|
|
387
436
|
export function isOwnedUnixProcessGroupAlive(child, platform = process.platform, signalProcess = (pid, signal) => process.kill(pid, signal)) {
|
|
388
437
|
const processId = Number(child?.pid || 0);
|
|
389
438
|
if (platform === 'win32'
|
|
@@ -513,7 +562,7 @@ export function installAgentTerminationHandlers({
|
|
|
513
562
|
shutdownTimeoutMs = 8000,
|
|
514
563
|
platform = process.platform,
|
|
515
564
|
signalProcess = (pid, signal) => process.kill(pid, signal),
|
|
516
|
-
drainWindowsTree =
|
|
565
|
+
drainWindowsTree = drainOwnedWindowsAgentTreeUntilStopped,
|
|
517
566
|
windowsTerminateAttemptLimit = 3,
|
|
518
567
|
windowsTerminateRetryMs = 100,
|
|
519
568
|
unixTerminateRetryMs = 1000,
|
|
@@ -553,12 +602,14 @@ export function installAgentTerminationHandlers({
|
|
|
553
602
|
let poll = null;
|
|
554
603
|
let timeout = null;
|
|
555
604
|
let unixRetryTimer = null;
|
|
605
|
+
let windowsRetryTimer = null;
|
|
556
606
|
const finish = () => {
|
|
557
607
|
if (finished) return;
|
|
558
608
|
finished = true;
|
|
559
609
|
if (poll) clearInterval(poll);
|
|
560
610
|
if (timeout) clearTimeout(timeout);
|
|
561
611
|
if (unixRetryTimer) clearTimeout(unixRetryTimer);
|
|
612
|
+
if (windowsRetryTimer) clearTimeout(windowsRetryTimer);
|
|
562
613
|
exitProcess(exitCode);
|
|
563
614
|
};
|
|
564
615
|
const processId = Number(child?.pid || 0);
|
|
@@ -571,25 +622,37 @@ export function installAgentTerminationHandlers({
|
|
|
571
622
|
// restarts and unexpected Agent exits. The promise is stored
|
|
572
623
|
// on the child so spawnAgent cannot race ahead and launch a
|
|
573
624
|
// replacement while terminal shutdown is still draining.
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
625
|
+
const drainUntilStopped = () => {
|
|
626
|
+
if (finished) return;
|
|
627
|
+
if (!child.livedeskTreeStopPromise) {
|
|
628
|
+
child.livedeskTreeStopPromise = Promise.resolve().then(() => drainWindowsTree(child, {
|
|
629
|
+
platform,
|
|
630
|
+
maxAttempts: windowsTerminateAttemptLimit,
|
|
631
|
+
retryMs: windowsTerminateRetryMs,
|
|
632
|
+
retryForeverMs: windowsTerminateRetryMs,
|
|
633
|
+
reportTerminationFailure
|
|
634
|
+
}));
|
|
635
|
+
}
|
|
636
|
+
child.livedeskTreeStopPromise
|
|
637
|
+
.catch(error => ({ ok: false, error }))
|
|
638
|
+
.then(result => {
|
|
639
|
+
if (result?.ok !== false) {
|
|
640
|
+
finish();
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
586
643
|
reportTerminationFailure(
|
|
587
|
-
`[LiveDesk Client] Terminal shutdown
|
|
588
|
-
+ `${result.error?.message || 'bounded drain failed'}
|
|
644
|
+
`[LiveDesk Client] Terminal shutdown retained an exact Windows Agent tree: `
|
|
645
|
+
+ `${result.error?.message || 'bounded drain failed'}. Retrying before exit.`
|
|
646
|
+
);
|
|
647
|
+
child.livedeskTreeStopPromise = null;
|
|
648
|
+
child.livedeskWindowsDrainPromise = null;
|
|
649
|
+
windowsRetryTimer = setTimeout(
|
|
650
|
+
drainUntilStopped,
|
|
651
|
+
Math.max(100, Number(windowsTerminateRetryMs) || 100)
|
|
589
652
|
);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
|
|
653
|
+
});
|
|
654
|
+
};
|
|
655
|
+
drainUntilStopped();
|
|
593
656
|
return;
|
|
594
657
|
}
|
|
595
658
|
const finishWhenTreeStops = () => {
|
package/hub/package.json
CHANGED
package/hub/src/remote-hub.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import net from 'net';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import crypto from 'crypto';
|
|
1
|
+
import net from 'net';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { createHubRelayControl } from './transport/relay-hub-control.js';
|
|
4
5
|
|
|
5
6
|
const DEFAULT_REMOTE_HUB_PORT = 5197;
|
|
6
7
|
const DEFAULT_REMOTE_HUB_HOST = '0.0.0.0';
|
|
@@ -1026,10 +1027,12 @@ function serializeDevice(device, options = {}) {
|
|
|
1026
1027
|
disconnectedAt: device.disconnectedAt,
|
|
1027
1028
|
lastSeenAt: device.lastSeenAt,
|
|
1028
1029
|
lastStatusAt: device.lastStatusAt,
|
|
1029
|
-
lastDisconnectReason: device.lastDisconnectReason,
|
|
1030
|
-
remoteAddress: device.remoteAddress,
|
|
1031
|
-
remotePort: device.remotePort,
|
|
1032
|
-
|
|
1030
|
+
lastDisconnectReason: device.lastDisconnectReason,
|
|
1031
|
+
remoteAddress: device.remoteAddress,
|
|
1032
|
+
remotePort: device.remotePort,
|
|
1033
|
+
controlTransport: device.controlTransport || 'direct-tcp',
|
|
1034
|
+
relayEndpoint: device.relayEndpoint || '',
|
|
1035
|
+
channels: {
|
|
1033
1036
|
control: !!device.socket && !device.socket.destroyed,
|
|
1034
1037
|
input: !!device.inputSocket && !device.inputSocket.destroyed,
|
|
1035
1038
|
frame: !!device.frameSocket && !device.frameSocket.destroyed,
|
|
@@ -1560,9 +1563,18 @@ export function createRemoteHub(options = {}) {
|
|
|
1560
1563
|
const hostInstanceId = safeString(options.hostInstanceId ?? env.LIVEDESK_HUB_INSTANCE_ID ?? env.MINDEXEC_BRIDGE_INSTANCE_ID ?? crypto.randomUUID(), 128) || crypto.randomUUID();
|
|
1561
1564
|
const publicEndpoint = safeString(env.LIVEDESK_REMOTE_PUBLIC_ENDPOINT || env.MINDEXEC_REMOTE_PUBLIC_ENDPOINT || env.REMOTE_HUB_PUBLIC_ENDPOINT, 256);
|
|
1562
1565
|
const publicHost = safeString(env.LIVEDESK_REMOTE_PUBLIC_HOST || env.MINDEXEC_REMOTE_PUBLIC_HOST || env.REMOTE_HUB_PUBLIC_HOST, 128);
|
|
1563
|
-
const pairToken = safeString(
|
|
1564
|
-
options.pairToken || env.REMOTE_HUB_PAIR_TOKEN || env.LIVEDESK_CLIENT_PAIR_TOKEN || env.MINDEXEC_REMOTE_PAIR_TOKEN || crypto.randomBytes(6).toString('hex'),
|
|
1565
|
-
256);
|
|
1566
|
+
const pairToken = safeString(
|
|
1567
|
+
options.pairToken || env.REMOTE_HUB_PAIR_TOKEN || env.LIVEDESK_CLIENT_PAIR_TOKEN || env.MINDEXEC_REMOTE_PAIR_TOKEN || crypto.randomBytes(6).toString('hex'),
|
|
1568
|
+
256);
|
|
1569
|
+
const relayControl = options.relayControl && typeof options.relayControl === 'object'
|
|
1570
|
+
? options.relayControl
|
|
1571
|
+
: createHubRelayControl({
|
|
1572
|
+
env,
|
|
1573
|
+
pairToken,
|
|
1574
|
+
logEvent,
|
|
1575
|
+
logWarn,
|
|
1576
|
+
onPeerSocket: socket => handleTcpAgentSocket(socket)
|
|
1577
|
+
});
|
|
1566
1578
|
let pairingPin = /^\d{6}$/.test(String(options.pairingPin || env.LIVEDESK_PAIRING_PIN || '').trim())
|
|
1567
1579
|
? String(options.pairingPin || env.LIVEDESK_PAIRING_PIN).trim()
|
|
1568
1580
|
: generatePairingPin();
|
|
@@ -1933,14 +1945,23 @@ export function createRemoteHub(options = {}) {
|
|
|
1933
1945
|
publicIPv4Address: publicIPv4Cache.address,
|
|
1934
1946
|
publicIPv4UpdatedAt: publicIPv4Cache.updatedAt ? new Date(publicIPv4Cache.updatedAt).toISOString() : '',
|
|
1935
1947
|
publicIPv4Error: publicIPv4Cache.error,
|
|
1936
|
-
udp: udpTransport?.getStatus?.() || {
|
|
1937
|
-
enabled: false,
|
|
1938
|
-
preferred: false,
|
|
1939
|
-
started: false,
|
|
1940
|
-
port: 0,
|
|
1941
|
-
sessionCount: 0,
|
|
1942
|
-
readySessionCount: 0
|
|
1943
|
-
},
|
|
1948
|
+
udp: udpTransport?.getStatus?.() || {
|
|
1949
|
+
enabled: false,
|
|
1950
|
+
preferred: false,
|
|
1951
|
+
started: false,
|
|
1952
|
+
port: 0,
|
|
1953
|
+
sessionCount: 0,
|
|
1954
|
+
readySessionCount: 0
|
|
1955
|
+
},
|
|
1956
|
+
relay: relayControl?.getStatus?.() || {
|
|
1957
|
+
enabled: false,
|
|
1958
|
+
started: false,
|
|
1959
|
+
connected: false,
|
|
1960
|
+
state: 'unavailable',
|
|
1961
|
+
protocol: 'livedesk.relay.v1',
|
|
1962
|
+
endpoint: '',
|
|
1963
|
+
peerCount: 0
|
|
1964
|
+
},
|
|
1944
1965
|
externalExposure: isWildcardHost(host) || !isLoopbackHost(host),
|
|
1945
1966
|
diagnostics,
|
|
1946
1967
|
lastError
|
|
@@ -2151,6 +2172,23 @@ export function createRemoteHub(options = {}) {
|
|
|
2151
2172
|
}
|
|
2152
2173
|
}
|
|
2153
2174
|
|
|
2175
|
+
function failPendingCommandResultWaiter(commandId, error = 'command-not-sent') {
|
|
2176
|
+
const key = safeString(commandId, 128);
|
|
2177
|
+
const waiter = pendingCommandResultWaiters.get(key);
|
|
2178
|
+
if (!waiter) {
|
|
2179
|
+
return false;
|
|
2180
|
+
}
|
|
2181
|
+
pendingCommandResultWaiters.delete(key);
|
|
2182
|
+
clearTimeout(waiter.timer);
|
|
2183
|
+
waiter.resolve({
|
|
2184
|
+
ok: false,
|
|
2185
|
+
commandId: key,
|
|
2186
|
+
result: null,
|
|
2187
|
+
error: safeString(error, 500) || 'command-not-sent'
|
|
2188
|
+
});
|
|
2189
|
+
return true;
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2154
2192
|
function waitForCommandResult(device, commandId, timeoutMs = liveCaptureStopAckTimeoutMs) {
|
|
2155
2193
|
const key = safeString(commandId, 128);
|
|
2156
2194
|
if (!device?.deviceId || !device?.sessionId || !key) {
|
|
@@ -2845,9 +2883,13 @@ export function createRemoteHub(options = {}) {
|
|
|
2845
2883
|
const frameModes = Array.isArray(hello.frameModes)
|
|
2846
2884
|
? hello.frameModes.map(normalizeIncomingFrameModeProfile).filter(Boolean)
|
|
2847
2885
|
: [];
|
|
2848
|
-
const capabilities = typeof hello.capabilities === 'object' && hello.capabilities
|
|
2849
|
-
? { ...hello.capabilities }
|
|
2850
|
-
: {};
|
|
2886
|
+
const capabilities = typeof hello.capabilities === 'object' && hello.capabilities
|
|
2887
|
+
? { ...hello.capabilities }
|
|
2888
|
+
: {};
|
|
2889
|
+
const relayConnected = socket.__liveDeskRelayControl === true;
|
|
2890
|
+
if (relayConnected) {
|
|
2891
|
+
capabilities.dedicatedInputChannel = false;
|
|
2892
|
+
}
|
|
2851
2893
|
if (!capabilities.frameProtocol) {
|
|
2852
2894
|
capabilities.frameProtocol = frameProtocol || buildRemoteFrameProtocolDescriptor();
|
|
2853
2895
|
}
|
|
@@ -2893,10 +2935,14 @@ export function createRemoteHub(options = {}) {
|
|
|
2893
2935
|
disconnectedAt: '',
|
|
2894
2936
|
lastSeenAt: now,
|
|
2895
2937
|
lastStatusAt: '',
|
|
2896
|
-
lastDisconnectReason: '',
|
|
2897
|
-
remoteAddress: socket.remoteAddress || '',
|
|
2898
|
-
remotePort: socket.remotePort || 0,
|
|
2899
|
-
|
|
2938
|
+
lastDisconnectReason: '',
|
|
2939
|
+
remoteAddress: socket.remoteAddress || '',
|
|
2940
|
+
remotePort: socket.remotePort || 0,
|
|
2941
|
+
controlTransport: relayConnected ? 'encrypted-rendezvous-relay' : 'direct-tcp',
|
|
2942
|
+
relayEndpoint: relayConnected
|
|
2943
|
+
? safeString(socket.remoteAddress, 256)
|
|
2944
|
+
: '',
|
|
2945
|
+
status: {},
|
|
2900
2946
|
latestThumbnail: null,
|
|
2901
2947
|
latestLiveFrame: null,
|
|
2902
2948
|
recentFramePayloads: {
|
|
@@ -4654,12 +4700,23 @@ export function createRemoteHub(options = {}) {
|
|
|
4654
4700
|
scheduleAgentBinaryIngress(socket, state);
|
|
4655
4701
|
}
|
|
4656
4702
|
|
|
4657
|
-
function handleAgentMessage(socket, state, message) {
|
|
4658
|
-
if (!message || typeof message !== 'object') {
|
|
4659
|
-
return;
|
|
4660
|
-
}
|
|
4661
|
-
|
|
4703
|
+
function handleAgentMessage(socket, state, message) {
|
|
4704
|
+
if (!message || typeof message !== 'object') {
|
|
4705
|
+
return;
|
|
4706
|
+
}
|
|
4707
|
+
|
|
4662
4708
|
if (!state.authenticated) {
|
|
4709
|
+
if (message.type === 'hello' && socket.__liveDeskRelayControl === true) {
|
|
4710
|
+
message = {
|
|
4711
|
+
...message,
|
|
4712
|
+
capabilities: {
|
|
4713
|
+
...(message.capabilities && typeof message.capabilities === 'object'
|
|
4714
|
+
? message.capabilities
|
|
4715
|
+
: {}),
|
|
4716
|
+
dedicatedInputChannel: false
|
|
4717
|
+
}
|
|
4718
|
+
};
|
|
4719
|
+
}
|
|
4663
4720
|
if (message.type === 'slot.assign') {
|
|
4664
4721
|
if (!timingSafeStringEqual(message.pairToken, pairToken)) {
|
|
4665
4722
|
writeJsonLine(socket, { type: 'slot.assignment', ok: false, error: 'invalid-pair-token' });
|
|
@@ -5230,10 +5287,15 @@ export function createRemoteHub(options = {}) {
|
|
|
5230
5287
|
return getStatus({ includeSecrets: false });
|
|
5231
5288
|
}
|
|
5232
5289
|
|
|
5233
|
-
try {
|
|
5234
|
-
await udpTransport?.start?.();
|
|
5235
|
-
await listenOnPort(requestedPort);
|
|
5236
|
-
|
|
5290
|
+
try {
|
|
5291
|
+
await udpTransport?.start?.();
|
|
5292
|
+
await listenOnPort(requestedPort);
|
|
5293
|
+
try {
|
|
5294
|
+
await relayControl?.start?.();
|
|
5295
|
+
} catch {
|
|
5296
|
+
logWarn('relay', 'Encrypted relay control could not start; direct TCP remains available.');
|
|
5297
|
+
}
|
|
5298
|
+
} catch (error) {
|
|
5237
5299
|
if (error?.code === 'EADDRINUSE') {
|
|
5238
5300
|
const conflict = new Error(
|
|
5239
5301
|
`LiveDesk Hub requires client endpoint TCP ${requestedPort}, but that port is already in use. Close the owning process or start Hub with an explicit --remote-port value.`);
|
|
@@ -5266,8 +5328,9 @@ export function createRemoteHub(options = {}) {
|
|
|
5266
5328
|
}
|
|
5267
5329
|
}
|
|
5268
5330
|
|
|
5269
|
-
sockets.clear();
|
|
5270
|
-
await
|
|
5331
|
+
sockets.clear();
|
|
5332
|
+
await relayControl?.close?.();
|
|
5333
|
+
await udpTransport?.close?.();
|
|
5271
5334
|
if (!server) {
|
|
5272
5335
|
started = false;
|
|
5273
5336
|
return;
|
|
@@ -6790,35 +6853,100 @@ export function createRemoteHub(options = {}) {
|
|
|
6790
6853
|
|
|
6791
6854
|
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
6792
6855
|
return { ok: false, error: 'device-not-connected' };
|
|
6793
|
-
}
|
|
6794
|
-
|
|
6795
|
-
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6856
|
+
}
|
|
6857
|
+
|
|
6858
|
+
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
6859
|
+
// Register before writing the command. A local/relay Agent can answer
|
|
6860
|
+
// quickly enough that registering after sendCommand loses the only
|
|
6861
|
+
// proof that native capture actually stopped.
|
|
6862
|
+
const acknowledgementPromise = waitForCommandResult(
|
|
6863
|
+
device,
|
|
6864
|
+
commandId,
|
|
6865
|
+
liveCaptureStopAckTimeoutMs);
|
|
6866
|
+
const result = sendCommand(deviceId, {
|
|
6867
|
+
command: 'stream.stop',
|
|
6868
|
+
commandId,
|
|
6799
6869
|
payload: {
|
|
6800
6870
|
streamId,
|
|
6801
6871
|
requestedAt: new Date().toISOString()
|
|
6802
6872
|
}
|
|
6803
6873
|
});
|
|
6804
|
-
|
|
6805
|
-
if (!result.ok) {
|
|
6806
|
-
return result;
|
|
6807
|
-
}
|
|
6808
6874
|
|
|
6809
|
-
if (
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
streamState.stoppedAt = new Date().toISOString();
|
|
6813
|
-
streamState.stopReason = safeString(options.reason, 128) || 'manager-request';
|
|
6875
|
+
if (!result.ok) {
|
|
6876
|
+
failPendingCommandResultWaiter(commandId, result.error || 'stream-stop-not-sent');
|
|
6877
|
+
return result;
|
|
6814
6878
|
}
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
|
|
6879
|
+
|
|
6880
|
+
const targetStreams = options.stopAll === true
|
|
6881
|
+
? [...ensureDeviceLiveStreams(device).values()]
|
|
6882
|
+
: (streamState ? [streamState] : []);
|
|
6883
|
+
for (const targetStream of targetStreams) {
|
|
6884
|
+
targetStream.stopPending = true;
|
|
6885
|
+
targetStream.stopCommandId = commandId;
|
|
6886
|
+
targetStream.stopRequestedAt = new Date().toISOString();
|
|
6887
|
+
targetStream.stopReason = safeString(options.reason, 128) || 'manager-request';
|
|
6888
|
+
targetStream.stopError = '';
|
|
6889
|
+
targetStream.stopUnconfirmed = false;
|
|
6890
|
+
}
|
|
6891
|
+
const sessionId = device.sessionId;
|
|
6892
|
+
const stopPromise = acknowledgementPromise.then(acknowledgement => {
|
|
6893
|
+
const currentDevice = devices.get(String(deviceId || ''));
|
|
6894
|
+
const sessionCurrent = currentDevice?.sessionId === sessionId;
|
|
6895
|
+
const captureStopConfirmed = acknowledgement.ok === true
|
|
6896
|
+
&& acknowledgement.result?.stream === true
|
|
6897
|
+
&& acknowledgement.result?.stopped === true;
|
|
6898
|
+
const stoppedAt = new Date().toISOString();
|
|
6899
|
+
if (sessionCurrent) {
|
|
6900
|
+
const currentTargets = options.stopAll === true
|
|
6901
|
+
? [...ensureDeviceLiveStreams(currentDevice).values()]
|
|
6902
|
+
: (() => {
|
|
6903
|
+
const currentStream = getDeviceLiveStream(currentDevice, streamId);
|
|
6904
|
+
return currentStream ? [currentStream] : [];
|
|
6905
|
+
})();
|
|
6906
|
+
for (const targetStream of currentTargets) {
|
|
6907
|
+
if (targetStream.stopCommandId !== commandId) {
|
|
6908
|
+
continue;
|
|
6909
|
+
}
|
|
6910
|
+
targetStream.stopPending = false;
|
|
6911
|
+
targetStream.stopUnconfirmed = !captureStopConfirmed;
|
|
6912
|
+
targetStream.stopError = captureStopConfirmed
|
|
6913
|
+
? ''
|
|
6914
|
+
: safeString(acknowledgement.error || 'capture-stop-unconfirmed', 500);
|
|
6915
|
+
if (captureStopConfirmed) {
|
|
6916
|
+
targetStream.active = false;
|
|
6917
|
+
targetStream.open = false;
|
|
6918
|
+
targetStream.stoppedAt = stoppedAt;
|
|
6919
|
+
targetStream.stopConfirmedAt = stoppedAt;
|
|
6920
|
+
}
|
|
6921
|
+
}
|
|
6922
|
+
if (captureStopConfirmed) {
|
|
6923
|
+
currentDevice.counters.liveStreamsStopped += 1;
|
|
6924
|
+
emitRemoteEvent('RemoteLiveStreamStopped', currentDevice, {
|
|
6925
|
+
streamId,
|
|
6926
|
+
commandId,
|
|
6927
|
+
captureStopConfirmed: true
|
|
6928
|
+
});
|
|
6929
|
+
} else {
|
|
6930
|
+
emitRemoteEvent('RemoteLiveStreamStopUnconfirmed', currentDevice, {
|
|
6931
|
+
streamId,
|
|
6932
|
+
commandId,
|
|
6933
|
+
error: acknowledgement.error || 'capture-stop-unconfirmed'
|
|
6934
|
+
});
|
|
6935
|
+
}
|
|
6936
|
+
}
|
|
6937
|
+
return {
|
|
6938
|
+
...acknowledgement,
|
|
6939
|
+
streamId,
|
|
6940
|
+
captureStopConfirmed
|
|
6941
|
+
};
|
|
6942
|
+
});
|
|
6943
|
+
|
|
6944
|
+
const stopResult = { ok: true, commandId, streamId, stopPending: true };
|
|
6945
|
+
Object.defineProperty(stopResult, 'stopPromise', {
|
|
6946
|
+
value: stopPromise,
|
|
6947
|
+
enumerable: false
|
|
6948
|
+
});
|
|
6949
|
+
return stopResult;
|
|
6822
6950
|
}
|
|
6823
6951
|
|
|
6824
6952
|
function buildLiveCapturePauseResult(pause, extra = {}) {
|
|
@@ -6884,10 +7012,14 @@ export function createRemoteHub(options = {}) {
|
|
|
6884
7012
|
if (pause.stopCommandId
|
|
6885
7013
|
&& stopResult.alreadyStopped !== true
|
|
6886
7014
|
&& stopResult.synthetic !== true) {
|
|
6887
|
-
const acknowledgement =
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
7015
|
+
const acknowledgement = stopResult.stopPromise
|
|
7016
|
+
? await stopResult.stopPromise
|
|
7017
|
+
: {
|
|
7018
|
+
ok: false,
|
|
7019
|
+
commandId: pause.stopCommandId,
|
|
7020
|
+
result: null,
|
|
7021
|
+
error: 'capture-stop-confirmation-missing'
|
|
7022
|
+
};
|
|
6891
7023
|
device = devices.get(String(deviceId || '')) || device;
|
|
6892
7024
|
if (device.liveCapturePause?.token !== pause.token) {
|
|
6893
7025
|
return buildLiveCapturePauseResult(pause, {
|
|
@@ -6999,6 +7131,14 @@ export function createRemoteHub(options = {}) {
|
|
|
6999
7131
|
if (!pause?.token) {
|
|
7000
7132
|
return { ok: true, paused: false, alreadyResumed: true };
|
|
7001
7133
|
}
|
|
7134
|
+
const pauseToken = safeString(options.pauseToken, 128);
|
|
7135
|
+
if (!pauseToken || pauseToken !== pause.token) {
|
|
7136
|
+
return {
|
|
7137
|
+
ok: false,
|
|
7138
|
+
paused: true,
|
|
7139
|
+
error: 'LIVE_CAPTURE_PAUSE_TOKEN_MISMATCH'
|
|
7140
|
+
};
|
|
7141
|
+
}
|
|
7002
7142
|
if (pause.phase === 'pausing') {
|
|
7003
7143
|
return {
|
|
7004
7144
|
ok: false,
|
|
@@ -7007,12 +7147,19 @@ export function createRemoteHub(options = {}) {
|
|
|
7007
7147
|
pauseToken: pause.token
|
|
7008
7148
|
};
|
|
7009
7149
|
}
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
return {
|
|
7150
|
+
if (pause.captureStopConfirmed !== true) {
|
|
7151
|
+
return buildLiveCapturePauseResult(pause, {
|
|
7013
7152
|
ok: false,
|
|
7014
|
-
|
|
7015
|
-
|
|
7153
|
+
paused: true,
|
|
7154
|
+
error: 'LIVE_CAPTURE_STOP_UNCONFIRMED'
|
|
7155
|
+
});
|
|
7156
|
+
}
|
|
7157
|
+
if (pause.phase !== 'paused') {
|
|
7158
|
+
return buildLiveCapturePauseResult(pause, {
|
|
7159
|
+
ok: false,
|
|
7160
|
+
paused: true,
|
|
7161
|
+
error: 'LIVE_CAPTURE_PAUSE_PHASE_MISMATCH'
|
|
7162
|
+
});
|
|
7016
7163
|
}
|
|
7017
7164
|
|
|
7018
7165
|
device.liveCapturePause = null;
|
|
@@ -7101,32 +7248,81 @@ export function createRemoteHub(options = {}) {
|
|
|
7101
7248
|
}
|
|
7102
7249
|
if (device.synthetic === true || !device.socket || device.socket.destroyed) {
|
|
7103
7250
|
return { ok: false, error: 'device-audio-unavailable' };
|
|
7104
|
-
}
|
|
7105
|
-
const streamId = safeString(options.streamId, 128) || device.activeAudioStream?.streamId || '';
|
|
7106
|
-
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
7107
|
-
const
|
|
7108
|
-
|
|
7251
|
+
}
|
|
7252
|
+
const streamId = safeString(options.streamId, 128) || device.activeAudioStream?.streamId || '';
|
|
7253
|
+
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
7254
|
+
const acknowledgementPromise = waitForCommandResult(
|
|
7255
|
+
device,
|
|
7256
|
+
commandId,
|
|
7257
|
+
liveCaptureStopAckTimeoutMs);
|
|
7258
|
+
const result = sendCommand(deviceId, {
|
|
7259
|
+
command: 'audio.stop',
|
|
7109
7260
|
commandId,
|
|
7110
7261
|
payload: {
|
|
7111
7262
|
streamId,
|
|
7112
7263
|
requestedAt: new Date().toISOString()
|
|
7113
7264
|
}
|
|
7114
|
-
});
|
|
7115
|
-
if (!result.ok) {
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
device.activeAudioStream.
|
|
7121
|
-
device.activeAudioStream.
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7265
|
+
});
|
|
7266
|
+
if (!result.ok) {
|
|
7267
|
+
failPendingCommandResultWaiter(commandId, result.error || 'audio-stop-not-sent');
|
|
7268
|
+
return result;
|
|
7269
|
+
}
|
|
7270
|
+
if (device.activeAudioStream && (!streamId || device.activeAudioStream.streamId === streamId)) {
|
|
7271
|
+
device.activeAudioStream.stopPending = true;
|
|
7272
|
+
device.activeAudioStream.stopCommandId = commandId;
|
|
7273
|
+
device.activeAudioStream.stopRequestedAt = new Date().toISOString();
|
|
7274
|
+
device.activeAudioStream.stopReason = 'manager-request';
|
|
7275
|
+
device.activeAudioStream.stopError = '';
|
|
7276
|
+
device.activeAudioStream.stopUnconfirmed = false;
|
|
7277
|
+
}
|
|
7278
|
+
const sessionId = device.sessionId;
|
|
7279
|
+
const stopPromise = acknowledgementPromise.then(acknowledgement => {
|
|
7280
|
+
const currentDevice = devices.get(String(deviceId || ''));
|
|
7281
|
+
const audioStopConfirmed = acknowledgement.ok === true
|
|
7282
|
+
&& acknowledgement.result?.audio === true
|
|
7283
|
+
&& acknowledgement.result?.stopped === true;
|
|
7284
|
+
if (currentDevice?.sessionId === sessionId
|
|
7285
|
+
&& currentDevice.activeAudioStream
|
|
7286
|
+
&& currentDevice.activeAudioStream.stopCommandId === commandId
|
|
7287
|
+
&& (!streamId || currentDevice.activeAudioStream.streamId === streamId)) {
|
|
7288
|
+
const stoppedAt = new Date().toISOString();
|
|
7289
|
+
currentDevice.activeAudioStream.stopPending = false;
|
|
7290
|
+
currentDevice.activeAudioStream.stopUnconfirmed = !audioStopConfirmed;
|
|
7291
|
+
currentDevice.activeAudioStream.stopError = audioStopConfirmed
|
|
7292
|
+
? ''
|
|
7293
|
+
: safeString(acknowledgement.error || 'audio-stop-unconfirmed', 500);
|
|
7294
|
+
if (audioStopConfirmed) {
|
|
7295
|
+
currentDevice.activeAudioStream.active = false;
|
|
7296
|
+
currentDevice.activeAudioStream.open = false;
|
|
7297
|
+
currentDevice.activeAudioStream.stoppedAt = stoppedAt;
|
|
7298
|
+
currentDevice.activeAudioStream.stopConfirmedAt = stoppedAt;
|
|
7299
|
+
currentDevice.counters.audioStreamsStopped += 1;
|
|
7300
|
+
emitRemoteEvent('RemoteAudioStreamStopped', currentDevice, {
|
|
7301
|
+
streamId,
|
|
7302
|
+
commandId,
|
|
7303
|
+
captureStopConfirmed: true
|
|
7304
|
+
});
|
|
7305
|
+
} else {
|
|
7306
|
+
emitRemoteEvent('RemoteAudioStreamStopUnconfirmed', currentDevice, {
|
|
7307
|
+
streamId,
|
|
7308
|
+
commandId,
|
|
7309
|
+
error: acknowledgement.error || 'audio-stop-unconfirmed'
|
|
7310
|
+
});
|
|
7311
|
+
}
|
|
7312
|
+
}
|
|
7313
|
+
return {
|
|
7314
|
+
...acknowledgement,
|
|
7315
|
+
streamId,
|
|
7316
|
+
captureStopConfirmed: audioStopConfirmed
|
|
7317
|
+
};
|
|
7318
|
+
});
|
|
7319
|
+
const stopResult = { ok: true, commandId, streamId, stopPending: true };
|
|
7320
|
+
Object.defineProperty(stopResult, 'stopPromise', {
|
|
7321
|
+
value: stopPromise,
|
|
7322
|
+
enumerable: false
|
|
7323
|
+
});
|
|
7324
|
+
return stopResult;
|
|
7325
|
+
}
|
|
7130
7326
|
|
|
7131
7327
|
function getDeviceLiveFrame(deviceId, options = {}) {
|
|
7132
7328
|
const device = devices.get(String(deviceId || ''));
|