Show animated gif for event thumb

This commit is contained in:
Nicolas Mowen 2024-02-10 12:33:37 -07:00
parent a83f07d8c8
commit ae8932e27e
4 changed files with 31 additions and 25 deletions

View File

@ -597,8 +597,6 @@ def event_thumbnail(id, max_cache_age=2592000):
def event_preview(id: str, max_cache_age=2592000): def event_preview(id: str, max_cache_age=2592000):
try: try:
event: Event = Event.get(Event.id == id) event: Event = Event.get(Event.id == id)
if event.end_time is not None:
event_complete = True
except DoesNotExist: except DoesNotExist:
return make_response( return make_response(
jsonify({"success": False, "message": "Event not found"}), 404 jsonify({"success": False, "message": "Event not found"}), 404
@ -660,7 +658,6 @@ def event_preview(id: str, max_cache_age=2592000):
"gif", "gif",
"-", "-",
] ]
logger.info(f"running previous gif creation {' '.join(ffmpeg_cmd)}")
process = sp.run( process = sp.run(
ffmpeg_cmd, ffmpeg_cmd,
@ -685,9 +682,14 @@ def event_preview(id: str, max_cache_age=2592000):
if file > end_file: if file > end_file:
break break
selected_previews.append(f"file '{file}'") selected_previews.append(f"file '/tmp/cache/preview_frames/{file}'")
selected_previews.append("duration 0.12") selected_previews.append("duration 0.12")
if not selected_previews:
return make_response(
jsonify({"success": False, "message": "Preview not found"}), 404
)
last_file = selected_previews[-2] last_file = selected_previews[-2]
selected_previews.append(last_file) selected_previews.append(last_file)
@ -713,24 +715,24 @@ def event_preview(id: str, max_cache_age=2592000):
"gif", "gif",
"-", "-",
] ]
logger.info(
f"running current gif creation {' '.join(ffmpeg_cmd)} with files {' '.join(selected_previews)}"
)
process = sp.run( process = sp.run(
ffmpeg_cmd, ffmpeg_cmd,
input="\n".join(selected_previews), input=str.encode("\n".join(selected_previews)),
encoding="ascii",
capture_output=True, capture_output=True,
) )
if process.returncode != 0:
return make_response(
jsonify({"success": False, "message": "Unable to create preview gif"}),
500,
)
gif_bytes = process.stdout gif_bytes = process.stdout
response = make_response(gif_bytes) response = make_response(gif_bytes)
response.headers["Content-Type"] = "image/gif" response.headers["Content-Type"] = "image/gif"
if event_complete: response.headers["Cache-Control"] = f"private, max-age={max_cache_age}"
response.headers["Cache-Control"] = f"private, max-age={max_cache_age}"
else:
response.headers["Cache-Control"] = "no-store"
return response return response

View File

@ -4,18 +4,21 @@ import { LuStar } from "react-icons/lu";
import TimeAgo from "../dynamic/TimeAgo"; import TimeAgo from "../dynamic/TimeAgo";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
type EventThumbnailProps = { type AnimatedEventThumbnailProps = {
event: FrigateEvent; event: FrigateEvent;
onFavorite?: (e: Event, event: FrigateEvent) => void; onFavorite?: (e: Event, event: FrigateEvent) => void;
}; };
export function EventThumbnail({ event, onFavorite }: EventThumbnailProps) { export function AnimatedEventThumbnail({
event,
onFavorite,
}: AnimatedEventThumbnailProps) {
return ( return (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div <div
className="relative rounded bg-cover aspect-square h-24 bg-no-repeat bg-center mr-4" className="relative rounded bg-cover aspect-video h-24 bg-no-repeat bg-center mr-4"
style={{ style={{
backgroundImage: `url(${baseUrl}api/events/${event.id}/thumbnail.jpg)`, backgroundImage: `url(${baseUrl}api/events/${event.id}/preview.gif)`,
}} }}
> >
<LuStar <LuStar
@ -23,7 +26,7 @@ export function EventThumbnail({ event, onFavorite }: EventThumbnailProps) {
onClick={(e: Event) => (onFavorite ? onFavorite(e, event) : null)} onClick={(e: Event) => (onFavorite ? onFavorite(e, event) : null)}
fill={event.retain_indefinitely ? "currentColor" : "none"} 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 bottom-0 w-full h-6 bg-gradient-to-t from-slate-900/50 to-transparent rounded">
<div className="absolute left-1 bottom-0 text-xs text-white w-full"> <div className="absolute left-1 bottom-0 text-xs text-white w-full">
<TimeAgo time={event.start_time * 1000} dense /> <TimeAgo time={event.start_time * 1000} dense />
</div> </div>

View File

@ -40,22 +40,23 @@ export default function LivePlayer({
const { activeMotion, activeAudio, activeTracking } = const { activeMotion, activeAudio, activeTracking } =
useCameraActivity(cameraConfig); useCameraActivity(cameraConfig);
const cameraActive = useMemo(() => activeMotion || activeTracking, [activeMotion, activeTracking])
const liveMode = useCameraLiveMode(cameraConfig, preferredLiveMode); const liveMode = useCameraLiveMode(cameraConfig, preferredLiveMode);
const [liveReady, setLiveReady] = useState(false); const [liveReady, setLiveReady] = useState(false);
useEffect(() => { useEffect(() => {
if (!liveReady) { if (!liveReady) {
if (activeMotion && liveMode == "jsmpeg") { if (cameraActive && liveMode == "jsmpeg") {
setLiveReady(true); setLiveReady(true);
} }
return; return;
} }
if (!activeMotion && !activeTracking) { if (!cameraActive) {
setLiveReady(false); setLiveReady(false);
} }
}, [activeMotion, activeTracking, liveReady]); }, [cameraActive, liveReady]);
const { payload: recording } = useRecordingsState(cameraConfig.name); const { payload: recording } = useRecordingsState(cameraConfig.name);
@ -167,7 +168,7 @@ export default function LivePlayer({
: "outline-0" : "outline-0"
} transition-all duration-500 ${className}`} } transition-all duration-500 ${className}`}
> >
{(showStillWithoutActivity == false || activeMotion || activeTracking) && {(showStillWithoutActivity == false || cameraActive) &&
player} player}
<div <div
@ -179,7 +180,7 @@ export default function LivePlayer({
className="w-full h-full" className="w-full h-full"
camera={cameraConfig.name} camera={cameraConfig.name}
showFps={false} showFps={false}
reloadInterval={30000} reloadInterval={(cameraActive && !liveReady) ? 200 : 30000}
/> />
</div> </div>

View File

@ -1,4 +1,4 @@
import { EventThumbnail } from "@/components/image/EventThumbnail"; import { AnimatedEventThumbnail } from "@/components/image/AnimatedEventThumbnail";
import LivePlayer from "@/components/player/LivePlayer"; import LivePlayer from "@/components/player/LivePlayer";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
@ -62,7 +62,7 @@ function Live() {
<div className="flex"> <div className="flex">
{events.map((event) => { {events.map((event) => {
return ( return (
<EventThumbnail <AnimatedEventThumbnail
key={event.id} key={event.id}
event={event} event={event}
onFavorite={onFavorite} onFavorite={onFavorite}