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
This commit is contained in:
Nicolas Mowen 2025-01-03 08:11:18 -06:00
parent dff6a20764
commit f2cc16bf3c
8 changed files with 23 additions and 18 deletions

View File

@ -67,14 +67,15 @@ ffmpeg:
### Annke C800 ### 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 ```yaml
cameras: cameras:
annkec800: # <------ Name the camera annkec800: # <------ Name the camera
ffmpeg: ffmpeg:
apple_compatibility: true # <- Adds compatibility with MacOS and iPhone
output_args: 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: inputs:
- path: rtsp://user:password@camera-ip:554/H264/ch1/main/av_stream # <----- Update for your camera - path: rtsp://user:password@camera-ip:554/H264/ch1/main/av_stream # <----- Update for your camera

View File

@ -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 # 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. # 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 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 # Optional: Detect configuration
# NOTE: Can be overridden at the camera level # NOTE: Can be overridden at the camera level

View File

@ -167,7 +167,7 @@ class CameraConfig(FrigateBaseModel):
record_args = get_ffmpeg_arg_list( record_args = get_ffmpeg_arg_list(
parse_preset_output_record( parse_preset_output_record(
self.ffmpeg.output_args.record, self.ffmpeg.output_args.record,
self.ffmpeg.output_args._force_record_hvc1, self.ffmpeg.apple_compatibility,
) )
or self.ffmpeg.output_args.record or self.ffmpeg.output_args.record
) )

View File

@ -2,7 +2,7 @@ import shutil
from enum import Enum from enum import Enum
from typing import Union 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 from frigate.const import DEFAULT_FFMPEG_VERSION, INCLUDED_FFMPEG_VERSIONS
@ -42,7 +42,6 @@ class FfmpegOutputArgsConfig(FrigateBaseModel):
default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT, default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT,
title="Record role FFmpeg output arguments.", title="Record role FFmpeg output arguments.",
) )
_force_record_hvc1: bool = PrivateAttr(default=False)
class FfmpegConfig(FrigateBaseModel): class FfmpegConfig(FrigateBaseModel):
@ -64,6 +63,10 @@ class FfmpegConfig(FrigateBaseModel):
default=10.0, default=10.0,
title="Time in seconds to wait before FFmpeg retries connecting to the camera.", 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 @property
def ffmpeg_path(self) -> str: def ffmpeg_path(self) -> str:

View File

@ -458,13 +458,12 @@ class FrigateConfig(FrigateBaseModel):
camera_config.ffmpeg.hwaccel_args = self.ffmpeg.hwaccel_args camera_config.ffmpeg.hwaccel_args = self.ffmpeg.hwaccel_args
for input in camera_config.ffmpeg.inputs: for input in camera_config.ffmpeg.inputs:
need_record_fourcc = False and "record" in input.roles
need_detect_dimensions = "detect" in input.roles and ( need_detect_dimensions = "detect" in input.roles and (
camera_config.detect.height is None camera_config.detect.height is None
or camera_config.detect.width 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} stream_info = {"width": 0, "height": 0, "fourcc": None}
try: try:
stream_info = stream_info_retriever.get_stream_info( stream_info = stream_info_retriever.get_stream_info(
@ -488,14 +487,6 @@ class FrigateConfig(FrigateBaseModel):
else DEFAULT_DETECT_DIMENSIONS["height"] 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 # Warn if detect fps > 10
if camera_config.detect.fps > 10: if camera_config.detect.fps > 10:
logger.warning( logger.warning(

View File

@ -65,6 +65,7 @@ INCLUDED_FFMPEG_VERSIONS = ["7.0", "5.0"]
FFMPEG_HWACCEL_NVIDIA = "preset-nvidia" FFMPEG_HWACCEL_NVIDIA = "preset-nvidia"
FFMPEG_HWACCEL_VAAPI = "preset-vaapi" FFMPEG_HWACCEL_VAAPI = "preset-vaapi"
FFMPEG_HWACCEL_VULKAN = "preset-vulkan" FFMPEG_HWACCEL_VULKAN = "preset-vulkan"
FFMPEG_HVC1_ARGS = ["-tag:v", "hvc1"]
# Regex constants # Regex constants

View File

@ -6,6 +6,7 @@ from enum import Enum
from typing import Any from typing import Any
from frigate.const import ( from frigate.const import (
FFMPEG_HVC1_ARGS,
FFMPEG_HWACCEL_NVIDIA, FFMPEG_HWACCEL_NVIDIA,
FFMPEG_HWACCEL_VAAPI, FFMPEG_HWACCEL_VAAPI,
FFMPEG_HWACCEL_VULKAN, FFMPEG_HWACCEL_VULKAN,
@ -490,6 +491,6 @@ def parse_preset_output_record(arg: Any, force_record_hvc1: bool) -> list[str]:
if force_record_hvc1: if force_record_hvc1:
# Apple only supports HEVC if it is hvc1 (vs. hev1) # Apple only supports HEVC if it is hvc1 (vs. hev1)
preset += ["-tag:v", "hvc1"] preset += FFMPEG_HVC1_ARGS
return preset return preset

View File

@ -19,6 +19,7 @@ from frigate.const import (
CACHE_DIR, CACHE_DIR,
CLIPS_DIR, CLIPS_DIR,
EXPORT_DIR, EXPORT_DIR,
FFMPEG_HVC1_ARGS,
MAX_PLAYLIST_SECONDS, MAX_PLAYLIST_SECONDS,
PREVIEW_FRAME_TYPE, PREVIEW_FRAME_TYPE,
) )
@ -219,7 +220,7 @@ class RecordingExporter(threading.Thread):
if self.playback_factor == PlaybackFactorEnum.realtime: if self.playback_factor == PlaybackFactorEnum.realtime:
ffmpeg_cmd = ( 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(" ") ).split(" ")
elif self.playback_factor == PlaybackFactorEnum.timelapse_25x: elif self.playback_factor == PlaybackFactorEnum.timelapse_25x:
ffmpeg_cmd = ( ffmpeg_cmd = (
@ -227,11 +228,16 @@ class RecordingExporter(threading.Thread):
self.config.ffmpeg.ffmpeg_path, self.config.ffmpeg.ffmpeg_path,
self.config.ffmpeg.hwaccel_args, self.config.ffmpeg.hwaccel_args,
f"-an {ffmpeg_input}", 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, EncodeTypeEnum.timelapse,
) )
).split(" ") ).split(" ")
if self.config.ffmpeg.apple_compatibility:
ffmpeg_cmd += FFMPEG_HVC1_ARGS
ffmpeg_cmd.append(video_path)
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) -> list[str]: