livedesk 0.1.367 → 0.1.369
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/hub/src/mode4-atlas-pool.js +31 -8
- package/hub/src/mode4-atlas.js +5 -1
- package/hub/src/remote-hub.js +859 -357
- package/hub/src/server.js +131 -60
- package/package.json +5 -5
- package/web/dist/assets/index-CvOqfC-o.js +16 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/index-BGyMH9o5.js +0 -16
package/hub/src/remote-hub.js
CHANGED
|
@@ -24,9 +24,10 @@ const RECENT_LIVE_FRAME_CACHE_LIMIT = 1;
|
|
|
24
24
|
const RECENT_THUMBNAIL_FRAME_CACHE_MAX_BYTES = 2 * 1024 * 1024;
|
|
25
25
|
const RECENT_LIVE_FRAME_CACHE_MAX_BYTES = 8 * 1024 * 1024;
|
|
26
26
|
const LIVE_STREAM_PENDING_REUSE_MS = 5000;
|
|
27
|
-
const LIVE_STREAM_REPLACEMENT_PENDING_MS = 7000;
|
|
28
|
-
const LIVE_STREAM_MIN_FRESH_MS = 3000;
|
|
29
|
-
const LIVE_STREAM_MAX_FRESH_MS = 12000;
|
|
27
|
+
const LIVE_STREAM_REPLACEMENT_PENDING_MS = 7000;
|
|
28
|
+
const LIVE_STREAM_MIN_FRESH_MS = 3000;
|
|
29
|
+
const LIVE_STREAM_MAX_FRESH_MS = 12000;
|
|
30
|
+
const DEFAULT_REMOTE_INPUT_ACK_TIMEOUT_MS = 5000;
|
|
30
31
|
const REMOTE_PROTOCOL_VERSION = 2;
|
|
31
32
|
const REMOTE_AGENT_PROTOCOL = 'mindexec.remote.agent';
|
|
32
33
|
const REMOTE_FRAME_PROTOCOL = 'mindexec.remote.frame.v2';
|
|
@@ -643,6 +644,7 @@ function normalizeRemoteInputEvent(value = {}) {
|
|
|
643
644
|
controlCommandId: safeString(input.controlCommandId || input.commandId || input.ControlCommandId || input.CommandId, 128),
|
|
644
645
|
captureGeneration: Number.isSafeInteger(captureGeneration) && captureGeneration > 0 ? captureGeneration : 0,
|
|
645
646
|
hubConnectionId: safeString(input.hubConnectionId || input.HubConnectionId, 128),
|
|
647
|
+
inputEventId: safeString(input.inputEventId || input.InputEventId, 128) || crypto.randomUUID(),
|
|
646
648
|
inputSeq: Number.isSafeInteger(inputSeq) && inputSeq > 0 ? inputSeq : 0,
|
|
647
649
|
requestAck: input.requestAck !== false && input.RequestAck !== false,
|
|
648
650
|
issuedAtEpochMs: Number.isFinite(issuedAtEpochMs) ? issuedAtEpochMs : 0,
|
|
@@ -1430,11 +1432,16 @@ export function createRemoteHub(options = {}) {
|
|
|
1430
1432
|
const host = safeString(env.REMOTE_HUB_HOST || DEFAULT_REMOTE_HUB_HOST, 128);
|
|
1431
1433
|
const requestedPort = normalizePort(env.REMOTE_HUB_PORT || DEFAULT_REMOTE_HUB_PORT);
|
|
1432
1434
|
const heartbeatMs = clampNumber(env.REMOTE_HUB_HEARTBEAT_MS, 1000, 60000, DEFAULT_HEARTBEAT_MS);
|
|
1433
|
-
const taskTimeoutMs = clampNumber(
|
|
1434
|
-
options.taskTimeoutMs ?? env.REMOTE_HUB_TASK_TIMEOUT_MS,
|
|
1435
|
-
50,
|
|
1436
|
-
30 * 60 * 1000,
|
|
1437
|
-
DEFAULT_AGENT_TASK_TIMEOUT_MS);
|
|
1435
|
+
const taskTimeoutMs = clampNumber(
|
|
1436
|
+
options.taskTimeoutMs ?? env.REMOTE_HUB_TASK_TIMEOUT_MS,
|
|
1437
|
+
50,
|
|
1438
|
+
30 * 60 * 1000,
|
|
1439
|
+
DEFAULT_AGENT_TASK_TIMEOUT_MS);
|
|
1440
|
+
const inputAckTimeoutMs = clampNumber(
|
|
1441
|
+
options.inputAckTimeoutMs ?? env.REMOTE_HUB_INPUT_ACK_TIMEOUT_MS,
|
|
1442
|
+
250,
|
|
1443
|
+
30000,
|
|
1444
|
+
DEFAULT_REMOTE_INPUT_ACK_TIMEOUT_MS);
|
|
1438
1445
|
const managerPackage = safeString(options.managerPackage ?? env.LIVEDESK_MANAGER_PACKAGE ?? env.MINDEXEC_MANAGER_PACKAGE ?? '@livedesk/hub', 128);
|
|
1439
1446
|
const managerVersion = safeString(options.managerVersion ?? env.LIVEDESK_MANAGER_VERSION ?? env.MINDEXEC_MANAGER_VERSION ?? '', 64);
|
|
1440
1447
|
const hostInstanceId = safeString(options.hostInstanceId ?? env.LIVEDESK_HUB_INSTANCE_ID ?? env.MINDEXEC_BRIDGE_INSTANCE_ID ?? crypto.randomUUID(), 128) || crypto.randomUUID();
|
|
@@ -1976,9 +1983,11 @@ export function createRemoteHub(options = {}) {
|
|
|
1976
1983
|
+ Number(device?.counters?.liveFramesReceived || 0)
|
|
1977
1984
|
+ 1;
|
|
1978
1985
|
return {
|
|
1979
|
-
streamId: safeString(streamId, 128) || `${mode}-${Date.now()}`,
|
|
1980
|
-
frameSeq,
|
|
1981
|
-
commandId: safeString(options.commandId, 128),
|
|
1986
|
+
streamId: safeString(streamId, 128) || `${mode}-${Date.now()}`,
|
|
1987
|
+
frameSeq,
|
|
1988
|
+
commandId: safeString(options.commandId, 128),
|
|
1989
|
+
captureGeneration: Number(options.captureGeneration || 0),
|
|
1990
|
+
currentGenerationVerified: true,
|
|
1982
1991
|
width: clampNumber(options.width, 2, 3840, mode === 'thumbnail' ? 360 : 640),
|
|
1983
1992
|
height: clampNumber(options.height, 1, 2160, mode === 'thumbnail' ? 220 : 360),
|
|
1984
1993
|
mimeType: 'image/png',
|
|
@@ -2043,10 +2052,11 @@ export function createRemoteHub(options = {}) {
|
|
|
2043
2052
|
commandId: options.commandId || streamState.commandId,
|
|
2044
2053
|
width: options.maxWidth || 960,
|
|
2045
2054
|
height: options.maxHeight || 540,
|
|
2046
|
-
monitorIndex: options.monitorIndex ?? streamState.monitorIndex ?? 0,
|
|
2047
|
-
monitorCount: options.monitorCount ?? streamState.monitorCount ?? 1,
|
|
2048
|
-
fps: options.fps || streamState.fps
|
|
2049
|
-
|
|
2055
|
+
monitorIndex: options.monitorIndex ?? streamState.monitorIndex ?? 0,
|
|
2056
|
+
monitorCount: options.monitorCount ?? streamState.monitorCount ?? 1,
|
|
2057
|
+
fps: options.fps || streamState.fps,
|
|
2058
|
+
captureGeneration: streamState.captureGeneration
|
|
2059
|
+
});
|
|
2050
2060
|
device.latestLiveFrame = frame;
|
|
2051
2061
|
rememberRecentFramePayload(device, 'live', frame);
|
|
2052
2062
|
emitFrame({
|
|
@@ -2081,9 +2091,10 @@ export function createRemoteHub(options = {}) {
|
|
|
2081
2091
|
return;
|
|
2082
2092
|
}
|
|
2083
2093
|
|
|
2084
|
-
existing.lastDisconnectReason = 'replaced-by-new-session';
|
|
2085
|
-
failAllPendingTasks(existing, 'replaced-by-new-session');
|
|
2086
|
-
|
|
2094
|
+
existing.lastDisconnectReason = 'replaced-by-new-session';
|
|
2095
|
+
failAllPendingTasks(existing, 'replaced-by-new-session');
|
|
2096
|
+
failAllPendingInputFallbacks(existing, 'replaced-by-new-session');
|
|
2097
|
+
writeJsonLine(existing.socket, {
|
|
2087
2098
|
type: 'disconnect',
|
|
2088
2099
|
reason: 'replaced-by-new-session',
|
|
2089
2100
|
nextSessionId
|
|
@@ -2856,9 +2867,10 @@ export function createRemoteHub(options = {}) {
|
|
|
2856
2867
|
closeFrameSocket(device, reason);
|
|
2857
2868
|
device.disconnectedAt = new Date().toISOString();
|
|
2858
2869
|
device.lastDisconnectReason = reason;
|
|
2859
|
-
deactivateDeviceLiveStreams(device, reason, device.disconnectedAt);
|
|
2860
|
-
failAllPendingTasks(device, 'device-disconnected');
|
|
2861
|
-
|
|
2870
|
+
deactivateDeviceLiveStreams(device, reason, device.disconnectedAt);
|
|
2871
|
+
failAllPendingTasks(device, 'device-disconnected');
|
|
2872
|
+
failAllPendingInputFallbacks(device, 'device-disconnected');
|
|
2873
|
+
logWarn('remote', `device disconnected ${device.deviceName} (${deviceId}): ${reason}`);
|
|
2862
2874
|
emitRemoteEvent('RemoteDeviceDisconnected', device, { reason });
|
|
2863
2875
|
}
|
|
2864
2876
|
|
|
@@ -3116,10 +3128,10 @@ export function createRemoteHub(options = {}) {
|
|
|
3116
3128
|
return task;
|
|
3117
3129
|
}
|
|
3118
3130
|
|
|
3119
|
-
function failAllPendingTasks(device, error = 'device-disconnected') {
|
|
3120
|
-
if (!device?.pendingTaskCommands) {
|
|
3121
|
-
return 0;
|
|
3122
|
-
}
|
|
3131
|
+
function failAllPendingTasks(device, error = 'device-disconnected') {
|
|
3132
|
+
if (!device?.pendingTaskCommands) {
|
|
3133
|
+
return 0;
|
|
3134
|
+
}
|
|
3123
3135
|
|
|
3124
3136
|
const commandIds = [...device.pendingTaskCommands.keys()];
|
|
3125
3137
|
let failed = 0;
|
|
@@ -3127,11 +3139,263 @@ export function createRemoteHub(options = {}) {
|
|
|
3127
3139
|
if (failPendingTask(device, commandId, error)) {
|
|
3128
3140
|
failed += 1;
|
|
3129
3141
|
}
|
|
3130
|
-
}
|
|
3131
|
-
return failed;
|
|
3132
|
-
}
|
|
3133
|
-
|
|
3134
|
-
function
|
|
3142
|
+
}
|
|
3143
|
+
return failed;
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
function ensurePendingInputFallbacks(device) {
|
|
3147
|
+
if (!(device?.pendingInputFallbacks instanceof Map)) {
|
|
3148
|
+
Object.defineProperty(device, 'pendingInputFallbacks', {
|
|
3149
|
+
value: new Map(),
|
|
3150
|
+
enumerable: false,
|
|
3151
|
+
configurable: true,
|
|
3152
|
+
writable: true
|
|
3153
|
+
});
|
|
3154
|
+
}
|
|
3155
|
+
if (!(device?.completedInputFallbacks instanceof Map)) {
|
|
3156
|
+
Object.defineProperty(device, 'completedInputFallbacks', {
|
|
3157
|
+
value: new Map(),
|
|
3158
|
+
enumerable: false,
|
|
3159
|
+
configurable: true,
|
|
3160
|
+
writable: true
|
|
3161
|
+
});
|
|
3162
|
+
}
|
|
3163
|
+
return device.pendingInputFallbacks;
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
function pruneCompletedInputFallbacks(device, now = Date.now()) {
|
|
3167
|
+
ensurePendingInputFallbacks(device);
|
|
3168
|
+
for (const [key, expiresAt] of device.completedInputFallbacks.entries()) {
|
|
3169
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= now) {
|
|
3170
|
+
device.completedInputFallbacks.delete(key);
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
function getInputFallbackCompletionKeys(commandId, inputSeq) {
|
|
3176
|
+
const keys = [];
|
|
3177
|
+
const normalizedCommandId = safeString(commandId, 128);
|
|
3178
|
+
const normalizedInputSeq = Number(inputSeq || 0);
|
|
3179
|
+
if (normalizedCommandId) {
|
|
3180
|
+
keys.push(`command:${normalizedCommandId}`);
|
|
3181
|
+
}
|
|
3182
|
+
if (Number.isSafeInteger(normalizedInputSeq) && normalizedInputSeq > 0) {
|
|
3183
|
+
keys.push(`seq:${normalizedInputSeq}`);
|
|
3184
|
+
}
|
|
3185
|
+
return keys;
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
function rememberCompletedInputFallback(device, pending) {
|
|
3189
|
+
pruneCompletedInputFallbacks(device);
|
|
3190
|
+
const expiresAt = Date.now() + Math.max(inputAckTimeoutMs * 2, 10000);
|
|
3191
|
+
for (const key of getInputFallbackCompletionKeys(pending?.commandId, pending?.inputSeq)) {
|
|
3192
|
+
device.completedInputFallbacks.set(key, expiresAt);
|
|
3193
|
+
}
|
|
3194
|
+
while (device.completedInputFallbacks.size > 256) {
|
|
3195
|
+
const oldestKey = device.completedInputFallbacks.keys().next().value;
|
|
3196
|
+
if (!oldestKey) {
|
|
3197
|
+
break;
|
|
3198
|
+
}
|
|
3199
|
+
device.completedInputFallbacks.delete(oldestKey);
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
function wasInputFallbackCompleted(device, commandId, inputSeq) {
|
|
3204
|
+
pruneCompletedInputFallbacks(device);
|
|
3205
|
+
return getInputFallbackCompletionKeys(commandId, inputSeq)
|
|
3206
|
+
.some(key => device.completedInputFallbacks.has(key));
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
function findPendingInputFallback(device, commandId, inputSeq) {
|
|
3210
|
+
const pendingFallbacks = ensurePendingInputFallbacks(device);
|
|
3211
|
+
const key = safeString(commandId, 128);
|
|
3212
|
+
if (key && pendingFallbacks.has(key)) {
|
|
3213
|
+
return pendingFallbacks.get(key);
|
|
3214
|
+
}
|
|
3215
|
+
const sequence = Number(inputSeq || 0);
|
|
3216
|
+
if (!Number.isSafeInteger(sequence) || sequence <= 0) {
|
|
3217
|
+
return null;
|
|
3218
|
+
}
|
|
3219
|
+
for (const pending of pendingFallbacks.values()) {
|
|
3220
|
+
if (Number(pending?.inputSeq || 0) === sequence) {
|
|
3221
|
+
return pending;
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
return null;
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
function takePendingInputFallback(device, commandId, inputSeq) {
|
|
3228
|
+
const pending = findPendingInputFallback(device, commandId, inputSeq);
|
|
3229
|
+
if (!pending) {
|
|
3230
|
+
return null;
|
|
3231
|
+
}
|
|
3232
|
+
if (pending.timeoutTimer) {
|
|
3233
|
+
clearTimeout(pending.timeoutTimer);
|
|
3234
|
+
pending.timeoutTimer = null;
|
|
3235
|
+
}
|
|
3236
|
+
ensurePendingInputFallbacks(device).delete(pending.commandId);
|
|
3237
|
+
rememberCompletedInputFallback(device, pending);
|
|
3238
|
+
return pending;
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
function buildRemoteInputOutcome(device, message = {}, pending = null) {
|
|
3242
|
+
const result = message?.result && typeof message.result === 'object'
|
|
3243
|
+
? message.result
|
|
3244
|
+
: {};
|
|
3245
|
+
const inputSeq = Number(
|
|
3246
|
+
message.inputSeq
|
|
3247
|
+
?? message.InputSeq
|
|
3248
|
+
?? result.inputSeq
|
|
3249
|
+
?? result.InputSeq
|
|
3250
|
+
?? pending?.inputSeq
|
|
3251
|
+
?? 0) || 0;
|
|
3252
|
+
return {
|
|
3253
|
+
deviceId: device.deviceId,
|
|
3254
|
+
commandId: safeString(message.commandId || message.CommandId || pending?.commandId, 128),
|
|
3255
|
+
inputSeq,
|
|
3256
|
+
inputType: safeString(
|
|
3257
|
+
message.inputType
|
|
3258
|
+
|| message.InputType
|
|
3259
|
+
|| result.inputType
|
|
3260
|
+
|| result.InputType
|
|
3261
|
+
|| pending?.inputType,
|
|
3262
|
+
48),
|
|
3263
|
+
issuedAtEpochMs: Number(
|
|
3264
|
+
message.issuedAtEpochMs
|
|
3265
|
+
?? message.IssuedAtEpochMs
|
|
3266
|
+
?? result.issuedAtEpochMs
|
|
3267
|
+
?? result.IssuedAtEpochMs
|
|
3268
|
+
?? pending?.issuedAtEpochMs
|
|
3269
|
+
?? 0) || 0,
|
|
3270
|
+
hubReceivedAtEpochMs: Number(
|
|
3271
|
+
message.hubReceivedAtEpochMs
|
|
3272
|
+
?? message.HubReceivedAtEpochMs
|
|
3273
|
+
?? result.hubReceivedAtEpochMs
|
|
3274
|
+
?? result.HubReceivedAtEpochMs
|
|
3275
|
+
?? pending?.hubReceivedAtEpochMs
|
|
3276
|
+
?? 0) || 0,
|
|
3277
|
+
hubForwardedAtEpochMs: Number(
|
|
3278
|
+
message.hubForwardedAtEpochMs
|
|
3279
|
+
?? message.HubForwardedAtEpochMs
|
|
3280
|
+
?? result.hubForwardedAtEpochMs
|
|
3281
|
+
?? result.HubForwardedAtEpochMs
|
|
3282
|
+
?? pending?.hubForwardedAtEpochMs
|
|
3283
|
+
?? 0) || 0,
|
|
3284
|
+
fallback: pending?.fallback === true,
|
|
3285
|
+
hubAcknowledgedAtEpochMs: Date.now()
|
|
3286
|
+
};
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
function emitRemoteInputApplied(device, message = {}, pending = null) {
|
|
3290
|
+
const outcome = buildRemoteInputOutcome(device, message, pending);
|
|
3291
|
+
emitEvent('RemoteInputApplied', {
|
|
3292
|
+
...outcome,
|
|
3293
|
+
agentApplyMs: Number(
|
|
3294
|
+
message.agentApplyMs
|
|
3295
|
+
?? message.AgentApplyMs
|
|
3296
|
+
?? message.result?.agentApplyMs
|
|
3297
|
+
?? message.result?.AgentApplyMs
|
|
3298
|
+
?? 0) || 0
|
|
3299
|
+
});
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
function emitRemoteInputError(device, message = {}, pending = null, fallbackError = 'remote-input-failed') {
|
|
3303
|
+
const outcome = buildRemoteInputOutcome(device, message, pending);
|
|
3304
|
+
emitEvent('RemoteInputError', {
|
|
3305
|
+
...outcome,
|
|
3306
|
+
error: safeString(
|
|
3307
|
+
message.error
|
|
3308
|
+
|| message.Error
|
|
3309
|
+
|| message.result?.error
|
|
3310
|
+
|| message.result?.Error
|
|
3311
|
+
|| fallbackError,
|
|
3312
|
+
600) || fallbackError,
|
|
3313
|
+
failedAtEpochMs: Number(
|
|
3314
|
+
message.failedAtEpochMs
|
|
3315
|
+
?? message.FailedAtEpochMs
|
|
3316
|
+
?? message.result?.failedAtEpochMs
|
|
3317
|
+
?? message.result?.FailedAtEpochMs
|
|
3318
|
+
?? 0) || 0
|
|
3319
|
+
});
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
function handleRemoteInputOutcome(device, message = {}, source = 'input') {
|
|
3323
|
+
const commandId = safeString(message.commandId || message.CommandId, 128);
|
|
3324
|
+
const inputSeq = Number(message.inputSeq ?? message.InputSeq ?? 0) || 0;
|
|
3325
|
+
const pending = takePendingInputFallback(device, commandId, inputSeq);
|
|
3326
|
+
if (!pending
|
|
3327
|
+
&& source === 'main'
|
|
3328
|
+
&& wasInputFallbackCompleted(device, commandId, inputSeq)) {
|
|
3329
|
+
return false;
|
|
3330
|
+
}
|
|
3331
|
+
// Main-channel input acknowledgements only exist for a tracked
|
|
3332
|
+
// compatibility fallback. Ignore stale/unmatched messages so an old
|
|
3333
|
+
// session cannot acknowledge input from the current control session.
|
|
3334
|
+
if (!pending && source === 'main') {
|
|
3335
|
+
return false;
|
|
3336
|
+
}
|
|
3337
|
+
if (message.type === 'input.error') {
|
|
3338
|
+
emitRemoteInputError(device, message, pending);
|
|
3339
|
+
} else {
|
|
3340
|
+
emitRemoteInputApplied(device, message, pending);
|
|
3341
|
+
}
|
|
3342
|
+
return true;
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
function handleInputFallbackCommandResult(device, message = {}) {
|
|
3346
|
+
const commandId = safeString(message.commandId || message.CommandId, 128);
|
|
3347
|
+
const pending = findPendingInputFallback(device, commandId, 0);
|
|
3348
|
+
if (!pending) {
|
|
3349
|
+
return false;
|
|
3350
|
+
}
|
|
3351
|
+
const completed = takePendingInputFallback(device, commandId, pending.inputSeq);
|
|
3352
|
+
const result = message.result && typeof message.result === 'object' ? message.result : {};
|
|
3353
|
+
const error = safeString(message.error || message.Error || result.error || result.Error, 600);
|
|
3354
|
+
const failed = message.type === 'command.error'
|
|
3355
|
+
|| !!error
|
|
3356
|
+
|| result.ok === false
|
|
3357
|
+
|| safeString(result.status, 40).toLowerCase() === 'failed';
|
|
3358
|
+
if (failed) {
|
|
3359
|
+
emitRemoteInputError(device, { ...message, error: error || 'remote-input-failed' }, completed);
|
|
3360
|
+
} else {
|
|
3361
|
+
emitRemoteInputApplied(device, message, completed);
|
|
3362
|
+
}
|
|
3363
|
+
return true;
|
|
3364
|
+
}
|
|
3365
|
+
|
|
3366
|
+
function schedulePendingInputFallback(device, pending) {
|
|
3367
|
+
const pendingFallbacks = ensurePendingInputFallbacks(device);
|
|
3368
|
+
const existing = pendingFallbacks.get(pending.commandId);
|
|
3369
|
+
if (existing?.timeoutTimer) {
|
|
3370
|
+
clearTimeout(existing.timeoutTimer);
|
|
3371
|
+
}
|
|
3372
|
+
pendingFallbacks.set(pending.commandId, pending);
|
|
3373
|
+
pending.timeoutTimer = setTimeout(() => {
|
|
3374
|
+
const timedOut = takePendingInputFallback(device, pending.commandId, pending.inputSeq);
|
|
3375
|
+
if (timedOut) {
|
|
3376
|
+
emitRemoteInputError(device, { error: 'remote-input-timeout' }, timedOut, 'remote-input-timeout');
|
|
3377
|
+
}
|
|
3378
|
+
}, inputAckTimeoutMs);
|
|
3379
|
+
pending.timeoutTimer.unref?.();
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3382
|
+
function failAllPendingInputFallbacks(device, error = 'device-disconnected') {
|
|
3383
|
+
if (!device || typeof device !== 'object') {
|
|
3384
|
+
return 0;
|
|
3385
|
+
}
|
|
3386
|
+
const pendingFallbacks = ensurePendingInputFallbacks(device);
|
|
3387
|
+
const pending = [...pendingFallbacks.values()];
|
|
3388
|
+
for (const item of pending) {
|
|
3389
|
+
const failed = takePendingInputFallback(device, item.commandId, item.inputSeq);
|
|
3390
|
+
if (failed) {
|
|
3391
|
+
emitRemoteInputError(device, { error }, failed, error);
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
device.completedInputFallbacks.clear();
|
|
3395
|
+
return pending.length;
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
function scheduleTaskTimeout(device, task) {
|
|
3135
3399
|
const commandId = safeString(task?.commandId, 128);
|
|
3136
3400
|
if (!device?.pendingTaskTimers || !commandId) {
|
|
3137
3401
|
return;
|
|
@@ -3370,10 +3634,108 @@ export function createRemoteHub(options = {}) {
|
|
|
3370
3634
|
return true;
|
|
3371
3635
|
}
|
|
3372
3636
|
|
|
3373
|
-
function
|
|
3374
|
-
const
|
|
3375
|
-
const
|
|
3376
|
-
if (!
|
|
3637
|
+
function promotePendingLiveStream(device, streamState, message, transport) {
|
|
3638
|
+
const pending = getPendingLiveStreamDescriptor(streamState);
|
|
3639
|
+
const commandId = safeString(message.commandId, 128);
|
|
3640
|
+
if (!pending || !commandId || commandId !== safeString(pending.commandId, 128)) {
|
|
3641
|
+
return null;
|
|
3642
|
+
}
|
|
3643
|
+
const messageCaptureGeneration = Number(message.captureGeneration || 0);
|
|
3644
|
+
const pendingCaptureGeneration = Number(pending.captureGeneration || 0);
|
|
3645
|
+
if (messageCaptureGeneration > 0
|
|
3646
|
+
&& pendingCaptureGeneration > 0
|
|
3647
|
+
&& messageCaptureGeneration !== pendingCaptureGeneration) {
|
|
3648
|
+
emitRemoteEvent('RemoteLiveStreamOpenIgnored', device, {
|
|
3649
|
+
reason: 'stale-pending-capture-generation',
|
|
3650
|
+
streamId: streamState.streamId,
|
|
3651
|
+
commandId,
|
|
3652
|
+
captureGeneration: messageCaptureGeneration,
|
|
3653
|
+
pendingCaptureGeneration,
|
|
3654
|
+
transport
|
|
3655
|
+
});
|
|
3656
|
+
return null;
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
const previousCommandId = safeString(streamState.commandId, 128);
|
|
3660
|
+
const transfer = buildRemoteFrameTransferDescriptorFromMessage(
|
|
3661
|
+
message,
|
|
3662
|
+
pending.mode || pending.frameMode || DEFAULT_REMOTE_FRAME_MODE);
|
|
3663
|
+
const openedAt = safeString(message.openedAt, 80) || device.lastSeenAt || new Date().toISOString();
|
|
3664
|
+
const nextStreamState = {
|
|
3665
|
+
...streamState,
|
|
3666
|
+
...pending,
|
|
3667
|
+
streamId: streamState.streamId,
|
|
3668
|
+
commandId,
|
|
3669
|
+
active: true,
|
|
3670
|
+
open: true,
|
|
3671
|
+
openedAt,
|
|
3672
|
+
openTransport: transport,
|
|
3673
|
+
mode: transfer.mode,
|
|
3674
|
+
frameMode: transfer.frameMode,
|
|
3675
|
+
frameProfile: transfer.frameProfile,
|
|
3676
|
+
encoding: transfer.encoding,
|
|
3677
|
+
codec: transfer.codec,
|
|
3678
|
+
compression: transfer.compression,
|
|
3679
|
+
transferProtocol: transfer.transferProtocol,
|
|
3680
|
+
transferProtocolVersion: transfer.transferProtocolVersion,
|
|
3681
|
+
handshake: transfer.handshake,
|
|
3682
|
+
width: Number.isFinite(Number(message.width)) ? Number(message.width) : Number(pending.width || 0),
|
|
3683
|
+
height: Number.isFinite(Number(message.height)) ? Number(message.height) : Number(pending.height || 0),
|
|
3684
|
+
sourceWidth: Number.isFinite(Number(message.sourceWidth)) ? Number(message.sourceWidth) : Number(pending.sourceWidth || 0),
|
|
3685
|
+
sourceHeight: Number.isFinite(Number(message.sourceHeight)) ? Number(message.sourceHeight) : Number(pending.sourceHeight || 0),
|
|
3686
|
+
monitorIndex: Number.isFinite(Number(message.monitorIndex))
|
|
3687
|
+
? normalizeMonitorIndex(message.monitorIndex)
|
|
3688
|
+
: Number(pending.monitorIndex || 0),
|
|
3689
|
+
monitorCount: Number.isFinite(Number(message.monitorCount))
|
|
3690
|
+
? clampNumber(message.monitorCount, 1, 64, 1)
|
|
3691
|
+
: Number(pending.monitorCount || 1),
|
|
3692
|
+
streamPurpose: safeString(message.streamPurpose || pending.streamPurpose, 24) || 'wall',
|
|
3693
|
+
captureGeneration: messageCaptureGeneration > 0
|
|
3694
|
+
? messageCaptureGeneration
|
|
3695
|
+
: pendingCaptureGeneration,
|
|
3696
|
+
openedFrameSeq: Number.isFinite(Number(message.frameSeq)) ? Number(message.frameSeq) : 0,
|
|
3697
|
+
lastFrameAt: '',
|
|
3698
|
+
lastFrameSeq: 0,
|
|
3699
|
+
framesReceived: 0,
|
|
3700
|
+
latestFrame: null,
|
|
3701
|
+
stoppedAt: '',
|
|
3702
|
+
stopReason: ''
|
|
3703
|
+
};
|
|
3704
|
+
clearPendingLiveStreamDescriptor(device, streamState);
|
|
3705
|
+
setPendingLiveStreamDescriptor(nextStreamState, null);
|
|
3706
|
+
if (device.latestLiveFrame?.streamId === streamState.streamId
|
|
3707
|
+
&& safeString(device.latestLiveFrame.commandId, 128) === previousCommandId) {
|
|
3708
|
+
device.latestLiveFrame = null;
|
|
3709
|
+
}
|
|
3710
|
+
setDeviceLiveStream(device, nextStreamState);
|
|
3711
|
+
return {
|
|
3712
|
+
streamState: nextStreamState,
|
|
3713
|
+
transfer,
|
|
3714
|
+
previousCommandId
|
|
3715
|
+
};
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3718
|
+
function emitLiveStreamOpened(device, streamState, transfer, transport, previousCommandId = '') {
|
|
3719
|
+
emitRemoteEvent('RemoteLiveStreamOpened', device, {
|
|
3720
|
+
streamId: streamState.streamId,
|
|
3721
|
+
commandId: streamState.commandId,
|
|
3722
|
+
previousCommandId,
|
|
3723
|
+
handoff: !!previousCommandId,
|
|
3724
|
+
mode: transfer.mode,
|
|
3725
|
+
frameMode: transfer.frameMode,
|
|
3726
|
+
frameProfile: transfer.frameProfile,
|
|
3727
|
+
encoding: transfer.encoding,
|
|
3728
|
+
compression: transfer.compression,
|
|
3729
|
+
transport,
|
|
3730
|
+
captureGeneration: Number(streamState.captureGeneration || 0),
|
|
3731
|
+
streamPurpose: safeString(streamState.streamPurpose, 24) || 'wall'
|
|
3732
|
+
});
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3735
|
+
function applyLiveStreamOpen(device, message, transport = 'json') {
|
|
3736
|
+
const streamId = safeString(message.streamId, 128) || 'live';
|
|
3737
|
+
const streamState = getDeviceLiveStream(device, streamId);
|
|
3738
|
+
if (!streamState?.active) {
|
|
3377
3739
|
emitRemoteEvent('RemoteLiveStreamOpenIgnored', device, {
|
|
3378
3740
|
reason: 'stale-live-stream-open',
|
|
3379
3741
|
streamId,
|
|
@@ -3381,12 +3743,13 @@ export function createRemoteHub(options = {}) {
|
|
|
3381
3743
|
});
|
|
3382
3744
|
return false;
|
|
3383
3745
|
}
|
|
3384
|
-
|
|
3385
|
-
const commandId = safeString(message.commandId, 128);
|
|
3386
|
-
const activeCommandId = safeString(streamState.commandId, 128);
|
|
3387
|
-
const
|
|
3388
|
-
const
|
|
3389
|
-
const
|
|
3746
|
+
|
|
3747
|
+
const commandId = safeString(message.commandId, 128);
|
|
3748
|
+
const activeCommandId = safeString(streamState.commandId, 128);
|
|
3749
|
+
const pendingDescriptor = getPendingLiveStreamDescriptor(streamState);
|
|
3750
|
+
const pendingCommandId = safeString(pendingDescriptor?.commandId, 128);
|
|
3751
|
+
const isActiveGeneration = !commandId || !activeCommandId || commandId === activeCommandId;
|
|
3752
|
+
const isPendingGeneration = !!pendingCommandId && commandId === pendingCommandId;
|
|
3390
3753
|
if (!isActiveGeneration && !isPendingGeneration) {
|
|
3391
3754
|
emitRemoteEvent('RemoteLiveStreamOpenIgnored', device, {
|
|
3392
3755
|
reason: 'stale-live-stream-generation',
|
|
@@ -3396,21 +3759,31 @@ export function createRemoteHub(options = {}) {
|
|
|
3396
3759
|
pendingCommandId,
|
|
3397
3760
|
transport
|
|
3398
3761
|
});
|
|
3399
|
-
return false;
|
|
3400
|
-
}
|
|
3401
|
-
|
|
3402
|
-
const activatingPendingGeneration = isPendingGeneration && commandId !== activeCommandId;
|
|
3403
|
-
|
|
3404
|
-
|
|
3762
|
+
return false;
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
const activatingPendingGeneration = isPendingGeneration && commandId !== activeCommandId;
|
|
3766
|
+
if (activatingPendingGeneration) {
|
|
3767
|
+
const promoted = promotePendingLiveStream(device, streamState, message, transport);
|
|
3768
|
+
if (!promoted) {
|
|
3769
|
+
return false;
|
|
3770
|
+
}
|
|
3771
|
+
emitLiveStreamOpened(
|
|
3772
|
+
device,
|
|
3773
|
+
promoted.streamState,
|
|
3774
|
+
promoted.transfer,
|
|
3775
|
+
transport,
|
|
3776
|
+
promoted.previousCommandId);
|
|
3777
|
+
return true;
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3780
|
+
const transfer = buildRemoteFrameTransferDescriptorFromMessage(message, streamState.mode || DEFAULT_REMOTE_FRAME_MODE);
|
|
3781
|
+
const openedAt = safeString(message.openedAt, 80) || device.lastSeenAt || new Date().toISOString();
|
|
3405
3782
|
const nextStreamState = {
|
|
3406
3783
|
...streamState,
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
pendingRestartToken: activatingPendingGeneration ? '' : streamState.pendingRestartToken,
|
|
3411
|
-
open: true,
|
|
3412
|
-
openedAt,
|
|
3413
|
-
openTransport: transport,
|
|
3784
|
+
open: true,
|
|
3785
|
+
openedAt,
|
|
3786
|
+
openTransport: transport,
|
|
3414
3787
|
mode: transfer.mode,
|
|
3415
3788
|
frameMode: transfer.frameMode,
|
|
3416
3789
|
frameProfile: transfer.frameProfile,
|
|
@@ -3432,42 +3805,45 @@ export function createRemoteHub(options = {}) {
|
|
|
3432
3805
|
: Number(streamState.captureGeneration || 0),
|
|
3433
3806
|
openedFrameSeq: Number.isFinite(Number(message.frameSeq)) ? Number(message.frameSeq) : Number(streamState.openedFrameSeq || 0)
|
|
3434
3807
|
};
|
|
3435
|
-
setDeviceLiveStream(device, nextStreamState);
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
previousCommandId: activatingPendingGeneration ? activeCommandId : '',
|
|
3440
|
-
handoff: activatingPendingGeneration,
|
|
3441
|
-
mode: transfer.mode,
|
|
3442
|
-
frameMode: transfer.frameMode,
|
|
3443
|
-
frameProfile: transfer.frameProfile,
|
|
3444
|
-
encoding: transfer.encoding,
|
|
3445
|
-
compression: transfer.compression,
|
|
3446
|
-
transport
|
|
3447
|
-
});
|
|
3448
|
-
return true;
|
|
3449
|
-
}
|
|
3808
|
+
setDeviceLiveStream(device, nextStreamState);
|
|
3809
|
+
emitLiveStreamOpened(device, nextStreamState, transfer, transport);
|
|
3810
|
+
return true;
|
|
3811
|
+
}
|
|
3450
3812
|
|
|
3451
3813
|
function applyLiveFrame(device, message, framePayload, transport = 'json-base64') {
|
|
3452
3814
|
const frameData = Buffer.isBuffer(framePayload)
|
|
3453
3815
|
? ''
|
|
3454
3816
|
: safeString(framePayload, MAX_STREAM_BASE64_CHARS + 1);
|
|
3455
|
-
const frameSeq = Number(message.frameSeq);
|
|
3456
|
-
const streamId = safeString(message.streamId, 128) || 'live';
|
|
3457
|
-
const commandId = safeString(message.commandId, 128);
|
|
3458
|
-
|
|
3459
|
-
if (!streamState?.active) {
|
|
3817
|
+
const frameSeq = Number(message.frameSeq);
|
|
3818
|
+
const streamId = safeString(message.streamId, 128) || 'live';
|
|
3819
|
+
const commandId = safeString(message.commandId, 128);
|
|
3820
|
+
let streamState = getDeviceLiveStream(device, streamId);
|
|
3821
|
+
if (!streamState?.active) {
|
|
3460
3822
|
device.counters.liveFramesDropped += 1;
|
|
3461
3823
|
emitRemoteEvent('RemoteFrameDropped', device, {
|
|
3462
3824
|
reason: 'stale-live-stream-frame',
|
|
3463
3825
|
streamId,
|
|
3464
3826
|
frameSeq: Number.isFinite(frameSeq) ? frameSeq : null,
|
|
3465
3827
|
transport
|
|
3466
|
-
});
|
|
3467
|
-
return false;
|
|
3468
|
-
}
|
|
3469
|
-
|
|
3470
|
-
|
|
3828
|
+
});
|
|
3829
|
+
return false;
|
|
3830
|
+
}
|
|
3831
|
+
let activeCommandId = safeString(streamState.commandId, 128);
|
|
3832
|
+
let pendingCommandId = safeString(getPendingLiveStreamDescriptor(streamState)?.commandId, 128);
|
|
3833
|
+
if (pendingCommandId && commandId === pendingCommandId && commandId !== activeCommandId) {
|
|
3834
|
+
const promoted = promotePendingLiveStream(device, streamState, message, `${transport}-implicit`);
|
|
3835
|
+
if (promoted) {
|
|
3836
|
+
streamState = promoted.streamState;
|
|
3837
|
+
activeCommandId = safeString(streamState.commandId, 128);
|
|
3838
|
+
pendingCommandId = '';
|
|
3839
|
+
emitLiveStreamOpened(
|
|
3840
|
+
device,
|
|
3841
|
+
streamState,
|
|
3842
|
+
promoted.transfer,
|
|
3843
|
+
`${transport}-implicit`,
|
|
3844
|
+
promoted.previousCommandId);
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3471
3847
|
const isActiveGeneration = !commandId || !activeCommandId || commandId === activeCommandId;
|
|
3472
3848
|
if (!isActiveGeneration) {
|
|
3473
3849
|
device.counters.liveFramesDropped += 1;
|
|
@@ -3520,7 +3896,7 @@ export function createRemoteHub(options = {}) {
|
|
|
3520
3896
|
const contentHash = buildFrameContentHash(message, payload);
|
|
3521
3897
|
const sameContentStreak = computeSameContentStreak(streamState.latestFrame, contentHash);
|
|
3522
3898
|
const transfer = buildRemoteFrameTransferDescriptorFromMessage(message, streamState.mode || DEFAULT_REMOTE_FRAME_MODE);
|
|
3523
|
-
if (streamState.open !== true) {
|
|
3899
|
+
if (streamState.open !== true) {
|
|
3524
3900
|
streamState.open = true;
|
|
3525
3901
|
streamState.openedAt = device.lastSeenAt;
|
|
3526
3902
|
streamState.openTransport = `${transport}-implicit`;
|
|
@@ -3532,15 +3908,20 @@ export function createRemoteHub(options = {}) {
|
|
|
3532
3908
|
streamState.compression = transfer.compression;
|
|
3533
3909
|
streamState.transferProtocol = transfer.transferProtocol;
|
|
3534
3910
|
streamState.transferProtocolVersion = transfer.transferProtocolVersion;
|
|
3535
|
-
streamState.handshake = transfer.handshake;
|
|
3536
|
-
}
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3911
|
+
streamState.handshake = transfer.handshake;
|
|
3912
|
+
}
|
|
3913
|
+
const currentGenerationVerified = (!activeCommandId || commandId === activeCommandId)
|
|
3914
|
+
&& (activeCaptureGeneration <= 0
|
|
3915
|
+
|| (frameCaptureGeneration > 0
|
|
3916
|
+
&& frameCaptureGeneration === activeCaptureGeneration));
|
|
3917
|
+
device.latestLiveFrame = {
|
|
3918
|
+
streamId,
|
|
3919
|
+
frameSeq,
|
|
3540
3920
|
streamFrameSeq: Number.isFinite(Number(message.streamFrameSeq)) ? Number(message.streamFrameSeq) : frameSeq,
|
|
3541
|
-
commandId,
|
|
3921
|
+
commandId: commandId || activeCommandId,
|
|
3542
3922
|
sessionId: device.sessionId,
|
|
3543
3923
|
captureGeneration: activeCaptureGeneration || frameCaptureGeneration,
|
|
3924
|
+
currentGenerationVerified,
|
|
3544
3925
|
streamPurpose: safeString(message.streamPurpose || streamState.streamPurpose, 24) || 'wall',
|
|
3545
3926
|
width: Number.isFinite(Number(message.width)) ? Number(message.width) : 0,
|
|
3546
3927
|
height: Number.isFinite(Number(message.height)) ? Number(message.height) : 0,
|
|
@@ -3632,11 +4013,21 @@ export function createRemoteHub(options = {}) {
|
|
|
3632
4013
|
streamState.sourceWidth = device.latestLiveFrame.sourceWidth;
|
|
3633
4014
|
streamState.sourceHeight = device.latestLiveFrame.sourceHeight;
|
|
3634
4015
|
streamState.monitorIndex = device.latestLiveFrame.monitorIndex;
|
|
3635
|
-
streamState.monitorCount = device.latestLiveFrame.monitorCount;
|
|
3636
|
-
ensureDeviceLiveStreams(device).set(streamId, streamState);
|
|
3637
|
-
device.counters.liveFramesReceived += 1;
|
|
3638
|
-
|
|
3639
|
-
|
|
4016
|
+
streamState.monitorCount = device.latestLiveFrame.monitorCount;
|
|
4017
|
+
ensureDeviceLiveStreams(device).set(streamId, streamState);
|
|
4018
|
+
device.counters.liveFramesReceived += 1;
|
|
4019
|
+
if (streamState.framesReceived === 1 && liveStreamHasCurrentFrame(streamState)) {
|
|
4020
|
+
emitRemoteEvent('RemoteLiveStreamReady', device, {
|
|
4021
|
+
streamId,
|
|
4022
|
+
commandId: streamState.commandId,
|
|
4023
|
+
captureGeneration: Number(streamState.captureGeneration || 0),
|
|
4024
|
+
streamPurpose: safeString(streamState.streamPurpose, 24) || 'wall',
|
|
4025
|
+
monitorIndex: Number(streamState.monitorIndex || 0),
|
|
4026
|
+
frameSeq
|
|
4027
|
+
});
|
|
4028
|
+
}
|
|
4029
|
+
return true;
|
|
4030
|
+
}
|
|
3640
4031
|
|
|
3641
4032
|
function applyAudioFrame(device, message, framePayload, transport = 'json-base64') {
|
|
3642
4033
|
const audioData = Buffer.isBuffer(framePayload)
|
|
@@ -4010,32 +4401,27 @@ export function createRemoteHub(options = {}) {
|
|
|
4010
4401
|
return;
|
|
4011
4402
|
}
|
|
4012
4403
|
|
|
4013
|
-
if (state.inputOnly) {
|
|
4014
|
-
device.inputLastSeenAt = new Date().toISOString();
|
|
4015
|
-
if (message.type === 'input.applied') {
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
hubAcknowledgedAtEpochMs: Date.now()
|
|
4035
|
-
});
|
|
4036
|
-
}
|
|
4037
|
-
return;
|
|
4038
|
-
}
|
|
4404
|
+
if (state.inputOnly) {
|
|
4405
|
+
device.inputLastSeenAt = new Date().toISOString();
|
|
4406
|
+
if (message.type === 'input.applied') {
|
|
4407
|
+
handleRemoteInputOutcome(device, {
|
|
4408
|
+
...message,
|
|
4409
|
+
agentApplyMs: Number(message.agentApplyMs || 0) || 0
|
|
4410
|
+
}, 'input');
|
|
4411
|
+
}
|
|
4412
|
+
if (message.type === 'input.error') {
|
|
4413
|
+
const pending = takePendingInputFallback(
|
|
4414
|
+
device,
|
|
4415
|
+
message.commandId || message.CommandId,
|
|
4416
|
+
message.inputSeq ?? message.InputSeq);
|
|
4417
|
+
emitEvent('RemoteInputError', {
|
|
4418
|
+
...buildRemoteInputOutcome(device, message, pending),
|
|
4419
|
+
error: safeString(message.error || message.Error, 600) || 'remote-input-failed',
|
|
4420
|
+
failedAtEpochMs: Number(message.failedAtEpochMs ?? message.FailedAtEpochMs ?? 0) || 0
|
|
4421
|
+
});
|
|
4422
|
+
}
|
|
4423
|
+
return;
|
|
4424
|
+
}
|
|
4039
4425
|
if (state.frameOnly) {
|
|
4040
4426
|
device.frameLastSeenAt = new Date().toISOString();
|
|
4041
4427
|
return;
|
|
@@ -4067,15 +4453,19 @@ export function createRemoteHub(options = {}) {
|
|
|
4067
4453
|
device.counters.statusReceived += 1;
|
|
4068
4454
|
emitRemoteEvent('RemoteDeviceStatus', device);
|
|
4069
4455
|
break;
|
|
4070
|
-
case 'command.result':
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
const
|
|
4075
|
-
const
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4456
|
+
case 'command.result':
|
|
4457
|
+
case 'command.error':
|
|
4458
|
+
device.counters.commandResultsReceived += 1;
|
|
4459
|
+
{
|
|
4460
|
+
const commandId = safeString(message.commandId, 128);
|
|
4461
|
+
const error = safeString(
|
|
4462
|
+
message.error || (message.type === 'command.error' ? 'command-failed' : ''),
|
|
4463
|
+
500);
|
|
4464
|
+
const result = message.result ?? null;
|
|
4465
|
+
handleInputFallbackCommandResult(device, message);
|
|
4466
|
+
if (error || result?.ok === false || result?.status === 'failed') {
|
|
4467
|
+
failPendingLiveStream(device, commandId, error || result?.error || 'stream-start-failed');
|
|
4468
|
+
}
|
|
4079
4469
|
const task = applyTaskResult(device, commandId, result, error);
|
|
4080
4470
|
if (task) {
|
|
4081
4471
|
emitRemoteEvent('RemoteTaskResult', device, {
|
|
@@ -4090,8 +4480,12 @@ export function createRemoteHub(options = {}) {
|
|
|
4090
4480
|
commandId: safeString(message.commandId, 128),
|
|
4091
4481
|
result: message.result ?? null,
|
|
4092
4482
|
error: safeString(message.error, 500)
|
|
4093
|
-
});
|
|
4094
|
-
break;
|
|
4483
|
+
});
|
|
4484
|
+
break;
|
|
4485
|
+
case 'input.applied':
|
|
4486
|
+
case 'input.error':
|
|
4487
|
+
handleRemoteInputOutcome(device, message, 'main');
|
|
4488
|
+
break;
|
|
4095
4489
|
case 'thumbnail.frame': {
|
|
4096
4490
|
applyThumbnailFrame(device, message, message.data, 'json-base64');
|
|
4097
4491
|
break;
|
|
@@ -4512,9 +4906,10 @@ export function createRemoteHub(options = {}) {
|
|
|
4512
4906
|
}
|
|
4513
4907
|
|
|
4514
4908
|
async function close() {
|
|
4515
|
-
for (const device of devices.values()) {
|
|
4516
|
-
failAllPendingTasks(device, 'hub-shutdown');
|
|
4517
|
-
|
|
4909
|
+
for (const device of devices.values()) {
|
|
4910
|
+
failAllPendingTasks(device, 'hub-shutdown');
|
|
4911
|
+
failAllPendingInputFallbacks(device, 'hub-shutdown');
|
|
4912
|
+
if (device.socket && !device.socket.destroyed) {
|
|
4518
4913
|
writeJsonLine(device.socket, { type: 'disconnect', reason: 'hub-shutdown' });
|
|
4519
4914
|
device.socket.destroy();
|
|
4520
4915
|
}
|
|
@@ -4565,9 +4960,10 @@ export function createRemoteHub(options = {}) {
|
|
|
4565
4960
|
device.connected = false;
|
|
4566
4961
|
device.disconnectedAt = new Date().toISOString();
|
|
4567
4962
|
device.lastDisconnectReason = reason;
|
|
4568
|
-
deactivateDeviceLiveStreams(device, reason, device.disconnectedAt);
|
|
4569
|
-
failAllPendingTasks(device, 'device-disconnected');
|
|
4570
|
-
|
|
4963
|
+
deactivateDeviceLiveStreams(device, reason, device.disconnectedAt);
|
|
4964
|
+
failAllPendingTasks(device, 'device-disconnected');
|
|
4965
|
+
failAllPendingInputFallbacks(device, 'device-disconnected');
|
|
4966
|
+
emitRemoteEvent('RemoteDeviceDisconnected', device, { reason, synthetic: true });
|
|
4571
4967
|
return true;
|
|
4572
4968
|
}
|
|
4573
4969
|
|
|
@@ -4693,13 +5089,15 @@ export function createRemoteHub(options = {}) {
|
|
|
4693
5089
|
return { ok: false, error: 'device-not-connected' };
|
|
4694
5090
|
}
|
|
4695
5091
|
|
|
4696
|
-
const denied = policyError(device, 'allowControl');
|
|
4697
|
-
if (denied) return { ok: false, error: denied };
|
|
4698
|
-
|
|
5092
|
+
const denied = policyError(device, 'allowControl');
|
|
5093
|
+
if (denied) return { ok: false, error: denied };
|
|
5094
|
+
|
|
5095
|
+
if (device.capabilities?.controlPermission === false
|
|
5096
|
+
|| device.capabilities?.accessibilityPermission === false) {
|
|
5097
|
+
return { ok: false, error: 'INPUT_PERMISSION_DENIED' };
|
|
5098
|
+
}
|
|
4699
5099
|
if (!readCapabilityFlag(device.capabilities, 'control')) {
|
|
4700
|
-
|
|
4701
|
-
|| device.capabilities?.accessibilityPermission === false;
|
|
4702
|
-
return { ok: false, error: permissionMissing ? 'INPUT_PERMISSION_DENIED' : 'device-input-control-unavailable' };
|
|
5100
|
+
return { ok: false, error: 'device-input-control-unavailable' };
|
|
4703
5101
|
}
|
|
4704
5102
|
|
|
4705
5103
|
const normalized = normalizeRemoteInputEvent(input);
|
|
@@ -4708,11 +5106,14 @@ export function createRemoteHub(options = {}) {
|
|
|
4708
5106
|
}
|
|
4709
5107
|
const controlStream = getActiveControlStream(device);
|
|
4710
5108
|
if (!controlStream
|
|
4711
|
-
|| controlStream
|
|
5109
|
+
|| !liveStreamHasCurrentFrame(controlStream)
|
|
4712
5110
|
|| !safeString(controlStream.commandId, 128)
|
|
4713
5111
|
|| Number(controlStream.captureGeneration || 0) <= 0) {
|
|
4714
5112
|
return { ok: false, error: 'CONTROL_NOT_READY' };
|
|
4715
5113
|
}
|
|
5114
|
+
if (getPendingLiveStreamDescriptor(controlStream)) {
|
|
5115
|
+
return { ok: false, error: 'CAPTURE_TRANSITION_IN_PROGRESS' };
|
|
5116
|
+
}
|
|
4716
5117
|
if (!normalized.controlSessionId
|
|
4717
5118
|
|| normalized.controlSessionId !== safeString(device.sessionId, 160)) {
|
|
4718
5119
|
return {
|
|
@@ -4762,7 +5163,43 @@ export function createRemoteHub(options = {}) {
|
|
|
4762
5163
|
detachInputSocket(inputSocket, 'input-socket-write-failed');
|
|
4763
5164
|
}
|
|
4764
5165
|
|
|
4765
|
-
|
|
5166
|
+
const hubForwardedAtEpochMs = Date.now();
|
|
5167
|
+
const fallback = sendCommand(deviceId, {
|
|
5168
|
+
command: 'input.control',
|
|
5169
|
+
payload: {
|
|
5170
|
+
...normalized,
|
|
5171
|
+
hubForwardedAtEpochMs,
|
|
5172
|
+
issuedAt: normalized.issuedAt || new Date().toISOString()
|
|
5173
|
+
}
|
|
5174
|
+
});
|
|
5175
|
+
if (fallback.ok && normalized.requestAck && normalized.inputSeq > 0) {
|
|
5176
|
+
schedulePendingInputFallback(device, {
|
|
5177
|
+
commandId: fallback.commandId,
|
|
5178
|
+
inputSeq: normalized.inputSeq,
|
|
5179
|
+
inputType: normalized.type,
|
|
5180
|
+
issuedAtEpochMs: normalized.issuedAtEpochMs,
|
|
5181
|
+
hubReceivedAtEpochMs: normalized.hubReceivedAtEpochMs,
|
|
5182
|
+
hubForwardedAtEpochMs,
|
|
5183
|
+
fallback: true,
|
|
5184
|
+
timeoutTimer: null
|
|
5185
|
+
});
|
|
5186
|
+
}
|
|
5187
|
+
return fallback.ok
|
|
5188
|
+
? {
|
|
5189
|
+
...fallback,
|
|
5190
|
+
inputSocket: false,
|
|
5191
|
+
fallback: true,
|
|
5192
|
+
sessionId: device.sessionId,
|
|
5193
|
+
deliveryCommandId: fallback.commandId,
|
|
5194
|
+
commandId: controlStream.commandId,
|
|
5195
|
+
captureGeneration: controlStream.captureGeneration,
|
|
5196
|
+
inputSeq: normalized.inputSeq
|
|
5197
|
+
}
|
|
5198
|
+
: {
|
|
5199
|
+
...fallback,
|
|
5200
|
+
error: fallback.error || 'CONTROL_NOT_READY',
|
|
5201
|
+
detail: 'input-channel-not-connected'
|
|
5202
|
+
};
|
|
4766
5203
|
}
|
|
4767
5204
|
|
|
4768
5205
|
function notifyAgentProgress(deviceIds, progress = {}) {
|
|
@@ -5075,31 +5512,100 @@ export function createRemoteHub(options = {}) {
|
|
|
5075
5512
|
|| (device?.activeLiveStream?.streamId === id ? device.activeLiveStream : null);
|
|
5076
5513
|
}
|
|
5077
5514
|
|
|
5078
|
-
function setDeviceLiveStream(device, stream) {
|
|
5079
|
-
if (!device || !stream?.streamId) {
|
|
5080
|
-
return stream;
|
|
5081
|
-
}
|
|
5082
|
-
ensureDeviceLiveStreams(device).set(stream.streamId, stream);
|
|
5083
|
-
device.activeLiveStream = stream;
|
|
5084
|
-
return stream;
|
|
5085
|
-
}
|
|
5086
|
-
|
|
5087
|
-
function
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5515
|
+
function setDeviceLiveStream(device, stream) {
|
|
5516
|
+
if (!device || !stream?.streamId) {
|
|
5517
|
+
return stream;
|
|
5518
|
+
}
|
|
5519
|
+
ensureDeviceLiveStreams(device).set(stream.streamId, stream);
|
|
5520
|
+
device.activeLiveStream = stream;
|
|
5521
|
+
return stream;
|
|
5522
|
+
}
|
|
5523
|
+
|
|
5524
|
+
function ensureLiveStreamReplacementTimers(device) {
|
|
5525
|
+
if (!(device?.liveStreamReplacementTimers instanceof Map)) {
|
|
5526
|
+
Object.defineProperty(device, 'liveStreamReplacementTimers', {
|
|
5527
|
+
value: new Map(),
|
|
5528
|
+
enumerable: false,
|
|
5529
|
+
configurable: true,
|
|
5530
|
+
writable: true
|
|
5531
|
+
});
|
|
5532
|
+
}
|
|
5533
|
+
return device.liveStreamReplacementTimers;
|
|
5534
|
+
}
|
|
5535
|
+
|
|
5536
|
+
function getPendingLiveStreamDescriptor(stream) {
|
|
5537
|
+
return stream?.pendingDescriptor && typeof stream.pendingDescriptor === 'object'
|
|
5538
|
+
? stream.pendingDescriptor
|
|
5539
|
+
: null;
|
|
5540
|
+
}
|
|
5541
|
+
|
|
5542
|
+
function setPendingLiveStreamDescriptor(stream, descriptor) {
|
|
5543
|
+
const pending = descriptor && typeof descriptor === 'object'
|
|
5544
|
+
? { ...descriptor }
|
|
5545
|
+
: null;
|
|
5546
|
+
stream.pendingDescriptor = pending;
|
|
5547
|
+
// Keep the scalar fields for older status consumers while the
|
|
5548
|
+
// descriptor remains the canonical pending-generation state.
|
|
5549
|
+
stream.pendingCommandId = safeString(pending?.commandId, 128);
|
|
5550
|
+
stream.pendingCaptureGeneration = Number(pending?.captureGeneration || 0);
|
|
5551
|
+
stream.pendingStartedAt = safeString(pending?.startedAt, 80);
|
|
5552
|
+
stream.pendingRestartToken = safeString(pending?.restartToken, 128);
|
|
5553
|
+
return pending;
|
|
5554
|
+
}
|
|
5555
|
+
|
|
5556
|
+
function clearLiveStreamReplacementTimer(device, streamId) {
|
|
5557
|
+
const timers = ensureLiveStreamReplacementTimers(device);
|
|
5558
|
+
const key = safeString(streamId, 128);
|
|
5559
|
+
const timer = timers.get(key);
|
|
5560
|
+
if (timer) {
|
|
5561
|
+
clearTimeout(timer);
|
|
5562
|
+
}
|
|
5563
|
+
timers.delete(key);
|
|
5564
|
+
}
|
|
5565
|
+
|
|
5566
|
+
function clearPendingLiveStreamDescriptor(device, stream) {
|
|
5567
|
+
if (!stream) {
|
|
5568
|
+
return null;
|
|
5569
|
+
}
|
|
5570
|
+
const pending = getPendingLiveStreamDescriptor(stream);
|
|
5571
|
+
clearLiveStreamReplacementTimer(device, stream.streamId);
|
|
5572
|
+
setPendingLiveStreamDescriptor(stream, null);
|
|
5573
|
+
return pending;
|
|
5574
|
+
}
|
|
5575
|
+
|
|
5576
|
+
function scheduleLiveStreamReplacementTimeout(device, stream) {
|
|
5577
|
+
const pending = getPendingLiveStreamDescriptor(stream);
|
|
5578
|
+
if (!pending?.commandId) {
|
|
5579
|
+
clearLiveStreamReplacementTimer(device, stream?.streamId);
|
|
5580
|
+
return;
|
|
5581
|
+
}
|
|
5582
|
+
clearLiveStreamReplacementTimer(device, stream.streamId);
|
|
5583
|
+
const timer = setTimeout(() => {
|
|
5584
|
+
const current = getDeviceLiveStream(device, stream.streamId);
|
|
5585
|
+
const currentPending = getPendingLiveStreamDescriptor(current);
|
|
5586
|
+
if (currentPending?.commandId === pending.commandId) {
|
|
5587
|
+
failPendingLiveStream(device, pending.commandId, 'stream-start-timeout');
|
|
5588
|
+
}
|
|
5589
|
+
}, LIVE_STREAM_REPLACEMENT_PENDING_MS);
|
|
5590
|
+
timer.unref?.();
|
|
5591
|
+
ensureLiveStreamReplacementTimers(device).set(stream.streamId, timer);
|
|
5592
|
+
}
|
|
5593
|
+
|
|
5594
|
+
function failPendingLiveStream(device, commandId, error = 'stream-start-failed') {
|
|
5595
|
+
const key = safeString(commandId, 128);
|
|
5596
|
+
if (!device || !key) {
|
|
5597
|
+
return false;
|
|
5598
|
+
}
|
|
5599
|
+
const streams = ensureDeviceLiveStreams(device);
|
|
5600
|
+
for (const stream of streams.values()) {
|
|
5601
|
+
const pending = getPendingLiveStreamDescriptor(stream);
|
|
5602
|
+
if (safeString(pending?.commandId, 128) !== key) {
|
|
5603
|
+
continue;
|
|
5604
|
+
}
|
|
5605
|
+
clearPendingLiveStreamDescriptor(device, stream);
|
|
5606
|
+
emitRemoteEvent('RemoteLiveStreamRestartFailed', device, {
|
|
5607
|
+
streamId: stream.streamId,
|
|
5608
|
+
commandId: key,
|
|
5103
5609
|
error: safeString(error, 500) || 'stream-start-failed'
|
|
5104
5610
|
});
|
|
5105
5611
|
return true;
|
|
@@ -5107,12 +5613,13 @@ export function createRemoteHub(options = {}) {
|
|
|
5107
5613
|
return false;
|
|
5108
5614
|
}
|
|
5109
5615
|
|
|
5110
|
-
function deactivateDeviceLiveStreams(device, reason, stoppedAt = new Date().toISOString()) {
|
|
5111
|
-
const streams = ensureDeviceLiveStreams(device);
|
|
5112
|
-
for (const stream of streams.values()) {
|
|
5113
|
-
stream
|
|
5114
|
-
stream.
|
|
5115
|
-
stream.
|
|
5616
|
+
function deactivateDeviceLiveStreams(device, reason, stoppedAt = new Date().toISOString()) {
|
|
5617
|
+
const streams = ensureDeviceLiveStreams(device);
|
|
5618
|
+
for (const stream of streams.values()) {
|
|
5619
|
+
clearPendingLiveStreamDescriptor(device, stream);
|
|
5620
|
+
stream.active = false;
|
|
5621
|
+
stream.stoppedAt = stoppedAt;
|
|
5622
|
+
stream.stopReason = reason;
|
|
5116
5623
|
}
|
|
5117
5624
|
if (device?.activeLiveStream) {
|
|
5118
5625
|
device.activeLiveStream.active = false;
|
|
@@ -5139,59 +5646,6 @@ export function createRemoteHub(options = {}) {
|
|
|
5139
5646
|
safeString(stream?.streamPurpose, 24).toLowerCase() === 'control') || null;
|
|
5140
5647
|
}
|
|
5141
5648
|
|
|
5142
|
-
function stopCompetingMacCaptureStreams(deviceId, device, nextStreamId, nextPurpose) {
|
|
5143
|
-
if (normalizeLivePlatform(device?.platform) !== 'macos') {
|
|
5144
|
-
return { ok: true, stopped: [] };
|
|
5145
|
-
}
|
|
5146
|
-
|
|
5147
|
-
const activeControl = getActiveControlStream(device);
|
|
5148
|
-
if (nextPurpose !== 'control'
|
|
5149
|
-
&& activeControl
|
|
5150
|
-
&& activeControl.streamId !== nextStreamId) {
|
|
5151
|
-
return {
|
|
5152
|
-
ok: true,
|
|
5153
|
-
suppressed: true,
|
|
5154
|
-
reason: 'CONTROL_CAPTURE_OWNS_DEVICE',
|
|
5155
|
-
ownerStreamId: activeControl.streamId,
|
|
5156
|
-
ownerCommandId: activeControl.commandId,
|
|
5157
|
-
captureGeneration: Number(activeControl.captureGeneration || 0)
|
|
5158
|
-
};
|
|
5159
|
-
}
|
|
5160
|
-
|
|
5161
|
-
const stopped = [];
|
|
5162
|
-
for (const stream of getActiveLiveStreams(device)) {
|
|
5163
|
-
if (!stream?.streamId || stream.streamId === nextStreamId) {
|
|
5164
|
-
continue;
|
|
5165
|
-
}
|
|
5166
|
-
const stopCommandId = crypto.randomUUID();
|
|
5167
|
-
const result = sendCommand(deviceId, {
|
|
5168
|
-
command: 'stream.stop',
|
|
5169
|
-
commandId: stopCommandId,
|
|
5170
|
-
payload: {
|
|
5171
|
-
streamId: stream.streamId,
|
|
5172
|
-
requestedAt: new Date().toISOString(),
|
|
5173
|
-
reason: `mac-capture-owner-transition:${nextPurpose}`
|
|
5174
|
-
}
|
|
5175
|
-
});
|
|
5176
|
-
if (!result.ok) {
|
|
5177
|
-
return {
|
|
5178
|
-
ok: false,
|
|
5179
|
-
error: 'CAPTURE_TRANSITION_IN_PROGRESS',
|
|
5180
|
-
streamId: stream.streamId,
|
|
5181
|
-
detail: result.error || 'stream-stop-dispatch-failed'
|
|
5182
|
-
};
|
|
5183
|
-
}
|
|
5184
|
-
stream.active = false;
|
|
5185
|
-
stream.pendingCommandId = '';
|
|
5186
|
-
stream.pendingStartedAt = '';
|
|
5187
|
-
stream.pendingRestartToken = '';
|
|
5188
|
-
stream.stoppedAt = new Date().toISOString();
|
|
5189
|
-
stream.stopReason = `mac-capture-owner-transition:${nextPurpose}`;
|
|
5190
|
-
stopped.push({ streamId: stream.streamId, commandId: stopCommandId });
|
|
5191
|
-
}
|
|
5192
|
-
return { ok: true, stopped };
|
|
5193
|
-
}
|
|
5194
|
-
|
|
5195
5649
|
function nextCaptureGeneration(device) {
|
|
5196
5650
|
const current = Number(device?.captureGenerationCounter || 0);
|
|
5197
5651
|
const next = Number.isSafeInteger(current) && current > 0 ? current + 1 : 1;
|
|
@@ -5208,18 +5662,17 @@ export function createRemoteHub(options = {}) {
|
|
|
5208
5662
|
}
|
|
5209
5663
|
|
|
5210
5664
|
function normalizeLiveStreamStartOptions(options = {}) {
|
|
5211
|
-
const transfer = buildRemoteFrameTransferDescriptor(options.mode || options.frameMode || DEFAULT_REMOTE_FRAME_MODE);
|
|
5212
|
-
const streamPurpose = safeString(options.streamPurpose || options.purpose || 'wall', 24) || 'wall';
|
|
5213
|
-
const platform = normalizeLivePlatform(options.platform);
|
|
5214
|
-
const
|
|
5215
|
-
const
|
|
5216
|
-
const
|
|
5217
|
-
const
|
|
5218
|
-
const
|
|
5219
|
-
const
|
|
5220
|
-
const
|
|
5221
|
-
|
|
5222
|
-
return { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose, platform, macWallProfile };
|
|
5665
|
+
const transfer = buildRemoteFrameTransferDescriptor(options.mode || options.frameMode || DEFAULT_REMOTE_FRAME_MODE);
|
|
5666
|
+
const streamPurpose = safeString(options.streamPurpose || options.purpose || 'wall', 24) || 'wall';
|
|
5667
|
+
const platform = normalizeLivePlatform(options.platform);
|
|
5668
|
+
const maxFps = streamPurpose === 'control' ? 60 : 30;
|
|
5669
|
+
const defaultFps = 8;
|
|
5670
|
+
const maxWidth = clampNumber(options.maxWidth, 320, 3840, 640);
|
|
5671
|
+
const maxHeight = clampNumber(options.maxHeight, 180, 2160, 360);
|
|
5672
|
+
const fps = clampNumber(options.fps, 1, maxFps, defaultFps);
|
|
5673
|
+
const quality = clampNumber(options.quality, 20, 95, 45);
|
|
5674
|
+
const monitorIndex = normalizeMonitorIndex(options.monitorIndex ?? options.screenIndex ?? options.displayIndex);
|
|
5675
|
+
return { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose, platform };
|
|
5223
5676
|
}
|
|
5224
5677
|
|
|
5225
5678
|
function liveStreamStartStillPending(activeLiveStream) {
|
|
@@ -5230,13 +5683,35 @@ export function createRemoteHub(options = {}) {
|
|
|
5230
5683
|
return Number.isFinite(startedAt) && Date.now() - startedAt < LIVE_STREAM_PENDING_REUSE_MS;
|
|
5231
5684
|
}
|
|
5232
5685
|
|
|
5233
|
-
function liveStreamReplacementStillPending(activeLiveStream) {
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5686
|
+
function liveStreamReplacementStillPending(activeLiveStream) {
|
|
5687
|
+
const pending = getPendingLiveStreamDescriptor(activeLiveStream);
|
|
5688
|
+
if (!activeLiveStream?.active || !safeString(pending?.commandId, 128)) {
|
|
5689
|
+
return false;
|
|
5690
|
+
}
|
|
5691
|
+
const startedAt = Date.parse(pending.startedAt || '');
|
|
5692
|
+
return Number.isFinite(startedAt) && Date.now() - startedAt < LIVE_STREAM_REPLACEMENT_PENDING_MS;
|
|
5693
|
+
}
|
|
5694
|
+
|
|
5695
|
+
function liveStreamHasCurrentFrame(activeLiveStream) {
|
|
5696
|
+
if (!activeLiveStream?.active
|
|
5697
|
+
|| activeLiveStream.open !== true
|
|
5698
|
+
|| Number(activeLiveStream.framesReceived || 0) < 1) {
|
|
5699
|
+
return false;
|
|
5700
|
+
}
|
|
5701
|
+
const latestFrame = activeLiveStream.latestFrame;
|
|
5702
|
+
if (!latestFrame || latestFrame.currentGenerationVerified !== true) {
|
|
5703
|
+
return false;
|
|
5704
|
+
}
|
|
5705
|
+
const activeCommandId = safeString(activeLiveStream.commandId, 128);
|
|
5706
|
+
const frameCommandId = safeString(latestFrame.commandId, 128);
|
|
5707
|
+
if (activeCommandId && frameCommandId !== activeCommandId) {
|
|
5708
|
+
return false;
|
|
5709
|
+
}
|
|
5710
|
+
const activeCaptureGeneration = Number(activeLiveStream.captureGeneration || 0);
|
|
5711
|
+
const frameCaptureGeneration = Number(latestFrame.captureGeneration || 0);
|
|
5712
|
+
return activeCaptureGeneration <= 0
|
|
5713
|
+
|| (frameCaptureGeneration > 0 && frameCaptureGeneration === activeCaptureGeneration);
|
|
5714
|
+
}
|
|
5240
5715
|
|
|
5241
5716
|
function getLiveStreamFreshWindowMs(activeLiveStream) {
|
|
5242
5717
|
const fps = Number(activeLiveStream?.fps || 0);
|
|
@@ -5296,17 +5771,20 @@ export function createRemoteHub(options = {}) {
|
|
|
5296
5771
|
const activeLiveStream = getDeviceLiveStream(device, streamId);
|
|
5297
5772
|
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
5298
5773
|
if (activeLiveStream
|
|
5299
|
-
&& liveStreamMatchesOptions(activeLiveStream, normalized)
|
|
5300
|
-
&& (activeLiveStream.open === true || liveStreamStartStillPending(activeLiveStream))) {
|
|
5301
|
-
return {
|
|
5302
|
-
ok: true,
|
|
5303
|
-
commandId: activeLiveStream.commandId,
|
|
5304
|
-
|
|
5305
|
-
|
|
5774
|
+
&& liveStreamMatchesOptions(activeLiveStream, normalized)
|
|
5775
|
+
&& (activeLiveStream.open === true || liveStreamStartStillPending(activeLiveStream))) {
|
|
5776
|
+
return {
|
|
5777
|
+
ok: true,
|
|
5778
|
+
commandId: activeLiveStream.commandId,
|
|
5779
|
+
sessionId: device.sessionId,
|
|
5780
|
+
streamId,
|
|
5781
|
+
streamPurpose,
|
|
5782
|
+
fps,
|
|
5306
5783
|
mode: transfer.mode,
|
|
5307
|
-
frameMode: transfer.frameMode,
|
|
5784
|
+
frameMode: transfer.frameMode,
|
|
5308
5785
|
monitorIndex,
|
|
5309
5786
|
captureGeneration: Number(activeLiveStream.captureGeneration || 0),
|
|
5787
|
+
ready: liveStreamHasCurrentFrame(activeLiveStream),
|
|
5310
5788
|
synthetic: true,
|
|
5311
5789
|
reused: true
|
|
5312
5790
|
};
|
|
@@ -5359,7 +5837,20 @@ export function createRemoteHub(options = {}) {
|
|
|
5359
5837
|
mode: transfer.mode,
|
|
5360
5838
|
synthetic: true
|
|
5361
5839
|
});
|
|
5362
|
-
return {
|
|
5840
|
+
return {
|
|
5841
|
+
ok: true,
|
|
5842
|
+
commandId,
|
|
5843
|
+
sessionId: device.sessionId,
|
|
5844
|
+
streamId,
|
|
5845
|
+
streamPurpose,
|
|
5846
|
+
fps,
|
|
5847
|
+
mode: transfer.mode,
|
|
5848
|
+
frameMode: transfer.frameMode,
|
|
5849
|
+
monitorIndex,
|
|
5850
|
+
captureGeneration,
|
|
5851
|
+
ready: true,
|
|
5852
|
+
synthetic: true
|
|
5853
|
+
};
|
|
5363
5854
|
}
|
|
5364
5855
|
|
|
5365
5856
|
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
@@ -5374,70 +5865,57 @@ export function createRemoteHub(options = {}) {
|
|
|
5374
5865
|
const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
|
|
5375
5866
|
const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
|
|
5376
5867
|
const activeLiveStream = getDeviceLiveStream(device, streamId);
|
|
5377
|
-
const
|
|
5378
|
-
if (
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
};
|
|
5400
|
-
}
|
|
5401
|
-
if (activeLiveStream?.pendingCommandId) {
|
|
5402
|
-
if (liveStreamReplacementStillPending(activeLiveStream)) {
|
|
5403
|
-
emitRemoteEvent('RemoteLiveStreamRestartPending', device, {
|
|
5404
|
-
streamId,
|
|
5405
|
-
commandId: activeLiveStream.pendingCommandId,
|
|
5406
|
-
activeCommandId: activeLiveStream.commandId,
|
|
5407
|
-
reason: safeString(options.restartReason || 'duplicate-restart', 128)
|
|
5408
|
-
});
|
|
5409
|
-
return {
|
|
5410
|
-
ok: true,
|
|
5411
|
-
commandId: activeLiveStream.pendingCommandId,
|
|
5412
|
-
streamId,
|
|
5413
|
-
fps: Number(activeLiveStream.fps || fps),
|
|
5414
|
-
mode: activeLiveStream.mode || transfer.mode,
|
|
5415
|
-
frameMode: activeLiveStream.frameMode || transfer.frameMode,
|
|
5416
|
-
monitorIndex: Number(activeLiveStream.monitorIndex || monitorIndex),
|
|
5417
|
-
captureGeneration: Number(activeLiveStream.captureGeneration || 0),
|
|
5868
|
+
const pendingDescriptor = getPendingLiveStreamDescriptor(activeLiveStream);
|
|
5869
|
+
if (pendingDescriptor?.commandId) {
|
|
5870
|
+
const pendingMatchesRequest = liveStreamMatchesOptions(pendingDescriptor, normalized);
|
|
5871
|
+
if (pendingMatchesRequest && liveStreamReplacementStillPending(activeLiveStream)) {
|
|
5872
|
+
emitRemoteEvent('RemoteLiveStreamRestartPending', device, {
|
|
5873
|
+
streamId,
|
|
5874
|
+
commandId: pendingDescriptor.commandId,
|
|
5875
|
+
activeCommandId: activeLiveStream.commandId,
|
|
5876
|
+
reason: safeString(options.restartReason || 'duplicate-restart', 128)
|
|
5877
|
+
});
|
|
5878
|
+
return {
|
|
5879
|
+
ok: true,
|
|
5880
|
+
commandId: pendingDescriptor.commandId,
|
|
5881
|
+
sessionId: device.sessionId,
|
|
5882
|
+
streamId,
|
|
5883
|
+
streamPurpose: safeString(pendingDescriptor.streamPurpose, 24) || streamPurpose,
|
|
5884
|
+
fps: Number(pendingDescriptor.fps || fps),
|
|
5885
|
+
mode: pendingDescriptor.mode || transfer.mode,
|
|
5886
|
+
frameMode: pendingDescriptor.frameMode || transfer.frameMode,
|
|
5887
|
+
monitorIndex: Number(pendingDescriptor.monitorIndex ?? monitorIndex),
|
|
5888
|
+
captureGeneration: Number(pendingDescriptor.captureGeneration || 0),
|
|
5889
|
+
ready: false,
|
|
5418
5890
|
pending: true,
|
|
5419
5891
|
reused: true
|
|
5420
5892
|
};
|
|
5421
|
-
}
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5893
|
+
}
|
|
5894
|
+
failPendingLiveStream(
|
|
5895
|
+
device,
|
|
5896
|
+
pendingDescriptor.commandId,
|
|
5897
|
+
liveStreamReplacementStillPending(activeLiveStream)
|
|
5898
|
+
? 'stream-restart-superseded'
|
|
5899
|
+
: 'stream-start-timeout');
|
|
5900
|
+
}
|
|
5426
5901
|
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
5427
5902
|
if (activeLiveStream
|
|
5428
5903
|
&& options.forceRestart !== true
|
|
5429
5904
|
&& options.reuseExisting === true
|
|
5430
5905
|
&& liveStreamMatchesOptions(activeLiveStream, normalized)
|
|
5431
5906
|
&& liveStreamIsReusable(activeLiveStream)) {
|
|
5432
|
-
return {
|
|
5433
|
-
ok: true,
|
|
5434
|
-
commandId: activeLiveStream.commandId,
|
|
5435
|
-
|
|
5907
|
+
return {
|
|
5908
|
+
ok: true,
|
|
5909
|
+
commandId: activeLiveStream.commandId,
|
|
5910
|
+
sessionId: device.sessionId,
|
|
5911
|
+
streamId,
|
|
5912
|
+
streamPurpose,
|
|
5436
5913
|
fps: Number(activeLiveStream.fps || fps),
|
|
5437
5914
|
mode: activeLiveStream.mode || transfer.mode,
|
|
5438
5915
|
frameMode: activeLiveStream.frameMode || transfer.frameMode,
|
|
5439
5916
|
monitorIndex: Number(activeLiveStream.monitorIndex || monitorIndex),
|
|
5440
5917
|
captureGeneration: Number(activeLiveStream.captureGeneration || 0),
|
|
5918
|
+
ready: liveStreamHasCurrentFrame(activeLiveStream),
|
|
5441
5919
|
reused: true
|
|
5442
5920
|
};
|
|
5443
5921
|
}
|
|
@@ -5454,15 +5932,18 @@ export function createRemoteHub(options = {}) {
|
|
|
5454
5932
|
monitorIndex
|
|
5455
5933
|
});
|
|
5456
5934
|
}
|
|
5457
|
-
return {
|
|
5458
|
-
ok: true,
|
|
5459
|
-
commandId: activeLiveStream.commandId,
|
|
5460
|
-
|
|
5935
|
+
return {
|
|
5936
|
+
ok: true,
|
|
5937
|
+
commandId: activeLiveStream.commandId,
|
|
5938
|
+
sessionId: device.sessionId,
|
|
5939
|
+
streamId,
|
|
5940
|
+
streamPurpose,
|
|
5461
5941
|
fps,
|
|
5462
5942
|
mode: transfer.mode,
|
|
5463
5943
|
frameMode: transfer.frameMode,
|
|
5464
5944
|
monitorIndex,
|
|
5465
5945
|
captureGeneration: Number(activeLiveStream.captureGeneration || 0),
|
|
5946
|
+
ready: liveStreamHasCurrentFrame(activeLiveStream),
|
|
5466
5947
|
reused: true
|
|
5467
5948
|
};
|
|
5468
5949
|
}
|
|
@@ -5522,24 +6003,20 @@ export function createRemoteHub(options = {}) {
|
|
|
5522
6003
|
return result;
|
|
5523
6004
|
}
|
|
5524
6005
|
|
|
5525
|
-
const previousCommandId = safeString(activeLiveStream?.commandId, 128);
|
|
5526
|
-
const replacingActiveStream = activeLiveStream?.active === true
|
|
5527
|
-
&& !!previousCommandId
|
|
5528
|
-
&& previousCommandId !== commandId;
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
openTransport: replacingActiveStream ? activeLiveStream.openTransport || '' : '',
|
|
5540
|
-
mode: transfer.mode,
|
|
5541
|
-
frameMode: transfer.frameMode,
|
|
5542
|
-
frameProfile: transfer.frameProfile,
|
|
6006
|
+
const previousCommandId = safeString(activeLiveStream?.commandId, 128);
|
|
6007
|
+
const replacingActiveStream = activeLiveStream?.active === true
|
|
6008
|
+
&& !!previousCommandId
|
|
6009
|
+
&& previousCommandId !== commandId;
|
|
6010
|
+
const nextDescriptor = {
|
|
6011
|
+
streamId,
|
|
6012
|
+
commandId,
|
|
6013
|
+
active: true,
|
|
6014
|
+
open: false,
|
|
6015
|
+
openedAt: '',
|
|
6016
|
+
openTransport: '',
|
|
6017
|
+
mode: transfer.mode,
|
|
6018
|
+
frameMode: transfer.frameMode,
|
|
6019
|
+
frameProfile: transfer.frameProfile,
|
|
5543
6020
|
encoding: transfer.encoding,
|
|
5544
6021
|
codec: transfer.codec,
|
|
5545
6022
|
compression: transfer.compression,
|
|
@@ -5550,17 +6027,32 @@ export function createRemoteHub(options = {}) {
|
|
|
5550
6027
|
maxWidth,
|
|
5551
6028
|
maxHeight,
|
|
5552
6029
|
quality,
|
|
5553
|
-
monitorIndex,
|
|
5554
|
-
monitorCount: 1,
|
|
5555
|
-
startedAt:
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
6030
|
+
monitorIndex,
|
|
6031
|
+
monitorCount: 1,
|
|
6032
|
+
startedAt: now,
|
|
6033
|
+
restartToken: safeString(options.restartToken, 128),
|
|
6034
|
+
stoppedAt: '',
|
|
6035
|
+
stopReason: '',
|
|
6036
|
+
lastFrameAt: '',
|
|
5559
6037
|
lastFrameSeq: 0,
|
|
5560
6038
|
framesReceived: 0,
|
|
5561
6039
|
streamPurpose,
|
|
5562
|
-
captureGeneration
|
|
5563
|
-
|
|
6040
|
+
captureGeneration,
|
|
6041
|
+
latestFrame: null,
|
|
6042
|
+
width: 0,
|
|
6043
|
+
height: 0,
|
|
6044
|
+
sourceWidth: 0,
|
|
6045
|
+
sourceHeight: 0
|
|
6046
|
+
};
|
|
6047
|
+
if (replacingActiveStream) {
|
|
6048
|
+
setPendingLiveStreamDescriptor(activeLiveStream, nextDescriptor);
|
|
6049
|
+
scheduleLiveStreamReplacementTimeout(device, activeLiveStream);
|
|
6050
|
+
setDeviceLiveStream(device, activeLiveStream);
|
|
6051
|
+
} else {
|
|
6052
|
+
const nextStreamState = { ...nextDescriptor };
|
|
6053
|
+
setPendingLiveStreamDescriptor(nextStreamState, null);
|
|
6054
|
+
setDeviceLiveStream(device, nextStreamState);
|
|
6055
|
+
}
|
|
5564
6056
|
device.counters.liveStreamsStarted += 1;
|
|
5565
6057
|
emitRemoteEvent('RemoteLiveStreamStarted', device, {
|
|
5566
6058
|
streamId,
|
|
@@ -5573,7 +6065,19 @@ export function createRemoteHub(options = {}) {
|
|
|
5573
6065
|
monitorIndex
|
|
5574
6066
|
});
|
|
5575
6067
|
|
|
5576
|
-
return {
|
|
6068
|
+
return {
|
|
6069
|
+
ok: true,
|
|
6070
|
+
commandId,
|
|
6071
|
+
sessionId: device.sessionId,
|
|
6072
|
+
streamId,
|
|
6073
|
+
streamPurpose,
|
|
6074
|
+
fps,
|
|
6075
|
+
mode: transfer.mode,
|
|
6076
|
+
frameMode: transfer.frameMode,
|
|
6077
|
+
monitorIndex,
|
|
6078
|
+
captureGeneration,
|
|
6079
|
+
ready: false
|
|
6080
|
+
};
|
|
5577
6081
|
}
|
|
5578
6082
|
|
|
5579
6083
|
function stopLiveStream(deviceId, options = {}) {
|
|
@@ -5583,24 +6087,25 @@ export function createRemoteHub(options = {}) {
|
|
|
5583
6087
|
const purposeStreamId = streamPurpose ? makeStableLiveStreamId(deviceId, streamPurpose) : '';
|
|
5584
6088
|
const streamId = requestedStreamId || purposeStreamId || device?.activeLiveStream?.streamId || '';
|
|
5585
6089
|
const streamState = getDeviceLiveStream(device, streamId);
|
|
5586
|
-
if (streamState && streamState.active !== true) {
|
|
5587
|
-
|
|
6090
|
+
if (streamState && streamState.active !== true) {
|
|
6091
|
+
clearPendingLiveStreamDescriptor(device, streamState);
|
|
6092
|
+
return {
|
|
5588
6093
|
ok: true,
|
|
5589
6094
|
commandId: safeString(options.commandId, 128),
|
|
5590
6095
|
streamId,
|
|
5591
|
-
alreadyStopped: true
|
|
5592
|
-
};
|
|
5593
|
-
}
|
|
5594
|
-
if (
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
streamState.
|
|
5602
|
-
streamState.stoppedAt = new Date().toISOString();
|
|
5603
|
-
streamState.stopReason = 'manager-request';
|
|
6096
|
+
alreadyStopped: true
|
|
6097
|
+
};
|
|
6098
|
+
}
|
|
6099
|
+
if (streamState) {
|
|
6100
|
+
clearPendingLiveStreamDescriptor(device, streamState);
|
|
6101
|
+
}
|
|
6102
|
+
if (device?.synthetic === true && device.connected) {
|
|
6103
|
+
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
6104
|
+
device.counters.commandsSent += 1;
|
|
6105
|
+
if (streamState) {
|
|
6106
|
+
streamState.active = false;
|
|
6107
|
+
streamState.stoppedAt = new Date().toISOString();
|
|
6108
|
+
streamState.stopReason = 'manager-request';
|
|
5604
6109
|
}
|
|
5605
6110
|
device.counters.liveStreamsStopped += 1;
|
|
5606
6111
|
emitRemoteEvent('RemoteLiveStreamStopped', device, {
|
|
@@ -5628,14 +6133,11 @@ export function createRemoteHub(options = {}) {
|
|
|
5628
6133
|
if (!result.ok) {
|
|
5629
6134
|
return result;
|
|
5630
6135
|
}
|
|
5631
|
-
|
|
5632
|
-
if (streamState) {
|
|
5633
|
-
streamState.active = false;
|
|
5634
|
-
streamState.
|
|
5635
|
-
streamState.
|
|
5636
|
-
streamState.pendingRestartToken = '';
|
|
5637
|
-
streamState.stoppedAt = new Date().toISOString();
|
|
5638
|
-
streamState.stopReason = 'manager-request';
|
|
6136
|
+
|
|
6137
|
+
if (streamState) {
|
|
6138
|
+
streamState.active = false;
|
|
6139
|
+
streamState.stoppedAt = new Date().toISOString();
|
|
6140
|
+
streamState.stopReason = 'manager-request';
|
|
5639
6141
|
}
|
|
5640
6142
|
device.counters.liveStreamsStopped += 1;
|
|
5641
6143
|
emitRemoteEvent('RemoteLiveStreamStopped', device, {
|