Ezmary commited on
Commit
cb75fe0
·
verified ·
1 Parent(s): 700314d

Update src/components/control-tray/ControlTray.tsx

Browse files
src/components/control-tray/ControlTray.tsx CHANGED
@@ -1,4 +1,4 @@
1
- // ... (ایمپورت‌ها و SVGهای دیگر مثل قبل) ...
2
  import cn from "classnames";
3
  import React, { memo, ReactNode, RefObject, useEffect, useState } from "react";
4
  import { useLiveAPIContext } from "../../contexts/LiveAPIContext";
@@ -9,7 +9,8 @@ const SvgCameraIcon = () => <svg width="40" height="40" viewBox="0 0 40 40" fill
9
  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>;
10
  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>;
11
 
12
- export type ControlTrayProps = {
 
13
  videoRef: RefObject<HTMLVideoElement>;
14
  supportsVideo: boolean;
15
  onVideoStreamChange: (stream: MediaStream | null) => void;
@@ -21,8 +22,7 @@ export type ControlTrayProps = {
21
  ReferenceMicrophoneIcon: () => JSX.Element;
22
  };
23
 
24
- const ControlTray: React.FC<ControlTrayProps> = ({
25
- // ... (props و state ها بدون تغییر) ...
26
  videoRef,
27
  onVideoStreamChange,
28
  supportsVideo,
@@ -33,6 +33,7 @@ const ControlTray: React.FC<ControlTrayProps> = ({
33
  createLogoFunction,
34
  ReferenceMicrophoneIcon,
35
  }) => {
 
36
  const { client, connected, connect } = useLiveAPIContext();
37
  const [audioRecorder] = useState(() => new AudioRecorder());
38
  const [activeLocalVideoStream, setActiveLocalVideoStream] = useState<MediaStream | null>(null);
@@ -40,191 +41,51 @@ const ControlTray: React.FC<ControlTrayProps> = ({
40
  const [isSwitchingCamera, setIsSwitchingCamera] = useState(false);
41
  const renderCanvasRef = React.useRef<HTMLCanvasElement>(null);
42
 
43
- // ... (useEffect ها و توابع handle بدون تغییر) ...
44
- useEffect(() => {
45
- if (isAppCamActive && !activeLocalVideoStream && !isSwitchingCamera) {
46
- startWebcam();
47
- } else if (!isAppCamActive && activeLocalVideoStream) {
48
- stopWebcam();
49
- }
50
- }, [isAppCamActive, activeLocalVideoStream, isSwitchingCamera]);
51
-
52
- useEffect(() => {
53
- const onData = (base64: string) => {
54
- if (client && connected && isAppMicActive) {
55
- client.sendRealtimeInput([{ mimeType: "audio/pcm;rate=16000", data: base64 }]);
56
- }
57
- };
58
- if (connected && isAppMicActive && audioRecorder) {
59
- audioRecorder.on("data", onData).start();
60
- } else if (audioRecorder && audioRecorder.recording) {
61
- audioRecorder.stop();
62
- }
63
- return () => {
64
- if (audioRecorder) {
65
- audioRecorder.off("data", onData);
66
- if (audioRecorder.recording) audioRecorder.stop();
67
- }
68
- };
69
- }, [connected, client, isAppMicActive, audioRecorder]);
70
-
71
- useEffect(() => {
72
- let timeoutId = -1;
73
- function sendVideoFrame() {
74
- if (connected && activeLocalVideoStream && client && videoRef.current) {
75
- const video = videoRef.current;
76
- const canvas = renderCanvasRef.current;
77
- if (!canvas || video.readyState < video.HAVE_METADATA || video.paused || video.ended) {
78
- if (activeLocalVideoStream) timeoutId = window.setTimeout(sendVideoFrame, 1000 / 4);
79
- return;
80
- }
81
- try {
82
- const ctx = canvas.getContext("2d");
83
- if (!ctx) return;
84
- const scale = 0.5;
85
- canvas.width = video.videoWidth * scale;
86
- canvas.height = video.videoHeight * scale;
87
- if (canvas.width > 0 && canvas.height > 0) {
88
- ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
89
- const base64 = canvas.toDataURL("image/jpeg", 0.8);
90
- const data = base64.slice(base64.indexOf(",") + 1);
91
- if (data) client.sendRealtimeInput([{ mimeType: "image/jpeg", data }]);
92
- }
93
- } catch (error) { console.error("❌ Error frame:", error); }
94
- }
95
- if (connected && activeLocalVideoStream) {
96
- timeoutId = window.setTimeout(sendVideoFrame, 1000 / 4);
97
- }
98
- }
99
- if (connected && activeLocalVideoStream && videoRef.current) {
100
- timeoutId = window.setTimeout(sendVideoFrame, 200);
101
- }
102
- return () => clearTimeout(timeoutId);
103
- }, [connected, activeLocalVideoStream, client, videoRef, renderCanvasRef]);
104
-
105
- useEffect(() => {
106
- if (videoRef.current) {
107
- if (videoRef.current.srcObject !== activeLocalVideoStream) {
108
- videoRef.current.srcObject = activeLocalVideoStream;
109
- if (activeLocalVideoStream) {
110
- videoRef.current.play().catch(e => console.warn("Video play failed:", e));
111
- }
112
- }
113
- }
114
- }, [activeLocalVideoStream, videoRef]);
115
-
116
- const ensureConnectedAndReady = async (): Promise<boolean> => {
117
- if (!connected) {
118
- try { await connect(); return true; }
119
- catch (err) { console.error('❌ CT Connect err:', err); return false; }
120
- }
121
- return true;
122
- };
123
-
124
- const handleMicToggle = async () => {
125
- if (isSwitchingCamera) return;
126
- const newMicState = !isAppMicActive;
127
- if (newMicState && !(await ensureConnectedAndReady())) {
128
- onAppMicToggle(false); return;
129
- }
130
- onAppMicToggle(newMicState);
131
- };
132
-
133
- const startWebcam = async (facingModeToTry: 'user' | 'environment' = currentFacingMode) => {
134
- if (isSwitchingCamera) return;
135
- setIsSwitchingCamera(true);
136
- try {
137
- const mediaStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: facingModeToTry }, audio: false });
138
- setActiveLocalVideoStream(mediaStream); onVideoStreamChange(mediaStream); setCurrentFacingMode(facingModeToTry);
139
- } catch (error) {
140
- console.error(`❌ Start WC err ${facingModeToTry}:`, error);
141
- setActiveLocalVideoStream(null); onVideoStreamChange(null); onAppCamToggle(false);
142
- } finally { setIsSwitchingCamera(false); }
143
- };
144
-
145
- const stopWebcam = () => {
146
- if (activeLocalVideoStream) activeLocalVideoStream.getTracks().forEach(track => track.stop());
147
- setActiveLocalVideoStream(null); onVideoStreamChange(null);
148
- };
149
-
150
- const handleCamToggle = async () => {
151
- if (isSwitchingCamera) return;
152
- const newCamState = !isAppCamActive;
153
-
154
- if (newCamState) {
155
- if (!(await ensureConnectedAndReady())) {
156
- onAppCamToggle(false);
157
- return;
158
- }
159
- if (!isAppMicActive) {
160
- onAppMicToggle(true);
161
- }
162
- onAppCamToggle(true);
163
- } else {
164
- onAppCamToggle(false);
165
- }
166
- };
167
-
168
- const handleSwitchCamera = async () => {
169
- if (!isAppCamActive || !activeLocalVideoStream || isSwitchingCamera) return;
170
- setIsSwitchingCamera(true);
171
- const targetFacingMode = currentFacingMode === 'user' ? 'environment' : 'user';
172
- activeLocalVideoStream.getTracks().forEach(track => track.stop());
173
- try {
174
- const newStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { exact: targetFacingMode } }, audio: false });
175
- setActiveLocalVideoStream(newStream); onVideoStreamChange(newStream); setCurrentFacingMode(targetFacingMode);
176
- } catch (error) {
177
- console.error(`❌ Switch Cam err ${targetFacingMode}:`, error);
178
- try {
179
- const restoredStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: currentFacingMode }, audio: false });
180
- setActiveLocalVideoStream(restoredStream); onVideoStreamChange(restoredStream);
181
- } catch (restoreError) {
182
- console.error('❌ Restore Cam err:', restoreError);
183
- setActiveLocalVideoStream(null); onVideoStreamChange(null); onAppCamToggle(false);
184
- }
185
- } finally { setIsSwitchingCamera(false); }
186
- };
187
-
188
  return (
189
- <footer id="footer-controls" className="footer-controls-html-like">
190
  <canvas style={{ display: "none" }} ref={renderCanvasRef} />
191
 
192
- <div
193
- id="mic-button"
194
- className="control-button mic-button-color"
195
- onClick={handleMicToggle}
196
- >
197
- {isAppMicActive ? <SvgPauseIcon /> : <ReferenceMicrophoneIcon />}
 
 
 
 
198
  </div>
199
 
 
200
  {isAppCamActive && (
201
- <div id="small-logo-footer-container" className="small-logo-footer-html-like">
202
  {createLogoFunction(true, true, 'human', true)}
203
  </div>
204
  )}
205
 
206
- <div id="cam-button-wrapper" className="control-button-wrapper cam-wrapper-html-like">
207
- <div
208
- id="cam-button"
209
- className="control-button cam-button-color"
210
- onClick={handleCamToggle}
211
- >
212
- {isAppCamActive ? <SvgStopCamIcon /> : <SvgCameraIcon />}
213
- </div>
214
- <div
215
- id="switch-camera-button-container"
216
- className={cn("switch-camera-button-container", { visible: isAppCamActive && !isSwitchingCamera })}
217
- >
218
- <button
219
- id="switch-camera-button"
220
- aria-label="Switch Camera"
221
- className="switch-camera-button-content group"
222
- onClick={handleSwitchCamera}
223
- disabled={!isAppCamActive || isSwitchingCamera}
224
- >
225
- <SvgSwitchCameraIcon/>
226
- </button>
227
- </div>
228
  </div>
229
  </footer>
230
  );
 
