Added stop to the container if no device needs frames
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
"""JPEG proxy: prefetch from Frigate per camera/size and serve cover-cropped JPEGs."""
|
||||
"""JPEG proxy: prefetch from Frigate per camera/size and serve cover-cropped JPEGs.
|
||||
|
||||
Prefetch runs only while clients are requesting a stream; after IDLE_TIMEOUT
|
||||
seconds with no hits it pauses until the next request.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -20,6 +24,8 @@ from PIL import Image
|
||||
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"))
|
||||
# Stop prefetching a stream after this many seconds with no HTTP clients.
|
||||
IDLE_TIMEOUT = float(os.environ.get("IDLE_TIMEOUT", "30"))
|
||||
DEFAULT_SIZE = int(os.environ.get("DEFAULT_SIZE", "480"))
|
||||
MAX_SIZE = int(os.environ.get("MAX_SIZE", "720"))
|
||||
MIN_SIZE = int(os.environ.get("MIN_SIZE", "64"))
|
||||
@@ -58,10 +64,12 @@ class StreamConfig:
|
||||
class CameraStream:
|
||||
config: StreamConfig
|
||||
lock: threading.Lock = field(default_factory=threading.Lock)
|
||||
wake: threading.Event = field(default_factory=threading.Event)
|
||||
jpeg: bytes = b""
|
||||
etag: str = ""
|
||||
width: int = 0
|
||||
height: int = 0
|
||||
last_access: float = 0.0
|
||||
|
||||
def snapshot(self) -> tuple[bytes, str, int, int]:
|
||||
with self.lock:
|
||||
@@ -74,6 +82,20 @@ class CameraStream:
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
def touch(self) -> None:
|
||||
with self.lock:
|
||||
self.last_access = time.monotonic()
|
||||
self.wake.set()
|
||||
|
||||
def is_active(self, now: float | None = None) -> bool:
|
||||
if IDLE_TIMEOUT <= 0:
|
||||
return True
|
||||
with self.lock:
|
||||
last = self.last_access
|
||||
if last <= 0:
|
||||
return False
|
||||
return ((now if now is not None else time.monotonic()) - last) < IDLE_TIMEOUT
|
||||
|
||||
|
||||
class StreamRegistry:
|
||||
def __init__(self, sources: dict[str, CameraSource]) -> None:
|
||||
@@ -228,7 +250,20 @@ def _prefetch_loop(stream: CameraStream) -> None:
|
||||
cfg = stream.config
|
||||
session = requests.Session()
|
||||
interval = 1.0 / max(PREFETCH_HZ, 0.5)
|
||||
idle = True
|
||||
while True:
|
||||
if not stream.is_active():
|
||||
if not idle:
|
||||
print(f"[{cfg.label}] prefetch paused (idle)", flush=True)
|
||||
idle = True
|
||||
stream.wake.wait(timeout=1.0)
|
||||
stream.wake.clear()
|
||||
continue
|
||||
|
||||
if idle:
|
||||
print(f"[{cfg.label}] prefetch active", flush=True)
|
||||
idle = False
|
||||
|
||||
started = time.monotonic()
|
||||
try:
|
||||
source_url = _frigate_url(cfg.source.url, cfg.width, cfg.height, cfg.zoom)
|
||||
@@ -244,10 +279,13 @@ def _prefetch_loop(stream: CameraStream) -> None:
|
||||
print(f"[{cfg.label}] prefetch failed: {exc}", flush=True)
|
||||
delay = interval - (time.monotonic() - started)
|
||||
if delay > 0:
|
||||
time.sleep(delay)
|
||||
# Wake early if a client touches the stream (or it goes idle).
|
||||
stream.wake.wait(timeout=delay)
|
||||
stream.wake.clear()
|
||||
|
||||
|
||||
def _jpeg_response(stream: CameraStream):
|
||||
stream.touch()
|
||||
body, etag, width, height = stream.snapshot()
|
||||
if not body:
|
||||
return "Image not ready", 503, {"Content-Type": "text/plain"}
|
||||
@@ -288,19 +326,22 @@ def create_app() -> Flask:
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
now = time.monotonic()
|
||||
result = {}
|
||||
for stream in registry.all_streams():
|
||||
body, _, width, height = stream.snapshot()
|
||||
label = stream.config.label
|
||||
result[label] = {
|
||||
"ready": bool(body),
|
||||
"active": stream.is_active(now),
|
||||
"bytes": len(body),
|
||||
"size": f"{width}x{height}",
|
||||
"target": f"{stream.config.width}x{stream.config.height}",
|
||||
"zoom": stream.config.zoom,
|
||||
"source": stream.config.source.url,
|
||||
}
|
||||
return {"streams": result}, 200 if any(s["ready"] for s in result.values()) else 503
|
||||
# Healthy once the process is up; streams may be idle with no clients.
|
||||
return {"streams": result, "idle_timeout": IDLE_TIMEOUT}, 200
|
||||
|
||||
@app.get("/")
|
||||
@app.get("/latest.jpg")
|
||||
|
||||
Reference in New Issue
Block a user