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