Skip to content

aneeshpatne/CCTV

Repository files navigation

CCTV

Know what moved without scrubbing through hours of footage.

A self-hosted ESP32-CAM recorder that produces a live health overlay, searchable motion clips, and scheduled Discord summaries.

Python 3.12+ Swift 6.2 macOS 26 FastAPI 0.124.4 GNU GPLv3


Overview

CCTV accepts an MJPEG stream and control telemetry from a single ESP32-CAM. It adds an operational HUD, detects sustained motion, records camera-timed video segments, and indexes both recordings and events. A self-hoster receives a live browser view, time-based footage retrieval, motion-specific clips, statistics, and optional Discord digests—useful outputs when the important question is what happened and when.

The operator interface is a responsive, video-first HTML, CSS, and JavaScript dashboard served by FastAPI. A Swift worker handles the latency-sensitive path with URLSession, Core Image, Metal, VideoToolbox, and Vision; Python supervises that worker, persists metadata with SQLite and SQLAlchemy, serves media, and runs scheduled jobs. Native events cross a versioned JSON pipe, while an OpenCV implementation remains available as an automatic fallback.

Features

Area What the project provides
Camera lifecycle Polls the ESP32 status endpoints, dynamically selects framesize 11 or 12 from sustained scene brightness, synchronizes its clock, reapplies AWB/exposure/AGC policy after reconnects, and exposes OTA recovery state through Redis when available.
Native capture Parses multipart MJPEG with a bounded latest-frame queue, follows camera arrival timestamps, and uses hardware JPEG processing and VideoToolbox encoders. The recorded Apple M4 canary reduced median capture-tree CPU by 65.6% and representative segment size by 69.3% versus the Python path.
Live observability Composites timestamp, measured FPS, Wi-Fi RSSI, SoC temperature, motion state, and person/animal labels into the outgoing frame. Telemetry failures leave the feed running with unavailable values.
Signal recovery Detects a three-second JPEG stall, keeps archive and RTSP outputs alive with a NO SIGNAL · RECONNECTING frame, retries the stream, and coalesces disconnects into one camera startup sequence.
Recording and retention Writes atomically finalized, camera-timed HEVC MP4 segments; registers them in a SQLite catalog; reconciles the catalog at startup; and prunes older footage from 90% disk use toward 85% without deleting recent or partial files.
Motion intelligence Uses ROI-aware VideoToolbox motion vectors, temporal persistence, and global-lighting rejection. Vision classification runs only for motion candidates and adds person or animal annotations without changing the base event schema.
Review and retrieval Serves a dashboard plus APIs for recording lists, exact timestamps, arbitrary ranges, hourly/day windows, motion queries, hourly statistics, and accurately trimmed H.264 event clips with configurable pre/post padding.
Scheduled reporting Merges nearby overnight events, downloads their clips from the API, compresses them under the configured Discord transport limit, and sends a summary and videos through a retrying gRPC client. An optional OpenAI job produces a daily text summary and plots.
Operational resilience Restarts a failed capture process, latches to the Python/OpenCV fallback after three native failures in five minutes, publishes health metrics every ten seconds, and includes launchd definitions for capture, API, and the nightly job.

Note

Native capture, the Python fallback, indexed recording, motion persistence, the FastAPI API, the browser dashboard, and nightly clip delivery are implemented. The system currently targets one indoor camera; vehicle classification is intentionally disabled. The RTSP server and Discord webhook gRPC server are required external services and are not included here. The AI daily summary is optional and requires an OpenAI API key.

From camera feed to useful evidence

flowchart LR
    A["ESP32-CAM MJPEG"] --> B["Camera startup and capture"]
    B --> C["Decode, motion analysis, and HUD"]
    C --> D["HEVC segments"]
    C --> E["H.264 RTSP stream"]
    C --> F["Versioned events"]
    D --> G["Recording catalog"]
    F --> H["Motion database"]
    G --> I["FastAPI and dashboard"]
    H --> I
    E --> J["Live browser view"]
    I --> K["Trimmed event clips"]
    K --> L["Nightly Discord digest"]
    C --> M{"JPEG stalled?"}
    M -- "Yes" --> N["No-signal keepalive"]
    N --> O["Reconnect and retune"]
    O --> B
