#!/usr/bin/env python3 """Doorbell JPEG proxy: prefetch from Frigate and serve a baseline 480px-wide JPEG.""" from __future__ import annotations import hashlib import io import os import threading import time import requests from flask import Flask, request from PIL import Image FRIGATE_URL = os.environ.get( "FRIGATE_URL", "http://10.0.1.197:5050/api/doorbell/latest.jpg?h=270" ) TARGET_WIDTH = int(os.environ.get("TARGET_WIDTH", "480")) JPEG_QUALITY = int(os.environ.get("JPEG_QUALITY", "40")) FETCH_TIMEOUT = float(os.environ.get("FETCH_TIMEOUT", "5")) PREFETCH_HZ = float(os.environ.get("PREFETCH_HZ", "10")) app = Flask(__name__) _lock = threading.Lock() _jpeg = b"" _etag = "" _width = 0 _height = 0 def _encode(frame: Image.Image) -> tuple[bytes, int, int]: frame = frame.convert("RGB") if frame.width != TARGET_WIDTH: height = max(1, round(frame.height * TARGET_WIDTH / frame.width)) frame = frame.resize((TARGET_WIDTH, height), Image.BILINEAR) buf = io.BytesIO() frame.save( buf, format="JPEG", quality=JPEG_QUALITY, subsampling=2, optimize=False, progressive=False, ) return buf.getvalue(), frame.width, frame.height def _prefetch_loop() -> None: session = requests.Session() interval = 1.0 / max(PREFETCH_HZ, 0.5) while True: started = time.monotonic() try: response = session.get(FRIGATE_URL, timeout=FETCH_TIMEOUT) response.raise_for_status() with Image.open(io.BytesIO(response.content)) as image: encoded, width, height = _encode(image) etag = '"' + hashlib.md5(encoded).hexdigest() + '"' with _lock: global _jpeg, _etag, _width, _height _jpeg = encoded _etag = etag _width = width _height = height except Exception as exc: # noqa: BLE001 — keep last good frame print(f"prefetch failed: {exc}", flush=True) delay = interval - (time.monotonic() - started) if delay > 0: time.sleep(delay) def _snapshot() -> tuple[bytes, str, int, int]: with _lock: return _jpeg, _etag, _width, _height @app.get("/") @app.get("/latest.jpg") def latest(): body, etag, width, height = _snapshot() if not body: return "Image not ready", 503, {"Content-Type": "text/plain"} if request.headers.get("If-None-Match") == etag: return ("", 304, {"ETag": etag, "Cache-Control": "no-cache"}) return ( body, 200, { "Content-Type": "image/jpeg", "Content-Length": str(len(body)), "ETag": etag, "Last-Modified": time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()), "Cache-Control": "no-cache", "Accept-Ranges": "none", "X-Image-Size": f"{width}x{height}", }, ) @app.get("/health") def health(): body, _, width, height = _snapshot() status = 200 if body else 503 return {"ready": bool(body), "bytes": len(body), "size": f"{width}x{height}"}, status if __name__ == "__main__": threading.Thread(target=_prefetch_loop, name="prefetch", daemon=True).start() app.run(host="0.0.0.0", port=int(os.environ.get("PORT", "80")), threaded=True)