#camera #rtsp #motion-detection #camera-capture #detect #onvif #clip #snap #ptz #doctor

app camgrab-cli

CLI interface for camgrab - capture, record, and monitor RTSP/ONVIF cameras

2 stable releases

new 1.0.1 Mar 2, 2026

#230 in Network programming

MIT license

360KB
7.5K SLoC

camgrab

English | 中文

CI Release Crates.io License: MIT Rust Docker GitHub Stars Last Commit Issues Platform

A modern CLI tool for capturing snapshots, recording clips, detecting motion, and managing IP cameras via RTSP/ONVIF.

Why camgrab?

Most camera tools either depend on heavy external binaries (ffmpeg) or are full-blown NVR systems you have to self-host. camgrab is a single binary with zero external dependencies — connect to your cameras and start working in seconds.

camgrab ffmpeg + cron motion Frigate ZoneMinder
Single binary, zero deps Yes No (ffmpeg ~50MB) No (C libs) No (Docker stack) No (LAMP stack)
RTSP snapshot + clip Built-in Manual scripting No clips Via API Via API
Native motion detection Built-in (zones + filters) Scene filter only Yes ML-based Zones
ONVIF PTZ control Built-in No No Via MQTT Limited
Multi-camera concurrent Async (tokio) Sequential scripts Per-process Yes Yes
Daemon + cron scheduler Built-in External cron Built-in Always-on Always-on
Notifications Webhook / MQTT / Email DIY Limited MQTT Email
Cloud storage (S3) Built-in DIY No No No
Resource usage ~20MB RAM ~100MB+ ~50MB+ ~2GB+ ~500MB+
Setup complexity camgrab add && camgrab snap Write scripts Edit config files Docker compose LAMP + config

Features

  • Zero external dependencies — Pure Rust RTSP client via retina; no ffmpeg required
  • Snapshot capture — Single-frame capture with H.264 decode (openh264) and MJPEG passthrough
  • Video clip recording — Duration-based RTSP recording with AVCC-to-Annex B conversion
  • Native motion detection — Frame differencing with configurable zones, sensitivity, and filters
  • Multi-camera concurrent — Snap or watch all cameras simultaneously with tokio async runtime
  • ONVIF discovery + PTZ — Auto-discover cameras on the network; pan, tilt, zoom, and preset management
  • Built-in notifications — Webhook, MQTT, and email alerting out of the box
  • Daemon mode with scheduler — Cron-based automated captures and continuous monitoring
  • Cloud storage — Local filesystem and S3/MinIO backends with atomic writes
  • Health checkscamgrab doctor verifies connectivity, stream info, and system configuration
  • JSON output — Pass --json on any command for machine-readable output
  • Cross-platform — Linux, macOS, Windows; pre-built binaries for 5 targets

Installation

Download the latest release for your platform from GitHub Releases:

Platform Archive
Linux x86_64 camgrab-v*-linux-x86_64.tar.gz
Linux ARM64 camgrab-v*-linux-arm64.tar.gz
macOS Intel camgrab-v*-macos-x86_64.tar.gz
macOS Apple Silicon camgrab-v*-macos-arm64.tar.gz
Windows x86_64 camgrab-v*-windows-x86_64.zip
# Example: Linux x86_64
curl -LO https://github.com/justinhuangcode/camgrab/releases/latest/download/camgrab-v1.0.0-linux-x86_64.tar.gz
tar xzf camgrab-v1.0.0-linux-x86_64.tar.gz
sudo mv camgrab /usr/local/bin/

Homebrew (macOS / Linux)

brew tap justinhuangcode/tap
brew install camgrab

Docker

docker pull ghcr.io/justinhuangcode/camgrab:latest

Via Cargo

cargo install --git https://github.com/justinhuangcode/camgrab -p camgrab-cli

From source

git clone https://github.com/justinhuangcode/camgrab.git
cd camgrab
cargo build --release
# Binary at target/release/camgrab

Requirements: Rust 1.88+

Quick Start

# 1. Add a camera (interactive)
camgrab add

