Ezmary commited on
Commit
688a2e9
·
verified ·
1 Parent(s): fd05d74

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

Browse files
src/components/control-tray/ControlTray.tsx CHANGED
@@ -1,28 +1,15 @@
1
  /**
2
  * Copyright 2024 Google LLC
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
  */
16
 
17
  import cn from "classnames";
18
-
19
  import { memo, ReactNode, RefObject, useEffect, useRef, useState } from "react";
20
  import { useLiveAPIContext } from "../../contexts/LiveAPIContext";
21
  import { UseMediaStreamResult } from "../../hooks/use-media-stream-mux";
22
  import { useScreenCapture } from "../../hooks/use-screen-capture";
23
  import { useWebcam } from "../../hooks/use-webcam";
24
  import { AudioRecorder } from "../../lib/audio-recorder";
25
- import { audioContext } from "../../lib/utils";
26
  import { isIOS } from "../../lib/platform";
27
  import AudioPulse from "../audio-pulse/AudioPulse";
28
  import "./control-tray.scss";
@@ -40,19 +27,17 @@ type MediaStreamButtonProps = {
40
  offIcon: string;
41
  start: () => Promise<any>;
42
  stop: () => any;
 
43
  };
44
 
45
- /**
46
- * button used for triggering webcam or screen-capture
47
- */
48
  const MediaStreamButton = memo(
49
- ({ isStreaming, onIcon, offIcon, start, stop }: MediaStreamButtonProps) =>
50
  isStreaming ? (
51
- <button className="action-button" onClick={stop}>
52
  <span className="material-symbols-outlined">{onIcon}</span>
53
  </button>
54
  ) : (
55
- <button className="action-button" onClick={start}>
56
  <span className="material-symbols-outlined">{offIcon}</span>
57
  </button>
58
  ),
@@ -64,50 +49,56 @@ function ControlTray({
64
  onVideoStreamChange = () => {},
65
  supportsVideo,
66
  }: ControlTrayProps) {
67
- const videoStreams = [useWebcam(), useScreenCapture()];
68
- const [activeVideoStream, setActiveVideoStream] =
69
- useState<MediaStream | null>(null);
70
- const [webcam, screenCapture] = videoStreams;
 
 
 
71
  const [inVolume, setInVolume] = useState(0);
72
  const [audioRecorder] = useState(() => new AudioRecorder());
73
  const [muted, setMuted] = useState(false);
74
- const [videoDevices, setVideoDevices] = useState<MediaDeviceInfo[]>([]);
75
- const [currentDeviceIndex, setCurrentDeviceIndex] = useState(0);
76
  const renderCanvasRef = useRef<HTMLCanvasElement>(null);
77
  const connectButtonRef = useRef<HTMLButtonElement>(null);
78
  const [simulatedVolume, setSimulatedVolume] = useState(0);
79
  const isIOSDevice = isIOS();
80
 
81
- const { client, connected, connect, disconnect, volume } =
82
- useLiveAPIContext();
83
-
84
- // Add iOS detection
85
  const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
86
 
 
 
 
 
 
 
 
 
 
 
 
87
  useEffect(() => {
88
  if (!connected && connectButtonRef.current) {
89
- console.log('🎯 Setting focus on connect button - connection status:', connected);
90
  connectButtonRef.current.focus();
91
  }
92
  }, [connected]);
93
 
94
- // Add iOS volume simulation effect
95
  useEffect(() => {
 
96
  if (isIOSDevice && connected && !muted) {
97
- console.log('📱 Starting iOS volume simulation');
98
- const interval = setInterval(() => {
99
- // Create a smooth pulsing effect
100
- const pulse = (Math.sin(Date.now() / 500) + 1) / 2; // Values between 0 and 1
101
- setSimulatedVolume(0.02 + pulse * 0.03); // Small range for subtle effect
102
  }, 50);
103
-
104
- return () => {
105
- console.log('🛑 Stopping iOS volume simulation');
106
- clearInterval(interval);
107
- };
108
  }
 
 
 
109
  }, [connected, muted, isIOSDevice]);
110
 
 
111
  useEffect(() => {
112
  document.documentElement.style.setProperty(
113
  "--volume",
@@ -115,236 +106,239 @@ function ControlTray({
115
  );
116
  }, [inVolume, simulatedVolume, isIOSDevice]);
117
 
 
118
  useEffect(() => {
119
  const onData = (base64: string) => {
120
- console.log('🎤 Sending audio data chunk');
121
- client.sendRealtimeInput([
122
- {
123
- mimeType: "audio/pcm;rate=16000",
124
- data: base64,
125
- },
126
- ]);
127
  };
128
-
129
  if (connected && !muted && audioRecorder) {
130
- console.log('🎙️ Starting audio recorder');
131
  audioRecorder.on("data", onData).on("volume", setInVolume).start();
132
- } else {
133
- console.log('⏹️ Stopping audio recorder');
134
  audioRecorder.stop();
135
  }
136
-
137
  return () => {
138
- console.log('🧹 Cleaning up audio recorder listeners');
139
- audioRecorder.off("data", onData).off("volume", setInVolume);
 
140
  };
141
  }, [connected, client, muted, audioRecorder]);
142
 
 
143
  useEffect(() => {
144
- if (videoRef.current) {
145
- console.log('🎥 Setting video stream:', activeVideoStream ? 'active' : 'null');
146
- videoRef.current.srcObject = activeVideoStream;
 
 
 
 
 
 
147
  }
 
148
 
 
 
149
  let timeoutId = -1;
150
-
151
  function sendVideoFrame() {
152
- const video = videoRef.current;
153
- const canvas = renderCanvasRef.current;
154
-
155
- if (!video || !canvas) {
156
- console.log('⚠️ Missing video or canvas reference');
157
- return;
158
- }
159
-
160
- const ctx = canvas.getContext("2d")!;
161
- canvas.width = video.videoWidth * 0.25;
162
- canvas.height = video.videoHeight * 0.25;
163
- if (canvas.width + canvas.height > 0) {
164
- ctx.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height);
165
- const base64 = canvas.toDataURL("image/jpeg", 1.0);
166
- const data = base64.slice(base64.indexOf(",") + 1, Infinity);
167
- console.log('📸 Sending video frame');
168
- client.sendRealtimeInput([{ mimeType: "image/jpeg", data }]);
169
- }
170
- if (connected) {
171
  timeoutId = window.setTimeout(sendVideoFrame, 1000 / 0.5);
172
  }
 
 
 
 
 
 
 
 
 
 
 
 
173
  }
174
- if (connected && activeVideoStream !== null) {
175
- console.log('🎬 Starting video frame capture');
176
- requestAnimationFrame(sendVideoFrame);
177
- }
178
- return () => {
179
- console.log('⏹️ Stopping video frame capture');
180
- clearTimeout(timeoutId);
181
- };
182
  }, [connected, activeVideoStream, client, videoRef]);
183
 
184
- // Add effect to track available video devices
185
  useEffect(() => {
186
- async function getVideoDevices() {
187
- console.log('📹 Enumerating video devices...');
188
- try {
189
- const devices = await navigator.mediaDevices.enumerateDevices();
190
- const videoInputs = devices.filter(device => device.kind === 'videoinput');
191
- console.log('��� Available video devices:', videoInputs.length);
192
- videoInputs.forEach((device, index) => {
193
- console.log(`📹 Device ${index}:`, {
194
- deviceId: device.deviceId,
195
- label: device.label
196
- });
197
- });
198
- setVideoDevices(videoInputs);
199
- } catch (error) {
200
- console.error('❌ Error enumerating devices:', error);
201
  }
202
  }
 
 
203
 
204
- // Get initial device list
205
- getVideoDevices();
206
 
207
- // Listen for device changes
208
- navigator.mediaDevices.addEventListener('devicechange', getVideoDevices);
209
- return () => {
210
- navigator.mediaDevices.removeEventListener('devicechange', getVideoDevices);
211
- };
212
- }, []);
 
 
 
 
213
 
214
- const rotateWebcam = async () => {
215
- console.log('🔄 Rotating webcam...');
216
- if (videoDevices.length <= 1) {
217
- console.log('⚠️ Not enough video devices to rotate');
218
- return;
 
 
219
  }
220
 
221
- const nextIndex = (currentDeviceIndex + 1) % videoDevices.length;
222
- console.log(`🎯 Switching to device index ${nextIndex}`);
223
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  try {
225
- const newStream = await navigator.mediaDevices.getUserMedia({
226
- video: { deviceId: { exact: videoDevices[nextIndex].deviceId } },
227
- audio: false
228
- });
229
-
230
- console.log('✅ Got new video stream');
231
- setActiveVideoStream(newStream);
232
- onVideoStreamChange(newStream);
233
- setCurrentDeviceIndex(nextIndex);
234
-
235
- // Clean up old stream
236
- if (activeVideoStream) {
237
- activeVideoStream.getTracks().forEach(track => track.stop());
238
  }
239
- } catch (error) {
240
- console.error('❌ Error rotating webcam:', error);
 
 
 
241
  }
242
  };
243
 
244
- //handler for swapping from one video-stream to the next
245
- const changeStreams = (next?: UseMediaStreamResult) => async () => {
246
- if (next) {
247
- console.log('🔄 Starting new video stream');
248
- const mediaStream = await next.start();
249
- setActiveVideoStream(mediaStream);
250
- onVideoStreamChange(mediaStream);
251
- } else {
252
- console.log('⏹️ Stopping video stream');
253
- setActiveVideoStream(null);
254
- onVideoStreamChange(null);
255
- }
256
 
257
- videoStreams.filter((msr) => msr !== next).forEach((msr) => {
258
- console.log('🛑 Stopping other video streams');
259
- msr.stop();
260
- });
261
- };
262
 
263
  return (
264
  <section className="control-tray">
265
  <canvas style={{ display: "none" }} ref={renderCanvasRef} />
 
266
  <nav className={cn("actions-nav", { disabled: !connected })}>
 
267
  <button
268
  className={cn("action-button mic-button")}
269
  onClick={() => setMuted(!muted)}
 
 
270
  >
271
- {!muted ? (
272
- <span className="material-symbols-outlined filled">mic</span>
273
- ) : (
274
- <span className="material-symbols-outlined filled">mic_off</span>
275
- )}
276
  </button>
277
 
 
278
  <div className="action-button no-action outlined">
279
- <AudioPulse volume={volume} active={connected} hover={false} />
280
  </div>
281
 
 
282
  {supportsVideo && (
283
  <>
284
- {!isIOSDevice && (
 
285
  <MediaStreamButton
286
- isStreaming={screenCapture.isStreaming}
287
- start={changeStreams(screenCapture)}
288
- stop={changeStreams()}
289
  onIcon="cancel_presentation"
290
  offIcon="present_to_all"
 
 
 
291
  />
292
  )}
293
- {webcam.isStreaming && videoDevices.length > 1 && (
294
- <button
295
- className="action-button"
 
 
296
  onClick={rotateWebcam}
297
  title="Switch camera"
 
298
  >
299
  <span className="material-symbols-outlined">switch_camera</span>
300
  </button>
301
  )}
 
 
302
  <MediaStreamButton
303
- isStreaming={webcam.isStreaming}
304
- start={changeStreams(webcam)}
305
- stop={changeStreams()}
306
  onIcon="videocam_off"
307
  offIcon="videocam"
 
308
  />
309
  </>
310
  )}
311
  {children}
312
  </nav>
313
 
 
314
  <div className={cn("connection-container", { connected })}>
315
  <div className="connection-button-container">
316
  <button
317
  ref={connectButtonRef}
318
  className={cn("action-button connect-toggle", { connected })}
319
  onClick={async () => {
320
- console.log('🎮 ControlTray: Connection button clicked');
321
- try {
322
- if (connected) {
323
- console.log('📴 ControlTray: Initiating disconnect...');
324
- await disconnect();
325
- console.log('✅ ControlTray: Disconnected successfully');
326
- } else {
327
- console.log('🔌 ControlTray: Starting connection process...');
328
- console.log('📱 ControlTray: Device info:', { isIOSDevice, isSafari });
329
-
330
- console.log('📞 ControlTray: Calling LiveAPIContext.connect()...');
331
- await connect();
332
- console.log('✅ ControlTray: Connected successfully via LiveAPIContext');
333
- }
334
- } catch (err) {
335
- console.error('❌ ControlTray: Connection error:', err);
336
- }
337
  }}
 
 
338
  >
339
- <span className="material-symbols-outlined filled">
340
- {connected ? "pause" : "play_arrow"}
341
- </span>
342
  </button>
343
  </div>
344
- <span className="text-indicator">Streaming</span>
345
  </div>
346
  </section>
347
  );
348
  }
349
 
350
- export default memo(ControlTray);
 
1
  /**
2
  * Copyright 2024 Google LLC
3
+ * ... (لایسنس و توضیحات دیگر مثل قبل) ...
 
 
 
 
 
 
 
 
 
 
 
4
  */
5
 
6
  import cn from "classnames";
 
7
  import { memo, ReactNode, RefObject, useEffect, useRef, useState } from "react";
8
  import { useLiveAPIContext } from "../../contexts/LiveAPIContext";
9
  import { UseMediaStreamResult } from "../../hooks/use-media-stream-mux";
10
  import { useScreenCapture } from "../../hooks/use-screen-capture";
11
  import { useWebcam } from "../../hooks/use-webcam";
12
  import { AudioRecorder } from "../../lib/audio-recorder";
 
13
  import { isIOS } from "../../lib/platform";
14
  import AudioPulse from "../audio-pulse/AudioPulse";
15
  import "./control-tray.scss";
 
27
  offIcon: string;
28
  start: () => Promise<any>;
29
  stop: () => any;
30
+ disabled?: boolean;
31
  };
32
 
 
 
 
33
  const MediaStreamButton = memo(
34
+ ({ isStreaming, onIcon, offIcon, start, stop, disabled }: MediaStreamButtonProps) =>
35
  isStreaming ? (
36
+ <button className="action-button" onClick={stop} title={`Stop ${offIcon}`} disabled={disabled}>
37
  <span className="material-symbols-outlined">{onIcon}</span>
38
  </button>
39
  ) : (
40
+ <button className="action-button" onClick={start} title={`Start ${offIcon}`} disabled={disabled}>
41
  <span className="material-symbols-outlined">{offIcon}</span>
42
  </button>
43
  ),
 
49
  onVideoStreamChange = () => {},
50
  supportsVideo,
51
  }: ControlTrayProps) {
52
+ const webcam = useWebcam();
53
+ const screenCapture = useScreenCapture();
54
+
55
+ const [activeVideoStream, setActiveVideoStream] = useState<MediaStream | null>(null);
56
+ const [currentFacingMode, setCurrentFacingMode] = useState<'user' | 'environment' | null>(null);
57
+ const [isSwitchingCamera, setIsSwitchingCamera] = useState(false);
58
+ const [isLikelyDesktop, setIsLikelyDesktop] = useState(false); // <-- State برای تشخیص دسکتاپ
59
  const [inVolume, setInVolume] = useState(0);
60
  const [audioRecorder] = useState(() => new AudioRecorder());
61
  const [muted, setMuted] = useState(false);
 
 
62
  const renderCanvasRef = useRef<HTMLCanvasElement>(null);
63
  const connectButtonRef = useRef<HTMLButtonElement>(null);
64
  const [simulatedVolume, setSimulatedVolume] = useState(0);
65
  const isIOSDevice = isIOS();
66
 
67
+ const { client, connected, connect, disconnect, volume } = useLiveAPIContext();
 
 
 
68
  const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
69
 
70
+ // --- useEffect ها ---
71
+
72
+ // بررسی نوع دستگاه (دسکتاپ یا موبایل/تبلت) در زمان Mount
73
+ useEffect(() => {
74
+ // navigator.maxTouchPoints > 0 معمولا نشان‌دهنده دستگاه لمسی است
75
+ const desktopCheck = typeof navigator !== 'undefined' && navigator.maxTouchPoints <= 0;
76
+ setIsLikelyDesktop(desktopCheck);
77
+ console.log(`Device check: Likely Desktop? ${desktopCheck} (maxTouchPoints: ${navigator.maxTouchPoints})`);
78
+ }, []); // فقط یک بار در زمان Mount اجرا شود
79
+
80
+ // Focus effect
81
  useEffect(() => {
82
  if (!connected && connectButtonRef.current) {
 
83
  connectButtonRef.current.focus();
84
  }
85
  }, [connected]);
86
 
87
+ // iOS volume simulation
88
  useEffect(() => {
89
+ let interval: number | undefined;
90
  if (isIOSDevice && connected && !muted) {
91
+ interval = window.setInterval(() => {
92
+ const pulse = (Math.sin(Date.now() / 500) + 1) / 2;
93
+ setSimulatedVolume(0.02 + pulse * 0.03);
 
 
94
  }, 50);
 
 
 
 
 
95
  }
96
+ return () => {
97
+ if (interval) clearInterval(interval);
98
+ };
99
  }, [connected, muted, isIOSDevice]);
100
 
101
+ // CSS volume update
102
  useEffect(() => {
103
  document.documentElement.style.setProperty(
104
  "--volume",
 
106
  );
107
  }, [inVolume, simulatedVolume, isIOSDevice]);
108
 
109
+ // Audio recording
110
  useEffect(() => {
111
  const onData = (base64: string) => {
112
+ if (client && connected) {
113
+ client.sendRealtimeInput([{ mimeType: "audio/pcm;rate=16000", data: base64 }]);
114
+ }
 
 
 
 
115
  };
 
116
  if (connected && !muted && audioRecorder) {
 
117
  audioRecorder.on("data", onData).on("volume", setInVolume).start();
118
+ } else if (audioRecorder) {
 
119
  audioRecorder.stop();
120
  }
 
121
  return () => {
122
+ if (audioRecorder) {
123
+ audioRecorder.off("data", onData).off("volume", setInVolume).stop();
124
+ }
125
  };
126
  }, [connected, client, muted, audioRecorder]);
127
 
128
+ // Stop video on disconnect
129
  useEffect(() => {
130
+ if (!connected && activeVideoStream) {
131
+ console.log('🔌 Disconnected, stopping video stream.');
132
+ activeVideoStream.getTracks().forEach(track => track.stop());
133
+ setActiveVideoStream(null);
134
+ onVideoStreamChange(null);
135
+ setCurrentFacingMode(null);
136
+ setIsSwitchingCamera(false);
137
+ webcam.stop();
138
+ screenCapture.stop();
139
  }
140
+ }, [connected, activeVideoStream, onVideoStreamChange, webcam, screenCapture]);
141
 
142
+ // Video frame sending
143
+ useEffect(() => {
144
  let timeoutId = -1;
 
145
  function sendVideoFrame() {
146
+ if (connected && activeVideoStream) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  timeoutId = window.setTimeout(sendVideoFrame, 1000 / 0.5);
148
  }
149
+ const video = videoRef.current; const canvas = renderCanvasRef.current;
150
+ if (!video || !canvas || video.readyState < video.HAVE_METADATA || video.paused || video.ended || !client) return;
151
+ try {
152
+ const ctx = canvas.getContext("2d"); if (!ctx) return;
153
+ const scale = 0.25; canvas.width = video.videoWidth * scale; canvas.height = video.videoHeight * scale;
154
+ if (canvas.width > 0 && canvas.height > 0) {
155
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
156
+ const base64 = canvas.toDataURL("image/jpeg", 0.8);
157
+ const data = base64.slice(base64.indexOf(",") + 1);
158
+ client.sendRealtimeInput([{ mimeType: "image/jpeg", data }]);
159
+ }
160
+ } catch (error) { console.error("❌ Error processing video frame:", error); }
161
  }
162
+ if (connected && activeVideoStream && videoRef.current) { setTimeout(sendVideoFrame, 200); }
163
+ return () => { clearTimeout(timeoutId); };
 
 
 
 
 
 
164
  }, [connected, activeVideoStream, client, videoRef]);
