Ezmary commited on
Commit
d7461ed
·
verified ·
1 Parent(s): f76e9d1

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

Browse files
src/components/control-tray/ControlTray.tsx CHANGED
@@ -3,7 +3,7 @@ Copyright 2024 Google LLC
3
  ... (لایسنس و توضیحات دیگر مثل قبل) ...
4
  */
5
  import cn from "classnames";
6
- import React, { memo, ReactNode, RefObject, useEffect, useRef, useState } from "react";
7
  import { useLiveAPIContext } from "../../contexts/LiveAPIContext";
8
  import { AudioRecorder } from "../../lib/audio-recorder";
9
 
@@ -26,7 +26,7 @@ export type ControlTrayProps = {
26
  createLogoFunction: (isMini: boolean, isActive: boolean, type?: 'human' | 'ai') => ReactNode;
27
  };
28
 
29
- function ControlTray({
30
  videoRef,
31
  onVideoStreamChange,
32
  supportsVideo,
@@ -35,15 +35,15 @@ function ControlTray({
35
  isAppCamActive,
36
  onAppCamToggle,
37
  createLogoFunction,
38
- }: ControlTrayProps) {
39
- const { client, connected, connect, disconnect } = useLiveAPIContext();
40
  const [audioRecorder] = useState(() => new AudioRecorder());
41
  const [activeLocalVideoStream, setActiveLocalVideoStream] = useState<MediaStream | null>(null);
42
  const [currentFacingMode, setCurrentFacingMode] = useState<'user' | 'environment'>('user');
43
  const [isSwitchingCamera, setIsSwitchingCamera] = useState(false);
44
- const renderCanvasRef = useRef<HTMLCanvasElement>(null);
 
45
 
46
- // Manage webcam based on app-level camera active state
47
  useEffect(() => {
48
  if (isAppCamActive && !activeLocalVideoStream && !isSwitchingCamera) {
49
  startWebcam();
@@ -53,7 +53,6 @@ function ControlTray({
53
  // eslint-disable-next-line react-hooks/exhaustive-deps
54
  }, [isAppCamActive]);
55
 
56
- // Audio Recorder Logic
57
  useEffect(() => {
58
  const onData = (base64: string) => {
59
  if (client && connected && isAppMicActive) {
@@ -61,7 +60,7 @@ function ControlTray({
61
  }
62
  };
63
  if (connected && isAppMicActive && audioRecorder) {
64
- audioRecorder.on("data", onData).start(); // Removed .on("volume", setInVolume) if not used
65
  } else if (audioRecorder && audioRecorder.recording) {
66
  audioRecorder.stop();
67
  }
@@ -73,11 +72,9 @@ function ControlTray({
73
  };
74
  }, [connected, client, isAppMicActive, audioRecorder]);
75
 
76
- // Send Video Frame Logic
77
  useEffect(() => {
78
  let timeoutId = -1;
79
  function sendVideoFrame() {
80
- // ... (مثل قبل، با فرکانس 4FPS و scale 0.5) ...
81
  if (connected && activeLocalVideoStream && client && videoRef.current) {
82
  const video = videoRef.current;
83
  const canvas = renderCanvasRef.current;
@@ -96,7 +93,6 @@ function ControlTray({
96
  const base64 = canvas.toDataURL("image/jpeg", 0.8);
97
  const data = base64.slice(base64.indexOf(",") + 1);
98
  if (data) {
99
- // console.log(`🖼️ Sending video frame`);
100
  client.sendRealtimeInput([{ mimeType: "image/jpeg", data }]);
101
  }
102
  }
@@ -115,7 +111,6 @@ function ControlTray({
115
  }, [connected, activeLocalVideoStream, client, videoRef, renderCanvasRef]);
116
 
117
 
118
- // Assign stream to video element
119
  useEffect(() => {
120
  if (videoRef.current) {
121
  if (videoRef.current.srcObject !== activeLocalVideoStream) {
@@ -130,9 +125,7 @@ function ControlTray({
130
  const ensureConnectedAndReady = async (): Promise<boolean> => {
131
  if (!connected) {
132
  try {
133
- console.log("ControlTray: Not connected. Attempting to connect...");
134
  await connect();
135
- console.log("ControlTray: Connection successful.");
136
  return true;
137
  } catch (err) {
138
  console.error('❌ ControlTray: Connection error:', err);
@@ -146,21 +139,18 @@ function ControlTray({
146
  if (isSwitchingCamera) return;
147
  const newMicState = !isAppMicActive;
148
 
149
- if (newMicState) { // Turning mic ON
150
  if (!(await ensureConnectedAndReady())) {
151
- onAppMicToggle(false); // Keep mic state off if connection fails
152
  return;
153
  }
154
  }
155
  onAppMicToggle(newMicState);
156
- console.log(`ControlTray: Mic toggled to ${newMicState}`);
157
  };
158
 
159
  const startWebcam = async (facingModeToTry: 'user' | 'environment' = currentFacingMode) => {
160
  if (isSwitchingCamera) return;
161
  setIsSwitchingCamera(true);
162
-
163
- // ensureConnectedAndReady is now called by handleCamToggle before this
164
  try {
165
  const mediaStream = await navigator.mediaDevices.getUserMedia({
166
  video: { facingMode: facingModeToTry },
@@ -169,12 +159,11 @@ function ControlTray({
169
  setActiveLocalVideoStream(mediaStream);
170
  onVideoStreamChange(mediaStream);
171
  setCurrentFacingMode(facingModeToTry);
172
- console.log(`ControlTray: Webcam started with ${facingModeToTry}`);
173
  } catch (error) {
174
- console.error(`❌ ControlTray: Error starting webcam with ${facingModeToTry}:`, error);
175
  setActiveLocalVideoStream(null);
176
  onVideoStreamChange(null);
177
- onAppCamToggle(false); // Signal App to turn off cam state if start fails
178
  } finally {
179
  setIsSwitchingCamera(false);
180
  }
@@ -186,40 +175,31 @@ function ControlTray({
186
  }
187
  setActiveLocalVideoStream(null);
188
  onVideoStreamChange(null);
189
- console.log("ControlTray: Webcam stopped.");
190
  };
191
 
192
  const handleCamToggle = async () => {
193
  if (isSwitchingCamera) return;
194
  const newCamState = !isAppCamActive;
195
 
196
- if (newCamState) { // Turning camera ON
197
  if (!(await ensureConnectedAndReady())) {
198
- onAppCamToggle(false); // Keep cam state off if connection fails
199
  return;
200
  }
201
- // If mic is not already active, activate it as well
202
  if (!isAppMicActive) {
203
- onAppMicToggle(true); // This will trigger AudioRecorder via useEffect
204
- console.log("ControlTray: Mic also activated with camera.");
205
  }
206
- onAppCamToggle(true); // This will trigger startWebcam via useEffect
207
- } else { // Turning camera OFF
208
- onAppCamToggle(false); // This will trigger stopWebcam via useEffect
209
- // The disconnect logic if both are off is now in App.tsx's AppLogic
210
  }
211
- console.log(`ControlTray: Camera toggled to ${newCamState}`);
212
  };
213
 
214
  const handleSwitchCamera = async () => {
215
- // ... (منطق تعویض دوربین مثل قبل، اما مطمئن شوید که isAppCamActive را به درستی مدیریت می‌کند) ...
216
  if (!isAppCamActive || !activeLocalVideoStream || isSwitchingCamera) return;
217
-
218
  setIsSwitchingCamera(true);
219
  const targetFacingMode = currentFacingMode === 'user' ? 'environment' : 'user';
220
-
221
  activeLocalVideoStream.getTracks().forEach(track => track.stop());
222
-
223
  try {
224
  const newStream = await navigator.mediaDevices.getUserMedia({
225
  video: { facingMode: { exact: targetFacingMode } },
@@ -244,29 +224,43 @@ function ControlTray({
244
  setIsSwitchingCamera(false);
245
  }
246
  };
 
 
 
 
 
 
 
 
 
247
 
248
  return (
249
- <footer id="footer-controls" className={cn("footer-controls fixed bottom-0 left-1/2 transform -translate-x-1/2 z-50", {
250
- 'layout-default': !isAppCamActive,
251
- 'layout-with-small-logo': isAppCamActive
252
- })}>
253
  <canvas style={{ display: "none" }} ref={renderCanvasRef} />
254
 
 
255
  <div id="cam-button-wrapper" className="relative flex justify-center">
256
  <button
257
  id="cam-button"
258
- className="control-button cam-button-color"
259
  onClick={handleCamToggle}
260
  disabled={isSwitchingCamera || !supportsVideo}
261
  title={isAppCamActive ? "Stop Camera & Mic (if not used)" : "Start Camera & Mic"}
262
  >
263
  {isAppCamActive ? <SvgStopCamIcon /> : <SvgCameraIcon />}
264
  </button>
265
- <div id="switch-camera-button-container" className={cn("switch-camera-button-container", { 'visible': isAppCamActive && !isSwitchingCamera })}>
 
 
 
 
 
 
 
266
  <button
267
  id="switch-camera-button"
268
  aria-label="Switch Camera"
269
- className="switch-camera-button-content group"
270
  onClick={handleSwitchCamera}
271
  disabled={!isAppCamActive || isSwitchingCamera}
272
  >
@@ -275,15 +269,17 @@ function ControlTray({
275
  </div>
276
  </div>
277
 
 
278
  {isAppCamActive && (
279
- <div id="small-logo-container" className="flex items-center justify-center">
280
  {createLogoFunction(true, isAppCamActive, 'human')}
281
  </div>
282
  )}
283
 
 
284
  <button
285
  id="mic-button"
286
- className="control-button mic-button-color"
287
  onClick={handleMicToggle}
288
  disabled={isSwitchingCamera}
289
  title={isAppMicActive ? "Mute Microphone" : "Unmute Microphone"}
@@ -292,6 +288,6 @@ function ControlTray({
292
  </button>
293
  </footer>
294
  );
295
- }
296
 
297
  export default memo(ControlTray);
 
3
  ... (لایسنس و توضیحات دیگر مثل قبل) ...
4
  */
5
  import cn from "classnames";
6
+ import React, { memo, ReactNode, RefObject, useEffect, useState } from "react"; // removed useRef as renderCanvasRef is now passed if needed or handled differently
7
  import { useLiveAPIContext } from "../../contexts/LiveAPIContext";
8
  import { AudioRecorder } from "../../lib/audio-recorder";
9
 
 
26
  createLogoFunction: (isMini: boolean, isActive: boolean, type?: 'human' | 'ai') => ReactNode;
27
  };
28
 
29
+ const ControlTray: React.FC<ControlTrayProps> = ({
30
  videoRef,
31
  onVideoStreamChange,
32
  supportsVideo,
 
35
  isAppCamActive,
36
  onAppCamToggle,
37
  createLogoFunction,
38
+ }) => {
39
+ const { client, connected, connect } = useLiveAPIContext(); // disconnect is handled in AppInternalLogic
40
  const [audioRecorder] = useState(() => new AudioRecorder());
41
  const [activeLocalVideoStream, setActiveLocalVideoStream] = useState<MediaStream | null>(null);
42
  const [currentFacingMode, setCurrentFacingMode] = useState<'user' | 'environment'>('user');
43
  const [isSwitchingCamera, setIsSwitchingCamera] = useState(false);
44
+ const renderCanvasRef = React.useRef<HTMLCanvasElement>(null);
45
+
46
 
 
47
  useEffect(() => {
48
  if (isAppCamActive && !activeLocalVideoStream && !isSwitchingCamera) {
49
  startWebcam();
 
53
  // eslint-disable-next-line react-hooks/exhaustive-deps
54
  }, [isAppCamActive]);
55
 
 
56
  useEffect(() => {
57
  const onData = (base64: string) => {
58
  if (client && connected && isAppMicActive) {
 
60
  }
61
  };
62
  if (connected && isAppMicActive && audioRecorder) {
63
+ audioRecorder.on("data", onData).start();
64
  } else if (audioRecorder && audioRecorder.recording) {
65
  audioRecorder.stop();
66
  }
 
72
  };
73
  }, [connected, client, isAppMicActive, audioRecorder]);
74
 
 
75
  useEffect(() => {
76
  let timeoutId = -1;
77
  function sendVideoFrame() {
 
78
  if (connected && activeLocalVideoStream && client && videoRef.current) {
79
  const video = videoRef.current;
80
  const canvas = renderCanvasRef.current;
 
93
  const base64 = canvas.toDataURL("image/jpeg", 0.8);
94
  const data = base64.slice(base64.indexOf(",") + 1);
95
  if (data) {
 
96
  client.sendRealtimeInput([{ mimeType: "image/jpeg", data }]);
97
  }
98
  }
 
111
  }, [connected, activeLocalVideoStream, client, videoRef, renderCanvasRef]);
112
 
113
 
 
114
  useEffect(() => {
115
  if (videoRef.current) {
116
  if (videoRef.current.srcObject !== activeLocalVideoStream) {
 
125
  const ensureConnectedAndReady = async (): Promise<boolean> => {
126
  if (!connected) {
127
  try {
 
128
  await connect();
 
129
  return true;
130
  } catch (err) {
131
  console.error('❌ ControlTray: Connection error:', err);
 
139
  if (isSwitchingCamera) return;
140
  const newMicState = !isAppMicActive;
141
 
142
+ if (newMicState) {
143
  if (!(await ensureConnectedAndReady())) {
144
+ onAppMicToggle(false);
145
  return;
146
  }
147
  }
148
  onAppMicToggle(newMicState);
 
149
  };
150
 
151
  const startWebcam = async (facingModeToTry: 'user' | 'environment' = currentFacingMode) => {
152
  if (isSwitchingCamera) return;
153
  setIsSwitchingCamera(true);
 
 
154
  try {
155
  const mediaStream = await navigator.mediaDevices.getUserMedia({
156
  video: { facingMode: facingModeToTry },
 
159
  setActiveLocalVideoStream(mediaStream);
160
  onVideoStreamChange(mediaStream);
161
  setCurrentFacingMode(facingModeToTry);
 
162
  } catch (error) {
163
+ console.error(`❌ Error starting webcam with ${facingModeToTry}:`, error);
164
  setActiveLocalVideoStream(null);
165
  onVideoStreamChange(null);
166
+ onAppCamToggle(false);
167
  } finally {
168
  setIsSwitchingCamera(false);
169
  }
 
175
  }
176
  setActiveLocalVideoStream(null);
177
  onVideoStreamChange(null);
 
178
  };
179
 
180
  const handleCamToggle = async () => {
181
  if (isSwitchingCamera) return;
182
  const newCamState = !isAppCamActive;
183
 
184
+ if (newCamState) {
185
  if (!(await ensureConnectedAndReady())) {
186
+ onAppCamToggle(false);
187
  return;
188
  }
 
189
  if (!isAppMicActive) {
190
+ onAppMicToggle(true);
 
191
  }
192
+ onAppCamToggle(true);
193
+ } else {
194
+ onAppCamToggle(false);
 
195
  }
 
196
  };
197
 
198
  const handleSwitchCamera = async () => {
 
199
  if (!isAppCamActive || !activeLocalVideoStream || isSwitchingCamera) return;
 
200
  setIsSwitchingCamera(true);
201
  const targetFacingMode = currentFacingMode === 'user' ? 'environment' : 'user';
 
202
  activeLocalVideoStream.getTracks().forEach(track => track.stop());
 
203
  try {
204
  const newStream = await navigator.mediaDevices.getUserMedia({
205
  video: { facingMode: { exact: targetFacingMode } },
 
224
  setIsSwitchingCamera(false);
225
  }
226
  };
227
+
228
+ // کلاس‌های footer-controls از HTML مرجع برای موقعیت‌یابی و چیدمان
229
+ const footerClasses = cn(
230
+ "footer-controls w-full flex gap-4 absolute bottom-0 p-8 items-center z-50", // Tailwind: md:p-12
231
+ {
232
+ "layout-default justify-between": !isAppCamActive, // justify-between از HTML
233
+ "layout-with-small-logo justify-around": isAppCamActive, // justify-around از HTML
234
+ }
235
+ );
236
 
237
  return (
238
+ <footer id="footer-controls" className={footerClasses}>
 
 
 
239
  <canvas style={{ display: "none" }} ref={renderCanvasRef} />
240
 
241
+ {/* Cam Button Wrapper (Now on the Left) */}
242
  <div id="cam-button-wrapper" className="relative flex justify-center">
243
  <button
244
  id="cam-button"
245
+ className="control-button cam-button-color" // کلاس‌های HTML مرجع
246
  onClick={handleCamToggle}
247
  disabled={isSwitchingCamera || !supportsVideo}
248
  title={isAppCamActive ? "Stop Camera & Mic (if not used)" : "Start Camera & Mic"}
249
  >
250
  {isAppCamActive ? <SvgStopCamIcon /> : <SvgCameraIcon />}
251
  </button>
252
+ {/* Switch Camera Button - نمایش فقط وقتی دوربین فعال است */}
253
+ <div
254
+ id="switch-camera-button-container"
255
+ className={cn(
256
+ "switch-camera-button-container", // کلاس‌های HTML مرجع
257
+ { visible: isAppCamActive && !isSwitchingCamera }
258
+ )}
259
+ >
260
  <button
261
  id="switch-camera-button"
262
  aria-label="Switch Camera"
263
+ className="switch-camera-button-content group" // کلاس‌های HTML مرجع
264
  onClick={handleSwitchCamera}
265
  disabled={!isAppCamActive || isSwitchingCamera}
266
  >
 
269
  </div>
270
  </div>
271
 
272
+ {/* Small Logo Container (Center, if cam active) */}
273
  {isAppCamActive && (
274
+ <div id="small-logo-container" className="flex items-center justify-center"> {/* کلاس HTML مرجع */}
275
  {createLogoFunction(true, isAppCamActive, 'human')}
276
  </div>
277
  )}
278
 
279
+ {/* Mic Button (Now on the Right) */}
280
  <button
281
  id="mic-button"
282
+ className="control-button mic-button-color" // کلاس‌های HTML مرجع
283
  onClick={handleMicToggle}
284
  disabled={isSwitchingCamera}
285
  title={isAppMicActive ? "Mute Microphone" : "Unmute Microphone"}
 
288
  </button>
289
  </footer>
290
  );
291
+ };
292
 
293
  export default memo(ControlTray);