diff --git a/README.md b/README.md index f7b1e73..f27e4b8 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ The table below shows all available configurations and the cameras to which they In addition to the configurations in the table above, you can set any Basler camera property by including `options.basler.`. For example, it's common to set `options.basler.PixelFormat` to `RGB8`. ### Autodiscovery -Autodiscovery automatically connects to all cameras that are plugged into your machine or discoverable on the network, including `generic_usb`, `realsense` and `basler` cameras. Default configurations will be loaded for each camera. Please note that RTSP streams cannot be discovered in this manner; RTSP URLs must be specified in the configurations or can be discovered using a separate tool below. +Autodiscovery automatically connects to all cameras that are plugged into your machine or discoverable on the network, including `generic_usb`, `realsense`, `basler`, and ONVIF supported `rtsp` cameras. Default configurations will be loaded for each camera. Note that discovery of RTSP cameras will be disabled by default but can be enabled by setting `rtsp_discover_mode`. Refer to [RTSP Discovery](#rtsp-discovery) section for different options. Autodiscovery is great for simple applications where you don't need to set any special options on your cameras. It's also a convenient method for finding the serial numbers of your cameras (if the serial number isn't printed on the camera). ```python @@ -196,9 +196,10 @@ from framegrab import RTSPDiscovery, ONVIFDeviceInfo devices = RTSPDiscovery.discover_onvif_devices() ``` -The `discover_onvif_devices()` will provide a list of devices that it finds in the `ONVIFDeviceInfo` format. An optional mode `auto_discover_modes` can be used to try different default credentials to fetch RTSP URLs: +The `discover_onvif_devices()` will provide a list of devices that it finds in the `ONVIFDeviceInfo` format. An optional mode `auto_discover_mode` can be used to try different default credentials to fetch RTSP URLs: -- disable: Disable guessing camera credentials. +- off: No discovery. +- ip_only: Only discover the IP address of the camera. - light: Only try first two usernames and passwords ("admin:admin" and no username/password). - complete_fast: Try the entire DEFAULT_CREDENTIALS without delays in between. - complete_slow: Try the entire DEFAULT_CREDENTIALS with a delay of 1 seconds in between. diff --git a/pyproject.toml b/pyproject.toml index 17ed964..f5dac64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "framegrab" -version = "0.5.4" +version = "0.5.5" description = "Easily grab frames from cameras or streams" authors = ["Groundlight "] license = "MIT" diff --git a/src/framegrab/__init__.py b/src/framegrab/__init__.py index a2f8870..844f249 100644 --- a/src/framegrab/__init__.py +++ b/src/framegrab/__init__.py @@ -2,7 +2,7 @@ from .exceptions import GrabError from .grabber import FrameGrabber from .motion import MotionDetector -from .rtsp_discovery import AutodiscoverModes, ONVIFDeviceInfo, RTSPDiscovery +from .rtsp_discovery import AutodiscoverMode, ONVIFDeviceInfo, RTSPDiscovery try: import importlib.metadata @@ -20,6 +20,6 @@ "MotionDetector", "GrabError" "RTSPDiscovery", "ONVIFDeviceInfo", - "AutodiscoverModes", + "AutodiscoverMode", "preview_image", ] diff --git a/src/framegrab/cli/autodiscover.py b/src/framegrab/cli/autodiscover.py index becd236..b81a2a5 100644 --- a/src/framegrab/cli/autodiscover.py +++ b/src/framegrab/cli/autodiscover.py @@ -10,7 +10,7 @@ PREVIEW_RTSP_COMMAND_CHOICES, preview_image, ) -from framegrab.rtsp_discovery import AutodiscoverModes +from framegrab.rtsp_discovery import AutodiscoverMode @click.command() @@ -21,17 +21,17 @@ show_default=True, ) @click.option( - "--rtsp_discover_modes", + "--rtsp-discover-mode", type=click.Choice(PREVIEW_RTSP_COMMAND_CHOICES, case_sensitive=False), - default="light", + default="off", show_default=True, ) -def autodiscover(preview: str, rtsp_discover_modes: str = "light"): +def autodiscover(preview: str, rtsp_discover_mode: str = "off"): """Automatically discover cameras connected to the current host (e.g. USB).""" # Print message to stderr click.echo("Discovering cameras...", err=True) - grabbers = FrameGrabber.autodiscover(rtsp_discover_modes=rtsp_discover_modes) + grabbers = FrameGrabber.autodiscover(rtsp_discover_mode=rtsp_discover_mode) yaml_config = { "image_sources": [], diff --git a/src/framegrab/cli/clitools.py b/src/framegrab/cli/clitools.py index 882b17a..d11dabe 100644 --- a/src/framegrab/cli/clitools.py +++ b/src/framegrab/cli/clitools.py @@ -5,7 +5,7 @@ from imgcat import imgcat from PIL import Image -from framegrab.rtsp_discovery import AutodiscoverModes +from framegrab.rtsp_discovery import AutodiscoverMode def imgcat_preview(name: str, frame): @@ -46,7 +46,7 @@ def null_preview(name: str, frame): } PREVIEW_COMMAND_CHOICES = list(_PREVIEW_COMMANDS.keys()) -PREVIEW_RTSP_COMMAND_CHOICES = [mode.value for mode in AutodiscoverModes] +PREVIEW_RTSP_COMMAND_CHOICES = [mode.value for mode in AutodiscoverMode] def preview_image(frame, title: str, output_type: str): diff --git a/src/framegrab/grabber.py b/src/framegrab/grabber.py index b8736ba..574aa32 100644 --- a/src/framegrab/grabber.py +++ b/src/framegrab/grabber.py @@ -14,7 +14,7 @@ import yaml from .exceptions import GrabError -from .rtsp_discovery import AutodiscoverModes, RTSPDiscovery +from .rtsp_discovery import AutodiscoverMode, RTSPDiscovery from .unavailable_module import UnavailableModule logger = logging.getLogger(__name__) @@ -273,9 +273,7 @@ def create_grabber(config: dict, autogenerate_name: bool = True, warmup_delay: f return grabber @staticmethod - def autodiscover( - warmup_delay: float = 1.0, rtsp_discover_modes: AutodiscoverModes = AutodiscoverModes.light - ) -> dict: + def autodiscover(warmup_delay: float = 1.0, rtsp_discover_mode: AutodiscoverMode = AutodiscoverMode.off) -> dict: """Autodiscovers cameras and returns a dictionary of FrameGrabber objects warmup_delay (float, optional): The number of seconds to wait after creating the grabbers. USB @@ -283,14 +281,15 @@ def autodiscover( might result in dark or blurry images. Defaults to 1.0. Only happens if there are any generic_usb cameras in the config list. - rtsp_discover_modes (AutodiscoverModes, optional): Options to try different default credentials + rtsp_discover_mode (AutodiscoverMode, optional): Options to try different default credentials stored in DEFAULT_CREDENTIALS for RTSP cameras. - Consists of four options: - disable: Disable guessing camera credentials. + Consists of five options: + off: No discovery. + ip_only: Only discover the IP address of the camera. light: Only try first two usernames and passwords ("admin:admin" and no username/password). complete_fast: Try the entire DEFAULT_CREDENTIALS without delays in between. complete_slow: Try the entire DEFAULT_CREDENTIALS with a delay of 1 seconds in between. - Defaults to AutodiscoverModes.light. + Defaults to off. """ autodiscoverable_input_types = ( InputTypes.REALSENSE, @@ -304,20 +303,22 @@ def autodiscover( for input_type in autodiscoverable_input_types: logger.info(f"Autodiscovering {input_type} cameras...") + # If the input type is RTSP and rtsp_discover_modes is provided, use RTSPDiscovery to find the cameras if input_type == InputTypes.RTSP: - onvif_devices = RTSPDiscovery.discover_onvif_devices(auto_discover_modes=rtsp_discover_modes) - for device in onvif_devices: - for index, rtsp_url in enumerate(device.rtsp_urls): - grabber = FrameGrabber.create_grabber( - { - "input_type": input_type, - "id": {"rtsp_url": rtsp_url}, - "name": f"RTSP Camera - {device.ip} - {index}", - }, - autogenerate_name=False, - warmup_delay=0, - ) - grabber_list.append(grabber) + if rtsp_discover_mode is not None: + onvif_devices = RTSPDiscovery.discover_onvif_devices(auto_discover_mode=rtsp_discover_mode) + for device in onvif_devices: + for index, rtsp_url in enumerate(device.rtsp_urls): + grabber = FrameGrabber.create_grabber( + { + "input_type": input_type, + "id": {"rtsp_url": rtsp_url}, + "name": f"RTSP Camera - {device.ip} - {index}", + }, + autogenerate_name=False, + warmup_delay=0, + ) + grabber_list.append(grabber) continue for _ in range( diff --git a/src/framegrab/rtsp_discovery.py b/src/framegrab/rtsp_discovery.py index a1a65d3..5b0be19 100644 --- a/src/framegrab/rtsp_discovery.py +++ b/src/framegrab/rtsp_discovery.py @@ -27,17 +27,19 @@ ] -class AutodiscoverModes(str, Enum): +class AutodiscoverMode(str, Enum): """ Enum for camera discovery modes. Options to try different default credentials stored in DEFAULT_CREDENTIALS. - Consists of four options: - disable: Disable guessing camera credentials. + Consists of five options: + off: No discovery. + ip_only: Only discover the IP address of the camera. light: Only try first two usernames and passwords ("admin:admin" and no username/password). complete_fast: Try the entire DEFAULT_CREDENTIALS without delays in between. complete_slow: Try the entire DEFAULT_CREDENTIALS with a delay of 1 seconds in between. """ - disable = "disable" + off = "off" + ip_only = "ip_only" light = "light" complete_fast = "complete_fast" complete_slow = "complete_slow" @@ -67,19 +69,20 @@ class RTSPDiscovery: @staticmethod def discover_onvif_devices( - auto_discover_modes: AutodiscoverModes = AutodiscoverModes.disable, + auto_discover_mode: AutodiscoverMode = AutodiscoverMode.ip_only, ) -> List[ONVIFDeviceInfo]: """ Uses WSDiscovery to find ONVIF supported devices. Parameters: - auto_discover_modes (AutodiscoverModes, optional): Options to try different default credentials stored in DEFAULT_CREDENTIALS. - Consists of four options: - disable: Disable guessing camera credentials. + auto_discover_mode (AutodiscoverMode, optional): Options to try different default credentials stored in DEFAULT_CREDENTIALS. + Consists of five options: + off: No discovery. + ip_only: Only discover the IP address of the camera. light: Only try first two usernames and passwords ("admin:admin" and no username/password). complete_fast: Try the entire DEFAULT_CREDENTIALS without delays in between. complete_slow: Try the entire DEFAULT_CREDENTIALS with a delay of 1 seconds in between. - Defaults to disable. + Defaults to ip_only. Returns: List[ONVIFDeviceInfo]: A list of ONVIFDeviceInfos with IP address, port number, and ONVIF service address. @@ -87,6 +90,11 @@ def discover_onvif_devices( device_ips = [] logger.debug("Starting WSDiscovery for ONVIF devices") + + if auto_discover_mode == AutodiscoverMode.off: + logger.debug("ONVIF device discovery disabled") + return device_ips + wsd = WSDiscovery() wsd.start() types = [QName("http://www.onvif.org/ver10/network/wsdl", "NetworkVideoTransmitter")] @@ -100,8 +108,8 @@ def discover_onvif_devices( logger.debug(f"Found ONVIF service at {xaddr}") device_ip = ONVIFDeviceInfo(ip=ip, port=port, username="", password="", xaddr=xaddr, rtsp_urls=[]) - if auto_discover_modes is not AutodiscoverModes.disable: - RTSPDiscovery._try_logins(device=device_ip, auto_discover_modes=auto_discover_modes) + if auto_discover_mode is not AutodiscoverMode.ip_only: + RTSPDiscovery._try_logins(device=device_ip, auto_discover_mode=auto_discover_mode) device_ips.append(device_ip) wsd.stop() @@ -151,15 +159,16 @@ def generate_rtsp_urls(device: ONVIFDeviceInfo) -> List[str]: device.rtsp_urls = rtsp_urls return rtsp_urls - def _try_logins(device: ONVIFDeviceInfo, auto_discover_modes: AutodiscoverModes) -> bool: + def _try_logins(device: ONVIFDeviceInfo, auto_discover_mode: AutodiscoverMode) -> bool: """ Fetch RTSP URLs from an ONVIF supported device, given a username/password. Parameters: device (ONVIFDeviceInfo): Pydantic Model that stores information about camera RTSP address, port number, username, and password. - auto_discover_modes (AutodiscoverModes | None, optional): Options to try different default credentials stored in DEFAULT_CREDENTIALS. - Consists of four options: - disable: Disable guessing camera credentials. + auto_discover_mode (AutodiscoverMode): Options to try different default credentials stored in DEFAULT_CREDENTIALS. + Consists of five options: + off: No discovery. + ip_only: Only discover the IP address of the camera. light: Only try first two usernames and passwords ("admin:admin" and no username/password). complete_fast: Try the entire DEFAULT_CREDENTIALS without delays in between. complete_slow: Try the entire DEFAULT_CREDENTIALS with a delay of 1 seconds in between. @@ -170,10 +179,10 @@ def _try_logins(device: ONVIFDeviceInfo, auto_discover_modes: AutodiscoverModes) credentials = DEFAULT_CREDENTIALS - if auto_discover_modes == AutodiscoverModes.disable: + if auto_discover_mode == AutodiscoverMode.ip_only or auto_discover_mode == AutodiscoverMode.off: return False - if auto_discover_modes == AutodiscoverModes.light: + if auto_discover_mode == AutodiscoverMode.light: credentials = DEFAULT_CREDENTIALS[:2] for username, password in credentials: @@ -187,7 +196,7 @@ def _try_logins(device: ONVIFDeviceInfo, auto_discover_modes: AutodiscoverModes) logger.debug(f"RTSP URL fetched successfully with {username}:{password} for device IP {device.ip}") return True - if auto_discover_modes == AutodiscoverModes.complete_slow: + if auto_discover_mode == AutodiscoverMode.complete_slow: time.sleep(1) # Return False when there are no correct credentials diff --git a/test/test_rtsp_discovery.py b/test/test_rtsp_discovery.py index 4957d94..448b28b 100644 --- a/test/test_rtsp_discovery.py +++ b/test/test_rtsp_discovery.py @@ -2,7 +2,7 @@ from wsdiscovery.service import Service from unittest.mock import patch -from framegrab.rtsp_discovery import RTSPDiscovery, ONVIFDeviceInfo, AutodiscoverModes +from framegrab.rtsp_discovery import RTSPDiscovery, ONVIFDeviceInfo, AutodiscoverMode class TestRTSPDiscovery(unittest.TestCase): @@ -27,5 +27,5 @@ def test_generate_rtsp_urls(self): def test_try_logins(self): device = ONVIFDeviceInfo(ip="0") - assert False == RTSPDiscovery._try_logins(device=device, auto_discover_modes=AutodiscoverModes.complete_fast) + assert False == RTSPDiscovery._try_logins(device=device, auto_discover_mode=AutodiscoverMode.complete_fast) assert device.rtsp_urls == []