mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-08 06:15:43 +03:00
* POC: Added FastAPI with one endpoint (get /logs/service) * POC: Revert error_log * POC: Converted preview related endpoints to FastAPI * POC: Converted two more endpoints to FastAPI * POC: lint * Convert all media endpoints to FastAPI. Added /media prefix (/media/camera && media/events && /media/preview) * Convert all notifications API endpoints to FastAPI * Convert first review API endpoints to FastAPI * Convert remaining review API endpoints to FastAPI * Convert export endpoints to FastAPI * Fix path parameters * Convert events endpoints to FastAPI * Use body for multiple events endpoints * Use body for multiple events endpoints (create and end event) * Convert app endpoints to FastAPI * Convert app endpoints to FastAPI * Convert auth endpoints to FastAPI * Removed flask app in favour of FastAPI app. Implemented FastAPI middleware to check CSRF, connect and disconnect from DB. Added middleware x-forwared-for headers * Added starlette plugin to expose custom headers * Use slowapi as the limiter * Use query parameters for the frame latest endpoint * Use query parameters for the media snapshot.jpg endpoint * Use query parameters for the media MJPEG feed endpoint * Revert initial nginx.conf change * Added missing even_id for /events/search endpoint * Removed left over comment * Use FastAPI TestClient * severity query parameter should be a string * Use the same pattern for all tests * Fix endpoint * Revert media routers to old names. Order routes to make sure the dynamic ones from media.py are only used whenever there's no match on auth/etc * Reverted paths for media on tsx files * Deleted file * Fix test_http to use TestClient * Formatting * Bind timeline to DB * Fix http tests * Replace filename with pathvalidate * Fix latest.ext handling and disable uvicorn access logs * Add cosntraints to api provided values * Formatting * Remove unused * Remove unused * Get rate limiter working --------- Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
111 lines
2.9 KiB
TypeScript
111 lines
2.9 KiB
TypeScript
import { useApiHost } from "@/api";
|
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
import useSWR from "swr";
|
|
import ActivityIndicator from "../indicators/activity-indicator";
|
|
import { useResizeObserver } from "@/hooks/resize-observer";
|
|
import { isDesktop } from "react-device-detect";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
type CameraImageProps = {
|
|
className?: string;
|
|
camera: string;
|
|
onload?: () => void;
|
|
searchParams?: string;
|
|
};
|
|
|
|
export default function CameraImage({
|
|
className,
|
|
camera,
|
|
onload,
|
|
searchParams = "",
|
|
}: CameraImageProps) {
|
|
const { data: config } = useSWR("config");
|
|
const apiHost = useApiHost();
|
|
const [imageLoaded, setImageLoaded] = useState(false);
|
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
const imgRef = useRef<HTMLImageElement | null>(null);
|
|
|
|
const { name } = config ? config.cameras[camera] : "";
|
|
const enabled = config ? config.cameras[camera].enabled : "True";
|
|
|
|
const [{ width: containerWidth, height: containerHeight }] =
|
|
useResizeObserver(containerRef);
|
|
|
|
const requestHeight = useMemo(() => {
|
|
if (!config || containerHeight == 0) {
|
|
return 360;
|
|
}
|
|
|
|
return Math.min(
|
|
config.cameras[camera].detect.height,
|
|
Math.round(containerHeight * (isDesktop ? 1.1 : 1.25)),
|
|
);
|
|
}, [config, camera, containerHeight]);
|
|
|
|
const [isPortraitImage, setIsPortraitImage] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setImageLoaded(false);
|
|
setIsPortraitImage(false);
|
|
}, [camera]);
|
|
|
|
useEffect(() => {
|
|
if (!config || !imgRef.current) {
|
|
return;
|
|
}
|
|
|
|
const newSrc = `${apiHost}api/${name}/latest.webp?height=${requestHeight}${
|
|
searchParams ? `&${searchParams}` : ""
|
|
}`;
|
|
|
|
if (imgRef.current.src !== newSrc) {
|
|
imgRef.current.src = newSrc;
|
|
}
|
|
}, [apiHost, name, searchParams, requestHeight, config, camera]);
|
|
|
|
const handleImageLoad = () => {
|
|
if (imgRef.current && containerWidth && containerHeight) {
|
|
const { naturalWidth, naturalHeight } = imgRef.current;
|
|
setIsPortraitImage(
|
|
naturalWidth / naturalHeight < containerWidth / containerHeight,
|
|
);
|
|
}
|
|
|
|
setImageLoaded(true);
|
|
|
|
if (onload) {
|
|
onload();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={className} ref={containerRef}>
|
|
{enabled ? (
|
|
<img
|
|
ref={imgRef}
|
|
className={cn(
|
|
"object-contain",
|
|
imageLoaded
|
|
? isPortraitImage
|
|
? "h-full w-auto"
|
|
: "h-auto w-full"
|
|
: "invisible",
|
|
"rounded-lg md:rounded-2xl",
|
|
)}
|
|
onLoad={handleImageLoad}
|
|
loading="lazy"
|
|
/>
|
|
) : (
|
|
<div className="pt-6 text-center">
|
|
Camera is disabled in config, no stream or snapshot available!
|
|
</div>
|
|
)}
|
|
{!imageLoaded && enabled ? (
|
|
<div className="absolute bottom-0 left-0 right-0 top-0 flex items-center justify-center">
|
|
<ActivityIndicator />
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|