Skip to content

Instantly share code, notes, and snippets.

Created November 13, 2023 06:07
Show Gist options
  • Save jorticus/7869a8e22403e4babb103a48fcab466b to your computer and use it in GitHub Desktop.
Save jorticus/7869a8e22403e4babb103a48fcab466b to your computer and use it in GitHub Desktop.
dnsmasq rtmp device discovery
# Dynamically populate NGINX config with network cameras
# detected via DHCP leases table and DNSMasq
import os
import sys
import time
DHCP_LEASES_FILE = '/tmp/dhcp.leases' # OpenWRT DNSMASQ
NGINX_CAMERAS_FILE = '/etc/nginx/cameras.conf' # This file will be included by nginx.conf
'default': {'user': 'admin', 'password': 'password'}
# Add MAC address for cameras with specific login details
'name': 'Reolink',
'mac_prefix': 'ec:71:db:',
'stream_hi': 'rtmp://{host}/bcs/channel0_main.bcs?channel=0&stream=0&user={user}&password={password}',
'stream_lo': 'rtmp://{host}/bcs/channel0_sub.bcs?channel=0&stream=1&user={user}&password={password}',
'snapshot': 'http://{host}/cgi-bin/api.cgi?cmd=Snap&channel=0&rs=abc123&user={user}&password={password}',
def reload_nginx():
# Restart NGINX to start streaming the new cameras
RESTART_COMMAND = 'docker container restart nginx'
def get_formatted(d, prop, **args):
val = d.get(prop)
if val:
return val.format(**args)
return None
def get_definition_for_mac(mac):
mac_prefix = mac[:9]
for cam_definition in SUPPORTED_CAMERAS:
if mac_prefix == cam_definition['mac_prefix']:
return cam_definition
return None
def make_camera_for_mac(mac, ip, name):
definition = get_definition_for_mac(mac)
if definition:
print(f'Found camera {mac} @ {ip} ({name})')
# Obtain login details for this specific camera, if available, otherwise use default
login = LOGIN_DETAILS.get(mac, LOGIN_DETAILS['default'])
# Obtain the streaming URLs
fmt_args = dict(host=ip, user=login['user'], password=login['password'])
stream_hi = get_formatted(definition, 'stream_hi', **fmt_args)
stream_lo = get_formatted(definition, 'stream_lo', **fmt_args)
snapshot = get_formatted(definition, 'snapshot', **fmt_args)
cam = {
'mac': mac,
'ip': ip,
'name': name,
'stream_hi': stream_hi,
'stream_lo': stream_lo,
'snapshot': snapshot
print(f" stream hi: {cam['stream_hi']}")
print(f" stream lo: {cam['stream_lo']}")
return cam
return None
def get_cameras_from_current_leases(leases_file):
cameras = []
if os.path.isfile(leases_file):
print(f'Read {leases_file}')
with open(leases_file, 'r') as f:
leases = f.readlines()
# Scan the DHCP leases file to find any connected cameras
for lease in leases:
# Example lease:
# '0 ec:71:db:ee:30:9b Camera1 01:ec:71:db:ee:30:9b'
expiry, mac, ip, name, _ = lease.split(' ', maxsplit=5)
camera = make_camera_for_mac(mac, ip, name)
if camera:
except Exception as e:
except Exception as e:
return cameras
def get_camera_from_args(action, mac, ip, name):
if action == 'add':
return make_camera_for_mac(mac, ip, name)
except Exception as e:
return None
def write_nginx_conf(conf_path, cameras):
print(f'Update {conf_path}:')
# Each line creates a new RTMP stream within nginx-rtmp.
# 'pull' means the stream is pulling data from a remote source (as opposed to being pushed/published into nginx by a remote device)
# 'static' means the stream will start pulling data from the camera immediately.
lines = [
f"pull {cam['stream_hi']} name={cam['name']} static;"
for cam in cameras
for ln in lines:
with open(NGINX_CAMERAS_FILE, 'w') as f:
if __name__ == "__main__":
# Hack: Let dnsmasq update the DHCP leases before continuing
if len(sys.argv) > 1:
cameras = get_cameras_from_current_leases(DHCP_LEASES_FILE)
# # Optional: If called from dnsmasq (dhcp-script), add additional camera using arguments.
# if len(sys.argv) > 1:
# camera = get_camera_from_args(*sys.argv)
# if camera:
# cameras.append(camera)
write_nginx_conf(NGINX_CAMERAS_FILE, cameras)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment