{"id":50432686,"url":"https://github.com/ljufa/esp32_camera","last_synced_at":"2026-05-31T15:01:19.786Z","repository":{"id":357930231,"uuid":"1235899607","full_name":"ljufa/esp32_camera","owner":"ljufa","description":"Self-hosted security camera system - ESP32-CAM firmware (ESP-IDF/C) + Rust server with MJPEG streaming,   software motion detection, MP4 recording, and Telegram alerts. Dockerised, multi-camera.","archived":false,"fork":false,"pushed_at":"2026-05-14T21:14:58.000Z","size":768,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-14T23:07:46.972Z","etag":null,"topics":["esp32-cam","ffmpeg","idf","mjpeg-stream","motion-detection","multicamera","ov2640","rust","security-camera","self-hosted","telegram-bot"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ljufa.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-11T19:03:56.000Z","updated_at":"2026-05-14T21:15:02.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ljufa/esp32_camera","commit_stats":null,"previous_names":["ljufa/esp32_camera"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/ljufa/esp32_camera","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ljufa%2Fesp32_camera","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ljufa%2Fesp32_camera/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ljufa%2Fesp32_camera/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ljufa%2Fesp32_camera/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ljufa","download_url":"https://codeload.github.com/ljufa/esp32_camera/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ljufa%2Fesp32_camera/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33735663,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-31T02:00:06.040Z","response_time":95,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["esp32-cam","ffmpeg","idf","mjpeg-stream","motion-detection","multicamera","ov2640","rust","security-camera","self-hosted","telegram-bot"],"created_at":"2026-05-31T15:01:19.047Z","updated_at":"2026-05-31T15:01:19.769Z","avatar_url":"https://github.com/ljufa.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ESP32 Security Camera\n\nA minimal, self-hosted security camera system built around an AI-Thinker ESP32-CAM (original ESP32, OV2640). The firmware streams JPEG frames over HTTP to a lightweight Rust server that handles live viewing, software motion detection, video encoding, and optional Telegram notifications.\n\n```\n┌─────────────────────┐        HTTP POST /upload/\u003cid\u003e       ┌──────────────────────────┐\n│  ESP32-CAM firmware │  ──────────────────────────────►    │  Rust server (Docker)    │\n│                     │                                     │                          │\n│  • OV2640 capture   │                                     │  • Live MJPEG streams    │\n│  • PIR wake-up      │                                     │  • Motion detection      │\n│  • OTA updates      │                                     │  • MP4 encoding (ffmpeg) │\n│  • HTTP keep-alive  │                                     │  • Telegram alerts       │\n└─────────────────────┘                                     │  • Web dashboard         │\n                                                            └──────────────────────────┘\n```\n\n---\n\n## Firmware\n\nSee [`firmware/README.md`](firmware/README.md) for hardware requirements, pin mapping, build instructions, and configuration reference.\n\n---\n\n## Server\n\n### Quick start (Docker Compose)\n\n```bash\ncd server\n\n# Create your env file from the template\ncp .env.example .env\n$EDITOR .env   # fill in TRAEFIK_HOST, TELEGRAM_TOKEN, etc.\n\ndocker compose up -d\n```\n\nThe server listens on port **8080** inside the container. The compose file assumes a Traefik reverse proxy on an external `proxy` network — adjust or remove the `labels` section for a simpler setup.\n\n### Without Docker (local dev)\n\n```bash\ncd server\ncargo run --release\n```\n\nEnvironment variables (all optional except `SAVE_DIR`):\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `SAVE_DIR` | required | Directory for saved frames and videos |\n| `DB_DIR` | `$SAVE_DIR/db` | Camera config database directory |\n| `FIRMWARE_DIR` | — | Directory served at `/firmware/`; omit to disable OTA |\n| `SERVER_BIND_ADDRESS` | `0.0.0.0:8080` | Listen address |\n| `TELEGRAM_TOKEN` | — | Bot token; omit to disable notifications |\n| `TELEGRAM_CHAT_ID` | — | Chat/group ID for alerts |\n| `MOTION_TIMEOUT_MS` | `60000` | Idle time before a session closes |\n| `PIXEL_THRESHOLD` | `40` | Per-pixel diff threshold (0–255) |\n| `MOTION_CHECK_EVERY` | `5` | Check every Nth frame |\n| `MOTION_PERCENT` | `1.0` | % of pixels that must change to trigger |\n| `RETAIN_DAYS` | `7` | Days before raw frames are deleted by cron |\n\n### HTTP API\n\n| Method | Path | Description |\n|--------|------|-------------|\n| `POST` | `/upload/\u003ccamera_id\u003e` | Receive a JPEG frame from firmware |\n| `GET` | `/stream/\u003ccamera_id\u003e` | MJPEG stream (multipart/x-mixed-replace) |\n| `GET` | `/` | Web dashboard |\n| `GET` | `/status.json` | JSON status for all cameras |\n| `PATCH` | `/api/camera/\u003cid\u003e/config` | Update camera settings |\n| `DELETE` | `/api/camera/\u003cid\u003e` | Remove a camera |\n| `GET` | `/firmware/version` | Current firmware version string (OTA check) |\n| `GET` | `/firmware/\u003cfilename\u003e` | Download firmware binary (OTA update) |\n| `GET` | `/swagger-ui` | Interactive API docs |\n\n### Storage layout\n\n```\n$SAVE_DIR/\n└── \u003ccamera-name\u003e/\n    └── \u003cDD-MM-YYYY\u003e/\n        ├── .raw/\n        │   └── \u003cHH-MM-SS\u003e/\n        │       ├── frame_000001.jpg\n        │       └── frame_000002.jpg\n        └── \u003cHH-MM-SS\u003e.mp4   ← encoded after motion session ends\n```\n\n### Cleanup cron\n\nThe container runs a cron job (`cleanup.sh`) that deletes raw `.jpg` frames older than `RETAIN_DAYS` days and removes any empty directories left behind. MP4 videos are not touched by cleanup — delete them manually if needed.\n\nThe schedule is controlled by `CLEANUP_CRON` (default `*/20 * * * *` — every 20 minutes). Change it in `docker-compose.yaml`:\n\n```yaml\nenvironment:\n  - CLEANUP_CRON=0 3 * * *   # once a day at 03:00\n  - RETAIN_DAYS=7\n```\n\n### Web dashboard\n\nThe dashboard at `/` auto-refreshes every 2 seconds via `/status.json`. Each camera gets its own card with:\n\n- **Live MJPEG stream** — starts automatically when the camera is active, reconnects on error or when the browser tab regains focus\n- **LIVE / OFFLINE badge** with current FPS\n- **Viewer count** and IP list of active stream consumers\n- **Settings panel** (gear icon) — all settings are persisted in the database and survive a server restart:\n  - Rename the camera (display name used for file paths and Telegram messages)\n  - Toggle motion detection and Telegram notifications\n  - Rotation (0 / 90 / 180 / 270°) and mirror — applied server-side via CSS transform on the stream\n  - Pixel threshold, motion percentage, motion timeout, and check-every-N-frames tuning\n  - Delete camera — removes config from the database; frames on disk are kept; device will reappear with defaults if it keeps posting\n- **Filebrowser link** — camera title links to `/fb/files/\u003ccamera-name\u003e/` for browsing saved recordings\n\n### Authentication\n\nThe compose setup uses two separate Traefik Basic Auth credentials:\n\n| Credential | Variable | Protects | Protocol |\n|------------|----------|----------|----------|\n| `camera_stream` | `TRAEFIK_BASIC_AUTH_USERS` | `POST /upload/*` | **HTTP** (plain) |\n| `camera_ui` | `TRAEFIK_UI_BASIC_AUTH_USERS` | Dashboard, streams, API | **HTTPS** |\n| *(none)* | — | `GET /firmware/*` | **HTTP** (plain) |\n\nThe upload and firmware routes are intentionally HTTP-only — skipping TLS cuts latency for high-frequency JPEG POSTs and avoids auth complexity for OTA on the device side. Everything the browser touches goes over HTTPS.\n\nGenerate each password hash with:\n```bash\necho $(htpasswd -nb put_your_username_here put_your_password_here) | sed -e s/\\\\$/\\\\$\\\\$/g\n```\n\n### OTA firmware updates\n\nThe server serves firmware from `FIRMWARE_DIR` (mounted from `firmware/build/` in the repo). To publish a new version:\n\n```bash\n# 1. Bump PROJECT_VER in firmware/CMakeLists.txt, then build\ncd firmware \u0026\u0026 idf.py build\n\n# 2. Write version file alongside the binary\ncd ../server \u0026\u0026 cargo make package-firmware\n\n# 3. Commit and push\ngit add ../firmware/build/esp32_security_camera.bin ../firmware/build/version\ngit commit -m \"firmware 0.x.0\"\ngit push\n\n# 4. Pull on the server and restart\ngit pull \u0026\u0026 docker compose restart camera-server\n```\n\nDevices check for updates on every boot. If the version file on the server differs from the running firmware, the device downloads the binary, flashes it, and reboots.\n\n### Motion detection\n\nFrames are downscaled to 160×120 greyscale before comparison. A session starts when `MOTION_PERCENT`% of pixels differ by more than `PIXEL_THRESHOLD`. After `MOTION_TIMEOUT_MS` of inactivity the session closes, raw frames are encoded into an MP4 with ffmpeg, and the video is sent to Telegram.\n\n### Telegram bot\n\nCreate a bot via [@BotFather](https://t.me/BotFather) and set `TELEGRAM_TOKEN` + `TELEGRAM_CHAT_ID`.\n\nBot commands:\n\n| Command | Description |\n|---------|-------------|\n| `/help` | Show available commands |\n| `/list` | List all cameras with status |\n| `/motion_on \u003cid\u003e` | Enable motion detection |\n| `/motion_off \u003cid\u003e` | Disable motion detection |\n| `/notify_on \u003cid\u003e` | Enable Telegram notifications |\n| `/notify_off \u003cid\u003e` | Disable Telegram notifications |\n\n---\n\n## Disclaimer\n\nThis is a personal hobby project, shared as-is for anyone who finds it useful. It is **not** production-ready software and comes with no guarantees of reliability, security, or fitness for any particular purpose. Use it at your own risk. The author is not responsible for any damage, data loss, security incidents, or other consequences arising from the use of this project.\n\n## License\n\nMIT — see [LICENSE](LICENSE). The license explicitly provides the software *\"as is, without warranty of any kind\"* and excludes the author from any liability, which reflects the hobby nature of this project.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fljufa%2Fesp32_camera","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fljufa%2Fesp32_camera","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fljufa%2Fesp32_camera/lists"}