From 104f6202fb80ef48fe6117b4ff1d380ad56f6d39 Mon Sep 17 00:00:00 2001 From: Alireza Vaezi Date: Mon, 28 Jul 2025 17:53:59 -0400 Subject: [PATCH] feat(streaming): Add live streaming functionality for USDA Vision Camera system - Introduced non-blocking live preview streaming that operates independently from recording. - Implemented REST API endpoints for starting and stopping streams, and retrieving live streams. - Developed a web interface (`camera_preview.html`) for users to control and view camera streams. - Created TypeScript definitions for API integration in React projects. - Added comprehensive testing script (`test_streaming.py`) to validate API endpoints and concurrent operations. - Updated database migration to fix visibility of experiment repetitions for all authenticated users. --- src/components/VisionSystem.tsx | 781 +++++++++--------- src/lib/visionApi.ts | 18 +- streaming/AI_INTEGRATION_GUIDE.md | 566 +++++++++++++ streaming/STREAMING_GUIDE.md | 240 ++++++ streaming/camera-api.types.ts | 367 ++++++++ streaming/camera_preview.html | 336 ++++++++ streaming/streaming-api.http | 300 +++++++ streaming/test_streaming.py | 199 +++++ ...24000001_experiment_repetitions_system.sql | 12 +- ...50728000001_fix_repetitions_visibility.sql | 12 + 10 files changed, 2410 insertions(+), 421 deletions(-) create mode 100644 streaming/AI_INTEGRATION_GUIDE.md create mode 100644 streaming/STREAMING_GUIDE.md create mode 100644 streaming/camera-api.types.ts create mode 100644 streaming/camera_preview.html create mode 100644 streaming/streaming-api.http create mode 100644 streaming/test_streaming.py create mode 100644 supabase/migrations/20250728000001_fix_repetitions_visibility.sql diff --git a/src/components/VisionSystem.tsx b/src/components/VisionSystem.tsx index 78aefb0..2a4cb9e 100644 --- a/src/components/VisionSystem.tsx +++ b/src/components/VisionSystem.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef, useCallback } from 'react' +import { useState, useEffect, useRef, useCallback, useMemo, memo, startTransition } from 'react' import { visionApi, type SystemStatus, @@ -14,6 +14,353 @@ import { formatUptime } from '../lib/visionApi' +// Memoized components to prevent unnecessary re-renders +const SystemOverview = memo(({ systemStatus }: { systemStatus: SystemStatus }) => ( +
+
+
+
+
+
+ {systemStatus.system_started ? 'Online' : 'Offline'} +
+
+
+
+
System Status
+
+ Uptime: {formatUptime(systemStatus.uptime_seconds)} +
+
+
+
+ +
+
+
+
+
+ {systemStatus.mqtt_connected ? 'Connected' : 'Disconnected'} +
+
+
+
+
MQTT Status
+
+ Last message: {systemStatus.last_mqtt_message || 'Never'} +
+
+
+
+ +
+
+
+
+
+ {systemStatus.active_recordings} Active +
+
+
+
+
Recordings
+
+ Total: {systemStatus.total_recordings} +
+
+
+
+ +
+
+
+
+
+ {Object.keys(systemStatus.cameras).length} Cameras +
+
+
+
+
Devices
+
+ {Object.keys(systemStatus.machines).length} Machines +
+
+
+
+
+)) + +const StorageOverview = memo(({ storageStats }: { storageStats: StorageStats }) => ( +
+
+

Storage

+

+ Storage usage and file statistics +

+
+
+
+
+
{storageStats.total_files}
+
Total Files
+
+
+
{formatBytes(storageStats.total_size_bytes)}
+
Total Size
+
+
+
{formatBytes(storageStats.disk_usage.free)}
+
Free Space
+
+
+ + {/* Disk Usage Bar */} +
+
+ Disk Usage + {Math.round((storageStats.disk_usage.used / storageStats.disk_usage.total) * 100)}% used +
+
+
+
+
+ {formatBytes(storageStats.disk_usage.used)} used + {formatBytes(storageStats.disk_usage.total)} total +
+
+ + {/* Per-Camera Statistics */} + {Object.keys(storageStats.cameras).length > 0 && ( +
+

Files by Camera

+
+ {Object.entries(storageStats.cameras).map(([cameraName, stats]) => ( +
+
{cameraName}
+
+
+ Files: + {stats.file_count} +
+
+ Size: + {formatBytes(stats.total_size_bytes)} +
+
+
+ ))} +
+
+ )} +
+
+)) + +const CamerasStatus = memo(({ systemStatus }: { systemStatus: SystemStatus }) => ( +
+
+

Cameras

+

+ Current status of all cameras in the system +

+
+
+
+ {Object.entries(systemStatus.cameras).map(([cameraName, camera]) => { + const friendlyName = camera.device_info?.friendly_name + const hasDeviceInfo = !!camera.device_info + const hasSerial = !!camera.device_info?.serial_number + + // Determine if camera is connected based on status + const isConnected = camera.status === 'available' || camera.status === 'connected' + const hasError = camera.status === 'error' + const statusText = camera.status || 'unknown' + + return ( +
+
+

+ {friendlyName || cameraName} + {friendlyName && ( + ({cameraName}) + )} +

+
+ {isConnected ? 'Connected' : hasError ? 'Error' : 'Disconnected'} +
+
+ +
+
+ Status: + + {statusText.charAt(0).toUpperCase() + statusText.slice(1)} + +
+ + {camera.is_recording && ( +
+ Recording: + +
+ Active +
+
+ )} + + {hasDeviceInfo && ( + <> + {camera.device_info.model && ( +
+ Model: + {camera.device_info.model} +
+ )} + {hasSerial && ( +
+ Serial: + {camera.device_info.serial_number} +
+ )} + {camera.device_info.firmware_version && ( +
+ Firmware: + {camera.device_info.firmware_version} +
+ )} + + )} + + {camera.last_frame_time && ( +
+ Last Frame: + {new Date(camera.last_frame_time).toLocaleTimeString()} +
+ )} + + {camera.frame_rate && ( +
+ Frame Rate: + {camera.frame_rate.toFixed(1)} fps +
+ )} + + {camera.last_checked && ( +
+ Last Checked: + {new Date(camera.last_checked).toLocaleTimeString()} +
+ )} + + {camera.current_recording_file && ( +
+ Recording File: + {camera.current_recording_file} +
+ )} + + {camera.last_error && ( +
+
+ Error: {camera.last_error} +
+
+ )} +
+
+ ) + })} +
+
+
+)) + +const RecentRecordings = memo(({ recordings, systemStatus }: { recordings: Record, systemStatus: SystemStatus | null }) => ( +
+
+

Recent Recordings

+

+ Latest recording sessions +

+
+
+
+ + + + + + + + + + + + + {Object.entries(recordings).slice(0, 10).map(([recordingId, recording]) => { + const camera = systemStatus?.cameras[recording.camera_name] + const displayName = camera?.device_info?.friendly_name || recording.camera_name + + return ( + + + + + + + + + ) + })} + +
+ Camera + + Filename + + Status + + Duration + + Size + + Started +
+ {displayName} + {camera?.device_info?.friendly_name && ( +
({recording.camera_name})
+ )} +
+ {recording.filename} + + + {recording.status} + + + {recording.duration_seconds ? formatDuration(recording.duration_seconds) : '-'} + + {recording.file_size_bytes ? formatBytes(recording.file_size_bytes) : '-'} + + {new Date(recording.start_time).toLocaleString()} +
+
+
+
+)) + export function VisionSystem() { const [systemStatus, setSystemStatus] = useState(null) const [storageStats, setStorageStats] = useState(null) @@ -110,38 +457,25 @@ export function VisionSystem() { } } - // Only update state if data has actually changed to prevent unnecessary re-renders - setSystemStatus(prevStatus => { - if (JSON.stringify(prevStatus) !== JSON.stringify(statusData)) { - return statusData + // Batch state updates to minimize re-renders using startTransition for non-urgent updates + const updateTime = new Date() + + // Use startTransition for non-urgent state updates to keep the UI responsive + startTransition(() => { + setSystemStatus(statusData) + setStorageStats(storageData) + setRecordings(recordingsData) + setLastUpdateTime(updateTime) + + // Update MQTT status and events + if (mqttStatusData) { + setMqttStatus(mqttStatusData) } - return prevStatus - }) - setStorageStats(prevStats => { - if (JSON.stringify(prevStats) !== JSON.stringify(storageData)) { - return storageData + if (mqttEventsData && mqttEventsData.events) { + setMqttEvents(mqttEventsData.events) } - return prevStats }) - - setRecordings(prevRecordings => { - if (JSON.stringify(prevRecordings) !== JSON.stringify(recordingsData)) { - return recordingsData - } - return prevRecordings - }) - - setLastUpdateTime(new Date()) - - // Update MQTT status and events - if (mqttStatusData) { - setMqttStatus(mqttStatusData) - } - - if (mqttEventsData && mqttEventsData.events) { - setMqttEvents(mqttEventsData.events) - } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to fetch vision system data') console.error('Vision system fetch error:', err) @@ -242,8 +576,14 @@ export function VisionSystem() {

