Files
road-monitoring-ui/components/video-player-section.tsx
sumona-banerjeee a83545d1db Applied Options
2026-01-29 12:22:21 +05:30

418 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { useEffect, useRef, useState, useCallback } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Badge } from "@/components/ui/badge"
import { Target, AlertTriangle, Film, Activity, Gauge, Monitor, SignpostBig } from "lucide-react"
import type { DetectionData, DetectionType } from "@/app/page"
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://127.0.0.1:8000/api/v1"
type VideoPlayerSectionProps = {
data: DetectionData
videoId: string
detectionType: DetectionType
}
type DetectionLog = {
frame: number
detections: Array<{
id: number
type?: string // For signboards
bbox: { x1: number; y1: number; x2: number; y2: number }
confidence: number
}>
timestamp: string
}
function SummarySection({ data, show, detectionType }: { data: DetectionData; show: boolean; detectionType: DetectionType }) {
if (!show) return null
const isPothole = detectionType === "pothole-detection"
const isSignboard = detectionType === "sign-board-detection"
const stats = [
{
label: isPothole ? "Unique Potholes" : "Unique Signboards",
value: (isPothole ? data.summary.unique_potholes : data.summary.unique_signboards) || 0,
icon: isPothole ? AlertTriangle : SignpostBig,
color: isPothole ? "text-red-500" : "text-blue-500",
bgColor: isPothole ? "bg-red-50 dark:bg-red-950/30" : "bg-blue-50 dark:bg-blue-950/30",
},
{
label: "Total Detections",
value: data.summary.total_detections || 0,
icon: Target,
color: "text-blue-500",
bgColor: "bg-blue-50 dark:bg-blue-950/30",
},
{
label: "Total Frames",
value: data.summary.total_frames || data.video_info.total_frames,
icon: Film,
color: "text-purple-500",
bgColor: "bg-purple-50 dark:bg-purple-950/30",
},
{
label: "Detection Rate",
value: `${(data.summary.detection_rate || 0).toFixed(1)}%`,
icon: Activity,
color: "text-green-500",
bgColor: "bg-green-50 dark:bg-green-950/30",
},
{
label: "Video FPS",
value: (data.video_info.fps || 0).toFixed(1),
icon: Gauge,
color: "text-orange-500",
bgColor: "bg-orange-50 dark:bg-orange-950/30",
},
{
label: "Resolution",
value: `${data.video_info.width}×${data.video_info.height}`,
icon: Monitor,
color: "text-cyan-500",
bgColor: "bg-cyan-50 dark:bg-cyan-950/30",
},
]
return (
<Card className="animate-in fade-in slide-in-from-bottom duration-500">
<CardHeader>
<CardTitle>Detection Summary</CardTitle>
<CardDescription>
Overview of {isPothole ? "pothole" : "signboard"} detection results
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
{stats.map((stat, index) => {
const Icon = stat.icon
return (
<div
key={stat.label}
className="flex flex-col items-center justify-center p-4 rounded-lg transition-all hover:scale-105 animate-in fade-in slide-in-from-bottom duration-500"
style={{ animationDelay: `${index * 100}ms` }}
>
<div className={`${stat.bgColor} p-3 rounded-full mb-3 transition-all`}>
<Icon className={`h-5 w-5 ${stat.color}`} />
</div>
<div className={`text-3xl font-bold ${stat.color} mb-1`}>{stat.value}</div>
<div className="text-xs text-muted-foreground text-center">{stat.label}</div>
</div>
)
})}
</div>
</CardContent>
</Card>
)
}
export default function VideoPlayerSection({ data, videoId, detectionType }: VideoPlayerSectionProps) {
const videoRef = useRef<HTMLVideoElement>(null)
const [currentFrame, setCurrentFrame] = useState(0)
const [detectionsCount, setDetectionsCount] = useState(0)
const [logs, setLogs] = useState<DetectionLog[]>([])
const [showSummary, setShowSummary] = useState(false)
const [hasPlayedOnce, setHasPlayedOnce] = useState(false)
const [videoError, setVideoError] = useState<string | null>(null)
const frameDetectionMap = useRef<Map<number, any[]>>(new Map())
const lastProcessedFrame = useRef(-1)
const loggedFrames = useRef<Set<number>>(new Set())
const MAX_LOGS = 50
const isPothole = detectionType === "pothole-detection"
const isSignboard = detectionType === "sign-board-detection"
// Build optimized frame detection map
useEffect(() => {
const map = new Map()
if (data.frames && Array.isArray(data.frames)) {
console.log(`[VideoPlayer] Building frame map from ${data.frames.length} frames`)
data.frames.forEach((frameData) => {
const frameId = frameData.frame_id
// Handle both pothole and signboard detections
const detections = isPothole ? (frameData.potholes || []) : (frameData.signboards || [])
if (detections.length > 0) {
map.set(frameId, detections)
}
})
console.log(`[VideoPlayer] Frame map built: ${map.size} frames with detections`)
}
frameDetectionMap.current = map
}, [data, isPothole])
// Load processed video from backend
useEffect(() => {
if (videoRef.current && videoId) {
const videoUrl = `${API_URL}/video/${videoId}`
console.log(`[VideoPlayer] Loading processed video from: ${videoUrl}`)
videoRef.current.src = videoUrl
// Handle video load errors
const handleError = () => {
console.error("[VideoPlayer] Failed to load processed video")
setVideoError("Failed to load processed video. Please try refreshing the page.")
}
const handleLoaded = () => {
console.log("[VideoPlayer] Processed video loaded successfully")
setVideoError(null)
}
videoRef.current.addEventListener("error", handleError)
videoRef.current.addEventListener("loadeddata", handleLoaded)
return () => {
if (videoRef.current) {
videoRef.current.removeEventListener("error", handleError)
videoRef.current.removeEventListener("loadeddata", handleLoaded)
}
}
}
}, [videoId])
// Add detection log with deduplication and size limit
const addDetectionLog = useCallback((frame: number, detections: any[]) => {
if (loggedFrames.current.has(frame)) return
loggedFrames.current.add(frame)
setLogs((prev) => {
const newLog: DetectionLog = {
frame,
detections: detections.map((det) => ({
id: isPothole ? det.pothole_id : det.signboard_id,
type: isSignboard ? det.type : undefined,
bbox: det.bbox,
confidence: det.confidence,
})),
timestamp: new Date().toLocaleTimeString(),
}
const updated = [newLog, ...prev].slice(0, MAX_LOGS)
return updated
})
}, [isPothole, isSignboard])
// Track current frame and log detections
const updateFrameInfo = useCallback(() => {
const video = videoRef.current
if (!video) return
const frame = Math.round(video.currentTime * data.video_info.fps)
if (!video.paused && !video.ended && frame !== lastProcessedFrame.current) {
lastProcessedFrame.current = frame
setCurrentFrame(frame)
const detections = frameDetectionMap.current.get(frame)
setDetectionsCount(detections?.length || 0)
if (detections && detections.length > 0) {
addDetectionLog(frame, detections)
}
}
}, [data.video_info.fps, addDetectionLog])
// Video playback monitoring
useEffect(() => {
const video = videoRef.current
if (!video) return
let animationId: number
const animate = () => {
updateFrameInfo()
animationId = requestAnimationFrame(animate)
}
animationId = requestAnimationFrame(animate)
return () => {
cancelAnimationFrame(animationId)
}
}, [updateFrameInfo])
// Handle video end - show summary
useEffect(() => {
const video = videoRef.current
if (!video) return
const handleEnded = () => {
if (!hasPlayedOnce) {
setHasPlayedOnce(true)
setShowSummary(true)
}
}
video.addEventListener("ended", handleEnded)
return () => {
video.removeEventListener("ended", handleEnded)
}
}, [hasPlayedOnce])
// Handle seeking and time updates
useEffect(() => {
const video = videoRef.current
if (!video) return
const handleTimeUpdate = () => {
const frame = Math.round(video.currentTime * data.video_info.fps)
setCurrentFrame(frame)
const detections = frameDetectionMap.current.get(frame)
setDetectionsCount(detections?.length || 0)
}
const handleSeeked = () => {
loggedFrames.current.clear()
handleTimeUpdate()
}
const handlePlay = () => {
if (video.currentTime < 0.1) {
setLogs([])
loggedFrames.current.clear()
lastProcessedFrame.current = -1
}
}
video.addEventListener("timeupdate", handleTimeUpdate)
video.addEventListener("seeked", handleSeeked)
video.addEventListener("play", handlePlay)
return () => {
video.removeEventListener("timeupdate", handleTimeUpdate)
video.removeEventListener("seeked", handleSeeked)
video.removeEventListener("play", handlePlay)
}
}, [data.video_info.fps])
return (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Video Playback with Detection</CardTitle>
<CardDescription>
Watch the processed video with {isPothole ? "pothole" : "signboard"} detections already drawn
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Video Player */}
<div className="lg:col-span-2 space-y-4">
{videoError && (
<div className="bg-destructive/10 text-destructive p-4 rounded-lg mb-4">
{videoError}
</div>
)}
<div className="relative bg-black rounded-lg overflow-hidden">
<video
ref={videoRef}
controls
className="w-full h-auto"
style={{
aspectRatio: `${data.video_info.width} / ${data.video_info.height}`,
}}
>
Your browser does not support the video tag.
</video>
</div>
{/* Video Info */}
<div className="flex flex-wrap gap-4 text-sm">
<div className="flex items-center gap-2">
<span className="text-muted-foreground">Current Frame:</span>
<Badge variant="secondary">{currentFrame}</Badge>
</div>
<div className="flex items-center gap-2">
<span className="text-muted-foreground">Detections:</span>
<Badge variant={detectionsCount > 0 ? (isPothole ? "destructive" : "default") : "secondary"}>
{detectionsCount}
</Badge>
</div>
<div className="flex items-center gap-2">
<span className="text-muted-foreground">Resolution:</span>
<Badge variant="outline">
{data.video_info.width}×{data.video_info.height}
</Badge>
</div>
<div className="flex items-center gap-2">
<span className="text-muted-foreground">FPS:</span>
<Badge variant="outline">{data.video_info.fps.toFixed(1)}</Badge>
</div>
</div>
</div>
{/* Detection Logs */}
<div className="space-y-4">
<div>
<h4 className="text-sm font-semibold mb-2">Detection Logs</h4>
<p className="text-xs text-muted-foreground mb-3">
Real-time frame-by-frame {isPothole ? "pothole" : "signboard"} tracking (last {MAX_LOGS})
</p>
</div>
<ScrollArea className="h-[400px] rounded-md border bg-muted/30 p-4">
<div className="space-y-2">
{logs.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-8">
Play the video to see detection logs
</p>
) : (
logs.map((log, index) => (
<div
key={`${log.frame}-${index}`}
className={`text-xs p-3 bg-card rounded-md border-l-2 ${
isPothole ? "border-red-500" : "border-blue-500"
}`}
>
<div className="flex items-center justify-between mb-2">
<span className="font-semibold text-foreground">Frame: {log.frame}</span>
<span className="text-muted-foreground text-[10px]">{log.timestamp}</span>
</div>
{log.detections.length === 0 ? (
<div className="text-muted-foreground">No detections</div>
) : (
<div className="space-y-2">
{log.detections.map((det, idx) => (
<div key={idx} className="space-y-1">
<div className="font-medium text-foreground">
{isPothole ? (
<>Pothole ID: {det.id}</>
) : (
<>{det.type || 'Signboard'} ID: {det.id}</>
)}
{" "}| Confidence: {(det.confidence * 100).toFixed(1)}%
</div>
<div className="text-muted-foreground font-mono text-[10px]">
Coordinates: ({Math.round(det.bbox.x1)}, {Math.round(det.bbox.y1)}) ({Math.round(det.bbox.x2)}, {Math.round(det.bbox.y2)})
</div>
</div>
))}
</div>
)}
</div>
))
)}
</div>
</ScrollArea>
</div>
</div>
</CardContent>
</Card>
{/* Summary Section - Shows after first complete playback and persists */}
<SummarySection data={data} show={showSummary} detectionType={detectionType} />
</div>
)
}