# ipcam
A command-line tool for managing IP cameras (Tapo, Reolink) via RTSP and vendor APIs.
## Features
- Capture snapshots from individual cameras or all cameras in parallel
- Assemble multi-camera snapshots into a single tiled grid image
- Record video clips and build timelapses using ffmpeg
- Print or pipe RTSP stream URLs (main and sub streams)
- Pan/tilt/zoom control via ONVIF (Tapo) or Reolink API
- Poll motion detection events and watch for status changes
- Health-check all cameras and run a hook command on status change
- End-to-end camera test (network reachability, RTSP URL, snapshot)
- Discover cameras on the local network via ONVIF WS-Discovery
- Interactive `init` wizard that auto-discovers and configures cameras
- Frigate NVR integration: list events and fetch latest snapshots
- go2rtc restream proxy support for cameras behind a proxy
- JSON output for every command (`--json`)
- Shell completions for bash, zsh, and fish
## Installation
### Prerequisites
- Rust toolchain (install via [rustup](https://rustup.rs))
- [ffmpeg](https://ffmpeg.org/download.html) — required for snapshots, recording, and timelapse
### Build and install
```bash
cargo install --path .
```
## Quick Start
Run the interactive setup wizard to discover cameras on your network and generate a config file:
```bash
ipcam init
```
Or use `--auto` to skip prompts and generate a config from detected cameras:
```bash
ipcam init --auto
```
Once configured, a few common commands:
```bash
# List configured cameras
ipcam list
# Capture a snapshot
ipcam snapshot front-door
# Check all cameras are reachable
ipcam status
# Run an end-to-end test on all cameras
ipcam test
```
## Configuration
The config file is TOML and lives at:
- **macOS:** `~/Library/Application Support/ipcam/config.toml`
- **Linux:** `~/.config/ipcam/config.toml`
Run `ipcam config` to print the exact path on your system.
### Example config
```toml
[go2rtc]
host = "192.168.1.10"
port = 8554 # default
[frigate]
host = "192.168.1.11"
port = 5001 # default
[[cameras]]
name = "front-door"
type = "reolink"
host = "192.168.1.101"
username = "admin"
password = "your-password"
[[cameras]]
name = "backyard"
type = "tapo"
host = "192.168.1.102"
username = "admin"
password = "your-password"
# Optional: override RTSP port (default: 554)
rtsp_port = 554
# Optional: override ONVIF port (default: 2020 for Tapo, 8000 for Reolink)
onvif_port = 2020
# Optional: use a go2rtc restream instead of direct RTSP
go2rtc_stream = "backyard"
# Optional: Frigate camera name if it differs from the config name
frigate_name = "backyard_cam"
```
### Config fields
| `name` | yes | Unique identifier used in all commands |
| `type` | yes | `tapo` or `reolink` |
| `host` | yes | IP address of the camera |
| `username` | no | Camera username (default: `admin`) |
| `password` | no | Camera password |
| `rtsp_port` | no | RTSP port (default: 554) |
| `onvif_port` | no | ONVIF port (default: 2020 for Tapo, 8000 for Reolink) |
| `go2rtc_stream` | no | go2rtc stream name when using a restream proxy |
| `frigate_name` | no | Frigate camera name (default: config `name` with `-` replaced by `_`) |
## Commands
| `init` | Interactive setup wizard; discovers cameras via ONVIF and writes config |
| `list` | List all configured cameras |
| `info <camera>` | Show camera model and firmware (Reolink) or basic info (Tapo) |
| `snapshot <camera>` | Capture a JPEG snapshot |
| `snapshot --all` | Capture snapshots from all cameras in parallel |
| `snapshot --grid` | Capture from all cameras and tile into a single image |
| `snapshot --every <interval>` | Capture snapshots on a repeating interval |
| `snapshot-all` | Alias for `snapshot --all` |
| `stream <camera>` | Print the RTSP URL, or pipe to a file with `--output` |
| `record <camera>` | Record a video clip (default 30 s) |
| `timelapse <camera>` | Capture frames at an interval and encode to MP4 |
| `status [camera]` | Check which cameras are online and report latency |
| `test [camera]` | End-to-end test: network, RTSP URL, snapshot |
| `watch` | Poll camera health continuously; run a hook on status changes |
| `events <camera>` | Show motion detection status; use `--watch` to poll continuously |
| `ptz <camera> <action>` | Pan/tilt/zoom control: `left`, `right`, `up`, `down`, `stop`, `preset` |
| `discover` | Scan the network for ONVIF cameras |
| `frigate events` | List recent Frigate NVR events |
| `frigate snapshot <camera>` | Fetch the latest snapshot from Frigate |
| `config` | Show the config file path and whether it exists |
| `completions <shell>` | Print a shell completion script |
All commands accept `--json` for machine-readable output and `--config <path>` to override the config file location.
## Examples
### Snapshot
```bash
# Single camera, default filename (<camera>_<timestamp>.jpg)
ipcam snapshot front-door
# Single camera, custom output path
ipcam snapshot front-door --output /tmp/front.jpg
# All cameras saved to a directory
ipcam snapshot --all --output-dir /tmp/cams/
# Tiled grid of all cameras
ipcam snapshot --grid --output-dir /tmp/
# Capture every 5 minutes, save timestamped files
ipcam snapshot front-door --every 5m --output-dir /tmp/front-door/
```
### Status and test
```bash
# Check all cameras
ipcam status
# Check a single camera
ipcam status front-door
# Full end-to-end test (network + RTSP + snapshot)
ipcam test
# Test a single camera, JSON output
ipcam test front-door --json
```
### Watch
```bash
# Poll every 30 s, print status changes to stdout
ipcam watch
# Poll every minute, run a script on any status change
ipcam watch --interval 1m --exec 'notify-send "$CAMERA_NAME is $CAMERA_STATUS"'
```
The `--exec` command receives these environment variables:
| `CAMERA_NAME` | Camera name from config |
| `CAMERA_HOST` | Camera IP address |
| `CAMERA_STATUS` | `online` or `offline` |
| `CAMERA_DETAIL` | Human-readable detail (model or error message) |
### Timelapse
```bash
# 1-hour capture with a snapshot every 30 s, encoded to timelapse.mp4
ipcam timelapse front-door --interval 30s --duration 1h --output front-door.mp4
# Keep individual frames as well
ipcam timelapse front-door --interval 1m --duration 4h \
--output timelapse.mp4 --output-dir ./frames/
```
### Shell completions
```bash
# zsh
ipcam completions zsh > ~/.zfunc/_ipcam
# bash
ipcam completions bash > /etc/bash_completion.d/ipcam
# fish
ipcam completions fish > ~/.config/fish/completions/ipcam.fish
```
### PTZ control
```bash
# Pan left at speed 5 (default)
ipcam ptz backyard left
# Tilt up at speed 8
ipcam ptz backyard up --speed 8
# Go to preset position 1
ipcam ptz backyard preset 1
# Stop movement
ipcam ptz backyard stop
```
## Supported Cameras
### Tapo (TP-Link)
- Snapshots captured via ffmpeg over RTSP (sub-stream by default)
- PTZ control via ONVIF SOAP over HTTP (port 2020 by default)
- RTSP streams: `stream1` (main), `stream2` (sub)
- Motion detection: not supported (Tapo does not expose an API for this)
- Health check: TCP connect to the RTSP port
### Reolink
- Snapshots fetched via the Reolink HTTP/JSON API (`/cgi-bin/api.cgi?cmd=Snap`)
- PTZ control via the Reolink `PtzCtrl` API
- Motion detection via `GetMdState` API
- RTSP streams: `h264Preview_01_main` (main), `h264Preview_01_sub` (sub)
- Health check: `GetDevInfo` API call (also returns model name)
- Device info (model, firmware) available via `GetDevInfo`
## Integration
### go2rtc
If your cameras are behind a [go2rtc](https://github.com/AlexxIT/go2rtc) restream proxy, add a `[go2rtc]` section to your config and set `go2rtc_stream` on each camera. ipcam will use the proxy RTSP URL (`rtsp://<host>:<port>/<stream>`) instead of connecting to the camera directly.
Sub-stream URLs are constructed by appending `_sub` to the stream name (e.g. `backyard_sub`).
### Frigate NVR
Add a `[frigate]` section to your config pointing at your [Frigate](https://frigate.video) instance. Then use the `frigate` subcommand:
```bash
# List the 20 most recent events across all cameras
ipcam frigate events --limit 20
# Filter by camera
ipcam frigate events --camera front_door
# Save the latest Frigate snapshot for a camera
ipcam frigate snapshot front_door --output /tmp/latest.jpg
```
Frigate camera names use underscores (e.g. `front_door`). Set `frigate_name` in the camera config if the name differs from your ipcam name.
## License
MIT