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
This commit is contained in:
Nicolas Mowen 2024-02-23 13:38:11 -07:00 committed by GitHub
parent 64eaf60b24
commit 74a8fee69c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 110 additions and 55 deletions

View File

@ -2297,32 +2297,13 @@ def preview_hour(year_month, day, hour, camera_name, tz_name):
return preview_ts(camera_name, start_ts, end_ts) return preview_ts(camera_name, start_ts, end_ts)
@bp.route("/preview/<camera_name>/<frame_time>/thumbnail.jpg") @bp.route("/preview/<file_name>/thumbnail.jpg")
def preview_thumbnail(camera_name, frame_time): def preview_thumbnail(file_name: str):
"""Get a thumbnail from the cached preview jpgs.""" """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") 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)): with open(os.path.join(preview_dir, safe_file_name_current), "rb") as image_file:
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:
jpg_bytes = image_file.read() jpg_bytes = image_file.read()
response = make_response(jpg_bytes) response = make_response(jpg_bytes)
@ -2331,6 +2312,31 @@ def preview_thumbnail(camera_name, frame_time):
return response return response
@bp.route("/preview/<camera_name>/start/<int:start_ts>/end/<int:end_ts>/frames")
@bp.route("/preview/<camera_name>/start/<float:start_ts>/end/<float:end_ts>/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/<id>") @bp.route("/vod/event/<id>")
def vod_event(id): def vod_event(id):
try: try:
@ -2409,6 +2415,7 @@ def review():
review = ( review = (
ReviewSegment.select() ReviewSegment.select()
.where(reduce(operator.and_, clauses)) .where(reduce(operator.and_, clauses))
.order_by(ReviewSegment.severity.asc())
.order_by(ReviewSegment.start_time.desc()) .order_by(ReviewSegment.start_time.desc())
.limit(limit) .limit(limit)
.dicts() .dicts()

View File

@ -1,14 +1,8 @@
import VideoPlayer from "./VideoPlayer"; import VideoPlayer from "./VideoPlayer";
import React, { import { useCallback, useEffect, useMemo, useRef, useState } from "react";
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useApiHost } from "@/api"; import { useApiHost } from "@/api";
import Player from "video.js/dist/types/player"; 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 { ReviewSegment } from "@/types/review";
import { Slider } from "../ui/slider"; import { Slider } from "../ui/slider";
import { getIconForLabel, getIconForSubLabel } from "@/utils/iconUtil"; import { getIconForLabel, getIconForSubLabel } from "@/utils/iconUtil";
@ -43,16 +37,12 @@ export default function PreviewThumbnailPlayer({
}: PreviewPlayerProps) { }: PreviewPlayerProps) {
const apiHost = useApiHost(); const apiHost = useApiHost();
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
const playerRef = useRef<Player | null>(null);
const [hoverTimeout, setHoverTimeout] = useState<NodeJS.Timeout | null>(); const [hoverTimeout, setHoverTimeout] = useState<NodeJS.Timeout | null>();
const [playback, setPlayback] = useState(false); const [playback, setPlayback] = useState(false);
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(0);
const playingBack = useMemo( const playingBack = useMemo(() => playback, [playback, autoPlayback]);
() => relevantPreview && playback,
[playback, autoPlayback, relevantPreview]
);
useEffect(() => { useEffect(() => {
if (!autoPlayback) { if (!autoPlayback) {
@ -76,10 +66,6 @@ export default function PreviewThumbnailPlayer({
const onPlayback = useCallback( const onPlayback = useCallback(
(isHovered: Boolean) => { (isHovered: Boolean) => {
if (!relevantPreview) {
return;
}
if (isHovered) { if (isHovered) {
setHoverTimeout( setHoverTimeout(
setTimeout(() => { setTimeout(() => {
@ -94,16 +80,9 @@ export default function PreviewThumbnailPlayer({
setPlayback(false); setPlayback(false);
setProgress(0); setProgress(0);
if (playerRef.current) {
playerRef.current.pause();
playerRef.current.currentTime(
review.start_time - relevantPreview.start
);
}
} }
}, },
[hoverTimeout, relevantPreview, review, playerRef] [hoverTimeout, review]
); );
return ( return (
@ -115,10 +94,8 @@ export default function PreviewThumbnailPlayer({
> >
{playingBack ? ( {playingBack ? (
<PreviewContent <PreviewContent
playerRef={playerRef}
review={review} review={review}
relevantPreview={relevantPreview} relevantPreview={relevantPreview}
playback={playingBack}
setProgress={setProgress} setProgress={setProgress}
setReviewed={setReviewed} setReviewed={setReviewed}
/> />
@ -173,21 +150,18 @@ export default function PreviewThumbnailPlayer({
} }
type PreviewContentProps = { type PreviewContentProps = {
playerRef: React.MutableRefObject<Player | null>;
review: ReviewSegment; review: ReviewSegment;
relevantPreview: Preview | undefined; relevantPreview: Preview | undefined;
playback: boolean;
setProgress?: (progress: number) => void; setProgress?: (progress: number) => void;
setReviewed?: () => void; setReviewed?: () => void;
}; };
function PreviewContent({ function PreviewContent({
playerRef,
review, review,
relevantPreview, relevantPreview,
playback,
setProgress, setProgress,
setReviewed, setReviewed,
}: PreviewContentProps) { }: PreviewContentProps) {
const playerRef = useRef<Player | null>(null);
const playerStartTime = useMemo(() => { const playerStartTime = useMemo(() => {
if (!relevantPreview) { if (!relevantPreview) {
return 0; return 0;
@ -219,7 +193,7 @@ function PreviewContent({
// preview // preview
if (relevantPreview && playback) { if (relevantPreview) {
return ( return (
<VideoPlayer <VideoPlayer
options={{ options={{
@ -293,5 +267,79 @@ function PreviewContent({
}} }}
/> />
); );
} else if (isCurrentHour(review.start_time)) {
return (
<InProgressPreview
review={review}
setProgress={setProgress}
setReviewed={setReviewed}
/>
);
} }
} }
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<string[]>(
`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 (
<img
className="h-full w-full"
loading="lazy"
src={`${apiHost}${review.thumb_path.replace("/media/frigate/", "")}`}
/>
);
}
return (
<div className="w-full h-full flex items-center bg-black">
<img
className="w-full"
src={`${apiHost}api/preview/${previewFrames[key]}/thumbnail.jpg`}
onLoad={handleLoad}
/>
</div>
);
}