diff --git a/web/src/api/ws.jsx b/web/src/api/ws.jsx
index 734200215..8995a065b 100644
--- a/web/src/api/ws.jsx
+++ b/web/src/api/ws.jsx
@@ -120,6 +120,15 @@ export function useSnapshotsState(camera) {
return { payload, send, connected };
}
+export function usePtzCommand(camera) {
+ const {
+ value: { payload },
+ send,
+ connected,
+ } = useWs(`${camera}/ptz`, `${camera}/ptz`);
+ return { payload, send, connected };
+}
+
export function useRestart() {
const {
value: { payload },
diff --git a/web/src/components/CameraControlPanel.jsx b/web/src/components/CameraControlPanel.jsx
new file mode 100644
index 000000000..6e8571c37
--- /dev/null
+++ b/web/src/components/CameraControlPanel.jsx
@@ -0,0 +1,56 @@
+import { h } from 'preact';
+import { useState } from 'preact/hooks';
+import useSWR from 'swr';
+import { usePtzCommand } from '../api/ws';
+import ActivityIndicator from './ActivityIndicator';
+import Button from './Button';
+
+export default function CameraControlPanel({ camera = '' }) {
+ const { data: ptz } = useSWR(`${camera}/ptz/info`);
+ const [currentPreset, setCurrentPreset] = useState('');
+
+ const { payload: _, send: sendPtz } = usePtzCommand(camera);
+
+ const onSetPreview = async (e) => {
+ e.stopPropagation();
+
+ if (currentPreset == 'none') {
+ return;
+ }
+
+ sendPtz(`preset-${currentPreset}`);
+ setCurrentPreset('');
+ };
+
+ if (!ptz) {
+ return