/**
Copyright 2024 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
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'; // <<-- PROP از والد
onFacingModeChange: (mode: 'user' | 'environment') => void; // <<-- PROP از والد
};
const ControlTray: React.FC = ({
videoRef,
onVideoStreamChange,
supportsVideo,
isAppMicActive,
onAppMicToggle,
isAppCamActive,
onAppCamToggle,
ReferenceMicrophoneIcon,
currentFacingMode, // <<-- دریافت PROP
onFacingModeChange, // <<-- دریافت PROP
}) => {
const { client, connected, connect } = useLiveAPIContext();
const [audioRecorder] = useState(() => new AudioRecorder());
const [activeLocalVideoStream, setActiveLocalVideoStream] = useState(null);
// const [currentFacingMode, setCurrentFacingMode] = useState<'user' | 'environment'>('user'); // <<-- حذف state محلی
const [isSwitchingCamera, setIsSwitchingCamera] = useState(false);
const renderCanvasRef = React.useRef(null);
useEffect(() => {
// این 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 stopWebcam = useCallback(() => {
if (activeLocalVideoStream) {
activeLocalVideoStream.getTracks().forEach(track => track.stop());
}
setActiveLocalVideoStream(null);
onVideoStreamChange(null);
}, [activeLocalVideoStream, onVideoStreamChange]);
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);
onVideoStreamChange(mediaStream);
onFacingModeChange(facingModeToTry); // <<-- بهروزرسانی state والد
} catch (error) {
console.error(`❌ Start WC err ${facingModeToTry}:`, error);
setActiveLocalVideoStream(null);
onVideoStreamChange(null);
onAppCamToggle(false); // اگر دوربین روشن نشد، دکمه را خاموش کن
} finally {
setIsSwitchingCamera(false);
}
}, [
isSwitchingCamera,
activeLocalVideoStream, // currentFacingMode از props اینجا استفاده نمیشود مستقیما چون facingModeToTry پاس داده میشود
onVideoStreamChange,
onFacingModeChange,
onAppCamToggle,
]);
useEffect(() => {
if (isAppCamActive && !activeLocalVideoStream && !isSwitchingCamera) {
startWebcam(currentFacingMode); // <<-- استفاده از currentFacingMode (prop)
} else if (!isAppCamActive && activeLocalVideoStream) {
stopWebcam();
}
}, [isAppCamActive, activeLocalVideoStream, isSwitchingCamera, startWebcam, stopWebcam, 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) {
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 (!connected) {
try { await connect(); return true; }
catch (err) { console.error('❌ CT Connect err:', err); return false; }
}
return true;
};
const handleMicToggle = async () => {
if (isSwitchingCamera) return;
const newMicState = !isAppMicActive;
if (newMicState && !(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';
// توقف استریم فعلی
activeLocalVideoStream.getTracks().forEach(track => track.stop());
setActiveLocalVideoStream(null); // پاک کردن استریم فعلی از state
onVideoStreamChange(null);
try {
const newStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { exact: targetFacingMode } }, audio: false });
setActiveLocalVideoStream(newStream);
onVideoStreamChange(newStream);
onFacingModeChange(targetFacingMode); // <<-- بهروزرسانی state والد با جهت جدید
} catch (error) {
console.error(`❌ Switch Cam err to ${targetFacingMode}:`, error);
// تلاش برای بازگرداندن دوربین قبلی
try {
console.log(`Attempting to restore to ${currentFacingMode}`); // currentFacingMode اینجا جهت قبلی است
const restoredStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: {exact: currentFacingMode } }, audio: false });
setActiveLocalVideoStream(restoredStream);
onVideoStreamChange(restoredStream);
// onFacingModeChange(currentFacingMode); // نیازی نیست چون به حالت قبلی برگشته
} catch (restoreError) {
console.error(`❌ Restore Cam err to ${currentFacingMode}:`, restoreError);
setActiveLocalVideoStream(null);
onVideoStreamChange(null);
onAppCamToggle(false); // اگر بازیابی هم ناموفق بود، دوربین را خاموش کن
}
} finally {
setIsSwitchingCamera(false);
}
};
return (
);
};
export default memo(ControlTray);