diff --git a/docker/main/Dockerfile b/docker/main/Dockerfile index c512ceb848..1a475a650c 100644 --- a/docker/main/Dockerfile +++ b/docker/main/Dockerfile @@ -265,8 +265,8 @@ ENV PATH="/usr/local/go2rtc/bin:/usr/local/tempio/bin:/usr/local/nginx/sbin:${PA RUN --mount=type=bind,source=docker/main/install_deps.sh,target=/deps/install_deps.sh \ /deps/install_deps.sh -ENV DEFAULT_FFMPEG_VERSION="7.0" -ENV INCLUDED_FFMPEG_VERSIONS="${DEFAULT_FFMPEG_VERSION}:5.0" +ENV DEFAULT_FFMPEG_VERSION="8.0" +ENV INCLUDED_FFMPEG_VERSIONS="${DEFAULT_FFMPEG_VERSION}:7.0:5.0" RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \ && sed -i 's/args.append("setuptools")/args.append("setuptools==77.0.3")/' get-pip.py \ diff --git a/docker/main/install_deps.sh b/docker/main/install_deps.sh index ad0fea91a8..e197ce1b68 100755 --- a/docker/main/install_deps.sh +++ b/docker/main/install_deps.sh @@ -52,9 +52,13 @@ if [[ "${TARGETARCH}" == "amd64" ]]; then tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/5.0 --strip-components 1 amd64/bin/ffmpeg amd64/bin/ffprobe rm -rf ffmpeg.tar.xz mkdir -p /usr/lib/ffmpeg/7.0 - wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2026-03-19-13-03/ffmpeg-n7.1.3-43-g5a1f107b4c-linux64-gpl-7.1.tar.xz" + wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2024-09-19-12-51/ffmpeg-n7.0.2-18-g3e6cec1286-linux64-gpl-7.0.tar.xz" tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/7.0 --strip-components 1 amd64/bin/ffmpeg amd64/bin/ffprobe rm -rf ffmpeg.tar.xz + mkdir -p /usr/lib/ffmpeg/8.0 + wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2026-06-02-14-20/ffmpeg-n8.1.1-9-g58d4114d36-linux64-gpl-8.1.tar.xz" + tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/8.0 --strip-components 1 amd64/bin/ffmpeg amd64/bin/ffprobe + rm -rf ffmpeg.tar.xz fi # ffmpeg -> arm64 @@ -64,9 +68,13 @@ if [[ "${TARGETARCH}" == "arm64" ]]; then tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/5.0 --strip-components 1 arm64/bin/ffmpeg arm64/bin/ffprobe rm -f ffmpeg.tar.xz mkdir -p /usr/lib/ffmpeg/7.0 - wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2026-03-19-13-03/ffmpeg-n7.1.3-43-g5a1f107b4c-linuxarm64-gpl-7.1.tar.xz" + wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2024-09-19-12-51/ffmpeg-n7.0.2-18-g3e6cec1286-linuxarm64-gpl-7.0.tar.xz" tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/7.0 --strip-components 1 arm64/bin/ffmpeg arm64/bin/ffprobe rm -f ffmpeg.tar.xz + mkdir -p /usr/lib/ffmpeg/8.0 + wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2026-06-02-14-20/ffmpeg-n8.1.1-9-g58d4114d36-linuxarm64-gpl-8.1.tar.xz" + tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/8.0 --strip-components 1 arm64/bin/ffmpeg arm64/bin/ffprobe + rm -f ffmpeg.tar.xz fi # arch specific packages diff --git a/docker/main/rootfs/usr/local/ffmpeg/get_ffmpeg_path.py b/docker/main/rootfs/usr/local/ffmpeg/get_ffmpeg_path.py index 0f492cc5c5..9f4d08f2e3 100644 --- a/docker/main/rootfs/usr/local/ffmpeg/get_ffmpeg_path.py +++ b/docker/main/rootfs/usr/local/ffmpeg/get_ffmpeg_path.py @@ -5,11 +5,7 @@ from typing import Any from ruamel.yaml import YAML sys.path.insert(0, "/opt/frigate") -from frigate.const import ( - DEFAULT_FFMPEG_VERSION, - INCLUDED_FFMPEG_VERSIONS, -) -from frigate.util.config import find_config_file +from frigate.util.config import find_config_file, resolve_ffmpeg_path sys.path.remove("/opt/frigate") @@ -29,9 +25,4 @@ except FileNotFoundError: config: dict[str, Any] = {} path = config.get("ffmpeg", {}).get("path", "default") -if path == "default": - print(f"/usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffmpeg") -elif path in INCLUDED_FFMPEG_VERSIONS: - print(f"/usr/lib/ffmpeg/{path}/bin/ffmpeg") -else: - print(f"{path}/bin/ffmpeg") +print(resolve_ffmpeg_path(path, "ffmpeg")) diff --git a/docker/main/rootfs/usr/local/go2rtc/create_config.py b/docker/main/rootfs/usr/local/go2rtc/create_config.py index 5796a58aad..2b0fe3c925 100644 --- a/docker/main/rootfs/usr/local/go2rtc/create_config.py +++ b/docker/main/rootfs/usr/local/go2rtc/create_config.py @@ -11,12 +11,10 @@ sys.path.insert(0, "/opt/frigate") from frigate.config.env import substitute_frigate_vars from frigate.const import ( BIRDSEYE_PIPE, - DEFAULT_FFMPEG_VERSION, - INCLUDED_FFMPEG_VERSIONS, LIBAVFORMAT_VERSION_MAJOR, ) from frigate.ffmpeg_presets import parse_preset_hardware_acceleration_encode -from frigate.util.config import find_config_file +from frigate.util.config import find_config_file, resolve_ffmpeg_path from frigate.util.services import is_restricted_go2rtc_source sys.path.remove("/opt/frigate") @@ -81,12 +79,7 @@ if go2rtc_config.get("rtsp", {}).get("password") is not None: # ensure ffmpeg path is set correctly path = config.get("ffmpeg", {}).get("path", "default") -if path == "default": - ffmpeg_path = f"/usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffmpeg" -elif path in INCLUDED_FFMPEG_VERSIONS: - ffmpeg_path = f"/usr/lib/ffmpeg/{path}/bin/ffmpeg" -else: - ffmpeg_path = f"{path}/bin/ffmpeg" +ffmpeg_path = resolve_ffmpeg_path(path, "ffmpeg") if go2rtc_config.get("ffmpeg") is None: go2rtc_config["ffmpeg"] = {"bin": ffmpeg_path} diff --git a/docs/docs/configuration/reference.md b/docs/docs/configuration/reference.md index 3749697876..0559b37569 100644 --- a/docs/docs/configuration/reference.md +++ b/docs/docs/configuration/reference.md @@ -257,7 +257,7 @@ birdseye: # More information about presets at https://docs.frigate.video/configuration/ffmpeg_presets ffmpeg: # Optional: ffmpeg binary path (default: shown below) - # can also be set to `7.0` or `5.0` to specify one of the included versions + # can also be set to `8.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) diff --git a/frigate/config/camera/ffmpeg.py b/frigate/config/camera/ffmpeg.py index 05769dc66d..6341cbcd13 100644 --- a/frigate/config/camera/ffmpeg.py +++ b/frigate/config/camera/ffmpeg.py @@ -3,7 +3,7 @@ from typing import Union from pydantic import Field, field_validator -from frigate.const import DEFAULT_FFMPEG_VERSION, INCLUDED_FFMPEG_VERSIONS +from frigate.util.config import resolve_ffmpeg_path from ..base import FrigateBaseModel from ..env import EnvString @@ -49,7 +49,7 @@ class FfmpegConfig(FrigateBaseModel): path: str = Field( default="default", title="FFmpeg path", - description='Path to the FFmpeg binary to use or a version alias ("5.0" or "7.0").', + description='Path to the FFmpeg binary to use or a version alias ("5.0" or "8.0").', ) global_args: Union[str, list[str]] = Field( default=FFMPEG_GLOBAL_ARGS_DEFAULT, @@ -90,21 +90,11 @@ class FfmpegConfig(FrigateBaseModel): @property def ffmpeg_path(self) -> str: - if self.path == "default": - return f"/usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffmpeg" - elif self.path in INCLUDED_FFMPEG_VERSIONS: - return f"/usr/lib/ffmpeg/{self.path}/bin/ffmpeg" - else: - return f"{self.path}/bin/ffmpeg" + return resolve_ffmpeg_path(self.path, "ffmpeg") @property def ffprobe_path(self) -> str: - if self.path == "default": - return f"/usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffprobe" - elif self.path in INCLUDED_FFMPEG_VERSIONS: - return f"/usr/lib/ffmpeg/{self.path}/bin/ffprobe" - else: - return f"{self.path}/bin/ffprobe" + return resolve_ffmpeg_path(self.path, "ffprobe") class CameraRoleEnum(str, Enum): diff --git a/frigate/ffmpeg_presets.py b/frigate/ffmpeg_presets.py index c314b30eaf..4ebbd6c801 100644 --- a/frigate/ffmpeg_presets.py +++ b/frigate/ffmpeg_presets.py @@ -465,16 +465,6 @@ PRESETS_RECORD_OUTPUT = { "-c:a", "aac", ], - # NOTE: This preset originally used "-c:a copy" to pass through audio - # without re-encoding. FFmpeg 7.x introduced a threaded pipeline where - # demuxing, encoding, and muxing run in parallel via a Scheduler. This - # broke audio streamcopy from RTSP sources: packets are demuxed correctly - # but silently dropped before reaching the muxer (0 bytes written). The - # issue is specific to RTSP + streamcopy; file inputs and transcoding both - # work. Transcoding AAC audio is very lightweight (~30KiB per 10s segment) - # and adds negligible CPU overhead, so this is an acceptable workaround. - # The benefits of FFmpeg 7.x — particularly the removal of gamma correction - # hacks required by earlier versions — outweigh this trade-off. "preset-record-generic-audio-copy": [ "-f", "segment", @@ -486,10 +476,8 @@ PRESETS_RECORD_OUTPUT = { "1", "-strftime", "1", - "-c:v", + "-c", "copy", - "-c:a", - "aac", ], "preset-record-mjpeg": [ "-f", diff --git a/frigate/record/export.py b/frigate/record/export.py index 3a943cb3fe..e89742b1ab 100644 --- a/frigate/record/export.py +++ b/frigate/record/export.py @@ -456,7 +456,7 @@ class RecordingExporter(threading.Thread): diff = max(0.0, float(self.start_time) - float(preview.start_time)) ffmpeg_cmd = [ - "/usr/lib/ffmpeg/7.0/bin/ffmpeg", # hardcode path for exports thumbnail due to missing libwebp support + "/usr/lib/ffmpeg/8.0/bin/ffmpeg", # hardcode path for exports thumbnail due to missing libwebp support "-hide_banner", "-loglevel", "warning", diff --git a/frigate/util/classification.py b/frigate/util/classification.py index 66bacdeb04..30fc4c6687 100644 --- a/frigate/util/classification.py +++ b/frigate/util/classification.py @@ -394,7 +394,7 @@ def collect_state_classification_examples( # Step 3: Extract keyframes from recordings with crops applied keyframes = _extract_keyframes( - "/usr/lib/ffmpeg/7.0/bin/ffmpeg", timestamps, temp_dir, cameras + "/usr/lib/ffmpeg/8.0/bin/ffmpeg", timestamps, temp_dir, cameras ) # Step 4: Select 24 most visually distinct images (they're already cropped) @@ -566,7 +566,7 @@ def _extract_keyframes( relative_time = timestamp - recording.start_time try: - config = FfmpegConfig(path="/usr/lib/ffmpeg/7.0") + config = FfmpegConfig(path="/usr/lib/ffmpeg/8.0") image_data = get_image_from_recording( config, recording.path, diff --git a/frigate/util/config.py b/frigate/util/config.py index 186730dbfb..5a4f3816c0 100644 --- a/frigate/util/config.py +++ b/frigate/util/config.py @@ -8,7 +8,13 @@ from typing import Any, Optional, Union from ruamel.yaml import YAML -from frigate.const import CONFIG_DIR, EXPORT_DIR, REDACTED_CREDENTIAL_SENTINEL +from frigate.const import ( + CONFIG_DIR, + DEFAULT_FFMPEG_VERSION, + EXPORT_DIR, + INCLUDED_FFMPEG_VERSIONS, + REDACTED_CREDENTIAL_SENTINEL, +) from frigate.util.builtin import deep_merge from frigate.util.services import get_video_properties @@ -18,6 +24,26 @@ CURRENT_CONFIG_VERSION = "0.18-0" DEFAULT_CONFIG_FILE = os.path.join(CONFIG_DIR, "config.yml") +def resolve_ffmpeg_path(path: str, binary: str = "ffmpeg") -> str: + """Resolve an ffmpeg version alias or custom path to a binary path. + + A bare version alias that is no longer bundled (for example one that was + dropped when the default version changed) falls back to the default + bundled version so existing configs keep working across an upgrade or a + revert. Custom install paths (anything absolute) are used as-is. + """ + if path == "default" or ( + not path.startswith("/") and path not in INCLUDED_FFMPEG_VERSIONS + ): + version = DEFAULT_FFMPEG_VERSION + elif path in INCLUDED_FFMPEG_VERSIONS: + version = path + else: + return f"{path}/bin/{binary}" + + return f"/usr/lib/ffmpeg/{version}/bin/{binary}" + + def redact_credential(obj: dict[str, Any], key: str) -> None: """Replace obj[key] with the redaction sentinel if a value is saved, else drop.