Files
usda-vision/vision-system-remote/src/components/CameraPreviewModal.tsx
salirezav f6a37ca1ba Remove deprecated files and scripts to streamline the codebase
- Deleted unused API test files, RTSP diagnostic scripts, and development utility scripts to reduce clutter.
- Removed outdated database schema and modularization proposal documents to maintain focus on current architecture.
- Cleaned up configuration files and logging scripts that are no longer in use, enhancing project maintainability.
2025-11-02 10:07:59 -05:00

181 lines
6.5 KiB
TypeScript

import React, { useState, useEffect, useRef } from 'react'
import { visionApi } from '../services/api'
interface CameraPreviewModalProps {
cameraName: string
isOpen: boolean
onClose: () => void
onError?: (error: string) => void
}
export const CameraPreviewModal: React.FC<CameraPreviewModalProps> = ({
cameraName,
isOpen,
onClose,
onError,
}) => {
const [loading, setLoading] = useState(false)
const [streaming, setStreaming] = useState(false)
const [error, setError] = useState<string | null>(null)
const imgRef = useRef<HTMLImageElement>(null)
const streamUrlRef = useRef<string | null>(null)
useEffect(() => {
if (isOpen && cameraName) {
startStreaming()
}
return () => {
if (streaming) {
stopStreaming()
}
}
}, [isOpen, cameraName])
const startStreaming = async () => {
try {
setLoading(true)
setError(null)
const result = await visionApi.startStream(cameraName)
if (result.success) {
setStreaming(true)
const streamUrl = visionApi.getStreamUrl(cameraName)
streamUrlRef.current = streamUrl
if (imgRef.current) {
imgRef.current.src = `${streamUrl}?t=${Date.now()}`
}
} else {
throw new Error(result.message)
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to start stream'
setError(errorMessage)
onError?.(errorMessage)
} finally {
setLoading(false)
}
}
const stopStreaming = async () => {
try {
if (streaming) {
await visionApi.stopStream(cameraName)
setStreaming(false)
streamUrlRef.current = null
if (imgRef.current) {
imgRef.current.src = ''
}
}
} catch (err) {
console.error('Error stopping stream:', err)
}
}
const handleClose = () => {
stopStreaming()
onClose()
}
if (!isOpen) return null
return (
<div className="fixed inset-0 z-[999999] flex items-center justify-center overflow-y-auto">
<div
className="fixed inset-0 h-full w-full bg-gray-900/60 backdrop-blur-sm"
onClick={handleClose}
/>
<div className="relative w-11/12 max-w-5xl rounded-xl bg-white shadow-2xl dark:bg-gray-800 p-6" onClick={(e) => e.stopPropagation()}>
{/* Close Button */}
<button
onClick={handleClose}
className="absolute right-4 top-4 z-10 flex h-10 w-10 items-center justify-center rounded-lg bg-white dark:bg-gray-800 text-gray-400 border border-gray-300 dark:border-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-gray-700 dark:hover:text-white"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M18 6L6 18M6 6l12 12" />
</svg>
</button>
<div className="mt-2">
{/* Header */}
<div className="mb-4">
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
Camera Preview: {cameraName}
</h3>
</div>
{/* Content */}
<div className="mb-4">
{loading && (
<div className="flex items-center justify-center h-96 bg-gray-100 dark:bg-gray-900 rounded-lg">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 mx-auto" />
<p className="mt-4 text-gray-600 dark:text-gray-400">Starting camera stream...</p>
</div>
</div>
)}
{error && (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3 flex-1">
<h3 className="text-sm font-medium text-red-800 dark:text-red-200">Stream Error</h3>
<div className="mt-2 text-sm text-red-700 dark:text-red-300">
<p>{error}</p>
</div>
<div className="mt-4">
<button
onClick={startStreaming}
className="bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition-colors"
>
Retry
</button>
</div>
</div>
</div>
</div>
)}
{streaming && !loading && !error && (
<div className="bg-black rounded-lg overflow-hidden">
<img
ref={imgRef}
alt={`Live stream from ${cameraName}`}
className="w-full h-auto max-h-[70vh] object-contain"
onError={() => setError('Failed to load camera stream')}
/>
</div>
)}
</div>
{/* Footer */}
<div className="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-gray-700">
<div className="flex items-center space-x-2">
{streaming && (
<div className="flex items-center text-green-600 dark:text-green-400">
<div className="w-2 h-2 bg-green-500 rounded-full mr-2 animate-pulse" />
<span className="text-sm font-medium">Live Stream Active</span>
</div>
)}
</div>
<button
onClick={handleClose}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors"
>
Close
</button>
</div>
</div>
</div>
</div>
)
}