- Rust 100%
| .cargo | ||
| src | ||
| test_images | ||
| .env.dist | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| crab-cam.png | ||
| LICENSE | ||
| README.md | ||
| TODO.md | ||
motion-camera
A CLI app intended to run on raspberry pi (or other other computer with a camera attached) and detect motion.
Features
- Multiple strategies for motion detection
- Record short video when motion is detected
- Call configured webhooks when motion is detected
- Support all common camera image formats
Building
You'll likely want to cross-compile this to aarch64 to run on the raspberry pi. To do this, you need to install the appropriate cross-compiler package for your system.
Debian:
$ sudo apt-get install gcc-aarch64-linux-gnu
Arch:
$ sudo pacman -S aarch64-linux-gnu-gcc
You'll also need to install the rust toolchain for the target architecture:
$ rustup target add aarch64-unknown-linux-gnu
Ensure the linker is specified correctly in .cargo/config
Build:
$ cargo build --release --target=aarch64-unknown-linux-gnu
Camera Backends
The default v4l backend captures YUYV frames from USB and other conventional
V4L2 cameras. Select a device with:
$ ./motion-camera --backend v4l --cam /dev/video0
Raspberry Pi CSI cameras use libcamera instead of acting like conventional V4L2 capture devices. On Raspberry Pi OS, install the camera applications and verify that the camera is detected:
$ sudo apt install rpicam-apps
$ rpicam-hello --list-cameras
Then run motion-camera with the rpicam backend:
$ mkdir -p clips
$ ./motion-camera --backend rpicam --out-dir ./clips
This backend starts the system-provided rpicam-vid command and reads its raw
YUV420 output. It captures 1280x720 video at 10 fps and downsamples the luma
plane to 320x180 for motion detection. Recorded clips retain the full 1280x720
frames. Since libcamera remains outside the Rust binary, cross-compilation does
not require Raspberry Pi libraries or a target sysroot.
The --cam option only applies to the v4l backend. The initial rpicam
backend uses camera index 0.
For higher-quality clips without software H.264 encoding, install FFmpeg and
use the rpicam-h264 backend:
$ sudo apt install rpicam-apps ffmpeg
$ mkdir -p clips
$ ./motion-camera --backend rpicam-h264 --out-dir ./clips
This backend asks rpicam-vid for a continuous 1920x1080, 30 fps, High-profile
H.264 stream at 8 Mbps. It decodes one IDR frame every 500 ms for motion
detection and writes the original encoded stream to clips without re-encoding.
Completed raw clips are remuxed to MP4 by a background FFmpeg worker, so camera
capture continues during finalization.
The H.264 backend retains approximately two seconds of encoded video before motion is detected. Clips therefore begin at an independently decodable IDR before the notification timestamp. The pre-roll buffer is also limited to 8 MiB for memory safety. Existing five-second motion expiry and 30-second maximum clip duration behavior still applies.
--interval values above 500 ms skip IDRs for detection while retaining all
encoded frames. Values below 500 ms cannot make detection faster because the
initial H.264 GOP is fixed at 15 frames.
RTSP Live Stream
The rpicam-h264 backend can expose its existing encoded stream over RTSP
without re-encoding it. Start an opt-in listener with:
$ ./motion-camera --backend rpicam-h264 \
--rtsp-listen 0.0.0.0:8554 \
--out-dir ./clips
Then connect to rtsp://CAMERA_ADDRESS:8554/live. For example:
$ ffplay -rtsp_transport tcp rtsp://CAMERA_ADDRESS:8554/live
The initial implementation supports H.264 video over RTSP TCP interleaving. UDP transport, audio, authentication, and TLS are not yet supported. New and lagging clients wait for the next IDR before receiving video, normally no more than 500 ms with the default camera configuration. Up to four clients can be connected at once.
RTSP is disabled unless --rtsp-listen is provided. Binding to 0.0.0.0
exposes the unauthenticated stream on every network interface, so use an
appropriate firewall or bind to a private interface.
FFmpeg is required only when --out-dir is used with rpicam-h264. If remuxing
fails, the hidden raw .h264 file is retained in the output directory for
diagnosis. Temporary raw and .part.mp4 files are removed after a successful
remux.
To verify the raw frame layout expected by motion-camera, capture ten frames on the Pi:
$ rpicam-vid --camera 0 --nopreview --frames 10 --width 1280 --height 720 \
--framerate 10 --codec yuv420 --flush --output test.yuv
$ stat -c '%s bytes' test.yuv
The expected size is 13824000 bytes (10 frames of 1,382,400 bytes each).
Testing
A suite of detection pair images can be found in ./test_images. They're sorted
into directories. All of the image pairs in ./test_images/motion should
trigger motion detection, while all of the pairs in ./test_images/no-motion
should not trigger detection. There are test cases in the detect bundle which
will run every image pair against every detection strategy.
Test for ./test_images/light-change are ignored, because some of the
strategies cannot pass them.
One way to generate test images is to set environment variable
MOTION_CAM_WRITE_DEBUG_IMAGES=true before running the program. When motion is
detected, the pair of images that triggered detection will be saved to the
current working directory. Set the similarity threshold very high to write a
detection pair for frames without motion.
Detection Handlers
There is a DetectionHandler trait for generic detection handlers. Potential
handlers could include:
- webhook: multi-purpose
- discord: send direct message
- twilio: send text message
- email: send email
- cloud uploader: upload detection clip to cloud service
Discord
Set the environment variables:
MOTION_CAM_DISCORD_BOT_TOKENCreate a bot https://discord.com/developers/applications and get the tokenMOTION_CAM_DICORD_DM_USERCurrently needs to be the user ID who will recieve DMs when motion is detected. This user must be in at least one server with the bot. The bot will not send any messages in the server.
When both of these variables are set, the DiscordHandler will be registered.
MQTT / Home Assistant
Set the environment variables:
MOTION_CAM_MQTT_HOSTHostname or IP of the MQTT broker. When unset, the MqttHandler is not registered.MOTION_CAM_MQTT_PORTBroker port (default1883). Plain TCP only; TLS is not compiled in.MOTION_CAM_MQTT_USERNAME/MOTION_CAM_MQTT_PASSWORDBroker credentials.MOTION_CAM_MQTT_CLIENT_IDDefaults tomotion-camera-<location>.
The handler publishes a retained Home Assistant MQTT discovery config, so a
binary_sensor.<location>_motion entity (device class motion) appears in
Home Assistant automatically with no YAML required. Motion start publishes
ON and motion end publishes OFF to
motion-camera/<location>/motion/state. Availability is tracked on
motion-camera/<location>/status using a broker last-will, so the entity is
marked unavailable if motion-camera disconnects ungracefully. Topics are
derived from the --location CLI flag.
A live integration test can be run against a local broker with:
$ mosquitto -p 18883 &
$ MOTION_CAM_MQTT_TEST_BROKER=127.0.0.1:18883 cargo test broker
Doorbell Button
When MQTT is configured, a doorbell button is also published as a Home
Assistant MQTT event entity (event.<location>_button, device class
doorbell) under the same device as the motion sensor. Each press publishes
{"event_type": "button_press"} to motion-camera/<location>/button/event.
The button is read from a GPIO pin that is externally pulled up to 3.3 V and
shorts to ground when pressed (active low). Set the BCM pin number with
MOTION_CAM_DOORBELL_GPIO_PIN (default 4, physical pin 7). Presses are
detected on the falling edge with a 250 ms debounce. The internal pull-up is
enabled as a fallback, so the pin can be left unwired without generating
phantom presses; the external pull-up dominates once the button circuit is
connected. If the GPIO hardware is unavailable (e.g. when developing on
another machine), a warning is logged and the rest of the program keeps
running.
To exercise the Home Assistant integration without button hardware, set
MOTION_CAM_DOORBELL_TEST_INTERVAL to a number of seconds; a button_press
event is then published on that interval.
Embedded Web Server
A webserver is hosted on port 4444 with an API endpoint at PUT /detecting.
Send a boolean value (1, 0, true, false, etc) to turn motion detection on or
off. This can be used to automate turning on detection when you leave your house
for example.
In the future, this server may host a web UI to view or manage settings, and hopefully to view the live video stream eventually (via HLS or something similar).
Logging
This project uses env_logger with a default level of info. If you want to
see debug logs then set RUST_LOG=debug, or just pass the -v flag.