import VideoPlayer from "./VideoPlayer"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useApiHost } from "@/api"; import Player from "video.js/dist/types/player"; import { formatUnixTimestampToDateTime, isCurrentHour } from "@/utils/dateUtil"; import { ReviewSegment } from "@/types/review"; import { Slider } from "../ui/slider"; import { getIconForLabel, getIconForSubLabel } from "@/utils/iconUtil"; import TimeAgo from "../dynamic/TimeAgo"; import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; import { isMobile, isSafari } from "react-device-detect"; import Chip from "../Chip"; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger, } from "../ui/context-menu"; import { LuCheckSquare, LuFileUp, LuTrash } from "react-icons/lu"; import axios from "axios"; type PreviewPlayerProps = { review: ReviewSegment; relevantPreview?: Preview; autoPlayback?: boolean; setReviewed?: () => void; onClick?: () => void; }; type Preview = { camera: string; src: string; type: string; start: number; end: number; }; export default function PreviewThumbnailPlayer({ review, relevantPreview, autoPlayback = false, setReviewed, onClick, }: PreviewPlayerProps) { const apiHost = useApiHost(); const { data: config } = useSWR("config"); const [hoverTimeout, setHoverTimeout] = useState(); const [playback, setPlayback] = useState(false); const [progress, setProgress] = useState(0); const playingBack = useMemo(() => playback, [playback, autoPlayback]); useEffect(() => { if (!autoPlayback) { setPlayback(false); if (hoverTimeout) { clearTimeout(hoverTimeout); } return; } const timeout = setTimeout(() => { setPlayback(true); setHoverTimeout(null); }, 500); return () => { clearTimeout(timeout); }; }, [autoPlayback]); const onPlayback = useCallback( (isHovered: Boolean) => { if (isHovered) { setHoverTimeout( setTimeout(() => { setPlayback(true); setHoverTimeout(null); }, 500) ); } else { if (hoverTimeout) { clearTimeout(hoverTimeout); } setPlayback(false); setProgress(0); } }, [hoverTimeout, review] ); return ( onPlayback(true)} onMouseLeave={isMobile ? undefined : () => onPlayback(false)} onClick={onClick} > {playingBack && (
)} {(review.severity == "alert" || review.severity == "detection") && ( {review.data.objects.map((object) => { return getIconForLabel(object, "w-3 h-3 text-white"); })} {review.data.audio.map((audio) => { return getIconForLabel(audio, "w-3 h-3 text-white"); })} {review.data.sub_labels?.map((sub) => { return getIconForSubLabel(sub, "w-3 h-3 text-white"); })} )} {!playingBack && (
{config && formatUnixTimestampToDateTime(review.start_time, { strftime_fmt: config.ui.time_format == "24hour" ? "%b %-d, %H:%M" : "%b %-d, %I:%M %p", })}
)}
{playingBack && ( )} {!playingBack && review.has_been_reviewed && (
)} ); } type PreviewContentProps = { review: ReviewSegment; relevantPreview: Preview | undefined; setProgress?: (progress: number) => void; setReviewed?: () => void; }; function PreviewContent({ review, relevantPreview, setProgress, setReviewed, }: PreviewContentProps) { const playerRef = useRef(null); const playerStartTime = useMemo(() => { if (!relevantPreview) { return 0; } // start with a bit of padding return Math.max(0, review.start_time - relevantPreview.start - 8); }, []); // manual playback // safari is incapable of playing at a speed > 2x // so manual seeking is required on iOS const [manualPlayback, setManualPlayback] = useState(false); useEffect(() => { if (!manualPlayback || !playerRef.current) { return; } let counter = 0; const intervalId: NodeJS.Timeout = setInterval(() => { if (playerRef.current) { playerRef.current.currentTime(playerStartTime + counter); counter += 1; } }, 125); return () => clearInterval(intervalId); }, [manualPlayback, playerRef]); // preview if (relevantPreview) { return ( { playerRef.current = player; if (!relevantPreview) { return; } if (isSafari) { player.pause(); setManualPlayback(true); } else { player.currentTime(playerStartTime); player.playbackRate(8); } let lastPercent = 0; player.on("timeupdate", () => { if (!setProgress) { return; } const playerProgress = (player.currentTime() || 0) - playerStartTime; // end with a bit of padding const playerDuration = review.end_time - review.start_time + 8; const playerPercent = (playerProgress / playerDuration) * 100; if ( setReviewed && !review.has_been_reviewed && lastPercent < 50 && playerPercent > 50 ) { setReviewed(); } lastPercent = playerPercent; if (playerPercent > 100) { playerRef.current?.pause(); setManualPlayback(false); setProgress(100.0); } else { setProgress(playerPercent); } }); }} onDispose={() => { playerRef.current = null; }} /> ); } else if (isCurrentHour(review.start_time)) { return ( ); } } const MIN_LOAD_TIMEOUT_MS = 200; type InProgressPreviewProps = { review: ReviewSegment; setProgress?: (progress: number) => void; setReviewed?: () => void; }; function InProgressPreview({ review, setProgress, setReviewed, }: InProgressPreviewProps) { const apiHost = useApiHost(); const { data: previewFrames } = useSWR( `preview/${review.camera}/start/${Math.floor(review.start_time) - 4}/end/${ Math.ceil(review.end_time) + 4 }/frames` ); const [key, setKey] = useState(0); const handleLoad = useCallback(() => { if (!previewFrames) { return; } if (key == previewFrames.length - 1) { if (setProgress) { setProgress(100); } return; } setTimeout(() => { if (setProgress) { setProgress((key / (previewFrames.length - 1)) * 100); } if (setReviewed && key == Math.floor(previewFrames.length / 2)) { setReviewed(); } setKey(key + 1); }, MIN_LOAD_TIMEOUT_MS); }, [key, previewFrames]); if (!previewFrames || previewFrames.length == 0) { return ( ); } return (
); } type PreviewContextItemsProps = { review: ReviewSegment; setReviewed?: () => void; }; function PreviewContextItems({ review, setReviewed, }: PreviewContextItemsProps) { const exportReview = useCallback(() => { console.log( "trying to export to " + `export/${review.camera}/start/${review.start_time}/end/${review.end_time}` ); axios.post( `export/${review.camera}/start/${review.start_time}/end/${review.end_time}`, { playback: "realtime" } ); }, [review]); return ( {!review.has_been_reviewed && ( (setReviewed ? setReviewed() : null)}>
Mark As Reviewed
)} exportReview()}>
Export
Delete
); }