Ezmary commited on
Commit
62cb269
·
verified ·
1 Parent(s): 05ad47d

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

Browse files
src/components/control-tray/ControlTray.tsx CHANGED
@@ -5,15 +5,11 @@ Copyright 2024 Google LLC
5
  import cn from "classnames";
6
  import React, { memo, ReactNode, RefObject, useEffect, useRef, useState } from "react";
7
  import { useLiveAPIContext } from "../../contexts/LiveAPIContext";
8
- // import { UseMediaStreamResult } from "../../hooks/use-media-stream-mux"; // دیگر استفاده نمی‌شود؟
9
- // import { useScreenCapture } from "../../hooks/use-screen-capture"; // اگر نیاز به اشتراک صفحه نیست، حذف شود
10
- // import { useWebcam } from "../../hooks/use-webcam"; // منطق وب‌کم در همین فایل ادغام شده
11
  import { AudioRecorder } from "../../lib/audio-recorder";
12
  import { isIOS } from "../../lib/platform";
13
- // import AudioPulse from "../audio-pulse/AudioPulse"; // دیگر استفاده نمی‌شود
14
- import "./control-tray.scss"; // ممکن است خالی یا حاوی استایل‌های بسیار کمی باشد
15
 
16
- // SVG Icons for Controls
17
  const SvgPauseIcon = () => <svg width="37" height="37" viewBox="0 0 37 37" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M15.9872 29.6198V8.28342C15.9872 6.25781 15.132 5.44757 12.9713 5.44757H7.52469C5.36404 5.44757 4.50879 6.25781 4.50879 8.28342V29.6198C4.50879 31.6454 5.36404 32.4556 7.52469 32.4556H12.9713C15.132 32.4556 15.9872 31.6454 15.9872 29.6198Z" fill="#BE123C"/><path opacity="0.4" d="M31.5175 29.6198V8.28342C31.5175 6.25781 30.6622 5.44757 28.5016 5.44757H23.055C20.9093 5.44757 20.0391 6.25781 20.0391 8.28342V29.6198C20.0391 31.6454 20.8943 32.4556 23.055 32.4556H28.5016C30.6622 32.4556 31.5175 31.6454 31.5175 29.6198Z" fill="#BE123C"/></svg>;
18
  const SvgMicrophoneIcon = () => <svg width="38" height="38" viewBox="0 0 69 68" fill="none" xmlns="http://www.w3.org/2000/svg" transform="scale(0.55)"><path opacity="0.4" d="M49.9479 27.1824C49.0803 27.1824 48.3907 27.872 48.3907 28.7396V32.2544C48.3907 40.1293 41.984 46.5361 34.109 46.5361C26.234 46.5361 19.8273 40.1293 19.8273 32.2544V28.7173C19.8273 27.8497 19.1377 27.1601 18.2701 27.1601C17.4025 27.1601 16.7129 27.8497 16.7129 28.7173V32.2321C16.7129 41.2861 23.6758 48.7384 32.5518 49.5393V54.2776C32.5518 55.1452 33.2414 55.8348 34.109 55.8348C34.9766 55.8348 35.6662 55.1452 35.6662 54.2776V49.5393C44.52 48.7607 51.5051 41.2861 51.5051 32.2321V28.7173C51.4829 27.872 50.7933 27.1824 49.9479 27.1824Z" fill="#BE123C"/><path d="M34.1099 11.3434C28.682 11.3434 24.2773 15.7481 24.2773 21.176V32.5658C24.2773 37.9938 28.682 42.3984 34.1099 42.3984C39.5379 42.3984 43.9425 37.9938 43.9425 32.5658V21.176C43.9425 15.7481 39.5379 11.3434 34.1099 11.3434ZM37.0241 26.8042C36.8684 27.3826 36.3567 27.7608 35.7784 27.7608C35.6671 27.7608 35.5559 27.7385 35.4447 27.7163C34.5771 27.4716 33.665 27.4716 32.7974 27.7163C32.0856 27.9165 31.396 27.4938 31.218 26.8042C31.0178 26.1146 31.4404 25.4027 32.1301 25.2247C33.4426 24.8688 34.8218 24.8688 36.1343 25.2247C36.8017 25.4027 37.2021 26.1146 37.0241 26.8042ZM38.2031 22.4885C38.0029 23.0224 37.5135 23.3339 36.9796 23.3339C36.8239 23.3339 36.6904 23.3116 36.5347 23.2671C34.9775 22.6887 33.2423 22.6887 31.6852 23.2671C31.0178 23.5118 30.2614 23.1559 30.0167 22.4885C29.772 21.8212 30.128 21.0648 30.7953 20.8423C32.9309 20.0637 35.289 20.0637 37.4245 20.8423C38.0919 21.087 38.4478 21.8212 38.2031 22.4885Z" fill="#BE123C"/></svg>;