# Or specify directly
camgrab add front-door \
  --host 192.168.1.100 --port 554 \
  --username admin --password secret123

# 2. Capture a snapshot
camgrab snap front-door
camgrab snap front-door --out snapshot.jpg --format jpeg

# 3. Record a 30-second clip
camgrab clip front-door --duration 30 --out clip.mp4

# 4. Watch for motion
camgrab watch front-door --threshold 0.3 --cooldown 5

# 5. Discover cameras on the network
camgrab discover

# 6. Check camera health
camgrab doctor --all

Commands

Command Description
add [NAME] Add or update a camera (interactive or flags)
list List all configured cameras with status
snap <CAMERA> Capture a single-frame snapshot
clip <CAMERA> Record a video clip for a specified duration
watch <CAMERA> Monitor a camera feed for motion events
discover Auto-discover ONVIF cameras on the local network
doctor [CAMERA] Verify camera connectivity and stream info
ptz <CAMERA> Pan-Tilt-Zoom control via ONVIF
daemon start|stop|status|logs Manage the background daemon for scheduled operations

camgrab snap

Capture a single frame from a camera and save as an image.

camgrab snap <CAMERA> [OPTIONS]
Flag Default Description
-o, --out <PATH> auto Output path for snapshot
-f, --format <FMT> jpeg Image format: jpeg, png, webp
-t, --timeout <SEC> 10 Connection timeout in seconds
--transport <PROTO> tcp RTSP transport: tcp, udp
--json false Output result as JSON
camgrab snap front-door --out /backup/snapshot.png --format png
camgrab snap front-door --json | jq '.path'

camgrab clip

Record a video clip for a specified duration from an RTSP stream.

camgrab clip <CAMERA> [OPTIONS]
Flag Default Description
-o, --out <PATH> auto Output path for video clip
-d, --duration <SEC> 30 Recording duration in seconds
-f, --format <FMT> mp4 Video format: mp4, mkv, avi
--transport <PROTO> tcp RTSP transport: tcp, udp
--json false Output result as JSON
camgrab clip front-door --duration 60
camgrab clip front-door --out evidence.mp4 --duration 120

camgrab watch

Monitor a camera feed for motion, trigger actions on detection events.

camgrab watch <CAMERA> [OPTIONS]
Flag Default Description
-t, --threshold <FLOAT> 0.3 Motion detection threshold 0.0-1.0
-c, --cooldown <SEC> 5 Cooldown between events
--sensitivity <LEVEL> medium Detection sensitivity: low, medium, high
--zones-from <PATH> Path to JSON zone configuration file
--action <CMD> Command template to execute on detection
--json false Output events as JSON

Environment variables in actions:

  • $CAMGRAB_CAMERA — Camera name
  • $CAMGRAB_SCORE — Motion detection score (0.0-1.0)
  • $CAMGRAB_TIME — Event timestamp (RFC 3339)
  • $CAMGRAB_ZONE — Zone name (if using zones)
# Trigger webhook on motion
camgrab watch front-door --action 'curl -X POST https://api.home.com/alert'

# Capture snapshot on motion
camgrab watch front-door \
  --action 'camgrab snap $CAMGRAB_CAMERA --out /alerts/$CAMGRAB_CAMERA-$CAMGRAB_TIME.jpg'

# Stream JSON events for log aggregation
camgrab watch front-door --json | tee -a motion_events.log

camgrab ptz

Control Pan-Tilt-Zoom cameras via ONVIF protocol.

camgrab ptz <CAMERA> [OPTIONS]
Flag Description
--pan <FLOAT> Pan position -1.0 to 1.0
--tilt <FLOAT> Tilt position -1.0 to 1.0
--zoom <FLOAT> Zoom level 0.0 to 1.0
--goto-preset <N> Move to saved preset
--save-preset <N> Save current position as preset
--preset-name <NAME> Name for saved preset
--list-presets List all available presets
--home Return to home position
--stop Stop current movement
camgrab ptz front-door --pan 0.5 --tilt 0.2 --zoom 0.8
camgrab ptz front-door --goto-preset 2
camgrab ptz front-door --list-presets

