Ezmary commited on
Commit
3d6be6c
·
verified ·
1 Parent(s): 76ee455

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

Browse files
src/components/control-tray/ControlTray.tsx CHANGED
@@ -1,8 +1,8 @@
1
- // src/components/control-tray/ControlTray.tsx
2
 
3
  import cn from "classnames";
4
  import React, { memo, RefObject, useEffect, useState, useCallback, useRef } from "react";
5
- import { useAppContext } from "../../contexts/AppContext"; // <-- تغییر اصلی اینجاست
6
  import { AudioRecorder } from "../../lib/audio-recorder";
7
  import Logo from '../logo/Logo';
8
  import { PauseIconWithSurroundPulse, microphoneIcon, cameraIcon, stopCamIcon } from '../icons';
@@ -21,7 +21,7 @@ export type ControlTrayProps = {
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);
27
  const [isSwitchingCamera, setIsSwitchingCamera] = useState(false);
@@ -33,55 +33,34 @@ const ControlTray: React.FC<ControlTrayProps> = ({ videoRef, onUserSpeakingChang
33
  audioRecorderRef.current = new AudioRecorder();
34
  }
35
  const audioRecorder = audioRecorderRef.current;
36
-
37
  const handleAudioData = (base64: string, vol: number) => {
38
  if (client && connected && isAppMicActive) {
39
- if (base64) {
40
- client.sendRealtimeInput([{ mimeType: "audio/pcm;rate=16000", data: base64 }]);
41
- }
42
  setUserVolume(vol);
43
  onUserSpeakingChange(vol > 0.01);
44
  }
45
  };
46
-
47
- const onStop = () => {
48
- setUserVolume(0);
49
- onUserSpeakingChange(false);
50
- }
51
-
52
  if (isAppMicActive && connected) {
53
- audioRecorder.on("data", handleAudioData);
54
- audioRecorder.on("stop", onStop);
55
- if (!audioRecorder.recording) {
56
- audioRecorder.start();
57
- }
58
  } else {
59
- if (audioRecorder?.recording) {
60
- audioRecorder.stop();
61
- }
62
  }
63
-
64
- return () => {
65
- if (audioRecorder) {
66
- audioRecorder.off("data", handleAudioData);
67
- audioRecorder.off("stop", onStop);
68
- }
69
- };
70
  }, [isAppMicActive, connected, onUserSpeakingChange, client]);
71
 
72
  useEffect(() => {
73
- if (videoRef.current) {
74
- if (videoRef.current.srcObject !== activeLocalVideoStream) {
75
- videoRef.current.srcObject = activeLocalVideoStream;
76
- if (activeLocalVideoStream) videoRef.current.play().catch(e => console.warn("Video play failed:", e));
77
- }
78
  }
79
  }, [activeLocalVideoStream, videoRef]);
80
 
81
  const startWebcam = useCallback(async (facingModeToTry: 'user' | 'environment') => {
82
  if (isSwitchingCamera) return;
83
  setIsSwitchingCamera(true);
84
- if (activeLocalVideoStream) activeLocalVideoStream.getTracks().forEach(track => track.stop());
85
  try {
86
  const mediaStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: facingModeToTry }, audio: false });
87
  setActiveLocalVideoStream(mediaStream);
@@ -105,39 +84,37 @@ const ControlTray: React.FC<ControlTrayProps> = ({ videoRef, onUserSpeakingChang
105
  }, [isAppCamActive, activeLocalVideoStream, isSwitchingCamera, startWebcam, currentFacingMode]);
106
 