Loading

Fresh JPEGs are processed at their measured arrival cadence; CCTV_TARGET_FPS is an encoder hint and the no-signal keepalive rate rather than a fixed live-frame scheduler. A stalled stream is retried while recording continues. Blocking FFmpeg work is moved off FastAPI's async event loop, generated clips are reused until hourly cleanup, and transient Discord RPC failures receive up to three attempts.

Product outputs

flowchart TD
    CCTV["CCTV"]
    CCTV --> Live["Live awareness"]
    CCTV --> Evidence["Recorded evidence"]
    CCTV --> Activity["Activity history"]
    CCTV --> Reports["Scheduled reports"]

    Live --> Overlay["Health-overlay stream"]
    Live --> Status["Server and camera status"]
    Evidence --> Segments["HEVC recording segments"]
    Evidence --> Clips["Padded H.264 event clips"]
    Activity --> Events["Motion events and labels"]
    Activity --> Stats["Range and hourly statistics"]
    Reports --> Night["Overnight summary and clips"]
    Reports --> Daily["Optional AI summary and plots"]
Loading

Architecture

flowchart LR
    subgraph Edge["Camera and network edge"]
        Camera["ESP32-CAM\nMJPEG and control endpoints"]
        RTSP["External RTSP server"]
        Discord["External Discord gRPC bridge"]
        Redis["Optional Redis"]
    end

    subgraph Capture["Capture and domain services"]
        Orchestrator["Python orchestrator"]
        Native["Swift capture worker"]
        Fallback["Python/OpenCV fallback"]
        Digest["Nightly and daily jobs"]
    end

    subgraph Data["Persistence and media"]
        Recordings[("HEVC MP4 segments")]
        Catalog[("SQLite recording catalog")]
        Motion[("SQLite motion events")]
        Temp[("Temporary H.264 clips")]
    end

    subgraph Presentation["Presentation"]
        API["FastAPI service"]
        Dashboard["Browser dashboard"]
    end

    Orchestrator --> Native
    Orchestrator -. "failure fallback" .-> Fallback
    Camera --> Native
    Camera --> Fallback
    Native --> Recordings
    Native --> RTSP
    Native -- "JSON events" --> Orchestrator
    Orchestrator --> Catalog
    Orchestrator --> Motion
    Orchestrator --> Redis
    Catalog --> API
    Motion --> API
    Recordings --> API
    API --> Temp
    API --> Dashboard
    RTSP --> Dashboard
    API --> Digest
    Digest --> Discord
Loading

Python owns process supervision, camera recovery, storage policy, SQLite writes, and API/scheduled work; Swift owns frame acquisition, analysis, composition, and hardware encoding. The native worker emits additive, versioned event envelopes through a dedicated file descriptor so persistence stays outside the frame path. Actor-isolated runtime state and candidate-only Vision work prevent slow classification from backing up capture. The API queries indexed metadata instead of rescanning recordings and offloads video concatenation and transcoding to worker threads.

Tech stack

Layer Technology
Languages Python 3.12+, Swift 6.2, JavaScript, HTML, and CSS
Native capture URLSession, Core Image, Metal, Core Video, AVFoundation, and VideoToolbox
Detection VideoToolbox motion estimation and Apple Vision person/image classification
Fallback capture OpenCV and NumPy
Backend FastAPI 0.124.4 and Uvicorn
Persistence SQLite, SQLAlchemy 2.0.46, and a direct sqlite3 recording catalog
Video tooling FFmpeg/ffprobe, HEVC archives, H.264 RTSP, and h264_videotoolbox clip encoding
Messaging gRPC 1.71, Protocol Buffers, and an external Discord webhook bridge
Optional insights OpenAI Responses API and Matplotlib
Operations macOS launchd, shell launchers, health events, and benchmark scripts
Testing XCTest and Python unittest

Project structure