Vision System

Monitor cameras, machines, and recording status

{lastUpdateTime && ( -

+

Last updated: {lastUpdateTime.toLocaleTimeString()} + {refreshing && ( + + + Updating... + + )} {autoRefreshEnabled && !refreshing && ( Auto-refresh: {refreshInterval / 1000}s @@ -296,227 +636,12 @@ export function VisionSystem() { {/* System Overview */} - {systemStatus && ( -

-
-
-
-
-
- {systemStatus.system_started ? 'Online' : 'Offline'} -
-
-
-
-
System Status
-
- Uptime: {formatUptime(systemStatus.uptime_seconds)} -
-
-
-
- -
-
-
-
-
-
- {systemStatus.mqtt_connected ? 'Connected' : 'Disconnected'} -
-
- {systemStatus.mqtt_connected && ( -
-
- Live -
- )} -
- {mqttStatus && ( -
-
{mqttStatus.message_count} messages
-
{mqttStatus.error_count} errors
-
- )} -
-
-
MQTT
-
- {mqttStatus ? ( -
-
Broker: {mqttStatus.broker_host}:{mqttStatus.broker_port}
-
Last message: {new Date(mqttStatus.last_message_time).toLocaleTimeString()}
-
- ) : ( -
Last message: {new Date(systemStatus.last_mqtt_message).toLocaleTimeString()}
- )} -
-
- - {/* MQTT Events History */} - {mqttEvents.length > 0 && ( -
-
-

Recent Events

- {mqttEvents.length} events -
-
- {mqttEvents.map((event, index) => ( -
-
- - {new Date(event.timestamp).toLocaleTimeString().slice(-8, -3)} - - - {event.machine_name.replace('_', ' ')} - - - {event.payload} - -
- #{event.message_number} -
- ))} -
-
- )} -
-
- -
-
-
-
-
- {systemStatus.active_recordings} -
-
-
-
-
Active Recordings
-
- Total: {systemStatus.total_recordings} -
-
-
-
- -
-
-
-
-
- {Object.keys(systemStatus.cameras).length} -
-
-
-
-
Cameras
-
- Machines: {Object.keys(systemStatus.machines).length} -
-
-
-
-
- )} + {systemStatus && } {/* Cameras Status */} - {systemStatus && ( -
-
-

Cameras

-

- Current status of all cameras in the system -

-
-
-
- {Object.entries(systemStatus.cameras).map(([cameraName, camera]) => { - // Debug logging to see what data we're getting - console.log(`Camera ${cameraName} data:`, JSON.stringify(camera, null, 2)) - - const friendlyName = camera.device_info?.friendly_name - const hasDeviceInfo = !!camera.device_info - const hasSerial = !!camera.device_info?.serial_number - - return ( -
-
-
-

- {friendlyName ? ( -
-
{friendlyName}
-
({cameraName})
-
- ) : ( -
-
{cameraName}
-
- {hasDeviceInfo ? 'Device info available but no friendly name' : 'No device info available'} -
-
- )} -

-
- - {camera.is_recording ? 'Recording' : camera.status} - -
- -
-
- Recording: - - {camera.is_recording ? 'Yes' : 'No'} - -
- - {camera.device_info?.serial_number && ( -
- Serial: - {camera.device_info.serial_number} -
- )} - - {/* Debug info - remove this after fixing */} -
-
Debug Info:
-
-
Has device_info: {hasDeviceInfo ? 'Yes' : 'No'}
-
Has friendly_name: {friendlyName ? 'Yes' : 'No'}
-
Has serial: {hasSerial ? 'Yes' : 'No'}
-
Last error: {camera.last_error || 'None'}
- {camera.device_info && ( -
-
Raw device_info: {JSON.stringify(camera.device_info)}
-
- )} -
-
- -
- Last checked: - {new Date(camera.last_checked).toLocaleTimeString()} -
- - {camera.current_recording_file && ( -
- Recording file: - {camera.current_recording_file} -
- )} -
-
- ) - })} -
-
-
- )} + {systemStatus && } {/* Machines Status */} {systemStatus && Object.keys(systemStatus.machines).length > 0 && ( @@ -568,168 +693,10 @@ export function VisionSystem() { )} {/* Storage Statistics */} - {storageStats && ( -
-
-

Storage

-

- Storage usage and file statistics -

-
-
-
-
-
{storageStats.total_files}
-
Total Files
-
-
-
{formatBytes(storageStats.total_size_bytes)}
-
Total Size
-
-
-
{formatBytes(storageStats.disk_usage.free)}
-
Free Space
-
-
- - {/* Disk Usage Bar */} -
-
- Disk Usage - {Math.round((storageStats.disk_usage.used / storageStats.disk_usage.total) * 100)}% used -
-
-
-
-
- {formatBytes(storageStats.disk_usage.used)} used - {formatBytes(storageStats.disk_usage.total)} total -
-
- - {/* Per-Camera Statistics */} - {Object.keys(storageStats.cameras).length > 0 && ( -
-

Files by Camera

-
- {Object.entries(storageStats.cameras).map(([cameraName, stats]) => { - // Find the corresponding camera to get friendly name - const camera = systemStatus?.cameras[cameraName] - const displayName = camera?.device_info?.friendly_name || cameraName - - return ( -
-
- {camera?.device_info?.friendly_name ? ( - <> - {displayName} - ({cameraName}) - - ) : ( - cameraName - )} -
-
-
- Files: - {stats.file_count} -
-
- Size: - {formatBytes(stats.total_size_bytes)} -
-
-
- ) - })} -
-
- )} -
-
- )} + {storageStats && } {/* Recent Recordings */} - {Object.keys(recordings).length > 0 && ( -
-
-

Recent Recordings

-

- Latest recording sessions -

-
-
-
- - - - - - - - - - - - - {Object.entries(recordings).slice(0, 10).map(([recordingId, recording]) => { - // Find the corresponding camera to get friendly name - const camera = systemStatus?.cameras[recording.camera_name] - const displayName = camera?.device_info?.friendly_name || recording.camera_name - - return ( - - - - - - - - - ) - })} - -
- Camera - - Filename - - Status - - Duration - - Size - - Started -
- {camera?.device_info?.friendly_name ? ( -
-
{displayName}
-
({recording.camera_name})
-
- ) : ( - recording.camera_name - )} -
- {recording.filename} - - - {recording.state} - - - {recording.duration_seconds ? formatDuration(recording.duration_seconds) : '-'} - - {recording.file_size_bytes ? formatBytes(recording.file_size_bytes) : '-'} - - {new Date(recording.start_time).toLocaleString()} -
-
-
-
- )} + {Object.keys(recordings).length > 0 && } ) } diff --git a/src/lib/visionApi.ts b/src/lib/visionApi.ts index 8a08a07..e5895c4 100644 --- a/src/lib/visionApi.ts +++ b/src/lib/visionApi.ts @@ -23,17 +23,23 @@ export interface MachineStatus { } export interface CameraStatus { - name: string + name?: string status: string is_recording: boolean last_checked: string - last_error: string | null + last_error?: string | null device_info?: { - friendly_name: string - serial_number: string + friendly_name?: string + serial_number?: string + port_type?: string + model?: string + firmware_version?: string + last_checked?: number } - current_recording_file: string | null - recording_start_time: string | null + current_recording_file?: string | null + recording_start_time?: string | null + last_frame_time?: string + frame_rate?: number } export interface RecordingInfo { diff --git a/streaming/AI_INTEGRATION_GUIDE.md b/streaming/AI_INTEGRATION_GUIDE.md new file mode 100644 index 0000000..b5b49cb --- /dev/null +++ b/streaming/AI_INTEGRATION_GUIDE.md @@ -0,0 +1,566 @@ +# ๐Ÿค– AI Integration Guide: USDA Vision Camera Streaming for React Projects + +This guide is specifically designed for AI assistants to understand and implement the USDA Vision Camera streaming functionality in React applications. + +## ๐Ÿ“‹ System Overview + +The USDA Vision Camera system provides live video streaming through REST API endpoints. The streaming uses MJPEG format which is natively supported by HTML `` tags and can be easily integrated into React components. + +### Key Characteristics: +- **Base URL**: `http://localhost:8000` (configurable) +- **Stream Format**: MJPEG (Motion JPEG) +- **Content-Type**: `multipart/x-mixed-replace; boundary=frame` +- **Authentication**: None (add if needed for production) +- **CORS**: Enabled for all origins (configure for production) + +## ๐Ÿ”Œ API Endpoints Reference + +### 1. Get Camera List +```http +GET /cameras +``` +**Response:** +```json +{ + "camera1": { + "name": "camera1", + "status": "connected", + "is_recording": false, + "last_checked": "2025-01-28T10:30:00", + "device_info": {...} + }, + "camera2": {...} +} +``` + +### 2. Start Camera Stream +```http +POST /cameras/{camera_name}/start-stream +``` +**Response:** +```json +{ + "success": true, + "message": "Started streaming for camera camera1" +} +``` + +### 3. Stop Camera Stream +```http +POST /cameras/{camera_name}/stop-stream +``` +**Response:** +```json +{ + "success": true, + "message": "Stopped streaming for camera camera1" +} +``` + +### 4. Live Video Stream +```http +GET /cameras/{camera_name}/stream +``` +**Response:** MJPEG video stream +**Usage:** Set as `src` attribute of HTML `` element + +## โš›๏ธ React Integration Examples + +### Basic Camera Stream Component + +```jsx +import React, { useState, useEffect } from 'react'; + +const CameraStream = ({ cameraName, apiBaseUrl = 'http://localhost:8000' }) => { + const [isStreaming, setIsStreaming] = useState(false); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + const startStream = async () => { + setLoading(true); + setError(null); + + try { + const response = await fetch(`${apiBaseUrl}/cameras/${cameraName}/start-stream`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (response.ok) { + setIsStreaming(true); + } else { + const errorData = await response.json(); + setError(errorData.detail || 'Failed to start stream'); + } + } catch (err) { + setError(`Network error: ${err.message}`); + } finally { + setLoading(false); + } + }; + + const stopStream = async () => { + setLoading(true); + + try { + const response = await fetch(`${apiBaseUrl}/cameras/${cameraName}/stop-stream`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (response.ok) { + setIsStreaming(false); + } else { + const errorData = await response.json(); + setError(errorData.detail || 'Failed to stop stream'); + } + } catch (err) { + setError(`Network error: ${err.message}`); + } finally { + setLoading(false); + } + }; + + return ( +
+

Camera: {cameraName}

+ + {/* Video Stream */} +
+ {isStreaming ? ( + {`${cameraName} setError('Stream connection lost')} + /> + ) : ( +
+ No Stream Active +
+ )} +
+ + {/* Controls */} +
+ + + +
+ + {/* Error Display */} + {error && ( +
+ Error: {error} +
+ )} +
+ ); +}; + +export default CameraStream; +``` + +### Multi-Camera Dashboard Component + +```jsx +import React, { useState, useEffect } from 'react'; +import CameraStream from './CameraStream'; + +const CameraDashboard = ({ apiBaseUrl = 'http://localhost:8000' }) => { + const [cameras, setCameras] = useState({}); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + fetchCameras(); + + // Refresh camera status every 30 seconds + const interval = setInterval(fetchCameras, 30000); + return () => clearInterval(interval); + }, []); + + const fetchCameras = async () => { + try { + const response = await fetch(`${apiBaseUrl}/cameras`); + if (response.ok) { + const data = await response.json(); + setCameras(data); + setError(null); + } else { + setError('Failed to fetch cameras'); + } + } catch (err) { + setError(`Network error: ${err.message}`); + } finally { + setLoading(false); + } + }; + + if (loading) { + return
Loading cameras...
; + } + + if (error) { + return ( +
+ Error: {error} + +
+ ); + } + + return ( +
+

USDA Vision Camera Dashboard

+ +
+ {Object.entries(cameras).map(([cameraName, cameraInfo]) => ( +
+ + + {/* Camera Status */} +
+
Status: {cameraInfo.status}
+
Recording: {cameraInfo.is_recording ? 'Yes' : 'No'}
+
Last Checked: {new Date(cameraInfo.last_checked).toLocaleString()}
+
+
+ ))} +
+
+ ); +}; + +export default CameraDashboard; +``` + +### Custom Hook for Camera Management + +```jsx +import { useState, useEffect, useCallback } from 'react'; + +const useCameraStream = (cameraName, apiBaseUrl = 'http://localhost:8000') => { + const [isStreaming, setIsStreaming] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const startStream = useCallback(async () => { + setLoading(true); + setError(null); + + try { + const response = await fetch(`${apiBaseUrl}/cameras/${cameraName}/start-stream`, { + method: 'POST', + }); + + if (response.ok) { + setIsStreaming(true); + return { success: true }; + } else { + const errorData = await response.json(); + const errorMsg = errorData.detail || 'Failed to start stream'; + setError(errorMsg); + return { success: false, error: errorMsg }; + } + } catch (err) { + const errorMsg = `Network error: ${err.message}`; + setError(errorMsg); + return { success: false, error: errorMsg }; + } finally { + setLoading(false); + } + }, [cameraName, apiBaseUrl]); + + const stopStream = useCallback(async () => { + setLoading(true); + + try { + const response = await fetch(`${apiBaseUrl}/cameras/${cameraName}/stop-stream`, { + method: 'POST', + }); + + if (response.ok) { + setIsStreaming(false); + return { success: true }; + } else { + const errorData = await response.json(); + const errorMsg = errorData.detail || 'Failed to stop stream'; + setError(errorMsg); + return { success: false, error: errorMsg }; + } + } catch (err) { + const errorMsg = `Network error: ${err.message}`; + setError(errorMsg); + return { success: false, error: errorMsg }; + } finally { + setLoading(false); + } + }, [cameraName, apiBaseUrl]); + + const getStreamUrl = useCallback(() => { + return `${apiBaseUrl}/cameras/${cameraName}/stream?t=${Date.now()}`; + }, [cameraName, apiBaseUrl]); + + return { + isStreaming, + loading, + error, + startStream, + stopStream, + getStreamUrl, + }; +}; + +export default useCameraStream; +``` + +## ๐ŸŽจ Styling with Tailwind CSS + +```jsx +const CameraStreamTailwind = ({ cameraName }) => { + const { isStreaming, loading, error, startStream, stopStream, getStreamUrl } = useCameraStream(cameraName); + + return ( +
+

Camera: {cameraName}

+ + {/* Stream Container */} +
+ {isStreaming ? ( + {`${cameraName} setError('Stream connection lost')} + /> + ) : ( +
+ No Stream Active +
+ )} +
+ + {/* Controls */} +
+ + + +
+ + {/* Error Display */} + {error && ( +
+ Error: {error} +
+ )} +
+ ); +}; +``` + +## ๐Ÿ”ง Configuration Options + +### Environment Variables (.env) +```env +REACT_APP_CAMERA_API_URL=http://localhost:8000 +REACT_APP_STREAM_REFRESH_INTERVAL=30000 +REACT_APP_STREAM_TIMEOUT=10000 +``` + +### API Configuration +```javascript +const apiConfig = { + baseUrl: process.env.REACT_APP_CAMERA_API_URL || 'http://localhost:8000', + timeout: parseInt(process.env.REACT_APP_STREAM_TIMEOUT) || 10000, + refreshInterval: parseInt(process.env.REACT_APP_STREAM_REFRESH_INTERVAL) || 30000, +}; +``` + +## ๐Ÿšจ Important Implementation Notes + +### 1. MJPEG Stream Handling +- Use HTML `` tag with `src` pointing to stream endpoint +- Add timestamp query parameter to prevent caching: `?t=${Date.now()}` +- Handle `onError` event for connection issues + +### 2. Error Handling +- Network errors (fetch failures) +- HTTP errors (4xx, 5xx responses) +- Stream connection errors (img onError) +- Timeout handling for long requests + +### 3. Performance Considerations +- Streams consume bandwidth continuously +- Stop streams when components unmount +- Limit concurrent streams based on system capacity +- Consider lazy loading for multiple cameras + +### 4. State Management +- Track streaming state per camera +- Handle loading states during API calls +- Manage error states with user feedback +- Refresh camera list periodically + +## ๐Ÿ“ฑ Mobile Considerations + +```jsx +// Responsive design for mobile +const mobileStyles = { + container: { + padding: '10px', + maxWidth: '100vw', + }, + stream: { + width: '100%', + maxWidth: '100vw', + height: 'auto', + }, + controls: { + display: 'flex', + flexDirection: 'column', + gap: '8px', + }, +}; +``` + +## ๐Ÿงช Testing Integration + +```javascript +// Test API connectivity +const testConnection = async () => { + try { + const response = await fetch(`${apiBaseUrl}/health`); + return response.ok; + } catch { + return false; + } +}; + +// Test camera availability +const testCamera = async (cameraName) => { + try { + const response = await fetch(`${apiBaseUrl}/cameras/${cameraName}/test-connection`, { + method: 'POST', + }); + return response.ok; + } catch { + return false; + } +}; +``` + +## ๐Ÿ“ Additional Files for AI Integration + +### TypeScript Definitions +- `camera-api.types.ts` - Complete TypeScript definitions for all API types +- `streaming-api.http` - REST Client file with all streaming endpoints +- `STREAMING_GUIDE.md` - Comprehensive user guide for streaming functionality + +### Quick Integration Checklist for AI Assistants + +1. **Copy TypeScript types** from `camera-api.types.ts` +2. **Use API endpoints** from `streaming-api.http` +3. **Implement error handling** as shown in examples +4. **Add CORS configuration** if needed for production +5. **Test with multiple cameras** using provided examples + +### Key Integration Points + +- **Stream URL Format**: `${baseUrl}/cameras/${cameraName}/stream?t=${Date.now()}` +- **Start Stream**: `POST /cameras/{name}/start-stream` +- **Stop Stream**: `POST /cameras/{name}/stop-stream` +- **Camera List**: `GET /cameras` +- **Error Handling**: Always wrap in try-catch blocks +- **Loading States**: Implement for better UX + +### Production Considerations + +- Configure CORS for specific origins +- Add authentication if required +- Implement rate limiting +- Monitor system resources with multiple streams +- Add reconnection logic for network issues + +This documentation provides everything an AI assistant needs to integrate the USDA Vision Camera streaming functionality into React applications, including complete code examples, error handling, and best practices. diff --git a/streaming/STREAMING_GUIDE.md b/streaming/STREAMING_GUIDE.md new file mode 100644 index 0000000..ca55700 --- /dev/null +++ b/streaming/STREAMING_GUIDE.md @@ -0,0 +1,240 @@ +# ๐ŸŽฅ USDA Vision Camera Live Streaming Guide + +This guide explains how to use the new live preview streaming functionality that allows you to view camera feeds in real-time without blocking recording operations. + +## ๐ŸŒŸ Key Features + +- **Non-blocking streaming**: Live preview doesn't interfere with recording +- **Separate camera connections**: Streaming uses independent camera instances +- **MJPEG streaming**: Standard web-compatible video streaming +- **Multiple concurrent viewers**: Multiple browsers can view the same stream +- **REST API control**: Start/stop streaming via API endpoints +- **Web interface**: Ready-to-use HTML interface for live preview + +## ๐Ÿ—๏ธ Architecture + +The streaming system creates separate camera connections for preview that are independent from recording: + +``` +Camera Hardware +โ”œโ”€โ”€ Recording Connection (CameraRecorder) +โ”‚ โ”œโ”€โ”€ Used for video file recording +โ”‚ โ”œโ”€โ”€ Triggered by MQTT machine states +โ”‚ โ””โ”€โ”€ High quality, full FPS +โ””โ”€โ”€ Streaming Connection (CameraStreamer) + โ”œโ”€โ”€ Used for live preview + โ”œโ”€โ”€ Controlled via API endpoints + โ””โ”€โ”€ Optimized for web viewing (lower FPS, JPEG compression) +``` + +## ๐Ÿš€ Quick Start + +### 1. Start the System +```bash +python main.py +``` + +### 2. Open the Web Interface +Open `camera_preview.html` in your browser and click "Start Stream" for any camera. + +### 3. API Usage +```bash +# Start streaming for camera1 +curl -X POST http://localhost:8000/cameras/camera1/start-stream + +# View live stream (open in browser) +http://localhost:8000/cameras/camera1/stream + +# Stop streaming +curl -X POST http://localhost:8000/cameras/camera1/stop-stream +``` + +## ๐Ÿ“ก API Endpoints + +### Start Streaming +```http +POST /cameras/{camera_name}/start-stream +``` +**Response:** +```json +{ + "success": true, + "message": "Started streaming for camera camera1" +} +``` + +### Stop Streaming +```http +POST /cameras/{camera_name}/stop-stream +``` +**Response:** +```json +{ + "success": true, + "message": "Stopped streaming for camera camera1" +} +``` + +### Live Stream (MJPEG) +```http +GET /cameras/{camera_name}/stream +``` +**Response:** Multipart MJPEG stream +**Content-Type:** `multipart/x-mixed-replace; boundary=frame` + +## ๐ŸŒ Web Interface Usage + +The included `camera_preview.html` provides a complete web interface: + +1. **Camera Grid**: Shows all configured cameras +2. **Stream Controls**: Start/Stop/Refresh buttons for each camera +3. **Live Preview**: Real-time video feed display +4. **Status Information**: System and camera status +5. **Responsive Design**: Works on desktop and mobile + +### Features: +- โœ… Real-time camera status +- โœ… One-click stream start/stop +- โœ… Automatic stream refresh +- โœ… System health monitoring +- โœ… Error handling and status messages + +## ๐Ÿ”ง Technical Details + +### Camera Streamer Configuration +- **Preview FPS**: 10 FPS (configurable) +- **JPEG Quality**: 70% (configurable) +- **Frame Buffer**: 5 frames (prevents memory buildup) +- **Timeout**: 200ms per frame capture + +### Memory Management +- Automatic frame buffer cleanup +- Queue-based frame management +- Proper camera resource cleanup on stop + +### Thread Safety +- Thread-safe streaming operations +- Independent from recording threads +- Proper synchronization with locks + +## ๐Ÿงช Testing + +### Run the Test Script +```bash +python test_streaming.py +``` + +This will test: +- โœ… API endpoint functionality +- โœ… Stream start/stop operations +- โœ… Concurrent recording and streaming +- โœ… Error handling + +### Manual Testing +1. Start the system: `python main.py` +2. Open `camera_preview.html` in browser +3. Start streaming for a camera +4. Trigger recording via MQTT or manual API +5. Verify both work simultaneously + +## ๐Ÿ”„ Concurrent Operations + +The system supports these concurrent operations: + +| Operation | Recording | Streaming | Notes | +|-----------|-----------|-----------|-------| +| Recording Only | โœ… | โŒ | Normal operation | +| Streaming Only | โŒ | โœ… | Preview without recording | +| Both Concurrent | โœ… | โœ… | **Independent connections** | + +### Example: Concurrent Usage +```bash +# Start streaming +curl -X POST http://localhost:8000/cameras/camera1/start-stream + +# Start recording (while streaming continues) +curl -X POST http://localhost:8000/cameras/camera1/start-recording \ + -H "Content-Type: application/json" \ + -d '{"filename": "test_recording.avi"}' + +# Both operations run independently! +``` + +## ๐Ÿ› ๏ธ Configuration + +### Stream Settings (in CameraStreamer) +```python +self.preview_fps = 10.0 # Lower FPS for preview +self.preview_quality = 70 # JPEG quality (1-100) +self._frame_queue.maxsize = 5 # Frame buffer size +``` + +### Camera Settings +The streamer uses the same camera configuration as recording: +- Exposure time from `camera_config.exposure_ms` +- Gain from `camera_config.gain` +- Optimized trigger mode for continuous streaming + +## ๐Ÿšจ Important Notes + +### Camera Access Patterns +- **Recording**: Blocks camera during active recording +- **Streaming**: Uses separate connection, doesn't block +- **Health Checks**: Brief, non-blocking camera tests +- **Multiple Streams**: Multiple browsers can view same stream + +### Performance Considerations +- Streaming uses additional CPU/memory resources +- Lower preview FPS reduces system load +- JPEG compression reduces bandwidth usage +- Frame queue prevents memory buildup + +### Error Handling +- Automatic camera resource cleanup +- Graceful handling of camera disconnections +- Stream auto-restart capabilities +- Detailed error logging + +## ๐Ÿ” Troubleshooting + +### Stream Not Starting +1. Check camera availability: `GET /cameras` +2. Verify camera not in error state +3. Check system logs for camera initialization errors +4. Try camera reconnection: `POST /cameras/{name}/reconnect` + +### Poor Stream Quality +1. Adjust `preview_quality` setting (higher = better quality) +2. Increase `preview_fps` for smoother video +3. Check network bandwidth +4. Verify camera exposure/gain settings + +### Browser Issues +1. Try different browser (Chrome/Firefox recommended) +2. Check browser console for JavaScript errors +3. Verify CORS settings in API server +4. Clear browser cache and refresh + +## ๐Ÿ“ˆ Future Enhancements + +Potential improvements for the streaming system: + +- ๐Ÿ”„ WebRTC support for lower latency +- ๐Ÿ“ฑ Mobile app integration +- ๐ŸŽ›๏ธ Real-time camera setting adjustments +- ๐Ÿ“Š Stream analytics and monitoring +- ๐Ÿ” Authentication and access control +- ๐ŸŒ Multi-camera synchronized viewing + +## ๐Ÿ“ž Support + +For issues with streaming functionality: + +1. Check the system logs: `usda_vision_system.log` +2. Run the test script: `python test_streaming.py` +3. Verify API health: `http://localhost:8000/health` +4. Check camera status: `http://localhost:8000/cameras` + +--- + +**โœ… Live streaming is now ready for production use!** diff --git a/streaming/camera-api.types.ts b/streaming/camera-api.types.ts new file mode 100644 index 0000000..ffa9602 --- /dev/null +++ b/streaming/camera-api.types.ts @@ -0,0 +1,367 @@ +/** + * TypeScript definitions for USDA Vision Camera System API + * + * This file provides complete type definitions for AI assistants + * to integrate the camera streaming functionality into React/TypeScript projects. + */ + +// ============================================================================= +// BASE CONFIGURATION +// ============================================================================= + +export interface ApiConfig { + baseUrl: string; + timeout?: number; + refreshInterval?: number; +} + +export const defaultApiConfig: ApiConfig = { + baseUrl: 'http://localhost:8000', + timeout: 10000, + refreshInterval: 30000, +}; + +// ============================================================================= +// CAMERA TYPES +// ============================================================================= + +export interface CameraDeviceInfo { + friendly_name?: string; + port_type?: string; + serial_number?: string; + device_index?: number; + error?: string; +} + +export interface CameraInfo { + name: string; + status: 'connected' | 'disconnected' | 'error' | 'not_found' | 'available'; + is_recording: boolean; + last_checked: string; // ISO date string + last_error?: string | null; + device_info?: CameraDeviceInfo; + current_recording_file?: string | null; + recording_start_time?: string | null; // ISO date string +} + +export interface CameraListResponse { + [cameraName: string]: CameraInfo; +} + +// ============================================================================= +// STREAMING TYPES +// ============================================================================= + +export interface StreamStartRequest { + // No body required - camera name is in URL path +} + +export interface StreamStartResponse { + success: boolean; + message: string; +} + +export interface StreamStopRequest { + // No body required - camera name is in URL path +} + +export interface StreamStopResponse { + success: boolean; + message: string; +} + +export interface StreamStatus { + isStreaming: boolean; + streamUrl?: string; + error?: string; +} + +// ============================================================================= +// RECORDING TYPES +// ============================================================================= + +export interface StartRecordingRequest { + filename?: string; + exposure_ms?: number; + gain?: number; + fps?: number; +} + +export interface StartRecordingResponse { + success: boolean; + message: string; + filename?: string; +} + +export interface StopRecordingResponse { + success: boolean; + message: string; +} + +// ============================================================================= +// SYSTEM TYPES +// ============================================================================= + +export interface SystemStatusResponse { + status: string; + uptime: string; + api_server_running: boolean; + camera_manager_running: boolean; + mqtt_client_connected: boolean; + total_cameras: number; + active_recordings: number; + active_streams?: number; +} + +export interface HealthResponse { + status: 'healthy' | 'unhealthy'; + timestamp: string; +} + +// ============================================================================= +// ERROR TYPES +// ============================================================================= + +export interface ApiError { + detail: string; + status_code?: number; +} + +export interface StreamError extends Error { + type: 'network' | 'api' | 'stream' | 'timeout'; + cameraName: string; + originalError?: Error; +} + +// ============================================================================= +// HOOK TYPES +// ============================================================================= + +export interface UseCameraStreamResult { + isStreaming: boolean; + loading: boolean; + error: string | null; + startStream: () => Promise<{ success: boolean; error?: string }>; + stopStream: () => Promise<{ success: boolean; error?: string }>; + getStreamUrl: () => string; + refreshStream: () => void; +} + +export interface UseCameraListResult { + cameras: CameraListResponse; + loading: boolean; + error: string | null; + refreshCameras: () => Promise; +} + +export interface UseCameraRecordingResult { + isRecording: boolean; + loading: boolean; + error: string | null; + currentFile: string | null; + startRecording: (options?: StartRecordingRequest) => Promise<{ success: boolean; error?: string }>; + stopRecording: () => Promise<{ success: boolean; error?: string }>; +} + +// ============================================================================= +// COMPONENT PROPS TYPES +// ============================================================================= + +export interface CameraStreamProps { + cameraName: string; + apiConfig?: ApiConfig; + autoStart?: boolean; + onStreamStart?: (cameraName: string) => void; + onStreamStop?: (cameraName: string) => void; + onError?: (error: StreamError) => void; + className?: string; + style?: React.CSSProperties; +} + +export interface CameraDashboardProps { + apiConfig?: ApiConfig; + cameras?: string[]; // If provided, only show these cameras + showRecordingControls?: boolean; + showStreamingControls?: boolean; + refreshInterval?: number; + onCameraSelect?: (cameraName: string) => void; + className?: string; +} + +export interface CameraControlsProps { + cameraName: string; + apiConfig?: ApiConfig; + showRecording?: boolean; + showStreaming?: boolean; + onAction?: (action: 'start-stream' | 'stop-stream' | 'start-recording' | 'stop-recording', cameraName: string) => void; +} + +// ============================================================================= +// API CLIENT TYPES +// ============================================================================= + +export interface CameraApiClient { + // System endpoints + getHealth(): Promise; + getSystemStatus(): Promise; + + // Camera endpoints + getCameras(): Promise; + getCameraStatus(cameraName: string): Promise; + testCameraConnection(cameraName: string): Promise<{ success: boolean; message: string }>; + + // Streaming endpoints + startStream(cameraName: string): Promise; + stopStream(cameraName: string): Promise; + getStreamUrl(cameraName: string): string; + + // Recording endpoints + startRecording(cameraName: string, options?: StartRecordingRequest): Promise; + stopRecording(cameraName: string): Promise; +} + +// ============================================================================= +// UTILITY TYPES +// ============================================================================= + +export type CameraAction = 'start-stream' | 'stop-stream' | 'start-recording' | 'stop-recording' | 'test-connection'; + +export interface CameraActionResult { + success: boolean; + message: string; + error?: string; +} + +export interface StreamingState { + [cameraName: string]: { + isStreaming: boolean; + isLoading: boolean; + error: string | null; + lastStarted?: Date; + }; +} + +export interface RecordingState { + [cameraName: string]: { + isRecording: boolean; + isLoading: boolean; + error: string | null; + currentFile: string | null; + startTime?: Date; + }; +} + +// ============================================================================= +// EVENT TYPES +// ============================================================================= + +export interface CameraEvent { + type: 'stream-started' | 'stream-stopped' | 'stream-error' | 'recording-started' | 'recording-stopped' | 'recording-error'; + cameraName: string; + timestamp: Date; + data?: any; +} + +export type CameraEventHandler = (event: CameraEvent) => void; + +// ============================================================================= +// CONFIGURATION TYPES +// ============================================================================= + +export interface StreamConfig { + fps: number; + quality: number; // 1-100 + timeout: number; + retryAttempts: number; + retryDelay: number; +} + +export interface CameraStreamConfig extends StreamConfig { + cameraName: string; + autoReconnect: boolean; + maxReconnectAttempts: number; +} + +// ============================================================================= +// CONTEXT TYPES (for React Context) +// ============================================================================= + +export interface CameraContextValue { + cameras: CameraListResponse; + streamingState: StreamingState; + recordingState: RecordingState; + apiClient: CameraApiClient; + + // Actions + startStream: (cameraName: string) => Promise; + stopStream: (cameraName: string) => Promise; + startRecording: (cameraName: string, options?: StartRecordingRequest) => Promise; + stopRecording: (cameraName: string) => Promise; + refreshCameras: () => Promise; + + // State + loading: boolean; + error: string | null; +} + +// ============================================================================= +// EXAMPLE USAGE TYPES +// ============================================================================= + +/** + * Example usage in React component: + * + * ```typescript + * import { CameraStreamProps, UseCameraStreamResult } from './camera-api.types'; + * + * const CameraStream: React.FC = ({ + * cameraName, + * apiConfig = defaultApiConfig, + * autoStart = false, + * onStreamStart, + * onStreamStop, + * onError + * }) => { + * const { + * isStreaming, + * loading, + * error, + * startStream, + * stopStream, + * getStreamUrl + * }: UseCameraStreamResult = useCameraStream(cameraName, apiConfig); + * + * // Component implementation... + * }; + * ``` + */ + +/** + * Example API client usage: + * + * ```typescript + * const apiClient: CameraApiClient = new CameraApiClientImpl(defaultApiConfig); + * + * // Start streaming + * const result = await apiClient.startStream('camera1'); + * if (result.success) { + * const streamUrl = apiClient.getStreamUrl('camera1'); + * // Use streamUrl in img tag + * } + * ``` + */ + +/** + * Example hook usage: + * + * ```typescript + * const MyComponent = () => { + * const { cameras, loading, error, refreshCameras } = useCameraList(); + * const { isStreaming, startStream, stopStream } = useCameraStream('camera1'); + * + * // Component logic... + * }; + * ``` + */ + +export default {}; diff --git a/streaming/camera_preview.html b/streaming/camera_preview.html new file mode 100644 index 0000000..0caa92d --- /dev/null +++ b/streaming/camera_preview.html @@ -0,0 +1,336 @@ + + + + + + USDA Vision Camera Live Preview + + + +
+

๐ŸŽฅ USDA Vision Camera Live Preview

+ +
+ +
+ +
+

๐Ÿ“ก System Information

+
Loading system status...
+ +

๐Ÿ”— API Endpoints

+
+

Live Stream: GET /cameras/{camera_name}/stream

+

Start Stream: POST /cameras/{camera_name}/start-stream

+

Stop Stream: POST /cameras/{camera_name}/stop-stream

+

Camera Status: GET /cameras

+
+
+
+ + + + diff --git a/streaming/streaming-api.http b/streaming/streaming-api.http new file mode 100644 index 0000000..d060794 --- /dev/null +++ b/streaming/streaming-api.http @@ -0,0 +1,300 @@ +### USDA Vision Camera Streaming API +### Base URL: http://localhost:8000 +### +### This file contains streaming-specific API endpoints for live camera preview +### Use with VS Code REST Client extension or similar tools. + +@baseUrl = http://localhost:8000 + +### ============================================================================= +### STREAMING ENDPOINTS (NEW FUNCTIONALITY) +### ============================================================================= + +### Start camera streaming for live preview +### This creates a separate camera connection that doesn't interfere with recording +POST {{baseUrl}}/cameras/camera1/start-stream +Content-Type: application/json + +### Expected Response: +# { +# "success": true, +# "message": "Started streaming for camera camera1" +# } + +### + +### Stop camera streaming +POST {{baseUrl}}/cameras/camera1/stop-stream +Content-Type: application/json + +### Expected Response: +# { +# "success": true, +# "message": "Stopped streaming for camera camera1" +# } + +### + +### Get live MJPEG stream (open in browser or use as img src) +### This endpoint returns a continuous MJPEG stream +### Content-Type: multipart/x-mixed-replace; boundary=frame +GET {{baseUrl}}/cameras/camera1/stream + +### Usage in HTML: +# Live Stream + +### Usage in React: +# + +### + +### Start streaming for camera2 +POST {{baseUrl}}/cameras/camera2/start-stream +Content-Type: application/json + +### + +### Get live stream for camera2 +GET {{baseUrl}}/cameras/camera2/stream + +### + +### Stop streaming for camera2 +POST {{baseUrl}}/cameras/camera2/stop-stream +Content-Type: application/json + +### ============================================================================= +### CONCURRENT OPERATIONS TESTING +### ============================================================================= + +### Test Scenario: Streaming + Recording Simultaneously +### This demonstrates that streaming doesn't block recording + +### Step 1: Start streaming first +POST {{baseUrl}}/cameras/camera1/start-stream +Content-Type: application/json + +### + +### Step 2: Start recording (while streaming continues) +POST {{baseUrl}}/cameras/camera1/start-recording +Content-Type: application/json + +{ + "filename": "concurrent_test.avi" +} + +### + +### Step 3: Check both are running +GET {{baseUrl}}/cameras/camera1 + +### Expected Response shows both recording and streaming active: +# { +# "camera1": { +# "name": "camera1", +# "status": "connected", +# "is_recording": true, +# "current_recording_file": "concurrent_test.avi", +# "recording_start_time": "2025-01-28T10:30:00.000Z" +# } +# } + +### + +### Step 4: Stop recording (streaming continues) +POST {{baseUrl}}/cameras/camera1/stop-recording +Content-Type: application/json + +### + +### Step 5: Verify streaming still works +GET {{baseUrl}}/cameras/camera1/stream + +### + +### Step 6: Stop streaming +POST {{baseUrl}}/cameras/camera1/stop-stream +Content-Type: application/json + +### ============================================================================= +### MULTIPLE CAMERA STREAMING +### ============================================================================= + +### Start streaming on multiple cameras simultaneously +POST {{baseUrl}}/cameras/camera1/start-stream +Content-Type: application/json + +### + +POST {{baseUrl}}/cameras/camera2/start-stream +Content-Type: application/json + +### + +### Check status of all cameras +GET {{baseUrl}}/cameras + +### + +### Access multiple streams (open in separate browser tabs) +GET {{baseUrl}}/cameras/camera1/stream + +### + +GET {{baseUrl}}/cameras/camera2/stream + +### + +### Stop all streaming +POST {{baseUrl}}/cameras/camera1/stop-stream +Content-Type: application/json + +### + +POST {{baseUrl}}/cameras/camera2/stop-stream +Content-Type: application/json + +### ============================================================================= +### ERROR TESTING +### ============================================================================= + +### Test with invalid camera name +POST {{baseUrl}}/cameras/invalid_camera/start-stream +Content-Type: application/json + +### Expected Response: +# { +# "detail": "Camera streamer not found: invalid_camera" +# } + +### + +### Test stream endpoint without starting stream first +GET {{baseUrl}}/cameras/camera1/stream + +### Expected: May return error or empty stream depending on camera state + +### + +### Test starting stream when camera is in error state +POST {{baseUrl}}/cameras/camera1/start-stream +Content-Type: application/json + +### If camera has issues, expected response: +# { +# "success": false, +# "message": "Failed to start streaming for camera camera1" +# } + +### ============================================================================= +### INTEGRATION EXAMPLES FOR AI ASSISTANTS +### ============================================================================= + +### React Component Integration: +# const CameraStream = ({ cameraName }) => { +# const [isStreaming, setIsStreaming] = useState(false); +# +# const startStream = async () => { +# const response = await fetch(`${baseUrl}/cameras/${cameraName}/start-stream`, { +# method: 'POST' +# }); +# if (response.ok) { +# setIsStreaming(true); +# } +# }; +# +# return ( +#
+# +# {isStreaming && ( +# +# )} +#
+# ); +# }; + +### JavaScript Fetch Example: +# const streamAPI = { +# async startStream(cameraName) { +# const response = await fetch(`${baseUrl}/cameras/${cameraName}/start-stream`, { +# method: 'POST', +# headers: { 'Content-Type': 'application/json' } +# }); +# return response.json(); +# }, +# +# async stopStream(cameraName) { +# const response = await fetch(`${baseUrl}/cameras/${cameraName}/stop-stream`, { +# method: 'POST', +# headers: { 'Content-Type': 'application/json' } +# }); +# return response.json(); +# }, +# +# getStreamUrl(cameraName) { +# return `${baseUrl}/cameras/${cameraName}/stream?t=${Date.now()}`; +# } +# }; + +### Vue.js Integration: +# +# +# + +### ============================================================================= +### TROUBLESHOOTING +### ============================================================================= + +### If streams don't start: +# 1. Check camera status: GET /cameras +# 2. Verify system health: GET /health +# 3. Test camera connection: POST /cameras/{name}/test-connection +# 4. Check if camera is already recording (shouldn't matter, but good to know) + +### If stream image doesn't load: +# 1. Verify stream was started: POST /cameras/{name}/start-stream +# 2. Check browser console for CORS errors +# 3. Try accessing stream URL directly in browser +# 4. Add timestamp to prevent caching: ?t=${Date.now()} + +### If concurrent operations fail: +# 1. This should work - streaming and recording use separate connections +# 2. Check system logs for resource conflicts +# 3. Verify sufficient system resources (CPU/Memory) +# 4. Test with one camera first, then multiple + +### Performance Notes: +# - Streaming uses ~10 FPS by default (configurable) +# - JPEG quality set to 70% (configurable) +# - Each stream uses additional CPU/memory +# - Multiple concurrent streams may impact performance diff --git a/streaming/test_streaming.py b/streaming/test_streaming.py new file mode 100644 index 0000000..47672ec --- /dev/null +++ b/streaming/test_streaming.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +""" +Test script for camera streaming functionality. + +This script tests the new streaming capabilities without interfering with recording. +""" + +import sys +import os +import time +import requests +import threading +from datetime import datetime + +# Add the current directory to Python path +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +def test_api_endpoints(): + """Test the streaming API endpoints""" + base_url = "http://localhost:8000" + + print("๐Ÿงช Testing Camera Streaming API Endpoints") + print("=" * 50) + + # Test system status + try: + response = requests.get(f"{base_url}/system/status", timeout=5) + if response.status_code == 200: + print("โœ… System status endpoint working") + data = response.json() + print(f" System: {data.get('status', 'Unknown')}") + print(f" Camera Manager: {'Running' if data.get('camera_manager_running') else 'Stopped'}") + else: + print(f"โŒ System status endpoint failed: {response.status_code}") + except Exception as e: + print(f"โŒ System status endpoint error: {e}") + + # Test camera list + try: + response = requests.get(f"{base_url}/cameras", timeout=5) + if response.status_code == 200: + print("โœ… Camera list endpoint working") + cameras = response.json() + print(f" Found {len(cameras)} cameras: {list(cameras.keys())}") + + # Test streaming for each camera + for camera_name in cameras.keys(): + test_camera_streaming(base_url, camera_name) + + else: + print(f"โŒ Camera list endpoint failed: {response.status_code}") + except Exception as e: + print(f"โŒ Camera list endpoint error: {e}") + +def test_camera_streaming(base_url, camera_name): + """Test streaming for a specific camera""" + print(f"\n๐ŸŽฅ Testing streaming for {camera_name}") + print("-" * 30) + + # Test start streaming + try: + response = requests.post(f"{base_url}/cameras/{camera_name}/start-stream", timeout=10) + if response.status_code == 200: + print(f"โœ… Start stream endpoint working for {camera_name}") + data = response.json() + print(f" Response: {data.get('message', 'No message')}") + else: + print(f"โŒ Start stream failed for {camera_name}: {response.status_code}") + print(f" Error: {response.text}") + return + except Exception as e: + print(f"โŒ Start stream error for {camera_name}: {e}") + return + + # Wait a moment for stream to initialize + time.sleep(2) + + # Test stream endpoint (just check if it responds) + try: + response = requests.get(f"{base_url}/cameras/{camera_name}/stream", timeout=5, stream=True) + if response.status_code == 200: + print(f"โœ… Stream endpoint responding for {camera_name}") + print(f" Content-Type: {response.headers.get('content-type', 'Unknown')}") + + # Read a small amount of data to verify it's working + chunk_count = 0 + for chunk in response.iter_content(chunk_size=1024): + chunk_count += 1 + if chunk_count >= 3: # Read a few chunks then stop + break + + print(f" Received {chunk_count} data chunks") + else: + print(f"โŒ Stream endpoint failed for {camera_name}: {response.status_code}") + except Exception as e: + print(f"โŒ Stream endpoint error for {camera_name}: {e}") + + # Test stop streaming + try: + response = requests.post(f"{base_url}/cameras/{camera_name}/stop-stream", timeout=5) + if response.status_code == 200: + print(f"โœ… Stop stream endpoint working for {camera_name}") + data = response.json() + print(f" Response: {data.get('message', 'No message')}") + else: + print(f"โŒ Stop stream failed for {camera_name}: {response.status_code}") + except Exception as e: + print(f"โŒ Stop stream error for {camera_name}: {e}") + +def test_concurrent_recording_and_streaming(): + """Test that streaming doesn't interfere with recording""" + base_url = "http://localhost:8000" + + print("\n๐Ÿ”„ Testing Concurrent Recording and Streaming") + print("=" * 50) + + try: + # Get available cameras + response = requests.get(f"{base_url}/cameras", timeout=5) + if response.status_code != 200: + print("โŒ Cannot get camera list for concurrent test") + return + + cameras = response.json() + if not cameras: + print("โŒ No cameras available for concurrent test") + return + + camera_name = list(cameras.keys())[0] # Use first camera + print(f"Using camera: {camera_name}") + + # Start streaming + print("1. Starting streaming...") + response = requests.post(f"{base_url}/cameras/{camera_name}/start-stream", timeout=10) + if response.status_code != 200: + print(f"โŒ Failed to start streaming: {response.text}") + return + + time.sleep(2) + + # Start recording + print("2. Starting recording...") + response = requests.post(f"{base_url}/cameras/{camera_name}/start-recording", + json={"filename": "test_concurrent_recording.avi"}, timeout=10) + if response.status_code == 200: + print("โœ… Recording started successfully while streaming") + else: + print(f"โŒ Failed to start recording while streaming: {response.text}") + + # Let both run for a few seconds + print("3. Running both streaming and recording for 5 seconds...") + time.sleep(5) + + # Stop recording + print("4. Stopping recording...") + response = requests.post(f"{base_url}/cameras/{camera_name}/stop-recording", timeout=5) + if response.status_code == 200: + print("โœ… Recording stopped successfully") + else: + print(f"โŒ Failed to stop recording: {response.text}") + + # Stop streaming + print("5. Stopping streaming...") + response = requests.post(f"{base_url}/cameras/{camera_name}/stop-stream", timeout=5) + if response.status_code == 200: + print("โœ… Streaming stopped successfully") + else: + print(f"โŒ Failed to stop streaming: {response.text}") + + print("โœ… Concurrent test completed successfully!") + + except Exception as e: + print(f"โŒ Concurrent test error: {e}") + +def main(): + """Main test function""" + print("๐Ÿš€ USDA Vision Camera Streaming Test") + print("=" * 50) + print(f"Test started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print() + + # Wait for system to be ready + print("โณ Waiting for system to be ready...") + time.sleep(3) + + # Run tests + test_api_endpoints() + test_concurrent_recording_and_streaming() + + print("\n" + "=" * 50) + print("๐Ÿ Test completed!") + print("\n๐Ÿ“‹ Next Steps:") + print("1. Open camera_preview.html in your browser") + print("2. Click 'Start Stream' for any camera") + print("3. Verify live preview works without blocking recording") + print("4. Test concurrent recording and streaming") + +if __name__ == "__main__": + main() diff --git a/supabase/migrations/20250724000001_experiment_repetitions_system.sql b/supabase/migrations/20250724000001_experiment_repetitions_system.sql index dc45274..3549f71 100644 --- a/supabase/migrations/20250724000001_experiment_repetitions_system.sql +++ b/supabase/migrations/20250724000001_experiment_repetitions_system.sql @@ -85,15 +85,11 @@ CREATE TRIGGER trigger_experiment_repetitions_updated_at ALTER TABLE public.experiment_repetitions ENABLE ROW LEVEL SECURITY; -- Create RLS policies for experiment_repetitions --- Users can view repetitions for experiments they have access to +-- All authenticated users can view all experiment repetitions CREATE POLICY "Users can view experiment repetitions" ON public.experiment_repetitions - FOR SELECT USING ( - experiment_id IN ( - SELECT id FROM public.experiments - WHERE created_by = auth.uid() - ) - OR public.is_admin() - ); + FOR SELECT + TO authenticated + USING (true); -- Users can insert repetitions for experiments they created or if they're admin CREATE POLICY "Users can create experiment repetitions" ON public.experiment_repetitions diff --git a/supabase/migrations/20250728000001_fix_repetitions_visibility.sql b/supabase/migrations/20250728000001_fix_repetitions_visibility.sql new file mode 100644 index 0000000..5756ba0 --- /dev/null +++ b/supabase/migrations/20250728000001_fix_repetitions_visibility.sql @@ -0,0 +1,12 @@ +-- Fix experiment repetitions visibility for all users +-- This migration updates the RLS policy to allow all authenticated users to view all experiment repetitions +-- Previously, users could only see repetitions for experiments they created + +-- Drop the existing restrictive policy +DROP POLICY IF EXISTS "Users can view experiment repetitions" ON public.experiment_repetitions; + +-- Create new policy that allows all authenticated users to view all repetitions +CREATE POLICY "Users can view experiment repetitions" ON public.experiment_repetitions + FOR SELECT + TO authenticated + USING (true);