From 0ce596ec8fd6e41b69b3798ea3cc90fe9689d401 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 8 Jul 2024 08:14:10 -0500 Subject: [PATCH] UI tweaks (#12297) * 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 --- .../camera/AutoUpdatingCameraImage.tsx | 26 ++++---- web/src/components/icons/FrigatePlusIcon.tsx | 32 +++++---- .../overlay/ReviewActivityCalendar.tsx | 4 ++ web/src/components/player/HlsVideoPlayer.tsx | 3 + web/src/components/player/LivePlayer.tsx | 7 +- web/src/components/player/MsePlayer.tsx | 15 ++++- web/src/components/player/VideoControls.tsx | 16 +++-- .../player/dynamic/DynamicVideoPlayer.tsx | 3 + web/src/components/settings/PolygonDrawer.tsx | 12 +++- web/src/components/ui/alert-dialog.tsx | 66 ++++++++++--------- web/src/hooks/use-camera-activity.ts | 2 +- web/src/views/events/RecordingView.tsx | 12 +++- web/src/views/live/LiveCameraView.tsx | 2 +- 13 files changed, 127 insertions(+), 73 deletions(-) diff --git a/web/src/components/camera/AutoUpdatingCameraImage.tsx b/web/src/components/camera/AutoUpdatingCameraImage.tsx index 3730f069b..ee0f6eccc 100644 --- a/web/src/components/camera/AutoUpdatingCameraImage.tsx +++ b/web/src/components/camera/AutoUpdatingCameraImage.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import CameraImage from "./CameraImage"; type AutoUpdatingCameraImageProps = { @@ -22,7 +22,7 @@ export default function AutoUpdatingCameraImage({ }: AutoUpdatingCameraImageProps) { const [key, setKey] = useState(Date.now()); const [fps, setFps] = useState("0"); - const [timeoutId, setTimeoutId] = useState(); + const timeoutRef = useRef(null); useEffect(() => { if (reloadInterval == -1) { @@ -32,9 +32,9 @@ export default function AutoUpdatingCameraImage({ setKey(Date.now()); return () => { - if (timeoutId) { - clearTimeout(timeoutId); - setTimeoutId(undefined); + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; } }; // we know that these deps are correct @@ -46,19 +46,21 @@ export default function AutoUpdatingCameraImage({ return; } + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + const loadTime = Date.now() - key; if (showFps) { setFps((1000 / Math.max(loadTime, reloadInterval)).toFixed(1)); } - setTimeoutId( - setTimeout( - () => { - setKey(Date.now()); - }, - loadTime > reloadInterval ? 1 : reloadInterval, - ), + timeoutRef.current = setTimeout( + () => { + setKey(Date.now()); + }, + loadTime > reloadInterval ? 1 : reloadInterval, ); // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/web/src/components/icons/FrigatePlusIcon.tsx b/web/src/components/icons/FrigatePlusIcon.tsx index 24ee06eb5..15e196cd1 100644 --- a/web/src/components/icons/FrigatePlusIcon.tsx +++ b/web/src/components/icons/FrigatePlusIcon.tsx @@ -1,3 +1,4 @@ +import { forwardRef } from "react"; import { LuPlus } from "react-icons/lu"; import Logo from "../Logo"; import { cn } from "@/lib/utils"; @@ -6,17 +7,20 @@ type FrigatePlusIconProps = { className?: string; onClick?: () => void; }; -export default function FrigatePlusIcon({ - className, - onClick, -}: FrigatePlusIconProps) { - return ( -
- - -
- ); -} + +const FrigatePlusIcon = forwardRef( + ({ className, onClick }, ref) => { + return ( +
+ + +
+ ); + }, +); + +export default FrigatePlusIcon; diff --git a/web/src/components/overlay/ReviewActivityCalendar.tsx b/web/src/components/overlay/ReviewActivityCalendar.tsx index e11987032..6ac7c6a20 100644 --- a/web/src/components/overlay/ReviewActivityCalendar.tsx +++ b/web/src/components/overlay/ReviewActivityCalendar.tsx @@ -61,6 +61,7 @@ export default function ReviewActivityCalendar({ return ( ); } @@ -152,12 +154,14 @@ export function TimezoneAwareCalendar({ return ( ); } diff --git a/web/src/components/player/HlsVideoPlayer.tsx b/web/src/components/player/HlsVideoPlayer.tsx index 2086402cd..5562303b2 100644 --- a/web/src/components/player/HlsVideoPlayer.tsx +++ b/web/src/components/player/HlsVideoPlayer.tsx @@ -40,6 +40,7 @@ type HlsVideoPlayerProps = { setFullResolution?: React.Dispatch>; onUploadFrame?: (playTime: number) => Promise | undefined; toggleFullscreen?: () => void; + containerRef?: React.MutableRefObject; }; export default function HlsVideoPlayer({ videoRef, @@ -54,6 +55,7 @@ export default function HlsVideoPlayer({ setFullResolution, onUploadFrame, toggleFullscreen, + containerRef, }: HlsVideoPlayerProps) { const { data: config } = useSWR("config"); @@ -225,6 +227,7 @@ export default function HlsVideoPlayer({ }} fullscreen={fullscreen} toggleFullscreen={toggleFullscreen} + containerRef={containerRef} /> { + if (!video || video.buffered.length === 0) return 0; + return video.buffered.end(video.buffered.length - 1) - video.currentTime; + }; + useEffect(() => { if (!playbackEnabled) { return; @@ -385,9 +390,15 @@ function MSEPlayer({ muted={!audioEnabled} onPause={() => videoRef.current?.play()} 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); - handleLoadedMetadata?.(); onPlaying?.(); } if (onError != undefined) { diff --git a/web/src/components/player/VideoControls.tsx b/web/src/components/player/VideoControls.tsx index 5adebdc7c..70d9a4be8 100644 --- a/web/src/components/player/VideoControls.tsx +++ b/web/src/components/player/VideoControls.tsx @@ -71,6 +71,7 @@ type VideoControlsProps = { onSetPlaybackRate: (rate: number) => void; onUploadFrame?: () => void; toggleFullscreen?: () => void; + containerRef?: React.MutableRefObject; }; export default function VideoControls({ className, @@ -91,10 +92,11 @@ export default function VideoControls({ onSetPlaybackRate, onUploadFrame, toggleFullscreen, + containerRef, }: VideoControlsProps) { // layout - const containerRef = useRef(null); + const controlsContainerRef = useRef(null); // controls @@ -197,7 +199,7 @@ export default function VideoControls({ MIN_ITEMS_WRAP && "min-w-[75%] flex-wrap", )} - ref={containerRef} + ref={controlsContainerRef} > {video && features.volume && (
@@ -247,7 +249,7 @@ export default function VideoControls({ > {`${playbackRate}x`} onSetPlaybackRate(parseFloat(rate))} @@ -281,6 +283,7 @@ export default function VideoControls({ } }} onUploadFrame={onUploadFrame} + containerRef={containerRef} /> )} {features.fullscreen && toggleFullscreen && ( @@ -297,12 +300,14 @@ type FrigatePlusUploadButtonProps = { onOpen: () => void; onClose: () => void; onUploadFrame: () => void; + containerRef?: React.MutableRefObject; }; function FrigatePlusUploadButton({ video, onOpen, onClose, onUploadFrame, + containerRef, }: FrigatePlusUploadButtonProps) { const [videoImg, setVideoImg] = useState(); @@ -336,7 +341,10 @@ function FrigatePlusUploadButton({ }} /> - + Submit this frame to Frigate+? diff --git a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx index 62f8a75d7..2f347404f 100644 --- a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx +++ b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx @@ -30,6 +30,7 @@ type DynamicVideoPlayerProps = { onClipEnded?: () => void; setFullResolution: React.Dispatch>; toggleFullscreen: () => void; + containerRef?: React.MutableRefObject; }; export default function DynamicVideoPlayer({ className, @@ -45,6 +46,7 @@ export default function DynamicVideoPlayer({ onClipEnded, setFullResolution, toggleFullscreen, + containerRef, }: DynamicVideoPlayerProps) { const apiHost = useApiHost(); const { data: config } = useSWR("config"); @@ -208,6 +210,7 @@ export default function DynamicVideoPlayer({ setFullResolution={setFullResolution} onUploadFrame={onUploadFrameToPlus} toggleFullscreen={toggleFullscreen} + containerRef={containerRef} /> - isFinished ? setCursor("move") : setCursor("crosshair") + isActive + ? isFinished + ? setCursor("move") + : setCursor("crosshair") + : setCursor("default") } onMouseOut={() => - isFinished ? setCursor("default") : setCursor("crosshair") + isActive + ? isFinished + ? setCursor("default") + : setCursor("crosshair") + : setCursor("default") } /> {isFinished && isActive && ( diff --git a/web/src/components/ui/alert-dialog.tsx b/web/src/components/ui/alert-dialog.tsx index cc49f3960..151909842 100644 --- a/web/src/components/ui/alert-dialog.tsx +++ b/web/src/components/ui/alert-dialog.tsx @@ -1,14 +1,14 @@ -import * as React from "react" -import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" +import * as React from "react"; +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; -import { cn } from "@/lib/utils" -import { buttonVariants } from "@/components/ui/button" +import { cn } from "@/lib/utils"; +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< React.ElementRef, @@ -17,31 +17,33 @@ const AlertDialogOverlay = React.forwardRef< -)) -AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName +)); +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; const AlertDialogContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - + React.ComponentPropsWithoutRef & { + portalProps?: AlertDialogPrimitive.AlertDialogPortalProps; + } +>(({ className, portalProps, ...props }, ref) => ( + -)) -AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName +)); +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; const AlertDialogHeader = ({ className, @@ -50,12 +52,12 @@ const AlertDialogHeader = ({
-) -AlertDialogHeader.displayName = "AlertDialogHeader" +); +AlertDialogHeader.displayName = "AlertDialogHeader"; const AlertDialogFooter = ({ className, @@ -64,12 +66,12 @@ const AlertDialogFooter = ({
-) -AlertDialogFooter.displayName = "AlertDialogFooter" +); +AlertDialogFooter.displayName = "AlertDialogFooter"; const AlertDialogTitle = React.forwardRef< React.ElementRef, @@ -80,8 +82,8 @@ const AlertDialogTitle = React.forwardRef< className={cn("text-lg font-semibold", className)} {...props} /> -)) -AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName +)); +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; const AlertDialogDescription = React.forwardRef< React.ElementRef, @@ -92,9 +94,9 @@ const AlertDialogDescription = React.forwardRef< className={cn("text-sm text-muted-foreground", className)} {...props} /> -)) +)); AlertDialogDescription.displayName = - AlertDialogPrimitive.Description.displayName + AlertDialogPrimitive.Description.displayName; const AlertDialogAction = React.forwardRef< React.ElementRef, @@ -105,8 +107,8 @@ const AlertDialogAction = React.forwardRef< className={cn(buttonVariants(), className)} {...props} /> -)) -AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName +)); +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; const AlertDialogCancel = React.forwardRef< React.ElementRef, @@ -117,12 +119,12 @@ const AlertDialogCancel = React.forwardRef< className={cn( buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", - className + className, )} {...props} /> -)) -AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName +)); +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; export { AlertDialog, @@ -136,4 +138,4 @@ export { AlertDialogDescription, AlertDialogAction, AlertDialogCancel, -} +}; diff --git a/web/src/hooks/use-camera-activity.ts b/web/src/hooks/use-camera-activity.ts index 15b35de7f..815bd12f3 100644 --- a/web/src/hooks/use-camera-activity.ts +++ b/web/src/hooks/use-camera-activity.ts @@ -133,7 +133,7 @@ export function useCameraActivity( return false; } - return cameras[camera.name].camera_fps == 0; + return cameras[camera.name].camera_fps == 0 && stats["service"].uptime > 60; }, [camera, stats]); return { diff --git a/web/src/views/events/RecordingView.tsx b/web/src/views/events/RecordingView.tsx index f42e83a17..92c96441a 100644 --- a/web/src/views/events/RecordingView.tsx +++ b/web/src/views/events/RecordingView.tsx @@ -314,7 +314,7 @@ export function RecordingView({ return undefined; } - const aspect = camera.detect.width / camera.detect.height; + const aspect = getCameraAspect(mainCamera); if (!aspect) { return undefined; @@ -336,7 +336,14 @@ export function RecordingView({ return { width: `${Math.round(percent)}%`, }; - }, [config, mainCameraAspect, mainWidth, mainHeight, mainCamera]); + }, [ + config, + mainCameraAspect, + mainWidth, + mainHeight, + mainCamera, + getCameraAspect, + ]); const previewRowOverflows = useMemo(() => { if (!previewRowRef.current) { @@ -532,6 +539,7 @@ export function RecordingView({ isScrubbing={scrubbing || exportMode == "timeline"} setFullResolution={setFullResolution} toggleFullscreen={toggleFullscreen} + containerRef={mainLayoutRef} />
{isDesktop && ( diff --git a/web/src/views/live/LiveCameraView.tsx b/web/src/views/live/LiveCameraView.tsx index 65581d502..f148f01ee 100644 --- a/web/src/views/live/LiveCameraView.tsx +++ b/web/src/views/live/LiveCameraView.tsx @@ -399,7 +399,7 @@ export default function LiveCameraView({ onClick={() => setMic(!mic)} /> )} - {supportsAudioOutput && ( + {supportsAudioOutput && preferredLiveMode != "jsmpeg" && (