import VideoPlayer from "./VideoPlayer"; import React, { useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { useApiHost } from "@/api"; import Player from "video.js/dist/types/player"; import { formatUnixTimestampToDateTime } 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"; type PreviewPlayerProps = { review: ReviewSegment; relevantPreview?: Preview; autoPlayback?: boolean; setReviewed?: () => void; }; type Preview = { camera: string; src: string; type: string; start: number; end: number; }; export default function PreviewThumbnailPlayer({ review, relevantPreview, autoPlayback = false, setReviewed, }: PreviewPlayerProps) { const apiHost = useApiHost(); const { data: config } = useSWR("config"); const playerRef = useRef(null); const [hoverTimeout, setHoverTimeout] = useState(); const [playback, setPlayback] = useState(false); const [progress, setProgress] = useState(0); const playingBack = useMemo( () => relevantPreview && playback, [playback, autoPlayback, relevantPreview] ); 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 (!relevantPreview) { return; } if (isHovered) { setHoverTimeout( setTimeout(() => { setPlayback(true); setHoverTimeout(null); }, 500) ); } else { if (hoverTimeout) { clearTimeout(hoverTimeout); } setPlayback(false); setProgress(0); if (playerRef.current) { playerRef.current.pause(); playerRef.current.currentTime( review.start_time - relevantPreview.start ); } } }, [hoverTimeout, relevantPreview, review, playerRef] ); return (
onPlayback(true)} onMouseLeave={isMobile ? undefined : () => onPlayback(false)} > {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 = { playerRef: React.MutableRefObject; review: ReviewSegment; relevantPreview: Preview | undefined; playback: boolean; setProgress?: (progress: number) => void; setReviewed?: () => void; }; function PreviewContent({ playerRef, review, relevantPreview, playback, setProgress, setReviewed, }: PreviewContentProps) { // 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; } const intervalId: NodeJS.Timeout = setInterval(() => { if (playerRef.current) { playerRef.current.currentTime(playerRef.current.currentTime()!! + 1); } }, 125); return () => clearInterval(intervalId); }, [manualPlayback, playerRef]); // preview if (relevantPreview && playback) { return ( { playerRef.current = player; if (!relevantPreview) { return; } // start with a bit of padding const playerStartTime = Math.max( 0, review.start_time - relevantPreview.start - 8 ); if (isSafari) { player.pause(); setManualPlayback(true); } else { player.playbackRate(8); } player.currentTime(playerStartTime); 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 && playerPercent > 50 ) { setReviewed(); } if (playerPercent > 100) { playerRef.current?.pause(); setManualPlayback(false); setProgress(100.0); } else { setProgress(playerPercent); } }); }} onDispose={() => { playerRef.current = null; }} /> ); } }