diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5c3dc90d7..9f4ab92b3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Scripts live in config / scripts / mpegts
- Each script gets its own subfolder which contains an `mpegts.yml` definition and corresponding windows (powershell) and linux (bash) scripts
- The global MPEG-TS script can be configured in **Settings** > **FFmpeg** > **Default MPEG-TS Script**
+- Add `.avs` AviSynth Script support to all local libraries
+ - `.avs` was added as a valid extension, so they should behave the same any other video file
+ - There are two requirements for AviSynth Scripts to work:
+ - FFmpeg needs to be compiled with AviSynth support (not currently available in Docker)
+ - AviSynth itself needs to be installed
### Fixed
- Fix HLS Direct playback with Jellyfin 10.11
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..7f644ae06
--- /dev/null
+++ b/ErsatzTV.Application/FFmpeg/Commands/RefreshFFmpegCapabilitiesHandler.cs
@@ -0,0 +1,45 @@
+using ErsatzTV.Core;
+using ErsatzTV.Core.Domain;
+using ErsatzTV.Core.Interfaces.Metadata;
+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,
+ ILocalStatisticsProvider localStatisticsProvider)
+ : 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);
+
+ Option maybeFFprobePath = await dbContext.ConfigElements
+ .GetValue(ConfigElementKey.FFprobePath, cancellationToken)
+ .FilterT(File.Exists);
+
+ foreach (string ffprobePath in maybeFFprobePath)
+ {
+ Either result = await localStatisticsProvider.GetStatistics(
+ ffprobePath,
+ Path.Combine(FileSystemLayout.ResourcesCacheFolder, "test.avs"));
+
+ hardwareCapabilitiesFactory.SetAviSynthInstalled(result.IsRight);
+ }
+ }
+ }
+}
diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs
index fc40c28c1..e49eac0bc 100644
--- a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs
+++ b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs
@@ -1,6 +1,7 @@
using System.Diagnostics;
using System.Globalization;
using System.Threading.Channels;
+using ErsatzTV.Application.FFmpeg;
using ErsatzTV.Application.Subtitles;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
@@ -9,22 +10,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 +35,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 +62,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 +86,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);
+ await workerChannel.WriteAsync(new RefreshFFmpegCapabilities(), cancellationToken);
+
return Unit.Default;
}
}
diff --git a/ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs b/ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs
index 9610b88f2..63a93a451 100644
--- a/ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs
+++ b/ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs
@@ -73,6 +73,9 @@ public class GetTroubleshootingInfoHandler : IRequestHandler FFmpegProfiles,
List Channels,
List Watermarks,
+ bool AviSynthDemuxer,
+ bool AviSynthInstalled,
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..92ed9521c 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,9 +72,9 @@ 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.Raw or VideoFormat.RawVideo => new DecoderRawVideo(),
VideoFormat.Undetermined => new DecoderImplicit(),
VideoFormat.Copy => new DecoderImplicit(),
VideoFormat.GeneratedImage => 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..c0b4e4d51 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)
@@ -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(
@@ -95,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);
@@ -107,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(),
@@ -148,13 +152,13 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
// if we don't have a list of cuda devices, fall back to ffmpeg check
string[] arguments =
- {
+ [
"-f", "lavfi",
"-i", "nullsrc",
"-c:v", "h264_nvenc",
"-gpu", "list",
"-f", "null", "-"
- };
+ ];
BufferedCommandResult result = await Cli.Wrap(ffmpegPath)
.WithArguments(arguments)
@@ -288,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);
}
@@ -303,7 +308,27 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
return result;
}
- public List GetVideoToolboxEncoders() => VideoToolboxUtil.GetAvailableEncoders(_logger);
+ public List GetVideoToolboxEncoders() => VideoToolboxUtil.GetAvailableEncoders(logger);
+
+ public void SetAviSynthInstalled(bool aviSynthInstalled)
+ {
+ var cacheKey = string.Format(
+ CultureInfo.InvariantCulture,
+ FFmpegCapabilitiesCacheKeyFormat,
+ "avisynth_installed");
+
+ memoryCache.Set(cacheKey, aviSynthInstalled);
+ }
+
+ public bool IsAviSynthInstalled()
+ {
+ var cacheKey = string.Format(
+ CultureInfo.InvariantCulture,
+ FFmpegCapabilitiesCacheKeyFormat,
+ "avisynth_installed");
+
+ return memoryCache.TryGetValue(cacheKey, out bool installed) && installed;
+ }
private async Task> GetFFmpegCapabilities(
string ffmpegPath,
@@ -311,13 +336,13 @@ 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;
}
- string[] arguments = { "-hide_banner", $"-{capabilities}" };
+ string[] arguments = ["-hide_banner", $"-{capabilities}"];
BufferedCommandResult result = await Cli.Wrap(ffmpegPath)
.WithArguments(arguments)
@@ -328,21 +353,25 @@ 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;
}
- string[] arguments = { "-hide_banner", "-h", "long" };
+ string[] arguments = ["-hide_banner", "-h", "long"];
BufferedCommandResult result = await Cli.Wrap(ffmpegPath)
.WithArguments(arguments)
@@ -353,32 +382,70 @@ 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) &&
+ 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;
+
+ 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)
+ {
+ Match match = FormatRegex().Match(input);
+ return match.Success ? Tuple(match.Groups[1].Value, match.Groups[2].Value) : Option>.None;
+ }
+
private async Task GetVaapiCapabilities(
Option vaapiDisplay,
Option vaapiDriver,
@@ -390,7 +457,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
{
// this shouldn't really happen
- _logger.LogError(
+ logger.LogError(
"Cannot detect VAAPI capabilities without device {Device}",
vaapiDevice);
@@ -402,16 +469,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();
}
@@ -424,7 +491,7 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
{
if (display == "drm")
{
- _logger.LogDebug(
+ logger.LogDebug(
"Detected {Count} VAAPI profile entrypoints using {Driver} {Device}",
profileEntrypoints.Count,
driver,
@@ -432,26 +499,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();
@@ -461,32 +528,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"];
}
@@ -499,7 +566,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();
}
@@ -510,13 +577,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);
}
}
}
@@ -526,7 +593,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();
@@ -535,9 +602,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
@@ -545,14 +612,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)
@@ -560,9 +627,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/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.FFmpeg/Capabilities/IHardwareCapabilitiesFactory.cs b/ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilitiesFactory.cs
index baf6a5756..c7aef1a67 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(
@@ -29,4 +31,8 @@ public interface IHardwareCapabilitiesFactory
List GetVideoToolboxDecoders();
List GetVideoToolboxEncoders();
+
+ void SetAviSynthInstalled(bool aviSynthInstalled);
+
+ bool IsAviSynthInstalled();
}
diff --git a/ErsatzTV.FFmpeg/Format/VideoFormat.cs b/ErsatzTV.FFmpeg/Format/VideoFormat.cs
index 76b3df7ac..591b01f07 100644
--- a/ErsatzTV.FFmpeg/Format/VideoFormat.cs
+++ b/ErsatzTV.FFmpeg/Format/VideoFormat.cs
@@ -15,6 +15,7 @@ public static class VideoFormat
public const string Av1 = "av1";
public const string MpegTs = "mpegts";
public const string Raw = "raw";
+ public const string RawVideo = "rawvideo";
public const string Copy = "copy";
public const string GeneratedImage = "generated-image";
diff --git a/ErsatzTV.Infrastructure.Tests/Metadata/LocalStatisticsProviderTests.cs b/ErsatzTV.Infrastructure.Tests/Metadata/LocalStatisticsProviderTests.cs
index 9a0e78ef2..038670d68 100644
--- a/ErsatzTV.Infrastructure.Tests/Metadata/LocalStatisticsProviderTests.cs
+++ b/ErsatzTV.Infrastructure.Tests/Metadata/LocalStatisticsProviderTests.cs
@@ -2,6 +2,7 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
+using ErsatzTV.FFmpeg.Capabilities;
using ErsatzTV.Infrastructure.Metadata;
using Microsoft.Extensions.Logging;
using NSubstitute;
@@ -22,6 +23,7 @@ public class LocalStatisticsProviderTests
Substitute.For(),
Substitute.For(),
Substitute.For(),
+ Substitute.For(),
Substitute.For>());
var input = new LocalStatisticsProvider.FFprobe(
diff --git a/ErsatzTV.Infrastructure/Metadata/LocalStatisticsProvider.cs b/ErsatzTV.Infrastructure/Metadata/LocalStatisticsProvider.cs
index 4da341eaa..cd14d2218 100644
--- a/ErsatzTV.Infrastructure/Metadata/LocalStatisticsProvider.cs
+++ b/ErsatzTV.Infrastructure/Metadata/LocalStatisticsProvider.cs
@@ -11,6 +11,7 @@ using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
+using ErsatzTV.FFmpeg.Capabilities;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using File = TagLib.File;
@@ -20,6 +21,7 @@ namespace ErsatzTV.Infrastructure.Metadata;
public class LocalStatisticsProvider : ILocalStatisticsProvider
{
private readonly IClient _client;
+ private readonly IHardwareCapabilitiesFactory _hardwareCapabilitiesFactory;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger _logger;
private readonly IMetadataRepository _metadataRepository;
@@ -28,11 +30,13 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
IMetadataRepository metadataRepository,
ILocalFileSystem localFileSystem,
IClient client,
+ IHardwareCapabilitiesFactory hardwareCapabilitiesFactory,
ILogger logger)
{
_metadataRepository = metadataRepository;
_localFileSystem = localFileSystem;
_client = client;
+ _hardwareCapabilitiesFactory = hardwareCapabilitiesFactory;
_logger = logger;
}
@@ -52,6 +56,12 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
try
{
string filePath = await PathForMediaItem(mediaItem);
+
+ if (Path.GetExtension(filePath) == ".avs" && !_hardwareCapabilitiesFactory.IsAviSynthInstalled())
+ {
+ return BaseError.New(".avs files are not supported; compatible ffmpeg and avisynth are both required");
+ }
+
return await RefreshStatistics(ffmpegPath, ffprobePath, mediaItem, filePath);
}
catch (Exception ex)
diff --git a/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs
index cf3bdfc37..18127ca6c 100644
--- a/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs
+++ b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs
@@ -352,6 +352,7 @@ public class TranscodingTests
metadataRepository,
new LocalFileSystem(Substitute.For(), LoggerFactory.CreateLogger()),
Substitute.For(),
+ Substitute.For(),
LoggerFactory.CreateLogger());
await localStatisticsProvider.RefreshStatistics(ExecutableName("ffmpeg"), ExecutableName("ffprobe"), song);
@@ -500,6 +501,7 @@ public class TranscodingTests
metadataRepository,
new LocalFileSystem(Substitute.For(), LoggerFactory.CreateLogger()),
Substitute.For(),
+ Substitute.For(),
LoggerFactory.CreateLogger());
await localStatisticsProvider.RefreshStatistics(
diff --git a/ErsatzTV.Scanner/Application/FFmpeg/Commands/RefreshFFmpegCapabilities.cs b/ErsatzTV.Scanner/Application/FFmpeg/Commands/RefreshFFmpegCapabilities.cs
new file mode 100644
index 000000000..b9804e8ce
--- /dev/null
+++ b/ErsatzTV.Scanner/Application/FFmpeg/Commands/RefreshFFmpegCapabilities.cs
@@ -0,0 +1,3 @@
+namespace ErsatzTV.Scanner.Application.FFmpeg;
+
+public record RefreshFFmpegCapabilities : IRequest;
diff --git a/ErsatzTV.Scanner/Application/FFmpeg/Commands/RefreshFFmpegCapabilitiesHandler.cs b/ErsatzTV.Scanner/Application/FFmpeg/Commands/RefreshFFmpegCapabilitiesHandler.cs
new file mode 100644
index 000000000..918415e07
--- /dev/null
+++ b/ErsatzTV.Scanner/Application/FFmpeg/Commands/RefreshFFmpegCapabilitiesHandler.cs
@@ -0,0 +1,45 @@
+using ErsatzTV.Core;
+using ErsatzTV.Core.Domain;
+using ErsatzTV.Core.Interfaces.Metadata;
+using ErsatzTV.FFmpeg.Capabilities;
+using ErsatzTV.Infrastructure.Data;
+using ErsatzTV.Infrastructure.Extensions;
+using Microsoft.EntityFrameworkCore;
+
+namespace ErsatzTV.Scanner.Application.FFmpeg;
+
+public class RefreshFFmpegCapabilitiesHandler(
+ IDbContextFactory dbContextFactory,
+ IHardwareCapabilitiesFactory hardwareCapabilitiesFactory,
+ ILocalStatisticsProvider localStatisticsProvider)
+ : 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);
+
+ Option maybeFFprobePath = await dbContext.ConfigElements
+ .GetValue(ConfigElementKey.FFprobePath, cancellationToken)
+ .FilterT(File.Exists);
+
+ foreach (string ffprobePath in maybeFFprobePath)
+ {
+ Either result = await localStatisticsProvider.GetStatistics(
+ ffprobePath,
+ Path.Combine(FileSystemLayout.ResourcesCacheFolder, "test.avs"));
+
+ hardwareCapabilitiesFactory.SetAviSynthInstalled(result.IsRight);
+ }
+ }
+ }
+}
diff --git a/ErsatzTV.Scanner/Core/Metadata/LocalFolderScanner.cs b/ErsatzTV.Scanner/Core/Metadata/LocalFolderScanner.cs
index 2c4cd76e4..02e9ca7ef 100644
--- a/ErsatzTV.Scanner/Core/Metadata/LocalFolderScanner.cs
+++ b/ErsatzTV.Scanner/Core/Metadata/LocalFolderScanner.cs
@@ -19,7 +19,7 @@ public abstract class LocalFolderScanner
{
public static readonly ImmutableHashSet VideoFileExtensions = new[]
{
- ".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".ogg", ".ogv", ".mp4",
+ ".avs", ".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".ogg", ".ogv", ".mp4",
".m4p", ".m4v", ".avi", ".wmv", ".mov", ".mkv", ".m2ts", ".ts", ".webm"
}.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
diff --git a/ErsatzTV.Scanner/ErsatzTV.Scanner.csproj.DotSettings b/ErsatzTV.Scanner/ErsatzTV.Scanner.csproj.DotSettings
index a21ca000b..e89e1b47b 100644
--- a/ErsatzTV.Scanner/ErsatzTV.Scanner.csproj.DotSettings
+++ b/ErsatzTV.Scanner/ErsatzTV.Scanner.csproj.DotSettings
@@ -1,5 +1,6 @@
True
+ True
True
True
True
\ No newline at end of file
diff --git a/ErsatzTV.Scanner/Program.cs b/ErsatzTV.Scanner/Program.cs
index 7f608e2ad..500c2abd7 100644
--- a/ErsatzTV.Scanner/Program.cs
+++ b/ErsatzTV.Scanner/Program.cs
@@ -17,6 +17,7 @@ using ErsatzTV.Core.Jellyfin;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Core.Plex;
using ErsatzTV.Core.Search;
+using ErsatzTV.FFmpeg.Capabilities;
using ErsatzTV.FFmpeg.Runtime;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Data.Repositories;
@@ -207,6 +208,7 @@ public class Program
services.AddScoped();
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
services.AddScoped();
services.AddScoped();
diff --git a/ErsatzTV.Scanner/Worker.cs b/ErsatzTV.Scanner/Worker.cs
index 123b385dd..1a79ec136 100644
--- a/ErsatzTV.Scanner/Worker.cs
+++ b/ErsatzTV.Scanner/Worker.cs
@@ -1,6 +1,7 @@
using System.CommandLine;
using System.Diagnostics;
using ErsatzTV.Scanner.Application.Emby;
+using ErsatzTV.Scanner.Application.FFmpeg;
using ErsatzTV.Scanner.Application.Jellyfin;
using ErsatzTV.Scanner.Application.MediaSources;
using ErsatzTV.Scanner.Application.Plex;
@@ -28,6 +29,10 @@ public class Worker : BackgroundService
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
+ using IServiceScope scope = _serviceScopeFactory.CreateScope();
+ IMediator mediator = scope.ServiceProvider.GetRequiredService();
+ await mediator.Send(new RefreshFFmpegCapabilities(), stoppingToken);
+
RootCommand rootCommand = ConfigureCommandLine();
// need to strip program name (head) from command line args
diff --git a/ErsatzTV/ErsatzTV.csproj b/ErsatzTV/ErsatzTV.csproj
index fd0ae2f8b..283ca9d02 100644
--- a/ErsatzTV/ErsatzTV.csproj
+++ b/ErsatzTV/ErsatzTV.csproj
@@ -104,6 +104,7 @@
+
diff --git a/ErsatzTV/Pages/Troubleshooting/Troubleshooting.razor b/ErsatzTV/Pages/Troubleshooting/Troubleshooting.razor
index f3208d82e..b0cf038a0 100644
--- a/ErsatzTV/Pages/Troubleshooting/Troubleshooting.razor
+++ b/ErsatzTV/Pages/Troubleshooting/Troubleshooting.razor
@@ -157,6 +157,11 @@
info.VideoControllers,
info.Health,
info.FFmpegSettings,
+ AviSynth = new
+ {
+ Demuxer = info.AviSynthDemuxer,
+ Installed = info.AviSynthInstalled
+ },
info.Channels,
info.FFmpegProfiles
},
diff --git a/ErsatzTV/Resources/test.avs b/ErsatzTV/Resources/test.avs
new file mode 100644
index 000000000..5e656113d
--- /dev/null
+++ b/ErsatzTV/Resources/test.avs
@@ -0,0 +1,2 @@
+ColorBars(width=640, height=360, pixel_type="YV12")
+Trim(0, 299)
diff --git a/ErsatzTV/Services/RunOnce/ResourceExtractorService.cs b/ErsatzTV/Services/RunOnce/ResourceExtractorService.cs
index e5e02b09c..e95f9075f 100644
--- a/ErsatzTV/Services/RunOnce/ResourceExtractorService.cs
+++ b/ErsatzTV/Services/RunOnce/ResourceExtractorService.cs
@@ -26,6 +26,7 @@ public class ResourceExtractorService : BackgroundService
await ExtractResource(assembly, "ErsatzTV.png", stoppingToken);
await ExtractResource(assembly, "sequential-schedule.schema.json", stoppingToken);
await ExtractResource(assembly, "sequential-schedule-import.schema.json", stoppingToken);
+ await ExtractResource(assembly, "test.avs", stoppingToken);
await ExtractFontResource(assembly, "Sen.ttf", stoppingToken);
await ExtractFontResource(assembly, "Roboto-Regular.ttf", stoppingToken);
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;