Browse Source

cache ffmpeg capabilities

pull/2612/head
Jason Dove 9 months ago
parent
commit
dd4c85ff39
No known key found for this signature in database
  1. 1
      ErsatzTV.Application/ErsatzTV.Application.csproj.DotSettings
  2. 3
      ErsatzTV.Application/FFmpeg/Commands/RefreshFFmpegCapabilities.cs
  3. 29
      ErsatzTV.Application/FFmpeg/Commands/RefreshFFmpegCapabilitiesHandler.cs
  4. 56
      ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs
  5. 143
      ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs
  6. 2
      ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilitiesFactory.cs
  7. 5
      ErsatzTV/Services/SchedulerService.cs
  8. 4
      ErsatzTV/Services/WorkerService.cs

1
ErsatzTV.Application/ErsatzTV.Application.csproj.DotSettings

@ -9,6 +9,7 @@ @@ -9,6 +9,7 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=emby_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=ffmpegprofiles_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=ffmpegprofiles_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=ffmpeg_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=filler_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=filler_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=graphics_005Ccommands/@EntryIndexedValue">True</s:Boolean>

3
ErsatzTV.Application/FFmpeg/Commands/RefreshFFmpegCapabilities.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.FFmpeg;
public record RefreshFFmpegCapabilities : IRequest, IBackgroundServiceRequest;

29
ErsatzTV.Application/FFmpeg/Commands/RefreshFFmpegCapabilitiesHandler.cs

@ -0,0 +1,29 @@ @@ -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<TvContext> dbContextFactory,
IHardwareCapabilitiesFactory hardwareCapabilitiesFactory)
: IRequestHandler<RefreshFFmpegCapabilities>
{
public async Task Handle(RefreshFFmpegCapabilities request, CancellationToken cancellationToken)
{
hardwareCapabilitiesFactory.ClearCache();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<string> maybeFFmpegPath = await dbContext.ConfigElements
.GetValue<string>(ConfigElementKey.FFmpegPath, cancellationToken)
.FilterT(File.Exists);
foreach (string ffmpegPath in maybeFFmpegPath)
{
_ = await hardwareCapabilitiesFactory.GetFFmpegCapabilities(ffmpegPath);
}
}
}

56
ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs

