From 24410849b7229b23e60c1d3e720a6d2125b33da5 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Sat, 14 Jan 2023 14:02:20 -0300 Subject: [PATCH 01/16] Revisit FAQs (#5084) --- docs/docs/configuration/restream.md | 2 +- docs/docs/troubleshooting/faqs.md | 27 +++++++-------------------- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/docs/docs/configuration/restream.md b/docs/docs/configuration/restream.md index 3b3df0f25..fda1c874a 100644 --- a/docs/docs/configuration/restream.md +++ b/docs/docs/configuration/restream.md @@ -15,7 +15,7 @@ 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 +#### Changing Restream Codec {#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. diff --git a/docs/docs/troubleshooting/faqs.md b/docs/docs/troubleshooting/faqs.md index 00d038af0..ce68f6a17 100644 --- a/docs/docs/troubleshooting/faqs.md +++ b/docs/docs/troubleshooting/faqs.md @@ -7,38 +7,25 @@ title: Frequently Asked Questions This error message is due to a shm-size that is too small. Try updating your shm-size according to [this guide](../frigate/installation.md#calculating-required-shm-size). -### I am seeing a solid green image for my camera. - -A solid green image means that Frigate has not received any frames from ffmpeg. Check the logs to see why ffmpeg is exiting and adjust your ffmpeg args accordingly. - ### How can I get sound or audio in my recordings? {#audio-in-recordings} -By default, Frigate removes audio from recordings to reduce the likelihood of failing for invalid data. If you would like to include audio, you need to override the output args to remove `-an` for where you want to include audio. The recommended audio codec is `aac`. The default ffmpeg args are shown [here](../configuration/index.md/#full-configuration-reference). +By default, Frigate removes audio from recordings to reduce the likelihood of failing for invalid data. If you would like to include audio, you need to set a [FFmpeg preset](/configuration/ffmpeg_presets) that supports audio: -:::tip - -When using `-c:a aac`, do not forget to replace `-c copy` with `-c:v copy`. Example: - -```diff title="frigate.yml" +```yaml title="frigate.yml" ffmpeg: output_args: -- record: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c copy -an -+ record: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c:v copy -c:a aac + record: preset-record-generic-audio ``` -This is needed because the `-c` flag (without `:a` or `:v`) applies for both audio and video, thus making it conflicting with `-c:a aac`. - -::: - ### My mjpeg stream or snapshots look green and crazy -This almost always means that the width/height defined for your camera are not correct. Double check the resolution with vlc or another player. Also make sure you don't have the width and height values backwards. +This almost always means that the width/height defined for your camera are not correct. Double check the resolution with VLC or another player. Also make sure you don't have the width and height values backwards. ![mismatched-resolution](/img/mismatched-resolution-min.jpg) ### I can't view events or recordings in the Web UI. -Ensure your cameras send h264 encoded video +Ensure your cameras send h264 encoded video, or [transcode them](/configuration/restream.md#changing-restream-codec). ### "[mov,mp4,m4a,3gp,3g2,mj2 @ 0x5639eeb6e140] moov atom not found" @@ -46,8 +33,8 @@ These messages in the logs are expected in certain situations. Frigate checks th ### "On connect called" -If you see repeated "On connect called" messages in your config, check for another instance of Frigate. This happens when multiple Frigate containers are trying to connect to mqtt with the same client_id. +If you see repeated "On connect called" messages in your logs, check for another instance of Frigate. This happens when multiple Frigate containers are trying to connect to MQTT with the same `client_id`. ### Error: Database Is Locked -sqlite does not work well on a network share, if the `/media` folder is mapped to a network share then [this guide](../configuration/advanced.md#database) should be used to move the database to a location on the internal drive. +SQLite does not work well on a network share, if the `/media` folder is mapped to a network share then [this guide](../configuration/advanced.md#database) should be used to move the database to a location on the internal drive. From 19d17c8c81228cb8c197322e6a8d0136acdf66e5 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sat, 14 Jan 2023 10:03:29 -0700 Subject: [PATCH 02/16] Use memo for recordings timezone (#5086) --- web/src/routes/Events.jsx | 5 +++-- web/src/routes/Recording.jsx | 6 +----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/web/src/routes/Events.jsx b/web/src/routes/Events.jsx index 2f8fd8a9d..2b79efe90 100644 --- a/web/src/routes/Events.jsx +++ b/web/src/routes/Events.jsx @@ -296,6 +296,7 @@ export default function Events({ path, ...props }) { } const timezone = config.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone; + const locale = window.navigator?.language || 'en-US'; return (
@@ -514,8 +515,8 @@ export default function Events({ path, ...props }) { ({(event.top_score * 100).toFixed(0)}%)
- {new Date(event.start_time * 1000).toLocaleDateString({ timeZone: timezone })}{' '} - {new Date(event.start_time * 1000).toLocaleTimeString({ timeZone: timezone })} ( + {new Date(event.start_time * 1000).toLocaleDateString(locale, { timeZone: timezone })}{' '} + {new Date(event.start_time * 1000).toLocaleTimeString(locale, { timeZone: timezone })} ( {clipDuration(event.start_time, event.end_time)})
diff --git a/web/src/routes/Recording.jsx b/web/src/routes/Recording.jsx index f8fcd5e54..d36049342 100644 --- a/web/src/routes/Recording.jsx +++ b/web/src/routes/Recording.jsx @@ -10,11 +10,11 @@ import useSWR from 'swr'; export default function Recording({ camera, date, hour = '00', minute = '00', second = '00' }) { const { data: config } = useSWR('config'); - let timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; const currentDate = useMemo( () => (date ? parseISO(`${date}T${hour || '00'}:${minute || '00'}:${second || '00'}`) : new Date()), [date, hour, minute, second] ); + const timezone = useMemo(() => config.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, [config]); const apiHost = useApiHost(); const { data: recordingsSummary } = useSWR([`${camera}/recordings/summary`, { timezone }], { @@ -118,10 +118,6 @@ export default function Recording({ camera, date, hour = '00', minute = '00', se return ; } - if (config.ui.timezone) { - timezone = config.ui.timezone; - } - if (recordingsSummary.length === 0) { return (
From 60b23150286a54ab0ec14d0d8abf98c4680f919b Mon Sep 17 00:00:00 2001 From: Nate Meyer Date: Sat, 14 Jan 2023 14:14:27 -0500 Subject: [PATCH 03/16] Update library loading for tensorrt (#5087) * Update library loading for tensorrt * Add symlink to libnvrtc --- Dockerfile | 4 +++- docker/rootfs/etc/ld.so.conf.d/cuda_tensorrt.conf | 5 +++++ frigate/detectors/plugins/tensorrt.py | 6 ------ requirements-tensorrt.txt | 3 ++- 4 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 docker/rootfs/etc/ld.so.conf.d/cuda_tensorrt.conf diff --git a/Dockerfile b/Dockerfile index d03576919..18f528cc6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -269,7 +269,9 @@ COPY --from=rootfs / / # Frigate w/ TensorRT Support as separate image FROM frigate AS frigate-tensorrt RUN --mount=type=bind,from=trt-wheels,source=/trt-wheels,target=/deps/trt-wheels \ - pip3 install -U /deps/trt-wheels/*.whl + pip3 install -U /deps/trt-wheels/*.whl && \ + ln -s libnvrtc.so.11.2 /usr/local/lib/python3.9/dist-packages/nvidia/cuda_nvrtc/lib/libnvrtc.so && \ + ldconfig # Dev Container w/ TRT FROM devcontainer AS devcontainer-trt diff --git a/docker/rootfs/etc/ld.so.conf.d/cuda_tensorrt.conf b/docker/rootfs/etc/ld.so.conf.d/cuda_tensorrt.conf new file mode 100644 index 000000000..d4248d047 --- /dev/null +++ b/docker/rootfs/etc/ld.so.conf.d/cuda_tensorrt.conf @@ -0,0 +1,5 @@ +/usr/local/lib/python3.9/dist-packages/nvidia/cudnn/lib +/usr/local/lib/python3.9/dist-packages/nvidia/cuda_runtime/lib +/usr/local/lib/python3.9/dist-packages/nvidia/cublas/lib +/usr/local/lib/python3.9/dist-packages/nvidia/cuda_nvrtc/lib +/usr/local/lib/python3.9/dist-packages/tensorrt \ No newline at end of file diff --git a/frigate/detectors/plugins/tensorrt.py b/frigate/detectors/plugins/tensorrt.py index 71ce9b048..2d2a6788f 100644 --- a/frigate/detectors/plugins/tensorrt.py +++ b/frigate/detectors/plugins/tensorrt.py @@ -75,12 +75,6 @@ class TensorRtDetector(DetectionApi): def _load_engine(self, model_path): try: - ctypes.cdll.LoadLibrary( - "/usr/local/lib/python3.9/dist-packages/nvidia/cuda_runtime/lib/libcudart.so.11.0" - ) - ctypes.cdll.LoadLibrary( - "/usr/local/lib/python3.9/dist-packages/tensorrt/libnvinfer.so.8" - ) trt.init_libnvinfer_plugins(self.trt_logger, "") ctypes.cdll.LoadLibrary("/trt-models/libyolo_layer.so") diff --git a/requirements-tensorrt.txt b/requirements-tensorrt.txt index 1a53892c0..90517babd 100644 --- a/requirements-tensorrt.txt +++ b/requirements-tensorrt.txt @@ -5,4 +5,5 @@ cuda-python == 11.7; platform_machine == 'x86_64' cython == 0.29.*; platform_machine == 'x86_64' nvidia-cuda-runtime-cu11 == 11.7.*; platform_machine == 'x86_64' nvidia-cublas-cu11 == 11.11.*; platform_machine == 'x86_64' -nvidia-cudnn-cu11 == 8.7.*; platform_machine == 'x86_64' \ No newline at end of file +nvidia-cudnn-cu11 == 8.7.*; platform_machine == 'x86_64' +nvidia-cuda-nvrtc-cu11 == 11.7.*; platform_machine == 'x86_64' \ No newline at end of file From daadd206dd44cb245fbf8ce6bddb079bece1d525 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Sun, 15 Jan 2023 12:35:21 -0300 Subject: [PATCH 04/16] Update live view documentation to match newest go2rtc (#5083) * Remove host network suggestion from docs * Mention alternative network modes * Update docs/docs/configuration/live.md * Update docs/docs/configuration/live.md * Wording tweaks * Update docs/docs/configuration/live.md * Update docs/docs/configuration/live.md Co-authored-by: Blake Blackshear --- docker/rootfs/usr/local/go2rtc/go2rtc.yaml | 1 - docs/docs/configuration/live.md | 48 +++++++++++++--------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/docker/rootfs/usr/local/go2rtc/go2rtc.yaml b/docker/rootfs/usr/local/go2rtc/go2rtc.yaml index e068036b5..a2f8d6077 100644 --- a/docker/rootfs/usr/local/go2rtc/go2rtc.yaml +++ b/docker/rootfs/usr/local/go2rtc/go2rtc.yaml @@ -2,6 +2,5 @@ log: format: text webrtc: - listen: ":8555" candidates: - stun:8555 diff --git a/docs/docs/configuration/live.md b/docs/docs/configuration/live.md index d0353cb34..cec16751a 100644 --- a/docs/docs/configuration/live.md +++ b/docs/docs/configuration/live.md @@ -17,27 +17,37 @@ Live view options can be selected while viewing the live stream. The options are ### WebRTC extra configuration: -webRTC works by creating a websocket connection on extra ports. One of the following is required for webRTC to work: -* Frigate is run with `network_mode: host` to support automatic UDP port pass through locally and remotely. See https://github.com/AlexxIT/go2rtc#module-webrtc for more details -* Frigate is run with `network_mode: bridge` and has: - * Router setup to forward port `8555` to port `8555` on the Frigate device. - * For local webRTC, you will need to create your own go2rtc config: +WebRTC works by creating a TCP or UDP connection on port `8555`. However, it requires additional configuration: -```yaml -log: - format: text +* For external access, over the internet, setup your router to forward port `8555` to port `8555` on the Frigate device, for both TCP and UDP. +* For internal/local access, you will need to use a custom go2rtc config: + 1. Create your own go2rtc config, based on [Frigate's internal go2rtc config](https://github.com/blakeblackshear/frigate/blob/dev/docker/rootfs/usr/local/go2rtc/go2rtc.yaml). + 2. Add your internal IP to the list of `candidates`. Here is an example, assuming that `192.168.1.10` is the local IP of the device running Frigate: -webrtc: - candidates: - - :8555 # <--- enter Frigate host IP here - - stun:8555 -``` + ```yaml title="/config/frigate-go2rtc.yaml" + log: + format: text -and pass that config to Frigate via docker or `frigate-go2rtc.yaml` for addon users: + webrtc: + candidates: + - 192.168.1.10:8555 + - stun:8555 + ``` -See https://github.com/AlexxIT/go2rtc#module-webrtc for more details + 3. Mount this config file at `/config/frigate-go2rtc.yaml`. Here is an example, if you run Frigate through docker-compose: -```yaml -volumes: - - /path/to/your/go2rtc.yaml:/config/frigate-go2rtc.yaml:ro -``` + ```yaml title="docker-compose.yaml" + volumes: + - /path/to/your/go2rtc.yaml:/config/frigate-go2rtc.yaml + ``` + +:::note + +If you are having difficulties getting WebRTC to work and you are running Frigate with docker, you may want to try changing the container network mode: + +* `network: host`, in this mode you don't need to forward any ports. The services inside of the Frigate container will have full access to the network interfaces of your host machine as if they were running natively and not in a container. Any port conflicts will need to be resolved. This network mode is recommended by go2rtc, but we recommend you only use it if necessary. +* `network: bridge` creates a virtual network interface for the container, and the container will have full access to it. You also don't need to forward any ports, however, the IP for accessing Frigate locally will differ from the IP of the host machine. Your router will see Frigate as if it was a new device connected in the network. + +::: + +See https://github.com/AlexxIT/go2rtc#module-webrtc for more information about this. From 01b9d4d848db208e5bc3babe1be3055e2728a942 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sun, 15 Jan 2023 08:38:19 -0700 Subject: [PATCH 05/16] Rework audio encoding for restream (#5092) * Use memo for recordings timezone * Add audio encoding field and simplify stream creation * Update docs and tests * Fix bad logic --- docs/docs/configuration/index.md | 8 ++++++-- docs/docs/configuration/restream.md | 4 ++-- frigate/config.py | 16 ++++++++++++--- frigate/restream.py | 32 ++++++++++++++++++++--------- frigate/test/test_restream.py | 2 +- 5 files changed, 44 insertions(+), 18 deletions(-) diff --git a/docs/docs/configuration/index.md b/docs/docs/configuration/index.md index 34ee7c4a6..969f693e4 100644 --- a/docs/docs/configuration/index.md +++ b/docs/docs/configuration/index.md @@ -356,8 +356,12 @@ rtmp: restream: # Optional: Enable the restream (default: True) enabled: True - # Optional: Force audio compatibility with browsers (default: shown below) - force_audio: True + # Optional: Set the audio codecs to restream with + # possible values are aac, copy, opus. Set to copy + # only to avoid transcoding (default: shown below) + audio_encoding: + - aac + - opus # 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) diff --git a/docs/docs/configuration/restream.md b/docs/docs/configuration/restream.md index fda1c874a..8cea0e019 100644 --- a/docs/docs/configuration/restream.md +++ b/docs/docs/configuration/restream.md @@ -7,9 +7,9 @@ title: Restream Frigate can restream your video feed as an RTSP feed for other applications such as Home Assistant to utilize it at `rtsp://:8554/`. Port 8554 must be open. [This allows you to use a video feed for detection in Frigate and Home Assistant live view at the same time without having to make two separate connections to the camera](#reduce-connections-to-camera). The video feed is copied from the original video feed directly to avoid re-encoding. This feed does not include any annotation by Frigate. -#### Force Audio +#### Copy Audio -Different live view technologies (ex: MSE, WebRTC) support different audio codecs. The `restream -> force_audio` flag tells the restream to make multiple streams available so that all live view technologies are supported. Some camera streams don't work well with this, in which case `restream -> force_audio` should be disabled. +Different live view technologies (ex: MSE, WebRTC) support different audio codecs. The `restream -> audio_encoding` field tells the restream to make multiple streams available so that all live view technologies are supported. Some camera streams don't work well with this, in which case `restream -> audio_encoding` should be set to `copy` only. #### Birdseye Restream diff --git a/frigate/config.py b/frigate/config.py index 44c1472e1..bcf5e320c 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -524,16 +524,26 @@ class JsmpegStreamConfig(FrigateBaseModel): quality: int = Field(default=8, ge=1, le=31, title="Live camera view quality.") -class RestreamCodecEnum(str, Enum): +class RestreamVideoCodecEnum(str, Enum): copy = "copy" h264 = "h264" h265 = "h265" +class RestreamAudioCodecEnum(str, Enum): + aac = "aac" + copy = "copy" + opus = "opus" + + class RestreamConfig(FrigateBaseModel): enabled: bool = Field(default=True, title="Restreaming enabled.") - video_encoding: RestreamCodecEnum = Field( - default=RestreamCodecEnum.copy, title="Method for encoding the restream." + audio_encoding: list[RestreamAudioCodecEnum] = Field( + default=[RestreamAudioCodecEnum.aac, RestreamAudioCodecEnum.opus], + title="Codecs to supply for audio.", + ) + video_encoding: RestreamVideoCodecEnum = Field( + default=RestreamVideoCodecEnum.copy, title="Method for encoding the restream." ) force_audio: bool = Field( default=True, title="Force audio compatibility with the browser." diff --git a/frigate/restream.py b/frigate/restream.py index 0f99d4a8a..0fe43fc0e 100644 --- a/frigate/restream.py +++ b/frigate/restream.py @@ -6,7 +6,7 @@ import requests from typing import Optional -from frigate.config import FrigateConfig, RestreamCodecEnum +from frigate.config import FrigateConfig, RestreamAudioCodecEnum, RestreamVideoCodecEnum from frigate.const import BIRDSEYE_PIPE from frigate.ffmpeg_presets import ( parse_preset_hardware_acceleration_encode, @@ -18,18 +18,26 @@ logger = logging.getLogger(__name__) def get_manual_go2rtc_stream( - camera_url: str, codec: RestreamCodecEnum, engine: Optional[str] + camera_url: str, + aCodecs: list[RestreamAudioCodecEnum], + vCodec: RestreamVideoCodecEnum, + engine: Optional[str], ) -> str: """Get a manual stream for go2rtc.""" - if codec == RestreamCodecEnum.copy: - return f"ffmpeg:{camera_url}#video=copy#audio=aac#audio=opus" + stream = f"ffmpeg:{camera_url}" - if engine: - return ( - f"ffmpeg:{camera_url}#video={codec}#hardware={engine}#audio=aac#audio=opus" - ) + for aCodec in aCodecs: + stream += f"#audio={aCodec}" - return f"ffmpeg:{camera_url}#video={codec}#audio=aac#audio=opus" + if vCodec == RestreamVideoCodecEnum.copy: + stream += "#video=copy" + else: + stream += f"#video={vCodec}" + + if engine: + stream += f"#hardware={engine}" + + return stream class RestreamApi: @@ -50,7 +58,10 @@ class RestreamApi: if "restream" in input.roles: if ( input.path.startswith("rtsp") - and not camera.restream.force_audio + and camera.restream.video_encoding + == RestreamVideoCodecEnum.copy + and camera.restream.audio_encoding + == [RestreamAudioCodecEnum.copy] ): self.relays[ cam_name @@ -59,6 +70,7 @@ class RestreamApi: # go2rtc only supports rtsp for direct relay, otherwise ffmpeg is used self.relays[cam_name] = get_manual_go2rtc_stream( escape_special_characters(input.path), + camera.restream.audio_encoding, camera.restream.video_encoding, parse_preset_hardware_acceleration_go2rtc_engine( self.config.ffmpeg.hwaccel_args diff --git a/frigate/test/test_restream.py b/frigate/test/test_restream.py index 5de8c5cf1..2be781306 100644 --- a/frigate/test/test_restream.py +++ b/frigate/test/test_restream.py @@ -25,7 +25,7 @@ class TestRestream(TestCase): }, "restream": { "enabled": True, - "force_audio": False, + "audio_encoding": ["copy"], }, }, "front": { From 65bc644d032fa8d01c6ce291cb1bb1d98873ed3a Mon Sep 17 00:00:00 2001 From: Bernt Christian Egeland Date: Sun, 15 Jan 2023 16:39:03 +0100 Subject: [PATCH 06/16] Rework storage page to show sizes with relevant units (#5093) * new getUnitSize function * check if isNaN --- web/src/routes/Storage.jsx | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/web/src/routes/Storage.jsx b/web/src/routes/Storage.jsx index 6dfc9165a..06976ca76 100644 --- a/web/src/routes/Storage.jsx +++ b/web/src/routes/Storage.jsx @@ -22,6 +22,13 @@ export default function Storage() { return ; } + const getUnitSize = (MB) => { + if (isNaN(MB) || MB < 0) return 'Invalid number'; + if (MB < 1024) return `${MB} MB`; + + return `${(MB / 1024).toFixed(2)} GB`; + }; + let storage_usage; if ( service && @@ -31,13 +38,13 @@ export default function Storage() { Recordings - {service['storage']['/media/frigate/recordings']['used']} - {service['storage']['/media/frigate/recordings']['total']} + {getUnitSize(service['storage']['/media/frigate/recordings']['used'])} + {getUnitSize(service['storage']['/media/frigate/recordings']['total'])} Snapshots - {service['storage']['/media/frigate/clips']['used']} - {service['storage']['/media/frigate/clips']['total']} + {getUnitSize(service['storage']['/media/frigate/clips']['used'])} + {getUnitSize(service['storage']['/media/frigate/clips']['total'])} ); @@ -46,8 +53,8 @@ export default function Storage() { Recordings & Snapshots - {service['storage']['/media/frigate/recordings']['used']} - {service['storage']['/media/frigate/recordings']['total']} + {getUnitSize(service['storage']['/media/frigate/recordings']['used'])} + {getUnitSize(service['storage']['/media/frigate/recordings']['total'])} ); @@ -67,8 +74,8 @@ export default function Storage() { Location - Used MB - Total MB + Used + Total {storage_usage} @@ -82,20 +89,20 @@ export default function Storage() { Location - Used MB - Total MB + Used + Total /dev/shm - {service['storage']['/dev/shm']['used']} - {service['storage']['/dev/shm']['total']} + {getUnitSize(service['storage']['/dev/shm']['used'])} + {getUnitSize(service['storage']['/dev/shm']['total'])} /tmp/cache - {service['storage']['/tmp/cache']['used']} - {service['storage']['/tmp/cache']['total']} + {getUnitSize(service['storage']['/tmp/cache']['used'])} + {getUnitSize(service['storage']['/tmp/cache']['total'])} @@ -121,7 +128,7 @@ export default function Storage() { {Math.round(camera['usage_percent'] ?? 0)}% - {camera['bandwidth'] ? camera['bandwidth'] : 'Calculating...'} MB/hr + {camera['bandwidth'] ? getUnitSize(camera['bandwidth']) : 'Calculating...'}/hr From 99577a57e6a74ed757b811735702d278e04f6812 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sun, 15 Jan 2023 08:40:42 -0700 Subject: [PATCH 07/16] Add specific presets for restream and record with audio (#5094) * Add more ffmpeg presets * Update docs * Update tests * Update docs to optimize setup --- docs/docs/configuration/ffmpeg_presets.md | 34 ++++++++++++----------- docs/docs/configuration/restream.md | 10 +++++-- frigate/ffmpeg_presets.py | 23 ++++++++++++++- frigate/test/test_ffmpeg_presets.py | 4 +-- 4 files changed, 50 insertions(+), 21 deletions(-) diff --git a/docs/docs/configuration/ffmpeg_presets.md b/docs/docs/configuration/ffmpeg_presets.md index bde76b9b4..36df0f50d 100644 --- a/docs/docs/configuration/ffmpeg_presets.md +++ b/docs/docs/configuration/ffmpeg_presets.md @@ -28,15 +28,16 @@ Input args presets help make the config more readable and handle use cases for d See [the camera specific docs](/configuration/camera_specific.md) for more info on non-standard cameras and recommendations for using them in Frigate. -| Preset | Usage | Other Notes | -| ------------------------- | ----------------------- | --------------------------------------------------- | -| preset-http-jpeg-generic | HTTP Live Jpeg | Recommend restreaming live jpeg instead | -| preset-http-mjpeg-generic | HTTP Mjpeg Stream | Recommend restreaming mjpeg stream instead | -| preset-http-reolink | Reolink HTTP-FLV Stream | Only for reolink http, not when restreaming as rtsp | -| preset-rtmp-generic | RTMP Stream | | -| preset-rtsp-generic | RTSP Stream | This is the default when nothing is specified | -| preset-rtsp-udp | RTSP Stream via UDP | Use when camera is UDP only | -| preset-rtsp-blue-iris | Blue Iris RTSP Stream | Use when consuming a stream from Blue Iris | +| Preset | Usage | Other Notes | +| ------------------------- | ------------------------- | --------------------------------------------------- | +| preset-http-jpeg-generic | HTTP Live Jpeg | Recommend restreaming live jpeg instead | +| preset-http-mjpeg-generic | HTTP Mjpeg Stream | Recommend restreaming mjpeg stream instead | +| preset-http-reolink | Reolink HTTP-FLV Stream | Only for reolink http, not when restreaming as rtsp | +| preset-rtmp-generic | RTMP Stream | | +| preset-rtsp-generic | RTSP Stream | This is the default when nothing is specified | +| preset-rtsp-restream | RTSP Stream from restream | Use when using rtsp restream as source | +| preset-rtsp-udp | RTSP Stream via UDP | Use when camera is UDP only | +| preset-rtsp-blue-iris | Blue Iris RTSP Stream | Use when consuming a stream from Blue Iris | :::caution @@ -66,10 +67,11 @@ cameras: Output args presets help make the config more readable and handle use cases for different types of streams to ensure consistent recordings. -| Preset | Usage | Other Notes | -| --------------------------- | --------------------------------- | --------------------------------------------- | -| preset-record-generic | Record WITHOUT audio | This is the default when nothing is specified | -| preset-record-generic-audio | Record WITH audio | Use this to enable audio in recordings | -| preset-record-mjpeg | Record an mjpeg stream | Recommend restreaming mjpeg stream instead | -| preset-record-jpeg | Record live jpeg | Recommend restreaming live jpeg instead | -| preset-record-ubiquiti | Record ubiquiti stream with audio | Recordings with ubiquiti non-standard audio | +| Preset | Usage | Other Notes | +| -------------------------------- | --------------------------------- | --------------------------------------------- | +| preset-record-generic | Record WITHOUT audio | This is the default when nothing is specified | +| preset-record-generic-audio-aac | Record WITH aac audio | Use this to enable audio in recordings | +| preset-record-generic-audio-copy | Record WITH original audio | Use this to enable audio in recordings | +| preset-record-mjpeg | Record an mjpeg stream | Recommend restreaming mjpeg stream instead | +| preset-record-jpeg | Record live jpeg | Recommend restreaming live jpeg instead | +| preset-record-ubiquiti | Record ubiquiti stream with audio | Recordings with ubiquiti non-standard audio | diff --git a/docs/docs/configuration/restream.md b/docs/docs/configuration/restream.md index 8cea0e019..845baed8e 100644 --- a/docs/docs/configuration/restream.md +++ b/docs/docs/configuration/restream.md @@ -47,8 +47,11 @@ One connection is made to the camera. One for the restream, `detect` and `record cameras: test_cam: ffmpeg: + output_args: + record: preset-record-audio-copy inputs: - - path: rtsp://127.0.0.1:8554/test_cam # <--- the name here must match the name of the camera + - path: rtsp://127.0.0.1:8554/test_cam?video=copy&audio=aac # <--- the name here must match the name of the camera + input_args: preset-rtsp-restream roles: - record - detect @@ -65,8 +68,11 @@ Two connections are made to the camera. One for the sub stream, one for the rest cameras: test_cam: ffmpeg: + output_args: + record: preset-record-audio-copy inputs: - - path: rtsp://127.0.0.1:8554/test_cam # <--- the name here must match the name of the camera + - path: rtsp://127.0.0.1:8554/test_cam?video=copy&audio=aac # <--- the name here must match the name of the camera + input_args: preset-rtsp-restream roles: - record - path: rtsp://192.168.1.5:554/stream # <--- camera high res stream diff --git a/frigate/ffmpeg_presets.py b/frigate/ffmpeg_presets.py index e0694f9e7..650b2fdf6 100644 --- a/frigate/ffmpeg_presets.py +++ b/frigate/ffmpeg_presets.py @@ -247,6 +247,13 @@ PRESETS_INPUT = { "-use_wallclock_as_timestamps", "1", ], + "preset-rtsp-restream": _user_agent_args + + [ + "-rtsp_transport", + "tcp", + TIMEOUT_PARAM, + "5000000", + ], "preset-rtsp-udp": _user_agent_args + [ "-avoid_negative_ts", @@ -311,7 +318,7 @@ PRESETS_RECORD_OUTPUT = { "copy", "-an", ], - "preset-record-generic-audio": [ + "preset-record-generic-audio-aac": [ "-f", "segment", "-segment_time", @@ -327,6 +334,20 @@ PRESETS_RECORD_OUTPUT = { "-c:a", "aac", ], + "preset-record-generic-audio-copy": [ + "-f", + "segment", + "-segment_time", + "10", + "-segment_format", + "mp4", + "-reset_timestamps", + "1", + "-strftime", + "1", + "-c", + "copy", + ], "preset-record-mjpeg": [ "-f", "segment", diff --git a/frigate/test/test_ffmpeg_presets.py b/frigate/test/test_ffmpeg_presets.py index 7e3f68195..6ea623790 100644 --- a/frigate/test/test_ffmpeg_presets.py +++ b/frigate/test/test_ffmpeg_presets.py @@ -136,10 +136,10 @@ class TestFfmpegPresets(unittest.TestCase): def test_ffmpeg_output_record_preset(self): self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["output_args"][ "record" - ] = "preset-record-generic-audio" + ] = "preset-record-generic-audio-aac" frigate_config = FrigateConfig(**self.default_ffmpeg) frigate_config.cameras["back"].create_ffmpeg_cmds() - assert "preset-record-generic-audio" not in ( + assert "preset-record-generic-audio-aac" not in ( " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) ) assert "-c:v copy -c:a aac" in ( From 367ac28a9468225b11bb813e17c5573aac4bb9af Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sun, 15 Jan 2023 08:41:13 -0700 Subject: [PATCH 08/16] Fix qsv h265 (#5095) --- frigate/ffmpeg_presets.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frigate/ffmpeg_presets.py b/frigate/ffmpeg_presets.py index 650b2fdf6..3ff35805f 100644 --- a/frigate/ffmpeg_presets.py +++ b/frigate/ffmpeg_presets.py @@ -38,6 +38,8 @@ PRESETS_HW_ACCEL_DECODE = { "h264_qsv", ], "preset-intel-qsv-h265": [ + "-load_plugin", + "hevc_hw", "-hwaccel", "qsv", "-qsv_device", From 621aa0cf615dc4401e279c36be7875a2ddd01300 Mon Sep 17 00:00:00 2001 From: Bernt Christian Egeland Date: Sun, 15 Jan 2023 16:43:40 +0100 Subject: [PATCH 09/16] Rework events page to include timeago (#5097) * new timeago component * added timeago to event * clock icon * clock icon. flex items center * dense prop * moved clipDuration. cleaner jsx, sm hidden * renamed clipduration => getDurationFromTimestamps * func description * duration in parenthesis --- web/src/components/TimeAgo.jsx | 61 ++++++++++++++++++++++++++++ web/src/icons/Clock.jsx | 24 +++++++++++ web/src/routes/Events.jsx | 30 +++++++------- web/src/utils/dateUtil.ts | 73 ++++++++++++++++++++++++++++++++++ 4 files changed, 172 insertions(+), 16 deletions(-) create mode 100644 web/src/components/TimeAgo.jsx create mode 100644 web/src/icons/Clock.jsx diff --git a/web/src/components/TimeAgo.jsx b/web/src/components/TimeAgo.jsx new file mode 100644 index 000000000..eafce61db --- /dev/null +++ b/web/src/components/TimeAgo.jsx @@ -0,0 +1,61 @@ +import { h } from 'preact'; + +const timeAgo = ({ time, dense = false }) => { + if (!time) return 'Invalid Time'; + try { + const currentTime = new Date(); + const pastTime = new Date(time); + const elapsedTime = currentTime - pastTime; + if (elapsedTime < 0) return 'Invalid Time'; + + const timeUnits = [ + { unit: 'ye', full: 'year', value: 31536000 }, + { unit: 'mo', full: 'month', value: 0 }, + { unit: 'day', full: 'day', value: 86400 }, + { unit: 'h', full: 'hour', value: 3600 }, + { unit: 'm', full: 'minute', value: 60 }, + { unit: 's', full: 'second', value: 1 }, + ]; + + let elapsed = elapsedTime / 1000; + if (elapsed < 60) { + return 'just now'; + } + + for (let i = 0; i < timeUnits.length; i++) { + // if months + if (i === 1) { + // Get the month and year for the time provided + const pastMonth = pastTime.getUTCMonth(); + const pastYear = pastTime.getUTCFullYear(); + + // get current month and year + const currentMonth = currentTime.getUTCMonth(); + const currentYear = currentTime.getUTCFullYear(); + + let monthDiff = (currentYear - pastYear) * 12 + (currentMonth - pastMonth); + + // check if the time provided is the previous month but not exceeded 1 month ago. + if (currentTime.getUTCDate() < pastTime.getUTCDate()) { + monthDiff--; + } + + if (monthDiff > 0) { + const unitAmount = monthDiff; + return `${unitAmount}${dense ? timeUnits[i].unit[0] : ` ${timeUnits[i].full}`}${dense ? '' : 's'} ago`; + } + } else if (elapsed >= timeUnits[i].value) { + const unitAmount = Math.floor(elapsed / timeUnits[i].value); + return `${unitAmount}${dense ? timeUnits[i].unit[0] : ` ${timeUnits[i].full}`}${dense ? '' : 's'} ago`; + } + } + } catch { + return 'Invalid Time'; + } +}; + +const TimeAgo = (props) => { + return {timeAgo({ ...props })}; +}; + +export default TimeAgo; diff --git a/web/src/icons/Clock.jsx b/web/src/icons/Clock.jsx new file mode 100644 index 000000000..e813e006d --- /dev/null +++ b/web/src/icons/Clock.jsx @@ -0,0 +1,24 @@ +import { h } from 'preact'; +import { memo } from 'preact/compat'; + +export function Clock({ className = 'h-6 w-6', stroke = 'currentColor', fill = 'none', onClick = () => {} }) { + return ( + + + + ); +} + +export default memo(Clock); diff --git a/web/src/routes/Events.jsx b/web/src/routes/Events.jsx index 2b79efe90..87ae63ec8 100644 --- a/web/src/routes/Events.jsx +++ b/web/src/routes/Events.jsx @@ -15,6 +15,7 @@ import { UploadPlus } from '../icons/UploadPlus'; import { Clip } from '../icons/Clip'; import { Zone } from '../icons/Zone'; import { Camera } from '../icons/Camera'; +import { Clock } from '../icons/Clock'; import { Delete } from '../icons/Delete'; import { Download } from '../icons/Download'; import Menu, { MenuItem } from '../components/Menu'; @@ -22,8 +23,9 @@ import CalendarIcon from '../icons/Calendar'; import Calendar from '../components/Calendar'; import Button from '../components/Button'; import Dialog from '../components/Dialog'; -import { fromUnixTime, intervalToDuration, formatDuration } from 'date-fns'; import MultiSelect from '../components/MultiSelect'; +import { formatUnixTimestampToDateTime, getDurationFromTimestamps } from '../utils/dateUtil'; +import TimeAgo from '../components/TimeAgo'; const API_LIMIT = 25; @@ -39,16 +41,6 @@ const monthsAgo = (num) => { return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime() / 1000; }; -const clipDuration = (start_time, end_time) => { - const start = fromUnixTime(start_time); - const end = fromUnixTime(end_time); - let duration = 'In Progress'; - if (end_time) { - duration = formatDuration(intervalToDuration({ start, end })); - } - return duration; -}; - export default function Events({ path, ...props }) { const apiHost = useApiHost(); const [searchParams, setSearchParams] = useState({ @@ -511,13 +503,19 @@ export default function Events({ path, ...props }) {
{event.sub_label ? `${event.label.replaceAll('_', ' ')}: ${event.sub_label.replaceAll('_', ' ')}` - : event.label.replaceAll('_', ' ')}{' '} + : event.label.replaceAll('_', ' ')} ({(event.top_score * 100).toFixed(0)}%)
-
- {new Date(event.start_time * 1000).toLocaleDateString(locale, { timeZone: timezone })}{' '} - {new Date(event.start_time * 1000).toLocaleTimeString(locale, { timeZone: timezone })} ( - {clipDuration(event.start_time, event.end_time)}) +
+ + {formatUnixTimestampToDateTime(event.start_time, locale, timezone)} +
+ - + +
+
+ ( {getDurationFromTimestamps(event.start_time, event.end_time)} ) +
diff --git a/web/src/utils/dateUtil.ts b/web/src/utils/dateUtil.ts index 336fd7825..39cc993b4 100644 --- a/web/src/utils/dateUtil.ts +++ b/web/src/utils/dateUtil.ts @@ -1,6 +1,7 @@ export const longToDate = (long: number): Date => new Date(long * 1000); export const epochToLong = (date: number): number => date / 1000; export const dateToLong = (date: Date): number => epochToLong(date.getTime()); +import { fromUnixTime, intervalToDuration, formatDuration } from 'date-fns'; const getDateTimeYesterday = (dateTime: Date): Date => { const twentyFourHoursInMilliseconds = 24 * 60 * 60 * 1000; @@ -14,3 +15,75 @@ const getNowYesterday = (): Date => { export const getNowYesterdayInLong = (): number => { return dateToLong(getNowYesterday()); }; + +/** + * This function takes in a unix timestamp, locale, timezone, + * and returns a dateTime string. + * If unixTimestamp is not provided, it returns 'Invalid time' + * @param unixTimestamp: number + * @param locale: string + * @param timezone: string + * @returns string - dateTime or 'Invalid time' if unixTimestamp is not provided + */ +export const formatUnixTimestampToDateTime = (unixTimestamp: number, locale: string, timezone: string): string => { + if (isNaN(unixTimestamp)) { + return 'Invalid time'; + } + try { + const date = new Date(unixTimestamp * 1000); + const formatter = new Intl.DateTimeFormat(locale, { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + timeZone: timezone, + }); + return formatter.format(date); + } catch (error) { + return 'Invalid time'; + } +}; + +interface DurationToken { + xSeconds: string; + xMinutes: string; + xHours: string; +} + +/** + * This function takes in start and end time in unix timestamp, + * and returns the duration between start and end time in hours, minutes and seconds. + * If end time is not provided, it returns 'In Progress' + * @param start_time: number - Unix timestamp for start time + * @param end_time: number|null - Unix timestamp for end time + * @returns string - duration or 'In Progress' if end time is not provided + */ +export const getDurationFromTimestamps = (start_time: number, end_time: number | null): string => { + if (isNaN(start_time)) { + return 'Invalid start time'; + } + let duration = 'In Progress'; + if (end_time !== null) { + if (isNaN(end_time)) { + return 'Invalid end time'; + } + const start = fromUnixTime(start_time); + const end = fromUnixTime(end_time); + const formatDistanceLocale: DurationToken = { + xSeconds: '{{count}}s', + xMinutes: '{{count}}m', + xHours: '{{count}}h', + }; + const shortEnLocale = { + formatDistance: (token: keyof DurationToken, count: number) => + formatDistanceLocale[token].replace('{{count}}', count.toString()), + }; + duration = formatDuration(intervalToDuration({ start, end }), { + format: ['hours', 'minutes', 'seconds'], + locale: shortEnLocale, + }); + } + return duration; +}; From e39fb51decc01918417b701e949e60892e8fd008 Mon Sep 17 00:00:00 2001 From: yeahme49 <58564160+yeahme49@users.noreply.github.com> Date: Sun, 15 Jan 2023 11:25:49 -0600 Subject: [PATCH 10/16] Add Save Only button to config editor (#5090) * Add Save Only button to save config without restarting * Fixes * fix formatting * change to query parameter from header * lint fixes --- frigate/http.py | 17 +++++++++++------ web/src/routes/Config.jsx | 9 ++++++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/frigate/http.py b/frigate/http.py index 1f4fb4e75..815c700cc 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -710,6 +710,8 @@ def config_raw(): @bp.route("/config/save", methods=["POST"]) def config_save(): + save_option = request.args.get("save_option") + new_config = request.get_data().decode() if not new_config: @@ -753,13 +755,16 @@ def config_save(): 400, ) - try: - restart_frigate() - except Exception as e: - logging.error(f"Error restarting Frigate: {e}") - return "Config successfully saved, unable to restart Frigate", 200 + if save_option == "restart": + try: + restart_frigate() + except Exception as e: + logging.error(f"Error restarting Frigate: {e}") + return "Config successfully saved, unable to restart Frigate", 200 - return "Config successfully saved, restarting...", 200 + return "Config successfully saved, restarting...", 200 + else: + return "Config successfully saved.", 200 @bp.route("/config/schema.json") diff --git a/web/src/routes/Config.jsx b/web/src/routes/Config.jsx index 590201d47..e043bbf28 100644 --- a/web/src/routes/Config.jsx +++ b/web/src/routes/Config.jsx @@ -17,13 +17,13 @@ export default function Config() { const [success, setSuccess] = useState(); const [error, setError] = useState(); - const onHandleSaveConfig = async (e) => { + const onHandleSaveConfig = async (e, save_option) => { if (e) { e.stopPropagation(); } axios - .post('config/save', window.editor.getValue(), { + .post(`config/save?save_option=${save_option}`, window.editor.getValue(), { headers: { 'Content-Type': 'text/plain' }, }) .then((response) => { @@ -97,9 +97,12 @@ export default function Config() { - +
From 0de1da59434b2ff8e27566aed4903bd65faadf4f Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Mon, 16 Jan 2023 01:28:20 -0300 Subject: [PATCH 11/16] Upgrade go2rtc from v0.1-rc.8 to v0.1-rc.9 (#5104) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 18f528cc6..17bf01a19 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,7 +27,7 @@ RUN --mount=type=tmpfs,target=/tmp --mount=type=tmpfs,target=/var/cache/apt \ FROM wget AS go2rtc ARG TARGETARCH WORKDIR /rootfs/usr/local/go2rtc/bin -RUN wget -qO go2rtc "https://github.com/AlexxIT/go2rtc/releases/download/v0.1-rc.8/go2rtc_linux_${TARGETARCH}" \ +RUN wget -qO go2rtc "https://github.com/AlexxIT/go2rtc/releases/download/v0.1-rc.9/go2rtc_linux_${TARGETARCH}" \ && chmod +x go2rtc From 30f520f6f0fd6a4d50f382dcff194893a2e1fe8e Mon Sep 17 00:00:00 2001 From: Ryan Mounce Date: Mon, 16 Jan 2023 23:31:04 +1030 Subject: [PATCH 12/16] Patch nginx-vod-module to ignore RBSP trailing bits (#5114) Works around https://github.com/blakeblackshear/frigate/issues/4572 --- docker/build_nginx.sh | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docker/build_nginx.sh b/docker/build_nginx.sh index ca28bd257..fd1432f32 100755 --- a/docker/build_nginx.sh +++ b/docker/build_nginx.sh @@ -25,6 +25,22 @@ tar -zxf ${VOD_MODULE_VERSION}.tar.gz -C /tmp/nginx-vod-module --strip-component rm ${VOD_MODULE_VERSION}.tar.gz # Patch MAX_CLIPS to allow more clips to be added than the default 128 sed -i 's/MAX_CLIPS (128)/MAX_CLIPS (1080)/g' /tmp/nginx-vod-module/vod/media_set.h +patch -d /tmp/nginx-vod-module/ -p1 << 'EOF' +--- a/vod/avc_hevc_parser.c 2022-06-27 11:38:10.000000000 +0000 ++++ b/vod/avc_hevc_parser.c 2023-01-16 11:25:10.900521298 +0000 +@@ -3,6 +3,9 @@ + bool_t + avc_hevc_parser_rbsp_trailing_bits(bit_reader_state_t* reader) + { ++ // https://github.com/blakeblackshear/frigate/issues/4572 ++ return TRUE; ++ + uint32_t one_bit; + + if (reader->stream.eof_reached) +EOF + + mkdir /tmp/nginx-secure-token-module wget https://github.com/kaltura/nginx-secure-token-module/archive/refs/tags/${SECURE_TOKEN_MODULE_VERSION}.tar.gz tar -zxf ${SECURE_TOKEN_MODULE_VERSION}.tar.gz -C /tmp/nginx-secure-token-module --strip-components=1 @@ -47,4 +63,4 @@ cd /tmp/nginx --with-cc-opt="-O3 -Wno-error=implicit-fallthrough" make -j$(nproc) && make install -rm -rf /usr/local/nginx/html /usr/local/nginx/conf/*.default \ No newline at end of file +rm -rf /usr/local/nginx/html /usr/local/nginx/conf/*.default From 81b3fdb4231885053e1c6a4fef7fc31bb97da309 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Mon, 16 Jan 2023 16:16:46 -0700 Subject: [PATCH 13/16] Pre clear retained messagse (#5117) --- frigate/comms/mqtt.py | 51 +++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/frigate/comms/mqtt.py b/frigate/comms/mqtt.py index e74cc6bf2..b8c1a0ea6 100644 --- a/frigate/comms/mqtt.py +++ b/frigate/comms/mqtt.py @@ -139,35 +139,30 @@ class MqttClient(Communicator): # type: ignore[misc] ) # register callbacks + callback_types = [ + "recordings", + "snapshots", + "detect", + "motion", + "improve_contrast", + "motion_threshold", + "motion_contour_area", + ] + for name in self.config.cameras.keys(): - self.client.message_callback_add( - f"{self.mqtt_config.topic_prefix}/{name}/recordings/set", - self.on_mqtt_command, - ) - self.client.message_callback_add( - f"{self.mqtt_config.topic_prefix}/{name}/snapshots/set", - self.on_mqtt_command, - ) - self.client.message_callback_add( - f"{self.mqtt_config.topic_prefix}/{name}/detect/set", - self.on_mqtt_command, - ) - self.client.message_callback_add( - f"{self.mqtt_config.topic_prefix}/{name}/motion/set", - self.on_mqtt_command, - ) - self.client.message_callback_add( - f"{self.mqtt_config.topic_prefix}/{name}/improve_contrast/set", - self.on_mqtt_command, - ) - self.client.message_callback_add( - f"{self.mqtt_config.topic_prefix}/{name}/motion_threshold/set", - self.on_mqtt_command, - ) - self.client.message_callback_add( - f"{self.mqtt_config.topic_prefix}/{name}/motion_contour_area/set", - self.on_mqtt_command, - ) + for callback in callback_types: + # We need to pre-clear existing set topics because in previous + # versions the webUI retained on the /set topic but this is + # no longer the case. + self.client.publish( + f"{self.mqtt_config.topic_prefix}/{name}/{callback}/set", + None, + retain=True, + ) + self.client.message_callback_add( + f"{self.mqtt_config.topic_prefix}/{name}/{callback}/set", + self.on_mqtt_command, + ) self.client.message_callback_add( f"{self.mqtt_config.topic_prefix}/restart", self.on_mqtt_command From 3bec28ffef8c4b57d760ac8b10d9ba7cf01b56b5 Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Mon, 16 Jan 2023 17:17:03 -0600 Subject: [PATCH 14/16] handle timezones with partial hour offsets (#5115) * handle timezones with partial hour offsets * cleanup --- frigate/http.py | 49 ++++++++++++++++++++++++++++++------------------- frigate/util.py | 14 +++++++++++++- 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/frigate/http.py b/frigate/http.py index 815c700cc..a83b94751 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -41,6 +41,7 @@ from frigate.util import ( ffprobe_stream, restart_frigate, vainfo_hwaccel, + get_tz_modifiers, ) from frigate.storage import StorageMaintainer from frigate.version import VERSION @@ -91,7 +92,7 @@ def is_healthy(): @bp.route("/events/summary") def events_summary(): tz_name = request.args.get("timezone", default="utc", type=str) - tz_offset = f"{int(datetime.now(pytz.timezone(tz_name)).utcoffset().total_seconds()/60/60)} hour" + hour_modifier, minute_modifier = get_tz_modifiers(tz_name) has_clip = request.args.get("has_clip", type=int) has_snapshot = request.args.get("has_snapshot", type=int) @@ -111,7 +112,10 @@ def events_summary(): Event.camera, Event.label, fn.strftime( - "%Y-%m-%d", fn.datetime(Event.start_time, "unixepoch", tz_offset) + "%Y-%m-%d", + fn.datetime( + Event.start_time, "unixepoch", hour_modifier, minute_modifier + ), ).alias("day"), Event.zones, fn.COUNT(Event.id).alias("count"), @@ -121,7 +125,10 @@ def events_summary(): Event.camera, Event.label, fn.strftime( - "%Y-%m-%d", fn.datetime(Event.start_time, "unixepoch", tz_offset) + "%Y-%m-%d", + fn.datetime( + Event.start_time, "unixepoch", hour_modifier, minute_modifier + ), ), Event.zones, ) @@ -912,12 +919,14 @@ def get_recordings_storage_usage(): @bp.route("//recordings/summary") def recordings_summary(camera_name): tz_name = request.args.get("timezone", default="utc", type=str) - tz_offset = f"{int(datetime.now(pytz.timezone(tz_name)).utcoffset().total_seconds()/60/60)} hour" + hour_modifier, minute_modifier = get_tz_modifiers(tz_name) recording_groups = ( Recordings.select( fn.strftime( "%Y-%m-%d %H", - fn.datetime(Recordings.start_time, "unixepoch", tz_offset), + fn.datetime( + Recordings.start_time, "unixepoch", hour_modifier, minute_modifier + ), ).alias("hour"), fn.SUM(Recordings.duration).alias("duration"), fn.SUM(Recordings.motion).alias("motion"), @@ -927,13 +936,17 @@ def recordings_summary(camera_name): .group_by( fn.strftime( "%Y-%m-%d %H", - fn.datetime(Recordings.start_time, "unixepoch", tz_offset), + fn.datetime( + Recordings.start_time, "unixepoch", hour_modifier, minute_modifier + ), ) ) .order_by( fn.strftime( "%Y-%m-%d H", - fn.datetime(Recordings.start_time, "unixepoch", tz_offset), + fn.datetime( + Recordings.start_time, "unixepoch", hour_modifier, minute_modifier + ), ).desc() ) ) @@ -942,7 +955,9 @@ def recordings_summary(camera_name): Event.select( fn.strftime( "%Y-%m-%d %H", - fn.datetime(Event.start_time, "unixepoch", tz_offset), + fn.datetime( + Event.start_time, "unixepoch", hour_modifier, minute_modifier + ), ).alias("hour"), fn.COUNT(Event.id).alias("count"), ) @@ -950,7 +965,9 @@ def recordings_summary(camera_name): .group_by( fn.strftime( "%Y-%m-%d %H", - fn.datetime(Event.start_time, "unixepoch", tz_offset), + fn.datetime( + Event.start_time, "unixepoch", hour_modifier, minute_modifier + ), ), ) .objects() @@ -1147,17 +1164,11 @@ def vod_hour_no_timezone(year_month, day, hour, camera_name): # TODO make this nicer when vod module is removed @bp.route("/vod/////") def vod_hour(year_month, day, hour, camera_name, tz_name): - tz_offset = int( - datetime.now(pytz.timezone(tz_name.replace(",", "/"))) - .utcoffset() - .total_seconds() - / 60 - / 60 - ) parts = year_month.split("-") - start_date = datetime( - int(parts[0]), int(parts[1]), int(day), int(hour), tzinfo=timezone.utc - ) - timedelta(hours=tz_offset) + start_date = ( + datetime(int(parts[0]), int(parts[1]), int(day), int(hour), tzinfo=timezone.utc) + - datetime.now(pytz.timezone(tz_name.replace(",", "/"))).utcoffset() + ) end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1) start_ts = start_date.timestamp() end_ts = end_date.timestamp() diff --git a/frigate/util.py b/frigate/util.py index 6de537a0d..69ead2a7a 100755 --- a/frigate/util.py +++ b/frigate/util.py @@ -14,12 +14,13 @@ from abc import ABC, abstractmethod from collections import Counter from collections.abc import Mapping from multiprocessing import shared_memory -from typing import Any, AnyStr +from typing import Any, AnyStr, Tuple import cv2 import numpy as np import os import psutil +import pytz from frigate.const import REGEX_HTTP_CAMERA_USER_PASS, REGEX_RTSP_CAMERA_USER_PASS @@ -1040,3 +1041,14 @@ class SharedMemoryFrameManager(FrameManager): self.shm_store[name].close() self.shm_store[name].unlink() del self.shm_store[name] + + +def get_tz_modifiers(tz_name: str) -> Tuple[str, str]: + seconds_offset = ( + datetime.datetime.now(pytz.timezone(tz_name)).utcoffset().total_seconds() + ) + hours_offset = int(seconds_offset / 60 / 60) + minutes_offset = int(seconds_offset / 60 - hours_offset * 60) + hour_modifier = f"{hours_offset} hour" + minute_modifier = f"{minutes_offset} minute" + return hour_modifier, minute_modifier From a7751f210b89513f8e0ce9366dc298e32aa9892f Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Mon, 16 Jan 2023 20:30:48 -0300 Subject: [PATCH 15/16] Add dependabot auto merge workflow (#5105) --- .github/workflows/dependabot-auto-merge.yaml | 22 ++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/dependabot-auto-merge.yaml diff --git a/.github/workflows/dependabot-auto-merge.yaml b/.github/workflows/dependabot-auto-merge.yaml new file mode 100644 index 000000000..873350876 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yaml @@ -0,0 +1,22 @@ +name: dependabot-auto-merge +on: pull_request + +permissions: + contents: write + +jobs: + dependabot-auto-merge: + runs-on: ubuntu-latest + if: github.actor == 'dependabot[bot]' + steps: + - name: Get Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Enable auto-merge for Dependabot PRs + if: steps.metadata.outputs.dependency-type == 'direct:development' && (steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.update-type == 'version-update:semver-patch') + run: gh pr merge --auto --squash "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 19afb035ffac957c1e469f43efc40babea8ff5ba Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Mon, 16 Jan 2023 16:50:35 -0700 Subject: [PATCH 16/16] Rewrite restream (#5106) * Tear out restream config * Rework birdseye restream * Create go2rtc config handler * Fix bug * Write start script * Rework style * Fix python run syntax * Output as json instead of yaml * Put old live config back and fix birdseye references * Fix camera webUI * Add frigate env var subsitutions * Fix webui checks * Check keys * Remove unused prest * Fix tests * Update restream docs * Update restream docs * Update live docs * Update camera specific recommendation * Update more docs * add links for the docs Co-authored-by: Felipe Santos * Update note about supported audio codecs * Move restream to go2rtc * Docs fixes * Add verification of stream name * Ensure that webUI uses camera name * Update docs to reflect new live stream name * Fix check * Formatting * Remove audio from detect Co-authored-by: Felipe Santos * Fix docs * Don't handle env variable substitution * Add go2rtc version * Clarify docs Co-authored-by: Felipe Santos --- docker/rootfs/etc/services.d/go2rtc/run | 8 +- .../rootfs/usr/local/go2rtc/create_config.py | 31 ++++++ docker/rootfs/usr/local/go2rtc/go2rtc.yaml | 6 -- docs/docs/configuration/camera_specific.md | 12 +-- docs/docs/configuration/cameras.md | 3 +- docs/docs/configuration/index.md | 44 ++++---- docs/docs/configuration/live.md | 101 +++++++++++++----- docs/docs/configuration/restream.md | 48 +++------ docs/docs/troubleshooting/faqs.md | 4 +- frigate/config.py | 71 ++++++------ frigate/ffmpeg_presets.py | 19 ---- frigate/output.py | 14 +-- frigate/restream.py | 58 +--------- frigate/test/test_config.py | 96 ++--------------- frigate/test/test_restream.py | 82 -------------- web/src/routes/Birdseye.jsx | 6 +- web/src/routes/Camera.jsx | 15 +-- 17 files changed, 212 insertions(+), 406 deletions(-) create mode 100644 docker/rootfs/usr/local/go2rtc/create_config.py delete mode 100644 docker/rootfs/usr/local/go2rtc/go2rtc.yaml delete mode 100644 frigate/test/test_restream.py diff --git a/docker/rootfs/etc/services.d/go2rtc/run b/docker/rootfs/etc/services.d/go2rtc/run index 9b41e517b..db5c2a869 100755 --- a/docker/rootfs/etc/services.d/go2rtc/run +++ b/docker/rootfs/etc/services.d/go2rtc/run @@ -4,12 +4,8 @@ set -o errexit -o nounset -o pipefail -if [[ -f "/config/frigate-go2rtc.yaml" ]]; then - config_path="/config/frigate-go2rtc.yaml" -else - config_path="/usr/local/go2rtc/go2rtc.yaml" -fi +raw_config=$(python3 /usr/local/go2rtc/create_config.py) # Replace the bash process with the go2rtc process, redirecting stderr to stdout exec 2>&1 -exec go2rtc -config="${config_path}" +exec go2rtc -config="${raw_config}" diff --git a/docker/rootfs/usr/local/go2rtc/create_config.py b/docker/rootfs/usr/local/go2rtc/create_config.py new file mode 100644 index 000000000..20e8de494 --- /dev/null +++ b/docker/rootfs/usr/local/go2rtc/create_config.py @@ -0,0 +1,31 @@ +"""Creates a go2rtc config file.""" + +import json +import os +import yaml + + +config_file = os.environ.get("CONFIG_FILE", "/config/config.yml") + +# Check if we can use .yaml instead of .yml +config_file_yaml = config_file.replace(".yml", ".yaml") +if os.path.isfile(config_file_yaml): + config_file = config_file_yaml + +with open(config_file) as f: + raw_config = f.read() + +if config_file.endswith((".yaml", ".yml")): + config = yaml.safe_load(raw_config) +elif config_file.endswith(".json"): + config = json.loads(raw_config) + +go2rtc_config: dict[str, any] = config["go2rtc"] + +if not go2rtc_config.get("log", {}).get("format"): + go2rtc_config["log"] = {"format": "text"} + +if not go2rtc_config.get("webrtc", {}).get("candidates", []): + go2rtc_config["webrtc"] = {"candidates": ["stun:8555"]} + +print(json.dumps(go2rtc_config)) \ No newline at end of file diff --git a/docker/rootfs/usr/local/go2rtc/go2rtc.yaml b/docker/rootfs/usr/local/go2rtc/go2rtc.yaml deleted file mode 100644 index a2f8d6077..000000000 --- a/docker/rootfs/usr/local/go2rtc/go2rtc.yaml +++ /dev/null @@ -1,6 +0,0 @@ -log: - format: text - -webrtc: - candidates: - - stun:8555 diff --git a/docs/docs/configuration/camera_specific.md b/docs/docs/configuration/camera_specific.md index ca3766f65..8089f7104 100644 --- a/docs/docs/configuration/camera_specific.md +++ b/docs/docs/configuration/camera_specific.md @@ -14,6 +14,12 @@ This page makes use of presets of FFmpeg args. For more information on presets, Note that mjpeg cameras require encoding the video into h264 for recording, and restream roles. This will use significantly more CPU than if the cameras supported h264 feeds directly. It is recommended to use the restream role to create an h264 restream and then use that as the source for ffmpeg. ```yaml +go2rtc: + streams: + mjpeg_cam: ffmpeg:{your_mjpeg_stream_url}#video=h264#hardware # <- use hardware acceleration to create an h264 stream usable for other components. + +cameras: + ... mjpeg_cam: ffmpeg: inputs: @@ -21,12 +27,6 @@ Note that mjpeg cameras require encoding the video into h264 for recording, and roles: - detect - record - - path: {your_mjpeg_stream_url} - roles: - - restream - restream: - enabled: true - video_encoding: h264 ``` ## JPEG Stream Cameras diff --git a/docs/docs/configuration/cameras.md b/docs/docs/configuration/cameras.md index a889136f2..d8fefed8f 100644 --- a/docs/docs/configuration/cameras.md +++ b/docs/docs/configuration/cameras.md @@ -15,7 +15,6 @@ Each role can only be assigned to one input per camera. The options for roles ar | ---------- | ---------------------------------------------------------------------------------------- | | `detect` | Main feed for object detection | | `record` | Saves segments of the video feed based on configuration settings. [docs](record.md) | -| `restream` | Broadcast as RTSP feed and use the full res stream for live view. [docs](restream.md) | | `rtmp` | Deprecated: Broadcast as an RTMP feed for other services to consume. [docs](restream.md) | ```yaml @@ -29,7 +28,7 @@ cameras: - path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 roles: - detect - - rtmp + - rtmp # <- deprecated, recommend using restream instead - path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/live roles: - record diff --git a/docs/docs/configuration/index.md b/docs/docs/configuration/index.md index 969f693e4..254bbb1f4 100644 --- a/docs/docs/configuration/index.md +++ b/docs/docs/configuration/index.md @@ -126,6 +126,9 @@ environment_vars: birdseye: # Optional: Enable birdseye view (default: shown below) enabled: True + # Optional: Restream birdseye via RTSP (default: shown below) + # NOTE: Enabling this will set birdseye to run 24/7 which may increase CPU usage somewhat. + restream: False # Optional: Width of the output resolution (default: shown below) width: 1280 # Optional: Height of the output resolution (default: shown below) @@ -352,32 +355,21 @@ rtmp: enabled: False # Optional: Restream configuration -# NOTE: Can be overridden at the camera level -restream: - # Optional: Enable the restream (default: True) - enabled: True - # Optional: Set the audio codecs to restream with - # possible values are aac, copy, opus. Set to copy - # only to avoid transcoding (default: shown below) - audio_encoding: - - aac - - opus - # 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 - # Optional: jsmpeg stream configuration for WebUI - jsmpeg: - # Optional: Set the height of the jsmpeg stream. (default: 720) - # This must be less than or equal to the height of the detect stream. Lower resolutions - # reduce bandwidth required for viewing the jsmpeg stream. Width is computed to match known aspect ratio. - height: 720 - # Optional: Set the encode quality of the jsmpeg stream (default: shown below) - # 1 is the highest quality, and 31 is the lowest. Lower quality feeds utilize less CPU resources. - quality: 8 +# Uses https://github.com/AlexxIT/go2rtc (v0.1-rc9) +go2rtc: + +# Optional: jsmpeg stream configuration for WebUI +live: + # Optional: Set the name of the stream that should be used for live view + # in frigate WebUI. (default: name of camera) + stream_name: camera_name + # Optional: Set the height of the jsmpeg stream. (default: 720) + # This must be less than or equal to the height of the detect stream. Lower resolutions + # reduce bandwidth required for viewing the jsmpeg stream. Width is computed to match known aspect ratio. + height: 720 + # Optional: Set the encode quality of the jsmpeg stream (default: shown below) + # 1 is the highest quality, and 31 is the lowest. Lower quality feeds utilize less CPU resources. + quality: 8 # Optional: in-feed timestamp style configuration # NOTE: Can be overridden at the camera level diff --git a/docs/docs/configuration/live.md b/docs/docs/configuration/live.md index cec16751a..f453a508f 100644 --- a/docs/docs/configuration/live.md +++ b/docs/docs/configuration/live.md @@ -9,44 +9,93 @@ Frigate has different live view options, some of which require [restream](restre Live view options can be selected while viewing the live stream. The options are: -| Source | Latency | Frame Rate | Resolution | Audio | Requires Restream | Other Limitations | -| ------ | ------- | -------------------------------------- | -------------- | ---------------------------- | ----------------- | -------------------------------------------- | -| jsmpeg | low | same as `detect -> fps`, capped at 10 | same as detect | no | no | none | -| mse | low | native | native | yes (depends on audio codec) | yes | not supported on iOS, Firefox is h.264 only | -| webrtc | lowest | native | native | yes (depends on audio codec) | yes | requires extra config, doesn't support h.265 | +| Source | Latency | Frame Rate | Resolution | Audio | Requires Restream | Other Limitations | +| ------ | ------- | ------------------------------------- | -------------- | ---------------------------- | ----------------- | -------------------------------------------- | +| jsmpeg | low | same as `detect -> fps`, capped at 10 | same as detect | no | no | none | +| mse | low | native | native | yes (depends on audio codec) | yes | not supported on iOS, Firefox is h.264 only | +| webrtc | lowest | native | native | yes (depends on audio codec) | yes | requires extra config, doesn't support h.265 | + +### Audio Support + +MSE Requires AAC audio, WebRTC requires PCMU/PCMA, or opus audio. If you want to support both MSE and WebRTC then your restream config needs to use ffmpeg to set both. + +```yaml +go2rtc: + streams: + test_cam: ffmpeg:rtsp://192.168.1.5:554/live0#video=copy#audio=aac#audio=opus +``` + +However, chances are that your camera already provides at least one usable audio type, so you just need restream to add the missing one. For example, if your camera outputs audio in AAC format: + +```yaml +go2rtc: + streams: + test_cam: ffmpeg:rtsp://192.168.1.5:554/live0#video=copy#audio=copy#audio=opus +``` + +Which will reuse your camera AAC audio, while also adding one track in OPUS format. + +If your camera uses RTSP and supports the audio type for the live view you want to use, then you can pass the camera stream to go2rtc without ffmpeg. + +```yaml +go2rtc: + streams: + test_cam: rtsp://192.168.1.5:554/live0 +``` + +### Setting Stream For Live UI + +There may be some cameras that you would prefer to use the sub stream for live view, but the main stream for recording. This can be done via `live -> stream_name`. + +```yaml +go2rtc: + streams: + test_cam: ffmpeg:rtsp://192.168.1.5:554/live0#video=copy#audio=aac#audio=opus + test_cam_sub: ffmpeg:rtsp://192.168.1.5:554/substream#video=copy#audio=aac#audio=opus + +cameras: + test_cam: + ffmpeg: + output_args: + record: preset-record-generic-audio-copy + inputs: + - path: rtsp://127.0.0.1:8554/test_cam?video=copy&audio=aac # <--- the name here must match the name of the camera in restream + input_args: preset-rtsp-restream + roles: + - record + - path: rtsp://127.0.0.1:8554/test_cam_sub?video=copy # <--- the name here must match the name of the camera_sub in restream + input_args: preset-rtsp-restream + roles: + - detect + live: + stream_name: test_cam_sub +``` ### WebRTC extra configuration: WebRTC works by creating a TCP or UDP connection on port `8555`. However, it requires additional configuration: -* For external access, over the internet, setup your router to forward port `8555` to port `8555` on the Frigate device, for both TCP and UDP. -* For internal/local access, you will need to use a custom go2rtc config: - 1. Create your own go2rtc config, based on [Frigate's internal go2rtc config](https://github.com/blakeblackshear/frigate/blob/dev/docker/rootfs/usr/local/go2rtc/go2rtc.yaml). - 2. Add your internal IP to the list of `candidates`. Here is an example, assuming that `192.168.1.10` is the local IP of the device running Frigate: +- For external access, over the internet, setup your router to forward port `8555` to port `8555` on the Frigate device, for both TCP and UDP. +- For internal/local access, you will need to use a custom go2rtc config: - ```yaml title="/config/frigate-go2rtc.yaml" - log: - format: text + 1. Add your internal IP to the list of `candidates`. Here is an example, assuming that `192.168.1.10` is the local IP of the device running Frigate: - webrtc: - candidates: - - 192.168.1.10:8555 - - stun:8555 - ``` - - 3. Mount this config file at `/config/frigate-go2rtc.yaml`. Here is an example, if you run Frigate through docker-compose: - - ```yaml title="docker-compose.yaml" - volumes: - - /path/to/your/go2rtc.yaml:/config/frigate-go2rtc.yaml - ``` + ```yaml + go2rtc: + streams: + test_cam: ... + webrtc: + candidates: + - 192.168.1.10:8555 + - stun:8555 + ``` :::note If you are having difficulties getting WebRTC to work and you are running Frigate with docker, you may want to try changing the container network mode: -* `network: host`, in this mode you don't need to forward any ports. The services inside of the Frigate container will have full access to the network interfaces of your host machine as if they were running natively and not in a container. Any port conflicts will need to be resolved. This network mode is recommended by go2rtc, but we recommend you only use it if necessary. -* `network: bridge` creates a virtual network interface for the container, and the container will have full access to it. You also don't need to forward any ports, however, the IP for accessing Frigate locally will differ from the IP of the host machine. Your router will see Frigate as if it was a new device connected in the network. +- `network: host`, in this mode you don't need to forward any ports. The services inside of the Frigate container will have full access to the network interfaces of your host machine as if they were running natively and not in a container. Any port conflicts will need to be resolved. This network mode is recommended by go2rtc, but we recommend you only use it if necessary. +- `network: bridge` creates a virtual network interface for the container, and the container will have full access to it. You also don't need to forward any ports, however, the IP for accessing Frigate locally will differ from the IP of the host machine. Your router will see Frigate as if it was a new device connected in the network. ::: diff --git a/docs/docs/configuration/restream.md b/docs/docs/configuration/restream.md index 845baed8e..76c8444d9 100644 --- a/docs/docs/configuration/restream.md +++ b/docs/docs/configuration/restream.md @@ -7,29 +7,11 @@ title: Restream Frigate can restream your video feed as an RTSP feed for other applications such as Home Assistant to utilize it at `rtsp://:8554/`. Port 8554 must be open. [This allows you to use a video feed for detection in Frigate and Home Assistant live view at the same time without having to make two separate connections to the camera](#reduce-connections-to-camera). The video feed is copied from the original video feed directly to avoid re-encoding. This feed does not include any annotation by Frigate. -#### Copy Audio - -Different live view technologies (ex: MSE, WebRTC) support different audio codecs. The `restream -> audio_encoding` field tells the restream to make multiple streams available so that all live view technologies are supported. Some camera streams don't work well with this, in which case `restream -> audio_encoding` should be set to `copy` only. +Frigate uses [go2rtc](https://github.com/AlexxIT/go2rtc) to provide its restream and MSE/WebRTC capabilities. The go2rtc config is hosted at the `go2rtc` in the config, see [go2rtc docs](https://github.com/AlexxIT/go2rtc#configuration) for more advanced configurations and features. #### Birdseye Restream -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 {#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 -``` +Birdseye RTSP restream can be enabled at `birdseye -> restream` and accessed at `rtsp://:8554/birdseye`. Enabling the restream will cause birdseye to run 24/7 which may increase CPU usage somewhat. ### RTMP (Deprecated) @@ -44,20 +26,21 @@ Some cameras only support one active connection or you may just want to have a s One connection is made to the camera. One for the restream, `detect` and `record` connect to the restream. ```yaml +go2rtc: + streams: + test_cam: ffmpeg:rtsp://192.168.1.5:554/live0#video=copy#audio=aac#audio=opus + cameras: test_cam: ffmpeg: output_args: - record: preset-record-audio-copy + record: preset-record-generic-audio-copy inputs: - - path: rtsp://127.0.0.1:8554/test_cam?video=copy&audio=aac # <--- the name here must match the name of the camera + - path: rtsp://127.0.0.1:8554/test_cam?video=copy&audio=aac # <--- the name here must match the name of the camera in restream input_args: preset-rtsp-restream roles: - record - detect - - path: rtsp://192.168.1.5:554/live0 # <--- 1 connection to camera stream - roles: - - restream ``` ### With Sub Stream @@ -65,20 +48,23 @@ cameras: Two connections are made to the camera. One for the sub stream, one for the restream, `record` connects to the restream. ```yaml +go2rtc: + streams: + test_cam: ffmpeg:rtsp://192.168.1.5:554/live0#video=copy#audio=aac#audio=opus + test_cam_sub: ffmpeg:rtsp://192.168.1.5:554/substream#video=copy#audio=aac#audio=opus + cameras: test_cam: ffmpeg: output_args: - record: preset-record-audio-copy + record: preset-record-generic-audio-copy inputs: - - path: rtsp://127.0.0.1:8554/test_cam?video=copy&audio=aac # <--- the name here must match the name of the camera + - path: rtsp://127.0.0.1:8554/test_cam?video=copy&audio=aac # <--- the name here must match the name of the camera in restream input_args: preset-rtsp-restream roles: - record - - path: rtsp://192.168.1.5:554/stream # <--- camera high res stream - roles: - - restream - - path: rtsp://192.168.1.5:554/substream # <--- camera sub stream + - path: rtsp://127.0.0.1:8554/test_cam_sub?video=copy&audio=aac # <--- the name here must match the name of the camera_sub in restream + input_args: preset-rtsp-restream roles: - detect ``` diff --git a/docs/docs/troubleshooting/faqs.md b/docs/docs/troubleshooting/faqs.md index ce68f6a17..15ce9bc12 100644 --- a/docs/docs/troubleshooting/faqs.md +++ b/docs/docs/troubleshooting/faqs.md @@ -14,7 +14,7 @@ By default, Frigate removes audio from recordings to reduce the likelihood of fa ```yaml title="frigate.yml" ffmpeg: output_args: - record: preset-record-generic-audio + record: preset-record-generic-audio-aac ``` ### My mjpeg stream or snapshots look green and crazy @@ -25,7 +25,7 @@ This almost always means that the width/height defined for your camera are not c ### I can't view events or recordings in the Web UI. -Ensure your cameras send h264 encoded video, or [transcode them](/configuration/restream.md#changing-restream-codec). +Ensure your cameras send h264 encoded video, or [transcode them](/configuration/restream.md). ### "[mov,mp4,m4a,3gp,3g2,mj2 @ 0x5639eeb6e140] moov atom not found" diff --git a/frigate/config.py b/frigate/config.py index bcf5e320c..9cf20937b 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -344,6 +344,7 @@ class BirdseyeModeEnum(str, Enum): class BirdseyeConfig(FrigateBaseModel): enabled: bool = Field(default=True, title="Enable birdseye view.") + restream: bool = Field(default=False, title="Restream birdseye via RTSP.") width: int = Field(default=1280, title="Birdseye width.") height: int = Field(default=720, title="Birdseye height.") quality: int = Field( @@ -405,7 +406,6 @@ class FfmpegConfig(FrigateBaseModel): class CameraRoleEnum(str, Enum): record = "record" - restream = "restream" rtmp = "rtmp" detect = "detect" @@ -519,39 +519,15 @@ class RtmpConfig(FrigateBaseModel): enabled: bool = Field(default=False, title="RTMP restreaming enabled.") -class JsmpegStreamConfig(FrigateBaseModel): - height: int = Field(default=720, title="Live camera view height.") - quality: int = Field(default=8, ge=1, le=31, title="Live camera view quality.") +class CameraLiveConfig(FrigateBaseModel): + stream_name: str = Field(default="", title="Name of restream to use as live view.") + height: int = Field(default=720, title="Live camera view height") + quality: int = Field(default=8, ge=1, le=31, title="Live camera view quality") -class RestreamVideoCodecEnum(str, Enum): - copy = "copy" - h264 = "h264" - h265 = "h265" - - -class RestreamAudioCodecEnum(str, Enum): - aac = "aac" - copy = "copy" - opus = "opus" - - -class RestreamConfig(FrigateBaseModel): - enabled: bool = Field(default=True, title="Restreaming enabled.") - audio_encoding: list[RestreamAudioCodecEnum] = Field( - default=[RestreamAudioCodecEnum.aac, RestreamAudioCodecEnum.opus], - title="Codecs to supply for audio.", - ) - video_encoding: RestreamVideoCodecEnum = Field( - default=RestreamVideoCodecEnum.copy, title="Method for encoding the restream." - ) - force_audio: bool = Field( - default=True, title="Force audio compatibility with the browser." - ) - birdseye: bool = Field(default=False, title="Restream the birdseye feed via RTSP.") - jsmpeg: JsmpegStreamConfig = Field( - default_factory=JsmpegStreamConfig, title="Jsmpeg Stream Configuration." - ) +class RestreamConfig(BaseModel): + class Config: + extra = Extra.allow class CameraUiConfig(FrigateBaseModel): @@ -578,8 +554,8 @@ class CameraConfig(FrigateBaseModel): rtmp: RtmpConfig = Field( default_factory=RtmpConfig, title="RTMP restreaming configuration." ) - restream: RestreamConfig = Field( - default_factory=RestreamConfig, title="Restreaming configuration." + live: CameraLiveConfig = Field( + default_factory=CameraLiveConfig, title="Live playback settings." ) snapshots: SnapshotsConfig = Field( default_factory=SnapshotsConfig, title="Snapshot configuration." @@ -621,7 +597,6 @@ class CameraConfig(FrigateBaseModel): config["ffmpeg"]["inputs"][0]["roles"] = [ "record", "detect", - "restream", ] if has_rtmp: @@ -758,9 +733,17 @@ def verify_config_roles(camera_config: CameraConfig) -> None: f"Camera {camera_config.name} has rtmp enabled, but rtmp is not assigned to an input." ) - if camera_config.restream.enabled and not "restream" in assigned_roles: - raise ValueError( - f"Camera {camera_config.name} has restream enabled, but restream is not assigned to an input." + +def verify_valid_live_stream_name( + frigate_config: FrigateConfig, camera_config: CameraConfig +) -> None: + """Verify that a restream exists to use for live view.""" + if ( + camera_config.live.stream_name + not in frigate_config.go2rtc.dict().get("streams", {}).keys() + ): + return ValueError( + f"No restream with name {camera_config.live.stream_name} exists for camera {camera_config.name}." ) @@ -854,7 +837,10 @@ class FrigateConfig(FrigateBaseModel): rtmp: RtmpConfig = Field( default_factory=RtmpConfig, title="Global RTMP restreaming configuration." ) - restream: RestreamConfig = Field( + live: CameraLiveConfig = Field( + default_factory=CameraLiveConfig, title="Live playback settings." + ) + go2rtc: RestreamConfig = Field( default_factory=RestreamConfig, title="Global restream configuration." ) birdseye: BirdseyeConfig = Field( @@ -895,7 +881,7 @@ class FrigateConfig(FrigateBaseModel): "record": ..., "snapshots": ..., "rtmp": ..., - "restream": ..., + "live": ..., "objects": ..., "motion": ..., "detect": ..., @@ -968,7 +954,12 @@ class FrigateConfig(FrigateBaseModel): **camera_config.motion.dict(exclude_unset=True), ) + # Set live view stream if none is set + if not camera_config.live.stream_name: + camera_config.live.stream_name = name + verify_config_roles(camera_config) + verify_valid_live_stream_name(config, camera_config) verify_old_retain_config(camera_config) verify_recording_retention(camera_config) verify_recording_segments_setup_with_reasonable_time(camera_config) diff --git a/frigate/ffmpeg_presets.py b/frigate/ffmpeg_presets.py index 3ff35805f..57bd9b9f5 100644 --- a/frigate/ffmpeg_presets.py +++ b/frigate/ffmpeg_presets.py @@ -102,17 +102,6 @@ PRESETS_HW_ACCEL_ENCODE = { "default": "ffmpeg -hide_banner {0} -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency {1}", } -PRESETS_HW_ACCEL_GO2RTC_ENGINE = { - "preset-rpi-32-h264": "v4l2m2m", - "preset-rpi-64-h264": "v4l2m2m", - "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.""" @@ -156,14 +145,6 @@ def parse_preset_hardware_acceleration_encode(arg: Any, input: str, output: str) ) -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/output.py b/frigate/output.py index 414f06bdf..9f02a7afd 100644 --- a/frigate/output.py +++ b/frigate/output.py @@ -415,15 +415,15 @@ def output_frames(config: FrigateConfig, video_output_queue): for camera, cam_config in config.cameras.items(): width = int( - cam_config.restream.jsmpeg.height + cam_config.live.height * (cam_config.frame_shape[1] / cam_config.frame_shape[0]) ) converters[camera] = FFMpegConverter( cam_config.frame_shape[1], cam_config.frame_shape[0], width, - cam_config.restream.jsmpeg.height, - cam_config.restream.jsmpeg.quality, + cam_config.live.height, + cam_config.live.quality, ) broadcasters[camera] = BroadcastThread( camera, converters[camera], websocket_server @@ -436,7 +436,7 @@ def output_frames(config: FrigateConfig, video_output_queue): config.birdseye.width, config.birdseye.height, config.birdseye.quality, - config.restream.birdseye, + config.birdseye.restream, ) broadcasters["birdseye"] = BroadcastThread( "birdseye", converters["birdseye"], websocket_server @@ -449,7 +449,7 @@ def output_frames(config: FrigateConfig, video_output_queue): birdseye_manager = BirdsEyeFrameManager(config, frame_manager) - if config.restream.birdseye: + if config.birdseye.restream: birdseye_buffer = frame_manager.create( "birdseye", birdseye_manager.yuv_shape[0] * birdseye_manager.yuv_shape[1], @@ -479,7 +479,7 @@ def output_frames(config: FrigateConfig, video_output_queue): converters[camera].write(frame.tobytes()) if config.birdseye.enabled and ( - config.restream.birdseye + config.birdseye.restream or any( ws.environ["PATH_INFO"].endswith("birdseye") for ws in websocket_server.manager @@ -494,7 +494,7 @@ def output_frames(config: FrigateConfig, video_output_queue): ): frame_bytes = birdseye_manager.frame.tobytes() - if config.restream.birdseye: + if config.birdseye.restream: birdseye_buffer[:] = frame_bytes converters["birdseye"].write(frame_bytes) diff --git a/frigate/restream.py b/frigate/restream.py index 0fe43fc0e..95582ff98 100644 --- a/frigate/restream.py +++ b/frigate/restream.py @@ -4,42 +4,15 @@ import logging import requests -from typing import Optional - -from frigate.config import FrigateConfig, RestreamAudioCodecEnum, RestreamVideoCodecEnum +from frigate.config import FrigateConfig from frigate.const import BIRDSEYE_PIPE 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, - aCodecs: list[RestreamAudioCodecEnum], - vCodec: RestreamVideoCodecEnum, - engine: Optional[str], -) -> str: - """Get a manual stream for go2rtc.""" - stream = f"ffmpeg:{camera_url}" - - for aCodec in aCodecs: - stream += f"#audio={aCodec}" - - if vCodec == RestreamVideoCodecEnum.copy: - stream += "#video=copy" - else: - stream += f"#video={vCodec}" - - if engine: - stream += f"#hardware={engine}" - - return stream - - class RestreamApi: """Control go2rtc relay API.""" @@ -50,34 +23,7 @@ class RestreamApi: """Add cameras to go2rtc.""" self.relays: dict[str, str] = {} - for cam_name, camera in self.config.cameras.items(): - if not camera.restream.enabled: - continue - - for input in camera.ffmpeg.inputs: - if "restream" in input.roles: - if ( - input.path.startswith("rtsp") - and camera.restream.video_encoding - == RestreamVideoCodecEnum.copy - and camera.restream.audio_encoding - == [RestreamAudioCodecEnum.copy] - ): - self.relays[ - cam_name - ] = f"{escape_special_characters(input.path)}#backchannel=0" - 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), - camera.restream.audio_encoding, - camera.restream.video_encoding, - parse_preset_hardware_acceleration_go2rtc_engine( - self.config.ffmpeg.hwaccel_args - ), - ) - - if self.config.restream.birdseye: + if self.config.birdseye.restream: self.relays[ "birdseye" ] = f"exec:{parse_preset_hardware_acceleration_encode(self.config.ffmpeg.hwaccel_args, f'-f rawvideo -pix_fmt yuv420p -video_size {self.config.birdseye.width}x{self.config.birdseye.height} -r 10 -i {BIRDSEYE_PIPE}', '-rtsp_transport tcp -f rtsp {output}')}" diff --git a/frigate/test/test_config.py b/frigate/test/test_config.py index 97e63cfc5..87f811fb2 100644 --- a/frigate/test/test_config.py +++ b/frigate/test/test_config.py @@ -621,7 +621,7 @@ class TestConfig(unittest.TestCase): "inputs": [ { "path": "rtsp://10.0.0.1:554/video", - "roles": ["detect", "rtmp", "restream"], + "roles": ["detect", "rtmp"], }, {"path": "rtsp://10.0.0.1:554/record", "roles": ["record"]}, ] @@ -883,7 +883,6 @@ class TestConfig(unittest.TestCase): config = { "mqtt": {"host": "mqtt"}, - "restream": {"enabled": False}, "cameras": { "back": { "ffmpeg": { @@ -1096,30 +1095,6 @@ class TestConfig(unittest.TestCase): assert runtime_config.cameras["back"].snapshots.height == 150 assert runtime_config.cameras["back"].snapshots.enabled - def test_global_restream(self): - - config = { - "mqtt": {"host": "mqtt"}, - "restream": {"enabled": True}, - "cameras": { - "back": { - "ffmpeg": { - "inputs": [ - { - "path": "rtsp://10.0.0.1:554/video", - "roles": ["detect"], - }, - ] - }, - } - }, - } - frigate_config = FrigateConfig(**config) - assert config == frigate_config.dict(exclude_unset=True) - - runtime_config = frigate_config.runtime_config - assert runtime_config.cameras["back"].restream.enabled - def test_global_rtmp_disabled(self): config = { @@ -1166,56 +1141,6 @@ class TestConfig(unittest.TestCase): runtime_config = frigate_config.runtime_config assert not runtime_config.cameras["back"].rtmp.enabled - def test_default_restream(self): - - config = { - "mqtt": {"host": "mqtt"}, - "cameras": { - "back": { - "ffmpeg": { - "inputs": [ - { - "path": "rtsp://10.0.0.1:554/video", - "roles": ["detect"], - }, - ] - } - } - }, - } - frigate_config = FrigateConfig(**config) - assert config == frigate_config.dict(exclude_unset=True) - - runtime_config = frigate_config.runtime_config - assert runtime_config.cameras["back"].restream.enabled - - def test_global_restream_merge(self): - - config = { - "mqtt": {"host": "mqtt"}, - "restream": {"enabled": False}, - "cameras": { - "back": { - "ffmpeg": { - "inputs": [ - { - "path": "rtsp://10.0.0.1:554/video", - "roles": ["detect"], - }, - ] - }, - "restream": { - "enabled": True, - }, - } - }, - } - frigate_config = FrigateConfig(**config) - assert config == frigate_config.dict(exclude_unset=True) - - runtime_config = frigate_config.runtime_config - assert runtime_config.cameras["back"].restream.enabled - def test_global_rtmp_merge(self): config = { @@ -1247,7 +1172,6 @@ class TestConfig(unittest.TestCase): config = { "mqtt": {"host": "mqtt"}, - "restream": {"enabled": False}, "cameras": { "back": { "ffmpeg": { @@ -1275,7 +1199,7 @@ class TestConfig(unittest.TestCase): config = { "mqtt": {"host": "mqtt"}, - "restream": {"jsmpeg": {"quality": 4}}, + "live": {"quality": 4}, "cameras": { "back": { "ffmpeg": { @@ -1293,7 +1217,7 @@ class TestConfig(unittest.TestCase): assert config == frigate_config.dict(exclude_unset=True) runtime_config = frigate_config.runtime_config - assert runtime_config.cameras["back"].restream.jsmpeg.quality == 4 + assert runtime_config.cameras["back"].live.quality == 4 def test_default_live(self): @@ -1316,13 +1240,13 @@ class TestConfig(unittest.TestCase): assert config == frigate_config.dict(exclude_unset=True) runtime_config = frigate_config.runtime_config - assert runtime_config.cameras["back"].restream.jsmpeg.quality == 8 + assert runtime_config.cameras["back"].live.quality == 8 def test_global_live_merge(self): config = { "mqtt": {"host": "mqtt"}, - "restream": {"jsmpeg": {"quality": 4, "height": 480}}, + "live": {"quality": 4, "height": 480}, "cameras": { "back": { "ffmpeg": { @@ -1333,10 +1257,8 @@ class TestConfig(unittest.TestCase): }, ] }, - "restream": { - "jsmpeg": { - "quality": 7, - } + "live": { + "quality": 7, }, } }, @@ -1345,8 +1267,8 @@ class TestConfig(unittest.TestCase): assert config == frigate_config.dict(exclude_unset=True) runtime_config = frigate_config.runtime_config - assert runtime_config.cameras["back"].restream.jsmpeg.quality == 7 - assert runtime_config.cameras["back"].restream.jsmpeg.height == 480 + assert runtime_config.cameras["back"].live.quality == 7 + assert runtime_config.cameras["back"].live.height == 480 def test_global_timestamp_style(self): diff --git a/frigate/test/test_restream.py b/frigate/test/test_restream.py deleted file mode 100644 index 2be781306..000000000 --- a/frigate/test/test_restream.py +++ /dev/null @@ -1,82 +0,0 @@ -"""Test restream.py.""" - -from unittest import TestCase, main -from unittest.mock import patch - -from frigate.config import FrigateConfig -from frigate.restream import RestreamApi - - -class TestRestream(TestCase): - def setUp(self) -> None: - """Setup the tests.""" - self.config = { - "mqtt": {"host": "mqtt"}, - "restream": {"enabled": False}, - "cameras": { - "back": { - "ffmpeg": { - "inputs": [ - { - "path": "rtsp://10.0.0.1:554/video", - "roles": ["detect", "restream"], - }, - ] - }, - "restream": { - "enabled": True, - "audio_encoding": ["copy"], - }, - }, - "front": { - "ffmpeg": { - "inputs": [ - { - "path": "http://10.0.0.1:554/video/stream", - "roles": ["detect", "restream"], - }, - ] - }, - "restream": { - "enabled": True, - }, - }, - }, - } - - @patch("frigate.restream.requests") - 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) - restream.add_cameras() - assert restream.relays["back"].startswith("rtsp") - - @patch("frigate.restream.requests") - 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) diff --git a/web/src/routes/Birdseye.jsx b/web/src/routes/Birdseye.jsx index 6571d8eab..feaf6ff0e 100644 --- a/web/src/routes/Birdseye.jsx +++ b/web/src/routes/Birdseye.jsx @@ -18,7 +18,7 @@ export default function Birdseye() { } let player; - if (viewSource == 'mse' && config.restream.birdseye) { + if (viewSource == 'mse' && config.birdseye.restream) { if ('MediaSource' in window) { player = ( @@ -36,7 +36,7 @@ export default function Birdseye() { ); } - } else if (viewSource == 'webrtc' && config.restream.birdseye) { + } else if (viewSource == 'webrtc' && config.birdseye.restream) { player = (
@@ -61,7 +61,7 @@ export default function Birdseye() { Birdseye - {config.restream.birdseye && ( + {config.birdseye.restream && (