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;