livedesk 0.1.368 → 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 +848 -268
- 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-BJNF_t-6.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;
|
|
@@ -5176,13 +5683,35 @@ export function createRemoteHub(options = {}) {
|
|
|
5176
5683
|
return Number.isFinite(startedAt) && Date.now() - startedAt < LIVE_STREAM_PENDING_REUSE_MS;
|
|
5177
5684
|
}
|
|
5178
5685
|
|
|
5179
|
-
function liveStreamReplacementStillPending(activeLiveStream) {
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
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
|
+
}
|
|
5186
5715
|
|
|
5187
5716
|
function getLiveStreamFreshWindowMs(activeLiveStream) {
|
|
5188
5717
|
const fps = Number(activeLiveStream?.fps || 0);
|
|
@@ -5242,17 +5771,20 @@ export function createRemoteHub(options = {}) {
|
|
|
5242
5771
|
const activeLiveStream = getDeviceLiveStream(device, streamId);
|
|
5243
5772
|
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
5244
5773
|
if (activeLiveStream
|
|
5245
|
-
&& liveStreamMatchesOptions(activeLiveStream, normalized)
|
|
5246
|
-
&& (activeLiveStream.open === true || liveStreamStartStillPending(activeLiveStream))) {
|
|
5247
|
-
return {
|
|
5248
|
-
ok: true,
|
|
5249
|
-
commandId: activeLiveStream.commandId,
|
|
5250
|
-
|
|
5251
|
-
|
|
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,
|
|
5252
5783
|
mode: transfer.mode,
|
|
5253
|
-
frameMode: transfer.frameMode,
|
|
5784
|
+
frameMode: transfer.frameMode,
|
|
5254
5785
|
monitorIndex,
|
|
5255
5786
|
captureGeneration: Number(activeLiveStream.captureGeneration || 0),
|
|
5787
|
+
ready: liveStreamHasCurrentFrame(activeLiveStream),
|
|
5256
5788
|
synthetic: true,
|
|
5257
5789
|
reused: true
|
|
5258
5790
|
};
|
|
@@ -5305,7 +5837,20 @@ export function createRemoteHub(options = {}) {
|
|
|
5305
5837
|
mode: transfer.mode,
|
|
5306
5838
|
synthetic: true
|
|
5307
5839
|
});
|
|
5308
|
-
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
|
+
};
|
|
5309
5854
|
}
|
|
5310
5855
|
|
|
5311
5856
|
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
@@ -5320,46 +5865,57 @@ export function createRemoteHub(options = {}) {
|
|
|
5320
5865
|
const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
|
|
5321
5866
|
const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
|
|
5322
5867
|
const activeLiveStream = getDeviceLiveStream(device, streamId);
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
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,
|
|
5340
5890
|
pending: true,
|
|
5341
5891
|
reused: true
|
|
5342
5892
|
};
|
|
5343
|
-
}
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5893
|
+
}
|
|
5894
|
+
failPendingLiveStream(
|
|
5895
|
+
device,
|
|
5896
|
+
pendingDescriptor.commandId,
|
|
5897
|
+
liveStreamReplacementStillPending(activeLiveStream)
|
|
5898
|
+
? 'stream-restart-superseded'
|
|
5899
|
+
: 'stream-start-timeout');
|
|
5900
|
+
}
|
|
5348
5901
|
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
5349
5902
|
if (activeLiveStream
|
|
5350
5903
|
&& options.forceRestart !== true
|
|
5351
5904
|
&& options.reuseExisting === true
|
|
5352
5905
|
&& liveStreamMatchesOptions(activeLiveStream, normalized)
|
|
5353
5906
|
&& liveStreamIsReusable(activeLiveStream)) {
|
|
5354
|
-
return {
|
|
5355
|
-
ok: true,
|
|
5356
|
-
commandId: activeLiveStream.commandId,
|
|
5357
|
-
|
|
5907
|
+
return {
|
|
5908
|
+
ok: true,
|
|
5909
|
+
commandId: activeLiveStream.commandId,
|
|
5910
|
+
sessionId: device.sessionId,
|
|
5911
|
+
streamId,
|
|
5912
|
+
streamPurpose,
|
|
5358
5913
|
fps: Number(activeLiveStream.fps || fps),
|
|
5359
5914
|
mode: activeLiveStream.mode || transfer.mode,
|
|
5360
5915
|
frameMode: activeLiveStream.frameMode || transfer.frameMode,
|
|
5361
5916
|
monitorIndex: Number(activeLiveStream.monitorIndex || monitorIndex),
|
|
5362
5917
|
captureGeneration: Number(activeLiveStream.captureGeneration || 0),
|
|
5918
|
+
ready: liveStreamHasCurrentFrame(activeLiveStream),
|
|
5363
5919
|
reused: true
|
|
5364
5920
|
};
|
|
5365
5921
|
}
|
|
@@ -5376,15 +5932,18 @@ export function createRemoteHub(options = {}) {
|
|
|
5376
5932
|
monitorIndex
|
|
5377
5933
|
});
|
|
5378
5934
|
}
|
|
5379
|
-
return {
|
|
5380
|
-
ok: true,
|
|
5381
|
-
commandId: activeLiveStream.commandId,
|
|
5382
|
-
|
|
5935
|
+
return {
|
|
5936
|
+
ok: true,
|
|
5937
|
+
commandId: activeLiveStream.commandId,
|
|
5938
|
+
sessionId: device.sessionId,
|
|
5939
|
+
streamId,
|
|
5940
|
+
streamPurpose,
|
|
5383
5941
|
fps,
|
|
5384
5942
|
mode: transfer.mode,
|
|
5385
5943
|
frameMode: transfer.frameMode,
|
|
5386
5944
|
monitorIndex,
|
|
5387
5945
|
captureGeneration: Number(activeLiveStream.captureGeneration || 0),
|
|
5946
|
+
ready: liveStreamHasCurrentFrame(activeLiveStream),
|
|
5388
5947
|
reused: true
|
|
5389
5948
|
};
|
|
5390
5949
|
}
|
|
@@ -5444,24 +6003,20 @@ export function createRemoteHub(options = {}) {
|
|
|
5444
6003
|
return result;
|
|
5445
6004
|
}
|
|
5446
6005
|
|
|
5447
|
-
const previousCommandId = safeString(activeLiveStream?.commandId, 128);
|
|
5448
|
-
const replacingActiveStream = activeLiveStream?.active === true
|
|
5449
|
-
&& !!previousCommandId
|
|
5450
|
-
&& previousCommandId !== commandId;
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
openTransport: replacingActiveStream ? activeLiveStream.openTransport || '' : '',
|
|
5462
|
-
mode: transfer.mode,
|
|
5463
|
-
frameMode: transfer.frameMode,
|
|
5464
|
-
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,
|
|
5465
6020
|
encoding: transfer.encoding,
|
|
5466
6021
|
codec: transfer.codec,
|
|
5467
6022
|
compression: transfer.compression,
|
|
@@ -5472,17 +6027,32 @@ export function createRemoteHub(options = {}) {
|
|
|
5472
6027
|
maxWidth,
|
|
5473
6028
|
maxHeight,
|
|
5474
6029
|
quality,
|
|
5475
|
-
monitorIndex,
|
|
5476
|
-
monitorCount: 1,
|
|
5477
|
-
startedAt:
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
6030
|
+
monitorIndex,
|
|
6031
|
+
monitorCount: 1,
|
|
6032
|
+
startedAt: now,
|
|
6033
|
+
restartToken: safeString(options.restartToken, 128),
|
|
6034
|
+
stoppedAt: '',
|
|
6035
|
+
stopReason: '',
|
|
6036
|
+
lastFrameAt: '',
|
|
5481
6037
|
lastFrameSeq: 0,
|
|
5482
6038
|
framesReceived: 0,
|
|
5483
6039
|
streamPurpose,
|
|
5484
|
-
captureGeneration
|
|
5485
|
-
|
|
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
|
+
}
|
|
5486
6056
|
device.counters.liveStreamsStarted += 1;
|
|
5487
6057
|
emitRemoteEvent('RemoteLiveStreamStarted', device, {
|
|
5488
6058
|
streamId,
|
|
@@ -5495,7 +6065,19 @@ export function createRemoteHub(options = {}) {
|
|
|
5495
6065
|
monitorIndex
|
|
5496
6066
|
});
|
|
5497
6067
|
|
|
5498
|
-
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
|
+
};
|
|
5499
6081
|
}
|
|
5500
6082
|
|
|
5501
6083
|
function stopLiveStream(deviceId, options = {}) {
|
|
@@ -5505,24 +6087,25 @@ export function createRemoteHub(options = {}) {
|
|
|
5505
6087
|
const purposeStreamId = streamPurpose ? makeStableLiveStreamId(deviceId, streamPurpose) : '';
|
|
5506
6088
|
const streamId = requestedStreamId || purposeStreamId || device?.activeLiveStream?.streamId || '';
|
|
5507
6089
|
const streamState = getDeviceLiveStream(device, streamId);
|
|
5508
|
-
if (streamState && streamState.active !== true) {
|
|
5509
|
-
|
|
6090
|
+
if (streamState && streamState.active !== true) {
|
|
6091
|
+
clearPendingLiveStreamDescriptor(device, streamState);
|
|
6092
|
+
return {
|
|
5510
6093
|
ok: true,
|
|
5511
6094
|
commandId: safeString(options.commandId, 128),
|
|
5512
6095
|
streamId,
|
|
5513
|
-
alreadyStopped: true
|
|
5514
|
-
};
|
|
5515
|
-
}
|
|
5516
|
-
if (
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
streamState.
|
|
5524
|
-
streamState.stoppedAt = new Date().toISOString();
|
|
5525
|
-
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';
|
|
5526
6109
|
}
|
|
5527
6110
|
device.counters.liveStreamsStopped += 1;
|
|
5528
6111
|
emitRemoteEvent('RemoteLiveStreamStopped', device, {
|
|
@@ -5550,14 +6133,11 @@ export function createRemoteHub(options = {}) {
|
|
|
5550
6133
|
if (!result.ok) {
|
|
5551
6134
|
return result;
|
|
5552
6135
|
}
|
|
5553
|
-
|
|
5554
|
-
if (streamState) {
|
|
5555
|
-
streamState.active = false;
|
|
5556
|
-
streamState.
|
|
5557
|
-
streamState.
|
|
5558
|
-
streamState.pendingRestartToken = '';
|
|
5559
|
-
streamState.stoppedAt = new Date().toISOString();
|
|
5560
|
-
streamState.stopReason = 'manager-request';
|
|
6136
|
+
|
|
6137
|
+
if (streamState) {
|
|
6138
|
+
streamState.active = false;
|
|
6139
|
+
streamState.stoppedAt = new Date().toISOString();
|
|
6140
|
+
streamState.stopReason = 'manager-request';
|
|
5561
6141
|
}
|
|
5562
6142
|
device.counters.liveStreamsStopped += 1;
|
|
5563
6143
|
emitRemoteEvent('RemoteLiveStreamStopped', device, {
|