diff --git a/docs/docs/configuration/camera_specific.md b/docs/docs/configuration/camera_specific.md index 212e2f3d0a..ab5e605d29 100644 --- a/docs/docs/configuration/camera_specific.md +++ b/docs/docs/configuration/camera_specific.md @@ -79,14 +79,15 @@ rtsp://USERNAME:PASSWORD@CAMERA-IP/cam/realmonitor?channel=1&subtype=3 # new hig ### 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://USERNAME:PASSWORD@CAMERA-IP/H264/ch1/main/av_stream # <----- Update for your camera diff --git a/docs/docs/configuration/reference.md b/docs/docs/configuration/reference.md index caea37516d..ad7ec90c51 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 37e5f408e7..69fa1b4550 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 4750a950f8..4ab93d7b96 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 219ce5f76a..c4247e6f2f 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 4f71f1382d..559d7552f6 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 df0e36ff82..2089480443 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 a4b9ee521f..e083d92080 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]: