@limrun/ui 0.7.0 → 0.8.0

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.
@@ -51,6 +51,10 @@ interface RemoteControlProps {
51
51
  // showFrame controls whether to display the device frame
52
52
  // around the video. Defaults to true.
53
53
  showFrame?: boolean;
54
+
55
+ // When true, drops after a working session auto-reconnect instead of
56
+ // surfacing the manual "Retry" button. Defaults to false.
57
+ autoReconnect?: boolean;
54
58
  }
55
59
 
56
60
  interface ScreenshotData {
@@ -71,6 +75,7 @@ export interface RemoteControlHandle {
71
75
  sendKeyEvent: (event: ImperativeKeyboardEvent) => void;
72
76
  screenshot: () => Promise<ScreenshotData>;
73
77
  terminateApp: (bundleId: string) => Promise<void>;
78
+ reconnect: () => void;
74
79
  }
75
80
 
76
81
  const debugLog = (...args: any[]) => {
@@ -108,20 +113,21 @@ type DeviceConfig = {
108
113
  loadingLogo: string;
109
114
  loadingLogoSize: string;
110
115
  videoPosition: {
111
- portrait: { heightMultiplier?: number; widthMultiplier?: number; };
112
- landscape: { heightMultiplier?: number; widthMultiplier?: number; };
116
+ portrait: { heightMultiplier?: number; widthMultiplier?: number };
117
+ landscape: { heightMultiplier?: number; widthMultiplier?: number };
113
118
  };
114
119
  frame: {
115
120
  image: string;
116
121
  imageLandscape: string;
117
- }
118
- }
122
+ };
123
+ };
119
124
 
120
125
  const ANDROID_TABLET_VIDEO_WIDTH = 1920;
121
126
  const ANDROID_TABLET_VIDEO_HEIGHT = 1200;
122
127
  const MAX_CONNECTION_ATTEMPTS = 3;
123
128
  const CONNECTION_RETRY_DELAY_MS = 1000;
124
129
  const CONNECTION_SUCCESS_TIMEOUT_MS = 15000;
130
+ const ICE_DISCONNECTED_GRACE_MS = 3000;
125
131
 
126
132
  const isAndroidTabletVideo = (width: number, height: number): boolean =>
127
133
  (width === ANDROID_TABLET_VIDEO_WIDTH && height === ANDROID_TABLET_VIDEO_HEIGHT) ||
@@ -191,7 +197,18 @@ function getAndroidKeycodeAndMeta(event: React.KeyboardEvent): { keycode: number
191
197
  }
192
198
 