165
 
166
+ // Assign stream to video element
167
  useEffect(() => {
168
+ if (videoRef.current) {
169
+ if (videoRef.current.srcObject !== activeVideoStream) {
170
+ videoRef.current.srcObject = activeVideoStream;
171
+ if (activeVideoStream) { videoRef.current.play().catch(e => console.warn("Video play failed:", e)); }
 
 
 
 
 
 
 
 
 
 
 
172
  }
173
  }
174
+ }, [activeVideoStream, videoRef]);
175
+ // --- پایان useEffect ها ---
176
 
 
 
177
 
178
+ // Function to stop all video streams
179
+ const stopAllVideoStreams = () => {
180
+ console.log('⏹️ Stopping all video streams...');
181
+ if (activeVideoStream) {
182
+ activeVideoStream.getTracks().forEach(track => track.stop());
183
+ setActiveVideoStream(null);
184
+ onVideoStreamChange(null);
185
+ }
186
+ webcam.stop(); screenCapture.stop(); setCurrentFacingMode(null); setIsSwitchingCamera(false);
187
+ };
188
 
189
+ // Handler for starting/stopping webcam or screen share
190
+ const changeStreams = (streamType: 'webcam' | 'screen' | 'none') => async () => {
191
+ if (isSwitchingCamera) return;
192
+ // Only allow starting screen share if it's likely a desktop
193
+ if (streamType === 'screen' && !isLikelyDesktop) {
194
+ console.warn("Screen share requested on non-desktop device, ignoring.");
195
+ return;
196
  }
197
 
198
+ stopAllVideoStreams();
199
+
200
+ if (streamType === 'webcam') {
201
+ const initialFacingMode = 'user'; console.log(`🚀 Starting webcam with initial facingMode: ${initialFacingMode}`);
202
+ try {
203
+ const mediaStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: initialFacingMode }, audio: false });
204
+ setActiveVideoStream(mediaStream); onVideoStreamChange(mediaStream); setCurrentFacingMode(initialFacingMode);
205
+ } catch (error) {
206
+ console.error(`❌ Error starting webcam with ${initialFacingMode}:`, error);
207
+ try { // Fallback
208
+ const fallbackStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' }, audio: false });
209
+ setActiveVideoStream(fallbackStream); onVideoStreamChange(fallbackStream); setCurrentFacingMode('environment');
210
+ } catch (fallbackError) { console.error('❌ Error starting webcam fallback:', fallbackError); setActiveVideoStream(null); onVideoStreamChange(null); setCurrentFacingMode(null); }
211
+ }
212
+ } else if (streamType === 'screen' && isLikelyDesktop) { // Double check desktop condition
213
+ console.log('🚀 Starting screen capture');
214
+ try {
215
+ const mediaStream = await screenCapture.start();
216
+ setActiveVideoStream(mediaStream); onVideoStreamChange(mediaStream); setCurrentFacingMode(null);
217
+ } catch (error) { console.error('❌ Error starting screen capture:', error); setActiveVideoStream(null); onVideoStreamChange(null); setCurrentFacingMode(null); }
218
+ } else { console.log('ℹ️ Video stream turned off or invalid request.'); }
219
+ };
220
+
221
+ // Handler for rotating webcam
222
+ const rotateWebcam = async () => {
223
+ if (isSwitchingCamera || !activeVideoStream || currentFacingMode === null) return;
224
+ const targetFacingMode = currentFacingMode === 'user' ? 'environment' : 'user';
225
+ console.log(`🔄 Rotating webcam... Target: ${targetFacingMode}`);
226
+ setIsSwitchingCamera(true);
227
+ activeVideoStream.getTracks().forEach(track => track.stop()); // Stop tracks only
228
+
229
  try {
230
+ const newStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { exact: targetFacingMode } }, audio: false });
231
+ if (videoRef.current) { videoRef.current.srcObject = newStream; videoRef.current.play().catch(e => console.warn("Play fail switch:", e)); }
232
+ setActiveVideoStream(newStream); onVideoStreamChange(newStream); setCurrentFacingMode(targetFacingMode);
233
+ } catch (error: any) {
234
+ console.error(`❌ Error switching camera:`, error.name);
235
+ let recoveredStream: MediaStream | null = null; // Fallback logic...
236
+ if (error.name === 'OverconstrainedError' || error.name === 'ConstraintNotSatisfiedError') {
237
+ try { recoveredStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: targetFacingMode }, audio: false }); setCurrentFacingMode(targetFacingMode); } catch (retryError: any) { console.error(`Retry fail:`, retryError.name); }
 
 
 
 
 
