Refactor video streaming feature and update dependencies
- Replaced npm ci with npm install in docker-compose for better package management. - Introduced remote component loading for the VideoStreamingPage with error handling. - Updated the title in index.html to "Experiments Dashboard" for clarity. - Added new video remote service configuration in docker-compose for improved integration. - Removed deprecated files and components related to the video streaming feature to streamline the codebase. - Updated package.json and package-lock.json to include @originjs/vite-plugin-federation for module federation support.
This commit is contained in:
14
video-remote/index.html
Normal file
14
video-remote/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>video-remote</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
3985
video-remote/package-lock.json
generated
Normal file
3985
video-remote/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
video-remote/package.json
Normal file
28
video-remote/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "video-remote",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve:dist": "serve -s dist -l 3001",
|
||||
"preview": "vite preview --port 3001"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@originjs/vite-plugin-federation": "^1.3.3",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"http-server": "^14.1.1",
|
||||
"serve": "^14.2.3",
|
||||
"vite": "^7.0.4",
|
||||
"typescript": "~5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
20
video-remote/src/App.tsx
Normal file
20
video-remote/src/App.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { VideoList } from './components/VideoList'
|
||||
import { VideoModal } from './components/VideoModal'
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [selected, setSelected] = useState<string | null>(null)
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-4">
|
||||
<h1 className="text-2xl font-bold">Video Library</h1>
|
||||
<VideoList onSelect={(id) => setSelected(id)} />
|
||||
<VideoModal fileId={selected} onClose={() => setSelected(null)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
|
||||
36
video-remote/src/components/FiltersBar.tsx
Normal file
36
video-remote/src/components/FiltersBar.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React, { useState } from 'react'
|
||||
|
||||
type Props = {
|
||||
onChange: (filters: { camera_name?: string; start_date?: string; end_date?: string }) => void
|
||||
}
|
||||
|
||||
export const FiltersBar: React.FC<Props> = ({ onChange }) => {
|
||||
const [camera, setCamera] = useState('')
|
||||
const [start, setStart] = useState('')
|
||||
const [end, setEnd] = useState('')
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm mb-1">Camera</label>
|
||||
<input value={camera} onChange={e=>setCamera(e.target.value)} placeholder="camera1" className="w-full px-3 py-2 border rounded" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm mb-1">Start date</label>
|
||||
<input type="date" value={start} onChange={e=>setStart(e.target.value)} className="w-full px-3 py-2 border rounded" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm mb-1">End date</label>
|
||||
<input type="date" value={end} onChange={e=>setEnd(e.target.value)} className="w-full px-3 py-2 border rounded" />
|
||||
</div>
|
||||
<div className="flex items-end gap-2">
|
||||
<button className="px-3 py-2 border rounded" onClick={()=>onChange({ camera_name: camera || undefined, start_date: start || undefined, end_date: end || undefined })}>Apply</button>
|
||||
<button className="px-3 py-2 border rounded" onClick={()=>{ setCamera(''); setStart(''); setEnd(''); onChange({}) }}>Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FiltersBar
|
||||
|
||||
|
||||
22
video-remote/src/components/Pagination.tsx
Normal file
22
video-remote/src/components/Pagination.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
page: number
|
||||
totalPages: number
|
||||
onChange: (page: number) => void
|
||||
}
|
||||
|
||||
export const Pagination: React.FC<Props> = ({ page, totalPages, onChange }) => {
|
||||
if (totalPages <= 1) return null
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<button disabled={page<=1} onClick={()=>onChange(page-1)} className="px-3 py-1 border rounded disabled:opacity-50">Prev</button>
|
||||
<span className="text-sm">Page {page} / {totalPages}</span>
|
||||
<button disabled={page>=totalPages} onClick={()=>onChange(page+1)} className="px-3 py-1 border rounded disabled:opacity-50">Next</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Pagination
|
||||
|
||||
|
||||
42
video-remote/src/components/VideoCard.tsx
Normal file
42
video-remote/src/components/VideoCard.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react'
|
||||
import { VideoThumbnail } from './VideoThumbnail'
|
||||
|
||||
export type VideoFile = {
|
||||
file_id: string
|
||||
filename: string
|
||||
camera_name: string
|
||||
file_size_bytes: number
|
||||
created_at: string
|
||||
status: string
|
||||
format: string
|
||||
is_streamable?: boolean
|
||||
needs_conversion?: boolean
|
||||
metadata?: { duration_seconds: number; width: number; height: number; fps: number; codec: string }
|
||||
}
|
||||
|
||||
export type VideoCardProps = {
|
||||
video: VideoFile
|
||||
onClick?: (video: VideoFile) => void
|
||||
showMetadata?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const VideoCard: React.FC<VideoCardProps> = ({ video, onClick }) => {
|
||||
return (
|
||||
<div
|
||||
className={`bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow overflow-hidden ${onClick ? 'cursor-pointer' : ''}`}
|
||||
onClick={onClick ? () => onClick(video) : undefined}
|
||||
>
|
||||
<VideoThumbnail fileId={video.file_id} width={640} height={360} className="w-full h-auto" alt={video.filename} />
|
||||
<div className="p-4">
|
||||
<div className="text-sm text-gray-500 mb-1">{video.camera_name}</div>
|
||||
<div className="font-semibold text-gray-900 truncate" title={video.filename}>{video.filename}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default VideoCard
|
||||
|
||||
|
||||
|
||||
52
video-remote/src/components/VideoList.tsx
Normal file
52
video-remote/src/components/VideoList.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { fetchVideos } from '../services/video'
|
||||
import { Pagination } from './Pagination'
|
||||
import VideoCard from './VideoCard'
|
||||
import { FiltersBar } from './FiltersBar'
|
||||
|
||||
type Props = { onSelect?: (fileId: string) => void }
|
||||
|
||||
export const VideoList: React.FC<Props> = ({ onSelect }) => {
|
||||
const [videos, setVideos] = useState<any[]>([])
|
||||
const [page, setPage] = useState(1)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const [filters, setFilters] = useState<{ camera_name?: string; start_date?: string; end_date?: string }>({})
|
||||
|
||||
async function load(p: number, f = filters) {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await fetchVideos({ page: p, limit: 12, ...f })
|
||||
setVideos(res.videos)
|
||||
setTotalPages(res.total_pages || 1)
|
||||
} catch (e: any) {
|
||||
setError(e?.message || 'Failed to load videos')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => { load(page) }, [page])
|
||||
|
||||
if (loading) return <div className="p-6">Loading videos…</div>
|
||||
if (error) return <div className="p-6 text-red-600">{error}</div>
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<FiltersBar onChange={(f) => { setFilters(f); setPage(1); load(1, f) }} />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||
{videos.map(v => (
|
||||
<VideoCard key={v.file_id} video={v} onClick={onSelect ? () => onSelect(v.file_id) : undefined} />
|
||||
))}
|
||||
</div>
|
||||
<Pagination page={page} totalPages={totalPages} onChange={setPage} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default VideoList
|
||||
|
||||
|
||||
29
video-remote/src/components/VideoModal.tsx
Normal file
29
video-remote/src/components/VideoModal.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
const BASE = (import.meta as any).env?.VITE_VISION_API_URL || '/api'
|
||||
|
||||
type Props = {
|
||||
fileId: string | null
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export const VideoModal: React.FC<Props> = ({ fileId, onClose }) => {
|
||||
if (!fileId) return null
|
||||
const src = `${BASE}/videos/${fileId}/stream`
|
||||
return (
|
||||
<div className="fixed inset-0 z-[1000] bg-black/50 flex items-center justify-center">
|
||||
<div className="bg-white rounded-lg w-full max-w-3xl overflow-hidden">
|
||||
<div className="flex items-center justify-between p-3 border-b">
|
||||
<div className="font-semibold">Video</div>
|
||||
<button onClick={onClose} className="px-2 py-1 border rounded">Close</button>
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<video src={src} controls className="w-full h-auto" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default VideoModal
|
||||
|
||||
|
||||
26
video-remote/src/components/VideoThumbnail.tsx
Normal file
26
video-remote/src/components/VideoThumbnail.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
const BASE = (import.meta as any).env?.VITE_VISION_API_URL || '/api'
|
||||
|
||||
type Props = {
|
||||
fileId: string
|
||||
width?: number
|
||||
height?: number
|
||||
alt?: string
|
||||
className?: string
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
export const VideoThumbnail: React.FC<Props> = ({ fileId, width = 320, height = 180, alt = '', className = '', onClick }) => {
|
||||
const p = new URLSearchParams()
|
||||
if (width) p.append('width', String(width))
|
||||
if (height) p.append('height', String(height))
|
||||
const qs = p.toString()
|
||||
const src = `${BASE}/videos/${fileId}/thumbnail${qs ? `?${qs}` : ''}`
|
||||
return (
|
||||
<img src={src} alt={alt} width={width} height={height} className={className} onClick={onClick} />
|
||||
)
|
||||
}
|
||||
|
||||
export default VideoThumbnail
|
||||
|
||||
|
||||
3
video-remote/src/index.css
Normal file
3
video-remote/src/index.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
|
||||
9
video-remote/src/main.tsx
Normal file
9
video-remote/src/main.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
|
||||
const root = createRoot(document.getElementById('root')!)
|
||||
root.render(<App />)
|
||||
|
||||
|
||||
24
video-remote/src/services/video.ts
Normal file
24
video-remote/src/services/video.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
type VideoListParams = { camera_name?: string; start_date?: string; end_date?: string; limit?: number; page?: number; offset?: number; include_metadata?: boolean }
|
||||
type VideoFile = { file_id: string; camera_name: string; filename: string; file_size_bytes: number; format: string; status: string; created_at: string; is_streamable: boolean; needs_conversion: boolean }
|
||||
type VideoListResponse = { videos: VideoFile[]; total_count: number; page?: number; total_pages?: number }
|
||||
|
||||
const BASE = (import.meta as any).env?.VITE_VISION_API_URL || '/api'
|
||||
|
||||
async function get<T>(path: string): Promise<T> {
|
||||
const res = await fetch(path, { headers: { 'Accept': 'application/json' } })
|
||||
if (!res.ok) throw new Error(`Request failed: ${res.status}`)
|
||||
return res.json() as Promise<T>
|
||||
}
|
||||
|
||||
export async function fetchVideos(params: VideoListParams = {}): Promise<VideoListResponse> {
|
||||
const search = new URLSearchParams()
|
||||
Object.entries(params).forEach(([k, v]) => {
|
||||
if (v !== undefined && v !== null) search.append(k, String(v))
|
||||
})
|
||||
const qs = search.toString()
|
||||
return get(`${BASE}/videos/${qs ? `?${qs}` : ''}`)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
14
video-remote/tsconfig.json
Normal file
14
video-remote/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
|
||||
34
video-remote/vite.config.ts
Normal file
34
video-remote/vite.config.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import federation from '@originjs/vite-plugin-federation'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
tailwindcss(),
|
||||
federation({
|
||||
name: 'videoRemote',
|
||||
filename: 'remoteEntry.js',
|
||||
exposes: {
|
||||
'./App': './src/App.tsx',
|
||||
'./VideoCard': './src/components/VideoCard.tsx',
|
||||
},
|
||||
shared: {
|
||||
react: { singleton: true, eager: true },
|
||||
'react-dom': { singleton: true, eager: true },
|
||||
},
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
port: 3001,
|
||||
host: '0.0.0.0',
|
||||
allowedHosts: ['exp-dash', 'localhost'],
|
||||
cors: true
|
||||
},
|
||||
build: {
|
||||
target: 'esnext',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user