@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.
- package/dist/components/remote-control.d.ts +2 -0
- package/dist/demo.d.ts +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +507 -484
- package/index.html +199 -0
- package/package.json +43 -43
- package/src/components/remote-control.css +1 -1
- package/src/components/remote-control.tsx +222 -110
- package/src/demo.tsx +185 -0
- package/tsconfig.json +25 -25
- package/tsconfig.node.json +25 -25
|
@@ -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
|
-
(
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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(
|
|
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(
|
|
528
|
-
|
|
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(
|
|
551
|
-
|
|
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 =
|
|
630
|
-
|
|
631
|
-
|
|
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 (
|
|
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(
|
|
733
|
-
|
|
734
|
-
|
|
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(
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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(
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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(
|
|
852
|
-
|
|
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(
|
|
862
|
-
|
|
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(
|
|
873
|
-
|
|
874
|
-
|
|
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
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
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
|
-
|
|
1368
|
-
|
|
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 = `${
|
|
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
|
-
|
|
1831
|
-
:
|
|
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
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
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
|
+
};
|