From 8433e16558fcaf5537c1bb84fe9c4cf0bf6cda6f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 05:24:38 +0000 Subject: [PATCH] Fix preview frame tracking and partial file cleanup - write_frame_to_cache() now returns bool; callers only append the timestamp to output_frames when cv2.imwrite() actually succeeded, preventing dangling timestamps that cause ffmpeg "Impossible to open" errors when the cache disk is full - FFMpegConverter removes the partial output mp4 on ffmpeg failure so stale partial files don't accumulate on the recording disk https://claude.ai/code/session_016bxjbVpx8DqpjysnGYmXdx --- frigate/output/preview.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/frigate/output/preview.py b/frigate/output/preview.py index f49fc61c9..e2f2aa03a 100644 --- a/frigate/output/preview.py +++ b/frigate/output/preview.py @@ -201,6 +201,7 @@ class FFMpegConverter(threading.Thread): ) else: logger.error(f"Error saving preview for {self.config.name} :: {p.stderr}") + Path(self.path).unlink(missing_ok=True) # unlink files from cache # don't delete last frame as it will be used as first frame in next segment @@ -345,7 +346,7 @@ class PreviewRecorder: return False - def write_frame_to_cache(self, frame_time: float, frame: np.ndarray) -> None: + def write_frame_to_cache(self, frame_time: float, frame: np.ndarray) -> bool: # resize yuv frame small_frame = np.zeros((self.out_height * 3 // 2, self.out_width), np.uint8) copy_yuv_to_position( @@ -360,7 +361,7 @@ class PreviewRecorder: small_frame, cv2.COLOR_YUV2BGR_I420, ) - cv2.imwrite( + result = cv2.imwrite( get_cache_image_name(self.config.name, frame_time), small_frame, [ @@ -368,6 +369,11 @@ class PreviewRecorder: PREVIEW_QUALITY_WEBP[self.config.record.preview.quality], ], ) + if not result: + logger.warning( + f"Failed to write preview frame for {self.config.name} at {frame_time}, likely no space in cache" + ) + return result def write_data( self, @@ -381,8 +387,8 @@ class PreviewRecorder: # always write the first frame if self.start_time == 0: self.start_time = frame_time - self.output_frames.append(frame_time) - self.write_frame_to_cache(frame_time, frame) + if self.write_frame_to_cache(frame_time, frame): + self.output_frames.append(frame_time) return # check if PREVIEW clip should be generated and cached frames reset @@ -390,8 +396,8 @@ class PreviewRecorder: if len(self.output_frames) > 0: # save last frame to ensure consistent duration if self.config.record: - self.output_frames.append(frame_time) - self.write_frame_to_cache(frame_time, frame) + if self.write_frame_to_cache(frame_time, frame): + self.output_frames.append(frame_time) # write the preview if any frames exist for this hour FFMpegConverter( @@ -409,13 +415,13 @@ class PreviewRecorder: # include first frame to ensure consistent duration if self.config.record.enabled: - self.output_frames.append(frame_time) - self.write_frame_to_cache(frame_time, frame) + if self.write_frame_to_cache(frame_time, frame): + self.output_frames.append(frame_time) return elif self.should_write_frame(current_tracked_objects, motion_boxes, frame_time): - self.output_frames.append(frame_time) - self.write_frame_to_cache(frame_time, frame) + if self.write_frame_to_cache(frame_time, frame): + self.output_frames.append(frame_time) return def flag_offline(self, frame_time: float) -> None: