English | δΈζ
A fast, single-binary IP camera CLI for snapshots, recording, and motion detection. π·
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 | |
| 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 |
- 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 checks β
camgrab doctorverifies connectivity, stream info, and system configuration - JSON output β Pass
--jsonon any command for machine-readable output - Cross-platform β Linux, macOS, Windows; pre-built binaries for 5 targets
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/brew tap justinhuangcode/tap
brew install camgrabdocker pull ghcr.io/justinhuangcode/camgrab:latestcargo install --git https://github.com/justinhuangcode/camgrab -p camgrab-cligit clone https://github.com/justinhuangcode/camgrab.git
cd camgrab
cargo build --release
# Binary at target/release/camgrabRequirements: Rust 1.88+
# 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| 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 |
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'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 120Monitor 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.logControl 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-presetsStart, 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 |
# 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 startThe Docker image supports linux/amd64 and linux/arm64.
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.
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[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 * * * *"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
- 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
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
- 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
# 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- Decrease sensitivity:
--sensitivity low - Increase threshold:
--threshold 0.5 - Use motion zones to exclude problematic areas
- Increase cooldown to reduce event spam:
--cooldown 10
- 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
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_cliprustfmtwith default settings- Clippy pedantic lints enabled (configured in workspace
Cargo.toml) - No unsafe code (
#![deny(unsafe_code)])
Contributions are welcome! Please see the guidelines:
- Fork the repository and create a feature branch
- Write tests for new functionality
- Run
cargo fmtandcargo clippybefore submitting - Submit a pull request with a clear description of changes
See CHANGELOG.md for release history.