Skip to content

Add relative movement by clicking for supported ptzs #10629

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions frigate/comms/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,9 @@ def _on_ptz_command(self, camera_name: str, payload: str) -> None:
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 = ""
Expand Down
4 changes: 4 additions & 0 deletions frigate/ptz/onvif.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -536,6 +537,9 @@ def handle_command(
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
):
Expand Down
70 changes: 68 additions & 2 deletions web/src/views/live/LiveCameraView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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,
}}
Expand All @@ -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);
Expand Down Expand Up @@ -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>
Expand Down