/** Copyright 2024 Google LLC ... (لایسنس و توضیحات دیگر مثل قبل) ... */ import cn from "classnames"; import React, { memo, ReactNode, RefObject, useEffect, useRef, useState } from "react"; import { useLiveAPIContext } from "../../contexts/LiveAPIContext"; import { AudioRecorder } from "../../lib/audio-recorder"; // 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; 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 } = useLiveAPIContext(); const [audioRecorder] = useState(() => new AudioRecorder()); const [muted, setMuted] = useState(true); const [inVolume, setInVolume] = useState(0); const [activeLocalVideoStream, setActiveLocalVideoStream] = useState(null); const [currentFacingMode, setCurrentFacingMode] = useState<'user' | 'environment'>('user'); const [isSwitchingCamera, setIsSwitchingCamera] = useState(false); useEffect(() => { setMuted(!isAppMicActive); }, [isAppMicActive]); useEffect(() => { console.log(`isAppCamActive effect: ${isAppCamActive}, current activeLocalVideoStream: ${activeLocalVideoStream ? 'Exists' : 'Null'}`); if (isAppCamActive && !activeLocalVideoStream) { console.log("isAppCamActive TRUE and no stream, calling startWebcam()"); startWebcam(); } else if (!isAppCamActive && activeLocalVideoStream) { console.log("isAppCamActive FALSE and stream exists, calling stopWebcam()"); stopWebcam(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isAppCamActive]); // activeLocalVideoStream removed to prevent re-triggering startWebcam if it fails and sets stream to null 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.recording) { // Check if it's recording before stopping audioRecorder.stop(); } return () => { if (audioRecorder) { audioRecorder.off("data", onData).off("volume", setInVolume); if (audioRecorder.recording) audioRecorder.stop(); } }; }, [connected, client, muted, audioRecorder]); const renderCanvasRef = useRef(null); useEffect(() => { let timeoutId = -1; function sendVideoFrame() { if (connected && activeLocalVideoStream) { timeoutId = window.setTimeout(sendVideoFrame, 1000 / 0.5); } 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); 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); } return () => { clearTimeout(timeoutId); }; }, [connected, activeLocalVideoStream, client, videoRef]); useEffect(() => { if (videoRef.current) { console.log("VideoRef Effect: Current srcObject:", videoRef.current.srcObject ? 'Exists' : 'Null', "New activeLocalVideoStream:", activeLocalVideoStream ? 'Exists' : 'Null'); if (videoRef.current.srcObject !== activeLocalVideoStream) { videoRef.current.srcObject = activeLocalVideoStream; console.log("VideoRef Effect: Assigned new stream to srcObject."); if (activeLocalVideoStream) { console.log("VideoRef Effect: Attempting to play video."); videoRef.current.play().then(() => { console.log("VideoRef Effect: Video playback started successfully."); }).catch(e => { console.warn("VideoRef Effect: Video play failed:", e); }); } else { console.log("VideoRef Effect: activeLocalVideoStream is null, video srcObject set to null."); } } } else { console.warn("VideoRef Effect: videoRef.current is null."); } }, [activeLocalVideoStream, videoRef]); const ensureConnected = async () => { console.log("ensureConnected called. Current connected state:", connected); if (!connected) { try { console.log("ensureConnected: Attempting to connect..."); await connect(); console.log("ensureConnected: Connection successful."); return true; } catch (err) { console.error('❌ ensureConnected: Connection error:', err); return false; } } console.log("ensureConnected: Already connected."); return true; }; const handleMicToggle = async () => { console.log("handleMicToggle called. isSwitchingCamera:", isSwitchingCamera, "Current isAppMicActive:", isAppMicActive); if (isSwitchingCamera) return; const newMicState = !isAppMicActive; if (newMicState) { if(!await ensureConnected()) { console.log("handleMicToggle: ensureConnected failed while trying to turn mic ON."); onAppMicToggle(false); return; } } onAppMicToggle(newMicState); console.log("handleMicToggle: Mic state toggled to", newMicState); }; const startWebcam = async (facingModeToTry: 'user' | 'environment' = currentFacingMode) => { console.log("Attempting to start webcam. Current facingMode:", facingModeToTry, "isSwitchingCamera:", isSwitchingCamera); if (isSwitchingCamera) { console.log("startWebcam: Already switching camera, aborting."); return; } setIsSwitchingCamera(true); if(!await ensureConnected()) { console.error("startWebcam: ensureConnected failed. Aborting webcam start."); onAppCamToggle(false); setIsSwitchingCamera(false); return; } console.log("startWebcam: ensureConnected succeeded."); try { console.log(`🚀 Getting user media with facingMode: ${facingModeToTry}`); const mediaStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: facingModeToTry }, audio: false, }); console.log("startWebcam: getUserMedia successful. Stream ID:", mediaStream.id); setActiveLocalVideoStream(mediaStream); onVideoStreamChange(mediaStream); setCurrentFacingMode(facingModeToTry); onAppCamToggle(true); console.log("startWebcam: Webcam started and states updated."); } catch (error) { console.error(`❌ Error starting webcam with ${facingModeToTry}:`, error); if (facingModeToTry === 'user' && (error as Error).name !== 'NotFoundError') { try { console.log("startWebcam: Attempting fallback to 'environment' facingMode."); const fallbackStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' }, audio: false }); console.log("startWebcam: Fallback getUserMedia successful. Stream ID:", fallbackStream.id); 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); console.log("startWebcam: Finished attempt, isSwitchingCamera set to false."); } }; const stopWebcam = () => { console.log("stopWebcam called."); if (activeLocalVideoStream) { console.log("stopWebcam: Stopping tracks for stream ID:", activeLocalVideoStream.id); activeLocalVideoStream.getTracks().forEach(track => track.stop()); } else { console.log("stopWebcam: No activeLocalVideoStream to stop."); } setActiveLocalVideoStream(null); onVideoStreamChange(null); onAppCamToggle(false); console.log("stopWebcam: Webcam stopped and states updated."); }; const handleCamToggle = () => { console.log("handleCamToggle called. Current isAppCamActive:", isAppCamActive); if (isAppCamActive) { console.log("handleCamToggle: Current cam is active, calling stopWebcam."); stopWebcam(); } else { console.log("handleCamToggle: Current cam is inactive, calling startWebcam."); startWebcam(); } }; const handleSwitchCamera = async () => { console.log("handleSwitchCamera called. isAppCamActive:", isAppCamActive, "Stream exists:", !!activeLocalVideoStream, "isSwitchingCamera:", isSwitchingCamera); if (!isAppCamActive || !activeLocalVideoStream || isSwitchingCamera) { console.log("handleSwitchCamera: Pre-conditions not met, aborting."); return; } setIsSwitchingCamera(true); const targetFacingMode = currentFacingMode === 'user' ? 'environment' : 'user'; console.log(`handleSwitchCamera: Stopping current stream tracks (Stream ID: ${activeLocalVideoStream.id}). Target facingMode: ${targetFacingMode}`); activeLocalVideoStream.getTracks().forEach(track => track.stop()); setActiveLocalVideoStream(null); onVideoStreamChange(null); try { console.log(`🔄 Attempting to switch camera to: ${targetFacingMode}`); const newStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { exact: targetFacingMode } }, audio: false, }); console.log("handleSwitchCamera: Switched camera successfully. New Stream ID:", newStream.id); setActiveLocalVideoStream(newStream); onVideoStreamChange(newStream); setCurrentFacingMode(targetFacingMode); 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); console.log(`handleSwitchCamera: Attempting to restore camera to previous mode: ${currentFacingMode}`); try { const restoredStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: currentFacingMode }, audio: false, }); console.log("handleSwitchCamera: Restored camera successfully. Stream ID:", restoredStream.id); setActiveLocalVideoStream(restoredStream); onVideoStreamChange(restoredStream); 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); stopWebcam(); onAppCamToggle(false); } } finally { setIsSwitchingCamera(false); console.log("handleSwitchCamera: Finished attempt, isSwitchingCamera set to false."); } }; useEffect(() => { if (!connected) { console.log("Not connected: Stopping video and audio if active."); if (activeLocalVideoStream) { activeLocalVideoStream.getTracks().forEach(track => track.stop()); setActiveLocalVideoStream(null); onVideoStreamChange(null); onAppCamToggle(false); } if (audioRecorder.recording) { audioRecorder.stop(); onAppMicToggle(false); } } }, [connected, audioRecorder, activeLocalVideoStream, onAppCamToggle, onAppMicToggle, onVideoStreamChange]); return ( ); } export default memo(ControlTray);