A FastAPI + React TypeScript application for real-time object detection scoring on RTSP camera streams. Create polygon zones, define scoring criteria (distance, coordinates, size), and stream scores to home automation systems via SSE or MQTT. NOT a video recorder or NVR.
- Backend: Python 3.12, FastAPI, Uvicorn, Pydantic v2, PyYAML, FFmpeg (RTSP processing), Shapely (polygon geometry)
- Frontend: React 19.2, TypeScript 5+, Vite, Tailwind CSS, shadcn/ui component system (optional animation: framer-motion, react-bits, aceternity UI, motion-bits)
- Features:
- RTSP stream management (add/edit/delete)
- Polygon zone editor with visual overlays on live stream preview
- Real-time object detection scoring: distance from target, camera coordinates, bounding box size
- SSE score streaming (mandatory) + optional MQTT publishing
- NO video recording or storage (live frames only for inference)
- Endpoints:
/(React SPA),/api/streams(REST),/api/zones(REST),/api/scores/stream(SSE),/health,/metrics - Security Warning: LAN-only deployment; no authentication. RTSP credentials stored in plaintext. Do NOT expose to the internet without proper hardening.
# From repo root
docker compose up --buildOpen http://localhost:8000 to view the landing page.
- Click "Add stream" on the landing page
- Enter a name (e.g., "Front Door Camera")
- Enter the RTSP URL (e.g.,
rtsp://username:[email protected]:554/stream) - Click "Add Stream"
- Click the stream button to start playback
Note: RTSP URLs with credentials are stored in plaintext in config/config.yml. This is a LAN-only tool.
curl http://localhost:8000/health
# Returns: {"status":"ok"}curl http://localhost:8000/metrics
# Returns Prometheus-format metricsdocker compose downIf port 8000 is in use, set the APP_PORT environment variable:
APP_PORT=8080 docker compose up --buildOr edit docker-compose.yml and change the ports mapping.
Use the Vite development server for rapid UI iteration. The frontend uses Tailwind CSS and the shadcn/ui component library.
cd frontend
npm install
npx shadcn@latest init # idempotent: ensures shadcn config is up to date
npm run devThe dev server runs on http://localhost:5173 and proxies API calls to the backend (configured in vite.config.ts).
Design Tokens:
- Tailwind tokens are defined in
tailwind.config.ts; extend this file instead of writing ad-hoc CSS. - Use Tailwind utilities for spacing, colors, typography, and responsive breakpoints.
- Breakpoints:
sm(640px),md(768px),lg(1024px),xl(1280px),2xl(1536px). - Minimum touch target size:
44x44px(useh-11 w-11or equivalent Tailwind spacing).
Component Architecture:
- UI primitives live under
src/components/ui/and are generated vianpx shadcn@latest add <component>. - Custom components SHOULD compose shadcn/ui exports and utilities such as
cnfor class merging. - Use
class-variance-authority(CVA) for component variants; see shadcn/ui examples. - Global theming (light/dark) is managed through the
ThemeProviderestablished inmain.tsx.
Adding New Components:
cd frontend
npx shadcn@latest add button # Adds Button component to src/components/ui/button.tsx
npx shadcn@latest add card # Adds Card component
npx shadcn@latest add dialog # Adds Dialog componentComponent Documentation:
- Document prop types and shadcn/ui primitives used in JSDoc comments.
- Example:
/** * StreamCard - Displays a single RTSP stream with status and actions. * Composes shadcn/ui Card, Badge, Button, and DropdownMenu primitives. * @param stream - Stream object with id, name, url, status * @param onEdit - Callback when edit button is clicked * @param onDelete - Callback when delete button is clicked */ export function StreamCard({ stream, onEdit, onDelete }: StreamCardProps) { // ... }
- Strict Mode: TypeScript strict mode is enabled in
tsconfig.json. All types must be explicit. - Linting: ESLint with Tailwind CSS and accessibility plugins. Run
npm run lintto check. - Formatting: Prettier is configured. Run
npm run formatto auto-format code. - Testing: Vitest + React Testing Library. Run
npm run testto execute tests.
- Production Build:
npm run buildcreates an optimized bundle indist/. - Bundle Size: Target <500KB gzipped. Tree-shake unused shadcn/ui components by importing only what you use.
- Environment Variables: Frontend uses hardcoded API base URL
/api(relative path). No build-time configuration needed.
cd frontend
# Development
npm run dev # Start Vite dev server (http://localhost:5173)
npm run build # Build production bundle
npm run preview # Preview production build locally
# Code Quality
npm run lint # Run ESLint
npm run format # Format code with Prettier
npm run test # Run Vitest tests
npm run test:ui # Run tests with UI
# shadcn/ui
npx shadcn@latest add <component> # Add a new component
npx shadcn@latest list # List available componentsbackend/
src/app/
main.py # FastAPI ASGI application entry point
config_io.py # YAML persistence (atomic writes)
logging_config.py # JSON logging configuration
metrics.py # Prometheus metrics
api/
health.py # Health endpoint
streams.py # REST API for streams + playback
errors.py # Error schemas and handlers
models/
stream.py # Pydantic models (Stream, NewStream, EditStream)
services/
streams_service.py # Business logic for stream management
utils/
rtsp.py # FFmpeg-based RTSP/MJPEG playback utilities
validation.py # RTSP URL validation with FFmpeg probe
strings.py # Credential masking helpers
middleware/
rate_limit.py # Rate limiting middleware
request_id.py # Request ID middleware
tests/ # Backend tests
frontend/
src/
components/ # React components
pages/ # Page components (landing, add, edit, play)
hooks/ # Custom React hooks
services/ # API client services
lib/ # Utility functions
tests/ # Frontend tests
package.json
tsconfig.json
vite.config.ts
index.html
config/
config.yml # Stream persistence (mounted volume)
GET /api/streams- List all streams (credentials masked)POST /api/streams- Create a new streamGET /api/streams/{id}- Get stream detailsPATCH /api/streams/{id}- Update stream (partial)DELETE /api/streams/{id}- Delete streamPOST /api/streams/reorder- Reorder streams (drag-drop persistence)
GET /play/{id}.mjpg- MJPEG stream (multipart/x-mixed-replace, ≤5 FPS, no audio)
GET /health- Health check (returns{"status":"ok"})GET /metrics- Prometheus metrics
- Add streams: Name + RTSP URL with validation (2s connectivity probe)
- Edit streams: Update name or URL; re-validates on save
- Delete streams: Confirmation dialog before deletion
- Reorder streams: Drag-and-drop with visual handles; persists immediately
- Status tracking: Streams marked
Inactiveif RTSP unreachable
- MJPEG streaming: Server-side RTSP decode via OpenCV/FFmpeg
- Frame rate cap: ≤5 FPS to reduce bandwidth and CPU
- No audio: Video only
- Error handling: Banner with "Back to streams" link on failure
- Design system: shadcn/ui primitives on Tailwind CSS ensure consistent spacing, typography, and theming.
- Header animation: Centered on landing, animates to top-left on playback (400-700ms)
- Equal-width buttons: Stream buttons in responsive grid (same width per row)
- Mobile-friendly: Responsive layout
- Accessibility: Keyboard navigation, ARIA labels, focus management baked into shadcn/ui components
GitHub Actions builds the linux/amd64 Docker image, performs a smoke test hitting /health until it returns 200 ok (30s timeout), and publishes the image to GitHub Container Registry (ghcr.io).
Pull the latest published image:
docker pull ghcr.io/clsferguson/proximeter:latest- No authentication: Anyone on your network can view/manage streams
- No TLS: All traffic is unencrypted
- Plaintext credentials: RTSP URLs with passwords stored in
config/config.yml - File writes: Only writes to
/app/config/config.yml(mounted volume) - Rate limiting: Basic protection (5 req/s, burst 10) on mutating endpoints
DO NOT expose this application to the internet without proper hardening:
- Add authentication (e.g., OAuth, basic auth with TLS)
- Enable TLS/HTTPS
- Encrypt credentials at rest
- Implement proper access controls
- Add CSRF protection for production use
MIT © 2025 clsferguson