livedesk 0.1.436 → 0.1.438
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 +5 -1
- package/client/bin/livedesk-client.js +2 -0
- package/client/package.json +5 -5
- package/hub/package.json +1 -1
- package/hub/src/remote-hub.js +718 -121
- package/hub/src/server.js +84 -27
- package/hub/src/transport/udp-hub-transport.js +36 -21
- package/hub/src/transport/udp-protocol.js +105 -35
- 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-2-aflaqj.js +0 -25
package/hub/src/remote-hub.js
CHANGED
|
@@ -28,6 +28,7 @@ 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 DEFAULT_LIVE_CAPTURE_STOP_ACK_TIMEOUT_MS = 8000;
|
|
31
32
|
const REMOTE_PROTOCOL_VERSION = 2;
|
|
32
33
|
const REMOTE_AGENT_PROTOCOL = 'mindexec.remote.agent';
|
|
33
34
|
const REMOTE_FRAME_PROTOCOL = 'mindexec.remote.frame.v2';
|
|
@@ -154,7 +155,10 @@ const DUPLICATE_DEVICE_LOG_THROTTLE_MS = 60000;
|
|
|
154
155
|
const DUPLICATE_DEVICE_RETRY_AFTER_MS = 30000;
|
|
155
156
|
const AGENT_BINARY_INGRESS_QUEUE_PACKETS = 8;
|
|
156
157
|
const MODE5_AGENT_BINARY_INGRESS_QUEUE_PACKETS = 2;
|
|
157
|
-
|
|
158
|
+
// Native capture backends can emit short bursts (ScreenCaptureKit commonly
|
|
159
|
+
// queues up to eight frames). Drain one frame per event-loop turn so a burst
|
|
160
|
+
// cannot immediately overflow the smaller Hub-to-browser send lanes.
|
|
161
|
+
const AGENT_BINARY_INGRESS_DRAIN_BUDGET_PACKETS = 1;
|
|
158
162
|
const SYNTHETIC_FRAME_DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAADElEQVR42mP8z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC';
|
|
159
163
|
const SYNTHETIC_FRAME_PAYLOAD = Buffer.from(SYNTHETIC_FRAME_DATA_URL.split(',')[1], 'base64');
|
|
160
164
|
const SYNTHETIC_FRAME_HASH = crypto.createHash('sha256').update(SYNTHETIC_FRAME_PAYLOAD).digest('hex').slice(0, 16);
|
|
@@ -683,6 +687,13 @@ function normalizeRemoteInputEvent(value = {}) {
|
|
|
683
687
|
const issuedAtEpochMs = Number(input.issuedAtEpochMs ?? input.IssuedAtEpochMs);
|
|
684
688
|
const hubReceivedAtEpochMs = Number(input.hubReceivedAtEpochMs ?? input.HubReceivedAtEpochMs);
|
|
685
689
|
const captureGeneration = Number(input.captureGeneration ?? input.CaptureGeneration);
|
|
690
|
+
const rawPressedCodes = input.pressedCodes ?? input.PressedCodes;
|
|
691
|
+
const pressedCodes = Array.isArray(rawPressedCodes)
|
|
692
|
+
? [...new Set(rawPressedCodes
|
|
693
|
+
.map(code => safeString(code, 80))
|
|
694
|
+
.filter(Boolean))]
|
|
695
|
+
.slice(0, 64)
|
|
696
|
+
: null;
|
|
686
697
|
return {
|
|
687
698
|
type,
|
|
688
699
|
// A missing monitor is not equivalent to the primary display. Control
|
|
@@ -697,8 +708,9 @@ function normalizeRemoteInputEvent(value = {}) {
|
|
|
697
708
|
code: safeString(input.code || input.Code, 80),
|
|
698
709
|
keyCode: Number.isFinite(keyCode) ? Math.max(0, Math.min(65535, Math.trunc(keyCode))) : 0,
|
|
699
710
|
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,
|
|
711
|
+
text: safeText(input.text || input.Text, 256),
|
|
712
|
+
repeat: input.repeat === true || input.Repeat === true,
|
|
713
|
+
pressedCodes,
|
|
702
714
|
shiftKey: input.shiftKey === true || input.ShiftKey === true,
|
|
703
715
|
ctrlKey: input.ctrlKey === true || input.ControlKey === true || input.CtrlKey === true,
|
|
704
716
|
altKey: input.altKey === true || input.AltKey === true,
|
|
@@ -710,8 +722,9 @@ function normalizeRemoteInputEvent(value = {}) {
|
|
|
710
722
|
hubConnectionId: safeString(input.hubConnectionId || input.HubConnectionId, 128),
|
|
711
723
|
inputEventId: safeString(input.inputEventId || input.InputEventId, 128) || crypto.randomUUID(),
|
|
712
724
|
inputSeq: Number.isSafeInteger(inputSeq) && inputSeq > 0 ? inputSeq : 0,
|
|
713
|
-
requestAck: input.requestAck !== false && input.RequestAck !== false,
|
|
714
|
-
|
|
725
|
+
requestAck: input.requestAck !== false && input.RequestAck !== false,
|
|
726
|
+
reason: safeString(input.reason || input.Reason, 120),
|
|
727
|
+
issuedAtEpochMs: Number.isFinite(issuedAtEpochMs) ? issuedAtEpochMs : 0,
|
|
715
728
|
hubReceivedAtEpochMs: Number.isFinite(hubReceivedAtEpochMs) ? hubReceivedAtEpochMs : 0,
|
|
716
729
|
issuedAt: safeString(input.issuedAt || input.IssuedAt, 80)
|
|
717
730
|
};
|
|
@@ -1016,10 +1029,26 @@ function serializeDevice(device, options = {}) {
|
|
|
1016
1029
|
audio: !!device.audioSocket && !device.audioSocket.destroyed,
|
|
1017
1030
|
file: !!device.fileSocket && !device.fileSocket.destroyed
|
|
1018
1031
|
},
|
|
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
|
-
|
|
1032
|
+
latestThumbnail: serializeRemoteFrame(device.latestThumbnail, device.deviceId, 'thumbnail', options),
|
|
1033
|
+
latestLiveFrame: serializeRemoteFrame(device.latestLiveFrame, device.deviceId, 'live', options),
|
|
1034
|
+
activeLiveStream: device.activeLiveStream ? { ...device.activeLiveStream } : null,
|
|
1035
|
+
liveCapturePause: device.liveCapturePause
|
|
1036
|
+
? {
|
|
1037
|
+
token: device.liveCapturePause.token,
|
|
1038
|
+
pausedAt: device.liveCapturePause.pausedAt,
|
|
1039
|
+
reason: device.liveCapturePause.reason,
|
|
1040
|
+
streamId: device.liveCapturePause.streamId,
|
|
1041
|
+
streamPurpose: device.liveCapturePause.streamPurpose,
|
|
1042
|
+
monitorIndex: device.liveCapturePause.monitorIndex,
|
|
1043
|
+
phase: device.liveCapturePause.phase,
|
|
1044
|
+
captureStopConfirmed: device.liveCapturePause.captureStopConfirmed === true,
|
|
1045
|
+
captureStopConfirmedAt: device.liveCapturePause.captureStopConfirmedAt || '',
|
|
1046
|
+
stopCommandId: device.liveCapturePause.stopCommandId || '',
|
|
1047
|
+
stopError: device.liveCapturePause.stopError || '',
|
|
1048
|
+
stopAttemptCount: Math.max(0, Number(device.liveCapturePause.stopAttemptCount) || 0)
|
|
1049
|
+
}
|
|
1050
|
+
: null,
|
|
1051
|
+
activeAudioStream: device.activeAudioStream ? { ...device.activeAudioStream } : null,
|
|
1023
1052
|
latestAudioFrame: device.latestAudioFrame
|
|
1024
1053
|
? {
|
|
1025
1054
|
streamId: device.latestAudioFrame.streamId,
|
|
@@ -1456,7 +1485,9 @@ class RemoteHubWebSocketAgentSocket {
|
|
|
1456
1485
|
}
|
|
1457
1486
|
|
|
1458
1487
|
export function createRemoteHub(options = {}) {
|
|
1459
|
-
const env = options.env || process.env;
|
|
1488
|
+
const env = options.env || process.env;
|
|
1489
|
+
const rejectFrameChannelForTest = isEnabledValue(env.LIVEDESK_TEST_MODE, false)
|
|
1490
|
+
&& isEnabledValue(env.LIVEDESK_TEST_REJECT_FRAME_CHANNEL, false);
|
|
1460
1491
|
const logEvent = options.logEvent || (() => {});
|
|
1461
1492
|
const logWarn = options.logWarn || (() => {});
|
|
1462
1493
|
const emitEvent = options.emitEvent || (() => {});
|
|
@@ -1508,6 +1539,11 @@ export function createRemoteHub(options = {}) {
|
|
|
1508
1539
|
250,
|
|
1509
1540
|
30000,
|
|
1510
1541
|
DEFAULT_REMOTE_INPUT_ACK_TIMEOUT_MS);
|
|
1542
|
+
const liveCaptureStopAckTimeoutMs = clampNumber(
|
|
1543
|
+
options.liveCaptureStopAckTimeoutMs ?? env.REMOTE_HUB_LIVE_CAPTURE_STOP_ACK_TIMEOUT_MS,
|
|
1544
|
+
250,
|
|
1545
|
+
30000,
|
|
1546
|
+
DEFAULT_LIVE_CAPTURE_STOP_ACK_TIMEOUT_MS);
|
|
1511
1547
|
const managerPackage = safeString(options.managerPackage ?? env.LIVEDESK_MANAGER_PACKAGE ?? env.MINDEXEC_MANAGER_PACKAGE ?? '@livedesk/hub', 128);
|
|
1512
1548
|
const managerVersion = safeString(options.managerVersion ?? env.LIVEDESK_MANAGER_VERSION ?? env.MINDEXEC_MANAGER_VERSION ?? '', 64);
|
|
1513
1549
|
const hostInstanceId = safeString(options.hostInstanceId ?? env.LIVEDESK_HUB_INSTANCE_ID ?? env.MINDEXEC_BRIDGE_INSTANCE_ID ?? crypto.randomUUID(), 128) || crypto.randomUUID();
|
|
@@ -1536,9 +1572,10 @@ export function createRemoteHub(options = {}) {
|
|
|
1536
1572
|
const sockets = new Map();
|
|
1537
1573
|
const inputSockets = new Map();
|
|
1538
1574
|
const frameSockets = new Map();
|
|
1539
|
-
const audioSockets = new Map();
|
|
1540
|
-
const fileSockets = new Map();
|
|
1541
|
-
const allSockets = new Set();
|
|
1575
|
+
const audioSockets = new Map();
|
|
1576
|
+
const fileSockets = new Map();
|
|
1577
|
+
const allSockets = new Set();
|
|
1578
|
+
const pendingCommandResultWaiters = new Map();
|
|
1542
1579
|
const duplicateDeviceLogAt = new Map();
|
|
1543
1580
|
let server = null;
|
|
1544
1581
|
let started = false;
|
|
@@ -2046,15 +2083,109 @@ export function createRemoteHub(options = {}) {
|
|
|
2046
2083
|
});
|
|
2047
2084
|
}
|
|
2048
2085
|
|
|
2049
|
-
function emitRemoteEvent(type, device = null, extra = {}) {
|
|
2050
|
-
emitEvent(type, {
|
|
2051
|
-
...extra,
|
|
2052
|
-
remoteHub: getStatus({ includeSecrets: false }),
|
|
2053
|
-
device: serializeDevice(device)
|
|
2054
|
-
});
|
|
2055
|
-
}
|
|
2056
|
-
|
|
2057
|
-
function
|
|
2086
|
+
function emitRemoteEvent(type, device = null, extra = {}) {
|
|
2087
|
+
emitEvent(type, {
|
|
2088
|
+
...extra,
|
|
2089
|
+
remoteHub: getStatus({ includeSecrets: false }),
|
|
2090
|
+
device: serializeDevice(device)
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
function settlePendingCommandResult(device, message = {}) {
|
|
2095
|
+
const commandId = safeString(message.commandId, 128);
|
|
2096
|
+
const waiter = pendingCommandResultWaiters.get(commandId);
|
|
2097
|
+
if (!waiter
|
|
2098
|
+
|| waiter.deviceId !== device?.deviceId
|
|
2099
|
+
|| waiter.sessionId !== device?.sessionId) {
|
|
2100
|
+
return false;
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
pendingCommandResultWaiters.delete(commandId);
|
|
2104
|
+
clearTimeout(waiter.timer);
|
|
2105
|
+
const result = message.result ?? null;
|
|
2106
|
+
const error = safeString(
|
|
2107
|
+
message.error
|
|
2108
|
+
|| result?.error
|
|
2109
|
+
|| (message.type === 'command.error' ? 'command-failed' : ''),
|
|
2110
|
+
500);
|
|
2111
|
+
waiter.resolve({
|
|
2112
|
+
ok: !error
|
|
2113
|
+
&& result?.ok !== false
|
|
2114
|
+
&& result?.status !== 'failed'
|
|
2115
|
+
&& result?.status !== 'rejected',
|
|
2116
|
+
commandId,
|
|
2117
|
+
result,
|
|
2118
|
+
error
|
|
2119
|
+
});
|
|
2120
|
+
return true;
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
function failPendingCommandResultWaiters(device, error = 'device-disconnected') {
|
|
2124
|
+
if (!device?.deviceId) {
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
for (const [commandId, waiter] of pendingCommandResultWaiters) {
|
|
2128
|
+
if (waiter.deviceId !== device.deviceId
|
|
2129
|
+
|| (waiter.sessionId && waiter.sessionId !== device.sessionId)) {
|
|
2130
|
+
continue;
|
|
2131
|
+
}
|
|
2132
|
+
pendingCommandResultWaiters.delete(commandId);
|
|
2133
|
+
clearTimeout(waiter.timer);
|
|
2134
|
+
waiter.resolve({
|
|
2135
|
+
ok: false,
|
|
2136
|
+
commandId,
|
|
2137
|
+
result: null,
|
|
2138
|
+
error: safeString(error, 500) || 'command-result-unavailable'
|
|
2139
|
+
});
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
function waitForCommandResult(device, commandId, timeoutMs = liveCaptureStopAckTimeoutMs) {
|
|
2144
|
+
const key = safeString(commandId, 128);
|
|
2145
|
+
if (!device?.deviceId || !device?.sessionId || !key) {
|
|
2146
|
+
return Promise.resolve({
|
|
2147
|
+
ok: false,
|
|
2148
|
+
commandId: key,
|
|
2149
|
+
result: null,
|
|
2150
|
+
error: 'command-result-wait-invalid'
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
return new Promise(resolve => {
|
|
2155
|
+
const previous = pendingCommandResultWaiters.get(key);
|
|
2156
|
+
if (previous) {
|
|
2157
|
+
clearTimeout(previous.timer);
|
|
2158
|
+
previous.resolve({
|
|
2159
|
+
ok: false,
|
|
2160
|
+
commandId: key,
|
|
2161
|
+
result: null,
|
|
2162
|
+
error: 'command-result-wait-replaced'
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
const timer = setTimeout(() => {
|
|
2166
|
+
const waiter = pendingCommandResultWaiters.get(key);
|
|
2167
|
+
if (!waiter || waiter.deviceId !== device.deviceId || waiter.sessionId !== device.sessionId) {
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
pendingCommandResultWaiters.delete(key);
|
|
2171
|
+
resolve({
|
|
2172
|
+
ok: false,
|
|
2173
|
+
commandId: key,
|
|
2174
|
+
result: null,
|
|
2175
|
+
error: 'command-result-timeout'
|
|
2176
|
+
});
|
|
2177
|
+
}, timeoutMs);
|
|
2178
|
+
timer.unref?.();
|
|
2179
|
+
pendingCommandResultWaiters.set(key, {
|
|
2180
|
+
deviceId: device.deviceId,
|
|
2181
|
+
sessionId: device.sessionId,
|
|
2182
|
+
timer,
|
|
2183
|
+
resolve
|
|
2184
|
+
});
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
function makeSyntheticFrame(device, streamId, mode = 'thumbnail', options = {}) {
|
|
2058
2189
|
const now = new Date().toISOString();
|
|
2059
2190
|
const descriptor = buildRemoteFrameTransferDescriptor(mode, mode === 'thumbnail' ? 'thumbnail' : DEFAULT_REMOTE_FRAME_MODE);
|
|
2060
2191
|
const frameSeq = Number(device?.counters?.thumbnailFramesReceived || 0)
|
|
@@ -2175,6 +2306,7 @@ export function createRemoteHub(options = {}) {
|
|
|
2175
2306
|
existing.lastDisconnectReason = 'replaced-by-new-session';
|
|
2176
2307
|
failAllPendingTasks(existing, 'replaced-by-new-session');
|
|
2177
2308
|
failAllPendingInputFallbacks(existing, 'replaced-by-new-session');
|
|
2309
|
+
failPendingCommandResultWaiters(existing, 'replaced-by-new-session');
|
|
2178
2310
|
writeJsonLine(existing.socket, {
|
|
2179
2311
|
type: 'disconnect',
|
|
2180
2312
|
reason: 'replaced-by-new-session',
|
|
@@ -2714,9 +2846,11 @@ export function createRemoteHub(options = {}) {
|
|
|
2714
2846
|
: getSupportedRemoteFrameModeProfiles();
|
|
2715
2847
|
}
|
|
2716
2848
|
|
|
2717
|
-
const device = {
|
|
2718
|
-
socket,
|
|
2719
|
-
inputSocket: null,
|
|
2849
|
+
const device = {
|
|
2850
|
+
socket,
|
|
2851
|
+
inputSocket: null,
|
|
2852
|
+
inputOwnerConnectionId: '',
|
|
2853
|
+
inputOwnerBindingKey: '',
|
|
2720
2854
|
frameSocket: null,
|
|
2721
2855
|
audioSocket: null,
|
|
2722
2856
|
fileSocket: null,
|
|
@@ -2757,9 +2891,13 @@ export function createRemoteHub(options = {}) {
|
|
|
2757
2891
|
recentFramePayloads: {
|
|
2758
2892
|
thumbnail: [],
|
|
2759
2893
|
live: []
|
|
2760
|
-
},
|
|
2761
|
-
activeLiveStream: null,
|
|
2762
|
-
|
|
2894
|
+
},
|
|
2895
|
+
activeLiveStream: null,
|
|
2896
|
+
// A manual pause is a Hub-owned capture gate, not an Agent-session
|
|
2897
|
+
// detail. Preserve the same lease across a transient Client
|
|
2898
|
+
// reconnect so no subscriber or watchdog can reacquire FFmpeg.
|
|
2899
|
+
liveCapturePause: existing?.liveCapturePause || null,
|
|
2900
|
+
activeAudioStream: null,
|
|
2763
2901
|
latestAudioFrame: null,
|
|
2764
2902
|
latestAudioStatus: null,
|
|
2765
2903
|
udp: {
|
|
@@ -2948,10 +3086,11 @@ export function createRemoteHub(options = {}) {
|
|
|
2948
3086
|
closeInputSocket(device, reason);
|
|
2949
3087
|
closeFrameSocket(device, reason);
|
|
2950
3088
|
device.disconnectedAt = new Date().toISOString();
|
|
2951
|
-
device.lastDisconnectReason = reason;
|
|
3089
|
+
device.lastDisconnectReason = reason;
|
|
2952
3090
|
deactivateDeviceLiveStreams(device, reason, device.disconnectedAt);
|
|
2953
3091
|
failAllPendingTasks(device, 'device-disconnected');
|
|
2954
3092
|
failAllPendingInputFallbacks(device, 'device-disconnected');
|
|
3093
|
+
failPendingCommandResultWaiters(device, 'device-disconnected');
|
|
2955
3094
|
logWarn('remote', `device disconnected ${device.deviceName} (${deviceId}): ${reason}`);
|
|
2956
3095
|
emitRemoteEvent('RemoteDeviceDisconnected', device, { reason });
|
|
2957
3096
|
}
|
|
@@ -4042,6 +4181,11 @@ export function createRemoteHub(options = {}) {
|
|
|
4042
4181
|
&& frameCaptureGeneration === activeCaptureGeneration));
|
|
4043
4182
|
const readyFrameReceivedBeforeFrame = streamState.readyFrameReceived === true;
|
|
4044
4183
|
const captureTimingAvailable = hasFrameCaptureTiming(message);
|
|
4184
|
+
const videoTransport = transport === 'udp-p2p'
|
|
4185
|
+
? 'p2p-udp'
|
|
4186
|
+
: transport === 'ws-binary'
|
|
4187
|
+
? 'direct-ws'
|
|
4188
|
+
: 'direct-tcp';
|
|
4045
4189
|
device.latestLiveFrame = {
|
|
4046
4190
|
streamId,
|
|
4047
4191
|
frameSeq,
|
|
@@ -4078,9 +4222,10 @@ export function createRemoteHub(options = {}) {
|
|
|
4078
4222
|
agentPaceMs: Number.isFinite(Number(message.agentPaceMs)) ? Number(message.agentPaceMs) : 0,
|
|
4079
4223
|
agentQueueBeforePaceMs: Number.isFinite(Number(message.agentQueueBeforePaceMs)) ? Number(message.agentQueueBeforePaceMs) : 0,
|
|
4080
4224
|
agentFrameAgeMs: Number.isFinite(Number(message.agentFrameAgeMs)) ? Number(message.agentFrameAgeMs) : 0,
|
|
4081
|
-
droppedByAgent: Number.isFinite(Number(message.droppedByAgent)) ? Number(message.droppedByAgent) : 0,
|
|
4082
|
-
hubIngressDropped: Number.isFinite(Number(message.hubIngressDropped)) ? Number(message.hubIngressDropped) : 0,
|
|
4083
|
-
|
|
4225
|
+
droppedByAgent: Number.isFinite(Number(message.droppedByAgent)) ? Number(message.droppedByAgent) : 0,
|
|
4226
|
+
hubIngressDropped: Number.isFinite(Number(message.hubIngressDropped)) ? Number(message.hubIngressDropped) : 0,
|
|
4227
|
+
udpIncompleteFrames: Number.isFinite(Number(message.udpIncompleteFrames)) ? Number(message.udpIncompleteFrames) : 0,
|
|
4228
|
+
hardwareEncoder: safeString(message.hardwareEncoder, 80),
|
|
4084
4229
|
platformProfile: safeString(message.platformProfile, 80),
|
|
4085
4230
|
monitorIndex: Number.isFinite(Number(message.monitorIndex)) ? normalizeMonitorIndex(message.monitorIndex) : Number(streamState.monitorIndex || 0),
|
|
4086
4231
|
monitorCount: Number.isFinite(Number(message.monitorCount)) ? clampNumber(message.monitorCount, 1, 64, 1) : Number(streamState.monitorCount || 1),
|
|
@@ -4094,12 +4239,12 @@ export function createRemoteHub(options = {}) {
|
|
|
4094
4239
|
captureAverageMs: captureTimingAvailable ? readOptionalFrameStageMs(message, 'captureAverageMs') : undefined,
|
|
4095
4240
|
captureP95Ms: captureTimingAvailable ? readOptionalFrameStageMs(message, 'captureP95Ms') : undefined,
|
|
4096
4241
|
slowFrameCount: Number.isFinite(Number(message.slowFrameCount)) ? Number(message.slowFrameCount) : 0,
|
|
4097
|
-
captureHelperRestarts: Number.isFinite(Number(message.captureHelperRestarts)) ? Number(message.captureHelperRestarts) : 0,
|
|
4098
|
-
capturedAt,
|
|
4099
|
-
receivedAt: device.lastSeenAt,
|
|
4100
|
-
byteLength,
|
|
4101
|
-
transport,
|
|
4102
|
-
contentHash,
|
|
4242
|
+
captureHelperRestarts: Number.isFinite(Number(message.captureHelperRestarts)) ? Number(message.captureHelperRestarts) : 0,
|
|
4243
|
+
capturedAt,
|
|
4244
|
+
receivedAt: device.lastSeenAt,
|
|
4245
|
+
byteLength,
|
|
4246
|
+
transport,
|
|
4247
|
+
contentHash,
|
|
4103
4248
|
captureMs: captureTimingAvailable ? readOptionalFrameStageMs(message, 'captureMs') : undefined,
|
|
4104
4249
|
captureStageMs: captureTimingAvailable ? readOptionalFrameStageMs(message, 'captureStageMs') : undefined,
|
|
4105
4250
|
convertMs: readFrameStageMs(message, 'convertMs'),
|
|
@@ -4108,10 +4253,20 @@ export function createRemoteHub(options = {}) {
|
|
|
4108
4253
|
captureSourceSkipped: Number.isFinite(Number(message.captureSourceSkipped)) ? Number(message.captureSourceSkipped) : 0,
|
|
4109
4254
|
deltaTileSize: Number.isFinite(Number(message.deltaTileSize)) ? Number(message.deltaTileSize) : 0,
|
|
4110
4255
|
deltaTileCount: Number.isFinite(Number(message.deltaTileCount)) ? Number(message.deltaTileCount) : 0,
|
|
4111
|
-
deltaVisualTileCount: Number.isFinite(Number(message.deltaVisualTileCount)) ? Number(message.deltaVisualTileCount) : 0,
|
|
4112
|
-
deltaRefreshTileCount: Number.isFinite(Number(message.deltaRefreshTileCount)) ? Number(message.deltaRefreshTileCount) : 0,
|
|
4113
|
-
deltaTotalTiles: Number.isFinite(Number(message.deltaTotalTiles)) ? Number(message.deltaTotalTiles) : 0,
|
|
4114
|
-
|
|
4256
|
+
deltaVisualTileCount: Number.isFinite(Number(message.deltaVisualTileCount)) ? Number(message.deltaVisualTileCount) : 0,
|
|
4257
|
+
deltaRefreshTileCount: Number.isFinite(Number(message.deltaRefreshTileCount)) ? Number(message.deltaRefreshTileCount) : 0,
|
|
4258
|
+
deltaTotalTiles: Number.isFinite(Number(message.deltaTotalTiles)) ? Number(message.deltaTotalTiles) : 0,
|
|
4259
|
+
videoTransport,
|
|
4260
|
+
frameTransportActive: safeString(message.frameTransportActive, 20) || (videoTransport === 'p2p-udp' ? 'p2p' : 'direct'),
|
|
4261
|
+
frameTransportProtocol: safeString(message.frameTransportProtocol, 20)
|
|
4262
|
+
|| (videoTransport === 'p2p-udp' ? 'udp' : videoTransport === 'direct-ws' ? 'ws' : 'tcp'),
|
|
4263
|
+
frameTransportState: safeString(message.frameTransportState, 20) || 'active',
|
|
4264
|
+
frameTransportP2pReady: message.frameTransportP2pReady === true,
|
|
4265
|
+
frameTransportReason: safeString(message.frameTransportReason, 240),
|
|
4266
|
+
frameTransportChangedAt: safeString(message.frameTransportChangedAt, 80),
|
|
4267
|
+
frameTransportSwitchCount: Number.isFinite(Number(message.frameTransportSwitchCount)) ? Number(message.frameTransportSwitchCount) : 0,
|
|
4268
|
+
frameTransportTcpFailureCount: Number.isFinite(Number(message.frameTransportTcpFailureCount)) ? Number(message.frameTransportTcpFailureCount) : 0,
|
|
4269
|
+
sameContentStreak,
|
|
4115
4270
|
payload,
|
|
4116
4271
|
accessToken: createFrameAccessToken()
|
|
4117
4272
|
};
|
|
@@ -4274,7 +4429,7 @@ export function createRemoteHub(options = {}) {
|
|
|
4274
4429
|
return true;
|
|
4275
4430
|
}
|
|
4276
4431
|
|
|
4277
|
-
function handleAgentBinaryFrame(socket, state, header, framePayload) {
|
|
4432
|
+
function handleAgentBinaryFrame(socket, state, header, framePayload) {
|
|
4278
4433
|
if (!state.authenticated || !state.device) {
|
|
4279
4434
|
writeJsonLine(socket, { type: 'error', error: 'hello-required' });
|
|
4280
4435
|
socket.destroy();
|
|
@@ -4298,28 +4453,29 @@ export function createRemoteHub(options = {}) {
|
|
|
4298
4453
|
}
|
|
4299
4454
|
|
|
4300
4455
|
const frameKind = safeString(header.frameKind || header.kind || header.frameType || '', 40).toLowerCase();
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4456
|
+
const binaryTransport = state.binaryTransport === 'ws-binary' ? 'ws-binary' : 'binary';
|
|
4457
|
+
if (frameKind === 'thumbnail' || header.type === 'thumbnail.binary') {
|
|
4458
|
+
applyThumbnailFrame(device, header, framePayload, binaryTransport);
|
|
4459
|
+
return;
|
|
4460
|
+
}
|
|
4461
|
+
|
|
4462
|
+
if (frameKind === 'stream' || frameKind === 'live' || header.type === 'stream.binary') {
|
|
4463
|
+
applyLiveFrame(device, {
|
|
4464
|
+
...header,
|
|
4465
|
+
hubIngressDropped: Number(state.binaryQueueDrops || 0)
|
|
4466
|
+
}, framePayload, binaryTransport);
|
|
4467
|
+
return;
|
|
4468
|
+
}
|
|
4469
|
+
|
|
4470
|
+
if (frameKind === 'audio' || header.type === 'audio.binary') {
|
|
4471
|
+
applyAudioFrame(device, header, framePayload, binaryTransport);
|
|
4472
|
+
return;
|
|
4473
|
+
}
|
|
4474
|
+
|
|
4475
|
+
if (frameKind === 'audio-status' || header.type === 'audio.status.binary') {
|
|
4476
|
+
applyAudioStatus(device, header, framePayload, binaryTransport);
|
|
4477
|
+
return;
|
|
4478
|
+
}
|
|
4323
4479
|
|
|
4324
4480
|
emitRemoteEvent('RemoteAgentMessageIgnored', device, {
|
|
4325
4481
|
messageType: safeString(header.type, 80),
|
|
@@ -4497,9 +4653,14 @@ export function createRemoteHub(options = {}) {
|
|
|
4497
4653
|
state.device = device;
|
|
4498
4654
|
state.inputOnly = true;
|
|
4499
4655
|
return;
|
|
4500
|
-
}
|
|
4501
|
-
if (channel === 'frame') {
|
|
4502
|
-
|
|
4656
|
+
}
|
|
4657
|
+
if (channel === 'frame') {
|
|
4658
|
+
if (rejectFrameChannelForTest) {
|
|
4659
|
+
writeJsonLine(socket, { type: 'error', error: 'test-frame-channel-unavailable' });
|
|
4660
|
+
socket.end();
|
|
4661
|
+
return;
|
|
4662
|
+
}
|
|
4663
|
+
const device = attachFrameSocket(socket, message);
|
|
4503
4664
|
if (!device) {
|
|
4504
4665
|
return;
|
|
4505
4666
|
}
|
|
@@ -4599,6 +4760,7 @@ export function createRemoteHub(options = {}) {
|
|
|
4599
4760
|
message.error || (message.type === 'command.error' ? 'command-failed' : ''),
|
|
4600
4761
|
500);
|
|
4601
4762
|
const result = message.result ?? null;
|
|
4763
|
+
settlePendingCommandResult(device, message);
|
|
4602
4764
|
handleInputFallbackCommandResult(device, message);
|
|
4603
4765
|
if (error || result?.ok === false || result?.status === 'failed') {
|
|
4604
4766
|
failPendingLiveStream(device, commandId, error || result?.error || 'stream-start-failed');
|
|
@@ -4652,10 +4814,11 @@ export function createRemoteHub(options = {}) {
|
|
|
4652
4814
|
socket.setNoDelay(true);
|
|
4653
4815
|
socket.setKeepAlive(true, heartbeatMs);
|
|
4654
4816
|
|
|
4655
|
-
const state = {
|
|
4656
|
-
authenticated: false,
|
|
4657
|
-
device: null,
|
|
4658
|
-
|
|
4817
|
+
const state = {
|
|
4818
|
+
authenticated: false,
|
|
4819
|
+
device: null,
|
|
4820
|
+
binaryTransport: 'ws-binary',
|
|
4821
|
+
inputOnly: false,
|
|
4659
4822
|
frameOnly: false,
|
|
4660
4823
|
audioOnly: false,
|
|
4661
4824
|
fileOnly: false,
|
|
@@ -4809,10 +4972,11 @@ export function createRemoteHub(options = {}) {
|
|
|
4809
4972
|
socket.setNoDelay(true);
|
|
4810
4973
|
socket.setKeepAlive(true, heartbeatMs);
|
|
4811
4974
|
|
|
4812
|
-
const state = {
|
|
4813
|
-
authenticated: false,
|
|
4814
|
-
device: null,
|
|
4815
|
-
|
|
4975
|
+
const state = {
|
|
4976
|
+
authenticated: false,
|
|
4977
|
+
device: null,
|
|
4978
|
+
binaryTransport: 'binary',
|
|
4979
|
+
inputOnly: false,
|
|
4816
4980
|
frameOnly: false,
|
|
4817
4981
|
audioOnly: false,
|
|
4818
4982
|
fileOnly: false,
|
|
@@ -5046,6 +5210,7 @@ export function createRemoteHub(options = {}) {
|
|
|
5046
5210
|
for (const device of devices.values()) {
|
|
5047
5211
|
failAllPendingTasks(device, 'hub-shutdown');
|
|
5048
5212
|
failAllPendingInputFallbacks(device, 'hub-shutdown');
|
|
5213
|
+
failPendingCommandResultWaiters(device, 'hub-shutdown');
|
|
5049
5214
|
if (device.socket && !device.socket.destroyed) {
|
|
5050
5215
|
writeJsonLine(device.socket, { type: 'disconnect', reason: 'hub-shutdown' });
|
|
5051
5216
|
device.socket.destroy();
|
|
@@ -5286,10 +5451,58 @@ export function createRemoteHub(options = {}) {
|
|
|
5286
5451
|
return { ok: false, error: 'device-not-found' };
|
|
5287
5452
|
}
|
|
5288
5453
|
|
|
5289
|
-
if (!device.connected) {
|
|
5290
|
-
return { ok: false, error: 'device-not-connected' };
|
|
5291
|
-
}
|
|
5292
|
-
|
|
5454
|
+
if (!device.connected) {
|
|
5455
|
+
return { ok: false, error: 'device-not-connected' };
|
|
5456
|
+
}
|
|
5457
|
+
|
|
5458
|
+
const normalized = normalizeRemoteInputEvent(input);
|
|
5459
|
+
if (!normalized.type) {
|
|
5460
|
+
return { ok: false, error: 'missing-input-type' };
|
|
5461
|
+
}
|
|
5462
|
+
|
|
5463
|
+
// A reset only releases native key state. The current browser input
|
|
5464
|
+
// owner must be able to send it even when its former Control binding
|
|
5465
|
+
// has just become stale during a monitor/capture-generation switch.
|
|
5466
|
+
const currentInputOwner = safeString(device.inputOwnerConnectionId, 128);
|
|
5467
|
+
const isCurrentOwnerKeyboardReset = normalized.type.toLowerCase() === 'keyboard.reset'
|
|
5468
|
+
&& normalized.hubConnectionId
|
|
5469
|
+
&& normalized.hubConnectionId === currentInputOwner;
|
|
5470
|
+
if (isCurrentOwnerKeyboardReset) {
|
|
5471
|
+
const inputSocket = device.inputSocket;
|
|
5472
|
+
if (inputSocket && !inputSocket.destroyed) {
|
|
5473
|
+
const sent = writeJsonLine(inputSocket, {
|
|
5474
|
+
type: 'input.control',
|
|
5475
|
+
payload: {
|
|
5476
|
+
...normalized,
|
|
5477
|
+
reason: normalized.reason || 'control-keyboard-owner-changed',
|
|
5478
|
+
pressedCodes: [],
|
|
5479
|
+
shiftKey: false,
|
|
5480
|
+
ctrlKey: false,
|
|
5481
|
+
altKey: false,
|
|
5482
|
+
metaKey: false,
|
|
5483
|
+
repeat: false,
|
|
5484
|
+
hubForwardedAtEpochMs: Date.now(),
|
|
5485
|
+
issuedAt: normalized.issuedAt || new Date().toISOString()
|
|
5486
|
+
}
|
|
5487
|
+
});
|
|
5488
|
+
if (sent) {
|
|
5489
|
+
device.inputOwnerBindingKey = '';
|
|
5490
|
+
device.counters.commandsSent += 1;
|
|
5491
|
+
device.inputLastSeenAt = new Date().toISOString();
|
|
5492
|
+
return {
|
|
5493
|
+
ok: true,
|
|
5494
|
+
inputSocket: true,
|
|
5495
|
+
releaseOnly: true,
|
|
5496
|
+
staleBindingAccepted: true
|
|
5497
|
+
};
|
|
5498
|
+
}
|
|
5499
|
+
detachInputSocket(inputSocket, 'input-owner-reset-write-failed');
|
|
5500
|
+
}
|
|
5501
|
+
if (readCapabilityFlag(device.capabilities, 'dedicatedInputChannel')) {
|
|
5502
|
+
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
5503
|
+
}
|
|
5504
|
+
}
|
|
5505
|
+
|
|
5293
5506
|
const denied = policyError(device, 'allowControl');
|
|
5294
5507
|
if (denied) return { ok: false, error: denied };
|
|
5295
5508
|
|
|
@@ -5301,10 +5514,6 @@ export function createRemoteHub(options = {}) {
|
|
|
5301
5514
|
return { ok: false, error: 'device-input-control-unavailable' };
|
|
5302
5515
|
}
|
|
5303
5516
|
|
|
5304
|
-
const normalized = normalizeRemoteInputEvent(input);
|
|
5305
|
-
if (!normalized.type) {
|
|
5306
|
-
return { ok: false, error: 'missing-input-type' };
|
|
5307
|
-
}
|
|
5308
5517
|
const controlStream = getActiveControlStream(device);
|
|
5309
5518
|
if (!controlStream
|
|
5310
5519
|
|| !liveStreamHasCurrentFrame(controlStream)
|
|
@@ -5350,15 +5559,62 @@ export function createRemoteHub(options = {}) {
|
|
|
5350
5559
|
|
|
5351
5560
|
const inputSocket = device.inputSocket;
|
|
5352
5561
|
if (inputSocket && !inputSocket.destroyed) {
|
|
5353
|
-
const
|
|
5562
|
+
const previousOwnerConnectionId = safeString(device.inputOwnerConnectionId, 128);
|
|
5563
|
+
const activeInputBindingKey = [
|
|
5564
|
+
safeString(device.sessionId, 160),
|
|
5565
|
+
safeString(controlStream.commandId, 128),
|
|
5566
|
+
Number(controlStream.captureGeneration || 0),
|
|
5567
|
+
activeMonitorIndex
|
|
5568
|
+
].join(':');
|
|
5569
|
+
const previousInputBindingKey = safeString(device.inputOwnerBindingKey, 512);
|
|
5570
|
+
const ownerChanged = normalized.hubConnectionId
|
|
5571
|
+
&& previousOwnerConnectionId
|
|
5572
|
+
&& normalized.hubConnectionId !== previousOwnerConnectionId;
|
|
5573
|
+
const bindingChanged = previousInputBindingKey
|
|
5574
|
+
&& previousInputBindingKey !== activeInputBindingKey;
|
|
5575
|
+
if (ownerChanged || bindingChanged) {
|
|
5576
|
+
const resetSent = writeJsonLine(inputSocket, {
|
|
5577
|
+
type: 'input.control',
|
|
5578
|
+
payload: {
|
|
5579
|
+
type: 'keyboard.reset',
|
|
5580
|
+
reason: ownerChanged
|
|
5581
|
+
? 'browser-input-owner-changed'
|
|
5582
|
+
: 'control-input-binding-changed',
|
|
5583
|
+
pressedCodes: [],
|
|
5584
|
+
shiftKey: false,
|
|
5585
|
+
ctrlKey: false,
|
|
5586
|
+
altKey: false,
|
|
5587
|
+
metaKey: false,
|
|
5588
|
+
repeat: false,
|
|
5589
|
+
requestAck: false,
|
|
5590
|
+
inputSeq: 0,
|
|
5591
|
+
hubConnectionId: previousOwnerConnectionId,
|
|
5592
|
+
monitorIndex: activeMonitorIndex,
|
|
5593
|
+
controlSessionId: safeString(device.sessionId, 160),
|
|
5594
|
+
controlCommandId: safeString(controlStream.commandId, 128),
|
|
5595
|
+
captureGeneration: Number(controlStream.captureGeneration || 0),
|
|
5596
|
+
hubForwardedAtEpochMs: Date.now(),
|
|
5597
|
+
issuedAt: new Date().toISOString()
|
|
5598
|
+
}
|
|
5599
|
+
});
|
|
5600
|
+
if (!resetSent) {
|
|
5601
|
+
detachInputSocket(inputSocket, 'input-owner-reset-write-failed');
|
|
5602
|
+
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
5603
|
+
}
|
|
5604
|
+
}
|
|
5605
|
+
const sent = writeJsonLine(inputSocket, {
|
|
5354
5606
|
type: 'input.control',
|
|
5355
5607
|
payload: {
|
|
5356
5608
|
...normalized,
|
|
5357
5609
|
hubForwardedAtEpochMs: Date.now(),
|
|
5358
5610
|
issuedAt: normalized.issuedAt || new Date().toISOString()
|
|
5359
5611
|
}
|
|
5360
|
-
});
|
|
5361
|
-
if (sent) {
|
|
5612
|
+
});
|
|
5613
|
+
if (sent) {
|
|
5614
|
+
if (normalized.hubConnectionId) {
|
|
5615
|
+
device.inputOwnerConnectionId = normalized.hubConnectionId;
|
|
5616
|
+
}
|
|
5617
|
+
device.inputOwnerBindingKey = activeInputBindingKey;
|
|
5362
5618
|
device.counters.commandsSent += 1;
|
|
5363
5619
|
device.inputLastSeenAt = new Date().toISOString();
|
|
5364
5620
|
return {
|
|
@@ -5371,9 +5627,16 @@ export function createRemoteHub(options = {}) {
|
|
|
5371
5627
|
};
|
|
5372
5628
|
}
|
|
5373
5629
|
|
|
5374
|
-
detachInputSocket(inputSocket, 'input-socket-write-failed');
|
|
5375
|
-
}
|
|
5376
|
-
|
|
5630
|
+
detachInputSocket(inputSocket, 'input-socket-write-failed');
|
|
5631
|
+
}
|
|
5632
|
+
|
|
5633
|
+
// Current Agents advertise this ordered side channel. Crossing to the
|
|
5634
|
+
// concurrent main command socket during a reconnect can invert
|
|
5635
|
+
// keydown/keyup, so only legacy Agents use the compatibility fallback.
|
|
5636
|
+
if (readCapabilityFlag(device.capabilities, 'dedicatedInputChannel')) {
|
|
5637
|
+
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
5638
|
+
}
|
|
5639
|
+
|
|
5377
5640
|
const hubForwardedAtEpochMs = Date.now();
|
|
5378
5641
|
const fallback = sendCommand(deviceId, {
|
|
5379
5642
|
command: 'input.control',
|
|
@@ -5417,6 +5680,49 @@ export function createRemoteHub(options = {}) {
|
|
|
5417
5680
|
};
|
|
5418
5681
|
}
|
|
5419
5682
|
|
|
5683
|
+
function releaseInputOwner(deviceId, ownerConnectionId, reason = 'browser-input-owner-closed') {
|
|
5684
|
+
const device = devices.get(String(deviceId || ''));
|
|
5685
|
+
const owner = safeString(ownerConnectionId, 128);
|
|
5686
|
+
if (!device || !owner || safeString(device.inputOwnerConnectionId, 128) !== owner) {
|
|
5687
|
+
return { ok: false, error: 'input-owner-not-current' };
|
|
5688
|
+
}
|
|
5689
|
+
|
|
5690
|
+
device.inputOwnerConnectionId = '';
|
|
5691
|
+
device.inputOwnerBindingKey = '';
|
|
5692
|
+
const inputSocket = device.inputSocket;
|
|
5693
|
+
if (!inputSocket || inputSocket.destroyed) {
|
|
5694
|
+
return { ok: true, queued: false };
|
|
5695
|
+
}
|
|
5696
|
+
const controlStream = getActiveControlStream(device);
|
|
5697
|
+
const sent = writeJsonLine(inputSocket, {
|
|
5698
|
+
type: 'input.control',
|
|
5699
|
+
payload: {
|
|
5700
|
+
type: 'keyboard.reset',
|
|
5701
|
+
reason: safeString(reason, 120) || 'browser-input-owner-closed',
|
|
5702
|
+
pressedCodes: [],
|
|
5703
|
+
shiftKey: false,
|
|
5704
|
+
ctrlKey: false,
|
|
5705
|
+
altKey: false,
|
|
5706
|
+
metaKey: false,
|
|
5707
|
+
repeat: false,
|
|
5708
|
+
requestAck: false,
|
|
5709
|
+
inputSeq: 0,
|
|
5710
|
+
hubConnectionId: owner,
|
|
5711
|
+
monitorIndex: normalizeMonitorIndex(controlStream?.monitorIndex),
|
|
5712
|
+
controlSessionId: safeString(device.sessionId, 160),
|
|
5713
|
+
controlCommandId: safeString(controlStream?.commandId, 128),
|
|
5714
|
+
captureGeneration: Number(controlStream?.captureGeneration || 0),
|
|
5715
|
+
hubForwardedAtEpochMs: Date.now(),
|
|
5716
|
+
issuedAt: new Date().toISOString()
|
|
5717
|
+
}
|
|
5718
|
+
});
|
|
5719
|
+
if (!sent) {
|
|
5720
|
+
detachInputSocket(inputSocket, 'input-owner-release-write-failed');
|
|
5721
|
+
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
5722
|
+
}
|
|
5723
|
+
return { ok: true, queued: true };
|
|
5724
|
+
}
|
|
5725
|
+
|
|
5420
5726
|
function notifyAgentProgress(deviceIds, progress = {}) {
|
|
5421
5727
|
const targets = [...new Set((deviceIds || [])
|
|
5422
5728
|
.map(deviceId => safeString(deviceId, 128))
|
|
@@ -5822,24 +6128,42 @@ export function createRemoteHub(options = {}) {
|
|
|
5822
6128
|
streamId: stream.streamId,
|
|
5823
6129
|
commandId: key,
|
|
5824
6130
|
error: safeString(error, 500) || 'stream-start-failed'
|
|
5825
|
-
});
|
|
5826
|
-
return true;
|
|
5827
|
-
}
|
|
5828
|
-
|
|
5829
|
-
|
|
6131
|
+
});
|
|
6132
|
+
return true;
|
|
6133
|
+
}
|
|
6134
|
+
for (const stream of streams.values()) {
|
|
6135
|
+
if (safeString(stream?.commandId, 128) !== key || stream?.active !== true) {
|
|
6136
|
+
continue;
|
|
6137
|
+
}
|
|
6138
|
+
clearPendingLiveStreamDescriptor(device, stream);
|
|
6139
|
+
stream.active = false;
|
|
6140
|
+
stream.open = false;
|
|
6141
|
+
stream.stoppedAt = new Date().toISOString();
|
|
6142
|
+
stream.stopReason = safeString(error, 500) || 'stream-start-failed';
|
|
6143
|
+
emitRemoteEvent('RemoteLiveStreamStartFailed', device, {
|
|
6144
|
+
streamId: stream.streamId,
|
|
6145
|
+
commandId: key,
|
|
6146
|
+
error: stream.stopReason
|
|
6147
|
+
});
|
|
6148
|
+
return true;
|
|
6149
|
+
}
|
|
6150
|
+
return false;
|
|
6151
|
+
}
|
|
5830
6152
|
|
|
5831
6153
|
function deactivateDeviceLiveStreams(device, reason, stoppedAt = new Date().toISOString()) {
|
|
5832
6154
|
const streams = ensureDeviceLiveStreams(device);
|
|
5833
6155
|
for (const stream of streams.values()) {
|
|
5834
6156
|
clearPendingLiveStreamDescriptor(device, stream);
|
|
5835
6157
|
stream.active = false;
|
|
6158
|
+
stream.open = false;
|
|
5836
6159
|
stream.stoppedAt = stoppedAt;
|
|
5837
6160
|
stream.stopReason = reason;
|
|
5838
|
-
}
|
|
5839
|
-
if (device?.activeLiveStream) {
|
|
5840
|
-
device.activeLiveStream.active = false;
|
|
5841
|
-
device.activeLiveStream.
|
|
5842
|
-
device.activeLiveStream.
|
|
6161
|
+
}
|
|
6162
|
+
if (device?.activeLiveStream) {
|
|
6163
|
+
device.activeLiveStream.active = false;
|
|
6164
|
+
device.activeLiveStream.open = false;
|
|
6165
|
+
device.activeLiveStream.stoppedAt = stoppedAt;
|
|
6166
|
+
device.activeLiveStream.stopReason = reason;
|
|
5843
6167
|
}
|
|
5844
6168
|
}
|
|
5845
6169
|
|
|
@@ -5861,6 +6185,48 @@ export function createRemoteHub(options = {}) {
|
|
|
5861
6185
|
safeString(stream?.streamPurpose, 24).toLowerCase() === 'control') || null;
|
|
5862
6186
|
}
|
|
5863
6187
|
|
|
6188
|
+
function claimSingleLiveCapture(deviceId, device, streamId, streamPurpose) {
|
|
6189
|
+
const purpose = safeString(streamPurpose, 24).toLowerCase() || 'wall';
|
|
6190
|
+
const activeControlStream = getActiveControlStream(device);
|
|
6191
|
+
if (purpose !== 'control'
|
|
6192
|
+
&& activeControlStream
|
|
6193
|
+
&& activeControlStream.streamId !== streamId) {
|
|
6194
|
+
return {
|
|
6195
|
+
ok: false,
|
|
6196
|
+
error: 'CONTROL_CAPTURE_OWNS_DEVICE',
|
|
6197
|
+
activeStreamId: activeControlStream.streamId,
|
|
6198
|
+
activeStreamPurpose: 'control',
|
|
6199
|
+
activeCommandId: safeString(activeControlStream.commandId, 128),
|
|
6200
|
+
captureGeneration: Number(activeControlStream.captureGeneration || 0)
|
|
6201
|
+
};
|
|
6202
|
+
}
|
|
6203
|
+
|
|
6204
|
+
const competingStreams = getActiveLiveStreams(device)
|
|
6205
|
+
.filter(stream => stream.streamId !== streamId);
|
|
6206
|
+
for (const competingStream of competingStreams) {
|
|
6207
|
+
const result = stopLiveStream(deviceId, {
|
|
6208
|
+
streamId: competingStream.streamId,
|
|
6209
|
+
streamPurpose: competingStream.streamPurpose,
|
|
6210
|
+
reason: `${purpose}-capture-handoff`
|
|
6211
|
+
});
|
|
6212
|
+
if (result?.ok !== true) {
|
|
6213
|
+
return {
|
|
6214
|
+
ok: false,
|
|
6215
|
+
error: result?.error || 'capture-handoff-stop-failed',
|
|
6216
|
+
activeStreamId: competingStream.streamId,
|
|
6217
|
+
activeStreamPurpose: safeString(competingStream.streamPurpose, 24)
|
|
6218
|
+
};
|
|
6219
|
+
}
|
|
6220
|
+
emitRemoteEvent('RemoteLiveStreamCapturePreempted', device, {
|
|
6221
|
+
streamId: competingStream.streamId,
|
|
6222
|
+
streamPurpose: safeString(competingStream.streamPurpose, 24) || 'wall',
|
|
6223
|
+
replacementStreamId: streamId,
|
|
6224
|
+
replacementStreamPurpose: purpose
|
|
6225
|
+
});
|
|
6226
|
+
}
|
|
6227
|
+
return { ok: true };
|
|
6228
|
+
}
|
|
6229
|
+
|
|
5864
6230
|
function nextCaptureGeneration(device) {
|
|
5865
6231
|
const current = Number(device?.captureGenerationCounter || 0);
|
|
5866
6232
|
const next = Number.isSafeInteger(current) && current > 0 ? current + 1 : 1;
|
|
@@ -5997,11 +6363,19 @@ export function createRemoteHub(options = {}) {
|
|
|
5997
6363
|
&& Number(activeLiveStream.monitorIndex || 0) === normalized.monitorIndex;
|
|
5998
6364
|
}
|
|
5999
6365
|
|
|
6000
|
-
function startLiveStream(deviceId, options = {}) {
|
|
6001
|
-
const device = devices.get(String(deviceId || ''));
|
|
6002
|
-
const denied = policyError(device);
|
|
6003
|
-
if (denied) return { ok: false, error: denied };
|
|
6004
|
-
if (device?.
|
|
6366
|
+
function startLiveStream(deviceId, options = {}) {
|
|
6367
|
+
const device = devices.get(String(deviceId || ''));
|
|
6368
|
+
const denied = policyError(device);
|
|
6369
|
+
if (denied) return { ok: false, error: denied };
|
|
6370
|
+
if (device?.liveCapturePause?.token) {
|
|
6371
|
+
return {
|
|
6372
|
+
ok: false,
|
|
6373
|
+
error: 'LIVE_CAPTURE_PAUSED',
|
|
6374
|
+
pauseToken: device.liveCapturePause.token,
|
|
6375
|
+
pausedAt: device.liveCapturePause.pausedAt
|
|
6376
|
+
};
|
|
6377
|
+
}
|
|
6378
|
+
if (device?.synthetic === true && device.connected) {
|
|
6005
6379
|
if (!readCapabilityFlag(device.capabilities, 'liveStream')) {
|
|
6006
6380
|
return { ok: false, error: 'device-live-stream-unavailable' };
|
|
6007
6381
|
}
|
|
@@ -6012,6 +6386,8 @@ export function createRemoteHub(options = {}) {
|
|
|
6012
6386
|
if (modePolicyError) return { ok: false, error: modePolicyError };
|
|
6013
6387
|
const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
|
|
6014
6388
|
const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
|
|
6389
|
+
const captureClaim = claimSingleLiveCapture(deviceId, device, streamId, streamPurpose);
|
|
6390
|
+
if (!captureClaim.ok) return captureClaim;
|
|
6015
6391
|
const activeLiveStream = getDeviceLiveStream(device, streamId);
|
|
6016
6392
|
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
6017
6393
|
if (activeLiveStream
|
|
@@ -6111,6 +6487,8 @@ export function createRemoteHub(options = {}) {
|
|
|
6111
6487
|
if (modePolicyError) return { ok: false, error: modePolicyError };
|
|
6112
6488
|
const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
|
|
6113
6489
|
const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
|
|
6490
|
+
const captureClaim = claimSingleLiveCapture(deviceId, device, streamId, streamPurpose);
|
|
6491
|
+
if (!captureClaim.ok) return captureClaim;
|
|
6114
6492
|
const activeLiveStream = getDeviceLiveStream(device, streamId);
|
|
6115
6493
|
const pendingDescriptor = getPendingLiveStreamDescriptor(activeLiveStream);
|
|
6116
6494
|
if (pendingDescriptor?.commandId) {
|
|
@@ -6328,14 +6706,16 @@ export function createRemoteHub(options = {}) {
|
|
|
6328
6706
|
};
|
|
6329
6707
|
}
|
|
6330
6708
|
|
|
6331
|
-
function stopLiveStream(deviceId, options = {}) {
|
|
6709
|
+
function stopLiveStream(deviceId, options = {}) {
|
|
6332
6710
|
const device = devices.get(String(deviceId || ''));
|
|
6333
|
-
const streamPurpose = safeString(options.streamPurpose || options.purpose, 24);
|
|
6334
|
-
const requestedStreamId = safeString(options.streamId, 128);
|
|
6335
|
-
const purposeStreamId = streamPurpose ? makeStableLiveStreamId(deviceId, streamPurpose) : '';
|
|
6336
|
-
const streamId =
|
|
6711
|
+
const streamPurpose = safeString(options.streamPurpose || options.purpose, 24);
|
|
6712
|
+
const requestedStreamId = safeString(options.streamId, 128);
|
|
6713
|
+
const purposeStreamId = streamPurpose ? makeStableLiveStreamId(deviceId, streamPurpose) : '';
|
|
6714
|
+
const streamId = options.stopAll === true
|
|
6715
|
+
? ''
|
|
6716
|
+
: requestedStreamId || purposeStreamId || device?.activeLiveStream?.streamId || '';
|
|
6337
6717
|
const streamState = getDeviceLiveStream(device, streamId);
|
|
6338
|
-
if (streamState && streamState.active !== true) {
|
|
6718
|
+
if (streamState && streamState.active !== true && options.forceCommand !== true) {
|
|
6339
6719
|
clearPendingLiveStreamDescriptor(device, streamState);
|
|
6340
6720
|
return {
|
|
6341
6721
|
ok: true,
|
|
@@ -6352,9 +6732,10 @@ export function createRemoteHub(options = {}) {
|
|
|
6352
6732
|
device.counters.commandsSent += 1;
|
|
6353
6733
|
if (streamState) {
|
|
6354
6734
|
streamState.active = false;
|
|
6735
|
+
streamState.open = false;
|
|
6355
6736
|
streamState.stoppedAt = new Date().toISOString();
|
|
6356
|
-
streamState.stopReason = 'manager-request';
|
|
6357
|
-
}
|
|
6737
|
+
streamState.stopReason = safeString(options.reason, 128) || 'manager-request';
|
|
6738
|
+
}
|
|
6358
6739
|
device.counters.liveStreamsStopped += 1;
|
|
6359
6740
|
emitRemoteEvent('RemoteLiveStreamStopped', device, {
|
|
6360
6741
|
streamId,
|
|
@@ -6384,17 +6765,230 @@ export function createRemoteHub(options = {}) {
|
|
|
6384
6765
|
|
|
6385
6766
|
if (streamState) {
|
|
6386
6767
|
streamState.active = false;
|
|
6768
|
+
streamState.open = false;
|
|
6387
6769
|
streamState.stoppedAt = new Date().toISOString();
|
|
6388
|
-
streamState.stopReason = 'manager-request';
|
|
6389
|
-
}
|
|
6770
|
+
streamState.stopReason = safeString(options.reason, 128) || 'manager-request';
|
|
6771
|
+
}
|
|
6390
6772
|
device.counters.liveStreamsStopped += 1;
|
|
6391
6773
|
emitRemoteEvent('RemoteLiveStreamStopped', device, {
|
|
6392
6774
|
streamId,
|
|
6393
6775
|
commandId
|
|
6394
6776
|
});
|
|
6395
6777
|
|
|
6396
|
-
return { ok: true, commandId, streamId };
|
|
6397
|
-
}
|
|
6778
|
+
return { ok: true, commandId, streamId };
|
|
6779
|
+
}
|
|
6780
|
+
|
|
6781
|
+
function buildLiveCapturePauseResult(pause, extra = {}) {
|
|
6782
|
+
return {
|
|
6783
|
+
paused: true,
|
|
6784
|
+
pauseToken: pause.token,
|
|
6785
|
+
pausedAt: pause.pausedAt,
|
|
6786
|
+
streamId: pause.streamId,
|
|
6787
|
+
streamPurpose: pause.streamPurpose,
|
|
6788
|
+
monitorIndex: pause.monitorIndex,
|
|
6789
|
+
phase: pause.phase,
|
|
6790
|
+
captureStopConfirmed: pause.captureStopConfirmed === true,
|
|
6791
|
+
captureStopConfirmedAt: pause.captureStopConfirmedAt || '',
|
|
6792
|
+
stopCommandId: pause.stopCommandId || '',
|
|
6793
|
+
stopError: pause.stopError || '',
|
|
6794
|
+
stopAttemptCount: Math.max(0, Number(pause.stopAttemptCount) || 0),
|
|
6795
|
+
...extra
|
|
6796
|
+
};
|
|
6797
|
+
}
|
|
6798
|
+
|
|
6799
|
+
async function confirmLiveCaptureStopped(deviceId, pause) {
|
|
6800
|
+
let device = devices.get(String(deviceId || ''));
|
|
6801
|
+
if (!device || device.liveCapturePause?.token !== pause.token) {
|
|
6802
|
+
return buildLiveCapturePauseResult(pause, {
|
|
6803
|
+
ok: false,
|
|
6804
|
+
error: 'LIVE_CAPTURE_PAUSE_LEASE_STALE'
|
|
6805
|
+
});
|
|
6806
|
+
}
|
|
6807
|
+
|
|
6808
|
+
pause.phase = 'pausing';
|
|
6809
|
+
pause.captureStopConfirmed = false;
|
|
6810
|
+
pause.captureStopConfirmedAt = '';
|
|
6811
|
+
pause.stopError = '';
|
|
6812
|
+
pause.stopAttemptCount = Math.max(0, Number(pause.stopAttemptCount) || 0) + 1;
|
|
6813
|
+
|
|
6814
|
+
let stopResult = { ok: true, alreadyStopped: true, streamId: pause.streamId };
|
|
6815
|
+
if (device.synthetic !== true) {
|
|
6816
|
+
stopResult = stopLiveStream(deviceId, {
|
|
6817
|
+
reason: pause.reason,
|
|
6818
|
+
// Empty streamId is an intentional Agent contract: stop every
|
|
6819
|
+
// active/quarantined capture, including a native helper that
|
|
6820
|
+
// outlived the Hub's last stream descriptor.
|
|
6821
|
+
stopAll: true,
|
|
6822
|
+
forceCommand: true
|
|
6823
|
+
});
|
|
6824
|
+
}
|
|
6825
|
+
|
|
6826
|
+
if (stopResult?.ok === false) {
|
|
6827
|
+
pause.phase = 'stop-unconfirmed';
|
|
6828
|
+
pause.stopError = safeString(stopResult.error, 500) || 'stream-stop-command-failed';
|
|
6829
|
+
emitRemoteEvent('RemoteLiveCapturePauseFailed', device, {
|
|
6830
|
+
pauseToken: pause.token,
|
|
6831
|
+
streamId: pause.streamId,
|
|
6832
|
+
stopError: pause.stopError
|
|
6833
|
+
});
|
|
6834
|
+
return buildLiveCapturePauseResult(pause, {
|
|
6835
|
+
ok: false,
|
|
6836
|
+
error: 'LIVE_CAPTURE_STOP_UNCONFIRMED'
|
|
6837
|
+
});
|
|
6838
|
+
}
|
|
6839
|
+
|
|
6840
|
+
pause.stopCommandId = safeString(stopResult.commandId, 128);
|
|
6841
|
+
if (pause.stopCommandId
|
|
6842
|
+
&& stopResult.alreadyStopped !== true
|
|
6843
|
+
&& stopResult.synthetic !== true) {
|
|
6844
|
+
const acknowledgement = await waitForCommandResult(
|
|
6845
|
+
device,
|
|
6846
|
+
pause.stopCommandId,
|
|
6847
|
+
liveCaptureStopAckTimeoutMs);
|
|
6848
|
+
device = devices.get(String(deviceId || '')) || device;
|
|
6849
|
+
if (device.liveCapturePause?.token !== pause.token) {
|
|
6850
|
+
return buildLiveCapturePauseResult(pause, {
|
|
6851
|
+
ok: false,
|
|
6852
|
+
error: 'LIVE_CAPTURE_PAUSE_LEASE_STALE'
|
|
6853
|
+
});
|
|
6854
|
+
}
|
|
6855
|
+
if (!acknowledgement.ok
|
|
6856
|
+
|| acknowledgement.result?.stream !== true
|
|
6857
|
+
|| acknowledgement.result?.stopped !== true) {
|
|
6858
|
+
pause.phase = 'stop-unconfirmed';
|
|
6859
|
+
pause.stopError = safeString(
|
|
6860
|
+
acknowledgement.error
|
|
6861
|
+
|| acknowledgement.result?.error
|
|
6862
|
+
|| 'stream-stop-not-confirmed',
|
|
6863
|
+
500);
|
|
6864
|
+
emitRemoteEvent('RemoteLiveCapturePauseFailed', device, {
|
|
6865
|
+
pauseToken: pause.token,
|
|
6866
|
+
streamId: pause.streamId,
|
|
6867
|
+
stopCommandId: pause.stopCommandId,
|
|
6868
|
+
stopError: pause.stopError
|
|
6869
|
+
});
|
|
6870
|
+
return buildLiveCapturePauseResult(pause, {
|
|
6871
|
+
ok: false,
|
|
6872
|
+
error: 'LIVE_CAPTURE_STOP_UNCONFIRMED'
|
|
6873
|
+
});
|
|
6874
|
+
}
|
|
6875
|
+
}
|
|
6876
|
+
|
|
6877
|
+
pause.phase = 'paused';
|
|
6878
|
+
pause.captureStopConfirmed = true;
|
|
6879
|
+
pause.captureStopConfirmedAt = new Date().toISOString();
|
|
6880
|
+
pause.stopError = '';
|
|
6881
|
+
deactivateDeviceLiveStreams(device, pause.reason, pause.captureStopConfirmedAt);
|
|
6882
|
+
emitRemoteEvent('RemoteLiveCapturePaused', device, {
|
|
6883
|
+
pauseToken: pause.token,
|
|
6884
|
+
pausedAt: pause.pausedAt,
|
|
6885
|
+
streamId: pause.streamId,
|
|
6886
|
+
streamPurpose: pause.streamPurpose,
|
|
6887
|
+
monitorIndex: pause.monitorIndex,
|
|
6888
|
+
stopCommandId: pause.stopCommandId,
|
|
6889
|
+
captureStopConfirmedAt: pause.captureStopConfirmedAt
|
|
6890
|
+
});
|
|
6891
|
+
return buildLiveCapturePauseResult(pause, {
|
|
6892
|
+
ok: true,
|
|
6893
|
+
alreadyStopped: stopResult.alreadyStopped === true
|
|
6894
|
+
});
|
|
6895
|
+
}
|
|
6896
|
+
|
|
6897
|
+
async function pauseLiveCapture(deviceId, options = {}) {
|
|
6898
|
+
const device = devices.get(String(deviceId || ''));
|
|
6899
|
+
if (!device) {
|
|
6900
|
+
return { ok: false, paused: false, error: 'device-not-found' };
|
|
6901
|
+
}
|
|
6902
|
+
const denied = policyError(device);
|
|
6903
|
+
if (denied) return { ok: false, paused: false, error: denied };
|
|
6904
|
+
|
|
6905
|
+
let pause = device.liveCapturePause;
|
|
6906
|
+
if (pause?.token && pause.captureStopConfirmed === true) {
|
|
6907
|
+
return buildLiveCapturePauseResult(pause, {
|
|
6908
|
+
ok: true,
|
|
6909
|
+
alreadyPaused: true
|
|
6910
|
+
});
|
|
6911
|
+
}
|
|
6912
|
+
if (pause?.stopPromise) {
|
|
6913
|
+
return pause.stopPromise;
|
|
6914
|
+
}
|
|
6915
|
+
|
|
6916
|
+
if (!pause?.token) {
|
|
6917
|
+
const activeLiveStream = device.activeLiveStream?.active === true
|
|
6918
|
+
? device.activeLiveStream
|
|
6919
|
+
: null;
|
|
6920
|
+
pause = {
|
|
6921
|
+
token: crypto.randomUUID(),
|
|
6922
|
+
pausedAt: new Date().toISOString(),
|
|
6923
|
+
reason: safeString(options.reason, 128) || 'user-video-pause',
|
|
6924
|
+
streamId: safeString(activeLiveStream?.streamId, 128),
|
|
6925
|
+
streamPurpose: safeString(activeLiveStream?.streamPurpose, 24),
|
|
6926
|
+
monitorIndex: Math.max(0, Math.floor(Number(activeLiveStream?.monitorIndex) || 0)),
|
|
6927
|
+
phase: 'pausing',
|
|
6928
|
+
captureStopConfirmed: false,
|
|
6929
|
+
captureStopConfirmedAt: '',
|
|
6930
|
+
stopCommandId: '',
|
|
6931
|
+
stopError: '',
|
|
6932
|
+
stopAttemptCount: 0
|
|
6933
|
+
};
|
|
6934
|
+
// Install the authoritative gate before asking the Agent to stop.
|
|
6935
|
+
// A late subscriber or watchdog therefore cannot reacquire FFmpeg
|
|
6936
|
+
// in the pause transition window.
|
|
6937
|
+
device.liveCapturePause = pause;
|
|
6938
|
+
}
|
|
6939
|
+
|
|
6940
|
+
const operation = confirmLiveCaptureStopped(deviceId, pause);
|
|
6941
|
+
pause.stopPromise = operation;
|
|
6942
|
+
try {
|
|
6943
|
+
return await operation;
|
|
6944
|
+
} finally {
|
|
6945
|
+
if (pause.stopPromise === operation) {
|
|
6946
|
+
delete pause.stopPromise;
|
|
6947
|
+
}
|
|
6948
|
+
}
|
|
6949
|
+
}
|
|
6950
|
+
|
|
6951
|
+
function resumeLiveCapture(deviceId, options = {}) {
|
|
6952
|
+
const device = devices.get(String(deviceId || ''));
|
|
6953
|
+
const denied = policyError(device);
|
|
6954
|
+
if (denied) return { ok: false, error: denied };
|
|
6955
|
+
const pause = device?.liveCapturePause;
|
|
6956
|
+
if (!pause?.token) {
|
|
6957
|
+
return { ok: true, paused: false, alreadyResumed: true };
|
|
6958
|
+
}
|
|
6959
|
+
if (pause.phase === 'pausing') {
|
|
6960
|
+
return {
|
|
6961
|
+
ok: false,
|
|
6962
|
+
paused: true,
|
|
6963
|
+
error: 'LIVE_CAPTURE_STOP_IN_PROGRESS',
|
|
6964
|
+
pauseToken: pause.token
|
|
6965
|
+
};
|
|
6966
|
+
}
|
|
6967
|
+
const pauseToken = safeString(options.pauseToken, 128);
|
|
6968
|
+
if (!pauseToken || pauseToken !== pause.token) {
|
|
6969
|
+
return {
|
|
6970
|
+
ok: false,
|
|
6971
|
+
error: 'LIVE_CAPTURE_PAUSE_TOKEN_MISMATCH'
|
|
6972
|
+
};
|
|
6973
|
+
}
|
|
6974
|
+
|
|
6975
|
+
device.liveCapturePause = null;
|
|
6976
|
+
emitRemoteEvent('RemoteLiveCaptureResumed', device, {
|
|
6977
|
+
pauseToken,
|
|
6978
|
+
resumedAt: new Date().toISOString(),
|
|
6979
|
+
streamId: pause.streamId,
|
|
6980
|
+
streamPurpose: pause.streamPurpose,
|
|
6981
|
+
monitorIndex: pause.monitorIndex
|
|
6982
|
+
});
|
|
6983
|
+
return {
|
|
6984
|
+
ok: true,
|
|
6985
|
+
paused: false,
|
|
6986
|
+
resumed: true,
|
|
6987
|
+
streamId: pause.streamId,
|
|
6988
|
+
streamPurpose: pause.streamPurpose,
|
|
6989
|
+
monitorIndex: pause.monitorIndex
|
|
6990
|
+
};
|
|
6991
|
+
}
|
|
6398
6992
|
|
|
6399
6993
|
function startAudioStream(deviceId, options = {}) {
|
|
6400
6994
|
const device = devices.get(String(deviceId || ''));
|
|
@@ -6556,15 +7150,18 @@ export function createRemoteHub(options = {}) {
|
|
|
6556
7150
|
sendCommand,
|
|
6557
7151
|
sendLegacyClientUpdate,
|
|
6558
7152
|
sendInputControl,
|
|
7153
|
+
releaseInputOwner,
|
|
6559
7154
|
notifyAgentProgress,
|
|
6560
7155
|
requestAgentTask,
|
|
6561
7156
|
requestAgentTaskBatch,
|
|
6562
7157
|
setHostTarget,
|
|
6563
7158
|
rotatePairingPin,
|
|
6564
|
-
requestThumbnail,
|
|
6565
|
-
startLiveStream,
|
|
6566
|
-
stopLiveStream,
|
|
6567
|
-
|
|
7159
|
+
requestThumbnail,
|
|
7160
|
+
startLiveStream,
|
|
7161
|
+
stopLiveStream,
|
|
7162
|
+
pauseLiveCapture,
|
|
7163
|
+
resumeLiveCapture,
|
|
7164
|
+
startAudioStream,
|
|
6568
7165
|
stopAudioStream,
|
|
6569
7166
|
getDeviceLiveFrame,
|
|
6570
7167
|
getDeviceThumbnail,
|