mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-05 22:57:40 +03:00
add profile support and decouple relative move from autotracking
This commit is contained in:
parent
5d67ba76fd
commit
380780b4fa
@ -117,6 +117,11 @@ class OnvifConfig(FrigateBaseModel):
|
|||||||
title="Disable TLS verify",
|
title="Disable TLS verify",
|
||||||
description="Skip TLS verification and disable digest auth for ONVIF (unsafe; use in safe networks only).",
|
description="Skip TLS verification and disable digest auth for ONVIF (unsafe; use in safe networks only).",
|
||||||
)
|
)
|
||||||
|
profile: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
title="ONVIF profile",
|
||||||
|
description="Specific ONVIF media profile to use for PTZ control, matched by token or name. If not set, the first profile with valid PTZ configuration is selected automatically.",
|
||||||
|
)
|
||||||
autotracking: PtzAutotrackConfig = Field(
|
autotracking: PtzAutotrackConfig = Field(
|
||||||
default_factory=PtzAutotrackConfig,
|
default_factory=PtzAutotrackConfig,
|
||||||
title="Autotracking",
|
title="Autotracking",
|
||||||
|
|||||||
@ -161,22 +161,56 @@ class OnvifController:
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# build list of valid PTZ profiles
|
||||||
|
valid_profiles = [
|
||||||
|
p
|
||||||
|
for p in profiles
|
||||||
|
if p.VideoEncoderConfiguration
|
||||||
|
and p.PTZConfiguration
|
||||||
|
and (
|
||||||
|
p.PTZConfiguration.DefaultContinuousPanTiltVelocitySpace is not None
|
||||||
|
or p.PTZConfiguration.DefaultContinuousZoomVelocitySpace is not None
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
# log available profiles with names and tokens for debugging
|
||||||
|
for p in valid_profiles:
|
||||||
|
logger.debug(
|
||||||
|
"Onvif profile for %s: name='%s', token='%s'",
|
||||||
|
camera_name,
|
||||||
|
getattr(p, "Name", None),
|
||||||
|
p.token,
|
||||||
|
)
|
||||||
|
|
||||||
|
configured_profile = self.config.cameras[camera_name].onvif.profile
|
||||||
profile = None
|
profile = None
|
||||||
for _, onvif_profile in enumerate(profiles):
|
|
||||||
if (
|
if configured_profile is not None:
|
||||||
onvif_profile.VideoEncoderConfiguration
|
# match by exact token first, then by name
|
||||||
and onvif_profile.PTZConfiguration
|
for p in valid_profiles:
|
||||||
and (
|
if p.token == configured_profile:
|
||||||
onvif_profile.PTZConfiguration.DefaultContinuousPanTiltVelocitySpace
|
profile = p
|
||||||
is not None
|
break
|
||||||
or onvif_profile.PTZConfiguration.DefaultContinuousZoomVelocitySpace
|
if profile is None:
|
||||||
is not None
|
for p in valid_profiles:
|
||||||
|
if getattr(p, "Name", None) == configured_profile:
|
||||||
|
profile = p
|
||||||
|
break
|
||||||
|
if profile is None:
|
||||||
|
available = [
|
||||||
|
f"name='{getattr(p, 'Name', None)}', token='{p.token}'"
|
||||||
|
for p in valid_profiles
|
||||||
|
]
|
||||||
|
logger.error(
|
||||||
|
"Onvif profile '%s' not found for camera %s. Available profiles: %s",
|
||||||
|
configured_profile,
|
||||||
|
camera_name,
|
||||||
|
available,
|
||||||
)
|
)
|
||||||
):
|
return False
|
||||||
# use the first profile that has a valid ptz configuration
|
else:
|
||||||
profile = onvif_profile
|
# use the first profile that has a valid ptz configuration
|
||||||
logger.debug(f"Selected Onvif profile for {camera_name}: {profile}")
|
profile = valid_profiles[0] if valid_profiles else None
|
||||||
break
|
|
||||||
|
|
||||||
if profile is None:
|
if profile is None:
|
||||||
logger.error(
|
logger.error(
|
||||||
@ -184,6 +218,8 @@ class OnvifController:
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
logger.debug(f"Selected Onvif profile for {camera_name}: {profile}")
|
||||||
|
|
||||||
# get the PTZ config for the profile
|
# get the PTZ config for the profile
|
||||||
try:
|
try:
|
||||||
configs = profile.PTZConfiguration
|
configs = profile.PTZConfiguration
|
||||||
@ -218,48 +254,92 @@ class OnvifController:
|
|||||||
move_request.ProfileToken = profile.token
|
move_request.ProfileToken = profile.token
|
||||||
self.cams[camera_name]["move_request"] = move_request
|
self.cams[camera_name]["move_request"] = move_request
|
||||||
|
|
||||||
# extra setup for autotracking cameras
|
# get PTZ configuration options for feature detection and relative movement
|
||||||
if (
|
ptz_config = None
|
||||||
self.config.cameras[camera_name].onvif.autotracking.enabled_in_config
|
fov_space_id = None
|
||||||
and self.config.cameras[camera_name].onvif.autotracking.enabled
|
|
||||||
):
|
try:
|
||||||
request = ptz.create_type("GetConfigurationOptions")
|
request = ptz.create_type("GetConfigurationOptions")
|
||||||
request.ConfigurationToken = profile.PTZConfiguration.token
|
request.ConfigurationToken = profile.PTZConfiguration.token
|
||||||
ptz_config = await ptz.GetConfigurationOptions(request)
|
ptz_config = await ptz.GetConfigurationOptions(request)
|
||||||
logger.debug(f"Onvif config for {camera_name}: {ptz_config}")
|
logger.debug(
|
||||||
|
f"Onvif PTZ configuration options for {camera_name}: {ptz_config}"
|
||||||
|
)
|
||||||
|
except (Fault, ONVIFError, TransportError, Exception) as e:
|
||||||
|
logger.debug(
|
||||||
|
f"Unable to get PTZ configuration options for {camera_name}: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# detect FOV translation space for relative movement
|
||||||
|
if ptz_config is not None:
|
||||||
|
try:
|
||||||
|
fov_space_id = next(
|
||||||
|
(
|
||||||
|
i
|
||||||
|
for i, space in enumerate(
|
||||||
|
ptz_config.Spaces.RelativePanTiltTranslationSpace
|
||||||
|
)
|
||||||
|
if "TranslationSpaceFov" in space["URI"]
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
fov_space_id = None
|
||||||
|
|
||||||
|
autotracking_config = self.config.cameras[camera_name].onvif.autotracking
|
||||||
|
autotracking_enabled = (
|
||||||
|
autotracking_config.enabled_in_config and autotracking_config.enabled
|
||||||
|
)
|
||||||
|
|
||||||
|
# autotracking-only: status request and service capabilities
|
||||||
|
if autotracking_enabled:
|
||||||
|
status_request = ptz.create_type("GetStatus")
|
||||||
|
status_request.ProfileToken = profile.token
|
||||||
|
self.cams[camera_name]["status_request"] = status_request
|
||||||
|
|
||||||
service_capabilities_request = ptz.create_type("GetServiceCapabilities")
|
service_capabilities_request = ptz.create_type("GetServiceCapabilities")
|
||||||
self.cams[camera_name]["service_capabilities_request"] = (
|
self.cams[camera_name]["service_capabilities_request"] = (
|
||||||
service_capabilities_request
|
service_capabilities_request
|
||||||
)
|
)
|
||||||
|
|
||||||
fov_space_id = next(
|
# setup relative move request when FOV relative movement is supported
|
||||||
(
|
if (
|
||||||
i
|
fov_space_id is not None
|
||||||
for i, space in enumerate(
|
and configs.DefaultRelativePanTiltTranslationSpace is not None
|
||||||
ptz_config.Spaces.RelativePanTiltTranslationSpace
|
):
|
||||||
)
|
# one-off GetStatus to seed Translation field
|
||||||
if "TranslationSpaceFov" in space["URI"]
|
status = None
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
# status request for autotracking and filling ptz-parameters
|
|
||||||
status_request = ptz.create_type("GetStatus")
|
|
||||||
status_request.ProfileToken = profile.token
|
|
||||||
self.cams[camera_name]["status_request"] = status_request
|
|
||||||
try:
|
try:
|
||||||
status = await ptz.GetStatus(status_request)
|
one_off_status_request = ptz.create_type("GetStatus")
|
||||||
logger.debug(f"Onvif status config for {camera_name}: {status}")
|
one_off_status_request.ProfileToken = profile.token
|
||||||
|
status = await ptz.GetStatus(one_off_status_request)
|
||||||
|
logger.debug(f"Onvif status for {camera_name}: {status}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Unable to get status from camera: {camera_name}: {e}")
|
logger.warning(f"Unable to get status from camera {camera_name}: {e}")
|
||||||
status = None
|
|
||||||
|
|
||||||
# autotracking relative panning/tilting needs a relative zoom value set to 0
|
rel_move_request = ptz.create_type("RelativeMove")
|
||||||
# if camera supports relative movement
|
rel_move_request.ProfileToken = profile.token
|
||||||
|
logger.debug(f"{camera_name}: Relative move request: {rel_move_request}")
|
||||||
|
|
||||||
|
fov_uri = ptz_config["Spaces"]["RelativePanTiltTranslationSpace"][
|
||||||
|
fov_space_id
|
||||||
|
]["URI"]
|
||||||
|
|
||||||
|
if rel_move_request.Translation is None:
|
||||||
|
if status is not None:
|
||||||
|
# seed from current position
|
||||||
|
rel_move_request.Translation = status.Position
|
||||||
|
rel_move_request.Translation.PanTilt.space = fov_uri
|
||||||
|
else:
|
||||||
|
# fallback: construct Translation explicitly
|
||||||
|
rel_move_request.Translation = {
|
||||||
|
"PanTilt": {"x": 0, "y": 0, "space": fov_uri}
|
||||||
|
}
|
||||||
|
|
||||||
|
# configure zoom on relative move request
|
||||||
if (
|
if (
|
||||||
self.config.cameras[camera_name].onvif.autotracking.zooming
|
autotracking_enabled
|
||||||
!= ZoomingModeEnum.disabled
|
and autotracking_config.zooming != ZoomingModeEnum.disabled
|
||||||
):
|
):
|
||||||
zoom_space_id = next(
|
zoom_space_id = next(
|
||||||
(
|
(
|
||||||
@ -271,60 +351,43 @@ class OnvifController:
|
|||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
# setup relative moving request for autotracking
|
|
||||||
move_request = ptz.create_type("RelativeMove")
|
|
||||||
move_request.ProfileToken = profile.token
|
|
||||||
logger.debug(f"{camera_name}: Relative move request: {move_request}")
|
|
||||||
if move_request.Translation is None and fov_space_id is not None:
|
|
||||||
move_request.Translation = status.Position
|
|
||||||
move_request.Translation.PanTilt.space = ptz_config["Spaces"][
|
|
||||||
"RelativePanTiltTranslationSpace"
|
|
||||||
][fov_space_id]["URI"]
|
|
||||||
|
|
||||||
# try setting relative zoom translation space
|
|
||||||
try:
|
|
||||||
if (
|
|
||||||
self.config.cameras[camera_name].onvif.autotracking.zooming
|
|
||||||
!= ZoomingModeEnum.disabled
|
|
||||||
):
|
|
||||||
if zoom_space_id is not None:
|
if zoom_space_id is not None:
|
||||||
move_request.Translation.Zoom.space = ptz_config["Spaces"][
|
rel_move_request.Translation.Zoom.space = ptz_config["Spaces"][
|
||||||
"RelativeZoomTranslationSpace"
|
"RelativeZoomTranslationSpace"
|
||||||
][zoom_space_id]["URI"]
|
][zoom_space_id]["URI"]
|
||||||
else:
|
except Exception as e:
|
||||||
if (
|
autotracking_config.zooming = ZoomingModeEnum.disabled
|
||||||
move_request["Translation"] is not None
|
logger.warning(
|
||||||
and "Zoom" in move_request["Translation"]
|
f"Disabling autotracking zooming for {camera_name}: Relative zoom not supported. Exception: {e}"
|
||||||
):
|
|
||||||
del move_request["Translation"]["Zoom"]
|
|
||||||
if (
|
|
||||||
move_request["Speed"] is not None
|
|
||||||
and "Zoom" in move_request["Speed"]
|
|
||||||
):
|
|
||||||
del move_request["Speed"]["Zoom"]
|
|
||||||
logger.debug(
|
|
||||||
f"{camera_name}: Relative move request after deleting zoom: {move_request}"
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
else:
|
||||||
self.config.cameras[
|
# remove zoom fields from relative move request
|
||||||
camera_name
|
if (
|
||||||
].onvif.autotracking.zooming = ZoomingModeEnum.disabled
|
rel_move_request["Translation"] is not None
|
||||||
logger.warning(
|
and "Zoom" in rel_move_request["Translation"]
|
||||||
f"Disabling autotracking zooming for {camera_name}: Relative zoom not supported. Exception: {e}"
|
):
|
||||||
|
del rel_move_request["Translation"]["Zoom"]
|
||||||
|
if (
|
||||||
|
rel_move_request["Speed"] is not None
|
||||||
|
and "Zoom" in rel_move_request["Speed"]
|
||||||
|
):
|
||||||
|
del rel_move_request["Speed"]["Zoom"]
|
||||||
|
logger.debug(
|
||||||
|
f"{camera_name}: Relative move request after deleting zoom: {rel_move_request}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if move_request.Speed is None:
|
if rel_move_request.Speed is None:
|
||||||
move_request.Speed = configs.DefaultPTZSpeed if configs else None
|
rel_move_request.Speed = configs.DefaultPTZSpeed if configs else None
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"{camera_name}: Relative move request after setup: {move_request}"
|
f"{camera_name}: Relative move request after setup: {rel_move_request}"
|
||||||
)
|
)
|
||||||
self.cams[camera_name]["relative_move_request"] = move_request
|
self.cams[camera_name]["relative_move_request"] = rel_move_request
|
||||||
|
|
||||||
# setup absolute moving request for autotracking zooming
|
# setup absolute move request
|
||||||
move_request = ptz.create_type("AbsoluteMove")
|
abs_move_request = ptz.create_type("AbsoluteMove")
|
||||||
move_request.ProfileToken = profile.token
|
abs_move_request.ProfileToken = profile.token
|
||||||
self.cams[camera_name]["absolute_move_request"] = move_request
|
self.cams[camera_name]["absolute_move_request"] = abs_move_request
|
||||||
|
|
||||||
# setup existing presets
|
# setup existing presets
|
||||||
try:
|
try:
|
||||||
@ -358,48 +421,48 @@ class OnvifController:
|
|||||||
|
|
||||||
if configs.DefaultRelativeZoomTranslationSpace:
|
if configs.DefaultRelativeZoomTranslationSpace:
|
||||||
supported_features.append("zoom-r")
|
supported_features.append("zoom-r")
|
||||||
if (
|
if ptz_config is not None:
|
||||||
self.config.cameras[camera_name].onvif.autotracking.enabled_in_config
|
|
||||||
and self.config.cameras[camera_name].onvif.autotracking.enabled
|
|
||||||
):
|
|
||||||
try:
|
try:
|
||||||
# get camera's zoom limits from onvif config
|
|
||||||
self.cams[camera_name]["relative_zoom_range"] = (
|
self.cams[camera_name]["relative_zoom_range"] = (
|
||||||
ptz_config.Spaces.RelativeZoomTranslationSpace[0]
|
ptz_config.Spaces.RelativeZoomTranslationSpace[0]
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if (
|
if autotracking_config.zooming == ZoomingModeEnum.relative:
|
||||||
self.config.cameras[camera_name].onvif.autotracking.zooming
|
autotracking_config.zooming = ZoomingModeEnum.disabled
|
||||||
== ZoomingModeEnum.relative
|
|
||||||
):
|
|
||||||
self.config.cameras[
|
|
||||||
camera_name
|
|
||||||
].onvif.autotracking.zooming = ZoomingModeEnum.disabled
|
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Disabling autotracking zooming for {camera_name}: Relative zoom not supported. Exception: {e}"
|
f"Disabling autotracking zooming for {camera_name}: Relative zoom not supported. Exception: {e}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if configs.DefaultAbsoluteZoomPositionSpace:
|
if configs.DefaultAbsoluteZoomPositionSpace:
|
||||||
supported_features.append("zoom-a")
|
supported_features.append("zoom-a")
|
||||||
if (
|
if ptz_config is not None:
|
||||||
self.config.cameras[camera_name].onvif.autotracking.enabled_in_config
|
|
||||||
and self.config.cameras[camera_name].onvif.autotracking.enabled
|
|
||||||
):
|
|
||||||
try:
|
try:
|
||||||
# get camera's zoom limits from onvif config
|
|
||||||
self.cams[camera_name]["absolute_zoom_range"] = (
|
self.cams[camera_name]["absolute_zoom_range"] = (
|
||||||
ptz_config.Spaces.AbsoluteZoomPositionSpace[0]
|
ptz_config.Spaces.AbsoluteZoomPositionSpace[0]
|
||||||
)
|
)
|
||||||
self.cams[camera_name]["zoom_limits"] = configs.ZoomLimits
|
self.cams[camera_name]["zoom_limits"] = configs.ZoomLimits
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if self.config.cameras[camera_name].onvif.autotracking.zooming:
|
if autotracking_config.zooming != ZoomingModeEnum.disabled:
|
||||||
self.config.cameras[
|
autotracking_config.zooming = ZoomingModeEnum.disabled
|
||||||
camera_name
|
|
||||||
].onvif.autotracking.zooming = ZoomingModeEnum.disabled
|
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Disabling autotracking zooming for {camera_name}: Absolute zoom not supported. Exception: {e}"
|
f"Disabling autotracking zooming for {camera_name}: Absolute zoom not supported. Exception: {e}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# disable autotracking zoom if required ranges are unavailable
|
||||||
|
if autotracking_config.zooming != ZoomingModeEnum.disabled:
|
||||||
|
if autotracking_config.zooming == ZoomingModeEnum.relative:
|
||||||
|
if "relative_zoom_range" not in self.cams[camera_name]:
|
||||||
|
autotracking_config.zooming = ZoomingModeEnum.disabled
|
||||||
|
logger.warning(
|
||||||
|
f"Disabling autotracking zooming for {camera_name}: Relative zoom range unavailable"
|
||||||
|
)
|
||||||
|
if autotracking_config.zooming == ZoomingModeEnum.absolute:
|
||||||
|
if "absolute_zoom_range" not in self.cams[camera_name]:
|
||||||
|
autotracking_config.zooming = ZoomingModeEnum.disabled
|
||||||
|
logger.warning(
|
||||||
|
f"Disabling autotracking zooming for {camera_name}: Absolute zoom range unavailable"
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self.cams[camera_name]["video_source_token"] is not None
|
self.cams[camera_name]["video_source_token"] is not None
|
||||||
and imaging is not None
|
and imaging is not None
|
||||||
@ -416,10 +479,9 @@ class OnvifController:
|
|||||||
except (Fault, ONVIFError, TransportError, Exception) as e:
|
except (Fault, ONVIFError, TransportError, Exception) as e:
|
||||||
logger.debug(f"Focus not supported for {camera_name}: {e}")
|
logger.debug(f"Focus not supported for {camera_name}: {e}")
|
||||||
|
|
||||||
|
# detect FOV relative movement support
|
||||||
if (
|
if (
|
||||||
self.config.cameras[camera_name].onvif.autotracking.enabled_in_config
|
fov_space_id is not None
|
||||||
and self.config.cameras[camera_name].onvif.autotracking.enabled
|
|
||||||
and fov_space_id is not None
|
|
||||||
and configs.DefaultRelativePanTiltTranslationSpace is not None
|
and configs.DefaultRelativePanTiltTranslationSpace is not None
|
||||||
):
|
):
|
||||||
supported_features.append("pt-r-fov")
|
supported_features.append("pt-r-fov")
|
||||||
@ -548,11 +610,8 @@ class OnvifController:
|
|||||||
move_request.Translation.PanTilt.x = pan
|
move_request.Translation.PanTilt.x = pan
|
||||||
move_request.Translation.PanTilt.y = tilt
|
move_request.Translation.PanTilt.y = tilt
|
||||||
|
|
||||||
if (
|
# include zoom if requested and camera supports relative zoom
|
||||||
"zoom-r" in self.cams[camera_name]["features"]
|
if zoom != 0 and "zoom-r" in self.cams[camera_name]["features"]:
|
||||||
and self.config.cameras[camera_name].onvif.autotracking.zooming
|
|
||||||
== ZoomingModeEnum.relative
|
|
||||||
):
|
|
||||||
move_request.Speed = {
|
move_request.Speed = {
|
||||||
"PanTilt": {
|
"PanTilt": {
|
||||||
"x": speed,
|
"x": speed,
|
||||||
@ -568,11 +627,7 @@ class OnvifController:
|
|||||||
move_request.Translation.PanTilt.x = 0
|
move_request.Translation.PanTilt.x = 0
|
||||||
move_request.Translation.PanTilt.y = 0
|
move_request.Translation.PanTilt.y = 0
|
||||||
|
|
||||||
if (
|
if zoom != 0 and "zoom-r" in self.cams[camera_name]["features"]:
|
||||||
"zoom-r" in self.cams[camera_name]["features"]
|
|
||||||
and self.config.cameras[camera_name].onvif.autotracking.zooming
|
|
||||||
== ZoomingModeEnum.relative
|
|
||||||
):
|
|
||||||
move_request.Translation.Zoom.x = 0
|
move_request.Translation.Zoom.x = 0
|
||||||
|
|
||||||
self.cams[camera_name]["active"] = False
|
self.cams[camera_name]["active"] = False
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user