138 lines
3.7 KiB
TypeScript
138 lines
3.7 KiB
TypeScript
'use client';
|
|
|
|
import { RefObject, useEffect } from 'react';
|
|
import { AlertTriangle, Loader2 } from 'lucide-react';
|
|
import type {
|
|
DetectionResultLog,
|
|
VideoAnnotationFrameDetection,
|
|
} from '@/types';
|
|
import { useDetectionThumbnails } from './useDetectionThumbnails';
|
|
import BoundingBoxOverlay from '@/components/annotation/boundingBoxOverlay';
|
|
import {
|
|
MediaPlayer,
|
|
MediaProvider,
|
|
Poster,
|
|
isVideoProvider,
|
|
type MediaPlayerInstance,
|
|
useMediaProvider,
|
|
} from '@vidstack/react';
|
|
import {
|
|
DefaultVideoLayout,
|
|
defaultLayoutIcons,
|
|
} from '@vidstack/react/player/layouts/default';
|
|
|
|
interface AnnotatedVideoPlayerProps {
|
|
videoRef: RefObject<MediaPlayerInstance | null>;
|
|
logs: DetectionResultLog[];
|
|
visibleDetections: VideoAnnotationFrameDetection[];
|
|
videoWidth: number;
|
|
videoHeight: number;
|
|
videoUrl: string | null;
|
|
isLoading: boolean;
|
|
isError: boolean;
|
|
onRetry: () => void;
|
|
onTimeUpdate: () => void;
|
|
onVideoFrame: (mediaTime: number) => void;
|
|
onSeeked: () => void;
|
|
}
|
|
|
|
function VideoFrameSync({
|
|
onVideoFrame,
|
|
}: Pick<AnnotatedVideoPlayerProps, 'onVideoFrame'>) {
|
|
const provider = useMediaProvider();
|
|
|
|
useEffect(() => {
|
|
if (!isVideoProvider(provider)) return;
|
|
|
|
const video = provider.video;
|
|
let callbackId: number;
|
|
|
|
const update = (_now: number, metadata: VideoFrameCallbackMetadata) => {
|
|
onVideoFrame(metadata.mediaTime);
|
|
callbackId = video.requestVideoFrameCallback(update);
|
|
};
|
|
|
|
callbackId = video.requestVideoFrameCallback(update);
|
|
|
|
return () => video.cancelVideoFrameCallback(callbackId);
|
|
}, [onVideoFrame, provider]);
|
|
|
|
return null;
|
|
}
|
|
|
|
export default function AnnotatedVideoPlayer({
|
|
videoRef,
|
|
logs,
|
|
visibleDetections,
|
|
videoWidth,
|
|
videoHeight,
|
|
videoUrl,
|
|
isLoading,
|
|
isError,
|
|
onRetry,
|
|
onTimeUpdate,
|
|
onVideoFrame,
|
|
onSeeked,
|
|
}: AnnotatedVideoPlayerProps) {
|
|
const thumbnails = useDetectionThumbnails(logs);
|
|
|
|
return (
|
|
<div className="relative overflow-hidden rounded-lg border bg-black">
|
|
{videoUrl ? (
|
|
<MediaPlayer
|
|
ref={videoRef}
|
|
src={{ src: videoUrl, type: 'video/mp4' }}
|
|
viewType="video"
|
|
streamType="on-demand"
|
|
crossOrigin
|
|
playsInline
|
|
onTimeUpdate={onTimeUpdate}
|
|
onSeeked={onSeeked}
|
|
className="detection-video-player aspect-video w-full"
|
|
>
|
|
<MediaProvider>
|
|
<Poster className="vds-poster" />
|
|
</MediaProvider>
|
|
<VideoFrameSync onVideoFrame={onVideoFrame} />
|
|
<BoundingBoxOverlay
|
|
detections={visibleDetections}
|
|
videoWidth={videoWidth}
|
|
videoHeight={videoHeight}
|
|
/>
|
|
<DefaultVideoLayout
|
|
thumbnails={thumbnails}
|
|
icons={defaultLayoutIcons}
|
|
/>
|
|
</MediaPlayer>
|
|
) : (
|
|
<div className="flex aspect-video w-full flex-col items-center justify-center gap-3 text-center text-muted-foreground">
|
|
{isError ? (
|
|
<>
|
|
<AlertTriangle className="h-7 w-7 text-destructive" />
|
|
<p className="text-sm font-medium text-destructive">
|
|
Failed to load the annotated video.
|
|
</p>
|
|
<button
|
|
type="button"
|
|
onClick={onRetry}
|
|
className="text-xs font-semibold uppercase tracking-wider text-primary hover:underline"
|
|
>
|
|
Retry
|
|
</button>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Loader2 className="h-7 w-7 animate-spin text-primary" />
|
|
<p className="text-xs">
|
|
{isLoading
|
|
? 'Loading annotated video...'
|
|
: 'Preparing video...'}
|
|
</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|