"use client"; import React, { useState, useRef, useEffect, useCallback } from "react"; import { Camera, SwitchCamera, Upload, AlertCircle, ArrowLeft, RefreshCw } from "lucide-react"; import { BoothConfig } from "@/types/booth"; import { useI18n } from "@/lib/i18n"; import { MascotGuide } from "@/components/MascotGuide"; interface CameraScreenProps { config: BoothConfig; onPhotoCaptured: (imageBase64: string) => void; onBack: () => void; } export const CameraScreen: React.FC = ({ config, onPhotoCaptured, onBack, }) => { const { t, isWeChat } = useI18n(); const [facingMode, setFacingMode] = useState<"user" | "environment">( config.camera.defaultFacingMode || "user" ); const [stream, setStream] = useState(null); const [cameraError, setCameraError] = useState(null); const [countdown, setCountdown] = useState(null); const [isFlashing, setIsFlashing] = useState(false); const [hasMultipleCameras, setHasMultipleCameras] = useState(false); const [isSwitchingCamera, setIsSwitchingCamera] = useState(false); const videoRef = useRef(null); const fileInputRef = useRef(null); const streamRef = useRef(null); const stopStream = useCallback(() => { if (streamRef.current) { streamRef.current.getTracks().forEach((track) => { try { track.stop(); } catch (e) { console.warn("Error stopping track:", e); } }); streamRef.current = null; } if (videoRef.current) { videoRef.current.srcObject = null; } setStream(null); }, []); const checkCameraDevices = useCallback(async () => { if (typeof navigator === "undefined" || !navigator.mediaDevices?.enumerateDevices) { return; } try { const devices = await navigator.mediaDevices.enumerateDevices(); const videoInputs = devices.filter((device) => device.kind === "videoinput"); setHasMultipleCameras(videoInputs.length > 1); } catch (err) { console.warn("Could not enumerate camera devices:", err); } }, []); useEffect(() => { if (typeof navigator !== "undefined" && navigator.mediaDevices?.addEventListener) { const handleDeviceChange = () => { checkCameraDevices(); }; navigator.mediaDevices.addEventListener("devicechange", handleDeviceChange); return () => { navigator.mediaDevices.removeEventListener("devicechange", handleDeviceChange); }; } }, [checkCameraDevices]); const startCamera = useCallback(async (targetFacingMode: "user" | "environment") => { try { setCameraError(null); stopStream(); if ( typeof window !== "undefined" && !window.isSecureContext && window.location.hostname !== "localhost" && window.location.hostname !== "127.0.0.1" ) { setCameraError( t({ en: "Camera access requires HTTPS or localhost. Please take a photo with your device.", zh: "摄像头需要安全连接(HTTPS)。请直接使用手机拍摄或上传照片。" }) ); return; } if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) { setCameraError( t({ en: "Live camera is not supported. Please click Upload to take a photo.", zh: "当前环境不支持实时取景,请点击右下角按钮直接拍照或上传。" }) ); return; } await new Promise((resolve) => setTimeout(resolve, 60)); let newStream: MediaStream | null = null; try { const primaryConstraints: MediaStreamConstraints = { audio: false, video: { facingMode: { ideal: targetFacingMode }, width: { ideal: 1920 }, height: { ideal: 1080 }, }, }; newStream = await navigator.mediaDevices.getUserMedia(primaryConstraints); } catch (tier1Error: any) { try { const secondaryConstraints: MediaStreamConstraints = { audio: false, video: { facingMode: { ideal: targetFacingMode }, }, }; newStream = await navigator.mediaDevices.getUserMedia(secondaryConstraints); } catch (tier2Error: any) { newStream = await navigator.mediaDevices.getUserMedia({ audio: false, video: true, }); } } if (!newStream) { throw new Error("Unable to acquire media stream."); } streamRef.current = newStream; setStream(newStream); if (videoRef.current) { const video = videoRef.current; video.srcObject = newStream; video.setAttribute("playsinline", "true"); video.setAttribute("webkit-playsinline", "true"); video.onloadedmetadata = () => { video.play().catch((playErr) => { console.log("[Camera] Video play deferred:", playErr); }); }; } checkCameraDevices(); } catch (err: any) { console.error("Camera access error:", err); let errorMsg = t({ en: "Camera access was denied or not found. Please click Upload to take/choose a photo.", zh: "无法访问摄像头。请点击下方上传按钮直接拍照或从相册选择。" }); setCameraError(errorMsg); } }, [stopStream, checkCameraDevices, t]); useEffect(() => { startCamera(facingMode); return () => { stopStream(); }; }, [facingMode, startCamera, stopStream]); const toggleCameraFacingMode = async () => { if (isSwitchingCamera) return; setIsSwitchingCamera(true); const nextMode = facingMode === "user" ? "environment" : "user"; setFacingMode(nextMode); setTimeout(() => { setIsSwitchingCamera(false); }, 400); }; const handleCapture = () => { if (!videoRef.current) return; const video = videoRef.current; const videoWidth = video.videoWidth || 1080; const videoHeight = video.videoHeight || 1920; const canvas = document.createElement("canvas"); canvas.width = videoWidth; canvas.height = videoHeight; const ctx = canvas.getContext("2d"); if (!ctx) return; if (facingMode === "user") { ctx.translate(canvas.width, 0); ctx.scale(-1, 1); } ctx.drawImage(video, 0, 0, canvas.width, canvas.height); const photoBase64 = canvas.toDataURL("image/jpeg", 0.92); setIsFlashing(true); setTimeout(() => { setIsFlashing(false); stopStream(); onPhotoCaptured(photoBase64); }, 200); }; const startCountdown = () => { if (countdown !== null) return; const seconds = config.camera.countdownSeconds || 3; setCountdown(seconds); let remaining = seconds; const interval = setInterval(() => { remaining -= 1; if (remaining > 0) { setCountdown(remaining); } else { clearInterval(interval); setCountdown(null); handleCapture(); } }, 1000); }; const handleFileUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { const result = reader.result as string; stopStream(); onPhotoCaptured(result); }; reader.readAsDataURL(file); }; return (
{/* Top Header */}
{hasMultipleCameras ? ( ) : (
)}
{/* Video Viewport - Made bigger, flex-1 with Mascot overlaying */}
{cameraError ? (

{cameraError}

) : ( <>