Hamed744 commited on
Commit
38d6fd2
·
verified ·
1 Parent(s): 55b95aa

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

Browse files
src/components/control-tray/ControlTray.tsx CHANGED
@@ -1,4 +1,4 @@
1
- // src/components/control-tray/ControlTray.tsx (نسخه نهایی با اصلاح جایگاه دکمه تعویض دوربین)
2
 
3
  import cn from "classnames";
4
  import React, { memo, RefObject, useEffect, useState, useCallback, useRef } from "react";
@@ -18,9 +18,11 @@ export type ControlTrayProps = {
18
  onAppCamToggle: (active: boolean) => void;
19
  currentFacingMode: 'user' | 'environment';
20
  onFacingModeChange: (mode: 'user' | 'environment') => void;
 
 
21
  };
22
 
23
- const ControlTray: React.FC<ControlTrayProps> = ({ videoRef, onUserSpeakingChange, isAppMicActive, onAppMicToggle, isAppCamActive, onAppCamToggle, currentFacingMode, onFacingModeChange }) => {
24
  const { client, connected, connect, volume } = useAppContext();
25
  const audioRecorderRef = useRef<AudioRecorder | null>(null);
26
  const [activeLocalVideoStream, setActiveLocalVideoStream] = useState<MediaStream | null>(null);
@@ -29,9 +31,7 @@ const ControlTray: React.FC<ControlTrayProps> = ({ videoRef, onUserSpeakingChang
29
  const [userVolume, setUserVolume] = useState(0);
30
 
31
  useEffect(() => {
32
- if (!audioRecorderRef.current) {
33
- audioRecorderRef.current = new AudioRecorder();
34
- }
35
  const audioRecorder = audioRecorderRef.current;
36
  const handleAudioData = (base64: string, vol: number) => {
37
  if (client && connected && isAppMicActive) {
@@ -42,25 +42,26 @@ const ControlTray: React.FC<ControlTrayProps> = ({ videoRef, onUserSpeakingChang
42
  };
43
  const onStop = () => { setUserVolume(0); onUserSpeakingChange(false); };
44
  if (isAppMicActive && connected) {
45
- audioRecorder.on("data", handleAudioData).on("stop", onStop);
 
46
  if (!audioRecorder.recording) audioRecorder.start();
47
- } else {
48
- if (audioRecorder?.recording) audioRecorder.stop();
49
- }
50
- return () => { audioRecorder?.off("data", handleAudioData).off("stop", onStop); };
51
  }, [isAppMicActive, connected, onUserSpeakingChange, client]);
52
 
53
  useEffect(() => {
54
- if (videoRef.current && videoRef.current.srcObject !== activeLocalVideoStream) {
55
- videoRef.current.srcObject = activeLocalVideoStream;
56
- if (activeLocalVideoStream) videoRef.current.play().catch(e => console.warn("Video play failed:", e));
 
 
57
  }
58
  }, [activeLocalVideoStream, videoRef]);
59
 
60
  const startWebcam = useCallback(async (facingModeToTry: 'user' | 'environment') => {
61
  if (isSwitchingCamera) return;
62
  setIsSwitchingCamera(true);
63
- activeLocalVideoStream?.getTracks().forEach(track => track.stop());
64
  try {
65
  const mediaStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: facingModeToTry }, audio: false });
66
  setActiveLocalVideoStream(mediaStream);
@@ -69,9 +70,7 @@ const ControlTray: React.FC<ControlTrayProps> = ({ videoRef, onUserSpeakingChang
69
  console.error(`❌ Start WC err ${facingModeToTry}:`, error);
70
  setActiveLocalVideoStream(null);
71
  onAppCamToggle(false);
72
- } finally {
73
- setIsSwitchingCamera(false);
74
- }
75
  }, [isSwitchingCamera, activeLocalVideoStream, onFacingModeChange, onAppCamToggle]);
76
 
77
  useEffect(() => {
@@ -84,62 +83,65 @@ const ControlTray: React.FC<ControlTrayProps> = ({ videoRef, onUserSpeakingChang
84
  }, [isAppCamActive, activeLocalVideoStream, isSwitchingCamera, startWebcam, currentFacingMode]);
85
 
86
  useEffect(() => {
87
- let timeoutId: number;
88
  function sendVideoFrame() {
89
- if (connected && activeLocalVideoStream) {
90
  const video = videoRef.current;
91
  const canvas = renderCanvasRef.current;
92
- if (video && canvas && video.readyState >= video.HAVE_METADATA && !video.paused) {
93
- try {
94
- const ctx = canvas.getContext("2d");
95
- if (ctx) {
96
- const scale = 0.5;
97
- canvas.width = video.videoWidth * scale;
98
- canvas.height = video.videoHeight * scale;
99
- if (canvas.width > 0 && canvas.height > 0) {
100
- ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
101
- const base64 = canvas.toDataURL("image/jpeg", 0.8);
102
- const data = base64.slice(base64.indexOf(",") + 1);
103
- if (data && client) client.sendRealtimeInput([{ mimeType: "image/jpeg", data }]);
104
- }
105
- }
106
- } catch (error) { console.error("❌ Error frame:", error); }
107
  }
108
- timeoutId = window.setTimeout(sendVideoFrame, 1000 / 4);
 
 
 
 
 
 
 
 
 
 
 
 
109
  }
 
110
  }
111
- if (connected && activeLocalVideoStream) timeoutId = window.setTimeout(sendVideoFrame, 200);
112
  return () => clearTimeout(timeoutId);
113
  }, [connected, activeLocalVideoStream, client, videoRef, renderCanvasRef]);
114
 
115
  const ensureConnectedAndReady = async (): Promise<boolean> => {
 
116
  if (!connected) {
117
- try { await connect(); return true; } catch (err) { console.error('❌ CT Connect err:', err); return false; }
 
118
  }
119
  return true;
120
  };
121
 
122
  const handleMicToggle = async () => {
123
- if (isSwitchingCamera) return;
124
  const newMicState = !isAppMicActive;
125
- if (newMicState && !(await ensureConnectedAndReady())) { onAppMicToggle(false); return; }
 
 
126
  onAppMicToggle(newMicState);
127
  };
128
 
129
  const handleCamToggle = async () => {
130
- if (isSwitchingCamera) return;
131
  const newCamState = !isAppCamActive;
132
  if (newCamState) {
133
  if (!(await ensureConnectedAndReady())) { onAppCamToggle(false); return; }
134
  if (!isAppMicActive) onAppMicToggle(true);
135
  onAppCamToggle(true);
136
- } else {
137
- onAppCamToggle(false);
138
- }
139
  };
140
 
141
  const handleSwitchCamera = async () => {
142
- if (!isAppCamActive || !activeLocalVideoStream || isSwitchingCamera) return;
143
  setIsSwitchingCamera(true);
144
  const targetFacingMode = currentFacingMode === 'user' ? 'environment' : 'user';
145
  activeLocalVideoStream.getTracks().forEach(track => track.stop());
@@ -151,50 +153,38 @@ const ControlTray: React.FC<ControlTrayProps> = ({ videoRef, onUserSpeakingChang
151
  } catch (error) {
152
  console.error(`❌ Switch Cam err to ${targetFacingMode}:`, error);
153
  try {
154
- const restoredStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { exact: currentFacingMode } }, audio: false });
155
  setActiveLocalVideoStream(restoredStream);
156
  } catch (restoreError) {
157
  console.error(`❌ Restore Cam err to ${currentFacingMode}:`, restoreError);
158
  setActiveLocalVideoStream(null);
159
  onAppCamToggle(false);
160
  }
161
- } finally {
162
- setIsSwitchingCamera(false);
163
- }
164
  };
165
 
166
  const isSpeaking = userVolume > 0.01;
167
 
168
  return (
169
- // ✅ ساختار JSX به حالت صحیح اولیه برگردانده شد
170
  <footer id="footer-controls" className="footer-controls-html-like">
171
  <canvas style={{ display: "none" }} ref={renderCanvasRef} />
172
-
173
- {/* دکمه میکروفون در سمت چپ */}
174
- <div id="mic-button" className="control-button mic-button-color" onClick={handleMicToggle}>
175
  {isAppMicActive ? <PauseIconWithSurroundPulse userVolume={userVolume} /> : microphoneIcon}
176
  </div>
177
 
178
- {/* لوگو در وسط (فقط وقتی دوربین فعال است) */}
179
  {isAppCamActive && (
180
  <div id="small-logo-footer-container" className="small-logo-footer-html-like">
181
- <Logo
182
- isMini={true}
183
- isActive={true}
184
- isAi={false}
185
- speakingVolume={volume}
186
- isUserSpeaking={isSpeaking}
187
- />
188
  </div>
189
  )}
190
 
191
- {/* نگهدارنده دکمه دوربین و تعویض دوربین در سمت راست */}
192
- <div className="control-button-wrapper">
193
- <div id="cam-button" className="control-button cam-button-color" onClick={handleCamToggle}>
194
  {isAppCamActive ? stopCamIcon : cameraIcon}
195
  </div>
196
  <div id="switch-camera-button-container" className={cn("switch-camera-button-container", { visible: isAppCamActive && !isSwitchingCamera })}>
197
- <button id="switch-camera-button" aria-label="Switch Camera" className="switch-camera-button-content group" onClick={handleSwitchCamera} disabled={!isAppCamActive || isSwitchingCamera}>
198
  <SvgSwitchCameraIcon/>
199
  </button>
200
  </div>
 
1
+ // src/components/control-tray/ControlTray.tsx (نسخه نهایی با قابلیت غیرفعال شدن)
2
 
3
  import cn from "classnames";
4
  import React, { memo, RefObject, useEffect, useState, useCallback, useRef } from "react";
 
18
  onAppCamToggle: (active: boolean) => void;
19
  currentFacingMode: 'user' | 'environment';
20
  onFacingModeChange: (mode: 'user' | 'environment') => void;
21
+ // ✅ پراپرتی جدید برای غیرفعال کردن کنترل‌ها
22
+ isTimeUp: boolean;
23
  };
24
 
25
+ const ControlTray: React.FC<ControlTrayProps> = ({ videoRef, onUserSpeakingChange, isAppMicActive, onAppMicToggle, isAppCamActive, onAppCamToggle, currentFacingMode, onFacingModeChange, isTimeUp }) => {
26
  const { client, connected, connect, volume } = useAppContext();
27
  const audioRecorderRef = useRef<AudioRecorder | null>(null);
28
  const [activeLocalVideoStream, setActiveLocalVideoStream] = useState<MediaStream | null>(null);
 
31
  const [userVolume, setUserVolume] = useState(0);
32
 
33
  useEffect(() => {
34
+ if (!audioRecorderRef.current) audioRecorderRef.current = new AudioRecorder();
 
 
35
  const audioRecorder = audioRecorderRef.current;
36
  const handleAudioData = (base64: string, vol: number) => {
37
  if (client && connected && isAppMicActive) {
 
42
  };
43
  const onStop = () => { setUserVolume(0); onUserSpeakingChange(false); };
44
  if (isAppMicActive && connected) {
45
+ audioRecorder.on("data", handleAudioData);
46
+ audioRecorder.on("stop", onStop);
47
  if (!audioRecorder.recording) audioRecorder.start();
48
+ } else if (audioRecorder?.recording) { audioRecorder.stop(); }
49
+ return () => { if (audioRecorder) { audioRecorder.off("data", handleAudioData); audioRecorder.off("stop", onStop); } };
 
 
50
  }, [isAppMicActive, connected, onUserSpeakingChange, client]);
51
 
52
  useEffect(() => {
53
+ if (videoRef.current) {
54
+ if (videoRef.current.srcObject !== activeLocalVideoStream) {
55
+ videoRef.current.srcObject = activeLocalVideoStream;
56
+ if (activeLocalVideoStream) videoRef.current.play().catch(e => console.warn("Video play failed:", e));
57
+ }
58
  }
59
  }, [activeLocalVideoStream, videoRef]);
60
 
61
  const startWebcam = useCallback(async (facingModeToTry: 'user' | 'environment') => {
62
  if (isSwitchingCamera) return;
63
  setIsSwitchingCamera(true);
64
+ if (activeLocalVideoStream) activeLocalVideoStream.getTracks().forEach(track => track.stop());
65
  try {
66
  const mediaStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: facingModeToTry }, audio: false });
67
  setActiveLocalVideoStream(mediaStream);
 
70
  console.error(`❌ Start WC err ${facingModeToTry}:`, error);
71
  setActiveLocalVideoStream(null);
72
  onAppCamToggle(false);
73
+ } finally { setIsSwitchingCamera(false); }
 
 
74
  }, [isSwitchingCamera, activeLocalVideoStream, onFacingModeChange, onAppCamToggle]);
75
 
76
  useEffect(() => {
 
83
  }, [isAppCamActive, activeLocalVideoStream, isSwitchingCamera, startWebcam, currentFacingMode]);
84
 
85
  useEffect(() => {
86
+ let timeoutId = -1;
87
  function sendVideoFrame() {
88
+ if (connected && activeLocalVideoStream && client && videoRef.current) {
89
  const video = videoRef.current;
90
  const canvas = renderCanvasRef.current;
91
+ if (!canvas || video.readyState < video.HAVE_METADATA || video.paused || video.ended) {
92
+ if (activeLocalVideoStream) timeoutId = window.setTimeout(sendVideoFrame, 1000 / 4);
93
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
94
  }
95
+ try {
96
+ const ctx = canvas.getContext("2d");
97
+ if (!ctx) return;
98
+ const scale = 0.5;
99
+ canvas.width = video.videoWidth * scale;
100
+ canvas.height = video.videoHeight * scale;
101
+ if (canvas.width > 0 && canvas.height > 0) {
102
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
103
+ const base64 = canvas.toDataURL("image/jpeg", 0.8);
104
+ const data = base64.slice(base64.indexOf(",") + 1);
105
+ if (data) client.sendRealtimeInput([{ mimeType: "image/jpeg", data }]);
106
+ }
107
+ } catch (error) { console.error("❌ Error frame:", error); }
108
  }
109
+ if (connected && activeLocalVideoStream) timeoutId = window.setTimeout(sendVideoFrame, 1000 / 4);
110
  }
111
+ if (connected && activeLocalVideoStream && videoRef.current) timeoutId = window.setTimeout(sendVideoFrame, 200);
112
  return () => clearTimeout(timeoutId);
113
  }, [connected, activeLocalVideoStream, client, videoRef, renderCanvasRef]);
114
 
115
  const ensureConnectedAndReady = async (): Promise<boolean> => {
116
+ if (isTimeUp) return false; // اگر زمان تمام شده، اجازه اتصال نده
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 || isTimeUp) return;
126
  const newMicState = !isAppMicActive;
127
+ if (newMicState && !(await ensureConnectedAndReady())) {
128
+ onAppMicToggle(false); return;
129
+ }
130
  onAppMicToggle(newMicState);
131
  };
132
 
133
  const handleCamToggle = async () => {
134
+ if (isSwitchingCamera || isTimeUp) return;
135
  const newCamState = !isAppCamActive;
136
  if (newCamState) {
137
  if (!(await ensureConnectedAndReady())) { onAppCamToggle(false); return; }
138
  if (!isAppMicActive) onAppMicToggle(true);
139
  onAppCamToggle(true);
140
+ } else { onAppCamToggle(false); }
 
 
141
  };
142
 
143
  const handleSwitchCamera = async () => {
144
+ if (!isAppCamActive || !activeLocalVideoStream || isSwitchingCamera || isTimeUp) return;
145
  setIsSwitchingCamera(true);
146
  const targetFacingMode = currentFacingMode === 'user' ? 'environment' : 'user';
147
  activeLocalVideoStream.getTracks().forEach(track => track.stop());
 
153
  } catch (error) {
154
  console.error(`❌ Switch Cam err to ${targetFacingMode}:`, error);
155
  try {
156
+ const restoredStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: {exact: currentFacingMode } }, audio: false });
157
  setActiveLocalVideoStream(restoredStream);
158
  } catch (restoreError) {
159
  console.error(`❌ Restore Cam err to ${currentFacingMode}:`, restoreError);
160
  setActiveLocalVideoStream(null);
161
  onAppCamToggle(false);
162
  }
163
+ } finally { setIsSwitchingCamera(false); }
 
 
164
  };
165
 
166
  const isSpeaking = userVolume > 0.01;
167
 
168
  return (
 
169
  <footer id="footer-controls" className="footer-controls-html-like">
170
  <canvas style={{ display: "none" }} ref={renderCanvasRef} />
171
+ {/* ✅ غیرفعال کردن دکمه‌ها وقتی زمان تمام شده */}
172
+ <div id="mic-button" className={cn("control-button mic-button-color", { "disabled": isTimeUp })} onClick={handleMicToggle}>
 
173
  {isAppMicActive ? <PauseIconWithSurroundPulse userVolume={userVolume} /> : microphoneIcon}
174
  </div>
175
 
 
176
  {isAppCamActive && (
177
  <div id="small-logo-footer-container" className="small-logo-footer-html-like">
178
+ <Logo isMini={true} isActive={true} isAi={false} speakingVolume={volume} isUserSpeaking={isSpeaking}/>
 
 
 
 
 
 
179
  </div>
180
  )}
181
 
182
+ <div id="cam-button-wrapper" className="control-button-wrapper">
183
+ <div id="cam-button" className={cn("control-button cam-button-color", { "disabled": isTimeUp })} onClick={handleCamToggle}>
 
184
  {isAppCamActive ? stopCamIcon : cameraIcon}
185
  </div>
186
  <div id="switch-camera-button-container" className={cn("switch-camera-button-container", { visible: isAppCamActive && !isSwitchingCamera })}>
187
+ <button id="switch-camera-button" aria-label="Switch Camera" className="switch-camera-button-content group" onClick={handleSwitchCamera} disabled={!isAppCamActive || isSwitchingCamera || isTimeUp}>
188
  <SvgSwitchCameraIcon/>
189
  </button>
190
  </div>