Spaces:
Running
Running
// src/components/control-tray/ControlTray.tsx | |
// ... (ایمپورتهای اولیه و SVGهای دیگر مثل قبل) ... | |
import cn from "classnames"; | |
import React, { memo, ReactNode, RefObject, useEffect, useState } from "react"; | |
import { useLiveAPIContext } from "../../contexts/LiveAPIContext"; | |
import { AudioRecorder } from "../../lib/audio-recorder"; | |
import LogoAnimation from '../logo-animation/LogoAnimation'; | |
// ... (SVG Icons: SvgPauseIcon, SvgCameraIcon, SvgStopCamIcon, SvgSwitchCameraIcon بدون تغییر) ... | |
export type ControlTrayProps = { | |
videoRef: RefObject<HTMLVideoElement>; | |
supportsVideo: boolean; | |
onVideoStreamChange: (stream: MediaStream | null) => void; | |
isAppMicActive: boolean; | |
onAppMicToggle: (active: boolean) => void; | |
isAppCamActive: boolean; | |
onAppCamToggle: (active: boolean) => void; | |
ReferenceMicrophoneIcon: () => JSX.Element; | |
initialFacingMode: 'user' | 'environment'; // *** NEW PROP *** | |
onFacingModeChange: (newMode: 'user' | 'environment') => void; // *** NEW PROP *** | |
}; | |
const ControlTray: React.FC<ControlTrayProps> = ({ | |
videoRef, | |
onVideoStreamChange, | |
supportsVideo, | |
isAppMicActive, | |
onAppMicToggle, | |
isAppCamActive, | |
onAppCamToggle, | |
ReferenceMicrophoneIcon, | |
initialFacingMode, // *** NEW PROP *** | |
onFacingModeChange, // *** NEW PROP *** | |
}) => { | |
const { client, connected, connect } = useLiveAPIContext(); | |
const [audioRecorder] = useState(() => new AudioRecorder()); | |
const [activeLocalVideoStream, setActiveLocalVideoStream] = useState<MediaStream | null>(null); | |
// *** currentFacingMode حالا از initialFacingMode مقدار اولیه میگیرد *** | |
const [currentFacingMode, setCurrentFacingMode] = useState<'user' | 'environment'>(initialFacingMode); | |
const [isSwitchingCamera, setIsSwitchingCamera] = useState(false); | |
const renderCanvasRef = React.useRef<HTMLCanvasElement>(null); | |
// وقتی initialFacingMode از بیرون تغییر میکند، state داخلی را هم آپدیت کن | |
useEffect(() => { | |
setCurrentFacingMode(initialFacingMode); | |
}, [initialFacingMode]); | |
// ... (useEffect های audioRecorder, sendVideoFrame, videoRef.srcObject بدون تغییر) ... | |
useEffect(() => { | |
if (isAppCamActive && !activeLocalVideoStream && !isSwitchingCamera) { | |
// هنگام شروع وبکم، از currentFacingMode داخلی استفاده کن | |
startWebcam(currentFacingMode); | |
} else if (!isAppCamActive && activeLocalVideoStream) { | |
stopWebcam(); | |
} | |
// eslint-disable-next-line react-hooks/exhaustive-deps | |
}, [isAppCamActive, activeLocalVideoStream, isSwitchingCamera]); // currentFacingMode را از وابستگیها حذف میکنیم تا startWebcam فقط یکبار با مقدار اولیه اجرا شود و بعدا توسط handleSwitchCamera مدیریت شود. | |
const startWebcam = async (facingModeToTry: 'user' | 'environment' = currentFacingMode) => { | |
if (isSwitchingCamera) return; | |
setIsSwitchingCamera(true); | |
try { | |
const mediaStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: facingModeToTry }, audio: false }); | |
setActiveLocalVideoStream(mediaStream); | |
onVideoStreamChange(mediaStream); | |
setCurrentFacingMode(facingModeToTry); // آپدیت state داخلی | |
onFacingModeChange(facingModeToTry); // اطلاع به والد | |
} catch (error) { | |
console.error(`❌ Start WC err ${facingModeToTry}:`, error); | |
setActiveLocalVideoStream(null); | |
onVideoStreamChange(null); | |
onAppCamToggle(false); // اگر وبکم شروع نشد، دکمه دوربین را خاموش کن | |
} finally { | |
setIsSwitchingCamera(false); | |
} | |
}; | |
// ... (stopWebcam, ensureConnectedAndReady, handleMicToggle, handleCamToggle بدون تغییر عمده) ... | |
// فقط handleCamToggle ممکن است نیاز به استفاده از currentFacingMode داشته باشد اگر اولین بار دوربین را با جهت خاصی میخواهید روشن کنید | |
const handleSwitchCamera = async () => { | |
if (!isAppCamActive || !activeLocalVideoStream || isSwitchingCamera) return; | |
setIsSwitchingCamera(true); | |
const targetFacingMode = currentFacingMode === 'user' ? 'environment' : 'user'; | |
activeLocalVideoStream.getTracks().forEach(track => track.stop()); | |
try { | |
const newStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { exact: targetFacingMode } }, audio: false }); | |
setActiveLocalVideoStream(newStream); | |
onVideoStreamChange(newStream); | |
setCurrentFacingMode(targetFacingMode); // آپدیت state داخلی | |
onFacingModeChange(targetFacingMode); // اطلاع به والد | |
} catch (error) { | |
console.error(`❌ Switch Cam err ${targetFacingMode}:`, error); | |
// سعی در بازگرداندن به دوربین قبلی | |
try { | |
const restoredStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: currentFacingMode }, audio: false }); // استفاده از currentFacingMode قبلی | |
setActiveLocalVideoStream(restoredStream); | |
onVideoStreamChange(restoredStream); | |
// onFacingModeChange(currentFacingMode); // نیازی نیست چون به حالت قبلی برگشتیم | |
} catch (restoreError) { | |
console.error('❌ Restore Cam err:', restoreError); | |
setActiveLocalVideoStream(null); | |
onVideoStreamChange(null); | |
onAppCamToggle(false); | |
} | |
} finally { | |
setIsSwitchingCamera(false); | |
} | |
}; | |
// ... (بقیه useEffect ها و توابع handle مثل قبل) | |
const ensureConnectedAndReady = async (): Promise<boolean> => { /* ... */ return false; }; | |
const handleMicToggle = async () => { /* ... */ }; | |
const handleCamToggle = async () => { /* ... */ }; | |
return ( | |
<footer id="footer-controls" className="footer-controls-html-like"> | |
{/* ... (JSX فوتر مثل قبل) ... */} | |
<canvas style={{ display: "none" }} ref={renderCanvasRef} /> | |
<div id="mic-button" className="control-button mic-button-color" onClick={handleMicToggle} > | |
{isAppMicActive ? <SvgPauseIcon /> : <ReferenceMicrophoneIcon />} | |
</div> | |
{isAppCamActive && ( | |
<div id="small-logo-footer-container" className="small-logo-footer-html-like"> | |
<LogoAnimation isMini={true} isActive={true} type="human" forFooter={true} /> | |
</div> | |
)} | |
<div id="cam-button-wrapper" className="control-button-wrapper cam-wrapper-html-like"> | |
<div id="cam-button" className="control-button cam-button-color" onClick={handleCamToggle} > | |
{isAppCamActive ? <SvgStopCamIcon /> : <SvgCameraIcon />} | |
</div> | |
<div id="switch-camera-button-container" className={cn("switch-camera-button-container", { visible: isAppCamActive && !isSwitchingCamera })} > | |
<button id="switch-camera-button" aria-label="Switch Camera" className="switch-camera-button-content group" onClick={handleSwitchCamera} disabled={!isAppCamActive || isSwitchingCamera} > | |
<SvgSwitchCameraIcon/> | |
</button> | |
</div> | |
</div> | |
</footer> | |
); | |
}; | |
export default memo(ControlTray); |