Add CPU fallback

This commit is contained in:
Nicolas Mowen 2026-01-15 10:12:53 -07:00
parent c2d09f4029
commit 100c7165ee
3 changed files with 55 additions and 4 deletions

View File

@ -42,3 +42,8 @@ class ExportRecordingsCustomBody(BaseModel):
title="FFmpeg output arguments", title="FFmpeg output arguments",
description="Custom FFmpeg output arguments. If not provided, defaults to timelapse output args.", 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.",
)

View File

@ -484,6 +484,7 @@ def export_recording_custom(
existing_image = sanitize_filepath(body.image_path) if body.image_path else None existing_image = sanitize_filepath(body.image_path) if body.image_path else None
ffmpeg_input_args = body.ffmpeg_input_args ffmpeg_input_args = body.ffmpeg_input_args
ffmpeg_output_args = body.ffmpeg_output_args ffmpeg_output_args = body.ffmpeg_output_args
cpu_fallback = body.cpu_fallback
export_case_id = body.export_case_id export_case_id = body.export_case_id
if export_case_id is not None: if export_case_id is not None:
@ -569,6 +570,7 @@ def export_recording_custom(
export_case_id, export_case_id,
ffmpeg_input_args, ffmpeg_input_args,
ffmpeg_output_args, ffmpeg_output_args,
cpu_fallback,
) )
exporter.start() exporter.start()
return JSONResponse( return JSONResponse(

View File

@ -62,6 +62,7 @@ class RecordingExporter(threading.Thread):
export_case_id: Optional[str] = None, export_case_id: Optional[str] = None,
ffmpeg_input_args: Optional[str] = None, ffmpeg_input_args: Optional[str] = None,
ffmpeg_output_args: Optional[str] = None, ffmpeg_output_args: Optional[str] = None,
cpu_fallback: bool = False,
) -> None: ) -> None:
super().__init__() super().__init__()
self.config = config self.config = config
@ -75,6 +76,7 @@ class RecordingExporter(threading.Thread):
self.export_case_id = export_case_id self.export_case_id = export_case_id
self.ffmpeg_input_args = ffmpeg_input_args self.ffmpeg_input_args = ffmpeg_input_args
self.ffmpeg_output_args = ffmpeg_output_args self.ffmpeg_output_args = ffmpeg_output_args
self.cpu_fallback = cpu_fallback
# ensure export thumb dir # ensure export thumb dir
Path(os.path.join(CLIPS_DIR, "export")).mkdir(exist_ok=True) Path(os.path.join(CLIPS_DIR, "export")).mkdir(exist_ok=True)
@ -179,7 +181,9 @@ class RecordingExporter(threading.Thread):
return thumb_path 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: 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" playlist_lines = f"http://127.0.0.1:5000/vod/{self.camera}/start/{self.start_time}/end/{self.end_time}/index.m3u8"
ffmpeg_input = ( 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" 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: 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 = ( ffmpeg_cmd = (
parse_preset_hardware_acceleration_encode( parse_preset_hardware_acceleration_encode(
self.config.ffmpeg.ffmpeg_path, 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_input_args} -an {ffmpeg_input}".strip(),
f"{self.ffmpeg_output_args} -movflags +faststart".strip(), f"{self.ffmpeg_output_args} -movflags +faststart".strip(),
EncodeTypeEnum.timelapse, EncodeTypeEnum.timelapse,
@ -241,7 +250,9 @@ class RecordingExporter(threading.Thread):
return ffmpeg_cmd, playlist_lines 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 = [] playlist_lines = []
codec = "-c copy" 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: 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 = ( ffmpeg_cmd = (
parse_preset_hardware_acceleration_encode( parse_preset_hardware_acceleration_encode(
self.config.ffmpeg.ffmpeg_path, 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_input_args} {TIMELAPSE_DATA_INPUT_ARGS} {ffmpeg_input}".strip(),
f"{self.ffmpeg_output_args} -movflags +faststart {video_path}".strip(), f"{self.ffmpeg_output_args} -movflags +faststart {video_path}".strip(),
EncodeTypeEnum.timelapse, EncodeTypeEnum.timelapse,
@ -379,6 +395,34 @@ class RecordingExporter(threading.Thread):
capture_output=True, 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: if p.returncode != 0:
logger.error( logger.error(
f"Failed to export {self.playback_source.value} for command {' '.join(ffmpeg_cmd)}" f"Failed to export {self.playback_source.value} for command {' '.join(ffmpeg_cmd)}"