validate hardware accel, use hw accel for error messages (#1471)
* only display supported hw accels in ffmpeg profile editor * qsv capability improvements * qsv fixes * update changelog
This commit is contained in:
@@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Include `inputstream.ffmpegdirect` properties in channels.m3u when requested by Kodi
|
||||
- Log playout item title and path when starting a stream
|
||||
- This will help with media server libraries where the URL passed to ffmpeg doesn't indicate which file is streaming
|
||||
- Add QSV Capabilities to Troubleshooting page
|
||||
|
||||
### Fixed
|
||||
- Fix playout bug that caused some schedule items with fixed start times to be pushed to the next day
|
||||
@@ -25,6 +26,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Note that ffmpeg is still *always* required for playback to work
|
||||
- Fix PGS subtitle pixel format with Intel VAAPI
|
||||
- Fix some cases where `Copy` button would fail to copy to clipboard
|
||||
- Fix some cases where ffmpeg process would remain running after properly closing ErsatzTV
|
||||
- Fix QSV HLS segment duration
|
||||
- This behavior caused extremely slow QSV stream starts
|
||||
|
||||
### Changed
|
||||
- Upgrade ffmpeg to 6.1, which is now *required* for all installs
|
||||
@@ -36,6 +40,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Jellyfin collection scanning will no longer happen after every (automatic or forced) library scan
|
||||
- Automatic/periodic scans will check collections one time after all libraries have been scanned
|
||||
- There is a new table in the `Media` > `Libraries` page with a button to manually re-scan Jellyfin collections as needed
|
||||
- In FFmpeg Profile editor, only display hardware acceleration kinds that are supported by the configured ffmpeg
|
||||
- Test QSV acceleration if configured, and fallback to software mode if test fails
|
||||
- Detect QSV capabilities on Linux (supported decoders, encoders)
|
||||
|
||||
## [0.8.2-beta] - 2023-09-14
|
||||
### Added
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
|
||||
<PackageReference Include="MediatR" Version="12.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.7.30">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.FFmpegProfiles;
|
||||
|
||||
public record GetSupportedHardwareAccelerationKinds : IRequest<List<HardwareAccelerationKind>>;
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.FFmpeg;
|
||||
using ErsatzTV.FFmpeg.Capabilities;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.FFmpegProfiles;
|
||||
|
||||
public class
|
||||
GetSupportedHardwareAccelerationKindsHandler : IRequestHandler<GetSupportedHardwareAccelerationKinds,
|
||||
List<HardwareAccelerationKind>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IHardwareCapabilitiesFactory _hardwareCapabilitiesFactory;
|
||||
|
||||
public GetSupportedHardwareAccelerationKindsHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IHardwareCapabilitiesFactory hardwareCapabilitiesFactory)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_hardwareCapabilitiesFactory = hardwareCapabilitiesFactory;
|
||||
}
|
||||
|
||||
public async Task<List<HardwareAccelerationKind>> Handle(
|
||||
GetSupportedHardwareAccelerationKinds request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, string> validation = await Validate(dbContext);
|
||||
|
||||
return await validation.Match(
|
||||
GetHardwareAccelerationKinds,
|
||||
_ => Task.FromResult(new List<HardwareAccelerationKind> { HardwareAccelerationKind.None }));
|
||||
}
|
||||
|
||||
private async Task<List<HardwareAccelerationKind>> GetHardwareAccelerationKinds(string ffmpegPath)
|
||||
{
|
||||
var result = new List<HardwareAccelerationKind> { HardwareAccelerationKind.None };
|
||||
|
||||
IFFmpegCapabilities ffmpegCapabilities = await _hardwareCapabilitiesFactory.GetFFmpegCapabilities(ffmpegPath);
|
||||
|
||||
if (ffmpegCapabilities.HasHardwareAcceleration(HardwareAccelerationMode.Nvenc))
|
||||
{
|
||||
result.Add(HardwareAccelerationKind.Nvenc);
|
||||
}
|
||||
|
||||
if (ffmpegCapabilities.HasHardwareAcceleration(HardwareAccelerationMode.Qsv))
|
||||
{
|
||||
result.Add(HardwareAccelerationKind.Qsv);
|
||||
}
|
||||
|
||||
if (ffmpegCapabilities.HasHardwareAcceleration(HardwareAccelerationMode.Vaapi))
|
||||
{
|
||||
result.Add(HardwareAccelerationKind.Vaapi);
|
||||
}
|
||||
|
||||
if (ffmpegCapabilities.HasHardwareAcceleration(HardwareAccelerationMode.VideoToolbox))
|
||||
{
|
||||
result.Add(HardwareAccelerationKind.VideoToolbox);
|
||||
}
|
||||
|
||||
if (ffmpegCapabilities.HasHardwareAcceleration(HardwareAccelerationMode.Amf))
|
||||
{
|
||||
result.Add(HardwareAccelerationKind.Amf);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static async Task<Validation<BaseError, string>> Validate(TvContext dbContext) =>
|
||||
await FFmpegPathMustExist(dbContext);
|
||||
|
||||
private static Task<Validation<BaseError, string>> FFmpegPathMustExist(TvContext dbContext) =>
|
||||
dbContext.ConfigElements.GetValue<string>(ConfigElementKey.FFmpegPath)
|
||||
.FilterT(File.Exists)
|
||||
.Map(maybePath => maybePath.ToValidation<BaseError>("FFmpeg path does not exist on filesystem"));
|
||||
}
|
||||
@@ -11,6 +11,7 @@ using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.Application.Streaming;
|
||||
@@ -18,6 +19,7 @@ namespace ErsatzTV.Application.Streaming;
|
||||
public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly IHostApplicationLifetime _hostApplicationLifetime;
|
||||
private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
|
||||
private readonly IHlsPlaylistFilter _hlsPlaylistFilter;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
@@ -38,6 +40,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
|
||||
ILogger<HlsSessionWorker> sessionWorkerLogger,
|
||||
IFFmpegSegmenterService ffmpegSegmenterService,
|
||||
IConfigElementRepository configElementRepository,
|
||||
IHostApplicationLifetime hostApplicationLifetime,
|
||||
ChannelWriter<IBackgroundServiceRequest> workerChannel)
|
||||
{
|
||||
_hlsPlaylistFilter = hlsPlaylistFilter;
|
||||
@@ -49,6 +52,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
|
||||
_sessionWorkerLogger = sessionWorkerLogger;
|
||||
_ffmpegSegmenterService = ffmpegSegmenterService;
|
||||
_configElementRepository = configElementRepository;
|
||||
_hostApplicationLifetime = hostApplicationLifetime;
|
||||
_workerChannel = workerChannel;
|
||||
}
|
||||
|
||||
@@ -81,7 +85,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
|
||||
_ffmpegSegmenterService.SessionWorkers.AddOrUpdate(request.ChannelNumber, _ => worker, (_, _) => worker);
|
||||
|
||||
// fire and forget worker
|
||||
_ = worker.Run(request.ChannelNumber, idleTimeout, cancellationToken)
|
||||
_ = worker.Run(request.ChannelNumber, idleTimeout, _hostApplicationLifetime.ApplicationStopping)
|
||||
.ContinueWith(
|
||||
_ =>
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Health;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.FFmpeg.Capabilities;
|
||||
using ErsatzTV.FFmpeg.Capabilities.Qsv;
|
||||
using ErsatzTV.FFmpeg.Runtime;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -74,6 +75,7 @@ public class GetTroubleshootingInfoHandler : IRequestHandler<GetTroubleshootingI
|
||||
.ToList();
|
||||
|
||||
string nvidiaCapabilities = null;
|
||||
string qsvCapabilities = null;
|
||||
string vaapiCapabilities = null;
|
||||
Option<ConfigElement> maybeFFmpegPath =
|
||||
await _configElementRepository.GetConfigElement(ConfigElementKey.FFmpegPath);
|
||||
@@ -87,6 +89,20 @@ public class GetTroubleshootingInfoHandler : IRequestHandler<GetTroubleshootingI
|
||||
{
|
||||
nvidiaCapabilities = await _hardwareCapabilitiesFactory.GetNvidiaOutput(ffmpegPath.Value);
|
||||
|
||||
if (!_memoryCache.TryGetValue("ffmpeg.render_devices", out List<string> vaapiDevices))
|
||||
{
|
||||
vaapiDevices = new List<string> { "/dev/dri/renderD128" };
|
||||
}
|
||||
|
||||
foreach (string qsvDevice in vaapiDevices)
|
||||
{
|
||||
QsvOutput output = await _hardwareCapabilitiesFactory.GetQsvOutput(ffmpegPath.Value, qsvDevice);
|
||||
qsvCapabilities += $"Checking device {qsvDevice}{Environment.NewLine}";
|
||||
qsvCapabilities += $"Exit Code: {output.ExitCode}{Environment.NewLine}{Environment.NewLine}";
|
||||
qsvCapabilities += output.Output;
|
||||
qsvCapabilities += Environment.NewLine + Environment.NewLine;
|
||||
}
|
||||
|
||||
if (_runtimeInfo.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
var allDrivers = new List<VaapiDriver>
|
||||
@@ -94,11 +110,6 @@ public class GetTroubleshootingInfoHandler : IRequestHandler<GetTroubleshootingI
|
||||
|
||||
foreach (VaapiDriver activeDriver in allDrivers)
|
||||
{
|
||||
if (!_memoryCache.TryGetValue("ffmpeg.render_devices", out List<string> vaapiDevices))
|
||||
{
|
||||
vaapiDevices = new List<string> { "/dev/dri/renderD128" };
|
||||
}
|
||||
|
||||
foreach (string vaapiDevice in vaapiDevices)
|
||||
{
|
||||
foreach (string output in await _hardwareCapabilitiesFactory.GetVaapiOutput(
|
||||
@@ -123,6 +134,7 @@ public class GetTroubleshootingInfoHandler : IRequestHandler<GetTroubleshootingI
|
||||
activeFFmpegProfiles,
|
||||
channels,
|
||||
nvidiaCapabilities,
|
||||
qsvCapabilities,
|
||||
vaapiCapabilities);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,4 +10,5 @@ public record TroubleshootingInfo(
|
||||
IEnumerable<FFmpegProfile> FFmpegProfiles,
|
||||
IEnumerable<Channel> Channels,
|
||||
string NvidiaCapabilities,
|
||||
string QsvCapabilities,
|
||||
string VaapiCapabilities);
|
||||
|
||||
@@ -480,11 +480,13 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
||||
|
||||
var videoInputFile = new VideoInputFile(videoPath, new List<VideoStream> { ffmpegVideoStream });
|
||||
|
||||
// TODO: ignore accel if this already failed once
|
||||
HardwareAccelerationMode hwAccel = GetHardwareAccelerationMode(playbackSettings, FillerKind.None);
|
||||
_logger.LogDebug("HW accel mode: {HwAccel}", hwAccel);
|
||||
|
||||
var ffmpegState = new FFmpegState(
|
||||
false,
|
||||
hwAccel,
|
||||
HardwareAccelerationMode.None, // no hw accel decode since errors loop
|
||||
hwAccel,
|
||||
VaapiDriverName(hwAccel, vaapiDriver),
|
||||
VaapiDeviceName(hwAccel, vaapiDevice),
|
||||
|
||||
@@ -175,8 +175,7 @@ public static class FFmpegPlaybackSettingsCalculator
|
||||
bool hlsRealtime) =>
|
||||
new()
|
||||
{
|
||||
// HardwareAcceleration = ffmpegProfile.HardwareAcceleration,
|
||||
HardwareAcceleration = HardwareAccelerationKind.None,
|
||||
HardwareAcceleration = ffmpegProfile.HardwareAcceleration,
|
||||
FormatFlags = CommonFormatFlags,
|
||||
VideoFormat = ffmpegProfile.VideoFormat,
|
||||
VideoBitrate = ffmpegProfile.VideoBitrate,
|
||||
|
||||
@@ -494,6 +494,7 @@ public class PipelineBuilderBaseTests
|
||||
{
|
||||
public DefaultFFmpegCapabilities()
|
||||
: base(
|
||||
new System.Collections.Generic.HashSet<string>(),
|
||||
new System.Collections.Generic.HashSet<string>(),
|
||||
new System.Collections.Generic.HashSet<string>(),
|
||||
new System.Collections.Generic.HashSet<string>())
|
||||
|
||||
@@ -5,20 +5,38 @@ namespace ErsatzTV.FFmpeg.Capabilities;
|
||||
|
||||
public class FFmpegCapabilities : IFFmpegCapabilities
|
||||
{
|
||||
private readonly IReadOnlySet<string> _ffmpegHardwareAccelerations;
|
||||
private readonly IReadOnlySet<string> _ffmpegDecoders;
|
||||
private readonly IReadOnlySet<string> _ffmpegEncoders;
|
||||
private readonly IReadOnlySet<string> _ffmpegFilters;
|
||||
|
||||
public FFmpegCapabilities(
|
||||
IReadOnlySet<string> ffmpegHardwareAccelerations,
|
||||
IReadOnlySet<string> ffmpegDecoders,
|
||||
IReadOnlySet<string> ffmpegFilters,
|
||||
IReadOnlySet<string> ffmpegEncoders)
|
||||
{
|
||||
_ffmpegHardwareAccelerations = ffmpegHardwareAccelerations;
|
||||
_ffmpegDecoders = ffmpegDecoders;
|
||||
_ffmpegFilters = ffmpegFilters;
|
||||
_ffmpegEncoders = ffmpegEncoders;
|
||||
}
|
||||
|
||||
public bool HasHardwareAcceleration(HardwareAccelerationMode hardwareAccelerationMode)
|
||||
{
|
||||
string accelToCheck = hardwareAccelerationMode switch
|
||||
{
|
||||
HardwareAccelerationMode.Amf => "amf",
|
||||
HardwareAccelerationMode.Nvenc => "cuda",
|
||||
HardwareAccelerationMode.Qsv => "qsv",
|
||||
HardwareAccelerationMode.Vaapi => "vaapi",
|
||||
HardwareAccelerationMode.VideoToolbox => "videotoolbox",
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
return !string.IsNullOrWhiteSpace(accelToCheck) && _ffmpegHardwareAccelerations.Contains(accelToCheck);
|
||||
}
|
||||
|
||||
public bool HasDecoder(string decoder) => _ffmpegDecoders.Contains(decoder);
|
||||
|
||||
public bool HasEncoder(string encoder) => _ffmpegEncoders.Contains(encoder);
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using CliWrap;
|
||||
using CliWrap.Buffered;
|
||||
using ErsatzTV.FFmpeg.Capabilities.Qsv;
|
||||
using ErsatzTV.FFmpeg.Capabilities.Vaapi;
|
||||
using ErsatzTV.FFmpeg.GlobalOption.HardwareAcceleration;
|
||||
using ErsatzTV.FFmpeg.Runtime;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -15,24 +19,35 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
|
||||
private const string ArchitectureCacheKey = "ffmpeg.hardware.nvidia.architecture";
|
||||
private const string ModelCacheKey = "ffmpeg.hardware.nvidia.model";
|
||||
private const string VaapiCacheKeyFormat = "ffmpeg.hardware.vaapi.{0}.{1}";
|
||||
private const string QsvCacheKeyFormat = "ffmpeg.hardware.qsv.{0}";
|
||||
private const string FFmpegCapabilitiesCacheKeyFormat = "ffmpeg.{0}";
|
||||
private readonly ILogger<HardwareCapabilitiesFactory> _logger;
|
||||
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly IRuntimeInfo _runtimeInfo;
|
||||
|
||||
public HardwareCapabilitiesFactory(IMemoryCache memoryCache, ILogger<HardwareCapabilitiesFactory> logger)
|
||||
public HardwareCapabilitiesFactory(
|
||||
IMemoryCache memoryCache,
|
||||
IRuntimeInfo runtimeInfo,
|
||||
ILogger<HardwareCapabilitiesFactory> logger)
|
||||
{
|
||||
_memoryCache = memoryCache;
|
||||
_runtimeInfo = runtimeInfo;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IFFmpegCapabilities> GetFFmpegCapabilities(string ffmpegPath)
|
||||
{
|
||||
IReadOnlySet<string> ffmpegDecoders = await GetFFmpegCapabilities(ffmpegPath, "decoders");
|
||||
IReadOnlySet<string> ffmpegFilters = await GetFFmpegCapabilities(ffmpegPath, "filters");
|
||||
IReadOnlySet<string> ffmpegEncoders = await GetFFmpegCapabilities(ffmpegPath, "encoders");
|
||||
// TODO: validate videotoolbox somehow
|
||||
// TODO: validate amf somehow
|
||||
|
||||
return new FFmpegCapabilities(ffmpegDecoders, ffmpegFilters, ffmpegEncoders);
|
||||
IReadOnlySet<string> ffmpegHardwareAccelerations =
|
||||
await GetFFmpegCapabilities(ffmpegPath, "hwaccels", ParseFFmpegAccelLine);
|
||||
IReadOnlySet<string> ffmpegDecoders = await GetFFmpegCapabilities(ffmpegPath, "decoders", ParseFFmpegLine);
|
||||
IReadOnlySet<string> ffmpegFilters = await GetFFmpegCapabilities(ffmpegPath, "filters", ParseFFmpegLine);
|
||||
IReadOnlySet<string> ffmpegEncoders = await GetFFmpegCapabilities(ffmpegPath, "encoders", ParseFFmpegLine);
|
||||
|
||||
return new FFmpegCapabilities(ffmpegHardwareAccelerations, ffmpegDecoders, ffmpegFilters, ffmpegEncoders);
|
||||
}
|
||||
|
||||
public async Task<IHardwareCapabilities> GetHardwareCapabilities(
|
||||
@@ -40,14 +55,31 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
|
||||
string ffmpegPath,
|
||||
HardwareAccelerationMode hardwareAccelerationMode,
|
||||
Option<string> vaapiDriver,
|
||||
Option<string> vaapiDevice) =>
|
||||
hardwareAccelerationMode switch
|
||||
Option<string> vaapiDevice)
|
||||
{
|
||||
if (hardwareAccelerationMode is HardwareAccelerationMode.None)
|
||||
{
|
||||
return new NoHardwareCapabilities();
|
||||
}
|
||||
|
||||
if (!ffmpegCapabilities.HasHardwareAcceleration(hardwareAccelerationMode))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"FFmpeg does not support {HardwareAcceleration} acceleration; will use software mode",
|
||||
hardwareAccelerationMode);
|
||||
|
||||
return new NoHardwareCapabilities();
|
||||
}
|
||||
|
||||
return hardwareAccelerationMode switch
|
||||
{
|
||||
HardwareAccelerationMode.Nvenc => await GetNvidiaCapabilities(ffmpegPath, ffmpegCapabilities),
|
||||
HardwareAccelerationMode.Qsv => await GetQsvCapabilities(ffmpegPath, vaapiDevice),
|
||||
HardwareAccelerationMode.Vaapi => await GetVaapiCapabilities(vaapiDriver, vaapiDevice),
|
||||
HardwareAccelerationMode.Amf => new AmfHardwareCapabilities(),
|
||||
_ => new DefaultHardwareCapabilities()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<string> GetNvidiaOutput(string ffmpegPath)
|
||||
{
|
||||
@@ -72,6 +104,33 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
|
||||
return output;
|
||||
}
|
||||
|
||||
public async Task<QsvOutput> GetQsvOutput(string ffmpegPath, Option<string> qsvDevice)
|
||||
{
|
||||
var option = new QsvHardwareAccelerationOption(qsvDevice);
|
||||
var arguments = option.GlobalOptions.ToList();
|
||||
|
||||
arguments.AddRange(
|
||||
new[]
|
||||
{
|
||||
"-f", "lavfi",
|
||||
"-i", "nullsrc",
|
||||
"-t", "00:00:01",
|
||||
"-c:v", "h264_qsv",
|
||||
"-f", "null", "-"
|
||||
});
|
||||
|
||||
BufferedCommandResult result = await Cli.Wrap(ffmpegPath)
|
||||
.WithArguments(arguments)
|
||||
.WithValidation(CommandResultValidation.None)
|
||||
.ExecuteBufferedAsync(Encoding.UTF8);
|
||||
|
||||
string output = string.IsNullOrWhiteSpace(result.StandardOutput)
|
||||
? result.StandardError
|
||||
: result.StandardOutput;
|
||||
|
||||
return new QsvOutput(result.ExitCode, output);
|
||||
}
|
||||
|
||||
public async Task<Option<string>> GetVaapiOutput(Option<string> vaapiDriver, string vaapiDevice)
|
||||
{
|
||||
BufferedCommandResult whichResult = await Cli.Wrap("which")
|
||||
@@ -99,13 +158,16 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
|
||||
return result.StandardOutput;
|
||||
}
|
||||
|
||||
private async Task<IReadOnlySet<string>> GetFFmpegCapabilities(string ffmpegPath, string capabilities)
|
||||
private async Task<IReadOnlySet<string>> GetFFmpegCapabilities(
|
||||
string ffmpegPath,
|
||||
string capabilities,
|
||||
Func<string, Option<string>> parseLine)
|
||||
{
|
||||
var cacheKey = string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, capabilities);
|
||||
if (_memoryCache.TryGetValue(cacheKey, out IReadOnlySet<string>? cachedDecoders) &&
|
||||
cachedDecoders is not null)
|
||||
if (_memoryCache.TryGetValue(cacheKey, out IReadOnlySet<string>? cachedCapabilities) &&
|
||||
cachedCapabilities is not null)
|
||||
{
|
||||
return cachedDecoders;
|
||||
return cachedCapabilities;
|
||||
}
|
||||
|
||||
string[] arguments = { "-hide_banner", $"-{capabilities}" };
|
||||
@@ -120,10 +182,17 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
|
||||
: result.StandardOutput;
|
||||
|
||||
return output.Split("\n").Map(s => s.Trim())
|
||||
.Bind(l => ParseFFmpegLine(l))
|
||||
.Bind(l => parseLine(l))
|
||||
.ToImmutableHashSet();
|
||||
}
|
||||
|
||||
private static Option<string> ParseFFmpegAccelLine(string input)
|
||||
{
|
||||
const string PATTERN = @"^([\w]+)$";
|
||||
Match match = Regex.Match(input, PATTERN);
|
||||
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+).*";
|
||||
@@ -195,6 +264,71 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
|
||||
return new NoHardwareCapabilities();
|
||||
}
|
||||
|
||||
private async Task<IHardwareCapabilities> GetQsvCapabilities(string ffmpegPath, Option<string> qsvDevice)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_runtimeInfo.IsOSPlatform(OSPlatform.Linux) && qsvDevice.IsNone)
|
||||
{
|
||||
// this shouldn't really happen
|
||||
_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) &&
|
||||
profileEntrypoints is not null)
|
||||
{
|
||||
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");
|
||||
return new NoHardwareCapabilities();
|
||||
}
|
||||
|
||||
if (_runtimeInfo.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
Option<string> vaapiOutput = await GetVaapiOutput(Option<string>.None, device);
|
||||
if (vaapiOutput.IsNone)
|
||||
{
|
||||
_logger.LogWarning("Unable to determine QSV capabilities; please install vainfo");
|
||||
return new DefaultHardwareCapabilities();
|
||||
}
|
||||
|
||||
foreach (string o in vaapiOutput)
|
||||
{
|
||||
profileEntrypoints = VaapiCapabilityParser.ParseFull(o);
|
||||
}
|
||||
|
||||
if (profileEntrypoints?.Any() ?? false)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Detected {Count} VAAPI profile entrypoints for using QSV device {Device}",
|
||||
profileEntrypoints.Count,
|
||||
device);
|
||||
|
||||
_memoryCache.Set(cacheKey, profileEntrypoints);
|
||||
return new VaapiHardwareCapabilities(profileEntrypoints, _logger);
|
||||
}
|
||||
}
|
||||
|
||||
// not sure how to check capabilities on windows
|
||||
return new DefaultHardwareCapabilities();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"Error detecting QSV capabilities; some hardware accelerated features will be unavailable");
|
||||
return new NoHardwareCapabilities();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IHardwareCapabilities> GetNvidiaCapabilities(
|
||||
string ffmpegPath,
|
||||
IFFmpegCapabilities ffmpegCapabilities)
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace ErsatzTV.FFmpeg.Capabilities;
|
||||
|
||||
public interface IFFmpegCapabilities
|
||||
{
|
||||
bool HasHardwareAcceleration(HardwareAccelerationMode hardwareAccelerationMode);
|
||||
bool HasDecoder(string decoder);
|
||||
bool HasEncoder(string encoder);
|
||||
bool HasFilter(string filter);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using ErsatzTV.FFmpeg.Capabilities.Qsv;
|
||||
|
||||
namespace ErsatzTV.FFmpeg.Capabilities;
|
||||
|
||||
public interface IHardwareCapabilitiesFactory
|
||||
@@ -13,5 +15,7 @@ public interface IHardwareCapabilitiesFactory
|
||||
|
||||
Task<string> GetNvidiaOutput(string ffmpegPath);
|
||||
|
||||
Task<QsvOutput> GetQsvOutput(string ffmpegPath, Option<string> qsvDevice);
|
||||
|
||||
Task<Option<string>> GetVaapiOutput(Option<string> vaapiDriver, string vaapiDevice);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.FFmpeg.Capabilities.Qsv;
|
||||
|
||||
public record QsvOutput(int ExitCode, string Output);
|
||||
@@ -7,18 +7,21 @@ public class OutputFormatHls : IPipelineStep
|
||||
private readonly FrameState _desiredState;
|
||||
private readonly Option<string> _mediaFrameRate;
|
||||
private readonly string _playlistPath;
|
||||
private readonly bool _oneSecondGop;
|
||||
private readonly string _segmentTemplate;
|
||||
|
||||
public OutputFormatHls(
|
||||
FrameState desiredState,
|
||||
Option<string> mediaFrameRate,
|
||||
string segmentTemplate,
|
||||
string playlistPath)
|
||||
string playlistPath,
|
||||
bool oneSecondGop = false)
|
||||
{
|
||||
_desiredState = desiredState;
|
||||
_mediaFrameRate = mediaFrameRate;
|
||||
_segmentTemplate = segmentTemplate;
|
||||
_playlistPath = playlistPath;
|
||||
_oneSecondGop = oneSecondGop;
|
||||
}
|
||||
|
||||
public IList<EnvironmentVariable> EnvironmentVariables => Array.Empty<EnvironmentVariable>();
|
||||
@@ -33,9 +36,11 @@ public class OutputFormatHls : IPipelineStep
|
||||
const int SEGMENT_SECONDS = 4;
|
||||
int frameRate = _desiredState.FrameRate.IfNone(GetFrameRateFromMedia);
|
||||
|
||||
int gop = _oneSecondGop ? frameRate : frameRate * SEGMENT_SECONDS;
|
||||
|
||||
return new List<string>
|
||||
{
|
||||
"-g", $"{frameRate * SEGMENT_SECONDS}",
|
||||
"-g", $"{gop}",
|
||||
"-keyint_min", $"{frameRate * SEGMENT_SECONDS}",
|
||||
"-force_key_frames", $"expr:gte(t,n_forced*{SEGMENT_SECONDS})",
|
||||
"-f", "hls",
|
||||
|
||||
@@ -274,7 +274,8 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
|
||||
desiredState,
|
||||
videoStream.FrameRate,
|
||||
segmentTemplate,
|
||||
playlistPath));
|
||||
playlistPath,
|
||||
ffmpegState.EncoderHardwareAccelerationMode is HardwareAccelerationMode.Qsv));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ public class PipelineBuilderFactory : IPipelineBuilderFactory
|
||||
reportsFolder,
|
||||
fontsFolder,
|
||||
_logger),
|
||||
HardwareAccelerationMode.Qsv => new QsvPipelineBuilder(
|
||||
HardwareAccelerationMode.Qsv when capabilities is not NoHardwareCapabilities => new QsvPipelineBuilder(
|
||||
ffmpegCapabilities,
|
||||
capabilities,
|
||||
hardwareAccelerationMode,
|
||||
@@ -76,7 +76,7 @@ public class PipelineBuilderFactory : IPipelineBuilderFactory
|
||||
reportsFolder,
|
||||
fontsFolder,
|
||||
_logger),
|
||||
HardwareAccelerationMode.VideoToolbox => new VideoToolboxPipelineBuilder(
|
||||
HardwareAccelerationMode.VideoToolbox when capabilities is not NoHardwareCapabilities => new VideoToolboxPipelineBuilder(
|
||||
ffmpegCapabilities,
|
||||
capabilities,
|
||||
hardwareAccelerationMode,
|
||||
@@ -87,7 +87,7 @@ public class PipelineBuilderFactory : IPipelineBuilderFactory
|
||||
reportsFolder,
|
||||
fontsFolder,
|
||||
_logger),
|
||||
HardwareAccelerationMode.Amf => new AmfPipelineBuilder(
|
||||
HardwareAccelerationMode.Amf when capabilities is not NoHardwareCapabilities => new AmfPipelineBuilder(
|
||||
ffmpegCapabilities,
|
||||
capabilities,
|
||||
hardwareAccelerationMode,
|
||||
|
||||
@@ -245,6 +245,7 @@ public class TranscodingTests
|
||||
//new FakeNvidiaCapabilitiesFactory(),
|
||||
new HardwareCapabilitiesFactory(
|
||||
MemoryCache,
|
||||
new RuntimeInfo(),
|
||||
LoggerFactory.CreateLogger<HardwareCapabilitiesFactory>()),
|
||||
LoggerFactory.CreateLogger<PipelineBuilderFactory>()),
|
||||
Substitute.For<IConfigElementRepository>(),
|
||||
@@ -861,6 +862,7 @@ public class TranscodingTests
|
||||
//new FakeNvidiaCapabilitiesFactory(),
|
||||
new HardwareCapabilitiesFactory(
|
||||
MemoryCache,
|
||||
new RuntimeInfo(),
|
||||
LoggerFactory.CreateLogger<HardwareCapabilitiesFactory>()),
|
||||
LoggerFactory.CreateLogger<PipelineBuilderFactory>()),
|
||||
Substitute.For<IConfigElementRepository>(),
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
</MudElement>
|
||||
<MudElement HtmlTag="div" Class="mt-3">
|
||||
<MudSelect Label="Hardware Acceleration" @bind-Value="_model.HardwareAcceleration" For="@(() => _model.HardwareAcceleration)">
|
||||
@foreach (HardwareAccelerationKind hwAccel in Enum.GetValues<HardwareAccelerationKind>())
|
||||
@foreach (HardwareAccelerationKind hwAccel in _hardwareAccelerationKinds)
|
||||
{
|
||||
<MudSelectItem Value="@hwAccel">@hwAccel</MudSelectItem>
|
||||
}
|
||||
@@ -88,7 +88,7 @@
|
||||
</MudSelect>
|
||||
</MudElement>
|
||||
}
|
||||
@if (_model.HardwareAcceleration == HardwareAccelerationKind.Vaapi || _model.HardwareAcceleration == HardwareAccelerationKind.Qsv)
|
||||
@if (_model.HardwareAcceleration is HardwareAccelerationKind.Vaapi or HardwareAccelerationKind.Qsv)
|
||||
{
|
||||
<MudElement HtmlTag="div" Class="mt-3">
|
||||
<MudSelect Disabled="@(_model.HardwareAcceleration != HardwareAccelerationKind.Vaapi && _model.HardwareAcceleration != HardwareAccelerationKind.Qsv)"
|
||||
@@ -167,6 +167,7 @@
|
||||
private ValidationMessageStore _messageStore;
|
||||
|
||||
private List<ResolutionViewModel> _resolutions = new();
|
||||
private List<HardwareAccelerationKind> _hardwareAccelerationKinds = new();
|
||||
private List<string> _vaapiDevices = new();
|
||||
private PersistingComponentStateSubscription _persistingSubscription;
|
||||
|
||||
@@ -196,6 +197,15 @@
|
||||
_resolutions = restoredResolutions;
|
||||
}
|
||||
|
||||
if (!ApplicationState.TryTakeFromJson("_hardwareAccelerationKinds", out List<HardwareAccelerationKind> restoredHardwareAccelerationKinds))
|
||||
{
|
||||
_hardwareAccelerationKinds = await _mediator.Send(new GetSupportedHardwareAccelerationKinds(), _cts.Token);
|
||||
}
|
||||
else
|
||||
{
|
||||
_hardwareAccelerationKinds = restoredHardwareAccelerationKinds;
|
||||
}
|
||||
|
||||
if (IsEdit)
|
||||
{
|
||||
if (!ApplicationState.TryTakeFromJson("_model", out FFmpegProfileEditViewModel restoredProfile))
|
||||
@@ -221,6 +231,11 @@
|
||||
_model = new FFmpegProfileEditViewModel(await _mediator.Send(new NewFFmpegProfile(), _cts.Token));
|
||||
}
|
||||
|
||||
if (!_hardwareAccelerationKinds.Contains(_model.HardwareAcceleration))
|
||||
{
|
||||
_model.HardwareAcceleration = HardwareAccelerationKind.None;
|
||||
}
|
||||
|
||||
_editContext = new EditContext(_model);
|
||||
_messageStore = new ValidationMessageStore(_editContext);
|
||||
|
||||
@@ -236,6 +251,7 @@
|
||||
{
|
||||
ApplicationState.PersistAsJson("_model", _model);
|
||||
ApplicationState.PersistAsJson("_resolutions", _resolutions);
|
||||
ApplicationState.PersistAsJson("_hardwareAccelerationKinds", _hardwareAccelerationKinds);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,16 @@
|
||||
Copy
|
||||
</MudButton>
|
||||
</MudExpansionPanel>
|
||||
<MudExpansionPanel Text="QSV Capabilities" Class="mb-6">
|
||||
<div class="overflow-y-scroll" style="max-height: 500px">
|
||||
<pre>
|
||||
<code @ref="_qsvView">@_qsvCapabilities</code>
|
||||
</pre>
|
||||
</div>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="mt-4" OnClick="() => CopyToClipboard(_qsvView)">
|
||||
Copy
|
||||
</MudButton>
|
||||
</MudExpansionPanel>
|
||||
<MudExpansionPanel Text="VAAPI Capabilities">
|
||||
<div class="overflow-y-scroll" style="max-height: 500px">
|
||||
<pre>
|
||||
@@ -46,9 +56,11 @@
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private string _troubleshootingInfo;
|
||||
private string _nvidiaCapabilities;
|
||||
private string _qsvCapabilities;
|
||||
private string _vaapiCapabilities;
|
||||
private ElementReference _troubleshootingView;
|
||||
private ElementReference _nvidiaView;
|
||||
private ElementReference _qsvView;
|
||||
private ElementReference _vaapiView;
|
||||
|
||||
public void Dispose()
|
||||
@@ -73,6 +85,7 @@
|
||||
});
|
||||
|
||||
_nvidiaCapabilities = info.NvidiaCapabilities;
|
||||
_qsvCapabilities = info.QsvCapabilities;
|
||||
_vaapiCapabilities = info.VaapiCapabilities;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
Reference in New Issue
Block a user