diff --git a/launch_env.sh b/launch_env.sh index 60b112fa7..7cf6ccc2d 100755 --- a/launch_env.sh +++ b/launch_env.sh @@ -15,6 +15,9 @@ export VECLIB_MAXIMUM_THREADS=1 # headroom for this until ui is moved to the CPU. export QCOM_PRIORITY=12 +# C3XL panel runs at ~31.6Hz; targeting 60 causes constant false FPS warnings. +export FPS=30 + if [ -z "$AGNOS_VERSION" ]; then export AGNOS_VERSION="19.7-c3xl-dev" fi @@ -24,8 +27,9 @@ export STAGING_ROOT="/data/safe_staging" # Comma4-UI-Streamer (MJPEG UI stream) - set STREAM=0 to disable export STREAM=1 export STREAM_PORT=8082 -export STREAM_QUALITY=50 -export STREAM_FPS=10 +export STREAM_QUALITY=40 +export STREAM_FPS=5 +export STREAM_MAX_WIDTH=540 # --- system time (this hardware has no battery-backed RTC) --- # On a cold boot there is no sane clock: AGNOS restores a stale timestamp (or the epoch), diff --git a/openpilot/system/ui/lib/ui_stream.py b/openpilot/system/ui/lib/ui_stream.py index 39ffa84e3..79e56af21 100644 --- a/openpilot/system/ui/lib/ui_stream.py +++ b/openpilot/system/ui/lib/ui_stream.py @@ -24,16 +24,35 @@ class StreamState: self.frame = b"" self.lock = threading.Lock() self.event = threading.Event() + self.clients = 0 + self.snapshot_pending = False def update(self, jpeg): with self.lock: self.frame = jpeg + self.snapshot_pending = False self.event.set() def get(self): with self.lock: return self.frame + def client_enter(self): + with self.lock: + self.clients += 1 + + def client_exit(self): + with self.lock: + self.clients = max(0, self.clients - 1) + + def request_snapshot(self): + with self.lock: + self.snapshot_pending = True + + def has_demand(self): + with self.lock: + return self.clients > 0 or self.snapshot_pending + def wait(self, t=2.0): self.event.wait(t) self.event.clear() @@ -237,6 +256,7 @@ class StreamHandler(BaseHTTPRequestHandler): self.send_header("Cache-Control", "no-cache") self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() + self.server._state.client_enter() try: while True: self.server._state.wait(2.0) @@ -247,7 +267,11 @@ class StreamHandler(BaseHTTPRequestHandler): self.wfile.write(b"\r\n") except (BrokenPipeError, ConnectionResetError): pass + finally: + self.server._state.client_exit() elif self.path == "/snapshot": + self.server._state.request_snapshot() + self.server._state.wait(1.0) f = self.server._state.get() if f: self.send_response(200) @@ -423,6 +447,9 @@ def _encode_worker(): raw, w, h, quality = item try: img = Image.frombytes("RGBA", (w, h), raw).transpose(Image.FLIP_TOP_BOTTOM).convert("RGB") + max_width = int(os.getenv("STREAM_MAX_WIDTH", "540")) + if img.width > max_width: + img.thumbnail((max_width, max_width * img.height // img.width), Image.Resampling.BILINEAR) buf = io.BytesIO() img.save(buf, "JPEG", quality=quality) if _state is not None: @@ -503,7 +530,7 @@ def capture_frame(app, quality=50, target_fps=10): _app = app """Call from the render loop: snapshot texture, queue raw for async JPEG encode.""" global _counter - if _state is None or app._render_texture is None: + if _state is None or app._render_texture is None or not _state.has_demand(): return _counter += 1 skip = max(1, int(getattr(app, "_target_fps", 60)) // max(1, int(target_fps)))