diff --git a/.devcontainer/post_create.sh b/.devcontainer/post_create.sh index 1a1832f3b6..ee08880163 100755 --- a/.devcontainer/post_create.sh +++ b/.devcontainer/post_create.sh @@ -17,7 +17,7 @@ sudo chown -R "$(id -u):$(id -g)" /media/frigate # When started as a service, LIBAVFORMAT_VERSION_MAJOR is defined in the # s6 service file. For dev, where frigate is started from an interactive # shell, we define it in .bashrc instead. -echo 'export LIBAVFORMAT_VERSION_MAJOR=$(ffmpeg -version | grep -Po "libavformat\W+\K\d+")' >> $HOME/.bashrc +echo 'export LIBAVFORMAT_VERSION_MAJOR=$(/usr/lib/ffmpeg/7.0/bin/ffmpeg -version | grep -Po "libavformat\W+\K\d+")' >> $HOME/.bashrc make version diff --git a/docker/main/Dockerfile b/docker/main/Dockerfile index 5ae0418458..92e28a3816 100644 --- a/docker/main/Dockerfile +++ b/docker/main/Dockerfile @@ -201,7 +201,8 @@ ENV ALLOW_RESET=True # Disable tokenizer parallelism warning ENV TOKENIZERS_PARALLELISM=true -ENV PATH="/usr/lib/btbn-ffmpeg/bin:/usr/local/go2rtc/bin:/usr/local/tempio/bin:/usr/local/nginx/sbin:${PATH}" +ENV PATH="/usr/local/go2rtc/bin:/usr/local/tempio/bin:/usr/local/nginx/sbin:${PATH}" +ENV LIBAVFORMAT_VERSION_MAJOR=60 # Install dependencies RUN --mount=type=bind,source=docker/main/install_deps.sh,target=/deps/install_deps.sh \ diff --git a/docker/main/install_deps.sh b/docker/main/install_deps.sh index 5c6ae619d7..34f2e093e7 100755 --- a/docker/main/install_deps.sh +++ b/docker/main/install_deps.sh @@ -39,18 +39,26 @@ apt-get -qq install --no-install-recommends --no-install-suggests -y \ # btbn-ffmpeg -> amd64 if [[ "${TARGETARCH}" == "amd64" ]]; then - mkdir -p /usr/lib/btbn-ffmpeg - wget -qO btbn-ffmpeg.tar.xz "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2024-09-04-18-56/ffmpeg-n7.0.2-15-g0458a86656-linux64-gpl-7.0.tar.xz" - tar -xf btbn-ffmpeg.tar.xz -C /usr/lib/btbn-ffmpeg --strip-components 1 - rm -rf btbn-ffmpeg.tar.xz /usr/lib/btbn-ffmpeg/doc /usr/lib/btbn-ffmpeg/bin/ffplay + mkdir -p /usr/lib/ffmpeg/5.0 + mkdir -p /usr/lib/ffmpeg/7.0 + wget -qO btbn-ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2022-07-31-12-37/ffmpeg-n5.1-2-g915ef932a3-linux64-gpl-5.1.tar.xz" + tar -xf btbn-ffmpeg.tar.xz -C /usr/lib/ffmpeg/5.0 --strip-components 1 + rm -rf btbn-ffmpeg.tar.xz /usr/lib/ffmpeg/5.0/doc /usr/lib/ffmpeg/5.0/bin/ffplay + wget -qO btbn-ffmpeg.tar.xz "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2024-09-13-12-57/ffmpeg-n7.0.2-17-gf705bc5b73-linux64-gpl-7.0.tar.xz" + tar -xf btbn-ffmpeg.tar.xz -C /usr/lib/ffmpeg/7.0 --strip-components 1 + rm -rf btbn-ffmpeg.tar.xz /usr/lib/ffmpeg/7.0/doc /usr/lib/ffmpeg/7.0/bin/ffplay fi # ffmpeg -> arm64 if [[ "${TARGETARCH}" == "arm64" ]]; then - mkdir -p /usr/lib/btbn-ffmpeg - wget -qO btbn-ffmpeg.tar.xz "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2024-09-04-18-56/ffmpeg-n7.0.2-15-g0458a86656-linuxarm64-gpl-7.0.tar.xz" - tar -xf btbn-ffmpeg.tar.xz -C /usr/lib/btbn-ffmpeg --strip-components 1 - rm -rf btbn-ffmpeg.tar.xz /usr/lib/btbn-ffmpeg/doc /usr/lib/btbn-ffmpeg/bin/ffplay + mkdir -p /usr/lib/ffmpeg/5.0 + mkdir -p /usr/lib/ffmpeg/7.0 + wget -qO btbn-ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2022-07-31-12-37/ffmpeg-n5.1-2-g915ef932a3-linuxarm64-gpl-5.1.tar.xz" + tar -xf btbn-ffmpeg.tar.xz -C /usr/lib/ffmpeg/5.0 --strip-components 1 + rm -rf btbn-ffmpeg.tar.xz /usr/lib/ffmpeg/5.0/doc /usr/lib/ffmpeg/5.0/bin/ffplay + wget -qO btbn-ffmpeg.tar.xz "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2024-09-13-12-57/ffmpeg-n7.0.2-17-gf705bc5b73-linuxarm64-gpl-7.0.tar.xz" + tar -xf btbn-ffmpeg.tar.xz -C /usr/lib/ffmpeg/7.0 --strip-components 1 + rm -rf btbn-ffmpeg.tar.xz /usr/lib/ffmpeg/7.0/doc /usr/lib/ffmpeg/7.0/bin/ffplay fi # arch specific packages @@ -59,11 +67,15 @@ if [[ "${TARGETARCH}" == "amd64" ]]; then echo 'deb https://deb.debian.org/debian bookworm main contrib non-free' >/etc/apt/sources.list.d/debian-bookworm.list apt-get -qq update apt-get -qq install --no-install-recommends --no-install-suggests -y \ - intel-opencl-icd \ - mesa-va-drivers radeontop libva-drm2 intel-media-va-driver-non-free i965-va-driver libmfx1 intel-gpu-tools + intel-opencl-icd intel-media-va-driver-non-free i965-va-driver \ + libmfx-gen1.2 libmfx1 onevpl-tools intel-gpu-tools \ + libva-drm2 \ + mesa-va-drivers radeontop + # something about this dependency requires it to be installed in a separate call rather than in the line above apt-get -qq install --no-install-recommends --no-install-suggests -y \ i965-va-driver-shaders + rm -f /etc/apt/sources.list.d/debian-bookworm.list fi diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/run b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/run index 50da2aef9c..eacce294fb 100755 --- a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/run +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/run @@ -44,8 +44,6 @@ function migrate_db_path() { echo "[INFO] Preparing Frigate..." migrate_db_path -export LIBAVFORMAT_VERSION_MAJOR=$(ffmpeg -version | grep -Po 'libavformat\W+\K\d+') - echo "[INFO] Starting Frigate..." cd /opt/frigate || echo "[ERROR] Failed to change working directory to /opt/frigate" diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/run b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/run index 851d787996..9c4922d81d 100755 --- a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/run +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/run @@ -43,8 +43,6 @@ function get_ip_and_port_from_supervisor() { export FRIGATE_GO2RTC_WEBRTC_CANDIDATE_INTERNAL="${ip_address}:${webrtc_port}" } -export LIBAVFORMAT_VERSION_MAJOR=$(ffmpeg -version | grep -Po 'libavformat\W+\K\d+') - if [[ -f "/dev/shm/go2rtc.yaml" ]]; then echo "[INFO] Removing stale config from last run..." rm /dev/shm/go2rtc.yaml diff --git a/docker/main/rootfs/usr/local/go2rtc/create_config.py b/docker/main/rootfs/usr/local/go2rtc/create_config.py index 6855b3b893..6229586e57 100644 --- a/docker/main/rootfs/usr/local/go2rtc/create_config.py +++ b/docker/main/rootfs/usr/local/go2rtc/create_config.py @@ -105,16 +105,34 @@ **FRIGATE_ENV_VARS ) +# ensure ffmpeg path is set correctly +path = config.get("ffmpeg", {}).get("path", "default") +if path == "default": + if int(os.getenv("", "59") or "59") >= 59: + ffmpeg_path = "/usr/lib/ffmpeg/7.0/bin/ffmpeg" + else: + ffmpeg_path = "ffmpeg" +elif path == "7.0": + ffmpeg_path = "/usr/lib/ffmpeg/7.0/bin/ffmpeg" +elif path == "5.0": + ffmpeg_path = "/usr/lib/ffmpeg/5.0/bin/ffmpeg" +else: + ffmpeg_path = f"{path}/bin/ffmpeg" + +if go2rtc_config.get("ffmpeg") is None: + go2rtc_config["ffmpeg"] = {"bin": ffmpeg_path} +elif go2rtc_config["ffmpeg"].get("bin") is None: + go2rtc_config["ffmpeg"]["bin"] = ffmpeg_path + # need to replace ffmpeg command when using ffmpeg4 -if int(os.environ["LIBAVFORMAT_VERSION_MAJOR"]) < 59: - if go2rtc_config.get("ffmpeg") is None: - go2rtc_config["ffmpeg"] = { - "rtsp": "-fflags nobuffer -flags low_delay -stimeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i {input}" - } - elif go2rtc_config["ffmpeg"].get("rtsp") is None: +if int(os.environ.get("LIBAVFORMAT_VERSION_MAJOR", "59") or "59") < 59: + if go2rtc_config["ffmpeg"].get("rtsp") is None: go2rtc_config["ffmpeg"]["rtsp"] = ( "-fflags nobuffer -flags low_delay -stimeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i {input}" ) +else: + if go2rtc_config.get("ffmpeg") is None: + go2rtc_config["ffmpeg"] = {"path": ""} for name in go2rtc_config.get("streams", {}): stream = go2rtc_config["streams"][name] @@ -145,7 +163,7 @@ birdseye: dict[str, any] = config.get("birdseye") input = f"-f rawvideo -pix_fmt yuv420p -video_size {birdseye.get('width', 1280)}x{birdseye.get('height', 720)} -r 10 -i {BIRDSEYE_PIPE}" - ffmpeg_cmd = f"exec:{parse_preset_hardware_acceleration_encode(config.get('ffmpeg', {}).get('hwaccel_args'), input, '-rtsp_transport tcp -f rtsp {output}')}" + ffmpeg_cmd = f"exec:{parse_preset_hardware_acceleration_encode(ffmpeg_path, config.get('ffmpeg', {}).get('hwaccel_args'), input, '-rtsp_transport tcp -f rtsp {output}')}" if go2rtc_config.get("streams"): go2rtc_config["streams"]["birdseye"] = ffmpeg_cmd diff --git a/docker/rpi/Dockerfile b/docker/rpi/Dockerfile index 581ca7ff89..9860e65ecb 100644 --- a/docker/rpi/Dockerfile +++ b/docker/rpi/Dockerfile @@ -12,5 +12,7 @@ RUN rm -rf /usr/lib/btbn-ffmpeg/ RUN --mount=type=bind,source=docker/rpi/install_deps.sh,target=/deps/install_deps.sh \ /deps/install_deps.sh +ENV LIBAVFORMAT_VERSION_MAJOR=58 + WORKDIR /opt/frigate/ COPY --from=rootfs / / diff --git a/docs/docs/configuration/advanced.md b/docs/docs/configuration/advanced.md index 730b0f6b45..1c99ec1c50 100644 --- a/docs/docs/configuration/advanced.md +++ b/docs/docs/configuration/advanced.md @@ -162,15 +162,15 @@ listen [::]:5000 ipv6only=off; ### Custom ffmpeg build -Included with Frigate is a build of ffmpeg that works for the vast majority of users. However, there exists some hardware setups which have incompatibilities with the included build. In this case, a docker volume mapping can be used to overwrite the included ffmpeg build with an ffmpeg build that works for your specific hardware setup. +Included with Frigate is a build of ffmpeg that works for the vast majority of users. However, there exists some hardware setups which have incompatibilities with the included build. In this case, statically built ffmpeg binary can be downloaded to /config and used. To do this: -1. Download your ffmpeg build and uncompress to a folder on the host (let's use `/home/appdata/frigate/custom-ffmpeg` for this example). +1. Download your ffmpeg build and uncompress to the Frigate config folder. 2. Update your docker-compose or docker CLI to include `'/home/appdata/frigate/custom-ffmpeg':'/usr/lib/btbn-ffmpeg':'ro'` in the volume mappings. 3. Restart Frigate and the custom version will be used if the mapping was done correctly. -NOTE: The folder that is mapped from the host needs to be the folder that contains `/bin`. So if the full structure is `/home/appdata/frigate/custom-ffmpeg/bin/ffmpeg` then `/home/appdata/frigate/custom-ffmpeg` needs to be mapped to `/usr/lib/btbn-ffmpeg`. +NOTE: The folder that is set for the config needs to be the folder that contains `/bin`. So if the full structure is `/home/appdata/frigate/custom-ffmpeg/bin/ffmpeg` then the `ffmpeg -> path` field should be `/config/custom-ffmpeg/bin`. ### Custom go2rtc version diff --git a/docs/docs/configuration/reference.md b/docs/docs/configuration/reference.md index 2eae6ef908..1ae0739e77 100644 --- a/docs/docs/configuration/reference.md +++ b/docs/docs/configuration/reference.md @@ -210,6 +210,10 @@ birdseye: # Optional: ffmpeg configuration # More information about presets at https://docs.frigate.video/configuration/ffmpeg_presets ffmpeg: + # Optional: ffmpeg binry path (default: shown below) + # can also be set to `7.0` or `5.0` to specify one of the included versions + # or can be set to any path that holds `bin/ffmpeg` & `bin/ffprobe` + path: "default" # Optional: global ffmpeg args (default: shown below) global_args: -hide_banner -loglevel warning -threads 2 # Optional: global hwaccel args (default: auto detect) diff --git a/frigate/api/app.py b/frigate/api/app.py index 66e2522cd6..cc946687ec 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -418,7 +418,7 @@ def ffprobe(): output = [] for path in paths: - ffprobe = ffprobe_stream(path.strip()) + ffprobe = ffprobe_stream(current_app.frigate_config.ffmpeg, path.strip()) output.append( { "return_code": ffprobe.returncode, diff --git a/frigate/api/event.py b/frigate/api/event.py index 6e75602e9e..fd3c4ad0bd 100644 --- a/frigate/api/event.py +++ b/frigate/api/event.py @@ -302,8 +302,21 @@ def events_explore(): .dicts() ) - events = query.iterator() - return jsonify(list(events)) + events = list(query.iterator()) + + processed_events = [ + {k: v for k, v in event.items() if k != "data"} + | { + "data": { + k: v + for k, v in event["data"].items() + if k in ["type", "score", "top_score", "description"] + } + } + for event in events + ] + + return jsonify(processed_events) @EventBp.route("/event_ids") @@ -507,9 +520,11 @@ def events_search(): events = [ {k: v for k, v in event.items() if k != "data"} | { - k: v - for k, v in event["data"].items() - if k in ["type", "score", "top_score", "description"] + "data": { + k: v + for k, v in event["data"].items() + if k in ["type", "score", "top_score", "description"] + } } | { "search_distance": results[event["id"]]["distance"], diff --git a/frigate/api/media.py b/frigate/api/media.py index 8604b557bb..e146fa1950 100644 --- a/frigate/api/media.py +++ b/frigate/api/media.py @@ -17,6 +17,7 @@ from tzlocal import get_localzone_name from werkzeug.utils import secure_filename +from frigate.config import FrigateConfig from frigate.const import ( CACHE_DIR, CLIPS_DIR, @@ -216,9 +217,10 @@ def get_snapshot_from_recording(camera_name: str, frame_time: str, format: str): height = request.args.get("height", type=int) codec = "png" if format == "png" else "mjpeg" + config: FrigateConfig = current_app.frigate_config image_data = get_image_from_recording( - recording.path, time_in_segment, codec, height + config.ffmpeg, recording.path, time_in_segment, codec, height ) if not image_data: @@ -273,9 +275,12 @@ def submit_recording_snapshot_to_plus(camera_name: str, frame_time: str): ) try: + config: FrigateConfig = current_app.frigate_config recording: Recordings = recording_query.get() time_in_segment = frame_time - recording.start_time - image_data = get_image_from_recording(recording.path, time_in_segment, "png") + image_data = get_image_from_recording( + config.ffmpeg, recording.path, time_in_segment, "png" + ) if not image_data: return make_response( @@ -474,9 +479,11 @@ def recording_clip(camera_name, start_ts, end_ts): file_name = secure_filename(file_name) path = os.path.join(CLIPS_DIR, f"cache/{file_name}") + config: FrigateConfig = current_app.frigate_config + if not os.path.exists(path): ffmpeg_cmd = [ - "ffmpeg", + config.ffmpeg.ffmpeg_path, "-hide_banner", "-y", "-protocol_whitelist", @@ -1141,8 +1148,9 @@ def preview_gif(camera_name: str, start_ts, end_ts, max_cache_age=2592000): diff = start_ts - preview.start_time minutes = int(diff / 60) seconds = int(diff % 60) + config: FrigateConfig = current_app.frigate_config ffmpeg_cmd = [ - "ffmpeg", + config.ffmpeg.ffmpeg_path, "-hide_banner", "-loglevel", "warning", @@ -1206,9 +1214,10 @@ def preview_gif(camera_name: str, start_ts, end_ts, max_cache_age=2592000): last_file = selected_previews[-2] selected_previews.append(last_file) + config: FrigateConfig = current_app.frigate_config ffmpeg_cmd = [ - "ffmpeg", + config.ffmpeg.ffmpeg_path, "-hide_banner", "-loglevel", "warning", @@ -1301,8 +1310,9 @@ def preview_mp4(camera_name: str, start_ts, end_ts, max_cache_age=604800): diff = start_ts - preview.start_time minutes = int(diff / 60) seconds = int(diff % 60) + config: FrigateConfig = current_app.frigate_config ffmpeg_cmd = [ - "ffmpeg", + config.ffmpeg.ffmpeg_path, "-hide_banner", "-loglevel", "warning", @@ -1364,9 +1374,10 @@ def preview_mp4(camera_name: str, start_ts, end_ts, max_cache_age=604800): last_file = selected_previews[-2] selected_previews.append(last_file) + config: FrigateConfig = current_app.frigate_config ffmpeg_cmd = [ - "ffmpeg", + config.ffmpeg.ffmpeg_path, "-hide_banner", "-loglevel", "warning", diff --git a/frigate/api/review.py b/frigate/api/review.py index 6bb2a48002..d391828d5b 100644 --- a/frigate/api/review.py +++ b/frigate/api/review.py @@ -94,6 +94,18 @@ def review(): return jsonify([r for r in review]) +@ReviewBp.route("/review/event/") +def get_review_from_event(id: str): + try: + return model_to_dict( + ReviewSegment.get( + ReviewSegment.data["detections"].cast("text") % f'*"{id}"*' + ) + ) + except DoesNotExist: + return "Review item not found", 404 + + @ReviewBp.route("/review/") def get_review(id: str): try: diff --git a/frigate/app.py b/frigate/app.py index 03b667810f..342624d367 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -377,7 +377,7 @@ def check_db_data_migrations(self) -> None: except PermissionError: logger.error("Unable to write to /config to save export state") - migrate_exports(self.config.cameras.keys()) + migrate_exports(self.config.ffmpeg, self.config.cameras.keys()) def init_external_event_processor(self) -> None: self.external_event_processor = ExternalEventProcessor(self.config) diff --git a/frigate/config.py b/frigate/config.py index 972562328e..7e557f0a34 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -866,6 +866,7 @@ class FfmpegOutputArgsConfig(FrigateBaseModel): class FfmpegConfig(FrigateBaseModel): + path: str = Field(default="default", title="FFmpeg path") global_args: Union[str, List[str]] = Field( default=FFMPEG_GLOBAL_ARGS_DEFAULT, title="Global FFmpeg arguments." ) @@ -884,6 +885,34 @@ class FfmpegConfig(FrigateBaseModel): title="Time in seconds to wait before FFmpeg retries connecting to the camera.", ) + @property + def ffmpeg_path(self) -> str: + if self.path == "default": + if int(os.getenv("LIBAVFORMAT_VERSION_MAJOR", "59")) >= 59: + return "/usr/lib/ffmpeg/7.0/bin/ffmpeg" + else: + return "ffmpeg" + elif self.path == "7.0": + return "/usr/lib/ffmpeg/7.0/bin/ffmpeg" + elif self.path == "5.0": + return "/usr/lib/ffmpeg/5.0/bin/ffmpeg" + else: + return f"{self.path}/bin/ffmpeg" + + @property + def ffprobe_path(self) -> str: + if self.path == "default": + if int(os.getenv("LIBAVFORMAT_VERSION_MAJOR", "59")) >= 59: + return "/usr/lib/ffmpeg/7.0/bin/ffprobe" + else: + return "ffprobe" + elif self.path == "7.0": + return "/usr/lib/ffmpeg/7.0/bin/ffprobe" + elif self.path == "5.0": + return "/usr/lib/ffmpeg/5.0/bin/ffprobe" + else: + return f"{self.path}/bin/ffprobe" + class CameraRoleEnum(str, Enum): audio = "audio" @@ -1194,9 +1223,9 @@ def _get_ffmpeg_cmd(self, ffmpeg_input: CameraInput): ) cmd = ( - ["ffmpeg"] + [self.ffmpeg.ffmpeg_path] + global_args - + hwaccel_args + + (hwaccel_args if "detect" in ffmpeg_input.roles else []) + input_args + ["-i", escape_special_characters(ffmpeg_input.path)] + ffmpeg_output_args @@ -1520,7 +1549,9 @@ def runtime_config(self, plus_api: PlusApi = None) -> FrigateConfig: if need_detect_dimensions or need_record_fourcc: stream_info = {"width": 0, "height": 0, "fourcc": None} try: - stream_info = stream_info_retriever.get_stream_info(input.path) + stream_info = stream_info_retriever.get_stream_info( + config.ffmpeg, input.path + ) except Exception: logger.warn( f"Error detecting stream parameters automatically for {input.path} Applying default values." diff --git a/frigate/events/audio.py b/frigate/events/audio.py index 4302abb68a..662eb5189e 100644 --- a/frigate/events/audio.py +++ b/frigate/events/audio.py @@ -50,7 +50,7 @@ def get_ffmpeg_command(ffmpeg: FfmpegConfig) -> list[str]: or get_ffmpeg_arg_list(ffmpeg.input_args) ) return ( - ["ffmpeg", "-vn", "-threads", "1"] + [ffmpeg.ffmpeg_path, "-vn", "-threads", "1"] + input_args + ["-i"] + [ffmpeg_input.path] diff --git a/frigate/ffmpeg_presets.py b/frigate/ffmpeg_presets.py index 1cf9bef434..19103cdf85 100644 --- a/frigate/ffmpeg_presets.py +++ b/frigate/ffmpeg_presets.py @@ -49,12 +49,12 @@ def get_selected_gpu(self) -> str: FPS_VFR_PARAM = ( "-fps_mode vfr" - if int(os.getenv("LIBAVFORMAT_VERSION_MAJOR", "59")) >= 59 + if int(os.getenv("LIBAVFORMAT_VERSION_MAJOR", "59") or "59") >= 59 else "-vsync 2" ) TIMEOUT_PARAM = ( "-timeout" - if int(os.getenv("LIBAVFORMAT_VERSION_MAJOR", "59")) >= 59 + if int(os.getenv("LIBAVFORMAT_VERSION_MAJOR", "59") or "59") >= 59 else "-stimeout" ) @@ -111,17 +111,17 @@ def get_selected_gpu(self) -> str: ] PRESETS_HW_ACCEL_ENCODE_BIRDSEYE = { - "preset-rpi-64-h264": "ffmpeg -hide_banner {0} -c:v h264_v4l2m2m {1}", - "preset-rpi-64-h265": "ffmpeg -hide_banner {0} -c:v hevc_v4l2m2m {1}", - FFMPEG_HWACCEL_VAAPI: "ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device {2} {0} -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf format=vaapi|nv12,hwupload {1}", - "preset-intel-qsv-h264": "ffmpeg -hide_banner {0} -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 {1}", - "preset-intel-qsv-h265": "ffmpeg -hide_banner {0} -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 {1}", - FFMPEG_HWACCEL_NVIDIA: "ffmpeg -hide_banner {0} -c:v h264_nvenc -g 50 -profile:v high -level:v auto -preset:v p2 -tune:v ll {1}", - "preset-jetson-h264": "ffmpeg -hide_banner {0} -c:v h264_nvmpi -profile high {1}", - "preset-jetson-h265": "ffmpeg -hide_banner {0} -c:v h264_nvmpi -profile high {1}", - "preset-rk-h264": "ffmpeg -hide_banner {0} -c:v h264_rkmpp -profile:v high {1}", - "preset-rk-h265": "ffmpeg -hide_banner {0} -c:v hevc_rkmpp -profile:v high {1}", - "default": "ffmpeg -hide_banner {0} -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency {1}", + "preset-rpi-64-h264": "{0} -hide_banner {1} -c:v h264_v4l2m2m {2}", + "preset-rpi-64-h265": "{0} -hide_banner {1} -c:v hevc_v4l2m2m {2}", + FFMPEG_HWACCEL_VAAPI: "{0} -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device {3} {1} -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf format=vaapi|nv12,hwupload {2}", + "preset-intel-qsv-h264": "{0} -hide_banner {1} -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 {2}", + "preset-intel-qsv-h265": "{0} -hide_banner {1} -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 {2}", + FFMPEG_HWACCEL_NVIDIA: "{0} -hide_banner {1} -c:v h264_nvenc -g 50 -profile:v high -level:v auto -preset:v p2 -tune:v ll {2}", + "preset-jetson-h264": "{0} -hide_banner {1} -c:v h264_nvmpi -profile high {2}", + "preset-jetson-h265": "{0} -hide_banner {1} -c:v h264_nvmpi -profile high {2}", + "preset-rk-h264": "{0} -hide_banner {1} -c:v h264_rkmpp -profile:v high {2}", + "preset-rk-h265": "{0} -hide_banner {1} -c:v hevc_rkmpp -profile:v high {2}", + "default": "{0} -hide_banner {1} -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency {2}", } PRESETS_HW_ACCEL_ENCODE_BIRDSEYE["preset-nvidia-h264"] = ( PRESETS_HW_ACCEL_ENCODE_BIRDSEYE[FFMPEG_HWACCEL_NVIDIA] @@ -131,18 +131,18 @@ def get_selected_gpu(self) -> str: ) PRESETS_HW_ACCEL_ENCODE_TIMELAPSE = { - "preset-rpi-64-h264": "ffmpeg -hide_banner {0} -c:v h264_v4l2m2m -pix_fmt yuv420p {1}", - "preset-rpi-64-h265": "ffmpeg -hide_banner {0} -c:v hevc_v4l2m2m -pix_fmt yuv420p {1}", - FFMPEG_HWACCEL_VAAPI: "ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device {2} {0} -c:v h264_vaapi {1}", - "preset-intel-qsv-h264": "ffmpeg -hide_banner {0} -c:v h264_qsv -profile:v high -level:v 4.1 -async_depth:v 1 {1}", - "preset-intel-qsv-h265": "ffmpeg -hide_banner {0} -c:v hevc_qsv -profile:v high -level:v 4.1 -async_depth:v 1 {1}", - FFMPEG_HWACCEL_NVIDIA: "ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 8 {0} -c:v h264_nvenc {1}", - "preset-nvidia-h265": "ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 8 {0} -c:v hevc_nvenc {1}", - "preset-jetson-h264": "ffmpeg -hide_banner {0} -c:v h264_nvmpi -profile high {1}", - "preset-jetson-h265": "ffmpeg -hide_banner {0} -c:v hevc_nvmpi -profile high {1}", - "preset-rk-h264": "ffmpeg -hide_banner {0} -c:v h264_rkmpp -profile:v high {1}", - "preset-rk-h265": "ffmpeg -hide_banner {0} -c:v hevc_rkmpp -profile:v high {1}", - "default": "ffmpeg -hide_banner {0} -c:v libx264 -preset:v ultrafast -tune:v zerolatency {1}", + "preset-rpi-64-h264": "{0} -hide_banner {1} -c:v h264_v4l2m2m -pix_fmt yuv420p {2}", + "preset-rpi-64-h265": "{0} -hide_banner {1} -c:v hevc_v4l2m2m -pix_fmt yuv420p {2}", + FFMPEG_HWACCEL_VAAPI: "{0} -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device {3} {1} -c:v h264_vaapi {2}", + "preset-intel-qsv-h264": "{0} -hide_banner {1} -c:v h264_qsv -profile:v high -level:v 4.1 -async_depth:v 1 {2}", + "preset-intel-qsv-h265": "{0} -hide_banner {1} -c:v hevc_qsv -profile:v high -level:v 4.1 -async_depth:v 1 {2}", + FFMPEG_HWACCEL_NVIDIA: "{0} -hide_banner -hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 8 {1} -c:v h264_nvenc {2}", + "preset-nvidia-h265": "{0} -hide_banner -hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 8 {1} -c:v hevc_nvenc {2}", + "preset-jetson-h264": "{0} -hide_banner {1} -c:v h264_nvmpi -profile high {2}", + "preset-jetson-h265": "{0} -hide_banner {1} -c:v hevc_nvmpi -profile high {2}", + "preset-rk-h264": "{0} -hide_banner {1} -c:v h264_rkmpp -profile:v high {2}", + "preset-rk-h265": "{0} -hide_banner {1} -c:v hevc_rkmpp -profile:v high {2}", + "default": "{0} -hide_banner {1} -c:v libx264 -preset:v ultrafast -tune:v zerolatency {2}", } PRESETS_HW_ACCEL_ENCODE_TIMELAPSE["preset-nvidia-h264"] = ( PRESETS_HW_ACCEL_ENCODE_TIMELAPSE[FFMPEG_HWACCEL_NVIDIA] @@ -150,7 +150,7 @@ def get_selected_gpu(self) -> str: # encoding of previews is only done on CPU due to comparable encode times and better quality from libx264 PRESETS_HW_ACCEL_ENCODE_PREVIEW = { - "default": "ffmpeg -hide_banner {0} -c:v libx264 -profile:v baseline -preset:v ultrafast {1}", + "default": "{0} -hide_banner {1} -c:v libx264 -profile:v baseline -preset:v ultrafast {2}", } @@ -197,7 +197,11 @@ class EncodeTypeEnum(str, Enum): def parse_preset_hardware_acceleration_encode( - arg: Any, input: str, output: str, type: EncodeTypeEnum = EncodeTypeEnum.birdseye + ffmpeg_path: str, + arg: Any, + input: str, + output: str, + type: EncodeTypeEnum = EncodeTypeEnum.birdseye, ) -> str: """Return the correct scaling preset or default preset if none is set.""" if type == EncodeTypeEnum.birdseye: @@ -215,6 +219,7 @@ def parse_preset_hardware_acceleration_encode( arg = "default" return arg_map.get(arg, arg_map["default"]).format( + ffmpeg_path, input, output, _gpu_selector.get_selected_gpu(), diff --git a/frigate/output/birdseye.py b/frigate/output/birdseye.py index 6c42f450bc..00e7c7ad1b 100644 --- a/frigate/output/birdseye.py +++ b/frigate/output/birdseye.py @@ -15,7 +15,7 @@ import numpy as np from frigate.comms.config_updater import ConfigSubscriber -from frigate.config import BirdseyeModeEnum, FrigateConfig +from frigate.config import BirdseyeModeEnum, FfmpegConfig, FrigateConfig from frigate.const import BASE_DIR, BIRDSEYE_PIPE from frigate.util.image import ( SharedMemoryFrameManager, @@ -112,7 +112,7 @@ def get_camera_aspect( class FFMpegConverter(threading.Thread): def __init__( self, - camera: str, + ffmpeg: FfmpegConfig, input_queue: queue.Queue, stop_event: mp.Event, in_width: int, @@ -123,8 +123,8 @@ def __init__( birdseye_rtsp: bool = False, ): threading.Thread.__init__(self) - self.name = f"{camera}_output_converter" - self.camera = camera + self.name = "birdseye_output_converter" + self.camera = "birdseye" self.input_queue = input_queue self.stop_event = stop_event self.bd_pipe = None @@ -133,7 +133,7 @@ def __init__( self.recreate_birdseye_pipe() ffmpeg_cmd = [ - "ffmpeg", + ffmpeg.ffmpeg_path, "-threads", "1", "-f", @@ -725,7 +725,7 @@ def __init__( self.config = config self.input = queue.Queue(maxsize=10) self.converter = FFMpegConverter( - "birdseye", + config.ffmpeg, self.input, stop_event, config.birdseye.width, diff --git a/frigate/output/camera.py b/frigate/output/camera.py index b9c6073757..317d7902ef 100644 --- a/frigate/output/camera.py +++ b/frigate/output/camera.py @@ -6,7 +6,7 @@ import subprocess as sp import threading -from frigate.config import CameraConfig +from frigate.config import CameraConfig, FfmpegConfig logger = logging.getLogger(__name__) @@ -15,6 +15,7 @@ class FFMpegConverter(threading.Thread): def __init__( self, camera: str, + ffmpeg: FfmpegConfig, input_queue: queue.Queue, stop_event: mp.Event, in_width: int, @@ -30,7 +31,7 @@ def __init__( self.stop_event = stop_event ffmpeg_cmd = [ - "ffmpeg", + ffmpeg.ffmpeg_path, "-threads", "1", "-f", @@ -142,6 +143,7 @@ def __init__( ) self.converter = FFMpegConverter( config.name, + config.ffmpeg, self.input, stop_event, config.frame_shape[1], diff --git a/frigate/output/preview.py b/frigate/output/preview.py index f5a0f2e719..5b5dd4afa6 100644 --- a/frigate/output/preview.py +++ b/frigate/output/preview.py @@ -78,6 +78,7 @@ def __init__( # write a PREVIEW at fps and 1 key frame per clip self.ffmpeg_cmd = parse_preset_hardware_acceleration_encode( + config.ffmpeg.ffmpeg_path, config.ffmpeg.hwaccel_args, input="-f concat -y -protocol_whitelist pipe,file -safe 0 -threads 1 -i /dev/stdin", output=f"-threads 1 -g {PREVIEW_KEYFRAME_INTERVAL} -bf 0 -b:v {PREVIEW_QUALITY_BIT_RATES[self.config.record.preview.quality]} {FPS_VFR_PARAM} -movflags +faststart -pix_fmt yuv420p {self.path}", diff --git a/frigate/record/export.py b/frigate/record/export.py index feb96f01f8..7d38e60f11 100644 --- a/frigate/record/export.py +++ b/frigate/record/export.py @@ -14,7 +14,7 @@ from peewee import DoesNotExist -from frigate.config import FrigateConfig +from frigate.config import FfmpegConfig, FrigateConfig from frigate.const import ( CACHE_DIR, CLIPS_DIR, @@ -116,7 +116,7 @@ def save_thumbnail(self, id: str) -> str: minutes = int(diff / 60) seconds = int(diff % 60) ffmpeg_cmd = [ - "ffmpeg", + self.config.ffmpeg.ffmpeg_path, "-hide_banner", "-loglevel", "warning", @@ -230,11 +230,12 @@ def run(self) -> None: if self.playback_factor == PlaybackFactorEnum.realtime: ffmpeg_cmd = ( - f"ffmpeg -hide_banner {ffmpeg_input} -c copy -movflags +faststart {video_path}" + f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input} -c copy -movflags +faststart {video_path}" ).split(" ") elif self.playback_factor == PlaybackFactorEnum.timelapse_25x: ffmpeg_cmd = ( parse_preset_hardware_acceleration_encode( + self.config.ffmpeg.ffmpeg_path, self.config.ffmpeg.hwaccel_args, f"{TIMELAPSE_DATA_INPUT_ARGS} {ffmpeg_input}", f"{self.config.cameras[self.camera].record.export.timelapse_args} -movflags +faststart {video_path}", @@ -267,7 +268,7 @@ def run(self) -> None: logger.debug(f"Finished exporting {video_path}") -def migrate_exports(camera_names: list[str]): +def migrate_exports(ffmpeg: FfmpegConfig, camera_names: list[str]): Path(os.path.join(CLIPS_DIR, "export")).mkdir(exist_ok=True) exports = [] @@ -286,7 +287,7 @@ def migrate_exports(camera_names: list[str]): ) # use jpg because webp encoder can't get quality low enough ffmpeg_cmd = [ - "ffmpeg", + ffmpeg.ffmpeg_path, "-hide_banner", "-loglevel", "warning", diff --git a/frigate/record/maintainer.py b/frigate/record/maintainer.py index e066602236..5bcec62e60 100644 --- a/frigate/record/maintainer.py +++ b/frigate/record/maintainer.py @@ -209,7 +209,9 @@ async def validate_and_move_segment( if cache_path in self.end_time_cache: end_time, duration = self.end_time_cache[cache_path] else: - segment_info = await get_video_properties(cache_path, get_duration=True) + segment_info = await get_video_properties( + self.config.ffmpeg, cache_path, get_duration=True + ) if segment_info["duration"]: duration = float(segment_info["duration"]) @@ -387,7 +389,7 @@ async def move_segment( # add faststart to kept segments to improve metadata reading p = await asyncio.create_subprocess_exec( - "ffmpeg", + self.config.ffmpeg.ffmpeg_path, "-hide_banner", "-y", "-i", diff --git a/frigate/util/config.py b/frigate/util/config.py index a40efeafd1..729215e9ec 100644 --- a/frigate/util/config.py +++ b/frigate/util/config.py @@ -314,10 +314,10 @@ class StreamInfoRetriever: def __init__(self) -> None: self.stream_cache: dict[str, tuple[int, int]] = {} - def get_stream_info(self, path: str) -> str: + def get_stream_info(self, ffmpeg, path: str) -> str: if path in self.stream_cache: return self.stream_cache[path] - info = asyncio.run(get_video_properties(path)) + info = asyncio.run(get_video_properties(ffmpeg, path)) self.stream_cache[path] = info return info diff --git a/frigate/util/image.py b/frigate/util/image.py index f3186fe6a5..41024a599e 100644 --- a/frigate/util/image.py +++ b/frigate/util/image.py @@ -765,12 +765,16 @@ def add_mask(mask: str, mask_img: np.ndarray): def get_image_from_recording( - file_path: str, relative_frame_time: float, codec: str, height: Optional[int] = None + ffmpeg, # Ffmpeg Config + file_path: str, + relative_frame_time: float, + codec: str, + height: Optional[int] = None, ) -> Optional[any]: """retrieve a frame from given time in recording file.""" ffmpeg_cmd = [ - "ffmpeg", + ffmpeg.ffmpeg_path, "-hide_banner", "-loglevel", "warning", diff --git a/frigate/util/services.py b/frigate/util/services.py index 16f46e1149..03787d2485 100644 --- a/frigate/util/services.py +++ b/frigate/util/services.py @@ -378,11 +378,11 @@ def get_jetson_stats() -> dict[int, dict]: return results -def ffprobe_stream(path: str) -> sp.CompletedProcess: +def ffprobe_stream(ffmpeg, path: str) -> sp.CompletedProcess: """Run ffprobe on stream.""" clean_path = escape_special_characters(path) ffprobe_cmd = [ - "ffprobe", + ffmpeg.ffprobe_path, "-timeout", "1000000", "-print_format", @@ -438,7 +438,9 @@ def auto_detect_hwaccel() -> str: return "" -async def get_video_properties(url, get_duration=False) -> dict[str, any]: +async def get_video_properties( + ffmpeg, url: str, get_duration: bool = False +) -> dict[str, any]: async def calculate_duration(video: Optional[any]) -> float: duration = None @@ -453,7 +455,7 @@ async def calculate_duration(video: Optional[any]) -> float: # if cv2 failed need to use ffprobe if duration is None: p = await asyncio.create_subprocess_exec( - "ffprobe", + ffmpeg.ffprobe_path, "-v", "error", "-show_entries", diff --git a/web/src/components/card/AnimatedEventCard.tsx b/web/src/components/card/AnimatedEventCard.tsx index 8ee4acdcfb..fd8096ebc5 100644 --- a/web/src/components/card/AnimatedEventCard.tsx +++ b/web/src/components/card/AnimatedEventCard.tsx @@ -135,11 +135,13 @@ export function AnimatedEventCard({
- window - .open(`${baseUrl}review?id=${event.id}`, "_blank") - ?.focus() - } + onAuxClick={(e) => { + if (e.button === 1) { + window + .open(`${baseUrl}review?id=${event.id}`, "_blank") + ?.focus(); + } + }} > {!alertVideos ? ( shareOrCopy( - `${baseUrl}exports?id=${exportedRecording.id}`, + `${baseUrl}export?id=${exportedRecording.id}`, exportedRecording.name.replaceAll("_", " "), ) } diff --git a/web/src/components/filter/CamerasFilterButton.tsx b/web/src/components/filter/CamerasFilterButton.tsx index dfe3fdaa11..12c5431bf0 100644 --- a/web/src/components/filter/CamerasFilterButton.tsx +++ b/web/src/components/filter/CamerasFilterButton.tsx @@ -1,6 +1,6 @@ import { Button } from "../ui/button"; import { CameraGroupConfig } from "@/types/frigateConfig"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { DropdownMenu, DropdownMenuContent, @@ -29,7 +29,10 @@ export function CamerasFilterButton({ }: CameraFilterButtonProps) { const [open, setOpen] = useState(false); const [currentCameras, setCurrentCameras] = useState( - selectedCameras, + selectedCameras === undefined ? [...allCameras] : selectedCameras, + ); + const [allCamerasSelected, setAllCamerasSelected] = useState( + selectedCameras === undefined, ); const buttonText = useMemo(() => { @@ -37,11 +40,28 @@ export function CamerasFilterButton({ return "Cameras"; } - if (!selectedCameras || selectedCameras.length == 0) { + if (allCamerasSelected) { return "All Cameras"; } - return `${selectedCameras.includes("birdseye") ? selectedCameras.length - 1 : selectedCameras.length} Camera${selectedCameras.length !== 1 ? "s" : ""}`; + if (!currentCameras || currentCameras.length === 0) { + return "No cameras"; + } + + return `${currentCameras.includes("birdseye") ? currentCameras.length - 1 : currentCameras.length} Camera${ + currentCameras.length !== 1 ? "s" : "" + }`; + }, [allCamerasSelected, currentCameras]); + + // ui + + useEffect(() => { + setCurrentCameras( + selectedCameras === undefined ? [...allCameras] : selectedCameras, + ); + setAllCamerasSelected(selectedCameras === undefined); + // only refresh when state changes + // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedCameras]); const trigger = ( @@ -70,24 +90,28 @@ export function CamerasFilterButton({ )} -
+
{ + setAllCamerasSelected(isChecked); + if (isChecked) { - setCurrentCameras(undefined); + setCurrentCameras([...allCameras]); + } else { + setCurrentCameras([]); } }} /> {groups.length > 0 && ( <> - + {groups.map(([name, conf]) => { return (
setCurrentCameras([...conf.cameras])} > {name} @@ -96,7 +120,7 @@ export function CamerasFilterButton({ })} )} - +
{allCameras.map((item) => ( 1) { - updatedCameras.splice(updatedCameras.indexOf(item), 1); + if (index > -1) { + updatedCameras.splice(index, 1); setCurrentCameras(updatedCameras); } + + // Deselecting one camera should disable the "All Cameras" switch + setAllCamerasSelected(false); } }} /> ))}
- +
- + {isDesktop && }
- + {isDesktop && }