Add relative movement by clicking on camera image for supported ptzs (#10629)

This commit is contained in:
Josh Hawkins 2024-03-23 11:53:33 -05:00 committed by GitHub
parent 63bf986e08
commit 3a9607e59b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 75 additions and 2 deletions

View File

@ -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 = ""

View File

@ -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
): ):

View File

@ -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>