193
199
  export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>(
194
- ({ className, url, token, sessionId: propSessionId, openUrl, showFrame = true }: RemoteControlProps, ref) => {
200
+ (
201
+ {
202
+ className,
203
+ url,
204
+ token,
205
+ sessionId: propSessionId,
206
+ openUrl,
207
+ showFrame = true,
208
+ autoReconnect = false,
209
+ }: RemoteControlProps,
210
+ ref,
211
+ ) => {
195
212
  const containerRef = useRef<HTMLDivElement>(null);
196
213
  const videoRef = useRef<HTMLVideoElement>(null);
197
214
  const frameRef = useRef<HTMLImageElement>(null);
@@ -207,9 +224,13 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
207
224
  const retryTimeoutRef = useRef<number | undefined>(undefined);
208
225
  const connectionSuccessTimeoutRef = useRef<number | undefined>(undefined);
209
226
  const requestFrameIntervalRef = useRef<number | undefined>(undefined);
227
+ const iceDisconnectedGraceRef = useRef<number | undefined>(undefined);
210
228
  const connectionGenerationRef = useRef(0);
211
229
  const connectionAttemptRef = useRef(0);
212
230
  const controlChannelOpenedRef = useRef(false);
231
+ // Mirrored to a ref so stale closures in event handlers see the latest value.
232
+ const autoReconnectRef = useRef(autoReconnect);
233
+ autoReconnectRef.current = autoReconnect;
213
234
  const firstFrameShownRef = useRef(false);
214
235
  const pendingScreenshotResolversRef = useRef<
215
236
  Map<string, (value: ScreenshotData | PromiseLike<ScreenshotData>) => void>
@@ -342,10 +363,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
342
363
  case 'down':
343
364
  // For multi-touch: use ACTION_DOWN for first pointer, ACTION_POINTER_DOWN for additional pointers
344
365
  const currentPointerCount = activePointers.current.size;
345
- action =
346
- currentPointerCount === 0
347
- ? AMOTION_EVENT.ACTION_DOWN
348
- : AMOTION_EVENT.ACTION_POINTER_DOWN;
366
+ action = currentPointerCount === 0 ? AMOTION_EVENT.ACTION_DOWN : AMOTION_EVENT.ACTION_POINTER_DOWN;
349
367
  positionToSend = { x: videoX, y: videoY };
350
368
  activePointers.current.set(pointerId, positionToSend);
351
369
  if (pointerId === -1) {
@@ -376,9 +394,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
376
394
  // For multi-touch: use ACTION_UP for last pointer, ACTION_POINTER_UP for non-last pointers
377
395
  const remainingPointerCount = activePointers.current.size;
378
396
  action =
379
- remainingPointerCount === 0
380
- ? AMOTION_EVENT.ACTION_UP
381
- : AMOTION_EVENT.ACTION_POINTER_UP;
397
+ remainingPointerCount === 0 ? AMOTION_EVENT.ACTION_UP : AMOTION_EVENT.ACTION_POINTER_UP;
382
398
  }
383
399
  }
384
400
  break;
@@ -386,20 +402,20 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
386
402
 
387
403
  // Send message if action and position determined
388
404
  if (action !== null && positionToSend !== null) {
389
- debugLog('[rc-touch][mouse->touch] sending', {
390
- pointerId,
391
- eventType,
392
- action,
393
- actionName: motionActionToString(action),
394
- positionToSend,
395
- video: { width: videoWidth, height: videoHeight },
396
- altHeld: isAltHeldRef.current,
397
- activePointersAfter: Array.from(activePointers.current.entries()).map(([id, pos]) => ({
398
- id,
399
- x: pos.x,
400
- y: pos.y,
401
- })),
402
- });
405
+ debugLog('[rc-touch][mouse->touch] sending', {
406
+ pointerId,
407
+ eventType,
408
+ action,
409
+ actionName: motionActionToString(action),
410
+ positionToSend,
411
+ video: { width: videoWidth, height: videoHeight },
412
+ altHeld: isAltHeldRef.current,
413
+ activePointersAfter: Array.from(activePointers.current.entries()).map(([id, pos]) => ({
414
+ id,
415
+ x: pos.x,
416
+ y: pos.y,
417
+ })),
418
+ });
403
419
  const message = createTouchControlMessage(
404
420
  action,
405
421
  pointerId,
@@ -412,11 +428,11 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
412
428
  buttons,
413
429
  );
414
430
  if (message) {
415
- debugLog('[rc-touch][mouse->touch] buffer', {
416
- pointerId,
417
- actionName: motionActionToString(action),
418
- byteLength: message.byteLength,
419
- });
431
+ debugLog('[rc-touch][mouse->touch] buffer', {
432
+ pointerId,
433
+ actionName: motionActionToString(action),
434
+ byteLength: message.byteLength,
435
+ });
420
436
  sendBinaryControlMessage(message);
421
437
  }
422
438
  } else if (eventType === 'up' || eventType === 'cancel') {
@@ -448,7 +464,12 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
448
464
  // This is iOS-specific; Android doesn't use this modifier injection.
449
465
  if (platform === 'ios' && dataChannelRef.current && dataChannelRef.current.readyState === 'open') {
450
466
  const action = nextHeld ? ANDROID_KEYS.ACTION_DOWN : ANDROID_KEYS.ACTION_UP;
451
- const message = createInjectKeycodeMessage(action, ANDROID_KEYS.KEYCODE_ALT_LEFT, 0, ANDROID_KEYS.META_NONE);
467
+ const message = createInjectKeycodeMessage(
468
+ action,
469
+ ANDROID_KEYS.KEYCODE_ALT_LEFT,
470
+ 0,
471
+ ANDROID_KEYS.META_NONE,
472
+ );
452
473
  debugLog('[rc-touch][alt] sending Indigo modifier keycode', {
453
474
  action,
454
475
  keycode: ANDROID_KEYS.KEYCODE_ALT_LEFT,
@@ -524,8 +545,14 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
524
545
 
525
546
  const clampedRelativeX = Math.max(0, Math.min(ctx.actualWidth, relativeX));
526
547
  const clampedRelativeY = Math.max(0, Math.min(ctx.actualHeight, relativeY));
527
- const videoX = Math.max(0, Math.min(ctx.videoWidth, (clampedRelativeX / ctx.actualWidth) * ctx.videoWidth));
528
- const videoY = Math.max(0, Math.min(ctx.videoHeight, (clampedRelativeY / ctx.actualHeight) * ctx.videoHeight));
548
+ const videoX = Math.max(
549
+ 0,
550
+ Math.min(ctx.videoWidth, (clampedRelativeX / ctx.actualWidth) * ctx.videoWidth),
551
+ );
552
+ const videoY = Math.max(
553
+ 0,
554
+ Math.min(ctx.videoHeight, (clampedRelativeY / ctx.actualHeight) * ctx.videoHeight),
555
+ );
529
556
 
530
557
  return {
531
558
  videoX,
@@ -547,8 +574,14 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
547
574
 
548
575
  const clampedRelativeX = Math.max(0, Math.min(ctx.actualWidth, relativeX));
549
576
  const clampedRelativeY = Math.max(0, Math.min(ctx.actualHeight, relativeY));
550
- const videoX = Math.max(0, Math.min(ctx.videoWidth, (clampedRelativeX / ctx.actualWidth) * ctx.videoWidth));
551
- const videoY = Math.max(0, Math.min(ctx.videoHeight, (clampedRelativeY / ctx.actualHeight) * ctx.videoHeight));
577
+ const videoX = Math.max(
578
+ 0,
579
+ Math.min(ctx.videoWidth, (clampedRelativeX / ctx.actualWidth) * ctx.videoWidth),
580
+ );
581
+ const videoY = Math.max(
582
+ 0,
583
+ Math.min(ctx.videoHeight, (clampedRelativeY / ctx.actualHeight) * ctx.videoHeight),
584
+ );
552
585
  const mirrorVideoX = ctx.videoWidth - videoX;
553
586
  const mirrorVideoY = ctx.videoHeight - videoY;
554
587
 
@@ -583,15 +616,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
583
616
  x1: number,
584
617
  y1: number,
585
618
  ) => {
586
- const msg = createTwoFingerTouchControlMessage(
587
- action,
588
- videoWidth,
589
- videoHeight,
590
- x0,
591
- y0,
592
- x1,
593
- y1,
594
- );
619
+ const msg = createTwoFingerTouchControlMessage(action, videoWidth, videoHeight, x0, y0, x1, y1);
595
620
  debugLog('[rc-touch2] sendTwoFingerMessage (iOS)', {
596
621
  actionName: motionActionToString(action),
597
622
  video: { width: videoWidth, height: videoHeight },
@@ -626,9 +651,10 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
626
651
 
627
652
  if (platform === 'ios') {
628
653
  // iOS: use special two-finger message (type=18)
629
- const action = eventType === 'down' ? AMOTION_EVENT.ACTION_DOWN
630
- : eventType === 'move' ? AMOTION_EVENT.ACTION_MOVE
631
- : AMOTION_EVENT.ACTION_UP;
654
+ const action =
655
+ eventType === 'down' ? AMOTION_EVENT.ACTION_DOWN
656
+ : eventType === 'move' ? AMOTION_EVENT.ACTION_MOVE
657
+ : AMOTION_EVENT.ACTION_UP;
632
658
  sendTwoFingerMessage(action, videoWidth, videoHeight, x0, y0, x1, y1);
633
659
  } else {
634
660
  // Android: send two separate single-touch messages with proper action codes
@@ -681,7 +707,12 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
681
707
  // to ensure consistent behavior across focus transitions.
682
708
  }
683
709
 
684
- if (!dataChannelRef.current || dataChannelRef.current.readyState !== 'open' || !videoRef.current || !ctx) {
710
+ if (
711
+ !dataChannelRef.current ||
712
+ dataChannelRef.current.readyState !== 'open' ||
713
+ !videoRef.current ||
714
+ !ctx
715
+ ) {
685
716
  return;
686
717
  }
687
718
 
@@ -729,25 +760,47 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
729
760
  pointerId0: t0.identifier,
730
761
  pointerId1: t1.identifier,
731
762
  };
732
- applyTwoFingerEvent('down', g0.videoWidth, g0.videoHeight,
733
- g0.videoX, g0.videoY, g1.videoX, g1.videoY,
734
- t0.identifier, t1.identifier);
763
+ applyTwoFingerEvent(
764
+ 'down',
765
+ g0.videoWidth,
766
+ g0.videoHeight,
767
+ g0.videoX,
768
+ g0.videoY,
769
+ g1.videoX,
770
+ g1.videoY,
771
+ t0.identifier,
772
+ t1.identifier,
773
+ );
735
774
  } else if (twoFingerStateRef.current.source === 'real-touch') {
736
775
  // Continuing two-finger gesture (move)
737
776
  twoFingerStateRef.current.finger0 = { x: g0.videoX, y: g0.videoY };
738
777
  twoFingerStateRef.current.finger1 = { x: g1.videoX, y: g1.videoY };
739
- applyTwoFingerEvent('move', g0.videoWidth, g0.videoHeight,
740
- g0.videoX, g0.videoY, g1.videoX, g1.videoY,
741
- twoFingerStateRef.current.pointerId0,
742
- twoFingerStateRef.current.pointerId1);
778
+ applyTwoFingerEvent(
779
+ 'move',
780
+ g0.videoWidth,
781
+ g0.videoHeight,
782
+ g0.videoX,
783
+ g0.videoY,
784
+ g1.videoX,
785
+ g1.videoY,
786
+ twoFingerStateRef.current.pointerId0,
787
+ twoFingerStateRef.current.pointerId1,
788
+ );
743
789
  }
744
790
  } else if (allTouches.length < 2 && twoFingerStateRef.current?.source === 'real-touch') {
745
791
  // Finger lifted - end two-finger gesture using last known state
746
792
  const state = twoFingerStateRef.current;
747
- applyTwoFingerEvent('up', state.videoSize.width, state.videoSize.height,
748
- state.finger0.x, state.finger0.y,
749
- state.finger1.x, state.finger1.y,
750
- state.pointerId0, state.pointerId1);
793
+ applyTwoFingerEvent(
794
+ 'up',
795
+ state.videoSize.width,
796
+ state.videoSize.height,
797
+ state.finger0.x,
798
+ state.finger0.y,
799
+ state.finger1.x,
800
+ state.finger1.y,
801
+ state.pointerId0,
802
+ state.pointerId1,
803
+ );
751
804
  twoFingerStateRef.current = null;
752
805
  // Don't process remaining finger - gesture ended
753
806
  return;
@@ -848,8 +901,17 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
848
901
  pointerId1: ALT_POINTER_ID_MIRROR,
849
902
  };
850
903
  videoRef.current?.focus();
851
- applyTwoFingerEvent('down', videoWidth, videoHeight, videoX, videoY, mirrorX, mirrorY,
852
- ALT_POINTER_ID_PRIMARY, ALT_POINTER_ID_MIRROR);
904
+ applyTwoFingerEvent(
905
+ 'down',
906
+ videoWidth,
907
+ videoHeight,
908
+ videoX,
909
+ videoY,
910
+ mirrorX,
911
+ mirrorY,
912
+ ALT_POINTER_ID_PRIMARY,
913
+ ALT_POINTER_ID_MIRROR,
914
+ );
853
915
  return;
854
916
  }
855
917
 
@@ -858,8 +920,17 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
858
920
  // Update positions
859
921
  twoFingerStateRef.current.finger0 = { x: videoX, y: videoY };
860
922
  twoFingerStateRef.current.finger1 = { x: mirrorX, y: mirrorY };
861
- applyTwoFingerEvent('move', videoWidth, videoHeight, videoX, videoY, mirrorX, mirrorY,
862
- ALT_POINTER_ID_PRIMARY, ALT_POINTER_ID_MIRROR);
923
+ applyTwoFingerEvent(
924
+ 'move',
925
+ videoWidth,
926
+ videoHeight,
927
+ videoX,
928
+ videoY,
929
+ mirrorX,
930
+ mirrorY,
931
+ ALT_POINTER_ID_PRIMARY,
932
+ ALT_POINTER_ID_MIRROR,
933
+ );
863
934
  }
864
935
  return;
865
936
  }
@@ -869,9 +940,17 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
869
940
  if (state?.source === 'alt-mouse') {
870
941
  // End gesture at last known positions
871
942
  const { finger0, finger1, videoSize } = state;
872
- applyTwoFingerEvent('up', videoSize.width, videoSize.height,
873
- finger0.x, finger0.y, finger1.x, finger1.y,
874
- ALT_POINTER_ID_PRIMARY, ALT_POINTER_ID_MIRROR);
943
+ applyTwoFingerEvent(
944
+ 'up',
945
+ videoSize.width,
946
+ videoSize.height,
947
+ finger0.x,
948
+ finger0.y,
949
+ finger1.x,
950
+ finger1.y,
951
+ ALT_POINTER_ID_PRIMARY,
952
+ ALT_POINTER_ID_MIRROR,
953
+ );
875
954
  twoFingerStateRef.current = null;
876
955
  }
877
956
  return;
@@ -1039,6 +1118,13 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
1039
1118
  }
1040
1119
  };
1041
1120
 
1121
+ const clearIceDisconnectedGrace = () => {
1122
+ if (iceDisconnectedGraceRef.current !== undefined) {
1123
+ window.clearTimeout(iceDisconnectedGraceRef.current);
1124
+ iceDisconnectedGraceRef.current = undefined;
1125
+ }
1126
+ };
1127
+
1042
1128
  const markFirstFrameShown = () => {
1043
1129
  if (firstFrameShownRef.current) {
1044
1130
  return;
@@ -1050,6 +1136,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
1050
1136
 
1051
1137
  const teardownConnection = () => {
1052
1138
  clearConnectionSuccessTimeout();
1139
+ clearIceDisconnectedGrace();
1053
1140
  stopRequestFrameLoop();
1054
1141
  if (wsRef.current) {
1055
1142
  wsRef.current.onopen = null;
@@ -1093,10 +1180,16 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
1093
1180
  }
1094
1181
 
1095
1182
  if (controlChannelOpenedRef.current) {
1096
- updateStatus(`Connection failed after it was established: ${reason}`);
1097
- setRetryExhausted(true);
1098
- teardownConnection();
1099
- return;
1183
+ if (!autoReconnectRef.current) {
1184
+ updateStatus(`Connection failed after it was established: ${reason}`);
1185
+ setRetryExhausted(true);
1186
+ teardownConnection();
1187
+ return;
1188
+ }
1189
+ // Reset so the upcoming retry gets a fresh MAX_CONNECTION_ATTEMPTS budget.
1190
+ updateStatus(`Reconnecting after established session dropped: ${reason}`);
1191
+ controlChannelOpenedRef.current = false;
1192
+ connectionAttemptRef.current = -1;
1100
1193
  }
1101
1194
 
1102
1195
  clearScheduledRetry();
@@ -1133,8 +1226,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
1133
1226
  setVideoLoaded(false);
1134
1227
  teardownConnection();
1135
1228
 
1136
- const isCurrentAttempt = () =>
1137
- generation === connectionGenerationRef.current;
1229
+ const isCurrentAttempt = () => generation === connectionGenerationRef.current;
1138
1230
 
1139
1231
  connectionSuccessTimeoutRef.current = window.setTimeout(() => {
1140
1232
  connectionSuccessTimeoutRef.current = undefined;
@@ -1262,7 +1354,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
1262
1354
  return getCodecPriority(a) - getCodecPriority(b);
1263
1355
  });
1264
1356
  videoTransceiver.setCodecPreferences(sortedCodecs);
1265
- debugLog('Set codec preferences:', sortedCodecs.map(c => c.mimeType).join(', '));
1357
+ debugLog('Set codec preferences:', sortedCodecs.map((c) => c.mimeType).join(', '));
1266
1358
  }
1267
1359
  }
1268
1360
 
@@ -1364,9 +1456,32 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
1364
1456
  if (!isCurrentAttempt() || peerConnectionRef.current !== peerConnection) {
1365
1457
  return;
1366
1458
  }
1367
- updateStatus('ICE state: ' + peerConnection.iceConnectionState);
1368
- if (peerConnection.iceConnectionState === 'failed') {
1459
+ const iceState = peerConnection.iceConnectionState;
1460
+ updateStatus('ICE state: ' + iceState);
1461
+ if (iceState === 'connected' || iceState === 'completed') {
1462
+ clearIceDisconnectedGrace();
1463
+ return;
1464
+ }
1465
+ if (iceState === 'failed') {
1466
+ clearIceDisconnectedGrace();
1369
1467
  scheduleRetry('ICE connection entered failed state', generation);
1468
+ return;
1469
+ }
1470
+ if (
1471
+ iceState === 'disconnected' &&
1472
+ autoReconnectRef.current &&
1473
+ iceDisconnectedGraceRef.current === undefined
1474
+ ) {
1475
+ // Cap the browser's natural disconnected→failed escalation to recover faster.
1476
+ iceDisconnectedGraceRef.current = window.setTimeout(() => {
1477
+ iceDisconnectedGraceRef.current = undefined;
1478
+ if (!isCurrentAttempt() || peerConnectionRef.current !== peerConnection) {
1479
+ return;
1480
+ }
1481
+ if (peerConnection.iceConnectionState === 'disconnected') {
1482
+ scheduleRetry('ICE stayed disconnected past grace period', generation);
1483
+ }
1484
+ }, ICE_DISCONNECTED_GRACE_MS);
1370
1485
  }
1371
1486
  };
1372
1487
 
@@ -1576,6 +1691,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
1576
1691
  connectionAttemptRef.current = 0;
1577
1692
  controlChannelOpenedRef.current = false;
1578
1693
  clearScheduledRetry();
1694
+ clearIceDisconnectedGrace();
1579
1695
  teardownConnection();
1580
1696
  updateStatus('Stopped');
1581
1697
  };
@@ -1612,9 +1728,9 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
1612
1728
  useEffect(() => {
1613
1729
  const video = videoRef.current;
1614
1730
  const frame = frameRef.current;
1615
-
1731
+
1616
1732
  if (!video) return;
1617
-
1733
+
1618
1734
  // If no frame, no positioning needed
1619
1735
  if (!showFrame || !frame) {
1620
1736
  setVideoStyle({});
@@ -1624,16 +1740,16 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
1624
1740
  const updateVideoPosition = () => {
1625
1741
  const frameWidth = frame.clientWidth;
1626
1742
  const frameHeight = frame.clientHeight;
1627
-
1743
+
1628
1744
  if (frameWidth === 0 || frameHeight === 0) return;
1629
-
1745
+
1630
1746
  // Determine landscape based on video's intrinsic dimensions
1631
1747
  const landscape = video.videoWidth > video.videoHeight;
1632
1748
  setIsLandscape(landscape);
1633
1749
  setUseAndroidTabletFrame(
1634
1750
  platform === 'android' && isAndroidTabletVideo(video.videoWidth, video.videoHeight),
1635
1751
  );
1636
-
1752
+
1637
1753
  const pos = landscape ? config.videoPosition.landscape : config.videoPosition.portrait;
1638
1754
  let newStyle: React.CSSProperties = {};
1639
1755
  if (pos.heightMultiplier) {
@@ -1645,7 +1761,11 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
1645
1761
  // Let the other dimension follow the video stream's intrinsic aspect ratio.
1646
1762
  newStyle.height = 'auto';
1647
1763
  }
1648
- newStyle.borderRadius = `${landscape ? frameHeight * config.videoBorderRadiusMultiplier : frameWidth * config.videoBorderRadiusMultiplier}px`;
1764
+ newStyle.borderRadius = `${
1765
+ landscape ?
1766
+ frameHeight * config.videoBorderRadiusMultiplier
1767
+ : frameWidth * config.videoBorderRadiusMultiplier
1768
+ }px`;
1649
1769
  setVideoStyle(newStyle);
1650
1770
  };
1651
1771
 
@@ -1655,10 +1775,10 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
1655
1775
 
1656
1776
  resizeObserver.observe(frame);
1657
1777
  resizeObserver.observe(video);
1658
-
1778
+
1659
1779
  // Also update when the frame image loads
1660
1780
  frame.addEventListener('load', updateVideoPosition);
1661
-
1781
+
1662
1782
  // Update when video metadata loads (to get correct intrinsic dimensions)
1663
1783
  video.addEventListener('loadedmetadata', updateVideoPosition);
1664
1784
 
@@ -1821,22 +1941,22 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
1821
1941
  }, 30000);
1822
1942
  });
1823
1943
  },
