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():
 | 
			
		||||
                command = OnvifCommandEnum.preset
 | 
			
		||||
                param = payload.lower()[payload.index("_") + 1 :]
 | 
			
		||||
            elif "move_relative" in payload.lower():
 | 
			
		||||
                command = OnvifCommandEnum.move_relative
 | 
			
		||||
                param = payload.lower()[payload.index("_") + 1 :]
 | 
			
		||||
            else:
 | 
			
		||||
                command = OnvifCommandEnum[payload.lower()]
 | 
			
		||||
                param = ""
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,7 @@ class OnvifCommandEnum(str, Enum):
 | 
			
		||||
    init = "init"
 | 
			
		||||
    move_down = "move_down"
 | 
			
		||||
    move_left = "move_left"
 | 
			
		||||
    move_relative = "move_relative"
 | 
			
		||||
    move_right = "move_right"
 | 
			
		||||
    move_up = "move_up"
 | 
			
		||||
    preset = "preset"
 | 
			
		||||
@ -536,6 +537,9 @@ class OnvifController:
 | 
			
		||||
            self._stop(camera_name)
 | 
			
		||||
        elif command == OnvifCommandEnum.preset:
 | 
			
		||||
            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 (
 | 
			
		||||
            command == OnvifCommandEnum.zoom_in or command == OnvifCommandEnum.zoom_out
 | 
			
		||||
        ):
 | 
			
		||||
 | 
			
		||||
@ -45,6 +45,7 @@ import {
 | 
			
		||||
  FaMicrophoneSlash,
 | 
			
		||||
} from "react-icons/fa";
 | 
			
		||||
import { GiSpeaker, GiSpeakerOff } from "react-icons/gi";
 | 
			
		||||
import { HiViewfinderCircle } from "react-icons/hi2";
 | 
			
		||||
import { IoMdArrowBack } from "react-icons/io";
 | 
			
		||||
import { LuEar, LuEarOff, LuVideo, LuVideoOff } from "react-icons/lu";
 | 
			
		||||
import {
 | 
			
		||||
@ -82,6 +83,45 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
 | 
			
		||||
  );
 | 
			
		||||
  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
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
@ -277,6 +317,8 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
 | 
			
		||||
        >
 | 
			
		||||
          <div
 | 
			
		||||
            className={`flex flex-col justify-center items-center ${growClassName}`}
 | 
			
		||||
            ref={clickOverlayRef}
 | 
			
		||||
            onClick={handleOverlayClick}
 | 
			
		||||
            style={{
 | 
			
		||||
              aspectRatio: aspectRatio,
 | 
			
		||||
            }}
 | 
			
		||||
@ -293,14 +335,28 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
 | 
			
		||||
              preferredLiveMode={preferredLiveMode}
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
          {camera.onvif.host != "" && <PtzControlPanel camera={camera.name} />}
 | 
			
		||||
          {camera.onvif.host != "" && (
 | 
			
		||||
            <PtzControlPanel
 | 
			
		||||
              camera={camera.name}
 | 
			
		||||
              clickOverlay={clickOverlay}
 | 
			
		||||
              setClickOverlay={setClickOverlay}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
        </TransformComponent>
 | 
			
		||||
      </div>
 | 
			
		||||
    </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 { send: sendPtz } = usePtzCommand(camera);
 | 
			
		||||
@ -442,6 +498,16 @@ function PtzControlPanel({ camera }: { camera: string }) {
 | 
			
		||||
          </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 && (
 | 
			
		||||
        <DropdownMenu>
 | 
			
		||||
          <DropdownMenuTrigger asChild>
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user