From 8864753d2b2175287f8f1f213e93dd7613c0a3a0 Mon Sep 17 00:00:00 2001 From: Trey Moen Date: Sun, 1 Feb 2026 07:41:26 -0700 Subject: [PATCH] fix(clip): no empty frames --- selfdrive/ui/tests/diff/replay.py | 1 + system/ui/lib/application.py | 66 +++++++++++++++++-------------- tools/clip/run.py | 2 + 3 files changed, 39 insertions(+), 30 deletions(-) diff --git a/selfdrive/ui/tests/diff/replay.py b/selfdrive/ui/tests/diff/replay.py index 9da157660e6cf0..5fc208fc1aa41e 100755 --- a/selfdrive/ui/tests/diff/replay.py +++ b/selfdrive/ui/tests/diff/replay.py @@ -85,6 +85,7 @@ def run_replay(): if not HEADLESS: rl.set_config_flags(rl.FLAG_WINDOW_HIDDEN) gui_app.init_window("ui diff test", fps=FPS) + gui_app.begin_recording() main_layout = MiciMainLayout() main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index da314a394f57e1..89fd318a84d2a7 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -220,6 +220,7 @@ def __init__(self, width: int | None = None, height: int | None = None): self._ffmpeg_queue: queue.Queue | None = None self._ffmpeg_thread: threading.Thread | None = None self._ffmpeg_stop_event: threading.Event | None = None + self._recording = False self._textures: dict[str, rl.Texture] = {} self._target_fps: int = _DEFAULT_FPS self._last_fps_log_time: float = time.monotonic() @@ -283,39 +284,11 @@ def _close(sig, frame): self._render_texture = rl.load_render_texture(self._width, self._height) rl.set_texture_filter(self._render_texture.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR) - if RECORD: - output_fps = fps * RECORD_SPEED - ffmpeg_args = [ - 'ffmpeg', - '-v', 'warning', # Reduce ffmpeg log spam - '-nostats', # Suppress encoding progress - '-f', 'rawvideo', # Input format - '-pix_fmt', 'rgba', # Input pixel format - '-s', f'{self._width}x{self._height}', # Input resolution - '-r', str(fps), # Input frame rate - '-i', 'pipe:0', # Input from stdin - '-vf', 'vflip,format=yuv420p', # Flip vertically and convert to yuv420p - '-r', str(output_fps), # Output frame rate (for speed multiplier) - '-c:v', 'libx264', - '-preset', 'ultrafast', - ] - if RECORD_BITRATE: - ffmpeg_args += ['-b:v', RECORD_BITRATE, '-maxrate', RECORD_BITRATE, '-bufsize', RECORD_BITRATE] - ffmpeg_args += [ - '-y', # Overwrite existing file - '-f', 'mp4', # Output format - RECORD_OUTPUT, # Output file path - ] - self._ffmpeg_proc = subprocess.Popen(ffmpeg_args, stdin=subprocess.PIPE) - self._ffmpeg_queue = queue.Queue(maxsize=60) # Buffer up to 60 frames - self._ffmpeg_stop_event = threading.Event() - self._ffmpeg_thread = threading.Thread(target=self._ffmpeg_writer_thread, daemon=True) - self._ffmpeg_thread.start() - # OFFSCREEN disables FPS limiting for fast offline rendering (e.g. clips) rl.set_target_fps(0 if OFFSCREEN else fps) self._target_fps = fps + self._set_styles() self._load_fonts() self._patch_text_functions() @@ -355,6 +328,39 @@ def _startup_profile_context(self): print(f"{green}UI window ready in {elapsed_ms:.1f} ms{reset}") sys.exit(0) + def begin_recording(self): + if not RECORD or self._recording: + return + + self._recording = True + output_fps = self._target_fps * RECORD_SPEED + ffmpeg_args = [ + 'ffmpeg', + '-v', 'warning', # Reduce ffmpeg log spam + '-nostats', # Suppress encoding progress + '-f', 'rawvideo', # Input format + '-pix_fmt', 'rgba', # Input pixel format + '-s', f'{self._width}x{self._height}', # Input resolution + '-r', str(self._target_fps), # Input frame rate + '-i', 'pipe:0', # Input from stdin + '-vf', 'vflip,format=yuv420p', # Flip vertically and convert to yuv420p + '-r', str(output_fps), # Output frame rate (for speed multiplier) + '-c:v', 'libx264', + '-preset', 'ultrafast', + ] + if RECORD_BITRATE: + ffmpeg_args += ['-b:v', RECORD_BITRATE, '-maxrate', RECORD_BITRATE, '-bufsize', RECORD_BITRATE] + ffmpeg_args += [ + '-y', # Overwrite existing file + '-f', 'mp4', # Output format + RECORD_OUTPUT, # Output file path + ] + self._ffmpeg_proc = subprocess.Popen(ffmpeg_args, stdin=subprocess.PIPE) + self._ffmpeg_queue = queue.Queue(maxsize=60) # Buffer up to 60 frames + self._ffmpeg_stop_event = threading.Event() + self._ffmpeg_thread = threading.Thread(target=self._ffmpeg_writer_thread, daemon=True) + self._ffmpeg_thread.start() + def _ffmpeg_writer_thread(self): """Background thread that writes frames to ffmpeg.""" while True: @@ -560,7 +566,7 @@ def render(self): rl.end_drawing() - if RECORD: + if self._recording: image = rl.load_image_from_texture(self._render_texture.texture) data_size = image.width * image.height * 4 data = bytes(rl.ffi.buffer(image.data, data_size)) diff --git a/tools/clip/run.py b/tools/clip/run.py index 324ee6669060d6..fd58365538976f 100755 --- a/tools/clip/run.py +++ b/tools/clip/run.py @@ -314,6 +314,8 @@ def clip(route: Route, output: str, start: int, end: int, headless: bool = True, if should_render: main_layout.render() render_overlays(rl, gui_app, font, FONT_SCALE, metadata, title, start, frame_idx, show_metadata, show_time) + if road_view.frame is not None: + gui_app.begin_recording() frame_idx += 1 pbar.update(1) timer.lap("render")