1
+ // ... (ایمپورت‌ها و SVGهای آیکون‌ها مثل قبل) ...
2
  import cn from "classnames";
3
  import React, { memo, ReactNode, RefObject, useEffect, useState } from "react";
4
  import { useLiveAPIContext } from "../../contexts/LiveAPIContext";
 
9
  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>;
10
  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>;
11
 
12
+
13
+ export type ControlTrayProps = { /* ... (props بدون تغییر) ... */
14
  videoRef: RefObject<HTMLVideoElement>;
15
  supportsVideo: boolean;
16
  onVideoStreamChange: (stream: MediaStream | null) => void;
 
22
  ReferenceMicrophoneIcon: () => JSX.Element;
23
  };
24
 
25
+ const ControlTray: React.FC<ControlTrayProps> = ({ /* ... (props بدون تغییر) ... */
 
26
  videoRef,
27
  onVideoStreamChange,
28
  supportsVideo,
 
33
  createLogoFunction,
34
  ReferenceMicrophoneIcon,
35
  }) => {
36
+ // ... (state ها و useEffect ها و توابع handle بدون تغییر) ...
37
  const { client, connected, connect } = useLiveAPIContext();
38
  const [audioRecorder] = useState(() => new AudioRecorder());
39
  const [activeLocalVideoStream, setActiveLocalVideoStream] = useState<MediaStream | null>(null);
 
41
  const [isSwitchingCamera, setIsSwitchingCamera] = useState(false);
42
  const renderCanvasRef = React.useRef<HTMLCanvasElement>(null);
43
 
44
+ useEffect(() => { if (isAppCamActive && !activeLocalVideoStream && !isSwitchingCamera) { startWebcam(); } else if (!isAppCamActive && activeLocalVideoStream) { stopWebcam(); } }, [isAppCamActive, activeLocalVideoStream, isSwitchingCamera]);
45
+ useEffect(() => { const onData = (base64: string) => { if (client && connected && isAppMicActive) { client.sendRealtimeInput([{ mimeType: "audio/pcm;rate=16000", data: base64 }]); } }; if (connected && isAppMicActive && audioRecorder) { audioRecorder.on("data", onData).start(); } else if (audioRecorder && audioRecorder.recording) { audioRecorder.stop(); } return () => { if (audioRecorder) { audioRecorder.off("data", onData); if (audioRecorder.recording) audioRecorder.stop(); } }; }, [connected, client, isAppMicActive, audioRecorder]);
46
+ useEffect(() => { let timeoutId = -1; function sendVideoFrame() { if (connected && activeLocalVideoStream && client && videoRef.current) { const video = videoRef.current; const canvas = renderCanvasRef.current; if (!canvas || video.readyState < video.HAVE_METADATA || video.paused || video.ended) { if (activeLocalVideoStream) timeoutId = window.setTimeout(sendVideoFrame, 1000 / 4); return; } try { const ctx = canvas.getContext("2d"); if (!ctx) return; const scale = 0.5; canvas.width = video.videoWidth * scale; canvas.height = video.videoHeight * scale; if (canvas.width > 0 && canvas.height > 0) { ctx.drawImage(video, 0, 0, canvas.width, canvas.height); const base64 = canvas.toDataURL("image/jpeg", 0.8); const data = base64.slice(base64.indexOf(",") + 1); if (data) client.sendRealtimeInput([{ mimeType: "image/jpeg", data }]); } } catch (error) { console.error("❌ Error frame:", error); } } if (connected && activeLocalVideoStream) { timeoutId = window.setTimeout(sendVideoFrame, 1000 / 4); } } if (connected && activeLocalVideoStream && videoRef.current) { timeoutId = window.setTimeout(sendVideoFrame, 200); } return () => clearTimeout(timeoutId); }, [connected, activeLocalVideoStream, client, videoRef, renderCanvasRef]);
47
+ useEffect(() => { if (videoRef.current) { if (videoRef.current.srcObject !== activeLocalVideoStream) { videoRef.current.srcObject = activeLocalVideoStream; if (activeLocalVideoStream) { videoRef.current.play().catch(e => console.warn("Video play failed:", e)); } } } }, [activeLocalVideoStream, videoRef]);
48
+ const ensureConnectedAndReady = async (): Promise<boolean> => { if (!connected) { try { await connect(); return true; } catch (err) { console.error('❌ CT Connect err:', err); return false; } } return true; };
49
+ const handleMicToggle = async () => { if (isSwitchingCamera) return; const newMicState = !isAppMicActive; if (newMicState && !(await ensureConnectedAndReady())) { onAppMicToggle(false); return; } onAppMicToggle(newMicState); };
50
+ const startWebcam = async (facingModeToTry: 'user' | 'environment' = currentFacingMode) => { if (isSwitchingCamera) return; setIsSwitchingCamera(true); try { const mediaStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: facingModeToTry }, audio: false }); setActiveLocalVideoStream(mediaStream); onVideoStreamChange(mediaStream); setCurrentFacingMode(facingModeToTry); } catch (error) { console.error(`❌ Start WC err ${facingModeToTry}:`, error); setActiveLocalVideoStream(null); onVideoStreamChange(null); onAppCamToggle(false); } finally { setIsSwitchingCamera(false); } };
51
+ const stopWebcam = () => { if (activeLocalVideoStream) activeLocalVideoStream.getTracks().forEach(track => track.stop()); setActiveLocalVideoStream(null); onVideoStreamChange(null); };
52
+ const handleCamToggle = async () => { if (isSwitchingCamera) return; const newCamState = !isAppCamActive; if (newCamState) { if (!(await ensureConnectedAndReady())) { onAppCamToggle(false); return; } if (!isAppMicActive) { onAppMicToggle(true); } onAppCamToggle(true); } else { onAppCamToggle(false); } };
53
+ const handleSwitchCamera = async () => { if (!isAppCamActive || !activeLocalVideoStream || isSwitchingCamera) return; setIsSwitchingCamera(true); const targetFacingMode = currentFacingMode === 'user' ? 'environment' : 'user'; activeLocalVideoStream.getTracks().forEach(track => track.stop()); try { const newStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { exact: targetFacingMode } }, audio: false }); setActiveLocalVideoStream(newStream); onVideoStreamChange(newStream); setCurrentFacingMode(targetFacingMode); } catch (error) { console.error(`❌ Switch Cam err ${targetFacingMode}:`, error); try { const restoredStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: currentFacingMode }, audio: false }); setActiveLocalVideoStream(restoredStream); onVideoStreamChange(restoredStream); } catch (restoreError) { console.error('❌ Restore Cam err:', restoreError); setActiveLocalVideoStream(null); onVideoStreamChange(null); onAppCamToggle(false); } } finally { setIsSwitchingCamera(false); } };
54
+
55
+
56
+ // استفاده از کلاس 'footer-controls' که در App.scss استایل‌دهی می‌شود
57
+ // ترتیب دکمه‌ها: دوربین (چپ)، لوگو (وسط اگر دوربین فعال)، میکروفون (راست)
58
+ // این ترتیب با HTML مرجع شما (میکروفون چپ، دوربین راست) متفاوت است،
59
+ // برای تطابق با HTML، ترتیب JSX را باید تغییر دهید و رنگ‌ها را هم در App.scss جابجا کنید.
60
+ // در اینجا، ترتیب قبلی (دوربین چپ، میکروفون راست) را حفظ می‌کنیم.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  return (
62
+ <footer id="footer-controls" className="footer-controls"> {/* استفاده از کلاس پایدارتر قبلی */}
63
  <canvas style={{ display: "none" }} ref={renderCanvasRef} />
