* Use full resolution aspect for main camera style in history view

* Only check for offline cameras after 60s of uptime

* only call onPlaying when loadeddata is fired or after timeout

* revert to inline funcs

* Portal frigate plus alert dialog

* remove duplicated logic

* increase onplaying timeout

* Use a ref instead of a state and clear timeout in AutoUpdatingCameraImage

* default to the selected month for selectedDay

* Use buffered time instead of timeout

* Use default cursor when not editing polygons
This commit is contained in:
Josh Hawkins 2024-07-08 08:14:10 -05:00 committed by GitHub
parent 2ea1d34f4f
commit 0ce596ec8f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 127 additions and 73 deletions

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import CameraImage from "./CameraImage"; import CameraImage from "./CameraImage";
type AutoUpdatingCameraImageProps = { type AutoUpdatingCameraImageProps = {
@ -22,7 +22,7 @@ export default function AutoUpdatingCameraImage({
}: 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");
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout>(); const timeoutRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => { useEffect(() => {
if (reloadInterval == -1) { if (reloadInterval == -1) {
@ -32,9 +32,9 @@ export default function AutoUpdatingCameraImage({
setKey(Date.now()); setKey(Date.now());
return () => { return () => {
if (timeoutId) { if (timeoutRef.current) {
clearTimeout(timeoutId); clearTimeout(timeoutRef.current);
setTimeoutId(undefined); timeoutRef.current = null;
} }
}; };
// we know that these deps are correct // we know that these deps are correct
@ -46,19 +46,21 @@ export default function AutoUpdatingCameraImage({
return; return;
} }
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
const loadTime = Date.now() - key; const loadTime = Date.now() - key;
if (showFps) { if (showFps) {
setFps((1000 / Math.max(loadTime, reloadInterval)).toFixed(1)); setFps((1000 / Math.max(loadTime, reloadInterval)).toFixed(1));
} }
setTimeoutId( timeoutRef.current = setTimeout(
setTimeout( () => {
() => { setKey(Date.now());
setKey(Date.now()); },
}, loadTime > reloadInterval ? 1 : reloadInterval,
loadTime > reloadInterval ? 1 : reloadInterval,
),
); );
// we know that these deps are correct // we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps

View File

@ -1,3 +1,4 @@
import { forwardRef } from "react";
import { LuPlus } from "react-icons/lu"; import { LuPlus } from "react-icons/lu";
import Logo from "../Logo"; import Logo from "../Logo";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -6,17 +7,20 @@ type FrigatePlusIconProps = {
className?: string; className?: string;
onClick?: () => void; onClick?: () => void;
}; };
export default function FrigatePlusIcon({
className, const FrigatePlusIcon = forwardRef<HTMLDivElement, FrigatePlusIconProps>(
onClick, ({ className, onClick }, ref) => {
}: FrigatePlusIconProps) { return (
return ( <div
<div ref={ref}
className={cn("relative flex items-center", className)} className={cn("relative flex items-center", className)}
onClick={onClick} onClick={onClick}
> >
<Logo className="size-full" /> <Logo className="size-full" />
<LuPlus className="absolute size-2 translate-x-3 translate-y-3/4" /> <LuPlus className="absolute size-2 translate-x-3 translate-y-3/4" />
</div> </div>
); );
} },
);
export default FrigatePlusIcon;

View File

@ -61,6 +61,7 @@ export default function ReviewActivityCalendar({
return ( return (
<Calendar <Calendar
key={selectedDay ? selectedDay.toISOString() : "reset"}
mode="single" mode="single"
disabled={disabledDates} disabled={disabledDates}
showOutsideDays={false} showOutsideDays={false}
@ -70,6 +71,7 @@ export default function ReviewActivityCalendar({
components={{ components={{
DayContent: ReviewActivityDay, DayContent: ReviewActivityDay,
}} }}
defaultMonth={selectedDay ?? new Date()}
/> />
); );
} }
@ -152,12 +154,14 @@ export function TimezoneAwareCalendar({
return ( return (
<Calendar <Calendar
key={selectedDay ? selectedDay.toISOString() : "reset"}
mode="single" mode="single"
disabled={disabledDates} disabled={disabledDates}
showOutsideDays={false} showOutsideDays={false}
today={today} today={today}
selected={selectedDay} selected={selectedDay}
onSelect={onSelect} onSelect={onSelect}
defaultMonth={selectedDay ?? new Date()}
/> />
); );
} }

View File

@ -40,6 +40,7 @@ type HlsVideoPlayerProps = {
setFullResolution?: React.Dispatch<React.SetStateAction<VideoResolutionType>>; setFullResolution?: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
onUploadFrame?: (playTime: number) => Promise<AxiosResponse> | undefined; onUploadFrame?: (playTime: number) => Promise<AxiosResponse> | undefined;
toggleFullscreen?: () => void; toggleFullscreen?: () => void;
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
}; };
export default function HlsVideoPlayer({ export default function HlsVideoPlayer({
videoRef, videoRef,
@ -54,6 +55,7 @@ export default function HlsVideoPlayer({
setFullResolution, setFullResolution,
onUploadFrame, onUploadFrame,
toggleFullscreen, toggleFullscreen,
containerRef,
}: HlsVideoPlayerProps) { }: HlsVideoPlayerProps) {
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
@ -225,6 +227,7 @@ export default function HlsVideoPlayer({
}} }}
fullscreen={fullscreen} fullscreen={fullscreen}
toggleFullscreen={toggleFullscreen} toggleFullscreen={toggleFullscreen}
containerRef={containerRef}
/> />
<TransformComponent <TransformComponent
wrapperStyle={{ wrapperStyle={{

View File

@ -256,9 +256,10 @@ export default function LivePlayer({
)} )}
<div <div
className={`absolute inset-0 w-full ${ className={cn(
showStillWithoutActivity && !liveReady ? "visible" : "invisible" "absolute inset-0 w-full",
}`} showStillWithoutActivity && !liveReady ? "visible" : "invisible",
)}
> >
<AutoUpdatingCameraImage <AutoUpdatingCameraImage
className="size-full" className="size-full"

View File

@ -297,6 +297,11 @@ function MSEPlayer({
}; };
}; };
const getBufferedTime = (video: HTMLVideoElement | null) => {
if (!video || video.buffered.length === 0) return 0;
return video.buffered.end(video.buffered.length - 1) - video.currentTime;
};
useEffect(() => { useEffect(() => {
if (!playbackEnabled) { if (!playbackEnabled) {
return; return;
@ -385,9 +390,15 @@ function MSEPlayer({
muted={!audioEnabled} muted={!audioEnabled}
onPause={() => videoRef.current?.play()} onPause={() => videoRef.current?.play()}
onProgress={() => { onProgress={() => {
if (!isPlaying) { // if we have > 3 seconds of buffered data and we're still not playing,
// something might be wrong - maybe codec issue, no audio, etc
// so mark the player as playing so that error handlers will fire
if (
!isPlaying &&
playbackEnabled &&
getBufferedTime(videoRef.current) > 3
) {
setIsPlaying(true); setIsPlaying(true);
handleLoadedMetadata?.();
onPlaying?.(); onPlaying?.();
} }
if (onError != undefined) { if (onError != undefined) {

View File

@ -71,6 +71,7 @@ type VideoControlsProps = {
onSetPlaybackRate: (rate: number) => void; onSetPlaybackRate: (rate: number) => void;
onUploadFrame?: () => void; onUploadFrame?: () => void;
toggleFullscreen?: () => void; toggleFullscreen?: () => void;
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
}; };
export default function VideoControls({ export default function VideoControls({
className, className,
@ -91,10 +92,11 @@ export default function VideoControls({
onSetPlaybackRate, onSetPlaybackRate,
onUploadFrame, onUploadFrame,
toggleFullscreen, toggleFullscreen,
containerRef,
}: VideoControlsProps) { }: VideoControlsProps) {
// layout // layout
const containerRef = useRef<HTMLDivElement | null>(null); const controlsContainerRef = useRef<HTMLDivElement | null>(null);
// controls // controls
@ -197,7 +199,7 @@ export default function VideoControls({
MIN_ITEMS_WRAP && MIN_ITEMS_WRAP &&
"min-w-[75%] flex-wrap", "min-w-[75%] flex-wrap",
)} )}
ref={containerRef} ref={controlsContainerRef}
> >
{video && features.volume && ( {video && features.volume && (
<div className="flex cursor-pointer items-center justify-normal gap-2"> <div className="flex cursor-pointer items-center justify-normal gap-2">
@ -247,7 +249,7 @@ export default function VideoControls({
> >
<DropdownMenuTrigger>{`${playbackRate}x`}</DropdownMenuTrigger> <DropdownMenuTrigger>{`${playbackRate}x`}</DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
portalProps={{ container: containerRef.current }} portalProps={{ container: controlsContainerRef.current }}
> >
<DropdownMenuRadioGroup <DropdownMenuRadioGroup
onValueChange={(rate) => onSetPlaybackRate(parseFloat(rate))} onValueChange={(rate) => onSetPlaybackRate(parseFloat(rate))}
@ -281,6 +283,7 @@ export default function VideoControls({
} }
}} }}
onUploadFrame={onUploadFrame} onUploadFrame={onUploadFrame}
containerRef={containerRef}
/> />
)} )}
{features.fullscreen && toggleFullscreen && ( {features.fullscreen && toggleFullscreen && (
@ -297,12 +300,14 @@ type FrigatePlusUploadButtonProps = {
onOpen: () => void; onOpen: () => void;
onClose: () => void; onClose: () => void;
onUploadFrame: () => void; onUploadFrame: () => void;
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
}; };
function FrigatePlusUploadButton({ function FrigatePlusUploadButton({
video, video,
onOpen, onOpen,
onClose, onClose,
onUploadFrame, onUploadFrame,
containerRef,
}: FrigatePlusUploadButtonProps) { }: FrigatePlusUploadButtonProps) {
const [videoImg, setVideoImg] = useState<string>(); const [videoImg, setVideoImg] = useState<string>();
@ -336,7 +341,10 @@ function FrigatePlusUploadButton({
}} }}
/> />
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent className="md:max-w-2xl lg:max-w-3xl xl:max-w-4xl"> <AlertDialogContent
portalProps={{ container: containerRef?.current }}
className="md:max-w-2xl lg:max-w-3xl xl:max-w-4xl"
>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Submit this frame to Frigate+?</AlertDialogTitle> <AlertDialogTitle>Submit this frame to Frigate+?</AlertDialogTitle>
</AlertDialogHeader> </AlertDialogHeader>

View File

@ -30,6 +30,7 @@ type DynamicVideoPlayerProps = {
onClipEnded?: () => void; onClipEnded?: () => void;
setFullResolution: React.Dispatch<React.SetStateAction<VideoResolutionType>>; setFullResolution: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
toggleFullscreen: () => void; toggleFullscreen: () => void;
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
}; };
export default function DynamicVideoPlayer({ export default function DynamicVideoPlayer({
className, className,
@ -45,6 +46,7 @@ export default function DynamicVideoPlayer({
onClipEnded, onClipEnded,
setFullResolution, setFullResolution,
toggleFullscreen, toggleFullscreen,
containerRef,
}: DynamicVideoPlayerProps) { }: DynamicVideoPlayerProps) {
const apiHost = useApiHost(); const apiHost = useApiHost();
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
@ -208,6 +210,7 @@ export default function DynamicVideoPlayer({
setFullResolution={setFullResolution} setFullResolution={setFullResolution}
onUploadFrame={onUploadFrameToPlus} onUploadFrame={onUploadFrameToPlus}
toggleFullscreen={toggleFullscreen} toggleFullscreen={toggleFullscreen}
containerRef={containerRef}
/> />
<PreviewPlayer <PreviewPlayer
className={cn( className={cn(

View File

@ -131,10 +131,18 @@ export default function PolygonDrawer({
closed={isFinished} closed={isFinished}
fill={colorString(isActive || isHovered ? true : false)} fill={colorString(isActive || isHovered ? true : false)}
onMouseOver={() => onMouseOver={() =>
isFinished ? setCursor("move") : setCursor("crosshair") isActive
? isFinished
? setCursor("move")
: setCursor("crosshair")
: setCursor("default")
} }
onMouseOut={() => onMouseOut={() =>
isFinished ? setCursor("default") : setCursor("crosshair") isActive
? isFinished
? setCursor("default")
: setCursor("crosshair")
: setCursor("default")
} }
/> />
{isFinished && isActive && ( {isFinished && isActive && (

View File

@ -1,14 +1,14 @@
import * as React from "react" import * as React from "react";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button" import { buttonVariants } from "@/components/ui/button";
const AlertDialog = AlertDialogPrimitive.Root const AlertDialog = AlertDialogPrimitive.Root;
const AlertDialogTrigger = AlertDialogPrimitive.Trigger const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
const AlertDialogPortal = AlertDialogPrimitive.Portal const AlertDialogPortal = AlertDialogPrimitive.Portal;
const AlertDialogOverlay = React.forwardRef< const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>, React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
@ -17,31 +17,33 @@ const AlertDialogOverlay = React.forwardRef<
<AlertDialogPrimitive.Overlay <AlertDialogPrimitive.Overlay
className={cn( className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className className,
)} )}
{...props} {...props}
ref={ref} ref={ref}
/> />
)) ));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
const AlertDialogContent = React.forwardRef< const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>, React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content> React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content> & {
>(({ className, ...props }, ref) => ( portalProps?: AlertDialogPrimitive.AlertDialogPortalProps;
<AlertDialogPortal> }
>(({ className, portalProps, ...props }, ref) => (
<AlertDialogPortal {...portalProps}>
<AlertDialogOverlay /> <AlertDialogOverlay />
<AlertDialogPrimitive.Content <AlertDialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className className,
)} )}
{...props} {...props}
/> />
</AlertDialogPortal> </AlertDialogPortal>
)) ));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({ const AlertDialogHeader = ({
className, className,
@ -50,12 +52,12 @@ const AlertDialogHeader = ({
<div <div
className={cn( className={cn(
"flex flex-col space-y-2 text-center sm:text-left", "flex flex-col space-y-2 text-center sm:text-left",
className className,
)} )}
{...props} {...props}
/> />
) );
AlertDialogHeader.displayName = "AlertDialogHeader" AlertDialogHeader.displayName = "AlertDialogHeader";
const AlertDialogFooter = ({ const AlertDialogFooter = ({
className, className,
@ -64,12 +66,12 @@ const AlertDialogFooter = ({
<div <div
className={cn( className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className className,
)} )}
{...props} {...props}
/> />
) );
AlertDialogFooter.displayName = "AlertDialogFooter" AlertDialogFooter.displayName = "AlertDialogFooter";
const AlertDialogTitle = React.forwardRef< const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>, React.ElementRef<typeof AlertDialogPrimitive.Title>,
@ -80,8 +82,8 @@ const AlertDialogTitle = React.forwardRef<
className={cn("text-lg font-semibold", className)} className={cn("text-lg font-semibold", className)}
{...props} {...props}
/> />
)) ));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = React.forwardRef< const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>, React.ElementRef<typeof AlertDialogPrimitive.Description>,
@ -92,9 +94,9 @@ const AlertDialogDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}
/> />
)) ));
AlertDialogDescription.displayName = AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = React.forwardRef< const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>, React.ElementRef<typeof AlertDialogPrimitive.Action>,
@ -105,8 +107,8 @@ const AlertDialogAction = React.forwardRef<
className={cn(buttonVariants(), className)} className={cn(buttonVariants(), className)}
{...props} {...props}
/> />
)) ));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = React.forwardRef< const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>, React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
@ -117,12 +119,12 @@ const AlertDialogCancel = React.forwardRef<
className={cn( className={cn(
buttonVariants({ variant: "outline" }), buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0", "mt-2 sm:mt-0",
className className,
)} )}
{...props} {...props}
/> />
)) ));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
export { export {
AlertDialog, AlertDialog,
@ -136,4 +138,4 @@ export {
AlertDialogDescription, AlertDialogDescription,
AlertDialogAction, AlertDialogAction,
AlertDialogCancel, AlertDialogCancel,
} };

View File

@ -133,7 +133,7 @@ export function useCameraActivity(
return false; return false;
} }
return cameras[camera.name].camera_fps == 0; return cameras[camera.name].camera_fps == 0 && stats["service"].uptime > 60;
}, [camera, stats]); }, [camera, stats]);
return { return {

View File

@ -314,7 +314,7 @@ export function RecordingView({
return undefined; return undefined;
} }
const aspect = camera.detect.width / camera.detect.height; const aspect = getCameraAspect(mainCamera);
if (!aspect) { if (!aspect) {
return undefined; return undefined;
@ -336,7 +336,14 @@ export function RecordingView({
return { return {
width: `${Math.round(percent)}%`, width: `${Math.round(percent)}%`,
}; };
}, [config, mainCameraAspect, mainWidth, mainHeight, mainCamera]); }, [
config,
mainCameraAspect,
mainWidth,
mainHeight,
mainCamera,
getCameraAspect,
]);
const previewRowOverflows = useMemo(() => { const previewRowOverflows = useMemo(() => {
if (!previewRowRef.current) { if (!previewRowRef.current) {
@ -532,6 +539,7 @@ export function RecordingView({
isScrubbing={scrubbing || exportMode == "timeline"} isScrubbing={scrubbing || exportMode == "timeline"}
setFullResolution={setFullResolution} setFullResolution={setFullResolution}
toggleFullscreen={toggleFullscreen} toggleFullscreen={toggleFullscreen}
containerRef={mainLayoutRef}
/> />
</div> </div>
{isDesktop && ( {isDesktop && (

View File

@ -399,7 +399,7 @@ export default function LiveCameraView({
onClick={() => setMic(!mic)} onClick={() => setMic(!mic)}
/> />
)} )}
{supportsAudioOutput && ( {supportsAudioOutput && preferredLiveMode != "jsmpeg" && (
<CameraFeatureToggle <CameraFeatureToggle
className="p-2 md:p-0" className="p-2 md:p-0"
variant={fullscreen ? "overlay" : "primary"} variant={fullscreen ? "overlay" : "primary"}