1944
+ reconnect: () => start(),
1824
1945
  }));
1825
1946
 
1826
1947
  // Show indicators when Alt is held and we have a valid hover point (null when outside)
1827
1948
  const showAltIndicators = isAltHeld && hoverPoint !== null;
1828
1949
  const frameImageSrc =
1829
- platform === 'android' && useAndroidTabletFrame
1830
- ? (isLandscape ? pixelTabletFrameImageLandscape : pixelTabletFrameImage)
1831
- : (isLandscape ? config.frame.imageLandscape : config.frame.image);
1950
+ platform === 'android' && useAndroidTabletFrame ?
1951
+ isLandscape ? pixelTabletFrameImageLandscape
1952
+ : pixelTabletFrameImage
1953
+ : isLandscape ? config.frame.imageLandscape
1954
+ : config.frame.image;
1832
1955
 
1833
1956
  return (
1834
1957
  <div
1835
1958
  ref={containerRef}
1836
- className={clsx(
1837
- 'rc-container',
1838
- className,
1839
- )}
1959
+ className={clsx('rc-container', className)}
1840
1960
  style={{ touchAction: 'none' }} // Keep touchAction none for the container
1841
1961
  // Attach unified handler to all interaction events on the container
1842
1962
  // This helps capture mouseleave correctly even if the video element itself isn't hovered
@@ -1878,21 +1998,17 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
1878
1998
  )}
