This commit is contained in:
legop3
2026-07-17 23:25:03 -04:00
parent fe33f27bd0
commit 512adfc1e0
+56 -19
View File
@@ -106,6 +106,7 @@ let motionWatchdogTimer = null;
let panTiltRenewTimer = null; let panTiltRenewTimer = null;
let zoomRepeatTimer = null; let zoomRepeatTimer = null;
let desiredMotion = STOP_MOTION; let desiredMotion = STOP_MOTION;
let pendingFullStopCommand = false;
let pendingPanTiltCommand = false; let pendingPanTiltCommand = false;
let pendingZoomCommand = false; let pendingZoomCommand = false;
let motionCommandPromise = null; let motionCommandPromise = null;
@@ -1143,10 +1144,14 @@ function panTiltMatches(left = STOP_MOTION, right = STOP_MOTION) {
return left.pan === right.pan && left.tilt === right.tilt; return left.pan === right.pan && left.tilt === right.tilt;
} }
function requestMotionCommands({ panTilt = false, zoom = false } = {}) { function requestMotionCommands({ fullStop = false, panTilt = false, zoom = false } = {}) {
pendingFullStopCommand = pendingFullStopCommand || fullStop;
pendingPanTiltCommand = pendingPanTiltCommand || panTilt; pendingPanTiltCommand = pendingPanTiltCommand || panTilt;
pendingZoomCommand = pendingZoomCommand || zoom; pendingZoomCommand = pendingZoomCommand || zoom;
if (motionCommandPromise || (!pendingPanTiltCommand && !pendingZoomCommand)) { if (
motionCommandPromise ||
(!pendingFullStopCommand && !pendingPanTiltCommand && !pendingZoomCommand)
) {
return motionCommandPromise || Promise.resolve(); return motionCommandPromise || Promise.resolve();
} }
@@ -1157,7 +1162,27 @@ function requestMotionCommands({ panTilt = false, zoom = false } = {}) {
building a delayed command backlog that would continue after release. building a delayed command backlog that would continue after release.
*/ */
motionCommandPromise = (async () => { motionCommandPromise = (async () => {
while (pendingPanTiltCommand || pendingZoomCommand) { while (pendingFullStopCommand || pendingPanTiltCommand || pendingZoomCommand) {
const sendFullStop = pendingFullStopCommand;
pendingFullStopCommand = false;
if (sendFullStop) {
try {
await initialize();
/*
Reserve ONVIF Stop for real all-axis safety events. The TrackMix
appears to treat even an axis-filtered Stop as global, so ordinary
user releases below use zero velocity instead.
*/
await callOnvif('stop', {
profileToken: state.profileToken,
panTilt: true,
zoom: true,
});
} catch (err) {
logger.warn('PTZ full stop command failed', { error: getErrorMessage(err) });
}
}
const sendPanTilt = pendingPanTiltCommand; const sendPanTilt = pendingPanTiltCommand;
pendingPanTiltCommand = false; pendingPanTiltCommand = false;
if (sendPanTilt) { if (sendPanTilt) {
@@ -1166,12 +1191,17 @@ function requestMotionCommands({ panTilt = false, zoom = false } = {}) {
try { try {
await initialize(); await initialize();
if (!pan && !tilt) { if (!pan && !tilt) {
// Stop only pan/tilt. Omitting Zoom is essential because a zoom /*
// finger/key may still be held while direction returns to neutral. Zero pan/tilt velocity stops only that axis under ContinuousMove.
await callOnvif('stop', { Do not use ONVIF Stop here: this TrackMix ignores the requested
axis filter and can also stop zoom that is still being held.
*/
await callOnvif('continuousMove', {
profileToken: state.profileToken, profileToken: state.profileToken,
panTilt: true, x: 0,
zoom: false, y: 0,
onlySendPanTilt: true,
timeout: PAN_TILT_TIMEOUT_MS,
}); });
} else { } else {
await callOnvif('continuousMove', { await callOnvif('continuousMove', {
@@ -1194,12 +1224,16 @@ function requestMotionCommands({ panTilt = false, zoom = false } = {}) {
try { try {
await initialize(); await initialize();
if (!zoom) { if (!zoom) {
// Stop only zoom so repeated zoom release cannot interrupt a smooth /*
// pan/tilt ContinuousMove that is already running on the camera. Zero zoom velocity is the axis-local release. The camera's Stop
await callOnvif('stop', { implementation is over-broad and halts pan/tilt even when the
request includes only Zoom=true.
*/
await callOnvif('continuousMove', {
profileToken: state.profileToken, profileToken: state.profileToken,
panTilt: false, zoom: 0,
zoom: true, onlySendZoom: true,
timeout: ZOOM_PULSE_TIMEOUT_MS,
}); });
} else { } else {
/* /*
@@ -1223,7 +1257,9 @@ function requestMotionCommands({ panTilt = false, zoom = false } = {}) {
})().finally(() => { })().finally(() => {
motionCommandPromise = null; motionCommandPromise = null;
// Cover an intent arriving between the loop check and promise cleanup. // Cover an intent arriving between the loop check and promise cleanup.
if (pendingPanTiltCommand || pendingZoomCommand) requestMotionCommands(); if (pendingFullStopCommand || pendingPanTiltCommand || pendingZoomCommand) {
requestMotionCommands();
}
}); });
return motionCommandPromise; return motionCommandPromise;
@@ -1295,14 +1331,15 @@ function acceptMotionIntent(socket, payload = {}) {
function forceMotionStop(reason = 'safety-stop') { function forceMotionStop(reason = 'safety-stop') {
/* /*
Lifecycle stops force both axis-specific Stop commands even when local state Lifecycle stops force a real all-axis ONVIF Stop even when local state is
is already zero. The browser may have lost its final packet, or the camera already zero. The browser may have lost its final packet, or the camera may
may have accepted a command whose response has not returned, so deduplicating have accepted a command whose response has not returned, so deduplicating a
a safety stop would trust precisely the state we are trying to recover from. safety stop would trust precisely the state we are trying to recover from.
*/ */
clearPanTiltRenewal(); clearPanTiltRenewal();
clearZoomRepeat(); clearZoomRepeat();
queueMotionIntent(STOP_MOTION, reason, { forceAxes: true }); queueMotionIntent(STOP_MOTION, reason);
requestMotionCommands({ fullStop: true });
return motionCommandPromise || Promise.resolve(); return motionCommandPromise || Promise.resolve();
} }