feat(video-streaming): add ApiStatusIndicator, PerformanceDashboard, VideoDebugger, and VideoErrorBoundary components
- Implemented ApiStatusIndicator to monitor video API connection status with health check functionality. - Created PerformanceDashboard for monitoring video streaming performance metrics in development mode. - Developed VideoDebugger for diagnosing video streaming issues with direct access to test video URLs. - Added VideoErrorBoundary to handle errors in video streaming components with user-friendly messages and recovery options. - Introduced utility functions for performance monitoring and thumbnail caching to optimize video streaming operations. - Added comprehensive tests for video streaming API connectivity and functionality.
This commit is contained in:
14
.env.example
Normal file
14
.env.example
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Environment Configuration for Pecan Experiments Application
|
||||||
|
|
||||||
|
# USDA Vision Camera System API Configuration
|
||||||
|
# Default: http://vision:8000 (current working setup)
|
||||||
|
# For localhost setup, use: http://localhost:8000
|
||||||
|
# For remote systems, use: http://192.168.1.100:8000 (replace with actual IP)
|
||||||
|
VITE_VISION_API_URL=http://vision:8000
|
||||||
|
|
||||||
|
# Supabase Configuration (if needed for production)
|
||||||
|
# VITE_SUPABASE_URL=your_supabase_url
|
||||||
|
# VITE_SUPABASE_ANON_KEY=your_supabase_anon_key
|
||||||
|
|
||||||
|
# Development Configuration
|
||||||
|
# VITE_DEV_MODE=true
|
||||||
415
API Documentations/docs/AI_AGENT_VIDEO_INTEGRATION_GUIDE.md
Normal file
415
API Documentations/docs/AI_AGENT_VIDEO_INTEGRATION_GUIDE.md
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
# 🤖 AI Agent Video Integration Guide
|
||||||
|
|
||||||
|
This guide provides comprehensive step-by-step instructions for AI agents and external systems to successfully integrate with the USDA Vision Camera System's video streaming functionality.
|
||||||
|
|
||||||
|
## 🎯 Overview
|
||||||
|
|
||||||
|
The USDA Vision Camera System provides a complete video streaming API that allows AI agents to:
|
||||||
|
- Browse and select videos from multiple cameras
|
||||||
|
- Stream videos with seeking capabilities
|
||||||
|
- Generate thumbnails for preview
|
||||||
|
- Access video metadata and technical information
|
||||||
|
|
||||||
|
## 🔗 API Base Configuration
|
||||||
|
|
||||||
|
### Connection Details
|
||||||
|
```bash
|
||||||
|
# Default API Base URL
|
||||||
|
API_BASE_URL="http://localhost:8000"
|
||||||
|
|
||||||
|
# For remote access, replace with actual server IP/hostname
|
||||||
|
API_BASE_URL="http://192.168.1.100:8000"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
**⚠️ IMPORTANT: No authentication is currently required.**
|
||||||
|
- All endpoints are publicly accessible
|
||||||
|
- No API keys or tokens needed
|
||||||
|
- CORS is enabled for web browser integration
|
||||||
|
|
||||||
|
## 📋 Step-by-Step Integration Workflow
|
||||||
|
|
||||||
|
### Step 1: Verify System Connectivity
|
||||||
|
```bash
|
||||||
|
# Test basic connectivity
|
||||||
|
curl -f "${API_BASE_URL}/health" || echo "❌ System not accessible"
|
||||||
|
|
||||||
|
# Check system status
|
||||||
|
curl "${API_BASE_URL}/system/status"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "healthy",
|
||||||
|
"timestamp": "2025-08-05T10:30:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: List Available Videos
|
||||||
|
```bash
|
||||||
|
# Get all videos with metadata
|
||||||
|
curl "${API_BASE_URL}/videos/?include_metadata=true&limit=50"
|
||||||
|
|
||||||
|
# Filter by specific camera
|
||||||
|
curl "${API_BASE_URL}/videos/?camera_name=camera1&include_metadata=true"
|
||||||
|
|
||||||
|
# Filter by date range
|
||||||
|
curl "${API_BASE_URL}/videos/?start_date=2025-08-04T00:00:00&end_date=2025-08-05T23:59:59"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response Structure:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"videos": [
|
||||||
|
{
|
||||||
|
"file_id": "camera1_auto_blower_separator_20250804_143022.mp4",
|
||||||
|
"camera_name": "camera1",
|
||||||
|
"filename": "camera1_auto_blower_separator_20250804_143022.mp4",
|
||||||
|
"file_size_bytes": 31457280,
|
||||||
|
"format": "mp4",
|
||||||
|
"status": "completed",
|
||||||
|
"created_at": "2025-08-04T14:30:22",
|
||||||
|
"start_time": "2025-08-04T14:30:22",
|
||||||
|
"end_time": "2025-08-04T14:32:22",
|
||||||
|
"machine_trigger": "blower_separator",
|
||||||
|
"is_streamable": true,
|
||||||
|
"needs_conversion": false,
|
||||||
|
"metadata": {
|
||||||
|
"duration_seconds": 120.5,
|
||||||
|
"width": 1920,
|
||||||
|
"height": 1080,
|
||||||
|
"fps": 30.0,
|
||||||
|
"codec": "mp4v",
|
||||||
|
"bitrate": 5000000,
|
||||||
|
"aspect_ratio": 1.777
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total_count": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Select and Validate Video
|
||||||
|
```bash
|
||||||
|
# Get detailed video information
|
||||||
|
FILE_ID="camera1_auto_blower_separator_20250804_143022.mp4"
|
||||||
|
curl "${API_BASE_URL}/videos/${FILE_ID}"
|
||||||
|
|
||||||
|
# Validate video is playable
|
||||||
|
curl -X POST "${API_BASE_URL}/videos/${FILE_ID}/validate"
|
||||||
|
|
||||||
|
# Get streaming technical details
|
||||||
|
curl "${API_BASE_URL}/videos/${FILE_ID}/info"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Generate Video Thumbnail
|
||||||
|
```bash
|
||||||
|
# Generate thumbnail at 5 seconds, 320x240 resolution
|
||||||
|
curl "${API_BASE_URL}/videos/${FILE_ID}/thumbnail?timestamp=5.0&width=320&height=240" \
|
||||||
|
--output "thumbnail_${FILE_ID}.jpg"
|
||||||
|
|
||||||
|
# Generate multiple thumbnails for preview
|
||||||
|
for timestamp in 1 30 60 90; do
|
||||||
|
curl "${API_BASE_URL}/videos/${FILE_ID}/thumbnail?timestamp=${timestamp}&width=160&height=120" \
|
||||||
|
--output "preview_${timestamp}s.jpg"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Stream Video Content
|
||||||
|
```bash
|
||||||
|
# Stream entire video
|
||||||
|
curl "${API_BASE_URL}/videos/${FILE_ID}/stream" --output "video.mp4"
|
||||||
|
|
||||||
|
# Stream specific byte range (for seeking)
|
||||||
|
curl -H "Range: bytes=0-1048575" \
|
||||||
|
"${API_BASE_URL}/videos/${FILE_ID}/stream" \
|
||||||
|
--output "video_chunk.mp4"
|
||||||
|
|
||||||
|
# Test range request support
|
||||||
|
curl -I -H "Range: bytes=0-1023" \
|
||||||
|
"${API_BASE_URL}/videos/${FILE_ID}/stream"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Programming Language Examples
|
||||||
|
|
||||||
|
### Python Integration
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
from typing import List, Dict, Optional
|
||||||
|
|
||||||
|
class USDAVideoClient:
|
||||||
|
def __init__(self, base_url: str = "http://localhost:8000"):
|
||||||
|
self.base_url = base_url.rstrip('/')
|
||||||
|
self.session = requests.Session()
|
||||||
|
|
||||||
|
def list_videos(self, camera_name: Optional[str] = None,
|
||||||
|
include_metadata: bool = True, limit: int = 50) -> Dict:
|
||||||
|
"""List available videos with optional filtering."""
|
||||||
|
params = {
|
||||||
|
'include_metadata': include_metadata,
|
||||||
|
'limit': limit
|
||||||
|
}
|
||||||
|
if camera_name:
|
||||||
|
params['camera_name'] = camera_name
|
||||||
|
|
||||||
|
response = self.session.get(f"{self.base_url}/videos/", params=params)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def get_video_info(self, file_id: str) -> Dict:
|
||||||
|
"""Get detailed video information."""
|
||||||
|
response = self.session.get(f"{self.base_url}/videos/{file_id}")
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def get_thumbnail(self, file_id: str, timestamp: float = 1.0,
|
||||||
|
width: int = 320, height: int = 240) -> bytes:
|
||||||
|
"""Generate and download video thumbnail."""
|
||||||
|
params = {
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'width': width,
|
||||||
|
'height': height
|
||||||
|
}
|
||||||
|
response = self.session.get(
|
||||||
|
f"{self.base_url}/videos/{file_id}/thumbnail",
|
||||||
|
params=params
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.content
|
||||||
|
|
||||||
|
def stream_video_range(self, file_id: str, start_byte: int,
|
||||||
|
end_byte: int) -> bytes:
|
||||||
|
"""Stream specific byte range of video."""
|
||||||
|
headers = {'Range': f'bytes={start_byte}-{end_byte}'}
|
||||||
|
response = self.session.get(
|
||||||
|
f"{self.base_url}/videos/{file_id}/stream",
|
||||||
|
headers=headers
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.content
|
||||||
|
|
||||||
|
def validate_video(self, file_id: str) -> bool:
|
||||||
|
"""Validate that video is accessible and playable."""
|
||||||
|
response = self.session.post(f"{self.base_url}/videos/{file_id}/validate")
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json().get('is_valid', False)
|
||||||
|
|
||||||
|
# Usage example
|
||||||
|
client = USDAVideoClient("http://192.168.1.100:8000")
|
||||||
|
|
||||||
|
# List videos from camera1
|
||||||
|
videos = client.list_videos(camera_name="camera1")
|
||||||
|
print(f"Found {videos['total_count']} videos")
|
||||||
|
|
||||||
|
# Select first video
|
||||||
|
if videos['videos']:
|
||||||
|
video = videos['videos'][0]
|
||||||
|
file_id = video['file_id']
|
||||||
|
|
||||||
|
# Validate video
|
||||||
|
if client.validate_video(file_id):
|
||||||
|
print(f"✅ Video {file_id} is valid")
|
||||||
|
|
||||||
|
# Get thumbnail
|
||||||
|
thumbnail = client.get_thumbnail(file_id, timestamp=5.0)
|
||||||
|
with open(f"thumbnail_{file_id}.jpg", "wb") as f:
|
||||||
|
f.write(thumbnail)
|
||||||
|
|
||||||
|
# Stream first 1MB
|
||||||
|
chunk = client.stream_video_range(file_id, 0, 1048575)
|
||||||
|
print(f"Downloaded {len(chunk)} bytes")
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript/Node.js Integration
|
||||||
|
```javascript
|
||||||
|
class USDAVideoClient {
|
||||||
|
constructor(baseUrl = 'http://localhost:8000') {
|
||||||
|
this.baseUrl = baseUrl.replace(/\/$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
async listVideos(options = {}) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
include_metadata: options.includeMetadata || true,
|
||||||
|
limit: options.limit || 50
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.cameraName) {
|
||||||
|
params.append('camera_name', options.cameraName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${this.baseUrl}/videos/?${params}`);
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getVideoInfo(fileId) {
|
||||||
|
const response = await fetch(`${this.baseUrl}/videos/${fileId}`);
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getThumbnail(fileId, options = {}) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
timestamp: options.timestamp || 1.0,
|
||||||
|
width: options.width || 320,
|
||||||
|
height: options.height || 240
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.baseUrl}/videos/${fileId}/thumbnail?${params}`
|
||||||
|
);
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
return response.blob();
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateVideo(fileId) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.baseUrl}/videos/${fileId}/validate`,
|
||||||
|
{ method: 'POST' }
|
||||||
|
);
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
const result = await response.json();
|
||||||
|
return result.is_valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStreamUrl(fileId) {
|
||||||
|
return `${this.baseUrl}/videos/${fileId}/stream`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage example
|
||||||
|
const client = new USDAVideoClient('http://192.168.1.100:8000');
|
||||||
|
|
||||||
|
async function integrateWithVideos() {
|
||||||
|
try {
|
||||||
|
// List videos
|
||||||
|
const videos = await client.listVideos({ cameraName: 'camera1' });
|
||||||
|
console.log(`Found ${videos.total_count} videos`);
|
||||||
|
|
||||||
|
if (videos.videos.length > 0) {
|
||||||
|
const video = videos.videos[0];
|
||||||
|
const fileId = video.file_id;
|
||||||
|
|
||||||
|
// Validate video
|
||||||
|
const isValid = await client.validateVideo(fileId);
|
||||||
|
if (isValid) {
|
||||||
|
console.log(`✅ Video ${fileId} is valid`);
|
||||||
|
|
||||||
|
// Get thumbnail
|
||||||
|
const thumbnail = await client.getThumbnail(fileId, {
|
||||||
|
timestamp: 5.0,
|
||||||
|
width: 320,
|
||||||
|
height: 240
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create video element for playback
|
||||||
|
const videoElement = document.createElement('video');
|
||||||
|
videoElement.controls = true;
|
||||||
|
videoElement.src = client.getStreamUrl(fileId);
|
||||||
|
document.body.appendChild(videoElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Integration error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚨 Error Handling
|
||||||
|
|
||||||
|
### Common HTTP Status Codes
|
||||||
|
```bash
|
||||||
|
# Success responses
|
||||||
|
200 # OK - Request successful
|
||||||
|
206 # Partial Content - Range request successful
|
||||||
|
|
||||||
|
# Client error responses
|
||||||
|
400 # Bad Request - Invalid parameters
|
||||||
|
404 # Not Found - Video file doesn't exist
|
||||||
|
416 # Range Not Satisfiable - Invalid range request
|
||||||
|
|
||||||
|
# Server error responses
|
||||||
|
500 # Internal Server Error - Failed to process video
|
||||||
|
503 # Service Unavailable - Video module not available
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Response Format
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"detail": "Video camera1_recording_20250804_143022.avi not found"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Robust Error Handling Example
|
||||||
|
```python
|
||||||
|
def safe_video_operation(client, file_id):
|
||||||
|
try:
|
||||||
|
# Validate video first
|
||||||
|
if not client.validate_video(file_id):
|
||||||
|
return {"error": "Video is not valid or accessible"}
|
||||||
|
|
||||||
|
# Get video info
|
||||||
|
video_info = client.get_video_info(file_id)
|
||||||
|
|
||||||
|
# Check if streamable
|
||||||
|
if not video_info.get('is_streamable', False):
|
||||||
|
return {"error": "Video is not streamable"}
|
||||||
|
|
||||||
|
return {"success": True, "video_info": video_info}
|
||||||
|
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
if e.response.status_code == 404:
|
||||||
|
return {"error": "Video not found"}
|
||||||
|
elif e.response.status_code == 416:
|
||||||
|
return {"error": "Invalid range request"}
|
||||||
|
else:
|
||||||
|
return {"error": f"HTTP error: {e.response.status_code}"}
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
return {"error": "Cannot connect to video server"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": f"Unexpected error: {str(e)}"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Integration Checklist
|
||||||
|
|
||||||
|
### Pre-Integration
|
||||||
|
- [ ] Verify network connectivity to USDA Vision Camera System
|
||||||
|
- [ ] Test basic API endpoints (`/health`, `/system/status`)
|
||||||
|
- [ ] Understand video file naming conventions
|
||||||
|
- [ ] Plan error handling strategy
|
||||||
|
|
||||||
|
### Video Selection
|
||||||
|
- [ ] Implement video listing with appropriate filters
|
||||||
|
- [ ] Add video validation before processing
|
||||||
|
- [ ] Handle pagination for large video collections
|
||||||
|
- [ ] Implement caching for video metadata
|
||||||
|
|
||||||
|
### Video Playback
|
||||||
|
- [ ] Test video streaming with range requests
|
||||||
|
- [ ] Implement thumbnail generation for previews
|
||||||
|
- [ ] Add progress tracking for video playback
|
||||||
|
- [ ] Handle different video formats (MP4, AVI)
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- [ ] Handle network connectivity issues
|
||||||
|
- [ ] Manage video not found scenarios
|
||||||
|
- [ ] Deal with invalid range requests
|
||||||
|
- [ ] Implement retry logic for transient failures
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- [ ] Use range requests for efficient seeking
|
||||||
|
- [ ] Implement client-side caching where appropriate
|
||||||
|
- [ ] Monitor bandwidth usage for video streaming
|
||||||
|
- [ ] Consider thumbnail caching for better UX
|
||||||
|
|
||||||
|
## 🎯 Next Steps
|
||||||
|
|
||||||
|
1. **Test Integration**: Use the provided examples to test basic connectivity
|
||||||
|
2. **Implement Error Handling**: Add robust error handling for production use
|
||||||
|
3. **Optimize Performance**: Implement caching and efficient streaming
|
||||||
|
4. **Monitor Usage**: Track API usage and performance metrics
|
||||||
|
5. **Security Review**: Consider authentication if exposing externally
|
||||||
|
|
||||||
|
This guide provides everything needed for successful integration with the USDA Vision Camera System's video streaming functionality. The system is designed to be simple and reliable for AI agents and external systems to consume video content efficiently.
|
||||||
@@ -1,32 +1,27 @@
|
|||||||
# API Changes Summary: Camera Settings and Video Format Updates
|
# API Changes Summary: Camera Settings and Video Format Updates
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
This document tracks major API changes including camera settings enhancements and the MP4 video format update.
|
This document tracks major API changes including camera settings enhancements and the MP4 video format update.
|
||||||
|
|
||||||
## 🎥 Latest Update: MP4 Video Format (v2.1)
|
## 🎥 Latest Update: MP4 Video Format (v2.1)
|
||||||
|
|
||||||
**Date**: August 2025
|
**Date**: August 2025
|
||||||
|
|
||||||
**Major Changes**:
|
**Major Changes**:
|
||||||
|
- **Video Format**: Changed from AVI/XVID to MP4/MPEG-4 format
|
||||||
- **Video Format**: Changed from AVI/XVID to MP4/H.264 format
|
|
||||||
- **File Extensions**: New recordings use `.mp4` instead of `.avi`
|
- **File Extensions**: New recordings use `.mp4` instead of `.avi`
|
||||||
- **File Size**: ~40% reduction in file sizes
|
- **File Size**: ~40% reduction in file sizes
|
||||||
- **Streaming**: Better web browser compatibility
|
- **Streaming**: Better web browser compatibility
|
||||||
|
|
||||||
**New Configuration Fields**:
|
**New Configuration Fields**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"video_format": "mp4", // File format: "mp4" or "avi"
|
"video_format": "mp4", // File format: "mp4" or "avi"
|
||||||
"video_codec": "h264", // Video codec: "h264", "mp4v", "XVID", "MJPG"
|
"video_codec": "mp4v", // Video codec: "mp4v", "XVID", "MJPG"
|
||||||
"video_quality": 95 // Quality: 0-100 (higher = better)
|
"video_quality": 95 // Quality: 0-100 (higher = better)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Frontend Impact**:
|
**Frontend Impact**:
|
||||||
|
|
||||||
- ✅ Better streaming performance and browser support
|
- ✅ Better streaming performance and browser support
|
||||||
- ✅ Smaller file sizes for faster transfers
|
- ✅ Smaller file sizes for faster transfers
|
||||||
- ✅ Universal HTML5 video player compatibility
|
- ✅ Universal HTML5 video player compatibility
|
||||||
@@ -43,14 +38,12 @@ Enhanced the `POST /cameras/{camera_name}/start-recording` API endpoint to accep
|
|||||||
## Changes Made
|
## Changes Made
|
||||||
|
|
||||||
### 1. API Models (`usda_vision_system/api/models.py`)
|
### 1. API Models (`usda_vision_system/api/models.py`)
|
||||||
|
|
||||||
- **Enhanced `StartRecordingRequest`** to include optional parameters:
|
- **Enhanced `StartRecordingRequest`** to include optional parameters:
|
||||||
- `exposure_ms: Optional[float]` - Exposure time in milliseconds
|
- `exposure_ms: Optional[float]` - Exposure time in milliseconds
|
||||||
- `gain: Optional[float]` - Camera gain value
|
- `gain: Optional[float]` - Camera gain value
|
||||||
- `fps: Optional[float]` - Target frames per second
|
- `fps: Optional[float]` - Target frames per second
|
||||||
|
|
||||||
### 2. Camera Recorder (`usda_vision_system/camera/recorder.py`)
|
### 2. Camera Recorder (`usda_vision_system/camera/recorder.py`)
|
||||||
|
|
||||||
- **Added `update_camera_settings()` method** to dynamically update camera settings:
|
- **Added `update_camera_settings()` method** to dynamically update camera settings:
|
||||||
- Updates exposure time using `mvsdk.CameraSetExposureTime()`
|
- Updates exposure time using `mvsdk.CameraSetExposureTime()`
|
||||||
- Updates gain using `mvsdk.CameraSetAnalogGain()`
|
- Updates gain using `mvsdk.CameraSetAnalogGain()`
|
||||||
@@ -59,23 +52,20 @@ Enhanced the `POST /cameras/{camera_name}/start-recording` API endpoint to accep
|
|||||||
- Returns boolean indicating success/failure
|
- Returns boolean indicating success/failure
|
||||||
|
|
||||||
### 3. Camera Manager (`usda_vision_system/camera/manager.py`)
|
### 3. Camera Manager (`usda_vision_system/camera/manager.py`)
|
||||||
|
|
||||||
- **Enhanced `manual_start_recording()` method** to accept new parameters:
|
- **Enhanced `manual_start_recording()` method** to accept new parameters:
|
||||||
- Added optional `exposure_ms`, `gain`, and `fps` parameters
|
- Added optional `exposure_ms`, `gain`, and `fps` parameters
|
||||||
- Calls `update_camera_settings()` if any settings are provided
|
- Calls `update_camera_settings()` if any settings are provided
|
||||||
- **Automatic datetime prefix**: Always prepends timestamp to filename
|
- **Automatic datetime prefix**: Always prepends timestamp to filename
|
||||||
- If custom filename provided: `{timestamp}_{custom_filename}`
|
- If custom filename provided: `{timestamp}_{custom_filename}`
|
||||||
- If no filename provided: `{camera_name}_manual_{timestamp}.mp4`
|
- If no filename provided: `{camera_name}_manual_{timestamp}.avi`
|
||||||
|
|
||||||
### 4. API Server (`usda_vision_system/api/server.py`)
|
### 4. API Server (`usda_vision_system/api/server.py`)
|
||||||
|
|
||||||
- **Updated start-recording endpoint** to:
|
- **Updated start-recording endpoint** to:
|
||||||
- Pass new camera settings to camera manager
|
- Pass new camera settings to camera manager
|
||||||
- Handle filename response with datetime prefix
|
- Handle filename response with datetime prefix
|
||||||
- Maintain backward compatibility with existing requests
|
- Maintain backward compatibility with existing requests
|
||||||
|
|
||||||
### 5. API Tests (`api-tests.http`)
|
### 5. API Tests (`api-tests.http`)
|
||||||
|
|
||||||
- **Added comprehensive test examples**:
|
- **Added comprehensive test examples**:
|
||||||
- Basic recording (existing functionality)
|
- Basic recording (existing functionality)
|
||||||
- Recording with camera settings
|
- Recording with camera settings
|
||||||
@@ -85,9 +75,8 @@ Enhanced the `POST /cameras/{camera_name}/start-recording` API endpoint to accep
|
|||||||
## Usage Examples
|
## Usage Examples
|
||||||
|
|
||||||
### Basic Recording (unchanged)
|
### Basic Recording (unchanged)
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST http://vision:8000/cameras/camera1/start-recording
|
POST http://localhost:8000/cameras/camera1/start-recording
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -95,13 +84,11 @@ Content-Type: application/json
|
|||||||
"filename": "test.avi"
|
"filename": "test.avi"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Result**: File saved as `20241223_143022_test.avi`
|
**Result**: File saved as `20241223_143022_test.avi`
|
||||||
|
|
||||||
### Recording with Camera Settings
|
### Recording with Camera Settings
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST http://vision:8000/cameras/camera1/start-recording
|
POST http://localhost:8000/cameras/camera1/start-recording
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -112,16 +99,13 @@ Content-Type: application/json
|
|||||||
"fps": 5.0
|
"fps": 5.0
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Result**:
|
**Result**:
|
||||||
|
|
||||||
- Camera settings updated before recording
|
- Camera settings updated before recording
|
||||||
- File saved as `20241223_143022_high_quality.avi`
|
- File saved as `20241223_143022_high_quality.avi`
|
||||||
|
|
||||||
### Maximum FPS Recording
|
### Maximum FPS Recording
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST http://vision:8000/cameras/camera1/start-recording
|
POST http://localhost:8000/cameras/camera1/start-recording
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -132,17 +116,14 @@ Content-Type: application/json
|
|||||||
"fps": 0
|
"fps": 0
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Result**:
|
**Result**:
|
||||||
|
|
||||||
- Camera captures at maximum possible speed (no delay between frames)
|
- Camera captures at maximum possible speed (no delay between frames)
|
||||||
- Video file saved with 30 FPS metadata for proper playback
|
- Video file saved with 30 FPS metadata for proper playback
|
||||||
- Actual capture rate depends on camera hardware and exposure settings
|
- Actual capture rate depends on camera hardware and exposure settings
|
||||||
|
|
||||||
### Settings Only (no filename)
|
### Settings Only (no filename)
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST http://vision:8000/cameras/camera1/start-recording
|
POST http://localhost:8000/cameras/camera1/start-recording
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -152,41 +133,34 @@ Content-Type: application/json
|
|||||||
"fps": 7.0
|
"fps": 7.0
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
**Result**:
|
||||||
**Result**:
|
|
||||||
|
|
||||||
- Camera settings updated
|
- Camera settings updated
|
||||||
- File saved as `camera1_manual_20241223_143022.avi`
|
- File saved as `camera1_manual_20241223_143022.avi`
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
### 1. **Backward Compatibility**
|
### 1. **Backward Compatibility**
|
||||||
|
|
||||||
- All existing API calls continue to work unchanged
|
- All existing API calls continue to work unchanged
|
||||||
- New parameters are optional
|
- New parameters are optional
|
||||||
- Default behavior preserved when no settings provided
|
- Default behavior preserved when no settings provided
|
||||||
|
|
||||||
### 2. **Automatic Datetime Prefix**
|
### 2. **Automatic Datetime Prefix**
|
||||||
|
|
||||||
- **ALL filenames now have datetime prefix** regardless of what's sent
|
- **ALL filenames now have datetime prefix** regardless of what's sent
|
||||||
- Format: `YYYYMMDD_HHMMSS_` (Atlanta timezone)
|
- Format: `YYYYMMDD_HHMMSS_` (Atlanta timezone)
|
||||||
- Ensures unique filenames and chronological ordering
|
- Ensures unique filenames and chronological ordering
|
||||||
|
|
||||||
### 3. **Dynamic Camera Settings**
|
### 3. **Dynamic Camera Settings**
|
||||||
|
|
||||||
- Settings can be changed per recording without restarting system
|
- Settings can be changed per recording without restarting system
|
||||||
- Based on proven implementation from `old tests/camera_video_recorder.py`
|
- Based on proven implementation from `old tests/camera_video_recorder.py`
|
||||||
- Proper error handling and logging
|
- Proper error handling and logging
|
||||||
|
|
||||||
### 4. **Maximum FPS Capture**
|
### 4. **Maximum FPS Capture**
|
||||||
|
|
||||||
- **`fps: 0`** = Capture at maximum possible speed (no delay between frames)
|
- **`fps: 0`** = Capture at maximum possible speed (no delay between frames)
|
||||||
- **`fps > 0`** = Capture at specified frame rate with controlled timing
|
- **`fps > 0`** = Capture at specified frame rate with controlled timing
|
||||||
- **`fps` omitted** = Uses camera config default (usually 3.0 fps)
|
- **`fps` omitted** = Uses camera config default (usually 3.0 fps)
|
||||||
- Video files saved with 30 FPS metadata when fps=0 for proper playback
|
- Video files saved with 30 FPS metadata when fps=0 for proper playback
|
||||||
|
|
||||||
### 5. **Parameter Validation**
|
### 5. **Parameter Validation**
|
||||||
|
|
||||||
- Uses Pydantic models for automatic validation
|
- Uses Pydantic models for automatic validation
|
||||||
- Optional parameters with proper type checking
|
- Optional parameters with proper type checking
|
||||||
- Descriptive field documentation
|
- Descriptive field documentation
|
||||||
@@ -194,7 +168,6 @@ Content-Type: application/json
|
|||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Run the test script to verify functionality:
|
Run the test script to verify functionality:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start the system first
|
# Start the system first
|
||||||
python main.py
|
python main.py
|
||||||
@@ -204,7 +177,6 @@ python test_api_changes.py
|
|||||||
```
|
```
|
||||||
|
|
||||||
The test script verifies:
|
The test script verifies:
|
||||||
|
|
||||||
- Basic recording functionality
|
- Basic recording functionality
|
||||||
- Camera settings application
|
- Camera settings application
|
||||||
- Filename datetime prefix handling
|
- Filename datetime prefix handling
|
||||||
@@ -213,27 +185,22 @@ The test script verifies:
|
|||||||
## Implementation Notes
|
## Implementation Notes
|
||||||
|
|
||||||
### Camera Settings Mapping
|
### Camera Settings Mapping
|
||||||
|
|
||||||
- **Exposure**: Converted from milliseconds to microseconds for SDK
|
- **Exposure**: Converted from milliseconds to microseconds for SDK
|
||||||
- **Gain**: Converted to camera units (multiplied by 100)
|
- **Gain**: Converted to camera units (multiplied by 100)
|
||||||
- **FPS**: Stored in camera config, used by recording loop
|
- **FPS**: Stored in camera config, used by recording loop
|
||||||
|
|
||||||
### Error Handling
|
### Error Handling
|
||||||
|
|
||||||
- Settings update failures are logged but don't prevent recording
|
- Settings update failures are logged but don't prevent recording
|
||||||
- Invalid camera names return appropriate HTTP errors
|
- Invalid camera names return appropriate HTTP errors
|
||||||
- Camera initialization failures are handled gracefully
|
- Camera initialization failures are handled gracefully
|
||||||
|
|
||||||
### Filename Generation
|
### Filename Generation
|
||||||
|
|
||||||
- Uses `format_filename_timestamp()` from timezone utilities
|
- Uses `format_filename_timestamp()` from timezone utilities
|
||||||
- Ensures Atlanta timezone consistency
|
- Ensures Atlanta timezone consistency
|
||||||
- Handles both custom and auto-generated filenames
|
- Handles both custom and auto-generated filenames
|
||||||
|
|
||||||
## Similar to Old Implementation
|
## Similar to Old Implementation
|
||||||
|
|
||||||
The camera settings functionality mirrors the proven approach in `old tests/camera_video_recorder.py`:
|
The camera settings functionality mirrors the proven approach in `old tests/camera_video_recorder.py`:
|
||||||
|
|
||||||
- Same parameter names and ranges
|
- Same parameter names and ranges
|
||||||
- Same SDK function calls
|
- Same SDK function calls
|
||||||
- Same conversion factors
|
- Same conversion factors
|
||||||
|
|||||||
@@ -13,18 +13,16 @@ This document provides comprehensive documentation for all API endpoints in the
|
|||||||
- [💾 Storage & File Management](#-storage--file-management)
|
- [💾 Storage & File Management](#-storage--file-management)
|
||||||
- [🔄 Camera Recovery & Diagnostics](#-camera-recovery--diagnostics)
|
- [🔄 Camera Recovery & Diagnostics](#-camera-recovery--diagnostics)
|
||||||
- [📺 Live Streaming](#-live-streaming)
|
- [📺 Live Streaming](#-live-streaming)
|
||||||
|
- [🎬 Video Streaming & Playback](#-video-streaming--playback)
|
||||||
- [🌐 WebSocket Real-time Updates](#-websocket-real-time-updates)
|
- [🌐 WebSocket Real-time Updates](#-websocket-real-time-updates)
|
||||||
|
|
||||||
## 🔧 System Status & Health
|
## 🔧 System Status & Health
|
||||||
|
|
||||||
### Get System Status
|
### Get System Status
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /system/status
|
GET /system/status
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response**: `SystemStatusResponse`
|
**Response**: `SystemStatusResponse`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"system_started": true,
|
"system_started": true,
|
||||||
@@ -52,13 +50,10 @@ GET /system/status
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Health Check
|
### Health Check
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /health
|
GET /health
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response**: Simple health status
|
**Response**: Simple health status
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"status": "healthy",
|
"status": "healthy",
|
||||||
@@ -69,21 +64,16 @@ GET /health
|
|||||||
## 📷 Camera Management
|
## 📷 Camera Management
|
||||||
|
|
||||||
### Get All Cameras
|
### Get All Cameras
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /cameras
|
GET /cameras
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response**: `Dict[str, CameraStatusResponse]`
|
**Response**: `Dict[str, CameraStatusResponse]`
|
||||||
|
|
||||||
### Get Specific Camera Status
|
### Get Specific Camera Status
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /cameras/{camera_name}/status
|
GET /cameras/{camera_name}/status
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response**: `CameraStatusResponse`
|
**Response**: `CameraStatusResponse`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "camera1",
|
"name": "camera1",
|
||||||
@@ -108,13 +98,12 @@ GET /cameras/{camera_name}/status
|
|||||||
## 🎥 Recording Control
|
## 🎥 Recording Control
|
||||||
|
|
||||||
### Start Recording
|
### Start Recording
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /cameras/{camera_name}/start-recording
|
POST /cameras/{camera_name}/start-recording
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"filename": "test_recording.mp4",
|
"filename": "test_recording.avi",
|
||||||
"exposure_ms": 2.0,
|
"exposure_ms": 2.0,
|
||||||
"gain": 4.0,
|
"gain": 4.0,
|
||||||
"fps": 5.0
|
"fps": 5.0
|
||||||
@@ -122,36 +111,30 @@ Content-Type: application/json
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Request Model**: `StartRecordingRequest`
|
**Request Model**: `StartRecordingRequest`
|
||||||
|
|
||||||
- `filename` (optional): Custom filename (datetime prefix will be added automatically)
|
- `filename` (optional): Custom filename (datetime prefix will be added automatically)
|
||||||
- `exposure_ms` (optional): Exposure time in milliseconds
|
- `exposure_ms` (optional): Exposure time in milliseconds
|
||||||
- `gain` (optional): Camera gain value
|
- `gain` (optional): Camera gain value
|
||||||
- `fps` (optional): Target frames per second
|
- `fps` (optional): Target frames per second
|
||||||
|
|
||||||
**Response**: `StartRecordingResponse`
|
**Response**: `StartRecordingResponse`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "Recording started for camera1",
|
"message": "Recording started for camera1",
|
||||||
"filename": "20240115_103000_test_recording.mp4"
|
"filename": "20240115_103000_test_recording.avi"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Key Features**:
|
**Key Features**:
|
||||||
|
|
||||||
- ✅ **Automatic datetime prefix**: All filenames get `YYYYMMDD_HHMMSS_` prefix
|
- ✅ **Automatic datetime prefix**: All filenames get `YYYYMMDD_HHMMSS_` prefix
|
||||||
- ✅ **Dynamic camera settings**: Adjust exposure, gain, and FPS per recording
|
- ✅ **Dynamic camera settings**: Adjust exposure, gain, and FPS per recording
|
||||||
- ✅ **Backward compatibility**: All existing API calls work unchanged
|
- ✅ **Backward compatibility**: All existing API calls work unchanged
|
||||||
|
|
||||||
### Stop Recording
|
### Stop Recording
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /cameras/{camera_name}/stop-recording
|
POST /cameras/{camera_name}/stop-recording
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response**: `StopRecordingResponse`
|
**Response**: `StopRecordingResponse`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
@@ -163,13 +146,10 @@ POST /cameras/{camera_name}/stop-recording
|
|||||||
## 🤖 Auto-Recording Management
|
## 🤖 Auto-Recording Management
|
||||||
|
|
||||||
### Enable Auto-Recording for Camera
|
### Enable Auto-Recording for Camera
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /cameras/{camera_name}/auto-recording/enable
|
POST /cameras/{camera_name}/auto-recording/enable
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response**: `AutoRecordingConfigResponse`
|
**Response**: `AutoRecordingConfigResponse`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
@@ -180,21 +160,16 @@ POST /cameras/{camera_name}/auto-recording/enable
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Disable Auto-Recording for Camera
|
### Disable Auto-Recording for Camera
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /cameras/{camera_name}/auto-recording/disable
|
POST /cameras/{camera_name}/auto-recording/disable
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response**: `AutoRecordingConfigResponse`
|
**Response**: `AutoRecordingConfigResponse`
|
||||||
|
|
||||||
### Get Auto-Recording Status
|
### Get Auto-Recording Status
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /auto-recording/status
|
GET /auto-recording/status
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response**: `AutoRecordingStatusResponse`
|
**Response**: `AutoRecordingStatusResponse`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"running": true,
|
"running": true,
|
||||||
@@ -205,7 +180,6 @@ GET /auto-recording/status
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Auto-Recording Features**:
|
**Auto-Recording Features**:
|
||||||
|
|
||||||
- 🤖 **MQTT-triggered recording**: Automatically starts/stops based on machine state
|
- 🤖 **MQTT-triggered recording**: Automatically starts/stops based on machine state
|
||||||
- 🔄 **Retry logic**: Failed recordings are retried with configurable delays
|
- 🔄 **Retry logic**: Failed recordings are retried with configurable delays
|
||||||
- 📊 **Per-camera control**: Enable/disable auto-recording individually
|
- 📊 **Per-camera control**: Enable/disable auto-recording individually
|
||||||
@@ -214,13 +188,10 @@ GET /auto-recording/status
|
|||||||
## 🎛️ Camera Configuration
|
## 🎛️ Camera Configuration
|
||||||
|
|
||||||
### Get Camera Configuration
|
### Get Camera Configuration
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /cameras/{camera_name}/config
|
GET /cameras/{camera_name}/config
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response**: `CameraConfigResponse`
|
**Response**: `CameraConfigResponse`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "camera1",
|
"name": "camera1",
|
||||||
@@ -255,7 +226,6 @@ GET /cameras/{camera_name}/config
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Update Camera Configuration
|
### Update Camera Configuration
|
||||||
|
|
||||||
```http
|
```http
|
||||||
PUT /cameras/{camera_name}/config
|
PUT /cameras/{camera_name}/config
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
@@ -269,13 +239,11 @@ Content-Type: application/json
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Apply Configuration (Restart Required)
|
### Apply Configuration (Restart Required)
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /cameras/{camera_name}/apply-config
|
POST /cameras/{camera_name}/apply-config
|
||||||
```
|
```
|
||||||
|
|
||||||
**Configuration Categories**:
|
**Configuration Categories**:
|
||||||
|
|
||||||
- ✅ **Real-time**: `exposure_ms`, `gain`, `target_fps`, `sharpness`, `contrast`, etc.
|
- ✅ **Real-time**: `exposure_ms`, `gain`, `target_fps`, `sharpness`, `contrast`, etc.
|
||||||
- ⚠️ **Restart required**: `noise_filter_enabled`, `denoise_3d_enabled`, `bit_depth`, `video_format`, `video_codec`, `video_quality`
|
- ⚠️ **Restart required**: `noise_filter_enabled`, `denoise_3d_enabled`, `bit_depth`, `video_format`, `video_codec`, `video_quality`
|
||||||
|
|
||||||
@@ -284,21 +252,16 @@ For detailed configuration options, see [Camera Configuration API Guide](api/CAM
|
|||||||
## 📡 MQTT & Machine Status
|
## 📡 MQTT & Machine Status
|
||||||
|
|
||||||
### Get All Machines
|
### Get All Machines
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /machines
|
GET /machines
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response**: `Dict[str, MachineStatusResponse]`
|
**Response**: `Dict[str, MachineStatusResponse]`
|
||||||
|
|
||||||
### Get MQTT Status
|
### Get MQTT Status
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /mqtt/status
|
GET /mqtt/status
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response**: `MQTTStatusResponse`
|
**Response**: `MQTTStatusResponse`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"connected": true,
|
"connected": true,
|
||||||
@@ -313,13 +276,10 @@ GET /mqtt/status
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Get MQTT Events History
|
### Get MQTT Events History
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /mqtt/events?limit=10
|
GET /mqtt/events?limit=10
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response**: `MQTTEventsHistoryResponse`
|
**Response**: `MQTTEventsHistoryResponse`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"events": [
|
"events": [
|
||||||
@@ -340,13 +300,10 @@ GET /mqtt/events?limit=10
|
|||||||
## 💾 Storage & File Management
|
## 💾 Storage & File Management
|
||||||
|
|
||||||
### Get Storage Statistics
|
### Get Storage Statistics
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /storage/stats
|
GET /storage/stats
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response**: `StorageStatsResponse`
|
**Response**: `StorageStatsResponse`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"base_path": "/storage",
|
"base_path": "/storage",
|
||||||
@@ -372,7 +329,6 @@ GET /storage/stats
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Get File List
|
### Get File List
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /storage/files
|
POST /storage/files
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
@@ -384,9 +340,7 @@ Content-Type: application/json
|
|||||||
"limit": 50
|
"limit": 50
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response**: `FileListResponse`
|
**Response**: `FileListResponse`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"files": [
|
"files": [
|
||||||
@@ -403,7 +357,6 @@ Content-Type: application/json
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Cleanup Old Files
|
### Cleanup Old Files
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /storage/cleanup
|
POST /storage/cleanup
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
@@ -412,9 +365,7 @@ Content-Type: application/json
|
|||||||
"max_age_days": 30
|
"max_age_days": 30
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response**: `CleanupResponse`
|
**Response**: `CleanupResponse`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"files_removed": 25,
|
"files_removed": 25,
|
||||||
@@ -426,55 +377,42 @@ Content-Type: application/json
|
|||||||
## 🔄 Camera Recovery & Diagnostics
|
## 🔄 Camera Recovery & Diagnostics
|
||||||
|
|
||||||
### Test Camera Connection
|
### Test Camera Connection
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /cameras/{camera_name}/test-connection
|
POST /cameras/{camera_name}/test-connection
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response**: `CameraTestResponse`
|
**Response**: `CameraTestResponse`
|
||||||
|
|
||||||
### Reconnect Camera
|
### Reconnect Camera
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /cameras/{camera_name}/reconnect
|
POST /cameras/{camera_name}/reconnect
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response**: `CameraRecoveryResponse`
|
**Response**: `CameraRecoveryResponse`
|
||||||
|
|
||||||
### Restart Camera Grab Process
|
### Restart Camera Grab Process
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /cameras/{camera_name}/restart-grab
|
POST /cameras/{camera_name}/restart-grab
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response**: `CameraRecoveryResponse`
|
**Response**: `CameraRecoveryResponse`
|
||||||
|
|
||||||
### Reset Camera Timestamp
|
### Reset Camera Timestamp
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /cameras/{camera_name}/reset-timestamp
|
POST /cameras/{camera_name}/reset-timestamp
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response**: `CameraRecoveryResponse`
|
**Response**: `CameraRecoveryResponse`
|
||||||
|
|
||||||
### Full Camera Reset
|
### Full Camera Reset
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /cameras/{camera_name}/full-reset
|
POST /cameras/{camera_name}/full-reset
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response**: `CameraRecoveryResponse`
|
**Response**: `CameraRecoveryResponse`
|
||||||
|
|
||||||
### Reinitialize Camera
|
### Reinitialize Camera
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /cameras/{camera_name}/reinitialize
|
POST /cameras/{camera_name}/reinitialize
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response**: `CameraRecoveryResponse`
|
**Response**: `CameraRecoveryResponse`
|
||||||
|
|
||||||
**Recovery Response Example**:
|
**Recovery Response Example**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
@@ -488,39 +426,176 @@ POST /cameras/{camera_name}/reinitialize
|
|||||||
## 📺 Live Streaming
|
## 📺 Live Streaming
|
||||||
|
|
||||||
### Get Live MJPEG Stream
|
### Get Live MJPEG Stream
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /cameras/{camera_name}/stream
|
GET /cameras/{camera_name}/stream
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response**: MJPEG video stream (multipart/x-mixed-replace)
|
**Response**: MJPEG video stream (multipart/x-mixed-replace)
|
||||||
|
|
||||||
### Start Camera Stream
|
### Start Camera Stream
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /cameras/{camera_name}/start-stream
|
POST /cameras/{camera_name}/start-stream
|
||||||
```
|
```
|
||||||
|
|
||||||
### Stop Camera Stream
|
### Stop Camera Stream
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /cameras/{camera_name}/stop-stream
|
POST /cameras/{camera_name}/stop-stream
|
||||||
```
|
```
|
||||||
|
|
||||||
**Streaming Features**:
|
**Streaming Features**:
|
||||||
|
|
||||||
- 📺 **MJPEG format**: Compatible with web browsers and React apps
|
- 📺 **MJPEG format**: Compatible with web browsers and React apps
|
||||||
- 🔄 **Concurrent operation**: Stream while recording simultaneously
|
- 🔄 **Concurrent operation**: Stream while recording simultaneously
|
||||||
- ⚡ **Low latency**: Real-time preview for monitoring
|
- ⚡ **Low latency**: Real-time preview for monitoring
|
||||||
|
|
||||||
For detailed streaming integration, see [Streaming Guide](guides/STREAMING_GUIDE.md).
|
For detailed streaming integration, see [Streaming Guide](guides/STREAMING_GUIDE.md).
|
||||||
|
|
||||||
|
## 🎬 Video Streaming & Playback
|
||||||
|
|
||||||
|
The system includes a comprehensive video streaming module that provides YouTube-like video playback capabilities with HTTP range request support, thumbnail generation, and intelligent caching.
|
||||||
|
|
||||||
|
### List Videos
|
||||||
|
```http
|
||||||
|
GET /videos/
|
||||||
|
```
|
||||||
|
**Query Parameters:**
|
||||||
|
- `camera_name` (optional): Filter by camera name
|
||||||
|
- `start_date` (optional): Filter videos created after this date (ISO format)
|
||||||
|
- `end_date` (optional): Filter videos created before this date (ISO format)
|
||||||
|
- `limit` (optional): Maximum number of results (default: 50, max: 1000)
|
||||||
|
- `include_metadata` (optional): Include video metadata (default: false)
|
||||||
|
|
||||||
|
**Response**: `VideoListResponse`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"videos": [
|
||||||
|
{
|
||||||
|
"file_id": "camera1_auto_blower_separator_20250804_143022.mp4",
|
||||||
|
"camera_name": "camera1",
|
||||||
|
"filename": "camera1_auto_blower_separator_20250804_143022.mp4",
|
||||||
|
"file_size_bytes": 31457280,
|
||||||
|
"format": "mp4",
|
||||||
|
"status": "completed",
|
||||||
|
"created_at": "2025-08-04T14:30:22",
|
||||||
|
"start_time": "2025-08-04T14:30:22",
|
||||||
|
"end_time": "2025-08-04T14:32:22",
|
||||||
|
"machine_trigger": "blower_separator",
|
||||||
|
"is_streamable": true,
|
||||||
|
"needs_conversion": false,
|
||||||
|
"metadata": {
|
||||||
|
"duration_seconds": 120.5,
|
||||||
|
"width": 1920,
|
||||||
|
"height": 1080,
|
||||||
|
"fps": 30.0,
|
||||||
|
"codec": "mp4v",
|
||||||
|
"bitrate": 5000000,
|
||||||
|
"aspect_ratio": 1.777
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total_count": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Video Information
|
||||||
|
```http
|
||||||
|
GET /videos/{file_id}
|
||||||
|
```
|
||||||
|
**Response**: `VideoInfoResponse` with detailed video information including metadata.
|
||||||
|
|
||||||
|
### Stream Video
|
||||||
|
```http
|
||||||
|
GET /videos/{file_id}/stream
|
||||||
|
```
|
||||||
|
**Headers:**
|
||||||
|
- `Range: bytes=0-1023` (optional): Request specific byte range for seeking
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- ✅ **HTTP Range Requests**: Enables video seeking and progressive download
|
||||||
|
- ✅ **Partial Content**: Returns 206 status for range requests
|
||||||
|
- ✅ **Format Conversion**: Automatic AVI to MP4 conversion for web compatibility
|
||||||
|
- ✅ **Intelligent Caching**: Optimized performance with byte-range caching
|
||||||
|
- ✅ **CORS Enabled**: Ready for web browser integration
|
||||||
|
|
||||||
|
**Response Headers:**
|
||||||
|
- `Accept-Ranges: bytes`
|
||||||
|
- `Content-Length: {size}`
|
||||||
|
- `Content-Range: bytes {start}-{end}/{total}` (for range requests)
|
||||||
|
- `Cache-Control: public, max-age=3600`
|
||||||
|
|
||||||
|
### Get Video Thumbnail
|
||||||
|
```http
|
||||||
|
GET /videos/{file_id}/thumbnail?timestamp=5.0&width=320&height=240
|
||||||
|
```
|
||||||
|
**Query Parameters:**
|
||||||
|
- `timestamp` (optional): Time position in seconds (default: 1.0)
|
||||||
|
- `width` (optional): Thumbnail width in pixels (default: 320)
|
||||||
|
- `height` (optional): Thumbnail height in pixels (default: 240)
|
||||||
|
|
||||||
|
**Response**: JPEG image data with caching headers
|
||||||
|
|
||||||
|
### Get Streaming Information
|
||||||
|
```http
|
||||||
|
GET /videos/{file_id}/info
|
||||||
|
```
|
||||||
|
**Response**: `StreamingInfoResponse`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"file_id": "camera1_recording_20250804_143022.avi",
|
||||||
|
"file_size_bytes": 52428800,
|
||||||
|
"content_type": "video/mp4",
|
||||||
|
"supports_range_requests": true,
|
||||||
|
"chunk_size_bytes": 262144
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Video Validation
|
||||||
|
```http
|
||||||
|
POST /videos/{file_id}/validate
|
||||||
|
```
|
||||||
|
**Response**: Validation status and accessibility check
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"file_id": "camera1_recording_20250804_143022.avi",
|
||||||
|
"is_valid": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cache Management
|
||||||
|
```http
|
||||||
|
POST /videos/{file_id}/cache/invalidate
|
||||||
|
```
|
||||||
|
**Response**: Cache invalidation status
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"file_id": "camera1_recording_20250804_143022.avi",
|
||||||
|
"cache_invalidated": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Admin: Cache Cleanup
|
||||||
|
```http
|
||||||
|
POST /admin/videos/cache/cleanup?max_size_mb=100
|
||||||
|
```
|
||||||
|
**Response**: Cache cleanup results
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cache_cleaned": true,
|
||||||
|
"entries_removed": 15,
|
||||||
|
"max_size_mb": 100
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Video Streaming Features**:
|
||||||
|
- 🎥 **Multiple Formats**: Native MP4 support with AVI conversion
|
||||||
|
- 📱 **Web Compatible**: Direct integration with HTML5 video elements
|
||||||
|
- ⚡ **High Performance**: Intelligent caching and adaptive chunking
|
||||||
|
- 🖼️ **Thumbnail Generation**: Extract preview images at any timestamp
|
||||||
|
- 🔄 **Range Requests**: Efficient seeking and progressive download
|
||||||
|
|
||||||
## 🌐 WebSocket Real-time Updates
|
## 🌐 WebSocket Real-time Updates
|
||||||
|
|
||||||
### Connect to WebSocket
|
### Connect to WebSocket
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const ws = new WebSocket('ws://vision:8000/ws');
|
const ws = new WebSocket('ws://localhost:8000/ws');
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
const update = JSON.parse(event.data);
|
const update = JSON.parse(event.data);
|
||||||
@@ -529,7 +604,6 @@ ws.onmessage = (event) => {
|
|||||||
```
|
```
|
||||||
|
|
||||||
**WebSocket Message Types**:
|
**WebSocket Message Types**:
|
||||||
|
|
||||||
- `system_status`: System status changes
|
- `system_status`: System status changes
|
||||||
- `camera_status`: Camera status updates
|
- `camera_status`: Camera status updates
|
||||||
- `recording_started`: Recording start events
|
- `recording_started`: Recording start events
|
||||||
@@ -538,7 +612,6 @@ ws.onmessage = (event) => {
|
|||||||
- `auto_recording_event`: Auto-recording status changes
|
- `auto_recording_event`: Auto-recording status changes
|
||||||
|
|
||||||
**Example WebSocket Message**:
|
**Example WebSocket Message**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "recording_started",
|
"type": "recording_started",
|
||||||
@@ -554,28 +627,26 @@ ws.onmessage = (event) => {
|
|||||||
## 🚀 Quick Start Examples
|
## 🚀 Quick Start Examples
|
||||||
|
|
||||||
### Basic System Monitoring
|
### Basic System Monitoring
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Check system health
|
# Check system health
|
||||||
curl http://vision:8000/health
|
curl http://localhost:8000/health
|
||||||
|
|
||||||
# Get overall system status
|
# Get overall system status
|
||||||
curl http://vision:8000/system/status
|
curl http://localhost:8000/system/status
|
||||||
|
|
||||||
# Get all camera statuses
|
# Get all camera statuses
|
||||||
curl http://vision:8000/cameras
|
curl http://localhost:8000/cameras
|
||||||
```
|
```
|
||||||
|
|
||||||
### Manual Recording Control
|
### Manual Recording Control
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start recording with default settings
|
# Start recording with default settings
|
||||||
curl -X POST http://vision:8000/cameras/camera1/start-recording \
|
curl -X POST http://localhost:8000/cameras/camera1/start-recording \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"filename": "manual_test.avi"}'
|
-d '{"filename": "manual_test.avi"}'
|
||||||
|
|
||||||
# Start recording with custom camera settings
|
# Start recording with custom camera settings
|
||||||
curl -X POST http://vision:8000/cameras/camera1/start-recording \
|
curl -X POST http://localhost:8000/cameras/camera1/start-recording \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"filename": "high_quality.avi",
|
"filename": "high_quality.avi",
|
||||||
@@ -585,30 +656,57 @@ curl -X POST http://vision:8000/cameras/camera1/start-recording \
|
|||||||
}'
|
}'
|
||||||
|
|
||||||
# Stop recording
|
# Stop recording
|
||||||
curl -X POST http://vision:8000/cameras/camera1/stop-recording
|
curl -X POST http://localhost:8000/cameras/camera1/stop-recording
|
||||||
```
|
```
|
||||||
|
|
||||||
### Auto-Recording Management
|
### Auto-Recording Management
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Enable auto-recording for camera1
|
# Enable auto-recording for camera1
|
||||||
curl -X POST http://vision:8000/cameras/camera1/auto-recording/enable
|
curl -X POST http://localhost:8000/cameras/camera1/auto-recording/enable
|
||||||
|
|
||||||
# Check auto-recording status
|
# Check auto-recording status
|
||||||
curl http://vision:8000/auto-recording/status
|
curl http://localhost:8000/auto-recording/status
|
||||||
|
|
||||||
# Disable auto-recording for camera1
|
# Disable auto-recording for camera1
|
||||||
curl -X POST http://vision:8000/cameras/camera1/auto-recording/disable
|
curl -X POST http://localhost:8000/cameras/camera1/auto-recording/disable
|
||||||
|
```
|
||||||
|
|
||||||
|
### Video Streaming Operations
|
||||||
|
```bash
|
||||||
|
# List all videos
|
||||||
|
curl http://localhost:8000/videos/
|
||||||
|
|
||||||
|
# List videos from specific camera with metadata
|
||||||
|
curl "http://localhost:8000/videos/?camera_name=camera1&include_metadata=true"
|
||||||
|
|
||||||
|
# Get video information
|
||||||
|
curl http://localhost:8000/videos/camera1_recording_20250804_143022.avi
|
||||||
|
|
||||||
|
# Get video thumbnail
|
||||||
|
curl "http://localhost:8000/videos/camera1_recording_20250804_143022.avi/thumbnail?timestamp=5.0&width=320&height=240" \
|
||||||
|
--output thumbnail.jpg
|
||||||
|
|
||||||
|
# Get streaming info
|
||||||
|
curl http://localhost:8000/videos/camera1_recording_20250804_143022.avi/info
|
||||||
|
|
||||||
|
# Stream video with range request
|
||||||
|
curl -H "Range: bytes=0-1023" \
|
||||||
|
http://localhost:8000/videos/camera1_recording_20250804_143022.avi/stream
|
||||||
|
|
||||||
|
# Validate video file
|
||||||
|
curl -X POST http://localhost:8000/videos/camera1_recording_20250804_143022.avi/validate
|
||||||
|
|
||||||
|
# Clean up video cache (admin)
|
||||||
|
curl -X POST "http://localhost:8000/admin/videos/cache/cleanup?max_size_mb=100"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Camera Configuration
|
### Camera Configuration
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Get current camera configuration
|
# Get current camera configuration
|
||||||
curl http://vision:8000/cameras/camera1/config
|
curl http://localhost:8000/cameras/camera1/config
|
||||||
|
|
||||||
# Update camera settings (real-time)
|
# Update camera settings (real-time)
|
||||||
curl -X PUT http://vision:8000/cameras/camera1/config \
|
curl -X PUT http://localhost:8000/cameras/camera1/config \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"exposure_ms": 1.5,
|
"exposure_ms": 1.5,
|
||||||
@@ -623,47 +721,47 @@ curl -X PUT http://vision:8000/cameras/camera1/config \
|
|||||||
### ✨ New in Latest Version
|
### ✨ New in Latest Version
|
||||||
|
|
||||||
#### 1. Enhanced Recording API
|
#### 1. Enhanced Recording API
|
||||||
|
|
||||||
- **Dynamic camera settings**: Set exposure, gain, and FPS per recording
|
- **Dynamic camera settings**: Set exposure, gain, and FPS per recording
|
||||||
- **Automatic datetime prefixes**: All filenames get timestamp prefixes
|
- **Automatic datetime prefixes**: All filenames get timestamp prefixes
|
||||||
- **Backward compatibility**: Existing API calls work unchanged
|
- **Backward compatibility**: Existing API calls work unchanged
|
||||||
|
|
||||||
#### 2. Auto-Recording Feature
|
#### 2. Auto-Recording Feature
|
||||||
|
|
||||||
- **Per-camera control**: Enable/disable auto-recording individually
|
- **Per-camera control**: Enable/disable auto-recording individually
|
||||||
- **MQTT integration**: Automatic recording based on machine states
|
- **MQTT integration**: Automatic recording based on machine states
|
||||||
- **Retry logic**: Failed recordings are automatically retried
|
- **Retry logic**: Failed recordings are automatically retried
|
||||||
- **Status tracking**: Monitor auto-recording attempts and failures
|
- **Status tracking**: Monitor auto-recording attempts and failures
|
||||||
|
|
||||||
#### 3. Advanced Camera Configuration
|
#### 3. Advanced Camera Configuration
|
||||||
|
|
||||||
- **Real-time settings**: Update exposure, gain, image quality without restart
|
- **Real-time settings**: Update exposure, gain, image quality without restart
|
||||||
- **Image enhancement**: Sharpness, contrast, saturation, gamma controls
|
- **Image enhancement**: Sharpness, contrast, saturation, gamma controls
|
||||||
- **Noise reduction**: Configurable noise filtering and 3D denoising
|
- **Noise reduction**: Configurable noise filtering and 3D denoising
|
||||||
- **HDR support**: High Dynamic Range imaging capabilities
|
- **HDR support**: High Dynamic Range imaging capabilities
|
||||||
|
|
||||||
#### 4. Live Streaming
|
#### 4. Live Streaming
|
||||||
|
|
||||||
- **MJPEG streaming**: Real-time camera preview
|
- **MJPEG streaming**: Real-time camera preview
|
||||||
- **Concurrent operation**: Stream while recording simultaneously
|
- **Concurrent operation**: Stream while recording simultaneously
|
||||||
- **Web-compatible**: Direct integration with React/HTML video elements
|
- **Web-compatible**: Direct integration with React/HTML video elements
|
||||||
|
|
||||||
#### 5. Enhanced Monitoring
|
#### 5. Enhanced Monitoring
|
||||||
|
|
||||||
- **MQTT event history**: Track machine state changes over time
|
- **MQTT event history**: Track machine state changes over time
|
||||||
- **Storage statistics**: Monitor disk usage and file counts
|
- **Storage statistics**: Monitor disk usage and file counts
|
||||||
- **WebSocket updates**: Real-time system status notifications
|
- **WebSocket updates**: Real-time system status notifications
|
||||||
|
|
||||||
|
#### 6. Video Streaming Module
|
||||||
|
- **HTTP Range Requests**: Efficient video seeking and progressive download
|
||||||
|
- **Thumbnail Generation**: Extract preview images from videos at any timestamp
|
||||||
|
- **Format Conversion**: Automatic AVI to MP4 conversion for web compatibility
|
||||||
|
- **Intelligent Caching**: Byte-range caching for optimal streaming performance
|
||||||
|
- **Admin Tools**: Cache management and video validation endpoints
|
||||||
|
|
||||||
### 🔄 Migration Notes
|
### 🔄 Migration Notes
|
||||||
|
|
||||||
#### From Previous Versions
|
#### From Previous Versions
|
||||||
|
|
||||||
1. **Recording API**: All existing calls work, but now return filenames with datetime prefixes
|
1. **Recording API**: All existing calls work, but now return filenames with datetime prefixes
|
||||||
2. **Configuration**: New camera settings are optional and backward compatible
|
2. **Configuration**: New camera settings are optional and backward compatible
|
||||||
3. **Auto-recording**: New feature, requires enabling in `config.json` and per camera
|
3. **Auto-recording**: New feature, requires enabling in `config.json` and per camera
|
||||||
|
|
||||||
#### Configuration Updates
|
#### Configuration Updates
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"cameras": [
|
"cameras": [
|
||||||
@@ -689,34 +787,38 @@ curl -X PUT http://vision:8000/cameras/camera1/config \
|
|||||||
- [📷 Camera Configuration API Guide](api/CAMERA_CONFIG_API.md) - Detailed camera settings
|
- [📷 Camera Configuration API Guide](api/CAMERA_CONFIG_API.md) - Detailed camera settings
|
||||||
- [🤖 Auto-Recording Feature Guide](features/AUTO_RECORDING_FEATURE_GUIDE.md) - React integration
|
- [🤖 Auto-Recording Feature Guide](features/AUTO_RECORDING_FEATURE_GUIDE.md) - React integration
|
||||||
- [📺 Streaming Guide](guides/STREAMING_GUIDE.md) - Live video streaming
|
- [📺 Streaming Guide](guides/STREAMING_GUIDE.md) - Live video streaming
|
||||||
|
- [🎬 Video Streaming Guide](VIDEO_STREAMING.md) - Video playback and streaming
|
||||||
|
- [🤖 AI Agent Video Integration Guide](AI_AGENT_VIDEO_INTEGRATION_GUIDE.md) - Complete integration guide for AI agents
|
||||||
- [🔧 Camera Recovery Guide](guides/CAMERA_RECOVERY_GUIDE.md) - Troubleshooting
|
- [🔧 Camera Recovery Guide](guides/CAMERA_RECOVERY_GUIDE.md) - Troubleshooting
|
||||||
- [📡 MQTT Logging Guide](guides/MQTT_LOGGING_GUIDE.md) - MQTT configuration
|
- [📡 MQTT Logging Guide](guides/MQTT_LOGGING_GUIDE.md) - MQTT configuration
|
||||||
|
|
||||||
## 📞 Support & Integration
|
## 📞 Support & Integration
|
||||||
|
|
||||||
### API Base URL
|
### API Base URL
|
||||||
|
- **Development**: `http://localhost:8000`
|
||||||
- **Development**: `http://vision:8000`
|
|
||||||
- **Production**: Configure in `config.json` under `system.api_host` and `system.api_port`
|
- **Production**: Configure in `config.json` under `system.api_host` and `system.api_port`
|
||||||
|
|
||||||
### Error Handling
|
### Error Handling
|
||||||
|
|
||||||
All endpoints return standard HTTP status codes:
|
All endpoints return standard HTTP status codes:
|
||||||
|
|
||||||
- `200`: Success
|
- `200`: Success
|
||||||
- `404`: Resource not found (camera, file, etc.)
|
- `206`: Partial Content (for video range requests)
|
||||||
|
- `400`: Bad Request (invalid parameters)
|
||||||
|
- `404`: Resource not found (camera, file, video, etc.)
|
||||||
|
- `416`: Range Not Satisfiable (invalid video range request)
|
||||||
- `500`: Internal server error
|
- `500`: Internal server error
|
||||||
- `503`: Service unavailable (camera manager, MQTT, etc.)
|
- `503`: Service unavailable (camera manager, MQTT, etc.)
|
||||||
|
|
||||||
### Rate Limiting
|
**Video Streaming Specific Errors:**
|
||||||
|
- `404`: Video file not found or not streamable
|
||||||
|
- `416`: Invalid range request (malformed Range header)
|
||||||
|
- `500`: Failed to read video data or generate thumbnail
|
||||||
|
|
||||||
|
### Rate Limiting
|
||||||
- No rate limiting currently implemented
|
- No rate limiting currently implemented
|
||||||
- WebSocket connections are limited to reasonable concurrent connections
|
- WebSocket connections are limited to reasonable concurrent connections
|
||||||
|
|
||||||
### CORS Support
|
### CORS Support
|
||||||
|
|
||||||
- CORS is enabled for web dashboard integration
|
- CORS is enabled for web dashboard integration
|
||||||
- Configure allowed origins in the API server settings
|
- Configure allowed origins in the API server settings
|
||||||
|
|
||||||
```
|
```
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -6,30 +6,30 @@ Quick reference for the most commonly used API endpoints. For complete documenta
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Health check
|
# Health check
|
||||||
curl http://vision:8000/health
|
curl http://localhost:8000/health
|
||||||
|
|
||||||
# System overview
|
# System overview
|
||||||
curl http://vision:8000/system/status
|
curl http://localhost:8000/system/status
|
||||||
|
|
||||||
# All cameras
|
# All cameras
|
||||||
curl http://vision:8000/cameras
|
curl http://localhost:8000/cameras
|
||||||
|
|
||||||
# All machines
|
# All machines
|
||||||
curl http://vision:8000/machines
|
curl http://localhost:8000/machines
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🎥 Recording Control
|
## 🎥 Recording Control
|
||||||
|
|
||||||
### Start Recording (Basic)
|
### Start Recording (Basic)
|
||||||
```bash
|
```bash
|
||||||
curl -X POST http://vision:8000/cameras/camera1/start-recording \
|
curl -X POST http://localhost:8000/cameras/camera1/start-recording \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"filename": "test.avi"}'
|
-d '{"filename": "test.avi"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Start Recording (With Settings)
|
### Start Recording (With Settings)
|
||||||
```bash
|
```bash
|
||||||
curl -X POST http://vision:8000/cameras/camera1/start-recording \
|
curl -X POST http://localhost:8000/cameras/camera1/start-recording \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"filename": "high_quality.avi",
|
"filename": "high_quality.avi",
|
||||||
@@ -41,30 +41,30 @@ curl -X POST http://vision:8000/cameras/camera1/start-recording \
|
|||||||
|
|
||||||
### Stop Recording
|
### Stop Recording
|
||||||
```bash
|
```bash
|
||||||
curl -X POST http://vision:8000/cameras/camera1/stop-recording
|
curl -X POST http://localhost:8000/cameras/camera1/stop-recording
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🤖 Auto-Recording
|
## 🤖 Auto-Recording
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Enable auto-recording
|
# Enable auto-recording
|
||||||
curl -X POST http://vision:8000/cameras/camera1/auto-recording/enable
|
curl -X POST http://localhost:8000/cameras/camera1/auto-recording/enable
|
||||||
|
|
||||||
# Disable auto-recording
|
# Disable auto-recording
|
||||||
curl -X POST http://vision:8000/cameras/camera1/auto-recording/disable
|
curl -X POST http://localhost:8000/cameras/camera1/auto-recording/disable
|
||||||
|
|
||||||
# Check auto-recording status
|
# Check auto-recording status
|
||||||
curl http://vision:8000/auto-recording/status
|
curl http://localhost:8000/auto-recording/status
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🎛️ Camera Configuration
|
## 🎛️ Camera Configuration
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Get camera config
|
# Get camera config
|
||||||
curl http://vision:8000/cameras/camera1/config
|
curl http://localhost:8000/cameras/camera1/config
|
||||||
|
|
||||||
# Update camera settings
|
# Update camera settings
|
||||||
curl -X PUT http://vision:8000/cameras/camera1/config \
|
curl -X PUT http://localhost:8000/cameras/camera1/config \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"exposure_ms": 1.5,
|
"exposure_ms": 1.5,
|
||||||
@@ -77,41 +77,41 @@ curl -X PUT http://vision:8000/cameras/camera1/config \
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start streaming
|
# Start streaming
|
||||||
curl -X POST http://vision:8000/cameras/camera1/start-stream
|
curl -X POST http://localhost:8000/cameras/camera1/start-stream
|
||||||
|
|
||||||
# Get MJPEG stream (use in browser/video element)
|
# Get MJPEG stream (use in browser/video element)
|
||||||
# http://vision:8000/cameras/camera1/stream
|
# http://localhost:8000/cameras/camera1/stream
|
||||||
|
|
||||||
# Stop streaming
|
# Stop streaming
|
||||||
curl -X POST http://vision:8000/cameras/camera1/stop-stream
|
curl -X POST http://localhost:8000/cameras/camera1/stop-stream
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔄 Camera Recovery
|
## 🔄 Camera Recovery
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Test connection
|
# Test connection
|
||||||
curl -X POST http://vision:8000/cameras/camera1/test-connection
|
curl -X POST http://localhost:8000/cameras/camera1/test-connection
|
||||||
|
|
||||||
# Reconnect camera
|
# Reconnect camera
|
||||||
curl -X POST http://vision:8000/cameras/camera1/reconnect
|
curl -X POST http://localhost:8000/cameras/camera1/reconnect
|
||||||
|
|
||||||
# Full reset
|
# Full reset
|
||||||
curl -X POST http://vision:8000/cameras/camera1/full-reset
|
curl -X POST http://localhost:8000/cameras/camera1/full-reset
|
||||||
```
|
```
|
||||||
|
|
||||||
## 💾 Storage Management
|
## 💾 Storage Management
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Storage statistics
|
# Storage statistics
|
||||||
curl http://vision:8000/storage/stats
|
curl http://localhost:8000/storage/stats
|
||||||
|
|
||||||
# List files
|
# List files
|
||||||
curl -X POST http://vision:8000/storage/files \
|
curl -X POST http://localhost:8000/storage/files \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"camera_name": "camera1", "limit": 10}'
|
-d '{"camera_name": "camera1", "limit": 10}'
|
||||||
|
|
||||||
# Cleanup old files
|
# Cleanup old files
|
||||||
curl -X POST http://vision:8000/storage/cleanup \
|
curl -X POST http://localhost:8000/storage/cleanup \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"max_age_days": 30}'
|
-d '{"max_age_days": 30}'
|
||||||
```
|
```
|
||||||
@@ -120,17 +120,17 @@ curl -X POST http://vision:8000/storage/cleanup \
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# MQTT status
|
# MQTT status
|
||||||
curl http://vision:8000/mqtt/status
|
curl http://localhost:8000/mqtt/status
|
||||||
|
|
||||||
# Recent MQTT events
|
# Recent MQTT events
|
||||||
curl http://vision:8000/mqtt/events?limit=10
|
curl http://localhost:8000/mqtt/events?limit=10
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🌐 WebSocket Connection
|
## 🌐 WebSocket Connection
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Connect to real-time updates
|
// Connect to real-time updates
|
||||||
const ws = new WebSocket('ws://vision:8000/ws');
|
const ws = new WebSocket('ws://localhost:8000/ws');
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
const update = JSON.parse(event.data);
|
const update = JSON.parse(event.data);
|
||||||
|
|||||||
@@ -1,24 +1,20 @@
|
|||||||
# 🎥 MP4 Video Format Update - Frontend Integration Guide
|
# 🎥 MP4 Video Format Update - Frontend Integration Guide
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
The USDA Vision Camera System has been updated to record videos in **MP4 format** instead of AVI format for better streaming compatibility and smaller file sizes.
|
The USDA Vision Camera System has been updated to record videos in **MP4 format** instead of AVI format for better streaming compatibility and smaller file sizes.
|
||||||
|
|
||||||
## 🔄 What Changed
|
## 🔄 What Changed
|
||||||
|
|
||||||
### Video Format
|
### Video Format
|
||||||
|
|
||||||
- **Before**: AVI files with XVID codec (`.avi` extension)
|
- **Before**: AVI files with XVID codec (`.avi` extension)
|
||||||
- **After**: MP4 files with H.264 codec (`.mp4` extension)
|
- **After**: MP4 files with MPEG-4 codec (`.mp4` extension)
|
||||||
|
|
||||||
### File Extensions
|
### File Extensions
|
||||||
|
|
||||||
- All new video recordings now use `.mp4` extension
|
- All new video recordings now use `.mp4` extension
|
||||||
- Existing `.avi` files remain accessible and functional
|
- Existing `.avi` files remain accessible and functional
|
||||||
- File size reduction: ~40% smaller than equivalent AVI files
|
- File size reduction: ~40% smaller than equivalent AVI files
|
||||||
|
|
||||||
### API Response Updates
|
### API Response Updates
|
||||||
|
|
||||||
New fields added to camera configuration responses:
|
New fields added to camera configuration responses:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -32,17 +28,13 @@ New fields added to camera configuration responses:
|
|||||||
## 🌐 Frontend Impact
|
## 🌐 Frontend Impact
|
||||||
|
|
||||||
### 1. Video Player Compatibility
|
### 1. Video Player Compatibility
|
||||||
|
|
||||||
**✅ Better Browser Support**
|
**✅ Better Browser Support**
|
||||||
|
|
||||||
- MP4 format has native support in all modern browsers
|
- MP4 format has native support in all modern browsers
|
||||||
- No need for additional codecs or plugins
|
- No need for additional codecs or plugins
|
||||||
- Better mobile device compatibility (iOS/Android)
|
- Better mobile device compatibility (iOS/Android)
|
||||||
|
|
||||||
### 2. File Handling Updates
|
### 2. File Handling Updates
|
||||||
|
|
||||||
**File Extension Handling**
|
**File Extension Handling**
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Update file extension checks
|
// Update file extension checks
|
||||||
const isVideoFile = (filename) => {
|
const isVideoFile = (filename) => {
|
||||||
@@ -58,9 +50,7 @@ const getVideoMimeType = (filename) => {
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 3. Video Streaming
|
### 3. Video Streaming
|
||||||
|
|
||||||
**Improved Streaming Performance**
|
**Improved Streaming Performance**
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// MP4 files can be streamed directly without conversion
|
// MP4 files can be streamed directly without conversion
|
||||||
const videoUrl = `/api/videos/${videoId}/stream`;
|
const videoUrl = `/api/videos/${videoId}/stream`;
|
||||||
@@ -73,9 +63,7 @@ const videoUrl = `/api/videos/${videoId}/stream`;
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 4. File Size Display
|
### 4. File Size Display
|
||||||
|
|
||||||
**Updated Size Expectations**
|
**Updated Size Expectations**
|
||||||
|
|
||||||
- MP4 files are ~40% smaller than equivalent AVI files
|
- MP4 files are ~40% smaller than equivalent AVI files
|
||||||
- Update any file size warnings or storage calculations
|
- Update any file size warnings or storage calculations
|
||||||
- Better compression means faster downloads and uploads
|
- Better compression means faster downloads and uploads
|
||||||
@@ -83,11 +71,9 @@ const videoUrl = `/api/videos/${videoId}/stream`;
|
|||||||
## 📡 API Changes
|
## 📡 API Changes
|
||||||
|
|
||||||
### Camera Configuration Endpoint
|
### Camera Configuration Endpoint
|
||||||
|
|
||||||
**GET** `/cameras/{camera_name}/config`
|
**GET** `/cameras/{camera_name}/config`
|
||||||
|
|
||||||
**New Response Fields:**
|
**New Response Fields:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "camera1",
|
"name": "camera1",
|
||||||
@@ -109,9 +95,7 @@ const videoUrl = `/api/videos/${videoId}/stream`;
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Video Listing Endpoints
|
### Video Listing Endpoints
|
||||||
|
|
||||||
**File Extension Updates**
|
**File Extension Updates**
|
||||||
|
|
||||||
- Video files in responses will now have `.mp4` extensions
|
- Video files in responses will now have `.mp4` extensions
|
||||||
- Existing `.avi` files will still appear in listings
|
- Existing `.avi` files will still appear in listings
|
||||||
- Filter by both extensions when needed
|
- Filter by both extensions when needed
|
||||||
@@ -119,49 +103,42 @@ const videoUrl = `/api/videos/${videoId}/stream`;
|
|||||||
## 🔧 Configuration Options
|
## 🔧 Configuration Options
|
||||||
|
|
||||||
### Video Format Settings
|
### Video Format Settings
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"video_format": "mp4", // Options: "mp4", "avi"
|
"video_format": "mp4", // Options: "mp4", "avi"
|
||||||
"video_codec": "h264", // Options: "h264", "mp4v", "XVID", "MJPG"
|
"video_codec": "mp4v", // Options: "mp4v", "XVID", "MJPG"
|
||||||
"video_quality": 95 // Range: 0-100 (higher = better quality)
|
"video_quality": 95 // Range: 0-100 (higher = better quality)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Recommended Settings
|
### Recommended Settings
|
||||||
|
- **Production**: `"mp4"` format, `"mp4v"` codec, `95` quality
|
||||||
- **Production**: `"mp4"` format, `"h264"` codec, `95` quality
|
- **Storage Optimized**: `"mp4"` format, `"mp4v"` codec, `85` quality
|
||||||
- **Storage Optimized**: `"mp4"` format, `"h264"` codec, `85` quality
|
|
||||||
- **Legacy Mode**: `"avi"` format, `"XVID"` codec, `95` quality
|
- **Legacy Mode**: `"avi"` format, `"XVID"` codec, `95` quality
|
||||||
|
|
||||||
## 🎯 Frontend Implementation Checklist
|
## 🎯 Frontend Implementation Checklist
|
||||||
|
|
||||||
### ✅ Video Player Updates
|
### ✅ Video Player Updates
|
||||||
|
|
||||||
- [ ] Verify HTML5 video player works with MP4 files
|
- [ ] Verify HTML5 video player works with MP4 files
|
||||||
- [ ] Update video MIME type handling
|
- [ ] Update video MIME type handling
|
||||||
- [ ] Test streaming performance with new format
|
- [ ] Test streaming performance with new format
|
||||||
|
|
||||||
### ✅ File Management
|
### ✅ File Management
|
||||||
|
|
||||||
- [ ] Update file extension filters to include `.mp4`
|
- [ ] Update file extension filters to include `.mp4`
|
||||||
- [ ] Modify file type detection logic
|
- [ ] Modify file type detection logic
|
||||||
- [ ] Update download/upload handling for MP4 files
|
- [ ] Update download/upload handling for MP4 files
|
||||||
|
|
||||||
### ✅ UI/UX Updates
|
### ✅ UI/UX Updates
|
||||||
|
|
||||||
- [ ] Update file size expectations in UI
|
- [ ] Update file size expectations in UI
|
||||||
- [ ] Modify any format-specific icons or indicators
|
- [ ] Modify any format-specific icons or indicators
|
||||||
- [ ] Update help text or tooltips mentioning video formats
|
- [ ] Update help text or tooltips mentioning video formats
|
||||||
|
|
||||||
### ✅ Configuration Interface
|
### ✅ Configuration Interface
|
||||||
|
|
||||||
- [ ] Add video format settings to camera config UI
|
- [ ] Add video format settings to camera config UI
|
||||||
- [ ] Include video quality slider/selector
|
- [ ] Include video quality slider/selector
|
||||||
- [ ] Add restart warning for video format changes
|
- [ ] Add restart warning for video format changes
|
||||||
|
|
||||||
### ✅ Testing
|
### ✅ Testing
|
||||||
|
|
||||||
- [ ] Test video playback with new MP4 files
|
- [ ] Test video playback with new MP4 files
|
||||||
- [ ] Verify backward compatibility with existing AVI files
|
- [ ] Verify backward compatibility with existing AVI files
|
||||||
- [ ] Test streaming performance and loading times
|
- [ ] Test streaming performance and loading times
|
||||||
@@ -169,13 +146,11 @@ const videoUrl = `/api/videos/${videoId}/stream`;
|
|||||||
## 🔄 Backward Compatibility
|
## 🔄 Backward Compatibility
|
||||||
|
|
||||||
### Existing AVI Files
|
### Existing AVI Files
|
||||||
|
|
||||||
- All existing `.avi` files remain fully functional
|
- All existing `.avi` files remain fully functional
|
||||||
- No conversion or migration required
|
- No conversion or migration required
|
||||||
- Video player should handle both formats
|
- Video player should handle both formats
|
||||||
|
|
||||||
### API Compatibility
|
### API Compatibility
|
||||||
|
|
||||||
- All existing API endpoints continue to work
|
- All existing API endpoints continue to work
|
||||||
- New fields are additive (won't break existing code)
|
- New fields are additive (won't break existing code)
|
||||||
- Default values provided for new configuration fields
|
- Default values provided for new configuration fields
|
||||||
@@ -183,7 +158,6 @@ const videoUrl = `/api/videos/${videoId}/stream`;
|
|||||||
## 📊 Performance Benefits
|
## 📊 Performance Benefits
|
||||||
|
|
||||||
### File Size Reduction
|
### File Size Reduction
|
||||||
|
|
||||||
```
|
```
|
||||||
Example 5-minute recording at 1280x1024:
|
Example 5-minute recording at 1280x1024:
|
||||||
- AVI/XVID: ~180 MB
|
- AVI/XVID: ~180 MB
|
||||||
@@ -191,14 +165,12 @@ Example 5-minute recording at 1280x1024:
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Streaming Improvements
|
### Streaming Improvements
|
||||||
|
|
||||||
- Faster initial load times
|
- Faster initial load times
|
||||||
- Better progressive download support
|
- Better progressive download support
|
||||||
- Reduced bandwidth usage
|
- Reduced bandwidth usage
|
||||||
- Native browser optimization
|
- Native browser optimization
|
||||||
|
|
||||||
### Storage Efficiency
|
### Storage Efficiency
|
||||||
|
|
||||||
- More recordings fit in same storage space
|
- More recordings fit in same storage space
|
||||||
- Faster backup and transfer operations
|
- Faster backup and transfer operations
|
||||||
- Reduced storage costs over time
|
- Reduced storage costs over time
|
||||||
@@ -206,19 +178,16 @@ Example 5-minute recording at 1280x1024:
|
|||||||
## 🚨 Important Notes
|
## 🚨 Important Notes
|
||||||
|
|
||||||
### Restart Required
|
### Restart Required
|
||||||
|
|
||||||
- Video format changes require camera service restart
|
- Video format changes require camera service restart
|
||||||
- Mark video format settings as "restart required" in UI
|
- Mark video format settings as "restart required" in UI
|
||||||
- Provide clear user feedback about restart necessity
|
- Provide clear user feedback about restart necessity
|
||||||
|
|
||||||
### Browser Compatibility
|
### Browser Compatibility
|
||||||
|
|
||||||
- MP4 format supported in all modern browsers
|
- MP4 format supported in all modern browsers
|
||||||
- Better mobile device support than AVI
|
- Better mobile device support than AVI
|
||||||
- No additional plugins or codecs needed
|
- No additional plugins or codecs needed
|
||||||
|
|
||||||
### Quality Assurance
|
### Quality Assurance
|
||||||
|
|
||||||
- Video quality maintained at 95/100 setting
|
- Video quality maintained at 95/100 setting
|
||||||
- No visual degradation compared to AVI
|
- No visual degradation compared to AVI
|
||||||
- High bitrate ensures professional quality
|
- High bitrate ensures professional quality
|
||||||
|
|||||||
@@ -97,11 +97,11 @@ python test_system.py
|
|||||||
### Dashboard Integration
|
### Dashboard Integration
|
||||||
```javascript
|
```javascript
|
||||||
// React component example
|
// React component example
|
||||||
const systemStatus = await fetch('http://vision:8000/system/status');
|
const systemStatus = await fetch('http://localhost:8000/system/status');
|
||||||
const cameras = await fetch('http://vision:8000/cameras');
|
const cameras = await fetch('http://localhost:8000/cameras');
|
||||||
|
|
||||||
// WebSocket for real-time updates
|
// WebSocket for real-time updates
|
||||||
const ws = new WebSocket('ws://vision:8000/ws');
|
const ws = new WebSocket('ws://localhost:8000/ws');
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
const update = JSON.parse(event.data);
|
const update = JSON.parse(event.data);
|
||||||
// Handle real-time system updates
|
// Handle real-time system updates
|
||||||
@@ -111,13 +111,13 @@ ws.onmessage = (event) => {
|
|||||||
### Manual Control
|
### Manual Control
|
||||||
```bash
|
```bash
|
||||||
# Start recording manually
|
# Start recording manually
|
||||||
curl -X POST http://vision:8000/cameras/camera1/start-recording
|
curl -X POST http://localhost:8000/cameras/camera1/start-recording
|
||||||
|
|
||||||
# Stop recording manually
|
# Stop recording manually
|
||||||
curl -X POST http://vision:8000/cameras/camera1/stop-recording
|
curl -X POST http://localhost:8000/cameras/camera1/stop-recording
|
||||||
|
|
||||||
# Get system status
|
# Get system status
|
||||||
curl http://vision:8000/system/status
|
curl http://localhost:8000/system/status
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📊 System Capabilities
|
## 📊 System Capabilities
|
||||||
@@ -151,7 +151,7 @@ curl http://vision:8000/system/status
|
|||||||
### Troubleshooting
|
### Troubleshooting
|
||||||
- **Test Suite**: `python test_system.py`
|
- **Test Suite**: `python test_system.py`
|
||||||
- **Time Check**: `python check_time.py`
|
- **Time Check**: `python check_time.py`
|
||||||
- **API Health**: `curl http://vision:8000/health`
|
- **API Health**: `curl http://localhost:8000/health`
|
||||||
- **Debug Mode**: `python main.py --log-level DEBUG`
|
- **Debug Mode**: `python main.py --log-level DEBUG`
|
||||||
|
|
||||||
## 🎯 Production Readiness
|
## 🎯 Production Readiness
|
||||||
|
|||||||
@@ -48,6 +48,20 @@ Complete project overview and final status documentation. Contains:
|
|||||||
- Camera-specific settings comparison
|
- Camera-specific settings comparison
|
||||||
- MQTT topics and machine mappings
|
- MQTT topics and machine mappings
|
||||||
|
|
||||||
|
### 🎬 [VIDEO_STREAMING.md](VIDEO_STREAMING.md) **⭐ UPDATED**
|
||||||
|
**Complete video streaming module documentation**:
|
||||||
|
- Comprehensive API endpoint documentation
|
||||||
|
- Authentication and security information
|
||||||
|
- Error handling and troubleshooting
|
||||||
|
- Performance optimization guidelines
|
||||||
|
|
||||||
|
### 🤖 [AI_AGENT_VIDEO_INTEGRATION_GUIDE.md](AI_AGENT_VIDEO_INTEGRATION_GUIDE.md) **⭐ NEW**
|
||||||
|
**Complete integration guide for AI agents and external systems**:
|
||||||
|
- Step-by-step integration workflow
|
||||||
|
- Programming language examples (Python, JavaScript)
|
||||||
|
- Error handling and debugging strategies
|
||||||
|
- Performance optimization recommendations
|
||||||
|
|
||||||
### 🔧 [API_CHANGES_SUMMARY.md](API_CHANGES_SUMMARY.md)
|
### 🔧 [API_CHANGES_SUMMARY.md](API_CHANGES_SUMMARY.md)
|
||||||
Summary of API changes and enhancements made to the system.
|
Summary of API changes and enhancements made to the system.
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,16 @@ The USDA Vision Camera System now includes a modular video streaming system that
|
|||||||
|
|
||||||
## 🌟 Features
|
## 🌟 Features
|
||||||
|
|
||||||
- **HTTP Range Request Support** - Enables seeking and progressive download
|
- **Progressive Streaming** - True chunked streaming for web browsers (no download required)
|
||||||
- **Native MP4 Support** - Direct streaming of MP4 files with automatic AVI conversion
|
- **HTTP Range Request Support** - Enables seeking and progressive download with 206 Partial Content
|
||||||
- **Intelligent Caching** - Optimized streaming performance
|
- **Native MP4 Support** - Direct streaming of MP4 files optimized for web playback
|
||||||
|
- **Memory Efficient** - 8KB chunked delivery, no large file loading into memory
|
||||||
|
- **Browser Compatible** - Works with HTML5 `<video>` tag in all modern browsers
|
||||||
|
- **Intelligent Caching** - Optimized streaming performance with byte-range caching
|
||||||
- **Thumbnail Generation** - Extract preview images from videos
|
- **Thumbnail Generation** - Extract preview images from videos
|
||||||
- **Modular Architecture** - Clean separation of concerns
|
- **Modular Architecture** - Clean separation of concerns
|
||||||
|
- **No Authentication Required** - Open access for internal network use
|
||||||
|
- **CORS Enabled** - Ready for web browser integration
|
||||||
|
|
||||||
## 🏗️ Architecture
|
## 🏗️ Architecture
|
||||||
|
|
||||||
@@ -30,11 +35,16 @@ usda_vision_system/video/
|
|||||||
GET /videos/
|
GET /videos/
|
||||||
```
|
```
|
||||||
**Query Parameters:**
|
**Query Parameters:**
|
||||||
- `camera_name` - Filter by camera
|
- `camera_name` (optional): Filter by camera name
|
||||||
- `start_date` - Filter by date range
|
- `start_date` (optional): Filter videos created after this date (ISO format: 2025-08-04T14:30:22)
|
||||||
- `end_date` - Filter by date range
|
- `end_date` (optional): Filter videos created before this date (ISO format: 2025-08-04T14:30:22)
|
||||||
- `limit` - Maximum results (default: 50)
|
- `limit` (optional): Maximum results (default: 50, max: 1000)
|
||||||
- `include_metadata` - Include video metadata
|
- `include_metadata` (optional): Include video metadata (default: false)
|
||||||
|
|
||||||
|
**Example Request:**
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8000/videos/?camera_name=camera1&include_metadata=true&limit=10"
|
||||||
|
```
|
||||||
|
|
||||||
**Response:**
|
**Response:**
|
||||||
```json
|
```json
|
||||||
@@ -48,8 +58,20 @@ GET /videos/
|
|||||||
"format": "mp4",
|
"format": "mp4",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"created_at": "2025-08-04T14:30:22",
|
"created_at": "2025-08-04T14:30:22",
|
||||||
|
"start_time": "2025-08-04T14:30:22",
|
||||||
|
"end_time": "2025-08-04T14:32:22",
|
||||||
|
"machine_trigger": "blower_separator",
|
||||||
"is_streamable": true,
|
"is_streamable": true,
|
||||||
"needs_conversion": true
|
"needs_conversion": false,
|
||||||
|
"metadata": {
|
||||||
|
"duration_seconds": 120.5,
|
||||||
|
"width": 1920,
|
||||||
|
"height": 1080,
|
||||||
|
"fps": 30.0,
|
||||||
|
"codec": "mp4v",
|
||||||
|
"bitrate": 5000000,
|
||||||
|
"aspect_ratio": 1.777
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"total_count": 1
|
"total_count": 1
|
||||||
@@ -61,28 +83,70 @@ GET /videos/
|
|||||||
GET /videos/{file_id}/stream
|
GET /videos/{file_id}/stream
|
||||||
```
|
```
|
||||||
**Headers:**
|
**Headers:**
|
||||||
- `Range: bytes=0-1023` - Request specific byte range
|
- `Range: bytes=0-1023` (optional): Request specific byte range for seeking
|
||||||
|
|
||||||
**Features:**
|
**Example Requests:**
|
||||||
- Supports HTTP range requests for seeking
|
```bash
|
||||||
- Returns 206 Partial Content for range requests
|
# Stream entire video (progressive streaming)
|
||||||
- Automatic format conversion for web compatibility
|
curl http://localhost:8000/videos/camera1_auto_blower_separator_20250805_123329.mp4/stream
|
||||||
- Intelligent caching for performance
|
|
||||||
|
# Stream specific byte range (for seeking)
|
||||||
|
curl -H "Range: bytes=0-1023" \
|
||||||
|
http://localhost:8000/videos/camera1_auto_blower_separator_20250805_123329.mp4/stream
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response Headers:**
|
||||||
|
- `Accept-Ranges: bytes`
|
||||||
|
- `Content-Length: {size}`
|
||||||
|
- `Content-Range: bytes {start}-{end}/{total}` (for range requests)
|
||||||
|
- `Cache-Control: public, max-age=3600`
|
||||||
|
- `Content-Type: video/mp4`
|
||||||
|
|
||||||
|
**Streaming Implementation:**
|
||||||
|
- ✅ **Progressive Streaming**: Uses FastAPI `StreamingResponse` with 8KB chunks
|
||||||
|
- ✅ **HTTP Range Requests**: Returns 206 Partial Content for seeking
|
||||||
|
- ✅ **Memory Efficient**: No large file loading, streams directly from disk
|
||||||
|
- ✅ **Browser Compatible**: Works with HTML5 `<video>` tag playback
|
||||||
|
- ✅ **Chunked Delivery**: Optimal 8KB chunk size for smooth playback
|
||||||
|
- ✅ **CORS Enabled**: Ready for web browser integration
|
||||||
|
|
||||||
|
**Response Status Codes:**
|
||||||
|
- `200 OK`: Full video streaming (progressive chunks)
|
||||||
|
- `206 Partial Content`: Range request successful
|
||||||
|
- `404 Not Found`: Video not found or not streamable
|
||||||
|
- `416 Range Not Satisfiable`: Invalid range request
|
||||||
|
|
||||||
### Get Video Info
|
### Get Video Info
|
||||||
```http
|
```http
|
||||||
GET /videos/{file_id}
|
GET /videos/{file_id}
|
||||||
```
|
```
|
||||||
**Response includes metadata:**
|
**Example Request:**
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/videos/camera1_recording_20250804_143022.avi
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response includes complete metadata:**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"file_id": "camera1_recording_20250804_143022.avi",
|
"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",
|
||||||
|
"start_time": "2025-08-04T14:30:22",
|
||||||
|
"end_time": "2025-08-04T14:32:22",
|
||||||
|
"machine_trigger": "vibratory_conveyor",
|
||||||
|
"is_streamable": true,
|
||||||
|
"needs_conversion": true,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"duration_seconds": 120.5,
|
"duration_seconds": 120.5,
|
||||||
"width": 1920,
|
"width": 1920,
|
||||||
"height": 1080,
|
"height": 1080,
|
||||||
"fps": 30.0,
|
"fps": 30.0,
|
||||||
"codec": "XVID",
|
"codec": "XVID",
|
||||||
|
"bitrate": 5000000,
|
||||||
"aspect_ratio": 1.777
|
"aspect_ratio": 1.777
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,13 +156,31 @@ GET /videos/{file_id}
|
|||||||
```http
|
```http
|
||||||
GET /videos/{file_id}/thumbnail?timestamp=5.0&width=320&height=240
|
GET /videos/{file_id}/thumbnail?timestamp=5.0&width=320&height=240
|
||||||
```
|
```
|
||||||
Returns JPEG thumbnail image.
|
**Query Parameters:**
|
||||||
|
- `timestamp` (optional): Time position in seconds to extract thumbnail from (default: 1.0)
|
||||||
|
- `width` (optional): Thumbnail width in pixels (default: 320)
|
||||||
|
- `height` (optional): Thumbnail height in pixels (default: 240)
|
||||||
|
|
||||||
|
**Example Request:**
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8000/videos/camera1_recording_20250804_143022.avi/thumbnail?timestamp=5.0&width=320&height=240" \
|
||||||
|
--output thumbnail.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**: JPEG image data with caching headers
|
||||||
|
- `Content-Type: image/jpeg`
|
||||||
|
- `Cache-Control: public, max-age=3600`
|
||||||
|
|
||||||
### Streaming Info
|
### Streaming Info
|
||||||
```http
|
```http
|
||||||
GET /videos/{file_id}/info
|
GET /videos/{file_id}/info
|
||||||
```
|
```
|
||||||
Returns technical streaming details:
|
**Example Request:**
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/videos/camera1_recording_20250804_143022.avi/info
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**: Technical streaming details
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"file_id": "camera1_recording_20250804_143022.avi",
|
"file_id": "camera1_recording_20250804_143022.avi",
|
||||||
@@ -109,16 +191,73 @@ Returns technical streaming details:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Video Validation
|
||||||
|
```http
|
||||||
|
POST /videos/{file_id}/validate
|
||||||
|
```
|
||||||
|
**Example Request:**
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/videos/camera1_recording_20250804_143022.avi/validate
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**: Validation status
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"file_id": "camera1_recording_20250804_143022.avi",
|
||||||
|
"is_valid": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cache Management
|
||||||
|
```http
|
||||||
|
POST /videos/{file_id}/cache/invalidate
|
||||||
|
```
|
||||||
|
**Example Request:**
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/videos/camera1_recording_20250804_143022.avi/cache/invalidate
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**: Cache invalidation status
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"file_id": "camera1_recording_20250804_143022.avi",
|
||||||
|
"cache_invalidated": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Admin: Cache Cleanup
|
||||||
|
```http
|
||||||
|
POST /admin/videos/cache/cleanup?max_size_mb=100
|
||||||
|
```
|
||||||
|
**Example Request:**
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:8000/admin/videos/cache/cleanup?max_size_mb=100"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**: Cache cleanup results
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cache_cleaned": true,
|
||||||
|
"entries_removed": 15,
|
||||||
|
"max_size_mb": 100
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## 🌐 React Integration
|
## 🌐 React Integration
|
||||||
|
|
||||||
### Basic Video Player
|
### Basic Video Player
|
||||||
```jsx
|
```jsx
|
||||||
function VideoPlayer({ fileId }) {
|
function VideoPlayer({ fileId }) {
|
||||||
return (
|
return (
|
||||||
<video controls width="100%">
|
<video
|
||||||
<source
|
controls
|
||||||
src={`${API_BASE_URL}/videos/${fileId}/stream`}
|
width="100%"
|
||||||
type="video/mp4"
|
preload="metadata"
|
||||||
|
style={{ maxWidth: '800px' }}
|
||||||
|
>
|
||||||
|
<source
|
||||||
|
src={`${API_BASE_URL}/videos/${fileId}/stream`}
|
||||||
|
type="video/mp4"
|
||||||
/>
|
/>
|
||||||
Your browser does not support video playback.
|
Your browser does not support video playback.
|
||||||
</video>
|
</video>
|
||||||
@@ -187,6 +326,101 @@ video_module = create_video_module(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Configuration Parameters
|
||||||
|
- **`enable_caching`**: Enable/disable intelligent byte-range caching (default: True)
|
||||||
|
- **`cache_size_mb`**: Maximum cache size in MB (default: 100)
|
||||||
|
- **`cache_max_age_minutes`**: Cache entry expiration time (default: 30)
|
||||||
|
- **`enable_conversion`**: Enable/disable automatic AVI to MP4 conversion (default: True)
|
||||||
|
- **`conversion_quality`**: Video conversion quality: "low", "medium", "high" (default: "medium")
|
||||||
|
|
||||||
|
### System Requirements
|
||||||
|
- **OpenCV**: Required for thumbnail generation and metadata extraction
|
||||||
|
- **FFmpeg**: Optional, for video format conversion (graceful fallback if not available)
|
||||||
|
- **Storage**: Sufficient disk space for video files and cache
|
||||||
|
- **Memory**: Recommended 2GB+ RAM for caching and video processing
|
||||||
|
|
||||||
|
## 🔐 Authentication & Security
|
||||||
|
|
||||||
|
### Current Security Model
|
||||||
|
**⚠️ IMPORTANT: No authentication is currently implemented.**
|
||||||
|
|
||||||
|
- **Open Access**: All video streaming endpoints are publicly accessible
|
||||||
|
- **CORS Policy**: Currently set to allow all origins (`allow_origins=["*"]`)
|
||||||
|
- **Network Security**: Designed for internal network use only
|
||||||
|
- **No API Keys**: No authentication tokens or API keys required
|
||||||
|
- **No Rate Limiting**: No request rate limiting currently implemented
|
||||||
|
|
||||||
|
### Security Considerations for Production
|
||||||
|
|
||||||
|
#### For Internal Network Deployment
|
||||||
|
```bash
|
||||||
|
# Current configuration is suitable for:
|
||||||
|
# - Internal corporate networks
|
||||||
|
# - Isolated network segments
|
||||||
|
# - Development and testing environments
|
||||||
|
```
|
||||||
|
|
||||||
|
#### For External Access (Recommendations)
|
||||||
|
If you need to expose the video streaming API externally, consider implementing:
|
||||||
|
|
||||||
|
1. **Authentication Layer**
|
||||||
|
```python
|
||||||
|
# Example: Add JWT authentication
|
||||||
|
from fastapi import Depends, HTTPException
|
||||||
|
from fastapi.security import HTTPBearer
|
||||||
|
|
||||||
|
security = HTTPBearer()
|
||||||
|
|
||||||
|
async def verify_token(token: str = Depends(security)):
|
||||||
|
# Implement token verification logic
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **CORS Configuration**
|
||||||
|
```python
|
||||||
|
# Restrict CORS to specific domains
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["https://yourdomain.com"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["GET", "POST"],
|
||||||
|
allow_headers=["*"]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Rate Limiting**
|
||||||
|
```python
|
||||||
|
# Example: Add rate limiting
|
||||||
|
from slowapi import Limiter
|
||||||
|
|
||||||
|
limiter = Limiter(key_func=get_remote_address)
|
||||||
|
|
||||||
|
@app.get("/videos/")
|
||||||
|
@limiter.limit("10/minute")
|
||||||
|
async def list_videos():
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Network Security**
|
||||||
|
- Use HTTPS/TLS for encrypted communication
|
||||||
|
- Implement firewall rules to restrict access
|
||||||
|
- Consider VPN access for remote users
|
||||||
|
- Use reverse proxy (nginx) for additional security
|
||||||
|
|
||||||
|
### Access Control Summary
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Current Access Model │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ Authentication: ❌ None │
|
||||||
|
│ Authorization: ❌ None │
|
||||||
|
│ CORS: ✅ Enabled (all origins) │
|
||||||
|
│ Rate Limiting: ❌ None │
|
||||||
|
│ HTTPS: ⚠️ Depends on deployment │
|
||||||
|
│ Network Security: ⚠️ Firewall/VPN recommended │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
## 📊 Performance
|
## 📊 Performance
|
||||||
|
|
||||||
- **Caching**: Intelligent byte-range caching reduces disk I/O
|
- **Caching**: Intelligent byte-range caching reduces disk I/O
|
||||||
@@ -204,10 +438,10 @@ sudo systemctl restart usda-vision-camera
|
|||||||
### Check Status
|
### Check Status
|
||||||
```bash
|
```bash
|
||||||
# Check video module status
|
# Check video module status
|
||||||
curl http://vision:8000/system/video-module
|
curl http://localhost:8000/system/video-module
|
||||||
|
|
||||||
# Check available videos
|
# Check available videos
|
||||||
curl http://vision:8000/videos/
|
curl http://localhost:8000/videos/
|
||||||
```
|
```
|
||||||
|
|
||||||
### Logs
|
### Logs
|
||||||
@@ -226,18 +460,136 @@ PYTHONPATH=/home/alireza/USDA-vision-cameras python tests/test_video_module.py
|
|||||||
## 🔍 Troubleshooting
|
## 🔍 Troubleshooting
|
||||||
|
|
||||||
### Video Not Playing
|
### Video Not Playing
|
||||||
1. Check if file exists: `GET /videos/{file_id}`
|
1. **Check if file exists**: `GET /videos/{file_id}`
|
||||||
2. Verify streaming info: `GET /videos/{file_id}/info`
|
```bash
|
||||||
3. Test direct stream: `GET /videos/{file_id}/stream`
|
curl http://localhost:8000/videos/camera1_recording_20250804_143022.avi
|
||||||
|
```
|
||||||
|
2. **Verify streaming info**: `GET /videos/{file_id}/info`
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/videos/camera1_recording_20250804_143022.avi/info
|
||||||
|
```
|
||||||
|
3. **Test direct stream**: `GET /videos/{file_id}/stream`
|
||||||
|
```bash
|
||||||
|
curl -I http://localhost:8000/videos/camera1_recording_20250804_143022.avi/stream
|
||||||
|
```
|
||||||
|
4. **Validate video file**: `POST /videos/{file_id}/validate`
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/videos/camera1_recording_20250804_143022.avi/validate
|
||||||
|
```
|
||||||
|
|
||||||
### Performance Issues
|
### Performance Issues
|
||||||
1. Check cache status: `GET /admin/videos/cache/cleanup`
|
1. **Check cache status**: Clean up cache if needed
|
||||||
2. Monitor system resources
|
```bash
|
||||||
3. Adjust cache size in configuration
|
curl -X POST "http://localhost:8000/admin/videos/cache/cleanup?max_size_mb=100"
|
||||||
|
```
|
||||||
|
2. **Monitor system resources**: Check CPU, memory, and disk usage
|
||||||
|
3. **Adjust cache size**: Modify configuration parameters
|
||||||
|
4. **Invalidate specific cache**: For updated files
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/videos/{file_id}/cache/invalidate
|
||||||
|
```
|
||||||
|
|
||||||
### Format Issues
|
### Format Issues
|
||||||
- AVI files are automatically converted to MP4 for web compatibility
|
- **AVI files**: Automatically converted to MP4 for web compatibility
|
||||||
- Conversion requires FFmpeg (optional, graceful fallback)
|
- **Conversion requires FFmpeg**: Optional dependency with graceful fallback
|
||||||
|
- **Supported formats**: AVI (with conversion), MP4 (native), WebM (native)
|
||||||
|
|
||||||
|
### Common HTTP Status Codes
|
||||||
|
- **200**: Success - Video streamed successfully
|
||||||
|
- **206**: Partial Content - Range request successful
|
||||||
|
- **404**: Not Found - Video file doesn't exist or isn't streamable
|
||||||
|
- **416**: Range Not Satisfiable - Invalid range request
|
||||||
|
- **500**: Internal Server Error - Failed to read video data or generate thumbnail
|
||||||
|
|
||||||
|
### Browser Compatibility
|
||||||
|
- **Chrome/Chromium**: Full support for MP4 and range requests
|
||||||
|
- **Firefox**: Full support for MP4 and range requests
|
||||||
|
- **Safari**: Full support for MP4 and range requests
|
||||||
|
- **Edge**: Full support for MP4 and range requests
|
||||||
|
- **Mobile browsers**: Generally good support for MP4 streaming
|
||||||
|
|
||||||
|
### Error Scenarios and Solutions
|
||||||
|
|
||||||
|
#### Video File Issues
|
||||||
|
```bash
|
||||||
|
# Problem: Video not found (404)
|
||||||
|
curl http://localhost:8000/videos/nonexistent_video.mp4
|
||||||
|
# Response: {"detail": "Video nonexistent_video.mp4 not found"}
|
||||||
|
# Solution: Verify file_id exists using list endpoint
|
||||||
|
|
||||||
|
# Problem: Video not streamable
|
||||||
|
curl http://localhost:8000/videos/corrupted_video.avi/stream
|
||||||
|
# Response: {"detail": "Video corrupted_video.avi not found or not streamable"}
|
||||||
|
# Solution: Use validation endpoint to check file integrity
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Range Request Issues
|
||||||
|
```bash
|
||||||
|
# Problem: Invalid range request (416)
|
||||||
|
curl -H "Range: bytes=999999999-" http://localhost:8000/videos/small_video.mp4/stream
|
||||||
|
# Response: {"detail": "Invalid range request: Range exceeds file size"}
|
||||||
|
# Solution: Check file size first using /info endpoint
|
||||||
|
|
||||||
|
# Problem: Malformed range header
|
||||||
|
curl -H "Range: invalid-range" http://localhost:8000/videos/video.mp4/stream
|
||||||
|
# Response: {"detail": "Invalid range request: Malformed range header"}
|
||||||
|
# Solution: Use proper range format: "bytes=start-end"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Thumbnail Generation Issues
|
||||||
|
```bash
|
||||||
|
# Problem: Thumbnail generation failed (404)
|
||||||
|
curl http://localhost:8000/videos/audio_only.mp4/thumbnail
|
||||||
|
# Response: {"detail": "Could not generate thumbnail for audio_only.mp4"}
|
||||||
|
# Solution: Verify video has visual content and is not audio-only
|
||||||
|
|
||||||
|
# Problem: Invalid timestamp
|
||||||
|
curl "http://localhost:8000/videos/short_video.mp4/thumbnail?timestamp=999"
|
||||||
|
# Response: Returns thumbnail from last available frame
|
||||||
|
# Solution: Check video duration first using metadata
|
||||||
|
```
|
||||||
|
|
||||||
|
#### System Resource Issues
|
||||||
|
```bash
|
||||||
|
# Problem: Cache full or system overloaded (500)
|
||||||
|
curl http://localhost:8000/videos/large_video.mp4/stream
|
||||||
|
# Response: {"detail": "Failed to read video data"}
|
||||||
|
# Solution: Clean cache or wait for system resources
|
||||||
|
curl -X POST "http://localhost:8000/admin/videos/cache/cleanup?max_size_mb=50"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debugging Workflow
|
||||||
|
```bash
|
||||||
|
# Step 1: Check system health
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
|
||||||
|
# Step 2: Verify video exists and get info
|
||||||
|
curl http://localhost:8000/videos/your_video_id
|
||||||
|
|
||||||
|
# Step 3: Check streaming capabilities
|
||||||
|
curl http://localhost:8000/videos/your_video_id/info
|
||||||
|
|
||||||
|
# Step 4: Validate video file
|
||||||
|
curl -X POST http://localhost:8000/videos/your_video_id/validate
|
||||||
|
|
||||||
|
# Step 5: Test basic streaming
|
||||||
|
curl -I http://localhost:8000/videos/your_video_id/stream
|
||||||
|
|
||||||
|
# Step 6: Test range request
|
||||||
|
curl -I -H "Range: bytes=0-1023" http://localhost:8000/videos/your_video_id/stream
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance Monitoring
|
||||||
|
```bash
|
||||||
|
# Monitor cache usage
|
||||||
|
curl -X POST "http://localhost:8000/admin/videos/cache/cleanup?max_size_mb=100"
|
||||||
|
|
||||||
|
# Check system resources
|
||||||
|
curl http://localhost:8000/system/status
|
||||||
|
|
||||||
|
# Monitor video module status
|
||||||
|
curl http://localhost:8000/videos/ | jq '.total_count'
|
||||||
|
```
|
||||||
|
|
||||||
## 🎯 Next Steps
|
## 🎯 Next Steps
|
||||||
|
|
||||||
|
|||||||
302
API Documentations/docs/WEB_AI_AGENT_VIDEO_INTEGRATION.md
Normal file
302
API Documentations/docs/WEB_AI_AGENT_VIDEO_INTEGRATION.md
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
# 🤖 Web AI Agent - Video Integration Guide
|
||||||
|
|
||||||
|
This guide provides the essential information for integrating USDA Vision Camera video streaming into your web application.
|
||||||
|
|
||||||
|
## 🎯 Quick Start
|
||||||
|
|
||||||
|
### Video Streaming Status: ✅ READY
|
||||||
|
- **Progressive streaming implemented** - Videos play in browsers (no download)
|
||||||
|
- **86 MP4 files available** - All properly indexed and streamable
|
||||||
|
- **HTTP range requests supported** - Seeking and progressive playback work
|
||||||
|
- **Memory efficient** - 8KB chunked delivery
|
||||||
|
|
||||||
|
## 🚀 API Endpoints
|
||||||
|
|
||||||
|
### Base URL
|
||||||
|
```
|
||||||
|
http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1. List Available Videos
|
||||||
|
```http
|
||||||
|
GET /videos/?camera_name={camera}&limit={limit}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8000/videos/?camera_name=camera1&limit=10"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"videos": [
|
||||||
|
{
|
||||||
|
"file_id": "camera1_auto_blower_separator_20250805_123329.mp4",
|
||||||
|
"camera_name": "camera1",
|
||||||
|
"file_size_bytes": 1072014489,
|
||||||
|
"format": "mp4",
|
||||||
|
"status": "completed",
|
||||||
|
"is_streamable": true,
|
||||||
|
"created_at": "2025-08-05T12:43:12.631210"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total_count": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Stream Video (Progressive)
|
||||||
|
```http
|
||||||
|
GET /videos/{file_id}/stream
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8000/videos/camera1_auto_blower_separator_20250805_123329.mp4/stream"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- ✅ Progressive streaming (8KB chunks)
|
||||||
|
- ✅ HTTP range requests (206 Partial Content)
|
||||||
|
- ✅ Browser compatible (HTML5 video)
|
||||||
|
- ✅ Seeking support
|
||||||
|
- ✅ No authentication required
|
||||||
|
|
||||||
|
### 3. Get Video Thumbnail
|
||||||
|
```http
|
||||||
|
GET /videos/{file_id}/thumbnail?timestamp={seconds}&width={px}&height={px}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8000/videos/camera1_auto_blower_separator_20250805_123329.mp4/thumbnail?timestamp=5.0&width=320&height=240"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌐 Web Integration
|
||||||
|
|
||||||
|
### HTML5 Video Player
|
||||||
|
```html
|
||||||
|
<video controls width="100%" preload="metadata">
|
||||||
|
<source src="http://localhost:8000/videos/{file_id}/stream" type="video/mp4">
|
||||||
|
Your browser does not support video playback.
|
||||||
|
</video>
|
||||||
|
```
|
||||||
|
|
||||||
|
### React Component
|
||||||
|
```jsx
|
||||||
|
function VideoPlayer({ fileId, width = "100%" }) {
|
||||||
|
const streamUrl = `http://localhost:8000/videos/${fileId}/stream`;
|
||||||
|
const thumbnailUrl = `http://localhost:8000/videos/${fileId}/thumbnail`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<video
|
||||||
|
controls
|
||||||
|
width={width}
|
||||||
|
preload="metadata"
|
||||||
|
poster={thumbnailUrl}
|
||||||
|
style={{ maxWidth: '800px', borderRadius: '8px' }}
|
||||||
|
>
|
||||||
|
<source src={streamUrl} type="video/mp4" />
|
||||||
|
Your browser does not support video playback.
|
||||||
|
</video>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Video List Component
|
||||||
|
```jsx
|
||||||
|
function VideoList({ cameraName = null, limit = 20 }) {
|
||||||
|
const [videos, setVideos] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (cameraName) params.append('camera_name', cameraName);
|
||||||
|
params.append('limit', limit.toString());
|
||||||
|
|
||||||
|
fetch(`http://localhost:8000/videos/?${params}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
// Filter only streamable MP4 videos
|
||||||
|
const streamableVideos = data.videos.filter(
|
||||||
|
v => v.format === 'mp4' && v.is_streamable
|
||||||
|
);
|
||||||
|
setVideos(streamableVideos);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading videos:', error);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, [cameraName, limit]);
|
||||||
|
|
||||||
|
if (loading) return <div>Loading videos...</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="video-grid">
|
||||||
|
{videos.map(video => (
|
||||||
|
<div key={video.file_id} className="video-card">
|
||||||
|
<h3>{video.file_id}</h3>
|
||||||
|
<p>Camera: {video.camera_name}</p>
|
||||||
|
<p>Size: {(video.file_size_bytes / 1024 / 1024).toFixed(1)} MB</p>
|
||||||
|
<VideoPlayer fileId={video.file_id} width="100%" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Available Data
|
||||||
|
|
||||||
|
### Current Video Inventory
|
||||||
|
- **Total Videos**: 161 files
|
||||||
|
- **MP4 Files**: 86 (all streamable ✅)
|
||||||
|
- **AVI Files**: 75 (legacy format, not prioritized)
|
||||||
|
- **Cameras**: camera1, camera2
|
||||||
|
- **Date Range**: July 29 - August 5, 2025
|
||||||
|
|
||||||
|
### Video File Naming Convention
|
||||||
|
```
|
||||||
|
{camera}_{trigger}_{machine}_{YYYYMMDD}_{HHMMSS}.mp4
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
- `camera1_auto_blower_separator_20250805_123329.mp4`
|
||||||
|
- `camera2_auto_vibratory_conveyor_20250805_123042.mp4`
|
||||||
|
- `20250804_161305_manual_camera1_2025-08-04T20-13-09-634Z.mp4`
|
||||||
|
|
||||||
|
### Machine Triggers
|
||||||
|
- `auto_blower_separator` - Automatic recording triggered by blower separator
|
||||||
|
- `auto_vibratory_conveyor` - Automatic recording triggered by vibratory conveyor
|
||||||
|
- `manual` - Manual recording initiated by user
|
||||||
|
|
||||||
|
## 🔧 Technical Details
|
||||||
|
|
||||||
|
### Streaming Implementation
|
||||||
|
- **Method**: FastAPI `StreamingResponse` with async generators
|
||||||
|
- **Chunk Size**: 8KB for optimal performance
|
||||||
|
- **Range Requests**: Full HTTP/1.1 range request support
|
||||||
|
- **Status Codes**: 200 (full), 206 (partial), 404 (not found)
|
||||||
|
- **CORS**: Enabled for all origins
|
||||||
|
- **Caching**: Server-side byte-range caching
|
||||||
|
|
||||||
|
### Browser Compatibility
|
||||||
|
- ✅ Chrome/Chromium
|
||||||
|
- ✅ Firefox
|
||||||
|
- ✅ Safari
|
||||||
|
- ✅ Edge
|
||||||
|
- ✅ Mobile browsers
|
||||||
|
|
||||||
|
### Performance Characteristics
|
||||||
|
- **Memory Usage**: Low (8KB chunks, no large file loading)
|
||||||
|
- **Seeking**: Instant (HTTP range requests)
|
||||||
|
- **Startup Time**: Fast (metadata preload)
|
||||||
|
- **Bandwidth**: Adaptive (only downloads viewed portions)
|
||||||
|
|
||||||
|
## 🛠️ Error Handling
|
||||||
|
|
||||||
|
### Common Scenarios
|
||||||
|
```javascript
|
||||||
|
// Check if video is streamable
|
||||||
|
const checkVideo = async (fileId) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`http://localhost:8000/videos/${fileId}`);
|
||||||
|
const video = await response.json();
|
||||||
|
|
||||||
|
if (!video.is_streamable) {
|
||||||
|
console.warn(`Video ${fileId} is not streamable`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error checking video ${fileId}:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle video loading errors
|
||||||
|
const VideoPlayerWithErrorHandling = ({ fileId }) => {
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
const handleError = (e) => {
|
||||||
|
console.error('Video playback error:', e);
|
||||||
|
setError('Failed to load video. Please try again.');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className="error">❌ {error}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<video
|
||||||
|
controls
|
||||||
|
onError={handleError}
|
||||||
|
src={`http://localhost:8000/videos/${fileId}/stream`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTP Status Codes
|
||||||
|
- `200 OK` - Video streaming successfully
|
||||||
|
- `206 Partial Content` - Range request successful
|
||||||
|
- `404 Not Found` - Video not found or not streamable
|
||||||
|
- `416 Range Not Satisfiable` - Invalid range request
|
||||||
|
- `500 Internal Server Error` - Server error reading video
|
||||||
|
|
||||||
|
## 🔐 Security Notes
|
||||||
|
|
||||||
|
### Current Configuration
|
||||||
|
- **Authentication**: None (open access)
|
||||||
|
- **CORS**: Enabled for all origins
|
||||||
|
- **Network**: Designed for internal use
|
||||||
|
- **HTTPS**: Not required (HTTP works)
|
||||||
|
|
||||||
|
### For Production Use
|
||||||
|
Consider implementing:
|
||||||
|
- Authentication/authorization
|
||||||
|
- Rate limiting
|
||||||
|
- HTTPS/TLS encryption
|
||||||
|
- Network access controls
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
### Quick Test
|
||||||
|
```bash
|
||||||
|
# Test video listing
|
||||||
|
curl "http://localhost:8000/videos/?limit=5"
|
||||||
|
|
||||||
|
# Test video streaming
|
||||||
|
curl -I "http://localhost:8000/videos/camera1_auto_blower_separator_20250805_123329.mp4/stream"
|
||||||
|
|
||||||
|
# Test range request
|
||||||
|
curl -H "Range: bytes=0-1023" "http://localhost:8000/videos/camera1_auto_blower_separator_20250805_123329.mp4/stream" -o test_chunk.mp4
|
||||||
|
```
|
||||||
|
|
||||||
|
### Browser Test
|
||||||
|
Open: `file:///home/alireza/USDA-vision-cameras/test_video_streaming.html`
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
### Service Management
|
||||||
|
```bash
|
||||||
|
# Restart video service
|
||||||
|
sudo systemctl restart usda-vision-camera
|
||||||
|
|
||||||
|
# Check service status
|
||||||
|
sudo systemctl status usda-vision-camera
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
sudo journalctl -u usda-vision-camera -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Health Check
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**✅ Ready for Integration**: The video streaming system is fully operational and ready for web application integration. All MP4 files are streamable with progressive playback support.
|
||||||
@@ -185,7 +185,7 @@ POST /cameras/{camera_name}/apply-config
|
|||||||
|
|
||||||
### Example 1: Adjust Exposure and Gain
|
### Example 1: Adjust Exposure and Gain
|
||||||
```bash
|
```bash
|
||||||
curl -X PUT http://vision:8000/cameras/camera1/config \
|
curl -X PUT http://localhost:8000/cameras/camera1/config \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"exposure_ms": 1.5,
|
"exposure_ms": 1.5,
|
||||||
@@ -195,7 +195,7 @@ curl -X PUT http://vision:8000/cameras/camera1/config \
|
|||||||
|
|
||||||
### Example 2: Improve Image Quality
|
### Example 2: Improve Image Quality
|
||||||
```bash
|
```bash
|
||||||
curl -X PUT http://vision:8000/cameras/camera1/config \
|
curl -X PUT http://localhost:8000/cameras/camera1/config \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"sharpness": 150,
|
"sharpness": 150,
|
||||||
@@ -206,7 +206,7 @@ curl -X PUT http://vision:8000/cameras/camera1/config \
|
|||||||
|
|
||||||
### Example 3: Configure for Indoor Lighting
|
### Example 3: Configure for Indoor Lighting
|
||||||
```bash
|
```bash
|
||||||
curl -X PUT http://vision:8000/cameras/camera1/config \
|
curl -X PUT http://localhost:8000/cameras/camera1/config \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"anti_flicker_enabled": true,
|
"anti_flicker_enabled": true,
|
||||||
@@ -218,7 +218,7 @@ curl -X PUT http://vision:8000/cameras/camera1/config \
|
|||||||
|
|
||||||
### Example 4: Enable HDR Mode
|
### Example 4: Enable HDR Mode
|
||||||
```bash
|
```bash
|
||||||
curl -X PUT http://vision:8000/cameras/camera1/config \
|
curl -X PUT http://localhost:8000/cameras/camera1/config \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"hdr_enabled": true,
|
"hdr_enabled": true,
|
||||||
@@ -232,7 +232,7 @@ curl -X PUT http://vision:8000/cameras/camera1/config \
|
|||||||
```jsx
|
```jsx
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
|
||||||
const CameraConfig = ({ cameraName, apiBaseUrl = 'http://vision:8000' }) => {
|
const CameraConfig = ({ cameraName, apiBaseUrl = 'http://localhost:8000' }) => {
|
||||||
const [config, setConfig] = useState(null);
|
const [config, setConfig] = useState(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|||||||
@@ -56,27 +56,27 @@ When a camera has issues, follow this order:
|
|||||||
|
|
||||||
1. **Test Connection** - Diagnose the problem
|
1. **Test Connection** - Diagnose the problem
|
||||||
```http
|
```http
|
||||||
POST http://vision:8000/cameras/camera1/test-connection
|
POST http://localhost:8000/cameras/camera1/test-connection
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Try Reconnect** - Most common fix
|
2. **Try Reconnect** - Most common fix
|
||||||
```http
|
```http
|
||||||
POST http://vision:8000/cameras/camera1/reconnect
|
POST http://localhost:8000/cameras/camera1/reconnect
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Restart Grab** - If reconnect doesn't work
|
3. **Restart Grab** - If reconnect doesn't work
|
||||||
```http
|
```http
|
||||||
POST http://vision:8000/cameras/camera1/restart-grab
|
POST http://localhost:8000/cameras/camera1/restart-grab
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Full Reset** - For persistent issues
|
4. **Full Reset** - For persistent issues
|
||||||
```http
|
```http
|
||||||
POST http://vision:8000/cameras/camera1/full-reset
|
POST http://localhost:8000/cameras/camera1/full-reset
|
||||||
```
|
```
|
||||||
|
|
||||||
5. **Reinitialize** - For cameras that never worked
|
5. **Reinitialize** - For cameras that never worked
|
||||||
```http
|
```http
|
||||||
POST http://vision:8000/cameras/camera1/reinitialize
|
POST http://localhost:8000/cameras/camera1/reinitialize
|
||||||
```
|
```
|
||||||
|
|
||||||
## Response Format
|
## Response Format
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ When you run the system, you'll see:
|
|||||||
|
|
||||||
### MQTT Status
|
### MQTT Status
|
||||||
```http
|
```http
|
||||||
GET http://vision:8000/mqtt/status
|
GET http://localhost:8000/mqtt/status
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response:**
|
**Response:**
|
||||||
@@ -60,7 +60,7 @@ GET http://vision:8000/mqtt/status
|
|||||||
|
|
||||||
### Machine Status
|
### Machine Status
|
||||||
```http
|
```http
|
||||||
GET http://vision:8000/machines
|
GET http://localhost:8000/machines
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response:**
|
**Response:**
|
||||||
@@ -85,7 +85,7 @@ GET http://vision:8000/machines
|
|||||||
|
|
||||||
### System Status
|
### System Status
|
||||||
```http
|
```http
|
||||||
GET http://vision:8000/system/status
|
GET http://localhost:8000/system/status
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response:**
|
**Response:**
|
||||||
@@ -125,13 +125,13 @@ Tests all the API endpoints and shows expected responses.
|
|||||||
### 4. **Query APIs Directly**
|
### 4. **Query APIs Directly**
|
||||||
```bash
|
```bash
|
||||||
# Check MQTT status
|
# Check MQTT status
|
||||||
curl http://vision:8000/mqtt/status
|
curl http://localhost:8000/mqtt/status
|
||||||
|
|
||||||
# Check machine states
|
# Check machine states
|
||||||
curl http://vision:8000/machines
|
curl http://localhost:8000/machines
|
||||||
|
|
||||||
# Check overall system status
|
# Check overall system status
|
||||||
curl http://vision:8000/system/status
|
curl http://localhost:8000/system/status
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔧 Configuration
|
## 🔧 Configuration
|
||||||
|
|||||||
@@ -40,13 +40,13 @@ Open `camera_preview.html` in your browser and click "Start Stream" for any came
|
|||||||
### 3. API Usage
|
### 3. API Usage
|
||||||
```bash
|
```bash
|
||||||
# Start streaming for camera1
|
# Start streaming for camera1
|
||||||
curl -X POST http://vision:8000/cameras/camera1/start-stream
|
curl -X POST http://localhost:8000/cameras/camera1/start-stream
|
||||||
|
|
||||||
# View live stream (open in browser)
|
# View live stream (open in browser)
|
||||||
http://vision:8000/cameras/camera1/stream
|
http://localhost:8000/cameras/camera1/stream
|
||||||
|
|
||||||
# Stop streaming
|
# Stop streaming
|
||||||
curl -X POST http://vision:8000/cameras/camera1/stop-stream
|
curl -X POST http://localhost:8000/cameras/camera1/stop-stream
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📡 API Endpoints
|
## 📡 API Endpoints
|
||||||
@@ -150,10 +150,10 @@ The system supports these concurrent operations:
|
|||||||
### Example: Concurrent Usage
|
### Example: Concurrent Usage
|
||||||
```bash
|
```bash
|
||||||
# Start streaming
|
# Start streaming
|
||||||
curl -X POST http://vision:8000/cameras/camera1/start-stream
|
curl -X POST http://localhost:8000/cameras/camera1/start-stream
|
||||||
|
|
||||||
# Start recording (while streaming continues)
|
# Start recording (while streaming continues)
|
||||||
curl -X POST http://vision:8000/cameras/camera1/start-recording \
|
curl -X POST http://localhost:8000/cameras/camera1/start-recording \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"filename": "test_recording.avi"}'
|
-d '{"filename": "test_recording.avi"}'
|
||||||
|
|
||||||
@@ -232,8 +232,8 @@ For issues with streaming functionality:
|
|||||||
|
|
||||||
1. Check the system logs: `usda_vision_system.log`
|
1. Check the system logs: `usda_vision_system.log`
|
||||||
2. Run the test script: `python test_streaming.py`
|
2. Run the test script: `python test_streaming.py`
|
||||||
3. Verify API health: `http://vision:8000/health`
|
3. Verify API health: `http://localhost:8000/health`
|
||||||
4. Check camera status: `http://vision:8000/cameras`
|
4. Check camera status: `http://localhost:8000/cameras`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -73,10 +73,10 @@ Edit `config.json` to customize:
|
|||||||
- System parameters
|
- System parameters
|
||||||
|
|
||||||
### API Access
|
### API Access
|
||||||
- System status: `http://vision:8000/system/status`
|
- System status: `http://localhost:8000/system/status`
|
||||||
- Camera status: `http://vision:8000/cameras`
|
- Camera status: `http://localhost:8000/cameras`
|
||||||
- Manual recording: `POST http://vision:8000/cameras/camera1/start-recording`
|
- Manual recording: `POST http://localhost:8000/cameras/camera1/start-recording`
|
||||||
- Real-time updates: WebSocket at `ws://vision:8000/ws`
|
- Real-time updates: WebSocket at `ws://localhost:8000/ws`
|
||||||
|
|
||||||
## 📊 Test Results
|
## 📊 Test Results
|
||||||
|
|
||||||
@@ -146,18 +146,18 @@ The system provides everything needed for your React dashboard:
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Example API usage
|
// Example API usage
|
||||||
const systemStatus = await fetch('http://vision:8000/system/status');
|
const systemStatus = await fetch('http://localhost:8000/system/status');
|
||||||
const cameras = await fetch('http://vision:8000/cameras');
|
const cameras = await fetch('http://localhost:8000/cameras');
|
||||||
|
|
||||||
// WebSocket for real-time updates
|
// WebSocket for real-time updates
|
||||||
const ws = new WebSocket('ws://vision:8000/ws');
|
const ws = new WebSocket('ws://localhost:8000/ws');
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
const update = JSON.parse(event.data);
|
const update = JSON.parse(event.data);
|
||||||
// Handle real-time system updates
|
// Handle real-time system updates
|
||||||
};
|
};
|
||||||
|
|
||||||
// Manual recording control
|
// Manual recording control
|
||||||
await fetch('http://vision:8000/cameras/camera1/start-recording', {
|
await fetch('http://localhost:8000/cameras/camera1/start-recording', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ camera_name: 'camera1' })
|
body: JSON.stringify({ camera_name: 'camera1' })
|
||||||
|
|||||||
@@ -192,13 +192,13 @@ Comprehensive error tracking with:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Check system status
|
# Check system status
|
||||||
curl http://vision:8000/system/status
|
curl http://localhost:8000/system/status
|
||||||
|
|
||||||
# Check camera status
|
# Check camera status
|
||||||
curl http://vision:8000/cameras
|
curl http://localhost:8000/cameras
|
||||||
|
|
||||||
# Manual recording start
|
# Manual recording start
|
||||||
curl -X POST http://vision:8000/cameras/camera1/start-recording \
|
curl -X POST http://localhost:8000/cameras/camera1/start-recording \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"camera_name": "camera1"}'
|
-d '{"camera_name": "camera1"}'
|
||||||
```
|
```
|
||||||
@@ -246,4 +246,4 @@ This project is developed for USDA research purposes.
|
|||||||
For issues and questions:
|
For issues and questions:
|
||||||
1. Check the logs in `usda_vision_system.log`
|
1. Check the logs in `usda_vision_system.log`
|
||||||
2. Review the troubleshooting section
|
2. Review the troubleshooting section
|
||||||
3. Check API status at `http://vision:8000/health`
|
3. Check API status at `http://localhost:8000/health`
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ timedatectl status
|
|||||||
### API Endpoints
|
### API Endpoints
|
||||||
```bash
|
```bash
|
||||||
# System status includes time info
|
# System status includes time info
|
||||||
curl http://vision:8000/system/status
|
curl http://localhost:8000/system/status
|
||||||
|
|
||||||
# Example response includes:
|
# Example response includes:
|
||||||
{
|
{
|
||||||
|
|||||||
175
docs/VIDEO_STREAMING_INTEGRATION_COMPLETE.md
Normal file
175
docs/VIDEO_STREAMING_INTEGRATION_COMPLETE.md
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# Video Streaming Integration - Complete Implementation
|
||||||
|
|
||||||
|
This document provides a comprehensive overview of the completed video streaming integration with the USDA Vision Camera System.
|
||||||
|
|
||||||
|
## 🎯 Overview
|
||||||
|
|
||||||
|
The video streaming functionality has been successfully integrated into the Pecan Experiments React application, providing a complete video browsing and playback interface that connects to the USDA Vision Camera System API.
|
||||||
|
|
||||||
|
## ✅ Completed Features
|
||||||
|
|
||||||
|
### 1. Core Video Streaming Components
|
||||||
|
- **VideoList**: Displays filterable list of videos with pagination
|
||||||
|
- **VideoPlayer**: HTML5 video player with custom controls and range request support
|
||||||
|
- **VideoCard**: Individual video cards with thumbnails and metadata
|
||||||
|
- **VideoThumbnail**: Thumbnail component with caching and error handling
|
||||||
|
- **VideoModal**: Modal for video playback
|
||||||
|
- **Pagination**: Pagination controls for large video collections
|
||||||
|
|
||||||
|
### 2. API Integration
|
||||||
|
- **VideoApiService**: Complete API client for USDA Vision Camera System
|
||||||
|
- **Flexible Configuration**: Environment-based API URL configuration
|
||||||
|
- **Error Handling**: Comprehensive error handling with user-friendly messages
|
||||||
|
- **Performance Monitoring**: Built-in performance tracking and metrics
|
||||||
|
|
||||||
|
### 3. Performance Optimizations
|
||||||
|
- **Thumbnail Caching**: Intelligent caching system with LRU eviction
|
||||||
|
- **Performance Monitoring**: Real-time performance metrics and reporting
|
||||||
|
- **Efficient Loading**: Optimized API calls and data fetching
|
||||||
|
- **Memory Management**: Automatic cleanup and memory optimization
|
||||||
|
|
||||||
|
### 4. Error Handling & User Experience
|
||||||
|
- **Error Boundaries**: React error boundaries for graceful error handling
|
||||||
|
- **API Status Indicator**: Real-time API connectivity status
|
||||||
|
- **Loading States**: Comprehensive loading indicators
|
||||||
|
- **User Feedback**: Clear error messages and recovery options
|
||||||
|
|
||||||
|
### 5. Development Tools
|
||||||
|
- **Performance Dashboard**: Development-only performance monitoring UI
|
||||||
|
- **Debug Information**: Detailed error information in development mode
|
||||||
|
- **Cache Statistics**: Real-time cache performance metrics
|
||||||
|
|
||||||
|
## 🔧 Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
Create a `.env` file with the following configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# USDA Vision Camera System API Configuration
|
||||||
|
# Default: http://vision:8000 (Docker container)
|
||||||
|
# For local development without Docker: http://localhost:8000
|
||||||
|
# For remote systems: http://192.168.1.100:8000
|
||||||
|
VITE_VISION_API_URL=http://vision:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Endpoints Used
|
||||||
|
- `GET /videos/` - List videos with filtering and pagination
|
||||||
|
- `GET /videos/{file_id}` - Get detailed video information
|
||||||
|
- `GET /videos/{file_id}/stream` - Stream video content with range requests
|
||||||
|
- `GET /videos/{file_id}/thumbnail` - Generate video thumbnails
|
||||||
|
- `GET /videos/{file_id}/info` - Get streaming technical details
|
||||||
|
- `POST /videos/{file_id}/validate` - Validate video accessibility
|
||||||
|
|
||||||
|
## 🚀 Usage
|
||||||
|
|
||||||
|
### Navigation
|
||||||
|
The video streaming functionality is accessible through:
|
||||||
|
- **Main Navigation**: "Video Library" menu item
|
||||||
|
- **Vision System**: Integrated with existing vision system dashboard
|
||||||
|
|
||||||
|
### Features Available
|
||||||
|
1. **Browse Videos**: Filter by camera, date range, and sort options
|
||||||
|
2. **View Thumbnails**: Automatic thumbnail generation with caching
|
||||||
|
3. **Play Videos**: Full-featured video player with seeking capabilities
|
||||||
|
4. **Performance Monitoring**: Real-time performance metrics (development mode)
|
||||||
|
|
||||||
|
### User Interface
|
||||||
|
- **Responsive Design**: Works on desktop and mobile devices
|
||||||
|
- **Dark/Light Theme**: Follows application theme preferences
|
||||||
|
- **Accessibility**: Keyboard navigation and screen reader support
|
||||||
|
|
||||||
|
## 🔍 Technical Implementation
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
```
|
||||||
|
src/features/video-streaming/
|
||||||
|
├── components/ # React components
|
||||||
|
│ ├── VideoList.tsx # Video listing with filters
|
||||||
|
│ ├── VideoPlayer.tsx # Video playback component
|
||||||
|
│ ├── VideoCard.tsx # Individual video cards
|
||||||
|
│ ├── VideoThumbnail.tsx # Thumbnail component
|
||||||
|
│ ├── VideoModal.tsx # Video playback modal
|
||||||
|
│ ├── ApiStatusIndicator.tsx # API status display
|
||||||
|
│ ├── VideoErrorBoundary.tsx # Error handling
|
||||||
|
│ └── PerformanceDashboard.tsx # Dev tools
|
||||||
|
├── hooks/ # Custom React hooks
|
||||||
|
│ ├── useVideoList.ts # Video list management
|
||||||
|
│ ├── useVideoPlayer.ts # Video player state
|
||||||
|
│ └── useVideoInfo.ts # Video information
|
||||||
|
├── services/ # API services
|
||||||
|
│ └── videoApi.ts # USDA Vision API client
|
||||||
|
├── utils/ # Utilities
|
||||||
|
│ ├── videoUtils.ts # Video helper functions
|
||||||
|
│ ├── thumbnailCache.ts # Thumbnail caching
|
||||||
|
│ └── performanceMonitor.ts # Performance tracking
|
||||||
|
├── types/ # TypeScript types
|
||||||
|
└── VideoStreamingPage.tsx # Main page component
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Technologies
|
||||||
|
- **React 18**: Modern React with hooks and concurrent features
|
||||||
|
- **TypeScript**: Full type safety and IntelliSense
|
||||||
|
- **Tailwind CSS**: Utility-first styling
|
||||||
|
- **HTML5 Video**: Native video playback with custom controls
|
||||||
|
- **Fetch API**: Modern HTTP client for API calls
|
||||||
|
|
||||||
|
## 📊 Performance Features
|
||||||
|
|
||||||
|
### Thumbnail Caching
|
||||||
|
- **LRU Cache**: Least Recently Used eviction policy
|
||||||
|
- **Memory Management**: Configurable memory limits
|
||||||
|
- **Automatic Cleanup**: Expired entry removal
|
||||||
|
- **Statistics**: Real-time cache performance metrics
|
||||||
|
|
||||||
|
### Performance Monitoring
|
||||||
|
- **Operation Tracking**: Automatic timing of API calls
|
||||||
|
- **Success Rates**: Track success/failure rates
|
||||||
|
- **Memory Usage**: Monitor cache memory consumption
|
||||||
|
- **Development Dashboard**: Visual performance metrics
|
||||||
|
|
||||||
|
### Optimizations
|
||||||
|
- **Range Requests**: Efficient video seeking with HTTP range requests
|
||||||
|
- **Lazy Loading**: Thumbnails loaded on demand
|
||||||
|
- **Error Recovery**: Automatic retry mechanisms
|
||||||
|
- **Connection Pooling**: Efficient HTTP connection reuse
|
||||||
|
|
||||||
|
## 🛠️ Development
|
||||||
|
|
||||||
|
### Testing the Integration
|
||||||
|
1. **Start the Application**: `npm run dev`
|
||||||
|
2. **Navigate to Video Library**: Use the sidebar navigation
|
||||||
|
3. **Check API Status**: Look for the connection indicator
|
||||||
|
4. **Browse Videos**: Filter and sort available videos
|
||||||
|
5. **Play Videos**: Click on video cards to open the player
|
||||||
|
|
||||||
|
### Development Tools
|
||||||
|
- **Performance Dashboard**: Click the performance icon (bottom-right)
|
||||||
|
- **Browser DevTools**: Check console for performance logs
|
||||||
|
- **Network Tab**: Monitor API calls and response times
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
1. **API Connection Issues**: Check VITE_VISION_API_URL environment variable
|
||||||
|
2. **Video Not Playing**: Verify video file accessibility and format
|
||||||
|
3. **Thumbnail Errors**: Check thumbnail generation API endpoint
|
||||||
|
4. **Performance Issues**: Use the performance dashboard to identify bottlenecks
|
||||||
|
|
||||||
|
## 🔮 Future Enhancements
|
||||||
|
|
||||||
|
### Potential Improvements
|
||||||
|
- **Video Upload**: Add video upload functionality
|
||||||
|
- **Live Streaming**: Integrate live camera feeds
|
||||||
|
- **Video Analytics**: Add video analysis and metadata extraction
|
||||||
|
- **Offline Support**: Cache videos for offline viewing
|
||||||
|
- **Advanced Filters**: More sophisticated filtering options
|
||||||
|
|
||||||
|
### Integration Opportunities
|
||||||
|
- **Experiment Data**: Link videos to experiment data
|
||||||
|
- **Machine Learning**: Integrate with video analysis models
|
||||||
|
- **Export Features**: Video export and sharing capabilities
|
||||||
|
- **Collaboration**: Multi-user video annotation and comments
|
||||||
|
|
||||||
|
## 📝 Conclusion
|
||||||
|
|
||||||
|
The video streaming integration provides a robust, performant, and user-friendly interface for browsing and viewing videos from the USDA Vision Camera System. The implementation includes comprehensive error handling, performance optimizations, and development tools to ensure a smooth user experience and maintainable codebase.
|
||||||
|
|
||||||
|
The modular architecture allows for easy extension and customization, while the performance monitoring and caching systems ensure optimal performance even with large video collections.
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { VideoList, VideoModal } from './components';
|
import { VideoList, VideoModal, ApiStatusIndicator, VideoErrorBoundary, PerformanceDashboard } from './components';
|
||||||
import { type VideoFile, type VideoListFilters, type VideoListSortOptions } from './types';
|
import { type VideoFile, type VideoListFilters, type VideoListSortOptions } from './types';
|
||||||
|
|
||||||
export const VideoStreamingPage: React.FC = () => {
|
export const VideoStreamingPage: React.FC = () => {
|
||||||
@@ -50,123 +50,151 @@ export const VideoStreamingPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<VideoErrorBoundary>
|
||||||
{/* Header */}
|
<div className="space-y-6">
|
||||||
<div>
|
{/* Header */}
|
||||||
<h1 className="text-3xl font-bold text-gray-900">Video Library</h1>
|
<div className="flex items-start justify-between">
|
||||||
<p className="mt-2 text-gray-600">
|
|
||||||
Browse and view recorded videos from your camera system
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filters and Controls */}
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 p-6 shadow-theme-sm">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
||||||
{/* Camera Filter */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<h1 className="text-3xl font-bold text-gray-900">Video Library</h1>
|
||||||
Filter by Camera
|
<p className="mt-2 text-gray-600">
|
||||||
</label>
|
Browse and view recorded videos from your camera system
|
||||||
<select
|
</p>
|
||||||
value={filters.cameraName || 'all'}
|
|
||||||
onChange={(e) => handleCameraFilterChange(e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-brand-500"
|
|
||||||
>
|
|
||||||
<option value="all">All Cameras</option>
|
|
||||||
{availableCameras.map(camera => (
|
|
||||||
<option key={camera} value={camera}>
|
|
||||||
{camera}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<ApiStatusIndicator showDetails={false} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Sort Options */}
|
{/* Filters and Controls */}
|
||||||
<div>
|
<div className="bg-white rounded-xl border border-gray-200 p-6 shadow-theme-sm">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||||
Sort by
|
{/* Camera Filter */}
|
||||||
</label>
|
<div>
|
||||||
<div className="flex space-x-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
<select
|
Camera
|
||||||
value={sortOptions.field}
|
</label>
|
||||||
onChange={(e) => handleSortChange(e.target.value as VideoListSortOptions['field'], sortOptions.direction)}
|
<div className="relative">
|
||||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-brand-500"
|
<select
|
||||||
>
|
value={filters.cameraName || 'all'}
|
||||||
<option value="created_at">Date Created</option>
|
onChange={(e) => handleCameraFilterChange(e.target.value)}
|
||||||
<option value="file_size_bytes">File Size</option>
|
className="w-full px-3 py-2.5 border border-gray-300 rounded-lg bg-white text-gray-900 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-brand-500 shadow-theme-xs transition-colors"
|
||||||
<option value="camera_name">Camera Name</option>
|
>
|
||||||
<option value="filename">Filename</option>
|
<option value="all">All Cameras</option>
|
||||||
</select>
|
{availableCameras.map(camera => (
|
||||||
<button
|
<option key={camera} value={camera}>
|
||||||
onClick={() => handleSortChange(sortOptions.field, sortOptions.direction === 'asc' ? 'desc' : 'asc')}
|
{camera}
|
||||||
className="px-3 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-brand-500"
|
</option>
|
||||||
title={`Sort ${sortOptions.direction === 'asc' ? 'Descending' : 'Ascending'}`}
|
))}
|
||||||
>
|
</select>
|
||||||
{sortOptions.direction === 'asc' ? (
|
<div className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
</svg>
|
</svg>
|
||||||
) : (
|
</div>
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
</div>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4h13M3 8h9m-9 4h9m5-4v12m0 0l-4-4m4 4l4-4" />
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Date Range Filter */}
|
{/* Sort Options */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Date Range
|
Sort by
|
||||||
</label>
|
</label>
|
||||||
<div className="flex space-x-2">
|
<div className="flex gap-2">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<select
|
||||||
|
value={sortOptions.field}
|
||||||
|
onChange={(e) => handleSortChange(e.target.value as VideoListSortOptions['field'], sortOptions.direction)}
|
||||||
|
className="w-full px-3 py-2.5 border border-gray-300 rounded-lg bg-white text-gray-900 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-brand-500 shadow-theme-xs transition-colors"
|
||||||
|
>
|
||||||
|
<option value="created_at">Date Created</option>
|
||||||
|
<option value="file_size_bytes">File Size</option>
|
||||||
|
<option value="camera_name">Camera Name</option>
|
||||||
|
<option value="filename">Filename</option>
|
||||||
|
</select>
|
||||||
|
<div className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
|
||||||
|
<svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => handleSortChange(sortOptions.field, sortOptions.direction === 'asc' ? 'desc' : 'asc')}
|
||||||
|
className="px-3 py-2.5 border border-gray-300 rounded-lg bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-brand-500 shadow-theme-xs transition-colors"
|
||||||
|
title={`Sort ${sortOptions.direction === 'asc' ? 'Descending' : 'Ascending'}`}
|
||||||
|
>
|
||||||
|
{sortOptions.direction === 'asc' ? (
|
||||||
|
<svg className="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg className="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4h13M3 8h9m-9 4h9m5-4v12m0 0l-4-4m4 4l4-4" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Date Range Filter */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Start Date
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={filters.dateRange?.start || ''}
|
value={filters.dateRange?.start || ''}
|
||||||
onChange={(e) => handleDateRangeChange(e.target.value, filters.dateRange?.end || '')}
|
onChange={(e) => 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"
|
className="w-full px-3 py-2.5 border border-gray-300 rounded-lg bg-white text-gray-900 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-brand-500 shadow-theme-xs transition-colors"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
End Date
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={filters.dateRange?.end || ''}
|
value={filters.dateRange?.end || ''}
|
||||||
onChange={(e) => handleDateRangeChange(filters.dateRange?.start || '', e.target.value)}
|
onChange={(e) => 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"
|
className="w-full px-3 py-2.5 border border-gray-300 rounded-lg bg-white text-gray-900 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-brand-500 shadow-theme-xs transition-colors"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Clear Filters */}
|
||||||
|
{(filters.cameraName || filters.dateRange) && (
|
||||||
|
<div className="mt-6 pt-6 border-t border-gray-200">
|
||||||
|
<button
|
||||||
|
onClick={() => setFilters({})}
|
||||||
|
className="inline-flex items-center px-4 py-2.5 text-sm font-medium transition rounded-lg border bg-white text-gray-700 border-gray-300 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-brand-500 shadow-theme-xs"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
Clear All Filters
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Clear Filters */}
|
{/* Video List */}
|
||||||
{(filters.cameraName || filters.dateRange) && (
|
<VideoList
|
||||||
<div className="mt-4 pt-4 border-t border-gray-100">
|
filters={filters}
|
||||||
<button
|
sortOptions={sortOptions}
|
||||||
onClick={() => setFilters({})}
|
onVideoSelect={handleVideoSelect}
|
||||||
className="inline-flex items-center px-3 py-2 text-sm font-medium transition rounded-lg border bg-white text-gray-700 border-gray-300 hover:bg-gray-50"
|
limit={24}
|
||||||
>
|
/>
|
||||||
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
{/* Video Modal */}
|
||||||
</svg>
|
<VideoModal
|
||||||
Clear Filters
|
video={selectedVideo}
|
||||||
</button>
|
isOpen={isModalOpen}
|
||||||
</div>
|
onClose={handleModalClose}
|
||||||
)}
|
/>
|
||||||
|
|
||||||
|
{/* Performance Dashboard (development only) */}
|
||||||
|
<PerformanceDashboard />
|
||||||
</div>
|
</div>
|
||||||
|
</VideoErrorBoundary>
|
||||||
{/* Video List */}
|
|
||||||
<VideoList
|
|
||||||
filters={filters}
|
|
||||||
sortOptions={sortOptions}
|
|
||||||
onVideoSelect={handleVideoSelect}
|
|
||||||
limit={24}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Video Modal */}
|
|
||||||
<VideoModal
|
|
||||||
video={selectedVideo}
|
|
||||||
isOpen={isModalOpen}
|
|
||||||
onClose={handleModalClose}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
133
src/features/video-streaming/components/ApiStatusIndicator.tsx
Normal file
133
src/features/video-streaming/components/ApiStatusIndicator.tsx
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
/**
|
||||||
|
* ApiStatusIndicator Component
|
||||||
|
*
|
||||||
|
* A component that displays the connection status of the video streaming API
|
||||||
|
* and provides helpful information when the API is not accessible.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { videoApiService } from '../services/videoApi';
|
||||||
|
|
||||||
|
interface ApiStatusIndicatorProps {
|
||||||
|
className?: string;
|
||||||
|
showDetails?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ApiStatusIndicator: React.FC<ApiStatusIndicatorProps> = ({
|
||||||
|
className = '',
|
||||||
|
showDetails = false,
|
||||||
|
}) => {
|
||||||
|
const [isOnline, setIsOnline] = useState<boolean | null>(null);
|
||||||
|
const [isChecking, setIsChecking] = useState(false);
|
||||||
|
const [lastChecked, setLastChecked] = useState<Date | null>(null);
|
||||||
|
|
||||||
|
const checkApiStatus = async () => {
|
||||||
|
setIsChecking(true);
|
||||||
|
try {
|
||||||
|
const status = await videoApiService.healthCheck();
|
||||||
|
setIsOnline(status);
|
||||||
|
setLastChecked(new Date());
|
||||||
|
} catch (error) {
|
||||||
|
setIsOnline(false);
|
||||||
|
setLastChecked(new Date());
|
||||||
|
} finally {
|
||||||
|
setIsChecking(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkApiStatus();
|
||||||
|
|
||||||
|
// Check status every 30 seconds
|
||||||
|
const interval = setInterval(checkApiStatus, 30000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getStatusColor = () => {
|
||||||
|
if (isChecking) return 'bg-yellow-500';
|
||||||
|
if (isOnline === null) return 'bg-gray-500';
|
||||||
|
return isOnline ? 'bg-green-500' : 'bg-red-500';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusText = () => {
|
||||||
|
if (isChecking) return 'Checking...';
|
||||||
|
if (isOnline === null) return 'Unknown';
|
||||||
|
return isOnline ? 'Connected' : 'Disconnected';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusIcon = () => {
|
||||||
|
if (isChecking) {
|
||||||
|
return (
|
||||||
|
<div className="animate-spin rounded-full h-3 w-3 border-b-2 border-white"></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOnline) {
|
||||||
|
return (
|
||||||
|
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!showDetails) {
|
||||||
|
return (
|
||||||
|
<div className={`inline-flex items-center ${className}`}>
|
||||||
|
<div className={`w-2 h-2 rounded-full ${getStatusColor()} mr-2`}></div>
|
||||||
|
<span className="text-sm text-gray-600">{getStatusText()}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`bg-white border border-gray-200 rounded-lg p-4 ${className}`}>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="text-sm font-medium text-gray-900">Video API Status</h3>
|
||||||
|
<button
|
||||||
|
onClick={checkApiStatus}
|
||||||
|
disabled={isChecking}
|
||||||
|
className="text-xs text-blue-600 hover:text-blue-800 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center mb-2">
|
||||||
|
<div className={`w-3 h-3 rounded-full ${getStatusColor()} mr-2 flex items-center justify-center text-white`}>
|
||||||
|
{getStatusIcon()}
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium">{getStatusText()}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{lastChecked && (
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
Last checked: {lastChecked.toLocaleTimeString()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isOnline === false && (
|
||||||
|
<div className="mt-3 p-3 bg-red-50 border border-red-200 rounded-md">
|
||||||
|
<div className="text-sm text-red-800">
|
||||||
|
<strong>Connection Failed</strong>
|
||||||
|
<p className="mt-1">
|
||||||
|
Cannot connect to the USDA Vision Camera System. Please ensure:
|
||||||
|
</p>
|
||||||
|
<ul className="mt-2 list-disc list-inside space-y-1">
|
||||||
|
<li>The vision system is running</li>
|
||||||
|
<li>The API is accessible at the configured URL</li>
|
||||||
|
<li>Network connectivity is available</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -51,13 +51,13 @@ export const Pagination: React.FC<PaginationProps> = ({
|
|||||||
const isLastPage = currentPage === totalPages;
|
const isLastPage = currentPage === totalPages;
|
||||||
|
|
||||||
// Button base classes matching dashboard template
|
// Button base classes matching dashboard template
|
||||||
const baseButtonClasses = "inline-flex items-center justify-center px-3 py-2 text-sm font-medium transition rounded-lg border";
|
const baseButtonClasses = "inline-flex items-center justify-center px-3 py-2 text-sm font-medium transition-all duration-200 rounded-lg border";
|
||||||
|
|
||||||
// Active page button classes
|
// Active page button classes
|
||||||
const activeButtonClasses = "bg-brand-500 text-white border-brand-500 hover:bg-brand-600 shadow-theme-xs";
|
const activeButtonClasses = "bg-brand-500 text-white border-brand-500 hover:bg-brand-600 shadow-theme-sm";
|
||||||
|
|
||||||
// Inactive page button classes
|
// Inactive page button classes
|
||||||
const inactiveButtonClasses = "bg-white text-gray-700 border-gray-300 hover:bg-gray-50";
|
const inactiveButtonClasses = "bg-white text-gray-700 border-gray-300 hover:bg-gray-50 hover:border-gray-400 shadow-theme-xs";
|
||||||
|
|
||||||
// Disabled button classes
|
// Disabled button classes
|
||||||
const disabledButtonClasses = "bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed opacity-50";
|
const disabledButtonClasses = "bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed opacity-50";
|
||||||
|
|||||||
167
src/features/video-streaming/components/PerformanceDashboard.tsx
Normal file
167
src/features/video-streaming/components/PerformanceDashboard.tsx
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
/**
|
||||||
|
* PerformanceDashboard Component
|
||||||
|
*
|
||||||
|
* A development tool for monitoring video streaming performance.
|
||||||
|
* Only shown in development mode.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { performanceMonitor, thumbnailCache } from '../utils';
|
||||||
|
|
||||||
|
interface PerformanceDashboardProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PerformanceDashboard: React.FC<PerformanceDashboardProps> = ({
|
||||||
|
className = '',
|
||||||
|
}) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [stats, setStats] = useState<any>(null);
|
||||||
|
const [cacheStats, setCacheStats] = useState<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
const updateStats = () => {
|
||||||
|
setStats({
|
||||||
|
overall: performanceMonitor.getStats(),
|
||||||
|
getVideos: performanceMonitor.getStats('get_videos'),
|
||||||
|
getThumbnail: performanceMonitor.getStats('get_thumbnail'),
|
||||||
|
recentMetrics: performanceMonitor.getRecentMetrics(5),
|
||||||
|
});
|
||||||
|
setCacheStats(thumbnailCache.getStats());
|
||||||
|
};
|
||||||
|
|
||||||
|
updateStats();
|
||||||
|
const interval = setInterval(updateStats, 2000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// Only show in development
|
||||||
|
if (process.env.NODE_ENV !== 'development') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
className={`fixed bottom-4 right-4 bg-blue-600 text-white p-2 rounded-full shadow-lg hover:bg-blue-700 transition-colors z-50 ${className}`}
|
||||||
|
title="Open Performance Dashboard"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M3 3a1 1 0 000 2v8a2 2 0 002 2h2.586l-1.293 1.293a1 1 0 101.414 1.414L10 15.414l2.293 2.293a1 1 0 001.414-1.414L12.414 15H15a2 2 0 002-2V5a1 1 0 100-2H3zm11.707 4.707a1 1 0 00-1.414-1.414L10 9.586 8.707 8.293a1 1 0 00-1.414 0l-2 2a1 1 0 101.414 1.414L8 10.414l1.293 1.293a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`fixed bottom-4 right-4 bg-white border border-gray-200 rounded-lg shadow-xl p-4 max-w-md w-80 max-h-96 overflow-y-auto z-50 ${className}`}>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">Performance</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
className="text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{stats && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Overall Stats */}
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium text-gray-700 mb-2">Overall</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||||
|
<div>Operations: {stats.overall.totalOperations}</div>
|
||||||
|
<div>Success: {(stats.overall.successRate * 100).toFixed(1)}%</div>
|
||||||
|
<div>Avg: {stats.overall.averageDuration.toFixed(0)}ms</div>
|
||||||
|
<div>Max: {stats.overall.maxDuration.toFixed(0)}ms</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Video Loading Stats */}
|
||||||
|
{stats.getVideos.totalOperations > 0 && (
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium text-gray-700 mb-2">Video Loading</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||||
|
<div>Calls: {stats.getVideos.totalOperations}</div>
|
||||||
|
<div>Success: {(stats.getVideos.successRate * 100).toFixed(1)}%</div>
|
||||||
|
<div>Avg: {stats.getVideos.averageDuration.toFixed(0)}ms</div>
|
||||||
|
<div>Max: {stats.getVideos.maxDuration.toFixed(0)}ms</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Thumbnail Stats */}
|
||||||
|
{stats.getThumbnail.totalOperations > 0 && (
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium text-gray-700 mb-2">Thumbnails</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||||
|
<div>Calls: {stats.getThumbnail.totalOperations}</div>
|
||||||
|
<div>Success: {(stats.getThumbnail.successRate * 100).toFixed(1)}%</div>
|
||||||
|
<div>Avg: {stats.getThumbnail.averageDuration.toFixed(0)}ms</div>
|
||||||
|
<div>Max: {stats.getThumbnail.maxDuration.toFixed(0)}ms</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Cache Stats */}
|
||||||
|
{cacheStats && (
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium text-gray-700 mb-2">Cache</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||||
|
<div>Cached: {cacheStats.size}</div>
|
||||||
|
<div>Memory: {(cacheStats.totalMemory / 1024 / 1024).toFixed(1)}MB</div>
|
||||||
|
<div>Hits: {cacheStats.totalAccess}</div>
|
||||||
|
<div>Avg Size: {(cacheStats.averageSize / 1024).toFixed(0)}KB</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Recent Operations */}
|
||||||
|
{stats.recentMetrics.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium text-gray-700 mb-2">Recent</h4>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{stats.recentMetrics.map((metric: any, index: number) => (
|
||||||
|
<div key={index} className="flex justify-between text-xs">
|
||||||
|
<span className={metric.success ? 'text-green-600' : 'text-red-600'}>
|
||||||
|
{metric.operation}
|
||||||
|
</span>
|
||||||
|
<span>{metric.duration?.toFixed(0)}ms</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
performanceMonitor.clear();
|
||||||
|
thumbnailCache.clear();
|
||||||
|
}}
|
||||||
|
className="flex-1 px-2 py-1 text-xs bg-red-100 text-red-700 rounded hover:bg-red-200"
|
||||||
|
>
|
||||||
|
Clear All
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
console.log(performanceMonitor.getReport());
|
||||||
|
console.log('Cache Stats:', thumbnailCache.getStats());
|
||||||
|
}}
|
||||||
|
className="flex-1 px-2 py-1 text-xs bg-blue-100 text-blue-700 rounded hover:bg-blue-200"
|
||||||
|
>
|
||||||
|
Log Report
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -66,7 +66,7 @@ export const VideoCard: React.FC<VideoCardProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Streamable Indicator */}
|
{/* Streamable Indicator */}
|
||||||
{video.is_streamable && (
|
{video.is_streamable ? (
|
||||||
<div className="absolute bottom-2 left-2">
|
<div className="absolute bottom-2 left-2">
|
||||||
<div className="bg-green-500 text-white text-xs px-2 py-1 rounded flex items-center">
|
<div className="bg-green-500 text-white text-xs px-2 py-1 rounded flex items-center">
|
||||||
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||||
@@ -75,6 +75,15 @@ export const VideoCard: React.FC<VideoCardProps> = ({
|
|||||||
Streamable
|
Streamable
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="absolute bottom-2 left-2">
|
||||||
|
<div className="bg-yellow-500 text-white text-xs px-2 py-1 rounded flex items-center">
|
||||||
|
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
Processing
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Conversion Needed Indicator */}
|
{/* Conversion Needed Indicator */}
|
||||||
|
|||||||
196
src/features/video-streaming/components/VideoDebugger.tsx
Normal file
196
src/features/video-streaming/components/VideoDebugger.tsx
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
/**
|
||||||
|
* VideoDebugger Component
|
||||||
|
*
|
||||||
|
* A development tool for debugging video streaming issues.
|
||||||
|
* Provides direct access to test video URLs and diagnose problems.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { videoApiService } from '../services/videoApi';
|
||||||
|
|
||||||
|
interface VideoDebuggerProps {
|
||||||
|
fileId: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VideoDebugger: React.FC<VideoDebuggerProps> = ({
|
||||||
|
fileId,
|
||||||
|
className = '',
|
||||||
|
}) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [testResults, setTestResults] = useState<any>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const streamingUrl = videoApiService.getStreamingUrl(fileId);
|
||||||
|
const thumbnailUrl = videoApiService.getThumbnailUrl(fileId);
|
||||||
|
|
||||||
|
const runDiagnostics = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
const results: any = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
fileId,
|
||||||
|
streamingUrl,
|
||||||
|
thumbnailUrl,
|
||||||
|
tests: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Test 1: Video Info
|
||||||
|
try {
|
||||||
|
const videoInfo = await videoApiService.getVideoInfo(fileId);
|
||||||
|
results.tests.videoInfo = { success: true, data: videoInfo };
|
||||||
|
} catch (error) {
|
||||||
|
results.tests.videoInfo = { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 2: Streaming Info
|
||||||
|
try {
|
||||||
|
const streamingInfo = await videoApiService.getStreamingInfo(fileId);
|
||||||
|
results.tests.streamingInfo = { success: true, data: streamingInfo };
|
||||||
|
} catch (error) {
|
||||||
|
results.tests.streamingInfo = { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 3: HEAD request to streaming URL
|
||||||
|
try {
|
||||||
|
const response = await fetch(streamingUrl, { method: 'HEAD' });
|
||||||
|
results.tests.streamingHead = {
|
||||||
|
success: response.ok,
|
||||||
|
status: response.status,
|
||||||
|
headers: Object.fromEntries(response.headers.entries())
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
results.tests.streamingHead = { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 4: Range request test
|
||||||
|
try {
|
||||||
|
const response = await fetch(streamingUrl, {
|
||||||
|
headers: { 'Range': 'bytes=0-1023' }
|
||||||
|
});
|
||||||
|
results.tests.rangeRequest = {
|
||||||
|
success: response.ok,
|
||||||
|
status: response.status,
|
||||||
|
supportsRanges: response.headers.get('accept-ranges') === 'bytes',
|
||||||
|
contentRange: response.headers.get('content-range')
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
results.tests.rangeRequest = { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 5: Thumbnail test
|
||||||
|
try {
|
||||||
|
const response = await fetch(thumbnailUrl, { method: 'HEAD' });
|
||||||
|
results.tests.thumbnail = {
|
||||||
|
success: response.ok,
|
||||||
|
status: response.status,
|
||||||
|
contentType: response.headers.get('content-type')
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
results.tests.thumbnail = { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
results.error = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
}
|
||||||
|
|
||||||
|
setTestResults(results);
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only show in development
|
||||||
|
if (process.env.NODE_ENV !== 'development') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
className={`px-3 py-1 text-xs bg-yellow-100 text-yellow-800 rounded hover:bg-yellow-200 transition-colors ${className}`}
|
||||||
|
title="Open Video Debugger"
|
||||||
|
>
|
||||||
|
🔧 Debug
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`bg-white border border-gray-200 rounded-lg shadow-lg p-4 max-w-2xl ${className}`}>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">Video Debugger</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
className="text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Basic Info */}
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-gray-700 mb-2">Basic Info</h4>
|
||||||
|
<div className="text-sm space-y-1">
|
||||||
|
<div><strong>File ID:</strong> {fileId}</div>
|
||||||
|
<div><strong>Streaming URL:</strong> <a href={streamingUrl} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">{streamingUrl}</a></div>
|
||||||
|
<div><strong>Thumbnail URL:</strong> <a href={thumbnailUrl} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">{thumbnailUrl}</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Actions */}
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-gray-700 mb-2">Quick Actions</h4>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={runDiagnostics}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isLoading ? 'Running...' : 'Run Diagnostics'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => window.open(streamingUrl, '_blank')}
|
||||||
|
className="px-3 py-1 text-sm bg-green-600 text-white rounded hover:bg-green-700"
|
||||||
|
>
|
||||||
|
Open Video
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => window.open(thumbnailUrl, '_blank')}
|
||||||
|
className="px-3 py-1 text-sm bg-purple-600 text-white rounded hover:bg-purple-700"
|
||||||
|
>
|
||||||
|
Open Thumbnail
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Test Results */}
|
||||||
|
{testResults && (
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-gray-700 mb-2">Diagnostic Results</h4>
|
||||||
|
<div className="bg-gray-50 rounded p-3 text-xs font-mono max-h-64 overflow-y-auto">
|
||||||
|
<pre>{JSON.stringify(testResults, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Native Video Test */}
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-gray-700 mb-2">Native Video Test</h4>
|
||||||
|
<video
|
||||||
|
controls
|
||||||
|
width="100%"
|
||||||
|
height="200"
|
||||||
|
className="border rounded"
|
||||||
|
onLoadStart={() => console.log('Native video load started')}
|
||||||
|
onLoadedData={() => console.log('Native video data loaded')}
|
||||||
|
onError={(e) => console.error('Native video error:', e)}
|
||||||
|
>
|
||||||
|
<source src={streamingUrl} type="video/mp4" />
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
146
src/features/video-streaming/components/VideoErrorBoundary.tsx
Normal file
146
src/features/video-streaming/components/VideoErrorBoundary.tsx
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
/**
|
||||||
|
* VideoErrorBoundary Component
|
||||||
|
*
|
||||||
|
* Error boundary specifically designed for video streaming components.
|
||||||
|
* Provides user-friendly error messages and recovery options.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { Component, ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
fallback?: ReactNode;
|
||||||
|
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
hasError: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
errorInfo: React.ErrorInfo | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VideoErrorBoundary extends Component<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
hasError: false,
|
||||||
|
error: null,
|
||||||
|
errorInfo: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error: Error): State {
|
||||||
|
return {
|
||||||
|
hasError: true,
|
||||||
|
error,
|
||||||
|
errorInfo: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||||
|
this.setState({
|
||||||
|
error,
|
||||||
|
errorInfo,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call the onError callback if provided
|
||||||
|
if (this.props.onError) {
|
||||||
|
this.props.onError(error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log error for debugging
|
||||||
|
console.error('Video streaming error:', error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRetry = () => {
|
||||||
|
this.setState({
|
||||||
|
hasError: false,
|
||||||
|
error: null,
|
||||||
|
errorInfo: null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
// Use custom fallback if provided
|
||||||
|
if (this.props.fallback) {
|
||||||
|
return this.props.fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default error UI
|
||||||
|
return (
|
||||||
|
<div className="min-h-[400px] flex items-center justify-center bg-gray-50 rounded-lg border border-gray-200">
|
||||||
|
<div className="text-center max-w-md mx-auto p-6">
|
||||||
|
<div className="w-16 h-16 mx-auto mb-4 bg-red-100 rounded-full flex items-center justify-center">
|
||||||
|
<svg className="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
Video System Error
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p className="text-gray-600 mb-4">
|
||||||
|
Something went wrong with the video streaming component. This might be due to:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul className="text-sm text-gray-500 text-left mb-6 space-y-1">
|
||||||
|
<li>• Network connectivity issues</li>
|
||||||
|
<li>• Video API server problems</li>
|
||||||
|
<li>• Corrupted video files</li>
|
||||||
|
<li>• Browser compatibility issues</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<button
|
||||||
|
onClick={this.handleRetry}
|
||||||
|
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
Try Again
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
className="w-full px-4 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition-colors"
|
||||||
|
>
|
||||||
|
Reload Page
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error details for debugging (only in development) */}
|
||||||
|
{process.env.NODE_ENV === 'development' && this.state.error && (
|
||||||
|
<details className="mt-6 text-left">
|
||||||
|
<summary className="cursor-pointer text-sm text-gray-500 hover:text-gray-700">
|
||||||
|
Show Error Details
|
||||||
|
</summary>
|
||||||
|
<div className="mt-2 p-3 bg-gray-100 rounded text-xs font-mono text-gray-700 overflow-auto max-h-32">
|
||||||
|
<div className="font-semibold mb-1">Error:</div>
|
||||||
|
<div className="mb-2">{this.state.error.message}</div>
|
||||||
|
<div className="font-semibold mb-1">Stack:</div>
|
||||||
|
<div>{this.state.error.stack}</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Higher-order component for easier usage
|
||||||
|
export function withVideoErrorBoundary<P extends object>(
|
||||||
|
Component: React.ComponentType<P>,
|
||||||
|
fallback?: ReactNode
|
||||||
|
) {
|
||||||
|
return function WrappedComponent(props: P) {
|
||||||
|
return (
|
||||||
|
<VideoErrorBoundary fallback={fallback}>
|
||||||
|
<Component {...props} />
|
||||||
|
</VideoErrorBoundary>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -49,20 +49,19 @@ export const VideoList: React.FC<VideoListProps> = ({
|
|||||||
autoFetch: true,
|
autoFetch: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update filters when props change (but don't auto-fetch)
|
// Update filters when props change (without causing infinite loops)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (filters) {
|
if (filters) {
|
||||||
setLocalFilters(filters);
|
setLocalFilters(filters);
|
||||||
}
|
}
|
||||||
}, [filters]);
|
}, [filters]);
|
||||||
|
|
||||||
// Update sort when props change
|
// Update sort when props change (without causing infinite loops)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sortOptions) {
|
if (sortOptions) {
|
||||||
setLocalSort(sortOptions);
|
setLocalSort(sortOptions);
|
||||||
updateSort(sortOptions);
|
|
||||||
}
|
}
|
||||||
}, [sortOptions, updateSort]);
|
}, [sortOptions]);
|
||||||
|
|
||||||
const handleVideoClick = (video: any) => {
|
const handleVideoClick = (video: any) => {
|
||||||
if (onVideoSelect) {
|
if (onVideoSelect) {
|
||||||
@@ -134,6 +133,31 @@ export const VideoList: React.FC<VideoListProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClasses}>
|
<div className={containerClasses}>
|
||||||
|
{/* Top Pagination */}
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 p-4 bg-white rounded-xl border border-gray-200 shadow-theme-sm">
|
||||||
|
{/* Page Info */}
|
||||||
|
<PageInfo
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
totalItems={totalCount}
|
||||||
|
itemsPerPage={limit}
|
||||||
|
className="text-sm text-gray-600"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Pagination Controls */}
|
||||||
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
onPageChange={goToPage}
|
||||||
|
showFirstLast={true}
|
||||||
|
showPrevNext={true}
|
||||||
|
maxVisiblePages={5}
|
||||||
|
className="justify-center sm:justify-end"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Results Summary */}
|
{/* Results Summary */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600">
|
||||||
@@ -147,7 +171,7 @@ export const VideoList: React.FC<VideoListProps> = ({
|
|||||||
<button
|
<button
|
||||||
onClick={refetch}
|
onClick={refetch}
|
||||||
disabled={loading === 'loading'}
|
disabled={loading === 'loading'}
|
||||||
className="inline-flex items-center px-3 py-2 text-sm font-medium transition rounded-lg border bg-white text-gray-700 border-gray-300 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="inline-flex items-center px-3 py-2 text-sm font-medium transition rounded-lg border bg-white text-gray-700 border-gray-300 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed shadow-theme-xs"
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||||
@@ -168,16 +192,16 @@ export const VideoList: React.FC<VideoListProps> = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Bottom Pagination */}
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && videos.length > 0 && (
|
||||||
<div className="mt-8 space-y-4">
|
<div className="mt-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 p-4 bg-white rounded-xl border border-gray-200 shadow-theme-sm">
|
||||||
{/* Page Info */}
|
{/* Page Info */}
|
||||||
<PageInfo
|
<PageInfo
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
totalPages={totalPages}
|
totalPages={totalPages}
|
||||||
totalItems={totalCount}
|
totalItems={totalCount}
|
||||||
itemsPerPage={limit}
|
itemsPerPage={limit}
|
||||||
className="text-center"
|
className="text-sm text-gray-600"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Pagination Controls */}
|
{/* Pagination Controls */}
|
||||||
@@ -188,7 +212,7 @@ export const VideoList: React.FC<VideoListProps> = ({
|
|||||||
showFirstLast={true}
|
showFirstLast={true}
|
||||||
showPrevNext={true}
|
showPrevNext={true}
|
||||||
maxVisiblePages={5}
|
maxVisiblePages={5}
|
||||||
className="justify-center"
|
className="justify-center sm:justify-end"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { type VideoFile } from '../types';
|
import { type VideoFile } from '../types';
|
||||||
import { VideoPlayer } from './VideoPlayer';
|
import { VideoPlayer } from './VideoPlayer';
|
||||||
|
import { VideoDebugger } from './VideoDebugger';
|
||||||
import { useVideoInfo } from '../hooks/useVideoInfo';
|
import { useVideoInfo } from '../hooks/useVideoInfo';
|
||||||
import {
|
import {
|
||||||
formatFileSize,
|
formatFileSize,
|
||||||
@@ -64,7 +65,7 @@ export const VideoModal: React.FC<VideoModalProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
<div className="fixed inset-0 z-[999999] overflow-y-auto">
|
||||||
{/* Backdrop */}
|
{/* Backdrop */}
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 bg-black bg-opacity-75 transition-opacity"
|
className="fixed inset-0 bg-black bg-opacity-75 transition-opacity"
|
||||||
@@ -109,8 +110,8 @@ export const VideoModal: React.FC<VideoModalProps> = ({
|
|||||||
{video.status}
|
{video.status}
|
||||||
</span>
|
</span>
|
||||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${isWebCompatible(video.format)
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${isWebCompatible(video.format)
|
||||||
? 'bg-green-100 text-green-800'
|
? 'bg-green-100 text-green-800'
|
||||||
: 'bg-orange-100 text-orange-800'
|
: 'bg-orange-100 text-orange-800'
|
||||||
}`}>
|
}`}>
|
||||||
{getFormatDisplayName(video.format)}
|
{getFormatDisplayName(video.format)}
|
||||||
</span>
|
</span>
|
||||||
@@ -219,6 +220,9 @@ export const VideoModal: React.FC<VideoModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Video Debugger (development only) */}
|
||||||
|
<VideoDebugger fileId={video.file_id} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const VideoPlayer = forwardRef<HTMLVideoElement, VideoPlayerProps>(({
|
|||||||
onEnded,
|
onEnded,
|
||||||
onError,
|
onError,
|
||||||
}, forwardedRef) => {
|
}, forwardedRef) => {
|
||||||
const [videoInfo, setVideoInfo] = useState<{ filename?: string; mimeType: string }>({
|
const [videoInfo, setVideoInfo] = useState<{ filename?: string; mimeType: string; isStreamable?: boolean }>({
|
||||||
mimeType: 'video/mp4' // Default to MP4
|
mimeType: 'video/mp4' // Default to MP4
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ export const VideoPlayer = forwardRef<HTMLVideoElement, VideoPlayerProps>(({
|
|||||||
|
|
||||||
const streamingUrl = videoApiService.getStreamingUrl(fileId);
|
const streamingUrl = videoApiService.getStreamingUrl(fileId);
|
||||||
|
|
||||||
// Fetch video info to determine MIME type
|
// Fetch video info to determine MIME type and streamability
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchVideoInfo = async () => {
|
const fetchVideoInfo = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -49,11 +49,16 @@ export const VideoPlayer = forwardRef<HTMLVideoElement, VideoPlayerProps>(({
|
|||||||
// Extract filename from file_id or use a default pattern
|
// Extract filename from file_id or use a default pattern
|
||||||
const filename = info.file_id.includes('.') ? info.file_id : `${info.file_id}.mp4`;
|
const filename = info.file_id.includes('.') ? info.file_id : `${info.file_id}.mp4`;
|
||||||
const mimeType = getVideoMimeType(filename);
|
const mimeType = getVideoMimeType(filename);
|
||||||
setVideoInfo({ filename, mimeType });
|
setVideoInfo({
|
||||||
|
filename,
|
||||||
|
mimeType,
|
||||||
|
isStreamable: info.is_streamable
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Could not fetch video info, using default MIME type:', error);
|
console.warn('Could not fetch video info, using default MIME type:', error);
|
||||||
// Keep default MP4 MIME type
|
// Keep default MP4 MIME type, assume not streamable
|
||||||
|
setVideoInfo(prev => ({ ...prev, isStreamable: false }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -81,9 +86,10 @@ export const VideoPlayer = forwardRef<HTMLVideoElement, VideoPlayerProps>(({
|
|||||||
<video
|
<video
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="w-full h-full bg-black"
|
className="w-full h-full bg-black"
|
||||||
controls={!controls} // Use native controls if custom controls are disabled
|
controls={!controls || state.error} // Use native controls if custom controls are disabled or there's an error
|
||||||
style={{ width, height }}
|
style={{ width, height }}
|
||||||
playsInline // Important for iOS compatibility
|
playsInline // Important for iOS compatibility
|
||||||
|
preload="metadata" // Load metadata first for better UX
|
||||||
>
|
>
|
||||||
<source src={streamingUrl} type={videoInfo.mimeType} />
|
<source src={streamingUrl} type={videoInfo.mimeType} />
|
||||||
{/* Fallback for MP4 if original format fails */}
|
{/* Fallback for MP4 if original format fails */}
|
||||||
@@ -96,7 +102,10 @@ export const VideoPlayer = forwardRef<HTMLVideoElement, VideoPlayerProps>(({
|
|||||||
{/* Loading Overlay */}
|
{/* Loading Overlay */}
|
||||||
{state.isLoading && (
|
{state.isLoading && (
|
||||||
<div className="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center">
|
<div className="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center">
|
||||||
<div className="text-white text-lg">Loading...</div>
|
<div className="text-white text-center">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white mx-auto mb-2"></div>
|
||||||
|
<div className="text-lg">Loading video...</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { videoApiService } from '../services/videoApi';
|
import { videoApiService } from '../services/videoApi';
|
||||||
|
import { thumbnailCache } from '../utils/thumbnailCache';
|
||||||
import { type VideoThumbnailProps } from '../types';
|
import { type VideoThumbnailProps } from '../types';
|
||||||
|
|
||||||
export const VideoThumbnail: React.FC<VideoThumbnailProps> = ({
|
export const VideoThumbnail: React.FC<VideoThumbnailProps> = ({
|
||||||
@@ -29,6 +30,15 @@ export const VideoThumbnail: React.FC<VideoThumbnailProps> = ({
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
const cachedUrl = thumbnailCache.get(fileId, timestamp, width, height);
|
||||||
|
if (cachedUrl && isMounted) {
|
||||||
|
setThumbnailUrl(cachedUrl);
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch from API if not cached
|
||||||
const blob = await videoApiService.getThumbnailBlob(fileId, {
|
const blob = await videoApiService.getThumbnailBlob(fileId, {
|
||||||
timestamp,
|
timestamp,
|
||||||
width,
|
width,
|
||||||
@@ -36,7 +46,8 @@ export const VideoThumbnail: React.FC<VideoThumbnailProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isMounted) {
|
if (isMounted) {
|
||||||
const url = URL.createObjectURL(blob);
|
// Store in cache and get URL
|
||||||
|
const url = thumbnailCache.set(fileId, timestamp, width, height, blob);
|
||||||
setThumbnailUrl(url);
|
setThumbnailUrl(url);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -52,20 +63,11 @@ export const VideoThumbnail: React.FC<VideoThumbnailProps> = ({
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
isMounted = false;
|
isMounted = false;
|
||||||
if (thumbnailUrl) {
|
// Note: We don't revoke the URL here since it's managed by the cache
|
||||||
URL.revokeObjectURL(thumbnailUrl);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [fileId, timestamp, width, height]);
|
}, [fileId, timestamp, width, height]);
|
||||||
|
|
||||||
// Cleanup URL on unmount
|
// Note: URL cleanup is now handled by the thumbnail cache
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (thumbnailUrl) {
|
|
||||||
URL.revokeObjectURL(thumbnailUrl);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [thumbnailUrl]);
|
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (onClick && !isLoading && !error) {
|
if (onClick && !isLoading && !error) {
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ export { VideoCard } from './VideoCard';
|
|||||||
export { VideoList } from './VideoList';
|
export { VideoList } from './VideoList';
|
||||||
export { VideoModal } from './VideoModal';
|
export { VideoModal } from './VideoModal';
|
||||||
export { Pagination, PageInfo } from './Pagination';
|
export { Pagination, PageInfo } from './Pagination';
|
||||||
|
export { ApiStatusIndicator } from './ApiStatusIndicator';
|
||||||
|
export { VideoErrorBoundary, withVideoErrorBoundary } from './VideoErrorBoundary';
|
||||||
|
export { PerformanceDashboard } from './PerformanceDashboard';
|
||||||
|
export { VideoDebugger } from './VideoDebugger';
|
||||||
|
|
||||||
// Re-export component prop types for convenience
|
// Re-export component prop types for convenience
|
||||||
export type {
|
export type {
|
||||||
|
|||||||
@@ -57,9 +57,11 @@ export function useVideoList(options: UseVideoListOptions = {}) {
|
|||||||
const [loading, setLoading] = useState<LoadingState>('idle');
|
const [loading, setLoading] = useState<LoadingState>('idle');
|
||||||
const [error, setError] = useState<VideoError | null>(null);
|
const [error, setError] = useState<VideoError | null>(null);
|
||||||
const [hasMore, setHasMore] = useState(true);
|
const [hasMore, setHasMore] = useState(true);
|
||||||
|
const [currentParams, setCurrentParams] = useState<VideoListParams>(initialParams);
|
||||||
|
|
||||||
// Refs for cleanup and caching
|
// Refs for cleanup and caching
|
||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -154,9 +156,10 @@ export function useVideoList(options: UseVideoListOptions = {}) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = { ...initialParams, page, limit: initialParams.limit || 20 };
|
const params = { ...currentParams, page, limit: currentParams.limit || 20 };
|
||||||
|
setCurrentParams(params);
|
||||||
await fetchVideos(params, false);
|
await fetchVideos(params, false);
|
||||||
}, [initialParams, totalPages, loading, fetchVideos]);
|
}, [currentParams, totalPages, loading, fetchVideos]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Go to next page
|
* Go to next page
|
||||||
@@ -189,6 +192,7 @@ export function useVideoList(options: UseVideoListOptions = {}) {
|
|||||||
limit: initialParams.limit || 20,
|
limit: initialParams.limit || 20,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setCurrentParams(newParams);
|
||||||
fetchVideos(newParams, false);
|
fetchVideos(newParams, false);
|
||||||
}, [initialParams, fetchVideos]);
|
}, [initialParams, fetchVideos]);
|
||||||
|
|
||||||
|
|||||||
@@ -219,10 +219,29 @@ export function useVideoPlayer(options: UseVideoPlayerOptions = {}) {
|
|||||||
|
|
||||||
const handleLoadStart = () => {
|
const handleLoadStart = () => {
|
||||||
updateState({ isLoading: true, error: null });
|
updateState({ isLoading: true, error: null });
|
||||||
|
|
||||||
|
// Set a timeout to detect if loading takes too long
|
||||||
|
const loadTimeout = setTimeout(() => {
|
||||||
|
if (video && video.readyState < 2) { // HAVE_CURRENT_DATA
|
||||||
|
updateState({
|
||||||
|
isLoading: false,
|
||||||
|
error: 'Video loading timeout. The video may not be accessible or there may be a network issue.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 30000); // 30 second timeout
|
||||||
|
|
||||||
|
// Store timeout ID to clear it later
|
||||||
|
(video as any)._loadTimeout = loadTimeout;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLoadedData = () => {
|
const handleLoadedData = () => {
|
||||||
updateState({ isLoading: false });
|
updateState({ isLoading: false });
|
||||||
|
|
||||||
|
// Clear the loading timeout
|
||||||
|
if ((video as any)._loadTimeout) {
|
||||||
|
clearTimeout((video as any)._loadTimeout);
|
||||||
|
(video as any)._loadTimeout = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTimeUpdate = () => {
|
const handleTimeUpdate = () => {
|
||||||
@@ -252,6 +271,12 @@ export function useVideoPlayer(options: UseVideoPlayerOptions = {}) {
|
|||||||
const errorMessage = video.error?.message || 'Video playback error';
|
const errorMessage = video.error?.message || 'Video playback error';
|
||||||
updateState({ isLoading: false, error: errorMessage, isPlaying: false });
|
updateState({ isLoading: false, error: errorMessage, isPlaying: false });
|
||||||
onError?.(errorMessage);
|
onError?.(errorMessage);
|
||||||
|
|
||||||
|
// Clear the loading timeout
|
||||||
|
if ((video as any)._loadTimeout) {
|
||||||
|
clearTimeout((video as any)._loadTimeout);
|
||||||
|
(video as any)._loadTimeout = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVolumeChange = () => {
|
const handleVolumeChange = () => {
|
||||||
|
|||||||
@@ -13,9 +13,11 @@ import {
|
|||||||
type VideoListParams,
|
type VideoListParams,
|
||||||
type ThumbnailParams,
|
type ThumbnailParams,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
import { performanceMonitor } from '../utils/performanceMonitor';
|
||||||
|
|
||||||
// Configuration
|
// Configuration - Use environment variable or default to vision container
|
||||||
const API_BASE_URL = 'http://vision:8000'; // Based on the test script
|
// The API is accessible at vision:8000 in the current setup
|
||||||
|
const API_BASE_URL = import.meta.env.VITE_VISION_API_URL || 'http://vision:8000';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom error class for video API errors
|
* Custom error class for video API errors
|
||||||
@@ -85,11 +87,46 @@ export class VideoApiService {
|
|||||||
this.baseUrl = baseUrl;
|
this.baseUrl = baseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total count of videos with filters (without pagination)
|
||||||
|
*/
|
||||||
|
private totalCountCache = new Map<string, { count: number; timestamp: number }>();
|
||||||
|
private readonly CACHE_DURATION = 30000; // 30 seconds cache
|
||||||
|
|
||||||
|
private async getTotalCount(params: Omit<VideoListParams, 'limit' | 'offset' | 'page'>): Promise<number> {
|
||||||
|
// Create cache key from params
|
||||||
|
const cacheKey = JSON.stringify(params);
|
||||||
|
const cached = this.totalCountCache.get(cacheKey);
|
||||||
|
|
||||||
|
// Return cached result if still valid
|
||||||
|
if (cached && Date.now() - cached.timestamp < this.CACHE_DURATION) {
|
||||||
|
return cached.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryString = buildQueryString({ ...params, limit: 1000 }); // Use high limit to get accurate total
|
||||||
|
const url = `${this.baseUrl}/videos/${queryString ? `?${queryString}` : ''}`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await handleApiResponse<VideoListResponse>(response);
|
||||||
|
const count = result.videos.length; // Since backend returns wrong total_count, count the actual videos
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
this.totalCountCache.set(cacheKey, { count, timestamp: Date.now() });
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get list of videos with optional filtering
|
* Get list of videos with optional filtering
|
||||||
*/
|
*/
|
||||||
async getVideos(params: VideoListParams = {}): Promise<VideoListResponse> {
|
async getVideos(params: VideoListParams = {}): Promise<VideoListResponse> {
|
||||||
try {
|
return performanceMonitor.trackOperation('get_videos', async () => {
|
||||||
// Convert page-based params to offset-based for API compatibility
|
// Convert page-based params to offset-based for API compatibility
|
||||||
const apiParams = { ...params };
|
const apiParams = { ...params };
|
||||||
|
|
||||||
@@ -113,9 +150,18 @@ export class VideoApiService {
|
|||||||
|
|
||||||
// Add pagination metadata if page was requested
|
// Add pagination metadata if page was requested
|
||||||
if (params.page && params.limit) {
|
if (params.page && params.limit) {
|
||||||
const totalPages = Math.ceil(result.total_count / params.limit);
|
// Get accurate total count by calling without pagination
|
||||||
|
const totalCount = await this.getTotalCount({
|
||||||
|
camera_name: params.camera_name,
|
||||||
|
start_date: params.start_date,
|
||||||
|
end_date: params.end_date,
|
||||||
|
include_metadata: params.include_metadata,
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(totalCount / params.limit);
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
|
total_count: totalCount, // Use accurate total count
|
||||||
page: params.page,
|
page: params.page,
|
||||||
total_pages: totalPages,
|
total_pages: totalPages,
|
||||||
has_next: params.page < totalPages,
|
has_next: params.page < totalPages,
|
||||||
@@ -124,16 +170,7 @@ export class VideoApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
}, { params });
|
||||||
if (error instanceof VideoApiError) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new VideoApiError(
|
|
||||||
'NETWORK_ERROR',
|
|
||||||
'Failed to fetch videos',
|
|
||||||
{ originalError: error }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -205,7 +242,7 @@ export class VideoApiService {
|
|||||||
* Download thumbnail as blob
|
* Download thumbnail as blob
|
||||||
*/
|
*/
|
||||||
async getThumbnailBlob(fileId: string, params: ThumbnailParams = {}): Promise<Blob> {
|
async getThumbnailBlob(fileId: string, params: ThumbnailParams = {}): Promise<Blob> {
|
||||||
try {
|
return performanceMonitor.trackOperation('get_thumbnail', async () => {
|
||||||
const url = this.getThumbnailUrl(fileId, params);
|
const url = this.getThumbnailUrl(fileId, params);
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
|
|
||||||
@@ -218,16 +255,7 @@ export class VideoApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return await response.blob();
|
return await response.blob();
|
||||||
} catch (error) {
|
}, { fileId, params });
|
||||||
if (error instanceof VideoApiError) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new VideoApiError(
|
|
||||||
'NETWORK_ERROR',
|
|
||||||
`Failed to fetch thumbnail for ${fileId}`,
|
|
||||||
{ originalError: error, fileId }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
9
src/features/video-streaming/utils/index.ts
Normal file
9
src/features/video-streaming/utils/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Video Streaming Utils - Index
|
||||||
|
*
|
||||||
|
* Centralized export for all video streaming utilities.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './videoUtils';
|
||||||
|
export * from './thumbnailCache';
|
||||||
|
export * from './performanceMonitor';
|
||||||
197
src/features/video-streaming/utils/performanceMonitor.ts
Normal file
197
src/features/video-streaming/utils/performanceMonitor.ts
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
/**
|
||||||
|
* Performance Monitor for Video Streaming
|
||||||
|
*
|
||||||
|
* Tracks and reports performance metrics for video streaming operations.
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface PerformanceMetric {
|
||||||
|
operation: string;
|
||||||
|
startTime: number;
|
||||||
|
endTime?: number;
|
||||||
|
duration?: number;
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PerformanceStats {
|
||||||
|
totalOperations: number;
|
||||||
|
successfulOperations: number;
|
||||||
|
failedOperations: number;
|
||||||
|
averageDuration: number;
|
||||||
|
minDuration: number;
|
||||||
|
maxDuration: number;
|
||||||
|
successRate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PerformanceMonitor {
|
||||||
|
private metrics: PerformanceMetric[] = [];
|
||||||
|
private maxMetrics: number;
|
||||||
|
|
||||||
|
constructor(maxMetrics: number = 1000) {
|
||||||
|
this.maxMetrics = maxMetrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start tracking an operation
|
||||||
|
*/
|
||||||
|
startOperation(operation: string, metadata?: Record<string, any>): string {
|
||||||
|
const id = `${operation}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
|
||||||
|
const metric: PerformanceMetric = {
|
||||||
|
operation,
|
||||||
|
startTime: performance.now(),
|
||||||
|
success: false,
|
||||||
|
metadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.metrics.push(metric);
|
||||||
|
|
||||||
|
// Keep only the most recent metrics
|
||||||
|
if (this.metrics.length > this.maxMetrics) {
|
||||||
|
this.metrics.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* End tracking an operation
|
||||||
|
*/
|
||||||
|
endOperation(operation: string, success: boolean, error?: string): void {
|
||||||
|
const metric = this.metrics
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.find(m => m.operation === operation && !m.endTime);
|
||||||
|
|
||||||
|
if (metric) {
|
||||||
|
metric.endTime = performance.now();
|
||||||
|
metric.duration = metric.endTime - metric.startTime;
|
||||||
|
metric.success = success;
|
||||||
|
metric.error = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track a complete operation
|
||||||
|
*/
|
||||||
|
async trackOperation<T>(
|
||||||
|
operation: string,
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
metadata?: Record<string, any>
|
||||||
|
): Promise<T> {
|
||||||
|
this.startOperation(operation, metadata);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await fn();
|
||||||
|
this.endOperation(operation, true);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
this.endOperation(operation, false, error instanceof Error ? error.message : 'Unknown error');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get performance statistics for a specific operation
|
||||||
|
*/
|
||||||
|
getStats(operation?: string): PerformanceStats {
|
||||||
|
const filteredMetrics = operation
|
||||||
|
? this.metrics.filter(m => m.operation === operation && m.duration !== undefined)
|
||||||
|
: this.metrics.filter(m => m.duration !== undefined);
|
||||||
|
|
||||||
|
if (filteredMetrics.length === 0) {
|
||||||
|
return {
|
||||||
|
totalOperations: 0,
|
||||||
|
successfulOperations: 0,
|
||||||
|
failedOperations: 0,
|
||||||
|
averageDuration: 0,
|
||||||
|
minDuration: 0,
|
||||||
|
maxDuration: 0,
|
||||||
|
successRate: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const durations = filteredMetrics.map(m => m.duration!);
|
||||||
|
const successfulOps = filteredMetrics.filter(m => m.success).length;
|
||||||
|
const failedOps = filteredMetrics.length - successfulOps;
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalOperations: filteredMetrics.length,
|
||||||
|
successfulOperations: successfulOps,
|
||||||
|
failedOperations: failedOps,
|
||||||
|
averageDuration: durations.reduce((sum, d) => sum + d, 0) / durations.length,
|
||||||
|
minDuration: Math.min(...durations),
|
||||||
|
maxDuration: Math.max(...durations),
|
||||||
|
successRate: successfulOps / filteredMetrics.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get recent metrics
|
||||||
|
*/
|
||||||
|
getRecentMetrics(count: number = 10): PerformanceMetric[] {
|
||||||
|
return this.metrics
|
||||||
|
.filter(m => m.duration !== undefined)
|
||||||
|
.slice(-count)
|
||||||
|
.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all metrics
|
||||||
|
*/
|
||||||
|
clear(): void {
|
||||||
|
this.metrics = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export metrics for analysis
|
||||||
|
*/
|
||||||
|
exportMetrics(): PerformanceMetric[] {
|
||||||
|
return [...this.metrics];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a performance report
|
||||||
|
*/
|
||||||
|
getReport(): string {
|
||||||
|
const operations = [...new Set(this.metrics.map(m => m.operation))];
|
||||||
|
let report = 'Video Streaming Performance Report\n';
|
||||||
|
report += '=====================================\n\n';
|
||||||
|
|
||||||
|
for (const operation of operations) {
|
||||||
|
const stats = this.getStats(operation);
|
||||||
|
report += `${operation}:\n`;
|
||||||
|
report += ` Total Operations: ${stats.totalOperations}\n`;
|
||||||
|
report += ` Success Rate: ${(stats.successRate * 100).toFixed(1)}%\n`;
|
||||||
|
report += ` Average Duration: ${stats.averageDuration.toFixed(2)}ms\n`;
|
||||||
|
report += ` Min Duration: ${stats.minDuration.toFixed(2)}ms\n`;
|
||||||
|
report += ` Max Duration: ${stats.maxDuration.toFixed(2)}ms\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a singleton instance
|
||||||
|
export const performanceMonitor = new PerformanceMonitor();
|
||||||
|
|
||||||
|
// Helper functions for common operations
|
||||||
|
export const trackVideoLoad = (fileId: string) =>
|
||||||
|
performanceMonitor.startOperation('video_load', { fileId });
|
||||||
|
|
||||||
|
export const trackThumbnailLoad = (fileId: string, width: number, height: number) =>
|
||||||
|
performanceMonitor.startOperation('thumbnail_load', { fileId, width, height });
|
||||||
|
|
||||||
|
export const trackApiCall = (endpoint: string) =>
|
||||||
|
performanceMonitor.startOperation('api_call', { endpoint });
|
||||||
|
|
||||||
|
// Log performance stats periodically in development
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
setInterval(() => {
|
||||||
|
const stats = performanceMonitor.getStats();
|
||||||
|
if (stats.totalOperations > 0) {
|
||||||
|
console.log('Video Streaming Performance:', stats);
|
||||||
|
}
|
||||||
|
}, 60000); // Every minute
|
||||||
|
}
|
||||||
224
src/features/video-streaming/utils/thumbnailCache.ts
Normal file
224
src/features/video-streaming/utils/thumbnailCache.ts
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
/**
|
||||||
|
* Thumbnail Cache Utility
|
||||||
|
*
|
||||||
|
* Provides efficient caching for video thumbnails to improve performance
|
||||||
|
* and reduce API calls.
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface CacheEntry {
|
||||||
|
blob: Blob;
|
||||||
|
url: string;
|
||||||
|
timestamp: number;
|
||||||
|
accessCount: number;
|
||||||
|
lastAccessed: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ThumbnailCacheOptions {
|
||||||
|
maxSize: number; // Maximum number of cached thumbnails
|
||||||
|
maxAge: number; // Maximum age in milliseconds
|
||||||
|
maxMemory: number; // Maximum memory usage in bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ThumbnailCache {
|
||||||
|
private cache = new Map<string, CacheEntry>();
|
||||||
|
private options: ThumbnailCacheOptions;
|
||||||
|
|
||||||
|
constructor(options: Partial<ThumbnailCacheOptions> = {}) {
|
||||||
|
this.options = {
|
||||||
|
maxSize: options.maxSize || 100,
|
||||||
|
maxAge: options.maxAge || 30 * 60 * 1000, // 30 minutes
|
||||||
|
maxMemory: options.maxMemory || 50 * 1024 * 1024, // 50MB
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate cache key for a thumbnail
|
||||||
|
*/
|
||||||
|
private generateKey(fileId: string, timestamp: number, width: number, height: number): string {
|
||||||
|
return `${fileId}_${timestamp}_${width}x${height}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get thumbnail from cache
|
||||||
|
*/
|
||||||
|
get(fileId: string, timestamp: number, width: number, height: number): string | null {
|
||||||
|
const key = this.generateKey(fileId, timestamp, width, height);
|
||||||
|
const entry = this.cache.get(key);
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if entry is expired
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - entry.timestamp > this.options.maxAge) {
|
||||||
|
this.delete(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update access statistics
|
||||||
|
entry.accessCount++;
|
||||||
|
entry.lastAccessed = now;
|
||||||
|
|
||||||
|
return entry.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store thumbnail in cache
|
||||||
|
*/
|
||||||
|
set(fileId: string, timestamp: number, width: number, height: number, blob: Blob): string {
|
||||||
|
const key = this.generateKey(fileId, timestamp, width, height);
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Clean up existing entry if it exists
|
||||||
|
const existingEntry = this.cache.get(key);
|
||||||
|
if (existingEntry) {
|
||||||
|
URL.revokeObjectURL(existingEntry.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new entry
|
||||||
|
const entry: CacheEntry = {
|
||||||
|
blob,
|
||||||
|
url,
|
||||||
|
timestamp: now,
|
||||||
|
accessCount: 1,
|
||||||
|
lastAccessed: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.cache.set(key, entry);
|
||||||
|
|
||||||
|
// Cleanup if necessary
|
||||||
|
this.cleanup();
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete specific entry from cache
|
||||||
|
*/
|
||||||
|
delete(key: string): boolean {
|
||||||
|
const entry = this.cache.get(key);
|
||||||
|
if (entry) {
|
||||||
|
URL.revokeObjectURL(entry.url);
|
||||||
|
return this.cache.delete(key);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all cached thumbnails
|
||||||
|
*/
|
||||||
|
clear(): void {
|
||||||
|
for (const entry of this.cache.values()) {
|
||||||
|
URL.revokeObjectURL(entry.url);
|
||||||
|
}
|
||||||
|
this.cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache statistics
|
||||||
|
*/
|
||||||
|
getStats() {
|
||||||
|
const entries = Array.from(this.cache.values());
|
||||||
|
const totalSize = entries.reduce((sum, entry) => sum + entry.blob.size, 0);
|
||||||
|
const totalAccess = entries.reduce((sum, entry) => sum + entry.accessCount, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
size: this.cache.size,
|
||||||
|
totalMemory: totalSize,
|
||||||
|
totalAccess,
|
||||||
|
averageSize: entries.length > 0 ? totalSize / entries.length : 0,
|
||||||
|
averageAccess: entries.length > 0 ? totalAccess / entries.length : 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup expired and least used entries
|
||||||
|
*/
|
||||||
|
private cleanup(): void {
|
||||||
|
const now = Date.now();
|
||||||
|
const entries = Array.from(this.cache.entries());
|
||||||
|
|
||||||
|
// Remove expired entries
|
||||||
|
for (const [key, entry] of entries) {
|
||||||
|
if (now - entry.timestamp > this.options.maxAge) {
|
||||||
|
this.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we need to remove more entries
|
||||||
|
if (this.cache.size <= this.options.maxSize) {
|
||||||
|
const stats = this.getStats();
|
||||||
|
if (stats.totalMemory <= this.options.maxMemory) {
|
||||||
|
return; // No cleanup needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by access frequency and recency (LRU with access count)
|
||||||
|
const sortedEntries = Array.from(this.cache.entries()).sort(([, a], [, b]) => {
|
||||||
|
// Prioritize by access count, then by last accessed time
|
||||||
|
const scoreA = a.accessCount * 1000 + (a.lastAccessed / 1000);
|
||||||
|
const scoreB = b.accessCount * 1000 + (b.lastAccessed / 1000);
|
||||||
|
return scoreA - scoreB; // Ascending order (least valuable first)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove least valuable entries until we're under limits
|
||||||
|
while (
|
||||||
|
(this.cache.size > this.options.maxSize ||
|
||||||
|
this.getStats().totalMemory > this.options.maxMemory) &&
|
||||||
|
sortedEntries.length > 0
|
||||||
|
) {
|
||||||
|
const [key] = sortedEntries.shift()!;
|
||||||
|
this.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preload thumbnails for a list of videos
|
||||||
|
*/
|
||||||
|
async preload(
|
||||||
|
videos: Array<{ file_id: string }>,
|
||||||
|
getThumbnailBlob: (fileId: string, params: any) => Promise<Blob>,
|
||||||
|
options: { timestamp?: number; width?: number; height?: number } = {}
|
||||||
|
): Promise<void> {
|
||||||
|
const { timestamp = 1.0, width = 320, height = 240 } = options;
|
||||||
|
|
||||||
|
const promises = videos.slice(0, 10).map(async (video) => {
|
||||||
|
const key = this.generateKey(video.file_id, timestamp, width, height);
|
||||||
|
|
||||||
|
// Skip if already cached
|
||||||
|
if (this.cache.has(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const blob = await getThumbnailBlob(video.file_id, {
|
||||||
|
timestamp,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
});
|
||||||
|
this.set(video.file_id, timestamp, width, height, blob);
|
||||||
|
} catch (error) {
|
||||||
|
// Silently fail for preloading
|
||||||
|
console.warn(`Failed to preload thumbnail for ${video.file_id}:`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.allSettled(promises);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a singleton instance
|
||||||
|
export const thumbnailCache = new ThumbnailCache({
|
||||||
|
maxSize: 100,
|
||||||
|
maxAge: 30 * 60 * 1000, // 30 minutes
|
||||||
|
maxMemory: 50 * 1024 * 1024, // 50MB
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup on page unload
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
thumbnailCache.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
// Vision System API Client
|
// Vision System API Client
|
||||||
// Base URL for the vision system API
|
// Base URL for the vision system API - Use environment variable or default to vision container
|
||||||
const VISION_API_BASE_URL = 'http://vision:8000'
|
// The API is accessible at vision:8000 in the current setup
|
||||||
|
const VISION_API_BASE_URL = import.meta.env.VITE_VISION_API_URL || 'http://vision:8000'
|
||||||
|
|
||||||
// Types based on the API documentation
|
// Types based on the API documentation
|
||||||
export interface SystemStatus {
|
export interface SystemStatus {
|
||||||
|
|||||||
156
src/test/videoStreamingTest.ts
Normal file
156
src/test/videoStreamingTest.ts
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
/**
|
||||||
|
* Video Streaming API Test
|
||||||
|
*
|
||||||
|
* This test script verifies the video streaming functionality
|
||||||
|
* and API connectivity with the USDA Vision Camera System.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { videoApiService } from '../features/video-streaming/services/videoApi';
|
||||||
|
|
||||||
|
export interface TestResult {
|
||||||
|
test: string;
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
data?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VideoStreamingTester {
|
||||||
|
private results: TestResult[] = [];
|
||||||
|
|
||||||
|
async runAllTests(): Promise<TestResult[]> {
|
||||||
|
this.results = [];
|
||||||
|
|
||||||
|
console.log('🧪 Starting Video Streaming API Tests');
|
||||||
|
console.log('=====================================');
|
||||||
|
|
||||||
|
await this.testApiConnectivity();
|
||||||
|
await this.testVideoList();
|
||||||
|
await this.testVideoInfo();
|
||||||
|
await this.testStreamingUrls();
|
||||||
|
|
||||||
|
console.log('\n📊 Test Results Summary:');
|
||||||
|
console.log('========================');
|
||||||
|
|
||||||
|
const passed = this.results.filter(r => r.success).length;
|
||||||
|
const total = this.results.length;
|
||||||
|
|
||||||
|
this.results.forEach(result => {
|
||||||
|
const icon = result.success ? '✅' : '❌';
|
||||||
|
console.log(`${icon} ${result.test}: ${result.message}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`\n🎯 Tests Passed: ${passed}/${total}`);
|
||||||
|
|
||||||
|
return this.results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async testApiConnectivity(): Promise<void> {
|
||||||
|
try {
|
||||||
|
console.log('\n🔗 Testing API Connectivity...');
|
||||||
|
|
||||||
|
const isHealthy = await videoApiService.healthCheck();
|
||||||
|
|
||||||
|
if (isHealthy) {
|
||||||
|
this.addResult('API Connectivity', true, 'Successfully connected to video API');
|
||||||
|
} else {
|
||||||
|
this.addResult('API Connectivity', false, 'API is not responding');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.addResult('API Connectivity', false, `Connection failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async testVideoList(): Promise<void> {
|
||||||
|
try {
|
||||||
|
console.log('\n📋 Testing Video List...');
|
||||||
|
|
||||||
|
const response = await videoApiService.getVideos({
|
||||||
|
limit: 5,
|
||||||
|
include_metadata: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response && typeof response.total_count === 'number') {
|
||||||
|
this.addResult('Video List', true, `Found ${response.total_count} videos, retrieved ${response.videos.length} items`, response);
|
||||||
|
} else {
|
||||||
|
this.addResult('Video List', false, 'Invalid response format');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.addResult('Video List', false, `Failed to fetch videos: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async testVideoInfo(): Promise<void> {
|
||||||
|
try {
|
||||||
|
console.log('\n📹 Testing Video Info...');
|
||||||
|
|
||||||
|
// First get a video list to test with
|
||||||
|
const videoList = await videoApiService.getVideos({ limit: 1 });
|
||||||
|
|
||||||
|
if (videoList.videos.length === 0) {
|
||||||
|
this.addResult('Video Info', false, 'No videos available to test with');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstVideo = videoList.videos[0];
|
||||||
|
const videoInfo = await videoApiService.getVideoInfo(firstVideo.file_id);
|
||||||
|
|
||||||
|
if (videoInfo && videoInfo.file_id === firstVideo.file_id) {
|
||||||
|
this.addResult('Video Info', true, `Successfully retrieved info for ${firstVideo.file_id}`, videoInfo);
|
||||||
|
} else {
|
||||||
|
this.addResult('Video Info', false, 'Invalid video info response');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.addResult('Video Info', false, `Failed to fetch video info: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async testStreamingUrls(): Promise<void> {
|
||||||
|
try {
|
||||||
|
console.log('\n🎬 Testing Streaming URLs...');
|
||||||
|
|
||||||
|
// Get a video to test with
|
||||||
|
const videoList = await videoApiService.getVideos({ limit: 1 });
|
||||||
|
|
||||||
|
if (videoList.videos.length === 0) {
|
||||||
|
this.addResult('Streaming URLs', false, 'No videos available to test with');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstVideo = videoList.videos[0];
|
||||||
|
|
||||||
|
// Test streaming URL generation
|
||||||
|
const streamingUrl = videoApiService.getStreamingUrl(firstVideo.file_id);
|
||||||
|
const thumbnailUrl = videoApiService.getThumbnailUrl(firstVideo.file_id, {
|
||||||
|
timestamp: 1.0,
|
||||||
|
width: 320,
|
||||||
|
height: 240
|
||||||
|
});
|
||||||
|
|
||||||
|
if (streamingUrl && thumbnailUrl) {
|
||||||
|
this.addResult('Streaming URLs', true, `Generated URLs for ${firstVideo.file_id}`, {
|
||||||
|
streamingUrl,
|
||||||
|
thumbnailUrl
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.addResult('Streaming URLs', false, 'Failed to generate URLs');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.addResult('Streaming URLs', false, `Failed to test URLs: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private addResult(test: string, success: boolean, message: string, data?: any): void {
|
||||||
|
this.results.push({ test, success, message, data });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export for use in browser console
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
(window as any).VideoStreamingTester = VideoStreamingTester;
|
||||||
|
(window as any).runVideoStreamingTests = async () => {
|
||||||
|
const tester = new VideoStreamingTester();
|
||||||
|
return await tester.runAllTests();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VideoStreamingTester;
|
||||||
Reference in New Issue
Block a user