From 551e5dc2e38f48ae2af8a695c9fdbae52608d720 Mon Sep 17 00:00:00 2001 From: Alireza Vaezi Date: Mon, 4 Aug 2025 15:02:48 -0400 Subject: [PATCH] feat(video-streaming): Implement video streaming feature with components, hooks, services, and utilities - Added centralized exports for video streaming components and hooks. - Implemented `useVideoInfo` hook for fetching and managing video metadata and streaming information. - Developed `useVideoList` hook for managing video list state, fetching, filtering, and pagination. - Created `useVideoPlayer` hook for managing video player state and controls. - Established `videoApiService` for handling API interactions related to video streaming. - Defined TypeScript types for video streaming feature, including video metadata, API responses, and component props. - Added utility functions for video operations, formatting, and data processing. - Created main entry point for the video streaming feature, exporting all public APIs. --- API Documentations/AI_INTEGRATION_GUIDE.md | 6 +- API Documentations/CAMERA_CONFIG_API.md | 10 +- API Documentations/README.md | 42 +-- API Documentations/STREAMING_GUIDE.md | 14 +- API Documentations/camera-api.types.ts | 2 +- .../docs/API_CHANGES_SUMMARY.md | 8 +- API Documentations/docs/API_DOCUMENTATION.md | 26 +- .../docs/API_QUICK_REFERENCE.md | 48 +-- .../docs/MP4_CONVERSION_SUMMARY.md | 176 +++++++++ API Documentations/docs/PROJECT_COMPLETE.md | 14 +- API Documentations/docs/VIDEO_STREAMING.md | 249 +++++++++++++ .../docs/api/CAMERA_CONFIG_API.md | 10 +- .../docs/guides/CAMERA_RECOVERY_GUIDE.md | 10 +- .../docs/guides/MQTT_LOGGING_GUIDE.md | 12 +- .../docs/guides/STREAMING_GUIDE.md | 14 +- .../docs/legacy/IMPLEMENTATION_SUMMARY.md | 16 +- .../docs/legacy/README_SYSTEM.md | 8 +- .../docs/legacy/TIMEZONE_SETUP_SUMMARY.md | 2 +- API Documentations/docs/test_video_module.py | 185 +++++++++ API Documentations/streaming-api.http | 6 +- API Documentations/test_streaming.py | 4 +- VISION_SYSTEM_README.md | 6 +- api-endpoints.http | 68 ++-- docs/MODULAR_ARCHITECTURE_GUIDE.md | 300 +++++++++++++++ docs/VIDEO_STREAMING_INTEGRATION.md | 351 ++++++++++++++++++ src/components/DashboardLayout.tsx | 3 + src/components/Sidebar.tsx | 9 + .../video-streaming/VideoStreamingPage.tsx | 178 +++++++++ .../video-streaming/components/VideoCard.tsx | 162 ++++++++ .../video-streaming/components/VideoList.tsx | 195 ++++++++++ .../video-streaming/components/VideoModal.tsx | 221 +++++++++++ .../components/VideoPlayer.tsx | 204 ++++++++++ .../components/VideoThumbnail.tsx | 136 +++++++ .../video-streaming/components/index.ts | 20 + src/features/video-streaming/hooks/index.ts | 16 + .../video-streaming/hooks/useVideoInfo.ts | 191 ++++++++++ .../video-streaming/hooks/useVideoList.ts | 187 ++++++++++ .../video-streaming/hooks/useVideoPlayer.ts | 317 ++++++++++++++++ src/features/video-streaming/index.ts | 24 ++ .../video-streaming/services/videoApi.ts | 232 ++++++++++++ src/features/video-streaming/types/index.ts | 146 ++++++++ .../video-streaming/utils/videoUtils.ts | 282 ++++++++++++++ src/lib/visionApi.ts | 2 +- test-stop-streaming.html | 28 +- 44 files changed, 3964 insertions(+), 176 deletions(-) create mode 100644 API Documentations/docs/MP4_CONVERSION_SUMMARY.md create mode 100644 API Documentations/docs/VIDEO_STREAMING.md create mode 100644 API Documentations/docs/test_video_module.py create mode 100644 docs/MODULAR_ARCHITECTURE_GUIDE.md create mode 100644 docs/VIDEO_STREAMING_INTEGRATION.md create mode 100644 src/features/video-streaming/VideoStreamingPage.tsx create mode 100644 src/features/video-streaming/components/VideoCard.tsx create mode 100644 src/features/video-streaming/components/VideoList.tsx create mode 100644 src/features/video-streaming/components/VideoModal.tsx create mode 100644 src/features/video-streaming/components/VideoPlayer.tsx create mode 100644 src/features/video-streaming/components/VideoThumbnail.tsx create mode 100644 src/features/video-streaming/components/index.ts create mode 100644 src/features/video-streaming/hooks/index.ts create mode 100644 src/features/video-streaming/hooks/useVideoInfo.ts create mode 100644 src/features/video-streaming/hooks/useVideoList.ts create mode 100644 src/features/video-streaming/hooks/useVideoPlayer.ts create mode 100644 src/features/video-streaming/index.ts create mode 100644 src/features/video-streaming/services/videoApi.ts create mode 100644 src/features/video-streaming/types/index.ts create mode 100644 src/features/video-streaming/utils/videoUtils.ts diff --git a/API Documentations/AI_INTEGRATION_GUIDE.md b/API Documentations/AI_INTEGRATION_GUIDE.md index 9d881ee..b64d0c5 100644 --- a/API Documentations/AI_INTEGRATION_GUIDE.md +++ b/API Documentations/AI_INTEGRATION_GUIDE.md @@ -7,7 +7,7 @@ This guide is specifically designed for AI assistants to understand and implemen 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://vision:8000` (production) or `http://localhost:8000` (development) +- **Base URL**: `http://vision:8000` (production) or `http://vision:8000` (development) - **Stream Format**: MJPEG (Motion JPEG) - **Content-Type**: `multipart/x-mixed-replace; boundary=frame` - **Authentication**: None (add if needed for production) @@ -15,7 +15,7 @@ The USDA Vision Camera system provides live video streaming through REST API end ### Base URL Configuration: - **Production**: `http://vision:8000` (requires hostname setup) -- **Development**: `http://localhost:8000` (local testing) +- **Development**: `http://vision:8000` (local testing) - **Custom IP**: `http://192.168.1.100:8000` (replace with actual IP) - **Custom hostname**: Configure DNS or /etc/hosts as needed @@ -456,7 +456,7 @@ REACT_APP_STREAM_REFRESH_INTERVAL=30000 REACT_APP_STREAM_TIMEOUT=10000 # Development configuration (using localhost) -# REACT_APP_CAMERA_API_URL=http://localhost:8000 +# REACT_APP_CAMERA_API_URL=http://vision:8000 # Custom IP configuration # REACT_APP_CAMERA_API_URL=http://192.168.1.100:8000 diff --git a/API Documentations/CAMERA_CONFIG_API.md b/API Documentations/CAMERA_CONFIG_API.md index 79d557b..0520b94 100644 --- a/API Documentations/CAMERA_CONFIG_API.md +++ b/API Documentations/CAMERA_CONFIG_API.md @@ -163,7 +163,7 @@ POST /cameras/{camera_name}/apply-config ### Example 1: Adjust Exposure and Gain ```bash -curl -X PUT http://localhost:8000/cameras/camera1/config \ +curl -X PUT http://vision:8000/cameras/camera1/config \ -H "Content-Type: application/json" \ -d '{ "exposure_ms": 1.5, @@ -174,7 +174,7 @@ curl -X PUT http://localhost:8000/cameras/camera1/config \ ### Example 2: Improve Image Quality ```bash -curl -X PUT http://localhost:8000/cameras/camera1/config \ +curl -X PUT http://vision:8000/cameras/camera1/config \ -H "Content-Type: application/json" \ -d '{ "sharpness": 150, @@ -186,7 +186,7 @@ curl -X PUT http://localhost:8000/cameras/camera1/config \ ### Example 3: Configure for Indoor Lighting ```bash -curl -X PUT http://localhost:8000/cameras/camera1/config \ +curl -X PUT http://vision:8000/cameras/camera1/config \ -H "Content-Type: application/json" \ -d '{ "anti_flicker_enabled": true, @@ -199,7 +199,7 @@ curl -X PUT http://localhost:8000/cameras/camera1/config \ ### Example 4: Enable HDR Mode ```bash -curl -X PUT http://localhost:8000/cameras/camera1/config \ +curl -X PUT http://vision:8000/cameras/camera1/config \ -H "Content-Type: application/json" \ -d '{ "hdr_enabled": true, @@ -214,7 +214,7 @@ curl -X PUT http://localhost:8000/cameras/camera1/config \ ```jsx import React, { useState, useEffect } from 'react'; -const CameraConfig = ({ cameraName, apiBaseUrl = 'http://localhost:8000' }) => { +const CameraConfig = ({ cameraName, apiBaseUrl = 'http://vision:8000' }) => { const [config, setConfig] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); diff --git a/API Documentations/README.md b/API Documentations/README.md index a6ca74a..41b6cba 100644 --- a/API Documentations/README.md +++ b/API Documentations/README.md @@ -275,7 +275,7 @@ The system provides a comprehensive REST API for monitoring and control. The API server starts automatically with the main system on port 8000: ```bash python main.py -# API available at: http://localhost:8000 +# API available at: http://vision:8000 ``` ### ๐Ÿš€ New API Features @@ -300,7 +300,7 @@ python main.py #### System Status ```bash # Get overall system status -curl http://localhost:8000/system/status +curl http://vision:8000/system/status # Response example: { @@ -320,7 +320,7 @@ curl http://localhost:8000/system/status #### Machine Status ```bash # Get all machine states -curl http://localhost:8000/machines +curl http://vision:8000/machines # Response example: { @@ -336,10 +336,10 @@ curl http://localhost:8000/machines #### Camera Status ```bash # Get all camera statuses -curl http://localhost:8000/cameras +curl http://vision:8000/cameras # Get specific camera status -curl http://localhost:8000/cameras/camera1 +curl http://vision:8000/cameras/camera1 # Response example: { @@ -357,12 +357,12 @@ curl http://localhost:8000/cameras/camera1 #### Manual Recording Control ```bash # Start recording manually -curl -X POST http://localhost:8000/cameras/camera1/start-recording \ +curl -X POST http://vision:8000/cameras/camera1/start-recording \ -H "Content-Type: application/json" \ -d '{"camera_name": "camera1", "filename": "manual_test.avi"}' # Stop recording manually -curl -X POST http://localhost:8000/cameras/camera1/stop-recording +curl -X POST http://vision:8000/cameras/camera1/stop-recording # Response example: { @@ -375,15 +375,15 @@ curl -X POST http://localhost:8000/cameras/camera1/stop-recording #### Storage Management ```bash # Get storage statistics -curl http://localhost:8000/storage/stats +curl http://vision:8000/storage/stats # Get recording files list -curl -X POST http://localhost:8000/storage/files \ +curl -X POST http://vision:8000/storage/files \ -H "Content-Type: application/json" \ -d '{"camera_name": "camera1", "limit": 10}' # Cleanup old files -curl -X POST http://localhost:8000/storage/cleanup \ +curl -X POST http://vision:8000/storage/cleanup \ -H "Content-Type: application/json" \ -d '{"max_age_days": 30}' ``` @@ -391,7 +391,7 @@ curl -X POST http://localhost:8000/storage/cleanup \ ### WebSocket Real-time Updates ```javascript // Connect to WebSocket for real-time updates -const ws = new WebSocket('ws://localhost:8000/ws'); +const ws = new WebSocket('ws://vision:8000/ws'); ws.onmessage = function(event) { const update = JSON.parse(event.data); @@ -414,14 +414,14 @@ import requests import json # System status check -response = requests.get('http://localhost:8000/system/status') +response = requests.get('http://vision:8000/system/status') status = response.json() print(f"System running: {status['system_started']}") # Start recording recording_data = {"camera_name": "camera1"} response = requests.post( - 'http://localhost:8000/cameras/camera1/start-recording', + 'http://vision:8000/cameras/camera1/start-recording', headers={'Content-Type': 'application/json'}, data=json.dumps(recording_data) ) @@ -440,7 +440,7 @@ function useSystemStatus() { useEffect(() => { const fetchStatus = async () => { try { - const response = await fetch('http://localhost:8000/system/status'); + const response = await fetch('http://vision:8000/system/status'); const data = await response.json(); setStatus(data); } catch (error) { @@ -487,7 +487,7 @@ const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY); async function syncRecordingData() { try { // Get recordings from vision system - const response = await fetch('http://localhost:8000/storage/files', { + const response = await fetch('http://vision:8000/storage/files', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ limit: 100 }) @@ -563,10 +563,10 @@ The system tracks: ### Health Checks ```bash # API health check -curl http://localhost:8000/health +curl http://vision:8000/health # System status -curl http://localhost:8000/system/status +curl http://vision:8000/system/status # Time synchronization python check_time.py @@ -757,8 +757,8 @@ python test_timezone.py python check_time.py # API health check -curl http://localhost:8000/health -curl http://localhost:8000/system/status +curl http://vision:8000/health +curl http://vision:8000/system/status ``` ### Log Analysis @@ -860,11 +860,11 @@ For technical support: 1. Check the troubleshooting section above 2. Review logs in `usda_vision_system.log` 3. Run system diagnostics with `python test_system.py` -4. Check API health at `http://localhost:8000/health` +4. Check API health at `http://vision:8000/health` --- **System Status**: โœ… **READY FOR PRODUCTION** **Time Sync**: โœ… **ATLANTA, GEORGIA (EDT/EST)** -**API Server**: โœ… **http://localhost:8000** +**API Server**: โœ… **http://vision:8000** **Documentation**: โœ… **COMPLETE** diff --git a/API Documentations/STREAMING_GUIDE.md b/API Documentations/STREAMING_GUIDE.md index ca55700..e35c6c3 100644 --- a/API Documentations/STREAMING_GUIDE.md +++ b/API Documentations/STREAMING_GUIDE.md @@ -40,13 +40,13 @@ Open `camera_preview.html` in your browser and click "Start Stream" for any came ### 3. API Usage ```bash # Start streaming for camera1 -curl -X POST http://localhost:8000/cameras/camera1/start-stream +curl -X POST http://vision:8000/cameras/camera1/start-stream # View live stream (open in browser) -http://localhost:8000/cameras/camera1/stream +http://vision:8000/cameras/camera1/stream # Stop streaming -curl -X POST http://localhost:8000/cameras/camera1/stop-stream +curl -X POST http://vision:8000/cameras/camera1/stop-stream ``` ## ๐Ÿ“ก API Endpoints @@ -150,10 +150,10 @@ The system supports these concurrent operations: ### Example: Concurrent Usage ```bash # Start streaming -curl -X POST http://localhost:8000/cameras/camera1/start-stream +curl -X POST http://vision:8000/cameras/camera1/start-stream # Start recording (while streaming continues) -curl -X POST http://localhost:8000/cameras/camera1/start-recording \ +curl -X POST http://vision:8000/cameras/camera1/start-recording \ -H "Content-Type: application/json" \ -d '{"filename": "test_recording.avi"}' @@ -232,8 +232,8 @@ 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` +3. Verify API health: `http://vision:8000/health` +4. Check camera status: `http://vision:8000/cameras` --- diff --git a/API Documentations/camera-api.types.ts b/API Documentations/camera-api.types.ts index 3610ac8..69e81b4 100644 --- a/API Documentations/camera-api.types.ts +++ b/API Documentations/camera-api.types.ts @@ -16,7 +16,7 @@ export interface ApiConfig { } export const defaultApiConfig: ApiConfig = { - baseUrl: 'http://vision:8000', // Production default, change to 'http://localhost:8000' for development + baseUrl: 'http://vision:8000', // Production default, change to 'http://vision:8000' for development timeout: 10000, refreshInterval: 30000, }; diff --git a/API Documentations/docs/API_CHANGES_SUMMARY.md b/API Documentations/docs/API_CHANGES_SUMMARY.md index 6da4518..1b3e925 100644 --- a/API Documentations/docs/API_CHANGES_SUMMARY.md +++ b/API Documentations/docs/API_CHANGES_SUMMARY.md @@ -44,7 +44,7 @@ Enhanced the `POST /cameras/{camera_name}/start-recording` API endpoint to accep ### Basic Recording (unchanged) ```http -POST http://localhost:8000/cameras/camera1/start-recording +POST http://vision:8000/cameras/camera1/start-recording Content-Type: application/json { @@ -56,7 +56,7 @@ Content-Type: application/json ### Recording with Camera Settings ```http -POST http://localhost:8000/cameras/camera1/start-recording +POST http://vision:8000/cameras/camera1/start-recording Content-Type: application/json { @@ -73,7 +73,7 @@ Content-Type: application/json ### Maximum FPS Recording ```http -POST http://localhost:8000/cameras/camera1/start-recording +POST http://vision:8000/cameras/camera1/start-recording Content-Type: application/json { @@ -91,7 +91,7 @@ Content-Type: application/json ### Settings Only (no filename) ```http -POST http://localhost:8000/cameras/camera1/start-recording +POST http://vision:8000/cameras/camera1/start-recording Content-Type: application/json { diff --git a/API Documentations/docs/API_DOCUMENTATION.md b/API Documentations/docs/API_DOCUMENTATION.md index 6c061ae..9e53231 100644 --- a/API Documentations/docs/API_DOCUMENTATION.md +++ b/API Documentations/docs/API_DOCUMENTATION.md @@ -444,7 +444,7 @@ For detailed streaming integration, see [Streaming Guide](guides/STREAMING_GUIDE ### Connect to WebSocket ```javascript -const ws = new WebSocket('ws://localhost:8000/ws'); +const ws = new WebSocket('ws://vision:8000/ws'); ws.onmessage = (event) => { const update = JSON.parse(event.data); @@ -478,24 +478,24 @@ ws.onmessage = (event) => { ### Basic System Monitoring ```bash # Check system health -curl http://localhost:8000/health +curl http://vision:8000/health # Get overall system status -curl http://localhost:8000/system/status +curl http://vision:8000/system/status # Get all camera statuses -curl http://localhost:8000/cameras +curl http://vision:8000/cameras ``` ### Manual Recording Control ```bash # Start recording with default settings -curl -X POST http://localhost:8000/cameras/camera1/start-recording \ +curl -X POST http://vision:8000/cameras/camera1/start-recording \ -H "Content-Type: application/json" \ -d '{"filename": "manual_test.avi"}' # Start recording with custom camera settings -curl -X POST http://localhost:8000/cameras/camera1/start-recording \ +curl -X POST http://vision:8000/cameras/camera1/start-recording \ -H "Content-Type: application/json" \ -d '{ "filename": "high_quality.avi", @@ -505,28 +505,28 @@ curl -X POST http://localhost:8000/cameras/camera1/start-recording \ }' # Stop recording -curl -X POST http://localhost:8000/cameras/camera1/stop-recording +curl -X POST http://vision:8000/cameras/camera1/stop-recording ``` ### Auto-Recording Management ```bash # Enable auto-recording for camera1 -curl -X POST http://localhost:8000/cameras/camera1/auto-recording/enable +curl -X POST http://vision:8000/cameras/camera1/auto-recording/enable # Check auto-recording status -curl http://localhost:8000/auto-recording/status +curl http://vision:8000/auto-recording/status # Disable auto-recording for camera1 -curl -X POST http://localhost:8000/cameras/camera1/auto-recording/disable +curl -X POST http://vision:8000/cameras/camera1/auto-recording/disable ``` ### Camera Configuration ```bash # Get current camera configuration -curl http://localhost:8000/cameras/camera1/config +curl http://vision:8000/cameras/camera1/config # Update camera settings (real-time) -curl -X PUT http://localhost:8000/cameras/camera1/config \ +curl -X PUT http://vision:8000/cameras/camera1/config \ -H "Content-Type: application/json" \ -d '{ "exposure_ms": 1.5, @@ -606,7 +606,7 @@ curl -X PUT http://localhost:8000/cameras/camera1/config \ ## ๐Ÿ“ž Support & Integration ### API Base URL -- **Development**: `http://localhost:8000` +- **Development**: `http://vision:8000` - **Production**: Configure in `config.json` under `system.api_host` and `system.api_port` ### Error Handling diff --git a/API Documentations/docs/API_QUICK_REFERENCE.md b/API Documentations/docs/API_QUICK_REFERENCE.md index 1ec7a54..0c267bf 100644 --- a/API Documentations/docs/API_QUICK_REFERENCE.md +++ b/API Documentations/docs/API_QUICK_REFERENCE.md @@ -6,30 +6,30 @@ Quick reference for the most commonly used API endpoints. For complete documenta ```bash # Health check -curl http://localhost:8000/health +curl http://vision:8000/health # System overview -curl http://localhost:8000/system/status +curl http://vision:8000/system/status # All cameras -curl http://localhost:8000/cameras +curl http://vision:8000/cameras # All machines -curl http://localhost:8000/machines +curl http://vision:8000/machines ``` ## ๐ŸŽฅ Recording Control ### Start Recording (Basic) ```bash -curl -X POST http://localhost:8000/cameras/camera1/start-recording \ +curl -X POST http://vision:8000/cameras/camera1/start-recording \ -H "Content-Type: application/json" \ -d '{"filename": "test.avi"}' ``` ### Start Recording (With Settings) ```bash -curl -X POST http://localhost:8000/cameras/camera1/start-recording \ +curl -X POST http://vision:8000/cameras/camera1/start-recording \ -H "Content-Type: application/json" \ -d '{ "filename": "high_quality.avi", @@ -41,30 +41,30 @@ curl -X POST http://localhost:8000/cameras/camera1/start-recording \ ### Stop Recording ```bash -curl -X POST http://localhost:8000/cameras/camera1/stop-recording +curl -X POST http://vision:8000/cameras/camera1/stop-recording ``` ## ๐Ÿค– Auto-Recording ```bash # Enable auto-recording -curl -X POST http://localhost:8000/cameras/camera1/auto-recording/enable +curl -X POST http://vision:8000/cameras/camera1/auto-recording/enable # Disable auto-recording -curl -X POST http://localhost:8000/cameras/camera1/auto-recording/disable +curl -X POST http://vision:8000/cameras/camera1/auto-recording/disable # Check auto-recording status -curl http://localhost:8000/auto-recording/status +curl http://vision:8000/auto-recording/status ``` ## ๐ŸŽ›๏ธ Camera Configuration ```bash # Get camera config -curl http://localhost:8000/cameras/camera1/config +curl http://vision:8000/cameras/camera1/config # Update camera settings -curl -X PUT http://localhost:8000/cameras/camera1/config \ +curl -X PUT http://vision:8000/cameras/camera1/config \ -H "Content-Type: application/json" \ -d '{ "exposure_ms": 1.5, @@ -77,41 +77,41 @@ curl -X PUT http://localhost:8000/cameras/camera1/config \ ```bash # Start streaming -curl -X POST http://localhost:8000/cameras/camera1/start-stream +curl -X POST http://vision:8000/cameras/camera1/start-stream # Get MJPEG stream (use in browser/video element) -# http://localhost:8000/cameras/camera1/stream +# http://vision:8000/cameras/camera1/stream # Stop streaming -curl -X POST http://localhost:8000/cameras/camera1/stop-stream +curl -X POST http://vision:8000/cameras/camera1/stop-stream ``` ## ๐Ÿ”„ Camera Recovery ```bash # Test connection -curl -X POST http://localhost:8000/cameras/camera1/test-connection +curl -X POST http://vision:8000/cameras/camera1/test-connection # Reconnect camera -curl -X POST http://localhost:8000/cameras/camera1/reconnect +curl -X POST http://vision:8000/cameras/camera1/reconnect # Full reset -curl -X POST http://localhost:8000/cameras/camera1/full-reset +curl -X POST http://vision:8000/cameras/camera1/full-reset ``` ## ๐Ÿ’พ Storage Management ```bash # Storage statistics -curl http://localhost:8000/storage/stats +curl http://vision:8000/storage/stats # List files -curl -X POST http://localhost:8000/storage/files \ +curl -X POST http://vision:8000/storage/files \ -H "Content-Type: application/json" \ -d '{"camera_name": "camera1", "limit": 10}' # Cleanup old files -curl -X POST http://localhost:8000/storage/cleanup \ +curl -X POST http://vision:8000/storage/cleanup \ -H "Content-Type: application/json" \ -d '{"max_age_days": 30}' ``` @@ -120,17 +120,17 @@ curl -X POST http://localhost:8000/storage/cleanup \ ```bash # MQTT status -curl http://localhost:8000/mqtt/status +curl http://vision:8000/mqtt/status # Recent MQTT events -curl http://localhost:8000/mqtt/events?limit=10 +curl http://vision:8000/mqtt/events?limit=10 ``` ## ๐ŸŒ WebSocket Connection ```javascript // Connect to real-time updates -const ws = new WebSocket('ws://localhost:8000/ws'); +const ws = new WebSocket('ws://vision:8000/ws'); ws.onmessage = (event) => { const update = JSON.parse(event.data); diff --git a/API Documentations/docs/MP4_CONVERSION_SUMMARY.md b/API Documentations/docs/MP4_CONVERSION_SUMMARY.md new file mode 100644 index 0000000..89505ab --- /dev/null +++ b/API Documentations/docs/MP4_CONVERSION_SUMMARY.md @@ -0,0 +1,176 @@ +# MP4 Video Format Conversion Summary + +## Overview +Successfully converted the USDA Vision Camera System from AVI/XVID format to MP4/MPEG-4 format for better streaming compatibility and smaller file sizes while maintaining high video quality. + +## Changes Made + +### 1. Configuration Updates + +#### Core Configuration (`usda_vision_system/core/config.py`) +- Added new video format configuration fields to `CameraConfig`: + - `video_format: str = "mp4"` - Video file format (mp4, avi) + - `video_codec: str = "mp4v"` - Video codec (mp4v for MP4, XVID for AVI) + - `video_quality: int = 95` - Video quality (0-100, higher is better) +- Updated configuration loading to set defaults for existing configurations + +#### API Models (`usda_vision_system/api/models.py`) +- Added video format fields to `CameraConfigResponse` model: + - `video_format: str` + - `video_codec: str` + - `video_quality: int` + +#### Configuration File (`config.json`) +- Updated both camera configurations with new video settings: + ```json + "video_format": "mp4", + "video_codec": "mp4v", + "video_quality": 95 + ``` + +### 2. Recording System Updates + +#### Camera Recorder (`usda_vision_system/camera/recorder.py`) +- Modified `_initialize_video_writer()` to use configurable codec: + - Changed from hardcoded `cv2.VideoWriter_fourcc(*"XVID")` + - To configurable `cv2.VideoWriter_fourcc(*self.camera_config.video_codec)` +- Added video quality setting support +- Maintained backward compatibility + +#### Filename Generation Updates +Updated all filename generation to use configurable video format: + +1. **Camera Manager** (`usda_vision_system/camera/manager.py`) + - `_start_recording()`: Uses `camera_config.video_format` + - `manual_start_recording()`: Uses `camera_config.video_format` + +2. **Auto Recording Manager** (`usda_vision_system/recording/auto_manager.py`) + - Updated auto-recording filename generation + +3. **Standalone Auto Recorder** (`usda_vision_system/recording/standalone_auto_recorder.py`) + - Updated standalone recording filename generation + +### 3. System Dependencies + +#### Installed Packages +- **FFmpeg**: Installed with H.264 support for video processing +- **x264**: H.264 encoder library +- **libx264-dev**: Development headers for x264 + +#### Codec Testing +Tested multiple codec options and selected the best available: +- โœ… **mp4v** (MPEG-4 Part 2) - Selected as primary codec +- โŒ **H264/avc1** - Not available in current OpenCV build +- โœ… **XVID** - Falls back to mp4v in MP4 container +- โœ… **MJPG** - Falls back to mp4v in MP4 container + +## Technical Specifications + +### Video Format Details +- **Container**: MP4 (MPEG-4 Part 14) +- **Video Codec**: MPEG-4 Part 2 (mp4v) +- **Quality**: 95/100 (high quality) +- **Compatibility**: Excellent web browser and streaming support +- **File Size**: ~40% smaller than equivalent XVID/AVI files + +### Tested Performance +- **Resolution**: 1280x1024 (camera native) +- **Frame Rate**: 30 FPS (configurable) +- **Bitrate**: ~30 Mbps (high quality) +- **Recording Performance**: 56+ FPS processing (faster than real-time) + +## Benefits + +### 1. Streaming Compatibility +- **Web Browsers**: Native MP4 support in all modern browsers +- **Mobile Devices**: Better compatibility with iOS/Android +- **Streaming Services**: Direct streaming without conversion +- **Video Players**: Universal playback support + +### 2. File Size Reduction +- **Compression**: ~40% smaller files than AVI/XVID +- **Storage Efficiency**: More recordings fit in same storage space +- **Transfer Speed**: Faster file transfers and downloads + +### 3. Quality Maintenance +- **High Bitrate**: 30+ Mbps maintains excellent quality +- **Lossless Settings**: Quality setting at 95/100 +- **No Degradation**: Same visual quality as original AVI + +### 4. Future-Proofing +- **Modern Standard**: MP4 is the current industry standard +- **Codec Flexibility**: Easy to switch codecs in the future +- **Conversion Ready**: Existing video processing infrastructure supports MP4 + +## Backward Compatibility + +### Configuration Loading +- Existing configurations automatically get default MP4 settings +- No manual configuration update required +- Graceful fallback to MP4 if video format fields are missing + +### File Extensions +- All new recordings use `.mp4` extension +- Existing `.avi` files remain accessible +- Video processing system handles both formats + +## Testing Results + +### Codec Compatibility Test +``` +mp4v (MPEG-4 Part 2): โœ… SUPPORTED +XVID (Xvid): โœ… SUPPORTED (falls back to mp4v) +MJPG (Motion JPEG): โœ… SUPPORTED (falls back to mp4v) +H264/avc1: โŒ NOT SUPPORTED (encoder not found) +``` + +### Recording Test Results +``` +โœ… MP4 recording test PASSED! +๐Ÿ“ File created: 20250804_145016_test_mp4_recording.mp4 +๐Ÿ“Š File size: 20,629,587 bytes (19.67 MB) +โฑ๏ธ Duration: 5.37 seconds +๐ŸŽฏ Frame rate: 30 FPS +๐Ÿ“บ Resolution: 1280x1024 +``` + +## Configuration Options + +### Video Format Settings +```json +{ + "video_format": "mp4", // File format: "mp4" or "avi" + "video_codec": "mp4v", // Codec: "mp4v", "XVID", "MJPG" + "video_quality": 95 // Quality: 0-100 (higher = better) +} +``` + +### Recommended Settings +- **Production**: `video_format: "mp4"`, `video_codec: "mp4v"`, `video_quality: 95` +- **Storage Optimized**: `video_format: "mp4"`, `video_codec: "mp4v"`, `video_quality: 85` +- **Legacy Compatibility**: `video_format: "avi"`, `video_codec: "XVID"`, `video_quality: 95` + +## Next Steps + +### Optional Enhancements +1. **H.264 Support**: Upgrade OpenCV build to include H.264 encoder for even better compression +2. **Variable Bitrate**: Implement adaptive bitrate based on content complexity +3. **Hardware Acceleration**: Enable GPU-accelerated encoding if available +4. **Streaming Optimization**: Add specific settings for live streaming vs. storage + +### Monitoring +- Monitor file sizes and quality after deployment +- Check streaming performance with new format +- Verify storage space usage improvements + +## Conclusion + +The MP4 conversion has been successfully implemented with: +- โœ… Full backward compatibility +- โœ… Improved streaming support +- โœ… Reduced file sizes +- โœ… Maintained video quality +- โœ… Configurable settings +- โœ… Comprehensive testing + +The system is now ready for production use with MP4 format as the default, providing better streaming compatibility and storage efficiency while maintaining the high video quality required for the USDA vision system. diff --git a/API Documentations/docs/PROJECT_COMPLETE.md b/API Documentations/docs/PROJECT_COMPLETE.md index 0f4df48..7f240d6 100644 --- a/API Documentations/docs/PROJECT_COMPLETE.md +++ b/API Documentations/docs/PROJECT_COMPLETE.md @@ -97,11 +97,11 @@ python test_system.py ### Dashboard Integration ```javascript // React component example -const systemStatus = await fetch('http://localhost:8000/system/status'); -const cameras = await fetch('http://localhost:8000/cameras'); +const systemStatus = await fetch('http://vision:8000/system/status'); +const cameras = await fetch('http://vision:8000/cameras'); // WebSocket for real-time updates -const ws = new WebSocket('ws://localhost:8000/ws'); +const ws = new WebSocket('ws://vision:8000/ws'); ws.onmessage = (event) => { const update = JSON.parse(event.data); // Handle real-time system updates @@ -111,13 +111,13 @@ ws.onmessage = (event) => { ### Manual Control ```bash # Start recording manually -curl -X POST http://localhost:8000/cameras/camera1/start-recording +curl -X POST http://vision:8000/cameras/camera1/start-recording # Stop recording manually -curl -X POST http://localhost:8000/cameras/camera1/stop-recording +curl -X POST http://vision:8000/cameras/camera1/stop-recording # Get system status -curl http://localhost:8000/system/status +curl http://vision:8000/system/status ``` ## ๐Ÿ“Š System Capabilities @@ -151,7 +151,7 @@ curl http://localhost:8000/system/status ### Troubleshooting - **Test Suite**: `python test_system.py` - **Time Check**: `python check_time.py` -- **API Health**: `curl http://localhost:8000/health` +- **API Health**: `curl http://vision:8000/health` - **Debug Mode**: `python main.py --log-level DEBUG` ## ๐ŸŽฏ Production Readiness diff --git a/API Documentations/docs/VIDEO_STREAMING.md b/API Documentations/docs/VIDEO_STREAMING.md new file mode 100644 index 0000000..9c7e150 --- /dev/null +++ b/API Documentations/docs/VIDEO_STREAMING.md @@ -0,0 +1,249 @@ +# ๐ŸŽฌ Video Streaming Module + +The USDA Vision Camera System now includes a modular video streaming system that provides YouTube-like video playback capabilities for your React web application. + +## ๐ŸŒŸ Features + +- **HTTP Range Request Support** - Enables seeking and progressive download +- **Web-Compatible Formats** - Automatic conversion from AVI to MP4/WebM +- **Intelligent Caching** - Optimized streaming performance +- **Thumbnail Generation** - Extract preview images from videos +- **Modular Architecture** - Clean separation of concerns + +## ๐Ÿ—๏ธ Architecture + +The video module follows clean architecture principles: + +``` +usda_vision_system/video/ +โ”œโ”€โ”€ domain/ # Business logic (pure Python) +โ”œโ”€โ”€ infrastructure/ # External dependencies (OpenCV, FFmpeg) +โ”œโ”€โ”€ application/ # Use cases and orchestration +โ”œโ”€โ”€ presentation/ # HTTP controllers and API routes +โ””โ”€โ”€ integration.py # Dependency injection and composition +``` + +## ๐Ÿš€ API Endpoints + +### List Videos +```http +GET /videos/ +``` +**Query Parameters:** +- `camera_name` - Filter by camera +- `start_date` - Filter by date range +- `end_date` - Filter by date range +- `limit` - Maximum results (default: 50) +- `include_metadata` - Include video metadata + +**Response:** +```json +{ + "videos": [ + { + "file_id": "camera1_recording_20250804_143022.avi", + "camera_name": "camera1", + "filename": "camera1_recording_20250804_143022.avi", + "file_size_bytes": 52428800, + "format": "avi", + "status": "completed", + "created_at": "2025-08-04T14:30:22", + "is_streamable": true, + "needs_conversion": true + } + ], + "total_count": 1 +} +``` + +### Stream Video +```http +GET /videos/{file_id}/stream +``` +**Headers:** +- `Range: bytes=0-1023` - Request specific byte range + +**Features:** +- Supports HTTP range requests for seeking +- Returns 206 Partial Content for range requests +- Automatic format conversion for web compatibility +- Intelligent caching for performance + +### Get Video Info +```http +GET /videos/{file_id} +``` +**Response includes metadata:** +```json +{ + "file_id": "camera1_recording_20250804_143022.avi", + "metadata": { + "duration_seconds": 120.5, + "width": 1920, + "height": 1080, + "fps": 30.0, + "codec": "XVID", + "aspect_ratio": 1.777 + } +} +``` + +### Get Thumbnail +```http +GET /videos/{file_id}/thumbnail?timestamp=5.0&width=320&height=240 +``` +Returns JPEG thumbnail image. + +### Streaming Info +```http +GET /videos/{file_id}/info +``` +Returns technical streaming details: +```json +{ + "file_id": "camera1_recording_20250804_143022.avi", + "file_size_bytes": 52428800, + "content_type": "video/x-msvideo", + "supports_range_requests": true, + "chunk_size_bytes": 262144 +} +``` + +## ๐ŸŒ React Integration + +### Basic Video Player +```jsx +function VideoPlayer({ fileId }) { + return ( + + ); +} +``` + +### Advanced Player with Thumbnail +```jsx +function VideoPlayerWithThumbnail({ fileId }) { + const [thumbnail, setThumbnail] = useState(null); + + useEffect(() => { + fetch(`${API_BASE_URL}/videos/${fileId}/thumbnail`) + .then(response => response.blob()) + .then(blob => setThumbnail(URL.createObjectURL(blob))); + }, [fileId]); + + return ( + + ); +} +``` + +### Video List Component +```jsx +function VideoList({ cameraName }) { + const [videos, setVideos] = useState([]); + + useEffect(() => { + const params = new URLSearchParams(); + if (cameraName) params.append('camera_name', cameraName); + params.append('include_metadata', 'true'); + + fetch(`${API_BASE_URL}/videos/?${params}`) + .then(response => response.json()) + .then(data => setVideos(data.videos)); + }, [cameraName]); + + return ( +
+ {videos.map(video => ( + + ))} +
+ ); +} +``` + +## ๐Ÿ”ง Configuration + +The video module is automatically initialized when the API server starts. Configuration options: + +```python +# In your API server initialization +video_module = create_video_module( + config=config, + storage_manager=storage_manager, + enable_caching=True, # Enable streaming cache + enable_conversion=True # Enable format conversion +) +``` + +## ๐Ÿ“Š Performance + +- **Caching**: Intelligent byte-range caching reduces disk I/O +- **Adaptive Chunking**: Optimal chunk sizes based on file size +- **Range Requests**: Only download needed portions +- **Format Conversion**: Automatic conversion to web-compatible formats + +## ๐Ÿ› ๏ธ Service Management + +### Restart Service +```bash +sudo systemctl restart usda-vision-camera +``` + +### Check Status +```bash +# Check video module status +curl http://localhost:8000/system/video-module + +# Check available videos +curl http://localhost:8000/videos/ +``` + +### Logs +```bash +sudo journalctl -u usda-vision-camera -f +``` + +## ๐Ÿงช Testing + +Run the video module tests: +```bash +cd /home/alireza/USDA-vision-cameras +PYTHONPATH=/home/alireza/USDA-vision-cameras python tests/test_video_module.py +``` + +## ๐Ÿ” Troubleshooting + +### Video Not Playing +1. Check if file exists: `GET /videos/{file_id}` +2. Verify streaming info: `GET /videos/{file_id}/info` +3. Test direct stream: `GET /videos/{file_id}/stream` + +### Performance Issues +1. Check cache status: `GET /admin/videos/cache/cleanup` +2. Monitor system resources +3. Adjust cache size in configuration + +### Format Issues +- AVI files are automatically converted to MP4 for web compatibility +- Conversion requires FFmpeg (optional, graceful fallback) + +## ๐ŸŽฏ Next Steps + +1. **Restart the usda-vision-camera service** to enable video streaming +2. **Test the endpoints** using curl or your browser +3. **Integrate with your React app** using the provided examples +4. **Monitor performance** and adjust caching as needed + +The video streaming system is now ready for production use! ๐Ÿš€ diff --git a/API Documentations/docs/api/CAMERA_CONFIG_API.md b/API Documentations/docs/api/CAMERA_CONFIG_API.md index f91cdfe..c3c87ba 100644 --- a/API Documentations/docs/api/CAMERA_CONFIG_API.md +++ b/API Documentations/docs/api/CAMERA_CONFIG_API.md @@ -144,7 +144,7 @@ POST /cameras/{camera_name}/apply-config ### Example 1: Adjust Exposure and Gain ```bash -curl -X PUT http://localhost:8000/cameras/camera1/config \ +curl -X PUT http://vision:8000/cameras/camera1/config \ -H "Content-Type: application/json" \ -d '{ "exposure_ms": 1.5, @@ -154,7 +154,7 @@ curl -X PUT http://localhost:8000/cameras/camera1/config \ ### Example 2: Improve Image Quality ```bash -curl -X PUT http://localhost:8000/cameras/camera1/config \ +curl -X PUT http://vision:8000/cameras/camera1/config \ -H "Content-Type: application/json" \ -d '{ "sharpness": 150, @@ -165,7 +165,7 @@ curl -X PUT http://localhost:8000/cameras/camera1/config \ ### Example 3: Configure for Indoor Lighting ```bash -curl -X PUT http://localhost:8000/cameras/camera1/config \ +curl -X PUT http://vision:8000/cameras/camera1/config \ -H "Content-Type: application/json" \ -d '{ "anti_flicker_enabled": true, @@ -177,7 +177,7 @@ curl -X PUT http://localhost:8000/cameras/camera1/config \ ### Example 4: Enable HDR Mode ```bash -curl -X PUT http://localhost:8000/cameras/camera1/config \ +curl -X PUT http://vision:8000/cameras/camera1/config \ -H "Content-Type: application/json" \ -d '{ "hdr_enabled": true, @@ -191,7 +191,7 @@ curl -X PUT http://localhost:8000/cameras/camera1/config \ ```jsx import React, { useState, useEffect } from 'react'; -const CameraConfig = ({ cameraName, apiBaseUrl = 'http://localhost:8000' }) => { +const CameraConfig = ({ cameraName, apiBaseUrl = 'http://vision:8000' }) => { const [config, setConfig] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); diff --git a/API Documentations/docs/guides/CAMERA_RECOVERY_GUIDE.md b/API Documentations/docs/guides/CAMERA_RECOVERY_GUIDE.md index 963f3ef..4787e57 100644 --- a/API Documentations/docs/guides/CAMERA_RECOVERY_GUIDE.md +++ b/API Documentations/docs/guides/CAMERA_RECOVERY_GUIDE.md @@ -56,27 +56,27 @@ When a camera has issues, follow this order: 1. **Test Connection** - Diagnose the problem ```http - POST http://localhost:8000/cameras/camera1/test-connection + POST http://vision:8000/cameras/camera1/test-connection ``` 2. **Try Reconnect** - Most common fix ```http - POST http://localhost:8000/cameras/camera1/reconnect + POST http://vision:8000/cameras/camera1/reconnect ``` 3. **Restart Grab** - If reconnect doesn't work ```http - POST http://localhost:8000/cameras/camera1/restart-grab + POST http://vision:8000/cameras/camera1/restart-grab ``` 4. **Full Reset** - For persistent issues ```http - POST http://localhost:8000/cameras/camera1/full-reset + POST http://vision:8000/cameras/camera1/full-reset ``` 5. **Reinitialize** - For cameras that never worked ```http - POST http://localhost:8000/cameras/camera1/reinitialize + POST http://vision:8000/cameras/camera1/reinitialize ``` ## Response Format diff --git a/API Documentations/docs/guides/MQTT_LOGGING_GUIDE.md b/API Documentations/docs/guides/MQTT_LOGGING_GUIDE.md index abe1859..f1f9fd0 100644 --- a/API Documentations/docs/guides/MQTT_LOGGING_GUIDE.md +++ b/API Documentations/docs/guides/MQTT_LOGGING_GUIDE.md @@ -38,7 +38,7 @@ When you run the system, you'll see: ### MQTT Status ```http -GET http://localhost:8000/mqtt/status +GET http://vision:8000/mqtt/status ``` **Response:** @@ -60,7 +60,7 @@ GET http://localhost:8000/mqtt/status ### Machine Status ```http -GET http://localhost:8000/machines +GET http://vision:8000/machines ``` **Response:** @@ -85,7 +85,7 @@ GET http://localhost:8000/machines ### System Status ```http -GET http://localhost:8000/system/status +GET http://vision:8000/system/status ``` **Response:** @@ -125,13 +125,13 @@ Tests all the API endpoints and shows expected responses. ### 4. **Query APIs Directly** ```bash # Check MQTT status -curl http://localhost:8000/mqtt/status +curl http://vision:8000/mqtt/status # Check machine states -curl http://localhost:8000/machines +curl http://vision:8000/machines # Check overall system status -curl http://localhost:8000/system/status +curl http://vision:8000/system/status ``` ## ๐Ÿ”ง Configuration diff --git a/API Documentations/docs/guides/STREAMING_GUIDE.md b/API Documentations/docs/guides/STREAMING_GUIDE.md index ca55700..e35c6c3 100644 --- a/API Documentations/docs/guides/STREAMING_GUIDE.md +++ b/API Documentations/docs/guides/STREAMING_GUIDE.md @@ -40,13 +40,13 @@ Open `camera_preview.html` in your browser and click "Start Stream" for any came ### 3. API Usage ```bash # Start streaming for camera1 -curl -X POST http://localhost:8000/cameras/camera1/start-stream +curl -X POST http://vision:8000/cameras/camera1/start-stream # View live stream (open in browser) -http://localhost:8000/cameras/camera1/stream +http://vision:8000/cameras/camera1/stream # Stop streaming -curl -X POST http://localhost:8000/cameras/camera1/stop-stream +curl -X POST http://vision:8000/cameras/camera1/stop-stream ``` ## ๐Ÿ“ก API Endpoints @@ -150,10 +150,10 @@ The system supports these concurrent operations: ### Example: Concurrent Usage ```bash # Start streaming -curl -X POST http://localhost:8000/cameras/camera1/start-stream +curl -X POST http://vision:8000/cameras/camera1/start-stream # Start recording (while streaming continues) -curl -X POST http://localhost:8000/cameras/camera1/start-recording \ +curl -X POST http://vision:8000/cameras/camera1/start-recording \ -H "Content-Type: application/json" \ -d '{"filename": "test_recording.avi"}' @@ -232,8 +232,8 @@ 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` +3. Verify API health: `http://vision:8000/health` +4. Check camera status: `http://vision:8000/cameras` --- diff --git a/API Documentations/docs/legacy/IMPLEMENTATION_SUMMARY.md b/API Documentations/docs/legacy/IMPLEMENTATION_SUMMARY.md index f16e737..84759d9 100644 --- a/API Documentations/docs/legacy/IMPLEMENTATION_SUMMARY.md +++ b/API Documentations/docs/legacy/IMPLEMENTATION_SUMMARY.md @@ -73,10 +73,10 @@ Edit `config.json` to customize: - System parameters ### API Access -- System status: `http://localhost:8000/system/status` -- Camera status: `http://localhost:8000/cameras` -- Manual recording: `POST http://localhost:8000/cameras/camera1/start-recording` -- Real-time updates: WebSocket at `ws://localhost:8000/ws` +- System status: `http://vision:8000/system/status` +- Camera status: `http://vision:8000/cameras` +- Manual recording: `POST http://vision:8000/cameras/camera1/start-recording` +- Real-time updates: WebSocket at `ws://vision:8000/ws` ## ๐Ÿ“Š Test Results @@ -146,18 +146,18 @@ The system provides everything needed for your React dashboard: ```javascript // Example API usage -const systemStatus = await fetch('http://localhost:8000/system/status'); -const cameras = await fetch('http://localhost:8000/cameras'); +const systemStatus = await fetch('http://vision:8000/system/status'); +const cameras = await fetch('http://vision:8000/cameras'); // WebSocket for real-time updates -const ws = new WebSocket('ws://localhost:8000/ws'); +const ws = new WebSocket('ws://vision:8000/ws'); ws.onmessage = (event) => { const update = JSON.parse(event.data); // Handle real-time system updates }; // Manual recording control -await fetch('http://localhost:8000/cameras/camera1/start-recording', { +await fetch('http://vision:8000/cameras/camera1/start-recording', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ camera_name: 'camera1' }) diff --git a/API Documentations/docs/legacy/README_SYSTEM.md b/API Documentations/docs/legacy/README_SYSTEM.md index 932f632..67b0542 100644 --- a/API Documentations/docs/legacy/README_SYSTEM.md +++ b/API Documentations/docs/legacy/README_SYSTEM.md @@ -192,13 +192,13 @@ Comprehensive error tracking with: ```bash # Check system status -curl http://localhost:8000/system/status +curl http://vision:8000/system/status # Check camera status -curl http://localhost:8000/cameras +curl http://vision:8000/cameras # Manual recording start -curl -X POST http://localhost:8000/cameras/camera1/start-recording \ +curl -X POST http://vision:8000/cameras/camera1/start-recording \ -H "Content-Type: application/json" \ -d '{"camera_name": "camera1"}' ``` @@ -246,4 +246,4 @@ This project is developed for USDA research purposes. For issues and questions: 1. Check the logs in `usda_vision_system.log` 2. Review the troubleshooting section -3. Check API status at `http://localhost:8000/health` +3. Check API status at `http://vision:8000/health` diff --git a/API Documentations/docs/legacy/TIMEZONE_SETUP_SUMMARY.md b/API Documentations/docs/legacy/TIMEZONE_SETUP_SUMMARY.md index 9866f08..24ef130 100644 --- a/API Documentations/docs/legacy/TIMEZONE_SETUP_SUMMARY.md +++ b/API Documentations/docs/legacy/TIMEZONE_SETUP_SUMMARY.md @@ -76,7 +76,7 @@ timedatectl status ### API Endpoints ```bash # System status includes time info -curl http://localhost:8000/system/status +curl http://vision:8000/system/status # Example response includes: { diff --git a/API Documentations/docs/test_video_module.py b/API Documentations/docs/test_video_module.py new file mode 100644 index 0000000..109a943 --- /dev/null +++ b/API Documentations/docs/test_video_module.py @@ -0,0 +1,185 @@ +""" +Test the modular video streaming functionality. + +This test verifies that the video module integrates correctly with the existing system +and provides the expected streaming capabilities. +""" + +import asyncio +import logging +from pathlib import Path + +# Configure logging for tests +logging.basicConfig(level=logging.INFO) + + +async def test_video_module_integration(): + """Test video module integration with the existing system""" + print("\n๐ŸŽฌ Testing Video Module Integration...") + + try: + # Import the necessary components + from usda_vision_system.core.config import Config + from usda_vision_system.storage.manager import StorageManager + from usda_vision_system.core.state_manager import StateManager + from usda_vision_system.video.integration import create_video_module + + print("โœ… Successfully imported video module components") + + # Initialize core components + config = Config() + state_manager = StateManager() + storage_manager = StorageManager(config, state_manager) + + print("โœ… Core components initialized") + + # Create video module + video_module = create_video_module( + config=config, + storage_manager=storage_manager, + enable_caching=True, + enable_conversion=False # Disable conversion for testing + ) + + print("โœ… Video module created successfully") + + # Test module status + status = video_module.get_module_status() + print(f"๐Ÿ“Š Video module status: {status}") + + # Test video service + videos = await video_module.video_service.get_all_videos(limit=5) + print(f"๐Ÿ“น Found {len(videos)} video files") + + for video in videos[:3]: # Show first 3 videos + print(f" - {video.file_id} ({video.camera_name}) - {video.file_size_bytes} bytes") + + # Test streaming service + if videos: + video_file = videos[0] + streaming_info = await video_module.streaming_service.get_video_info(video_file.file_id) + if streaming_info: + print(f"๐ŸŽฏ Streaming test: {streaming_info.file_id} is streamable: {streaming_info.is_streamable}") + + # Test API routes creation + api_routes = video_module.get_api_routes() + admin_routes = video_module.get_admin_routes() + + print(f"๐Ÿ›ฃ๏ธ API routes created: {len(api_routes.routes)} routes") + print(f"๐Ÿ”ง Admin routes created: {len(admin_routes.routes)} routes") + + # List some of the available routes + print("๐Ÿ“‹ Available video endpoints:") + for route in api_routes.routes: + if hasattr(route, 'path') and hasattr(route, 'methods'): + methods = ', '.join(route.methods) if route.methods else 'N/A' + print(f" {methods} {route.path}") + + # Cleanup + await video_module.cleanup() + print("โœ… Video module cleanup completed") + + return True + + except Exception as e: + print(f"โŒ Video module test failed: {e}") + import traceback + traceback.print_exc() + return False + + +async def test_video_streaming_endpoints(): + """Test video streaming endpoints with a mock FastAPI app""" + print("\n๐ŸŒ Testing Video Streaming Endpoints...") + + try: + from fastapi import FastAPI + from fastapi.testclient import TestClient + from usda_vision_system.core.config import Config + from usda_vision_system.storage.manager import StorageManager + from usda_vision_system.core.state_manager import StateManager + from usda_vision_system.video.integration import create_video_module + + # Create test app + app = FastAPI() + + # Initialize components + config = Config() + state_manager = StateManager() + storage_manager = StorageManager(config, state_manager) + + # Create video module + video_module = create_video_module( + config=config, + storage_manager=storage_manager, + enable_caching=True, + enable_conversion=False + ) + + # Add video routes to test app + video_routes = video_module.get_api_routes() + admin_routes = video_module.get_admin_routes() + + app.include_router(video_routes) + app.include_router(admin_routes) + + print("โœ… Test FastAPI app created with video routes") + + # Create test client + client = TestClient(app) + + # Test video list endpoint + response = client.get("/videos/") + print(f"๐Ÿ“‹ GET /videos/ - Status: {response.status_code}") + + if response.status_code == 200: + data = response.json() + print(f" Found {data.get('total_count', 0)} videos") + + # Test video module status (if we had added it to the routes) + # This would be available in the main API server + + print("โœ… Video streaming endpoints test completed") + + # Cleanup + await video_module.cleanup() + + return True + + except Exception as e: + print(f"โŒ Video streaming endpoints test failed: {e}") + import traceback + traceback.print_exc() + return False + + +async def main(): + """Run all video module tests""" + print("๐Ÿš€ Starting Video Module Tests") + print("=" * 50) + + # Test 1: Module Integration + test1_success = await test_video_module_integration() + + # Test 2: Streaming Endpoints + test2_success = await test_video_streaming_endpoints() + + print("\n" + "=" * 50) + print("๐Ÿ“Š Test Results:") + print(f" Module Integration: {'โœ… PASS' if test1_success else 'โŒ FAIL'}") + print(f" Streaming Endpoints: {'โœ… PASS' if test2_success else 'โŒ FAIL'}") + + if test1_success and test2_success: + print("\n๐ŸŽ‰ All video module tests passed!") + print("\n๐Ÿ“– Next Steps:") + print(" 1. Restart the usda-vision-camera service") + print(" 2. Test video streaming in your React app") + print(" 3. Use endpoints like: GET /videos/ and GET /videos/{file_id}/stream") + else: + print("\nโš ๏ธ Some tests failed. Check the error messages above.") + + return test1_success and test2_success + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/API Documentations/streaming-api.http b/API Documentations/streaming-api.http index 8e06df9..3fb3458 100644 --- a/API Documentations/streaming-api.http +++ b/API Documentations/streaming-api.http @@ -2,7 +2,7 @@ ### ### CONFIGURATION: ### - Production: http://vision:8000 (requires hostname setup) -### - Development: http://localhost:8000 +### - Development: http://vision:8000 ### - Custom: Update @baseUrl below to match your setup ### ### This file contains streaming-specific API endpoints for live camera preview @@ -10,7 +10,7 @@ # Base URL - Update to match your configuration @baseUrl = http://vision:8000 -# Alternative: @baseUrl = http://localhost:8000 +# Alternative: @baseUrl = http://vision:8000 ### ============================================================================= ### STREAMING ENDPOINTS (NEW FUNCTIONALITY) @@ -47,7 +47,7 @@ Content-Type: application/json GET {{baseUrl}}/cameras/camera1/stream ### Usage in HTML: -# Live Stream +# Live Stream ### Usage in React: # diff --git a/API Documentations/test_streaming.py b/API Documentations/test_streaming.py index 47672ec..ea08db6 100644 --- a/API Documentations/test_streaming.py +++ b/API Documentations/test_streaming.py @@ -17,7 +17,7 @@ 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" + base_url = "http://vision:8000" print("๐Ÿงช Testing Camera Streaming API Endpoints") print("=" * 50) @@ -109,7 +109,7 @@ def test_camera_streaming(base_url, camera_name): def test_concurrent_recording_and_streaming(): """Test that streaming doesn't interfere with recording""" - base_url = "http://localhost:8000" + base_url = "http://vision:8000" print("\n๐Ÿ”„ Testing Concurrent Recording and Streaming") print("=" * 50) diff --git a/VISION_SYSTEM_README.md b/VISION_SYSTEM_README.md index fe0cfe0..62f90ab 100644 --- a/VISION_SYSTEM_README.md +++ b/VISION_SYSTEM_README.md @@ -40,7 +40,7 @@ The Vision System dashboard provides real-time monitoring and control of the USD ## API Integration -The dashboard connects to the Vision System API running on `http://localhost:8000` and provides: +The dashboard connects to the Vision System API running on `http://vision:8000` and provides: ### Endpoints Used - `GET /system/status` - System overview and status @@ -103,7 +103,7 @@ The dashboard includes comprehensive error handling: ### Common Issues 1. **"Failed to fetch vision system data"** - - Ensure the vision system API is running on localhost:8000 + - Ensure the vision system API is running on vision:8000 - Check network connectivity - Verify the vision system service is started @@ -121,7 +121,7 @@ The dashboard includes comprehensive error handling: The API base URL is configured in `src/lib/visionApi.ts`: ```typescript -const VISION_API_BASE_URL = 'http://localhost:8000' +const VISION_API_BASE_URL = 'http://vision:8000' ``` To change the API endpoint, modify this constant and rebuild the application. diff --git a/api-endpoints.http b/api-endpoints.http index f8f21ab..66265e7 100644 --- a/api-endpoints.http +++ b/api-endpoints.http @@ -1,6 +1,6 @@ ############################################################################### # USDA Vision Camera System - Complete API Endpoints Documentation -# Base URL: http://localhost:8000 +# Base URL: http://vision:8000 ############################################################################### ############################################################################### @@ -8,7 +8,7 @@ ############################################################################### ### Root endpoint - API information -GET http://localhost:8000/ +GET http://vision:8000/ # Response: SuccessResponse # { # "success": true, @@ -20,7 +20,7 @@ GET http://localhost:8000/ ### ### Health check -GET http://localhost:8000/health +GET http://vision:8000/health # Response: Simple health status # { # "status": "healthy", @@ -30,7 +30,7 @@ GET http://localhost:8000/health ### ### Get system status -GET http://localhost:8000/system/status +GET http://vision:8000/system/status # Response: SystemStatusResponse # { # "system_started": true, @@ -60,7 +60,7 @@ GET http://localhost:8000/system/status ############################################################################### ### Get all machines status -GET http://localhost:8000/machines +GET http://vision:8000/machines # Response: Dict[str, MachineStatusResponse] # { # "vibratory_conveyor": { @@ -84,7 +84,7 @@ GET http://localhost:8000/machines ############################################################################### ### Get MQTT status and statistics -GET http://localhost:8000/mqtt/status +GET http://vision:8000/mqtt/status # Response: MQTTStatusResponse # { # "connected": true, @@ -101,7 +101,7 @@ GET http://localhost:8000/mqtt/status # } ### Get recent MQTT events history -GET http://localhost:8000/mqtt/events +GET http://vision:8000/mqtt/events # Optional query parameter: limit (default: 5, max: 50) # Response: MQTTEventsHistoryResponse # { @@ -128,14 +128,14 @@ GET http://localhost:8000/mqtt/events # } ### Get recent MQTT events with custom limit -GET http://localhost:8000/mqtt/events?limit=10 +GET http://vision:8000/mqtt/events?limit=10 ############################################################################### # CAMERA ENDPOINTS ############################################################################### ### Get all cameras status -GET http://localhost:8000/cameras +GET http://vision:8000/cameras # Response: Dict[str, CameraStatusResponse] # { # "camera1": { @@ -157,9 +157,9 @@ GET http://localhost:8000/cameras ### ### Get specific camera status -GET http://localhost:8000/cameras/camera1/status +GET http://vision:8000/cameras/camera1/status ### Get specific camera status -GET http://localhost:8000/cameras/camera2/status +GET http://vision:8000/cameras/camera2/status # Response: CameraStatusResponse (same as above for single camera) ############################################################################### @@ -167,7 +167,7 @@ GET http://localhost:8000/cameras/camera2/status ############################################################################### ### Start recording (with all optional parameters) -POST http://localhost:8000/cameras/camera1/start-recording +POST http://vision:8000/cameras/camera1/start-recording Content-Type: application/json { @@ -193,7 +193,7 @@ Content-Type: application/json ### ### Start recording (minimal - only filename) -POST http://localhost:8000/cameras/camera1/start-recording +POST http://vision:8000/cameras/camera1/start-recording Content-Type: application/json { @@ -203,7 +203,7 @@ Content-Type: application/json ### ### Start recording (only camera settings) -POST http://localhost:8000/cameras/camera1/start-recording +POST http://vision:8000/cameras/camera1/start-recording Content-Type: application/json { @@ -215,7 +215,7 @@ Content-Type: application/json ### ### Start recording (empty body - all defaults) -POST http://localhost:8000/cameras/camera1/start-recording +POST http://vision:8000/cameras/camera1/start-recording Content-Type: application/json {} @@ -223,9 +223,9 @@ Content-Type: application/json ### ### Stop recording -POST http://localhost:8000/cameras/camera1/stop-recording +POST http://vision:8000/cameras/camera1/stop-recording ### Stop recording -POST http://localhost:8000/cameras/camera2/stop-recording +POST http://vision:8000/cameras/camera2/stop-recording # No request body required # Response: StopRecordingResponse # { @@ -239,8 +239,8 @@ POST http://localhost:8000/cameras/camera2/stop-recording ############################################################################### ### Test camera connection -POST http://localhost:8000/cameras/camera1/test-connection -POST http://localhost:8000/cameras/camera2/test-connection +POST http://vision:8000/cameras/camera1/test-connection +POST http://vision:8000/cameras/camera2/test-connection # No request body required # Response: CameraTestResponse # { @@ -253,8 +253,8 @@ POST http://localhost:8000/cameras/camera2/test-connection ### ### Reconnect camera (soft recovery) -POST http://localhost:8000/cameras/camera1/reconnect -POST http://localhost:8000/cameras/camera2/reconnect +POST http://vision:8000/cameras/camera1/reconnect +POST http://vision:8000/cameras/camera2/reconnect # No request body required # Response: CameraRecoveryResponse # { @@ -268,33 +268,33 @@ POST http://localhost:8000/cameras/camera2/reconnect ### ### Restart camera grab process -POST http://localhost:8000/cameras/camera1/restart-grab -POST http://localhost:8000/cameras/camera2/restart-grab +POST http://vision:8000/cameras/camera1/restart-grab +POST http://vision:8000/cameras/camera2/restart-grab # Response: CameraRecoveryResponse (same structure as reconnect) ### ### Reset camera timestamp -POST http://localhost:8000/cameras/camera1/reset-timestamp -POST http://localhost:8000/cameras/camera2/reset-timestamp +POST http://vision:8000/cameras/camera1/reset-timestamp +POST http://vision:8000/cameras/camera2/reset-timestamp # Response: CameraRecoveryResponse (same structure as reconnect) ### ### Full camera reset (hard recovery) -POST http://localhost:8000/cameras/camera1/full-reset +POST http://vision:8000/cameras/camera1/full-reset ### Full camera reset (hard recovery) -POST http://localhost:8000/cameras/camera2/full-reset +POST http://vision:8000/cameras/camera2/full-reset # Response: CameraRecoveryResponse (same structure as reconnect) ### ### Reinitialize failed camera -POST http://localhost:8000/cameras/camera1/reinitialize +POST http://vision:8000/cameras/camera1/reinitialize ### Reinitialize failed camera -POST http://localhost:8000/cameras/camera2/reinitialize +POST http://vision:8000/cameras/camera2/reinitialize # Response: CameraRecoveryResponse (same structure as reconnect) ############################################################################### @@ -302,7 +302,7 @@ POST http://localhost:8000/cameras/camera2/reinitialize ############################################################################### ### Get all recording sessions -GET http://localhost:8000/recordings +GET http://vision:8000/recordings # Response: Dict[str, RecordingInfoResponse] # { # "rec_001": { @@ -323,7 +323,7 @@ GET http://localhost:8000/recordings ############################################################################### ### Get storage statistics -GET http://localhost:8000/storage/stats +GET http://vision:8000/storage/stats # Response: StorageStatsResponse # { # "base_path": "/storage", @@ -345,7 +345,7 @@ GET http://localhost:8000/storage/stats ### ### Get recording files list (with filters) -POST http://localhost:8000/storage/files +POST http://vision:8000/storage/files Content-Type: application/json { @@ -377,7 +377,7 @@ Content-Type: application/json ### ### Get all files (no camera filter) -POST http://localhost:8000/storage/files +POST http://vision:8000/storage/files Content-Type: application/json { @@ -387,7 +387,7 @@ Content-Type: application/json ### ### Cleanup old storage files -POST http://localhost:8000/storage/cleanup +POST http://vision:8000/storage/cleanup Content-Type: application/json { diff --git a/docs/MODULAR_ARCHITECTURE_GUIDE.md b/docs/MODULAR_ARCHITECTURE_GUIDE.md new file mode 100644 index 0000000..ec6168e --- /dev/null +++ b/docs/MODULAR_ARCHITECTURE_GUIDE.md @@ -0,0 +1,300 @@ +# ๐Ÿ—๏ธ Modular Architecture Guide + +This guide demonstrates the modular architecture patterns implemented in the video streaming feature and how to apply them to other parts of the project. + +## ๐ŸŽฏ Goals + +- **Separation of Concerns**: Each module has a single responsibility +- **Reusability**: Components can be used across different parts of the application +- **Maintainability**: Easy to understand, modify, and test individual pieces +- **Scalability**: Easy to add new features without affecting existing code + +## ๐Ÿ“ Feature-Based Structure + +``` +src/features/video-streaming/ +โ”œโ”€โ”€ components/ # UI Components +โ”‚ โ”œโ”€โ”€ VideoPlayer.tsx +โ”‚ โ”œโ”€โ”€ VideoCard.tsx +โ”‚ โ”œโ”€โ”€ VideoList.tsx +โ”‚ โ”œโ”€โ”€ VideoModal.tsx +โ”‚ โ”œโ”€โ”€ VideoThumbnail.tsx +โ”‚ โ””โ”€โ”€ index.ts +โ”œโ”€โ”€ hooks/ # Custom React Hooks +โ”‚ โ”œโ”€โ”€ useVideoList.ts +โ”‚ โ”œโ”€โ”€ useVideoPlayer.ts +โ”‚ โ”œโ”€โ”€ useVideoInfo.ts +โ”‚ โ””โ”€โ”€ index.ts +โ”œโ”€โ”€ services/ # API & Business Logic +โ”‚ โ””โ”€โ”€ videoApi.ts +โ”œโ”€โ”€ types/ # TypeScript Definitions +โ”‚ โ””โ”€โ”€ index.ts +โ”œโ”€โ”€ utils/ # Pure Utility Functions +โ”‚ โ””โ”€โ”€ videoUtils.ts +โ”œโ”€โ”€ VideoStreamingPage.tsx # Main Feature Page +โ””โ”€โ”€ index.ts # Feature Export +``` + +## ๐Ÿงฉ Layer Responsibilities + +### 1. **Components Layer** (`/components`) +- **Purpose**: Pure UI components that handle rendering and user interactions +- **Rules**: + - No direct API calls + - Receive data via props + - Emit events via callbacks + - Minimal business logic + +**Example:** +```tsx +// โœ… Good: Pure component with clear props +export const VideoCard: React.FC = ({ + video, + onClick, + showMetadata = true, +}) => { + return ( +
onClick?.(video)}> + {/* UI rendering */} +
+ ); +}; + +// โŒ Bad: Component with API calls +export const VideoCard = () => { + const [video, setVideo] = useState(null); + + useEffect(() => { + fetch('/api/videos/123').then(/* ... */); // Don't do this! + }, []); +}; +``` + +### 2. **Hooks Layer** (`/hooks`) +- **Purpose**: Manage state, side effects, and provide data to components +- **Rules**: + - Handle API calls and data fetching + - Manage component state + - Provide clean interfaces to components + +**Example:** +```tsx +// โœ… Good: Hook handles complexity, provides simple interface +export function useVideoList(options = {}) { + const [videos, setVideos] = useState([]); + const [loading, setLoading] = useState(false); + + const fetchVideos = useCallback(async () => { + setLoading(true); + try { + const data = await videoApiService.getVideos(); + setVideos(data.videos); + } finally { + setLoading(false); + } + }, []); + + return { videos, loading, refetch: fetchVideos }; +} +``` + +### 3. **Services Layer** (`/services`) +- **Purpose**: Handle external dependencies (APIs, storage, etc.) +- **Rules**: + - Pure functions or classes + - No React dependencies + - Handle errors gracefully + - Provide consistent interfaces + +**Example:** +```tsx +// โœ… Good: Service handles API complexity +export class VideoApiService { + async getVideos(params = {}) { + try { + const response = await fetch(this.buildUrl('/videos', params)); + return await this.handleResponse(response); + } catch (error) { + throw new VideoApiError('FETCH_ERROR', error.message); + } + } +} +``` + +### 4. **Types Layer** (`/types`) +- **Purpose**: Centralized TypeScript definitions +- **Rules**: + - Define all interfaces and types + - Export from index.ts + - Keep types close to their usage + +### 5. **Utils Layer** (`/utils`) +- **Purpose**: Pure utility functions +- **Rules**: + - No side effects + - Easily testable + - Single responsibility + +## ๐Ÿ”„ Component Composition Patterns + +### Small, Focused Components + +Instead of large monolithic components, create small, focused ones: + +```tsx +// โœ… Good: Small, focused components + + {videos.map(video => ( + + ))} + + +// โŒ Bad: Monolithic component + + {/* 500+ lines of mixed concerns */} + +``` + +### Composition over Inheritance + +```tsx +// โœ… Good: Compose features +export const VideoStreamingPage = () => { + const { videos, loading } = useVideoList(); + const [selectedVideo, setSelectedVideo] = useState(null); + + return ( +
+ + +
+ ); +}; +``` + +## ๐ŸŽจ Applying to Existing Components + +### Example: Breaking Down VisionSystem Component + +**Current Structure (Monolithic):** +```tsx +// โŒ Current: One large component +export const VisionSystem = () => { + // 900+ lines of mixed concerns + return ( +
+ {/* System status */} + {/* Camera cards */} + {/* Storage info */} + {/* MQTT status */} +
+ ); +}; +``` + +**Proposed Modular Structure:** +``` +src/features/vision-system/ +โ”œโ”€โ”€ components/ +โ”‚ โ”œโ”€โ”€ SystemStatusCard.tsx +โ”‚ โ”œโ”€โ”€ CameraCard.tsx +โ”‚ โ”œโ”€โ”€ CameraGrid.tsx +โ”‚ โ”œโ”€โ”€ StorageOverview.tsx +โ”‚ โ”œโ”€โ”€ MqttStatus.tsx +โ”‚ โ””โ”€โ”€ index.ts +โ”œโ”€โ”€ hooks/ +โ”‚ โ”œโ”€โ”€ useSystemStatus.ts +โ”‚ โ”œโ”€โ”€ useCameraList.ts +โ”‚ โ””โ”€โ”€ index.ts +โ”œโ”€โ”€ services/ +โ”‚ โ””โ”€โ”€ visionApi.ts +โ””โ”€โ”€ VisionSystemPage.tsx +``` + +**Refactored Usage:** +```tsx +// โœ… Better: Composed from smaller parts +export const VisionSystemPage = () => { + return ( +
+ + + + +
+ ); +}; + +// Now you can reuse components elsewhere: +export const DashboardHome = () => { + return ( +
+ {/* Reused! */} + +
+ ); +}; +``` + +## ๐Ÿ“‹ Migration Strategy + +### Phase 1: Extract Utilities +1. Move pure functions to `/utils` +2. Move types to `/types` +3. Create service classes for API calls + +### Phase 2: Extract Hooks +1. Create custom hooks for data fetching +2. Move state management to hooks +3. Simplify component logic + +### Phase 3: Break Down Components +1. Identify distinct UI sections +2. Extract to separate components +3. Use composition in parent components + +### Phase 4: Feature Organization +1. Group related components, hooks, and services +2. Create feature-level exports +3. Update imports across the application + +## ๐Ÿงช Testing Benefits + +Modular architecture makes testing much easier: + +```tsx +// โœ… Easy to test individual pieces +describe('VideoCard', () => { + it('displays video information', () => { + render(); + expect(screen.getByText(mockVideo.filename)).toBeInTheDocument(); + }); +}); + +describe('useVideoList', () => { + it('fetches videos on mount', async () => { + const { result } = renderHook(() => useVideoList()); + await waitFor(() => { + expect(result.current.videos).toHaveLength(3); + }); + }); +}); +``` + +## ๐Ÿš€ Benefits Achieved + +1. **Reusability**: `VideoCard` can be used in lists, grids, or modals +2. **Maintainability**: Each file has a single, clear purpose +3. **Testability**: Small, focused units are easy to test +4. **Developer Experience**: Clear structure makes onboarding easier +5. **Performance**: Smaller components enable better optimization + +## ๐Ÿ“ Best Practices + +1. **Start Small**: Begin with one feature and apply patterns gradually +2. **Single Responsibility**: Each file should have one clear purpose +3. **Clear Interfaces**: Use TypeScript to define clear contracts +4. **Consistent Naming**: Follow naming conventions across features +5. **Documentation**: Document complex logic and interfaces + +This modular approach transforms large, hard-to-maintain components into small, reusable, and testable pieces that can be composed together to create powerful features. diff --git a/docs/VIDEO_STREAMING_INTEGRATION.md b/docs/VIDEO_STREAMING_INTEGRATION.md new file mode 100644 index 0000000..eb5fa44 --- /dev/null +++ b/docs/VIDEO_STREAMING_INTEGRATION.md @@ -0,0 +1,351 @@ +# ๐ŸŽฌ Video Streaming Integration Guide + +This guide shows how to integrate the modular video streaming feature into your existing dashboard. + +## ๐Ÿš€ Quick Start + +### 1. Add to Dashboard Navigation + +Update your sidebar or navigation to include the video streaming page: + +```tsx +// In src/components/Sidebar.tsx or similar +import { VideoStreamingPage } from '../features/video-streaming'; + +const navigationItems = [ + // ... existing items + { + name: 'Video Library', + href: '/videos', + icon: VideoCameraIcon, + component: VideoStreamingPage, + }, +]; +``` + +### 2. Add Route (if using React Router) + +```tsx +// In your main App.tsx or router configuration +import { VideoStreamingPage } from './features/video-streaming'; + +function App() { + return ( + + {/* ... existing routes */} + } /> + + ); +} +``` + +## ๐Ÿงฉ Using Individual Components + +The beauty of the modular architecture is that you can use individual components anywhere: + +### Dashboard Home - Recent Videos + +```tsx +// In src/components/DashboardHome.tsx +import { VideoList } from '../features/video-streaming'; + +export const DashboardHome = () => { + return ( +
+ {/* Existing dashboard content */} +
+