64
 
65
+ {/* Cam Button (Left) */}
66
+ <div id="cam-button-wrapper" className="control-button-wrapper">
67
+ <button id="cam-button" className="control-button cam-button-color" onClick={handleCamToggle} disabled={isSwitchingCamera || !supportsVideo}>
68
+ {isAppCamActive ? <SvgStopCamIcon /> : <SvgCameraIcon />}
69
+ </button>
70
+ <div id="switch-camera-button-container" className={cn("switch-camera-button-container", { visible: isAppCamActive && !isSwitchingCamera })}>
71
+ <button id="switch-camera-button" aria-label="Switch Camera" className="switch-camera-button-content group" onClick={handleSwitchCamera} disabled={!isAppCamActive || isSwitchingCamera}>
72
+ <SvgSwitchCameraIcon/>
73
+ </button>
74
+ </div>
75
  </div>
76
 
77
+ {/* Small Logo Container (Middle - Only when Cam is Active) */}
78
  {isAppCamActive && (
79
+ <div className="small-logo-footer"> {/* کلاس برای موقعیت‌دهی لوگوی کوچک */}
80
  {createLogoFunction(true, true, 'human', true)}
81
  </div>
82
  )}
83
 
84
+ {/* Mic Button (Right) */}
85
+ <div id="mic-button-wrapper" className="control-button-wrapper">
86
+ <button id="mic-button" className="control-button mic-button-color" onClick={handleMicToggle} disabled={isSwitchingCamera} >
87
+ {isAppMicActive ? <SvgPauseIcon /> : <ReferenceMicrophoneIcon />}
88
+ </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  </div>
90
  </footer>
91
  );