// src/components/control-tray/ControlTray.tsx (نسخه نهایی با قابلیت غیرفعال شدن) import cn from "classnames"; import React, { memo, RefObject, useEffect, useState, useCallback, useRef } from "react"; import { useAppContext } from "../../contexts/AppContext"; import { AudioRecorder } from "../../lib/audio-recorder"; import Logo from '../logo/Logo'; import { PauseIconWithSurroundPulse, microphoneIcon, cameraIcon, stopCamIcon } from '../icons'; const SvgSwitchCameraIcon = () => ; export type ControlTrayProps = { videoRef: RefObject; onUserSpeakingChange: (isSpeaking: boolean) => void; isAppMicActive: boolean; onAppMicToggle: (active: boolean) => void; isAppCamActive: boolean; onAppCamToggle: (active: boolean) => void; currentFacingMode: 'user' | 'environment'; onFacingModeChange: (mode: 'user' | 'environment') => void; // ✅ پراپرتی جدید برای غیرفعال کردن کنترل‌ها isTimeUp: boolean; }; const ControlTray: React.FC = ({ videoRef, onUserSpeakingChange, isAppMicActive, onAppMicToggle, isAppCamActive, onAppCamToggle, currentFacingMode, onFacingModeChange, isTimeUp }) => { const { client, connected, connect, volume } = useAppContext(); const audioRecorderRef = useRef(null); const [activeLocalVideoStream, setActiveLocalVideoStream] = useState(null); const [isSwitchingCamera, setIsSwitchingCamera] = useState(false); const renderCanvasRef = React.useRef(null); const [userVolume, setUserVolume] = useState(0); useEffect(() => { if (!audioRecorderRef.current) audioRecorderRef.current = new AudioRecorder(); const audioRecorder = audioRecorderRef.current; const handleAudioData = (base64: string, vol: number) => { if (client && connected && isAppMicActive) { if (base64) client.sendRealtimeInput([{ mimeType: "audio/pcm;rate=16000", data: base64 }]); setUserVolume(vol); onUserSpeakingChange(vol > 0.01); } }; const onStop = () => { setUserVolume(0); onUserSpeakingChange(false); }; if (isAppMicActive && connected) { audioRecorder.on("data", handleAudioData); audioRecorder.on("stop", onStop); if (!audioRecorder.recording) audioRecorder.start(); } else if (audioRecorder?.recording) { audioRecorder.stop(); } return () => { if (audioRecorder) { audioRecorder.off("data", handleAudioData); audioRecorder.off("stop", onStop); } }; }, [isAppMicActive, connected, onUserSpeakingChange, client]); 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 startWebcam = useCallback(async (facingModeToTry: 'user' | 'environment') => { if (isSwitchingCamera) return; setIsSwitchingCamera(true); if (activeLocalVideoStream) activeLocalVideoStream.getTracks().forEach(track => track.stop()); try { const mediaStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: facingModeToTry }, audio: false }); setActiveLocalVideoStream(mediaStream); onFacingModeChange(facingModeToTry); } catch (error) { console.error(`❌ Start WC err ${facingModeToTry}:`, error); setActiveLocalVideoStream(null); onAppCamToggle(false); } finally { setIsSwitchingCamera(false); } }, [isSwitchingCamera, activeLocalVideoStream, onFacingModeChange, onAppCamToggle]); useEffect(() => { if (isAppCamActive && !activeLocalVideoStream && !isSwitchingCamera) { startWebcam(currentFacingMode); } else if (!isAppCamActive && activeLocalVideoStream) { activeLocalVideoStream.getTracks().forEach(track => track.stop()); setActiveLocalVideoStream(null); } }, [isAppCamActive, activeLocalVideoStream, isSwitchingCamera, startWebcam, currentFacingMode]); useEffect(() => { let timeoutId = -1; function sendVideoFrame() { if (connected && activeLocalVideoStream && client && videoRef.current) { const video = videoRef.current; const canvas = renderCanvasRef.current; if (!canvas || video.readyState < video.HAVE_METADATA || video.paused || video.ended) { if (activeLocalVideoStream) timeoutId = window.setTimeout(sendVideoFrame, 1000 / 4); return; } try { const ctx = canvas.getContext("2d"); if (!ctx) return; const scale = 0.5; 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.8); const data = base64.slice(base64.indexOf(",") + 1); if (data) client.sendRealtimeInput([{ mimeType: "image/jpeg", data }]); } } catch (error) { console.error("❌ Error frame:", error); } } if (connected && activeLocalVideoStream) timeoutId = window.setTimeout(sendVideoFrame, 1000 / 4); } if (connected && activeLocalVideoStream && videoRef.current) timeoutId = window.setTimeout(sendVideoFrame, 200); return () => clearTimeout(timeoutId); }, [connected, activeLocalVideoStream, client, videoRef, renderCanvasRef]); const ensureConnectedAndReady = async (): Promise => { if (isTimeUp) return false; // اگر زمان تمام شده، اجازه اتصال نده if (!connected) { try { await connect(); return true; } catch (err) { console.error('❌ CT Connect err:', err); return false; } } return true; }; const handleMicToggle = async () => { if (isSwitchingCamera || isTimeUp) return; const newMicState = !isAppMicActive; if (newMicState && !(await ensureConnectedAndReady())) { onAppMicToggle(false); return; } onAppMicToggle(newMicState); }; const handleCamToggle = async () => { if (isSwitchingCamera || isTimeUp) return; const newCamState = !isAppCamActive; if (newCamState) { if (!(await ensureConnectedAndReady())) { onAppCamToggle(false); return; } if (!isAppMicActive) onAppMicToggle(true); onAppCamToggle(true); } else { onAppCamToggle(false); } }; const handleSwitchCamera = async () => { if (!isAppCamActive || !activeLocalVideoStream || isSwitchingCamera || isTimeUp) return; setIsSwitchingCamera(true); const targetFacingMode = currentFacingMode === 'user' ? 'environment' : 'user'; activeLocalVideoStream.getTracks().forEach(track => track.stop()); setActiveLocalVideoStream(null); try { const newStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { exact: targetFacingMode } }, audio: false }); setActiveLocalVideoStream(newStream); onFacingModeChange(targetFacingMode); } catch (error) { console.error(`❌ Switch Cam err to ${targetFacingMode}:`, error); try { const restoredStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: {exact: currentFacingMode } }, audio: false }); setActiveLocalVideoStream(restoredStream); } catch (restoreError) { console.error(`❌ Restore Cam err to ${currentFacingMode}:`, restoreError); setActiveLocalVideoStream(null); onAppCamToggle(false); } } finally { setIsSwitchingCamera(false); } }; const isSpeaking = userVolume > 0.01; return ( ); }; export default memo(ControlTray);