@limrun/ui 0.3.2 → 0.4.0-rc.10
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 +1 -0
- package/dist/demo.d.ts +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.css +1 -1
- package/dist/index.js +494 -414
- package/index.html +180 -0
- package/package.json +1 -1
- package/src/assets/Apple_logo_white.svg +4 -0
- package/src/assets/android_boot.gif +0 -0
- package/src/assets/android_boot.webp +0 -0
- package/src/assets/iphone16pro_black.webp +0 -0
- package/src/assets/iphone16pro_black_bg.webp +0 -0
- package/src/assets/iphone16pro_black_landscape.webp +0 -0
- package/src/assets/iphone16pro_black_landscape_bg.webp +0 -0
- package/src/assets/pixel9_black.webp +0 -0
- package/src/assets/pixel9_black_landscape.webp +0 -0
- package/src/components/remote-control.css +35 -41
- package/src/components/remote-control.tsx +179 -15
- package/src/demo.tsx +192 -0
package/index.html
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
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
|
+
height: '20vh';
|
|
137
|
+
width: '20vh';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.preview-item h3 {
|
|
141
|
+
text-align: center;
|
|
142
|
+
margin-bottom: 15px;
|
|
143
|
+
font-size: 1.2rem;
|
|
144
|
+
color: #374151;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.device-wrapper {
|
|
148
|
+
height: 100%;
|
|
149
|
+
width: 100%;
|
|
150
|
+
background: #f9fafb;
|
|
151
|
+
border-radius: 12px;
|
|
152
|
+
overflow: hidden;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.info-box {
|
|
156
|
+
background: #fef3c7;
|
|
157
|
+
border: 2px solid #fbbf24;
|
|
158
|
+
border-radius: 8px;
|
|
159
|
+
padding: 15px;
|
|
160
|
+
margin-bottom: 20px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.info-box h4 {
|
|
164
|
+
color: #92400e;
|
|
165
|
+
margin-bottom: 8px;
|
|
166
|
+
font-size: 0.95rem;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.info-box p {
|
|
170
|
+
color: #78350f;
|
|
171
|
+
font-size: 0.9rem;
|
|
172
|
+
line-height: 1.5;
|
|
173
|
+
}
|
|
174
|
+
</style>
|
|
175
|
+
</head>
|
|
176
|
+
<body>
|
|
177
|
+
<div id="root"></div>
|
|
178
|
+
<script type="module" src="/src/demo.tsx"></script>
|
|
179
|
+
</body>
|
|
180
|
+
</html>
|
package/package.json
CHANGED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="842.32007" height="1000.0001">
|
|
3
|
+
<path fill="#fff" d="M824.66636 779.30363c-15.12299 34.93724-33.02368 67.09674-53.7638 96.66374-28.27076 40.3074-51.4182 68.2078-69.25717 83.7012-27.65347 25.4313-57.2822 38.4556-89.00964 39.1963-22.77708 0-50.24539-6.4813-82.21973-19.629-32.07926-13.0861-61.55985-19.5673-88.51583-19.5673-28.27075 0-58.59083 6.4812-91.02193 19.5673-32.48053 13.1477-58.64639 19.9994-78.65196 20.6784-30.42501 1.29623-60.75123-12.0985-91.02193-40.2457-19.32039-16.8514-43.48632-45.7394-72.43607-86.6641-31.060778-43.7024-56.597041-94.37983-76.602609-152.15586C10.740416 658.44309 0 598.01283 0 539.50845c0-67.01648 14.481044-124.8172 43.486336-173.25401C66.28194 327.34823 96.60818 296.6578 134.5638 274.1276c37.95566-22.53016 78.96676-34.01129 123.1321-34.74585 24.16591 0 55.85633 7.47508 95.23784 22.166 39.27042 14.74029 64.48571 22.21538 75.54091 22.21538 8.26518 0 36.27668-8.7405 83.7629-26.16587 44.90607-16.16001 82.80614-22.85118 113.85458-20.21546 84.13326 6.78992 147.34122 39.95559 189.37699 99.70686-75.24463 45.59122-112.46573 109.4473-111.72502 191.36456.67899 63.8067 23.82643 116.90384 69.31888 159.06309 20.61664 19.56727 43.64066 34.69027 69.2571 45.4307-5.55531 16.11062-11.41933 31.54225-17.65372 46.35662zM631.70926 20.0057c0 50.01141-18.27108 96.70693-54.6897 139.92782-43.94932 51.38118-97.10817 81.07162-154.75459 76.38659-.73454-5.99983-1.16045-12.31444-1.16045-18.95003 0-48.01091 20.9006-99.39207 58.01678-141.40314 18.53027-21.27094 42.09746-38.95744 70.67685-53.0663C578.3158 9.00229 605.2903 1.31621 630.65988 0c.74076 6.68575 1.04938 13.37191 1.04938 20.00505z"/>
|
|
4
|
+
</svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,60 +1,54 @@
|
|
|
1
1
|
.rc-container {
|
|
2
2
|
position: relative;
|
|
3
|
-
display:
|
|
4
|
-
|
|
5
|
-
align-items: center;
|
|
6
|
-
justify-content: center;
|
|
3
|
+
display: inline-block;
|
|
4
|
+
box-sizing: border-box;
|
|
7
5
|
isolation: isolate;
|
|
8
|
-
contain: layout style;
|
|
9
6
|
|
|
10
|
-
background-color: rgba(0, 0, 0, 0.05);
|
|
11
7
|
touch-action: none;
|
|
12
|
-
|
|
13
|
-
--rc-spinner-color: #3b82f6;
|
|
14
|
-
--rc-text-muted: #6b7280;
|
|
15
8
|
}
|
|
16
9
|
|
|
17
|
-
.rc-
|
|
10
|
+
.rc-container-portrait {
|
|
18
11
|
height: 100%;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.rc-container-landscape {
|
|
19
15
|
width: 100%;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.rc-phone-frame {
|
|
19
|
+
position: relative;
|
|
20
|
+
z-index: 20;
|
|
21
|
+
display: block;
|
|
24
22
|
pointer-events: none;
|
|
25
|
-
|
|
23
|
+
user-select: none;
|
|
26
24
|
}
|
|
27
25
|
|
|
28
|
-
.rc-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
display: flex;
|
|
32
|
-
flex-direction: column;
|
|
33
|
-
align-items: center;
|
|
34
|
-
justify-content: center;
|
|
35
|
-
text-align: center;
|
|
26
|
+
.rc-phone-frame-portrait {
|
|
27
|
+
height: 100%;
|
|
28
|
+
max-width: 100%;
|
|
36
29
|
}
|
|
37
30
|
|
|
38
|
-
.rc-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
line-height: 1.25rem;
|
|
42
|
-
margin: 0;
|
|
43
|
-
font-family: inherit;
|
|
31
|
+
.rc-phone-frame-landscape {
|
|
32
|
+
width: 100%;
|
|
33
|
+
max-height: 100%;
|
|
44
34
|
}
|
|
45
35
|
|
|
46
|
-
.rc-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
animation: rc-spin 1s linear infinite;
|
|
36
|
+
.rc-video {
|
|
37
|
+
position: absolute;
|
|
38
|
+
width: auto;
|
|
39
|
+
outline: none;
|
|
40
|
+
pointer-events: none;
|
|
41
|
+
cursor: none;
|
|
42
|
+
background-color: black;
|
|
54
43
|
}
|
|
55
44
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
45
|
+
.rc-video-frameless {
|
|
46
|
+
position: relative;
|
|
47
|
+
height: 100%;
|
|
48
|
+
top: auto;
|
|
49
|
+
left: auto;
|
|
60
50
|
}
|
|
51
|
+
|
|
52
|
+
.rc-video-loading {
|
|
53
|
+
aspect-ratio: 9 / 19.5;
|
|
54
|
+
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import React, { useEffect, useRef,
|
|
1
|
+
import React, { useEffect, useRef, useMemo, useState, forwardRef, useImperativeHandle } from 'react';
|
|
2
2
|
import { clsx } from 'clsx';
|
|
3
3
|
import './remote-control.css';
|
|
4
4
|
|
|
5
5
|
import { ANDROID_KEYS, AMOTION_EVENT, codeMap } from '../core/constants';
|
|
6
|
+
|
|
7
|
+
import iphoneFrameImage from '../assets/iphone16pro_black_bg.webp';
|
|
8
|
+
import pixelFrameImage from '../assets/pixel9_black.webp';
|
|
9
|
+
import pixelFrameImageLandscape from '../assets/pixel9_black_landscape.webp';
|
|
10
|
+
import iphoneFrameImageLandscape from '../assets/iphone16pro_black_landscape_bg.webp';
|
|
11
|
+
import appleLogoSvg from '../assets/Apple_logo_white.svg';
|
|
12
|
+
import androidBootImage from '../assets/android_boot.webp';
|
|
6
13
|
import {
|
|
7
14
|
createTouchControlMessage,
|
|
8
15
|
createInjectKeycodeMessage,
|
|
@@ -37,6 +44,10 @@ interface RemoteControlProps {
|
|
|
37
44
|
//
|
|
38
45
|
// If not provided, the component will not open any URL.
|
|
39
46
|
openUrl?: string;
|
|
47
|
+
|
|
48
|
+
// showFrame controls whether to display the device frame
|
|
49
|
+
// around the video. Defaults to true.
|
|
50
|
+
showFrame?: boolean;
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
interface ScreenshotData {
|
|
@@ -70,6 +81,63 @@ const debugWarn = (...args: any[]) => {
|
|
|
70
81
|
}
|
|
71
82
|
};
|
|
72
83
|
|
|
84
|
+
type DevicePlatform = 'ios' | 'android';
|
|
85
|
+
|
|
86
|
+
const detectPlatform = (url: string): DevicePlatform => {
|
|
87
|
+
if (url.includes('/android_')) {
|
|
88
|
+
return 'android';
|
|
89
|
+
}
|
|
90
|
+
// Default to iOS if no Android pattern is found
|
|
91
|
+
return 'ios';
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
type DeviceConfig = {
|
|
95
|
+
videoBorderRadiusMultiplier: number;
|
|
96
|
+
loadingLogo: string;
|
|
97
|
+
loadingLogoSize: string;
|
|
98
|
+
videoPosition: {
|
|
99
|
+
portrait: { top: number; left: number; height: number; };
|
|
100
|
+
landscape: { top: number; left: number; width: number; };
|
|
101
|
+
};
|
|
102
|
+
frame: {
|
|
103
|
+
image: string;
|
|
104
|
+
imageLandscape: string;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Device-specific configuration for frame sizing and video positioning
|
|
109
|
+
// Video position percentages are relative to the frame image dimensions
|
|
110
|
+
const deviceConfig: Record<DevicePlatform, DeviceConfig> = {
|
|
111
|
+
ios: {
|
|
112
|
+
frame: {
|
|
113
|
+
image: iphoneFrameImage,
|
|
114
|
+
imageLandscape: iphoneFrameImageLandscape,
|
|
115
|
+
},
|
|
116
|
+
videoBorderRadiusMultiplier: 0.15,
|
|
117
|
+
loadingLogo: appleLogoSvg,
|
|
118
|
+
loadingLogoSize: '20%',
|
|
119
|
+
// Video position as percentage of frame dimensions
|
|
120
|
+
videoPosition: {
|
|
121
|
+
portrait: { top: 1.61, left: 3.6, height: 96.78 },
|
|
122
|
+
landscape: { top: 3.9, left: 1.61, width: 96.78 },
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
android: {
|
|
126
|
+
frame: {
|
|
127
|
+
image: pixelFrameImage,
|
|
128
|
+
imageLandscape: pixelFrameImageLandscape,
|
|
129
|
+
},
|
|
130
|
+
videoBorderRadiusMultiplier: 0.13,
|
|
131
|
+
loadingLogo: androidBootImage,
|
|
132
|
+
loadingLogoSize: '40%',
|
|
133
|
+
// Video position as percentage of frame dimensions
|
|
134
|
+
videoPosition: {
|
|
135
|
+
portrait: { top: 2.1, left: 4.5, height: 96.2 },
|
|
136
|
+
landscape: { top: 5, left: 2.25, width: 95.9 },
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
73
141
|
function getAndroidKeycodeAndMeta(event: React.KeyboardEvent): { keycode: number; metaState: number } | null {
|
|
74
142
|
const code = event.code;
|
|
75
143
|
const keycode = codeMap[code];
|
|
@@ -101,12 +169,15 @@ function getAndroidKeycodeAndMeta(event: React.KeyboardEvent): { keycode: number
|
|
|
101
169
|
}
|
|
102
170
|
|
|
103
171
|
export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>(
|
|
104
|
-
({ className, url, token, sessionId: propSessionId, openUrl }: RemoteControlProps, ref) => {
|
|
172
|
+
({ className, url, token, sessionId: propSessionId, openUrl, showFrame = true }: RemoteControlProps, ref) => {
|
|
105
173
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
174
|
+
const frameRef = useRef<HTMLImageElement>(null);
|
|
175
|
+
const [videoLoaded, setVideoLoaded] = useState(false);
|
|
176
|
+
const [isLandscape, setIsLandscape] = useState(false);
|
|
177
|
+
const [videoStyle, setVideoStyle] = useState<React.CSSProperties>({});
|
|
106
178
|
const wsRef = useRef<WebSocket | null>(null);
|
|
107
179
|
const peerConnectionRef = useRef<RTCPeerConnection | null>(null);
|
|
108
180
|
const dataChannelRef = useRef<RTCDataChannel | null>(null);
|
|
109
|
-
const [isConnected, setIsConnected] = useState<boolean>(false);
|
|
110
181
|
const keepAliveIntervalRef = useRef<number | undefined>(undefined);
|
|
111
182
|
const pendingScreenshotResolversRef = useRef<
|
|
112
183
|
Map<string, (value: ScreenshotData | PromiseLike<ScreenshotData>) => void>
|
|
@@ -124,6 +195,9 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
124
195
|
[propSessionId],
|
|
125
196
|
);
|
|
126
197
|
|
|
198
|
+
const platform = useMemo(() => detectPlatform(url), [url]);
|
|
199
|
+
const config = deviceConfig[platform];
|
|
200
|
+
|
|
127
201
|
const updateStatus = (message: string) => {
|
|
128
202
|
// Use the wrapper for conditional logging
|
|
129
203
|
debugLog(message);
|
|
@@ -569,7 +643,6 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
569
643
|
// Set up connection state monitoring
|
|
570
644
|
peerConnectionRef.current.onconnectionstatechange = () => {
|
|
571
645
|
updateStatus('Connection state: ' + peerConnectionRef.current?.connectionState);
|
|
572
|
-
setIsConnected(peerConnectionRef.current?.connectionState === 'connected');
|
|
573
646
|
};
|
|
574
647
|
|
|
575
648
|
peerConnectionRef.current.oniceconnectionstatechange = () => {
|
|
@@ -716,11 +789,13 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
716
789
|
dataChannelRef.current.close();
|
|
717
790
|
dataChannelRef.current = null;
|
|
718
791
|
}
|
|
719
|
-
setIsConnected(false);
|
|
720
792
|
updateStatus('Stopped');
|
|
721
793
|
};
|
|
722
794
|
|
|
723
795
|
useEffect(() => {
|
|
796
|
+
// Reset video loaded state when connection params change
|
|
797
|
+
setVideoLoaded(false);
|
|
798
|
+
|
|
724
799
|
// Start connection when component mounts
|
|
725
800
|
start();
|
|
726
801
|
|
|
@@ -740,6 +815,76 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
740
815
|
};
|
|
741
816
|
}, [url, token, propSessionId]);
|
|
742
817
|
|
|
818
|
+
// Calculate video position and border-radius based on frame dimensions
|
|
819
|
+
useEffect(() => {
|
|
820
|
+
const video = videoRef.current;
|
|
821
|
+
const frame = frameRef.current;
|
|
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
|
+
|
|
843
|
+
// Calculate position in pixels based on frame dimensions
|
|
844
|
+
const topPx = (pos.top / 100) * frameHeight;
|
|
845
|
+
const leftPx = (pos.left / 100) * frameWidth;
|
|
846
|
+
|
|
847
|
+
let newStyle: React.CSSProperties = {
|
|
848
|
+
top: `${topPx}px`,
|
|
849
|
+
left: `${leftPx}px`,
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
if ('height' in pos) {
|
|
853
|
+
const heightPx = (pos.height / 100) * frameHeight;
|
|
854
|
+
newStyle.height = `${heightPx}px`;
|
|
855
|
+
newStyle.borderRadius = `${frameWidth * config.videoBorderRadiusMultiplier}px`;
|
|
856
|
+
} else if ('width' in pos) {
|
|
857
|
+
const widthPx = (pos.width / 100) * frameWidth;
|
|
858
|
+
newStyle.width = `${widthPx}px`;
|
|
859
|
+
newStyle.borderRadius = `${frameHeight * config.videoBorderRadiusMultiplier}px`;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
setVideoStyle(newStyle);
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
866
|
+
updateVideoPosition();
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
resizeObserver.observe(frame);
|
|
870
|
+
resizeObserver.observe(video);
|
|
871
|
+
|
|
872
|
+
// Also update when the frame image loads
|
|
873
|
+
frame.addEventListener('load', updateVideoPosition);
|
|
874
|
+
|
|
875
|
+
// Update when video metadata loads (to get correct intrinsic dimensions)
|
|
876
|
+
video.addEventListener('loadedmetadata', updateVideoPosition);
|
|
877
|
+
|
|
878
|
+
// Initial calculation
|
|
879
|
+
updateVideoPosition();
|
|
880
|
+
|
|
881
|
+
return () => {
|
|
882
|
+
resizeObserver.disconnect();
|
|
883
|
+
video.removeEventListener('loadedmetadata', updateVideoPosition);
|
|
884
|
+
frame.removeEventListener('load', updateVideoPosition);
|
|
885
|
+
};
|
|
886
|
+
}, [config, showFrame]);
|
|
887
|
+
|
|
743
888
|
const handleVideoClick = () => {
|
|
744
889
|
if (videoRef.current) {
|
|
745
890
|
videoRef.current.focus();
|
|
@@ -851,7 +996,8 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
851
996
|
return (
|
|
852
997
|
<div
|
|
853
998
|
className={clsx(
|
|
854
|
-
'rc-container',
|
|
999
|
+
'rc-container',
|
|
1000
|
+
isLandscape ? 'rc-container-landscape' : 'rc-container-portrait', // Use custom CSS class instead of Tailwind
|
|
855
1001
|
className,
|
|
856
1002
|
)}
|
|
857
1003
|
style={{ touchAction: 'none' }} // Keep touchAction none for the container
|
|
@@ -866,17 +1012,41 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
866
1012
|
onTouchEnd={handleInteraction}
|
|
867
1013
|
onTouchCancel={handleInteraction}
|
|
868
1014
|
>
|
|
1015
|
+
{showFrame && (
|
|
1016
|
+
<img
|
|
1017
|
+
ref={frameRef}
|
|
1018
|
+
src={isLandscape ? config.frame.imageLandscape : config.frame.image}
|
|
1019
|
+
alt=""
|
|
1020
|
+
className={clsx('rc-phone-frame', isLandscape ? 'rc-phone-frame-landscape' : 'rc-phone-frame-portrait')}
|
|
1021
|
+
draggable={false}
|
|
1022
|
+
/>
|
|
1023
|
+
)}
|
|
869
1024
|
<video
|
|
870
1025
|
ref={videoRef}
|
|
871
|
-
className=
|
|
1026
|
+
className={clsx(
|
|
1027
|
+
'rc-video',
|
|
1028
|
+
!showFrame && 'rc-video-frameless',
|
|
1029
|
+
!videoLoaded && 'rc-video-loading',
|
|
1030
|
+
)}
|
|
1031
|
+
style={{
|
|
1032
|
+
...videoStyle,
|
|
1033
|
+
...(config.loadingLogo
|
|
1034
|
+
? {
|
|
1035
|
+
backgroundImage: `url("${config.loadingLogo}")`,
|
|
1036
|
+
backgroundRepeat: 'no-repeat',
|
|
1037
|
+
backgroundPosition: 'center',
|
|
1038
|
+
backgroundSize: config.loadingLogoSize,
|
|
1039
|
+
}
|
|
1040
|
+
: {}),
|
|
1041
|
+
}}
|
|
872
1042
|
autoPlay
|
|
873
1043
|
playsInline
|
|
874
1044
|
muted
|
|
875
|
-
tabIndex={0}
|
|
876
|
-
style={{ outline: 'none', pointerEvents: 'none' }}
|
|
1045
|
+
tabIndex={0}
|
|
877
1046
|
onKeyDown={handleKeyboard}
|
|
878
1047
|
onKeyUp={handleKeyboard}
|
|
879
1048
|
onClick={handleVideoClick}
|
|
1049
|
+
onLoadedMetadata={() => setVideoLoaded(true)}
|
|
880
1050
|
onFocus={() => {
|
|
881
1051
|
if (videoRef.current) {
|
|
882
1052
|
videoRef.current.style.outline = 'none';
|
|
@@ -888,12 +1058,6 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
888
1058
|
}
|
|
889
1059
|
}}
|
|
890
1060
|
/>
|
|
891
|
-
{!isConnected && (
|
|
892
|
-
<div className="rc-placeholder-wrapper">
|
|
893
|
-
<div className="rc-spinner"></div>
|
|
894
|
-
<p className="rc-placeholder-content">Connecting...</p>
|
|
895
|
-
</div>
|
|
896
|
-
)}
|
|
897
1061
|
</div>
|
|
898
1062
|
);
|
|
899
1063
|
},
|