From f2cc16bf3cee353b2960cbae387403bc292a463a Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Fri, 3 Jan 2025 08:11:18 -0600 Subject: [PATCH] Add ffmpeg config to increase HEVC compatibility with Apple devices (#15795) * Add config option for handling HEVC playback on Apple devices * Update docs * Remove unused --- docs/docs/configuration/camera_specific.md | 5 +++-- docs/docs/configuration/reference.md | 2 ++ frigate/config/camera/camera.py | 2 +- frigate/config/camera/ffmpeg.py | 7 +++++-- frigate/config/config.py | 11 +---------- frigate/const.py | 1 + frigate/ffmpeg_presets.py | 3 ++- frigate/record/export.py | 10 ++++++++-- 8 files changed, 23 insertions(+), 18 deletions(-) diff --git a/docs/docs/configuration/camera_specific.md b/docs/docs/configuration/camera_specific.md index 774754b9a..fca74a596 100644 --- a/docs/docs/configuration/camera_specific.md +++ b/docs/docs/configuration/camera_specific.md @@ -67,14 +67,15 @@ ffmpeg: ### Annke C800 -This camera is H.265 only. To be able to play clips on some devices (like MacOs or iPhone) the H.265 stream has to be repackaged and the audio stream has to be converted to aac. Unfortunately direct playback of in the browser is not working (yet), but the downloaded clip can be played locally. +This camera is H.265 only. To be able to play clips on some devices (like MacOs or iPhone) the H.265 stream has to be adjusted using the `apple_compatibility` config. ```yaml cameras: annkec800: # <------ Name the camera ffmpeg: + apple_compatibility: true # <- Adds compatibility with MacOS and iPhone output_args: - record: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c:v copy -tag:v hvc1 -bsf:v hevc_mp4toannexb -c:a aac + record: preset-record-generic-audio-aac inputs: - path: rtsp://user:password@camera-ip:554/H264/ch1/main/av_stream # <----- Update for your camera diff --git a/docs/docs/configuration/reference.md b/docs/docs/configuration/reference.md index caea37516..ad7ec90c5 100644 --- a/docs/docs/configuration/reference.md +++ b/docs/docs/configuration/reference.md @@ -244,6 +244,8 @@ ffmpeg: # If set too high, then if a ffmpeg crash or camera stream timeout occurs, you could potentially lose up to a maximum of retry_interval second(s) of footage # NOTE: this can be a useful setting for Wireless / Battery cameras to reduce how much footage is potentially lost during a connection timeout. retry_interval: 10 + # Optional: Set tag on HEVC (H.265) recording stream to improve compatibility with Apple players. (default: shown below) + apple_compatibility: false # Optional: Detect configuration # NOTE: Can be overridden at the camera level diff --git a/frigate/config/camera/camera.py b/frigate/config/camera/camera.py index 37e5f408e..69fa1b455 100644 --- a/frigate/config/camera/camera.py +++ b/frigate/config/camera/camera.py @@ -167,7 +167,7 @@ class CameraConfig(FrigateBaseModel): record_args = get_ffmpeg_arg_list( parse_preset_output_record( self.ffmpeg.output_args.record, - self.ffmpeg.output_args._force_record_hvc1, + self.ffmpeg.apple_compatibility, ) or self.ffmpeg.output_args.record ) diff --git a/frigate/config/camera/ffmpeg.py b/frigate/config/camera/ffmpeg.py index 4750a950f..4ab93d7b9 100644 --- a/frigate/config/camera/ffmpeg.py +++ b/frigate/config/camera/ffmpeg.py @@ -2,7 +2,7 @@ import shutil from enum import Enum from typing import Union -from pydantic import Field, PrivateAttr, field_validator +from pydantic import Field, field_validator from frigate.const import DEFAULT_FFMPEG_VERSION, INCLUDED_FFMPEG_VERSIONS @@ -42,7 +42,6 @@ class FfmpegOutputArgsConfig(FrigateBaseModel): default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT, title="Record role FFmpeg output arguments.", ) - _force_record_hvc1: bool = PrivateAttr(default=False) class FfmpegConfig(FrigateBaseModel): @@ -64,6 +63,10 @@ class FfmpegConfig(FrigateBaseModel): default=10.0, title="Time in seconds to wait before FFmpeg retries connecting to the camera.", ) + apple_compatibility: bool = Field( + default=False, + title="Set tag on HEVC (H.265) recording stream to improve compatibility with Apple players.", + ) @property def ffmpeg_path(self) -> str: diff --git a/frigate/config/config.py b/frigate/config/config.py index 219ce5f76..c4247e6f2 100644 --- a/frigate/config/config.py +++ b/frigate/config/config.py @@ -458,13 +458,12 @@ class FrigateConfig(FrigateBaseModel): camera_config.ffmpeg.hwaccel_args = self.ffmpeg.hwaccel_args for input in camera_config.ffmpeg.inputs: - need_record_fourcc = False and "record" in input.roles need_detect_dimensions = "detect" in input.roles and ( camera_config.detect.height is None or camera_config.detect.width is None ) - if need_detect_dimensions or need_record_fourcc: + if need_detect_dimensions: stream_info = {"width": 0, "height": 0, "fourcc": None} try: stream_info = stream_info_retriever.get_stream_info( @@ -488,14 +487,6 @@ class FrigateConfig(FrigateBaseModel): else DEFAULT_DETECT_DIMENSIONS["height"] ) - if need_record_fourcc: - # Apple only supports HEVC if it is hvc1 (vs. hev1) - camera_config.ffmpeg.output_args._force_record_hvc1 = ( - stream_info["fourcc"] == "hevc" - if stream_info.get("hevc") - else False - ) - # Warn if detect fps > 10 if camera_config.detect.fps > 10: logger.warning( diff --git a/frigate/const.py b/frigate/const.py index 4f71f1382..559d7552f 100644 --- a/frigate/const.py +++ b/frigate/const.py @@ -65,6 +65,7 @@ INCLUDED_FFMPEG_VERSIONS = ["7.0", "5.0"] FFMPEG_HWACCEL_NVIDIA = "preset-nvidia" FFMPEG_HWACCEL_VAAPI = "preset-vaapi" FFMPEG_HWACCEL_VULKAN = "preset-vulkan" +FFMPEG_HVC1_ARGS = ["-tag:v", "hvc1"] # Regex constants diff --git a/frigate/ffmpeg_presets.py b/frigate/ffmpeg_presets.py index 7065a4d7b..e9a8892db 100644 --- a/frigate/ffmpeg_presets.py +++ b/frigate/ffmpeg_presets.py @@ -6,6 +6,7 @@ from enum import Enum from typing import Any from frigate.const import ( + FFMPEG_HVC1_ARGS, FFMPEG_HWACCEL_NVIDIA, FFMPEG_HWACCEL_VAAPI, FFMPEG_HWACCEL_VULKAN, @@ -490,6 +491,6 @@ def parse_preset_output_record(arg: Any, force_record_hvc1: bool) -> list[str]: if force_record_hvc1: # Apple only supports HEVC if it is hvc1 (vs. hev1) - preset += ["-tag:v", "hvc1"] + preset += FFMPEG_HVC1_ARGS return preset diff --git a/frigate/record/export.py b/frigate/record/export.py index a4b9ee521..e083d9208 100644 --- a/frigate/record/export.py +++ b/frigate/record/export.py @@ -19,6 +19,7 @@ from frigate.const import ( CACHE_DIR, CLIPS_DIR, EXPORT_DIR, + FFMPEG_HVC1_ARGS, MAX_PLAYLIST_SECONDS, PREVIEW_FRAME_TYPE, ) @@ -219,7 +220,7 @@ class RecordingExporter(threading.Thread): if self.playback_factor == PlaybackFactorEnum.realtime: ffmpeg_cmd = ( - f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input} -c copy -movflags +faststart {video_path}" + f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input} -c copy -movflags +faststart" ).split(" ") elif self.playback_factor == PlaybackFactorEnum.timelapse_25x: ffmpeg_cmd = ( @@ -227,11 +228,16 @@ class RecordingExporter(threading.Thread): self.config.ffmpeg.ffmpeg_path, self.config.ffmpeg.hwaccel_args, f"-an {ffmpeg_input}", - f"{self.config.cameras[self.camera].record.export.timelapse_args} -movflags +faststart {video_path}", + f"{self.config.cameras[self.camera].record.export.timelapse_args} -movflags +faststart", EncodeTypeEnum.timelapse, ) ).split(" ") + if self.config.ffmpeg.apple_compatibility: + ffmpeg_cmd += FFMPEG_HVC1_ARGS + + ffmpeg_cmd.append(video_path) + return ffmpeg_cmd, playlist_lines def get_preview_export_command(self, video_path: str) -> list[str]: