Spaces:
Running
Running
Update src/components/control-tray/ControlTray.tsx
Browse files
src/components/control-tray/ControlTray.tsx
CHANGED
@@ -1,32 +1,23 @@
|
|
|
|
1 |
/**
|
2 |
Copyright 2024 Google LLC
|
3 |
-
|
4 |
-
you may not use this file except in compliance with the License.
|
5 |
-
You may obtain a copy of the License at
|
6 |
-
http://www.apache.org/licenses/LICENSE-2.0
|
7 |
-
Unless required by applicable law or agreed to in writing, software
|
8 |
-
distributed under the License is distributed on an "AS IS" BASIS,
|
9 |
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
10 |
-
See the License for the specific language governing permissions and
|
11 |
-
limitations under the License.
|
12 |
*/
|
13 |
import cn from "classnames";
|
14 |
import React, { memo, ReactNode, RefObject, useEffect, useState } from "react";
|
15 |
import { useLiveAPIContext } from "../../contexts/LiveAPIContext";
|
16 |
import { AudioRecorder } from "../../lib/audio-recorder";
|
17 |
-
import LogoAnimation from '../logo-animation/LogoAnimation';
|
18 |
|
19 |
-
//
|
20 |
-
|
21 |
|
22 |
-
//
|
23 |
-
const
|
|
|
|
|
|
|
24 |
|
25 |
-
// آیکون توقف دوربین
|
26 |
-
const SvgStopCamIcon = () => <svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"><path opacity="0.4" d="M29.0785 10.8076L6.91966 32.9665C4.61316 31.5002 3.70703 28.8807 3.70703 26.36V13.18C3.70703 7.54555 5.89821 5.35437 11.5327 5.35437H21.4177C26.1789 5.35437 28.4854 6.9195 29.0785 10.8076Z" fill="#2252A0"/><path opacity="0.4" d="M29.2427 15.2394V26.36C29.2427 26.4918 29.2262 26.5907 29.2262 26.706C29.2262 26.8213 29.2097 26.9366 29.2097 27.052H29.2262C29.045 32.1757 26.8208 34.1856 21.417 34.1856H11.532C11.1202 34.1856 10.7412 34.1692 10.3623 34.1197L29.2427 15.2394Z" fill="#2252A0"/><path opacity="0.4" d="M29.21 27.052C29.21 26.9366 29.2264 26.8213 29.2264 26.706C29.2429 26.8213 29.2429 26.9366 29.2264 27.052H29.21Z" fill="#2252A0"/><path opacity="0.4" d="M29.2264 12.4716C29.2429 12.5869 29.2429 12.7187 29.2264 12.834C29.2264 12.7187 29.21 12.6034 29.21 12.488L29.2264 12.4716Z" fill="#2252A0"/><path d="M37.4804 13.8061V25.734C37.4804 28.0899 36.3436 29.029 35.6682 29.3749C35.3551 29.5397 34.8774 29.7209 34.2678 29.7209C33.5594 29.7209 32.6697 29.4903 31.6483 28.7654L29.2264 27.052H29.21C29.21 26.9366 29.2264 26.8213 29.2264 26.706C29.2264 26.5907 29.2429 26.4918 29.2429 26.36V15.2394L34.6302 9.85205C35.0751 9.885 35.421 10.0168 35.6682 10.1651C36.3436 10.5111 37.4804 11.4501 37.4804 13.8061Z" fill="#2252A0"/><path d="M35.8666 3.67393C35.3723 3.17968 34.565 3.17968 34.0708 3.67393L3.6744 34.0868C3.18015 34.581 3.18015 35.3883 3.6744 35.8826C3.92152 36.1132 4.23455 36.245 4.56405 36.245C4.89355 36.245 5.20657 36.1132 5.4537 35.8661L35.8666 5.45323C36.3773 4.95898 36.3773 4.16818 35.8666 3.67393Z" fill="#2252A0"/></svg>;
|
27 |
-
|
28 |
-
// آیکون تعویض دوربین
|
29 |
-
const SvgSwitchCameraIcon = () => <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-[22px] h-[22px]"><path d="M11 19H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h5"/><path d="M13 5h7a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-5"/><path d="m17 12-3-3 3-3"/><path d="m7 12 3 3-3 3"/></svg>;
|
30 |
|
31 |
export type ControlTrayProps = {
|
32 |
videoRef: RefObject<HTMLVideoElement>;
|
@@ -37,6 +28,8 @@ export type ControlTrayProps = {
|
|
37 |
isAppCamActive: boolean;
|
38 |
onAppCamToggle: (active: boolean) => void;
|
39 |
ReferenceMicrophoneIcon: () => JSX.Element;
|
|
|
|
|
40 |
};
|
41 |
|
42 |
const ControlTray: React.FC<ControlTrayProps> = ({
|
@@ -48,151 +41,77 @@ const ControlTray: React.FC<ControlTrayProps> = ({
|
|
48 |
isAppCamActive,
|
49 |
onAppCamToggle,
|
50 |
ReferenceMicrophoneIcon,
|
|
|
51 |
}) => {
|
52 |
const { client, connected, connect } = useLiveAPIContext();
|
53 |
const [audioRecorder] = useState(() => new AudioRecorder());
|
54 |
const [activeLocalVideoStream, setActiveLocalVideoStream] = useState<MediaStream | null>(null);
|
55 |
-
|
|
|
56 |
const [isSwitchingCamera, setIsSwitchingCamera] = useState(false);
|
57 |
const renderCanvasRef = React.useRef<HTMLCanvasElement>(null);
|
58 |
|
|
|
59 |
useEffect(() => {
|
60 |
if (isAppCamActive && !activeLocalVideoStream && !isSwitchingCamera) {
|
61 |
-
startWebcam();
|
62 |
} else if (!isAppCamActive && activeLocalVideoStream) {
|
63 |
stopWebcam();
|
64 |
}
|
65 |
}, [isAppCamActive, activeLocalVideoStream, isSwitchingCamera]);
|
66 |
|
67 |
-
useEffect(() => {
|
68 |
-
const onData = (base64: string) => {
|
69 |
-
if (client && connected && isAppMicActive) {
|
70 |
-
client.sendRealtimeInput([{ mimeType: "audio/pcm;rate=16000", data: base64 }]);
|
71 |
-
}
|
72 |
-
};
|
73 |
-
if (connected && isAppMicActive && audioRecorder) {
|
74 |
-
audioRecorder.on("data", onData).start();
|
75 |
-
} else if (audioRecorder && audioRecorder.recording) {
|
76 |
-
audioRecorder.stop();
|
77 |
-
}
|
78 |
-
return () => {
|
79 |
-
if (audioRecorder) {
|
80 |
-
audioRecorder.off("data", onData);
|
81 |
-
if (audioRecorder.recording) audioRecorder.stop();
|
82 |
-
}
|
83 |
-
};
|
84 |
}, [connected, client, isAppMicActive, audioRecorder]);
|
85 |
-
|
86 |
-
useEffect(() => {
|
87 |
-
let timeoutId = -1;
|
88 |
-
function sendVideoFrame() {
|
89 |
-
if (connected && activeLocalVideoStream && client && videoRef.current) {
|
90 |
-
const video = videoRef.current;
|
91 |
-
const canvas = renderCanvasRef.current;
|
92 |
-
if (!canvas || video.readyState < video.HAVE_METADATA || video.paused || video.ended) {
|
93 |
-
if (activeLocalVideoStream) timeoutId = window.setTimeout(sendVideoFrame, 1000 / 4);
|
94 |
-
return;
|
95 |
-
}
|
96 |
-
try {
|
97 |
-
const ctx = canvas.getContext("2d");
|
98 |
-
if (!ctx) return;
|
99 |
-
const scale = 0.5;
|
100 |
-
canvas.width = video.videoWidth * scale;
|
101 |
-
canvas.height = video.videoHeight * scale;
|
102 |
-
if (canvas.width > 0 && canvas.height > 0) {
|
103 |
-
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
104 |
-
const base64 = canvas.toDataURL("image/jpeg", 0.8);
|
105 |
-
const data = base64.slice(base64.indexOf(",") + 1);
|
106 |
-
if (data) client.sendRealtimeInput([{ mimeType: "image/jpeg", data }]);
|
107 |
-
}
|
108 |
-
} catch (error) { console.error("❌ Error frame:", error); }
|
109 |
-
}
|
110 |
-
if (connected && activeLocalVideoStream) {
|
111 |
-
timeoutId = window.setTimeout(sendVideoFrame, 1000 / 4);
|
112 |
-
}
|
113 |
-
}
|
114 |
-
if (connected && activeLocalVideoStream && videoRef.current) {
|
115 |
-
timeoutId = window.setTimeout(sendVideoFrame, 200);
|
116 |
-
}
|
117 |
-
return () => clearTimeout(timeoutId);
|
118 |
}, [connected, activeLocalVideoStream, client, videoRef, renderCanvasRef]);
|
|
|
|
|
|
|
119 |
|
|
|
120 |
useEffect(() => {
|
121 |
-
if (
|
122 |
-
|
123 |
-
videoRef.current.srcObject = activeLocalVideoStream;
|
124 |
-
if (activeLocalVideoStream) {
|
125 |
-
videoRef.current.play().catch(e => console.warn("Video play failed:", e));
|
126 |
-
}
|
127 |
-
}
|
128 |
}
|
129 |
-
}, [
|
130 |
|
131 |
-
const ensureConnectedAndReady = async (): Promise<boolean> => {
|
132 |
-
if (!connected) {
|
133 |
-
try { await connect(); return true; }
|
134 |
-
catch (err) { console.error('❌ CT Connect err:', err); return false; }
|
135 |
-
}
|
136 |
-
return true;
|
137 |
-
};
|
138 |
|
139 |
-
const
|
140 |
-
|
141 |
-
const newMicState = !isAppMicActive;
|
142 |
-
if (newMicState && !(await ensureConnectedAndReady())) {
|
143 |
-
onAppMicToggle(false); return;
|
144 |
-
}
|
145 |
-
onAppMicToggle(newMicState);
|
146 |
-
};
|
147 |
|
148 |
-
const startWebcam = async (facingModeToTry:
|
149 |
if (isSwitchingCamera) return;
|
150 |
setIsSwitchingCamera(true);
|
151 |
try {
|
152 |
const mediaStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: facingModeToTry }, audio: false });
|
153 |
-
setActiveLocalVideoStream(mediaStream);
|
|
|
|
|
154 |
} catch (error) {
|
155 |
console.error(`❌ Start WC err ${facingModeToTry}:`, error);
|
156 |
setActiveLocalVideoStream(null); onVideoStreamChange(null); onAppCamToggle(false);
|
157 |
} finally { setIsSwitchingCamera(false); }
|
158 |
};
|
159 |
|
160 |
-
const stopWebcam = () => {
|
161 |
-
|
162 |
-
setActiveLocalVideoStream(null); onVideoStreamChange(null);
|
163 |
-
};
|
164 |
-
|
165 |
-
const handleCamToggle = async () => {
|
166 |
-
if (isSwitchingCamera) return;
|
167 |
-
const newCamState = !isAppCamActive;
|
168 |
-
|
169 |
-
if (newCamState) {
|
170 |
-
if (!(await ensureConnectedAndReady())) {
|
171 |
-
onAppCamToggle(false);
|
172 |
-
return;
|
173 |
-
}
|
174 |
-
if (!isAppMicActive) {
|
175 |
-
onAppMicToggle(true);
|
176 |
-
}
|
177 |
-
onAppCamToggle(true);
|
178 |
-
} else {
|
179 |
-
onAppCamToggle(false);
|
180 |
-
}
|
181 |
-
};
|
182 |
|
183 |
const handleSwitchCamera = async () => {
|
184 |
if (!isAppCamActive || !activeLocalVideoStream || isSwitchingCamera) return;
|
185 |
setIsSwitchingCamera(true);
|
186 |
-
const targetFacingMode = currentFacingMode === 'user' ? 'environment' : 'user';
|
187 |
activeLocalVideoStream.getTracks().forEach(track => track.stop());
|
188 |
try {
|
189 |
const newStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { exact: targetFacingMode } }, audio: false });
|
190 |
-
setActiveLocalVideoStream(newStream);
|
|
|
|
|
191 |
} catch (error) {
|
192 |
console.error(`❌ Switch Cam err ${targetFacingMode}:`, error);
|
193 |
try {
|
194 |
const restoredStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: currentFacingMode }, audio: false });
|
195 |
setActiveLocalVideoStream(restoredStream); onVideoStreamChange(restoredStream);
|
|
|
196 |
} catch (restoreError) {
|
197 |
console.error('❌ Restore Cam err:', restoreError);
|
198 |
setActiveLocalVideoStream(null); onVideoStreamChange(null); onAppCamToggle(false);
|
@@ -203,46 +122,16 @@ const ControlTray: React.FC<ControlTrayProps> = ({
|
|
203 |
return (
|
204 |
<footer id="footer-controls" className="footer-controls-html-like">
|
205 |
<canvas style={{ display: "none" }} ref={renderCanvasRef} />
|
206 |
-
|
207 |
-
<div
|
208 |
-
id="mic-button"
|
209 |
-
className="control-button mic-button-color"
|
210 |
-
onClick={handleMicToggle}
|
211 |
-
>
|
212 |
-
{isAppMicActive ? <SvgPauseIcon /> : <ReferenceMicrophoneIcon />}
|
213 |
-
</div>
|
214 |
-
|
215 |
-
{isAppCamActive && ( // نمایش لوگوی کوچک فقط وقتی دوربین فعال است
|
216 |
-
<div id="small-logo-footer-container" className="small-logo-footer-html-like">
|
217 |
-
<LogoAnimation isMini={true} isActive={true} type="human" forFooter={true} />
|
218 |
-
</div>
|
219 |
-
)}
|
220 |
-
|
221 |
-
<div id="cam-button-wrapper" className="control-button-wrapper cam-wrapper-html-like">
|
222 |
-
<div
|
223 |
-
id="cam-button"
|
224 |
-
className="control-button cam-button-color"
|
225 |
-
onClick={handleCamToggle}
|
226 |
-
>
|
227 |
-
{isAppCamActive ? <SvgStopCamIcon /> : <SvgCameraIcon />}
|
228 |
-
</div>
|
229 |
-
<div
|
230 |
-
id="switch-camera-button-container"
|
231 |
-
className={cn("switch-camera-button-container", { visible: isAppCamActive && !isSwitchingCamera })}
|
232 |
-
>
|
233 |
-
<button
|
234 |
-
id="switch-camera-button"
|
235 |
-
aria-label="Switch Camera"
|
236 |
-
className="switch-camera-button-content group"
|
237 |
-
onClick={handleSwitchCamera}
|
238 |
-
disabled={!isAppCamActive || isSwitchingCamera}
|
239 |
-
>
|
240 |
-
<SvgSwitchCameraIcon/>
|
241 |
-
</button>
|
242 |
-
</div>
|
243 |
-
</div>
|
244 |
</footer>
|
245 |
);
|
246 |
};
|
247 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
248 |
export default memo(ControlTray);
|
|
|
1 |
+
// src/components/control-tray/ControlTray.tsx
|
2 |
/**
|
3 |
Copyright 2024 Google LLC
|
4 |
+
... (لایسنس) ...
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
*/
|
6 |
import cn from "classnames";
|
7 |
import React, { memo, ReactNode, RefObject, useEffect, useState } from "react";
|
8 |
import { useLiveAPIContext } from "../../contexts/LiveAPIContext";
|
9 |
import { AudioRecorder } from "../../lib/audio-recorder";
|
10 |
+
import LogoAnimation from '../logo-animation/LogoAnimation';
|
11 |
|
12 |
+
// *** NEW: تعریف نوع FacingMode برای استفاده در پراپها ***
|
13 |
+
export type FacingMode = 'user' | 'environment';
|
14 |
|
15 |
+
// ... (SVG های دکمه ها: SvgPauseIcon, SvgCameraIcon, SvgStopCamIcon, SvgSwitchCameraIcon بدون تغییر) ...
|
16 |
+
const SvgPauseIcon = () => {/* ... */};
|
17 |
+
const SvgCameraIcon = () => {/* ... */};
|
18 |
+
const SvgStopCamIcon = () => {/* ... */};
|
19 |
+
const SvgSwitchCameraIcon = () => {/* ... */};
|
20 |
|
|
|
|
|
|
|
|
|
|
|
21 |
|
22 |
export type ControlTrayProps = {
|
23 |
videoRef: RefObject<HTMLVideoElement>;
|
|
|
28 |
isAppCamActive: boolean;
|
29 |
onAppCamToggle: (active: boolean) => void;
|
30 |
ReferenceMicrophoneIcon: () => JSX.Element;
|
31 |
+
// *** NEW: Callback برای اطلاعرسانی تغییر جهت دوربین به والد ***
|
32 |
+
onFacingModeChange?: (newMode: FacingMode) => void;
|
33 |
};
|
34 |
|
35 |
const ControlTray: React.FC<ControlTrayProps> = ({
|
|
|
41 |
isAppCamActive,
|
42 |
onAppCamToggle,
|
43 |
ReferenceMicrophoneIcon,
|
44 |
+
onFacingModeChange, // *** NEW ***
|
45 |
}) => {
|
46 |
const { client, connected, connect } = useLiveAPIContext();
|
47 |
const [audioRecorder] = useState(() => new AudioRecorder());
|
48 |
const [activeLocalVideoStream, setActiveLocalVideoStream] = useState<MediaStream | null>(null);
|
49 |
+
// *** currentFacingMode همچنان state داخلی است، اما تغییر آن را به والد اطلاع میدهیم ***
|
50 |
+
const [currentFacingMode, setCurrentFacingMode] = useState<FacingMode>('user');
|
51 |
const [isSwitchingCamera, setIsSwitchingCamera] = useState(false);
|
52 |
const renderCanvasRef = React.useRef<HTMLCanvasElement>(null);
|
53 |
|
54 |
+
// ... (useEffect های مربوط به audioRecorder, sendVideoFrame, videoRef.srcObject بدون تغییر) ...
|
55 |
useEffect(() => {
|
56 |
if (isAppCamActive && !activeLocalVideoStream && !isSwitchingCamera) {
|
57 |
+
startWebcam(currentFacingMode); // پاس دادن currentFacingMode اولیه
|
58 |
} else if (!isAppCamActive && activeLocalVideoStream) {
|
59 |
stopWebcam();
|
60 |
}
|
61 |
}, [isAppCamActive, activeLocalVideoStream, isSwitchingCamera]);
|
62 |
|
63 |
+
useEffect(() => { // ... useEffect برای audioRecorder ...
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
}, [connected, client, isAppMicActive, audioRecorder]);
|
65 |
+
useEffect(() => { // ... useEffect برای sendVideoFrame ...
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
66 |
}, [connected, activeLocalVideoStream, client, videoRef, renderCanvasRef]);
|
67 |
+
useEffect(() => { // ... useEffect برای videoRef.srcObject ...
|
68 |
+
}, [activeLocalVideoStream, videoRef]);
|
69 |
+
|
70 |
|
71 |
+
// *** NEW: useEffect برای فراخوانی onFacingModeChange هنگام تغییر currentFacingMode ***
|
72 |
useEffect(() => {
|
73 |
+
if (onFacingModeChange) {
|
74 |
+
onFacingModeChange(currentFacingMode);
|
|
|
|
|
|
|
|
|
|
|
75 |
}
|
76 |
+
}, [currentFacingMode, onFacingModeChange]);
|
77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
78 |
|
79 |
+
const ensureConnectedAndReady = async (): Promise<boolean> => { /* ... */ return false; };
|
80 |
+
const handleMicToggle = async () => { /* ... */ };
|
|
|
|
|
|
|
|
|
|
|
|
|
81 |
|
82 |
+
const startWebcam = async (facingModeToTry: FacingMode = currentFacingMode) => {
|
83 |
if (isSwitchingCamera) return;
|
84 |
setIsSwitchingCamera(true);
|
85 |
try {
|
86 |
const mediaStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: facingModeToTry }, audio: false });
|
87 |
+
setActiveLocalVideoStream(mediaStream);
|
88 |
+
onVideoStreamChange(mediaStream);
|
89 |
+
setCurrentFacingMode(facingModeToTry); // بروزرسانی state داخلی
|
90 |
} catch (error) {
|
91 |
console.error(`❌ Start WC err ${facingModeToTry}:`, error);
|
92 |
setActiveLocalVideoStream(null); onVideoStreamChange(null); onAppCamToggle(false);
|
93 |
} finally { setIsSwitchingCamera(false); }
|
94 |
};
|
95 |
|
96 |
+
const stopWebcam = () => { /* ... */ };
|
97 |
+
const handleCamToggle = async () => { /* ... */ };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
98 |
|
99 |
const handleSwitchCamera = async () => {
|
100 |
if (!isAppCamActive || !activeLocalVideoStream || isSwitchingCamera) return;
|
101 |
setIsSwitchingCamera(true);
|
102 |
+
const targetFacingMode: FacingMode = currentFacingMode === 'user' ? 'environment' : 'user';
|
103 |
activeLocalVideoStream.getTracks().forEach(track => track.stop());
|
104 |
try {
|
105 |
const newStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { exact: targetFacingMode } }, audio: false });
|
106 |
+
setActiveLocalVideoStream(newStream);
|
107 |
+
onVideoStreamChange(newStream);
|
108 |
+
setCurrentFacingMode(targetFacingMode); // بروزرسانی state داخلی
|
109 |
} catch (error) {
|
110 |
console.error(`❌ Switch Cam err ${targetFacingMode}:`, error);
|
111 |
try {
|
112 |
const restoredStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: currentFacingMode }, audio: false });
|
113 |
setActiveLocalVideoStream(restoredStream); onVideoStreamChange(restoredStream);
|
114 |
+
// setCurrentFacingMode(currentFacingMode); // جهت تغییر نکرده
|
115 |
} catch (restoreError) {
|
116 |
console.error('❌ Restore Cam err:', restoreError);
|
117 |
setActiveLocalVideoStream(null); onVideoStreamChange(null); onAppCamToggle(false);
|
|
|
122 |
return (
|
123 |
<footer id="footer-controls" className="footer-controls-html-like">
|
124 |
<canvas style={{ display: "none" }} ref={renderCanvasRef} />
|
125 |
+
{/* ... (دکمه میکروفون، لوگوی کوچک، دکمه دوربین مثل قبل) ... */}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
126 |
</footer>
|
127 |
);
|
128 |
};
|
129 |
|
130 |
+
// ... (بقیه کد ControlTray مثل قبل، شامل دکمهها و نمایش لوگو) ...
|
131 |
+
// در JSX نهایی ControlTray:
|
132 |
+
// <div id="mic-button" ...> ... </div>
|
133 |
+
// {isAppCamActive && <div id="small-logo-footer-container" ...> <LogoAnimation ... /> </div>}
|
134 |
+
// <div id="cam-button-wrapper" ...> ... <div id="switch-camera-button-container" ...> ... </div> </div>
|
135 |
+
|
136 |
+
|
137 |
export default memo(ControlTray);
|