camgrab daemon

Start, stop, and manage the background daemon for scheduled operations.

camgrab daemon <SUBCOMMAND>
Subcommand Description
start Start the daemon process
stop Stop the daemon process
restart Restart the daemon
status Check daemon status
logs View daemon logs (--follow for real-time)
reload Reload configuration without restart

Docker Usage

# Pull from GitHub Container Registry
docker pull ghcr.io/justinhuangcode/camgrab:latest

# One-off snapshot
docker run --rm --network host \
  -v ./config.toml:/etc/camgrab/config.toml:ro \
  -v ./output:/var/lib/camgrab \
  ghcr.io/justinhuangcode/camgrab:latest \
  snap front-door -o /var/lib/camgrab/snap.jpg

# Daemon mode for continuous monitoring
docker run -d --name camgrab --restart unless-stopped \
  --network host \
  -v ./config.toml:/etc/camgrab/config.toml:ro \
  -v camgrab-data:/var/lib/camgrab \
  ghcr.io/justinhuangcode/camgrab:latest \
  daemon start

The Docker image supports linux/amd64 and linux/arm64.

Configuration

Configuration files are stored in XDG-compliant locations:

  • Linux/macOS: ~/.config/camgrab/config.yaml
  • Windows: %APPDATA%\camgrab\config.yaml

Supports YAML, TOML, and JSON formats.

Example (YAML)

cameras:
  front-door:
    name: "Front Door Camera"
    host: "192.168.1.100"
    port: 554
    username: "admin"
    password: "secret123"
    protocol: rtsp
    transport: tcp
    stream_type: main
    timeout_secs: 10

  back-yard:
    name: "Back Yard Camera"
    host: "192.168.1.101"
    port: 554
    username: "admin"
    password: "secret456"
    custom_path: "/stream1"

storage:
  backends:
    - type: local
      path: "/var/lib/camgrab/storage"
    - type: s3
      bucket: "camgrab-clips"
      region: "us-east-1"
      prefix: "cameras"

notifications:
  webhook:
    enabled: true
    url: "https://api.example.com/webhook"
    method: POST
    headers:
      Authorization: "Bearer token123"
  mqtt:
    enabled: true
    broker: "mqtt.example.com:1883"
    topic: "camgrab/events"

daemon:
  schedules:
    - name: "hourly-snapshots"
      camera: "front-door"
      action: "snap"
      cron: "0 * * * *"
    - name: "night-motion-watch"
      camera: "back-yard"
      action: "watch"
      cron: "0 22 * * *"
      duration_secs: 28800
      threshold: 0.25

Example (TOML)

[cameras.front-door]
name = "Front Door Camera"
host = "192.168.1.100"
port = 554
username = "admin"
password = "secret123"
protocol = "rtsp"
transport = "tcp"
stream_type = "main"
timeout_secs = 10

[storage]
[[storage.backends]]
type = "local"
path = "/var/lib/camgrab/storage"

[notifications.webhook]
enabled = true
url = "https://api.example.com/webhook"
method = "POST"

[[daemon.schedules]]
name = "hourly-snapshots"
camera = "front-door"
action = "snap"
cron = "0 * * * *"

Architecture

The project is organized as a Cargo workspace with three crates:

