mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-10 21:25:24 +03:00
Implement downloading
This commit is contained in:
parent
2e36f6d257
commit
5657c9b5fe
@ -1166,26 +1166,32 @@ def preview_gif(camera_name: str, start_ts, end_ts, max_cache_age=2592000):
|
|||||||
|
|
||||||
@MediaBp.route("/<camera_name>/start/<int:start_ts>/end/<int:end_ts>/preview.mp4")
|
@MediaBp.route("/<camera_name>/start/<int:start_ts>/end/<int:end_ts>/preview.mp4")
|
||||||
@MediaBp.route("/<camera_name>/start/<float:start_ts>/end/<float:end_ts>/preview.mp4")
|
@MediaBp.route("/<camera_name>/start/<float:start_ts>/end/<float:end_ts>/preview.mp4")
|
||||||
def preview_mp4(camera_name: str, start_ts, end_ts, max_cache_age=2592000):
|
def preview_mp4(camera_name: str, start_ts, end_ts):
|
||||||
|
file_name = secure_filename(f"clip_{camera_name}_{start_ts}-{end_ts}.mp4")
|
||||||
|
path = os.path.join(CACHE_DIR, file_name)
|
||||||
|
|
||||||
if datetime.fromtimestamp(start_ts) < datetime.now().replace(minute=0, second=0):
|
if datetime.fromtimestamp(start_ts) < datetime.now().replace(minute=0, second=0):
|
||||||
# has preview mp4
|
# has preview mp4
|
||||||
preview: Previews = (
|
try:
|
||||||
Previews.select(
|
preview: Previews = (
|
||||||
Previews.camera,
|
Previews.select(
|
||||||
Previews.path,
|
Previews.camera,
|
||||||
Previews.duration,
|
Previews.path,
|
||||||
Previews.start_time,
|
Previews.duration,
|
||||||
Previews.end_time,
|
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 == camera_name)
|
||||||
|
.limit(1)
|
||||||
|
.get()
|
||||||
)
|
)
|
||||||
.where(
|
except DoesNotExist:
|
||||||
Previews.start_time.between(start_ts, end_ts)
|
preview = None
|
||||||
| Previews.end_time.between(start_ts, end_ts)
|
|
||||||
| ((start_ts > Previews.start_time) & (end_ts < Previews.end_time))
|
|
||||||
)
|
|
||||||
.where(Previews.camera == camera_name)
|
|
||||||
.limit(1)
|
|
||||||
.get()
|
|
||||||
)
|
|
||||||
|
|
||||||
if not preview:
|
if not preview:
|
||||||
return make_response(
|
return make_response(
|
||||||
@ -1200,6 +1206,7 @@ def preview_mp4(camera_name: str, start_ts, end_ts, max_cache_age=2592000):
|
|||||||
"-hide_banner",
|
"-hide_banner",
|
||||||
"-loglevel",
|
"-loglevel",
|
||||||
"warning",
|
"warning",
|
||||||
|
"-y",
|
||||||
"-ss",
|
"-ss",
|
||||||
f"00:{minutes}:{seconds}",
|
f"00:{minutes}:{seconds}",
|
||||||
"-t",
|
"-t",
|
||||||
@ -1210,13 +1217,11 @@ def preview_mp4(camera_name: str, start_ts, end_ts, max_cache_age=2592000):
|
|||||||
"8",
|
"8",
|
||||||
"-vf",
|
"-vf",
|
||||||
"setpts=0.12*PTS",
|
"setpts=0.12*PTS",
|
||||||
"-loop",
|
|
||||||
"0",
|
|
||||||
"-c:v",
|
"-c:v",
|
||||||
"copy",
|
"libx264",
|
||||||
"-f",
|
"-movflags",
|
||||||
"mp4",
|
"+faststart",
|
||||||
"-",
|
path,
|
||||||
]
|
]
|
||||||
|
|
||||||
process = sp.run(
|
process = sp.run(
|
||||||
@ -1231,7 +1236,6 @@ def preview_mp4(camera_name: str, start_ts, end_ts, max_cache_age=2592000):
|
|||||||
500,
|
500,
|
||||||
)
|
)
|
||||||
|
|
||||||
gif_bytes = process.stdout
|
|
||||||
else:
|
else:
|
||||||
# need to generate from existing images
|
# need to generate from existing images
|
||||||
preview_dir = os.path.join(CACHE_DIR, "preview_frames")
|
preview_dir = os.path.join(CACHE_DIR, "preview_frames")
|
||||||
@ -1275,13 +1279,11 @@ def preview_mp4(camera_name: str, start_ts, end_ts, max_cache_age=2592000):
|
|||||||
"0",
|
"0",
|
||||||
"-i",
|
"-i",
|
||||||
"/dev/stdin",
|
"/dev/stdin",
|
||||||
"-loop",
|
|
||||||
"0",
|
|
||||||
"-c:v",
|
"-c:v",
|
||||||
"libx264",
|
"libx264",
|
||||||
"-f",
|
"-movflags",
|
||||||
"gif",
|
"+faststart",
|
||||||
"-",
|
path,
|
||||||
]
|
]
|
||||||
|
|
||||||
process = sp.run(
|
process = sp.run(
|
||||||
@ -1297,11 +1299,14 @@ def preview_mp4(camera_name: str, start_ts, end_ts, max_cache_age=2592000):
|
|||||||
500,
|
500,
|
||||||
)
|
)
|
||||||
|
|
||||||
gif_bytes = process.stdout
|
response = make_response()
|
||||||
|
response.headers["Content-Description"] = "File Transfer"
|
||||||
response = make_response(gif_bytes)
|
response.headers["Cache-Control"] = "no-cache"
|
||||||
response.headers["Content-Type"] = "image/gif"
|
response.headers["Content-Type"] = "video/mp4"
|
||||||
response.headers["Cache-Control"] = f"private, max-age={max_cache_age}"
|
response.headers["Content-Length"] = os.path.getsize(path)
|
||||||
|
response.headers["X-Accel-Redirect"] = (
|
||||||
|
f"/cache/{file_name}" # nginx: https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ignore_headers
|
||||||
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
import ActivityIndicator from "../indicators/activity-indicator";
|
import ActivityIndicator from "../indicators/activity-indicator";
|
||||||
import { LuPencil, LuTrash } from "react-icons/lu";
|
import { LuTrash } from "react-icons/lu";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { isDesktop } from "react-device-detect";
|
import { isDesktop } from "react-device-detect";
|
||||||
import { FaPlay } from "react-icons/fa";
|
import { FaDownload, FaPlay } from "react-icons/fa";
|
||||||
import Chip from "../indicators/Chip";
|
import Chip from "../indicators/Chip";
|
||||||
import { Skeleton } from "../ui/skeleton";
|
import { Skeleton } from "../ui/skeleton";
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogTitle } from "../ui/dialog";
|
import { Dialog, DialogContent, DialogFooter, DialogTitle } from "../ui/dialog";
|
||||||
import { Input } from "../ui/input";
|
import { Input } from "../ui/input";
|
||||||
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
||||||
import { Export } from "@/types/export";
|
import { Export } from "@/types/export";
|
||||||
|
import { MdEditSquare } from "react-icons/md";
|
||||||
|
import { baseUrl } from "@/api/baseUrl";
|
||||||
|
|
||||||
type ExportProps = {
|
type ExportProps = {
|
||||||
className: string;
|
className: string;
|
||||||
@ -114,13 +116,22 @@ export default function ExportCard({
|
|||||||
<>
|
<>
|
||||||
<div className="absolute inset-0 z-10 bg-black bg-opacity-60 rounded-2xl" />
|
<div className="absolute inset-0 z-10 bg-black bg-opacity-60 rounded-2xl" />
|
||||||
<div className="absolute top-1 right-1 flex items-center gap-2">
|
<div className="absolute top-1 right-1 flex items-center gap-2">
|
||||||
|
<a
|
||||||
|
className="z-20"
|
||||||
|
download
|
||||||
|
href={`${baseUrl}${exportedRecording.video_path.replace("/media/frigate/", "")}`}
|
||||||
|
>
|
||||||
|
<Chip className="bg-gradient-to-br from-gray-400 to-gray-500 bg-gray-500 rounded-md cursor-pointer">
|
||||||
|
<FaDownload className="size-4 text-white" />
|
||||||
|
</Chip>
|
||||||
|
</a>
|
||||||
<Chip
|
<Chip
|
||||||
className="bg-gradient-to-br from-gray-400 to-gray-500 bg-gray-500 rounded-md cursor-pointer"
|
className="bg-gradient-to-br from-gray-400 to-gray-500 bg-gray-500 rounded-md cursor-pointer"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setEditName({ original: exportedRecording.name, update: "" })
|
setEditName({ original: exportedRecording.name, update: "" })
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<LuPencil className="size-4 text-white" />
|
<MdEditSquare className="size-4 text-white" />
|
||||||
</Chip>
|
</Chip>
|
||||||
<Chip
|
<Chip
|
||||||
className="bg-gradient-to-br from-gray-400 to-gray-500 bg-gray-500 rounded-md cursor-pointer"
|
className="bg-gradient-to-br from-gray-400 to-gray-500 bg-gray-500 rounded-md cursor-pointer"
|
||||||
@ -161,7 +172,7 @@ export default function ExportCard({
|
|||||||
)}
|
)}
|
||||||
<div className="absolute bottom-0 inset-x-0 rounded-b-l z-10 h-[20%] bg-gradient-to-t from-black/60 to-transparent pointer-events-none rounded-2xl">
|
<div className="absolute bottom-0 inset-x-0 rounded-b-l z-10 h-[20%] bg-gradient-to-t from-black/60 to-transparent pointer-events-none rounded-2xl">
|
||||||
<div className="flex h-full justify-between items-end mx-3 pb-1 text-white text-sm capitalize">
|
<div className="flex h-full justify-between items-end mx-3 pb-1 text-white text-sm capitalize">
|
||||||
{exportedRecording.name}
|
{exportedRecording.name.replaceAll("_", " ")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -34,7 +34,10 @@ function Exports() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return exports.filter((exp) =>
|
return exports.filter((exp) =>
|
||||||
exp.name.toLowerCase().includes(search.toLowerCase()),
|
exp.name
|
||||||
|
.toLowerCase()
|
||||||
|
.replaceAll("_", " ")
|
||||||
|
.includes(search.toLowerCase()),
|
||||||
);
|
);
|
||||||
}, [exports, search]);
|
}, [exports, search]);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user