mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	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
This commit is contained in:
		
							parent
							
								
									2ea1d34f4f
								
							
						
					
					
						commit
						0ce596ec8f
					
				| @ -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<string>("0"); | ||||
|   const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout>(); | ||||
|   const timeoutRef = useRef<NodeJS.Timeout | null>(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( | ||||
|     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
 | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { forwardRef } from "react"; | ||||
| import { LuPlus } from "react-icons/lu"; | ||||
| import Logo from "../Logo"; | ||||
| import { cn } from "@/lib/utils"; | ||||
| @ -6,12 +7,12 @@ type FrigatePlusIconProps = { | ||||
|   className?: string; | ||||
|   onClick?: () => void; | ||||
| }; | ||||
| export default function FrigatePlusIcon({ | ||||
|   className, | ||||
|   onClick, | ||||
| }: FrigatePlusIconProps) { | ||||
| 
 | ||||
| const FrigatePlusIcon = forwardRef<HTMLDivElement, FrigatePlusIconProps>( | ||||
|   ({ className, onClick }, ref) => { | ||||
|     return ( | ||||
|       <div | ||||
|         ref={ref} | ||||
|         className={cn("relative flex items-center", className)} | ||||
|         onClick={onClick} | ||||
|       > | ||||
| @ -19,4 +20,7 @@ export default function FrigatePlusIcon({ | ||||
|         <LuPlus className="absolute size-2 translate-x-3 translate-y-3/4" /> | ||||
|       </div> | ||||
|     ); | ||||
| } | ||||
|   }, | ||||
| ); | ||||
| 
 | ||||
| export default FrigatePlusIcon; | ||||
|  | ||||
| @ -61,6 +61,7 @@ export default function ReviewActivityCalendar({ | ||||
| 
 | ||||
|   return ( | ||||
|     <Calendar | ||||
|       key={selectedDay ? selectedDay.toISOString() : "reset"} | ||||
|       mode="single" | ||||
|       disabled={disabledDates} | ||||
|       showOutsideDays={false} | ||||
| @ -70,6 +71,7 @@ export default function ReviewActivityCalendar({ | ||||
|       components={{ | ||||
|         DayContent: ReviewActivityDay, | ||||
|       }} | ||||
|       defaultMonth={selectedDay ?? new Date()} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| @ -152,12 +154,14 @@ export function TimezoneAwareCalendar({ | ||||
| 
 | ||||
|   return ( | ||||
|     <Calendar | ||||
|       key={selectedDay ? selectedDay.toISOString() : "reset"} | ||||
|       mode="single" | ||||
|       disabled={disabledDates} | ||||
|       showOutsideDays={false} | ||||
|       today={today} | ||||
|       selected={selectedDay} | ||||
|       onSelect={onSelect} | ||||
|       defaultMonth={selectedDay ?? new Date()} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| @ -40,6 +40,7 @@ type HlsVideoPlayerProps = { | ||||
|   setFullResolution?: React.Dispatch<React.SetStateAction<VideoResolutionType>>; | ||||
|   onUploadFrame?: (playTime: number) => Promise<AxiosResponse> | undefined; | ||||
|   toggleFullscreen?: () => void; | ||||
|   containerRef?: React.MutableRefObject<HTMLDivElement | null>; | ||||
| }; | ||||
| export default function HlsVideoPlayer({ | ||||
|   videoRef, | ||||
| @ -54,6 +55,7 @@ export default function HlsVideoPlayer({ | ||||
|   setFullResolution, | ||||
|   onUploadFrame, | ||||
|   toggleFullscreen, | ||||
|   containerRef, | ||||
| }: HlsVideoPlayerProps) { | ||||
|   const { data: config } = useSWR<FrigateConfig>("config"); | ||||
| 
 | ||||
| @ -225,6 +227,7 @@ export default function HlsVideoPlayer({ | ||||
|         }} | ||||
|         fullscreen={fullscreen} | ||||
|         toggleFullscreen={toggleFullscreen} | ||||
|         containerRef={containerRef} | ||||
|       /> | ||||
|       <TransformComponent | ||||
|         wrapperStyle={{ | ||||
|  | ||||
| @ -256,9 +256,10 @@ export default function LivePlayer({ | ||||
|         )} | ||||
| 
 | ||||
|       <div | ||||
|         className={`absolute inset-0 w-full ${ | ||||
|           showStillWithoutActivity && !liveReady ? "visible" : "invisible" | ||||
|         }`}
 | ||||
|         className={cn( | ||||
|           "absolute inset-0 w-full", | ||||
|           showStillWithoutActivity && !liveReady ? "visible" : "invisible", | ||||
|         )} | ||||
|       > | ||||
|         <AutoUpdatingCameraImage | ||||
|           className="size-full" | ||||
|  | ||||
| @ -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(() => { | ||||
|     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) { | ||||
|  | ||||
| @ -71,6 +71,7 @@ type VideoControlsProps = { | ||||
|   onSetPlaybackRate: (rate: number) => void; | ||||
|   onUploadFrame?: () => void; | ||||
|   toggleFullscreen?: () => void; | ||||
|   containerRef?: React.MutableRefObject<HTMLDivElement | null>; | ||||
| }; | ||||
| export default function VideoControls({ | ||||
|   className, | ||||
| @ -91,10 +92,11 @@ export default function VideoControls({ | ||||
|   onSetPlaybackRate, | ||||
|   onUploadFrame, | ||||
|   toggleFullscreen, | ||||
|   containerRef, | ||||
| }: VideoControlsProps) { | ||||
|   // layout
 | ||||
| 
 | ||||
|   const containerRef = useRef<HTMLDivElement | null>(null); | ||||
|   const controlsContainerRef = useRef<HTMLDivElement | null>(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 && ( | ||||
|         <div className="flex cursor-pointer items-center justify-normal gap-2"> | ||||
| @ -247,7 +249,7 @@ export default function VideoControls({ | ||||
|         > | ||||
|           <DropdownMenuTrigger>{`${playbackRate}x`}</DropdownMenuTrigger> | ||||
|           <DropdownMenuContent | ||||
|             portalProps={{ container: containerRef.current }} | ||||
|             portalProps={{ container: controlsContainerRef.current }} | ||||
|           > | ||||
|             <DropdownMenuRadioGroup | ||||
|               onValueChange={(rate) => 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<HTMLDivElement | null>; | ||||
| }; | ||||
| function FrigatePlusUploadButton({ | ||||
|   video, | ||||
|   onOpen, | ||||
|   onClose, | ||||
|   onUploadFrame, | ||||
|   containerRef, | ||||
| }: FrigatePlusUploadButtonProps) { | ||||
|   const [videoImg, setVideoImg] = useState<string>(); | ||||
| 
 | ||||
| @ -336,7 +341,10 @@ function FrigatePlusUploadButton({ | ||||
|           }} | ||||
|         /> | ||||
|       </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> | ||||
|           <AlertDialogTitle>Submit this frame to Frigate+?</AlertDialogTitle> | ||||
|         </AlertDialogHeader> | ||||
|  | ||||
| @ -30,6 +30,7 @@ type DynamicVideoPlayerProps = { | ||||
|   onClipEnded?: () => void; | ||||
|   setFullResolution: React.Dispatch<React.SetStateAction<VideoResolutionType>>; | ||||
|   toggleFullscreen: () => void; | ||||
|   containerRef?: React.MutableRefObject<HTMLDivElement | null>; | ||||
| }; | ||||
| 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<FrigateConfig>("config"); | ||||
| @ -208,6 +210,7 @@ export default function DynamicVideoPlayer({ | ||||
|         setFullResolution={setFullResolution} | ||||
|         onUploadFrame={onUploadFrameToPlus} | ||||
|         toggleFullscreen={toggleFullscreen} | ||||
|         containerRef={containerRef} | ||||
|       /> | ||||
|       <PreviewPlayer | ||||
|         className={cn( | ||||
|  | ||||
| @ -131,10 +131,18 @@ export default function PolygonDrawer({ | ||||
|         closed={isFinished} | ||||
|         fill={colorString(isActive || isHovered ? true : false)} | ||||
|         onMouseOver={() => | ||||
|           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 && ( | ||||
|  | ||||
| @ -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<typeof AlertDialogPrimitive.Overlay>, | ||||
| @ -17,31 +17,33 @@ const AlertDialogOverlay = React.forwardRef< | ||||
|   <AlertDialogPrimitive.Overlay | ||||
|     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", | ||||
|       className | ||||
|       className, | ||||
|     )} | ||||
|     {...props} | ||||
|     ref={ref} | ||||
|   /> | ||||
| )) | ||||
| AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName | ||||
| )); | ||||
| AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; | ||||
| 
 | ||||
| const AlertDialogContent = React.forwardRef< | ||||
|   React.ElementRef<typeof AlertDialogPrimitive.Content>, | ||||
|   React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|   <AlertDialogPortal> | ||||
|   React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content> & { | ||||
|     portalProps?: AlertDialogPrimitive.AlertDialogPortalProps; | ||||
|   } | ||||
| >(({ className, portalProps, ...props }, ref) => ( | ||||
|   <AlertDialogPortal {...portalProps}> | ||||
|     <AlertDialogOverlay /> | ||||
|     <AlertDialogPrimitive.Content | ||||
|       ref={ref} | ||||
|       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", | ||||
|         className | ||||
|         className, | ||||
|       )} | ||||
|       {...props} | ||||
|     /> | ||||
|   </AlertDialogPortal> | ||||
| )) | ||||
| AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName | ||||
| )); | ||||
| AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; | ||||
| 
 | ||||
| const AlertDialogHeader = ({ | ||||
|   className, | ||||
| @ -50,12 +52,12 @@ const AlertDialogHeader = ({ | ||||
|   <div | ||||
|     className={cn( | ||||
|       "flex flex-col space-y-2 text-center sm:text-left", | ||||
|       className | ||||
|       className, | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| ) | ||||
| AlertDialogHeader.displayName = "AlertDialogHeader" | ||||
| ); | ||||
| AlertDialogHeader.displayName = "AlertDialogHeader"; | ||||
| 
 | ||||
| const AlertDialogFooter = ({ | ||||
|   className, | ||||
| @ -64,12 +66,12 @@ const AlertDialogFooter = ({ | ||||
|   <div | ||||
|     className={cn( | ||||
|       "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", | ||||
|       className | ||||
|       className, | ||||
|     )} | ||||
|     {...props} | ||||
|   /> | ||||
| ) | ||||
| AlertDialogFooter.displayName = "AlertDialogFooter" | ||||
| ); | ||||
| AlertDialogFooter.displayName = "AlertDialogFooter"; | ||||
| 
 | ||||
| const AlertDialogTitle = React.forwardRef< | ||||
|   React.ElementRef<typeof AlertDialogPrimitive.Title>, | ||||
| @ -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<typeof AlertDialogPrimitive.Description>, | ||||
| @ -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<typeof AlertDialogPrimitive.Action>, | ||||
| @ -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<typeof AlertDialogPrimitive.Cancel>, | ||||
| @ -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, | ||||
| } | ||||
| }; | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
| @ -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} | ||||
|               /> | ||||
|             </div> | ||||
|             {isDesktop && ( | ||||
|  | ||||
| @ -399,7 +399,7 @@ export default function LiveCameraView({ | ||||
|                   onClick={() => setMic(!mic)} | ||||
|                 /> | ||||
|               )} | ||||
|               {supportsAudioOutput && ( | ||||
|               {supportsAudioOutput && preferredLiveMode != "jsmpeg" && ( | ||||
|                 <CameraFeatureToggle | ||||
|                   className="p-2 md:p-0" | ||||
|                   variant={fullscreen ? "overlay" : "primary"} | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user