ipcam-checker
Async IP camera health checker for Python 3.13+.
Checks ping, RTSP streams, snapshots, open ports, ONVIF device info, VAPIX sensors/heaters, and SNMP metrics. Discovers cameras on a subnet via mDNS (Bonjour) and TCP port scan. Includes a telemetry system that tracks per-check wall time, CPU time, and thread counts.
Features
| Check | What it returns |
|---|---|
| Ping | Latency, jitter, packet loss |
| RTSP | Codec, resolution, FPS, bitrate, RTP packet stats |
| Snapshot | JPEG captured via HTTP or ffmpeg frame grab |
| Ports | TCP/UDP open port list |
| ONVIF | Manufacturer, model, firmware, profiles, PTZ/analytics caps |
| VAPIX | Temperature sensors, heater status (Axis cameras) |
| SNMP (Axis) | sysDescr, uptime, temp sensors, video signal, IF-MIB interface stats |
| Discovery | Subnet scan via mDNS + TCP port scan → list of likely cameras |
Example output
Axis camera (P1435-LE) — ONVIF + VAPIX + SNMP
==================================================
Axis-170 (192.168.2.170)
==================================================
ping: OK latency=1.747ms loss=0.0%
main: OK 1920x1080 25.0fps h264 (Main) yuvj420p Level41 10482.03kbps title='Session streamed with GStreamer' comment='rtsp-server' probe=100
rtp: pkts=76 lost=0(0.0%) jitter=0.008/0.022ms avg=10482.03kbps
sub : OK 640x480 25.0fps h264 (Main) yuvj420p Level41 533.35kbps title='Session streamed with GStreamer' comment='rtsp-server' probe=100
rtp: pkts=76 lost=0(0.0%) jitter=0.008/0.022ms avg=533.35kbps
ports: 80/tcp 443/tcp 554/tcp 161/udp
onvif: OK ONVIF 2.21 AXIS P1435-LE FW:9.80.105 SN:ACCC8ED43FB1
profiles: profile_1 h264(H264 640x400 25fps 2147483647kbps) profile_1 jpeg(H264 640x400 25fps 2147483647kbps)
caps: Analytics
vapix: OK Image Sensor=29.0°C Heater=26.8°C IR led=26.4°C IR led tele=26.4°C
heater: H0=Stopped
snmp: OK uptime=15065s
descr: ; AXIS P1435-LE; Network Camera; 9.80.105; Apr 03 2025 15:51; 70D.1; 1;
temp: 1=29°C(ok) 2=26°C(ok) 3=26°C(ok) 4=26°C(ok)
iface: eth0 100Mbps rx=26.0MB tx=52.6MB rx_err=16 rx_drop=18
timing: camera=8340.0ms cpu=1468.8ms threads=1→5
ping wall=219.4ms cpu=15.6ms
vapix wall=519.9ms cpu=765.6ms
snmp wall=797.3ms cpu=734.4ms
snapshot wall=1366.2ms cpu=1296.9ms
ports wall=2352.6ms cpu=1296.9ms
rtsp_sub wall=3961.0ms cpu=1390.6ms
rtsp_main wall=4202.9ms cpu=1421.9ms
onvif wall=8101.3ms cpu=1421.9ms
Sony network camera (SNC-EM600)
==================================================
Sony-182 (192.168.2.182)
==================================================
ping: OK latency=2.551ms loss=0.0%
main: OK 1280x720 25.0fps h264 (High) yuvj420p Level31 929.99kbps title='Sony RTSP Server' probe=100
rtp: pkts=76 lost=0(0.0%) jitter=0.0/0.0ms avg=929.99kbps
sub : OK 640x480 10.0fps h264 (Main) yuvj420p Level30 133.68kbps title='Sony RTSP Server' probe=100
rtp: pkts=31 lost=0(0.0%) jitter=20.0/20.0ms avg=133.68kbps
ports: 80/tcp 554/tcp 161/udp
onvif: OK ONVIF 17.6 Sony SNC-EM600 FW:3.2.0 SN:5233423
caps: PTZ Analytics
vapix: disabled
snmp: disabled
timing: camera=4290.7ms cpu=1453.1ms threads=1→19
ping wall=221.6ms cpu=31.2ms
snapshot wall=0.3ms cpu=0.0ms
onvif wall=1021.4ms cpu=875.0ms
ports wall=2008.3ms cpu=875.0ms
rtsp_main wall=3617.1ms cpu=968.8ms
rtsp_sub wall=3710.4ms cpu=968.8ms
Installation
pip install ipcam-checker
Optional extras:
pip install "ipcam-checker[onvif]" # ONVIF support (onvif-zeep)
pip install "ipcam-checker[discovery]" # mDNS discovery (zeroconf)
pip install "ipcam-checker[loki]" # Loki log push (python-logging-loki)
Quick start
import asyncio
from ipcam_checker import CameraConfig, Settings, check_cameras, setup_logging
CAMERAS = [
CameraConfig(
name="Axis-170",
ip="192.168.2.170",
rtsp_url_main="rtsp://192.168.2.170/axis-media/media.amp?videocodec=h264",
onvif_username="onvifadmin",
onvif_password="secret",
vapix_username="axisuser",
vapix_password="secret",
check_onvif=True,
check_vapix=True,
check_snmp="Axis",
snmp_community_read="public",
),
]
SETTINGS = Settings(
check_ping_enabled=True,
check_rtsp_enabled=True,
check_onvif_enabled=True,
check_vapix_enabled=True,
check_snmp_enabled=True,
)
async def main():
setup_logging(level="INFO")
async for result in check_cameras(CAMERAS, SETTINGS):
print(result.name, result.ping, result.snmp_result)
asyncio.run(main())
Camera configuration
CameraConfig(
# Identity
name="MyCamera",
ip="192.168.1.10",
# RTSP
rtsp_port=554,
rtsp_url_main="rtsp://192.168.1.10/stream1",
rtsp_url_sub="rtsp://192.168.1.10/stream2",
rtsp_username="admin",
rtsp_password="secret",
# Snapshot
snapshot_url="http://192.168.1.10/snapshot.jpg",
# ONVIF
onvif_port=80,
onvif_username="onvifadmin",
onvif_password="secret",
# VAPIX (Axis only)
vapix_port=80,
vapix_ssl=False,
vapix_username="user",
vapix_password="secret",
# SNMP
snmp_community_read="public",
# Per-camera check overrides (None = inherit global Settings flag)
check_ping=None, # bool | None
check_rtsp=None, # bool | None
check_snapshot=None, # bool | None
check_ports=None, # bool | None
check_onvif=True, # bool | None
check_vapix=True, # bool | None
check_snmp="Axis", # str | None — None=inherit, "Axis"=Axis SNMP impl
)
Per-camera overrides
Global Settings flags (e.g. check_onvif_enabled) act as defaults. Per-camera fields override them:
# Inherit global setting (default)
CameraConfig(name="Cam1", ip="...", check_snmp=None)
# Force SNMP on regardless of global flag
CameraConfig(name="Cam2", ip="...", check_snmp="Axis")
# Disable a check for just this camera
CameraConfig(name="Cam3", ip="...", check_ping=False)
Global settings
Settings(
# Check toggles (per-camera overrides win when set)
check_ping_enabled=True,
check_rtsp_enabled=True,
check_snapshot_enabled=True,
check_ports_enabled=False,
check_onvif_enabled=False,
check_vapix_enabled=False,
check_snmp_enabled=False,
# Ping
ping_count=4,
ping_timeout_s=2.0,
# RTSP / ffprobe
rtsp_timeout_s=10.0,
ffprobe_analyze_duration_s=5.0,
# ONVIF / VAPIX / SNMP timeouts
onvif_timeout_s=5.0,
vapix_timeout_s=5.0,
snmp_timeout_s=5.0,
snmp_port=161,
# Port scan
port_scan_tcp_ports=[80, 443, 554, 8000, 8443],
port_scan_udp_ports=[161],
port_scan_timeout_s=2.0,
# Concurrency
max_concurrent_cameras=50,
thread_pool_size=20,
# Logging
log_level="INFO",
log_file=Path("logs/ipcam.log"),
log_json=True, # JSON format (Loki/Promtail-ready)
log_console=False,
loki_url=None, # "http://loki:3100/loki/api/v1/push"
)
Camera discovery
Find cameras on a local subnet without a predefined list:
import asyncio
from ipcam_checker.discover import discover_cameras
async def main():
devices = await discover_cameras(
"192.168.2.0/24",
scan_ports=[80, 443, 554, 8080, 8554],
mdns_timeout_s=5.0,
port_timeout_s=0.5,
port_scan_workers=150,
)
for d in devices:
if d.likely_camera:
print(d.ip, d.open_ports, [s.service_type for s in d.mdns_services])
asyncio.run(main())
discover_cameras runs mDNS browsing and TCP port scan concurrently in threads. Results are merged by IP and sorted.
likely_camera is True when port 554 is open or a known camera mDNS service is present (_axis-video._tcp, _onvif._tcp, _rtsp._tcp).
Requires: pip install "ipcam-checker[discovery]"
Telemetry
Every CameraResult includes timing data:
result.telemetry.wall_ms # total camera check wall time
result.telemetry.cpu_ms # process CPU time (approx for async checks)
result.telemetry.threads_at_start # threading.active_count() before checks
result.telemetry.threads_at_end # threading.active_count() after checks
for c in result.telemetry.checks:
print(c.name, c.wall_ms, c.cpu_ms)
# ping 45.1ms 0.8ms
# ports 1201.3ms 2.1ms
# onvif 312.4ms 4.7ms
# snmp 95.6ms 3.1ms
Telemetry data is a plain Pydantic model — serialize with .model_dump() and ship to any backend.
Plugin system
Extend results with custom checks:
from ipcam_checker.plugins.base import AbstractPlugin
class MyPlugin(AbstractPlugin):
name = "my_plugin"
async def run(self, camera, result, executor, settings) -> dict:
# result already has ping, streams, onvif, etc.
return {"custom_value": 42}
# Pass plugins to check_cameras
async for result in check_cameras(cameras, settings, plugins=[MyPlugin()]):
print(result.plugin_results["my_plugin"])
Logging
Log output goes to file (JSON by default, Loki/Promtail-ready) and optionally to stderr. Third-party libraries (httpx, httpcore) are redirected to the log file only and suppressed from stdout.
from ipcam_checker import setup_logging
from pathlib import Path
setup_logging(
level="DEBUG",
log_file=Path("logs/ipcam.log"),
json_file=True, # JSON lines in file
console=False, # no stderr output
loki_url="http://loki:3100/loki/api/v1/push", # optional
)
Or via Settings:
settings = Settings(log_level="DEBUG", log_file=Path("logs/ipcam.log"))
settings.configure_logging()
Examples
| File | Description |
|---|---|
examples/test_local.py |
Check a predefined list of cameras, print full results + telemetry |
examples/discover_local.py |
Discover cameras on 192.168.2.0/24 via mDNS + port scan |
Requirements
- Python 3.13+
pydantic >= 2.0httpx >= 0.27icmplib >= 3.0Pillow >= 10.0local-ffmpeg >= 0.1.0python-json-logger >= 2.0
Optional:
onvif-zeep >= 0.2.12— ONVIF checkszeroconf >= 0.115— mDNS camera discoverypython-logging-loki >= 0.3— Loki log push
Metadata
Release files for ipcam-checker 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ipcam_checker-0.1.2.tar.gz | 38.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ipcam_checker-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 72.0 kB
Release files / ipcam_checker-0.1.2.tar.gz
| Download URL | ipcam_checker-0.1.2.tar.gz |
|---|---|
| Size | 38.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ca8c7174829bee34d1fb3c03cb32f2202fddf54ff61ea36d7086525025da298f
|
|
BLAKE2b-256 checksum How to use checksums |
2700f8be57644258d9a6d7a9870220513696516fccdfbd7e23d052075e7c6cab
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 13, 2026.
Transparency logRelease files / ipcam_checker-0.1.2-py3-none-any.whl
| Download URL | ipcam_checker-0.1.2-py3-none-any.whl |
|---|---|
| Size | 33.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4fb7882c825a67a6840f8aead7a3b2ddf74fcdff2877aa2f5d9cca60f5789f84
|
|
BLAKE2b-256 checksum How to use checksums |
be874a423cbfec2b20aad57445b654bd3e79b5b6c450b48a9f0e3cd72f3d81b0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 13, 2026.
Transparency log