Enable temporary caching of camera images to improve responsiveness of UI (#15614)

This commit is contained in:
Nicolas Mowen 2024-12-20 08:17:51 -06:00 committed by Blake Blackshear
parent edab4efa42
commit d0ad840ef4
4 changed files with 39 additions and 7 deletions

View File

@ -20,6 +20,7 @@ class MediaLatestFrameQueryParams(BaseModel):
regions: Optional[int] = None regions: Optional[int] = None
quality: Optional[int] = 70 quality: Optional[int] = 70
height: Optional[int] = None height: Optional[int] = None
store: Optional[int] = None
class MediaEventsSnapshotQueryParams(BaseModel): class MediaEventsSnapshotQueryParams(BaseModel):

View File

@ -182,11 +182,16 @@ def latest_frame(
frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA) frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA)
ret, img = cv2.imencode(f".{extension}", frame, quality_params) _, img = cv2.imencode(f".{extension}", frame, quality_params)
return Response( return Response(
content=img.tobytes(), content=img.tobytes(),
media_type=f"image/{mime_type}", media_type=f"image/{mime_type}",
headers={"Content-Type": f"image/{mime_type}", "Cache-Control": "no-store"}, headers={
"Content-Type": f"image/{mime_type}",
"Cache-Control": "no-store"
if not params.store
else "private, max-age=60",
},
) )
elif camera_name == "birdseye" and request.app.frigate_config.birdseye.restream: elif camera_name == "birdseye" and request.app.frigate_config.birdseye.restream:
frame = cv2.cvtColor( frame = cv2.cvtColor(
@ -199,11 +204,16 @@ def latest_frame(
frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA) frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA)
ret, img = cv2.imencode(f".{extension}", frame, quality_params) _, img = cv2.imencode(f".{extension}", frame, quality_params)
return Response( return Response(
content=img.tobytes(), content=img.tobytes(),
media_type=f"image/{mime_type}", media_type=f"image/{mime_type}",
headers={"Content-Type": f"image/{mime_type}", "Cache-Control": "no-store"}, headers={
"Content-Type": f"image/{mime_type}",
"Cache-Control": "no-store"
if not params.store
else "private, max-age=60",
},
) )
else: else:
return JSONResponse( return JSONResponse(

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import CameraImage from "./CameraImage"; import CameraImage from "./CameraImage";
type AutoUpdatingCameraImageProps = { type AutoUpdatingCameraImageProps = {
@ -8,6 +8,7 @@ type AutoUpdatingCameraImageProps = {
className?: string; className?: string;
cameraClasses?: string; cameraClasses?: string;
reloadInterval?: number; reloadInterval?: number;
periodicCache?: boolean;
}; };
const MIN_LOAD_TIMEOUT_MS = 200; const MIN_LOAD_TIMEOUT_MS = 200;
@ -19,6 +20,7 @@ export default function AutoUpdatingCameraImage({
className, className,
cameraClasses, cameraClasses,
reloadInterval = MIN_LOAD_TIMEOUT_MS, reloadInterval = MIN_LOAD_TIMEOUT_MS,
periodicCache = false,
}: AutoUpdatingCameraImageProps) { }: AutoUpdatingCameraImageProps) {
const [key, setKey] = useState(Date.now()); const [key, setKey] = useState(Date.now());
const [fps, setFps] = useState<string>("0"); const [fps, setFps] = useState<string>("0");
@ -42,6 +44,8 @@ export default function AutoUpdatingCameraImage({
}, [reloadInterval]); }, [reloadInterval]);
const handleLoad = useCallback(() => { const handleLoad = useCallback(() => {
setIsCached(true);
if (reloadInterval == -1) { if (reloadInterval == -1) {
return; return;
} }
@ -66,12 +70,28 @@ export default function AutoUpdatingCameraImage({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [key, setFps]); }, [key, setFps]);
// periodic cache to reduce loading indicator
const [isCached, setIsCached] = useState(false);
const cacheKey = useMemo(() => {
let baseParam = "";
if (periodicCache && !isCached) {
baseParam = "store=1";
} else {
baseParam = `cache=${key}`;
}
return `${baseParam}${searchParams ? `&${searchParams}` : ""}`;
}, [isCached, periodicCache, key, searchParams]);
return ( return (
<div className={className}> <div className={className}>
<CameraImage <CameraImage
camera={camera} camera={camera}
onload={handleLoad} onload={handleLoad}
searchParams={`cache=${key}${searchParams ? `&${searchParams}` : ""}`} searchParams={cacheKey}
className={cameraClasses} className={cameraClasses}
/> />
{showFps ? <span className="text-xs">Displaying at {fps}fps</span> : null} {showFps ? <span className="text-xs">Displaying at {fps}fps</span> : null}

View File

@ -294,10 +294,11 @@ export default function LivePlayer({
> >
<AutoUpdatingCameraImage <AutoUpdatingCameraImage
className="size-full" className="size-full"
cameraClasses="relative size-full flex justify-center"
camera={cameraConfig.name} camera={cameraConfig.name}
showFps={false} showFps={false}
reloadInterval={stillReloadInterval} reloadInterval={stillReloadInterval}
cameraClasses="relative size-full flex justify-center" periodicCache
/> />
</div> </div>