"""Export recordings to storage.""" import datetime import logging import os import subprocess as sp import threading from enum import Enum from frigate.config import FrigateConfig from frigate.const import EXPORT_DIR, MAX_PLAYLIST_SECONDS from frigate.ffmpeg_presets import ( EncodeTypeEnum, parse_preset_hardware_acceleration_encode, ) logger = logging.getLogger(__name__) def lower_priority(): os.nice(10) class PlaybackFactorEnum(str, Enum): realtime = "realtime" timelapse_25x = "timelapse_25x" class RecordingExporter(threading.Thread): """Exports a specific set of recordings for a camera to storage as a single file.""" def __init__( self, config: FrigateConfig, camera: str, start_time: int, end_time: int, playback_factor: PlaybackFactorEnum, ) -> None: threading.Thread.__init__(self) self.config = config self.camera = camera self.start_time = start_time self.end_time = end_time self.playback_factor = playback_factor def get_datetime_from_timestamp(self, timestamp: int) -> str: """Convenience fun to get a simple date time from timestamp.""" return datetime.datetime.fromtimestamp(timestamp).strftime("%Y_%m_%d_%H:%M") def run(self) -> None: logger.debug( f"Beginning export for {self.camera} from {self.start_time} to {self.end_time}" ) file_name = f"{EXPORT_DIR}/in_progress.{self.camera}@{self.get_datetime_from_timestamp(self.start_time)}__{self.get_datetime_from_timestamp(self.end_time)}.mp4" final_file_name = f"{EXPORT_DIR}/{self.camera}_{self.get_datetime_from_timestamp(self.start_time)}__{self.get_datetime_from_timestamp(self.end_time)}.mp4" 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 = ( f"-y -protocol_whitelist pipe,file,http,tcp -i {playlist_lines}" ) else: playlist_lines = [] playlist_start = self.start_time while playlist_start < self.end_time: playlist_lines.append( f"file 'http://127.0.0.1:5000/vod/{self.camera}/start/{playlist_start}/end/{min(playlist_start + MAX_PLAYLIST_SECONDS, self.end_time)}/index.m3u8'" ) playlist_start += MAX_PLAYLIST_SECONDS ffmpeg_input = "-y -protocol_whitelist pipe,file,http,tcp -f concat -safe 0 -i /dev/stdin" if self.playback_factor == PlaybackFactorEnum.realtime: ffmpeg_cmd = ( f"ffmpeg -hide_banner {ffmpeg_input} -c copy {file_name}" ).split(" ") elif self.playback_factor == PlaybackFactorEnum.timelapse_25x: ffmpeg_cmd = ( parse_preset_hardware_acceleration_encode( self.config.ffmpeg.hwaccel_args, ffmpeg_input, f"-vf setpts=0.04*PTS -r 30 -an {file_name}", EncodeTypeEnum.timelapse, ) ).split(" ") 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 recording for command {' '.join(ffmpeg_cmd)}" ) logger.error(p.stderr) return logger.debug(f"Updating finalized export {file_name}") os.rename(file_name, final_file_name) logger.debug(f"Finished exporting {file_name}")