diff --git a/CHANGELOG.md b/CHANGELOG.md index bdfc629d2..8deb5cd8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Add API endpoint to clean artwork cache folder (on demand) - POST `/api/maintenance/clean_artwork` - Add health check to warn about unsupported empty (classic) schedules +- Add health check to warn about incompatible ffmpeg due to missing filters + - This is directly applicable to homebrew `ffmpeg` on MacOS, which is no longer compatible with ErsatzTV + - `ffmpeg@7` or `ffmpeg-full` should be used instead ### Changed - Disable automatic artwork database cleanup diff --git a/ErsatzTV.Core/Health/Checks/IFFmpegCapabilitiesHealthCheck.cs b/ErsatzTV.Core/Health/Checks/IFFmpegCapabilitiesHealthCheck.cs new file mode 100644 index 000000000..0930c0811 --- /dev/null +++ b/ErsatzTV.Core/Health/Checks/IFFmpegCapabilitiesHealthCheck.cs @@ -0,0 +1,5 @@ +namespace ErsatzTV.Core.Health.Checks; + +public interface IFFmpegCapabilitiesHealthCheck : IHealthCheck +{ +} diff --git a/ErsatzTV.FFmpeg/Capabilities/FFmpegKnownFilter.cs b/ErsatzTV.FFmpeg/Capabilities/FFmpegKnownFilter.cs index ea82e9c11..5ef26f5a2 100644 --- a/ErsatzTV.FFmpeg/Capabilities/FFmpegKnownFilter.cs +++ b/ErsatzTV.FFmpeg/Capabilities/FFmpegKnownFilter.cs @@ -4,7 +4,28 @@ public record FFmpegKnownFilter { public static readonly FFmpegKnownFilter ScaleNpp = new("scale_npp"); public static readonly FFmpegKnownFilter TonemapOpenCL = new("tonemap_opencl"); - public static readonly FFmpegKnownFilter Libplacebo = new("libplacebo"); + //public static readonly FFmpegKnownFilter Libplacebo = new("libplacebo"); + public static readonly FFmpegKnownFilter AudioPad = new("apad"); + public static readonly FFmpegKnownFilter AudioResample = new("aresample"); + public static readonly FFmpegKnownFilter Color = new("color"); + public static readonly FFmpegKnownFilter ColorChannelMixer = new("colorchannelmixer"); + public static readonly FFmpegKnownFilter Colorspace = new("colorspace"); + public static readonly FFmpegKnownFilter Crop = new("crop"); + public static readonly FFmpegKnownFilter Fade = new("fade"); + public static readonly FFmpegKnownFilter Format = new("format"); + public static readonly FFmpegKnownFilter Fps = new("fps"); + public static readonly FFmpegKnownFilter FrameRate = new("framerate"); + public static readonly FFmpegKnownFilter Loop = new("loop"); + public static readonly FFmpegKnownFilter NormalizeLoudness = new("loudnorm"); + public static readonly FFmpegKnownFilter Overlay = new("overlay"); + public static readonly FFmpegKnownFilter Pad = new("pad"); + public static readonly FFmpegKnownFilter Realtime = new("realtime"); + public static readonly FFmpegKnownFilter SetPts = new("setpts"); + public static readonly FFmpegKnownFilter Scale = new("scale"); + public static readonly FFmpegKnownFilter Subtitles = new("subtitles"); + public static readonly FFmpegKnownFilter Tonemap = new("tonemap"); + public static readonly FFmpegKnownFilter Yadif = new("yadif"); + public static readonly FFmpegKnownFilter ZScale = new("zscale"); private FFmpegKnownFilter(string Name) => this.Name = Name; @@ -13,6 +34,31 @@ public record FFmpegKnownFilter public static IList AllFilters => [ ScaleNpp.Name, - TonemapOpenCL.Name + TonemapOpenCL.Name, + ]; + + public static IList RequiredFilters => + [ + AudioPad, + AudioResample, + Color, + ColorChannelMixer, + Colorspace, + Crop, + Fade, + Format, + Fps, + FrameRate, + Loop, + NormalizeLoudness, + Overlay, + Pad, + Realtime, + SetPts, + Scale, + Subtitles, + Tonemap, + Yadif, + ZScale ]; } diff --git a/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs b/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs index f7c72f817..777e8d8a9 100644 --- a/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs +++ b/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs @@ -66,8 +66,10 @@ public partial class HardwareCapabilitiesFactory( IReadOnlySet ffmpegDecoders = await GetFFmpegCapabilities(ffmpegPath, "decoders", ParseFFmpegLine) .Map(set => set.Intersect(FFmpegKnownDecoder.AllDecoders).ToImmutableHashSet()); + IEnumerable allFilterNames = + FFmpegKnownFilter.AllFilters.Union(FFmpegKnownFilter.RequiredFilters.Select(f => f.Name)); IReadOnlySet ffmpegFilters = await GetFFmpegCapabilities(ffmpegPath, "filters", ParseFFmpegLine) - .Map(set => set.Intersect(FFmpegKnownFilter.AllFilters).ToImmutableHashSet()); + .Map(set => set.Intersect(allFilterNames).ToImmutableHashSet()); IReadOnlySet ffmpegEncoders = await GetFFmpegCapabilities(ffmpegPath, "encoders", ParseFFmpegLine) .Map(set => set.Intersect(FFmpegKnownEncoder.AllEncoders).ToImmutableHashSet()); diff --git a/ErsatzTV.Infrastructure/Health/Checks/FFmpegCapabilitiesHealthCheck.cs b/ErsatzTV.Infrastructure/Health/Checks/FFmpegCapabilitiesHealthCheck.cs new file mode 100644 index 000000000..6191f15cc --- /dev/null +++ b/ErsatzTV.Infrastructure/Health/Checks/FFmpegCapabilitiesHealthCheck.cs @@ -0,0 +1,48 @@ +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Health; +using ErsatzTV.Core.Health.Checks; +using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.FFmpeg.Capabilities; + +namespace ErsatzTV.Infrastructure.Health.Checks; + +public class FFmpegCapabilitiesHealthCheck(IConfigElementRepository configElementRepository, IHardwareCapabilitiesFactory hardwareCapabilitiesFactory) + : BaseHealthCheck, IFFmpegCapabilitiesHealthCheck +{ + public override string Title => "FFmpeg Capabilities"; + + public async Task Check(CancellationToken cancellationToken) + { + Option maybeFFmpegPath = + await configElementRepository.GetConfigElement(ConfigElementKey.FFmpegPath, cancellationToken); + if (maybeFFmpegPath.IsNone) + { + // ffmpeg version health check will surface this + return NotApplicableResult(); + } + + foreach (ConfigElement ffmpegPath in maybeFFmpegPath) + { + var ffmpegCapabilities = await hardwareCapabilitiesFactory.GetFFmpegCapabilities(ffmpegPath.Value); + + List missingFilters = []; + + foreach (FFmpegKnownFilter filter in FFmpegKnownFilter.RequiredFilters) + { + if (!ffmpegCapabilities.HasFilter(filter)) + { + missingFilters.Add(filter.Name); + } + } + + if (missingFilters.Count > 0) + { + return FailResult( + $"FFmpeg is missing required filters and will NOT work correctly: [{string.Join(", ", missingFilters)}]", + "FFmpeg is missing required filters and will NOT work correctly"); + } + } + + return NotApplicableResult(); + } +} diff --git a/ErsatzTV.Infrastructure/Health/Checks/FFmpegVersionHealthCheck.cs b/ErsatzTV.Infrastructure/Health/Checks/FFmpegVersionHealthCheck.cs index 42e76dd77..cfdf14c37 100644 --- a/ErsatzTV.Infrastructure/Health/Checks/FFmpegVersionHealthCheck.cs +++ b/ErsatzTV.Infrastructure/Health/Checks/FFmpegVersionHealthCheck.cs @@ -6,7 +6,8 @@ using ErsatzTV.Core.Interfaces.Repositories; namespace ErsatzTV.Infrastructure.Health.Checks; -public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthCheck +public class FFmpegVersionHealthCheck(IConfigElementRepository configElementRepository) + : BaseHealthCheck, IFFmpegVersionHealthCheck { private const string BundledVersion = "7.1.1"; private const string BundledVersionVaapi = "7.1.1"; @@ -14,11 +15,6 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe private static readonly string[] FFmpegVersionArguments = { "-version" }; - private readonly IConfigElementRepository _configElementRepository; - - public FFmpegVersionHealthCheck(IConfigElementRepository configElementRepository) => - _configElementRepository = configElementRepository; - public override string Title => "FFmpeg Version"; public async Task Check(CancellationToken cancellationToken) @@ -26,14 +22,14 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe var link = new HealthCheckLink("https://github.com/ErsatzTV/ErsatzTV-ffmpeg/releases/tag/7.1.1"); Option maybeFFmpegPath = - await _configElementRepository.GetConfigElement(ConfigElementKey.FFmpegPath, cancellationToken); + await configElementRepository.GetConfigElement(ConfigElementKey.FFmpegPath, cancellationToken); if (maybeFFmpegPath.IsNone) { return FailResult("Unable to locate ffmpeg", "Unable to locate ffmpeg", link); } Option maybeFFprobePath = - await _configElementRepository.GetConfigElement(ConfigElementKey.FFprobePath, cancellationToken); + await configElementRepository.GetConfigElement(ConfigElementKey.FFprobePath, cancellationToken); if (maybeFFprobePath.IsNone) { return FailResult("Unable to locate ffprobe", "Unable to locate ffprobe", link); diff --git a/ErsatzTV.Infrastructure/Health/HealthCheckService.cs b/ErsatzTV.Infrastructure/Health/HealthCheckService.cs index 3ec586f7d..d90ee7459 100644 --- a/ErsatzTV.Infrastructure/Health/HealthCheckService.cs +++ b/ErsatzTV.Infrastructure/Health/HealthCheckService.cs @@ -18,6 +18,7 @@ public class HealthCheckService : IHealthCheckService public HealthCheckService( IMacOsConfigFolderHealthCheck macOsConfigFolderHealthCheck, IFFmpegVersionHealthCheck ffmpegVersionHealthCheck, + IFFmpegCapabilitiesHealthCheck ffmpegCapabilitiesHealthCheck, IFFmpegReportsHealthCheck ffmpegReportsHealthCheck, IHardwareAccelerationHealthCheck hardwareAccelerationHealthCheck, IMovieMetadataHealthCheck movieMetadataHealthCheck, @@ -43,6 +44,7 @@ public class HealthCheckService : IHealthCheckService macOsConfigFolderHealthCheck, unifiedDockerHealthCheck, ffmpegVersionHealthCheck, + ffmpegCapabilitiesHealthCheck, ffmpegReportsHealthCheck, hardwareAccelerationHealthCheck, movieMetadataHealthCheck, diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index 3119207c6..d09a766a5 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -761,6 +761,7 @@ public class Startup services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped();