'use client'
import { useState, useEffect, useRef } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Alert, AlertDescription } from '@/components/ui/alert'
import {
Camera,
RefreshCw,
Wifi,
WifiOff,
AlertCircle,
Loader2
} from 'lucide-react'
interface CameraInfo {
name: string
device_id: number
connected: boolean
last_frame_time: number
fps: number
resolution: string
}
interface CameraFeedProps {
cameraName?: string
className?: string
}
export function CameraFeed({ cameraName = 'robotics_usb_camera', className }: CameraFeedProps) {
const [camera, setCamera] = useState<CameraInfo | null>(null)
const [frameData, setFrameData] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [autoRefresh, setAutoRefresh] = useState(false)
const intervalRef = useRef<NodeJS.Timeout | null>(null)
const fetchCameraStatus = async () => {
try {
const response = await fetch(`http://localhost:8354/api/cameras/${cameraName}`)
if (response.ok) {
const data = await response.json()
setCamera(data)
setError(null)
} else {
setError(`Failed to fetch camera status: ${response.status}`)
}
} catch (err) {
setError(`Network error: ${err instanceof Error ? err.message : 'Unknown error'}`)
}
}
const fetchFrame = async () => {
if (!camera?.connected) return
setLoading(true)
try {
const response = await fetch(`http://localhost:8354/api/cameras/${cameraName}/frame?format=base64&quality=80`)
if (response.ok) {
const data = await response.json()
setFrameData(data.frame)
setError(null)
} else {
setError(`Failed to fetch frame: ${response.status}`)
}
} catch (err) {
setError(`Network error: ${err instanceof Error ? err.message : 'Unknown error'}`)
} finally {
setLoading(false)
}
}
const reconnectCamera = async () => {
try {
const response = await fetch(`http://localhost:8354/api/cameras/${cameraName}/reconnect`, {
method: 'POST'
})
if (response.ok) {
await fetchCameraStatus()
await fetchFrame()
} else {
setError(`Failed to reconnect: ${response.status}`)
}
} catch (err) {
setError(`Reconnect error: ${err instanceof Error ? err.message : 'Unknown error'}`)
}
}
useEffect(() => {
fetchCameraStatus()
}, [cameraName])
useEffect(() => {
if (autoRefresh && camera?.connected) {
intervalRef.current = setInterval(fetchFrame, 1000) // Update every second
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
}
}
} else {
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}
}, [autoRefresh, camera?.connected])
const toggleAutoRefresh = () => {
setAutoRefresh(!autoRefresh)
if (!autoRefresh) {
fetchFrame() // Immediate first frame
}
}
return (
<Card className={className}>
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Camera className="h-5 w-5" />
<CardTitle>Camera Feed</CardTitle>
{camera && (
<Badge variant={camera.connected ? "default" : "destructive"}>
{camera.connected ? <Wifi className="h-3 w-3 mr-1" /> : <WifiOff className="h-3 w-3 mr-1" />}
{camera.name}
</Badge>
)}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={fetchCameraStatus}
disabled={loading}
>
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
</Button>
<Button
variant="outline"
size="sm"
onClick={reconnectCamera}
disabled={loading}
>
Reconnect
</Button>
<Button
variant={autoRefresh ? "default" : "outline"}
size="sm"
onClick={toggleAutoRefresh}
>
{autoRefresh ? <Loader2 className="h-4 w-4 animate-spin mr-1" /> : null}
{autoRefresh ? 'Stop' : 'Live'}
</Button>
</div>
</div>
{camera && (
<CardDescription>
Device ID: {camera.device_id} | Resolution: {camera.resolution} | FPS: {camera.fps}
{camera.last_frame_time > 0 && (
<> | Last frame: {new Date(camera.last_frame_time * 1000).toLocaleTimeString()}</>
)}
</CardDescription>
)}
</CardHeader>
<CardContent>
{error && (
<Alert className="mb-4">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="relative bg-muted rounded-lg overflow-hidden" style={{ minHeight: '300px' }}>
{frameData ? (
<img
src={`data:image/jpeg;base64,${frameData}`}
alt="Camera feed"
className="w-full h-auto object-contain"
style={{ maxHeight: '400px' }}
/>
) : camera?.connected ? (
<div className="flex items-center justify-center h-64 text-muted-foreground">
{loading ? (
<div className="flex items-center space-x-2">
<Loader2 className="h-5 w-5 animate-spin" />
<span>Loading camera feed...</span>
</div>
) : (
<div className="text-center">
<Camera className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>Camera connected - Click "Live" to start feed</p>
</div>
)}
</div>
) : (
<div className="flex items-center justify-center h-64 text-muted-foreground">
<div className="text-center">
<WifiOff className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>Camera not connected</p>
<Button
variant="outline"
size="sm"
onClick={reconnectCamera}
className="mt-2"
>
Try Reconnect
</Button>
</div>
</div>
)}
{loading && frameData && (
<div className="absolute top-2 right-2">
<Badge variant="secondary">
<Loader2 className="h-3 w-3 animate-spin mr-1" />
Updating...
</Badge>
</div>
)}
</div>
{!camera && !error && (
<div className="flex items-center justify-center h-32 text-muted-foreground">
<Loader2 className="h-5 w-5 animate-spin mr-2" />
Loading camera information...
</div>
)}
</CardContent>
</Card>
)
}