mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Add relative movement by clicking on camera image for supported ptzs (#10629)
This commit is contained in:
		
							parent
							
								
									63bf986e08
								
							
						
					
					
						commit
						3a9607e59b
					
				| @ -315,6 +315,9 @@ class Dispatcher: | |||||||
|             if "preset" in payload.lower(): |             if "preset" in payload.lower(): | ||||||
|                 command = OnvifCommandEnum.preset |                 command = OnvifCommandEnum.preset | ||||||
|                 param = payload.lower()[payload.index("_") + 1 :] |                 param = payload.lower()[payload.index("_") + 1 :] | ||||||
|  |             elif "move_relative" in payload.lower(): | ||||||
|  |                 command = OnvifCommandEnum.move_relative | ||||||
|  |                 param = payload.lower()[payload.index("_") + 1 :] | ||||||
|             else: |             else: | ||||||
|                 command = OnvifCommandEnum[payload.lower()] |                 command = OnvifCommandEnum[payload.lower()] | ||||||
|                 param = "" |                 param = "" | ||||||
|  | |||||||
| @ -21,6 +21,7 @@ class OnvifCommandEnum(str, Enum): | |||||||
|     init = "init" |     init = "init" | ||||||
|     move_down = "move_down" |     move_down = "move_down" | ||||||
|     move_left = "move_left" |     move_left = "move_left" | ||||||
|  |     move_relative = "move_relative" | ||||||
|     move_right = "move_right" |     move_right = "move_right" | ||||||
|     move_up = "move_up" |     move_up = "move_up" | ||||||
|     preset = "preset" |     preset = "preset" | ||||||
| @ -536,6 +537,9 @@ class OnvifController: | |||||||
|             self._stop(camera_name) |             self._stop(camera_name) | ||||||
|         elif command == OnvifCommandEnum.preset: |         elif command == OnvifCommandEnum.preset: | ||||||
|             self._move_to_preset(camera_name, param) |             self._move_to_preset(camera_name, param) | ||||||
|  |         elif command == OnvifCommandEnum.move_relative: | ||||||
|  |             _, pan, tilt = param.split("_") | ||||||
|  |             self._move_relative(camera_name, float(pan), float(tilt), 0, 1) | ||||||
|         elif ( |         elif ( | ||||||
|             command == OnvifCommandEnum.zoom_in or command == OnvifCommandEnum.zoom_out |             command == OnvifCommandEnum.zoom_in or command == OnvifCommandEnum.zoom_out | ||||||
|         ): |         ): | ||||||
|  | |||||||
| @ -45,6 +45,7 @@ import { | |||||||
|   FaMicrophoneSlash, |   FaMicrophoneSlash, | ||||||
| } from "react-icons/fa"; | } from "react-icons/fa"; | ||||||
| import { GiSpeaker, GiSpeakerOff } from "react-icons/gi"; | import { GiSpeaker, GiSpeakerOff } from "react-icons/gi"; | ||||||
|  | import { HiViewfinderCircle } from "react-icons/hi2"; | ||||||
| import { IoMdArrowBack } from "react-icons/io"; | import { IoMdArrowBack } from "react-icons/io"; | ||||||
| import { LuEar, LuEarOff, LuVideo, LuVideoOff } from "react-icons/lu"; | import { LuEar, LuEarOff, LuVideo, LuVideoOff } from "react-icons/lu"; | ||||||
| import { | import { | ||||||
| @ -82,6 +83,45 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) { | |||||||
|   ); |   ); | ||||||
|   const { payload: audioState, send: sendAudio } = useAudioState(camera.name); |   const { payload: audioState, send: sendAudio } = useAudioState(camera.name); | ||||||
| 
 | 
 | ||||||
|  |   // click overlay for ptzs
 | ||||||
|  | 
 | ||||||
|  |   const [clickOverlay, setClickOverlay] = useState(false); | ||||||
|  |   const clickOverlayRef = useRef<HTMLDivElement>(null); | ||||||
|  |   const { send: sendPtz } = usePtzCommand(camera.name); | ||||||
|  | 
 | ||||||
