diff --git a/web/src/components/camera/AutoUpdatingCameraImage.tsx b/web/src/components/camera/AutoUpdatingCameraImage.tsx index 3730f069b1..ee0f6eccc7 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 24ee06eb53..15e196cd14 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 e119870322..6ac7c6a205 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 2086402cd3..5562303b2a 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 5adebdc7c2..70d9a4be8c 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 62f8a75d7d..2f347404f5 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 cc49f39602..1519098429 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 15b35de7ff..815bd12f35 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 f42e83a173..92c96441a2 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 65581d5022..f148f01ee5 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" && (