diff --git a/frigate/http.py b/frigate/http.py index f2731ec2f..7f29f3f94 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -2297,32 +2297,13 @@ def preview_hour(year_month, day, hour, camera_name, tz_name): return preview_ts(camera_name, start_ts, end_ts) -@bp.route("/preview///thumbnail.jpg") -def preview_thumbnail(camera_name, frame_time): +@bp.route("/preview//thumbnail.jpg") +def preview_thumbnail(file_name: str): """Get a thumbnail from the cached preview jpgs.""" + safe_file_name_current = secure_filename(file_name) preview_dir = os.path.join(CACHE_DIR, "preview_frames") - file_start = f"preview_{camera_name}" - file_check = f"{file_start}-{frame_time}.jpg" - selected_preview = None - for file in sorted(os.listdir(preview_dir)): - if file.startswith(file_start): - if file > file_check: - selected_preview = file - break - - if selected_preview is None: - return make_response( - jsonify( - { - "success": False, - "message": "Could not find valid preview jpg.", - } - ), - 404, - ) - - with open(os.path.join(preview_dir, selected_preview), "rb") as image_file: + with open(os.path.join(preview_dir, safe_file_name_current), "rb") as image_file: jpg_bytes = image_file.read() response = make_response(jpg_bytes) @@ -2331,6 +2312,31 @@ def preview_thumbnail(camera_name, frame_time): return response +@bp.route("/preview//start//end//frames") +@bp.route("/preview//start//end//frames") +def get_preview_frames_from_cache(camera_name: str, start_ts, end_ts): + """Get list of cached preview frames""" + preview_dir = os.path.join(CACHE_DIR, "preview_frames") + file_start = f"preview_{camera_name}" + start_file = f"{file_start}-{start_ts}.jpg" + end_file = f"{file_start}-{end_ts}.jpg" + selected_previews = [] + + for file in sorted(os.listdir(preview_dir)): + if not file.startswith(file_start): + continue + + if file < start_file: + continue + + if file > end_file: + break + + selected_previews.append(file) + + return jsonify(selected_previews) + + @bp.route("/vod/event/") def vod_event(id): try: @@ -2409,6 +2415,7 @@ def review(): review = ( ReviewSegment.select() .where(reduce(operator.and_, clauses)) + .order_by(ReviewSegment.severity.asc()) .order_by(ReviewSegment.start_time.desc()) .limit(limit) .dicts() diff --git a/web/src/components/player/PreviewThumbnailPlayer.tsx b/web/src/components/player/PreviewThumbnailPlayer.tsx index 8cd510a1a..bdde45697 100644 --- a/web/src/components/player/PreviewThumbnailPlayer.tsx +++ b/web/src/components/player/PreviewThumbnailPlayer.tsx @@ -1,14 +1,8 @@ import VideoPlayer from "./VideoPlayer"; -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; +import { 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 { formatUnixTimestampToDateTime, isCurrentHour } from "@/utils/dateUtil"; import { ReviewSegment } from "@/types/review"; import { Slider } from "../ui/slider"; import { getIconForLabel, getIconForSubLabel } from "@/utils/iconUtil"; @@ -43,16 +37,12 @@ export default function PreviewThumbnailPlayer({ }: 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] - ); + const playingBack = useMemo(() => playback, [playback, autoPlayback]); useEffect(() => { if (!autoPlayback) { @@ -76,10 +66,6 @@ export default function PreviewThumbnailPlayer({ const onPlayback = useCallback( (isHovered: Boolean) => { - if (!relevantPreview) { - return; - } - if (isHovered) { setHoverTimeout( setTimeout(() => { @@ -94,16 +80,9 @@ export default function PreviewThumbnailPlayer({ setPlayback(false); setProgress(0); - - if (playerRef.current) { - playerRef.current.pause(); - playerRef.current.currentTime( - review.start_time - relevantPreview.start - ); - } } }, - [hoverTimeout, relevantPreview, review, playerRef] + [hoverTimeout, review] ); return ( @@ -115,10 +94,8 @@ export default function PreviewThumbnailPlayer({ > {playingBack ? ( @@ -173,21 +150,18 @@ export default function PreviewThumbnailPlayer({ } 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) { + const playerRef = useRef(null); const playerStartTime = useMemo(() => { if (!relevantPreview) { return 0; @@ -219,7 +193,7 @@ function PreviewContent({ // preview - if (relevantPreview && playback) { + if (relevantPreview) { return ( ); + } 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 == previewFrames.length / 2) { + setReviewed(); + } + + setKey(key + 1); + }, MIN_LOAD_TIMEOUT_MS); + }, [key, previewFrames]); + + if (!previewFrames || previewFrames.length == 0) { + return ( + + ); + } + + return ( +
+ +
+ ); +}