238
  }
239
+ if (!recoveredStream) { try { recoveredStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { exact: currentFacingMode } }, audio: false }); } catch (restoreError) { console.error(`Restore fail:`, restoreError); } }
240
+ if (recoveredStream) { if (videoRef.current) { videoRef.current.srcObject = recoveredStream; videoRef.current.play().catch(e => console.warn("Play fail recovery:", e)); } setActiveVideoStream(recoveredStream); onVideoStreamChange(recoveredStream); }
241
+ else { if (videoRef.current) videoRef.current.srcObject = null; setActiveVideoStream(null); onVideoStreamChange(null); setCurrentFacingMode(null); }
242
+ } finally {
243
+ setIsSwitchingCamera(false);
244
  }
245
  };
246
 
247
+ // Determine stable streaming states
248
+ const isWebcamStableStreaming = activeVideoStream !== null && currentFacingMode !== null;
249
+ const isScreenCaptureStreaming = screenCapture.isStreaming && activeVideoStream !== null && currentFacingMode === null;
250
+
251
+ // Determine if the webcam button should appear active
252
+ const showWebcamAsActive = isWebcamStableStreaming || (isSwitchingCamera && currentFacingMode !== null);
253
+
254
+ // **** Condition to show Screen Share Button ****
255
+ const showScreenShareButton = supportsVideo && !isIOSDevice && isLikelyDesktop;
 
 
 
256
 
 
 
 
 
 
257
 
258
  return (
259
  <section className="control-tray">
260
  <canvas style={{ display: "none" }} ref={renderCanvasRef} />
261
+
262
  <nav className={cn("actions-nav", { disabled: !connected })}>
263
+ {/* Mic Button */}
264
  <button
265
  className={cn("action-button mic-button")}
266
  onClick={() => setMuted(!muted)}
267
+ disabled={!connected || isSwitchingCamera}
268
+ title={muted ? "Unmute Microphone" : "Mute Microphone"}
269
  >
270
+ <span className="material-symbols-outlined filled">{muted ? "mic_off" : "mic"}</span>
 
 
 
 
271
  </button>
272
 
273
+ {/* Volume Indicator */}
274
  <div className="action-button no-action outlined">
275
+ <AudioPulse volume={volume} active={connected && !muted} hover={false} />
276
  </div>
277
 
278
+ {/* Video Controls */}
279
  {supportsVideo && (
280
  <>
281
+ {/* Screen Share Button (Show only on non-iOS Desktop-like devices) */}
282
+ {showScreenShareButton && ( // <-- شرط جدید اینجا اعمال شده
283
  <MediaStreamButton
284
+ isStreaming={isScreenCaptureStreaming}
285
+ start={changeStreams('screen')}
286
+ stop={changeStreams('none')}
287
  onIcon="cancel_presentation"
288
  offIcon="present_to_all"
289
+ // Disable screen share button also if webcam is active or switching? Your choice.
290
+ // disabled={!connected || isSwitchingCamera || showWebcamAsActive}
291
+ disabled={!connected || isSwitchingCamera } // Kept simpler for now
292
  />
293
  )}
294
+
295
+ {/* Switch Camera Button */}
296
+ { (isWebcamStableStreaming || isSwitchingCamera) && (
297
+ <button
298
+ className="action-button"
299
  onClick={rotateWebcam}
300
  title="Switch camera"
301
+ disabled={!connected || isSwitchingCamera}
302
  >
303
  <span className="material-symbols-outlined">switch_camera</span>
304
  </button>
305
  )}
306
+
307
+ {/* Webcam On/Off Button */}
308
  <MediaStreamButton
309
+ isStreaming={showWebcamAsActive}
310
+ start={changeStreams('webcam')}
311
+ stop={changeStreams('none')}
312
  onIcon="videocam_off"
313
  offIcon="videocam"
314
+ disabled={!connected || isSwitchingCamera}
315
  />
316
  </>
317
  )}
318
  {children}
319
  </nav>
320
 
321
+ {/* Connection Controls */}
322
  <div className={cn("connection-container", { connected })}>
323
  <div className="connection-button-container">
324
  <button
325
  ref={connectButtonRef}
326
  className={cn("action-button connect-toggle", { connected })}
327
  onClick={async () => {
328
+ if (isSwitchingCamera) return;
329
+ try { if (connected) { await disconnect(); } else { await connect(); } }
330
+ catch (err) { console.error('❌ Connection/Disconnection error:', err); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  }}
332
+ disabled={isSwitchingCamera}
333
+ title={connected ? "Disconnect Stream" : "Connect Stream"}
334
  >
335
+ <span className="material-symbols-outlined filled">{connected ? "pause" : "play_arrow"}</span>
 
 
336
  </button>
337
  </div>
338
+ <span className="text-indicator">{connected ? "Streaming" : "Paused"}</span>
339
  </div>
340
  </section>
341
  );
342
  }
343
 
344
+ export default memo(ControlTray);