aegisai / frontend / src / components / VideoFeed.tsx
VideoFeed.tsx
Raw
import React, { useRef, useEffect, useState } from 'react';
import { Camera } from 'lucide-react';

interface VideoFeedProps {
  onFrameCapture: (base64: string) => void;
  isMonitoring: boolean;
}

export const VideoFeed: React.FC<VideoFeedProps> = ({ onFrameCapture, isMonitoring }) => {
  const videoRef = useRef<HTMLVideoElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [error, setError] = useState<string | null>(null);
  const [streamActive, setStreamActive] = useState(false);

  /* ------------------- Camera Setup ------------------- */
  useEffect(() => {
    let stream: MediaStream | null = null;

    const startCamera = async () => {
      try {
        stream = await navigator.mediaDevices.getUserMedia({ 
          video: { facingMode: 'user', width: { ideal: 1280 }, height: { ideal: 720 } }
        });
        if (videoRef.current) {
          videoRef.current.srcObject = stream;
          setStreamActive(true);
        }
      } catch (err) {
        console.error("Camera error:", err);
        setError("Could not access camera. Ensure permissions are granted.");
      }
    };

    startCamera();

    return () => {
      if (stream) stream.getTracks().forEach(track => track.stop());
    };
  }, []);

  /* ------------------- Frame Capture ------------------- */
  useEffect(() => {
    let intervalId: number;
    if (isMonitoring && streamActive) {
      intervalId = window.setInterval(() => {
        if (videoRef.current && canvasRef.current) {
          const video = videoRef.current;
          const canvas = canvasRef.current;
          const ctx = canvas.getContext('2d');

          if (ctx && video.readyState === 4) {
            canvas.width = video.videoWidth;
            canvas.height = video.videoHeight;
            ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
            onFrameCapture(canvas.toDataURL('image/jpeg', 0.8));
          }
        }
      }, 4000);
    }
    return () => window.clearInterval(intervalId);
  }, [isMonitoring, streamActive, onFrameCapture]);

  /* ------------------- Render ------------------- */
  return (
    <div className="relative w-full h-full bg-black rounded-lg overflow-hidden border border-slate-700 shadow-2xl group">

      {error ? (
        <div className="absolute inset-0 flex flex-col items-center justify-center text-red-500 font-mono text-sm p-4 text-center">
          <Camera className="mb-2 w-12 h-12" />
          <span className="font-bold tracking-widest">{error}</span>
        </div>
      ) : (
        <>
          <video 
            ref={videoRef} 
            autoPlay 
            playsInline 
            muted 
            className="w-full h-full object-cover opacity-80 grayscale-[10%]" 
          />
          <canvas ref={canvasRef} className="hidden" />

          {/* ---------------- HUD Overlay ---------------- */}
          <div className="absolute inset-0 pointer-events-none">

            {/* Corner Brackets */}
            <div className="absolute top-4 left-4 w-8 h-8 border-t-2 border-l-2 border-aegis-accent opacity-50" />
            <div className="absolute top-4 right-4 w-8 h-8 border-t-2 border-r-2 border-aegis-accent opacity-50" />
            <div className="absolute bottom-4 left-4 w-8 h-8 border-b-2 border-l-2 border-aegis-accent opacity-50" />
            <div className="absolute bottom-4 right-4 w-8 h-8 border-b-2 border-r-2 border-aegis-accent opacity-50" />

            {/* Center Reticle */}
            <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
              <div className={`w-12 h-12 border border-aegis-accent rounded-full flex items-center justify-center transition-all duration-300 ${isMonitoring ? 'scale-100 opacity-80' : 'scale-50 opacity-20'}`}>
                <div className="w-1 h-1 bg-red-500 rounded-full animate-pulse"></div>
              </div>
            </div>

            {/* Scan Line */}
            {isMonitoring && (
              <div className="absolute inset-0 bg-gradient-to-b from-transparent via-aegis-accent/10 to-transparent animate-scan" />
            )}

            {/* Status Indicators */}
            <div className="absolute top-4 left-14 bg-black/50 backdrop-blur px-2 py-1 rounded border border-gray-700">
              <div className="flex items-center gap-2">
                <div className={`w-2 h-2 rounded-full ${isMonitoring ? 'bg-red-500 animate-pulse' : 'bg-gray-500'}`} />
                <span className="font-mono text-xs text-white tracking-widest uppercase">
                  {isMonitoring ? 'AEGIS // LIVE' : 'AEGIS // STANDBY'}
                </span>
              </div>
            </div>

            <div className="absolute bottom-4 right-14 font-mono text-xs text-aegis-accent opacity-70">
              CAM_01 // 1080p // 30FPS
            </div>

          </div>
        </>
      )}
    </div>
  );
};