19
  const SvgCameraIcon = () => <svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"><path opacity="0.4" d="M21.8118 5.52235H11.9265C6.29183 5.52235 4.10059 7.7136 4.10059 13.3482V26.5286C4.10059 30.318 6.16002 34.3545 11.9265 34.3545H21.8118C27.4464 34.3545 29.6376 32.1633 29.6376 26.5286V13.3482C29.6376 7.7136 27.4464 5.52235 21.8118 5.52235Z" fill="#2252A0"/><path d="M19.3406 18.9169C21.0512 18.9169 22.438 17.5302 22.438 15.8195C22.438 14.1089 21.0512 12.7222 19.3406 12.7222C17.6299 12.7222 16.2432 14.1089 16.2432 15.8195C16.2432 17.5302 17.6299 18.9169 19.3406 18.9169Z" fill="#2252A0"/><path d="M36.0629 10.3332C35.3874 9.98721 33.9705 9.5918 32.0429 10.9428L29.6045 12.6562C29.621 12.8869 29.6374 13.1011 29.6374 13.3482V26.5286C29.6374 26.7758 29.6045 26.9899 29.6045 27.2206L32.0429 28.934C33.0643 29.659 33.954 29.8896 34.6625 29.8896C35.2721 29.8896 35.7499 29.7249 36.0629 29.5601C36.7384 29.2141 37.8752 28.275 37.8752 25.919V13.9743C37.8752 11.6183 36.7384 10.6792 36.0629 10.3332Z" fill="#2252A0"/></svg>;
@@ -23,10 +19,9 @@ const SvgSwitchCameraIcon = () => <svg xmlns="http://www.w3.org/2000/svg" viewBo
23
 
