"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 ( Detection Summary Overview of {isPothole ? "pothole" : "signboard"} detection results {stats.map((stat, index) => { const Icon = stat.icon return ( {stat.value} {stat.label} ) })} ) } export default function VideoPlayerSection({ data, videoId, detectionType }: VideoPlayerSectionProps) { const videoRef = useRef(null) const [currentFrame, setCurrentFrame] = useState(0) const [detectionsCount, setDetectionsCount] = useState(0) const [logs, setLogs] = useState([]) const [showSummary, setShowSummary] = useState(false) const [hasPlayedOnce, setHasPlayedOnce] = useState(false) const [videoError, setVideoError] = useState(null) const frameDetectionMap = useRef>(new Map()) const lastProcessedFrame = useRef(-1) const loggedFrames = useRef>(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 ( Video Playback with Detection Watch the processed video with {isPothole ? "pothole" : "signboard"} detections already drawn {/* Video Player */} {videoError && ( {videoError} )} Your browser does not support the video tag. {/* Video Info */} Current Frame: {currentFrame} Detections: 0 ? (isPothole ? "destructive" : "default") : "secondary"}> {detectionsCount} Resolution: {data.video_info.width}×{data.video_info.height} FPS: {data.video_info.fps.toFixed(1)} {/* Detection Logs */} Detection Logs Real-time frame-by-frame {isPothole ? "pothole" : "signboard"} tracking (last {MAX_LOGS}) {logs.length === 0 ? ( Play the video to see detection logs ) : ( logs.map((log, index) => ( Frame: {log.frame} {log.timestamp} {log.detections.length === 0 ? ( No detections ) : ( {log.detections.map((det, idx) => ( {isPothole ? ( <>Pothole ID: {det.id}> ) : ( <>{det.type || 'Signboard'} ID: {det.id}> )} {" "}| Confidence: {(det.confidence * 100).toFixed(1)}% Coordinates: ({Math.round(det.bbox.x1)}, {Math.round(det.bbox.y1)}) → ({Math.round(det.bbox.x2)}, {Math.round(det.bbox.y2)}) ))} )} )) )} {/* Summary Section - Shows after first complete playback and persists */} ) }
Real-time frame-by-frame {isPothole ? "pothole" : "signboard"} tracking (last {MAX_LOGS})
Play the video to see detection logs