camgrab/
├── Cargo.toml                    # Workspace manifest
├── Dockerfile                    # Multi-stage Docker build (multi-arch)
├── deny.toml                     # Cargo deny (license + advisory audit)
├── Formula/camgrab.rb            # Homebrew formula
├── .github/workflows/
│   ├── ci.yml                    # Check, Clippy, Fmt, Test, Build, Deny
│   └── release.yml               # Build → Release → Docker → Homebrew
│
├── crates/
│   ├── camgrab-cli/              # CLI binary
│   │   └── src/
│   │       ├── main.rs           # Entry point, clap command dispatch
│   │       └── commands/
│   │           ├── snap.rs       # Snapshot capture
│   │           ├── clip.rs       # Video clip recording
│   │           ├── watch.rs      # Motion detection monitor
│   │           ├── discover.rs   # ONVIF camera discovery
│   │           ├── doctor.rs     # Health check diagnostics
│   │           ├── ptz.rs        # PTZ control
│   │           ├── add.rs        # Camera configuration
│   │           └── list.rs       # Camera listing
│   │
│   ├── camgrab-core/             # Core library
│   │   └── src/
│   │       ├── lib.rs
│   │       ├── camera.rs         # Camera model + management
│   │       ├── error.rs          # Error types
│   │       ├── config/           # YAML/TOML/JSON config (de)serialization
│   │       ├── rtsp/             # Pure Rust RTSP client (retina + openh264)
│   │       ├── motion/           # Frame differencing, zones, filters
│   │       ├── onvif/            # SOAP/WS-Security device + PTZ + discovery
│   │       ├── storage/          # Local + S3/MinIO backends
│   │       └── notify/           # Webhook, MQTT, email
│   │
│   └── camgrab-daemon/           # Background daemon
│       └── src/
│           ├── lib.rs
│           ├── server.rs         # Axum HTTP API
│           └── scheduler.rs      # Cron job scheduling
│
└── examples/
    ├── rtsp_snapshot.rs
    └── rtsp_clip.rs

Performance

  • Memory: ~20MB base + ~5MB per active RTSP stream
  • CPU: ~2-5% per 1080p stream (motion detection disabled)
  • Motion detection overhead: ~8-15% additional CPU per stream
  • Concurrent streams: Tested with 32+ simultaneous connections
  • Startup time: <100ms cold start

Camera Compatibility

Tested with:

  • Hikvision DS-2CD2xxx series
  • Dahua IPC-HDW/HFW series
  • Reolink RLC-xxx series
  • Amcrest IP2M/IP4M series
  • Ubiquiti UniFi Protect cameras
  • Generic ONVIF-compliant cameras

Security Considerations

  • Credentials are stored in plain text in configuration files — ensure proper file permissions (chmod 600)
  • RTSP streams are typically unencrypted — use VPN or VLAN for sensitive deployments
  • RTSPS (RTSP over TLS) support available for encrypted streams
  • ONVIF communications support WS-Security with timestamp validation
  • Motion detection actions execute shell commands — sanitize inputs carefully

Troubleshooting

Connection Issues

# Verify camera is reachable
ping 192.168.1.100

# Test RTSP port
nc -zv 192.168.1.100 554

# Run doctor command with verbose logging
camgrab -v doctor front-door

Motion Detection False Positives

  • Decrease sensitivity: --sensitivity low
  • Increase threshold: --threshold 0.5
  • Use motion zones to exclude problematic areas
  • Increase cooldown to reduce event spam: --cooldown 10

Performance Issues

  • Use UDP transport on local networks: --transport udp
  • Use sub-stream for lower bandwidth: stream_type: sub
  • Increase timeout for slow cameras: timeout_secs: 30

Development

git clone https://github.com/justinhuangcode/camgrab.git
cd camgrab

# Build
cargo build --release

# Test
cargo test --workspace

# Lint
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings

# Run examples
cargo run --example rtsp_snapshot
cargo run --example rtsp_clip

Code Style

  • rustfmt with default settings
  • Clippy pedantic lints enabled (configured in workspace Cargo.toml)
  • No unsafe code (#![deny(unsafe_code)])

Contributing

Contributions are welcome! Please see the guidelines:

  1. Fork the repository and create a feature branch
  2. Write tests for new functionality
  3. Run cargo fmt and cargo clippy before submitting
  4. Submit a pull request with a clear description of changes

Changelog

See CHANGELOG.md for release history.

Acknowledgments

  • retina — Pure Rust RTSP client library
  • openh264 — H.264 decoding
  • tokio — Asynchronous runtime
  • clap — Command-line argument parsing

License

MIT

Dependencies

~62–87MB
~1.5M SLoC