.
├── native/
│   ├── Sources/CCTVCapture/       # Swift capture, detection, HUD, and encoders
│   ├── Tests/CCTVCaptureTests/    # Native configuration and pipeline tests
│   └── Package.swift              # Swift 6.2 package and macOS 26 target
├── image_processing/
│   ├── pipeline_orchestrator.py   # Supervision, recovery, catalog, and retention
│   └── camera_pipeline.py         # Python/OpenCV fallback pipeline
├── server/
│   ├── server.py                  # FastAPI dashboard, motion, and video routes
│   └── static/                    # Video-first browser dashboard
├── motion/                        # Nightly clips, daily plots, and AI summary jobs
├── utilities/                     # Camera client, SQLite models, and catalog helpers
├── discord_grpc/                  # Generated stubs and retrying Discord client
├── proto/discord_webhook.proto    # Discord bridge contract
├── tools/                         # Camera controls, diagnostics, and benchmarks
├── tests/                         # Python event-protocol and catalog tests
├── launchd/                       # Machine-specific service definitions
├── benchmarks/                    # Python baseline and native canary evidence
├── scripts/build-native.sh        # Release build for the Swift worker
├── requirements.txt               # Pinned Python dependencies
└── LICENSE                        # GNU GPLv3 text

Requirements

  • An Apple Silicon Mac running macOS 26. The native package declares macOS 26 as its minimum platform.
  • The full Xcode toolchain with Swift 6.2 or later. scripts/build-native.sh expects Xcode at /Applications/Xcode.app unless DEVELOPER_DIR is overridden.
  • Python 3.12 and a virtual environment.
  • FFmpeg and ffprobe with the h264_videotoolbox encoder available.
  • One ESP32-CAM reachable over the local network with MJPEG, control, status, RSSI, and system-health endpoints compatible with utilities/esp32cam_client.py.
  • A writable recordings directory and a writable motion-data directory. Defaults point to /Volumes/drive/CCTV/...; local paths can be supplied through environment variables.
  • An RTSP service that accepts the worker's publisher URL. Browser playback also needs a browser-compatible live URL configured through CCTV_LIVE_STREAM_URL.
  • For notifications, the separate Discord webhook gRPC service described by proto/discord_webhook.proto. For the optional AI digest, an OPENAI_API_KEY is also required.
  • Redis is optional for ordinary capture but required for the ESP32 OTA recovery flag and /esp32cam/recovery status route.

There is no camera simulator. Unit tests run without hardware, but live capture, recovery, RTSP publishing, telemetry, and end-to-end clip delivery require the physical camera and their respective local services. Manual development runs use shell configuration; the supplied launchd files are production-style examples with machine-specific absolute paths.

Warning

The FastAPI app binds to 0.0.0.0, allows CORS from every origin, and does not implement authentication. Keep it on a trusted LAN or place it behind an authenticated reverse proxy; do not expose port 8005 directly to the internet.

