From d1b83f167b5fed75ea0478c60966cba5eb3277a5 Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:33:54 -0600 Subject: [PATCH] detect avisynth demuxer --- .../Queries/GetTroubleshootingInfoHandler.cs | 6 +++ .../Troubleshooting/TroubleshootingInfo.cs | 1 + .../PipelineBuilderBaseTests.cs | 19 +++----- .../Capabilities/FFmpegCapabilities.cs | 47 +++++++------------ .../Capabilities/FFmpegKnownFilter.cs | 9 ++-- .../Capabilities/FFmpegKnownFormat.cs | 19 ++++++++ .../HardwareCapabilitiesFactory.cs | 40 +++++++++++++++- .../Capabilities/IFFmpegCapabilities.cs | 1 + .../Troubleshooting/Troubleshooting.razor | 4 ++ 9 files changed, 99 insertions(+), 47 deletions(-) create mode 100644 ErsatzTV.FFmpeg/Capabilities/FFmpegKnownFormat.cs diff --git a/ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs b/ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs index 9610b88f2..958aeba83 100644 --- a/ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs +++ b/ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs @@ -73,6 +73,8 @@ public class GetTroubleshootingInfoHandler : IRequestHandler FFmpegProfiles, List Channels, List Watermarks, + bool AviSynthDemuxer, string NvidiaCapabilities, string QsvCapabilities, string VaapiCapabilities, diff --git a/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs b/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs index 7032c1975..c10351fdf 100644 --- a/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs +++ b/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs @@ -544,16 +544,11 @@ public class PipelineBuilderBaseTests return command; } - public class DefaultFFmpegCapabilities : FFmpegCapabilities - { - public DefaultFFmpegCapabilities() - : base( - new System.Collections.Generic.HashSet(), - new System.Collections.Generic.HashSet(), - new System.Collections.Generic.HashSet(), - new System.Collections.Generic.HashSet(), - new System.Collections.Generic.HashSet()) - { - } - } + public class DefaultFFmpegCapabilities() : FFmpegCapabilities( + new System.Collections.Generic.HashSet(), + new System.Collections.Generic.HashSet(), + new System.Collections.Generic.HashSet(), + new System.Collections.Generic.HashSet(), + new System.Collections.Generic.HashSet(), + new System.Collections.Generic.HashSet()); } diff --git a/ErsatzTV.FFmpeg/Capabilities/FFmpegCapabilities.cs b/ErsatzTV.FFmpeg/Capabilities/FFmpegCapabilities.cs index 308638a2a..bee9914ae 100644 --- a/ErsatzTV.FFmpeg/Capabilities/FFmpegCapabilities.cs +++ b/ErsatzTV.FFmpeg/Capabilities/FFmpegCapabilities.cs @@ -3,34 +3,21 @@ using ErsatzTV.FFmpeg.Format; namespace ErsatzTV.FFmpeg.Capabilities; -public class FFmpegCapabilities : IFFmpegCapabilities +public class FFmpegCapabilities( + IReadOnlySet ffmpegHardwareAccelerations, + IReadOnlySet ffmpegDecoders, + IReadOnlySet ffmpegFilters, + IReadOnlySet ffmpegEncoders, + IReadOnlySet ffmpegOptions, + IReadOnlySet ffmpegDemuxFormats) + : IFFmpegCapabilities { - private readonly IReadOnlySet _ffmpegDecoders; - private readonly IReadOnlySet _ffmpegEncoders; - private readonly IReadOnlySet _ffmpegFilters; - private readonly IReadOnlySet _ffmpegHardwareAccelerations; - private readonly IReadOnlySet _ffmpegOptions; - - public FFmpegCapabilities( - IReadOnlySet ffmpegHardwareAccelerations, - IReadOnlySet ffmpegDecoders, - IReadOnlySet ffmpegFilters, - IReadOnlySet ffmpegEncoders, - IReadOnlySet ffmpegOptions) - { - _ffmpegHardwareAccelerations = ffmpegHardwareAccelerations; - _ffmpegDecoders = ffmpegDecoders; - _ffmpegFilters = ffmpegFilters; - _ffmpegEncoders = ffmpegEncoders; - _ffmpegOptions = ffmpegOptions; - } - public bool HasHardwareAcceleration(HardwareAccelerationMode hardwareAccelerationMode) { // AMF isn't a "hwaccel" in ffmpeg, so check for presence of encoders if (hardwareAccelerationMode is HardwareAccelerationMode.Amf) { - return _ffmpegEncoders.Any(e => e.EndsWith( + return ffmpegEncoders.Any(e => e.EndsWith( $"_{FFmpegKnownHardwareAcceleration.Amf.Name}", StringComparison.OrdinalIgnoreCase)); } @@ -38,7 +25,7 @@ public class FFmpegCapabilities : IFFmpegCapabilities // V4l2m2m isn't a "hwaccel" in ffmpeg, so check for presence of encoders if (hardwareAccelerationMode is HardwareAccelerationMode.V4l2m2m) { - return _ffmpegEncoders.Any(e => e.EndsWith( + return ffmpegEncoders.Any(e => e.EndsWith( $"_{FFmpegKnownHardwareAcceleration.V4l2m2m.Name}", StringComparison.OrdinalIgnoreCase)); } @@ -57,19 +44,21 @@ public class FFmpegCapabilities : IFFmpegCapabilities foreach (FFmpegKnownHardwareAcceleration accelToCheck in maybeAccelToCheck) { - return _ffmpegHardwareAccelerations.Contains(accelToCheck.Name); + return ffmpegHardwareAccelerations.Contains(accelToCheck.Name); } return false; } - public bool HasDecoder(FFmpegKnownDecoder decoder) => _ffmpegDecoders.Contains(decoder.Name); + public bool HasDecoder(FFmpegKnownDecoder decoder) => ffmpegDecoders.Contains(decoder.Name); + + public bool HasEncoder(FFmpegKnownEncoder encoder) => ffmpegEncoders.Contains(encoder.Name); - public bool HasEncoder(FFmpegKnownEncoder encoder) => _ffmpegEncoders.Contains(encoder.Name); + public bool HasFilter(FFmpegKnownFilter filter) => ffmpegFilters.Contains(filter.Name); - public bool HasFilter(FFmpegKnownFilter filter) => _ffmpegFilters.Contains(filter.Name); + public bool HasOption(FFmpegKnownOption ffmpegOption) => ffmpegOptions.Contains(ffmpegOption.Name); - public bool HasOption(FFmpegKnownOption ffmpegOption) => _ffmpegOptions.Contains(ffmpegOption.Name); + public bool HasDemuxFormat(FFmpegKnownFormat format) => ffmpegDemuxFormats.Contains(format.Name); public Option SoftwareDecoderForVideoFormat(string videoFormat) => videoFormat switch @@ -83,7 +72,7 @@ public class FFmpegCapabilities : IFFmpegCapabilities VideoFormat.MsMpeg4V3 => new DecoderMsMpeg4V3(), VideoFormat.Mpeg4 => new DecoderMpeg4(), VideoFormat.Vp9 => new DecoderVp9(), - VideoFormat.Av1 => new DecoderAv1(_ffmpegDecoders), + VideoFormat.Av1 => new DecoderAv1(ffmpegDecoders), VideoFormat.Raw => new DecoderRawVideo(), VideoFormat.Undetermined => new DecoderImplicit(), diff --git a/ErsatzTV.FFmpeg/Capabilities/FFmpegKnownFilter.cs b/ErsatzTV.FFmpeg/Capabilities/FFmpegKnownFilter.cs index 7c08015c6..ea82e9c11 100644 --- a/ErsatzTV.FFmpeg/Capabilities/FFmpegKnownFilter.cs +++ b/ErsatzTV.FFmpeg/Capabilities/FFmpegKnownFilter.cs @@ -11,9 +11,8 @@ public record FFmpegKnownFilter public string Name { get; } public static IList AllFilters => - new[] - { - ScaleNpp.Name, - TonemapOpenCL.Name - }; + [ + ScaleNpp.Name, + TonemapOpenCL.Name + ]; } diff --git a/ErsatzTV.FFmpeg/Capabilities/FFmpegKnownFormat.cs b/ErsatzTV.FFmpeg/Capabilities/FFmpegKnownFormat.cs new file mode 100644 index 000000000..598b61362 --- /dev/null +++ b/ErsatzTV.FFmpeg/Capabilities/FFmpegKnownFormat.cs @@ -0,0 +1,19 @@ +using System.Diagnostics.CodeAnalysis; + +namespace ErsatzTV.FFmpeg.Capabilities; + +[SuppressMessage("ReSharper", "IdentifierTypo")] +[SuppressMessage("ReSharper", "StringLiteralTypo")] +public record FFmpegKnownFormat +{ + public static readonly FFmpegKnownFormat AviSynth = new("avisynth"); + + private FFmpegKnownFormat(string Name) => this.Name = Name; + + public string Name { get; } + + public static IList AllFormats => + [ + AviSynth.Name + ]; +} diff --git a/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs b/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs index 01f01d8be..787011640 100644 --- a/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs +++ b/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs @@ -72,12 +72,16 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory IReadOnlySet ffmpegOptions = await GetFFmpegOptions(ffmpegPath) .Map(set => set.Intersect(FFmpegKnownOption.AllOptions).ToImmutableHashSet()); + IReadOnlySet ffmpegDemuxFormats = await GetFFmpegFormats(ffmpegPath, "D") + .Map(set => set.Intersect(FFmpegKnownFormat.AllFormats).ToImmutableHashSet()); + return new FFmpegCapabilities( ffmpegHardwareAccelerations, ffmpegDecoders, ffmpegFilters, ffmpegEncoders, - ffmpegOptions); + ffmpegOptions, + ffmpegDemuxFormats); } public async Task GetHardwareCapabilities( @@ -358,6 +362,33 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory .ToImmutableHashSet(); } + private async Task> GetFFmpegFormats(string ffmpegPath, string muxDemux) + { + var cacheKey = string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, "formats"); + if (_memoryCache.TryGetValue(cacheKey, out IReadOnlySet? cachedCapabilities) && + cachedCapabilities is not null) + { + return cachedCapabilities; + } + + string[] arguments = { "-hide_banner", "-formats" }; + + BufferedCommandResult result = await Cli.Wrap(ffmpegPath) + .WithArguments(arguments) + .WithValidation(CommandResultValidation.None) + .ExecuteBufferedAsync(Encoding.UTF8); + + string output = string.IsNullOrWhiteSpace(result.StandardOutput) + ? result.StandardError + : result.StandardOutput; + + return output.Split("\n").Map(s => s.Trim()) + .Bind(l => ParseFFmpegFormatLine(l)) + .Where(tuple => tuple.Item1.Contains(muxDemux)) + .Map(tuple => tuple.Item2) + .ToImmutableHashSet(); + } + private static Option ParseFFmpegAccelLine(string input) { const string PATTERN = @"^([\w]+)$"; @@ -379,6 +410,13 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory return match.Success ? match.Groups[1].Value : Option.None; } + private static Option> ParseFFmpegFormatLine(string input) + { + const string PATTERN = @"([DE]+)\s+(\w+)"; + Match match = Regex.Match(input, PATTERN); + return match.Success ? Tuple(match.Groups[1].Value, match.Groups[2].Value) : Option>.None; + } + private async Task GetVaapiCapabilities( Option vaapiDisplay, Option vaapiDriver, diff --git a/ErsatzTV.FFmpeg/Capabilities/IFFmpegCapabilities.cs b/ErsatzTV.FFmpeg/Capabilities/IFFmpegCapabilities.cs index 83f659eb4..460eeeb30 100644 --- a/ErsatzTV.FFmpeg/Capabilities/IFFmpegCapabilities.cs +++ b/ErsatzTV.FFmpeg/Capabilities/IFFmpegCapabilities.cs @@ -9,5 +9,6 @@ public interface IFFmpegCapabilities bool HasEncoder(FFmpegKnownEncoder encoder); bool HasFilter(FFmpegKnownFilter filter); bool HasOption(FFmpegKnownOption ffmpegOption); + bool HasDemuxFormat(FFmpegKnownFormat format); Option SoftwareDecoderForVideoFormat(string videoFormat); } diff --git a/ErsatzTV/Pages/Troubleshooting/Troubleshooting.razor b/ErsatzTV/Pages/Troubleshooting/Troubleshooting.razor index f3208d82e..bd9daa441 100644 --- a/ErsatzTV/Pages/Troubleshooting/Troubleshooting.razor +++ b/ErsatzTV/Pages/Troubleshooting/Troubleshooting.razor @@ -157,6 +157,10 @@ info.VideoControllers, info.Health, info.FFmpegSettings, + AviSynth = new + { + Demuxer = info.AviSynthDemuxer + }, info.Channels, info.FFmpegProfiles },