Runs on the NUC (or any machine with more CPU). Consumes video streams from the Pi (e.g. Hootcam Streamer) and handles:
- Motion detection
- Motion-triggered recording (pictures and movies)
- Snapshots, events, file browser
- REST API and MJPEG live view for the Hootcam UI
No direct camera access—video is pulled from stream_url per camera. stream_url can be an RTSP URL or an MJPEG-over-HTTP URL (e.g. from Hootcam Streamer with Spyglass: http://<pi-ip>:8080/stream and :8081/stream).
Part of the 3-part Hootcam setup:
- Pi – Hootcam Streamer – Publishes 2 MJPEG streams (cam0 on port 8080, cam1 on 8081). No motion or recording.
- NUC (this app) – Pulls those streams, runs motion detection, records to disk, serves API and MJPEG to the UI.
- UI – Hootcam UI – Web interface; talks only to this app (the NUC).
- Python 3.10+
- OpenCV (for stream capture), FastAPI, and other deps in
requirements.txt - Network access to the Pi’s stream URLs (e.g.
http://192.168.1.10:8080/streamandhttp://192.168.1.10:8081/streamfor Hootcam Streamer with Spyglass)
-
Create a virtualenv and install dependencies (recommended; keeps deps isolated from system Python):
cd hootcam-motion python3 -m venv .venv source .venv/bin/activate # On Windows: .venv\Scripts\activate pip install -r requirements.txt
-
Configure each camera’s stream_url
Before or after first run, set the stream URL for each camera. The UI talks to this app, so use the UI’s Cameras → Camera 0 (or 1) → Config and set Stream URL to the Pi’s stream. For Hootcam Streamer (Spyglass), use MJPEG HTTP URLs:
- Camera 0:
http://192.168.1.10:8080/stream - Camera 1:
http://192.168.1.10:8081/stream
Replace
192.168.1.10with your Pi’s IP. You can also set these via the API:PATCH /cameras/0/configandPATCH /cameras/1/configwith{ "stream_url": "http://..." }. - Camera 0:
-
Run (with venv activated)
uvicorn hootcam_motion.main:app --host 0.0.0.0 --port 8080
Use
--host 0.0.0.0so the UI and other clients can reach the API from other machines. -
Point the Hootcam UI at this server
In the UI’s
.env, set:VITE_HOOTCAM_STREAMER_URL=http://<nuc-ip>:8080
The UI will then use this app for all API calls and for the live MJPEG streams (which this app generates from the Pi’s stream frames).
- Global config – Target directory for recordings, log level, stream quality, stream max rate, etc. Use the UI Config page or
GET/PATCH /config. - Per-camera config – Motion threshold, event gap, pre/post capture, picture/movie output, filenames, and stream_url (e.g. MJPEG
http://<pi>:8082/streamor RTSP). Use the UI Cameras → Config orGET/PATCH /cameras/{id}/config. - Storage – Where recordings are saved. Use Storage in the UI or
GET/PATCH /storage.
Stream resolution and trickle-down – When video comes from a stream_url (e.g. Hootcam Streamer on the Pi), the resolution is normally set on the Pi. You can either edit the streamer’s config.yaml on the Pi and restart, or use trickle-down: set Streamer API URL in global config (Config page) to http://<pi-ip>:8084 (the streamer’s API port). Then when you save Cameras → Config (e.g. width, height, framerate, focus: autofocus mode and lens position for fixed focus), Motion pushes those values to the streamer; the streamer updates its config and restarts the Spyglass streams. No need to SSH to the Pi to change resolution or focus.
Config is stored in SQLite (and optionally in a JSON file). Default data directory is derived from HOOTCAM_TARGET_DIR or the current working directory.
Timestamps – All event and file timestamps (started_at, ended_at, file timestamps, and filenames) use US Central time (America/Chicago, CST/CDT).
Motion is detected by frame differencing: each new frame is compared to a reference frame (grayscale). Pixels whose intensity change is above a noise level (default 32) are counted as “changed.” That count is the changed-pixel count.
-
Motion threshold – Number of changed pixels required to declare motion. Default is 1500. So if 1500 or more pixels change (above the noise level), the frame is considered “motion.”
- Lower threshold (e.g. 500) → more sensitive; small movements or lighting changes can trigger.
- Higher threshold (e.g. 3000) → less sensitive; only larger motion triggers.
-
Threshold maximum – If set (non-zero), motion is only declared when the count is between threshold and threshold_maximum. Used to ignore huge changes (e.g. headlights or full-frame flash).
-
Minimum motion frames – Motion must be seen for this many consecutive frames before an event starts (default 1). Reduces false triggers from single-frame glitches.
So: “motion” = (changed pixels ≥ threshold, and ≤ threshold_max if set) for at least minimum_motion_frames frames in a row.
Recording is grouped into events. One event = one segment of continuous (or nearly continuous) motion.
-
Event start – When motion is first detected, an event starts. Optionally, the last pre_capture frames (before motion) are included so you see the lead-in.
-
While motion continues – Every frame with motion is recorded. After each motion frame, post_capture extra frames are also recorded (so you get a short tail after motion stops).
-
Event end – When there has been no motion for event_gap seconds (default 60), the event ends. The movie (and any pictures) are written to disk and the event is closed.
So motion is recorded until there have been event_gap seconds with no motion. For example, with default event_gap = 60:
- Motion at 0:00 → recording starts.
- Motion at 0:10, 0:20, 0:30 → still one event, keeps recording.
- Last motion at 0:35 → recording continues for post_capture frames, then the “no motion” timer starts.
- At 1:35 (60 s after last motion) the event ends and the movie is saved.
Settings that affect length:
| Setting | Default | Effect |
|---|---|---|
| Event gap | 60 s | Seconds of no motion before the event (and thus the recording) ends. |
| Post-capture | 0 frames | Extra frames recorded after each motion frame. Increase (e.g. framerate × 5) for a longer tail after motion stops. |
| Pre-capture | 0 frames | Frames from before motion to prepend to the event (rolling buffer). |
| Movie max time | 120 s (config) | Intended cap on movie length; see API/schema. In practice the event usually ends first due to event_gap. |
To get shorter clips: decrease event_gap (e.g. 15–30 s). To keep recording longer after motion stops: increase post_capture (e.g. 75 frames at 15 fps ≈ 5 s of tail).
To run Hootcam Motion automatically on the NUC (or any Linux server) after reboot:
-
Copy the systemd unit file (from the repo root):
sudo cp contrib/hootcam-motion.service /etc/systemd/system/
-
Edit the unit file to match your install:
sudo nano /etc/systemd/system/hootcam-motion.service
- WorkingDirectory: path to your hootcam-motion clone (e.g.
/home/nucuser/hootcam-motionor/opt/hootcam-motion). - ExecStart: full path to uvicorn from your venv. Example if the app is in
/home/nucuser/hootcam-motion:WorkingDirectory=/home/nucuser/hootcam-motion ExecStart=/home/nucuser/hootcam-motion/.venv/bin/uvicorn hootcam_motion.main:app --host 0.0.0.0 --port 8080
- User/Group: use a dedicated user (e.g. create with
sudo useradd -r -s /bin/false hootcam) or your normal user. Ensure that user can read the install dir and write to the recording target dir.
- WorkingDirectory: path to your hootcam-motion clone (e.g.
-
Optional – recordings on a specific path: Create an env file and uncomment it in the unit:
echo 'HOOTCAM_TARGET_DIR=/mnt/storage/hootcam-motion' | sudo tee /etc/hootcam-motion.env
In the unit file, uncomment:
EnvironmentFile=/etc/hootcam-motion.env -
Enable and start the service:
sudo systemctl daemon-reload sudo systemctl enable hootcam-motion sudo systemctl start hootcam-motion sudo systemctl status hootcam-motion
Useful commands: sudo systemctl stop hootcam-motion, sudo systemctl restart hootcam-motion, sudo journalctl -u hootcam-motion -f
See contrib/README.md for more detail.
- Pi (Hootcam Streamer): Runs two Spyglass instances; publishes MJPEG at
http://<pi>:8082/streamandhttp://<pi>:8083/stream. - NUC (this app): Opens each camera’s
stream_url(MJPEG HTTP or RTSP), reads frames in a background thread, runs motion detection, updateslatest_jpegfor the MJPEG stream, and drives recording (RecordingSession). Serves the same REST API as the legacy Hootcam Server. - UI: Points at the NUC only; never talks to the Pi directly.
Reused from hootcam-server
- Motion detection, recording (RecordingSession, pictures/movies, scripts), database, auth, API routes, MJPEG streaming (from latest frame), config load/save.
- Removed: Pi camera code (Picamera2, DualCameraService), hardware resolution listing, camera restart. Replaced with a frame source that reads from
stream_url(MJPEG HTTP or RTSP).
-
"no frame from stream for too long; marking failed" – The app is not receiving any frames from the camera’s
stream_url. Check: (1) Stream URL is set in the UI (Cameras → Camera 0/1 → Config) to the Pi’s stream (e.g.http://<pi-ip>:8082/streamandhttp://<pi-ip>:8083/stream). (2) Hootcam Streamer is running on the Pi and both streams are up. (3) The NUC can reach the Pi (e.g.curl http://<pi-ip>:8082/streamfrom the NUC). (4) No firewall blocking the stream ports. The log line includesstream_url=...so you can confirm which URL is being used. -
"Connection refused" / "Stream ends prematurely" (FFmpeg/OpenCV messages) – The stream reader retries automatically with exponential backoff (5 s, then 7.5 s, 11 s, … up to 60 s). You’ll see log lines like "Camera N: stream connection failed (connection refused or unreachable). Retrying in X s" and "Camera N: stream ended unexpectedly, reconnecting in 3 s". No need to restart the app; once the Pi or stream is back, the reader reconnects.
Same API as Hootcam Server: /docs for Swagger, /redoc for ReDoc. Key endpoints:
GET/PATCH /config– Global configGET/PATCH /storage– Recording pathGET/PATCH /cameras/{id}/config– Per-camera config (includingstream_url)GET /cameras/{id}/stream– MJPEG live streamGET /cameras/{id}/status– Connected = receiving frames from stream_url (Pi)POST /cameras/{id}/detection/startand.../pauseGET /events,GET /files, etc.
- Hootcam Streamer – Run on the Pi to publish the MJPEG streams this app consumes. Configure each camera’s
stream_urltohttp://<pi-ip>:8082/streamandhttp://<pi-ip>:8083/stream(or whatever ports you use). - Hootcam UI – Point its
VITE_HOOTCAM_STREAMER_URLat this app (NUC). - Hootcam Server – Legacy all-in-one Pi backend (cameras + motion + API). This app (Hootcam Motion) is the split alternative that offloads work to the NUC.