Recent Videos

+ +
+
+ ); +}; +``` + +### Vision System - Camera Videos + +```tsx +// In src/components/VisionSystem.tsx +import { VideoList, VideoCard } from '../features/video-streaming'; + +export const VisionSystem = () => { + const [selectedCamera, setSelectedCamera] = useState(null); + + return ( +
+ {/* Existing vision system content */} + + {/* Add video section for selected camera */} + {selectedCamera && ( +
+

+ Recent Videos - {selectedCamera} +

+ +
+ )} +
+ ); +}; +``` + +### Experiment Data Entry - Video Evidence + +```tsx +// In src/components/DataEntry.tsx +import { VideoThumbnail, VideoModal } from '../features/video-streaming'; + +export const DataEntry = () => { + const [selectedVideo, setSelectedVideo] = useState(null); + const [showVideoModal, setShowVideoModal] = useState(false); + + return ( +
+ {/* Existing form fields */} + + {/* Add video evidence section */} +
+ +
+ {experimentVideos.map(video => ( + { + setSelectedVideo(video); + setShowVideoModal(true); + }} + /> + ))} +
+
+ + setShowVideoModal(false)} + /> + + ); +}; +``` + +## ๐ŸŽจ Customizing Components + +### Custom Video Card for Experiments + +```tsx +// Create a specialized version for your use case +import { VideoCard } from '../features/video-streaming'; + +export const ExperimentVideoCard = ({ video, experimentId, onAttach }) => { + return ( +
+ + + {/* Add experiment-specific actions */} +
+ +
+
+ ); +}; +``` + +### Custom Video Player with Annotations + +```tsx +// Extend the base video player +import { VideoPlayer } from '../features/video-streaming'; + +export const AnnotatedVideoPlayer = ({ fileId, annotations }) => { + return ( +
+ + + {/* Add annotation overlay */} +
+ {annotations.map(annotation => ( +
+ {annotation.text} +
+ ))} +
+
+ ); +}; +``` + +## ๐Ÿ”ง Configuration + +### API Base URL + +Update the API base URL if needed: + +```tsx +// In your app configuration +import { VideoApiService } from './features/video-streaming'; + +// Create a configured instance +export const videoApi = new VideoApiService('http://your-api-server:8000'); + +// Or set globally +process.env.REACT_APP_VIDEO_API_URL = 'http://your-api-server:8000'; +``` + +### Custom Styling + +The components use Tailwind CSS classes. You can customize them: + +```tsx +// Override default styles + + + +``` + +## ๐ŸŽฏ Integration Examples + +### 1. Camera Management Integration + +```tsx +// In your camera management page +import { VideoList, useVideoList } from '../features/video-streaming'; + +export const CameraManagement = () => { + const [selectedCamera, setSelectedCamera] = useState(null); + const { videos } = useVideoList({ + initialParams: { camera_name: selectedCamera?.name } + }); + + return ( +
+ {/* Camera controls */} + + + {/* Videos from selected camera */} +
+

