@limrun/ui 0.4.0-rc.8 → 0.4.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/index.cjs +1 -1
- package/dist/index.css +1 -1
- package/dist/index.js +366 -340
- package/package.json +1 -1
- package/src/assets/android_boot.gif +0 -0
- package/src/assets/android_boot.webp +0 -0
- package/src/assets/iphone16pro_black_bg.webp +0 -0
- package/src/assets/iphone16pro_black_landscape_bg.webp +0 -0
- package/src/components/remote-control.css +29 -26
- package/src/components/remote-control.tsx +106 -35
- package/dist/demo.d.ts +0 -1
- package/index.html +0 -181
- package/src/demo.tsx +0 -192
- package/src/image.png +0 -0
- package/src/landscape.png +0 -0
- package/src/portrait.png +0 -0
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
.rc-container {
|
|
2
2
|
position: relative;
|
|
3
3
|
display: inline-block;
|
|
4
|
-
height: 100%;
|
|
5
4
|
box-sizing: border-box;
|
|
6
5
|
isolation: isolate;
|
|
7
6
|
|
|
8
7
|
touch-action: none;
|
|
9
8
|
}
|
|
10
9
|
|
|
10
|
+
.rc-container-portrait {
|
|
11
|
+
height: 100%;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.rc-container-landscape {
|
|
15
|
+
width: 100%;
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
.rc-phone-frame {
|
|
12
19
|
position: relative;
|
|
13
20
|
z-index: 20;
|
|
@@ -18,19 +25,38 @@
|
|
|
18
25
|
|
|
19
26
|
.rc-phone-frame-portrait {
|
|
20
27
|
height: 100%;
|
|
28
|
+
max-width: 100%;
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
.rc-phone-frame-landscape {
|
|
24
32
|
width: 100%;
|
|
33
|
+
max-height: 100%;
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
.rc-video {
|
|
28
37
|
position: absolute;
|
|
29
|
-
|
|
38
|
+
/* Size is controlled by inline style; defaults are for frameless mode */
|
|
39
|
+
width: 100%;
|
|
40
|
+
height: 100%;
|
|
30
41
|
outline: none;
|
|
31
42
|
pointer-events: none;
|
|
32
43
|
cursor: none;
|
|
33
44
|
background-color: black;
|
|
45
|
+
object-fit: contain;
|
|
46
|
+
object-position: center center;
|
|
47
|
+
inset: 0;
|
|
48
|
+
|
|
49
|
+
top: 50%;
|
|
50
|
+
left: 50%;
|
|
51
|
+
transform: translate(-50%, -50%);
|
|
52
|
+
right: auto;
|
|
53
|
+
bottom: auto;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.rc-video-ios-stretch {
|
|
57
|
+
/* iOS-only: intentionally stretch to fill the target screen box */
|
|
58
|
+
object-fit: fill;
|
|
59
|
+
z-index: 30;
|
|
34
60
|
}
|
|
35
61
|
|
|
36
62
|
.rc-video-frameless {
|
|
@@ -38,32 +64,9 @@
|
|
|
38
64
|
height: 100%;
|
|
39
65
|
top: auto;
|
|
40
66
|
left: auto;
|
|
67
|
+
width: 100%;
|
|
41
68
|
}
|
|
42
69
|
|
|
43
70
|
.rc-video-loading {
|
|
44
71
|
aspect-ratio: 9 / 19.5;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.rc-video-ios-portrait {
|
|
48
|
-
top: 1.61%;
|
|
49
|
-
left: 3.9%;
|
|
50
|
-
height: 96.78%;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
.rc-video-ios-landscape {
|
|
54
|
-
top: 3.9%;
|
|
55
|
-
left: 1.61%;
|
|
56
|
-
width: 96.78%;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
.rc-video-android-portrait {
|
|
60
|
-
top: 2.25%;
|
|
61
|
-
left: 4.5%;
|
|
62
|
-
height: 95.9%;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
.rc-video-android-landscape {
|
|
66
|
-
left: 2.25%;
|
|
67
|
-
top: 4.5%;
|
|
68
|
-
width: 95.9%;
|
|
69
72
|
}
|
|
@@ -4,11 +4,12 @@ import './remote-control.css';
|
|
|
4
4
|
|
|
5
5
|
import { ANDROID_KEYS, AMOTION_EVENT, codeMap } from '../core/constants';
|
|
6
6
|
|
|
7
|
-
import iphoneFrameImage from '../assets/
|
|
7
|
+
import iphoneFrameImage from '../assets/iphone16pro_black_bg.webp';
|
|
8
8
|
import pixelFrameImage from '../assets/pixel9_black.webp';
|
|
9
9
|
import pixelFrameImageLandscape from '../assets/pixel9_black_landscape.webp';
|
|
10
|
-
import iphoneFrameImageLandscape from '../assets/
|
|
10
|
+
import iphoneFrameImageLandscape from '../assets/iphone16pro_black_landscape_bg.webp';
|
|
11
11
|
import appleLogoSvg from '../assets/Apple_logo_white.svg';
|
|
12
|
+
import androidBootImage from '../assets/android_boot.webp';
|
|
12
13
|
import {
|
|
13
14
|
createTouchControlMessage,
|
|
14
15
|
createInjectKeycodeMessage,
|
|
@@ -90,21 +91,50 @@ const detectPlatform = (url: string): DevicePlatform => {
|
|
|
90
91
|
return 'ios';
|
|
91
92
|
};
|
|
92
93
|
|
|
94
|
+
type DeviceConfig = {
|
|
95
|
+
videoBorderRadiusMultiplier: number;
|
|
96
|
+
loadingLogo: string;
|
|
97
|
+
loadingLogoSize: string;
|
|
98
|
+
videoPosition: {
|
|
99
|
+
portrait: { heightMultiplier?: number; widthMultiplier?: number; };
|
|
100
|
+
landscape: { heightMultiplier?: number; widthMultiplier?: number; };
|
|
101
|
+
};
|
|
102
|
+
frame: {
|
|
103
|
+
image: string;
|
|
104
|
+
imageLandscape: string;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
93
108
|
// Device-specific configuration for frame sizing and video positioning
|
|
94
|
-
|
|
109
|
+
// Video position percentages are relative to the frame image dimensions
|
|
110
|
+
const deviceConfig: Record<DevicePlatform, DeviceConfig> = {
|
|
95
111
|
ios: {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
112
|
+
frame: {
|
|
113
|
+
image: iphoneFrameImage,
|
|
114
|
+
imageLandscape: iphoneFrameImageLandscape,
|
|
115
|
+
},
|
|
99
116
|
videoBorderRadiusMultiplier: 0.15,
|
|
100
117
|
loadingLogo: appleLogoSvg,
|
|
118
|
+
loadingLogoSize: '20%',
|
|
119
|
+
// Video position as percentage of frame dimensions
|
|
120
|
+
videoPosition: {
|
|
121
|
+
portrait: { heightMultiplier: 0.9678 },
|
|
122
|
+
landscape: { widthMultiplier: 0.9678 },
|
|
123
|
+
},
|
|
101
124
|
},
|
|
102
125
|
android: {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
126
|
+
frame: {
|
|
127
|
+
image: pixelFrameImage,
|
|
128
|
+
imageLandscape: pixelFrameImageLandscape,
|
|
129
|
+
},
|
|
106
130
|
videoBorderRadiusMultiplier: 0.13,
|
|
107
|
-
loadingLogo:
|
|
131
|
+
loadingLogo: androidBootImage,
|
|
132
|
+
loadingLogoSize: '40%',
|
|
133
|
+
// Video position as percentage of frame dimensions
|
|
134
|
+
videoPosition: {
|
|
135
|
+
portrait: { heightMultiplier: 0.967 },
|
|
136
|
+
landscape: { widthMultiplier: 0.962 },
|
|
137
|
+
},
|
|
108
138
|
},
|
|
109
139
|
};
|
|
110
140
|
|
|
@@ -144,6 +174,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
144
174
|
const frameRef = useRef<HTMLImageElement>(null);
|
|
145
175
|
const [videoLoaded, setVideoLoaded] = useState(false);
|
|
146
176
|
const [isLandscape, setIsLandscape] = useState(false);
|
|
177
|
+
const [videoStyle, setVideoStyle] = useState<React.CSSProperties>({});
|
|
147
178
|
const wsRef = useRef<WebSocket | null>(null);
|
|
148
179
|
const peerConnectionRef = useRef<RTCPeerConnection | null>(null);
|
|
149
180
|
const dataChannelRef = useRef<RTCDataChannel | null>(null);
|
|
@@ -784,33 +815,70 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
784
815
|
};
|
|
785
816
|
}, [url, token, propSessionId]);
|
|
786
817
|
|
|
787
|
-
//
|
|
818
|
+
// Calculate video position and border-radius based on frame dimensions
|
|
788
819
|
useEffect(() => {
|
|
789
820
|
const video = videoRef.current;
|
|
790
821
|
const frame = frameRef.current;
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
822
|
+
|
|
823
|
+
if (!video) return;
|
|
824
|
+
|
|
825
|
+
// If no frame, no positioning needed
|
|
826
|
+
if (!showFrame || !frame) {
|
|
827
|
+
setVideoStyle({});
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const updateVideoPosition = () => {
|
|
832
|
+
const frameWidth = frame.clientWidth;
|
|
833
|
+
const frameHeight = frame.clientHeight;
|
|
834
|
+
|
|
835
|
+
if (frameWidth === 0 || frameHeight === 0) return;
|
|
836
|
+
|
|
837
|
+
// Determine landscape based on video's intrinsic dimensions
|
|
838
|
+
const landscape = video.videoWidth > video.videoHeight;
|
|
839
|
+
setIsLandscape(landscape);
|
|
840
|
+
|
|
841
|
+
const pos = landscape ? config.videoPosition.landscape : config.videoPosition.portrait;
|
|
842
|
+
let newStyle: React.CSSProperties = {};
|
|
843
|
+
if (pos.heightMultiplier) {
|
|
844
|
+
newStyle.height = `${frameHeight * pos.heightMultiplier}px`;
|
|
845
|
+
// Let the other dimension follow the video stream's intrinsic aspect ratio.
|
|
846
|
+
newStyle.width = 'auto';
|
|
847
|
+
} else if (pos.widthMultiplier) {
|
|
848
|
+
newStyle.width = `${frameWidth * pos.widthMultiplier}px`;
|
|
849
|
+
// Let the other dimension follow the video stream's intrinsic aspect ratio.
|
|
850
|
+
newStyle.height = 'auto';
|
|
807
851
|
}
|
|
852
|
+
newStyle.borderRadius = `${landscape ? frameHeight * config.videoBorderRadiusMultiplier : frameWidth * config.videoBorderRadiusMultiplier}px`;
|
|
853
|
+
setVideoStyle(newStyle);
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
857
|
+
updateVideoPosition();
|
|
808
858
|
});
|
|
809
859
|
|
|
860
|
+
resizeObserver.observe(frame);
|
|
810
861
|
resizeObserver.observe(video);
|
|
862
|
+
|
|
863
|
+
// Also update when the frame image loads
|
|
864
|
+
frame.addEventListener('load', updateVideoPosition);
|
|
865
|
+
|
|
866
|
+
// Update when video metadata loads (to get correct intrinsic dimensions)
|
|
867
|
+
video.addEventListener('loadedmetadata', updateVideoPosition);
|
|
868
|
+
|
|
869
|
+
// IMPORTANT: When the WebRTC stream changes orientation, the intrinsic video size
|
|
870
|
+
// (videoWidth/videoHeight) can change without re-firing 'loadedmetadata'.
|
|
871
|
+
// The <video> element emits 'resize' in that case.
|
|
872
|
+
video.addEventListener('resize', updateVideoPosition);
|
|
873
|
+
|
|
874
|
+
// Initial calculation
|
|
875
|
+
updateVideoPosition();
|
|
811
876
|
|
|
812
877
|
return () => {
|
|
813
878
|
resizeObserver.disconnect();
|
|
879
|
+
video.removeEventListener('loadedmetadata', updateVideoPosition);
|
|
880
|
+
video.removeEventListener('resize', updateVideoPosition);
|
|
881
|
+
frame.removeEventListener('load', updateVideoPosition);
|
|
814
882
|
};
|
|
815
883
|
}, [config, showFrame]);
|
|
816
884
|
|
|
@@ -925,7 +993,8 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
925
993
|
return (
|
|
926
994
|
<div
|
|
927
995
|
className={clsx(
|
|
928
|
-
'rc-container',
|
|
996
|
+
'rc-container',
|
|
997
|
+
isLandscape ? 'rc-container-landscape' : 'rc-container-portrait', // Use custom CSS class instead of Tailwind
|
|
929
998
|
className,
|
|
930
999
|
)}
|
|
931
1000
|
style={{ touchAction: 'none' }} // Keep touchAction none for the container
|
|
@@ -943,7 +1012,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
943
1012
|
{showFrame && (
|
|
944
1013
|
<img
|
|
945
1014
|
ref={frameRef}
|
|
946
|
-
src={isLandscape ? config.
|
|
1015
|
+
src={isLandscape ? config.frame.imageLandscape : config.frame.image}
|
|
947
1016
|
alt=""
|
|
948
1017
|
className={clsx('rc-phone-frame', isLandscape ? 'rc-phone-frame-landscape' : 'rc-phone-frame-portrait')}
|
|
949
1018
|
draggable={false}
|
|
@@ -953,19 +1022,21 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
953
1022
|
ref={videoRef}
|
|
954
1023
|
className={clsx(
|
|
955
1024
|
'rc-video',
|
|
956
|
-
showFrame
|
|
1025
|
+
!showFrame && 'rc-video-frameless',
|
|
1026
|
+
showFrame && platform === 'ios' && 'rc-video-ios-stretch',
|
|
957
1027
|
!videoLoaded && 'rc-video-loading',
|
|
958
1028
|
)}
|
|
959
|
-
style={
|
|
960
|
-
|
|
1029
|
+
style={{
|
|
1030
|
+
...videoStyle,
|
|
1031
|
+
...(config.loadingLogo
|
|
961
1032
|
? {
|
|
962
1033
|
backgroundImage: `url("${config.loadingLogo}")`,
|
|
963
1034
|
backgroundRepeat: 'no-repeat',
|
|
964
1035
|
backgroundPosition: 'center',
|
|
965
|
-
backgroundSize:
|
|
1036
|
+
backgroundSize: config.loadingLogoSize,
|
|
966
1037
|
}
|
|
967
|
-
: {}
|
|
968
|
-
}
|
|
1038
|
+
: {}),
|
|
1039
|
+
}}
|
|
969
1040
|
autoPlay
|
|
970
1041
|
playsInline
|
|
971
1042
|
muted
|
package/dist/demo.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/index.html
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>RemoteControl Demo - @limrun/ui</title>
|
|
7
|
-
<style>
|
|
8
|
-
* {
|
|
9
|
-
margin: 0;
|
|
10
|
-
padding: 0;
|
|
11
|
-
box-sizing: border-box;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
body {
|
|
15
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
16
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
17
|
-
min-height: 100vh;
|
|
18
|
-
padding: 20px;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
#root {
|
|
22
|
-
display: flex;
|
|
23
|
-
flex-direction: column;
|
|
24
|
-
gap: 20px;
|
|
25
|
-
max-width: 1400px;
|
|
26
|
-
margin: 0 auto;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
.header {
|
|
30
|
-
text-align: center;
|
|
31
|
-
color: white;
|
|
32
|
-
padding: 20px;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
.header h1 {
|
|
36
|
-
font-size: 2.5rem;
|
|
37
|
-
margin-bottom: 10px;
|
|
38
|
-
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
.header p {
|
|
42
|
-
font-size: 1.1rem;
|
|
43
|
-
opacity: 0.9;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.demo-container {
|
|
47
|
-
background: white;
|
|
48
|
-
border-radius: 20px;
|
|
49
|
-
padding: 30px;
|
|
50
|
-
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
.controls {
|
|
54
|
-
display: flex;
|
|
55
|
-
flex-direction: column;
|
|
56
|
-
gap: 15px;
|
|
57
|
-
margin-bottom: 30px;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
.control-group {
|
|
61
|
-
display: flex;
|
|
62
|
-
flex-direction: column;
|
|
63
|
-
gap: 8px;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
.control-group label {
|
|
67
|
-
font-weight: 600;
|
|
68
|
-
font-size: 0.9rem;
|
|
69
|
-
color: #374151;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.control-group input,
|
|
73
|
-
.control-group select {
|
|
74
|
-
padding: 10px 15px;
|
|
75
|
-
border: 2px solid #e5e7eb;
|
|
76
|
-
border-radius: 8px;
|
|
77
|
-
font-size: 0.95rem;
|
|
78
|
-
transition: border-color 0.2s;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
.control-group input:focus,
|
|
82
|
-
.control-group select:focus {
|
|
83
|
-
outline: none;
|
|
84
|
-
border-color: #667eea;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
.button-group {
|
|
88
|
-
display: flex;
|
|
89
|
-
gap: 10px;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
button {
|
|
93
|
-
padding: 12px 24px;
|
|
94
|
-
border: none;
|
|
95
|
-
border-radius: 8px;
|
|
96
|
-
font-size: 1rem;
|
|
97
|
-
font-weight: 600;
|
|
98
|
-
cursor: pointer;
|
|
99
|
-
transition: all 0.2s;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
button.primary {
|
|
103
|
-
background: #667eea;
|
|
104
|
-
color: white;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
button.primary:hover {
|
|
108
|
-
background: #5568d3;
|
|
109
|
-
transform: translateY(-1px);
|
|
110
|
-
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
button.secondary {
|
|
114
|
-
background: #e5e7eb;
|
|
115
|
-
color: #374151;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
button.secondary:hover {
|
|
119
|
-
background: #d1d5db;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
button:disabled {
|
|
123
|
-
opacity: 0.5;
|
|
124
|
-
cursor: not-allowed;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
.device-preview {
|
|
128
|
-
display: flex;
|
|
129
|
-
gap: 30px;
|
|
130
|
-
justify-content: center;
|
|
131
|
-
flex-wrap: wrap;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
.preview-item {
|
|
135
|
-
flex: 1;
|
|
136
|
-
min-width: 300px;
|
|
137
|
-
max-width: 800px;
|
|
138
|
-
max-height: 800px;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
.preview-item h3 {
|
|
142
|
-
text-align: center;
|
|
143
|
-
margin-bottom: 15px;
|
|
144
|
-
font-size: 1.2rem;
|
|
145
|
-
color: #374151;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
.device-wrapper {
|
|
149
|
-
height: 100%;
|
|
150
|
-
width: 100%;
|
|
151
|
-
background: #f9fafb;
|
|
152
|
-
border-radius: 12px;
|
|
153
|
-
overflow: hidden;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
.info-box {
|
|
157
|
-
background: #fef3c7;
|
|
158
|
-
border: 2px solid #fbbf24;
|
|
159
|
-
border-radius: 8px;
|
|
160
|
-
padding: 15px;
|
|
161
|
-
margin-bottom: 20px;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
.info-box h4 {
|
|
165
|
-
color: #92400e;
|
|
166
|
-
margin-bottom: 8px;
|
|
167
|
-
font-size: 0.95rem;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
.info-box p {
|
|
171
|
-
color: #78350f;
|
|
172
|
-
font-size: 0.9rem;
|
|
173
|
-
line-height: 1.5;
|
|
174
|
-
}
|
|
175
|
-
</style>
|
|
176
|
-
</head>
|
|
177
|
-
<body>
|
|
178
|
-
<div id="root"></div>
|
|
179
|
-
<script type="module" src="/src/demo.tsx"></script>
|
|
180
|
-
</body>
|
|
181
|
-
</html>
|