feat(video-streaming): Implement video streaming feature with components, hooks, services, and utilities
- Added centralized exports for video streaming components and hooks. - Implemented `useVideoInfo` hook for fetching and managing video metadata and streaming information. - Developed `useVideoList` hook for managing video list state, fetching, filtering, and pagination. - Created `useVideoPlayer` hook for managing video player state and controls. - Established `videoApiService` for handling API interactions related to video streaming. - Defined TypeScript types for video streaming feature, including video metadata, API responses, and component props. - Added utility functions for video operations, formatting, and data processing. - Created main entry point for the video streaming feature, exporting all public APIs.
This commit is contained in:
300
docs/MODULAR_ARCHITECTURE_GUIDE.md
Normal file
300
docs/MODULAR_ARCHITECTURE_GUIDE.md
Normal file
@@ -0,0 +1,300 @@
|
||||
# 🏗️ Modular Architecture Guide
|
||||
|
||||
This guide demonstrates the modular architecture patterns implemented in the video streaming feature and how to apply them to other parts of the project.
|
||||
|
||||
## 🎯 Goals
|
||||
|
||||
- **Separation of Concerns**: Each module has a single responsibility
|
||||
- **Reusability**: Components can be used across different parts of the application
|
||||
- **Maintainability**: Easy to understand, modify, and test individual pieces
|
||||
- **Scalability**: Easy to add new features without affecting existing code
|
||||
|
||||
## 📁 Feature-Based Structure
|
||||
|
||||
```
|
||||
src/features/video-streaming/
|
||||
├── components/ # UI Components
|
||||
│ ├── VideoPlayer.tsx
|
||||
│ ├── VideoCard.tsx
|
||||
│ ├── VideoList.tsx
|
||||
│ ├── VideoModal.tsx
|
||||
│ ├── VideoThumbnail.tsx
|
||||
│ └── index.ts
|
||||
├── hooks/ # Custom React Hooks
|
||||
│ ├── useVideoList.ts
|
||||
│ ├── useVideoPlayer.ts
|
||||
│ ├── useVideoInfo.ts
|
||||
│ └── index.ts
|
||||
├── services/ # API & Business Logic
|
||||
│ └── videoApi.ts
|
||||
├── types/ # TypeScript Definitions
|
||||
│ └── index.ts
|
||||
├── utils/ # Pure Utility Functions
|
||||
│ └── videoUtils.ts
|
||||
├── VideoStreamingPage.tsx # Main Feature Page
|
||||
└── index.ts # Feature Export
|
||||
```
|
||||
|
||||
## 🧩 Layer Responsibilities
|
||||
|
||||
### 1. **Components Layer** (`/components`)
|
||||
- **Purpose**: Pure UI components that handle rendering and user interactions
|
||||
- **Rules**:
|
||||
- No direct API calls
|
||||
- Receive data via props
|
||||
- Emit events via callbacks
|
||||
- Minimal business logic
|
||||
|
||||
**Example:**
|
||||
```tsx
|
||||
// ✅ Good: Pure component with clear props
|
||||
export const VideoCard: React.FC<VideoCardProps> = ({
|
||||
video,
|
||||
onClick,
|
||||
showMetadata = true,
|
||||
}) => {
|
||||
return (
|
||||
<div onClick={() => onClick?.(video)}>
|
||||
{/* UI rendering */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ❌ Bad: Component with API calls
|
||||
export const VideoCard = () => {
|
||||
const [video, setVideo] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/videos/123').then(/* ... */); // Don't do this!
|
||||
}, []);
|
||||
};
|
||||
```
|
||||
|
||||
### 2. **Hooks Layer** (`/hooks`)
|
||||
- **Purpose**: Manage state, side effects, and provide data to components
|
||||
- **Rules**:
|
||||
- Handle API calls and data fetching
|
||||
- Manage component state
|
||||
- Provide clean interfaces to components
|
||||
|
||||
**Example:**
|
||||
```tsx
|
||||
// ✅ Good: Hook handles complexity, provides simple interface
|
||||
export function useVideoList(options = {}) {
|
||||
const [videos, setVideos] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchVideos = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await videoApiService.getVideos();
|
||||
setVideos(data.videos);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { videos, loading, refetch: fetchVideos };
|
||||
}
|
||||
```
|
||||
|
||||
### 3. **Services Layer** (`/services`)
|
||||
- **Purpose**: Handle external dependencies (APIs, storage, etc.)
|
||||
- **Rules**:
|
||||
- Pure functions or classes
|
||||
- No React dependencies
|
||||
- Handle errors gracefully
|
||||
- Provide consistent interfaces
|
||||
|
||||
**Example:**
|
||||
```tsx
|
||||
// ✅ Good: Service handles API complexity
|
||||
export class VideoApiService {
|
||||
async getVideos(params = {}) {
|
||||
try {
|
||||
const response = await fetch(this.buildUrl('/videos', params));
|
||||
return await this.handleResponse(response);
|
||||
} catch (error) {
|
||||
throw new VideoApiError('FETCH_ERROR', error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. **Types Layer** (`/types`)
|
||||
- **Purpose**: Centralized TypeScript definitions
|
||||
- **Rules**:
|
||||
- Define all interfaces and types
|
||||
- Export from index.ts
|
||||
- Keep types close to their usage
|
||||
|
||||
### 5. **Utils Layer** (`/utils`)
|
||||
- **Purpose**: Pure utility functions
|
||||
- **Rules**:
|
||||
- No side effects
|
||||
- Easily testable
|
||||
- Single responsibility
|
||||
|
||||
## 🔄 Component Composition Patterns
|
||||
|
||||
### Small, Focused Components
|
||||
|
||||
Instead of large monolithic components, create small, focused ones:
|
||||
|
||||
```tsx
|
||||
// ✅ Good: Small, focused components
|
||||
<VideoList>
|
||||
{videos.map(video => (
|
||||
<VideoCard key={video.id} video={video} onClick={onVideoSelect} />
|
||||
))}
|
||||
</VideoList>
|
||||
|
||||
// ❌ Bad: Monolithic component
|
||||
<VideoSystemPage>
|
||||
{/* 500+ lines of mixed concerns */}
|
||||
</VideoSystemPage>
|
||||
```
|
||||
|
||||
### Composition over Inheritance
|
||||
|
||||
```tsx
|
||||
// ✅ Good: Compose features
|
||||
export const VideoStreamingPage = () => {
|
||||
const { videos, loading } = useVideoList();
|
||||
const [selectedVideo, setSelectedVideo] = useState(null);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<VideoList videos={videos} onVideoSelect={setSelectedVideo} />
|
||||
<VideoModal video={selectedVideo} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## 🎨 Applying to Existing Components
|
||||
|
||||
### Example: Breaking Down VisionSystem Component
|
||||
|
||||
**Current Structure (Monolithic):**
|
||||
```tsx
|
||||
// ❌ Current: One large component
|
||||
export const VisionSystem = () => {
|
||||
// 900+ lines of mixed concerns
|
||||
return (
|
||||
<div>
|
||||
{/* System status */}
|
||||
{/* Camera cards */}
|
||||
{/* Storage info */}
|
||||
{/* MQTT status */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
**Proposed Modular Structure:**
|
||||
```
|
||||
src/features/vision-system/
|
||||
├── components/
|
||||
│ ├── SystemStatusCard.tsx
|
||||
│ ├── CameraCard.tsx
|
||||
│ ├── CameraGrid.tsx
|
||||
│ ├── StorageOverview.tsx
|
||||
│ ├── MqttStatus.tsx
|
||||
│ └── index.ts
|
||||
├── hooks/
|
||||
│ ├── useSystemStatus.ts
|
||||
│ ├── useCameraList.ts
|
||||
│ └── index.ts
|
||||
├── services/
|
||||
│ └── visionApi.ts
|
||||
└── VisionSystemPage.tsx
|
||||
```
|
||||
|
||||
**Refactored Usage:**
|
||||
```tsx
|
||||
// ✅ Better: Composed from smaller parts
|
||||
export const VisionSystemPage = () => {
|
||||
return (
|
||||
<div>
|
||||
<SystemStatusCard />
|
||||
<CameraGrid />
|
||||
<StorageOverview />
|
||||
<MqttStatus />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Now you can reuse components elsewhere:
|
||||
export const DashboardHome = () => {
|
||||
return (
|
||||
<div>
|
||||
<SystemStatusCard /> {/* Reused! */}
|
||||
<QuickStats />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## 📋 Migration Strategy
|
||||
|
||||
### Phase 1: Extract Utilities
|
||||
1. Move pure functions to `/utils`
|
||||
2. Move types to `/types`
|
||||
3. Create service classes for API calls
|
||||
|
||||
### Phase 2: Extract Hooks
|
||||
1. Create custom hooks for data fetching
|
||||
2. Move state management to hooks
|
||||
3. Simplify component logic
|
||||
|
||||
### Phase 3: Break Down Components
|
||||
1. Identify distinct UI sections
|
||||
2. Extract to separate components
|
||||
3. Use composition in parent components
|
||||
|
||||
### Phase 4: Feature Organization
|
||||
1. Group related components, hooks, and services
|
||||
2. Create feature-level exports
|
||||
3. Update imports across the application
|
||||
|
||||
## 🧪 Testing Benefits
|
||||
|
||||
Modular architecture makes testing much easier:
|
||||
|
||||
```tsx
|
||||
// ✅ Easy to test individual pieces
|
||||
describe('VideoCard', () => {
|
||||
it('displays video information', () => {
|
||||
render(<VideoCard video={mockVideo} />);
|
||||
expect(screen.getByText(mockVideo.filename)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useVideoList', () => {
|
||||
it('fetches videos on mount', async () => {
|
||||
const { result } = renderHook(() => useVideoList());
|
||||
await waitFor(() => {
|
||||
expect(result.current.videos).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 🚀 Benefits Achieved
|
||||
|
||||
1. **Reusability**: `VideoCard` can be used in lists, grids, or modals
|
||||
2. **Maintainability**: Each file has a single, clear purpose
|
||||
3. **Testability**: Small, focused units are easy to test
|
||||
4. **Developer Experience**: Clear structure makes onboarding easier
|
||||
5. **Performance**: Smaller components enable better optimization
|
||||
|
||||
## 📝 Best Practices
|
||||
|
||||
1. **Start Small**: Begin with one feature and apply patterns gradually
|
||||
2. **Single Responsibility**: Each file should have one clear purpose
|
||||
3. **Clear Interfaces**: Use TypeScript to define clear contracts
|
||||
4. **Consistent Naming**: Follow naming conventions across features
|
||||
5. **Documentation**: Document complex logic and interfaces
|
||||
|
||||
This modular approach transforms large, hard-to-maintain components into small, reusable, and testable pieces that can be composed together to create powerful features.
|
||||
351
docs/VIDEO_STREAMING_INTEGRATION.md
Normal file
351
docs/VIDEO_STREAMING_INTEGRATION.md
Normal file
@@ -0,0 +1,351 @@
|
||||
# 🎬 Video Streaming Integration Guide
|
||||
|
||||
This guide shows how to integrate the modular video streaming feature into your existing dashboard.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### 1. Add to Dashboard Navigation
|
||||
|
||||
Update your sidebar or navigation to include the video streaming page:
|
||||
|
||||
```tsx
|
||||
// In src/components/Sidebar.tsx or similar
|
||||
import { VideoStreamingPage } from '../features/video-streaming';
|
||||
|
||||
const navigationItems = [
|
||||
// ... existing items
|
||||
{
|
||||
name: 'Video Library',
|
||||
href: '/videos',
|
||||
icon: VideoCameraIcon,
|
||||
component: VideoStreamingPage,
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### 2. Add Route (if using React Router)
|
||||
|
||||
```tsx
|
||||
// In your main App.tsx or router configuration
|
||||
import { VideoStreamingPage } from './features/video-streaming';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Routes>
|
||||
{/* ... existing routes */}
|
||||
<Route path="/videos" element={<VideoStreamingPage />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 🧩 Using Individual Components
|
||||
|
||||
The beauty of the modular architecture is that you can use individual components anywhere:
|
||||
|
||||
### Dashboard Home - Recent Videos
|
||||
|
||||
```tsx
|
||||
// In src/components/DashboardHome.tsx
|
||||
import { VideoList } from '../features/video-streaming';
|
||||
|
||||
export const DashboardHome = () => {
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Existing dashboard content */}
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">Recent Videos</h2>
|
||||
<VideoList
|
||||
limit={6}
|
||||
filters={{ /* recent videos only */ }}
|
||||
className="grid grid-cols-2 gap-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Vision System - Camera Videos
|
||||
|
||||
```tsx
|
||||
// In src/components/VisionSystem.tsx
|
||||
import { VideoList, VideoCard } from '../features/video-streaming';
|
||||
|
||||
export const VisionSystem = () => {
|
||||
const [selectedCamera, setSelectedCamera] = useState(null);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Existing vision system content */}
|
||||
|
||||
{/* Add video section for selected camera */}
|
||||
{selectedCamera && (
|
||||
<div className="mt-8">
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
Recent Videos - {selectedCamera}
|
||||
</h3>
|
||||
<VideoList
|
||||
filters={{ cameraName: selectedCamera }}
|
||||
limit={8}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Experiment Data Entry - Video Evidence
|
||||
|
||||
```tsx
|
||||
// In src/components/DataEntry.tsx
|
||||
import { VideoThumbnail, VideoModal } from '../features/video-streaming';
|
||||
|
||||
export const DataEntry = () => {
|
||||
const [selectedVideo, setSelectedVideo] = useState(null);
|
||||
const [showVideoModal, setShowVideoModal] = useState(false);
|
||||
|
||||
return (
|
||||
<form>
|
||||
{/* Existing form fields */}
|
||||
|
||||
{/* Add video evidence section */}
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Video Evidence
|
||||
</label>
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{experimentVideos.map(video => (
|
||||
<VideoThumbnail
|
||||
key={video.file_id}
|
||||
fileId={video.file_id}
|
||||
onClick={() => {
|
||||
setSelectedVideo(video);
|
||||
setShowVideoModal(true);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VideoModal
|
||||
video={selectedVideo}
|
||||
isOpen={showVideoModal}
|
||||
onClose={() => setShowVideoModal(false)}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## 🎨 Customizing Components
|
||||
|
||||
### Custom Video Card for Experiments
|
||||
|
||||
```tsx
|
||||
// Create a specialized version for your use case
|
||||
import { VideoCard } from '../features/video-streaming';
|
||||
|
||||
export const ExperimentVideoCard = ({ video, experimentId, onAttach }) => {
|
||||
return (
|
||||
<div className="relative">
|
||||
<VideoCard video={video} showMetadata={false} />
|
||||
|
||||
{/* Add experiment-specific actions */}
|
||||
<div className="absolute top-2 right-2">
|
||||
<button
|
||||
onClick={() => onAttach(video.file_id, experimentId)}
|
||||
className="bg-blue-500 text-white px-2 py-1 rounded text-xs"
|
||||
>
|
||||
Attach to Experiment
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Custom Video Player with Annotations
|
||||
|
||||
```tsx
|
||||
// Extend the base video player
|
||||
import { VideoPlayer } from '../features/video-streaming';
|
||||
|
||||
export const AnnotatedVideoPlayer = ({ fileId, annotations }) => {
|
||||
return (
|
||||
<div className="relative">
|
||||
<VideoPlayer fileId={fileId} />
|
||||
|
||||
{/* Add annotation overlay */}
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
{annotations.map(annotation => (
|
||||
<div
|
||||
key={annotation.id}
|
||||
className="absolute bg-yellow-400 bg-opacity-75 p-2 rounded"
|
||||
style={{
|
||||
left: `${annotation.x}%`,
|
||||
top: `${annotation.y}%`,
|
||||
}}
|
||||
>
|
||||
{annotation.text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### API Base URL
|
||||
|
||||
Update the API base URL if needed:
|
||||
|
||||
```tsx
|
||||
// In your app configuration
|
||||
import { VideoApiService } from './features/video-streaming';
|
||||
|
||||
// Create a configured instance
|
||||
export const videoApi = new VideoApiService('http://your-api-server:8000');
|
||||
|
||||
// Or set globally
|
||||
process.env.REACT_APP_VIDEO_API_URL = 'http://your-api-server:8000';
|
||||
```
|
||||
|
||||
### Custom Styling
|
||||
|
||||
The components use Tailwind CSS classes. You can customize them:
|
||||
|
||||
```tsx
|
||||
// Override default styles
|
||||
<VideoList
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-8" // Custom grid
|
||||
/>
|
||||
|
||||
<VideoCard
|
||||
className="border-2 border-blue-200 hover:border-blue-400" // Custom border
|
||||
/>
|
||||
```
|
||||
|
||||
## 🎯 Integration Examples
|
||||
|
||||
### 1. Camera Management Integration
|
||||
|
||||
```tsx
|
||||
// In your camera management page
|
||||
import { VideoList, useVideoList } from '../features/video-streaming';
|
||||
|
||||
export const CameraManagement = () => {
|
||||
const [selectedCamera, setSelectedCamera] = useState(null);
|
||||
const { videos } = useVideoList({
|
||||
initialParams: { camera_name: selectedCamera?.name }
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Camera controls */}
|
||||
<CameraControls onCameraSelect={setSelectedCamera} />
|
||||
|
||||
{/* Videos from selected camera */}
|
||||
<div>
|
||||
<h3>Videos from {selectedCamera?.name}</h3>
|
||||
<VideoList
|
||||
filters={{ cameraName: selectedCamera?.name }}
|
||||
limit={12}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Experiment Timeline Integration
|
||||
|
||||
```tsx
|
||||
// Show videos in experiment timeline
|
||||
import { VideoThumbnail } from '../features/video-streaming';
|
||||
|
||||
export const ExperimentTimeline = ({ experiment }) => {
|
||||
return (
|
||||
<div className="timeline">
|
||||
{experiment.events.map(event => (
|
||||
<div key={event.id} className="timeline-item">
|
||||
<div className="timeline-content">
|
||||
<h4>{event.title}</h4>
|
||||
<p>{event.description}</p>
|
||||
|
||||
{/* Show related videos */}
|
||||
{event.videos?.length > 0 && (
|
||||
<div className="flex space-x-2 mt-2">
|
||||
{event.videos.map(videoId => (
|
||||
<VideoThumbnail
|
||||
key={videoId}
|
||||
fileId={videoId}
|
||||
width={120}
|
||||
height={80}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## 📱 Responsive Design
|
||||
|
||||
The components are designed to be responsive:
|
||||
|
||||
```tsx
|
||||
// Automatic responsive grid
|
||||
<VideoList className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4" />
|
||||
|
||||
// Mobile-friendly video player
|
||||
<VideoPlayer
|
||||
fileId={video.file_id}
|
||||
className="w-full h-auto max-h-96"
|
||||
/>
|
||||
```
|
||||
|
||||
## 🔍 Search Integration
|
||||
|
||||
Add search functionality:
|
||||
|
||||
```tsx
|
||||
import { useVideoList } from '../features/video-streaming';
|
||||
|
||||
export const VideoSearch = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const { videos, loading } = useVideoList({
|
||||
initialParams: { search: searchTerm }
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder="Search videos..."
|
||||
className="w-full px-4 py-2 border rounded-lg"
|
||||
/>
|
||||
|
||||
<VideoList videos={videos} loading={loading} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## 🚀 Next Steps
|
||||
|
||||
1. **Start Small**: Begin by adding the video library page
|
||||
2. **Integrate Gradually**: Add individual components to existing pages
|
||||
3. **Customize**: Create specialized versions for your specific needs
|
||||
4. **Extend**: Add new features like annotations, bookmarks, or sharing
|
||||
|
||||
The modular architecture makes it easy to start simple and grow the functionality over time!
|
||||
Reference in New Issue
Block a user