Update Docker configuration, enhance error handling, and improve logging
- Added health check to the camera management API service in docker-compose.yml for better container reliability. - Updated installation scripts in Dockerfile to check for existing dependencies before installation, improving efficiency. - Enhanced error handling in the USDAVisionSystem class to allow partial operation if some components fail to start, preventing immediate shutdown. - Improved logging throughout the application, including more detailed error messages and critical error handling in the main loop. - Refactored WebSocketManager and CameraMonitor classes to use debug logging for connection events, reducing log noise.
This commit is contained in:
@@ -1,19 +1,23 @@
|
||||
import React, { useState } from 'react'
|
||||
|
||||
type Props = {
|
||||
onChange: (filters: { camera_name?: string; start_date?: string; end_date?: string }) => void
|
||||
onChange: (filters: { camera_name?: string; start_date?: string; end_date?: string; min_size_mb?: number; max_size_mb?: number }) => void
|
||||
}
|
||||
|
||||
export const FiltersBar: React.FC<Props> = ({ onChange }) => {
|
||||
const [camera, setCamera] = useState('')
|
||||
const [start, setStart] = useState('')
|
||||
const [end, setEnd] = useState('')
|
||||
const [minSize, setMinSize] = useState('')
|
||||
const [maxSize, setMaxSize] = useState('')
|
||||
|
||||
const handleApply = () => {
|
||||
onChange({
|
||||
camera_name: camera || undefined,
|
||||
start_date: start || undefined,
|
||||
end_date: end || undefined
|
||||
end_date: end || undefined,
|
||||
min_size_mb: minSize ? parseFloat(minSize) : undefined,
|
||||
max_size_mb: maxSize ? parseFloat(maxSize) : undefined
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,6 +25,8 @@ export const FiltersBar: React.FC<Props> = ({ onChange }) => {
|
||||
setCamera('')
|
||||
setStart('')
|
||||
setEnd('')
|
||||
setMinSize('')
|
||||
setMaxSize('')
|
||||
onChange({})
|
||||
}
|
||||
|
||||
@@ -35,7 +41,7 @@ export const FiltersBar: React.FC<Props> = ({ onChange }) => {
|
||||
Reset all
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Camera Name
|
||||
@@ -69,6 +75,34 @@ export const FiltersBar: React.FC<Props> = ({ onChange }) => {
|
||||
className="w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Min Size (MB)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
value={minSize}
|
||||
onChange={e => setMinSize(e.target.value)}
|
||||
placeholder="Min MB"
|
||||
className="w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Max Size (MB)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
value={maxSize}
|
||||
onChange={e => setMaxSize(e.target.value)}
|
||||
placeholder="Max MB"
|
||||
className="w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<button
|
||||
onClick={handleApply}
|
||||
|
||||
@@ -13,15 +13,45 @@ export const VideoList: React.FC<Props> = ({ onSelect }) => {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const [filters, setFilters] = useState<{ camera_name?: string; start_date?: string; end_date?: string }>({})
|
||||
const [filters, setFilters] = useState<{ camera_name?: string; start_date?: string; end_date?: string; min_size_mb?: number; max_size_mb?: number }>({})
|
||||
const [allVideos, setAllVideos] = useState<any[]>([])
|
||||
|
||||
async function load(p: number, f = filters) {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await fetchVideos({ page: p, limit: 12, ...f })
|
||||
setVideos(res.videos)
|
||||
setTotalPages(res.total_pages || 1)
|
||||
const { min_size_mb, max_size_mb, ...backendFilters } = f
|
||||
const hasSizeFilters = min_size_mb !== undefined || max_size_mb !== undefined
|
||||
|
||||
if (hasSizeFilters) {
|
||||
// When size filters are applied, fetch a larger batch and filter client-side
|
||||
// Fetch more videos to ensure we can filter properly
|
||||
const res = await fetchVideos({ page: 1, limit: 500, ...backendFilters })
|
||||
|
||||
// Filter by size on the client side
|
||||
const filtered = res.videos.filter((v: any) => {
|
||||
const sizeMB = v.file_size_bytes / (1024 * 1024)
|
||||
if (min_size_mb !== undefined && sizeMB < min_size_mb) return false
|
||||
if (max_size_mb !== undefined && sizeMB > max_size_mb) return false
|
||||
return true
|
||||
})
|
||||
|
||||
// Apply pagination to filtered results
|
||||
const itemsPerPage = 12
|
||||
const start = (p - 1) * itemsPerPage
|
||||
const end = start + itemsPerPage
|
||||
const paginatedVideos = filtered.slice(start, end)
|
||||
|
||||
setAllVideos(filtered)
|
||||
setVideos(paginatedVideos)
|
||||
setTotalPages(Math.ceil(filtered.length / itemsPerPage) || 1)
|
||||
} else {
|
||||
// No size filters - use normal backend pagination
|
||||
const res = await fetchVideos({ page: p, limit: 12, ...backendFilters })
|
||||
setVideos(res.videos)
|
||||
setTotalPages(res.total_pages || 1)
|
||||
setAllVideos(res.videos) // For display count
|
||||
}
|
||||
} catch (e: any) {
|
||||
setError(e?.message || 'Failed to load videos')
|
||||
} finally {
|
||||
@@ -93,7 +123,11 @@ export const VideoList: React.FC<Props> = ({ onSelect }) => {
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Showing <span className="font-medium text-gray-900 dark:text-white">{videos.length}</span> videos
|
||||
{filters.min_size_mb !== undefined || filters.max_size_mb !== undefined ? (
|
||||
<>Showing <span className="font-medium text-gray-900 dark:text-white">{videos.length}</span> of <span className="font-medium text-gray-900 dark:text-white">{allVideos.length}</span> filtered videos</>
|
||||
) : (
|
||||
<>Showing <span className="font-medium text-gray-900 dark:text-white">{videos.length}</span> videos</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useRef, useEffect } from 'react'
|
||||
import React, { useRef, useEffect, useState } from 'react'
|
||||
import videojs from 'video.js'
|
||||
import 'video.js/dist/video-js.css'
|
||||
|
||||
@@ -12,20 +12,43 @@ type Props = {
|
||||
export const VideoModal: React.FC<Props> = ({ fileId, onClose }) => {
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const playerRef = useRef<any>(null)
|
||||
// Use transcoded endpoint for browser compatibility (H.264)
|
||||
const src = fileId ? `${BASE}/videos/stream-transcoded?file_id=${encodeURIComponent(fileId)}` : null
|
||||
const [useTranscoded, setUseTranscoded] = useState(false)
|
||||
|
||||
// Try regular stream first (works for downloads), fallback to transcoded if needed
|
||||
const regularSrc = fileId ? `${BASE}/videos/stream?file_id=${encodeURIComponent(fileId)}` : null
|
||||
const transcodedSrc = fileId ? `${BASE}/videos/stream-transcoded?file_id=${encodeURIComponent(fileId)}` : null
|
||||
const src = useTranscoded ? transcodedSrc : regularSrc
|
||||
|
||||
// Reset transcoded flag when fileId changes
|
||||
useEffect(() => {
|
||||
setUseTranscoded(false)
|
||||
}, [fileId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!fileId || !src || !videoRef.current) return
|
||||
|
||||
// Dispose existing player if any
|
||||
if (playerRef.current) {
|
||||
playerRef.current.dispose()
|
||||
playerRef.current = null
|
||||
}
|
||||
|
||||
// Initialize Video.js player
|
||||
const player = videojs(videoRef.current, {
|
||||
controls: true,
|
||||
autoplay: true,
|
||||
preload: 'auto',
|
||||
autoplay: false, // Don't autoplay - let user control
|
||||
preload: 'metadata', // Load metadata first, then data on play
|
||||
fluid: false, // Disable fluid mode to respect container boundaries
|
||||
responsive: false, // Disable responsive mode to prevent overflow
|
||||
playbackRates: [0.5, 1, 1.25, 1.5, 2],
|
||||
html5: {
|
||||
vhs: {
|
||||
overrideNative: true
|
||||
},
|
||||
nativeVideoTracks: false,
|
||||
nativeAudioTracks: false,
|
||||
nativeTextTracks: false
|
||||
},
|
||||
sources: [
|
||||
{
|
||||
src: src,
|
||||
@@ -36,14 +59,38 @@ export const VideoModal: React.FC<Props> = ({ fileId, onClose }) => {
|
||||
|
||||
playerRef.current = player
|
||||
|
||||
player.on('error', () => {
|
||||
let errorHandled = false
|
||||
const handleError = () => {
|
||||
const error = player.error()
|
||||
if (error) {
|
||||
if (error && !errorHandled) {
|
||||
errorHandled = true
|
||||
console.error('Video.js error:', error)
|
||||
console.error('Error code:', error.code)
|
||||
console.error('Error message:', error.message)
|
||||
|
||||
// If regular stream fails and we haven't tried transcoded yet, switch to transcoded
|
||||
if (!useTranscoded && transcodedSrc) {
|
||||
console.log('Switching to transcoded endpoint...')
|
||||
// Clear the player ref to prevent double disposal
|
||||
playerRef.current = null
|
||||
// Dispose player before switching
|
||||
try {
|
||||
player.dispose()
|
||||
} catch (e) {
|
||||
// Ignore disposal errors
|
||||
}
|
||||
// Use setTimeout to allow cleanup to complete before switching
|
||||
setTimeout(() => {
|
||||
setUseTranscoded(true)
|
||||
}, 100)
|
||||
} else {
|
||||
// Show user-friendly error
|
||||
console.error('Video playback failed. Please try downloading the video instead.')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
player.on('error', handleError)
|
||||
|
||||
player.on('loadedmetadata', () => {
|
||||
console.log('Video metadata loaded, duration:', player.duration())
|
||||
@@ -60,7 +107,7 @@ export const VideoModal: React.FC<Props> = ({ fileId, onClose }) => {
|
||||
playerRef.current = null
|
||||
}
|
||||
}
|
||||
}, [fileId, src])
|
||||
}, [fileId, src, useTranscoded, transcodedSrc])
|
||||
|
||||
if (!fileId || !src) return null
|
||||
|
||||
@@ -127,7 +174,7 @@ export const VideoModal: React.FC<Props> = ({ fileId, onClose }) => {
|
||||
ref={videoRef}
|
||||
className="video-js vjs-default-skin w-full h-full"
|
||||
playsInline
|
||||
key={fileId}
|
||||
key={`${fileId}-${useTranscoded ? 'transcoded' : 'regular'}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
type VideoListParams = { camera_name?: string; start_date?: string; end_date?: string; limit?: number; page?: number; offset?: number; include_metadata?: boolean }
|
||||
type VideoListParams = { camera_name?: string; start_date?: string; end_date?: string; limit?: number; page?: number; offset?: number; include_metadata?: boolean; min_size_mb?: number; max_size_mb?: number }
|
||||
type VideoFile = { file_id: string; camera_name: string; filename: string; file_size_bytes: number; format: string; status: string; created_at: string; is_streamable: boolean; needs_conversion: boolean }
|
||||
type VideoListResponse = { videos: VideoFile[]; total_count: number; page?: number; total_pages?: number }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user