- 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.
181 lines
6.5 KiB
TypeScript
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>
|
|
)
|
|
}
|
|
|