livedesk 0.1.459 → 0.1.461

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.
@@ -132,6 +132,7 @@ const verifyMonitorPixels = process.argv.includes('--verify-monitor-pixels') ||
132
132
  const windowsInputAck = process.argv.includes('--windows-input-ack');
133
133
  const windowsInputHookLateStart = process.argv.includes('--windows-input-hook-late-start');
134
134
  const legacyFfmpegFirst = process.argv.includes('--legacy-ffmpeg-first');
135
+ const captureLifecycleProbe = process.argv.includes('--capture-lifecycle') || isMacPhysicalGate;
135
136
  if (windowsInputAck && !isWindows) {
136
137
  throw new Error('The Windows native input ACK diagnostic requires Windows hardware.');
137
138
  }
@@ -192,6 +193,46 @@ async function waitFor(getValue, timeoutMs, label) {
192
193
  throw new Error(`Timed out waiting for ${label}.`);
193
194
  }
194
195
 
196
+ async function requestLocalHubJson(route, options = {}) {
197
+ return runMacDiagnosticAbortableAttempt(
198
+ async signal => {
199
+ const response = await fetch(`${baseUrl}${route}`, {
200
+ ...options,
201
+ cache: 'no-store',
202
+ signal
203
+ });
204
+ const text = await response.text();
205
+ let payload = {};
206
+ try {
207
+ payload = text ? JSON.parse(text) : {};
208
+ } catch {
209
+ throw new Error(
210
+ `${route} returned non-JSON HTTP ${response.status}: ${text.slice(0, 300)}`
211
+ );
212
+ }
213
+ if (!response.ok) {
214
+ throw new Error(
215
+ `${route} returned HTTP ${response.status}: ${JSON.stringify(payload)}`
216
+ );
217
+ }
218
+ return payload;
219
+ },
220
+ {
221
+ abortState: isMacPhysicalGate ? macDiagnosticAbort : undefined,
222
+ timeoutMs: 20_000,
223
+ label: `Hub request ${route}`
224
+ }
225
+ );
226
+ }
227
+
228
+ function postLocalHubJson(route, payload) {
229
+ return requestLocalHubJson(route, {
230
+ method: 'POST',
231
+ headers: { 'Content-Type': 'application/json' },
232
+ body: JSON.stringify(payload)
233
+ });
234
+ }
235
+
195
236
  function waitForWebSocketJson(
196
237
  socket,
197
238
  predicate,
@@ -768,8 +809,35 @@ let macDeliveredFps = 0;
768
809
  let macFirstFrameMs = 0;
769
810
  let macSwitchFirstFrameMs = 0;
770
811
  let macTelemetry = {};
812
+ let macCaptureStages = { initial: null, switched: null };
771
813
  let macTransport = {};
772
814
  let macPlatformProfile = '';
815
+ let macInitialDisplayId = 0;
816
+ let macNextDisplayId = 0;
817
+ let macCaptureLifecycle = {
818
+ order: [],
819
+ wallGeneration: 0,
820
+ controlGeneration: 0,
821
+ resumedControlGeneration: 0,
822
+ restoredWallGeneration: 0,
823
+ wallHelperCount: -1,
824
+ controlHelperCount: -1,
825
+ pausedHelperCount: -1,
826
+ resumedControlHelperCount: -1,
827
+ controlStoppedHelperCount: -1,
828
+ restoredWallHelperCount: -1,
829
+ finalHelperCount: -1,
830
+ registryMaxConcurrent: -1,
831
+ registryFinalHelperCount: -1,
832
+ completedAt: '',
833
+ registrySampledAt: '',
834
+ pauseStopConfirmed: false,
835
+ pauseStopAcknowledged: false,
836
+ blockedWhilePaused: false,
837
+ resumeAccepted: false,
838
+ controlStopConfirmed: false,
839
+ finalStopConfirmed: false
840
+ };
773
841
  let macReportPath = '';
774
842
  let macReportClassification;
775
843
  let macRunError = '';
@@ -834,6 +902,287 @@ const sampleMacHelpers = async () => {
834
902
  .map(record => ({ pid: record.pid }));
835
903
  };
836
904
 
905
+ async function waitForCaptureHelperCount(expectedCount, label) {
906
+ if (!isMacPhysicalGate) return -1;
907
+ const helpers = await waitFor(async () => {
908
+ const current = await sampleMacHelpers();
909
+ return current.length === expectedCount ? current : null;
910
+ }, 10_000, label);
911
+ return helpers.length;
912
+ }
913
+
914
+ async function waitForFreshCaptureRegistry(connectedDeviceId, completedAtEpochMs) {
915
+ if (!isMacPhysicalGate) return -1;
916
+ return waitFor(async signal => {
917
+ const response = await fetch(`${baseUrl}/api/remote/devices`, {
918
+ cache: 'no-store',
919
+ signal
920
+ });
921
+ if (!response.ok) return null;
922
+ const payload = await response.json();
923
+ const device = payload.devices?.find(item =>
924
+ item.deviceId === connectedDeviceId || item.id === connectedDeviceId
925
+ );
926
+ const capture = device?.status?.capture;
927
+ const maximum = Number(capture?.helperMaxConcurrent);
928
+ const helperCount = Number(capture?.helperCount);
929
+ const sampledAt = String(capture?.sampledAt || '');
930
+ const sampledAtEpochMs = Date.parse(sampledAt);
931
+ if (
932
+ !Number.isInteger(maximum)
933
+ || maximum < 1
934
+ || !Number.isInteger(helperCount)
935
+ || helperCount < 0
936
+ || !Number.isFinite(sampledAtEpochMs)
937
+ || sampledAtEpochMs < completedAtEpochMs
938
+ ) {
939
+ return null;
940
+ }
941
+ return {
942
+ maximum,
943
+ helperCount,
944
+ sampledAt
945
+ };
946
+ }, 20_000, 'fresh RemoteFast capture registry after lifecycle completion');
947
+ }
948
+
949
+ async function waitForLifecycleFrame(connectedDeviceId, captureGeneration, streamPurpose) {
950
+ return waitFor(async signal => {
951
+ const response = await fetch(`${baseUrl}/api/remote/devices`, {
952
+ cache: 'no-store',
953
+ signal
954
+ });
955
+ if (!response.ok) return null;
956
+ const payload = await response.json();
957
+ const device = payload.devices?.find(item =>
958
+ item.deviceId === connectedDeviceId || item.id === connectedDeviceId
959
+ );
960
+ const frame = device?.latestLiveFrame;
961
+ const stream = device?.activeLiveStream;
962
+ if (
963
+ Number(frame?.captureGeneration || 0) !== Number(captureGeneration)
964
+ || String(frame?.streamPurpose || '') !== streamPurpose
965
+ || String(frame?.frameMode || '') !== 'mode3-h264-hw'
966
+ || frame?.currentGenerationVerified !== true
967
+ || Number(stream?.captureGeneration || 0) !== Number(captureGeneration)
968
+ || String(stream?.streamPurpose || '') !== streamPurpose
969
+ || stream?.readyFrameReceived !== true
970
+ ) {
971
+ return null;
972
+ }
973
+ return { frame, stream, device };
974
+ }, 20_000, `${streamPurpose} lifecycle generation ${captureGeneration}`);
975
+ }
976
+
977
+ function lifecycleStartPayload(streamPurpose, reason) {
978
+ const control = streamPurpose === 'control';
979
+ return {
980
+ mode: 'mode3-h264-hw',
981
+ frameMode: 'mode3-h264-hw',
982
+ fps: 30,
983
+ maxWidth: control ? requestedWidth : Math.min(requestedWidth, 1280),
984
+ maxHeight: control ? requestedHeight : Math.min(requestedHeight, 720),
985
+ quality: 75,
986
+ monitorIndex: requestedMonitorIndex,
987
+ streamPurpose,
988
+ forceRestart: true,
989
+ restartToken: `capture-lifecycle-${streamPurpose}-${Date.now()}`,
990
+ restartReason: reason
991
+ };
992
+ }
993
+
994
+ async function runCaptureLifecycleProbe(connectedDeviceId) {
995
+ const route = `/api/remote/devices/${encodeURIComponent(connectedDeviceId)}/live`;
996
+ const evidence = {
997
+ order: [],
998
+ wallGeneration: 0,
999
+ controlGeneration: 0,
1000
+ resumedControlGeneration: 0,
1001
+ restoredWallGeneration: 0,
1002
+ wallHelperCount: -1,
1003
+ controlHelperCount: -1,
1004
+ pausedHelperCount: -1,
1005
+ resumedControlHelperCount: -1,
1006
+ controlStoppedHelperCount: -1,
1007
+ restoredWallHelperCount: -1,
1008
+ finalHelperCount: -1,
1009
+ registryMaxConcurrent: -1,
1010
+ registryFinalHelperCount: -1,
1011
+ completedAt: '',
1012
+ registrySampledAt: '',
1013
+ pauseStopConfirmed: false,
1014
+ pauseStopAcknowledged: false,
1015
+ blockedWhilePaused: false,
1016
+ resumeAccepted: false,
1017
+ controlStopConfirmed: false,
1018
+ finalStopConfirmed: false
1019
+ };
1020
+
1021
+ const wallStart = await postLocalHubJson(
1022
+ `${route}/start`,
1023
+ lifecycleStartPayload('wall', 'physical-lifecycle-wall')
1024
+ );
1025
+ assert.equal(wallStart.ok, true, `Wall lifecycle start failed: ${JSON.stringify(wallStart)}`);
1026
+ await waitForLifecycleFrame(connectedDeviceId, wallStart.captureGeneration, 'wall');
1027
+ evidence.wallGeneration = Number(wallStart.captureGeneration || 0);
1028
+ evidence.wallHelperCount = await waitForCaptureHelperCount(1, 'one Wall capture helper');
1029
+ evidence.order.push('wall');
1030
+
1031
+ const controlStart = await postLocalHubJson(
1032
+ `${route}/start`,
1033
+ lifecycleStartPayload('control', 'physical-lifecycle-control')
1034
+ );
1035
+ assert.equal(
1036
+ controlStart.ok,
1037
+ true,
1038
+ `Control lifecycle start failed: ${JSON.stringify(controlStart)}`
1039
+ );
1040
+ assert.ok(
1041
+ Number(controlStart.captureGeneration) > evidence.wallGeneration,
1042
+ 'Control must replace Wall with a newer capture generation.'
1043
+ );
1044
+ await waitForLifecycleFrame(connectedDeviceId, controlStart.captureGeneration, 'control');
1045
+ evidence.controlGeneration = Number(controlStart.captureGeneration || 0);
1046
+ evidence.controlHelperCount = await waitForCaptureHelperCount(
1047
+ 1,
1048
+ 'one Control capture helper'
1049
+ );
1050
+ evidence.order.push('control');
1051
+
1052
+ const paused = await postLocalHubJson(`${route}/pause`, {
1053
+ reason: 'physical-lifecycle-user-pause'
1054
+ });
1055
+ assert.equal(paused.ok, true, `Capture lifecycle pause failed: ${JSON.stringify(paused)}`);
1056
+ assert.ok(paused.pauseToken, 'Capture lifecycle pause did not return an owner token.');
1057
+ assert.equal(paused.phase, 'paused');
1058
+ assert.equal(paused.captureStopConfirmed, true);
1059
+ assert.ok(paused.stopCommandId, 'Pause did not identify its acknowledged stop command.');
1060
+ evidence.pauseStopConfirmed = true;
1061
+ evidence.pauseStopAcknowledged = true;
1062
+ evidence.pausedHelperCount = await waitForCaptureHelperCount(
1063
+ 0,
1064
+ 'capture helper drain during pause'
1065
+ );
1066
+ evidence.order.push('paused');
1067
+
1068
+ const blocked = await postLocalHubJson(
1069
+ `${route}/start`,
1070
+ lifecycleStartPayload('control', 'physical-lifecycle-paused-watchdog')
1071
+ );
1072
+ assert.equal(blocked.ok, false, 'Paused capture unexpectedly restarted.');
1073
+ assert.equal(blocked.error, 'LIVE_CAPTURE_PAUSED');
1074
+ evidence.blockedWhilePaused = true;
1075
+ if (isMacPhysicalGate) {
1076
+ await delay(200);
1077
+ evidence.pausedHelperCount = await waitForCaptureHelperCount(
1078
+ 0,
1079
+ 'paused watchdog helper suppression'
1080
+ );
1081
+ }
1082
+ evidence.order.push('blocked');
1083
+
1084
+ const resumed = await postLocalHubJson(`${route}/resume`, {
1085
+ pauseToken: paused.pauseToken
1086
+ });
1087
+ assert.equal(resumed.ok, true, `Capture lifecycle resume failed: ${JSON.stringify(resumed)}`);
1088
+ evidence.resumeAccepted = true;
1089
+ evidence.order.push('resumed');
1090
+
1091
+ const resumedControl = await postLocalHubJson(
1092
+ `${route}/start`,
1093
+ lifecycleStartPayload('control', 'physical-lifecycle-control-resume')
1094
+ );
1095
+ assert.equal(
1096
+ resumedControl.ok,
1097
+ true,
1098
+ `Resumed Control lifecycle start failed: ${JSON.stringify(resumedControl)}`
1099
+ );
1100
+ assert.ok(
1101
+ Number(resumedControl.captureGeneration) > evidence.controlGeneration,
1102
+ 'Resumed Control must own a newer capture generation.'
1103
+ );
1104
+ await waitForLifecycleFrame(
1105
+ connectedDeviceId,
1106
+ resumedControl.captureGeneration,
1107
+ 'control'
1108
+ );
1109
+ evidence.resumedControlGeneration = Number(resumedControl.captureGeneration || 0);
1110
+ evidence.resumedControlHelperCount = await waitForCaptureHelperCount(
1111
+ 1,
1112
+ 'one resumed Control capture helper'
1113
+ );
1114
+ evidence.order.push('control-resumed');
1115
+
1116
+ const controlStopped = await postLocalHubJson(`${route}/stop`, {
1117
+ streamId: resumedControl.streamId,
1118
+ streamPurpose: 'control',
1119
+ reason: 'physical-lifecycle-control-leave'
1120
+ });
1121
+ assert.equal(
1122
+ controlStopped.ok,
1123
+ true,
1124
+ `Resumed Control stop failed: ${JSON.stringify(controlStopped)}`
1125
+ );
1126
+ assert.equal(
1127
+ controlStopped.captureStopConfirmed,
1128
+ true,
1129
+ 'Control leave did not confirm native capture shutdown.'
1130
+ );
1131
+ evidence.controlStopConfirmed = true;
1132
+ evidence.controlStoppedHelperCount = await waitForCaptureHelperCount(
1133
+ 0,
1134
+ 'capture helper drain after Control leave'
1135
+ );
1136
+ evidence.order.push('control-stopped');
1137
+
1138
+ const restoredWall = await postLocalHubJson(
1139
+ `${route}/start`,
1140
+ lifecycleStartPayload('wall', 'physical-lifecycle-wall-restore')
1141
+ );
1142
+ assert.equal(
1143
+ restoredWall.ok,
1144
+ true,
1145
+ `Restored Wall lifecycle start failed: ${JSON.stringify(restoredWall)}`
1146
+ );
1147
+ assert.ok(
1148
+ Number(restoredWall.captureGeneration) > evidence.resumedControlGeneration,
1149
+ 'Restored Wall must own a newer capture generation than resumed Control.'
1150
+ );
1151
+ await waitForLifecycleFrame(connectedDeviceId, restoredWall.captureGeneration, 'wall');
1152
+ evidence.restoredWallGeneration = Number(restoredWall.captureGeneration || 0);
1153
+ evidence.restoredWallHelperCount = await waitForCaptureHelperCount(
1154
+ 1,
1155
+ 'one restored Wall capture helper'
1156
+ );
1157
+ evidence.order.push('wall-restored');
1158
+
1159
+ const stopped = await postLocalHubJson(`${route}/stop`, {
1160
+ streamId: restoredWall.streamId,
1161
+ streamPurpose: 'wall',
1162
+ reason: 'physical-lifecycle-complete'
1163
+ });
1164
+ assert.equal(stopped.ok, true, `Restored Wall stop failed: ${JSON.stringify(stopped)}`);
1165
+ assert.equal(stopped.captureStopConfirmed, true, 'Final Wall stop was not confirmed.');
1166
+ evidence.finalStopConfirmed = true;
1167
+ evidence.finalHelperCount = await waitForCaptureHelperCount(
1168
+ 0,
1169
+ 'capture helper drain after lifecycle'
1170
+ );
1171
+ evidence.order.push('stopped');
1172
+ if (isMacPhysicalGate) {
1173
+ const completedAtEpochMs = Date.now();
1174
+ evidence.completedAt = new Date(completedAtEpochMs).toISOString();
1175
+ const registry = await waitForFreshCaptureRegistry(
1176
+ connectedDeviceId,
1177
+ completedAtEpochMs
1178
+ );
1179
+ evidence.registryMaxConcurrent = registry.maximum;
1180
+ evidence.registryFinalHelperCount = registry.helperCount;
1181
+ evidence.registrySampledAt = registry.sampledAt;
1182
+ }
1183
+ return evidence;
1184
+ }
1185
+
837
1186
  async function stopMacAgentAndSampleDrain() {
838
1187
  if (!isMacPhysicalGate) return;
839
1188
  if (helperSampleTimer) {
@@ -897,7 +1246,7 @@ async function writeMacPhysicalGateReport() {
897
1246
  afterCleanup: summarizeMacDiagnosticResourceSamples(macResourceSamples.afterCleanup)
898
1247
  };
899
1248
  snapshot = {
900
- schemaVersion: 2,
1249
+ schemaVersion: 3,
901
1250
  generatedAt,
902
1251
  runtimeId: clientRuntimeId,
903
1252
  requestedFps: 30,
@@ -915,10 +1264,14 @@ async function writeMacPhysicalGateReport() {
915
1264
  count: macMonitorCount,
916
1265
  initialGeneration: macInitialGeneration,
917
1266
  nextGeneration: macNextGeneration,
1267
+ initialDisplayId: macInitialDisplayId,
1268
+ nextDisplayId: macNextDisplayId,
918
1269
  initialHash: macInitialPixelHash,
919
1270
  nextHash: macNextPixelHash
920
1271
  },
921
1272
  modifier: macModifierProbe,
1273
+ lifecycle: macCaptureLifecycle,
1274
+ stages: macCaptureStages,
922
1275
  transport: macTransport,
923
1276
  resources,
924
1277
  diagnostic,
@@ -953,12 +1306,26 @@ async function writeMacPhysicalGateReport() {
953
1306
  );
954
1307
  console.log(
955
1308
  `Mac physical gate monitor: count=${macMonitorCount}, generation=${macInitialGeneration}->${macNextGeneration}, `
1309
+ + `display=${macInitialDisplayId || '-'}->${macNextDisplayId || '-'}, `
956
1310
  + `pixels=${macInitialPixelHash.slice(0, 12) || '-'}->${macNextPixelHash.slice(0, 12) || '-'}.`
957
1311
  );
958
1312
  console.log(
959
1313
  `Mac physical gate input/transport: modifiers=${macModifierProbe.appliedAcks}/${macModifierProbe.expectedAcks}, `
960
1314
  + `transport=${macTransport.active || '-'}/${macTransport.protocol || '-'}/${macTransport.state || '-'}.`
961
1315
  );
1316
+ console.log(
1317
+ `Mac physical gate lifecycle: ${macCaptureLifecycle.order.join(' -> ') || '-'}, `
1318
+ + `helpers=${macCaptureLifecycle.wallHelperCount}`
1319
+ + `/${macCaptureLifecycle.controlHelperCount}`
1320
+ + `/${macCaptureLifecycle.pausedHelperCount}`
1321
+ + `/${macCaptureLifecycle.resumedControlHelperCount}`
1322
+ + `/${macCaptureLifecycle.controlStoppedHelperCount}`
1323
+ + `/${macCaptureLifecycle.restoredWallHelperCount}`
1324
+ + `/${macCaptureLifecycle.finalHelperCount}, `
1325
+ + `registry-max=${macCaptureLifecycle.registryMaxConcurrent}, `
1326
+ + `registry-final=${macCaptureLifecycle.registryFinalHelperCount}, `
1327
+ + `registry-sampled=${macCaptureLifecycle.registrySampledAt || '-'}.`
1328
+ );
962
1329
  for (const gateCheck of macReportClassification.checks) {
963
1330
  console.log(`[${gateCheck.ok ? 'PASS' : 'FAIL'}] ${gateCheck.id}: ${gateCheck.detail}`);
964
1331
  }
@@ -1218,9 +1585,20 @@ try {
1218
1585
  });
1219
1586
  const payload = await response.json();
1220
1587
  const device = payload.devices?.find(item => item.deviceId === deviceId || item.id === deviceId);
1221
- return device?.deviceId || device?.id || '';
1222
- }, 15_000, 'Mode 3 smoke client');
1223
-
1588
+ return device?.deviceId || device?.id || '';
1589
+ }, 15_000, 'Mode 3 smoke client');
1590
+
1591
+ if (captureLifecycleProbe) {
1592
+ macCaptureLifecycle = await runCaptureLifecycleProbe(connectedDeviceId);
1593
+ console.log(
1594
+ `Capture lifecycle OK: ${macCaptureLifecycle.order.join(' -> ')}, `
1595
+ + `generation ${macCaptureLifecycle.wallGeneration}`
1596
+ + ` -> ${macCaptureLifecycle.controlGeneration}`
1597
+ + ` -> ${macCaptureLifecycle.resumedControlGeneration}`
1598
+ + ` -> ${macCaptureLifecycle.restoredWallGeneration}.`
1599
+ );
1600
+ }
1601
+
1224
1602
  frameSocket = new WebSocket(`ws://127.0.0.1:${httpPort}/api/remote/frames/ws?devices=${encodeURIComponent(connectedDeviceId)}`);
