From 0550c60a78a2e1764a4a642dc1fe82f52275473f Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Sat, 14 Oct 2023 21:13:18 -0500 Subject: [PATCH] allow older ffmpeg for testing (#1485) * allow older ffmpeg for testing * use proper option name --- .../PipelineBuilderBaseTests.cs | 1 + .../Capabilities/FFmpegCapabilities.cs | 7 +++- .../HardwareCapabilitiesFactory.cs | 42 ++++++++++++++++++- .../Capabilities/IFFmpegCapabilities.cs | 1 + .../InputOption/ReadrateInputOption.cs | 22 +++++++++- .../Pipeline/PipelineBuilderBase.cs | 8 ++-- 6 files changed, 72 insertions(+), 9 deletions(-) diff --git a/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs b/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs index 063a7e9b4..d0478dd6f 100644 --- a/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs +++ b/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs @@ -497,6 +497,7 @@ public class PipelineBuilderBaseTests 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 49380f799..b6a6e32f0 100644 --- a/ErsatzTV.FFmpeg/Capabilities/FFmpegCapabilities.cs +++ b/ErsatzTV.FFmpeg/Capabilities/FFmpegCapabilities.cs @@ -9,17 +9,20 @@ public class FFmpegCapabilities : IFFmpegCapabilities private readonly IReadOnlySet _ffmpegDecoders; private readonly IReadOnlySet _ffmpegEncoders; private readonly IReadOnlySet _ffmpegFilters; + private readonly IReadOnlySet _ffmpegOptions; public FFmpegCapabilities( IReadOnlySet ffmpegHardwareAccelerations, IReadOnlySet ffmpegDecoders, IReadOnlySet ffmpegFilters, - IReadOnlySet ffmpegEncoders) + IReadOnlySet ffmpegEncoders, + IReadOnlySet ffmpegOptions) { _ffmpegHardwareAccelerations = ffmpegHardwareAccelerations; _ffmpegDecoders = ffmpegDecoders; _ffmpegFilters = ffmpegFilters; _ffmpegEncoders = ffmpegEncoders; + _ffmpegOptions = ffmpegOptions; } public bool HasHardwareAcceleration(HardwareAccelerationMode hardwareAccelerationMode) @@ -43,6 +46,8 @@ public class FFmpegCapabilities : IFFmpegCapabilities public bool HasFilter(string filter) => _ffmpegFilters.Contains(filter); + public bool HasOption(string ffmpegOption) => _ffmpegOptions.Contains(ffmpegOption); + public Option SoftwareDecoderForVideoFormat(string videoFormat) => videoFormat switch { diff --git a/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs b/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs index 27f3fa889..1f977d1ef 100644 --- a/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs +++ b/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs @@ -46,8 +46,14 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory IReadOnlySet ffmpegDecoders = await GetFFmpegCapabilities(ffmpegPath, "decoders", ParseFFmpegLine); IReadOnlySet ffmpegFilters = await GetFFmpegCapabilities(ffmpegPath, "filters", ParseFFmpegLine); IReadOnlySet ffmpegEncoders = await GetFFmpegCapabilities(ffmpegPath, "encoders", ParseFFmpegLine); - - return new FFmpegCapabilities(ffmpegHardwareAccelerations, ffmpegDecoders, ffmpegFilters, ffmpegEncoders); + IReadOnlySet ffmpegOptions = await GetFFmpegOptions(ffmpegPath); + + return new FFmpegCapabilities( + ffmpegHardwareAccelerations, + ffmpegDecoders, + ffmpegFilters, + ffmpegEncoders, + ffmpegOptions); } public async Task GetHardwareCapabilities( @@ -185,6 +191,31 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory .Bind(l => parseLine(l)) .ToImmutableHashSet(); } + + private async Task> GetFFmpegOptions(string ffmpegPath) + { + var cacheKey = string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, "options"); + if (_memoryCache.TryGetValue(cacheKey, out IReadOnlySet? cachedCapabilities) && + cachedCapabilities is not null) + { + return cachedCapabilities; + } + + string[] arguments = { "-hide_banner", "-h", "long" }; + + 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 => ParseFFmpegOptionLine(l)) + .ToImmutableHashSet(); + } private static Option ParseFFmpegAccelLine(string input) { @@ -200,6 +231,13 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory return match.Success ? match.Groups[1].Value : Option.None; } + private static Option ParseFFmpegOptionLine(string input) + { + const string PATTERN = @"^-([a-z_]+)\s+.*"; + Match match = Regex.Match(input, PATTERN); + return match.Success ? match.Groups[1].Value : Option.None; + } + private async Task GetVaapiCapabilities( Option vaapiDriver, Option vaapiDevice) diff --git a/ErsatzTV.FFmpeg/Capabilities/IFFmpegCapabilities.cs b/ErsatzTV.FFmpeg/Capabilities/IFFmpegCapabilities.cs index fd63d9aaf..409baa9c2 100644 --- a/ErsatzTV.FFmpeg/Capabilities/IFFmpegCapabilities.cs +++ b/ErsatzTV.FFmpeg/Capabilities/IFFmpegCapabilities.cs @@ -8,5 +8,6 @@ public interface IFFmpegCapabilities bool HasDecoder(string decoder); bool HasEncoder(string encoder); bool HasFilter(string filter); + bool HasOption(string ffmpegOption); Option SoftwareDecoderForVideoFormat(string videoFormat); } diff --git a/ErsatzTV.FFmpeg/InputOption/ReadrateInputOption.cs b/ErsatzTV.FFmpeg/InputOption/ReadrateInputOption.cs index 368453292..0e696ae62 100644 --- a/ErsatzTV.FFmpeg/InputOption/ReadrateInputOption.cs +++ b/ErsatzTV.FFmpeg/InputOption/ReadrateInputOption.cs @@ -1,17 +1,26 @@ using System.Globalization; +using ErsatzTV.FFmpeg.Capabilities; using ErsatzTV.FFmpeg.Environment; +using Microsoft.Extensions.Logging; namespace ErsatzTV.FFmpeg.InputOption; public class ReadrateInputOption : IInputOption { + private readonly IFFmpegCapabilities _ffmpegCapabilities; private readonly int _initialBurstSeconds; + private readonly ILogger _logger; - public ReadrateInputOption(int initialBurstSeconds = 0) + public ReadrateInputOption( + IFFmpegCapabilities ffmpegCapabilities, + int initialBurstSeconds, + ILogger logger) { + _ffmpegCapabilities = ffmpegCapabilities; _initialBurstSeconds = initialBurstSeconds; + _logger = logger; } - + public IList EnvironmentVariables => Array.Empty(); public IList GlobalOptions => Array.Empty(); @@ -22,6 +31,15 @@ public class ReadrateInputOption : IInputOption if (_initialBurstSeconds > 0) { + if (!_ffmpegCapabilities.HasOption("readrate_initial_burst")) + { + _logger.LogWarning( + "FFmpeg is missing {Option} option; unable to transcode faster than realtime", + "readrate_initial_burst"); + + return result; + } + result.AddRange( new[] { diff --git a/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs b/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs index 2ad19651f..412823912 100644 --- a/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs +++ b/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs @@ -84,7 +84,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder }; concatInputFile.AddOption(new ConcatInputFormat()); - concatInputFile.AddOption(new ReadrateInputOption()); + concatInputFile.AddOption(new ReadrateInputOption(_ffmpegCapabilities, 0, _logger)); concatInputFile.AddOption(new InfiniteLoopInputOption(HardwareAccelerationMode.None)); foreach (int threadCount in ffmpegState.ThreadCount) @@ -130,7 +130,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder new EncoderCopyAll() }; - concatInputFile.AddOption(new ReadrateInputOption()); + concatInputFile.AddOption(new ReadrateInputOption(_ffmpegCapabilities, 0, _logger)); SetMetadataServiceProvider(ffmpegState, pipelineSteps); SetMetadataServiceName(ffmpegState, pipelineSteps); @@ -640,8 +640,8 @@ public abstract class PipelineBuilderBase : IPipelineBuilder }; } - _audioInputFile.Iter(a => a.AddOption(new ReadrateInputOption(initialBurst))); - videoInputFile.AddOption(new ReadrateInputOption(initialBurst)); + _audioInputFile.Iter(a => a.AddOption(new ReadrateInputOption(_ffmpegCapabilities, initialBurst, _logger))); + videoInputFile.AddOption(new ReadrateInputOption(_ffmpegCapabilities, initialBurst, _logger)); } private static void SetStillImageInfiniteLoop(