1879
1999
  <video
1880
2000
  ref={videoRef}
1881
- className={clsx(
1882
- 'rc-video',
1883
- !showFrame && 'rc-video-frameless',
1884
- !videoLoaded && 'rc-video-loading',
1885
- )}
2001
+ className={clsx('rc-video', !showFrame && 'rc-video-frameless', !videoLoaded && 'rc-video-loading')}
1886
2002
  style={{
1887
2003
  ...videoStyle,
1888
- ...(config.loadingLogo
1889
- ? {
1890
- backgroundImage: `url("${config.loadingLogo}")`,
1891
- backgroundRepeat: 'no-repeat',
1892
- backgroundPosition: 'center',
1893
- backgroundSize: config.loadingLogoSize,
1894
- }
1895
- : {}),
2004
+ ...(config.loadingLogo ?
2005
+ {
2006
+ backgroundImage: `url("${config.loadingLogo}")`,
2007
+ backgroundRepeat: 'no-repeat',
2008
+ backgroundPosition: 'center',
2009
+ backgroundSize: config.loadingLogoSize,
2010
+ }
2011
+ : {}),
1896
2012
  }}
1897
2013
  autoPlay
1898
2014
  playsInline
@@ -1914,11 +2030,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
1914
2030
  }}
1915
2031
  />
1916
2032
  {retryExhausted && (
1917
- <button
1918
- type="button"
1919
- className="rc-retry-button"
1920
- onClick={handleManualRetry}
1921
- >
2033
+ <button type="button" className="rc-retry-button" onClick={handleManualRetry}>
1922
2034
  Retry
1923
2035
  </button>
1924
2036
  )}
@@ -1954,4 +2066,4 @@ const toScreenshotData = (message: any): ScreenshotData | null => {
1954
2066
  }
1955
2067
 
1956
2068
  return null;
1957
- };
2069
+ };