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