livedesk 0.1.437 → 0.1.439
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/client/package.json +5 -5
- package/hub/package.json +1 -1
- package/hub/src/remote-hub.js +724 -125
- package/hub/src/server.js +84 -27
- package/hub/src/transport/udp-hub-transport.js +21 -12
- package/hub/src/transport/udp-protocol.js +38 -17
- package/package.json +6 -6
- package/web/dist/assets/{icons-VU9ZIqNm.js → icons-BIyRF-ou.js} +1 -1
- package/web/dist/assets/{index-Bd1_4ydL.css → index-BPpXtm8K.css} +1 -1
- package/web/dist/assets/index-BS2HuoZk.js +25 -0
- package/web/dist/assets/{react-JoD2dl9B.js → react-C7a9r614.js} +1 -1
- package/web/dist/index.html +4 -4
- package/web/dist/assets/index-DxEkHN2-.js +0 -25
package/hub/src/remote-hub.js
CHANGED
|
@@ -28,7 +28,13 @@ const LIVE_STREAM_REPLACEMENT_PENDING_MS = 7000;
|
|
|
28
28
|
const LIVE_STREAM_MIN_FRESH_MS = 3000;
|
|
29
29
|
const LIVE_STREAM_MAX_FRESH_MS = 12000;
|
|
30
30
|
const DEFAULT_REMOTE_INPUT_ACK_TIMEOUT_MS = 5000;
|
|
31
|
-
const
|
|
31
|
+
const DEFAULT_LIVE_CAPTURE_STOP_ACK_TIMEOUT_MS = 8000;
|
|
32
|
+
const REMOTE_POLICY_BYPASS_COMMANDS = new Set([
|
|
33
|
+
'ping',
|
|
34
|
+
'stream.stop',
|
|
35
|
+
'audio.stop'
|
|
36
|
+
]);
|
|
37
|
+
const REMOTE_PROTOCOL_VERSION = 2;
|
|
32
38
|
const REMOTE_AGENT_PROTOCOL = 'mindexec.remote.agent';
|
|
33
39
|
const REMOTE_FRAME_PROTOCOL = 'mindexec.remote.frame.v2';
|
|
34
40
|
const REMOTE_FRAME_PROTOCOL_VERSION = 2;
|
|
@@ -154,7 +160,10 @@ const DUPLICATE_DEVICE_LOG_THROTTLE_MS = 60000;
|
|
|
154
160
|
const DUPLICATE_DEVICE_RETRY_AFTER_MS = 30000;
|
|
155
161
|
const AGENT_BINARY_INGRESS_QUEUE_PACKETS = 8;
|
|
156
162
|
const MODE5_AGENT_BINARY_INGRESS_QUEUE_PACKETS = 2;
|
|
157
|
-
|
|
163
|
+
// Native capture backends can emit short bursts (ScreenCaptureKit commonly
|
|
164
|
+
// queues up to eight frames). Drain one frame per event-loop turn so a burst
|
|
165
|
+
// cannot immediately overflow the smaller Hub-to-browser send lanes.
|
|
166
|
+
const AGENT_BINARY_INGRESS_DRAIN_BUDGET_PACKETS = 1;
|
|
158
167
|
const SYNTHETIC_FRAME_DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAADElEQVR42mP8z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC';
|
|
159
168
|
const SYNTHETIC_FRAME_PAYLOAD = Buffer.from(SYNTHETIC_FRAME_DATA_URL.split(',')[1], 'base64');
|
|
160
169
|
const SYNTHETIC_FRAME_HASH = crypto.createHash('sha256').update(SYNTHETIC_FRAME_PAYLOAD).digest('hex').slice(0, 16);
|
|
@@ -683,6 +692,13 @@ function normalizeRemoteInputEvent(value = {}) {
|
|
|
683
692
|
const issuedAtEpochMs = Number(input.issuedAtEpochMs ?? input.IssuedAtEpochMs);
|
|
684
693
|
const hubReceivedAtEpochMs = Number(input.hubReceivedAtEpochMs ?? input.HubReceivedAtEpochMs);
|
|
685
694
|
const captureGeneration = Number(input.captureGeneration ?? input.CaptureGeneration);
|
|
695
|
+
const rawPressedCodes = input.pressedCodes ?? input.PressedCodes;
|
|
696
|
+
const pressedCodes = Array.isArray(rawPressedCodes)
|
|
697
|
+
? [...new Set(rawPressedCodes
|
|
698
|
+
.map(code => safeString(code, 80))
|
|
699
|
+
.filter(Boolean))]
|
|
700
|
+
.slice(0, 64)
|
|
701
|
+
: null;
|
|
686
702
|
return {
|
|
687
703
|
type,
|
|
688
704
|
// A missing monitor is not equivalent to the primary display. Control
|
|
@@ -697,8 +713,9 @@ function normalizeRemoteInputEvent(value = {}) {
|
|
|
697
713
|
code: safeString(input.code || input.Code, 80),
|
|
698
714
|
keyCode: Number.isFinite(keyCode) ? Math.max(0, Math.min(65535, Math.trunc(keyCode))) : 0,
|
|
699
715
|
location: Number.isFinite(location) ? Math.max(0, Math.min(255, Math.trunc(location))) : 0,
|
|
700
|
-
text: safeText(input.text || input.Text, 256),
|
|
701
|
-
repeat: input.repeat === true || input.Repeat === true,
|
|
716
|
+
text: safeText(input.text || input.Text, 256),
|
|
717
|
+
repeat: input.repeat === true || input.Repeat === true,
|
|
718
|
+
pressedCodes,
|
|
702
719
|
shiftKey: input.shiftKey === true || input.ShiftKey === true,
|
|
703
720
|
ctrlKey: input.ctrlKey === true || input.ControlKey === true || input.CtrlKey === true,
|
|
704
721
|
altKey: input.altKey === true || input.AltKey === true,
|
|
@@ -710,8 +727,9 @@ function normalizeRemoteInputEvent(value = {}) {
|
|
|
710
727
|
hubConnectionId: safeString(input.hubConnectionId || input.HubConnectionId, 128),
|
|
711
728
|
inputEventId: safeString(input.inputEventId || input.InputEventId, 128) || crypto.randomUUID(),
|
|
712
729
|
inputSeq: Number.isSafeInteger(inputSeq) && inputSeq > 0 ? inputSeq : 0,
|
|
713
|
-
requestAck: input.requestAck !== false && input.RequestAck !== false,
|
|
714
|
-
|
|
730
|
+
requestAck: input.requestAck !== false && input.RequestAck !== false,
|
|
731
|
+
reason: safeString(input.reason || input.Reason, 120),
|
|
732
|
+
issuedAtEpochMs: Number.isFinite(issuedAtEpochMs) ? issuedAtEpochMs : 0,
|
|
715
733
|
hubReceivedAtEpochMs: Number.isFinite(hubReceivedAtEpochMs) ? hubReceivedAtEpochMs : 0,
|
|
716
734
|
issuedAt: safeString(input.issuedAt || input.IssuedAt, 80)
|
|
717
735
|
};
|
|
@@ -1016,10 +1034,26 @@ function serializeDevice(device, options = {}) {
|
|
|
1016
1034
|
audio: !!device.audioSocket && !device.audioSocket.destroyed,
|
|
1017
1035
|
file: !!device.fileSocket && !device.fileSocket.destroyed
|
|
1018
1036
|
},
|
|
1019
|
-
latestThumbnail: serializeRemoteFrame(device.latestThumbnail, device.deviceId, 'thumbnail', options),
|
|
1020
|
-
latestLiveFrame: serializeRemoteFrame(device.latestLiveFrame, device.deviceId, 'live', options),
|
|
1021
|
-
activeLiveStream: device.activeLiveStream ? { ...device.activeLiveStream } : null,
|
|
1022
|
-
|
|
1037
|
+
latestThumbnail: serializeRemoteFrame(device.latestThumbnail, device.deviceId, 'thumbnail', options),
|
|
1038
|
+
latestLiveFrame: serializeRemoteFrame(device.latestLiveFrame, device.deviceId, 'live', options),
|
|
1039
|
+
activeLiveStream: device.activeLiveStream ? { ...device.activeLiveStream } : null,
|
|
1040
|
+
liveCapturePause: device.liveCapturePause
|
|
1041
|
+
? {
|
|
1042
|
+
token: device.liveCapturePause.token,
|
|
1043
|
+
pausedAt: device.liveCapturePause.pausedAt,
|
|
1044
|
+
reason: device.liveCapturePause.reason,
|
|
1045
|
+
streamId: device.liveCapturePause.streamId,
|
|
1046
|
+
streamPurpose: device.liveCapturePause.streamPurpose,
|
|
1047
|
+
monitorIndex: device.liveCapturePause.monitorIndex,
|
|
1048
|
+
phase: device.liveCapturePause.phase,
|
|
1049
|
+
captureStopConfirmed: device.liveCapturePause.captureStopConfirmed === true,
|
|
1050
|
+
captureStopConfirmedAt: device.liveCapturePause.captureStopConfirmedAt || '',
|
|
1051
|
+
stopCommandId: device.liveCapturePause.stopCommandId || '',
|
|
1052
|
+
stopError: device.liveCapturePause.stopError || '',
|
|
1053
|
+
stopAttemptCount: Math.max(0, Number(device.liveCapturePause.stopAttemptCount) || 0)
|
|
1054
|
+
}
|
|
1055
|
+
: null,
|
|
1056
|
+
activeAudioStream: device.activeAudioStream ? { ...device.activeAudioStream } : null,
|
|
1023
1057
|
latestAudioFrame: device.latestAudioFrame
|
|
1024
1058
|
? {
|
|
1025
1059
|
streamId: device.latestAudioFrame.streamId,
|
|
@@ -1486,12 +1520,16 @@ export function createRemoteHub(options = {}) {
|
|
|
1486
1520
|
}
|
|
1487
1521
|
}
|
|
1488
1522
|
|
|
1489
|
-
function policyError(device, permission = '') {
|
|
1490
|
-
const policy = getDevicePolicy(device);
|
|
1491
|
-
|
|
1492
|
-
if (
|
|
1493
|
-
|
|
1494
|
-
|
|
1523
|
+
function policyError(device, permission = '', command = '') {
|
|
1524
|
+
const policy = getDevicePolicy(device);
|
|
1525
|
+
const commandName = safeString(command, 80);
|
|
1526
|
+
if (policy.accessMode === 'block-remote-access'
|
|
1527
|
+
&& !REMOTE_POLICY_BYPASS_COMMANDS.has(commandName)) {
|
|
1528
|
+
return 'remote-access-blocked-by-settings';
|
|
1529
|
+
}
|
|
1530
|
+
if (permission && policy[permission] === false) {
|
|
1531
|
+
return `${permission}-blocked-by-settings`;
|
|
1532
|
+
}
|
|
1495
1533
|
return '';
|
|
1496
1534
|
}
|
|
1497
1535
|
|
|
@@ -1510,6 +1548,11 @@ export function createRemoteHub(options = {}) {
|
|
|
1510
1548
|
250,
|
|
1511
1549
|
30000,
|
|
1512
1550
|
DEFAULT_REMOTE_INPUT_ACK_TIMEOUT_MS);
|
|
1551
|
+
const liveCaptureStopAckTimeoutMs = clampNumber(
|
|
1552
|
+
options.liveCaptureStopAckTimeoutMs ?? env.REMOTE_HUB_LIVE_CAPTURE_STOP_ACK_TIMEOUT_MS,
|
|
1553
|
+
250,
|
|
1554
|
+
30000,
|
|
1555
|
+
DEFAULT_LIVE_CAPTURE_STOP_ACK_TIMEOUT_MS);
|
|
1513
1556
|
const managerPackage = safeString(options.managerPackage ?? env.LIVEDESK_MANAGER_PACKAGE ?? env.MINDEXEC_MANAGER_PACKAGE ?? '@livedesk/hub', 128);
|
|
1514
1557
|
const managerVersion = safeString(options.managerVersion ?? env.LIVEDESK_MANAGER_VERSION ?? env.MINDEXEC_MANAGER_VERSION ?? '', 64);
|
|
1515
1558
|
const hostInstanceId = safeString(options.hostInstanceId ?? env.LIVEDESK_HUB_INSTANCE_ID ?? env.MINDEXEC_BRIDGE_INSTANCE_ID ?? crypto.randomUUID(), 128) || crypto.randomUUID();
|
|
@@ -1538,9 +1581,10 @@ export function createRemoteHub(options = {}) {
|
|
|
1538
1581
|
const sockets = new Map();
|
|
1539
1582
|
const inputSockets = new Map();
|
|
1540
1583
|
const frameSockets = new Map();
|
|
1541
|
-
const audioSockets = new Map();
|
|
1542
|
-
const fileSockets = new Map();
|
|
1543
|
-
const allSockets = new Set();
|
|
1584
|
+
const audioSockets = new Map();
|
|
1585
|
+
const fileSockets = new Map();
|
|
1586
|
+
const allSockets = new Set();
|
|
1587
|
+
const pendingCommandResultWaiters = new Map();
|
|
1544
1588
|
const duplicateDeviceLogAt = new Map();
|
|
1545
1589
|
let server = null;
|
|
1546
1590
|
let started = false;
|
|
@@ -2048,15 +2092,109 @@ export function createRemoteHub(options = {}) {
|
|
|
2048
2092
|
});
|
|
2049
2093
|
}
|
|
2050
2094
|
|
|
2051
|
-
function emitRemoteEvent(type, device = null, extra = {}) {
|
|
2052
|
-
emitEvent(type, {
|
|
2053
|
-
...extra,
|
|
2054
|
-
remoteHub: getStatus({ includeSecrets: false }),
|
|
2055
|
-
device: serializeDevice(device)
|
|
2056
|
-
});
|
|
2057
|
-
}
|
|
2058
|
-
|
|
2059
|
-
function
|
|
2095
|
+
function emitRemoteEvent(type, device = null, extra = {}) {
|
|
2096
|
+
emitEvent(type, {
|
|
2097
|
+
...extra,
|
|
2098
|
+
remoteHub: getStatus({ includeSecrets: false }),
|
|
2099
|
+
device: serializeDevice(device)
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
function settlePendingCommandResult(device, message = {}) {
|
|
2104
|
+
const commandId = safeString(message.commandId, 128);
|
|
2105
|
+
const waiter = pendingCommandResultWaiters.get(commandId);
|
|
2106
|
+
if (!waiter
|
|
2107
|
+
|| waiter.deviceId !== device?.deviceId
|
|
2108
|
+
|| waiter.sessionId !== device?.sessionId) {
|
|
2109
|
+
return false;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
pendingCommandResultWaiters.delete(commandId);
|
|
2113
|
+
clearTimeout(waiter.timer);
|
|
2114
|
+
const result = message.result ?? null;
|
|
2115
|
+
const error = safeString(
|
|
2116
|
+
message.error
|
|
2117
|
+
|| result?.error
|
|
2118
|
+
|| (message.type === 'command.error' ? 'command-failed' : ''),
|
|
2119
|
+
500);
|
|
2120
|
+
waiter.resolve({
|
|
2121
|
+
ok: !error
|
|
2122
|
+
&& result?.ok !== false
|
|
2123
|
+
&& result?.status !== 'failed'
|
|
2124
|
+
&& result?.status !== 'rejected',
|
|
2125
|
+
commandId,
|
|
2126
|
+
result,
|
|
2127
|
+
error
|
|
2128
|
+
});
|
|
2129
|
+
return true;
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
function failPendingCommandResultWaiters(device, error = 'device-disconnected') {
|
|
2133
|
+
if (!device?.deviceId) {
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
for (const [commandId, waiter] of pendingCommandResultWaiters) {
|
|
2137
|
+
if (waiter.deviceId !== device.deviceId
|
|
2138
|
+
|| (waiter.sessionId && waiter.sessionId !== device.sessionId)) {
|
|
2139
|
+
continue;
|
|
2140
|
+
}
|
|
2141
|
+
pendingCommandResultWaiters.delete(commandId);
|
|
2142
|
+
clearTimeout(waiter.timer);
|
|
2143
|
+
waiter.resolve({
|
|
2144
|
+
ok: false,
|
|
2145
|
+
commandId,
|
|
2146
|
+
result: null,
|
|
2147
|
+
error: safeString(error, 500) || 'command-result-unavailable'
|
|
2148
|
+
});
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
function waitForCommandResult(device, commandId, timeoutMs = liveCaptureStopAckTimeoutMs) {
|
|
2153
|
+
const key = safeString(commandId, 128);
|
|
2154
|
+
if (!device?.deviceId || !device?.sessionId || !key) {
|
|
2155
|
+
return Promise.resolve({
|
|
2156
|
+
ok: false,
|
|
2157
|
+
commandId: key,
|
|
2158
|
+
result: null,
|
|
2159
|
+
error: 'command-result-wait-invalid'
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
return new Promise(resolve => {
|
|
2164
|
+
const previous = pendingCommandResultWaiters.get(key);
|
|
2165
|
+
if (previous) {
|
|
2166
|
+
clearTimeout(previous.timer);
|
|
2167
|
+
previous.resolve({
|
|
2168
|
+
ok: false,
|
|
2169
|
+
commandId: key,
|
|
2170
|
+
result: null,
|
|
2171
|
+
error: 'command-result-wait-replaced'
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
const timer = setTimeout(() => {
|
|
2175
|
+
const waiter = pendingCommandResultWaiters.get(key);
|
|
2176
|
+
if (!waiter || waiter.deviceId !== device.deviceId || waiter.sessionId !== device.sessionId) {
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
pendingCommandResultWaiters.delete(key);
|
|
2180
|
+
resolve({
|
|
2181
|
+
ok: false,
|
|
2182
|
+
commandId: key,
|
|
2183
|
+
result: null,
|
|
2184
|
+
error: 'command-result-timeout'
|
|
2185
|
+
});
|
|
2186
|
+
}, timeoutMs);
|
|
2187
|
+
timer.unref?.();
|
|
2188
|
+
pendingCommandResultWaiters.set(key, {
|
|
2189
|
+
deviceId: device.deviceId,
|
|
2190
|
+
sessionId: device.sessionId,
|
|
2191
|
+
timer,
|
|
2192
|
+
resolve
|
|
2193
|
+
});
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
function makeSyntheticFrame(device, streamId, mode = 'thumbnail', options = {}) {
|
|
2060
2198
|
const now = new Date().toISOString();
|
|
2061
2199
|
const descriptor = buildRemoteFrameTransferDescriptor(mode, mode === 'thumbnail' ? 'thumbnail' : DEFAULT_REMOTE_FRAME_MODE);
|
|
2062
2200
|
const frameSeq = Number(device?.counters?.thumbnailFramesReceived || 0)
|
|
@@ -2177,6 +2315,7 @@ export function createRemoteHub(options = {}) {
|
|
|
2177
2315
|
existing.lastDisconnectReason = 'replaced-by-new-session';
|
|
2178
2316
|
failAllPendingTasks(existing, 'replaced-by-new-session');
|
|
2179
2317
|
failAllPendingInputFallbacks(existing, 'replaced-by-new-session');
|
|
2318
|
+
failPendingCommandResultWaiters(existing, 'replaced-by-new-session');
|
|
2180
2319
|
writeJsonLine(existing.socket, {
|
|
2181
2320
|
type: 'disconnect',
|
|
2182
2321
|
reason: 'replaced-by-new-session',
|
|
@@ -2716,9 +2855,11 @@ export function createRemoteHub(options = {}) {
|
|
|
2716
2855
|
: getSupportedRemoteFrameModeProfiles();
|
|
2717
2856
|
}
|
|
2718
2857
|
|
|
2719
|
-
const device = {
|
|
2720
|
-
socket,
|
|
2721
|
-
inputSocket: null,
|
|
2858
|
+
const device = {
|
|
2859
|
+
socket,
|
|
2860
|
+
inputSocket: null,
|
|
2861
|
+
inputOwnerConnectionId: '',
|
|
2862
|
+
inputOwnerBindingKey: '',
|
|
2722
2863
|
frameSocket: null,
|
|
2723
2864
|
audioSocket: null,
|
|
2724
2865
|
fileSocket: null,
|
|
@@ -2759,9 +2900,13 @@ export function createRemoteHub(options = {}) {
|
|
|
2759
2900
|
recentFramePayloads: {
|
|
2760
2901
|
thumbnail: [],
|
|
2761
2902
|
live: []
|
|
2762
|
-
},
|
|
2763
|
-
activeLiveStream: null,
|
|
2764
|
-
|
|
2903
|
+
},
|
|
2904
|
+
activeLiveStream: null,
|
|
2905
|
+
// A manual pause is a Hub-owned capture gate, not an Agent-session
|
|
2906
|
+
// detail. Preserve the same lease across a transient Client
|
|
2907
|
+
// reconnect so no subscriber or watchdog can reacquire FFmpeg.
|
|
2908
|
+
liveCapturePause: existing?.liveCapturePause || null,
|
|
2909
|
+
activeAudioStream: null,
|
|
2765
2910
|
latestAudioFrame: null,
|
|
2766
2911
|
latestAudioStatus: null,
|
|
2767
2912
|
udp: {
|
|
@@ -2950,10 +3095,11 @@ export function createRemoteHub(options = {}) {
|
|
|
2950
3095
|
closeInputSocket(device, reason);
|
|
2951
3096
|
closeFrameSocket(device, reason);
|
|
2952
3097
|
device.disconnectedAt = new Date().toISOString();
|
|
2953
|
-
device.lastDisconnectReason = reason;
|
|
3098
|
+
device.lastDisconnectReason = reason;
|
|
2954
3099
|
deactivateDeviceLiveStreams(device, reason, device.disconnectedAt);
|
|
2955
3100
|
failAllPendingTasks(device, 'device-disconnected');
|
|
2956
3101
|
failAllPendingInputFallbacks(device, 'device-disconnected');
|
|
3102
|
+
failPendingCommandResultWaiters(device, 'device-disconnected');
|
|
2957
3103
|
logWarn('remote', `device disconnected ${device.deviceName} (${deviceId}): ${reason}`);
|
|
2958
3104
|
emitRemoteEvent('RemoteDeviceDisconnected', device, { reason });
|
|
2959
3105
|
}
|
|
@@ -4044,6 +4190,11 @@ export function createRemoteHub(options = {}) {
|
|
|
4044
4190
|
&& frameCaptureGeneration === activeCaptureGeneration));
|
|
4045
4191
|
const readyFrameReceivedBeforeFrame = streamState.readyFrameReceived === true;
|
|
4046
4192
|
const captureTimingAvailable = hasFrameCaptureTiming(message);
|
|
4193
|
+
const videoTransport = transport === 'udp-p2p'
|
|
4194
|
+
? 'p2p-udp'
|
|
4195
|
+
: transport === 'ws-binary'
|
|
4196
|
+
? 'direct-ws'
|
|
4197
|
+
: 'direct-tcp';
|
|
4047
4198
|
device.latestLiveFrame = {
|
|
4048
4199
|
streamId,
|
|
4049
4200
|
frameSeq,
|
|
@@ -4080,9 +4231,10 @@ export function createRemoteHub(options = {}) {
|
|
|
4080
4231
|
agentPaceMs: Number.isFinite(Number(message.agentPaceMs)) ? Number(message.agentPaceMs) : 0,
|
|
4081
4232
|
agentQueueBeforePaceMs: Number.isFinite(Number(message.agentQueueBeforePaceMs)) ? Number(message.agentQueueBeforePaceMs) : 0,
|
|
4082
4233
|
agentFrameAgeMs: Number.isFinite(Number(message.agentFrameAgeMs)) ? Number(message.agentFrameAgeMs) : 0,
|
|
4083
|
-
droppedByAgent: Number.isFinite(Number(message.droppedByAgent)) ? Number(message.droppedByAgent) : 0,
|
|
4084
|
-
hubIngressDropped: Number.isFinite(Number(message.hubIngressDropped)) ? Number(message.hubIngressDropped) : 0,
|
|
4085
|
-
|
|
4234
|
+
droppedByAgent: Number.isFinite(Number(message.droppedByAgent)) ? Number(message.droppedByAgent) : 0,
|
|
4235
|
+
hubIngressDropped: Number.isFinite(Number(message.hubIngressDropped)) ? Number(message.hubIngressDropped) : 0,
|
|
4236
|
+
udpIncompleteFrames: Number.isFinite(Number(message.udpIncompleteFrames)) ? Number(message.udpIncompleteFrames) : 0,
|
|
4237
|
+
hardwareEncoder: safeString(message.hardwareEncoder, 80),
|
|
4086
4238
|
platformProfile: safeString(message.platformProfile, 80),
|
|
4087
4239
|
monitorIndex: Number.isFinite(Number(message.monitorIndex)) ? normalizeMonitorIndex(message.monitorIndex) : Number(streamState.monitorIndex || 0),
|
|
4088
4240
|
monitorCount: Number.isFinite(Number(message.monitorCount)) ? clampNumber(message.monitorCount, 1, 64, 1) : Number(streamState.monitorCount || 1),
|
|
@@ -4096,12 +4248,12 @@ export function createRemoteHub(options = {}) {
|
|
|
4096
4248
|
captureAverageMs: captureTimingAvailable ? readOptionalFrameStageMs(message, 'captureAverageMs') : undefined,
|
|
4097
4249
|
captureP95Ms: captureTimingAvailable ? readOptionalFrameStageMs(message, 'captureP95Ms') : undefined,
|
|
4098
4250
|
slowFrameCount: Number.isFinite(Number(message.slowFrameCount)) ? Number(message.slowFrameCount) : 0,
|
|
4099
|
-
captureHelperRestarts: Number.isFinite(Number(message.captureHelperRestarts)) ? Number(message.captureHelperRestarts) : 0,
|
|
4100
|
-
capturedAt,
|
|
4101
|
-
receivedAt: device.lastSeenAt,
|
|
4102
|
-
byteLength,
|
|
4103
|
-
transport,
|
|
4104
|
-
contentHash,
|
|
4251
|
+
captureHelperRestarts: Number.isFinite(Number(message.captureHelperRestarts)) ? Number(message.captureHelperRestarts) : 0,
|
|
4252
|
+
capturedAt,
|
|
4253
|
+
receivedAt: device.lastSeenAt,
|
|
4254
|
+
byteLength,
|
|
4255
|
+
transport,
|
|
4256
|
+
contentHash,
|
|
4105
4257
|
captureMs: captureTimingAvailable ? readOptionalFrameStageMs(message, 'captureMs') : undefined,
|
|
4106
4258
|
captureStageMs: captureTimingAvailable ? readOptionalFrameStageMs(message, 'captureStageMs') : undefined,
|
|
4107
4259
|
convertMs: readFrameStageMs(message, 'convertMs'),
|
|
@@ -4110,10 +4262,20 @@ export function createRemoteHub(options = {}) {
|
|
|
4110
4262
|
captureSourceSkipped: Number.isFinite(Number(message.captureSourceSkipped)) ? Number(message.captureSourceSkipped) : 0,
|
|
4111
4263
|
deltaTileSize: Number.isFinite(Number(message.deltaTileSize)) ? Number(message.deltaTileSize) : 0,
|
|
4112
4264
|
deltaTileCount: Number.isFinite(Number(message.deltaTileCount)) ? Number(message.deltaTileCount) : 0,
|
|
4113
|
-
deltaVisualTileCount: Number.isFinite(Number(message.deltaVisualTileCount)) ? Number(message.deltaVisualTileCount) : 0,
|
|
4114
|
-
deltaRefreshTileCount: Number.isFinite(Number(message.deltaRefreshTileCount)) ? Number(message.deltaRefreshTileCount) : 0,
|
|
4115
|
-
deltaTotalTiles: Number.isFinite(Number(message.deltaTotalTiles)) ? Number(message.deltaTotalTiles) : 0,
|
|
4116
|
-
|
|
4265
|
+
deltaVisualTileCount: Number.isFinite(Number(message.deltaVisualTileCount)) ? Number(message.deltaVisualTileCount) : 0,
|
|
4266
|
+
deltaRefreshTileCount: Number.isFinite(Number(message.deltaRefreshTileCount)) ? Number(message.deltaRefreshTileCount) : 0,
|
|
4267
|
+
deltaTotalTiles: Number.isFinite(Number(message.deltaTotalTiles)) ? Number(message.deltaTotalTiles) : 0,
|
|
4268
|
+
videoTransport,
|
|
4269
|
+
frameTransportActive: safeString(message.frameTransportActive, 20) || (videoTransport === 'p2p-udp' ? 'p2p' : 'direct'),
|
|
4270
|
+
frameTransportProtocol: safeString(message.frameTransportProtocol, 20)
|
|
4271
|
+
|| (videoTransport === 'p2p-udp' ? 'udp' : videoTransport === 'direct-ws' ? 'ws' : 'tcp'),
|
|
4272
|
+
frameTransportState: safeString(message.frameTransportState, 20) || 'active',
|
|
4273
|
+
frameTransportP2pReady: message.frameTransportP2pReady === true,
|
|
4274
|
+
frameTransportReason: safeString(message.frameTransportReason, 240),
|
|
4275
|
+
frameTransportChangedAt: safeString(message.frameTransportChangedAt, 80),
|
|
4276
|
+
frameTransportSwitchCount: Number.isFinite(Number(message.frameTransportSwitchCount)) ? Number(message.frameTransportSwitchCount) : 0,
|
|
4277
|
+
frameTransportTcpFailureCount: Number.isFinite(Number(message.frameTransportTcpFailureCount)) ? Number(message.frameTransportTcpFailureCount) : 0,
|
|
4278
|
+
sameContentStreak,
|
|
4117
4279
|
payload,
|
|
4118
4280
|
accessToken: createFrameAccessToken()
|
|
4119
4281
|
};
|
|
@@ -4276,7 +4438,7 @@ export function createRemoteHub(options = {}) {
|
|
|
4276
4438
|
return true;
|
|
4277
4439
|
}
|
|
4278
4440
|
|
|
4279
|
-
function handleAgentBinaryFrame(socket, state, header, framePayload) {
|
|
4441
|
+
function handleAgentBinaryFrame(socket, state, header, framePayload) {
|
|
4280
4442
|
if (!state.authenticated || !state.device) {
|
|
4281
4443
|
writeJsonLine(socket, { type: 'error', error: 'hello-required' });
|
|
4282
4444
|
socket.destroy();
|
|
@@ -4300,28 +4462,29 @@ export function createRemoteHub(options = {}) {
|
|
|
4300
4462
|
}
|
|
4301
4463
|
|
|
4302
4464
|
const frameKind = safeString(header.frameKind || header.kind || header.frameType || '', 40).toLowerCase();
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4465
|
+
const binaryTransport = state.binaryTransport === 'ws-binary' ? 'ws-binary' : 'binary';
|
|
4466
|
+
if (frameKind === 'thumbnail' || header.type === 'thumbnail.binary') {
|
|
4467
|
+
applyThumbnailFrame(device, header, framePayload, binaryTransport);
|
|
4468
|
+
return;
|
|
4469
|
+
}
|
|
4470
|
+
|
|
4471
|
+
if (frameKind === 'stream' || frameKind === 'live' || header.type === 'stream.binary') {
|
|
4472
|
+
applyLiveFrame(device, {
|
|
4473
|
+
...header,
|
|
4474
|
+
hubIngressDropped: Number(state.binaryQueueDrops || 0)
|
|
4475
|
+
}, framePayload, binaryTransport);
|
|
4476
|
+
return;
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4479
|
+
if (frameKind === 'audio' || header.type === 'audio.binary') {
|
|
4480
|
+
applyAudioFrame(device, header, framePayload, binaryTransport);
|
|
4481
|
+
return;
|
|
4482
|
+
}
|
|
4483
|
+
|
|
4484
|
+
if (frameKind === 'audio-status' || header.type === 'audio.status.binary') {
|
|
4485
|
+
applyAudioStatus(device, header, framePayload, binaryTransport);
|
|
4486
|
+
return;
|
|
4487
|
+
}
|
|
4325
4488
|
|
|
4326
4489
|
emitRemoteEvent('RemoteAgentMessageIgnored', device, {
|
|
4327
4490
|
messageType: safeString(header.type, 80),
|
|
@@ -4606,6 +4769,7 @@ export function createRemoteHub(options = {}) {
|
|
|
4606
4769
|
message.error || (message.type === 'command.error' ? 'command-failed' : ''),
|
|
4607
4770
|
500);
|
|
4608
4771
|
const result = message.result ?? null;
|
|
4772
|
+
settlePendingCommandResult(device, message);
|
|
4609
4773
|
handleInputFallbackCommandResult(device, message);
|
|
4610
4774
|
if (error || result?.ok === false || result?.status === 'failed') {
|
|
4611
4775
|
failPendingLiveStream(device, commandId, error || result?.error || 'stream-start-failed');
|
|
@@ -4659,10 +4823,11 @@ export function createRemoteHub(options = {}) {
|
|
|
4659
4823
|
socket.setNoDelay(true);
|
|
4660
4824
|
socket.setKeepAlive(true, heartbeatMs);
|
|
4661
4825
|
|
|
4662
|
-
const state = {
|
|
4663
|
-
authenticated: false,
|
|
4664
|
-
device: null,
|
|
4665
|
-
|
|
4826
|
+
const state = {
|
|
4827
|
+
authenticated: false,
|
|
4828
|
+
device: null,
|
|
4829
|
+
binaryTransport: 'ws-binary',
|
|
4830
|
+
inputOnly: false,
|
|
4666
4831
|
frameOnly: false,
|
|
4667
4832
|
audioOnly: false,
|
|
4668
4833
|
fileOnly: false,
|
|
@@ -4816,10 +4981,11 @@ export function createRemoteHub(options = {}) {
|
|
|
4816
4981
|
socket.setNoDelay(true);
|
|
4817
4982
|
socket.setKeepAlive(true, heartbeatMs);
|
|
4818
4983
|
|
|
4819
|
-
const state = {
|
|
4820
|
-
authenticated: false,
|
|
4821
|
-
device: null,
|
|
4822
|
-
|
|
4984
|
+
const state = {
|
|
4985
|
+
authenticated: false,
|
|
4986
|
+
device: null,
|
|
4987
|
+
binaryTransport: 'binary',
|
|
4988
|
+
inputOnly: false,
|
|
4823
4989
|
frameOnly: false,
|
|
4824
4990
|
audioOnly: false,
|
|
4825
4991
|
fileOnly: false,
|
|
@@ -5053,6 +5219,7 @@ export function createRemoteHub(options = {}) {
|
|
|
5053
5219
|
for (const device of devices.values()) {
|
|
5054
5220
|
failAllPendingTasks(device, 'hub-shutdown');
|
|
5055
5221
|
failAllPendingInputFallbacks(device, 'hub-shutdown');
|
|
5222
|
+
failPendingCommandResultWaiters(device, 'hub-shutdown');
|
|
5056
5223
|
if (device.socket && !device.socket.destroyed) {
|
|
5057
5224
|
writeJsonLine(device.socket, { type: 'disconnect', reason: 'hub-shutdown' });
|
|
5058
5225
|
device.socket.destroy();
|
|
@@ -5190,7 +5357,7 @@ export function createRemoteHub(options = {}) {
|
|
|
5190
5357
|
: commandName.startsWith('agent.')
|
|
5191
5358
|
? 'allowAgent'
|
|
5192
5359
|
: '';
|
|
5193
|
-
const denied = policyError(device, requiredPermission);
|
|
5360
|
+
const denied = policyError(device, requiredPermission, commandName);
|
|
5194
5361
|
if (denied) return { ok: false, error: denied };
|
|
5195
5362
|
if (device?.synthetic === true && device.connected) {
|
|
5196
5363
|
const commandId = safeString(command?.commandId, 128) || crypto.randomUUID();
|
|
@@ -5293,10 +5460,58 @@ export function createRemoteHub(options = {}) {
|
|
|
5293
5460
|
return { ok: false, error: 'device-not-found' };
|
|
5294
5461
|
}
|
|
5295
5462
|
|
|
5296
|
-
if (!device.connected) {
|
|
5297
|
-
return { ok: false, error: 'device-not-connected' };
|
|
5298
|
-
}
|
|
5299
|
-
|
|
5463
|
+
if (!device.connected) {
|
|
5464
|
+
return { ok: false, error: 'device-not-connected' };
|
|
5465
|
+
}
|
|
5466
|
+
|
|
5467
|
+
const normalized = normalizeRemoteInputEvent(input);
|
|
5468
|
+
if (!normalized.type) {
|
|
5469
|
+
return { ok: false, error: 'missing-input-type' };
|
|
5470
|
+
}
|
|
5471
|
+
|
|
5472
|
+
// A reset only releases native key state. The current browser input
|
|
5473
|
+
// owner must be able to send it even when its former Control binding
|
|
5474
|
+
// has just become stale during a monitor/capture-generation switch.
|
|
5475
|
+
const currentInputOwner = safeString(device.inputOwnerConnectionId, 128);
|
|
5476
|
+
const isCurrentOwnerKeyboardReset = normalized.type.toLowerCase() === 'keyboard.reset'
|
|
5477
|
+
&& normalized.hubConnectionId
|
|
5478
|
+
&& normalized.hubConnectionId === currentInputOwner;
|
|
5479
|
+
if (isCurrentOwnerKeyboardReset) {
|
|
5480
|
+
const inputSocket = device.inputSocket;
|
|
5481
|
+
if (inputSocket && !inputSocket.destroyed) {
|
|
5482
|
+
const sent = writeJsonLine(inputSocket, {
|
|
5483
|
+
type: 'input.control',
|
|
5484
|
+
payload: {
|
|
5485
|
+
...normalized,
|
|
5486
|
+
reason: normalized.reason || 'control-keyboard-owner-changed',
|
|
5487
|
+
pressedCodes: [],
|
|
5488
|
+
shiftKey: false,
|
|
5489
|
+
ctrlKey: false,
|
|
5490
|
+
altKey: false,
|
|
5491
|
+
metaKey: false,
|
|
5492
|
+
repeat: false,
|
|
5493
|
+
hubForwardedAtEpochMs: Date.now(),
|
|
5494
|
+
issuedAt: normalized.issuedAt || new Date().toISOString()
|
|
5495
|
+
}
|
|
5496
|
+
});
|
|
5497
|
+
if (sent) {
|
|
5498
|
+
device.inputOwnerBindingKey = '';
|
|
5499
|
+
device.counters.commandsSent += 1;
|
|
5500
|
+
device.inputLastSeenAt = new Date().toISOString();
|
|
5501
|
+
return {
|
|
5502
|
+
ok: true,
|
|
5503
|
+
inputSocket: true,
|
|
5504
|
+
releaseOnly: true,
|
|
5505
|
+
staleBindingAccepted: true
|
|
5506
|
+
};
|
|
5507
|
+
}
|
|
5508
|
+
detachInputSocket(inputSocket, 'input-owner-reset-write-failed');
|
|
5509
|
+
}
|
|
5510
|
+
if (readCapabilityFlag(device.capabilities, 'dedicatedInputChannel')) {
|
|
5511
|
+
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
5512
|
+
}
|
|
5513
|
+
}
|
|
5514
|
+
|
|
5300
5515
|
const denied = policyError(device, 'allowControl');
|
|
5301
5516
|
if (denied) return { ok: false, error: denied };
|
|
5302
5517
|
|
|
@@ -5308,10 +5523,6 @@ export function createRemoteHub(options = {}) {
|
|
|
5308
5523
|
return { ok: false, error: 'device-input-control-unavailable' };
|
|
5309
5524
|
}
|
|
5310
5525
|
|
|
5311
|
-
const normalized = normalizeRemoteInputEvent(input);
|
|
5312
|
-
if (!normalized.type) {
|
|
5313
|
-
return { ok: false, error: 'missing-input-type' };
|
|
5314
|
-
}
|
|
5315
5526
|
const controlStream = getActiveControlStream(device);
|
|
5316
5527
|
if (!controlStream
|
|
5317
5528
|
|| !liveStreamHasCurrentFrame(controlStream)
|
|
@@ -5357,15 +5568,62 @@ export function createRemoteHub(options = {}) {
|
|
|
5357
5568
|
|
|
5358
5569
|
const inputSocket = device.inputSocket;
|
|
5359
5570
|
if (inputSocket && !inputSocket.destroyed) {
|
|
5360
|
-
const
|
|
5571
|
+
const previousOwnerConnectionId = safeString(device.inputOwnerConnectionId, 128);
|
|
5572
|
+
const activeInputBindingKey = [
|
|
5573
|
+
safeString(device.sessionId, 160),
|
|
5574
|
+
safeString(controlStream.commandId, 128),
|
|
5575
|
+
Number(controlStream.captureGeneration || 0),
|
|
5576
|
+
activeMonitorIndex
|
|
5577
|
+
].join(':');
|
|
5578
|
+
const previousInputBindingKey = safeString(device.inputOwnerBindingKey, 512);
|
|
5579
|
+
const ownerChanged = normalized.hubConnectionId
|
|
5580
|
+
&& previousOwnerConnectionId
|
|
5581
|
+
&& normalized.hubConnectionId !== previousOwnerConnectionId;
|
|
5582
|
+
const bindingChanged = previousInputBindingKey
|
|
5583
|
+
&& previousInputBindingKey !== activeInputBindingKey;
|
|
5584
|
+
if (ownerChanged || bindingChanged) {
|
|
5585
|
+
const resetSent = writeJsonLine(inputSocket, {
|
|
5586
|
+
type: 'input.control',
|
|
5587
|
+
payload: {
|
|
5588
|
+
type: 'keyboard.reset',
|
|
5589
|
+
reason: ownerChanged
|
|
5590
|
+
? 'browser-input-owner-changed'
|
|
5591
|
+
: 'control-input-binding-changed',
|
|
5592
|
+
pressedCodes: [],
|
|
5593
|
+
shiftKey: false,
|
|
5594
|
+
ctrlKey: false,
|
|
5595
|
+
altKey: false,
|
|
5596
|
+
metaKey: false,
|
|
5597
|
+
repeat: false,
|
|
5598
|
+
requestAck: false,
|
|
5599
|
+
inputSeq: 0,
|
|
5600
|
+
hubConnectionId: previousOwnerConnectionId,
|
|
5601
|
+
monitorIndex: activeMonitorIndex,
|
|
5602
|
+
controlSessionId: safeString(device.sessionId, 160),
|
|
5603
|
+
controlCommandId: safeString(controlStream.commandId, 128),
|
|
5604
|
+
captureGeneration: Number(controlStream.captureGeneration || 0),
|
|
5605
|
+
hubForwardedAtEpochMs: Date.now(),
|
|
5606
|
+
issuedAt: new Date().toISOString()
|
|
5607
|
+
}
|
|
5608
|
+
});
|
|
5609
|
+
if (!resetSent) {
|
|
5610
|
+
detachInputSocket(inputSocket, 'input-owner-reset-write-failed');
|
|
5611
|
+
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
5612
|
+
}
|
|
5613
|
+
}
|
|
5614
|
+
const sent = writeJsonLine(inputSocket, {
|
|
5361
5615
|
type: 'input.control',
|
|
5362
5616
|
payload: {
|
|
5363
5617
|
...normalized,
|
|
5364
5618
|
hubForwardedAtEpochMs: Date.now(),
|
|
5365
5619
|
issuedAt: normalized.issuedAt || new Date().toISOString()
|
|
5366
5620
|
}
|
|
5367
|
-
});
|
|
5368
|
-
if (sent) {
|
|
5621
|
+
});
|
|
5622
|
+
if (sent) {
|
|
5623
|
+
if (normalized.hubConnectionId) {
|
|
5624
|
+
device.inputOwnerConnectionId = normalized.hubConnectionId;
|
|
5625
|
+
}
|
|
5626
|
+
device.inputOwnerBindingKey = activeInputBindingKey;
|
|
5369
5627
|
device.counters.commandsSent += 1;
|
|
5370
5628
|
device.inputLastSeenAt = new Date().toISOString();
|
|
5371
5629
|
return {
|
|
@@ -5378,9 +5636,16 @@ export function createRemoteHub(options = {}) {
|
|
|
5378
5636
|
};
|
|
5379
5637
|
}
|
|
5380
5638
|
|
|
5381
|
-
detachInputSocket(inputSocket, 'input-socket-write-failed');
|
|
5382
|
-
}
|
|
5383
|
-
|
|
5639
|
+
detachInputSocket(inputSocket, 'input-socket-write-failed');
|
|
5640
|
+
}
|
|
5641
|
+
|
|
5642
|
+
// Current Agents advertise this ordered side channel. Crossing to the
|
|
5643
|
+
// concurrent main command socket during a reconnect can invert
|
|
5644
|
+
// keydown/keyup, so only legacy Agents use the compatibility fallback.
|
|
5645
|
+
if (readCapabilityFlag(device.capabilities, 'dedicatedInputChannel')) {
|
|
5646
|
+
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
5647
|
+
}
|
|
5648
|
+
|
|
5384
5649
|
const hubForwardedAtEpochMs = Date.now();
|
|
5385
5650
|
const fallback = sendCommand(deviceId, {
|
|
5386
5651
|
command: 'input.control',
|
|
@@ -5424,6 +5689,49 @@ export function createRemoteHub(options = {}) {
|
|
|
5424
5689
|
};
|
|
5425
5690
|
}
|
|
5426
5691
|
|
|
5692
|
+
function releaseInputOwner(deviceId, ownerConnectionId, reason = 'browser-input-owner-closed') {
|
|
5693
|
+
const device = devices.get(String(deviceId || ''));
|
|
5694
|
+
const owner = safeString(ownerConnectionId, 128);
|
|
5695
|
+
if (!device || !owner || safeString(device.inputOwnerConnectionId, 128) !== owner) {
|
|
5696
|
+
return { ok: false, error: 'input-owner-not-current' };
|
|
5697
|
+
}
|
|
5698
|
+
|
|
5699
|
+
device.inputOwnerConnectionId = '';
|
|
5700
|
+
device.inputOwnerBindingKey = '';
|
|
5701
|
+
const inputSocket = device.inputSocket;
|
|
5702
|
+
if (!inputSocket || inputSocket.destroyed) {
|
|
5703
|
+
return { ok: true, queued: false };
|
|
5704
|
+
}
|
|
5705
|
+
const controlStream = getActiveControlStream(device);
|
|
5706
|
+
const sent = writeJsonLine(inputSocket, {
|
|
5707
|
+
type: 'input.control',
|
|
5708
|
+
payload: {
|
|
5709
|
+
type: 'keyboard.reset',
|
|
5710
|
+
reason: safeString(reason, 120) || 'browser-input-owner-closed',
|
|
5711
|
+
pressedCodes: [],
|
|
5712
|
+
shiftKey: false,
|
|
5713
|
+
ctrlKey: false,
|
|
5714
|
+
altKey: false,
|
|
5715
|
+
metaKey: false,
|
|
5716
|
+
repeat: false,
|
|
5717
|
+
requestAck: false,
|
|
5718
|
+
inputSeq: 0,
|
|
5719
|
+
hubConnectionId: owner,
|
|
5720
|
+
monitorIndex: normalizeMonitorIndex(controlStream?.monitorIndex),
|
|
5721
|
+
controlSessionId: safeString(device.sessionId, 160),
|
|
5722
|
+
controlCommandId: safeString(controlStream?.commandId, 128),
|
|
5723
|
+
captureGeneration: Number(controlStream?.captureGeneration || 0),
|
|
5724
|
+
hubForwardedAtEpochMs: Date.now(),
|
|
5725
|
+
issuedAt: new Date().toISOString()
|
|
5726
|
+
}
|
|
5727
|
+
});
|
|
5728
|
+
if (!sent) {
|
|
5729
|
+
detachInputSocket(inputSocket, 'input-owner-release-write-failed');
|
|
5730
|
+
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
5731
|
+
}
|
|
5732
|
+
return { ok: true, queued: true };
|
|
5733
|
+
}
|
|
5734
|
+
|
|
5427
5735
|
function notifyAgentProgress(deviceIds, progress = {}) {
|
|
5428
5736
|
const targets = [...new Set((deviceIds || [])
|
|
5429
5737
|
.map(deviceId => safeString(deviceId, 128))
|
|
@@ -5829,24 +6137,42 @@ export function createRemoteHub(options = {}) {
|
|
|
5829
6137
|
streamId: stream.streamId,
|
|
5830
6138
|
commandId: key,
|
|
5831
6139
|
error: safeString(error, 500) || 'stream-start-failed'
|
|
5832
|
-
});
|
|
5833
|
-
return true;
|
|
5834
|
-
}
|
|
5835
|
-
|
|
5836
|
-
|
|
6140
|
+
});
|
|
6141
|
+
return true;
|
|
6142
|
+
}
|
|
6143
|
+
for (const stream of streams.values()) {
|
|
6144
|
+
if (safeString(stream?.commandId, 128) !== key || stream?.active !== true) {
|
|
6145
|
+
continue;
|
|
6146
|
+
}
|
|
6147
|
+
clearPendingLiveStreamDescriptor(device, stream);
|
|
6148
|
+
stream.active = false;
|
|
6149
|
+
stream.open = false;
|
|
6150
|
+
stream.stoppedAt = new Date().toISOString();
|
|
6151
|
+
stream.stopReason = safeString(error, 500) || 'stream-start-failed';
|
|
6152
|
+
emitRemoteEvent('RemoteLiveStreamStartFailed', device, {
|
|
6153
|
+
streamId: stream.streamId,
|
|
6154
|
+
commandId: key,
|
|
6155
|
+
error: stream.stopReason
|
|
6156
|
+
});
|
|
6157
|
+
return true;
|
|
6158
|
+
}
|
|
6159
|
+
return false;
|
|
6160
|
+
}
|
|
5837
6161
|
|
|
5838
6162
|
function deactivateDeviceLiveStreams(device, reason, stoppedAt = new Date().toISOString()) {
|
|
5839
6163
|
const streams = ensureDeviceLiveStreams(device);
|
|
5840
6164
|
for (const stream of streams.values()) {
|
|
5841
6165
|
clearPendingLiveStreamDescriptor(device, stream);
|
|
5842
6166
|
stream.active = false;
|
|
6167
|
+
stream.open = false;
|
|
5843
6168
|
stream.stoppedAt = stoppedAt;
|
|
5844
6169
|
stream.stopReason = reason;
|
|
5845
|
-
}
|
|
5846
|
-
if (device?.activeLiveStream) {
|
|
5847
|
-
device.activeLiveStream.active = false;
|
|
5848
|
-
device.activeLiveStream.
|
|
5849
|
-
device.activeLiveStream.
|
|
6170
|
+
}
|
|
6171
|
+
if (device?.activeLiveStream) {
|
|
6172
|
+
device.activeLiveStream.active = false;
|
|
6173
|
+
device.activeLiveStream.open = false;
|
|
6174
|
+
device.activeLiveStream.stoppedAt = stoppedAt;
|
|
6175
|
+
device.activeLiveStream.stopReason = reason;
|
|
5850
6176
|
}
|
|
5851
6177
|
}
|
|
5852
6178
|
|
|
@@ -5868,6 +6194,48 @@ export function createRemoteHub(options = {}) {
|
|
|
5868
6194
|
safeString(stream?.streamPurpose, 24).toLowerCase() === 'control') || null;
|
|
5869
6195
|
}
|
|
5870
6196
|
|
|
6197
|
+
function claimSingleLiveCapture(deviceId, device, streamId, streamPurpose) {
|
|
6198
|
+
const purpose = safeString(streamPurpose, 24).toLowerCase() || 'wall';
|
|
6199
|
+
const activeControlStream = getActiveControlStream(device);
|
|
6200
|
+
if (purpose !== 'control'
|
|
6201
|
+
&& activeControlStream
|
|
6202
|
+
&& activeControlStream.streamId !== streamId) {
|
|
6203
|
+
return {
|
|
6204
|
+
ok: false,
|
|
6205
|
+
error: 'CONTROL_CAPTURE_OWNS_DEVICE',
|
|
6206
|
+
activeStreamId: activeControlStream.streamId,
|
|
6207
|
+
activeStreamPurpose: 'control',
|
|
6208
|
+
activeCommandId: safeString(activeControlStream.commandId, 128),
|
|
6209
|
+
captureGeneration: Number(activeControlStream.captureGeneration || 0)
|
|
6210
|
+
};
|
|
6211
|
+
}
|
|
6212
|
+
|
|
6213
|
+
const competingStreams = getActiveLiveStreams(device)
|
|
6214
|
+
.filter(stream => stream.streamId !== streamId);
|
|
6215
|
+
for (const competingStream of competingStreams) {
|
|
6216
|
+
const result = stopLiveStream(deviceId, {
|
|
6217
|
+
streamId: competingStream.streamId,
|
|
6218
|
+
streamPurpose: competingStream.streamPurpose,
|
|
6219
|
+
reason: `${purpose}-capture-handoff`
|
|
6220
|
+
});
|
|
6221
|
+
if (result?.ok !== true) {
|
|
6222
|
+
return {
|
|
6223
|
+
ok: false,
|
|
6224
|
+
error: result?.error || 'capture-handoff-stop-failed',
|
|
6225
|
+
activeStreamId: competingStream.streamId,
|
|
6226
|
+
activeStreamPurpose: safeString(competingStream.streamPurpose, 24)
|
|
6227
|
+
};
|
|
6228
|
+
}
|
|
6229
|
+
emitRemoteEvent('RemoteLiveStreamCapturePreempted', device, {
|
|
6230
|
+
streamId: competingStream.streamId,
|
|
6231
|
+
streamPurpose: safeString(competingStream.streamPurpose, 24) || 'wall',
|
|
6232
|
+
replacementStreamId: streamId,
|
|
6233
|
+
replacementStreamPurpose: purpose
|
|
6234
|
+
});
|
|
6235
|
+
}
|
|
6236
|
+
return { ok: true };
|
|
6237
|
+
}
|
|
6238
|
+
|
|
5871
6239
|
function nextCaptureGeneration(device) {
|
|
5872
6240
|
const current = Number(device?.captureGenerationCounter || 0);
|
|
5873
6241
|
const next = Number.isSafeInteger(current) && current > 0 ? current + 1 : 1;
|
|
@@ -6004,11 +6372,19 @@ export function createRemoteHub(options = {}) {
|
|
|
6004
6372
|
&& Number(activeLiveStream.monitorIndex || 0) === normalized.monitorIndex;
|
|
6005
6373
|
}
|
|
6006
6374
|
|
|
6007
|
-
function startLiveStream(deviceId, options = {}) {
|
|
6008
|
-
const device = devices.get(String(deviceId || ''));
|
|
6009
|
-
const denied = policyError(device);
|
|
6010
|
-
if (denied) return { ok: false, error: denied };
|
|
6011
|
-
if (device?.
|
|
6375
|
+
function startLiveStream(deviceId, options = {}) {
|
|
6376
|
+
const device = devices.get(String(deviceId || ''));
|
|
6377
|
+
const denied = policyError(device);
|
|
6378
|
+
if (denied) return { ok: false, error: denied };
|
|
6379
|
+
if (device?.liveCapturePause?.token) {
|
|
6380
|
+
return {
|
|
6381
|
+
ok: false,
|
|
6382
|
+
error: 'LIVE_CAPTURE_PAUSED',
|
|
6383
|
+
pauseToken: device.liveCapturePause.token,
|
|
6384
|
+
pausedAt: device.liveCapturePause.pausedAt
|
|
6385
|
+
};
|
|
6386
|
+
}
|
|
6387
|
+
if (device?.synthetic === true && device.connected) {
|
|
6012
6388
|
if (!readCapabilityFlag(device.capabilities, 'liveStream')) {
|
|
6013
6389
|
return { ok: false, error: 'device-live-stream-unavailable' };
|
|
6014
6390
|
}
|
|
@@ -6019,6 +6395,8 @@ export function createRemoteHub(options = {}) {
|
|
|
6019
6395
|
if (modePolicyError) return { ok: false, error: modePolicyError };
|
|
6020
6396
|
const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
|
|
6021
6397
|
const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
|
|
6398
|
+
const captureClaim = claimSingleLiveCapture(deviceId, device, streamId, streamPurpose);
|
|
6399
|
+
if (!captureClaim.ok) return captureClaim;
|
|
6022
6400
|
const activeLiveStream = getDeviceLiveStream(device, streamId);
|
|
6023
6401
|
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
6024
6402
|
if (activeLiveStream
|
|
@@ -6118,6 +6496,8 @@ export function createRemoteHub(options = {}) {
|
|
|
6118
6496
|
if (modePolicyError) return { ok: false, error: modePolicyError };
|
|
6119
6497
|
const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
|
|
6120
6498
|
const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
|
|
6499
|
+
const captureClaim = claimSingleLiveCapture(deviceId, device, streamId, streamPurpose);
|
|
6500
|
+
if (!captureClaim.ok) return captureClaim;
|
|
6121
6501
|
const activeLiveStream = getDeviceLiveStream(device, streamId);
|
|
6122
6502
|
const pendingDescriptor = getPendingLiveStreamDescriptor(activeLiveStream);
|
|
6123
6503
|
if (pendingDescriptor?.commandId) {
|
|
@@ -6335,14 +6715,16 @@ export function createRemoteHub(options = {}) {
|
|
|
6335
6715
|
};
|
|
6336
6716
|
}
|
|
6337
6717
|
|
|
6338
|
-
function stopLiveStream(deviceId, options = {}) {
|
|
6718
|
+
function stopLiveStream(deviceId, options = {}) {
|
|
6339
6719
|
const device = devices.get(String(deviceId || ''));
|
|
6340
|
-
const streamPurpose = safeString(options.streamPurpose || options.purpose, 24);
|
|
6341
|
-
const requestedStreamId = safeString(options.streamId, 128);
|
|
6342
|
-
const purposeStreamId = streamPurpose ? makeStableLiveStreamId(deviceId, streamPurpose) : '';
|
|
6343
|
-
const streamId =
|
|
6720
|
+
const streamPurpose = safeString(options.streamPurpose || options.purpose, 24);
|
|
6721
|
+
const requestedStreamId = safeString(options.streamId, 128);
|
|
6722
|
+
const purposeStreamId = streamPurpose ? makeStableLiveStreamId(deviceId, streamPurpose) : '';
|
|
6723
|
+
const streamId = options.stopAll === true
|
|
6724
|
+
? ''
|
|
6725
|
+
: requestedStreamId || purposeStreamId || device?.activeLiveStream?.streamId || '';
|
|
6344
6726
|
const streamState = getDeviceLiveStream(device, streamId);
|
|
6345
|
-
if (streamState && streamState.active !== true) {
|
|
6727
|
+
if (streamState && streamState.active !== true && options.forceCommand !== true) {
|
|
6346
6728
|
clearPendingLiveStreamDescriptor(device, streamState);
|
|
6347
6729
|
return {
|
|
6348
6730
|
ok: true,
|
|
@@ -6359,9 +6741,10 @@ export function createRemoteHub(options = {}) {
|
|
|
6359
6741
|
device.counters.commandsSent += 1;
|
|
6360
6742
|
if (streamState) {
|
|
6361
6743
|
streamState.active = false;
|
|
6744
|
+
streamState.open = false;
|
|
6362
6745
|
streamState.stoppedAt = new Date().toISOString();
|
|
6363
|
-
streamState.stopReason = 'manager-request';
|
|
6364
|
-
}
|
|
6746
|
+
streamState.stopReason = safeString(options.reason, 128) || 'manager-request';
|
|
6747
|
+
}
|
|
6365
6748
|
device.counters.liveStreamsStopped += 1;
|
|
6366
6749
|
emitRemoteEvent('RemoteLiveStreamStopped', device, {
|
|
6367
6750
|
streamId,
|
|
@@ -6391,17 +6774,230 @@ export function createRemoteHub(options = {}) {
|
|
|
6391
6774
|
|
|
6392
6775
|
if (streamState) {
|
|
6393
6776
|
streamState.active = false;
|
|
6777
|
+
streamState.open = false;
|
|
6394
6778
|
streamState.stoppedAt = new Date().toISOString();
|
|
6395
|
-
streamState.stopReason = 'manager-request';
|
|
6396
|
-
}
|
|
6779
|
+
streamState.stopReason = safeString(options.reason, 128) || 'manager-request';
|
|
6780
|
+
}
|
|
6397
6781
|
device.counters.liveStreamsStopped += 1;
|
|
6398
6782
|
emitRemoteEvent('RemoteLiveStreamStopped', device, {
|
|
6399
6783
|
streamId,
|
|
6400
6784
|
commandId
|
|
6401
6785
|
});
|
|
6402
6786
|
|
|
6403
|
-
return { ok: true, commandId, streamId };
|
|
6404
|
-
}
|
|
6787
|
+
return { ok: true, commandId, streamId };
|
|
6788
|
+
}
|
|
6789
|
+
|
|
6790
|
+
function buildLiveCapturePauseResult(pause, extra = {}) {
|
|
6791
|
+
return {
|
|
6792
|
+
paused: true,
|
|
6793
|
+
pauseToken: pause.token,
|
|
6794
|
+
pausedAt: pause.pausedAt,
|
|
6795
|
+
streamId: pause.streamId,
|
|
6796
|
+
streamPurpose: pause.streamPurpose,
|
|
6797
|
+
monitorIndex: pause.monitorIndex,
|
|
6798
|
+
phase: pause.phase,
|
|
6799
|
+
captureStopConfirmed: pause.captureStopConfirmed === true,
|
|
6800
|
+
captureStopConfirmedAt: pause.captureStopConfirmedAt || '',
|
|
6801
|
+
stopCommandId: pause.stopCommandId || '',
|
|
6802
|
+
stopError: pause.stopError || '',
|
|
6803
|
+
stopAttemptCount: Math.max(0, Number(pause.stopAttemptCount) || 0),
|
|
6804
|
+
...extra
|
|
6805
|
+
};
|
|
6806
|
+
}
|
|
6807
|
+
|
|
6808
|
+
async function confirmLiveCaptureStopped(deviceId, pause) {
|
|
6809
|
+
let device = devices.get(String(deviceId || ''));
|
|
6810
|
+
if (!device || device.liveCapturePause?.token !== pause.token) {
|
|
6811
|
+
return buildLiveCapturePauseResult(pause, {
|
|
6812
|
+
ok: false,
|
|
6813
|
+
error: 'LIVE_CAPTURE_PAUSE_LEASE_STALE'
|
|
6814
|
+
});
|
|
6815
|
+
}
|
|
6816
|
+
|
|
6817
|
+
pause.phase = 'pausing';
|
|
6818
|
+
pause.captureStopConfirmed = false;
|
|
6819
|
+
pause.captureStopConfirmedAt = '';
|
|
6820
|
+
pause.stopError = '';
|
|
6821
|
+
pause.stopAttemptCount = Math.max(0, Number(pause.stopAttemptCount) || 0) + 1;
|
|
6822
|
+
|
|
6823
|
+
let stopResult = { ok: true, alreadyStopped: true, streamId: pause.streamId };
|
|
6824
|
+
if (device.synthetic !== true) {
|
|
6825
|
+
stopResult = stopLiveStream(deviceId, {
|
|
6826
|
+
reason: pause.reason,
|
|
6827
|
+
// Empty streamId is an intentional Agent contract: stop every
|
|
6828
|
+
// active/quarantined capture, including a native helper that
|
|
6829
|
+
// outlived the Hub's last stream descriptor.
|
|
6830
|
+
stopAll: true,
|
|
6831
|
+
forceCommand: true
|
|
6832
|
+
});
|
|
6833
|
+
}
|
|
6834
|
+
|
|
6835
|
+
if (stopResult?.ok === false) {
|
|
6836
|
+
pause.phase = 'stop-unconfirmed';
|
|
6837
|
+
pause.stopError = safeString(stopResult.error, 500) || 'stream-stop-command-failed';
|
|
6838
|
+
emitRemoteEvent('RemoteLiveCapturePauseFailed', device, {
|
|
6839
|
+
pauseToken: pause.token,
|
|
6840
|
+
streamId: pause.streamId,
|
|
6841
|
+
stopError: pause.stopError
|
|
6842
|
+
});
|
|
6843
|
+
return buildLiveCapturePauseResult(pause, {
|
|
6844
|
+
ok: false,
|
|
6845
|
+
error: 'LIVE_CAPTURE_STOP_UNCONFIRMED'
|
|
6846
|
+
});
|
|
6847
|
+
}
|
|
6848
|
+
|
|
6849
|
+
pause.stopCommandId = safeString(stopResult.commandId, 128);
|
|
6850
|
+
if (pause.stopCommandId
|
|
6851
|
+
&& stopResult.alreadyStopped !== true
|
|
6852
|
+
&& stopResult.synthetic !== true) {
|
|
6853
|
+
const acknowledgement = await waitForCommandResult(
|
|
6854
|
+
device,
|
|
6855
|
+
pause.stopCommandId,
|
|
6856
|
+
liveCaptureStopAckTimeoutMs);
|
|
6857
|
+
device = devices.get(String(deviceId || '')) || device;
|
|
6858
|
+
if (device.liveCapturePause?.token !== pause.token) {
|
|
6859
|
+
return buildLiveCapturePauseResult(pause, {
|
|
6860
|
+
ok: false,
|
|
6861
|
+
error: 'LIVE_CAPTURE_PAUSE_LEASE_STALE'
|
|
6862
|
+
});
|
|
6863
|
+
}
|
|
6864
|
+
if (!acknowledgement.ok
|
|
6865
|
+
|| acknowledgement.result?.stream !== true
|
|
6866
|
+
|| acknowledgement.result?.stopped !== true) {
|
|
6867
|
+
pause.phase = 'stop-unconfirmed';
|
|
6868
|
+
pause.stopError = safeString(
|
|
6869
|
+
acknowledgement.error
|
|
6870
|
+
|| acknowledgement.result?.error
|
|
6871
|
+
|| 'stream-stop-not-confirmed',
|
|
6872
|
+
500);
|
|
6873
|
+
emitRemoteEvent('RemoteLiveCapturePauseFailed', device, {
|
|
6874
|
+
pauseToken: pause.token,
|
|
6875
|
+
streamId: pause.streamId,
|
|
6876
|
+
stopCommandId: pause.stopCommandId,
|
|
6877
|
+
stopError: pause.stopError
|
|
6878
|
+
});
|
|
6879
|
+
return buildLiveCapturePauseResult(pause, {
|
|
6880
|
+
ok: false,
|
|
6881
|
+
error: 'LIVE_CAPTURE_STOP_UNCONFIRMED'
|
|
6882
|
+
});
|
|
6883
|
+
}
|
|
6884
|
+
}
|
|
6885
|
+
|
|
6886
|
+
pause.phase = 'paused';
|
|
6887
|
+
pause.captureStopConfirmed = true;
|
|
6888
|
+
pause.captureStopConfirmedAt = new Date().toISOString();
|
|
6889
|
+
pause.stopError = '';
|
|
6890
|
+
deactivateDeviceLiveStreams(device, pause.reason, pause.captureStopConfirmedAt);
|
|
6891
|
+
emitRemoteEvent('RemoteLiveCapturePaused', device, {
|
|
6892
|
+
pauseToken: pause.token,
|
|
6893
|
+
pausedAt: pause.pausedAt,
|
|
6894
|
+
streamId: pause.streamId,
|
|
6895
|
+
streamPurpose: pause.streamPurpose,
|
|
6896
|
+
monitorIndex: pause.monitorIndex,
|
|
6897
|
+
stopCommandId: pause.stopCommandId,
|
|
6898
|
+
captureStopConfirmedAt: pause.captureStopConfirmedAt
|
|
6899
|
+
});
|
|
6900
|
+
return buildLiveCapturePauseResult(pause, {
|
|
6901
|
+
ok: true,
|
|
6902
|
+
alreadyStopped: stopResult.alreadyStopped === true
|
|
6903
|
+
});
|
|
6904
|
+
}
|
|
6905
|
+
|
|
6906
|
+
async function pauseLiveCapture(deviceId, options = {}) {
|
|
6907
|
+
const device = devices.get(String(deviceId || ''));
|
|
6908
|
+
if (!device) {
|
|
6909
|
+
return { ok: false, paused: false, error: 'device-not-found' };
|
|
6910
|
+
}
|
|
6911
|
+
const denied = policyError(device, '', 'stream.stop');
|
|
6912
|
+
if (denied) return { ok: false, paused: false, error: denied };
|
|
6913
|
+
|
|
6914
|
+
let pause = device.liveCapturePause;
|
|
6915
|
+
if (pause?.token && pause.captureStopConfirmed === true) {
|
|
6916
|
+
return buildLiveCapturePauseResult(pause, {
|
|
6917
|
+
ok: true,
|
|
6918
|
+
alreadyPaused: true
|
|
6919
|
+
});
|
|
6920
|
+
}
|
|
6921
|
+
if (pause?.stopPromise) {
|
|
6922
|
+
return pause.stopPromise;
|
|
6923
|
+
}
|
|
6924
|
+
|
|
6925
|
+
if (!pause?.token) {
|
|
6926
|
+
const activeLiveStream = device.activeLiveStream?.active === true
|
|
6927
|
+
? device.activeLiveStream
|
|
6928
|
+
: null;
|
|
6929
|
+
pause = {
|
|
6930
|
+
token: crypto.randomUUID(),
|
|
6931
|
+
pausedAt: new Date().toISOString(),
|
|
6932
|
+
reason: safeString(options.reason, 128) || 'user-video-pause',
|
|
6933
|
+
streamId: safeString(activeLiveStream?.streamId, 128),
|
|
6934
|
+
streamPurpose: safeString(activeLiveStream?.streamPurpose, 24),
|
|
6935
|
+
monitorIndex: Math.max(0, Math.floor(Number(activeLiveStream?.monitorIndex) || 0)),
|
|
6936
|
+
phase: 'pausing',
|
|
6937
|
+
captureStopConfirmed: false,
|
|
6938
|
+
captureStopConfirmedAt: '',
|
|
6939
|
+
stopCommandId: '',
|
|
6940
|
+
stopError: '',
|
|
6941
|
+
stopAttemptCount: 0
|
|
6942
|
+
};
|
|
6943
|
+
// Install the authoritative gate before asking the Agent to stop.
|
|
6944
|
+
// A late subscriber or watchdog therefore cannot reacquire FFmpeg
|
|
6945
|
+
// in the pause transition window.
|
|
6946
|
+
device.liveCapturePause = pause;
|
|
6947
|
+
}
|
|
6948
|
+
|
|
6949
|
+
const operation = confirmLiveCaptureStopped(deviceId, pause);
|
|
6950
|
+
pause.stopPromise = operation;
|
|
6951
|
+
try {
|
|
6952
|
+
return await operation;
|
|
6953
|
+
} finally {
|
|
6954
|
+
if (pause.stopPromise === operation) {
|
|
6955
|
+
delete pause.stopPromise;
|
|
6956
|
+
}
|
|
6957
|
+
}
|
|
6958
|
+
}
|
|
6959
|
+
|
|
6960
|
+
function resumeLiveCapture(deviceId, options = {}) {
|
|
6961
|
+
const device = devices.get(String(deviceId || ''));
|
|
6962
|
+
const denied = policyError(device);
|
|
6963
|
+
if (denied) return { ok: false, error: denied };
|
|
6964
|
+
const pause = device?.liveCapturePause;
|
|
6965
|
+
if (!pause?.token) {
|
|
6966
|
+
return { ok: true, paused: false, alreadyResumed: true };
|
|
6967
|
+
}
|
|
6968
|
+
if (pause.phase === 'pausing') {
|
|
6969
|
+
return {
|
|
6970
|
+
ok: false,
|
|
6971
|
+
paused: true,
|
|
6972
|
+
error: 'LIVE_CAPTURE_STOP_IN_PROGRESS',
|
|
6973
|
+
pauseToken: pause.token
|
|
6974
|
+
};
|
|
6975
|
+
}
|
|
6976
|
+
const pauseToken = safeString(options.pauseToken, 128);
|
|
6977
|
+
if (!pauseToken || pauseToken !== pause.token) {
|
|
6978
|
+
return {
|
|
6979
|
+
ok: false,
|
|
6980
|
+
error: 'LIVE_CAPTURE_PAUSE_TOKEN_MISMATCH'
|
|
6981
|
+
};
|
|
6982
|
+
}
|
|
6983
|
+
|
|
6984
|
+
device.liveCapturePause = null;
|
|
6985
|
+
emitRemoteEvent('RemoteLiveCaptureResumed', device, {
|
|
6986
|
+
pauseToken,
|
|
6987
|
+
resumedAt: new Date().toISOString(),
|
|
6988
|
+
streamId: pause.streamId,
|
|
6989
|
+
streamPurpose: pause.streamPurpose,
|
|
6990
|
+
monitorIndex: pause.monitorIndex
|
|
6991
|
+
});
|
|
6992
|
+
return {
|
|
6993
|
+
ok: true,
|
|
6994
|
+
paused: false,
|
|
6995
|
+
resumed: true,
|
|
6996
|
+
streamId: pause.streamId,
|
|
6997
|
+
streamPurpose: pause.streamPurpose,
|
|
6998
|
+
monitorIndex: pause.monitorIndex
|
|
6999
|
+
};
|
|
7000
|
+
}
|
|
6405
7001
|
|
|
6406
7002
|
function startAudioStream(deviceId, options = {}) {
|
|
6407
7003
|
const device = devices.get(String(deviceId || ''));
|
|
@@ -6563,15 +7159,18 @@ export function createRemoteHub(options = {}) {
|
|
|
6563
7159
|
sendCommand,
|
|
6564
7160
|
sendLegacyClientUpdate,
|
|
6565
7161
|
sendInputControl,
|
|
7162
|
+
releaseInputOwner,
|
|
6566
7163
|
notifyAgentProgress,
|
|
6567
7164
|
requestAgentTask,
|
|
6568
7165
|
requestAgentTaskBatch,
|
|
6569
7166
|
setHostTarget,
|
|
6570
7167
|
rotatePairingPin,
|
|
6571
|
-
requestThumbnail,
|
|
6572
|
-
startLiveStream,
|
|
6573
|
-
stopLiveStream,
|
|
6574
|
-
|
|
7168
|
+
requestThumbnail,
|
|
7169
|
+
startLiveStream,
|
|
7170
|
+
stopLiveStream,
|
|
7171
|
+
pauseLiveCapture,
|
|
7172
|
+
resumeLiveCapture,
|
|
7173
|
+
startAudioStream,
|
|
6575
7174
|
stopAudioStream,
|
|
6576
7175
|
getDeviceLiveFrame,
|
|
6577
7176
|
getDeviceThumbnail,
|