Use existing components for preview filmstrip instead of gif

This commit is contained in:
Nicolas Mowen 2024-04-18 21:02:27 -06:00
parent 392ff1319d
commit 32dc70b8a5
2 changed files with 87 additions and 48 deletions

View File

@ -1,14 +1,17 @@
import { baseUrl } from "@/api/baseUrl";
import TimeAgo from "../dynamic/TimeAgo"; import TimeAgo from "../dynamic/TimeAgo";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo } from "react";
import useSWR from "swr"; import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { ReviewSegment } from "@/types/review"; import { ReviewSegment } from "@/types/review";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Skeleton } from "../ui/skeleton";
import { RecordingStartingPoint } from "@/types/record"; import { RecordingStartingPoint } from "@/types/record";
import axios from "axios"; import axios from "axios";
import { Preview } from "@/types/preview";
import {
InProgressPreview,
VideoPreview,
} from "../player/PreviewThumbnailPlayer";
type AnimatedEventCardProps = { type AnimatedEventCardProps = {
event: ReviewSegment; event: ReviewSegment;
@ -16,6 +19,12 @@ type AnimatedEventCardProps = {
export function AnimatedEventCard({ event }: AnimatedEventCardProps) { export function AnimatedEventCard({ event }: AnimatedEventCardProps) {
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
// preview
const { data: previews } = useSWR<Preview[]>(
`/preview/${event.camera}/start/${Math.round(event.start_time)}/end/${Math.round(event.end_time || event.start_time + 20)}`,
);
// interaction // interaction
const navigate = useNavigate(); const navigate = useNavigate();
@ -35,16 +44,6 @@ export function AnimatedEventCard({ event }: AnimatedEventCardProps) {
// image behavior // image behavior
const [loaded, setLoaded] = useState(false);
const [error, setError] = useState(0);
const imageUrl = useMemo(() => {
if (error > 0) {
return `${baseUrl}api/review/${event.id}/preview.gif?key=${error}`;
}
return `${baseUrl}api/review/${event.id}/preview.gif`;
}, [error, event]);
const aspectRatio = useMemo(() => { const aspectRatio = useMemo(() => {
if (!config) { if (!config) {
return 1; return 1;
@ -63,18 +62,36 @@ export function AnimatedEventCard({ event }: AnimatedEventCardProps) {
aspectRatio: aspectRatio, aspectRatio: aspectRatio,
}} }}
> >
<img <div
className="size-full rounded object-cover object-center cursor-pointer" className="size-full rounded cursor-pointer"
src={imageUrl}
onClick={onOpenReview} onClick={onOpenReview}
onLoad={() => setLoaded(true)} >
onError={() => { {previews ? (
if (error < 2) { <VideoPreview
setError(error + 1); relevantPreview={previews[previews.length - 1]}
} startTime={event.start_time}
}} endTime={event.end_time}
/> loop
{!loaded && <Skeleton className="absolute inset-0" />} showProgress={false}
setReviewed={() => {}}
setIgnoreClick={() => {}}
isPlayingBack={() => {}}
/>
) : (
<InProgressPreview
review={event}
timeRange={{
after: event.start_time,
before: event.end_time ?? event.start_time + 20,
}}
loop
showProgress={false}
setReviewed={() => {}}
setIgnoreClick={() => {}}
isPlayingBack={() => {}}
/>
)}
</div>
<div className="absolute bottom-0 inset-x-0 h-6 bg-gradient-to-t from-slate-900/50 to-transparent rounded"> <div className="absolute bottom-0 inset-x-0 h-6 bg-gradient-to-t from-slate-900/50 to-transparent rounded">
<div className="w-full absolute left-1 bottom-0 text-xs text-white"> <div className="w-full absolute left-1 bottom-0 text-xs text-white">
<TimeAgo time={event.start_time * 1000} dense /> <TimeAgo time={event.start_time * 1000} dense />

View File

@ -342,15 +342,19 @@ type VideoPreviewProps = {
relevantPreview: Preview; relevantPreview: Preview;
startTime: number; startTime: number;
endTime?: number; endTime?: number;
showProgress?: boolean;
loop?: boolean;
setReviewed: () => void; setReviewed: () => void;
setIgnoreClick: (ignore: boolean) => void; setIgnoreClick: (ignore: boolean) => void;
isPlayingBack: (ended: boolean) => void; isPlayingBack: (ended: boolean) => void;
onTimeUpdate?: (time: number | undefined) => void; onTimeUpdate?: (time: number | undefined) => void;
}; };
function VideoPreview({ export function VideoPreview({
relevantPreview, relevantPreview,
startTime, startTime,
endTime, endTime,
showProgress = true,
loop = false,
setReviewed, setReviewed,
setIgnoreClick, setIgnoreClick,
isPlayingBack, isPlayingBack,
@ -425,6 +429,11 @@ function VideoPreview({
if (playerPercent > 100) { if (playerPercent > 100) {
setReviewed(); setReviewed();
if (loop && playerRef.current) {
playerRef.current.currentTime = playerStartTime;
return;
}
if (isMobile) { if (isMobile) {
isPlayingBack(false); isPlayingBack(false);
@ -553,17 +562,19 @@ function VideoPreview({
> >
<source src={relevantPreview.src} type={relevantPreview.type} /> <source src={relevantPreview.src} type={relevantPreview.type} />
</video> </video>
<NoThumbSlider {showProgress && (
ref={sliderRef} <NoThumbSlider
className="absolute inset-x-0 bottom-0 z-30" ref={sliderRef}
value={[progress]} className="absolute inset-x-0 bottom-0 z-30"
onValueChange={onManualSeek} value={[progress]}
onValueCommit={onStopManualSeek} onValueChange={onManualSeek}
min={0} onValueCommit={onStopManualSeek}
step={1} min={0}
max={100} step={1}
onMouseMove={isMobile ? undefined : onProgressHover} max={100}
/> onMouseMove={isMobile ? undefined : onProgressHover}
/>
)}
</div> </div>
); );
} }
@ -572,14 +583,18 @@ const MIN_LOAD_TIMEOUT_MS = 200;
type InProgressPreviewProps = { type InProgressPreviewProps = {
review: ReviewSegment; review: ReviewSegment;
timeRange: TimeRange; timeRange: TimeRange;
showProgress?: boolean;
loop?: boolean;
setReviewed: (reviewId: string) => void; setReviewed: (reviewId: string) => void;
setIgnoreClick: (ignore: boolean) => void; setIgnoreClick: (ignore: boolean) => void;
isPlayingBack: (ended: boolean) => void; isPlayingBack: (ended: boolean) => void;
onTimeUpdate?: (time: number | undefined) => void; onTimeUpdate?: (time: number | undefined) => void;
}; };
function InProgressPreview({ export function InProgressPreview({
review, review,
timeRange, timeRange,
showProgress = true,
loop = false,
setReviewed, setReviewed,
setIgnoreClick, setIgnoreClick,
isPlayingBack, isPlayingBack,
@ -615,6 +630,11 @@ function InProgressPreview({
setReviewed(review.id); setReviewed(review.id);
} }
if (loop) {
setKey(0);
return;
}
if (isMobile) { if (isMobile) {
isPlayingBack(false); isPlayingBack(false);
@ -717,17 +737,19 @@ function InProgressPreview({
src={`${apiHost}api/preview/${previewFrames[key]}/thumbnail.webp`} src={`${apiHost}api/preview/${previewFrames[key]}/thumbnail.webp`}
onLoad={handleLoad} onLoad={handleLoad}
/> />
<NoThumbSlider {showProgress && (
ref={sliderRef} <NoThumbSlider
className="absolute inset-x-0 bottom-0 z-30" ref={sliderRef}
value={[key]} className="absolute inset-x-0 bottom-0 z-30"
onValueChange={onManualSeek} value={[key]}
onValueCommit={onStopManualSeek} onValueChange={onManualSeek}
min={0} onValueCommit={onStopManualSeek}
step={1} min={0}
max={previewFrames.length - 1} step={1}
onMouseMove={isMobile ? undefined : onProgressHover} max={previewFrames.length - 1}
/> onMouseMove={isMobile ? undefined : onProgressHover}
/>
)}
</div> </div>
); );
} }