Browse Source

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
pull/1474/head
Jason Dove 2 years ago committed by GitHub
parent
commit
c3fe263978
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      CHANGELOG.md
  2. 1
      ErsatzTV.Application/ErsatzTV.Application.csproj
  3. 5
      ErsatzTV.Application/FFmpegProfiles/Queries/GetSupportedHardwareAccelerationKinds.cs
  4. 79
      ErsatzTV.Application/FFmpegProfiles/Queries/GetSupportedHardwareAccelerationKindsHandler.cs
  5. 6
      ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs
  6. 22
      ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs
  7. 1
      ErsatzTV.Application/Troubleshooting/TroubleshootingInfo.cs
  8. 4
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  9. 3
      ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs
  10. 1
      ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs
  11. 18
      ErsatzTV.FFmpeg/Capabilities/FFmpegCapabilities.cs
  12. 158
      ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs
  13. 1
      ErsatzTV.FFmpeg/Capabilities/IFFmpegCapabilities.cs
  14. 4
      ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilitiesFactory.cs
  15. 3
      ErsatzTV.FFmpeg/Capabilities/Qsv/QsvOutput.cs
  16. 9
      ErsatzTV.FFmpeg/OutputFormat/OutputFormatHls.cs
  17. 3
      ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs
  18. 6
      ErsatzTV.FFmpeg/Pipeline/PipelineBuilderFactory.cs
  19. 2
      ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs
  20. 20
      ErsatzTV/Pages/FFmpegEditor.razor
  21. 13
      ErsatzTV/Pages/Troubleshooting.razor

7
CHANGELOG.md

@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -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/). @@ -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/). @@ -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

1
ErsatzTV.Application/ErsatzTV.Application.csproj

@ -14,6 +14,7 @@ @@ -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>

5
ErsatzTV.Application/FFmpegProfiles/Queries/GetSupportedHardwareAccelerationKinds.cs

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.FFmpegProfiles;
public record GetSupportedHardwareAccelerationKinds : IRequest<List<HardwareAccelerationKind>>;

79
ErsatzTV.Application/FFmpegProfiles/Queries/GetSupportedHardwareAccelerationKindsHandler.cs

@ -0,0 +1,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"));
}

6
ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs

@ -11,6 +11,7 @@ using ErsatzTV.Core.Interfaces.FFmpeg; @@ -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; @@ -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 @@ -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 @@ -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 @@ -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(
_ =>
{

22
ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs

@ -7,6 +7,7 @@ using ErsatzTV.Core.FFmpeg; @@ -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 @@ -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 @@ -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 @@ -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 @@ -123,6 +134,7 @@ public class GetTroubleshootingInfoHandler : IRequestHandler<GetTroubleshootingI
activeFFmpegProfiles,
channels,
nvidiaCapabilities,
qsvCapabilities,
vaapiCapabilities);
}

1
ErsatzTV.Application/Troubleshooting/TroubleshootingInfo.cs

@ -10,4 +10,5 @@ public record TroubleshootingInfo( @@ -10,4 +10,5 @@ public record TroubleshootingInfo(
IEnumerable<FFmpegProfile> FFmpegProfiles,
IEnumerable<Channel> Channels,
string NvidiaCapabilities,
string QsvCapabilities,
string VaapiCapabilities);

4
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -480,11 +480,13 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -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),

3
ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs

@ -175,8 +175,7 @@ public static class FFmpegPlaybackSettingsCalculator @@ -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,

1
ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs

@ -494,6 +494,7 @@ public class PipelineBuilderBaseTests @@ -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>())

18
ErsatzTV.FFmpeg/Capabilities/FFmpegCapabilities.cs

@ -5,20 +5,38 @@ namespace ErsatzTV.FFmpeg.Capabilities; @@ -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);

158
ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs

@ -1,10 +1,14 @@ @@ -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 @@ -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 @@ -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)
{
@ -71,6 +103,33 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory @@ -71,6 +103,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)
{
@ -99,13 +158,16 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory @@ -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 @@ -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 @@ -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)

1
ErsatzTV.FFmpeg/Capabilities/IFFmpegCapabilities.cs

@ -4,6 +4,7 @@ namespace ErsatzTV.FFmpeg.Capabilities; @@ -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);

4
ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilitiesFactory.cs

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
using ErsatzTV.FFmpeg.Capabilities.Qsv;
namespace ErsatzTV.FFmpeg.Capabilities;
public interface IHardwareCapabilitiesFactory
@ -13,5 +15,7 @@ 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);
}

3
ErsatzTV.FFmpeg/Capabilities/Qsv/QsvOutput.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.FFmpeg.Capabilities.Qsv;
public record QsvOutput(int ExitCode, string Output);

9
ErsatzTV.FFmpeg/OutputFormat/OutputFormatHls.cs

@ -7,18 +7,21 @@ public class OutputFormatHls : IPipelineStep @@ -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 @@ -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",

3
ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs

@ -274,7 +274,8 @@ public abstract class PipelineBuilderBase : IPipelineBuilder @@ -274,7 +274,8 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
desiredState,
videoStream.FrameRate,
segmentTemplate,
playlistPath));
playlistPath,
ffmpegState.EncoderHardwareAccelerationMode is HardwareAccelerationMode.Qsv));
}
}

6
ErsatzTV.FFmpeg/Pipeline/PipelineBuilderFactory.cs

@ -65,7 +65,7 @@ public class PipelineBuilderFactory : IPipelineBuilderFactory @@ -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 @@ -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 @@ -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,

2
ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs

@ -245,6 +245,7 @@ public class TranscodingTests @@ -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 @@ -861,6 +862,7 @@ public class TranscodingTests
//new FakeNvidiaCapabilitiesFactory(),
new HardwareCapabilitiesFactory(
MemoryCache,
new RuntimeInfo(),
LoggerFactory.CreateLogger<HardwareCapabilitiesFactory>()),
LoggerFactory.CreateLogger<PipelineBuilderFactory>()),
Substitute.For<IConfigElementRepository>(),

20
ErsatzTV/Pages/FFmpegEditor.razor

@ -69,7 +69,7 @@ @@ -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 @@ @@ -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 @@ @@ -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;
@ -195,6 +196,15 @@ @@ -195,6 +196,15 @@
{
_resolutions = restoredResolutions;
}
if (!ApplicationState.TryTakeFromJson("_hardwareAccelerationKinds", out List<HardwareAccelerationKind> restoredHardwareAccelerationKinds))
{
_hardwareAccelerationKinds = await _mediator.Send(new GetSupportedHardwareAccelerationKinds(), _cts.Token);
}
else
{
_hardwareAccelerationKinds = restoredHardwareAccelerationKinds;
}
if (IsEdit)
{
@ -221,6 +231,11 @@ @@ -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 @@ @@ -236,6 +251,7 @@
{
ApplicationState.PersistAsJson("_model", _model);
ApplicationState.PersistAsJson("_resolutions", _resolutions);
ApplicationState.PersistAsJson("_hardwareAccelerationKinds", _hardwareAccelerationKinds);
return Task.CompletedTask;
}

13
ErsatzTV/Pages/Troubleshooting.razor

@ -29,6 +29,16 @@ @@ -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 @@ @@ -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 @@ @@ -73,6 +85,7 @@
});
_nvidiaCapabilities = info.NvidiaCapabilities;
_qsvCapabilities = info.QsvCapabilities;
_vaapiCapabilities = info.VaapiCapabilities;
}
catch (Exception ex)

Loading…
Cancel
Save