24
  export type ControlTrayProps = {
25
  videoRef: RefObject<HTMLVideoElement>;
26
- children?: ReactNode; // ممکن است دیگر استفاده نشود
27
  supportsVideo: boolean;
28
  onVideoStreamChange: (stream: MediaStream | null) => void;
29
- // Props جدید از App.tsx
30
  isAppMicActive: boolean;
31
  onAppMicToggle: (active: boolean) => void;
32
  isAppCamActive: boolean;
@@ -44,18 +39,17 @@ function ControlTray({
44
  onAppCamToggle,
45
  createLogoFunction,
46
  }: ControlTrayProps) {
47
- const { client, connected, connect, disconnect, volume } // volume ممکن است برای UI جدید استفاده نشود
48
  = useLiveAPIContext();
49
 
50
  const [audioRecorder] = useState(() => new AudioRecorder());
51
- const [muted, setMuted] = useState(true); // Initial state, will be synced with isAppMicActive
52
- const [inVolume, setInVolume] = useState(0); // برای نمایش ولوم در آینده، اگر نیاز شد
53
 
54
  const [activeLocalVideoStream, setActiveLocalVideoStream] = useState<MediaStream | null>(null);
55
  const [currentFacingMode, setCurrentFacingMode] = useState<'user' | 'environment'>('user');
56
  const [isSwitchingCamera, setIsSwitchingCamera] = useState(false);
57
 
58
- // Sync with App's mic/cam state
59
  useEffect(() => {
60
  setMuted(!isAppMicActive);
61
  }, [isAppMicActive]);
@@ -69,8 +63,6 @@ function ControlTray({
69
  // eslint-disable-next-line react-hooks/exhaustive-deps
70
  }, [isAppCamActive]);
71
 
72
-
73
- // Audio recording logic (from your original ControlTray)
74
  useEffect(() => {
75
  const onData = (base64: string) => {
76
  if (client && connected) {
@@ -89,13 +81,12 @@ function ControlTray({
89
  };
90
  }, [connected, client, muted, audioRecorder]);
91
 
92
- // Video frame sending logic (simplified from your original ControlTray)
93
  const renderCanvasRef = useRef<HTMLCanvasElement>(null);
94
  useEffect(() => {
95
  let timeoutId = -1;
96
  function sendVideoFrame() {
97
  if (connected && activeLocalVideoStream) {
98
- timeoutId = window.setTimeout(sendVideoFrame, 1000 / 0.5); // ~2 FPS
99
  }
100
  const video = videoRef.current;
101
  const canvas = renderCanvasRef.current;
@@ -104,12 +95,12 @@ function ControlTray({
104
  try {
105
  const ctx = canvas.getContext("2d");
106
  if (!ctx) return;
107
- const scale = 0.25; // کاهش اندازه فریم برای ارسال
108
  canvas.width = video.videoWidth * scale;
109
  canvas.height = video.videoHeight * scale;
110
  if (canvas.width > 0 && canvas.height > 0) {
111
  ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
112
- const base64 = canvas.toDataURL("image/jpeg", 0.7); // کیفیت JPEG
113
  const data = base64.slice(base64.indexOf(",") + 1);
114
  client.sendRealtimeInput([{ mimeType: "image/jpeg", data }]);
115
  }
@@ -119,12 +110,11 @@ function ControlTray({
119
  }
120
 
121
  if (connected && activeLocalVideoStream && videoRef.current) {
122
- setTimeout(sendVideoFrame, 200); // Start sending frames after a short delay
123
  }
124
  return () => { clearTimeout(timeoutId); };
125
  }, [connected, activeLocalVideoStream, client, videoRef]);
126
 
127
- // Assign stream to video element
128
  useEffect(() => {
129
  if (videoRef.current) {
130
  if (videoRef.current.srcObject !== activeLocalVideoStream) {
@@ -136,7 +126,6 @@ function ControlTray({
136
  }
137
  }, [activeLocalVideoStream, videoRef]);
138
 
139
-
140
  const ensureConnected = async () => {
141
  if (!connected) {
142
  try {
@@ -152,25 +141,24 @@ function ControlTray({
152
  const handleMicToggle = async () => {
153
  if (isSwitchingCamera) return;
154
  const newMicState = !isAppMicActive;
155
- if (newMicState) { // Turning mic ON
156
  if(!await ensureConnected()) {
157
- onAppMicToggle(false); // Failed to connect, revert UI
158
  return;
159
  }
160
  }
161
  onAppMicToggle(newMicState);
162
- // If mic is turned off and cam is also off, consider disconnecting
163
  if (!newMicState && !isAppCamActive && connected) {
164
- // await disconnect(); // یا تصمیم بگیرید که اتصال را نگه دارید
165
  }
166
  };
167
 
168
  const startWebcam = async (facingModeToTry: 'user' | 'environment' = currentFacingMode) => {
169
  if (isSwitchingCamera) return;
170
- setIsSwitchingCamera(true); // Prevent rapid clicks
171
 
172
  if(!await ensureConnected()) {
173
- onAppCamToggle(false); // Failed to connect, revert UI
174
  setIsSwitchingCamera(false);
175
  return;
176
  }
@@ -179,16 +167,15 @@ function ControlTray({
179
  console.log(`🚀 Starting webcam with facingMode: ${facingModeToTry}`);
180
  const mediaStream = await navigator.mediaDevices.getUserMedia({
181
  video: { facingMode: facingModeToTry },
182
- audio: false, // Audio is handled by AudioRecorder
183
  });
184
  setActiveLocalVideoStream(mediaStream);
185
- onVideoStreamChange(mediaStream); // Update App.tsx's videoStream if needed for other components
186
  setCurrentFacingMode(facingModeToTry);
187
- onAppCamToggle(true); // Update App's state
188
  } catch (error) {
189
  console.error(`❌ Error starting webcam with ${facingModeToTry}:`, error);
190
- // Try fallback if initial failed
191
- if (facingModeToTry === 'user' && (error as Error).name !== 'NotFoundError') { // Avoid retry if no camera found
192
  try {
193
  const fallbackStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' }, audio: false });
194
  setActiveLocalVideoStream(fallbackStream);
@@ -217,10 +204,9 @@ function ControlTray({
217
  }
218
  setActiveLocalVideoStream(null);
219
  onVideoStreamChange(null);
220
- onAppCamToggle(false); // Update App's state
221
- // If mic is also off, consider disconnecting
222
  if (!isAppMicActive && connected) {
223
- // await disconnect(); // یا تصمیم بگیرید که اتصال را نگه دارید
224
  }
225
  };
226
 
@@ -238,9 +224,8 @@ function ControlTray({
238
  setIsSwitchingCamera(true);
239
  const targetFacingMode = currentFacingMode === 'user' ? 'environment' : 'user';
240
 
241
- // Stop current tracks before requesting new ones
242
  activeLocalVideoStream.getTracks().forEach(track => track.stop());
243
- setActiveLocalVideoStream(null); // Clear stream temporarily
244
  onVideoStreamChange(null);
245
 
246
  try {
@@ -252,30 +237,26 @@ function ControlTray({
252
  setActiveLocalVideoStream(newStream);
253
  onVideoStreamChange(newStream);
254
  setCurrentFacingMode(targetFacingMode);
255
- // Ensure video element plays the new stream
256
  if (videoRef.current) {
257
  videoRef.current.srcObject = newStream;
258
  videoRef.current.play().catch(e => console.warn("Play failed on switch:", e));
259
  }
260
  } catch (error) {
261
  console.error(`❌ Error switching camera to ${targetFacingMode}:`, error);
262
- // Try to restore previous stream or a default if switching failed
263
  console.log(`Attempting to restore camera to: ${currentFacingMode}`);
264
  try {
265
  const restoredStream = await navigator.mediaDevices.getUserMedia({
266
- video: { facingMode: currentFacingMode }, // Try previous mode
267
  audio: false,
268
  });
269
  setActiveLocalVideoStream(restoredStream);
270
  onVideoStreamChange(restoredStream);
271
- // No need to change currentFacingMode as it's restored
272
  if (videoRef.current) {
273
  videoRef.current.srcObject = restoredStream;
274
  videoRef.current.play().catch(e => console.warn("Play failed on restore:", e));
275
  }
276
  } catch (restoreError) {
277
  console.error('❌ Error restoring camera:', restoreError);
278
- // If all fails, turn off camera
279
  stopWebcam();
280
  onAppCamToggle(false);
281
  }
@@ -284,22 +265,24 @@ function ControlTray({
284
  }
285
  };
286
 
287
- // Stop streams on disconnect
288
  useEffect(() => {
289
  if (!connected) {
290
  if (activeLocalVideoStream) {
291
  activeLocalVideoStream.getTracks().forEach(track => track.stop());
292
  setActiveLocalVideoStream(null);
293
  onVideoStreamChange(null);
294
- onAppCamToggle(false); // Ensure UI consistency
295
  }
296
- if (audioRecorder.isRecording()) {
 
297
  audioRecorder.stop();
298
- onAppMicToggle(false); // Ensure UI consistency
299
  }
 
300
  }
301
  // eslint-disable-next-line react-hooks/exhaustive-deps
302
- }, [connected]);
 
303
 
304
 
305
  return (
@@ -308,7 +291,6 @@ function ControlTray({
308
  'layout-with-small-logo': isAppCamActive
309
  })}>
310
  <canvas style={{ display: "none" }} ref={renderCanvasRef} />
311
- {/* Cam Button Wrapper (Now on the Left) */}
312
  <div id="cam-button-wrapper" className="relative flex justify-center">
313
  <button
314
  id="cam-button"
@@ -332,26 +314,22 @@ function ControlTray({
332
  </div>
333
  </div>
334
 
335
- {/* Small Logo Container (middle when cam is active) */}
336
  {isAppCamActive && (
337
  <div id="small-logo-container" className="flex items-center justify-center">
338
  {createLogoFunction(true, isAppCamActive, 'human')}
339
  </div>
340
  )}
341
 
342
- {/* Mic Button (Now on the Right) */}
343
  <button
344
  id="mic-button"
345
  className="control-button mic-button-color"
346
  onClick={handleMicToggle}
347
- disabled={isSwitchingCamera} // Disable if camera is switching, or other conditions
348
  title={isAppMicActive ? "Mute Microphone" : "Unmute Microphone"}
349
  >
350
  {isAppMicActive ? <SvgPauseIcon /> : <SvgMicrophoneIcon />}
351
  </button>
352
 
353
- {/* Original connect/disconnect button, hidden but logic can be reused or adapted */}
354
- {/* This button's functionality is now implicitly handled by mic/cam toggles */}
355
  <button
356
  style={{ display: 'none'}}
357
  onClick={async () => {
 
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
  import { isIOS } from "../../lib/platform";
10
+ // import "./control-tray.scss"; // این فایل دیگر نباید استایلی داشته باشد یا باید حذف شود
 
11
 
12
+ // SVG Icons for Controls (همانطور که قبلا تعریف شد)
13
  const SvgPauseIcon = () => <svg width="37" height="37" viewBox="0 0 37 37" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M15.9872 29.6198V8.28342C15.9872 6.25781 15.132 5.44757 12.9713 5.44757H7.52469C5.36404 5.44757 4.50879 6.25781 4.50879 8.28342V29.6198C4.50879 31.6454 5.36404 32.4556 7.52469 32.4556H12.9713C15.132 32.4556 15.9872 31.6454 15.9872 29.6198Z" fill="#BE123C"/><path opacity="0.4" d="M31.5175 29.6198V8.28342C31.5175 6.25781 30.6622 5.44757 28.5016 5.44757H23.055C20.9093 5.44757 20.0391 6.25781 20.0391 8.28342V29.6198C20.0391 31.6454 20.8943 32.4556 23.055 32.4556H28.5016C30.6622 32.4556 31.5175 31.6454 31.5175 29.6198Z" fill="#BE123C"/></svg>;
14
  const SvgMicrophoneIcon = () => <svg width="38" height="38" viewBox="0 0 69 68" fill="none" xmlns="http://www.w3.org/2000/svg" transform="scale(0.55)"><path opacity="0.4" d="M49.9479 27.1824C49.0803 27.1824 48.3907 27.872 48.3907 28.7396V32.2544C48.3907 40.1293 41.984 46.5361 34.109 46.5361C26.234 46.5361 19.8273 40.1293 19.8273 32.2544V28.7173C19.8273 27.8497 19.1377 27.1601 18.2701 27.1601C17.4025 27.1601 16.7129 27.8497 16.7129 28.7173V32.2321C16.7129 41.2861 23.6758 48.7384 32.5518 49.5393V54.2776C32.5518 55.1452 33.2414 55.8348 34.109 55.8348C34.9766 55.8348 35.6662 55.1452 35.6662 54.2776V49.5393C44.52 48.7607 51.5051 41.2861 51.5051 32.2321V28.7173C51.4829 27.872 50.7933 27.1824 49.9479 27.1824Z" fill="#BE123C"/><path d="M34.1099 11.3434C28.682 11.3434 24.2773 15.7481 24.2773 21.176V32.5658C24.2773 37.9938 28.682 42.3984 34.1099 42.3984C39.5379 42.3984 43.9425 37.9938 43.9425 32.5658V21.176C43.9425 15.7481 39.5379 11.3434 34.1099 11.3434ZM37.0241 26.8042C36.8684 27.3826 36.3567 27.7608 35.7784 27.7608C35.6671 27.7608 35.5559 27.7385 35.4447 27.7163C34.5771 27.4716 33.665 27.4716 32.7974 27.7163C32.0856 27.9165 31.396 27.4938 31.218 26.8042C31.0178 26.1146 31.4404 25.4027 32.1301 25.2247C33.4426 24.8688 34.8218 24.8688 36.1343 25.2247C36.8017 25.4027 37.2021 26.1146 37.0241 26.8042ZM38.2031 22.4885C38.0029 23.0224 37.5135 23.3339 36.9796 23.3339C36.8239 23.3339 36.6904 23.3116 36.5347 23.2671C34.9775 22.6887 33.2423 22.6887 31.6852 23.2671C31.0178 23.5118 30.2614 23.1559 30.0167 22.4885C29.772 21.8212 30.128 21.0648 30.7953 20.8423C32.9309 20.0637 35.289 20.0637 37.4245 20.8423C38.0919 21.087 38.4478 21.8212 38.2031 22.4885Z" fill="#BE123C"/></svg>;
15
  const SvgCameraIcon = () => <svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"><path opacity="0.4" d="M21.8118 5.52235H11.9265C6.29183 5.52235 4.10059 7.7136 4.10059 13.3482V26.5286C4.10059 30.318 6.16002 34.3545 11.9265 34.3545H21.8118C27.4464 34.3545 29.6376 32.1633 29.6376 26.5286V13.3482C29.6376 7.7136 27.4464 5.52235 21.8118 5.52235Z" fill="#2252A0"/><path d="M19.3406 18.9169C21.0512 18.9169 22.438 17.5302 22.438 15.8195C22.438 14.1089 21.0512 12.7222 19.3406 12.7222C17.6299 12.7222 16.2432 14.1089 16.2432 15.8195C16.2432 17.5302 17.6299 18.9169 19.3406 18.9169Z" fill="#2252A0"/><path d="M36.0629 10.3332C35.3874 9.98721 33.9705 9.5918 32.0429 10.9428L29.6045 12.6562C29.621 12.8869 29.6374 13.1011 29.6374 13.3482V26.5286C29.6374 26.7758 29.6045 26.9899 29.6045 27.2206L32.0429 28.934C33.0643 29.659 33.954 29.8896 34.6625 29.8896C35.2721 29.8896 35.7499 29.7249 36.0629 29.5601C36.7384 29.2141 37.8752 28.275 37.8752 25.919V13.9743C37.8752 11.6183 36.7384 10.6792 36.0629 10.3332Z" fill="#2252A0"/></svg>;
 
19
 
20
  export type ControlTrayProps = {
21
  videoRef: RefObject<HTMLVideoElement>;
22
+ children?: ReactNode;
23
  supportsVideo: boolean;
24
  onVideoStreamChange: (stream: MediaStream | null) => void;
 
25
  isAppMicActive: boolean;
26
  onAppMicToggle: (active: boolean) => void;
27
  isAppCamActive: boolean;
 
39
  onAppCamToggle,
40
  createLogoFunction,
41
  }: ControlTrayProps) {
42
+ const { client, connected, connect, disconnect, volume }
43
  = useLiveAPIContext();
44
 
45
  const [audioRecorder] = useState(() => new AudioRecorder());
46
+ const [muted, setMuted] = useState(true);
47
+ const [inVolume, setInVolume] = useState(0);
48
 
49
  const [activeLocalVideoStream, setActiveLocalVideoStream] = useState<MediaStream | null>(null);
50
  const [currentFacingMode, setCurrentFacingMode] = useState<'user' | 'environment'>('user');
51
  const [isSwitchingCamera, setIsSwitchingCamera] = useState(false);
52
 
 
53
  useEffect(() => {
54
  setMuted(!isAppMicActive);
55
  }, [isAppMicActive]);
 
63
  // eslint-disable-next-line react-hooks/exhaustive-deps
64
  }, [isAppCamActive]);
65
 
 
 
66
  useEffect(() => {
67
  const onData = (base64: string) => {
68
  if (client && connected) {
 
81
  };
82
  }, [connected, client, muted, audioRecorder]);
83
 
 
84
  const renderCanvasRef = useRef<HTMLCanvasElement>(null);
85
  useEffect(() => {
86
  let timeoutId = -1;
87
  function sendVideoFrame() {
88
  if (connected && activeLocalVideoStream) {
89
+ timeoutId = window.setTimeout(sendVideoFrame, 1000 / 0.5);
90
  }
91
  const video = videoRef.current;
92
  const canvas = renderCanvasRef.current;
 
95
  try {
96
  const ctx = canvas.getContext("2d");
97
  if (!ctx) return;
98
+ const scale = 0.25;
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.7);
104
  const data = base64.slice(base64.indexOf(",") + 1);
105
  client.sendRealtimeInput([{ mimeType: "image/jpeg", data }]);
106
  }
 
110
  }
111
 
112
  if (connected && activeLocalVideoStream && videoRef.current) {
113
+ setTimeout(sendVideoFrame, 200);
114
  }
115
  return () => { clearTimeout(timeoutId); };
116
  }, [connected, activeLocalVideoStream, client, videoRef]);
117
 
 
118
  useEffect(() => {
119
  if (videoRef.current) {
120
  if (videoRef.current.srcObject !== activeLocalVideoStream) {
 
126
  }
127
  }, [activeLocalVideoStream, videoRef]);
128
 
 
129
  const ensureConnected = async () => {
130
  if (!connected) {
131
  try {
 
141
  const handleMicToggle = async () => {
142
  if (isSwitchingCamera) return;
143
  const newMicState = !isAppMicActive;
144
+ if (newMicState) {
145
  if(!await ensureConnected()) {
146
+ onAppMicToggle(false);
147
  return;
148
  }
149
  }
150
  onAppMicToggle(newMicState);
 
151
  if (!newMicState && !isAppCamActive && connected) {
152
+ // await disconnect();
153
  }
154
  };
155
 
156
  const startWebcam = async (facingModeToTry: 'user' | 'environment' = currentFacingMode) => {
157
  if (isSwitchingCamera) return;
158
+ setIsSwitchingCamera(true);
159
 
160
  if(!await ensureConnected()) {
161
+ onAppCamToggle(false);
162
  setIsSwitchingCamera(false);
163
  return;
164
  }
 
167
  console.log(`🚀 Starting webcam with facingMode: ${facingModeToTry}`);
168
  const mediaStream = await navigator.mediaDevices.getUserMedia({
169
  video: { facingMode: facingModeToTry },
170
+ audio: false,
171
  });
172
  setActiveLocalVideoStream(mediaStream);
173
+ onVideoStreamChange(mediaStream);
174
  setCurrentFacingMode(facingModeToTry);
175
+ onAppCamToggle(true);
176
  } catch (error) {
177
  console.error(`❌ Error starting webcam with ${facingModeToTry}:`, error);
178
+ if (facingModeToTry === 'user' && (error as Error).name !== 'NotFoundError') {
 
179
  try {
180
  const fallbackStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' }, audio: false });
181
  setActiveLocalVideoStream(fallbackStream);
 
204
  }
205
  setActiveLocalVideoStream(null);
206
  onVideoStreamChange(null);
207
+ onAppCamToggle(false);
 
208
  if (!isAppMicActive && connected) {
209
+ // await disconnect();
210
  }
211
  };
212
 
 
224
  setIsSwitchingCamera(true);
225
  const targetFacingMode = currentFacingMode === 'user' ? 'environment' : 'user';
226
 
 
227
  activeLocalVideoStream.getTracks().forEach(track => track.stop());
228
+ setActiveLocalVideoStream(null);
229
  onVideoStreamChange(null);
230
 
231
  try {
 
237
  setActiveLocalVideoStream(newStream);
238
  onVideoStreamChange(newStream);
239
  setCurrentFacingMode(targetFacingMode);
 
240
  if (videoRef.current) {
241
  videoRef.current.srcObject = newStream;
242
  videoRef.current.play().catch(e => console.warn("Play failed on switch:", e));
243
  }
244
  } catch (error) {
245
  console.error(`❌ Error switching camera to ${targetFacingMode}:`, error);
 
246
  console.log(`Attempting to restore camera to: ${currentFacingMode}`);
247
  try {
248
  const restoredStream = await navigator.mediaDevices.getUserMedia({
249
+ video: { facingMode: currentFacingMode },
250
  audio: false,
251
  });
252
  setActiveLocalVideoStream(restoredStream);
253
  onVideoStreamChange(restoredStream);
 
254
  if (videoRef.current) {
255
  videoRef.current.srcObject = restoredStream;
256
  videoRef.current.play().catch(e => console.warn("Play failed on restore:", e));
257
  }
258
  } catch (restoreError) {
259
  console.error('❌ Error restoring camera:', restoreError);
 
260
  stopWebcam();
261
  onAppCamToggle(false);
262
  }
 
265
  }
266
  };
267
 
 
268
  useEffect(() => {
269
  if (!connected) {
270
  if (activeLocalVideoStream) {
271
  activeLocalVideoStream.getTracks().forEach(track => track.stop());
272
  setActiveLocalVideoStream(null);
273
  onVideoStreamChange(null);
274
+ onAppCamToggle(false);
275
  }
276
+ // --- START OF CORRECTION ---
277
+ if (audioRecorder.recording) { // Changed from isRecording()
278
  audioRecorder.stop();
279
+ onAppMicToggle(false);
280
  }
281
+ // --- END OF CORRECTION ---
282
  }
283
  // eslint-disable-next-line react-hooks/exhaustive-deps
284
+ }, [connected, audioRecorder, activeLocalVideoStream, onAppCamToggle, onAppMicToggle, onVideoStreamChange]);
285
+ // Added dependencies to useEffect based on usage
286
 
287
 
288
  return (
 
291
  'layout-with-small-logo': isAppCamActive
292
  })}>
293
  <canvas style={{ display: "none" }} ref={renderCanvasRef} />
 
294
  <div id="cam-button-wrapper" className="relative flex justify-center">
295
  <button
296
  id="cam-button"
 
314
  </div>
315
  </div>
316
 
 
317
  {isAppCamActive && (
318
  <div id="small-logo-container" className="flex items-center justify-center">
319
  {createLogoFunction(true, isAppCamActive, 'human')}
320
  </div>
321
  )}
322
 
 
323
  <button
324
  id="mic-button"
325
  className="control-button mic-button-color"
326
  onClick={handleMicToggle}
327
+ disabled={isSwitchingCamera}
328
  title={isAppMicActive ? "Mute Microphone" : "Unmute Microphone"}
329
  >
330
  {isAppMicActive ? <SvgPauseIcon /> : <SvgMicrophoneIcon />}
331
  </button>
332
 
 
 
333
  <button
334
  style={{ display: 'none'}}
335
  onClick={async () => {