From 100c7165ee186043d8b4893fa64676689b74a236 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Thu, 15 Jan 2026 10:12:53 -0700 Subject: [PATCH] Add CPU fallback --- .../defs/request/export_recordings_body.py | 5 ++ frigate/api/export.py | 2 + frigate/record/export.py | 52 +++++++++++++++++-- 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/frigate/api/defs/request/export_recordings_body.py b/frigate/api/defs/request/export_recordings_body.py index 82105d64b..5092d28e4 100644 --- a/frigate/api/defs/request/export_recordings_body.py +++ b/frigate/api/defs/request/export_recordings_body.py @@ -42,3 +42,8 @@ class ExportRecordingsCustomBody(BaseModel): title="FFmpeg output arguments", description="Custom FFmpeg output arguments. If not provided, defaults to timelapse output args.", ) + cpu_fallback: bool = Field( + default=False, + title="CPU Fallback", + description="If true, retry export without hardware acceleration if the initial export fails.", + ) diff --git a/frigate/api/export.py b/frigate/api/export.py index cc01e22eb..23f975618 100644 --- a/frigate/api/export.py +++ b/frigate/api/export.py @@ -484,6 +484,7 @@ def export_recording_custom( existing_image = sanitize_filepath(body.image_path) if body.image_path else None ffmpeg_input_args = body.ffmpeg_input_args ffmpeg_output_args = body.ffmpeg_output_args + cpu_fallback = body.cpu_fallback export_case_id = body.export_case_id if export_case_id is not None: @@ -569,6 +570,7 @@ def export_recording_custom( export_case_id, ffmpeg_input_args, ffmpeg_output_args, + cpu_fallback, ) exporter.start() return JSONResponse( diff --git a/frigate/record/export.py b/frigate/record/export.py index cf38f1260..afaed1a2a 100644 --- a/frigate/record/export.py +++ b/frigate/record/export.py @@ -62,6 +62,7 @@ class RecordingExporter(threading.Thread): export_case_id: Optional[str] = None, ffmpeg_input_args: Optional[str] = None, ffmpeg_output_args: Optional[str] = None, + cpu_fallback: bool = False, ) -> None: super().__init__() self.config = config @@ -75,6 +76,7 @@ class RecordingExporter(threading.Thread): self.export_case_id = export_case_id self.ffmpeg_input_args = ffmpeg_input_args self.ffmpeg_output_args = ffmpeg_output_args + self.cpu_fallback = cpu_fallback # ensure export thumb dir Path(os.path.join(CLIPS_DIR, "export")).mkdir(exist_ok=True) @@ -179,7 +181,9 @@ class RecordingExporter(threading.Thread): return thumb_path - def get_record_export_command(self, video_path: str) -> list[str]: + def get_record_export_command( + self, video_path: str, use_hwaccel: bool = True + ) -> list[str]: if (self.end_time - self.start_time) <= MAX_PLAYLIST_SECONDS: playlist_lines = f"http://127.0.0.1:5000/vod/{self.camera}/start/{self.start_time}/end/{self.end_time}/index.m3u8" ffmpeg_input = ( @@ -219,10 +223,15 @@ class RecordingExporter(threading.Thread): ffmpeg_input = "-y -protocol_whitelist pipe,file,http,tcp -f concat -safe 0 -i /dev/stdin" if self.ffmpeg_input_args is not None and self.ffmpeg_output_args is not None: + hwaccel_args = ( + self.config.cameras[self.camera].record.export.hwaccel_args + if use_hwaccel + else None + ) ffmpeg_cmd = ( parse_preset_hardware_acceleration_encode( self.config.ffmpeg.ffmpeg_path, - self.config.cameras[self.camera].record.export.hwaccel_args, + hwaccel_args, f"{self.ffmpeg_input_args} -an {ffmpeg_input}".strip(), f"{self.ffmpeg_output_args} -movflags +faststart".strip(), EncodeTypeEnum.timelapse, @@ -241,7 +250,9 @@ class RecordingExporter(threading.Thread): return ffmpeg_cmd, playlist_lines - def get_preview_export_command(self, video_path: str) -> list[str]: + def get_preview_export_command( + self, video_path: str, use_hwaccel: bool = True + ) -> list[str]: playlist_lines = [] codec = "-c copy" @@ -310,10 +321,15 @@ class RecordingExporter(threading.Thread): ) if self.ffmpeg_input_args is not None and self.ffmpeg_output_args is not None: + hwaccel_args = ( + self.config.cameras[self.camera].record.export.hwaccel_args + if use_hwaccel + else None + ) ffmpeg_cmd = ( parse_preset_hardware_acceleration_encode( self.config.ffmpeg.ffmpeg_path, - self.config.cameras[self.camera].record.export.hwaccel_args, + hwaccel_args, f"{self.ffmpeg_input_args} {TIMELAPSE_DATA_INPUT_ARGS} {ffmpeg_input}".strip(), f"{self.ffmpeg_output_args} -movflags +faststart {video_path}".strip(), EncodeTypeEnum.timelapse, @@ -379,6 +395,34 @@ class RecordingExporter(threading.Thread): capture_output=True, ) + # If export failed and cpu_fallback is enabled, retry without hwaccel + if ( + p.returncode != 0 + and self.cpu_fallback + and self.ffmpeg_input_args is not None + and self.ffmpeg_output_args is not None + ): + logger.warning( + f"Export with hardware acceleration failed, retrying without hwaccel for {self.export_id}" + ) + + if self.playback_source == PlaybackSourceEnum.recordings: + ffmpeg_cmd, playlist_lines = self.get_record_export_command( + video_path, use_hwaccel=False + ) + else: + ffmpeg_cmd, playlist_lines = self.get_preview_export_command( + video_path, use_hwaccel=False + ) + + p = sp.run( + ffmpeg_cmd, + input="\n".join(playlist_lines), + encoding="ascii", + preexec_fn=lower_priority, + capture_output=True, + ) + if p.returncode != 0: logger.error( f"Failed to export {self.playback_source.value} for command {' '.join(ffmpeg_cmd)}"