mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Make jsmpeg players fully responsive (#11567)
* make jsmpeg canvas responsive * make birdseye responsive too
This commit is contained in:
		
							parent
							
								
									5900a2a4ba
								
							
						
					
					
						commit
						c1330704cf
					
				| @ -5,12 +5,14 @@ import JSMpegPlayer from "./JSMpegPlayer"; | ||||
| import MSEPlayer from "./MsePlayer"; | ||||
| import { LivePlayerMode } from "@/types/live"; | ||||
| import { cn } from "@/lib/utils"; | ||||
| import React from "react"; | ||||
| 
 | ||||
| type LivePlayerProps = { | ||||
|   className?: string; | ||||
|   birdseyeConfig: BirdseyeConfig; | ||||
|   liveMode: LivePlayerMode; | ||||
|   onClick?: () => void; | ||||
|   containerRef?: React.MutableRefObject<HTMLDivElement | null>; | ||||
| }; | ||||
| 
 | ||||
| export default function BirdseyeLivePlayer({ | ||||
| @ -18,6 +20,7 @@ export default function BirdseyeLivePlayer({ | ||||
|   birdseyeConfig, | ||||
|   liveMode, | ||||
|   onClick, | ||||
|   containerRef, | ||||
| }: LivePlayerProps) { | ||||
|   let player; | ||||
|   if (liveMode == "webrtc") { | ||||
| @ -50,6 +53,7 @@ export default function BirdseyeLivePlayer({ | ||||
|         camera="birdseye" | ||||
|         width={birdseyeConfig.width} | ||||
|         height={birdseyeConfig.height} | ||||
|         containerRef={containerRef} | ||||
|       /> | ||||
|     ); | ||||
|   } else { | ||||
|  | ||||
| @ -1,13 +1,15 @@ | ||||
| import { baseUrl } from "@/api/baseUrl"; | ||||
| import { useResizeObserver } from "@/hooks/resize-observer"; | ||||
| // @ts-expect-error we know this doesn't have types
 | ||||
| import JSMpeg from "@cycjimmy/jsmpeg-player"; | ||||
| import { useEffect, useRef } from "react"; | ||||
| import React, { useEffect, useMemo, useRef } from "react"; | ||||
| 
 | ||||
| type JSMpegPlayerProps = { | ||||
|   className?: string; | ||||
|   camera: string; | ||||
|   width: number; | ||||
|   height: number; | ||||
|   containerRef?: React.MutableRefObject<HTMLDivElement | null>; | ||||
| }; | ||||
| 
 | ||||
| export default function JSMpegPlayer({ | ||||
| @ -15,10 +17,57 @@ export default function JSMpegPlayer({ | ||||
|   width, | ||||
|   height, | ||||
|   className, | ||||
|   containerRef, | ||||
| }: JSMpegPlayerProps) { | ||||
|   const url = `${baseUrl.replace(/^http/, "ws")}live/jsmpeg/${camera}`; | ||||
|   const playerRef = useRef<HTMLDivElement | null>(null); | ||||
|   const containerRef = useRef<HTMLDivElement | null>(null); | ||||
|   const internalContainerRef = useRef<HTMLDivElement | null>(null); | ||||
| 
 | ||||
|   const [{ width: containerWidth, height: containerHeight }] = | ||||
|     useResizeObserver(containerRef ?? internalContainerRef); | ||||
| 
 | ||||
|   const stretch = true; | ||||
|   const aspectRatio = width / height; | ||||
| 
 | ||||
|   const fitAspect = useMemo( | ||||
|     () => containerWidth / containerHeight, | ||||
|     [containerWidth, containerHeight], | ||||
|   ); | ||||
| 
 | ||||
|   const scaledHeight = useMemo(() => { | ||||
|     if (containerRef?.current && width && height) { | ||||
|       const scaledHeight = | ||||
|         aspectRatio < (fitAspect ?? 0) | ||||
|           ? Math.floor( | ||||
|               Math.min(containerHeight, containerRef.current?.clientHeight), | ||||
|             ) | ||||
|           : aspectRatio > fitAspect | ||||
|             ? Math.floor(containerWidth / aspectRatio) | ||||
|             : Math.floor(containerWidth / aspectRatio) / 1.5; | ||||
|       const finalHeight = stretch | ||||
|         ? scaledHeight | ||||
|         : Math.min(scaledHeight, height); | ||||
| 
 | ||||
|       if (finalHeight > 0) { | ||||
|         return finalHeight; | ||||
|       } | ||||
|     } | ||||
|   }, [ | ||||
|     aspectRatio, | ||||
|     containerWidth, | ||||
|     containerHeight, | ||||
|     fitAspect, | ||||
|     height, | ||||
|     width, | ||||
|     stretch, | ||||
|     containerRef, | ||||
|   ]); | ||||
| 
 | ||||
|   const scaledWidth = useMemo(() => { | ||||
|     if (aspectRatio && scaledHeight) { | ||||
|       return Math.ceil(scaledHeight * aspectRatio); | ||||
|     } | ||||
|   }, [scaledHeight, aspectRatio]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (!playerRef.current) { | ||||
| @ -28,7 +77,7 @@ export default function JSMpegPlayer({ | ||||
|     const video = new JSMpeg.VideoElement( | ||||
|       playerRef.current, | ||||
|       url, | ||||
|       {}, | ||||
|       { canvas: "#video-canvas" }, | ||||
|       { protocols: [], audio: false, videoBufferSize: 1024 * 1024 * 4 }, | ||||
|     ); | ||||
| 
 | ||||
| @ -44,12 +93,16 @@ export default function JSMpegPlayer({ | ||||
|   }, [url]); | ||||
| 
 | ||||
|   return ( | ||||
|     <div className={className} ref={containerRef}> | ||||
|       <div | ||||
|         ref={playerRef} | ||||
|         className="jsmpeg h-full" | ||||
|         style={{ aspectRatio: width / height }} | ||||
|       /> | ||||
|     <div className={className} ref={internalContainerRef}> | ||||
|       <div ref={playerRef} className="jsmpeg"> | ||||
|         <canvas | ||||
|           id="video-canvas" | ||||
|           style={{ | ||||
|             width: scaledWidth ?? width, | ||||
|             height: scaledHeight ?? height, | ||||
|           }} | ||||
|         ></canvas> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| @ -28,6 +28,7 @@ type LivePlayerProps = { | ||||
|   pip?: boolean; | ||||
|   onClick?: () => void; | ||||
|   setFullResolution?: React.Dispatch<React.SetStateAction<VideoResolutionType>>; | ||||
|   containerRef?: React.MutableRefObject<HTMLDivElement | null>; | ||||
| }; | ||||
| 
 | ||||
| export default function LivePlayer({ | ||||
| @ -43,6 +44,7 @@ export default function LivePlayer({ | ||||
|   pip, | ||||
|   onClick, | ||||
|   setFullResolution, | ||||
|   containerRef, | ||||
| }: LivePlayerProps) { | ||||
|   // camera activity
 | ||||
| 
 | ||||
| @ -138,10 +140,11 @@ export default function LivePlayer({ | ||||
|     if (cameraActive || !showStillWithoutActivity) { | ||||
|       player = ( | ||||
|         <JSMpegPlayer | ||||
|           className="flex size-full justify-center overflow-hidden rounded-lg md:rounded-2xl" | ||||
|           className="flex justify-center overflow-hidden rounded-lg md:rounded-2xl" | ||||
|           camera={cameraConfig.live.stream_name} | ||||
|           width={cameraConfig.detect.width} | ||||
|           height={cameraConfig.detect.height} | ||||
|           containerRef={containerRef} | ||||
|         /> | ||||
|       ); | ||||
|     } else { | ||||
| @ -156,9 +159,7 @@ export default function LivePlayer({ | ||||
|       ref={cameraRef} | ||||
|       data-camera={cameraConfig.name} | ||||
|       className={cn( | ||||
|         "relative flex justify-center", | ||||
|         liveMode === "jsmpeg" ? "size-full" : "w-full", | ||||
|         "cursor-pointer outline", | ||||
|         "relative flex w-full cursor-pointer justify-center outline", | ||||
|         activeTracking | ||||
|           ? "outline-3 rounded-lg shadow-severity_alert outline-severity_alert md:rounded-2xl" | ||||
|           : "outline-0 outline-background", | ||||
|  | ||||
| @ -23,6 +23,7 @@ export default function LiveBirdseyeView() { | ||||
|   const navigate = useNavigate(); | ||||
|   const { isPortrait } = useMobileOrientation(); | ||||
|   const mainRef = useRef<HTMLDivElement | null>(null); | ||||
|   const containerRef = useRef<HTMLDivElement>(null); | ||||
|   const [{ width: windowWidth, height: windowHeight }] = | ||||
|     useResizeObserver(window); | ||||
| 
 | ||||
| @ -75,7 +76,7 @@ export default function LiveBirdseyeView() { | ||||
|         return "absolute inset-y-2 left-[50%] -translate-x-[50%]"; | ||||
|       } | ||||
|     } else { | ||||
|       return "absolute top-2 bottom-2 left-[50%] -translate-x-[50%]"; | ||||
|       return "absolute top-0 bottom-0 left-[50%] -translate-x-[50%]"; | ||||
|     } | ||||
|   }, [cameraAspectRatio, fullscreen, isPortrait]); | ||||
| 
 | ||||
| @ -159,30 +160,33 @@ export default function LiveBirdseyeView() { | ||||
|             </div> | ||||
|           </TooltipProvider> | ||||
|         </div> | ||||
|         <TransformComponent | ||||
|           wrapperStyle={{ | ||||
|             width: "100%", | ||||
|             height: "100%", | ||||
|           }} | ||||
|           contentStyle={{ | ||||
|             position: "relative", | ||||
|             width: "100%", | ||||
|             height: "100%", | ||||
|           }} | ||||
|         > | ||||
|           <div | ||||
|             className={growClassName} | ||||
|             style={{ | ||||
|               aspectRatio: aspectRatio, | ||||
|         <div id="player-container" className="size-full" ref={containerRef}> | ||||
|           <TransformComponent | ||||
|             wrapperStyle={{ | ||||
|               width: "100%", | ||||
|               height: "100%", | ||||
|             }} | ||||
|             contentStyle={{ | ||||
|               position: "relative", | ||||
|               width: "100%", | ||||
|               height: "100%", | ||||
|             }} | ||||
|           > | ||||
|             <BirdseyeLivePlayer | ||||
|               className="h-full" | ||||
|               birdseyeConfig={config.birdseye} | ||||
|               liveMode={preferredLiveMode} | ||||
|             /> | ||||
|           </div> | ||||
|         </TransformComponent> | ||||
|             <div | ||||
|               className={growClassName} | ||||
|               style={{ | ||||
|                 aspectRatio: aspectRatio, | ||||
|               }} | ||||
|             > | ||||
|               <BirdseyeLivePlayer | ||||
|                 className={`${fullscreen ? "*:rounded-none" : ""}`} | ||||
|                 birdseyeConfig={config.birdseye} | ||||
|                 liveMode={preferredLiveMode} | ||||
|                 containerRef={containerRef} | ||||
|               /> | ||||
|             </div> | ||||
|           </TransformComponent> | ||||
|         </div> | ||||
|       </div> | ||||
|     </TransformWrapper> | ||||
|   ); | ||||
|  | ||||
| @ -85,6 +85,7 @@ export default function LiveCameraView({ | ||||
|   const navigate = useNavigate(); | ||||
|   const { isPortrait } = useMobileOrientation(); | ||||
|   const mainRef = useRef<HTMLDivElement | null>(null); | ||||
|   const containerRef = useRef<HTMLDivElement>(null); | ||||
|   const [{ width: windowWidth, height: windowHeight }] = | ||||
|     useResizeObserver(window); | ||||
| 
 | ||||
| @ -389,48 +390,51 @@ export default function LiveCameraView({ | ||||
|             </div> | ||||
|           </TooltipProvider> | ||||
|         </div> | ||||
|         <TransformComponent | ||||
|           wrapperStyle={{ | ||||
|             width: "100%", | ||||
|             height: "100%", | ||||
|           }} | ||||
|           contentStyle={{ | ||||
|             position: "relative", | ||||
|             width: "100%", | ||||
|             height: "100%", | ||||
|             padding: "8px", | ||||
|           }} | ||||
|         > | ||||
|           <div | ||||
|             className={`flex flex-col items-center justify-center ${growClassName}`} | ||||
|             ref={clickOverlayRef} | ||||
|             onClick={handleOverlayClick} | ||||
|             style={{ | ||||
|               aspectRatio: aspectRatio, | ||||
|         <div id="player-container" className="size-full" ref={containerRef}> | ||||
|           <TransformComponent | ||||
|             wrapperStyle={{ | ||||
|               width: "100%", | ||||
|               height: "100%", | ||||
|             }} | ||||
|             contentStyle={{ | ||||
|               position: "relative", | ||||
|               width: "100%", | ||||
|               height: "100%", | ||||
|               padding: "8px", | ||||
|             }} | ||||
|           > | ||||
|             <LivePlayer | ||||
|               key={camera.name} | ||||
|               className={`m-1 ${fullscreen ? "*:rounded-none" : ""}`} | ||||
|               windowVisible | ||||
|               showStillWithoutActivity={false} | ||||
|               cameraConfig={camera} | ||||
|               playAudio={audio} | ||||
|               micEnabled={mic} | ||||
|               iOSCompatFullScreen={isIOS} | ||||
|               preferredLiveMode={preferredLiveMode} | ||||
|               pip={pip} | ||||
|               setFullResolution={setFullResolution} | ||||
|             /> | ||||
|           </div> | ||||
|           {camera.onvif.host != "" && ( | ||||
|             <PtzControlPanel | ||||
|               camera={camera.name} | ||||
|               clickOverlay={clickOverlay} | ||||
|               setClickOverlay={setClickOverlay} | ||||
|             /> | ||||
|           )} | ||||
|         </TransformComponent> | ||||
|             <div | ||||
|               className={`flex flex-col items-center justify-center ${growClassName}`} | ||||
|               ref={clickOverlayRef} | ||||
|               onClick={handleOverlayClick} | ||||
|               style={{ | ||||
|                 aspectRatio: aspectRatio, | ||||
|               }} | ||||
|             > | ||||
|               <LivePlayer | ||||
|                 key={camera.name} | ||||
|                 className={`${fullscreen ? "*:rounded-none" : ""}`} | ||||
|                 windowVisible | ||||
|                 showStillWithoutActivity={false} | ||||
|                 cameraConfig={camera} | ||||
|                 playAudio={audio} | ||||
|                 micEnabled={mic} | ||||
|                 iOSCompatFullScreen={isIOS} | ||||
|                 preferredLiveMode={preferredLiveMode} | ||||
|                 pip={pip} | ||||
|                 setFullResolution={setFullResolution} | ||||
|                 containerRef={containerRef} | ||||
|               /> | ||||
|             </div> | ||||
|             {camera.onvif.host != "" && ( | ||||
|               <PtzControlPanel | ||||
|                 camera={camera.name} | ||||
|                 clickOverlay={clickOverlay} | ||||
|                 setClickOverlay={setClickOverlay} | ||||
|               /> | ||||
|             )} | ||||
|           </TransformComponent> | ||||
|         </div> | ||||
|       </div> | ||||
|     </TransformWrapper> | ||||
|   ); | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user