Skip to content

Commit 3be9d3d

Browse files
committed
Add relative movement by clicking on camera image for supported ptzs
1 parent 4159334 commit 3be9d3d

File tree

3 files changed

+75
-2
lines changed

3 files changed

+75
-2
lines changed

frigate/comms/dispatcher.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,9 @@ def _on_ptz_command(self, camera_name: str, payload: str) -> None:
315315
if "preset" in payload.lower():
316316
command = OnvifCommandEnum.preset
317317
param = payload.lower()[payload.index("_") + 1 :]
318+
elif "move_relative" in payload.lower():
319+
command = OnvifCommandEnum.move_relative
320+
param = payload.lower()[payload.index("_") + 1 :]
318321
else:
319322
command = OnvifCommandEnum[payload.lower()]
320323
param = ""

frigate/ptz/onvif.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class OnvifCommandEnum(str, Enum):
2121
init = "init"
2222
move_down = "move_down"
2323
move_left = "move_left"
24+
move_relative = "move_relative"
2425
move_right = "move_right"
2526
move_up = "move_up"
2627
preset = "preset"
@@ -536,6 +537,9 @@ def handle_command(
536537
self._stop(camera_name)
537538
elif command == OnvifCommandEnum.preset:
538539
self._move_to_preset(camera_name, param)
540+
elif command == OnvifCommandEnum.move_relative:
541+
_, pan, tilt = param.split("_")
542+
self._move_relative(camera_name, float(pan), float(tilt), 0, 1)
539543
elif (
540544
command == OnvifCommandEnum.zoom_in or command == OnvifCommandEnum.zoom_out
541545
):

web/src/views/live/LiveCameraView.tsx

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
FaMicrophoneSlash,
4646
} from "react-icons/fa";
4747
import { GiSpeaker, GiSpeakerOff } from "react-icons/gi";
48+
import { HiViewfinderCircle } from "react-icons/hi2";
4849
import { IoMdArrowBack } from "react-icons/io";
4950
import { LuEar, LuEarOff, LuVideo, LuVideoOff } from "react-icons/lu";
5051
import {
@@ -82,6 +83,45 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
8283
);
8384
const { payload: audioState, send: sendAudio } = useAudioState(camera.name);
8485

86+
// click overlay for ptzs
87+
88+
const [clickOverlay, setClickOverlay] = useState(false);
89+
const clickOverlayRef = useRef<HTMLDivElement>(null);
90+
const { send: sendPtz } = usePtzCommand(camera.name);
91+
92+
const handleOverlayClick = useCallback(
93+
(
94+
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>,
95+
) => {
96+
if (!clickOverlay) {
97+
return;
98+
}
99+
100+
let clientX;
101+
let clientY;
102+
if (isMobile && e.nativeEvent instanceof TouchEvent) {
103+
clientX = e.nativeEvent.touches[0].clientX;
104+
clientY = e.nativeEvent.touches[0].clientY;
105+
} else if (e.nativeEvent instanceof MouseEvent) {
106+
clientX = e.nativeEvent.clientX;
107+
clientY = e.nativeEvent.clientY;
108+
}
109+
110+
if (clickOverlayRef.current && clientX && clientY) {
111+
const rect = clickOverlayRef.current.getBoundingClientRect();
112+
113+
const normalizedX = (clientX - rect.left) / rect.width;
114+
const normalizedY = (clientY - rect.top) / rect.height;
115+
116+
const pan = (normalizedX - 0.5) * 2;
117+
const tilt = (0.5 - normalizedY) * 2;
118+
119+
sendPtz(`move_relative_${pan}_${tilt}`);
120+
}
121+
},
122+
[clickOverlayRef, clickOverlay, sendPtz],
123+
);
124+
85125
// fullscreen state
86126

87127
useEffect(() => {
@@ -277,6 +317,8 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
277317
>
278318
<div
279319
className={`flex flex-col justify-center items-center ${growClassName}`}
320+
ref={clickOverlayRef}
321+
onClick={handleOverlayClick}
280322
style={{
281323
aspectRatio: aspectRatio,
282324
}}
@@ -293,14 +335,28 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
293335
preferredLiveMode={preferredLiveMode}
294336
/>
295337
</div>
296-
{camera.onvif.host != "" && <PtzControlPanel camera={camera.name} />}
338+
{camera.onvif.host != "" && (
339+
<PtzControlPanel
340+
camera={camera.name}
341+
clickOverlay={clickOverlay}
342+
setClickOverlay={setClickOverlay}
343+
/>
344+
)}
297345
</TransformComponent>
298346
</div>
299347
</TransformWrapper>
300348
);
301349
}
302350

303-
function PtzControlPanel({ camera }: { camera: string }) {
351+
function PtzControlPanel({
352+
camera,
353+
clickOverlay,
354+
setClickOverlay,
355+
}: {
356+
camera: string;
357+
clickOverlay: boolean;
358+
setClickOverlay: React.Dispatch<React.SetStateAction<boolean>>;
359+
}) {
304360
const { data: ptz } = useSWR<CameraPtzInfo>(`${camera}/ptz/info`);
305361

306362
const { send: sendPtz } = usePtzCommand(camera);
@@ -442,6 +498,16 @@ function PtzControlPanel({ camera }: { camera: string }) {
442498
</Button>
443499
</>
444500
)}
501+
{ptz?.features?.includes("pt-r-fov") && (
502+
<>
503+
<Button
504+
className={`${clickOverlay ? "text-selected" : "text-primary-foreground"}`}
505+
onClick={() => setClickOverlay(!clickOverlay)}
506+
>
507+
<HiViewfinderCircle />
508+
</Button>
509+
</>
510+
)}
445511
{(ptz?.presets?.length ?? 0) > 0 && (
446512
<DropdownMenu>
447513
<DropdownMenuTrigger asChild>

0 commit comments

Comments
 (0)