ptz control updates

This commit is contained in:
legop3
2026-07-17 23:06:12 -04:00
parent 4e6e9e3021
commit afe8ffc62e
14 changed files with 522 additions and 328 deletions
@@ -148,13 +148,25 @@ export default function ControlPadPanel({ compact = false, disabled = false }) {
return () => {
clearRepeatTimer();
/*
Mobile controls can unmount when layouts change or the driver leaves the
control surface. Clear the shared flag so a stale mobile precision choice
cannot leave desktop/keyboard camera tilt in fine-step mode.
Mobile controls can unmount during an orientation/layout change while a
pointer is still captured by the disappearing element. Publish a neutral
vector directly during cleanup so neither rover drive nor PTZ pan/tilt
can retain the last cell merely because pointerup had nowhere to land.
*/
activeCellRef.current = null;
setDriveVector({ x: 0, y: 0, boost: false }, { source: SOURCE });
registerInputState(SOURCE, {
keys: [],
vector: { x: 0, y: 0, boost: false },
activeCell: 'stop',
speedMode: speedModeRef.current,
lastEvent: 'unmount',
});
// Clear the shared flag too, so a stale mobile precision choice cannot
// leave desktop/keyboard camera tilt in fine-step mode.
setCameraPrecisionMode(false);
};
}, [clearRepeatTimer, setCameraPrecisionMode]);
}, [clearRepeatTimer, registerInputState, setCameraPrecisionMode, setDriveVector]);
useEffect(() => {
if (!disabled) return;
+18 -38
View File
@@ -188,46 +188,30 @@ function PtzLightingControls({ ptz, disabled = false }) {
}
function PtzMobileZoomButtons({ disabled = false }) {
const { nudgeServo } = useControlActions();
const repeatTimerRef = useRef(null);
const { setCameraAxisIntent } = useControlActions();
const stopZoom = useCallback(() => {
/*
Mobile zoom is intentionally routed through the normal camera-up/down
control action instead of emitting PTZ socket commands directly. That
keeps the zoom buttons on the same path as keyboard/gamepad camera tilt,
and the PTZ adapter remains the one place that translates "camera nudge"
into Reolink zoom pulses.
Zero only releases the zoom axis. The PTZ adapter combines it with any
pan/tilt direction still held on the movement pad, so lifting one finger
cannot erase the other finger's intent.
*/
if (repeatTimerRef.current) {
clearInterval(repeatTimerRef.current);
repeatTimerRef.current = null;
}
/*
Zero is a zoom-only release signal in the PTZ adapter. Using the global
stop action here previously erased a simultaneously held pan/tilt vector,
making mixed touch controls unexpectedly stop the camera.
*/
nudgeServo(0);
}, [nudgeServo]);
setCameraAxisIntent(0);
}, [setCameraAxisIntent]);
const startZoom = useCallback(
(direction) => (event) => {
/*
Send an immediate nudge and then repeat while held. The adapter turns
each nudge into a short zoom pulse, so repeating the standard action is
the simplest way to get continuous hold-to-zoom without adding another
PTZ-specific command loop.
Publish held state once. The adapter owns the single motion heartbeat,
so this button no longer creates a second interval whose queued callback
could run after pointerup and restart zoom.
*/
event.preventDefault();
if (disabled) return;
stopZoom();
nudgeServo(direction);
repeatTimerRef.current = setInterval(() => {
nudgeServo(direction);
}, 120);
event.currentTarget.setPointerCapture?.(event.pointerId);
setCameraAxisIntent(direction);
},
[disabled, nudgeServo, stopZoom],
[disabled, setCameraAxisIntent],
);
const stopFromPointer = useCallback(
(event) => {
@@ -242,16 +226,12 @@ function PtzMobileZoomButtons({ disabled = false }) {
() => () => {
/*
A touch surface can unmount during orientation changes or fullscreen
close while a pointer is still down. Clear the repeat timer here so a
held zoom button cannot keep firing camera-up/down actions after the
mobile controls have disappeared.
close while a pointer is still down. Explicitly clear zoom here because
an unmounted DOM node cannot deliver its pointerup/pointercancel event.
*/
if (repeatTimerRef.current) {
clearInterval(repeatTimerRef.current);
repeatTimerRef.current = null;
}
setCameraAxisIntent(0);
},
[],
[setCameraAxisIntent],
);
return (
@@ -263,7 +243,7 @@ function PtzMobileZoomButtons({ disabled = false }) {
onPointerDown={startZoom(-1)}
onPointerUp={stopFromPointer}
onPointerCancel={stopFromPointer}
onPointerLeave={stopFromPointer}
onLostPointerCapture={stopFromPointer}
onContextMenu={(event) => event.preventDefault()}
>
Zoom out
@@ -275,7 +255,7 @@ function PtzMobileZoomButtons({ disabled = false }) {
onPointerDown={startZoom(1)}
onPointerUp={stopFromPointer}
onPointerCancel={stopFromPointer}
onPointerLeave={stopFromPointer}
onLostPointerCapture={stopFromPointer}
onContextMenu={(event) => event.preventDefault()}
>
Zoom in
+24 -13
View File
@@ -1,7 +1,7 @@
// Vip PTZ Camera Card
// Purpose: Provides the verified-user entry point and fullscreen controller for the single Reolink PTZ camera.
// Scope: Owns PTZ UI state only; server-side PTZ ownership, rover handoff, and command authorization remain authoritative.
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { createPortal } from 'react-dom';
import ChatPanel from '../ChatPanel/index.jsx';
import CardFrame from '../CardFrame/index.jsx';
@@ -11,13 +11,12 @@ import PtzLiveVideo from '../PtzLiveVideo/index.jsx';
import ReplaySourcesPanel from '../ReplaySourcesPanel/index.jsx';
import KeyPill from './VipAudioUploadCard/KeyPill.jsx';
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
import { useControlSelector } from '../../controls/index.js';
import { useControlActions, useControlSelector } from '../../controls/index.js';
import { formatKeyLabel } from '../../controls/keymapUtils.js';
import { usePtzCameraSnapshots } from '../../hooks/usePtzCameraSnapshot.js';
import { isFeatureEnabled } from '../../lib/features.js';
const PTZ_CAMERA_ID = 'ptz-camera';
const PTZ_ZOOM_SPEED = 0.55;
function formatRemaining(deadline) {
const remaining = Math.max(0, Math.ceil((Number(deadline || 0) - Date.now()) / 1000));
@@ -218,22 +217,25 @@ function PtzLightingControls({ ptz, disabled = false }) {
}
function PtzMobileZoomButtons({ disabled = false }) {
const { ptzMove, ptzStop } = useSessionActions();
const { setCameraAxisIntent } = useControlActions();
const stopZoom = useCallback(() => {
ptzStop().catch(() => {});
}, [ptzStop]);
// This is a zoom-only release; the shared adapter retains any simultaneous
// pan/tilt intent from the movement pad in its next combined motion state.
setCameraAxisIntent(0);
}, [setCameraAxisIntent]);
const startZoom = useCallback(
(direction) => (event) => {
/*
Mobile needs explicit zoom targets because the regular mobile drive pad
is already used for pan/tilt. Desktop does not render these buttons; it
uses the mapped camera up/down controls shown in the reference panel.
The adapter owns renewal for held PTZ state. Publishing the direction
once avoids a component-local repeat timer and keeps this legacy card on
the exact same motion path as the dedicated PTZ route.
*/
event.preventDefault();
if (disabled) return;
ptzMove({ pan: 0, tilt: 0, zoom: direction * PTZ_ZOOM_SPEED }).catch(() => {});
event.currentTarget.setPointerCapture?.(event.pointerId);
setCameraAxisIntent(direction);
},
[disabled, ptzMove],
[disabled, setCameraAxisIntent],
);
const stopFromPointer = useCallback(
(event) => {
@@ -244,6 +246,15 @@ function PtzMobileZoomButtons({ disabled = false }) {
[disabled, stopZoom],
);
useEffect(
() => () => {
// Orientation changes can unmount the button before pointerup. Clear the
// held zoom state explicitly instead of waiting for the server watchdog.
setCameraAxisIntent(0);
},
[setCameraAxisIntent],
);
return (
<div className="mobile-touch-control grid grid-cols-2 gap-0.5 text-sm">
<button
@@ -253,7 +264,7 @@ function PtzMobileZoomButtons({ disabled = false }) {
onPointerDown={startZoom(-1)}
onPointerUp={stopFromPointer}
onPointerCancel={stopFromPointer}
onPointerLeave={stopFromPointer}
onLostPointerCapture={stopFromPointer}
onContextMenu={(event) => event.preventDefault()}
>
Zoom out
@@ -265,7 +276,7 @@ function PtzMobileZoomButtons({ disabled = false }) {
onPointerDown={startZoom(1)}
onPointerUp={stopFromPointer}
onPointerCancel={stopFromPointer}
onPointerLeave={stopFromPointer}
onLostPointerCapture={stopFromPointer}
onContextMenu={(event) => event.preventDefault()}
>
Zoom in