Browse Source

add health check to verify ffmpeg capabilities (filters)

pull/2798/head
Jason Dove 6 months ago
parent
commit
40fa0810a0
No known key found for this signature in database
  1. 3
      CHANGELOG.md
  2. 5
      ErsatzTV.Core/Health/Checks/IFFmpegCapabilitiesHealthCheck.cs
  3. 50
      ErsatzTV.FFmpeg/Capabilities/FFmpegKnownFilter.cs
  4. 4
      ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs
  5. 48
      ErsatzTV.Infrastructure/Health/Checks/FFmpegCapabilitiesHealthCheck.cs
  6. 12
      ErsatzTV.Infrastructure/Health/Checks/FFmpegVersionHealthCheck.cs
  7. 2
      ErsatzTV.Infrastructure/Health/HealthCheckService.cs
  8. 1
      ErsatzTV/Startup.cs

3
CHANGELOG.md

@ -18,6 +18,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -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

5
ErsatzTV.Core/Health/Checks/IFFmpegCapabilitiesHealthCheck.cs

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
namespace ErsatzTV.Core.Health.Checks;
public interface IFFmpegCapabilitiesHealthCheck : IHealthCheck
{
}

50
ErsatzTV.FFmpeg/Capabilities/FFmpegKnownFilter.cs

@ -4,7 +4,28 @@ public record FFmpegKnownFilter @@ -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("loop");
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 @@ -13,6 +34,31 @@ public record FFmpegKnownFilter
public static IList<string> AllFilters =>
[
ScaleNpp.Name,
TonemapOpenCL.Name
TonemapOpenCL.Name,
];
public static IList<FFmpegKnownFilter> RequiredFilters =>
[
AudioPad,
AudioResample,
Color,
ColorChannelMixer,
Colorspace,
Crop,
Fade,
Format,
Fps,
FrameRate,
Loop,
NormalizeLoudness,
Overlay,
Pad,
Realtime,
SetPts,
Scale,
Subtitles,
Tonemap,
Yadif,
ZScale
];
}

4
ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs

@ -66,8 +66,10 @@ public partial class HardwareCapabilitiesFactory( @@ -66,8 +66,10 @@ public partial class HardwareCapabilitiesFactory(
IReadOnlySet<string> ffmpegDecoders = await GetFFmpegCapabilities(ffmpegPath, "decoders", ParseFFmpegLine)
.Map(set => set.Intersect(FFmpegKnownDecoder.AllDecoders).ToImmutableHashSet());
IEnumerable<string> allFilterNames =
FFmpegKnownFilter.AllFilters.Union(FFmpegKnownFilter.RequiredFilters.Select(f => f.Name));
IReadOnlySet<string> ffmpegFilters = await GetFFmpegCapabilities(ffmpegPath, "filters", ParseFFmpegLine)
.Map(set => set.Intersect(FFmpegKnownFilter.AllFilters).ToImmutableHashSet());
.Map(set => set.Intersect(allFilterNames).ToImmutableHashSet());
IReadOnlySet<string> ffmpegEncoders = await GetFFmpegCapabilities(ffmpegPath, "encoders", ParseFFmpegLine)
.Map(set => set.Intersect(FFmpegKnownEncoder.AllEncoders).ToImmutableHashSet());

48
ErsatzTV.Infrastructure/Health/Checks/FFmpegCapabilitiesHealthCheck.cs

@ -0,0 +1,48 @@ @@ -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<HealthCheckResult> Check(CancellationToken cancellationToken)
{
Option<ConfigElement> 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<string> 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();
}
}

12
ErsatzTV.Infrastructure/Health/Checks/FFmpegVersionHealthCheck.cs

@ -6,7 +6,8 @@ using ErsatzTV.Core.Interfaces.Repositories; @@ -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 @@ -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<HealthCheckResult> Check(CancellationToken cancellationToken)
@ -26,14 +22,14 @@ public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthChe @@ -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<ConfigElement> 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<ConfigElement> 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);

2
ErsatzTV.Infrastructure/Health/HealthCheckService.cs

@ -18,6 +18,7 @@ public class HealthCheckService : IHealthCheckService @@ -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 @@ -43,6 +44,7 @@ public class HealthCheckService : IHealthCheckService
macOsConfigFolderHealthCheck,
unifiedDockerHealthCheck,
ffmpegVersionHealthCheck,
ffmpegCapabilitiesHealthCheck,
ffmpegReportsHealthCheck,
hardwareAccelerationHealthCheck,
movieMetadataHealthCheck,

1
ErsatzTV/Startup.cs

@ -761,6 +761,7 @@ public class Startup @@ -761,6 +761,7 @@ public class Startup
services.AddScoped<IMacOsConfigFolderHealthCheck, MacOsConfigFolderHealthCheck>();
services.AddScoped<IFFmpegVersionHealthCheck, FFmpegVersionHealthCheck>();
services.AddScoped<IFFmpegCapabilitiesHealthCheck, FFmpegCapabilitiesHealthCheck>();
services.AddScoped<IFFmpegReportsHealthCheck, FFmpegReportsHealthCheck>();
services.AddScoped<IHardwareAccelerationHealthCheck, HardwareAccelerationHealthCheck>();
services.AddScoped<IMovieMetadataHealthCheck, MovieMetadataHealthCheck>();

Loading…
Cancel
Save