@ -9,22 +9,12 @@ using ErsatzTV.Core.Interfaces.Repositories; @@ -9,22 +9,12 @@ using ErsatzTV.Core.Interfaces.Repositories;
namespace ErsatzTV.Application.FFmpegProfiles;
public class UpdateFFmpegSettingsHandler : IRequestHandler<UpdateFFmpegSettings, Either<BaseError, Unit>>
public class UpdateFFmpegSettingsHandler(
IConfigElementRepository configElementRepository,
ILocalFileSystem localFileSystem,
ChannelWriter<IBackgroundServiceRequest> workerChannel)
: IRequestHandler<UpdateFFmpegSettings, Either<BaseError, Unit>>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly ILocalFileSystem _localFileSystem;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public UpdateFFmpegSettingsHandler(
IConfigElementRepository configElementRepository,
ILocalFileSystem localFileSystem,
ChannelWriter<IBackgroundServiceRequest> workerChannel)
{
_configElementRepository = configElementRepository;
_localFileSystem = localFileSystem;
_workerChannel = workerChannel;
}
public Task<Either<BaseError, Unit>> Handle(
UpdateFFmpegSettings request,
CancellationToken cancellationToken) =>
@ -44,7 +34,7 @@ public class UpdateFFmpegSettingsHandler : IRequestHandler<UpdateFFmpegSettings, @@ -44,7 +34,7 @@ public class UpdateFFmpegSettingsHandler : IRequestHandler<UpdateFFmpegSettings,
private async Task<Validation<BaseError, Unit>> 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<UpdateFFmpegSettings, @@ -71,21 +61,21 @@ public class UpdateFFmpegSettingsHandler : IRequestHandler<UpdateFFmpegSettings,
private async Task<Unit> 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<UpdateFFmpegSettings, @@ -95,12 +85,12 @@ public class UpdateFFmpegSettingsHandler : IRequestHandler<UpdateFFmpegSettings,
Directory.CreateDirectory(FileSystemLayout.FFmpegReportsFolder);
}
await _configElementRepository.Upsert(
await configElementRepository.Upsert(
ConfigElementKey.FFmpegPreferredLanguageCode,
request.Settings.PreferredAudioLanguageCode,
cancellationToken);
await _configElementRepository.Upsert(
await configElementRepository.Upsert(
ConfigElementKey.FFmpegUseEmbeddedSubtitles,
request.Settings.UseEmbeddedSubtitles,
cancellationToken);
@ -111,7 +101,7 @@ public class UpdateFFmpegSettingsHandler : IRequestHandler<UpdateFFmpegSettings, @@ -111,7 +101,7 @@ public class UpdateFFmpegSettingsHandler : IRequestHandler<UpdateFFmpegSettings,
request.Settings.ExtractEmbeddedSubtitles = false;
}
await _configElementRepository.Upsert(
await configElementRepository.Upsert(
ConfigElementKey.FFmpegExtractEmbeddedSubtitles,
request.Settings.ExtractEmbeddedSubtitles,
cancellationToken);
@ -119,44 +109,44 @@ public class UpdateFFmpegSettingsHandler : IRequestHandler<UpdateFFmpegSettings, @@ -119,44 +109,44 @@ public class UpdateFFmpegSettingsHandler : IRequestHandler<UpdateFFmpegSettings,
// queue extracting all embedded subtitles
if (request.Settings.ExtractEmbeddedSubtitles)
{
await _workerChannel.WriteAsync(new ExtractEmbeddedSubtitles(Option<int>.None), cancellationToken);
await workerChannel.WriteAsync(new ExtractEmbeddedSubtitles(Option<int>.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);

143
ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs

@ -1,4 +1,5 @@ @@ -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; @@ -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<HardwareCapabilitiesFactory> 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 @@ -36,19 +41,14 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
"-f", "null", "-"
};
private readonly ILogger<HardwareCapabilitiesFactory> _logger;
private readonly IMemoryCache _memoryCache;
private readonly IRuntimeInfo _runtimeInfo;
public HardwareCapabilitiesFactory(
IMemoryCache memoryCache,
IRuntimeInfo runtimeInfo,
ILogger<HardwareCapabilitiesFactory> 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<IFFmpegCapabilities> GetFFmpegCapabilities(string ffmpegPath)
@ -99,7 +99,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory @@ -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 @@ -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 @@ -292,13 +292,14 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
return [];
}
[SuppressMessage("ReSharper", "InconsistentNaming")]
public List<string> GetVideoToolboxDecoders()
{
var result = new List<string>();
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 @@ -307,7 +308,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
return result;
}
public List<string> GetVideoToolboxEncoders() => VideoToolboxUtil.GetAvailableEncoders(_logger);
public List<string> GetVideoToolboxEncoders() => VideoToolboxUtil.GetAvailableEncoders(logger);
private async Task<IReadOnlySet<string>> GetFFmpegCapabilities(
string ffmpegPath,
@ -315,7 +316,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory @@ -315,7 +316,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
Func<string, Option<string>> parseLine)
{
var cacheKey = string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, capabilities);
if (_memoryCache.TryGetValue(cacheKey, out IReadOnlySet<string>? cachedCapabilities) &&
if (memoryCache.TryGetValue(cacheKey, out IReadOnlySet<string>? cachedCapabilities) &&
cachedCapabilities is not null)
{
return cachedCapabilities;
@ -332,15 +333,19 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory @@ -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<IReadOnlySet<string>> GetFFmpegOptions(string ffmpegPath)
{
var cacheKey = string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, "options");
if (_memoryCache.TryGetValue(cacheKey, out IReadOnlySet<string>? cachedCapabilities) &&
if (memoryCache.TryGetValue(cacheKey, out IReadOnlySet<string>? cachedCapabilities) &&
cachedCapabilities is not null)
{
return cachedCapabilities;
@ -357,15 +362,19 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory @@ -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<IReadOnlySet<string>> GetFFmpegFormats(string ffmpegPath, string muxDemux)
{
var cacheKey = string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, "formats");
if (_memoryCache.TryGetValue(cacheKey, out IReadOnlySet<string>? cachedCapabilities) &&
if (memoryCache.TryGetValue(cacheKey, out IReadOnlySet<string>? cachedCapabilities) &&
cachedCapabilities is not null)
{
return cachedCapabilities;
@ -382,38 +391,38 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory @@ -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<string> 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<string>.None;
}
private static Option<string> 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<string>.None;
}
private static Option<string> 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<string>.None;
}
private static Option<Tuple<string, string>> 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<Tuple<string, string>>.None;
}
@ -428,7 +437,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory @@ -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 @@ -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<VaapiProfileEntrypoint>? profileEntrypoints) &&
if (memoryCache.TryGetValue(cacheKey, out List<VaapiProfileEntrypoint>? profileEntrypoints) &&
profileEntrypoints is not null)
{
return new VaapiHardwareCapabilities(profileEntrypoints, _logger);
return new VaapiHardwareCapabilities(profileEntrypoints, logger);
}
Option<string> 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 @@ -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 @@ -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 @@ -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<VaapiProfileEntrypoint>? profileEntrypoints) &&
if (memoryCache.TryGetValue(cacheKey, out List<VaapiProfileEntrypoint>? 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<string>? vaapiDisplays))
if (!memoryCache.TryGetValue("ffmpeg.vaapi_displays", out List<string>? vaapiDisplays))
{
vaapiDisplays = ["drm"];
}
@ -537,7 +546,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory @@ -537,7 +546,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
Option<string> vaapiOutput = await GetVaapiOutput(vaapiDisplay, Option<string>.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 @@ -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 @@ -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 @@ -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 @@ -583,14 +592,14 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
Option<List<CudaDevice>> 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 @@ -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();
}

2
ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilitiesFactory.cs

@ -4,6 +4,8 @@ namespace ErsatzTV.FFmpeg.Capabilities; @@ -4,6 +4,8 @@ namespace ErsatzTV.FFmpeg.Capabilities;
public interface IHardwareCapabilitiesFactory
{
void ClearCache();
Task<IFFmpegCapabilities> GetFFmpegCapabilities(string ffmpegPath);
Task<IHardwareCapabilities> GetHardwareCapabilities(

5
ErsatzTV/Services/SchedulerService.cs

@ -4,6 +4,7 @@ using Bugsnag; @@ -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 @@ -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 @@ -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);
}

4
ErsatzTV/Services/WorkerService.cs

@ -3,6 +3,7 @@ using System.Threading.Channels; @@ -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 @@ -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;

Loading…
Cancel
Save