/** Copyright 2024 Google LLC ... (لایسنس و توضیحات دیگر مثل قبل) ... */ import cn from "classnames"; import React, { memo, ReactNode, RefObject, useEffect, useRef, useState } from "react"; import { useLiveAPIContext } from "../../contexts/LiveAPIContext"; // import { UseMediaStreamResult } from "../../hooks/use-media-stream-mux"; // دیگر استفاده نمی‌شود؟ // import { useScreenCapture } from "../../hooks/use-screen-capture"; // اگر نیاز به اشتراک صفحه نیست، حذف شود // import { useWebcam } from "../../hooks/use-webcam"; // منطق وب‌کم در همین فایل ادغام شده import { AudioRecorder } from "../../lib/audio-recorder"; import { isIOS } from "../../lib/platform"; // import AudioPulse from "../audio-pulse/AudioPulse"; // دیگر استفاده نمی‌شود import "./control-tray.scss"; // ممکن است خالی یا حاوی استایل‌های بسیار کمی باشد // SVG Icons for Controls const SvgPauseIcon = () => ; const SvgMicrophoneIcon = () => ; const SvgCameraIcon = () => ; const SvgStopCamIcon = () => ; const SvgSwitchCameraIcon = () => ; export type ControlTrayProps = { videoRef: RefObject; children?: ReactNode; // ممکن است دیگر استفاده نشود supportsVideo: boolean; onVideoStreamChange: (stream: MediaStream | null) => void; // Props جدید از App.tsx isAppMicActive: boolean; onAppMicToggle: (active: boolean) => void; isAppCamActive: boolean; onAppCamToggle: (active: boolean) => void; createLogoFunction: (isMini: boolean, isActive: boolean, type?: 'human' | 'ai') => ReactNode; }; function ControlTray({ videoRef, onVideoStreamChange, supportsVideo, isAppMicActive, onAppMicToggle, isAppCamActive, onAppCamToggle, createLogoFunction, }: ControlTrayProps) { const { client, connected, connect, disconnect, volume } // volume ممکن است برای UI جدید استفاده نشود = useLiveAPIContext(); const [audioRecorder] = useState(() => new AudioRecorder()); const [muted, setMuted] = useState(true); // Initial state, will be synced with isAppMicActive const [inVolume, setInVolume] = useState(0); // برای نمایش ولوم در آینده، اگر نیاز شد const [activeLocalVideoStream, setActiveLocalVideoStream] = useState(null); const [currentFacingMode, setCurrentFacingMode] = useState<'user' | 'environment'>('user'); const [isSwitchingCamera, setIsSwitchingCamera] = useState(false); // Sync with App's mic/cam state useEffect(() => { setMuted(!isAppMicActive); }, [isAppMicActive]); useEffect(() => { if (isAppCamActive && !activeLocalVideoStream) { startWebcam(); } else if (!isAppCamActive && activeLocalVideoStream) { stopWebcam(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isAppCamActive]); // Audio recording logic (from your original ControlTray) useEffect(() => { const onData = (base64: string) => { if (client && connected) { client.sendRealtimeInput([{ mimeType: "audio/pcm;rate=16000", data: base64 }]); } }; if (connected && !muted && audioRecorder) { audioRecorder.on("data", onData).on("volume", setInVolume).start(); } else if (audioRecorder) { audioRecorder.stop(); } return () => { if (audioRecorder) { audioRecorder.off("data", onData).off("volume", setInVolume).stop(); } }; }, [connected, client, muted, audioRecorder]); // Video frame sending logic (simplified from your original ControlTray) const renderCanvasRef = useRef(null); useEffect(() => { let timeoutId = -1; function sendVideoFrame() { if (connected && activeLocalVideoStream) { timeoutId = window.setTimeout(sendVideoFrame, 1000 / 0.5); // ~2 FPS } const video = videoRef.current; const canvas = renderCanvasRef.current; if (!video || !canvas || video.readyState < video.HAVE_METADATA || video.paused || video.ended || !client) return; try { const ctx = canvas.getContext("2d"); if (!ctx) return; const scale = 0.25; // کاهش اندازه فریم برای ارسال canvas.width = video.videoWidth * scale; canvas.height = video.videoHeight * scale; if (canvas.width > 0 && canvas.height > 0) { ctx.drawImage(video, 0, 0, canvas.width, canvas.height); const base64 = canvas.toDataURL("image/jpeg", 0.7); // کیفیت JPEG const data = base64.slice(base64.indexOf(",") + 1); client.sendRealtimeInput([{ mimeType: "image/jpeg", data }]); } } catch (error) { console.error("❌ Error processing video frame:", error); } } if (connected && activeLocalVideoStream && videoRef.current) { setTimeout(sendVideoFrame, 200); // Start sending frames after a short delay } return () => { clearTimeout(timeoutId); }; }, [connected, activeLocalVideoStream, client, videoRef]); // Assign stream to video element useEffect(() => { if (videoRef.current) { if (videoRef.current.srcObject !== activeLocalVideoStream) { videoRef.current.srcObject = activeLocalVideoStream; if (activeLocalVideoStream) { videoRef.current.play().catch(e => console.warn("Video play failed:", e)); } } } }, [activeLocalVideoStream, videoRef]); const ensureConnected = async () => { if (!connected) { try { await connect(); } catch (err) { console.error('❌ Connection error:', err); return false; } } return true; }; const handleMicToggle = async () => { if (isSwitchingCamera) return; const newMicState = !isAppMicActive; if (newMicState) { // Turning mic ON if(!await ensureConnected()) { onAppMicToggle(false); // Failed to connect, revert UI return; } } onAppMicToggle(newMicState); // If mic is turned off and cam is also off, consider disconnecting if (!newMicState && !isAppCamActive && connected) { // await disconnect(); // یا تصمیم بگیرید که اتصال را نگه دارید } }; const startWebcam = async (facingModeToTry: 'user' | 'environment' = currentFacingMode) => { if (isSwitchingCamera) return; setIsSwitchingCamera(true); // Prevent rapid clicks if(!await ensureConnected()) { onAppCamToggle(false); // Failed to connect, revert UI setIsSwitchingCamera(false); return; } try { console.log(`🚀 Starting webcam with facingMode: ${facingModeToTry}`); const mediaStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: facingModeToTry }, audio: false, // Audio is handled by AudioRecorder }); setActiveLocalVideoStream(mediaStream); onVideoStreamChange(mediaStream); // Update App.tsx's videoStream if needed for other components setCurrentFacingMode(facingModeToTry); onAppCamToggle(true); // Update App's state } catch (error) { console.error(`❌ Error starting webcam with ${facingModeToTry}:`, error); // Try fallback if initial failed if (facingModeToTry === 'user' && (error as Error).name !== 'NotFoundError') { // Avoid retry if no camera found try { const fallbackStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' }, audio: false }); setActiveLocalVideoStream(fallbackStream); onVideoStreamChange(fallbackStream); setCurrentFacingMode('environment'); onAppCamToggle(true); } catch (fallbackError) { console.error('❌ Error starting webcam fallback:', fallbackError); setActiveLocalVideoStream(null); onVideoStreamChange(null); onAppCamToggle(false); } } else { setActiveLocalVideoStream(null); onVideoStreamChange(null); onAppCamToggle(false); } } finally { setIsSwitchingCamera(false); } }; const stopWebcam = () => { if (activeLocalVideoStream) { activeLocalVideoStream.getTracks().forEach(track => track.stop()); } setActiveLocalVideoStream(null); onVideoStreamChange(null); onAppCamToggle(false); // Update App's state // If mic is also off, consider disconnecting if (!isAppMicActive && connected) { // await disconnect(); // یا تصمیم بگیرید که اتصال را نگه دارید } }; const handleCamToggle = () => { if (isAppCamActive) { stopWebcam(); } else { startWebcam(); } }; const handleSwitchCamera = async () => { if (!isAppCamActive || !activeLocalVideoStream || isSwitchingCamera) return; setIsSwitchingCamera(true); const targetFacingMode = currentFacingMode === 'user' ? 'environment' : 'user'; // Stop current tracks before requesting new ones activeLocalVideoStream.getTracks().forEach(track => track.stop()); setActiveLocalVideoStream(null); // Clear stream temporarily onVideoStreamChange(null); try { console.log(`🔄 Switching camera to: ${targetFacingMode}`); const newStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { exact: targetFacingMode } }, audio: false, }); setActiveLocalVideoStream(newStream); onVideoStreamChange(newStream); setCurrentFacingMode(targetFacingMode); // Ensure video element plays the new stream if (videoRef.current) { videoRef.current.srcObject = newStream; videoRef.current.play().catch(e => console.warn("Play failed on switch:", e)); } } catch (error) { console.error(`❌ Error switching camera to ${targetFacingMode}:`, error); // Try to restore previous stream or a default if switching failed console.log(`Attempting to restore camera to: ${currentFacingMode}`); try { const restoredStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: currentFacingMode }, // Try previous mode audio: false, }); setActiveLocalVideoStream(restoredStream); onVideoStreamChange(restoredStream); // No need to change currentFacingMode as it's restored if (videoRef.current) { videoRef.current.srcObject = restoredStream; videoRef.current.play().catch(e => console.warn("Play failed on restore:", e)); } } catch (restoreError) { console.error('❌ Error restoring camera:', restoreError); // If all fails, turn off camera stopWebcam(); onAppCamToggle(false); } } finally { setIsSwitchingCamera(false); } }; // Stop streams on disconnect useEffect(() => { if (!connected) { if (activeLocalVideoStream) { activeLocalVideoStream.getTracks().forEach(track => track.stop()); setActiveLocalVideoStream(null); onVideoStreamChange(null); onAppCamToggle(false); // Ensure UI consistency } if (audioRecorder.isRecording()) { audioRecorder.stop(); onAppMicToggle(false); // Ensure UI consistency } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [connected]); return ( ); } export default memo(ControlTray);