mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-09 12:45:25 +03:00
Start working on animation
This commit is contained in:
parent
2d22800a3d
commit
d75b21311e
159
frigate/http.py
159
frigate/http.py
@ -593,6 +593,101 @@ def event_thumbnail(id, max_cache_age=2592000):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/events/<id>/preview.mp4")
|
||||||
|
def event_preview(id: str, max_cache_age=2592000):
|
||||||
|
try:
|
||||||
|
event = Event.get(Event.id == id)
|
||||||
|
if event.end_time is not None:
|
||||||
|
event_complete = True
|
||||||
|
except DoesNotExist:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": "Event not found"}), 404
|
||||||
|
)
|
||||||
|
|
||||||
|
if datetime.fromtimestamp(event.start_time) < datetime.now().replace(
|
||||||
|
minute=0, second=0
|
||||||
|
):
|
||||||
|
# has preview mp4
|
||||||
|
start_ts = event.start_time
|
||||||
|
end_ts = event.start_time + 10
|
||||||
|
preview = (
|
||||||
|
Previews.select(
|
||||||
|
Previews.camera,
|
||||||
|
Previews.path,
|
||||||
|
Previews.duration,
|
||||||
|
Previews.start_time,
|
||||||
|
Previews.end_time,
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
Previews.start_time.between(start_ts, end_ts)
|
||||||
|
| Previews.end_time.between(start_ts, end_ts)
|
||||||
|
| ((start_ts > Previews.start_time) & (end_ts < Previews.end_time))
|
||||||
|
)
|
||||||
|
.where(Previews.camera == event.camera)
|
||||||
|
.limit(1)
|
||||||
|
.dicts()
|
||||||
|
.get()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not preview:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": "Preview not found"}), 404
|
||||||
|
)
|
||||||
|
|
||||||
|
diff = event.start_time - preview.start_time
|
||||||
|
minutes = int(diff / 60)
|
||||||
|
seconds = int(diff % 60)
|
||||||
|
ffmpeg_cmd = [
|
||||||
|
"ffmpeg",
|
||||||
|
"-hide_banner",
|
||||||
|
"-loglevel",
|
||||||
|
"warning",
|
||||||
|
"-ss",
|
||||||
|
f"00:{minutes}:{seconds}",
|
||||||
|
"-t",
|
||||||
|
"20",
|
||||||
|
"-i",
|
||||||
|
preview.path,
|
||||||
|
"r",
|
||||||
|
"8",
|
||||||
|
"-vf",
|
||||||
|
"setpts=0.12*PTS",
|
||||||
|
"-loop",
|
||||||
|
"0",
|
||||||
|
"-c:v",
|
||||||
|
"gif",
|
||||||
|
"-f",
|
||||||
|
"image2pipe",
|
||||||
|
"-",
|
||||||
|
]
|
||||||
|
|
||||||
|
process = sp.run(
|
||||||
|
ffmpeg_cmd,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
gif_bytes = process.stdout
|
||||||
|
else:
|
||||||
|
# need to generate from existing images
|
||||||
|
preview_dir = os.path.join(CACHE_DIR, "preview_frames")
|
||||||
|
file_start = f"preview_{event.camera}"
|
||||||
|
file_check = f"{file_start}-{event.start_time}.jpg"
|
||||||
|
selected_preview = None
|
||||||
|
|
||||||
|
for file in os.listdir(preview_dir):
|
||||||
|
if file.startswith(file_start):
|
||||||
|
if file < file_check:
|
||||||
|
selected_preview = file
|
||||||
|
break
|
||||||
|
|
||||||
|
response = make_response(gif_bytes)
|
||||||
|
response.headers["Content-Type"] = "image/gif"
|
||||||
|
if event_complete:
|
||||||
|
response.headers["Cache-Control"] = f"private, max-age={max_cache_age}"
|
||||||
|
else:
|
||||||
|
response.headers["Cache-Control"] = "no-store"
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/timeline")
|
@bp.route("/timeline")
|
||||||
def timeline():
|
def timeline():
|
||||||
camera = request.args.get("camera", "all")
|
camera = request.args.get("camera", "all")
|
||||||
@ -888,9 +983,9 @@ def event_snapshot(id):
|
|||||||
else:
|
else:
|
||||||
response.headers["Cache-Control"] = "no-store"
|
response.headers["Cache-Control"] = "no-store"
|
||||||
if download:
|
if download:
|
||||||
response.headers[
|
response.headers["Content-Disposition"] = (
|
||||||
"Content-Disposition"
|
f"attachment; filename=snapshot-{id}.jpg"
|
||||||
] = f"attachment; filename=snapshot-{id}.jpg"
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@ -1077,9 +1172,9 @@ def event_clip(id):
|
|||||||
if download:
|
if download:
|
||||||
response.headers["Content-Disposition"] = "attachment; filename=%s" % file_name
|
response.headers["Content-Disposition"] = "attachment; filename=%s" % file_name
|
||||||
response.headers["Content-Length"] = os.path.getsize(clip_path)
|
response.headers["Content-Length"] = os.path.getsize(clip_path)
|
||||||
response.headers[
|
response.headers["X-Accel-Redirect"] = (
|
||||||
"X-Accel-Redirect"
|
f"/clips/{file_name}" # nginx: https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ignore_headers
|
||||||
] = f"/clips/{file_name}" # nginx: https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ignore_headers
|
)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@ -1784,9 +1879,9 @@ def get_recordings_storage_usage():
|
|||||||
|
|
||||||
total_mb = recording_stats["total"]
|
total_mb = recording_stats["total"]
|
||||||
|
|
||||||
camera_usages: dict[
|
camera_usages: dict[str, dict] = (
|
||||||
str, dict
|
current_app.storage_maintainer.calculate_camera_usages()
|
||||||
] = current_app.storage_maintainer.calculate_camera_usages()
|
)
|
||||||
|
|
||||||
for camera_name in camera_usages.keys():
|
for camera_name in camera_usages.keys():
|
||||||
if camera_usages.get(camera_name, {}).get("usage"):
|
if camera_usages.get(camera_name, {}).get("usage"):
|
||||||
@ -1974,9 +2069,9 @@ def recording_clip(camera_name, start_ts, end_ts):
|
|||||||
if download:
|
if download:
|
||||||
response.headers["Content-Disposition"] = "attachment; filename=%s" % file_name
|
response.headers["Content-Disposition"] = "attachment; filename=%s" % file_name
|
||||||
response.headers["Content-Length"] = os.path.getsize(path)
|
response.headers["Content-Length"] = os.path.getsize(path)
|
||||||
response.headers[
|
response.headers["X-Accel-Redirect"] = (
|
||||||
"X-Accel-Redirect"
|
f"/cache/{file_name}" # nginx: https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ignore_headers
|
||||||
] = f"/cache/{file_name}" # nginx: https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ignore_headers
|
)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@ -2265,9 +2360,11 @@ def export_recording(camera_name: str, start_time, end_time):
|
|||||||
camera_name,
|
camera_name,
|
||||||
int(start_time),
|
int(start_time),
|
||||||
int(end_time),
|
int(end_time),
|
||||||
PlaybackFactorEnum[playback_factor]
|
(
|
||||||
if playback_factor in PlaybackFactorEnum.__members__.values()
|
PlaybackFactorEnum[playback_factor]
|
||||||
else PlaybackFactorEnum.realtime,
|
if playback_factor in PlaybackFactorEnum.__members__.values()
|
||||||
|
else PlaybackFactorEnum.realtime
|
||||||
|
),
|
||||||
)
|
)
|
||||||
exporter.start()
|
exporter.start()
|
||||||
return make_response(
|
return make_response(
|
||||||
@ -2423,12 +2520,16 @@ def ffprobe():
|
|||||||
output.append(
|
output.append(
|
||||||
{
|
{
|
||||||
"return_code": ffprobe.returncode,
|
"return_code": ffprobe.returncode,
|
||||||
"stderr": ffprobe.stderr.decode("unicode_escape").strip()
|
"stderr": (
|
||||||
if ffprobe.returncode != 0
|
ffprobe.stderr.decode("unicode_escape").strip()
|
||||||
else "",
|
if ffprobe.returncode != 0
|
||||||
"stdout": json.loads(ffprobe.stdout.decode("unicode_escape").strip())
|
else ""
|
||||||
if ffprobe.returncode == 0
|
),
|
||||||
else "",
|
"stdout": (
|
||||||
|
json.loads(ffprobe.stdout.decode("unicode_escape").strip())
|
||||||
|
if ffprobe.returncode == 0
|
||||||
|
else ""
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -2441,12 +2542,16 @@ def vainfo():
|
|||||||
return jsonify(
|
return jsonify(
|
||||||
{
|
{
|
||||||
"return_code": vainfo.returncode,
|
"return_code": vainfo.returncode,
|
||||||
"stderr": vainfo.stderr.decode("unicode_escape").strip()
|
"stderr": (
|
||||||
if vainfo.returncode != 0
|
vainfo.stderr.decode("unicode_escape").strip()
|
||||||
else "",
|
if vainfo.returncode != 0
|
||||||
"stdout": vainfo.stdout.decode("unicode_escape").strip()
|
else ""
|
||||||
if vainfo.returncode == 0
|
),
|
||||||
else "",
|
"stdout": (
|
||||||
|
vainfo.stdout.decode("unicode_escape").strip()
|
||||||
|
if vainfo.returncode == 0
|
||||||
|
else ""
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
44
web/src/components/image/AnimatedEventThumbnail.tsx
Normal file
44
web/src/components/image/AnimatedEventThumbnail.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { baseUrl } from "@/api/baseUrl";
|
||||||
|
import { Event as FrigateEvent } from "@/types/event";
|
||||||
|
import { LuStar } from "react-icons/lu";
|
||||||
|
import TimeAgo from "../dynamic/TimeAgo";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||||
|
|
||||||
|
type EventThumbnailProps = {
|
||||||
|
event: FrigateEvent;
|
||||||
|
onFavorite?: (e: Event, event: FrigateEvent) => void;
|
||||||
|
};
|
||||||
|
export function EventThumbnail({ event, onFavorite }: EventThumbnailProps) {
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div
|
||||||
|
className="relative rounded bg-cover aspect-square h-24 bg-no-repeat bg-center mr-4"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${baseUrl}api/events/${event.id}/thumbnail.jpg)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LuStar
|
||||||
|
className="absolute h-6 w-6 text-yellow-300 top-1 right-1 cursor-pointer"
|
||||||
|
onClick={(e: Event) => (onFavorite ? onFavorite(e, event) : null)}
|
||||||
|
fill={event.retain_indefinitely ? "currentColor" : "none"}
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-0 w-full h-6 bg-gradient-to-t from-slate-900/50 to-transparent">
|
||||||
|
<div className="absolute left-1 bottom-0 text-xs text-white w-full">
|
||||||
|
<TimeAgo time={event.start_time * 1000} dense />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{`${event.label} ${
|
||||||
|
event.sub_label ? `(${event.sub_label})` : ""
|
||||||
|
} detected with score of ${(event.data.score * 100).toFixed(0)}% ${
|
||||||
|
event.data.sub_label_score
|
||||||
|
? `(${event.data.sub_label_score * 100}%)`
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user