@livedesk/hub 0.1.66 → 0.1.68

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/src/remote-hub.js CHANGED
@@ -17,14 +17,15 @@ import {
17
17
  BoundedSegmentedBuffer,
18
18
  createBoundedAgentBinaryIngressLane
19
19
  } from './transport/agent-binary-ingress.js';
20
- import {
21
- REMOTE_CLIPBOARD_FILE_ACTIONS,
20
+ import {
21
+ REMOTE_CLIPBOARD_FILE_ACTIONS,
22
22
  REMOTE_CLIPBOARD_LIMITS,
23
23
  REMOTE_CLIPBOARD_PROTOCOL,
24
24
  normalizeRemoteClipboardCommandResult,
25
25
  normalizeRemoteClipboardManifest,
26
26
  remoteClipboardContentNeedsFileTransfer
27
- } from './remote-clipboard-contract.mjs';
27
+ } from './remote-clipboard-contract.mjs';
28
+ import { liveProfileSatisfiesDemand } from './shared-wall-profile-contract.mjs';
28
29
 
29
30
  const DEFAULT_REMOTE_HUB_PORT = 5197;
30
31
  const DEFAULT_REMOTE_HUB_HOST = '0.0.0.0';
@@ -74,6 +75,8 @@ const LIVE_STREAM_MIN_FRESH_MS = 3000;
74
75
  const LIVE_STREAM_MAX_FRESH_MS = 12000;
75
76
  const TASK_BATCH_OWNER = Symbol('remote-hub-task-batch-owner');
76
77
  const DEFAULT_REMOTE_INPUT_ACK_TIMEOUT_MS = 5000;
78
+ const REMOTE_INPUT_FAILURE_LOG_INTERVAL_MS = 5000;
79
+ const REMOTE_INPUT_FAILURE_LOG_KEY_LIMIT = 32;
77
80
  // RemoteFast owns a shared 7s native stop deadline. Keep four seconds for a
78
81
  // priority command.result to cross either Direct or encrypted Relay transport.
79
82
  const DEFAULT_LIVE_CAPTURE_STOP_ACK_TIMEOUT_MS = 11000;
@@ -1530,7 +1533,7 @@ function readCapabilityFlag(capabilities, key) {
1530
1533
  return /^(1|true|yes|on)$/i.test(String(value || '').trim());
1531
1534
  }
1532
1535
 
1533
- function createDeviceCounters(overrides = {}) {
1536
+ function createDeviceCounters(overrides = {}) {
1534
1537
  return {
1535
1538
  messagesReceived: 0,
1536
1539
  statusReceived: 0,
@@ -1546,8 +1549,10 @@ function createDeviceCounters(overrides = {}) {
1546
1549
  audioStreamsStopped: 0,
1547
1550
  audioFramesReceived: 0,
1548
1551
  audioFramesDropped: 0,
1549
- audioStatusReceived: 0,
1550
- tasksQueued: 0,
1552
+ audioStatusReceived: 0,
1553
+ remoteInputsApplied: 0,
1554
+ remoteInputsRejected: 0,
1555
+ tasksQueued: 0,
1551
1556
  taskResultsReceived: 0,
1552
1557
  taskResultsFailed: 0,
1553
1558
  taskResultsTimedOut: 0,
@@ -1815,6 +1820,38 @@ function serializeDeviceTask(task) {
1815
1820
  };
1816
1821
  }
1817
1822
 
1823
+ function createRemoteInputDiagnostics() {
1824
+ return {
1825
+ appliedCount: 0,
1826
+ rejectedCount: 0,
1827
+ lastOutcome: null,
1828
+ lastApplied: null,
1829
+ lastFailure: null
1830
+ };
1831
+ }
1832
+
1833
+ function serializeRemoteInputDiagnostics(diagnostics) {
1834
+ const source = diagnostics && typeof diagnostics === 'object'
1835
+ ? diagnostics
1836
+ : createRemoteInputDiagnostics();
1837
+ return {
1838
+ appliedCount: Math.max(0, Number(source.appliedCount) || 0),
1839
+ rejectedCount: Math.max(0, Number(source.rejectedCount) || 0),
1840
+ lastOutcome: source.lastOutcome ? { ...source.lastOutcome } : null,
1841
+ lastApplied: source.lastApplied ? { ...source.lastApplied } : null,
1842
+ lastFailure: source.lastFailure ? { ...source.lastFailure } : null
1843
+ };
1844
+ }
1845
+
1846
+ function firstOptionalBoolean(...values) {
1847
+ for (const value of values) {
1848
+ if (typeof value === 'boolean') {
1849
+ return value;
1850
+ }
1851
+ }
1852
+ return null;
1853
+ }
1854
+
1818
1855
  function serializeDevice(device, options = {}) {
1819
1856
  if (!device) {
1820
1857
  return null;
@@ -1895,8 +1932,9 @@ function serializeDevice(device, options = {}) {
1895
1932
  recentTasks: Array.isArray(device.recentTasks)
1896
1933
  ? device.recentTasks.map(serializeDeviceTask).filter(Boolean)
1897
1934
  : [],
1898
- status: { ...device.status },
1899
- counters: { ...device.counters }
1935
+ status: { ...device.status },
1936
+ remoteInputDiagnostics: serializeRemoteInputDiagnostics(device.remoteInputDiagnostics),
1937
+ counters: { ...device.counters }
1900
1938
  };
1901
1939
  }
1902
1940
 
@@ -4942,9 +4980,11 @@ export function createRemoteHub(options = {}) {
4942
4980
  latestAudioStatus: null,
4943
4981
  latestTask,
4944
4982
  recentTasks: latestTask ? [latestTask] : [],
4945
- pendingTaskCommands: new Map(),
4946
- pendingTaskTimers: new Map(),
4947
- counters: createDeviceCounters({
4983
+ pendingTaskCommands: new Map(),
4984
+ pendingTaskTimers: new Map(),
4985
+ remoteInputDiagnostics: createRemoteInputDiagnostics(),
4986
+ remoteInputFailureLogAt: new Map(),
4987
+ counters: createDeviceCounters({
4948
4988
  messagesReceived: 1,
4949
4989
  statusReceived: 1,
4950
4990
  tasksQueued: latestTask ? 1 : 0,
@@ -5239,9 +5279,11 @@ export function createRemoteHub(options = {}) {
5239
5279
  },
5240
5280
  latestTask: null,
5241
5281
  recentTasks: [],
5242
- pendingTaskCommands: new Map(),
5243
- pendingTaskTimers: new Map(),
5244
- synthetic: false,
5282
+ pendingTaskCommands: new Map(),
5283
+ pendingTaskTimers: new Map(),
5284
+ remoteInputDiagnostics: createRemoteInputDiagnostics(),
5285
+ remoteInputFailureLogAt: new Map(),
5286
+ synthetic: false,
5245
5287
  counters: createDeviceCounters({ messagesReceived: 1 })
5246
5288
  };
5247
5289
 
@@ -5979,9 +6021,10 @@ export function createRemoteHub(options = {}) {
5979
6021
  function acknowledgePendingDedicatedInput(device, message = {}) {
5980
6022
  const state = device?.pendingDedicatedInputAckState;
5981
6023
  if (!state?.current) {
5982
- return false;
6024
+ return null;
5983
6025
  }
5984
6026
  if (dedicatedInputAckMatches(state.current, message)) {
6027
+ const acknowledged = state.current;
5985
6028
  if (state.timeoutTimer) {
5986
6029
  clearTimeout(state.timeoutTimer);
5987
6030
  state.timeoutTimer = null;
@@ -5989,15 +6032,16 @@ export function createRemoteHub(options = {}) {
5989
6032
  state.current = state.next;
5990
6033
  state.next = null;
5991
6034
  armPendingDedicatedInputAck(device);
5992
- return true;
6035
+ return acknowledged;
5993
6036
  }
5994
6037
  if (dedicatedInputAckMatches(state.next, message)) {
5995
6038
  // A later ordered acknowledgement proves that the input reader is
5996
6039
  // still draining, even if an earlier diagnostic response vanished.
6040
+ const acknowledged = state.next;
5997
6041
  clearPendingDedicatedInputAcks(device);
5998
- return true;
6042
+ return acknowledged;
5999
6043
  }
6000
- return false;
6044
+ return null;
6001
6045
  }
6002
6046
 
6003
6047
  function pruneCompletedInputFallbacks(device, now = Date.now()) {
@@ -6104,14 +6148,39 @@ export function createRemoteHub(options = {}) {
6104
6148
  || pending?.inputEventId,
6105
6149
  128),
6106
6150
  inputSeq,
6107
- inputType: safeString(
6151
+ inputType: safeString(
6108
6152
  message.inputType
6109
6153
  || message.InputType
6110
6154
  || result.inputType
6111
6155
  || result.InputType
6112
6156
  || pending?.inputType,
6113
- 48),
6114
- monitorIndex: Number(
6157
+ 48),
6158
+ controlSessionId: safeString(
6159
+ message.controlSessionId
6160
+ || message.ControlSessionId
6161
+ || result.controlSessionId
6162
+ || result.ControlSessionId
6163
+ || pending?.controlSessionId
6164
+ || device.sessionId,
6165
+ 160),
6166
+ controlCommandId: safeString(
6167
+ message.controlCommandId
6168
+ || message.ControlCommandId
6169
+ || result.controlCommandId
6170
+ || result.ControlCommandId
6171
+ || pending?.controlCommandId,
6172
+ 128),
6173
+ captureGeneration: Math.max(0, Number(
6174
+ message.captureGeneration
6175
+ ?? message.CaptureGeneration
6176
+ ?? result.captureGeneration
6177
+ ?? result.CaptureGeneration
6178
+ ?? pending?.captureGeneration
6179
+ ?? 0) || 0),
6180
+ inputSocketConnectionId: safeString(
6181
+ pending?.inputSocketConnectionId,
6182
+ 128),
6183
+ monitorIndex: Number(
6115
6184
  message.monitorIndex
6116
6185
  ?? message.MonitorIndex
6117
6186
  ?? result.monitorIndex
@@ -6177,27 +6246,144 @@ export function createRemoteHub(options = {}) {
6177
6246
  || message.FocusChanged === true
6178
6247
  || result.focusChanged === true
6179
6248
  || result.FocusChanged === true,
6180
- inputDiagnostic: message.inputDiagnostic === true
6249
+ inputDiagnostic: message.inputDiagnostic === true
6181
6250
  || message.InputDiagnostic === true
6182
- || result.inputDiagnostic === true
6183
- || result.InputDiagnostic === true,
6184
- fallback: pending?.fallback === true,
6185
- hubAcknowledgedAtEpochMs: Date.now()
6186
- };
6187
- }
6188
-
6189
- function emitRemoteInputApplied(device, message = {}, pending = null) {
6190
- const outcome = buildRemoteInputOutcome(device, message, pending);
6191
- emitEvent('RemoteInputApplied', {
6192
- ...outcome,
6193
- agentApplyMs: Number(
6194
- message.agentApplyMs
6195
- ?? message.AgentApplyMs
6196
- ?? message.result?.agentApplyMs
6197
- ?? message.result?.AgentApplyMs
6198
- ?? 0) || 0
6199
- });
6200
- }
6251
+ || result.inputDiagnostic === true
6252
+ || result.InputDiagnostic === true,
6253
+ failureStage: safeString(
6254
+ message.failureStage
6255
+ || message.FailureStage
6256
+ || result.failureStage
6257
+ || result.FailureStage,
6258
+ 40),
6259
+ exceptionType: safeString(
6260
+ message.exceptionType
6261
+ || message.ExceptionType
6262
+ || result.exceptionType
6263
+ || result.ExceptionType,
6264
+ 80),
6265
+ permissionSupported: firstOptionalBoolean(
6266
+ message.permissionSupported,
6267
+ message.PermissionSupported,
6268
+ result.permissionSupported,
6269
+ result.PermissionSupported),
6270
+ permissionGranted: firstOptionalBoolean(
6271
+ message.permissionGranted,
6272
+ message.PermissionGranted,
6273
+ result.permissionGranted,
6274
+ result.PermissionGranted),
6275
+ macAccessibilityTrusted: firstOptionalBoolean(
6276
+ message.macAccessibilityTrusted,
6277
+ message.MacAccessibilityTrusted,
6278
+ result.macAccessibilityTrusted,
6279
+ result.MacAccessibilityTrusted),
6280
+ macPostEventAccess: firstOptionalBoolean(
6281
+ message.macPostEventAccess,
6282
+ message.MacPostEventAccess,
6283
+ result.macPostEventAccess,
6284
+ result.MacPostEventAccess),
6285
+ fallback: pending?.fallback === true,
6286
+ hubAcknowledgedAtEpochMs: Date.now()
6287
+ };
6288
+ }
6289
+
6290
+ function rememberRemoteInputOutcome(device, outcome, state) {
6291
+ const diagnostics = device.remoteInputDiagnostics
6292
+ || (device.remoteInputDiagnostics = createRemoteInputDiagnostics());
6293
+ const record = {
6294
+ state,
6295
+ receivedAt: new Date(outcome.hubAcknowledgedAtEpochMs).toISOString(),
6296
+ inputType: safeString(outcome.inputType, 48),
6297
+ inputSeq: Math.max(0, Number(outcome.inputSeq) || 0),
6298
+ monitorIndex: Math.max(0, Number(outcome.monitorIndex) || 0),
6299
+ sessionId: safeString(outcome.controlSessionId || device.sessionId, 160),
6300
+ controlCommandId: safeString(outcome.controlCommandId, 128),
6301
+ captureGeneration: Math.max(0, Number(outcome.captureGeneration) || 0),
6302
+ inputSocketConnectionId: safeString(outcome.inputSocketConnectionId, 128),
6303
+ fallback: outcome.fallback === true
6304
+ };
6305
+ if (state === 'applied') {
6306
+ diagnostics.appliedCount += 1;
6307
+ device.counters.remoteInputsApplied += 1;
6308
+ const applied = {
6309
+ ...record,
6310
+ duplicate: outcome.duplicate === true,
6311
+ agentApplyMs: Math.max(0, Number(outcome.agentApplyMs) || 0),
6312
+ nativeInputBackend: safeString(outcome.nativeInputBackend, 80)
6313
+ };
6314
+ diagnostics.lastApplied = applied;
6315
+ diagnostics.lastOutcome = applied;
6316
+ return applied;
6317
+ }
6318
+
6319
+ diagnostics.rejectedCount += 1;
6320
+ device.counters.remoteInputsRejected += 1;
6321
+ const failed = {
6322
+ ...record,
6323
+ errorKind: outcome.errorKind === 'native-apply' ? 'native-apply' : 'route',
6324
+ failureStage: safeString(outcome.failureStage, 40) || 'unknown',
6325
+ exceptionType: safeString(outcome.exceptionType, 80),
6326
+ permissionSupported: firstOptionalBoolean(outcome.permissionSupported),
6327
+ permissionGranted: firstOptionalBoolean(outcome.permissionGranted),
6328
+ macAccessibilityTrusted: firstOptionalBoolean(outcome.macAccessibilityTrusted),
6329
+ macPostEventAccess: firstOptionalBoolean(outcome.macPostEventAccess),
6330
+ error: safeDiagnosticText(outcome.error, 600) || 'remote-input-failed',
6331
+ failedAtEpochMs: Math.max(0, Number(outcome.failedAtEpochMs) || 0)
6332
+ };
6333
+ diagnostics.lastFailure = failed;
6334
+ diagnostics.lastOutcome = failed;
6335
+ return failed;
6336
+ }
6337
+
6338
+ function logRemoteInputFailure(device, failure) {
6339
+ const rateLimits = device.remoteInputFailureLogAt
6340
+ || (device.remoteInputFailureLogAt = new Map());
6341
+ const key = `${failure.errorKind}:${failure.failureStage}:${failure.exceptionType}:${failure.error}`;
6342
+ const now = Date.now();
6343
+ const lastAt = Number(rateLimits.get(key) || 0);
6344
+ if (lastAt > 0 && now - lastAt < REMOTE_INPUT_FAILURE_LOG_INTERVAL_MS) {
6345
+ return;
6346
+ }
6347
+ rateLimits.delete(key);
6348
+ rateLimits.set(key, now);
6349
+ while (rateLimits.size > REMOTE_INPUT_FAILURE_LOG_KEY_LIMIT) {
6350
+ const oldestKey = rateLimits.keys().next().value;
6351
+ if (!oldestKey) break;
6352
+ rateLimits.delete(oldestKey);
6353
+ }
6354
+ const shortId = value => safeString(value, 128).slice(0, 12) || 'unknown';
6355
+ const optionalFlag = value => typeof value === 'boolean' ? String(value) : 'unknown';
6356
+ logWarn(
6357
+ 'remote',
6358
+ `input rejected device=${device.deviceName} (${device.deviceId})`
6359
+ + ` session=${shortId(failure.sessionId)}`
6360
+ + ` command=${shortId(failure.controlCommandId)}`
6361
+ + ` generation=${failure.captureGeneration}`
6362
+ + ` monitor=${failure.monitorIndex}`
6363
+ + ` type=${failure.inputType || 'unknown'}`
6364
+ + ` seq=${failure.inputSeq}`
6365
+ + ` kind=${failure.errorKind}`
6366
+ + ` stage=${failure.failureStage}`
6367
+ + ` exception=${failure.exceptionType || 'unknown'}`
6368
+ + ` permission=${optionalFlag(failure.permissionGranted)}`
6369
+ + ` accessibility=${optionalFlag(failure.macAccessibilityTrusted)}`
6370
+ + ` postEvent=${optionalFlag(failure.macPostEventAccess)}`
6371
+ + ` error=${failure.error}`);
6372
+ }
6373
+
6374
+ function emitRemoteInputApplied(device, message = {}, pending = null) {
6375
+ const outcome = {
6376
+ ...buildRemoteInputOutcome(device, message, pending),
6377
+ agentApplyMs: Number(
6378
+ message.agentApplyMs
6379
+ ?? message.AgentApplyMs
6380
+ ?? message.result?.agentApplyMs
6381
+ ?? message.result?.AgentApplyMs
6382
+ ?? 0) || 0
6383
+ };
6384
+ rememberRemoteInputOutcome(device, outcome, 'applied');
6385
+ emitEvent('RemoteInputApplied', outcome);
6386
+ }
6201
6387
 
6202
6388
  function emitRemoteInputError(
6203
6389
  device,
@@ -6206,29 +6392,37 @@ export function createRemoteHub(options = {}) {
6206
6392
  fallbackError = 'remote-input-failed',
6207
6393
  errorKind = 'route') {
6208
6394
  const outcome = buildRemoteInputOutcome(device, message, pending);
6209
- emitEvent('RemoteInputError', {
6395
+ const failure = {
6210
6396
  ...outcome,
6211
6397
  errorKind: errorKind === 'native-apply' ? 'native-apply' : 'route',
6212
- error: safeString(
6213
- message.error
6214
- || message.Error
6215
- || message.result?.error
6216
- || message.result?.Error
6217
- || fallbackError,
6398
+ error: safeDiagnosticText(
6399
+ message.error
6400
+ || message.Error
6401
+ || message.result?.error
6402
+ || message.result?.Error
6403
+ || fallbackError,
6218
6404
  600) || fallbackError,
6219
6405
  failedAtEpochMs: Number(
6220
6406
  message.failedAtEpochMs
6221
6407
  ?? message.FailedAtEpochMs
6222
6408
  ?? message.result?.failedAtEpochMs
6223
- ?? message.result?.FailedAtEpochMs
6224
- ?? 0) || 0
6225
- });
6226
- }
6227
-
6228
- function handleRemoteInputOutcome(device, message = {}, source = 'input') {
6409
+ ?? message.result?.FailedAtEpochMs
6410
+ ?? 0) || 0
6411
+ };
6412
+ const persistentFailure = rememberRemoteInputOutcome(device, failure, 'rejected');
6413
+ logRemoteInputFailure(device, persistentFailure);
6414
+ emitEvent('RemoteInputError', failure);
6415
+ }
6416
+
6417
+ function handleRemoteInputOutcome(
6418
+ device,
6419
+ message = {},
6420
+ source = 'input',
6421
+ acknowledgedPending = null) {
6229
6422
  const commandId = safeString(message.commandId || message.CommandId, 128);
6230
6423
  const inputSeq = Number(message.inputSeq ?? message.InputSeq ?? 0) || 0;
6231
- const pending = takePendingInputFallback(device, commandId, inputSeq);
6424
+ const pending = acknowledgedPending
6425
+ || takePendingInputFallback(device, commandId, inputSeq);
6232
6426
  if (!pending
6233
6427
  && source === 'main'
6234
6428
  && wasInputFallbackCompleted(device, commandId, inputSeq)) {
@@ -8571,12 +8765,12 @@ export function createRemoteHub(options = {}) {
8571
8765
  if (state.inputOnly) {
8572
8766
  device.inputLastSeenAt = new Date().toISOString();
8573
8767
  if (message.type === 'input.applied' || message.type === 'input.error') {
8574
- acknowledgePendingDedicatedInput(device, message);
8768
+ const acknowledgedPending = acknowledgePendingDedicatedInput(device, message);
8575
8769
  handleRemoteInputOutcome(device, {
8576
- ...message,
8577
- agentApplyMs: Number(message.agentApplyMs || 0) || 0
8578
- }, 'input');
8579
- }
8770
+ ...message,
8771
+ agentApplyMs: Number(message.agentApplyMs || 0) || 0
8772
+ }, 'input', acknowledgedPending);
8773
+ }
8580
8774
  return;
8581
8775
  }
8582
8776
  if (state.frameOnly) {
@@ -10923,23 +11117,29 @@ export function createRemoteHub(options = {}) {
10923
11117
  ensureLiveStreamReplacementTimers(device).set(stream.streamId, timer);
10924
11118
  }
10925
11119
 
10926
- function failPendingLiveStream(device, commandId, error = 'stream-start-failed') {
11120
+ function failPendingLiveStream(device, commandId, error = 'stream-start-failed') {
10927
11121
  const key = safeString(commandId, 128);
10928
11122
  if (!device || !key) {
10929
11123
  return false;
10930
11124
  }
10931
11125
  const streams = ensureDeviceLiveStreams(device);
10932
11126
  for (const stream of streams.values()) {
10933
- const pending = getPendingLiveStreamDescriptor(stream);
10934
- if (safeString(pending?.commandId, 128) !== key) {
10935
- continue;
10936
- }
10937
- clearPendingLiveStreamDescriptor(device, stream);
10938
- emitRemoteEvent('RemoteLiveStreamRestartFailed', device, {
10939
- streamId: stream.streamId,
10940
- commandId: key,
10941
- error: safeString(error, 500) || 'stream-start-failed'
10942
- });
11127
+ const pending = getPendingLiveStreamDescriptor(stream);
11128
+ if (safeString(pending?.commandId, 128) !== key) {
11129
+ continue;
11130
+ }
11131
+ const failedOwner = { ...pending };
11132
+ clearPendingLiveStreamDescriptor(device, stream);
11133
+ emitRemoteEvent('RemoteLiveStreamRestartFailed', device, {
11134
+ streamId: stream.streamId,
11135
+ commandId: key,
11136
+ streamPurpose: safeString(failedOwner.streamPurpose, 24) || 'wall',
11137
+ captureGeneration: Number(failedOwner.captureGeneration || 0),
11138
+ monitorIndex: Number(failedOwner.monitorIndex || 0),
11139
+ retainedCommandId: safeString(stream.commandId, 128),
11140
+ retainedCaptureGeneration: Number(stream.captureGeneration || 0),
11141
+ error: safeString(error, 500) || 'stream-start-failed'
11142
+ });
10943
11143
  return true;
10944
11144
  }
10945
11145
  for (const stream of streams.values()) {
@@ -11276,9 +11476,23 @@ export function createRemoteHub(options = {}) {
11276
11476
  === normalized.streamPurpose;
11277
11477
  }
11278
11478
 
11279
- function sharedLiveProfileResult(device, activeLiveStream, normalized, extra = {}) {
11280
- return {
11281
- ok: true,
11479
+ function sharedLiveProfileResult(
11480
+ device,
11481
+ activeLiveStream,
11482
+ normalized,
11483
+ extra = {},
11484
+ subscriberRequestedProfile = null
11485
+ ) {
11486
+ const requested = subscriberRequestedProfile && typeof subscriberRequestedProfile === 'object'
11487
+ ? {
11488
+ fps: clampNumber(subscriberRequestedProfile.fps, 1, normalized.streamPurpose === 'control' ? 60 : 30, normalized.fps),
11489
+ maxWidth: clampNumber(subscriberRequestedProfile.maxWidth, 320, 3840, normalized.maxWidth),
11490
+ maxHeight: clampNumber(subscriberRequestedProfile.maxHeight, 180, 2160, normalized.maxHeight),
11491
+ quality: clampNumber(subscriberRequestedProfile.quality, 20, 95, normalized.quality)
11492
+ }
11493
+ : normalized;
11494
+ return {
11495
+ ok: true,
11282
11496
  commandId: activeLiveStream.commandId,
11283
11497
  sessionId: device.sessionId,
11284
11498
  streamId: activeLiveStream.streamId,
@@ -11290,12 +11504,12 @@ export function createRemoteHub(options = {}) {
11290
11504
  captureGeneration: Number(activeLiveStream.captureGeneration || 0),
11291
11505
  ready: liveStreamHasCurrentFrame(activeLiveStream),
11292
11506
  reused: true,
11293
- sharedProfileReused: true,
11294
- requestedProfile: {
11295
- fps: normalized.fps,
11296
- maxWidth: normalized.maxWidth,
11297
- maxHeight: normalized.maxHeight,
11298
- quality: normalized.quality
11507
+ sharedProfileReused: true,
11508
+ requestedProfile: {
11509
+ fps: requested.fps,
11510
+ maxWidth: requested.maxWidth,
11511
+ maxHeight: requested.maxHeight,
11512
+ quality: requested.quality
11299
11513
  },
11300
11514
  effectiveProfile: {
11301
11515
  fps: Number(activeLiveStream.fps || normalized.fps),
@@ -11506,10 +11720,11 @@ export function createRemoteHub(options = {}) {
11506
11720
  const pendingDescriptor = getPendingLiveStreamDescriptor(activeLiveStream);
11507
11721
  if (pendingDescriptor?.commandId) {
11508
11722
  const pendingMatchesRequest = liveStreamMatchesOptions(pendingDescriptor, normalized);
11509
- const pendingMatchesSharedOwner = options.forceRestart !== true
11510
- && options.reuseExisting === true
11511
- && options.reuseSharedExisting === true
11512
- && liveStreamMatchesOwnerIdentity(pendingDescriptor, normalized);
11723
+ const pendingMatchesSharedOwner = options.forceRestart !== true
11724
+ && options.reuseExisting === true
11725
+ && options.reuseSharedExisting === true
11726
+ && liveStreamMatchesOwnerIdentity(pendingDescriptor, normalized)
11727
+ && liveProfileSatisfiesDemand(pendingDescriptor, normalized);
11513
11728
  if ((pendingMatchesRequest || pendingMatchesSharedOwner)
11514
11729
  && liveStreamReplacementStillPending(activeLiveStream)) {
11515
11730
  emitRemoteEvent('RemoteLiveStreamRestartPending', device, {
@@ -11519,10 +11734,10 @@ export function createRemoteHub(options = {}) {
11519
11734
  reason: safeString(options.restartReason || 'duplicate-restart', 128)
11520
11735
  });
11521
11736
  if (pendingMatchesSharedOwner && !pendingMatchesRequest) {
11522
- return sharedLiveProfileResult(device, pendingDescriptor, normalized, {
11523
- ready: false,
11524
- pending: true
11525
- });
11737
+ return sharedLiveProfileResult(device, pendingDescriptor, normalized, {
11738
+ ready: false,
11739
+ pending: true
11740
+ }, options.subscriberRequestedProfile);
11526
11741
  }
11527
11742
  return {
11528
11743
  ok: true,
@@ -11599,10 +11814,11 @@ export function createRemoteHub(options = {}) {
11599
11814
  }
11600
11815
  if (activeLiveStream
11601
11816
  && options.forceRestart !== true
11602
- && options.reuseExisting === true
11603
- && options.reuseSharedExisting === true
11604
- && liveStreamMatchesOwnerIdentity(activeLiveStream, normalized)
11605
- && liveStreamIsReusable(activeLiveStream)) {
11817
+ && options.reuseExisting === true
11818
+ && options.reuseSharedExisting === true
11819
+ && liveStreamMatchesOwnerIdentity(activeLiveStream, normalized)
11820
+ && liveStreamIsReusable(activeLiveStream)
11821
+ && liveProfileSatisfiesDemand(activeLiveStream, normalized)) {
11606
11822
  emitRemoteEvent('RemoteLiveStreamSharedProfileReused', device, {
11607
11823
  streamId,
11608
11824
  commandId: activeLiveStream.commandId,
@@ -11614,7 +11830,13 @@ export function createRemoteHub(options = {}) {
11614
11830
  effectiveMaxWidth: Number(activeLiveStream.maxWidth || normalized.maxWidth),
11615
11831
  effectiveMaxHeight: Number(activeLiveStream.maxHeight || normalized.maxHeight)
11616
11832
  });
11617
- return sharedLiveProfileResult(device, activeLiveStream, normalized);
11833
+ return sharedLiveProfileResult(
11834
+ device,
11835
+ activeLiveStream,
11836
+ normalized,
11837
+ {},
11838
+ options.subscriberRequestedProfile
11839
+ );
11618
11840
  }
11619
11841
  if (activeLiveStream
11620
11842
  && options.forceRestart !== true
@@ -11779,10 +12001,11 @@ export function createRemoteHub(options = {}) {
11779
12001
  fps,
11780
12002
  mode: transfer.mode,
11781
12003
  frameMode: transfer.frameMode,
11782
- monitorIndex,
11783
- captureGeneration,
11784
- ready: false
11785
- };
12004
+ monitorIndex,
12005
+ captureGeneration,
12006
+ ready: false,
12007
+ pending: replacingActiveStream
12008
+ };
11786
12009
  }
11787
12010
 
11788
12011
  function stopLiveStream(deviceId, options = {}) {