aegisai / frontend / src / hooks / useCamera.ts
useCamera.ts
Raw
/**
 * useCamera Hook
 * Manages webcam access and frame capture
 */

import { useRef, useEffect, useState, useCallback } from 'react';

interface UseCameraOptions {
  onFrameCapture?: (base64: string) => void;
  captureInterval?: number;
  isActive?: boolean;
}

interface UseCameraReturn {
  videoRef: React.RefObject<HTMLVideoElement | null>;
  canvasRef: React.RefObject<HTMLCanvasElement | null>;
  error: string | null;
  streamActive: boolean;
  captureFrame: () => string | null;
  restartCamera: () => Promise<void>;
}

export const useCamera = ({
  onFrameCapture,
  captureInterval = 4000,
  isActive = false
}: UseCameraOptions = {}): UseCameraReturn => {
  const videoRef = useRef<HTMLVideoElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const streamRef = useRef<MediaStream | null>(null);
  
  const [error, setError] = useState<string | null>(null);
  const [streamActive, setStreamActive] = useState(false);

  const captureFrame = useCallback((): string | null => {
    if (!videoRef.current || !canvasRef.current) return null;

    const video = videoRef.current;
    const canvas = canvasRef.current;
    const ctx = canvas.getContext('2d');

    if (!ctx || video.readyState !== 4) return null;

    try {
      canvas.width = video.videoWidth;
      canvas.height = video.videoHeight;
      
      ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
      
      return canvas.toDataURL('image/jpeg', 0.8);
    } catch (err) {
      console.error('Frame capture error:', err);
      return null;
    }
  }, []);

  const startCamera = useCallback(async () => {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({
        video: {
          facingMode: 'user',
          width: { ideal: 1280 },
          height: { ideal: 720 }
        }
      });

      if (videoRef.current) {
        videoRef.current.srcObject = stream;
        streamRef.current = stream;
        setStreamActive(true);
        setError(null);
      }
    } catch (err) {
      console.error('Camera access error:', err);
      setError('Could not access camera. Ensure permissions are granted.');
      setStreamActive(false);
    }
  }, []);

  const stopCamera = useCallback(() => {
    if (streamRef.current) {
      streamRef.current.getTracks().forEach(track => track.stop());
      streamRef.current = null;
      setStreamActive(false);
    }
  }, []);

  const restartCamera = useCallback(async () => {
    stopCamera();
    await startCamera();
  }, [startCamera, stopCamera]);

  // Initialize camera on mount
  useEffect(() => {
    startCamera();
    return () => stopCamera();
  }, [startCamera, stopCamera]);

  // Auto-capture frames when active
  useEffect(() => {
    if (!isActive || !streamActive || !onFrameCapture) return;

    const intervalId = window.setInterval(() => {
      const frame = captureFrame();
      if (frame) {
        onFrameCapture(frame);
      }
    }, captureInterval);

    return () => window.clearInterval(intervalId);
  }, [isActive, streamActive, onFrameCapture, captureInterval, captureFrame]);

  return {
    videoRef,
    canvasRef,
    error,
    streamActive,
    captureFrame,
    restartCamera
  };
};