diff --git a/frigate/api/transcode.py b/frigate/api/transcode.py index a469a9c5e..0cf972445 100644 --- a/frigate/api/transcode.py +++ b/frigate/api/transcode.py @@ -15,6 +15,10 @@ from frigate.api.auth import require_camera_access, require_role from frigate.api.defs.tags import Tags from frigate.config import FrigateConfig from frigate.const import PROCESS_PRIORITY_LOW +from frigate.ffmpeg_presets import ( + EncodeTypeEnum, + parse_preset_hardware_acceleration_encode, +) logger = logging.getLogger(__name__) @@ -83,51 +87,29 @@ def _build_ffmpeg_cmd( output_playlist = os.path.join(output_dir, "master.m3u8") segment_pattern = os.path.join(output_dir, "segment-%d.ts") - return [ + hwaccel_args = config.ffmpeg.hwaccel_args + + input_args = ( + f"-loglevel warning -y -protocol_whitelist pipe,file,http,tcp -i {input_url}" + ) + output_args = ( + f"-c:a aac -b:a 128k" + f" -f hls -hls_time 6 -hls_list_size 0" + f" -hls_segment_type mpegts" + f" -hls_flags independent_segments+append_list" + f" -hls_segment_filename {segment_pattern}" + f" {output_playlist}" + ) + + cmd_str = parse_preset_hardware_acceleration_encode( config.ffmpeg.ffmpeg_path, - "-hide_banner", - "-loglevel", - "warning", - "-y", - "-protocol_whitelist", - "pipe,file,http,tcp", - "-i", - input_url, - # Scale down to 720p max, preserve aspect ratio - "-vf", - "scale=-2:'min(720,ih)'", - # Encode to H.264 (software — universally available) - "-c:v", - "libx264", - "-preset", - "ultrafast", - "-crf", - "26", - "-profile:v", - "high", - "-level:v", - "4.1", - "-pix_fmt", - "yuv420p", - "-c:a", - "aac", - "-b:a", - "128k", - # Output as HLS with MPEG-TS segments - "-f", - "hls", - "-hls_time", - "6", - "-hls_list_size", - "0", - "-hls_segment_type", - "mpegts", - "-hls_flags", - "independent_segments+append_list", - "-hls_segment_filename", - segment_pattern, - output_playlist, - ] + hwaccel_args, + input_args, + output_args, + EncodeTypeEnum.transcode, + ) + + return cmd_str.split(" ") async def _wait_for_playlist(session_id: str) -> bool: diff --git a/frigate/ffmpeg_presets.py b/frigate/ffmpeg_presets.py index 0652ec645..a6f86a49f 100644 --- a/frigate/ffmpeg_presets.py +++ b/frigate/ffmpeg_presets.py @@ -208,6 +208,37 @@ PRESETS_HW_ACCEL_ENCODE_PREVIEW = { "default": "{0} -hide_banner {1} -c:v libx264 -profile:v baseline -preset:v ultrafast {2}", } +# Presets for on-demand H.265 to H.264 transcode playback +PRESETS_HW_ACCEL_ENCODE_TRANSCODE = { + # Based on birdseye presets with 720p scaling added + "preset-rpi-64-h264": "{0} -hide_banner {1} -c:v h264_v4l2m2m -vf scale=-2:'min(720,ih)' {2}", + "preset-rpi-64-h265": "{0} -hide_banner {1} -c:v h264_v4l2m2m -vf scale=-2:'min(720,ih)' {2}", + FFMPEG_HWACCEL_VAAPI: "{0} -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device {3} {1} -c:v h264_vaapi -bf 0 -profile:v high -level:v 4.1 -vf format=vaapi|nv12,hwupload,scale_vaapi=w=-2:h=720 {2}", + "preset-intel-qsv-h264": "{0} -hide_banner {1} -c:v h264_qsv -profile:v high -level:v 4.1 -async_depth:v 1 -vf scale=-2:'min(720,ih)' {2}", + "preset-intel-qsv-h265": "{0} -hide_banner {1} -c:v h264_qsv -profile:v high -level:v 4.1 -async_depth:v 1 -vf scale=-2:'min(720,ih)' {2}", + FFMPEG_HWACCEL_NVIDIA: "{0} -hide_banner -hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 8 {1} -c:v h264_nvenc -profile:v high -preset:v p2 -tune:v ll -vf scale_cuda=w=-2:h=720 {2}", + "preset-jetson-h264": "{0} -hide_banner {1} -c:v h264_nvmpi -profile high -vf scale=-2:'min(720,ih)' {2}", + "preset-jetson-h265": "{0} -hide_banner {1} -c:v h264_nvmpi -profile high -vf scale=-2:'min(720,ih)' {2}", + FFMPEG_HWACCEL_RKMPP: "{0} -hide_banner {1} -c:v h264_rkmpp -profile:v high -vf scale=-2:'min(720,ih)' {2}", + FFMPEG_HWACCEL_AMF: "{0} -hide_banner {1} -c:v h264_amf -profile:v high -vf scale=-2:'min(720,ih)' {2}", + "default": "{0} -hide_banner {1} -c:v libx264 -preset:v ultrafast -crf 26 -profile:v high -level:v 4.1 -pix_fmt yuv420p -vf scale=-2:'min(720,ih)' {2}", +} +PRESETS_HW_ACCEL_ENCODE_TRANSCODE["preset-nvidia-h264"] = ( + PRESETS_HW_ACCEL_ENCODE_TRANSCODE[FFMPEG_HWACCEL_NVIDIA] +) +PRESETS_HW_ACCEL_ENCODE_TRANSCODE["preset-nvidia-h265"] = ( + PRESETS_HW_ACCEL_ENCODE_TRANSCODE[FFMPEG_HWACCEL_NVIDIA] +) +PRESETS_HW_ACCEL_ENCODE_TRANSCODE[f"{FFMPEG_HWACCEL_RKMPP}-no-dump_extra"] = ( + PRESETS_HW_ACCEL_ENCODE_TRANSCODE[FFMPEG_HWACCEL_RKMPP] +) +PRESETS_HW_ACCEL_ENCODE_TRANSCODE["preset-rk-h264"] = PRESETS_HW_ACCEL_ENCODE_TRANSCODE[ + FFMPEG_HWACCEL_RKMPP +] +PRESETS_HW_ACCEL_ENCODE_TRANSCODE["preset-rk-h265"] = PRESETS_HW_ACCEL_ENCODE_TRANSCODE[ + FFMPEG_HWACCEL_RKMPP +] + def parse_preset_hardware_acceleration_decode( arg: Any, @@ -251,6 +282,7 @@ class EncodeTypeEnum(str, Enum): birdseye = "birdseye" preview = "preview" timelapse = "timelapse" + transcode = "transcode" def parse_preset_hardware_acceleration_encode( @@ -267,6 +299,8 @@ def parse_preset_hardware_acceleration_encode( arg_map = PRESETS_HW_ACCEL_ENCODE_PREVIEW elif type == EncodeTypeEnum.timelapse: arg_map = PRESETS_HW_ACCEL_ENCODE_TIMELAPSE + elif type == EncodeTypeEnum.transcode: + arg_map = PRESETS_HW_ACCEL_ENCODE_TRANSCODE if not isinstance(arg, str): return arg_map["default"].format(ffmpeg_path, input, output)