1225
1603
  await waitForWebSocketOpen(frameSocket);
1226
1604
 
@@ -1345,6 +1723,7 @@ try {
1345
1723
  version: Number(telemetryFrame.metadata.nativeCaptureTelemetryVersion || 0),
1346
1724
  ageMs: Number(telemetryFrame.metadata.nativeCaptureTelemetryAgeMs),
1347
1725
  hardwareAccelerated: telemetryFrame.metadata.nativeEncoderHardwareAccelerated === true,
1726
+ displayId: Number(telemetryFrame.metadata.nativeCaptureDisplayId || 0),
1348
1727
  captureFps: Number(telemetryFrame.metadata.nativeCaptureFps || 0),
1349
1728
  compressionFps: Number(telemetryFrame.metadata.nativeCompressionFps || 0),
1350
1729
  stdoutFps: Number(telemetryFrame.metadata.nativeEncodedFps || 0),
@@ -1361,6 +1740,12 @@ try {
1361
1740
  reason: String(telemetryFrame.metadata.frameTransportReason || ''),
1362
1741
  p2pReady: telemetryFrame.metadata.frameTransportP2pReady === true
1363
1742
  };
1743
+ macInitialDisplayId = Number(macTelemetry.displayId || 0);
1744
+ macCaptureStages.initial = {
1745
+ deliveredFps,
1746
+ telemetry: { ...macTelemetry },
1747
+ transport: { ...macTransport }
1748
+ };
1364
1749
  }
1365
1750
  console.log(`Mode 3 control capture OK: first ${firstFrameMs.toFixed(0)}ms, ${deliveredFps.toFixed(1)} fps, native age ${averageFrameAgeMs.toFixed(1)}ms avg/${maxFrameAgeMs.toFixed(1)}ms max, ${frames[0].metadata.width}x${frames[0].metadata.height}, ${frames[0].metadata.hardwareEncoder}/${frames[0].metadata.platformProfile}.`);
1366
1751
  if (process.argv.includes('--logs')) {
@@ -1453,6 +1838,15 @@ try {
1453
1838
  const firstSwitchedFrame = switchedFrames[0];
1454
1839
  activeInputBindingFrame = switchedFrames.at(-1);
1455
1840
  const switchedFirstFrameMs = firstSwitchedFrame.receivedAt - switchedAt;
1841
+ const switchedSteadyFrames = switchedFrames.slice(
1842
+ Math.min(10, Math.max(0, switchedFrames.length - 2))
1843
+ );
1844
+ const switchedElapsedMs = switchedSteadyFrames.length > 1
1845
+ ? switchedSteadyFrames.at(-1).receivedAt - switchedSteadyFrames[0].receivedAt
1846
+ : 0;
1847
+ const switchedDeliveredFps = switchedSteadyFrames.length > 1
1848
+ ? (switchedSteadyFrames.length - 1) * 1000 / Math.max(1, switchedElapsedMs)
1849
+ : 0;
1456
1850
  assert.equal(firstSwitchedFrame.metadata.isKeyFrame, true, 'Mode 3 monitor transition did not start on a key frame.');
1457
1851
  assert.notEqual(String(firstSwitchedFrame.metadata.commandId || ''), previousCommandId, 'Mode 3 monitor transition reused the previous command binding.');
1458
1852
  if (!isMacPhysicalGate) {
@@ -1531,6 +1925,7 @@ try {
1531
1925
  version: Number(switchedTelemetryFrame.metadata.nativeCaptureTelemetryVersion || 0),
1532
1926
  ageMs: Number(switchedTelemetryFrame.metadata.nativeCaptureTelemetryAgeMs),
1533
1927
  hardwareAccelerated: switchedTelemetryFrame.metadata.nativeEncoderHardwareAccelerated === true,
1928
+ displayId: Number(switchedTelemetryFrame.metadata.nativeCaptureDisplayId || 0),
1534
1929
  captureFps: Number(switchedTelemetryFrame.metadata.nativeCaptureFps || 0),
1535
1930
  compressionFps: Number(switchedTelemetryFrame.metadata.nativeCompressionFps || 0),
1536
1931
  stdoutFps: Number(switchedTelemetryFrame.metadata.nativeEncodedFps || 0),
@@ -1547,6 +1942,13 @@ try {
1547
1942
  reason: String(switchedTelemetryFrame.metadata.frameTransportReason || ''),
1548
1943
  p2pReady: switchedTelemetryFrame.metadata.frameTransportP2pReady === true
1549
1944
  };
1945
+ macDeliveredFps = switchedDeliveredFps;
1946
+ macNextDisplayId = Number(macTelemetry.displayId || 0);
1947
+ macCaptureStages.switched = {
1948
+ deliveredFps: switchedDeliveredFps,
1949
+ telemetry: { ...macTelemetry },
1950
+ transport: { ...macTransport }
1951
+ };
1550
1952
  }
1551
1953
  macModifierProbe = await runMacModifierProbe({
1552
1954
  deviceId: connectedDeviceId,