livedesk 0.1.457 → 0.1.459

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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import assert from 'node:assert/strict';
4
- import { spawn, spawnSync } from 'node:child_process';
4
+ import { execFile, spawn, spawnSync } from 'node:child_process';
5
5
  import { createHash, randomBytes } from 'node:crypto';
6
6
  import { existsSync } from 'node:fs';
7
7
  import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
@@ -13,13 +13,15 @@ import { fileURLToPath } from 'node:url';
13
13
  import { setTimeout as delay } from 'node:timers/promises';
14
14
  import WebSocket from 'ws';
15
15
  import {
16
+ buildMacDiagnosticResourceSample,
16
17
  buildMacModifierProbeSteps,
17
18
  buildMacModifierReleaseSteps,
18
19
  classifyMacPhysicalGate,
20
+ createAsyncSingleFlightSampler,
19
21
  noteMacModifierAfterAppliedAck,
20
22
  noteMacModifierBeforeSend,
21
- parseMacHelperProcesses,
22
- parseMacLiveDeskRuntimeProcesses
23
+ parseMacLiveDeskRuntimeProcesses,
24
+ summarizeMacDiagnosticResourceSamples
23
25
  } from './livedesk-mac-physical-gate-core.mjs';
24
26
  import {
25
27
  acquireMacDiagnosticLease,
@@ -38,6 +40,7 @@ const publicMacPhysicalDiagnostic = String(rawArguments[0] || '').toLowerCase()
38
40
  const macPhysicalGate = process.argv.includes('--mac-physical-gate') || publicMacPhysicalDiagnostic;
39
41
  const isWindows = process.platform === 'win32';
40
42
  const isMacPhysicalGate = process.platform === 'darwin' && macPhysicalGate;
43
+ const MAC_PHYSICAL_RESOURCE_SAMPLE_INTERVAL_MS = 500;
41
44
  if (macPhysicalGate && !isMacPhysicalGate) {
42
45
  const message = 'Mac physical diagnostic requires macOS hardware; no LiveDesk process was started.';
43
46
  if (publicMacPhysicalDiagnostic) {
@@ -126,7 +129,15 @@ const requestedHeight = heightArgumentIndex >= 0
126
129
  const transitionToMode5 = process.argv.includes('--transition-mode5');
127
130
  const switchMonitor = process.argv.includes('--switch-monitor') || isMacPhysicalGate;
128
131
  const verifyMonitorPixels = process.argv.includes('--verify-monitor-pixels') || isMacPhysicalGate;
132
+ const windowsInputAck = process.argv.includes('--windows-input-ack');
133
+ const windowsInputHookLateStart = process.argv.includes('--windows-input-hook-late-start');
129
134
  const legacyFfmpegFirst = process.argv.includes('--legacy-ffmpeg-first');
135
+ if (windowsInputAck && !isWindows) {
136
+ throw new Error('The Windows native input ACK diagnostic requires Windows hardware.');
137
+ }
138
+ if (windowsInputHookLateStart && !windowsInputAck) {
139
+ throw new Error('The Windows hook late-start diagnostic requires --windows-input-ack.');
140
+ }
130
141
  const reportArgumentIndex = rawArguments.findIndex(
131
142
  argument => argument === '--report' || argument === '--gate-report'
132
143
  );
@@ -313,9 +324,28 @@ function readMacProcessSnapshot() {
313
324
  return String(result.stdout || '');
314
325
  }
315
326
 
316
- function readMacCaptureHelpers() {
317
- if (!isMacPhysicalGate) return [];
318
- return parseMacHelperProcesses(readMacProcessSnapshot());
327
+ function readMacProcessResourceSnapshot() {
328
+ return new Promise((resolveSnapshot, rejectSnapshot) => {
329
+ execFile(
330
+ '/bin/ps',
331
+ ['-axo', 'pid=,ppid=,comm=,%cpu=,rss=,args='],
332
+ {
333
+ encoding: 'utf8',
334
+ timeout: 2_000,
335
+ windowsHide: true,
336
+ maxBuffer: 4 * 1024 * 1024
337
+ },
338
+ (error, stdout, stderr) => {
339
+ if (error) {
340
+ rejectSnapshot(new Error(
341
+ `resource ps failed: ${String(stderr || error.message || error)}`
342
+ ));
343
+ return;
344
+ }
345
+ resolveSnapshot(String(stdout || ''));
346
+ }
347
+ );
348
+ });
319
349
  }
320
350
 
321
351
  function readConflictingMacLiveDeskRuntimes() {
@@ -446,6 +476,150 @@ async function runMacModifierProbe(binding) {
446
476
  };
447
477
  }
448
478
 
479
+ async function runWindowsInputAckProbe(binding, { expectHookLateStartFailure = false } = {}) {
480
+ const socket = new WebSocket(`ws://127.0.0.1:${httpPort}/api/remote/input/ws`, {
481
+ perMessageDeflate: false
482
+ });
483
+ const sendInput = async (input, timeoutMs = 5_000) => {
484
+ const outcome = waitForWebSocketJson(
485
+ socket,
486
+ message => (
487
+ (message.type === 'RemoteInputApplied' || message.type === 'RemoteInputError')
488
+ && Number(message.inputSeq) === Number(input.inputSeq)
489
+ ),
490
+ timeoutMs
491
+ );
492
+ socket.send(JSON.stringify({
493
+ type: 'input',
494
+ requestId: input.inputEventId,
495
+ deviceId: binding.deviceId,
496
+ input: {
497
+ ...input,
498
+ requestAck: true,
499
+ issuedAtEpochMs: Date.now(),
500
+ monitorIndex: binding.monitorIndex,
501
+ controlSessionId: binding.sessionId,
502
+ controlCommandId: binding.commandId,
503
+ captureGeneration: binding.captureGeneration
504
+ }
505
+ }));
506
+ return outcome;
507
+ };
508
+
509
+ let resetApplied = false;
510
+ try {
511
+ const ready = waitForWebSocketJson(socket, message => message.type === 'RemoteInputSocketReady');
512
+ await Promise.all([waitForWebSocketOpen(socket), ready]);
513
+ const probe = await sendInput({
514
+ type: 'keyboard.probe',
515
+ inputSeq: 1,
516
+ inputEventId: `windows-physical-probe-${Date.now()}`,
517
+ pressedCodes: [],
518
+ shiftKey: false,
519
+ ctrlKey: false,
520
+ altKey: false,
521
+ metaKey: false,
522
+ repeat: false
523
+ });
524
+ if (expectHookLateStartFailure) {
525
+ assert.equal(
526
+ probe.type,
527
+ 'RemoteInputError',
528
+ 'The forced late hook start unexpectedly reached native input injection.'
529
+ );
530
+ assert.match(
531
+ String(probe.error || ''),
532
+ /diagnostic keyboard isolation hook did not become ready/i,
533
+ 'The forced late hook start did not fail at the bounded ready deadline.'
534
+ );
535
+ const reset = await sendInput({
536
+ type: 'keyboard.reset',
537
+ inputSeq: 2,
538
+ inputEventId: `windows-late-start-reset-${Date.now()}`,
539
+ pressedCodes: [],
540
+ shiftKey: false,
541
+ ctrlKey: false,
542
+ altKey: false,
543
+ metaKey: false,
544
+ repeat: false,
545
+ reason: 'windows-hook-late-start-cleanup'
546
+ });
547
+ assert.equal(reset.type, 'RemoteInputApplied', String(reset.error || 'Windows late-start reset failed.'));
548
+ assert.equal(reset.duplicate, false, 'Windows late-start reset was acknowledged as a duplicate.');
549
+ resetApplied = true;
550
+ return {
551
+ lateStartFailure: true,
552
+ error: String(probe.error || ''),
553
+ resetApplied
554
+ };
555
+ }
556
+ assert.equal(probe.type, 'RemoteInputApplied', String(probe.error || 'Windows native input probe failed.'));
557
+ assert.equal(probe.duplicate, false, 'Windows native input probe was acknowledged as a duplicate.');
558
+ assert.equal(probe.nativeInputBackend, 'win32-sendinput-hook-isolated');
559
+ assert.equal(Number(probe.nativeEventsRequested), 8);
560
+ assert.equal(Number(probe.nativeEventsAccepted), 8);
561
+ assert.equal(Number(probe.hookEventsObserved), 8);
562
+ assert.equal(Number(probe.hookEventsBlocked), 8);
563
+ assert.equal(probe.focusChanged, false, 'Windows native input probe changed the foreground window.');
564
+ assert.equal(probe.inputDiagnostic, true);
565
+
566
+ const reset = await sendInput({
567
+ type: 'keyboard.reset',
568
+ inputSeq: 2,
569
+ inputEventId: `windows-physical-reset-${Date.now()}`,
570
+ pressedCodes: [],
571
+ shiftKey: false,
572
+ ctrlKey: false,
573
+ altKey: false,
574
+ metaKey: false,
575
+ repeat: false,
576
+ reason: 'windows-native-input-diagnostic-cleanup'
577
+ });
578
+ assert.equal(reset.type, 'RemoteInputApplied', String(reset.error || 'Windows native input reset failed.'));
579
+ assert.equal(reset.duplicate, false, 'Windows native input reset was acknowledged as a duplicate.');
580
+ resetApplied = true;
581
+ return {
582
+ backend: String(probe.nativeInputBackend || ''),
583
+ requested: Number(probe.nativeEventsRequested || 0),
584
+ accepted: Number(probe.nativeEventsAccepted || 0),
585
+ hookObserved: Number(probe.hookEventsObserved || 0),
586
+ hookBlocked: Number(probe.hookEventsBlocked || 0),
587
+ duplicate: probe.duplicate === true,
588
+ focusChanged: probe.focusChanged === true,
589
+ resetApplied
590
+ };
591
+ } finally {
592
+ if (!resetApplied && socket.readyState === WebSocket.OPEN) {
593
+ try {
594
+ socket.send(JSON.stringify({
595
+ type: 'input',
596
+ deviceId: binding.deviceId,
597
+ fireAndForget: true,
598
+ input: {
599
+ type: 'keyboard.reset',
600
+ inputSeq: 0,
601
+ requestAck: false,
602
+ pressedCodes: [],
603
+ shiftKey: false,
604
+ ctrlKey: false,
605
+ altKey: false,
606
+ metaKey: false,
607
+ repeat: false,
608
+ reason: 'windows-native-input-diagnostic-finally',
609
+ monitorIndex: binding.monitorIndex,
610
+ controlSessionId: binding.sessionId,
611
+ controlCommandId: binding.commandId,
612
+ captureGeneration: binding.captureGeneration
613
+ }
614
+ }));
615
+ } catch {
616
+ // Closing the exact input owner also triggers RemoteFast ReleaseAll.
617
+ }
618
+ }
619
+ try { socket.close(); } catch {}
620
+ }
621
+ }
622
+
449
623
  async function decodeKeyFrame(accessUnit, label) {
450
624
  const directory = await mkdtemp(path.join(tmpdir(), `livedesk-${label}-`));
451
625
  const inputPath = path.join(directory, 'frame.h264');
@@ -469,6 +643,10 @@ async function decodeKeyFrame(accessUnit, label) {
469
643
  windowsHide: true
470
644
  });
471
645
  ownedDecoders.set(decoder, { directory, label });
646
+ if (isMacPhysicalGate && Number(decoder?.pid || 0) > 1) {
647
+ macOwnedDecoderPids.add(Number(decoder.pid));
648
+ requestMacResourceSample('steadyCapture');
649
+ }
472
650
  let errors = '';
473
651
  let settled = false;
474
652
  let unsubscribeAbort = () => {};
@@ -566,12 +744,20 @@ let hub;
566
744
  let agent;
567
745
  let frameSocket;
568
746
  const ownedDecoders = new Map();
747
+ const macOwnedDecoderPids = new Set();
569
748
  let hubLogs = [];
570
749
  let agentLogs = [];
571
750
  let helperSampleTimer;
572
751
  let helperMaxConcurrent = 0;
573
752
  const helperObservedPids = new Set();
574
753
  let helperFinalCount = 0;
754
+ let macResourcePhase = '';
755
+ const macResourceSamples = {
756
+ baseline: [],
757
+ steadyCapture: [],
758
+ afterCleanup: []
759
+ };
760
+ const macResourceSamplingErrors = new Set();
575
761
  let macModifierProbe = { expectedAcks: 8, appliedAcks: 0, applied: [], errors: [] };
576
762
  let macInitialPixelHash = '';
577
763
  let macNextPixelHash = '';
@@ -590,12 +776,62 @@ let macRunError = '';
590
776
  const macCleanupErrors = [];
591
777
  const macSignalHandlers = new Map();
592
778
 
593
- const sampleMacHelpers = () => {
779
+ const collectMacResourceSample = async phase => {
780
+ if (!isMacPhysicalGate) {
781
+ return {
782
+ totalLiveDeskProcessCount: 0,
783
+ captureHelperCount: 0,
784
+ unownedLiveDeskPids: [],
785
+ processes: []
786
+ };
787
+ }
788
+ let sample;
789
+ try {
790
+ sample = buildMacDiagnosticResourceSample(
791
+ await readMacProcessResourceSnapshot(),
792
+ {
793
+ diagnosticPid: process.pid,
794
+ hubPid: hub?.pid,
795
+ agentPid: agent?.pid,
796
+ decoderPids: [...ownedDecoders.keys()]
797
+ .filter(decoder => decoder?.exitCode === null && decoder?.signalCode === null)
798
+ .map(decoder => decoder?.pid)
799
+ }
800
+ );
801
+ } catch {
802
+ const safePhase = String(phase || 'unassigned').replace(/[^a-z-]/gi, '') || 'unassigned';
803
+ macResourceSamplingErrors.add(`${safePhase}:unavailable`);
804
+ throw new Error(`Mac ${safePhase} process resource telemetry is unavailable.`);
805
+ }
806
+ helperMaxConcurrent = Math.max(helperMaxConcurrent, sample.captureHelperCount);
807
+ for (const helper of sample.processes.filter(record => record.role === 'capture-helper')) {
808
+ helperObservedPids.add(helper.pid);
809
+ }
810
+ if (Object.prototype.hasOwnProperty.call(macResourceSamples, phase)) {
811
+ const phaseSamples = macResourceSamples[phase];
812
+ phaseSamples.push(sample);
813
+ while (phaseSamples.length > 240) phaseSamples.shift();
814
+ }
815
+ return sample;
816
+ };
817
+
818
+ const macResourceSampler = createAsyncSingleFlightSampler(collectMacResourceSample);
819
+
820
+ const sampleMacResources = phase => macResourceSampler.run(phase);
821
+
822
+ const requestMacResourceSample = phase => {
823
+ const pending = macResourceSampler.request(phase);
824
+ pending?.catch(() => {
825
+ // A later required sample reports persistent process telemetry failure.
826
+ });
827
+ };
828
+
829
+ const sampleMacHelpers = async () => {
594
830
  if (!isMacPhysicalGate) return [];
595
- const helpers = readMacCaptureHelpers();
596
- helperMaxConcurrent = Math.max(helperMaxConcurrent, helpers.length);
597
- for (const helper of helpers) helperObservedPids.add(helper.pid);
598
- return helpers;
831
+ const sample = await sampleMacResources(macResourcePhase);
832
+ return sample.processes
833
+ .filter(record => record.role === 'capture-helper')
834
+ .map(record => ({ pid: record.pid }));
599
835
  };
600
836
 
601
837
  async function stopMacAgentAndSampleDrain() {
@@ -614,7 +850,7 @@ async function stopMacAgentAndSampleDrain() {
614
850
  } finally {
615
851
  const drainDeadline = Date.now() + 5_000;
616
852
  do {
617
- const helpers = sampleMacHelpers();
853
+ const helpers = await sampleMacHelpers();
618
854
  helperFinalCount = helpers.length;
619
855
  if (helperFinalCount === 0) break;
620
856
  await delay(100);
@@ -641,8 +877,27 @@ async function writeMacPhysicalGateReport() {
641
877
  while (true) {
642
878
  const diagnostic = readDiagnosticState();
643
879
  const stateToken = diagnosticStateToken(diagnostic);
880
+ const expectedSteadyProcesses = [
881
+ { role: 'hub', pid: hub?.pid, ownedByDiagnostic: true },
882
+ { role: 'remote-fast', pid: agent?.pid, ownedByDiagnostic: true },
883
+ ...[...macOwnedDecoderPids].map(pid => ({
884
+ role: 'decoder',
885
+ pid,
886
+ ownedByDiagnostic: true
887
+ }))
888
+ ];
889
+ const resources = {
890
+ sampleIntervalMs: MAC_PHYSICAL_RESOURCE_SAMPLE_INTERVAL_MS,
891
+ samplingErrors: [...macResourceSamplingErrors],
892
+ baseline: summarizeMacDiagnosticResourceSamples(macResourceSamples.baseline),
893
+ steadyCapture: summarizeMacDiagnosticResourceSamples(
894
+ macResourceSamples.steadyCapture,
895
+ { expectedProcesses: expectedSteadyProcesses }
896
+ ),
897
+ afterCleanup: summarizeMacDiagnosticResourceSamples(macResourceSamples.afterCleanup)
898
+ };
644
899
  snapshot = {
645
- schemaVersion: 1,
900
+ schemaVersion: 2,
646
901
  generatedAt,
647
902
  runtimeId: clientRuntimeId,
648
903
  requestedFps: 30,
@@ -665,6 +920,7 @@ async function writeMacPhysicalGateReport() {
665
920
  },
666
921
  modifier: macModifierProbe,
667
922
  transport: macTransport,
923
+ resources,
668
924
  diagnostic,
669
925
  error: diagnostic.runError
670
926
  };
@@ -688,6 +944,13 @@ async function writeMacPhysicalGateReport() {
688
944
  `Mac physical gate helper: max=${helperMaxConcurrent}, final=${helperFinalCount}, `
689
945
  + `pids=${[...helperObservedPids].join(',') || '-'}.`
690
946
  );
947
+ const resourceEvidence = snapshot.resources;
948
+ console.log(
949
+ `Mac physical gate resources: baseline=${resourceEvidence.baseline.totalLiveDeskProcessCount}, `
950
+ + `steady-peak=${resourceEvidence.steadyCapture.totalLiveDeskProcessCountPeak}, `
951
+ + `after-cleanup=${resourceEvidence.afterCleanup.totalLiveDeskProcessCount}, `
952
+ + `samples=${resourceEvidence.steadyCapture.sampleCount}.`
953
+ );
691
954
  console.log(
692
955
  `Mac physical gate monitor: count=${macMonitorCount}, generation=${macInitialGeneration}->${macNextGeneration}, `
693
956
  + `pixels=${macInitialPixelHash.slice(0, 12) || '-'}->${macNextPixelHash.slice(0, 12) || '-'}.`
@@ -766,6 +1029,12 @@ async function cleanupOwnedDiagnosticResources() {
766
1029
  }
767
1030
  }
768
1031
  if (!isMacPhysicalGate) return;
1032
+ macResourcePhase = 'afterCleanup';
1033
+ try {
1034
+ await sampleMacResources(macResourcePhase);
1035
+ } catch (error) {
1036
+ recordMacCleanupError('resource-snapshot-after-cleanup', error);
1037
+ }
769
1038
  const teardownPlan = buildMacDiagnosticTeardownPlan({
770
1039
  agentStopped,
771
1040
  hubStopped,
@@ -843,14 +1112,12 @@ try {
843
1112
  macIsolation = await prepareMacDiagnosticIsolation();
844
1113
  if (isMacPhysicalGate) {
845
1114
  macDiagnosticAbort.throwIfRequested();
846
- sampleMacHelpers();
1115
+ macResourcePhase = 'baseline';
1116
+ await sampleMacResources(macResourcePhase);
1117
+ macResourcePhase = '';
847
1118
  helperSampleTimer = setInterval(() => {
848
- try {
849
- sampleMacHelpers();
850
- } catch {
851
- // The final synchronous sample reports a persistent ps failure.
852
- }
853
- }, 100);
1119
+ requestMacResourceSample(macResourcePhase);
1120
+ }, MAC_PHYSICAL_RESOURCE_SAMPLE_INTERVAL_MS);
854
1121
  helperSampleTimer.unref?.();
855
1122
  }
856
1123
  const isolatedHubEnvironment = isMacPhysicalGate
@@ -929,6 +1196,10 @@ try {
929
1196
  LIVEDESK_UDP_PREFER_P2P: '0',
930
1197
  LIVEDESK_RELAY_ENABLED: '0'
931
1198
  } : {}),
1199
+ ...(windowsInputAck ? { LIVEDESK_INPUT_DIAGNOSTIC: '1' } : {}),
1200
+ ...(windowsInputHookLateStart ? {
1201
+ LIVEDESK_INPUT_DIAGNOSTIC_HOOK_START_DELAY_MS: '2500'
1202
+ } : {}),
932
1203
  LIVEDESK_FFMPEG: legacyFfmpegFirst ? ffmpegPath : ffmpegStaticPath,
933
1204
  LIVEDESK_FFMPEG_PATHS: (legacyFfmpegFirst
934
1205
  ? [ffmpegPath, ffmpegStaticPath]
@@ -989,7 +1260,11 @@ try {
989
1260
  frameSocket.on('message', handleMessage);
990
1261
  });
991
1262
 
992
- frameSocket.send(JSON.stringify({
1263
+ if (isMacPhysicalGate) {
1264
+ macResourcePhase = 'steadyCapture';
1265
+ await sampleMacResources(macResourcePhase);
1266
+ }
1267
+ frameSocket.send(JSON.stringify({
993
1268
  type: 'subscribe',
994
1269
  deviceIds: [connectedDeviceId],
995
1270
  autoStartLive: true,
@@ -1007,6 +1282,7 @@ try {
1007
1282
  }));
1008
1283
 
1009
1284
  const frames = await withMacDiagnosticAbort(framePromise, cancelFrameWait);
1285
+ let activeInputBindingFrame = frames.at(-1);
1010
1286
  const initialCaptureIdentity = switchMonitor && isWindows
1011
1287
  ? await waitFor(
1012
1288
  () => findWindowsCaptureIdentity(agentLogs, requestedMonitorIndex),
@@ -1175,6 +1451,7 @@ try {
1175
1451
  )
1176
1452
  : null;
1177
1453
  const firstSwitchedFrame = switchedFrames[0];
1454
+ activeInputBindingFrame = switchedFrames.at(-1);
1178
1455
  const switchedFirstFrameMs = firstSwitchedFrame.receivedAt - switchedAt;
1179
1456
  assert.equal(firstSwitchedFrame.metadata.isKeyFrame, true, 'Mode 3 monitor transition did not start on a key frame.');
1180
1457
  assert.notEqual(String(firstSwitchedFrame.metadata.commandId || ''), previousCommandId, 'Mode 3 monitor transition reused the previous command binding.');
@@ -1286,6 +1563,31 @@ try {
1286
1563
  console.log(`Mode 3 monitor transition OK: ${requestedMonitorIndex} -> ${nextMonitorIndex}, first frame ${switchedFirstFrameMs.toFixed(0)}ms, generation ${previousCaptureGeneration} -> ${firstSwitchedFrame.metadata.captureGeneration}.`);
1287
1564
  }
1288
1565
 
1566
+ if (windowsInputAck) {
1567
+ const inputProbe = await runWindowsInputAckProbe({
1568
+ deviceId: connectedDeviceId,
1569
+ sessionId: String(activeInputBindingFrame.metadata.sessionId || ''),
1570
+ commandId: String(activeInputBindingFrame.metadata.commandId || ''),
1571
+ captureGeneration: Number(activeInputBindingFrame.metadata.captureGeneration || 0),
1572
+ monitorIndex: Number(activeInputBindingFrame.metadata.monitorIndex || 0)
1573
+ }, {
1574
+ expectHookLateStartFailure: windowsInputHookLateStart
1575
+ });
1576
+ if (windowsInputHookLateStart) {
1577
+ console.log(
1578
+ `Windows diagnostic hook late-start cleanup OK: expected timeout, `
1579
+ + `Agent responsive, reset=${inputProbe.resetApplied}.`
1580
+ );
1581
+ } else {
1582
+ console.log(
1583
+ `Windows native modifier input ACK: ${inputProbe.accepted}/${inputProbe.requested} `
1584
+ + `via ${inputProbe.backend}, hook=${inputProbe.hookObserved}/${inputProbe.hookBlocked}, `
1585
+ + `duplicate=${inputProbe.duplicate}, `
1586
+ + `focusChanged=${inputProbe.focusChanged}, reset=${inputProbe.resetApplied}.`
1587
+ );
1588
+ }
1589
+ }
1590
+
1289
1591
  if (transitionToMode5) {
1290
1592
  const previousFrameSocket = frameSocket;
1291
1593
  frameSocket = new WebSocket(`ws://127.0.0.1:${httpPort}/api/remote/frames/ws?devices=${encodeURIComponent(connectedDeviceId)}`);