// 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);