From cb72480647c807a911c0dcd0a842ee3a2e38c835 Mon Sep 17 00:00:00 2001
From: 1okko <15377594951@189.cn>
Date: Wed, 21 Oct 2026 20:50:48 +0800
Subject: [PATCH] ui: remove MJPEG remote streaming
---
launch_env.sh | 7 -
openpilot/system/ui/lib/application.py | 16 -
openpilot/system/ui/lib/ui_stream.py | 551 -------------------------
3 files changed, 574 deletions(-)
delete mode 100644 openpilot/system/ui/lib/ui_stream.py
diff --git a/launch_env.sh b/launch_env.sh
index 7cf6ccc2d..9c9162564 100755
--- a/launch_env.sh
+++ b/launch_env.sh
@@ -24,13 +24,6 @@ fi
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=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),
# which is still far earlier than the TLS certificate validity and makes the model
diff --git a/openpilot/system/ui/lib/application.py b/openpilot/system/ui/lib/application.py
index 6d4ee00d7..4c89c1ad0 100644
--- a/openpilot/system/ui/lib/application.py
+++ b/openpilot/system/ui/lib/application.py
@@ -250,7 +250,6 @@ class GuiApplication(GuiApplicationExt):
self._ffmpeg_stop_event: threading.Event | None = None
self._textures: dict[str, rl.Texture] = {}
self._target_fps: int = _DEFAULT_FPS
- self._ui_stream = None
self._last_fps_log_time: float = time.monotonic()
self._frame = 0
self._window_close_requested = False
@@ -354,18 +353,6 @@ class GuiApplication(GuiApplicationExt):
rl.set_target_fps(0 if OFFSCREEN or vblank_control else fps)
self._target_fps = fps
- if os.getenv("STREAM") == "1":
- try:
- from openpilot.system.ui.lib import ui_stream
- self._ui_stream = ui_stream
- if self._render_texture is None:
- self._render_texture = rl.load_render_texture(self._scaled_width, self._scaled_height)
- rl.set_texture_filter(self._render_texture.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR)
- ui_stream.start(int(os.getenv("STREAM_PORT", "8082")))
- cloudlog.warning("MJPEG UI stream on :" + str(os.getenv("STREAM_PORT", "8082")))
- except Exception as e:
- cloudlog.error("UI stream init failed: " + str(e))
- self._ui_stream = None
self._load_fonts()
self._patch_text_functions()
self._patch_scissor_mode()
@@ -714,9 +701,6 @@ class GuiApplication(GuiApplicationExt):
self._ffmpeg_queue.put(data) # Async write via background thread
rl.unload_image(image)
-
- if self._ui_stream is not None and self._render_texture is not None:
- self._ui_stream.capture_frame(self, int(os.getenv("STREAM_QUALITY", "50")), int(os.getenv("STREAM_FPS", "10")))
self._monitor_fps()
self._frame += 1
diff --git a/openpilot/system/ui/lib/ui_stream.py b/openpilot/system/ui/lib/ui_stream.py
deleted file mode 100644
index 79e56af21..000000000
--- a/openpilot/system/ui/lib/ui_stream.py
+++ /dev/null
@@ -1,551 +0,0 @@
-"""MJPEG stream server for sunnypilot UI with telemetry overlay.
-Imported lazily when STREAM=1."""
-import threading
-import io
-import os
-import json
-import queue
-import time
-import base64
-import hashlib
-import struct
-from http.server import HTTPServer, BaseHTTPRequestHandler
-from socketserver import ThreadingMixIn
-
-class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
- daemon_threads = True
-
-from PIL import Image
-import pyray as rl
-
-
-class StreamState:
- def __init__(self):
- 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()
-
-
-_CTRL_HTML = '
\n\nopenpilot remote\n\n
\nCTRL: OFF
\nENABLE TOUCH
\n'
-
-_OVERLAY_HTML = """
-
-openpilot live
-
-
-
-
-

-
⛶
-
OFF
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-"""
-
-
-class StreamHandler(BaseHTTPRequestHandler):
- protocol_version = "HTTP/1.1"
-
- def do_GET(self):
- if self.path == "/ws":
- _handle_ws(self)
- elif self.path == "/ctrl":
- self.send_response(200)
- self.send_header("Content-Type", "text/html")
- self.send_header("Content-Length", str(len(_CTRL_HTML)))
- self.end_headers()
- self.wfile.write(_CTRL_HTML.encode())
- elif self.path == "/stream":
- self.send_response(200)
- self.send_header("Content-Type", "multipart/x-mixed-replace; boundary=--frame")
- 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)
- f = self.server._state.get()
- if f:
- self.wfile.write(b"--frame\r\nContent-Type: image/jpeg\r\nContent-Length: " + str(len(f)).encode() + b"\r\n\r\n")
- self.wfile.write(f)
- 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)
- self.send_header("Content-Type", "image/jpeg")
- self.send_header("Content-Length", str(len(f)))
- self.send_header("Access-Control-Allow-Origin", "*")
- self.end_headers()
- self.wfile.write(f)
- else:
- self.send_response(503)
- self.end_headers()
- elif self.path == "/telemetry":
- try:
- with open("/tmp/telemetry.json", "r") as f:
- data = f.read().encode()
- self.send_response(200)
- self.send_header("Content-Type", "application/json")
- self.send_header("Content-Length", str(len(data)))
- self.send_header("Cache-Control", "no-cache")
- self.send_header("Access-Control-Allow-Origin", "*")
- self.end_headers()
- self.wfile.write(data)
- except:
- self.send_response(503)
- self.send_header("Content-Length", "0")
- self.send_header("Connection", "close")
- self.end_headers()
- elif self.path == "/":
- self.send_response(200)
- self.send_header("Content-Type", "text/html")
- self.send_header("Content-Length", str(len(_OVERLAY_HTML)))
- self.end_headers()
- self.wfile.write(_OVERLAY_HTML.encode())
- else:
- self.send_response(404)
- self.end_headers()
-
- def log_message(self, *a):
- pass
-
-
-_state = None
-_counter = 0
-
-_state = None
-_counter = 0
-_last_tel = 0.0
-_enc_queue = None
-_app = None
-_slot_pressed = [False, False]
-_inject_logged = False
-
-
-
-def _ws_accept(key):
- guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
- return base64.b64encode(hashlib.sha1((key + guid).encode()).digest()).decode()
-
-def _ws_read_frame(rfile):
- hdr = rfile.read(2)
- if len(hdr) < 2:
- return None, None
- b0, b1 = hdr[0], hdr[1]
- opcode = b0 & 0x0F
- ln = b1 & 0x7F
- if ln == 126:
- e = rfile.read(2)
- if len(e) < 2: return None, None
- ln = struct.unpack(">H", e)[0]
- elif ln == 127:
- e = rfile.read(8)
- if len(e) < 8: return None, None
- ln = struct.unpack(">Q", e)[0]
- mask = rfile.read(4) if (b1 & 0x80) else b""
- payload = rfile.read(ln) if ln else b""
- if mask and len(mask) == 4:
- payload = bytes(b ^ mask[i % 4] for i, b in enumerate(payload))
- return opcode, payload
-
-def _ws_send_frame(wfile, opcode, payload=b""):
- hdr = bytes([0x80 | opcode])
- ln = len(payload)
- if ln < 126:
- hdr += bytes([ln])
- elif ln < 65536:
- hdr += bytes([126]) + struct.pack(">H", ln)
- else:
- hdr += bytes([127]) + struct.pack(">Q", ln)
- wfile.write(hdr + payload)
- wfile.flush()
-
-def _inject_event(kind, x, y, slot):
- global _app, _slot_pressed
- app = _app
- if app is None:
- return
- try:
- from openpilot.system.ui.lib.application import MouseEvent, MousePos
- mouse = app._mouse
- w = float(app.width)
- h = float(app.height)
- px = max(0.0, min(w, float(x) * w))
- py = max(0.0, min(h, float(y) * h))
- now = time.monotonic()
- slot = 0 if int(slot) % 2 == 0 else 1
- pressed = _slot_pressed[slot]
- if kind == "down":
- if pressed: return
- ev = MouseEvent(MousePos(px, py), slot, True, False, True, now)
- _slot_pressed[slot] = True
- elif kind == "move":
- if not pressed: return
- ev = MouseEvent(MousePos(px, py), slot, False, False, True, now)
- elif kind == "up":
- if not pressed: return
- ev = MouseEvent(MousePos(px, py), slot, False, True, False, now)
- _slot_pressed[slot] = False
- else:
- return
- with mouse._lock:
- mouse._events.append(ev)
- global _inject_logged
- if not _inject_logged:
- _inject_logged = True
- try:
- from openpilot.common.swaglog import cloudlog
- cloudlog.warning("remote UI input active (first event)")
- except Exception:
- pass
- except Exception as e:
- try:
- from openpilot.common.swaglog import cloudlog
- cloudlog.error("remote input error: " + str(e))
- except Exception:
- pass
-
-def _handle_ws(handler):
- try:
- key = handler.headers.get("Sec-WebSocket-Key", "")
- if not key:
- return
- handler.send_response(101, "Switching Protocols")
- handler.send_header("Upgrade", "websocket")
- handler.send_header("Connection", "Upgrade")
- handler.send_header("Sec-WebSocket-Accept", _ws_accept(key))
- handler.end_headers()
- handler.wfile.flush()
- while True:
- opcode, payload = _ws_read_frame(handler.rfile)
- if opcode is None:
- break
- if opcode == 0x8:
- break
- if opcode == 0x9:
- _ws_send_frame(handler.wfile, 0xA, payload)
- continue
- if opcode == 0x1:
- try:
- msg = json.loads(payload.decode("utf-8", "replace"))
- t = msg.get("t")
- if t in ("down", "move", "up"):
- _inject_event(t, float(msg.get("x", 0.0)), float(msg.get("y", 0.0)), int(msg.get("i", 0)))
- except Exception:
- pass
- except Exception:
- pass
-
-def _encode_worker():
- while True:
- item = _enc_queue.get()
- if item is None:
- break
- 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:
- _state.update(buf.getvalue())
- except Exception:
- pass
-
-
-def start(port=8082):
- """Start the MJPEG HTTP server + JPEG encode worker in background threads."""
- global _state, _enc_queue
- _state = StreamState()
- _enc_queue = queue.Queue(maxsize=3)
- threading.Thread(target=_encode_worker, daemon=True).start()
- srv = ThreadingHTTPServer(("0.0.0.0", port), StreamHandler)
- srv._state = _state
- threading.Thread(target=srv.serve_forever, daemon=True).start()
- return _state
-
-
-def _write_telemetry():
- global _last_tel
- now = time.time()
- if now - _last_tel < 0.25:
- return
- _last_tel = now
- try:
- from openpilot.selfdrive.ui.ui_state import ui_state as us
- sm = us.sm
- except Exception:
- return
- data = {}
- try:
- cs = sm["carState"]
- data["vEgo"] = round(float(cs.vEgo), 2)
- data["aEgo"] = round(float(cs.aEgo), 2)
- data["gas"] = 100 if cs.gasPressed else 0
- data["brake"] = 100 if cs.brakePressed else 0
- except Exception:
- pass
- try:
- ss = sm["selfdriveState"]
- data["cruiseEnabled"] = bool(ss.enabled)
- data["driveState"] = "active" if ss.enabled else ("standby" if getattr(us, "started", False) else "off")
- vc = float(ss.vCruiseCluster if ss.vCruiseCluster > 0 else ss.vCruise)
- if vc > 0:
- data["setSpeed"] = int(round(vc * 2.23694)) # mph
- except Exception:
- pass
- try:
- mv = sm["modelV2"]
- if len(mv.lead) > 0:
- data["leadDist"] = round(float(mv.lead[0].dRel), 1) # meters
- data["modelExec"] = int(round(float(mv.modelExecutionTime) * 1000.0)) # ms
- except Exception:
- pass
- try:
- dev = sm["deviceState"]
- temps = [float(t) for t in dev.cpuTempC]
- if temps:
- data["cpuTemp"] = int(round(max(temps)))
- cpus = [int(c) for c in dev.cpuUsagePercent]
- if cpus:
- data["cpuUsage"] = max(cpus)
- data["memUsed"] = int(dev.memoryUsagePercent)
- except Exception:
- pass
- try:
- with open("/tmp/telemetry.json", "w") as f:
- json.dump(data, f)
- except Exception:
- pass
-
-
-def capture_frame(app, quality=50, target_fps=10):
- global _app
- if _app is None:
- _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 or not _state.has_demand():
- return
- _counter += 1
- skip = max(1, int(getattr(app, "_target_fps", 60)) // max(1, int(target_fps)))
- if _counter % skip != 0:
- return
- _write_telemetry()
- try:
- si = rl.load_image_from_texture(app._render_texture.texture)
- w, h = si.width, si.height
- raw = bytes(rl.ffi.buffer(si.data, w * h * 4))
- rl.unload_image(si)
- if _enc_queue is not None:
- try:
- _enc_queue.put_nowait((raw, w, h, int(quality)))
- except Exception:
- pass
- except Exception:
- pass