{isHumanActive && }
{/* Add AI icon if isAiActive is true and you have one */}
);
};
function ControlTray({
videoRef,
onVideoStreamChange = () => {},
supportsVideo,
}: ControlTrayProps) {
const webcam = useWebcam();
const screenCapture = useScreenCapture(); // Kept if screen share button is re-added
const [isCamActive, setIsCamActive] = useState(false); // New state for camera (visual)
const [isMicActive, setIsMicActive] = useState(false); // New state for mic (visual)
const [currentFacingMode, setCurrentFacingMode] = useState<'user' | 'environment'>('user');
const [isSwitchingCamera, setIsSwitchingCamera] = useState(false);
const [audioRecorder] = useState(() => new AudioRecorder());
// const [inVolume, setInVolume] = useState(0); // For original AudioPulse
const { client, connected, connect, disconnect, volume, setConfig } = useLiveAPIContext();
// Connect to API when component mounts or when mic/cam are first activated
useEffect(() => {
// This is a placeholder for connection logic.
// The original app had an explicit connect button.
// You might want to connect automatically, or when mic/cam is first toggled.
if (!connected && (isMicActive || isCamActive)) {
// connect(); // Example: connect when mic/cam active
}
}, [isMicActive, isCamActive, connected, connect]);
// Audio recording based on isMicActive AND connected
useEffect(() => {
const onData = (base64: string) => {
if (client && connected && isMicActive) { // Check isMicActive
client.sendRealtimeInput([{ mimeType: "audio/pcm;rate=16000", data: base64 }]);
}
};
// const onVolume = setInVolume; // For original AudioPulse
if (connected && isMicActive && audioRecorder) {
audioRecorder.on("data", onData)/*.on("volume", onVolume)*/.start();
} else if (audioRecorder) {
audioRecorder.stop();
}
return () => {
if (audioRecorder) {
audioRecorder.off("data", onData)/*.off("volume", onVolume)*/.stop();
}
};
}, [connected, client, isMicActive, audioRecorder]);
// Video stream management
useEffect(() => {
const videoElement = videoRef.current;
if (videoElement) {
if (isCamActive && webcam.stream) {
videoElement.srcObject = webcam.stream;
videoElement.play().catch(e => console.warn("Video play failed:", e));
onVideoStreamChange(webcam.stream);
} else {
videoElement.srcObject = null;
onVideoStreamChange(null);
}
}
}, [isCamActive, webcam.stream, videoRef, onVideoStreamChange]);
// Video frame sending (simplified, adapt frame rate as needed)
useEffect(() => {
let frameSenderId: number;
const sendFrame = () => {
if (connected && isCamActive && webcam.stream && videoRef.current && client) {
const video = videoRef.current;
const canvas = document.createElement('canvas'); // Temporary canvas
const scale = 0.25;
canvas.width = video.videoWidth * scale;
canvas.height = video.videoHeight * scale;
if (canvas.width > 0 && canvas.height > 0) {
const ctx = canvas.getContext("2d");
if (ctx) {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const base64 = canvas.toDataURL("image/jpeg", 0.8);
const data = base64.slice(base64.indexOf(",") + 1);
client.sendRealtimeInput([{ mimeType: "image/jpeg", data }]);
}
}
}
frameSenderId = window.setTimeout(sendFrame, 1000 / 2); // ~2 FPS, adjust as needed
};
if (isCamActive && connected) {
frameSenderId = window.setTimeout(sendFrame, 200); // Initial delay
}
return () => clearTimeout(frameSenderId);
}, [connected, isCamActive, webcam.stream, client, videoRef]);
const handleToggleMic = async () => {
if (!connected) { // Auto-connect if not connected
try {
await connect();
} catch (err) {
console.error("Failed to connect on mic toggle:", err);
return; // Don't toggle mic if connection fails
}
}
// After ensuring connection (or if already connected)
setIsMicActive(prev => !prev);
// The audioRecorder useEffect will handle starting/stopping based on isMicActive & connected
};
const handleToggleCam = async () => {
if (isSwitchingCamera) return;
const newCamState = !isCamActive;
if (newCamState) { // Turning camera ON
if (!connected) { // Auto-connect if not connected
try {
await connect();
} catch (err) {
console.error("Failed to connect on cam toggle:", err);
return;
}
}
try {
await webcam.start({ video: { facingMode: currentFacingMode }, audio: false });
setIsCamActive(true);
} catch (error) {
console.error("Error starting webcam:", error);
setIsCamActive(false); // Ensure it's off if start failed
}
} else { // Turning camera OFF
webcam.stop();
setIsCamActive(false);
// Optional: Disconnect if both mic and cam are off
// if (!isMicActive && !newCamState && connected) {
// disconnect();
// }
}
};
const handleSwitchCamera = async () => {
if (!isCamActive || isSwitchingCamera) return;
setIsSwitchingCamera(true);
const newFacingMode = currentFacingMode === 'user' ? 'environment' : 'user';
try {
await webcam.stop(); // Stop current stream
await webcam.start({ video: { facingMode: { exact: newFacingMode } }, audio: false });
setCurrentFacingMode(newFacingMode);
// onVideoStreamChange already handled by webcam.stream effect
} catch (error) {
console.error("Error switching camera:", error);
// Try to restore previous mode or stop
try {
await webcam.start({ video: { facingMode: currentFacingMode }, audio: false });
} catch (restoreError) {
console.error("Error restoring camera:", restoreError);
setIsCamActive(false); // Turn off cam if all fails
webcam.stop();
}
} finally {
setIsSwitchingCamera(false);
}
};
// Determine layout for footer controls
const footerLayoutClass = isCamActive ? 'layout-with-small-logo' : 'layout-default';
// Update large logo visibility in App.tsx (this is tricky, App.tsx owns that element)
// For simplicity, we'll just log. A better way is via context or Zustand.
useEffect(() => {
const largeLogoEl = document.getElementById('large-logo-container');
if (largeLogoEl) {
if (!isCamActive && isMicActive) {
largeLogoEl.innerHTML = ''; // Clear previous
const logoDiv = document.createElement('div');
// This is a hacky way to render the LogoDisplay component's HTML.
// ReactDOM.render(