107
  useEffect(() => {
108
- let timeoutId = -1;
109
  function sendVideoFrame() {
110
- if (connected && activeLocalVideoStream && client && videoRef.current) {
111
  const video = videoRef.current;
112
  const canvas = renderCanvasRef.current;
113
- if (!canvas || video.readyState < video.HAVE_METADATA || video.paused || video.ended) {
114
- if (activeLocalVideoStream) timeoutId = window.setTimeout(sendVideoFrame, 1000 / 4);
115
- return;
 
 
 
 
 
 
 
 
 
 
 
 
116
  }
117
- try {
118
- const ctx = canvas.getContext("2d");
119
- if (!ctx) return;
120
- const scale = 0.5;
121
- canvas.width = video.videoWidth * scale;
122
- canvas.height = video.videoHeight * scale;
123
- if (canvas.width > 0 && canvas.height > 0) {
124
- ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
125
- const base64 = canvas.toDataURL("image/jpeg", 0.8);
126
- const data = base64.slice(base64.indexOf(",") + 1);
127
- if (data) client.sendRealtimeInput([{ mimeType: "image/jpeg", data }]);
128
- }
129
- } catch (error) { console.error("❌ Error frame:", error); }
130
  }
131
- if (connected && activeLocalVideoStream) timeoutId = window.setTimeout(sendVideoFrame, 1000 / 4);
132
  }
133
- if (connected && activeLocalVideoStream && videoRef.current) timeoutId = window.setTimeout(sendVideoFrame, 200);
134
  return () => clearTimeout(timeoutId);
135
  }, [connected, activeLocalVideoStream, client, videoRef, renderCanvasRef]);
136
 
137
  const ensureConnectedAndReady = async (): Promise<boolean> => {
138
  if (!connected) {
139
- try { await connect(); return true; }
140
- catch (err) { console.error('❌ CT Connect err:', err); return false; }
141
  }
142
  return true;
143
  };
@@ -145,9 +122,7 @@ const ControlTray: React.FC<ControlTrayProps> = ({ videoRef, onUserSpeakingChang
145
  const handleMicToggle = async () => {
146
  if (isSwitchingCamera) return;
147
  const newMicState = !isAppMicActive;
148
- if (newMicState && !(await ensureConnectedAndReady())) {
149
- onAppMicToggle(false); return;
150
- }
151
  onAppMicToggle(newMicState);
152
  };
153
 
@@ -176,7 +151,7 @@ const ControlTray: React.FC<ControlTrayProps> = ({ videoRef, onUserSpeakingChang
176
  } catch (error) {
177
  console.error(`❌ Switch Cam err to ${targetFacingMode}:`, error);
178
  try {
179
- const restoredStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: {exact: currentFacingMode } }, audio: false });
180
  setActiveLocalVideoStream(restoredStream);
181
  } catch (restoreError) {
182
  console.error(`❌ Restore Cam err to ${currentFacingMode}:`, restoreError);
@@ -191,12 +166,16 @@ const ControlTray: React.FC<ControlTrayProps> = ({ videoRef, onUserSpeakingChang
191
  const isSpeaking = userVolume > 0.01;
192
 
193
  return (
 
194
  <footer id="footer-controls" className="footer-controls-html-like">
195
  <canvas style={{ display: "none" }} ref={renderCanvasRef} />
 
 
196
  <div id="mic-button" className="control-button mic-button-color" onClick={handleMicToggle}>
197
  {isAppMicActive ? <PauseIconWithSurroundPulse userVolume={userVolume} /> : microphoneIcon}
198
  </div>
199
 
 
200
  {isAppCamActive && (
201
  <div id="small-logo-footer-container" className="small-logo-footer-html-like">
202
  <Logo
@@ -209,7 +188,8 @@ const ControlTray: React.FC<ControlTrayProps> = ({ videoRef, onUserSpeakingChang
209
  </div>
210
  )}
211
 
212
- <div id="cam-button-wrapper" className="control-button-wrapper">
 
213
  <div id="cam-button" className="control-button cam-button-color" onClick={handleCamToggle}>
214
  {isAppCamActive ? stopCamIcon : cameraIcon}
215
  </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";
5
+ import { useAppContext } from "../../contexts/AppContext";
6
  import { AudioRecorder } from "../../lib/audio-recorder";
7
  import Logo from '../logo/Logo';
8
  import { PauseIconWithSurroundPulse, microphoneIcon, cameraIcon, stopCamIcon } from '../icons';
 
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);
27
  const [isSwitchingCamera, setIsSwitchingCamera] = useState(false);
 
33
  audioRecorderRef.current = new AudioRecorder();
34
  }
35
  const audioRecorder = audioRecorderRef.current;
 
36
  const handleAudioData = (base64: string, vol: number) => {
37
  if (client && connected && isAppMicActive) {
38
+ if (base64) client.sendRealtimeInput([{ mimeType: "audio/pcm;rate=16000", data: base64 }]);
 
 
39
  setUserVolume(vol);
40
  onUserSpeakingChange(vol > 0.01);
41
  }
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);
 
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
  };
 
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
 
 
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);
 
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
 
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>