Getting started

  1. Clone the repository and enter it.

    git clone https://github.com/aneeshpatne/CCTV.git
    cd CCTV
  2. Create the Python environment and install the pinned dependencies.

    python3.12 -m venv .venv
    source .venv/bin/activate
    python -m pip install --upgrade pip
    python -m pip install -r requirements.txt
  3. Install FFmpeg and verify hardware H.264 encoding.

    brew install ffmpeg
    ffmpeg -hide_banner -encoders | grep h264_videotoolbox
  4. Create local data directories and configure the camera, storage, and live endpoints for the current shell.

    mkdir -p recordings/esp_cam1 motion/data
    
    export ESP32CAM_BASE_URL="http://192.168.0.13"
    export ESP32CAM_STREAM_URL="http://192.168.0.13:81/stream"
    export CCTV_RECORDINGS_DIR="$PWD/recordings/esp_cam1"
    export MOTION_DB_DIR="$CCTV_RECORDINGS_DIR"
    export MOTION_DATA_DIR="$PWD/motion/data"
    export CCTV_RTSP_URL="rtsp://127.0.0.1:8554/esp_cam1_overlay"
    export CCTV_LIVE_STREAM_URL="http://127.0.0.1:8889/esp_cam1_overlay/"

    CCTV_PIPELINE_BACKEND accepts native, python, or auto; the default is native, with fallback when the binary is missing or repeatedly fails. Native tuning is available through CCTV_TARGET_FPS, CCTV_SEGMENT_SECONDS, CCTV_HEVC_BITRATE, CCTV_RTSP_BITRATE, and CCTV_POST_CONNECT_ADJUSTMENT_DELAY_SECONDS.

    Dynamic resolution uses full-frame BT.709 luminance before overlays. It selects framesize 11 below 25%, framesize 12 above 35%, holds the current setting inside that hysteresis band, observes for at least 30 seconds across a 60-second window, and starts a 15-minute cooldown only after the camera verifies the change. These defaults can be overridden with CCTV_DIM_BRIGHTNESS_THRESHOLD, CCTV_BRIGHT_BRIGHTNESS_THRESHOLD, CCTV_BRIGHTNESS_OBSERVATION_SECONDS, CCTV_BRIGHTNESS_WINDOW_SECONDS, and CCTV_RESOLUTION_COOLDOWN_SECONDS.

  5. Build the native capture worker.

    ./scripts/build-native.sh
  6. Start a compatible RTSP server, then run the orchestrator.

    source .venv/bin/activate
    python -m image_processing.pipeline_orchestrator
  7. In a second terminal with the same exported paths, start the API and open http://127.0.0.1:8005/.

    source .venv/bin/activate
    python -m server.server
  8. Optionally configure the external Discord bridge and run the overnight digest.

    export DISCORD_GRPC_TARGET="127.0.0.1:50051"
    export DISCORD_CHANNEL="cctv"
    python -m motion.motion

    The optional AI daily job additionally needs OPENAI_API_KEY and runs with python -m motion.night_message.

Important

The repository contains development defaults for camera IPs, recording volumes, RTSP/live stream URLs, Discord targets, and absolute paths in launchd/*.plist. Replace them for the target machine before installing services or distributing a configured copy. The external RTSP and Discord services must be started separately.

Once manual startup is verified, adapt the three property lists in launchd/, copy them to ~/Library/LaunchAgents/, and load only the services you use. Do not start a second capture process while the LaunchAgent is active: the configured ESP32 MJPEG stream is treated as single-consumer.

Running tests

Run the Python suite from the repository root:

source .venv/bin/activate
python -m unittest discover -s tests -v

Run the Swift suite from the native package:

cd native
DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer \
CLANG_MODULE_CACHE_PATH=/tmp/cctv-clang-module-cache \
SWIFT_MODULE_CACHE_PATH=/tmp/cctv-swift-module-cache \
  xcrun swift test --disable-sandbox

In an IDE, use Python unittest discovery for tests/, or open native/Package.swift in Xcode and choose Product → Test. The current tests cover native configuration, MJPEG parsing, variable-frame-rate timing, RTSP arguments, motion accumulation and event envelopes, plus Python-side event persistence and recording-catalog reconciliation. API, storage-pruning, hardware replay, and Discord integration are not currently covered by the committed automated suite.

Roadmap

  • Consolidate camera addresses, storage paths, stream URLs, and service targets into one shared configuration surface instead of per-process environment lookups and development defaults.
  • Generalize the single-camera orchestrator, catalog, and dashboard state into explicit per-camera configuration.
  • Replace the machine-specific launchd property lists with an installer or generated templates that resolve the repository and virtual-environment paths.
  • Add authenticated deployment guidance or application-level access control for installations that must be reachable beyond a trusted LAN.
  • Extend automated coverage to API clip generation, storage cleanup, camera replay/golden HUD output, and a stub Discord gRPC integration.

License

CCTV is released under the GNU General Public License v3.0. In practical terms, redistributed covered versions must remain under GPLv3 and include the corresponding source and license notices; consult the license text for the complete terms.


Built with Swift, VideoToolbox, FastAPI, and a preference for footage that explains itself.

About

Self-hosted ESP32-CAM monitoring with motion detection, searchable clips, live health overlays, and Discord alerts.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Contributors