mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-14 15:15:22 +03:00
snapshot endpoint height and format, yaml types, bugfixes
This commit is contained in:
parent
281ddde15c
commit
86c770038d
@ -179,14 +179,20 @@ def latest_frame(camera_name):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@MediaBp.route("/<camera_name>/recordings/<frame_time>/snapshot.png")
|
@MediaBp.route("/<camera_name>/recordings/<frame_time>/snapshot.<format>")
|
||||||
def get_snapshot_from_recording(camera_name: str, frame_time: str):
|
def get_snapshot_from_recording(camera_name: str, frame_time: str, format: str):
|
||||||
if camera_name not in current_app.frigate_config.cameras:
|
if camera_name not in current_app.frigate_config.cameras:
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify({"success": False, "message": "Camera not found"}),
|
jsonify({"success": False, "message": "Camera not found"}),
|
||||||
404,
|
404,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if format not in ["png", "jpg"]:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": "Invalid format"}),
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
|
||||||
frame_time = float(frame_time)
|
frame_time = float(frame_time)
|
||||||
recording_query = (
|
recording_query = (
|
||||||
Recordings.select(
|
Recordings.select(
|
||||||
@ -207,7 +213,41 @@ def get_snapshot_from_recording(camera_name: str, frame_time: str):
|
|||||||
try:
|
try:
|
||||||
recording: Recordings = recording_query.get()
|
recording: Recordings = recording_query.get()
|
||||||
time_in_segment = frame_time - recording.start_time
|
time_in_segment = frame_time - recording.start_time
|
||||||
image_data = get_image_from_recording(recording.path, time_in_segment)
|
|
||||||
|
height = request.args.get("height", type=int)
|
||||||
|
codec = "png" if format == "png" else "mjpeg"
|
||||||
|
|
||||||
|
ffmpeg_cmd = [
|
||||||
|
"ffmpeg",
|
||||||
|
"-hide_banner",
|
||||||
|
"-loglevel",
|
||||||
|
"warning",
|
||||||
|
"-ss",
|
||||||
|
f"00:00:{time_in_segment}",
|
||||||
|
"-i",
|
||||||
|
recording.path,
|
||||||
|
"-frames:v",
|
||||||
|
"1",
|
||||||
|
"-c:v",
|
||||||
|
codec,
|
||||||
|
"-f",
|
||||||
|
"image2pipe",
|
||||||
|
"-",
|
||||||
|
]
|
||||||
|
|
||||||
|
if height:
|
||||||
|
ffmpeg_cmd.insert(-3, "-vf")
|
||||||
|
ffmpeg_cmd.insert(-3, f"scale=-1:{height}")
|
||||||
|
|
||||||
|
process = sp.run(
|
||||||
|
ffmpeg_cmd,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if process.returncode == 0:
|
||||||
|
image_data = process.stdout
|
||||||
|
else:
|
||||||
|
image_data = None
|
||||||
|
|
||||||
if not image_data:
|
if not image_data:
|
||||||
return make_response(
|
return make_response(
|
||||||
@ -221,7 +261,7 @@ def get_snapshot_from_recording(camera_name: str, frame_time: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
response = make_response(image_data)
|
response = make_response(image_data)
|
||||||
response.headers["Content-Type"] = "image/png"
|
response.headers["Content-Type"] = f"image/{format}"
|
||||||
return response
|
return response
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
return make_response(
|
return make_response(
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
"""Utilities for builtin types manipulation."""
|
"""Utilities for builtin types manipulation."""
|
||||||
|
|
||||||
|
import ast
|
||||||
import copy
|
import copy
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
@ -210,10 +211,16 @@ def update_yaml_from_url(file_path, url):
|
|||||||
if len(new_value_list) > 1:
|
if len(new_value_list) > 1:
|
||||||
update_yaml_file(file_path, key_path, new_value_list)
|
update_yaml_file(file_path, key_path, new_value_list)
|
||||||
else:
|
else:
|
||||||
value = str(new_value_list[0])
|
value = new_value_list[0]
|
||||||
|
if "," in value:
|
||||||
if value.isnumeric():
|
# Skip conversion if we're a mask or zone string
|
||||||
value = int(value)
|
update_yaml_file(file_path, key_path, value)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
value = ast.literal_eval(value)
|
||||||
|
except (ValueError, SyntaxError):
|
||||||
|
pass
|
||||||
|
update_yaml_file(file_path, key_path, value)
|
||||||
|
|
||||||
update_yaml_file(file_path, key_path, value)
|
update_yaml_file(file_path, key_path, value)
|
||||||
|
|
||||||
|
|||||||
@ -159,15 +159,21 @@ export default function ObjectLifecycle({
|
|||||||
|
|
||||||
// image
|
// image
|
||||||
|
|
||||||
const [src, setSrc] = useState("");
|
const [src, setSrc] = useState(
|
||||||
|
`${apiHost}api/${event.camera}/recordings/${event.start_time + annotationOffset / 1000}/snapshot.jpg?height=500`,
|
||||||
|
);
|
||||||
const [hasError, setHasError] = useState(false);
|
const [hasError, setHasError] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newSrc = `${apiHost}api/${event.camera}/recordings/${timeIndex + annotationOffset / 1000}/snapshot.png`;
|
if (timeIndex) {
|
||||||
setSrc(newSrc);
|
const newSrc = `${apiHost}api/${event.camera}/recordings/${timeIndex + annotationOffset / 1000}/snapshot.jpg?height=500`;
|
||||||
|
setSrc(newSrc);
|
||||||
|
}
|
||||||
setImgLoaded(false);
|
setImgLoaded(false);
|
||||||
setHasError(false);
|
setHasError(false);
|
||||||
}, [timeIndex, annotationOffset, apiHost, event.camera]);
|
// we know that these deps are correct
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [timeIndex, annotationOffset]);
|
||||||
|
|
||||||
// carousels
|
// carousels
|
||||||
|
|
||||||
@ -310,10 +316,8 @@ export default function ObjectLifecycle({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row items-center justify-between">
|
<div className="mt-3 flex flex-row items-center justify-between">
|
||||||
<Heading as="h4" className="mt-3">
|
<Heading as="h4">Object Lifecycle</Heading>
|
||||||
Object Lifecycle
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@ -464,7 +468,7 @@ export default function ObjectLifecycle({
|
|||||||
opts={{
|
opts={{
|
||||||
align: "center",
|
align: "center",
|
||||||
}}
|
}}
|
||||||
className="w-full max-w-[75%] md:max-w-[85%]"
|
className="w-full max-w-[72%] md:max-w-[85%]"
|
||||||
setApi={setThumbnailApi}
|
setApi={setThumbnailApi}
|
||||||
>
|
>
|
||||||
<CarouselContent className="flex flex-row justify-center">
|
<CarouselContent className="flex flex-row justify-center">
|
||||||
|
|||||||
@ -99,7 +99,7 @@ export default function ReviewDetailDialog({
|
|||||||
? pane == "overview"
|
? pane == "overview"
|
||||||
? "sm:max-w-xl"
|
? "sm:max-w-xl"
|
||||||
: "pt-2 sm:max-w-4xl"
|
: "pt-2 sm:max-w-4xl"
|
||||||
: "max-h-[75dvh] overflow-hidden p-2 pb-4",
|
: "max-h-[80dvh] overflow-hidden p-2 pb-4",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{pane == "overview" && (
|
{pane == "overview" && (
|
||||||
@ -178,7 +178,7 @@ export default function ReviewDetailDialog({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{pane == "details" && selectedEvent && (
|
{pane == "details" && selectedEvent && (
|
||||||
<div className="scrollbar-container overflow-x-none mt-0 flex size-full flex-col gap-2 overflow-y-auto">
|
<div className="scrollbar-container overflow-x-none mt-0 flex size-full flex-col gap-2 overflow-y-auto overflow-x-hidden">
|
||||||
<ObjectLifecycle
|
<ObjectLifecycle
|
||||||
review={review}
|
review={review}
|
||||||
event={selectedEvent}
|
event={selectedEvent}
|
||||||
@ -218,7 +218,10 @@ function EventItem({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="relative"
|
className={cn(
|
||||||
|
"relative",
|
||||||
|
!event.has_snapshot && "flex flex-row items-center justify-center",
|
||||||
|
)}
|
||||||
onMouseEnter={isDesktop ? () => setHovered(true) : undefined}
|
onMouseEnter={isDesktop ? () => setHovered(true) : undefined}
|
||||||
onMouseLeave={isDesktop ? () => setHovered(false) : undefined}
|
onMouseLeave={isDesktop ? () => setHovered(false) : undefined}
|
||||||
key={event.id}
|
key={event.id}
|
||||||
@ -252,11 +255,7 @@ function EventItem({
|
|||||||
{hovered && (
|
{hovered && (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn("absolute right-1 top-1 flex items-center gap-2")}
|
||||||
"absolute",
|
|
||||||
event.has_snapshot ? "right-1" : "left-1",
|
|
||||||
"top-1 flex items-center gap-2",
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
|
|||||||
@ -114,7 +114,10 @@ export default function ZoneEditPane({
|
|||||||
{
|
{
|
||||||
message: "Zone name must not contain a period.",
|
message: "Zone name must not contain a period.",
|
||||||
},
|
},
|
||||||
),
|
)
|
||||||
|
.refine((value: string) => /^[a-zA-Z0-9_-]+$/.test(value), {
|
||||||
|
message: "Zone name has an illegal character.",
|
||||||
|
}),
|
||||||
inertia: z.coerce
|
inertia: z.coerce
|
||||||
.number()
|
.number()
|
||||||
.min(1, {
|
.min(1, {
|
||||||
|
|||||||
@ -112,7 +112,7 @@ export default function MotionTunerView({
|
|||||||
|
|
||||||
axios
|
axios
|
||||||
.put(
|
.put(
|
||||||
`config/set?cameras.${selectedCamera}.motion.threshold=${motionSettings.threshold}&cameras.${selectedCamera}.motion.contour_area=${motionSettings.contour_area}&cameras.${selectedCamera}.motion.improve_contrast=${motionSettings.improve_contrast}`,
|
`config/set?cameras.${selectedCamera}.motion.threshold=${motionSettings.threshold}&cameras.${selectedCamera}.motion.contour_area=${motionSettings.contour_area}&cameras.${selectedCamera}.motion.improve_contrast=${motionSettings.improve_contrast ? "True" : "False"}`,
|
||||||
{ requires_restart: 0 },
|
{ requires_restart: 0 },
|
||||||
)
|
)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user