Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added option to completely skip RTSP Discovery in autodiscover() #47

Merged
merged 12 commits into from
Jul 22, 2024
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<BASLER PROPERTY NAME>`. 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
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 <info@groundlight.ai>"]
license = "MIT"
Expand Down
4 changes: 2 additions & 2 deletions src/framegrab/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,6 +20,6 @@
"MotionDetector",
"GrabError" "RTSPDiscovery",
"ONVIFDeviceInfo",
"AutodiscoverModes",
"AutodiscoverMode",
"preview_image",
]
10 changes: 5 additions & 5 deletions src/framegrab/cli/autodiscover.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
PREVIEW_RTSP_COMMAND_CHOICES,
preview_image,
)
from framegrab.rtsp_discovery import AutodiscoverModes
from framegrab.rtsp_discovery import AutodiscoverMode


@click.command()
Expand All @@ -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": [],
Expand Down
4 changes: 2 additions & 2 deletions src/framegrab/cli/clitools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
43 changes: 22 additions & 21 deletions src/framegrab/grabber.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -273,24 +273,23 @@ 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
cameras often need a moment to warm up before they can be used; grabbing frames too early
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,
Expand All @@ -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(
Expand Down
45 changes: 27 additions & 18 deletions src/framegrab/rtsp_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -67,26 +69,32 @@ 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.
"""

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")]
Expand All @@ -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()
Expand Down Expand Up @@ -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.
Expand All @@ -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:
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions test/test_rtsp_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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 == []
Loading