|  |   const handleOverlayClick = useCallback( | ||||||
|  |     ( | ||||||
|  |       e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>, | ||||||
|  |     ) => { | ||||||
|  |       if (!clickOverlay) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       let clientX; | ||||||
|  |       let clientY; | ||||||
|  |       if (isMobile && e.nativeEvent instanceof TouchEvent) { | ||||||
|  |         clientX = e.nativeEvent.touches[0].clientX; | ||||||
|  |         clientY = e.nativeEvent.touches[0].clientY; | ||||||
|  |       } else if (e.nativeEvent instanceof MouseEvent) { | ||||||
|  |         clientX = e.nativeEvent.clientX; | ||||||
|  |         clientY = e.nativeEvent.clientY; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (clickOverlayRef.current && clientX && clientY) { | ||||||
|  |         const rect = clickOverlayRef.current.getBoundingClientRect(); | ||||||
|  | 
 | ||||||
|  |         const normalizedX = (clientX - rect.left) / rect.width; | ||||||
|  |         const normalizedY = (clientY - rect.top) / rect.height; | ||||||
|  | 
 | ||||||
|  |         const pan = (normalizedX - 0.5) * 2; | ||||||
|  |         const tilt = (0.5 - normalizedY) * 2; | ||||||
|  | 
 | ||||||
|  |         sendPtz(`move_relative_${pan}_${tilt}`); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     [clickOverlayRef, clickOverlay, sendPtz], | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|   // fullscreen state
 |   // fullscreen state
 | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
| @ -277,6 +317,8 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) { | |||||||
|         > |         > | ||||||
|           <div |           <div | ||||||
|             className={`flex flex-col justify-center items-center ${growClassName}`} |             className={`flex flex-col justify-center items-center ${growClassName}`} | ||||||
|  |             ref={clickOverlayRef} | ||||||
|  |             onClick={handleOverlayClick} | ||||||
|             style={{ |             style={{ | ||||||
|               aspectRatio: aspectRatio, |               aspectRatio: aspectRatio, | ||||||
|             }} |             }} | ||||||
| @ -293,14 +335,28 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) { | |||||||
|               preferredLiveMode={preferredLiveMode} |               preferredLiveMode={preferredLiveMode} | ||||||
|             /> |             /> | ||||||
|           </div> |           </div> | ||||||
|           {camera.onvif.host != "" && <PtzControlPanel camera={camera.name} />} |           {camera.onvif.host != "" && ( | ||||||
|  |             <PtzControlPanel | ||||||
|  |               camera={camera.name} | ||||||
|  |               clickOverlay={clickOverlay} | ||||||
|  |               setClickOverlay={setClickOverlay} | ||||||
|  |             /> | ||||||
|  |           )} | ||||||
|         </TransformComponent> |         </TransformComponent> | ||||||
|       </div> |       </div> | ||||||
|     </TransformWrapper> |     </TransformWrapper> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function PtzControlPanel({ camera }: { camera: string }) { | function PtzControlPanel({ | ||||||
|  |   camera, | ||||||
|  |   clickOverlay, | ||||||
|  |   setClickOverlay, | ||||||
|  | }: { | ||||||
|  |   camera: string; | ||||||
|  |   clickOverlay: boolean; | ||||||
|  |   setClickOverlay: React.Dispatch<React.SetStateAction<boolean>>; | ||||||
|  | }) { | ||||||
|   const { data: ptz } = useSWR<CameraPtzInfo>(`${camera}/ptz/info`); |   const { data: ptz } = useSWR<CameraPtzInfo>(`${camera}/ptz/info`); | ||||||
| 
 | 
 | ||||||
|   const { send: sendPtz } = usePtzCommand(camera); |   const { send: sendPtz } = usePtzCommand(camera); | ||||||
| @ -442,6 +498,16 @@ function PtzControlPanel({ camera }: { camera: string }) { | |||||||
|           </Button> |           </Button> | ||||||
|         </> |         </> | ||||||
|       )} |       )} | ||||||
|  |       {ptz?.features?.includes("pt-r-fov") && ( | ||||||
|  |         <> | ||||||
|  |           <Button | ||||||
|  |             className={`${clickOverlay ? "text-selected" : "text-primary-foreground"}`} | ||||||
|  |             onClick={() => setClickOverlay(!clickOverlay)} | ||||||
|  |           > | ||||||
|  |             <HiViewfinderCircle /> | ||||||
|  |           </Button> | ||||||
|  |         </> | ||||||
|  |       )} | ||||||
|       {(ptz?.presets?.length ?? 0) > 0 && ( |       {(ptz?.presets?.length ?? 0) > 0 && ( | ||||||
|         <DropdownMenu> |         <DropdownMenu> | ||||||
|           <DropdownMenuTrigger asChild> |           <DropdownMenuTrigger asChild> | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user