mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
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:
parent
64eaf60b24
commit
74a8fee69c
@ -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()
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user