livedesk 0.1.437 → 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/client/package.json +5 -5
- package/hub/package.json +1 -1
- package/hub/src/remote-hub.js +707 -117
- 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,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,
|
|
@@ -1510,6 +1539,11 @@ export function createRemoteHub(options = {}) {
|
|
|
1510
1539
|
250,
|
|
1511
1540
|
30000,
|
|
1512
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);
|
|
1513
1547
|
const managerPackage = safeString(options.managerPackage ?? env.LIVEDESK_MANAGER_PACKAGE ?? env.MINDEXEC_MANAGER_PACKAGE ?? '@livedesk/hub', 128);
|
|
1514
1548
|
const managerVersion = safeString(options.managerVersion ?? env.LIVEDESK_MANAGER_VERSION ?? env.MINDEXEC_MANAGER_VERSION ?? '', 64);
|
|
1515
1549
|
const hostInstanceId = safeString(options.hostInstanceId ?? env.LIVEDESK_HUB_INSTANCE_ID ?? env.MINDEXEC_BRIDGE_INSTANCE_ID ?? crypto.randomUUID(), 128) || crypto.randomUUID();
|
|
@@ -1538,9 +1572,10 @@ export function createRemoteHub(options = {}) {
|
|
|
1538
1572
|
const sockets = new Map();
|
|
1539
1573
|
const inputSockets = new Map();
|
|
1540
1574
|
const frameSockets = new Map();
|
|
1541
|
-
const audioSockets = new Map();
|
|
1542
|
-
const fileSockets = new Map();
|
|
1543
|
-
const allSockets = new Set();
|
|
1575
|
+
const audioSockets = new Map();
|
|
1576
|
+
const fileSockets = new Map();
|
|
1577
|
+
const allSockets = new Set();
|
|
1578
|
+
const pendingCommandResultWaiters = new Map();
|
|
1544
1579
|
const duplicateDeviceLogAt = new Map();
|
|
1545
1580
|
let server = null;
|
|
1546
1581
|
let started = false;
|
|
@@ -2048,15 +2083,109 @@ export function createRemoteHub(options = {}) {
|
|
|
2048
2083
|
});
|
|
2049
2084
|
}
|
|
2050
2085
|
|
|
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
|
|
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 = {}) {
|
|
2060
2189
|
const now = new Date().toISOString();
|
|
2061
2190
|
const descriptor = buildRemoteFrameTransferDescriptor(mode, mode === 'thumbnail' ? 'thumbnail' : DEFAULT_REMOTE_FRAME_MODE);
|
|
2062
2191
|
const frameSeq = Number(device?.counters?.thumbnailFramesReceived || 0)
|
|
@@ -2177,6 +2306,7 @@ export function createRemoteHub(options = {}) {
|
|
|
2177
2306
|
existing.lastDisconnectReason = 'replaced-by-new-session';
|
|
2178
2307
|
failAllPendingTasks(existing, 'replaced-by-new-session');
|
|
2179
2308
|
failAllPendingInputFallbacks(existing, 'replaced-by-new-session');
|
|
2309
|
+
failPendingCommandResultWaiters(existing, 'replaced-by-new-session');
|
|
2180
2310
|
writeJsonLine(existing.socket, {
|
|
2181
2311
|
type: 'disconnect',
|
|
2182
2312
|
reason: 'replaced-by-new-session',
|
|
@@ -2716,9 +2846,11 @@ export function createRemoteHub(options = {}) {
|
|
|
2716
2846
|
: getSupportedRemoteFrameModeProfiles();
|
|
2717
2847
|
}
|
|
2718
2848
|
|
|
2719
|
-
const device = {
|
|
2720
|
-
socket,
|
|
2721
|
-
inputSocket: null,
|
|
2849
|
+
const device = {
|
|
2850
|
+
socket,
|
|
2851
|
+
inputSocket: null,
|
|
2852
|
+
inputOwnerConnectionId: '',
|
|
2853
|
+
inputOwnerBindingKey: '',
|
|
2722
2854
|
frameSocket: null,
|
|
2723
2855
|
audioSocket: null,
|
|
2724
2856
|
fileSocket: null,
|
|
@@ -2759,9 +2891,13 @@ export function createRemoteHub(options = {}) {
|
|
|
2759
2891
|
recentFramePayloads: {
|
|
2760
2892
|
thumbnail: [],
|
|
2761
2893
|
live: []
|
|
2762
|
-
},
|
|
2763
|
-
activeLiveStream: null,
|
|
2764
|
-
|
|
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,
|
|
2765
2901
|
latestAudioFrame: null,
|
|
2766
2902
|
latestAudioStatus: null,
|
|
2767
2903
|
udp: {
|
|
@@ -2950,10 +3086,11 @@ export function createRemoteHub(options = {}) {
|
|
|
2950
3086
|
closeInputSocket(device, reason);
|
|
2951
3087
|
closeFrameSocket(device, reason);
|
|
2952
3088
|
device.disconnectedAt = new Date().toISOString();
|
|
2953
|
-
device.lastDisconnectReason = reason;
|
|
3089
|
+
device.lastDisconnectReason = reason;
|
|
2954
3090
|
deactivateDeviceLiveStreams(device, reason, device.disconnectedAt);
|
|
2955
3091
|
failAllPendingTasks(device, 'device-disconnected');
|
|
2956
3092
|
failAllPendingInputFallbacks(device, 'device-disconnected');
|
|
3093
|
+
failPendingCommandResultWaiters(device, 'device-disconnected');
|
|
2957
3094
|
logWarn('remote', `device disconnected ${device.deviceName} (${deviceId}): ${reason}`);
|
|
2958
3095
|
emitRemoteEvent('RemoteDeviceDisconnected', device, { reason });
|
|
2959
3096
|
}
|
|
@@ -4044,6 +4181,11 @@ export function createRemoteHub(options = {}) {
|
|
|
4044
4181
|
&& frameCaptureGeneration === activeCaptureGeneration));
|
|
4045
4182
|
const readyFrameReceivedBeforeFrame = streamState.readyFrameReceived === true;
|
|
4046
4183
|
const captureTimingAvailable = hasFrameCaptureTiming(message);
|
|
4184
|
+
const videoTransport = transport === 'udp-p2p'
|
|
4185
|
+
? 'p2p-udp'
|
|
4186
|
+
: transport === 'ws-binary'
|
|
4187
|
+
? 'direct-ws'
|
|
4188
|
+
: 'direct-tcp';
|
|
4047
4189
|
device.latestLiveFrame = {
|
|
4048
4190
|
streamId,
|
|
4049
4191
|
frameSeq,
|
|
@@ -4080,9 +4222,10 @@ export function createRemoteHub(options = {}) {
|
|
|
4080
4222
|
agentPaceMs: Number.isFinite(Number(message.agentPaceMs)) ? Number(message.agentPaceMs) : 0,
|
|
4081
4223
|
agentQueueBeforePaceMs: Number.isFinite(Number(message.agentQueueBeforePaceMs)) ? Number(message.agentQueueBeforePaceMs) : 0,
|
|
4082
4224
|
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
|
-
|
|
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),
|
|
4086
4229
|
platformProfile: safeString(message.platformProfile, 80),
|
|
4087
4230
|
monitorIndex: Number.isFinite(Number(message.monitorIndex)) ? normalizeMonitorIndex(message.monitorIndex) : Number(streamState.monitorIndex || 0),
|
|
4088
4231
|
monitorCount: Number.isFinite(Number(message.monitorCount)) ? clampNumber(message.monitorCount, 1, 64, 1) : Number(streamState.monitorCount || 1),
|
|
@@ -4096,12 +4239,12 @@ export function createRemoteHub(options = {}) {
|
|
|
4096
4239
|
captureAverageMs: captureTimingAvailable ? readOptionalFrameStageMs(message, 'captureAverageMs') : undefined,
|
|
4097
4240
|
captureP95Ms: captureTimingAvailable ? readOptionalFrameStageMs(message, 'captureP95Ms') : undefined,
|
|
4098
4241
|
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,
|
|
4242
|
+
captureHelperRestarts: Number.isFinite(Number(message.captureHelperRestarts)) ? Number(message.captureHelperRestarts) : 0,
|
|
4243
|
+
capturedAt,
|
|
4244
|
+
receivedAt: device.lastSeenAt,
|
|
4245
|
+
byteLength,
|
|
4246
|
+
transport,
|
|
4247
|
+
contentHash,
|
|
4105
4248
|
captureMs: captureTimingAvailable ? readOptionalFrameStageMs(message, 'captureMs') : undefined,
|
|
4106
4249
|
captureStageMs: captureTimingAvailable ? readOptionalFrameStageMs(message, 'captureStageMs') : undefined,
|
|
4107
4250
|
convertMs: readFrameStageMs(message, 'convertMs'),
|
|
@@ -4110,10 +4253,20 @@ export function createRemoteHub(options = {}) {
|
|
|
4110
4253
|
captureSourceSkipped: Number.isFinite(Number(message.captureSourceSkipped)) ? Number(message.captureSourceSkipped) : 0,
|
|
4111
4254
|
deltaTileSize: Number.isFinite(Number(message.deltaTileSize)) ? Number(message.deltaTileSize) : 0,
|
|
4112
4255
|
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
|
-
|
|
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,
|
|
4117
4270
|
payload,
|
|
4118
4271
|
accessToken: createFrameAccessToken()
|
|
4119
4272
|
};
|
|
@@ -4276,7 +4429,7 @@ export function createRemoteHub(options = {}) {
|
|
|
4276
4429
|
return true;
|
|
4277
4430
|
}
|
|
4278
4431
|
|
|
4279
|
-
function handleAgentBinaryFrame(socket, state, header, framePayload) {
|
|
4432
|
+
function handleAgentBinaryFrame(socket, state, header, framePayload) {
|
|
4280
4433
|
if (!state.authenticated || !state.device) {
|
|
4281
4434
|
writeJsonLine(socket, { type: 'error', error: 'hello-required' });
|
|
4282
4435
|
socket.destroy();
|
|
@@ -4300,28 +4453,29 @@ export function createRemoteHub(options = {}) {
|
|
|
4300
4453
|
}
|
|
4301
4454
|
|
|
4302
4455
|
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
|
-
|
|
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
|
+
}
|
|
4325
4479
|
|
|
4326
4480
|
emitRemoteEvent('RemoteAgentMessageIgnored', device, {
|
|
4327
4481
|
messageType: safeString(header.type, 80),
|
|
@@ -4606,6 +4760,7 @@ export function createRemoteHub(options = {}) {
|
|
|
4606
4760
|
message.error || (message.type === 'command.error' ? 'command-failed' : ''),
|
|
4607
4761
|
500);
|
|
4608
4762
|
const result = message.result ?? null;
|
|
4763
|
+
settlePendingCommandResult(device, message);
|
|
4609
4764
|
handleInputFallbackCommandResult(device, message);
|
|
4610
4765
|
if (error || result?.ok === false || result?.status === 'failed') {
|
|
4611
4766
|
failPendingLiveStream(device, commandId, error || result?.error || 'stream-start-failed');
|
|
@@ -4659,10 +4814,11 @@ export function createRemoteHub(options = {}) {
|
|
|
4659
4814
|
socket.setNoDelay(true);
|
|
4660
4815
|
socket.setKeepAlive(true, heartbeatMs);
|
|
4661
4816
|
|
|
4662
|
-
const state = {
|
|
4663
|
-
authenticated: false,
|
|
4664
|
-
device: null,
|
|
4665
|
-
|
|
4817
|
+
const state = {
|
|
4818
|
+
authenticated: false,
|
|
4819
|
+
device: null,
|
|
4820
|
+
binaryTransport: 'ws-binary',
|
|
4821
|
+
inputOnly: false,
|
|
4666
4822
|
frameOnly: false,
|
|
4667
4823
|
audioOnly: false,
|
|
4668
4824
|
fileOnly: false,
|
|
@@ -4816,10 +4972,11 @@ export function createRemoteHub(options = {}) {
|
|
|
4816
4972
|
socket.setNoDelay(true);
|
|
4817
4973
|
socket.setKeepAlive(true, heartbeatMs);
|
|
4818
4974
|
|
|
4819
|
-
const state = {
|
|
4820
|
-
authenticated: false,
|
|
4821
|
-
device: null,
|
|
4822
|
-
|
|
4975
|
+
const state = {
|
|
4976
|
+
authenticated: false,
|
|
4977
|
+
device: null,
|
|
4978
|
+
binaryTransport: 'binary',
|
|
4979
|
+
inputOnly: false,
|
|
4823
4980
|
frameOnly: false,
|
|
4824
4981
|
audioOnly: false,
|
|
4825
4982
|
fileOnly: false,
|
|
@@ -5053,6 +5210,7 @@ export function createRemoteHub(options = {}) {
|
|
|
5053
5210
|
for (const device of devices.values()) {
|
|
5054
5211
|
failAllPendingTasks(device, 'hub-shutdown');
|
|
5055
5212
|
failAllPendingInputFallbacks(device, 'hub-shutdown');
|
|
5213
|
+
failPendingCommandResultWaiters(device, 'hub-shutdown');
|
|
5056
5214
|
if (device.socket && !device.socket.destroyed) {
|
|
5057
5215
|
writeJsonLine(device.socket, { type: 'disconnect', reason: 'hub-shutdown' });
|
|
5058
5216
|
device.socket.destroy();
|
|
@@ -5293,10 +5451,58 @@ export function createRemoteHub(options = {}) {
|
|
|
5293
5451
|
return { ok: false, error: 'device-not-found' };
|
|
5294
5452
|
}
|
|
5295
5453
|
|
|
5296
|
-
if (!device.connected) {
|
|
5297
|
-
return { ok: false, error: 'device-not-connected' };
|
|
5298
|
-
}
|
|
5299
|
-
|
|
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
|
+
|
|
5300
5506
|
const denied = policyError(device, 'allowControl');
|
|
5301
5507
|
if (denied) return { ok: false, error: denied };
|
|
5302
5508
|
|
|
@@ -5308,10 +5514,6 @@ export function createRemoteHub(options = {}) {
|
|
|
5308
5514
|
return { ok: false, error: 'device-input-control-unavailable' };
|
|
5309
5515
|
}
|
|
5310
5516
|
|
|
5311
|
-
const normalized = normalizeRemoteInputEvent(input);
|
|
5312
|
-
if (!normalized.type) {
|
|
5313
|
-
return { ok: false, error: 'missing-input-type' };
|
|
5314
|
-
}
|
|
5315
5517
|
const controlStream = getActiveControlStream(device);
|
|
5316
5518
|
if (!controlStream
|
|
5317
5519
|
|| !liveStreamHasCurrentFrame(controlStream)
|
|
@@ -5357,15 +5559,62 @@ export function createRemoteHub(options = {}) {
|
|
|
5357
5559
|
|
|
5358
5560
|
const inputSocket = device.inputSocket;
|
|
5359
5561
|
if (inputSocket && !inputSocket.destroyed) {
|
|
5360
|
-
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, {
|
|
5361
5606
|
type: 'input.control',
|
|
5362
5607
|
payload: {
|
|
5363
5608
|
...normalized,
|
|
5364
5609
|
hubForwardedAtEpochMs: Date.now(),
|
|
5365
5610
|
issuedAt: normalized.issuedAt || new Date().toISOString()
|
|
5366
5611
|
}
|
|
5367
|
-
});
|
|
5368
|
-
if (sent) {
|
|
5612
|
+
});
|
|
5613
|
+
if (sent) {
|
|
5614
|
+
if (normalized.hubConnectionId) {
|
|
5615
|
+
device.inputOwnerConnectionId = normalized.hubConnectionId;
|
|
5616
|
+
}
|
|
5617
|
+
device.inputOwnerBindingKey = activeInputBindingKey;
|
|
5369
5618
|
device.counters.commandsSent += 1;
|
|
5370
5619
|
device.inputLastSeenAt = new Date().toISOString();
|
|
5371
5620
|
return {
|
|
@@ -5378,9 +5627,16 @@ export function createRemoteHub(options = {}) {
|
|
|
5378
5627
|
};
|
|
5379
5628
|
}
|
|
5380
5629
|
|
|
5381
|
-
detachInputSocket(inputSocket, 'input-socket-write-failed');
|
|
5382
|
-
}
|
|
5383
|
-
|
|
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
|
+
|
|
5384
5640
|
const hubForwardedAtEpochMs = Date.now();
|
|
5385
5641
|
const fallback = sendCommand(deviceId, {
|
|
5386
5642
|
command: 'input.control',
|
|
@@ -5424,6 +5680,49 @@ export function createRemoteHub(options = {}) {
|
|
|
5424
5680
|
};
|
|
5425
5681
|
}
|
|
5426
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
|
+
|
|
5427
5726
|
function notifyAgentProgress(deviceIds, progress = {}) {
|
|
5428
5727
|
const targets = [...new Set((deviceIds || [])
|
|
5429
5728
|
.map(deviceId => safeString(deviceId, 128))
|
|
@@ -5829,24 +6128,42 @@ export function createRemoteHub(options = {}) {
|
|
|
5829
6128
|
streamId: stream.streamId,
|
|
5830
6129
|
commandId: key,
|
|
5831
6130
|
error: safeString(error, 500) || 'stream-start-failed'
|
|
5832
|
-
});
|
|
5833
|
-
return true;
|
|
5834
|
-
}
|
|
5835
|
-
|
|
5836
|
-
|
|
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
|
+
}
|
|
5837
6152
|
|
|
5838
6153
|
function deactivateDeviceLiveStreams(device, reason, stoppedAt = new Date().toISOString()) {
|
|
5839
6154
|
const streams = ensureDeviceLiveStreams(device);
|
|
5840
6155
|
for (const stream of streams.values()) {
|
|
5841
6156
|
clearPendingLiveStreamDescriptor(device, stream);
|
|
5842
6157
|
stream.active = false;
|
|
6158
|
+
stream.open = false;
|
|
5843
6159
|
stream.stoppedAt = stoppedAt;
|
|
5844
6160
|
stream.stopReason = reason;
|
|
5845
|
-
}
|
|
5846
|
-
if (device?.activeLiveStream) {
|
|
5847
|
-
device.activeLiveStream.active = false;
|
|
5848
|
-
device.activeLiveStream.
|
|
5849
|
-
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;
|
|
5850
6167
|
}
|
|
5851
6168
|
}
|
|
5852
6169
|
|
|
@@ -5868,6 +6185,48 @@ export function createRemoteHub(options = {}) {
|
|
|
5868
6185
|
safeString(stream?.streamPurpose, 24).toLowerCase() === 'control') || null;
|
|
5869
6186
|
}
|
|
5870
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
|
+
|
|
5871
6230
|
function nextCaptureGeneration(device) {
|
|
5872
6231
|
const current = Number(device?.captureGenerationCounter || 0);
|
|
5873
6232
|
const next = Number.isSafeInteger(current) && current > 0 ? current + 1 : 1;
|
|
@@ -6004,11 +6363,19 @@ export function createRemoteHub(options = {}) {
|
|
|
6004
6363
|
&& Number(activeLiveStream.monitorIndex || 0) === normalized.monitorIndex;
|
|
6005
6364
|
}
|
|
6006
6365
|
|
|
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?.
|
|
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) {
|
|
6012
6379
|
if (!readCapabilityFlag(device.capabilities, 'liveStream')) {
|
|
6013
6380
|
return { ok: false, error: 'device-live-stream-unavailable' };
|
|
6014
6381
|
}
|
|
@@ -6019,6 +6386,8 @@ export function createRemoteHub(options = {}) {
|
|
|
6019
6386
|
if (modePolicyError) return { ok: false, error: modePolicyError };
|
|
6020
6387
|
const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
|
|
6021
6388
|
const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
|
|
6389
|
+
const captureClaim = claimSingleLiveCapture(deviceId, device, streamId, streamPurpose);
|
|
6390
|
+
if (!captureClaim.ok) return captureClaim;
|
|
6022
6391
|
const activeLiveStream = getDeviceLiveStream(device, streamId);
|
|
6023
6392
|
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
6024
6393
|
if (activeLiveStream
|
|
@@ -6118,6 +6487,8 @@ export function createRemoteHub(options = {}) {
|
|
|
6118
6487
|
if (modePolicyError) return { ok: false, error: modePolicyError };
|
|
6119
6488
|
const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
|
|
6120
6489
|
const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
|
|
6490
|
+
const captureClaim = claimSingleLiveCapture(deviceId, device, streamId, streamPurpose);
|
|
6491
|
+
if (!captureClaim.ok) return captureClaim;
|
|
6121
6492
|
const activeLiveStream = getDeviceLiveStream(device, streamId);
|
|
6122
6493
|
const pendingDescriptor = getPendingLiveStreamDescriptor(activeLiveStream);
|
|
6123
6494
|
if (pendingDescriptor?.commandId) {
|
|
@@ -6335,14 +6706,16 @@ export function createRemoteHub(options = {}) {
|
|
|
6335
6706
|
};
|
|
6336
6707
|
}
|
|
6337
6708
|
|
|
6338
|
-
function stopLiveStream(deviceId, options = {}) {
|
|
6709
|
+
function stopLiveStream(deviceId, options = {}) {
|
|
6339
6710
|
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 =
|
|
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 || '';
|
|
6344
6717
|
const streamState = getDeviceLiveStream(device, streamId);
|
|
6345
|
-
if (streamState && streamState.active !== true) {
|
|
6718
|
+
if (streamState && streamState.active !== true && options.forceCommand !== true) {
|
|
6346
6719
|
clearPendingLiveStreamDescriptor(device, streamState);
|
|
6347
6720
|
return {
|
|
6348
6721
|
ok: true,
|
|
@@ -6359,9 +6732,10 @@ export function createRemoteHub(options = {}) {
|
|
|
6359
6732
|
device.counters.commandsSent += 1;
|
|
6360
6733
|
if (streamState) {
|
|
6361
6734
|
streamState.active = false;
|
|
6735
|
+
streamState.open = false;
|
|
6362
6736
|
streamState.stoppedAt = new Date().toISOString();
|
|
6363
|
-
streamState.stopReason = 'manager-request';
|
|
6364
|
-
}
|
|
6737
|
+
streamState.stopReason = safeString(options.reason, 128) || 'manager-request';
|
|
6738
|
+
}
|
|
6365
6739
|
device.counters.liveStreamsStopped += 1;
|
|
6366
6740
|
emitRemoteEvent('RemoteLiveStreamStopped', device, {
|
|
6367
6741
|
streamId,
|
|
@@ -6391,17 +6765,230 @@ export function createRemoteHub(options = {}) {
|
|
|
6391
6765
|
|
|
6392
6766
|
if (streamState) {
|
|
6393
6767
|
streamState.active = false;
|
|
6768
|
+
streamState.open = false;
|
|
6394
6769
|
streamState.stoppedAt = new Date().toISOString();
|
|
6395
|
-
streamState.stopReason = 'manager-request';
|
|
6396
|
-
}
|
|
6770
|
+
streamState.stopReason = safeString(options.reason, 128) || 'manager-request';
|
|
6771
|
+
}
|
|
6397
6772
|
device.counters.liveStreamsStopped += 1;
|
|
6398
6773
|
emitRemoteEvent('RemoteLiveStreamStopped', device, {
|
|
6399
6774
|
streamId,
|
|
6400
6775
|
commandId
|
|
6401
6776
|
});
|
|
6402
6777
|
|
|
6403
|
-
return { ok: true, commandId, streamId };
|
|
6404
|
-
}
|
|
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
|
+
}
|
|
6405
6992
|
|
|
6406
6993
|
function startAudioStream(deviceId, options = {}) {
|
|
6407
6994
|
const device = devices.get(String(deviceId || ''));
|
|
@@ -6563,15 +7150,18 @@ export function createRemoteHub(options = {}) {
|
|
|
6563
7150
|
sendCommand,
|
|
6564
7151
|
sendLegacyClientUpdate,
|
|
6565
7152
|
sendInputControl,
|
|
7153
|
+
releaseInputOwner,
|
|
6566
7154
|
notifyAgentProgress,
|
|
6567
7155
|
requestAgentTask,
|
|
6568
7156
|
requestAgentTaskBatch,
|
|
6569
7157
|
setHostTarget,
|
|
6570
7158
|
rotatePairingPin,
|
|
6571
|
-
requestThumbnail,
|
|
6572
|
-
startLiveStream,
|
|
6573
|
-
stopLiveStream,
|
|
6574
|
-
|
|
7159
|
+
requestThumbnail,
|
|
7160
|
+
startLiveStream,
|
|
7161
|
+
stopLiveStream,
|
|
7162
|
+
pauseLiveCapture,
|
|
7163
|
+
resumeLiveCapture,
|
|
7164
|
+
startAudioStream,
|
|
6575
7165
|
stopAudioStream,
|
|
6576
7166
|
getDeviceLiveFrame,
|
|
6577
7167
|
getDeviceThumbnail,
|