diff --git a/ErsatzTV.Application/ErsatzTV.Application.csproj.DotSettings b/ErsatzTV.Application/ErsatzTV.Application.csproj.DotSettings index c80a049df..e810bb164 100644 --- a/ErsatzTV.Application/ErsatzTV.Application.csproj.DotSettings +++ b/ErsatzTV.Application/ErsatzTV.Application.csproj.DotSettings @@ -9,6 +9,7 @@ True True True + True True True True diff --git a/ErsatzTV.Application/FFmpeg/Commands/RefreshFFmpegCapabilities.cs b/ErsatzTV.Application/FFmpeg/Commands/RefreshFFmpegCapabilities.cs new file mode 100644 index 000000000..27fbdc33e --- /dev/null +++ b/ErsatzTV.Application/FFmpeg/Commands/RefreshFFmpegCapabilities.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Application.FFmpeg; + +public record RefreshFFmpegCapabilities : IRequest, IBackgroundServiceRequest; diff --git a/ErsatzTV.Application/FFmpeg/Commands/RefreshFFmpegCapabilitiesHandler.cs b/ErsatzTV.Application/FFmpeg/Commands/RefreshFFmpegCapabilitiesHandler.cs new file mode 100644 index 000000000..960851ced --- /dev/null +++ b/ErsatzTV.Application/FFmpeg/Commands/RefreshFFmpegCapabilitiesHandler.cs @@ -0,0 +1,29 @@ +using ErsatzTV.Core.Domain; +using ErsatzTV.FFmpeg.Capabilities; +using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Infrastructure.Extensions; +using Microsoft.EntityFrameworkCore; + +namespace ErsatzTV.Application.FFmpeg; + +public class RefreshFFmpegCapabilitiesHandler( + IDbContextFactory dbContextFactory, + IHardwareCapabilitiesFactory hardwareCapabilitiesFactory) + : IRequestHandler +{ + public async Task Handle(RefreshFFmpegCapabilities request, CancellationToken cancellationToken) + { + hardwareCapabilitiesFactory.ClearCache(); + + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + Option maybeFFmpegPath = await dbContext.ConfigElements + .GetValue(ConfigElementKey.FFmpegPath, cancellationToken) + .FilterT(File.Exists); + + foreach (string ffmpegPath in maybeFFmpegPath) + { + _ = await hardwareCapabilitiesFactory.GetFFmpegCapabilities(ffmpegPath); + } + } +} diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs index fc40c28c1..0b2d031b1 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs @@ -9,22 +9,12 @@ using ErsatzTV.Core.Interfaces.Repositories; namespace ErsatzTV.Application.FFmpegProfiles; -public class UpdateFFmpegSettingsHandler : IRequestHandler> +public class UpdateFFmpegSettingsHandler( + IConfigElementRepository configElementRepository, + ILocalFileSystem localFileSystem, + ChannelWriter workerChannel) + : IRequestHandler> { - private readonly IConfigElementRepository _configElementRepository; - private readonly ILocalFileSystem _localFileSystem; - private readonly ChannelWriter _workerChannel; - - public UpdateFFmpegSettingsHandler( - IConfigElementRepository configElementRepository, - ILocalFileSystem localFileSystem, - ChannelWriter workerChannel) - { - _configElementRepository = configElementRepository; - _localFileSystem = localFileSystem; - _workerChannel = workerChannel; - } - public Task> Handle( UpdateFFmpegSettings request, CancellationToken cancellationToken) => @@ -44,7 +34,7 @@ public class UpdateFFmpegSettingsHandler : IRequestHandler> ValidateToolPath(string path, string name) { - if (!_localFileSystem.FileExists(path)) + if (!localFileSystem.FileExists(path)) { return BaseError.New($"{name} path does not exist"); } @@ -71,21 +61,21 @@ public class UpdateFFmpegSettingsHandler : IRequestHandler ApplyUpdate(UpdateFFmpegSettings request, CancellationToken cancellationToken) { - await _configElementRepository.Upsert(ConfigElementKey.FFmpegPath, request.Settings.FFmpegPath, cancellationToken); - await _configElementRepository.Upsert(ConfigElementKey.FFprobePath, request.Settings.FFprobePath, cancellationToken); - await _configElementRepository.Upsert( + await configElementRepository.Upsert(ConfigElementKey.FFmpegPath, request.Settings.FFmpegPath, cancellationToken); + await configElementRepository.Upsert(ConfigElementKey.FFprobePath, request.Settings.FFprobePath, cancellationToken); + await configElementRepository.Upsert( ConfigElementKey.FFmpegDefaultProfileId, request.Settings.DefaultFFmpegProfileId.ToString(CultureInfo.InvariantCulture), cancellationToken); - await _configElementRepository.Upsert( + await configElementRepository.Upsert( ConfigElementKey.FFmpegSaveReports, request.Settings.SaveReports.ToString(), cancellationToken); - await _configElementRepository.Upsert( + await configElementRepository.Upsert( ConfigElementKey.FFmpegHlsDirectOutputFormat, request.Settings.HlsDirectOutputFormat, cancellationToken); - await _configElementRepository.Upsert( + await configElementRepository.Upsert( ConfigElementKey.FFmpegDefaultMpegTsScript, request.Settings.DefaultMpegTsScript, cancellationToken); @@ -95,12 +85,12 @@ public class UpdateFFmpegSettingsHandler : IRequestHandler.None), cancellationToken); + await workerChannel.WriteAsync(new ExtractEmbeddedSubtitles(Option.None), cancellationToken); } if (request.Settings.GlobalWatermarkId is not null) { - await _configElementRepository.Upsert( + await configElementRepository.Upsert( ConfigElementKey.FFmpegGlobalWatermarkId, request.Settings.GlobalWatermarkId.Value, cancellationToken); } else { - await _configElementRepository.Delete(ConfigElementKey.FFmpegGlobalWatermarkId, cancellationToken); + await configElementRepository.Delete(ConfigElementKey.FFmpegGlobalWatermarkId, cancellationToken); } if (request.Settings.GlobalFallbackFillerId is not null) { - await _configElementRepository.Upsert( + await configElementRepository.Upsert( ConfigElementKey.FFmpegGlobalFallbackFillerId, request.Settings.GlobalFallbackFillerId.Value, cancellationToken); } else { - await _configElementRepository.Delete(ConfigElementKey.FFmpegGlobalFallbackFillerId, cancellationToken); + await configElementRepository.Delete(ConfigElementKey.FFmpegGlobalFallbackFillerId, cancellationToken); } - await _configElementRepository.Upsert( + await configElementRepository.Upsert( ConfigElementKey.FFmpegSegmenterTimeout, request.Settings.HlsSegmenterIdleTimeout, cancellationToken); - await _configElementRepository.Upsert( + await configElementRepository.Upsert( ConfigElementKey.FFmpegWorkAheadSegmenters, request.Settings.WorkAheadSegmenterLimit, cancellationToken); - await _configElementRepository.Upsert( + await configElementRepository.Upsert( ConfigElementKey.FFmpegInitialSegmentCount, request.Settings.InitialSegmentCount, cancellationToken); diff --git a/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs b/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs index 787011640..943f4da39 100644 --- a/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs +++ b/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Runtime.InteropServices; using System.Text; @@ -17,12 +18,16 @@ using Microsoft.Extensions.Logging; namespace ErsatzTV.FFmpeg.Capabilities; -public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory +public partial class HardwareCapabilitiesFactory( + IMemoryCache memoryCache, + IRuntimeInfo runtimeInfo, + ILogger logger) + : IHardwareCapabilitiesFactory { private const string CudaDeviceKey = "ffmpeg.hardware.cuda.device"; - private static readonly CompositeFormat - VaapiCacheKeyFormat = CompositeFormat.Parse("ffmpeg.hardware.vaapi.{0}.{1}.{2}"); + private static readonly CompositeFormat VaapiCacheKeyFormat = + CompositeFormat.Parse("ffmpeg.hardware.vaapi.{0}.{1}.{2}"); private static readonly CompositeFormat QsvCacheKeyFormat = CompositeFormat.Parse("ffmpeg.hardware.qsv.{0}"); private static readonly CompositeFormat FFmpegCapabilitiesCacheKeyFormat = CompositeFormat.Parse("ffmpeg.{0}"); @@ -36,19 +41,14 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory "-f", "null", "-" }; - private readonly ILogger _logger; - - private readonly IMemoryCache _memoryCache; - private readonly IRuntimeInfo _runtimeInfo; - - public HardwareCapabilitiesFactory( - IMemoryCache memoryCache, - IRuntimeInfo runtimeInfo, - ILogger logger) + public void ClearCache() { - _memoryCache = memoryCache; - _runtimeInfo = runtimeInfo; - _logger = logger; + memoryCache.Remove(string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, "hwaccels")); + memoryCache.Remove(string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, "decoders")); + memoryCache.Remove(string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, "filters")); + memoryCache.Remove(string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, "encoders")); + memoryCache.Remove(string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, "options")); + memoryCache.Remove(string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, "formats")); } public async Task GetFFmpegCapabilities(string ffmpegPath) @@ -99,7 +99,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory if (!ffmpegCapabilities.HasHardwareAcceleration(hardwareAccelerationMode)) { - _logger.LogWarning( + logger.LogWarning( "FFmpeg does not support {HardwareAcceleration} acceleration; will use software mode", hardwareAccelerationMode); @@ -111,7 +111,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory HardwareAccelerationMode.Nvenc => GetNvidiaCapabilities(ffmpegCapabilities), HardwareAccelerationMode.Qsv => await GetQsvCapabilities(ffmpegPath, vaapiDevice), HardwareAccelerationMode.Vaapi => await GetVaapiCapabilities(vaapiDisplay, vaapiDriver, vaapiDevice), - HardwareAccelerationMode.VideoToolbox => new VideoToolboxHardwareCapabilities(ffmpegCapabilities, _logger), + HardwareAccelerationMode.VideoToolbox => new VideoToolboxHardwareCapabilities(ffmpegCapabilities, logger), HardwareAccelerationMode.Amf => new AmfHardwareCapabilities(), HardwareAccelerationMode.V4l2m2m => new V4l2m2mHardwareCapabilities(ffmpegCapabilities), HardwareAccelerationMode.Rkmpp => new RkmppHardwareCapabilities(), @@ -292,13 +292,14 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory return []; } + [SuppressMessage("ReSharper", "InconsistentNaming")] public List GetVideoToolboxDecoders() { var result = new List(); foreach (string fourCC in FourCC.AllVideoToolbox) { - if (VideoToolboxUtil.IsHardwareDecoderSupported(fourCC, _logger)) + if (VideoToolboxUtil.IsHardwareDecoderSupported(fourCC, logger)) { result.Add(fourCC); } @@ -307,7 +308,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory return result; } - public List GetVideoToolboxEncoders() => VideoToolboxUtil.GetAvailableEncoders(_logger); + public List GetVideoToolboxEncoders() => VideoToolboxUtil.GetAvailableEncoders(logger); private async Task> GetFFmpegCapabilities( string ffmpegPath, @@ -315,7 +316,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory Func> parseLine) { var cacheKey = string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, capabilities); - if (_memoryCache.TryGetValue(cacheKey, out IReadOnlySet? cachedCapabilities) && + if (memoryCache.TryGetValue(cacheKey, out IReadOnlySet? cachedCapabilities) && cachedCapabilities is not null) { return cachedCapabilities; @@ -332,15 +333,19 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory ? result.StandardError : result.StandardOutput; - return output.Split("\n").Map(s => s.Trim()) + var capabilitiesResult = output.Split("\n").Map(s => s.Trim()) .Bind(l => parseLine(l)) .ToImmutableHashSet(); + + memoryCache.Set(cacheKey, capabilitiesResult); + + return capabilitiesResult; } private async Task> GetFFmpegOptions(string ffmpegPath) { var cacheKey = string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, "options"); - if (_memoryCache.TryGetValue(cacheKey, out IReadOnlySet? cachedCapabilities) && + if (memoryCache.TryGetValue(cacheKey, out IReadOnlySet? cachedCapabilities) && cachedCapabilities is not null) { return cachedCapabilities; @@ -357,15 +362,19 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory ? result.StandardError : result.StandardOutput; - return output.Split("\n").Map(s => s.Trim()) + var capabilitiesResult = output.Split("\n").Map(s => s.Trim()) .Bind(l => ParseFFmpegOptionLine(l)) .ToImmutableHashSet(); + + memoryCache.Set(cacheKey, capabilitiesResult); + + return capabilitiesResult; } private async Task> GetFFmpegFormats(string ffmpegPath, string muxDemux) { var cacheKey = string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, "formats"); - if (_memoryCache.TryGetValue(cacheKey, out IReadOnlySet? cachedCapabilities) && + if (memoryCache.TryGetValue(cacheKey, out IReadOnlySet? cachedCapabilities) && cachedCapabilities is not null) { return cachedCapabilities; @@ -382,38 +391,38 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory ? result.StandardError : result.StandardOutput; - return output.Split("\n").Map(s => s.Trim()) + var capabilitiesResult = output.Split("\n").Map(s => s.Trim()) .Bind(l => ParseFFmpegFormatLine(l)) .Where(tuple => tuple.Item1.Contains(muxDemux)) .Map(tuple => tuple.Item2) .ToImmutableHashSet(); + + memoryCache.Set(cacheKey, capabilitiesResult); + + return capabilitiesResult; } private static Option ParseFFmpegAccelLine(string input) { - const string PATTERN = @"^([\w]+)$"; - Match match = Regex.Match(input, PATTERN); + Match match = AccelRegex().Match(input); return match.Success ? match.Groups[1].Value : Option.None; } private static Option ParseFFmpegLine(string input) { - const string PATTERN = @"^\s*?[A-Z\.]+\s+(\w+).*"; - Match match = Regex.Match(input, PATTERN); + Match match = FFmpegRegex().Match(input); 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); + Match match = OptionRegex().Match(input); 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); + Match match = FormatRegex().Match(input); return match.Success ? Tuple(match.Groups[1].Value, match.Groups[2].Value) : Option>.None; } @@ -428,7 +437,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory { // this shouldn't really happen - _logger.LogError( + logger.LogError( "Cannot detect VAAPI capabilities without device {Device}", vaapiDevice); @@ -440,16 +449,16 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory string device = vaapiDevice.IfNone(string.Empty); var cacheKey = string.Format(CultureInfo.InvariantCulture, VaapiCacheKeyFormat, display, driver, device); - if (_memoryCache.TryGetValue(cacheKey, out List? profileEntrypoints) && + if (memoryCache.TryGetValue(cacheKey, out List? profileEntrypoints) && profileEntrypoints is not null) { - return new VaapiHardwareCapabilities(profileEntrypoints, _logger); + return new VaapiHardwareCapabilities(profileEntrypoints, logger); } Option output = await GetVaapiOutput(display, vaapiDriver, device); if (output.IsNone) { - _logger.LogWarning("Unable to determine VAAPI capabilities; please install vainfo"); + logger.LogWarning("Unable to determine VAAPI capabilities; please install vainfo"); return new DefaultHardwareCapabilities(); } @@ -462,7 +471,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory { if (display == "drm") { - _logger.LogDebug( + logger.LogDebug( "Detected {Count} VAAPI profile entrypoints using {Driver} {Device}", profileEntrypoints.Count, driver, @@ -470,26 +479,26 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory } else { - _logger.LogDebug( + logger.LogDebug( "Detected {Count} VAAPI profile entrypoints using {Display} {Driver}", profileEntrypoints.Count, display, driver); } - _memoryCache.Set(cacheKey, profileEntrypoints); - return new VaapiHardwareCapabilities(profileEntrypoints, _logger); + memoryCache.Set(cacheKey, profileEntrypoints); + return new VaapiHardwareCapabilities(profileEntrypoints, logger); } } catch (Exception ex) { - _logger.LogWarning( + logger.LogWarning( ex, "Error detecting VAAPI capabilities; some hardware accelerated features will be unavailable"); return new NoHardwareCapabilities(); } - _logger.LogWarning( + logger.LogWarning( "Error detecting VAAPI capabilities; some hardware accelerated features will be unavailable"); return new NoHardwareCapabilities(); @@ -499,32 +508,32 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory { try { - if (_runtimeInfo.IsOSPlatform(OSPlatform.Linux) && qsvDevice.IsNone) + if (runtimeInfo.IsOSPlatform(OSPlatform.Linux) && qsvDevice.IsNone) { // this shouldn't really happen - _logger.LogError("Cannot detect QSV capabilities without device {Device}", qsvDevice); + logger.LogError("Cannot detect QSV capabilities without device {Device}", qsvDevice); return new NoHardwareCapabilities(); } string device = qsvDevice.IfNone(string.Empty); var cacheKey = string.Format(CultureInfo.InvariantCulture, QsvCacheKeyFormat, device); - if (_memoryCache.TryGetValue(cacheKey, out List? profileEntrypoints) && + if (memoryCache.TryGetValue(cacheKey, out List? profileEntrypoints) && profileEntrypoints is not null) { - return new VaapiHardwareCapabilities(profileEntrypoints, _logger); + return new VaapiHardwareCapabilities(profileEntrypoints, logger); } QsvOutput output = await GetQsvOutput(ffmpegPath, qsvDevice); if (output.ExitCode != 0) { - _logger.LogWarning("QSV test failed; some hardware accelerated features will be unavailable"); + logger.LogWarning("QSV test failed; some hardware accelerated features will be unavailable"); return new NoHardwareCapabilities(); } - if (_runtimeInfo.IsOSPlatform(OSPlatform.Linux)) + if (runtimeInfo.IsOSPlatform(OSPlatform.Linux)) { - if (!_memoryCache.TryGetValue("ffmpeg.vaapi_displays", out List? vaapiDisplays)) + if (!memoryCache.TryGetValue("ffmpeg.vaapi_displays", out List? vaapiDisplays)) { vaapiDisplays = ["drm"]; } @@ -537,7 +546,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory Option vaapiOutput = await GetVaapiOutput(vaapiDisplay, Option.None, device); if (vaapiOutput.IsNone) { - _logger.LogWarning("Unable to determine QSV capabilities; please install vainfo"); + logger.LogWarning("Unable to determine QSV capabilities; please install vainfo"); return new DefaultHardwareCapabilities(); } @@ -548,13 +557,13 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory if (profileEntrypoints is not null && profileEntrypoints.Count != 0) { - _logger.LogDebug( + logger.LogDebug( "Detected {Count} VAAPI profile entrypoints using QSV device {Device}", profileEntrypoints.Count, device); - _memoryCache.Set(cacheKey, profileEntrypoints); - return new VaapiHardwareCapabilities(profileEntrypoints, _logger); + memoryCache.Set(cacheKey, profileEntrypoints); + return new VaapiHardwareCapabilities(profileEntrypoints, logger); } } } @@ -564,7 +573,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory } catch (Exception ex) { - _logger.LogWarning( + logger.LogWarning( ex, "Error detecting QSV capabilities; some hardware accelerated features will be unavailable"); return new NoHardwareCapabilities(); @@ -573,9 +582,9 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory private IHardwareCapabilities GetNvidiaCapabilities(IFFmpegCapabilities ffmpegCapabilities) { - if (_memoryCache.TryGetValue(CudaDeviceKey, out CudaDevice? cudaDevice) && cudaDevice is not null) + if (memoryCache.TryGetValue(CudaDeviceKey, out CudaDevice? cudaDevice) && cudaDevice is not null) { - return new NvidiaHardwareCapabilities(cudaDevice, ffmpegCapabilities, _logger); + return new NvidiaHardwareCapabilities(cudaDevice, ffmpegCapabilities, logger); } try @@ -583,14 +592,14 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory Option> maybeDevices = CudaHelper.GetDevices(); foreach (CudaDevice firstDevice in maybeDevices.Map(list => list.HeadOrNone())) { - _logger.LogDebug( + logger.LogDebug( "Detected NVIDIA GPU model {Model} architecture SM {Major}.{Minor}", firstDevice.Model, firstDevice.Version.Major, firstDevice.Version.Minor); - _memoryCache.Set(CudaDeviceKey, firstDevice); - return new NvidiaHardwareCapabilities(firstDevice, ffmpegCapabilities, _logger); + memoryCache.Set(CudaDeviceKey, firstDevice); + return new NvidiaHardwareCapabilities(firstDevice, ffmpegCapabilities, logger); } } catch (FileNotFoundException) @@ -598,9 +607,21 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory // do nothing } - _logger.LogWarning( + logger.LogWarning( "Error detecting NVIDIA GPU capabilities; some hardware accelerated features will be unavailable"); return new NoHardwareCapabilities(); } + + [GeneratedRegex(@"^([\w]+)$")] + private static partial Regex AccelRegex(); + + [GeneratedRegex(@"^\s*?[A-Z\.]+\s+(\w+).*")] + private static partial Regex FFmpegRegex(); + + [GeneratedRegex(@"^-([a-z_]+)\s+.*")] + private static partial Regex OptionRegex(); + + [GeneratedRegex(@"([DE]+)\s+(\w+)")] + private static partial Regex FormatRegex(); } diff --git a/ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilitiesFactory.cs b/ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilitiesFactory.cs index baf6a5756..9adcf8d2d 100644 --- a/ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilitiesFactory.cs +++ b/ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilitiesFactory.cs @@ -4,6 +4,8 @@ namespace ErsatzTV.FFmpeg.Capabilities; public interface IHardwareCapabilitiesFactory { + void ClearCache(); + Task GetFFmpegCapabilities(string ffmpegPath); Task GetHardwareCapabilities( diff --git a/ErsatzTV/Services/SchedulerService.cs b/ErsatzTV/Services/SchedulerService.cs index 80d2df68b..b278117fe 100644 --- a/ErsatzTV/Services/SchedulerService.cs +++ b/ErsatzTV/Services/SchedulerService.cs @@ -4,6 +4,7 @@ using Bugsnag; using ErsatzTV.Application; using ErsatzTV.Application.Channels; using ErsatzTV.Application.Emby; +using ErsatzTV.Application.FFmpeg; using ErsatzTV.Application.Graphics; using ErsatzTV.Application.Jellyfin; using ErsatzTV.Application.Maintenance; @@ -65,6 +66,7 @@ public class SchedulerService : BackgroundService // run once immediately at startup if (!stoppingToken.IsCancellationRequested) { + await QueueFFmpegCapabilitiesRefresh(stoppingToken); await DoWork(stoppingToken); } @@ -396,4 +398,7 @@ public class SchedulerService : BackgroundService private ValueTask ReleaseMemory(CancellationToken cancellationToken) => _workerChannel.WriteAsync(new ReleaseMemory(false), cancellationToken); + + private ValueTask QueueFFmpegCapabilitiesRefresh(CancellationToken cancellationToken) => + _workerChannel.WriteAsync(new RefreshFFmpegCapabilities(), cancellationToken); } diff --git a/ErsatzTV/Services/WorkerService.cs b/ErsatzTV/Services/WorkerService.cs index 1b942838f..6e832061e 100644 --- a/ErsatzTV/Services/WorkerService.cs +++ b/ErsatzTV/Services/WorkerService.cs @@ -3,6 +3,7 @@ using System.Threading.Channels; using Bugsnag; using ErsatzTV.Application; using ErsatzTV.Application.Channels; +using ErsatzTV.Application.FFmpeg; using ErsatzTV.Application.Graphics; using ErsatzTV.Application.Maintenance; using ErsatzTV.Application.MediaCollections; @@ -52,6 +53,9 @@ public class WorkerService : BackgroundService switch (request) { + case RefreshFFmpegCapabilities refreshFFmpegCapabilities: + await mediator.Send(refreshFFmpegCapabilities, stoppingToken); + break; case RefreshChannelList refreshChannelList: await mediator.Send(refreshChannelList, stoppingToken); break;