// src/components/control-tray/ControlTray.tsx /** Copyright 2024 Google LLC ... (لایسنس) ... */ import cn from "classnames"; import React, { memo, ReactNode, RefObject, useEffect, useState, useCallback } from "react"; // useCallback اضافه شد import { useLiveAPIContext } from "../../contexts/LiveAPIContext"; import { AudioRecorder } from "../../lib/audio-recorder"; import LogoAnimation from '../logo-animation/LogoAnimation'; const SvgPauseIcon = () => ; const SvgCameraIcon = () => ; const SvgStopCamIcon = () => ; const SvgSwitchCameraIcon = () => ; export type ControlTrayProps = { videoRef: RefObject; supportsVideo: boolean; onVideoStreamChange: (stream: MediaStream | null) => void; isAppMicActive: boolean; onAppMicToggle: (active: boolean) => void; isAppCamActive: boolean; onAppCamToggle: (active: boolean) => void; ReferenceMicrophoneIcon: () => JSX.Element; currentFacingMode: 'user' | 'environment'; setCurrentFacingMode: React.Dispatch>; }; const ControlTray: React.FC = ({ videoRef, onVideoStreamChange, supportsVideo, isAppMicActive, onAppMicToggle, isAppCamActive, onAppCamToggle, ReferenceMicrophoneIcon, currentFacingMode, setCurrentFacingMode, }) => { const { client, connected, connect, disconnect: liveApiDisconnect } = useLiveAPIContext(); // disconnect به liveApiDisconnect تغییر نام یافت const [audioRecorder] = useState(() => new AudioRecorder()); const [activeLocalVideoStream, setActiveLocalVideoStream] = useState(null); const [isSwitchingCamera, setIsSwitchingCamera] = useState(false); const renderCanvasRef = React.useRef(null); const ensureConnectedAndReady = useCallback(async (): Promise => { if (!connected) { try { await connect(); return true; } catch (err) { console.error('❌ CT Connect err:', err); return false; } } return true; }, [connected, connect]); const _startWebcam = useCallback(async (facingModeToTry: 'user' | 'environment'): Promise => { // isSwitchingCamera در اینجا کنترل نمی‌شود، بلکه در توابع بالاتر try { const mediaStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: facingModeToTry }, audio: false }); setActiveLocalVideoStream(mediaStream); onVideoStreamChange(mediaStream); // این باید srcObject ویدیو را در AppCore تنظیم کند return mediaStream; } catch (error) { console.error(`❌ Start WC err ${facingModeToTry}:`, error); setActiveLocalVideoStream(null); onVideoStreamChange(null); onAppCamToggle(false); // اطلاع به AppCore که دوربین خاموش شود return null; } }, [onVideoStreamChange, onAppCamToggle]); // وابستگی‌ها const _stopWebcam = useCallback(() => { if (activeLocalVideoStream) { activeLocalVideoStream.getTracks().forEach(track => track.stop()); } setActiveLocalVideoStream(null); onVideoStreamChange(null); }, [activeLocalVideoStream, onVideoStreamChange]); useEffect(() => { if (isAppCamActive) { if (!activeLocalVideoStream && !isSwitchingCamera) { _startWebcam(currentFacingMode); } } else { if (activeLocalVideoStream) { _stopWebcam(); } } }, [isAppCamActive, _startWebcam, _stopWebcam, activeLocalVideoStream, isSwitchingCamera, currentFacingMode]); useEffect(() => { const onData = (base64: string) => { if (client && connected && isAppMicActive) { client.sendRealtimeInput([{ mimeType: "audio/pcm;rate=16000", data: base64 }]); } }; if (connected && isAppMicActive && audioRecorder) { audioRecorder.on("data", onData).start(); } else if (audioRecorder && audioRecorder.recording) { audioRecorder.stop(); } return () => { if (audioRecorder) { audioRecorder.off("data", onData); if (audioRecorder.recording) audioRecorder.stop(); } }; }, [connected, client, isAppMicActive, audioRecorder]); useEffect(() => { let timeoutId = -1; function sendVideoFrame() { if (connected && activeLocalVideoStream && client && videoRef.current) { // ... (منطق ارسال فریم ویدیو بدون تغییر) ... } if (connected && activeLocalVideoStream) { timeoutId = window.setTimeout(sendVideoFrame, 1000 / 4); // 4 FPS } } if (connected && activeLocalVideoStream && videoRef.current) { timeoutId = window.setTimeout(sendVideoFrame, 200); // شروع با کمی تاخیر } return () => clearTimeout(timeoutId); }, [connected, activeLocalVideoStream, client, videoRef]); // useEffect برای قطع اتصال وقتی هیچکدام فعال نیستند (این منطق به AppCore منتقل شد) // useEffect(() => { // if (!isAppMicActive && !isAppCamActive && connected) { // liveApiDisconnect(); // استفاده از نام جدید // } // }, [isAppMicActive, isAppCamActive, connected, liveApiDisconnect]); const handleMicToggle = async () => { if (isSwitchingCamera) return; const newMicState = !isAppMicActive; if (newMicState) { // روشن کردن میکروفون if (!(await ensureConnectedAndReady())) { onAppMicToggle(false); return; } } onAppMicToggle(newMicState); }; const handleCamToggle = async () => { if (isSwitchingCamera) return; const newCamState = !isAppCamActive; if (newCamState) { // روشن کردن دوربین if (!(await ensureConnectedAndReady())) { onAppCamToggle(false); return; } if (!isAppMicActive) { // اگر میکروفون خاموش است، آن را هم روشن کن onAppMicToggle(true); } onAppCamToggle(true); // این باعث می‌شود useEffect بالا _startWebcam را با currentFacingMode فعلی فراخوانی کند } else { // خاموش کردن دوربین onAppCamToggle(false); // این باعث می‌شود useEffect بالا _stopWebcam را فراخوانی کند } }; const handleSwitchCamera = async () => { if (!isAppCamActive || !activeLocalVideoStream || isSwitchingCamera) return; setIsSwitchingCamera(true); // جلوگیری از فراخوانی‌های همزمان const targetFacingMode = currentFacingMode === 'user' ? 'environment' : 'user'; _stopWebcam(); // توقف استریم فعلی await new Promise(resolve => setTimeout(resolve, 100)); // تاخیر کوچک const newStream = await _startWebcam(targetFacingMode); if (newStream) { setCurrentFacingMode(targetFacingMode); // آپدیت state در App.tsx } else { // اگر ناموفق بود، سعی در بازگرداندن به دوربین قبلی (currentFacingMode هنوز مقدار قبلی را دارد) await _startWebcam(currentFacingMode); } setIsSwitchingCamera(false); // آزاد کردن قفل }; return ( ); }; export default memo(ControlTray);