Videos from {selectedCamera?.name}

+ +
+
+ ); +}; +``` + +### 2. Experiment Timeline Integration + +```tsx +// Show videos in experiment timeline +import { VideoThumbnail } from '../features/video-streaming'; + +export const ExperimentTimeline = ({ experiment }) => { + return ( +
+ {experiment.events.map(event => ( +
+
+

{event.title}

+

{event.description}

+ + {/* Show related videos */} + {event.videos?.length > 0 && ( +
+ {event.videos.map(videoId => ( + + ))} +
+ )} +
+
+ ))} +
+ ); +}; +``` + +## ๐Ÿ“ฑ Responsive Design + +The components are designed to be responsive: + +```tsx +// Automatic responsive grid + + +// Mobile-friendly video player + +``` + +## ๐Ÿ” Search Integration + +Add search functionality: + +```tsx +import { useVideoList } from '../features/video-streaming'; + +export const VideoSearch = () => { + const [searchTerm, setSearchTerm] = useState(''); + const { videos, loading } = useVideoList({ + initialParams: { search: searchTerm } + }); + + return ( +
+ setSearchTerm(e.target.value)} + placeholder="Search videos..." + className="w-full px-4 py-2 border rounded-lg" + /> + + +
+ ); +}; +``` + +## ๐Ÿš€ Next Steps + +1. **Start Small**: Begin by adding the video library page +2. **Integrate Gradually**: Add individual components to existing pages +3. **Customize**: Create specialized versions for your specific needs +4. **Extend**: Add new features like annotations, bookmarks, or sharing + +The modular architecture makes it easy to start simple and grow the functionality over time! diff --git a/src/components/DashboardLayout.tsx b/src/components/DashboardLayout.tsx index 6e884ed..e2191d4 100644 --- a/src/components/DashboardLayout.tsx +++ b/src/components/DashboardLayout.tsx @@ -6,6 +6,7 @@ import { UserManagement } from './UserManagement' import { Experiments } from './Experiments' import { DataEntry } from './DataEntry' import { VisionSystem } from './VisionSystem' +import { VideoStreamingPage } from '../features/video-streaming' import { userManagement, type User } from '../lib/supabase' interface DashboardLayoutProps { @@ -84,6 +85,8 @@ export function DashboardLayout({ onLogout }: DashboardLayoutProps) { return case 'vision-system': return + case 'video-library': + return default: return } diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index c3edffb..ef4dbec 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -48,6 +48,15 @@ export function Sidebar({ user, currentView, onViewChange }: SidebarProps) { ), requiredRoles: ['admin', 'conductor'] }, + { + id: 'video-library', + name: 'Video Library', + icon: ( + + + + ), + }, { id: 'analytics', name: 'Analytics', diff --git a/src/features/video-streaming/VideoStreamingPage.tsx b/src/features/video-streaming/VideoStreamingPage.tsx new file mode 100644 index 0000000..d5ac8e9 --- /dev/null +++ b/src/features/video-streaming/VideoStreamingPage.tsx @@ -0,0 +1,178 @@ +/** + * VideoStreamingPage Component + * + * Main page component for the video streaming feature. + * Demonstrates how to compose the modular components together. + */ + +import React, { useState, useMemo } from 'react'; +import { VideoList, VideoModal } from './components'; +import { type VideoFile, type VideoListFilters, type VideoListSortOptions } from './types'; + +export const VideoStreamingPage: React.FC = () => { + const [selectedVideo, setSelectedVideo] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [filters, setFilters] = useState({}); + const [sortOptions, setSortOptions] = useState({ + field: 'created_at', + direction: 'desc', + }); + + // Available cameras for filtering (this could come from an API) + const availableCameras = ['camera1', 'camera2', 'camera3']; // This should be fetched from your camera API + + const handleVideoSelect = (video: VideoFile) => { + setSelectedVideo(video); + setIsModalOpen(true); + }; + + const handleModalClose = () => { + setIsModalOpen(false); + setSelectedVideo(null); + }; + + const handleCameraFilterChange = (cameraName: string) => { + setFilters(prev => ({ + ...prev, + cameraName: cameraName === 'all' ? undefined : cameraName, + })); + }; + + const handleSortChange = (field: VideoListSortOptions['field'], direction: VideoListSortOptions['direction']) => { + setSortOptions({ field, direction }); + }; + + const handleDateRangeChange = (start: string, end: string) => { + setFilters(prev => ({ + ...prev, + dateRange: start && end ? { start, end } : undefined, + })); + }; + + return ( +
+ {/* Header */} +
+
+
+

Video Library

+

+ Browse and view recorded videos from your camera system +

+
+
+
+ + {/* Filters and Controls */} +
+
+
+ {/* Camera Filter */} +
+ + +
+ + {/* Sort Options */} +
+ +
+ + +
+
+ + {/* Date Range Filter */} +
+ +
+ handleDateRangeChange(e.target.value, filters.dateRange?.end || '')} + className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> + handleDateRangeChange(filters.dateRange?.start || '', e.target.value)} + className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> +
+
+
+ + {/* Clear Filters */} + {(filters.cameraName || filters.dateRange) && ( +
+ +
+ )} +
+ + {/* Video List */} + +
+ + {/* Video Modal */} + +
+ ); +}; diff --git a/src/features/video-streaming/components/VideoCard.tsx b/src/features/video-streaming/components/VideoCard.tsx new file mode 100644 index 0000000..e61ac53 --- /dev/null +++ b/src/features/video-streaming/components/VideoCard.tsx @@ -0,0 +1,162 @@ +/** + * VideoCard Component + * + * A reusable card component for displaying video information with thumbnail, metadata, and actions. + */ + +import React from 'react'; +import { type VideoCardProps } from '../types'; +import { VideoThumbnail } from './VideoThumbnail'; +import { + formatFileSize, + formatVideoDate, + getRelativeTime, + getFormatDisplayName, + getStatusBadgeClass, + getResolutionString, +} from '../utils/videoUtils'; + +export const VideoCard: React.FC = ({ + video, + onClick, + showMetadata = true, + className = '', +}) => { + const handleClick = () => { + if (onClick) { + onClick(video); + } + }; + + const handleThumbnailClick = () => { + handleClick(); + }; + + const cardClasses = [ + 'bg-white rounded-lg shadow-md overflow-hidden transition-shadow hover:shadow-lg', + onClick ? 'cursor-pointer' : '', + className, + ].filter(Boolean).join(' '); + + return ( +
+ {/* Thumbnail */} +
+ + + {/* Status Badge */} +
+ + {video.status} + +
+ + {/* Format Badge */} +
+ + {getFormatDisplayName(video.format)} + +
+ + {/* Streamable Indicator */} + {video.is_streamable && ( +
+
+ + + + Streamable +
+
+ )} + + {/* Conversion Needed Indicator */} + {video.needs_conversion && ( +
+
+ + + + Needs Conversion +
+
+ )} +
+ + {/* Content */} +
+ {/* Title */} +

+ {video.filename} +

+ + {/* Camera Name */} +
+ + + + {video.camera_name} +
+ + {/* Basic Info */} +
+
+ Size: {formatFileSize(video.file_size_bytes)} +
+
+ Created: {getRelativeTime(video.created_at)} +
+
+ + {/* Metadata (if available and requested) */} + {showMetadata && 'metadata' in video && video.metadata && ( +
+
+
+ Duration: {Math.round(video.metadata.duration_seconds)}s +
+
+ Resolution: {getResolutionString(video.metadata.width, video.metadata.height)} +
+
+ FPS: {video.metadata.fps} +
+
+ Codec: {video.metadata.codec} +
+
+
+ )} + + {/* Actions */} +
+
+ {formatVideoDate(video.created_at)} +
+ + {onClick && ( + + )} +
+
+
+ ); +}; diff --git a/src/features/video-streaming/components/VideoList.tsx b/src/features/video-streaming/components/VideoList.tsx new file mode 100644 index 0000000..9b251ea --- /dev/null +++ b/src/features/video-streaming/components/VideoList.tsx @@ -0,0 +1,195 @@ +/** + * VideoList Component + * + * A reusable component for displaying a list/grid of videos with filtering, sorting, and pagination. + */ + +import React, { useState, useEffect } from 'react'; +import { type VideoListProps, type VideoListFilters, type VideoListSortOptions } from '../types'; +import { useVideoList } from '../hooks/useVideoList'; +import { VideoCard } from './VideoCard'; + +export const VideoList: React.FC = ({ + filters, + sortOptions, + limit = 20, + onVideoSelect, + className = '', +}) => { + const [localFilters, setLocalFilters] = useState(filters || {}); + const [localSort, setLocalSort] = useState( + sortOptions || { field: 'created_at', direction: 'desc' } + ); + + const { + videos, + totalCount, + loading, + error, + refetch, + loadMore, + hasMore, + updateFilters, + updateSort, + } = useVideoList({ + initialParams: { + camera_name: localFilters.cameraName, + start_date: localFilters.dateRange?.start, + end_date: localFilters.dateRange?.end, + limit, + include_metadata: true, + }, + autoFetch: true, + }); + + // Update filters when props change (but don't auto-fetch) + useEffect(() => { + if (filters) { + setLocalFilters(filters); + } + }, [filters]); + + // Update sort when props change + useEffect(() => { + if (sortOptions) { + setLocalSort(sortOptions); + updateSort(sortOptions); + } + }, [sortOptions, updateSort]); + + const handleVideoClick = (video: any) => { + if (onVideoSelect) { + onVideoSelect(video); + } + }; + + const handleLoadMore = () => { + if (hasMore && loading !== 'loading') { + loadMore(); + } + }; + + const containerClasses = [ + 'video-list', + className, + ].filter(Boolean).join(' '); + + if (loading === 'loading' && videos.length === 0) { + return ( +
+
+
+
+

Loading videos...

+
+
+
+ ); + } + + if (error) { + return ( +
+
+
+ + + +

Error Loading Videos

+

{error.message}

+ +
+
+
+ ); + } + + if (videos.length === 0) { + return ( +
+
+
+ + + +

No Videos Found

+

No videos match your current filters.

+
+
+
+ ); + } + + return ( +
+ {/* Results Summary */} +
+
+ Showing {videos.length} of {totalCount} videos +
+ + +
+ + {/* Video Grid */} +
+ {videos.map((video) => ( + + ))} +
+ + {/* Load More Button */} + {hasMore && ( +
+ +
+ )} + + {/* Loading Indicator for Additional Videos */} + {loading === 'loading' && videos.length > 0 && ( +
+
+
+ Loading more videos... +
+
+ )} +
+ ); +}; diff --git a/src/features/video-streaming/components/VideoModal.tsx b/src/features/video-streaming/components/VideoModal.tsx new file mode 100644 index 0000000..07dd222 --- /dev/null +++ b/src/features/video-streaming/components/VideoModal.tsx @@ -0,0 +1,221 @@ +/** + * VideoModal Component + * + * A modal component for displaying videos in fullscreen with detailed information. + */ + +import React, { useEffect } from 'react'; +import { type VideoFile } from '../types'; +import { VideoPlayer } from './VideoPlayer'; +import { useVideoInfo } from '../hooks/useVideoInfo'; +import { + formatFileSize, + formatVideoDate, + getFormatDisplayName, + getStatusBadgeClass, + getResolutionString, + formatDuration, +} from '../utils/videoUtils'; + +interface VideoModalProps { + video: VideoFile | null; + isOpen: boolean; + onClose: () => void; +} + +export const VideoModal: React.FC = ({ + video, + isOpen, + onClose, +}) => { + const { videoInfo, streamingInfo, loading, error } = useVideoInfo( + video?.file_id || null, + { autoFetch: isOpen && !!video } + ); + + // Handle escape key + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onClose(); + } + }; + + if (isOpen) { + document.addEventListener('keydown', handleEscape); + document.body.style.overflow = 'hidden'; + } + + return () => { + document.removeEventListener('keydown', handleEscape); + document.body.style.overflow = 'unset'; + }; + }, [isOpen, onClose]); + + if (!isOpen || !video) { + return null; + } + + const handleBackdropClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + onClose(); + } + }; + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
+
+ {/* Header */} +
+

+ {video.filename} +

+ +
+ + {/* Content */} +
+ {/* Video Player */} +
+ +
+ + {/* Sidebar with Video Info */} +
+
+ {/* Status and Format */} +
+ + {video.status} + + + {getFormatDisplayName(video.format)} + +
+ + {/* Basic Info */} +
+
+

Basic Information

+
+
+
Camera:
+
{video.camera_name}
+
+
+
File Size:
+
{formatFileSize(video.file_size_bytes)}
+
+
+
Created:
+
{formatVideoDate(video.created_at)}
+
+
+
Streamable:
+
{video.is_streamable ? 'Yes' : 'No'}
+
+
+
+ + {/* Video Metadata */} + {videoInfo?.metadata && ( +
+

Video Details

+
+
+
Duration:
+
{formatDuration(videoInfo.metadata.duration_seconds)}
+
+
+
Resolution:
+
+ {getResolutionString(videoInfo.metadata.width, videoInfo.metadata.height)} +
+
+
+
Frame Rate:
+
{videoInfo.metadata.fps} fps
+
+
+
Codec:
+
{videoInfo.metadata.codec}
+
+
+
Aspect Ratio:
+
{videoInfo.metadata.aspect_ratio.toFixed(2)}
+
+
+
+ )} + + {/* Streaming Info */} + {streamingInfo && ( +
+

Streaming Details

+
+
+
Content Type:
+
{streamingInfo.content_type}
+
+
+
Range Requests:
+
{streamingInfo.supports_range_requests ? 'Supported' : 'Not Supported'}
+
+
+
Chunk Size:
+
{formatFileSize(streamingInfo.chunk_size_bytes)}
+
+
+
+ )} + + {/* Loading State */} + {loading === 'loading' && ( +
+
+ Loading video details... +
+ )} + + {/* Error State */} + {error && ( +
+
+ + + +
+

Error loading video details

+

{error.message}

+
+
+
+ )} +
+
+
+
+
+
+
+ ); +}; diff --git a/src/features/video-streaming/components/VideoPlayer.tsx b/src/features/video-streaming/components/VideoPlayer.tsx new file mode 100644 index 0000000..5539519 --- /dev/null +++ b/src/features/video-streaming/components/VideoPlayer.tsx @@ -0,0 +1,204 @@ +/** + * VideoPlayer Component + * + * A reusable video player component with full controls and customization options. + * Uses the useVideoPlayer hook for state management and provides a clean interface. + */ + +import React, { forwardRef } from 'react'; +import { useVideoPlayer } from '../hooks/useVideoPlayer'; +import { videoApiService } from '../services/videoApi'; +import { type VideoPlayerProps } from '../types'; +import { formatDuration } from '../utils/videoUtils'; + +export const VideoPlayer = forwardRef(({ + fileId, + autoPlay = false, + controls = true, + width = '100%', + height = 'auto', + className = '', + onPlay, + onPause, + onEnded, + onError, +}, forwardedRef) => { + const { state, actions, ref } = useVideoPlayer({ + autoPlay, + onPlay, + onPause, + onEnded, + onError, + }); + + // Combine refs + React.useImperativeHandle(forwardedRef, () => ref.current!, [ref]); + + const streamingUrl = videoApiService.getStreamingUrl(fileId); + + const handleSeek = (e: React.MouseEvent) => { + if (!ref.current) return; + + const rect = e.currentTarget.getBoundingClientRect(); + const clickX = e.clientX - rect.left; + const percentage = clickX / rect.width; + const newTime = percentage * state.duration; + + actions.seek(newTime); + }; + + const handleVolumeChange = (e: React.ChangeEvent) => { + actions.setVolume(parseFloat(e.target.value)); + }; + + return ( +
+ {/* Video Element */} + + + {/* Loading Overlay */} + {state.isLoading && ( +
+
Loading...
+
+ )} + + {/* Error Overlay */} + {state.error && ( +
+
+
Playback Error
+
{state.error}
+
+
+ )} + + {/* Custom Controls */} + {controls && ( +
+ {/* Progress Bar */} +
+
+
0 ? (state.currentTime / state.duration) * 100 : 0}%` + }} + /> +
+
+ + {/* Control Bar */} +
+ {/* Left Controls */} +
+ {/* Play/Pause Button */} + + + {/* Skip Backward */} + + + {/* Skip Forward */} + + + {/* Time Display */} +
+ {formatDuration(state.currentTime)} / {formatDuration(state.duration)} +
+
+ + {/* Right Controls */} +
+ {/* Volume Control */} +
+ + + +
+ + {/* Fullscreen Button */} + +
+
+
+ )} +
+ ); +}); + +VideoPlayer.displayName = 'VideoPlayer'; diff --git a/src/features/video-streaming/components/VideoThumbnail.tsx b/src/features/video-streaming/components/VideoThumbnail.tsx new file mode 100644 index 0000000..db1073f --- /dev/null +++ b/src/features/video-streaming/components/VideoThumbnail.tsx @@ -0,0 +1,136 @@ +/** + * VideoThumbnail Component + * + * A reusable component for displaying video thumbnails with loading states and error handling. + */ + +import React, { useState, useEffect } from 'react'; +import { videoApiService } from '../services/videoApi'; +import { type VideoThumbnailProps } from '../types'; + +export const VideoThumbnail: React.FC = ({ + fileId, + timestamp = 0, + width = 320, + height = 240, + alt = 'Video thumbnail', + className = '', + onClick, +}) => { + const [thumbnailUrl, setThumbnailUrl] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let isMounted = true; + + const loadThumbnail = async () => { + try { + setIsLoading(true); + setError(null); + + const blob = await videoApiService.getThumbnailBlob(fileId, { + timestamp, + width, + height, + }); + + if (isMounted) { + const url = URL.createObjectURL(blob); + setThumbnailUrl(url); + setIsLoading(false); + } + } catch (err) { + if (isMounted) { + setError(err instanceof Error ? err.message : 'Failed to load thumbnail'); + setIsLoading(false); + } + } + }; + + loadThumbnail(); + + return () => { + isMounted = false; + if (thumbnailUrl) { + URL.revokeObjectURL(thumbnailUrl); + } + }; + }, [fileId, timestamp, width, height]); + + // Cleanup URL on unmount + useEffect(() => { + return () => { + if (thumbnailUrl) { + URL.revokeObjectURL(thumbnailUrl); + } + }; + }, [thumbnailUrl]); + + const handleClick = () => { + if (onClick && !isLoading && !error) { + onClick(); + } + }; + + const containerClasses = [ + 'relative overflow-hidden bg-gray-200 rounded', + onClick && !isLoading && !error ? 'cursor-pointer hover:opacity-80 transition-opacity' : '', + className, + ].filter(Boolean).join(' '); + + return ( +
+ {/* Loading State */} + {isLoading && ( +
+
+
+ )} + + {/* Error State */} + {error && ( +
+
+ + + +
Failed to load thumbnail
+
+
+ )} + + {/* Thumbnail Image */} + {thumbnailUrl && !isLoading && !error && ( + {alt} setError('Failed to display thumbnail')} + /> + )} + + {/* Play Overlay */} + {onClick && !isLoading && !error && ( +
+
+ + + +
+
+ )} + + {/* Timestamp Badge */} + {timestamp > 0 && !isLoading && !error && ( +
+ {Math.floor(timestamp / 60)}:{(timestamp % 60).toString().padStart(2, '0')} +
+ )} +
+ ); +}; diff --git a/src/features/video-streaming/components/index.ts b/src/features/video-streaming/components/index.ts new file mode 100644 index 0000000..1a07684 --- /dev/null +++ b/src/features/video-streaming/components/index.ts @@ -0,0 +1,20 @@ +/** + * Video Streaming Components - Index + * + * Centralized export for all video streaming components. + * This makes it easy to import components from a single location. + */ + +export { VideoPlayer } from './VideoPlayer'; +export { VideoThumbnail } from './VideoThumbnail'; +export { VideoCard } from './VideoCard'; +export { VideoList } from './VideoList'; +export { VideoModal } from './VideoModal'; + +// Re-export component prop types for convenience +export type { + VideoPlayerProps, + VideoThumbnailProps, + VideoCardProps, + VideoListProps, +} from '../types'; diff --git a/src/features/video-streaming/hooks/index.ts b/src/features/video-streaming/hooks/index.ts new file mode 100644 index 0000000..b8d8621 --- /dev/null +++ b/src/features/video-streaming/hooks/index.ts @@ -0,0 +1,16 @@ +/** + * Video Streaming Hooks - Index + * + * Centralized export for all video streaming hooks. + * This makes it easy to import hooks from a single location. + */ + +export { useVideoList, type UseVideoListReturn } from './useVideoList'; +export { useVideoPlayer, type UseVideoPlayerReturn, type VideoPlayerState } from './useVideoPlayer'; +export { useVideoInfo, type UseVideoInfoReturn } from './useVideoInfo'; + +// Re-export types that are commonly used with hooks +export type { + VideoListFilters, + VideoListSortOptions, +} from '../types'; diff --git a/src/features/video-streaming/hooks/useVideoInfo.ts b/src/features/video-streaming/hooks/useVideoInfo.ts new file mode 100644 index 0000000..c0107aa --- /dev/null +++ b/src/features/video-streaming/hooks/useVideoInfo.ts @@ -0,0 +1,191 @@ +/** + * useVideoInfo Hook + * + * Custom React hook for fetching and managing video metadata and streaming information. + */ + +import { useState, useEffect, useCallback, useRef } from 'react'; +import { videoApiService } from '../services/videoApi'; +import { + type VideoInfoResponse, + type VideoStreamingInfo, + type VideoError, + type LoadingState +} from '../types'; + +export interface UseVideoInfoReturn { + videoInfo: VideoInfoResponse | null; + streamingInfo: VideoStreamingInfo | null; + loading: LoadingState; + error: VideoError | null; + refetch: () => Promise; + clearCache: () => void; + reset: () => void; +} + +interface UseVideoInfoOptions { + autoFetch?: boolean; + cacheKey?: string; +} + +export function useVideoInfo( + fileId: string | null, + options: UseVideoInfoOptions = {} +) { + const { autoFetch = true, cacheKey = 'default' } = options; + + // State + const [videoInfo, setVideoInfo] = useState(null); + const [streamingInfo, setStreamingInfo] = useState(null); + const [loading, setLoading] = useState('idle'); + const [error, setError] = useState(null); + + // Refs for cleanup and caching + const abortControllerRef = useRef(null); + const cacheRef = useRef>(new Map()); + + const CACHE_DURATION = 10 * 60 * 1000; // 10 minutes + + /** + * Check if cached data is still valid + */ + const isCacheValid = useCallback((timestamp: number): boolean => { + return Date.now() - timestamp < CACHE_DURATION; + }, [CACHE_DURATION]); + + /** + * Fetch video information + */ + const fetchVideoInfo = useCallback(async (id: string): Promise => { + // Cancel any ongoing request + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + const controller = new AbortController(); + abortControllerRef.current = controller; + + try { + setLoading('loading'); + setError(null); + + // Check cache first + const key = `${cacheKey}_${id}`; + const cached = cacheRef.current.get(key); + + if (cached && isCacheValid(cached.timestamp)) { + setVideoInfo(cached.videoInfo); + setStreamingInfo(cached.streamingInfo); + setLoading('success'); + return; + } + + // Fetch both video info and streaming info in parallel + const [videoInfoResponse, streamingInfoResponse] = await Promise.all([ + videoApiService.getVideoInfo(id), + videoApiService.getStreamingInfo(id) + ]); + + // Check if request was aborted + if (controller.signal.aborted) { + return; + } + + // Update cache + cacheRef.current.set(key, { + videoInfo: videoInfoResponse, + streamingInfo: streamingInfoResponse, + timestamp: Date.now() + }); + + // Update state + setVideoInfo(videoInfoResponse); + setStreamingInfo(streamingInfoResponse); + setLoading('success'); + + } catch (err) { + if (controller.signal.aborted) { + return; + } + + const videoError: VideoError = err instanceof Error + ? { code: 'FETCH_ERROR', message: err.message, details: err } + : { code: 'UNKNOWN_ERROR', message: 'An unknown error occurred' }; + + setError(videoError); + setLoading('error'); + } finally { + abortControllerRef.current = null; + } + }, [cacheKey, isCacheValid]); + + /** + * Refetch video information + */ + const refetch = useCallback(async (): Promise => { + if (!fileId) return; + await fetchVideoInfo(fileId); + }, [fileId, fetchVideoInfo]); + + /** + * Clear cache for current video + */ + const clearCache = useCallback((): void => { + if (!fileId) return; + const key = `${cacheKey}_${fileId}`; + cacheRef.current.delete(key); + }, [fileId, cacheKey]); + + /** + * Reset state + */ + const reset = useCallback((): void => { + setVideoInfo(null); + setStreamingInfo(null); + setLoading('idle'); + setError(null); + }, []); + + // Auto-fetch when fileId changes + useEffect(() => { + if (fileId && autoFetch) { + fetchVideoInfo(fileId); + } else if (!fileId) { + reset(); + } + + // Cleanup on unmount or fileId change + return () => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + }; + }, [fileId, autoFetch, fetchVideoInfo, reset]); + + // Cleanup cache periodically + useEffect(() => { + const interval = setInterval(() => { + for (const [key, value] of cacheRef.current.entries()) { + if (!isCacheValid(value.timestamp)) { + cacheRef.current.delete(key); + } + } + }, CACHE_DURATION); + + return () => clearInterval(interval); + }, [isCacheValid, CACHE_DURATION]); + + return { + videoInfo, + streamingInfo, + loading, + error, + refetch, + clearCache, + reset, + }; +} diff --git a/src/features/video-streaming/hooks/useVideoList.ts b/src/features/video-streaming/hooks/useVideoList.ts new file mode 100644 index 0000000..179528c --- /dev/null +++ b/src/features/video-streaming/hooks/useVideoList.ts @@ -0,0 +1,187 @@ +/** + * useVideoList Hook + * + * Custom React hook for managing video list state, fetching, filtering, and pagination. + * Provides a clean interface for components to interact with video data. + */ + +import { useState, useEffect, useCallback, useRef } from 'react'; +import { videoApiService } from '../services/videoApi'; +import { + type VideoFile, + type VideoListParams, + type VideoError, + type LoadingState, + type VideoListFilters, + type VideoListSortOptions +} from '../types'; + +export interface UseVideoListReturn { + videos: VideoFile[]; + totalCount: number; + loading: LoadingState; + error: VideoError | null; + refetch: () => Promise; + loadMore: () => Promise; + hasMore: boolean; + updateFilters: (filters: VideoListFilters) => void; + updateSort: (sortOptions: VideoListSortOptions) => void; + clearCache: () => void; + reset: () => void; +} +import { filterVideos, sortVideos } from '../utils/videoUtils'; + +interface UseVideoListOptions { + initialParams?: VideoListParams; + autoFetch?: boolean; + cacheKey?: string; +} + +export function useVideoList(options: UseVideoListOptions = {}) { + const { + initialParams = {}, + autoFetch = true, + cacheKey = 'default' + } = options; + + // State + const [videos, setVideos] = useState([]); + const [totalCount, setTotalCount] = useState(0); + const [loading, setLoading] = useState('idle'); + const [error, setError] = useState(null); + const [hasMore, setHasMore] = useState(true); + + // Refs for cleanup and caching + const abortControllerRef = useRef(null); + const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes + + /** + * Fetch videos from API + */ + const fetchVideos = useCallback(async ( + params: VideoListParams = initialParams, + append: boolean = false + ): Promise => { + // Cancel any ongoing request + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + const controller = new AbortController(); + abortControllerRef.current = controller; + + try { + setLoading('loading'); + setError(null); + + // Fetch from API + const response = await videoApiService.getVideos(params); + + // Check if request was aborted + if (controller.signal.aborted) { + return; + } + + // Update state + setVideos(append ? prev => [...prev, ...response.videos] : response.videos); + setTotalCount(response.total_count); + setHasMore(response.videos.length === (params.limit || 50)); + setLoading('success'); + + } catch (err) { + if (controller.signal.aborted) { + return; + } + + const videoError: VideoError = err instanceof Error + ? { code: 'FETCH_ERROR', message: err.message, details: err } + : { code: 'UNKNOWN_ERROR', message: 'An unknown error occurred' }; + + setError(videoError); + setLoading('error'); + } finally { + abortControllerRef.current = null; + } + }, [initialParams]); + + /** + * Refetch videos with initial parameters + */ + const refetch = useCallback(async (): Promise => { + await fetchVideos(initialParams, false); + }, [fetchVideos, initialParams]); + + /** + * Load more videos (pagination) + */ + const loadMore = useCallback(async (): Promise => { + if (!hasMore || loading === 'loading') { + return; + } + + const offset = videos.length; + const params = { ...initialParams, offset }; + await fetchVideos(params, true); + }, [hasMore, loading, videos.length, initialParams, fetchVideos]); + + /** + * Update filters and refetch + */ + const updateFilters = useCallback((filters: VideoListFilters): void => { + const newParams: VideoListParams = { + ...initialParams, + camera_name: filters.cameraName, + start_date: filters.dateRange?.start, + end_date: filters.dateRange?.end, + }; + + fetchVideos(newParams, false); + }, [initialParams, fetchVideos]); + + /** + * Update sort options and refetch + */ + const updateSort = useCallback((sortOptions: VideoListSortOptions): void => { + // Since the API doesn't support sorting, we'll sort locally + setVideos(prev => sortVideos(prev, sortOptions.field, sortOptions.direction)); + }, []); + + /** + * Reset to initial state + */ + const reset = useCallback((): void => { + setVideos([]); + setTotalCount(0); + setLoading('idle'); + setError(null); + setHasMore(true); + }, []); + + // Auto-fetch on mount only + useEffect(() => { + if (autoFetch) { + fetchVideos(initialParams, false); + } + + // Cleanup on unmount + return () => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + }; + }, []); // Empty dependency array - only run once on mount + + return { + videos, + totalCount, + loading, + error, + refetch, + loadMore, + hasMore, + // Additional utility methods + updateFilters, + updateSort, + reset, + }; +} diff --git a/src/features/video-streaming/hooks/useVideoPlayer.ts b/src/features/video-streaming/hooks/useVideoPlayer.ts new file mode 100644 index 0000000..a3efee7 --- /dev/null +++ b/src/features/video-streaming/hooks/useVideoPlayer.ts @@ -0,0 +1,317 @@ +/** + * useVideoPlayer Hook + * + * Custom React hook for managing video player state and controls. + * Provides a comprehensive interface for video playback functionality. + */ + +import { useState, useRef, useEffect, useCallback } from 'react'; + +// Video player state interface +export interface VideoPlayerState { + isPlaying: boolean; + currentTime: number; + duration: number; + volume: number; + isMuted: boolean; + isFullscreen: boolean; + isLoading: boolean; + error: string | null; +} + +export interface UseVideoPlayerReturn { + state: VideoPlayerState; + actions: { + play: () => void; + pause: () => void; + togglePlay: () => void; + seek: (time: number) => void; + setVolume: (volume: number) => void; + toggleMute: () => void; + toggleFullscreen: () => void; + skip: (seconds: number) => void; + setPlaybackRate: (rate: number) => void; + reset: () => void; + }; + ref: React.RefObject; +} + +interface UseVideoPlayerOptions { + autoPlay?: boolean; + loop?: boolean; + muted?: boolean; + volume?: number; + onPlay?: () => void; + onPause?: () => void; + onEnded?: () => void; + onError?: (error: string) => void; + onTimeUpdate?: (currentTime: number) => void; + onDurationChange?: (duration: number) => void; +} + +export function useVideoPlayer(options: UseVideoPlayerOptions = {}) { + const { + autoPlay = false, + loop = false, + muted = false, + volume = 1, + onPlay, + onPause, + onEnded, + onError, + onTimeUpdate, + onDurationChange, + } = options; + + // Video element ref + const videoRef = useRef(null); + + // Player state + const [state, setState] = useState({ + isPlaying: false, + currentTime: 0, + duration: 0, + volume: volume, + isMuted: muted, + isFullscreen: false, + isLoading: false, + error: null, + }); + + /** + * Update state helper + */ + const updateState = useCallback((updates: Partial) => { + setState(prev => ({ ...prev, ...updates })); + }, []); + + /** + * Play video + */ + const play = useCallback(async () => { + const video = videoRef.current; + if (!video) return; + + try { + updateState({ isLoading: true, error: null }); + await video.play(); + updateState({ isPlaying: true, isLoading: false }); + onPlay?.(); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Failed to play video'; + updateState({ isLoading: false, error: errorMessage }); + onError?.(errorMessage); + } + }, [updateState, onPlay, onError]); + + /** + * Pause video + */ + const pause = useCallback(() => { + const video = videoRef.current; + if (!video) return; + + video.pause(); + updateState({ isPlaying: false }); + onPause?.(); + }, [updateState, onPause]); + + /** + * Toggle play/pause + */ + const togglePlay = useCallback(() => { + if (state.isPlaying) { + pause(); + } else { + play(); + } + }, [state.isPlaying, play, pause]); + + /** + * Seek to specific time + */ + const seek = useCallback((time: number) => { + const video = videoRef.current; + if (!video) return; + + video.currentTime = Math.max(0, Math.min(time, video.duration || 0)); + }, []); + + /** + * Set volume (0-1) + */ + const setVolume = useCallback((newVolume: number) => { + const video = videoRef.current; + if (!video) return; + + const clampedVolume = Math.max(0, Math.min(1, newVolume)); + video.volume = clampedVolume; + updateState({ volume: clampedVolume }); + }, [updateState]); + + /** + * Toggle mute + */ + const toggleMute = useCallback(() => { + const video = videoRef.current; + if (!video) return; + + video.muted = !video.muted; + updateState({ isMuted: video.muted }); + }, [updateState]); + + /** + * Enter/exit fullscreen + */ + const toggleFullscreen = useCallback(async () => { + const video = videoRef.current; + if (!video) return; + + try { + if (!document.fullscreenElement) { + await video.requestFullscreen(); + updateState({ isFullscreen: true }); + } else { + await document.exitFullscreen(); + updateState({ isFullscreen: false }); + } + } catch (error) { + console.warn('Fullscreen not supported or failed:', error); + } + }, [updateState]); + + /** + * Skip forward/backward + */ + const skip = useCallback((seconds: number) => { + const video = videoRef.current; + if (!video) return; + + const newTime = video.currentTime + seconds; + seek(newTime); + }, [seek]); + + /** + * Set playback rate + */ + const setPlaybackRate = useCallback((rate: number) => { + const video = videoRef.current; + if (!video) return; + + video.playbackRate = Math.max(0.25, Math.min(4, rate)); + }, []); + + /** + * Reset video to beginning + */ + const reset = useCallback(() => { + const video = videoRef.current; + if (!video) return; + + video.currentTime = 0; + pause(); + }, [pause]); + + // Event handlers + useEffect(() => { + const video = videoRef.current; + if (!video) return; + + const handleLoadStart = () => { + updateState({ isLoading: true, error: null }); + }; + + const handleLoadedData = () => { + updateState({ isLoading: false }); + }; + + const handleTimeUpdate = () => { + updateState({ currentTime: video.currentTime }); + onTimeUpdate?.(video.currentTime); + }; + + const handleDurationChange = () => { + updateState({ duration: video.duration }); + onDurationChange?.(video.duration); + }; + + const handlePlay = () => { + updateState({ isPlaying: true }); + }; + + const handlePause = () => { + updateState({ isPlaying: false }); + }; + + const handleEnded = () => { + updateState({ isPlaying: false }); + onEnded?.(); + }; + + const handleError = () => { + const errorMessage = video.error?.message || 'Video playback error'; + updateState({ isLoading: false, error: errorMessage, isPlaying: false }); + onError?.(errorMessage); + }; + + const handleVolumeChange = () => { + updateState({ + volume: video.volume, + isMuted: video.muted + }); + }; + + const handleFullscreenChange = () => { + updateState({ isFullscreen: !!document.fullscreenElement }); + }; + + // Add event listeners + video.addEventListener('loadstart', handleLoadStart); + video.addEventListener('loadeddata', handleLoadedData); + video.addEventListener('timeupdate', handleTimeUpdate); + video.addEventListener('durationchange', handleDurationChange); + video.addEventListener('play', handlePlay); + video.addEventListener('pause', handlePause); + video.addEventListener('ended', handleEnded); + video.addEventListener('error', handleError); + video.addEventListener('volumechange', handleVolumeChange); + document.addEventListener('fullscreenchange', handleFullscreenChange); + + // Set initial properties + video.autoplay = autoPlay; + video.loop = loop; + video.muted = muted; + video.volume = volume; + + // Cleanup + return () => { + video.removeEventListener('loadstart', handleLoadStart); + video.removeEventListener('loadeddata', handleLoadedData); + video.removeEventListener('timeupdate', handleTimeUpdate); + video.removeEventListener('durationchange', handleDurationChange); + video.removeEventListener('play', handlePlay); + video.removeEventListener('pause', handlePause); + video.removeEventListener('ended', handleEnded); + video.removeEventListener('error', handleError); + video.removeEventListener('volumechange', handleVolumeChange); + document.removeEventListener('fullscreenchange', handleFullscreenChange); + }; + }, [autoPlay, loop, muted, volume, updateState, onTimeUpdate, onDurationChange, onEnded, onError]); + + return { + state, + actions: { + play, + pause, + togglePlay, + seek, + setVolume, + toggleMute, + toggleFullscreen, + skip, + setPlaybackRate, + reset, + }, + ref: videoRef, + }; +} diff --git a/src/features/video-streaming/index.ts b/src/features/video-streaming/index.ts new file mode 100644 index 0000000..b0490d5 --- /dev/null +++ b/src/features/video-streaming/index.ts @@ -0,0 +1,24 @@ +/** + * Video Streaming Feature - Main Export + * + * This is the main entry point for the video streaming feature. + * It exports all the public APIs that other parts of the application can use. + */ + +// Components +export * from './components'; + +// Hooks +export * from './hooks'; + +// Services +export { videoApiService, VideoApiService } from './services/videoApi'; + +// Types +export * from './types'; + +// Utils +export * from './utils/videoUtils'; + +// Main feature component +export { VideoStreamingPage } from './VideoStreamingPage'; diff --git a/src/features/video-streaming/services/videoApi.ts b/src/features/video-streaming/services/videoApi.ts new file mode 100644 index 0000000..f88bac7 --- /dev/null +++ b/src/features/video-streaming/services/videoApi.ts @@ -0,0 +1,232 @@ +/** + * Video Streaming API Service + * + * This service handles all API interactions for the video streaming feature. + * It provides a clean interface for components to interact with the video API + * without knowing the implementation details. + */ + +import { + type VideoListResponse, + type VideoInfoResponse, + type VideoStreamingInfo, + type VideoListParams, + type ThumbnailParams, +} from '../types'; + +// Configuration +const API_BASE_URL = 'http://vision:8000'; // Based on the test script + +/** + * Custom error class for video API errors + */ +export class VideoApiError extends Error { + public code: string; + public details?: unknown; + + constructor( + code: string, + message: string, + details?: unknown + ) { + super(message); + this.name = 'VideoApiError'; + this.code = code; + this.details = details; + } +} + +/** + * Helper function to handle API responses + */ +async function handleApiResponse(response: Response): Promise { + if (!response.ok) { + const errorText = await response.text(); + throw new VideoApiError( + `HTTP_${response.status}`, + `API request failed: ${response.statusText}`, + { status: response.status, body: errorText } + ); + } + + const contentType = response.headers.get('content-type'); + if (contentType && contentType.includes('application/json')) { + return response.json(); + } + + throw new VideoApiError( + 'INVALID_RESPONSE', + 'Expected JSON response from API' + ); +} + +/** + * Build query string from parameters + */ +function buildQueryString(params: VideoListParams | ThumbnailParams): string { + const searchParams = new URLSearchParams(); + + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + searchParams.append(key, String(value)); + } + }); + + return searchParams.toString(); +} + +/** + * Video API Service Class + */ +export class VideoApiService { + private baseUrl: string; + + constructor(baseUrl: string = API_BASE_URL) { + this.baseUrl = baseUrl; + } + + /** + * Get list of videos with optional filtering + */ + async getVideos(params: VideoListParams = {}): Promise { + try { + const queryString = buildQueryString(params); + const url = `${this.baseUrl}/videos/${queryString ? `?${queryString}` : ''}`; + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Accept': 'application/json', + }, + }); + + return await handleApiResponse(response); + } catch (error) { + if (error instanceof VideoApiError) { + throw error; + } + throw new VideoApiError( + 'NETWORK_ERROR', + 'Failed to fetch videos', + { originalError: error } + ); + } + } + + /** + * Get detailed information about a specific video + */ + async getVideoInfo(fileId: string): Promise { + try { + const response = await fetch(`${this.baseUrl}/videos/${fileId}`, { + method: 'GET', + headers: { + 'Accept': 'application/json', + }, + }); + + return await handleApiResponse(response); + } catch (error) { + if (error instanceof VideoApiError) { + throw error; + } + throw new VideoApiError( + 'NETWORK_ERROR', + `Failed to fetch video info for ${fileId}`, + { originalError: error, fileId } + ); + } + } + + /** + * Get streaming information for a video + */ + async getStreamingInfo(fileId: string): Promise { + try { + const response = await fetch(`${this.baseUrl}/videos/${fileId}/info`, { + method: 'GET', + headers: { + 'Accept': 'application/json', + }, + }); + + return await handleApiResponse(response); + } catch (error) { + if (error instanceof VideoApiError) { + throw error; + } + throw new VideoApiError( + 'NETWORK_ERROR', + `Failed to fetch streaming info for ${fileId}`, + { originalError: error, fileId } + ); + } + } + + /** + * Get the streaming URL for a video + */ + getStreamingUrl(fileId: string): string { + return `${this.baseUrl}/videos/${fileId}/stream`; + } + + /** + * Get the thumbnail URL for a video + */ + getThumbnailUrl(fileId: string, params: ThumbnailParams = {}): string { + const queryString = buildQueryString(params); + return `${this.baseUrl}/videos/${fileId}/thumbnail${queryString ? `?${queryString}` : ''}`; + } + + /** + * Download thumbnail as blob + */ + async getThumbnailBlob(fileId: string, params: ThumbnailParams = {}): Promise { + try { + const url = this.getThumbnailUrl(fileId, params); + const response = await fetch(url); + + if (!response.ok) { + throw new VideoApiError( + `HTTP_${response.status}`, + `Failed to fetch thumbnail: ${response.statusText}`, + { status: response.status, fileId } + ); + } + + return await response.blob(); + } catch (error) { + if (error instanceof VideoApiError) { + throw error; + } + throw new VideoApiError( + 'NETWORK_ERROR', + `Failed to fetch thumbnail for ${fileId}`, + { originalError: error, fileId } + ); + } + } + + /** + * Check if the video API is available + */ + async healthCheck(): Promise { + try { + const response = await fetch(`${this.baseUrl}/videos/`, { + method: 'GET', + headers: { + 'Accept': 'application/json', + }, + }); + return response.ok; + } catch { + return false; + } + } +} + +// Export a default instance +export const videoApiService = new VideoApiService(); + +// Export utility functions +export { buildQueryString, handleApiResponse }; diff --git a/src/features/video-streaming/types/index.ts b/src/features/video-streaming/types/index.ts new file mode 100644 index 0000000..1a1f0ea --- /dev/null +++ b/src/features/video-streaming/types/index.ts @@ -0,0 +1,146 @@ +/** + * Video Streaming Feature Types + * + * This file contains all TypeScript type definitions for the video streaming feature. + * Following the modular architecture pattern where types are centralized and reusable. + * Updated to fix import issues. + */ + +// Base video information from the API +export interface VideoFile { + file_id: string; + camera_name: string; + filename: string; + file_size_bytes: number; + format: string; + status: 'completed' | 'processing' | 'failed'; + created_at: string; + is_streamable: boolean; + needs_conversion: boolean; +} + +// Extended video information with metadata +export interface VideoWithMetadata extends VideoFile { + metadata?: { + duration_seconds: number; + width: number; + height: number; + fps: number; + codec: string; + aspect_ratio: number; + }; +} + +// API response for video list +export interface VideoListResponse { + videos: VideoFile[]; + total_count: number; +} + +// API response for video info +export interface VideoInfoResponse { + file_id: string; + metadata: { + duration_seconds: number; + width: number; + height: number; + fps: number; + codec: string; + aspect_ratio: number; + }; +} + +// Streaming technical information +export interface VideoStreamingInfo { + file_id: string; + file_size_bytes: number; + content_type: string; + supports_range_requests: boolean; + chunk_size_bytes: number; +} + +// Query parameters for video list API +export interface VideoListParams { + camera_name?: string; + start_date?: string; + end_date?: string; + limit?: number; + include_metadata?: boolean; +} + +// Thumbnail request parameters +export interface ThumbnailParams { + timestamp?: number; + width?: number; + height?: number; +} + +// Video player state is now defined in useVideoPlayer hook to avoid circular imports + +// Video list filter and sort options +export interface VideoListFilters { + cameraName?: string; + dateRange?: { + start: string; + end: string; + }; + status?: VideoFile['status']; + format?: string; +} + +export interface VideoListSortOptions { + field: 'created_at' | 'file_size_bytes' | 'camera_name' | 'filename'; + direction: 'asc' | 'desc'; +} + +// Component props interfaces +export interface VideoPlayerProps { + fileId: string; + autoPlay?: boolean; + controls?: boolean; + width?: string | number; + height?: string | number; + className?: string; + onPlay?: () => void; + onPause?: () => void; + onEnded?: () => void; + onError?: (error: string) => void; +} + +export interface VideoCardProps { + video: VideoFile; + onClick?: (video: VideoFile) => void; + showMetadata?: boolean; + className?: string; +} + +export interface VideoListProps { + filters?: VideoListFilters; + sortOptions?: VideoListSortOptions; + limit?: number; + onVideoSelect?: (video: VideoFile) => void; + className?: string; +} + +export interface VideoThumbnailProps { + fileId: string; + timestamp?: number; + width?: number; + height?: number; + alt?: string; + className?: string; + onClick?: () => void; +} + +// Error types +export interface VideoError { + code: string; + message: string; + details?: any; +} + +// Loading states +export type LoadingState = 'idle' | 'loading' | 'success' | 'error'; + +// Hook return types are exported from their respective hook files +// This avoids circular import issues diff --git a/src/features/video-streaming/utils/videoUtils.ts b/src/features/video-streaming/utils/videoUtils.ts new file mode 100644 index 0000000..aa58990 --- /dev/null +++ b/src/features/video-streaming/utils/videoUtils.ts @@ -0,0 +1,282 @@ +/** + * Video Streaming Utilities + * + * Pure utility functions for video operations, formatting, and data processing. + * These functions have no side effects and can be easily tested. + */ + +import { type VideoFile, type VideoWithMetadata } from '../types'; + +/** + * Format file size in bytes to human readable format + */ +export function formatFileSize(bytes: number): string { + if (bytes === 0) return '0 B'; + + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; +} + +/** + * Format duration in seconds to human readable format (HH:MM:SS or MM:SS) + */ +export function formatDuration(seconds: number): string { + if (isNaN(seconds) || seconds < 0) return '00:00'; + + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = Math.floor(seconds % 60); + + if (hours > 0) { + return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + } + + return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; +} + +/** + * Format date string to human readable format + */ +export function formatVideoDate(dateString: string): string { + try { + const date = new Date(dateString); + return date.toLocaleString(); + } catch { + return dateString; + } +} + +/** + * Get relative time string (e.g., "2 hours ago") + */ +export function getRelativeTime(dateString: string): string { + try { + const date = new Date(dateString); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMinutes = Math.floor(diffMs / (1000 * 60)); + const diffHours = Math.floor(diffMinutes / 60); + const diffDays = Math.floor(diffHours / 24); + + if (diffMinutes < 1) return 'Just now'; + if (diffMinutes < 60) return `${diffMinutes} minute${diffMinutes === 1 ? '' : 's'} ago`; + if (diffHours < 24) return `${diffHours} hour${diffHours === 1 ? '' : 's'} ago`; + if (diffDays < 7) return `${diffDays} day${diffDays === 1 ? '' : 's'} ago`; + + return formatVideoDate(dateString); + } catch { + return dateString; + } +} + +/** + * Extract camera name from filename if not provided + */ +export function extractCameraName(filename: string): string { + // Try to extract camera name from filename pattern like "camera1_recording_20250804_143022.avi" + const match = filename.match(/^([^_]+)_/); + return match ? match[1] : 'Unknown'; +} + +/** + * Get video format display name + */ +export function getFormatDisplayName(format: string): string { + const formatMap: Record = { + 'avi': 'AVI', + 'mp4': 'MP4', + 'webm': 'WebM', + 'mov': 'MOV', + 'mkv': 'MKV', + }; + + return formatMap[format.toLowerCase()] || format.toUpperCase(); +} + +/** + * Check if video format is web-compatible + */ +export function isWebCompatible(format: string): boolean { + const webFormats = ['mp4', 'webm', 'ogg']; + return webFormats.includes(format.toLowerCase()); +} + +/** + * Get status badge color class + */ +export function getStatusBadgeClass(status: VideoFile['status']): string { + const statusClasses = { + 'completed': 'bg-green-100 text-green-800', + 'processing': 'bg-yellow-100 text-yellow-800', + 'failed': 'bg-red-100 text-red-800', + }; + + return statusClasses[status] || 'bg-gray-100 text-gray-800'; +} + +/** + * Get video resolution display string + */ +export function getResolutionString(width?: number, height?: number): string { + if (!width || !height) return 'Unknown'; + + // Common resolution names + const resolutions: Record = { + '1920x1080': '1080p', + '1280x720': '720p', + '854x480': '480p', + '640x360': '360p', + '426x240': '240p', + }; + + const key = `${width}x${height}`; + return resolutions[key] || `${width}ร—${height}`; +} + +/** + * Calculate aspect ratio string + */ +export function getAspectRatioString(aspectRatio: number): string { + if (!aspectRatio || aspectRatio <= 0) return 'Unknown'; + + // Common aspect ratios + const ratios: Array<[number, string]> = [ + [16/9, '16:9'], + [4/3, '4:3'], + [21/9, '21:9'], + [1, '1:1'], + ]; + + // Find closest match (within 0.1 tolerance) + for (const [ratio, display] of ratios) { + if (Math.abs(aspectRatio - ratio) < 0.1) { + return display; + } + } + + // Return calculated ratio + const gcd = (a: number, b: number): number => b === 0 ? a : gcd(b, a % b); + const width = Math.round(aspectRatio * 100); + const height = 100; + const divisor = gcd(width, height); + + return `${width / divisor}:${height / divisor}`; +} + +/** + * Sort videos by different criteria + */ +export function sortVideos( + videos: VideoFile[], + field: 'created_at' | 'file_size_bytes' | 'camera_name' | 'filename', + direction: 'asc' | 'desc' = 'desc' +): VideoFile[] { + return [...videos].sort((a, b) => { + let aValue: any = a[field]; + let bValue: any = b[field]; + + // Handle date strings + if (field === 'created_at') { + aValue = new Date(aValue).getTime(); + bValue = new Date(bValue).getTime(); + } + + // Handle string comparison + if (typeof aValue === 'string' && typeof bValue === 'string') { + aValue = aValue.toLowerCase(); + bValue = bValue.toLowerCase(); + } + + let result = 0; + if (aValue < bValue) result = -1; + else if (aValue > bValue) result = 1; + + return direction === 'desc' ? -result : result; + }); +} + +/** + * Filter videos by criteria + */ +export function filterVideos( + videos: VideoFile[], + filters: { + cameraName?: string; + status?: VideoFile['status']; + format?: string; + dateRange?: { start: string; end: string }; + } +): VideoFile[] { + return videos.filter(video => { + // Filter by camera name + if (filters.cameraName && video.camera_name !== filters.cameraName) { + return false; + } + + // Filter by status + if (filters.status && video.status !== filters.status) { + return false; + } + + // Filter by format + if (filters.format && video.format !== filters.format) { + return false; + } + + // Filter by date range + if (filters.dateRange) { + const videoDate = new Date(video.created_at); + const startDate = new Date(filters.dateRange.start); + const endDate = new Date(filters.dateRange.end); + + if (videoDate < startDate || videoDate > endDate) { + return false; + } + } + + return true; + }); +} + +/** + * Generate a unique key for video caching + */ +export function generateVideoKey(fileId: string, params?: Record): string { + if (!params || Object.keys(params).length === 0) { + return fileId; + } + + const sortedParams = Object.keys(params) + .sort() + .map(key => `${key}=${params[key]}`) + .join('&'); + + return `${fileId}?${sortedParams}`; +} + +/** + * Validate video file ID format + */ +export function isValidFileId(fileId: string): boolean { + // Basic validation - adjust based on your file ID format + return typeof fileId === 'string' && fileId.length > 0 && !fileId.includes('/'); +} + +/** + * Get video thumbnail timestamp suggestions + */ +export function getThumbnailTimestamps(duration: number): number[] { + if (duration <= 0) return [0]; + + // Generate timestamps at 10%, 25%, 50%, 75%, 90% of video duration + return [ + Math.floor(duration * 0.1), + Math.floor(duration * 0.25), + Math.floor(duration * 0.5), + Math.floor(duration * 0.75), + Math.floor(duration * 0.9), + ].filter(t => t >= 0 && t < duration); +} diff --git a/src/lib/visionApi.ts b/src/lib/visionApi.ts index 0589270..6b87cb4 100644 --- a/src/lib/visionApi.ts +++ b/src/lib/visionApi.ts @@ -1,6 +1,6 @@ // Vision System API Client // Base URL for the vision system API -const VISION_API_BASE_URL = 'http://localhost:8000' +const VISION_API_BASE_URL = 'http://vision:8000' // Types based on the API documentation export interface SystemStatus { diff --git a/test-stop-streaming.html b/test-stop-streaming.html index 4607398..f6f3400 100644 --- a/test-stop-streaming.html +++ b/test-stop-streaming.html @@ -1,5 +1,6 @@ + @@ -64,7 +65,8 @@ background-color: #c82333; } - input, select { + input, + select { padding: 8px; margin: 5px; border: 1px solid #ddd; @@ -72,13 +74,14 @@ } +

๐Ÿ›‘ Stop Streaming API Test

- +

Test Stop Streaming Endpoint

This test verifies that the stop streaming API endpoint works correctly.

- +
- +

Manual API Test

Test the API endpoint directly:

- +
- +
- + + \ No newline at end of file