Skip to content

Commit 74a8fee

Browse files
authored
Use manual jpg when preview is not finished yet (#9997)
* Use manual jpg when preview is not finished yet * Ensure safe filename and improve sorting * Ensure name is correct * Formatting
1 parent 64eaf60 commit 74a8fee

File tree

2 files changed

+110
-55
lines changed

2 files changed

+110
-55
lines changed

frigate/http.py

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2297,32 +2297,13 @@ def preview_hour(year_month, day, hour, camera_name, tz_name):
22972297
return preview_ts(camera_name, start_ts, end_ts)
22982298

22992299

2300-
@bp.route("/preview/<camera_name>/<frame_time>/thumbnail.jpg")
2301-
def preview_thumbnail(camera_name, frame_time):
2300+
@bp.route("/preview/<file_name>/thumbnail.jpg")
2301+
def preview_thumbnail(file_name: str):
23022302
"""Get a thumbnail from the cached preview jpgs."""
2303+
safe_file_name_current = secure_filename(file_name)
23032304
preview_dir = os.path.join(CACHE_DIR, "preview_frames")
2304-
file_start = f"preview_{camera_name}"
2305-
file_check = f"{file_start}-{frame_time}.jpg"
2306-
selected_preview = None
2307-
2308-
for file in sorted(os.listdir(preview_dir)):
2309-
if file.startswith(file_start):
2310-
if file > file_check:
2311-
selected_preview = file
2312-
break
23132305

2314-
if selected_preview is None:
2315-
return make_response(
2316-
jsonify(
2317-
{
2318-
"success": False,
2319-
"message": "Could not find valid preview jpg.",
2320-
}
2321-
),
2322-
404,
2323-
)
2324-
2325-
with open(os.path.join(preview_dir, selected_preview), "rb") as image_file:
2306+
with open(os.path.join(preview_dir, safe_file_name_current), "rb") as image_file:
23262307
jpg_bytes = image_file.read()
23272308

23282309
response = make_response(jpg_bytes)
@@ -2331,6 +2312,31 @@ def preview_thumbnail(camera_name, frame_time):
23312312
return response
23322313

23332314

2315+
@bp.route("/preview/<camera_name>/start/<int:start_ts>/end/<int:end_ts>/frames")
2316+
@bp.route("/preview/<camera_name>/start/<float:start_ts>/end/<float:end_ts>/frames")
2317+
def get_preview_frames_from_cache(camera_name: str, start_ts, end_ts):
2318+
"""Get list of cached preview frames"""
2319+
preview_dir = os.path.join(CACHE_DIR, "preview_frames")
2320+
file_start = f"preview_{camera_name}"
2321+
start_file = f"{file_start}-{start_ts}.jpg"
2322+
end_file = f"{file_start}-{end_ts}.jpg"
2323+
selected_previews = []
2324+
2325+
for file in sorted(os.listdir(preview_dir)):
2326+
if not file.startswith(file_start):
2327+
continue
2328+
2329+
if file < start_file:
2330+
continue
2331+
2332+
if file > end_file:
2333+
break
2334+
2335+
selected_previews.append(file)
2336+
2337+
return jsonify(selected_previews)
2338+
2339+
23342340
@bp.route("/vod/event/<id>")
23352341
def vod_event(id):
23362342
try:
@@ -2409,6 +2415,7 @@ def review():
24092415
review = (
24102416
ReviewSegment.select()
24112417
.where(reduce(operator.and_, clauses))
2418+
.order_by(ReviewSegment.severity.asc())
24122419
.order_by(ReviewSegment.start_time.desc())
24132420
.limit(limit)
24142421
.dicts()

web/src/components/player/PreviewThumbnailPlayer.tsx

Lines changed: 80 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
11
import VideoPlayer from "./VideoPlayer";
2-
import React, {
3-
useCallback,
4-
useEffect,
5-
useMemo,
6-
useRef,
7-
useState,
8-
} from "react";
2+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
93
import { useApiHost } from "@/api";
104
import Player from "video.js/dist/types/player";
11-
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
5+
import { formatUnixTimestampToDateTime, isCurrentHour } from "@/utils/dateUtil";
126
import { ReviewSegment } from "@/types/review";
137
import { Slider } from "../ui/slider";
148
import { getIconForLabel, getIconForSubLabel } from "@/utils/iconUtil";
@@ -43,16 +37,12 @@ export default function PreviewThumbnailPlayer({
4337
}: PreviewPlayerProps) {
4438
const apiHost = useApiHost();
4539
const { data: config } = useSWR<FrigateConfig>("config");
46-
const playerRef = useRef<Player | null>(null);
4740

4841
const [hoverTimeout, setHoverTimeout] = useState<NodeJS.Timeout | null>();
4942
const [playback, setPlayback] = useState(false);
5043
const [progress, setProgress] = useState(0);
5144

52-
const playingBack = useMemo(
53-
() => relevantPreview && playback,
54-
[playback, autoPlayback, relevantPreview]
55-
);
45+
const playingBack = useMemo(() => playback, [playback, autoPlayback]);
5646

5747
useEffect(() => {
5848
if (!autoPlayback) {
@@ -76,10 +66,6 @@ export default function PreviewThumbnailPlayer({
7666

7767
const onPlayback = useCallback(
7868
(isHovered: Boolean) => {
79-
if (!relevantPreview) {
80-
return;
81-
}
82-
8369
if (isHovered) {
8470
setHoverTimeout(
8571
setTimeout(() => {
@@ -94,16 +80,9 @@ export default function PreviewThumbnailPlayer({
9480

9581
setPlayback(false);
9682
setProgress(0);
97-
98-
if (playerRef.current) {
99-
playerRef.current.pause();
100-
playerRef.current.currentTime(
101-
review.start_time - relevantPreview.start
102-
);
103-
}
10483
}
10584
},
106-
[hoverTimeout, relevantPreview, review, playerRef]
85+
[hoverTimeout, review]
10786
);
10887

10988
return (
@@ -115,10 +94,8 @@ export default function PreviewThumbnailPlayer({
11594
>
11695
{playingBack ? (
11796
<PreviewContent
118-
playerRef={playerRef}
11997
review={review}
12098
relevantPreview={relevantPreview}
121-
playback={playingBack}
12299
setProgress={setProgress}
123100
setReviewed={setReviewed}
124101
/>
@@ -173,21 +150,18 @@ export default function PreviewThumbnailPlayer({
173150
}
174151

175152
type PreviewContentProps = {
176-
playerRef: React.MutableRefObject<Player | null>;
177153
review: ReviewSegment;
178154
relevantPreview: Preview | undefined;
179-
playback: boolean;
180155
setProgress?: (progress: number) => void;
181156
setReviewed?: () => void;
182157
};
183158
function PreviewContent({
184-
playerRef,
185159
review,
186160
relevantPreview,
187-
playback,
188161
setProgress,
189162
setReviewed,
190163
}: PreviewContentProps) {
164+
const playerRef = useRef<Player | null>(null);
191165
const playerStartTime = useMemo(() => {
192166
if (!relevantPreview) {
193167
return 0;
@@ -219,7 +193,7 @@ function PreviewContent({
219193

220194
// preview
221195

222-
if (relevantPreview && playback) {
196+
if (relevantPreview) {
223197
return (
224198
<VideoPlayer
225199
options={{
@@ -293,5 +267,79 @@ function PreviewContent({
293267
}}
294268
/>
295269
);
270+
} else if (isCurrentHour(review.start_time)) {
271+
return (
272+
<InProgressPreview
273+
review={review}
274+
setProgress={setProgress}
275+
setReviewed={setReviewed}
276+
/>
277+
);
278+
}
279+
}
280+
281+
const MIN_LOAD_TIMEOUT_MS = 200;
282+
type InProgressPreviewProps = {
283+
review: ReviewSegment;
284+
setProgress?: (progress: number) => void;
285+
setReviewed?: () => void;
286+
};
287+
function InProgressPreview({
288+
review,
289+
setProgress,
290+
setReviewed,
291+
}: InProgressPreviewProps) {
292+
const apiHost = useApiHost();
293+
const { data: previewFrames } = useSWR<string[]>(
294+
`preview/${review.camera}/start/${Math.floor(
295+
review.start_time
296+
) - 4}/end/${Math.ceil(review.end_time) + 4}/frames`
297+
);
298+
const [key, setKey] = useState(0);
299+
300+
const handleLoad = useCallback(() => {
301+
if (!previewFrames) {
302+
return;
303+
}
304+
305+
if (key == previewFrames.length - 1) {
306+
if (setProgress) {
307+
setProgress(100);
308+
}
309+
310+
return;
311+
}
312+
313+
setTimeout(() => {
314+
if (setProgress) {
315+
setProgress((key / (previewFrames.length - 1)) * 100);
316+
}
317+
318+
if (setReviewed && key == previewFrames.length / 2) {
319+
setReviewed();
320+
}
321+
322+
setKey(key + 1);
323+
}, MIN_LOAD_TIMEOUT_MS);
324+
}, [key, previewFrames]);
325+
326+
if (!previewFrames || previewFrames.length == 0) {
327+
return (
328+
<img
329+
className="h-full w-full"
330+
loading="lazy"
331+
src={`${apiHost}${review.thumb_path.replace("/media/frigate/", "")}`}
332+
/>
333+
);
296334
}
335+
336+
return (
337+
<div className="w-full h-full flex items-center bg-black">
338+
<img
339+
className="w-full"
340+
src={`${apiHost}api/preview/${previewFrames[key]}/thumbnail.jpg`}
341+
onLoad={handleLoad}
342+
/>
343+
</div>
344+
);
297345
}

0 commit comments

Comments
 (0)