From ea7d1aabba04672be87203bf36bab6c5c9838fda Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 3 Jan 2023 18:24:34 -0700 Subject: [PATCH] Ability to set different codec for restream and use go2rtc hardware (#4876) * Add video codec to restream config * Add handling of encode engine and video codec * Add test for video encoding * Set in main configuration docs as well * Add example to restream docs * Put back patch --- docs/docs/configuration/index.md | 4 ++++ docs/docs/configuration/restream.md | 16 +++++++++++++++ frigate/config.py | 9 +++++++++ frigate/ffmpeg_presets.py | 17 ++++++++++++++++ frigate/restream.py | 31 +++++++++++++++++++++++------ frigate/test/test_restream.py | 21 +++++++++++++++++-- 6 files changed, 90 insertions(+), 8 deletions(-) diff --git a/docs/docs/configuration/index.md b/docs/docs/configuration/index.md index e8cb46178..0489da3aa 100644 --- a/docs/docs/configuration/index.md +++ b/docs/docs/configuration/index.md @@ -356,6 +356,10 @@ restream: enabled: True # Optional: Force audio compatibility with browsers (default: shown below) force_audio: True + # Optional: Video encoding to be used. By default the codec will be copied but + # it can be switched to another or an MJPEG stream can be encoded and restreamed + # as h264 (default: shown below) + video_encoding: "copy" # Optional: Restream birdseye via RTSP (default: shown below) # NOTE: Enabling this will set birdseye to run 24/7 which may increase CPU usage somewhat. birdseye: False diff --git a/docs/docs/configuration/restream.md b/docs/docs/configuration/restream.md index 2a43616db..f733596ac 100644 --- a/docs/docs/configuration/restream.md +++ b/docs/docs/configuration/restream.md @@ -15,6 +15,22 @@ Different live view technologies (ex: MSE, WebRTC) support different audio codec Birdseye RTSP restream can be enabled at `restream -> birdseye` and accessed at `rtsp://:8554/birdseye`. Enabling the restream will cause birdseye to run 24/7 which may increase CPU usage somewhat. +#### Changing Restream Codec + +Generally it is recommended to let the codec from the camera be copied. But there may be some cases where h265 needs to be transcoded as h264 or an MJPEG stream can be encoded and restreamed as h264. In this case the encoding will need to be set, if any hardware acceleration presets are set then that will be used to encode the stream. + +```yaml +ffmpeg: + hwaccel_args: your-hwaccel-preset # <- highly recommended so the GPU is used + +cameras: + mjpeg_cam: + ffmpeg: + ... + restream: + video_encoding: h264 +``` + ### RTMP (Deprecated) In previous Frigate versions RTMP was used for re-streaming. RTMP has disadvantages however including being incompatible with H.265, high bitrates, and certain audio codecs. RTMP is deprecated and it is recommended to move to the new restream role. diff --git a/frigate/config.py b/frigate/config.py index 6b97b2e1a..1dc59c30f 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -514,8 +514,17 @@ class JsmpegStreamConfig(FrigateBaseModel): quality: int = Field(default=8, ge=1, le=31, title="Live camera view quality.") +class RestreamCodecEnum(str, Enum): + copy = "copy" + h264 = "h264" + h265 = "h265" + + class RestreamConfig(FrigateBaseModel): enabled: bool = Field(default=True, title="Restreaming enabled.") + video_encoding: RestreamCodecEnum = Field( + default=RestreamCodecEnum.copy, title="Method for encoding the restream." + ) force_audio: bool = Field( default=True, title="Force audio compatibility with the browser." ) diff --git a/frigate/ffmpeg_presets.py b/frigate/ffmpeg_presets.py index 1adbd3c9f..eb981a21c 100644 --- a/frigate/ffmpeg_presets.py +++ b/frigate/ffmpeg_presets.py @@ -230,6 +230,15 @@ PRESETS_HW_ACCEL_ENCODE = { ], } +PRESETS_HW_ACCEL_GO2RTC_ENGINE = { + "preset-intel-vaapi": "vaapi", + "preset-intel-qsv-h264": "vaapi", # go2rtc doesn't support qsv + "preset-intel-qsv-h265": "vaapi", + "preset-amd-vaapi": "vaapi", + "preset-nvidia-h264": "cuda", + "preset-nvidia-h265": "cuda", +} + def parse_preset_hardware_acceleration_decode(arg: Any) -> list[str]: """Return the correct preset if in preset format otherwise return None.""" @@ -267,6 +276,14 @@ def parse_preset_hardware_acceleration_encode(arg: Any) -> list[str]: return PRESETS_HW_ACCEL_ENCODE.get(arg, PRESETS_HW_ACCEL_ENCODE["default"]) +def parse_preset_hardware_acceleration_go2rtc_engine(arg: Any) -> list[str]: + """Return the correct engine for the preset otherwise returns None.""" + if not isinstance(arg, str): + return None + + return PRESETS_HW_ACCEL_GO2RTC_ENGINE.get(arg) + + PRESETS_INPUT = { "preset-http-jpeg-generic": _user_agent_args + [ diff --git a/frigate/restream.py b/frigate/restream.py index 6642fbeb4..6ef6ebade 100644 --- a/frigate/restream.py +++ b/frigate/restream.py @@ -3,18 +3,33 @@ import logging import requests -from frigate.util import escape_special_characters -from frigate.config import FrigateConfig +from typing import Optional + +from frigate.config import FrigateConfig, RestreamCodecEnum from frigate.const import BIRDSEYE_PIPE -from frigate.ffmpeg_presets import parse_preset_hardware_acceleration_encode +from frigate.ffmpeg_presets import ( + parse_preset_hardware_acceleration_encode, + parse_preset_hardware_acceleration_go2rtc_engine, +) +from frigate.util import escape_special_characters logger = logging.getLogger(__name__) -def get_manual_go2rtc_stream(camera_url: str) -> str: +def get_manual_go2rtc_stream( + camera_url: str, codec: RestreamCodecEnum, engine: Optional[str] +) -> str: """Get a manual stream for go2rtc.""" - return f"ffmpeg:{camera_url}#video=copy#audio=aac#audio=opus" + if codec == RestreamCodecEnum.copy: + return f"ffmpeg:{camera_url}#video=copy#audio=aac#audio=opus" + + if engine: + return ( + f"ffmpeg:{camera_url}#video={codec}#hardware={engine}#audio=aac#audio=opus" + ) + + return f"ffmpeg:{camera_url}#video={codec}#audio=aac#audio=opus" class RestreamApi: @@ -41,7 +56,11 @@ class RestreamApi: else: # go2rtc only supports rtsp for direct relay, otherwise ffmpeg is used self.relays[cam_name] = get_manual_go2rtc_stream( - escape_special_characters(input.path) + escape_special_characters(input.path), + camera.restream.video_encoding, + parse_preset_hardware_acceleration_go2rtc_engine( + self.config.ffmpeg.hwaccel_args + ), ) if self.config.restream.birdseye: diff --git a/frigate/test/test_restream.py b/frigate/test/test_restream.py index 6e4096a05..5de8c5cf1 100644 --- a/frigate/test/test_restream.py +++ b/frigate/test/test_restream.py @@ -45,7 +45,9 @@ class TestRestream(TestCase): } @patch("frigate.restream.requests") - def test_rtsp_stream(self, mock_requests) -> None: + def test_rtsp_stream( + self, mock_request + ) -> None: # need to ensure restream doesn't try to call API """Test that the normal rtsp stream is sent plainly.""" frigate_config = FrigateConfig(**self.config) restream = RestreamApi(frigate_config) @@ -53,13 +55,28 @@ class TestRestream(TestCase): assert restream.relays["back"].startswith("rtsp") @patch("frigate.restream.requests") - def test_http_stream(self, mock_requests) -> None: + def test_http_stream( + self, mock_request + ) -> None: # need to ensure restream doesn't try to call API """Test that the http stream is sent via ffmpeg.""" frigate_config = FrigateConfig(**self.config) restream = RestreamApi(frigate_config) restream.add_cameras() assert not restream.relays["front"].startswith("rtsp") + @patch("frigate.restream.requests") + def test_restream_codec_change( + self, mock_request + ) -> None: # need to ensure restream doesn't try to call API + """Test that the http stream is sent via ffmpeg.""" + self.config["cameras"]["front"]["restream"]["video_encoding"] = "h265" + self.config["ffmpeg"] = {"hwaccel_args": "preset-nvidia-h264"} + frigate_config = FrigateConfig(**self.config) + restream = RestreamApi(frigate_config) + restream.add_cameras() + assert "#hardware=cuda" in restream.relays["front"] + assert "#video=h265" in restream.relays["front"] + if __name__ == "__main__": main(verbosity=2)