Skip to content

About

An RTSP bridge to Reolink IP cameras

Resources

Stars

0 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

Neolink

Docker GHCR dependency status

Neolink is a small program that acts as a proxy between Reolink IP cameras and normal RTSP clients. Certain cameras, such as the Reolink B800, do not implement ONVIF or RTSP, but instead use a proprietary "Baichuan" protocol only compatible with their apps and NVRs (any camera that uses "port 9000" will likely be using this protocol). Neolink allows you to use NVR software such as Blue Iris, Frigate, or Shinobi to receive video from these cameras instead. The Reolink NVR is not required, and the cameras are unmodified. Your NVR software connects to Neolink, which forwards the video stream from the camera.

The Neolink project is not affiliated with Reolink in any way; everything it does has been reverse engineered.

This Fork

This is a maintained fork of QuantumEntangledAndy/neolink, which itself was a fork of thirtythreeforty/neolink.

This fork incorporates community PRs that were submitted to upstream but never merged, plus additional improvements:

Ported community contributions:

  • Buffer pool bucketing to prevent FD/memory leaks — @wafgo (#373)
  • Wireshark dissector fix for deprecated bit32 — @Maaggs (#389)
  • enable_audio and enable_low_latency config options — @fromagge (#394)
  • Encoding subcommand for video compression settings — @lorek123 (#395)
  • Disk management, SD card replay/download, and alarm search — @lorek123 (#396)
  • Timestamp overflow fix (u64) preventing corruption after ~71 min — @joshkautz (#398)
  • Channel backpressure fix preventing keepalive timeouts — @joshkautz (#399)
  • Pipeline sharing to prevent per-client resource exhaustion — @joshkautz (#400)

Additional changes:

  • Low-latency RTSP mode with frame draining, live-mode AppSrc, and reduced buffers
  • Docker images published to GHCR instead of Docker Hub
  • Updated dependencies and Debian Trixie base image
  • Use local timezone when using update_time to set camera time

Inherited features from upstream:

  • MQTT with Home Assistant discovery
  • Motion detection
  • Paused streams (when no RTSP client or no motion detected)
  • Save still images to disk
  • Battery camera support with idle disconnect
  • PTZ control, PIR, floodlight, siren, status LED
  • Multiple discovery methods (local, remote, map, relay, cellular)
  • TLS encryption for RTSP

Installation

Docker is the preferred way to run Neolink. Binary releases are not currently provided.

Docker (recommended)

Images are published to GHCR on every push to master and on tags:

docker pull ghcr.io/mutuallyassureddeployment/neolink:dev     # latest master
docker pull ghcr.io/mutuallyassureddeployment/neolink:latest   # latest tagged release

Run with:

docker run \
  --network host \
  --volume=$PWD/neolink.toml:/etc/neolink.toml \
  ghcr.io/mutuallyassureddeployment/neolink

--network host is only needed for local broadcast discovery. If you connect via IP address, normal bridge mode works fine. macOS does not support --network host.

Environment variables:

Variable Default Description
NEO_LINK_MODE rtsp Mode: rtsp, mqtt, or mqtt-rtsp
NEO_LINK_PORT 8554 RTSP listen port
NEO_HEALTH_PORT 8555 Port used by the image HEALTHCHECK (must match the [healthcheck] port in your config)

Healthcheck endpoint

Neolink runs a small HTTP healthcheck/diagnostics server so an orchestrator (Docker Compose, Kubernetes, ...) can tell whether the container is actually serving healthy cameras — not just that the process is alive.

It is enabled by default on port 8555 — no config required. The published image's HEALTHCHECK probes it over localhost inside the container, so you do not need to publish port 8555 unless you also want to reach /health from outside.

There is a single endpoint, GET /health, where the status code is the verdict and the JSON body is the detail:

  • 200 — healthy
  • 503 — unhealthy

Strict readiness: /health returns 503 whenever any enabled camera that should be connected is not (e.g. unreachable or stuck reconnecting). Cameras that are intentionally disconnected (manual disconnect, or idle/battery saving) report as idle and stay healthy. Disabled cameras are reported but never affect overall health.

The JSON body reports, per camera: state, live, configured streams, active_uses, uptime_secs, reconnect_attempts, and last_error, plus the neolink version/profile. Example:

curl -s localhost:8555/health | jq    # body for humans; exit code for probes (curl -f)

To change the port or turn it off, add a [healthcheck] section to neolink.toml:

[healthcheck]
enabled = true     # optional, default true — set false to disable
bind = "0.0.0.0"   # optional, default 0.0.0.0
port = 8555        # optional, default 8555

If you disable the endpoint, also override/disable the image's HEALTHCHECK (e.g. in your docker run/compose config), or the container will report unhealthy because the probe has nothing to talk to.

Docker Compose

A ready-to-edit docker-compose.yml is included:

services:
  neolink:
    image: ghcr.io/mutuallyassureddeployment/neolink:latest
    restart: unless-stopped
    # --network host is only needed for local broadcast discovery.
    ports:
      - "8554:8554"   # RTSP
      # - "8555:8555" # healthcheck — only needed to reach /health from outside
    volumes:
      - ./neolink.toml:/etc/neolink.toml:ro
    healthcheck:
      test: ["CMD", "curl", "-fsS", "-o", "/dev/null", "http://localhost:8555/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s

Building from source

Requires Rust and GStreamer development libraries:

# Ubuntu/Debian
sudo apt install \
  build-essential libssl-dev ca-certificates \
  libgstrtspserver-1.0-dev libgstreamer1.0-dev \
  libglib2.0-dev protobuf-compiler

cargo build --release

Documentation

Detailed docs are in the docs/ directory:

Configuration

Create a neolink.toml file. See sample_config.toml for all options.

Minimal example:

bind = "0.0.0.0"

[[cameras]]
name = "Camera01"
username = "admin"
password = "password"
uid = "ABCDEF0123456789"

[[cameras]]
name = "Camera02"
username = "admin"
password = "password"
address = "192.168.1.10:9000"

Start the RTSP server:

neolink rtsp --config=neolink.toml

Connect your NVR or RTSP client to:

rtsp://HOST:8554/CameraName

Where "CameraName" matches the name field in your config.

Per-camera options

[[cameras]]
name = "Camera01"
username = "admin"
password = "password"
address = "192.168.1.10:9000"
stream = "mainStream"          # "mainStream", "subStream", or "both" (default)
enable_audio = true            # disable to save CPU (default: true)
enable_low_latency = false     # reduce stream delay (default: false)
discovery = "local"            # "local", "remote", "map", "relay", "cellular"
idle_disconnect = false        # disconnect when idle to save battery
debug = false                  # dump raw XML from camera

Discovery

When connecting by UID, the camera IP is discovered with these methods (each implicitly enables prior methods):

  1. local — UDP broadcast on local network
  2. remote — Ask Reolink servers for the IP, then connect directly
  3. map — Register our IP with Reolink, camera connects to us
  4. relay — Reolink relays the connection (neither side needs direct access)

Use discovery = "cellular" for cellular cameras (skips local/remote).

If you know the IP, set address directly and skip discovery entirely.

MQTT

[mqtt]
broker_addr = "127.0.0.1"
port = 1883
credentials = ["username", "password"]

Start with MQTT + RTSP:

neolink mqtt-rtsp --config=neolink.toml

Or MQTT only:

neolink mqtt --config=neolink.toml

Messages

All messages are prefixed with neolink/ or neolink/{CAMERA_NAME}/.

Control (/control/...): led, ir, reboot, ptz, zoom, pir, floodlight, floodlight_tasks, wakeup, siren

Status (/status/...): battery, battery_level, motion, pir, ptz/preset, preview, floodlight_tasks

Query (/query/...): battery, pir, ptz/preset, preview

See the sample config for per-camera MQTT options (enable_motion, enable_preview, enable_battery, etc.).

Home Assistant Discovery

[cameras.mqtt.discovery]
topic = "homeassistant"
features = ["floodlight", "camera", "led", "ir", "motion", "reboot", "pt", "battery", "siren"]

Pause & Idle Disconnect

[cameras.pause]
on_motion = true    # pause when no motion
on_client = true    # pause when no RTSP client
timeout = 2.1       # seconds after motion stops before pausing

Add idle_disconnect = true to fully disconnect battery cameras when idle.

Other Commands

neolink image    --config=neolink.toml --file-path=snap.jpg CameraName
neolink battery  --config=neolink.toml CameraName
neolink pir      --config=neolink.toml CameraName [on|off]
neolink reboot   --config=neolink.toml CameraName
neolink status-light --config=neolink.toml CameraName [on|off]
neolink ptz      --config=neolink.toml CameraName control 32 [left|right|up|down|in|out]
neolink ptz      --config=neolink.toml CameraName preset [id]
neolink ptz      --config=neolink.toml CameraName zoom 2.5
neolink talk     --config=neolink.toml --microphone CameraName
neolink encoding --config=neolink.toml CameraName
neolink disk     --config=neolink.toml CameraName

License

Neolink is free software, released under the GNU Affero General Public License v3.

Donations

The original upstream project was created by @thirtythreeforty and maintained by @QuantumEntangledAndy. If you'd like to support the original author's work (note: the upstream repo is currently unmaintained):

ko-fi

About

An RTSP bridge to Reolink IP cameras

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages