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/).
- Include `inputstream.ffmpegdirect` properties in channels.m3u when requested by Kodi - Include `inputstream.ffmpegdirect` properties in channels.m3u when requested by Kodi
- Log playout item title and path when starting a stream - 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 - 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 ### Fixed
- Fix playout bug that caused some schedule items with fixed start times to be pushed to the next day - 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 - Note that ffmpeg is still *always* required for playback to work
- Fix PGS subtitle pixel format with Intel VAAPI - Fix PGS subtitle pixel format with Intel VAAPI
- Fix some cases where `Copy` button would fail to copy to clipboard - 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 ### Changed
- Upgrade ffmpeg to 6.1, which is now *required* for all installs - 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 - 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 - 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 - 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 ## [0.8.2-beta] - 2023-09-14
### Added ### Added

1
ErsatzTV.Application/ErsatzTV.Application.csproj

@ -14,6 +14,7 @@
<PackageReference Include="Humanizer.Core" Version="2.14.1" /> <PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="MediatR" Version="12.1.1" /> <PackageReference Include="MediatR" Version="12.1.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" /> <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"> <PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.7.30">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

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

@ -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 @@
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;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace ErsatzTV.Application.Streaming; namespace ErsatzTV.Application.Streaming;
@ -18,6 +19,7 @@ namespace ErsatzTV.Application.Streaming;
public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Either<BaseError, Unit>> public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Either<BaseError, Unit>>
{ {
private readonly IConfigElementRepository _configElementRepository; private readonly IConfigElementRepository _configElementRepository;
private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly IFFmpegSegmenterService _ffmpegSegmenterService; private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
private readonly IHlsPlaylistFilter _hlsPlaylistFilter; private readonly IHlsPlaylistFilter _hlsPlaylistFilter;
private readonly IServiceScopeFactory _serviceScopeFactory; private readonly IServiceScopeFactory _serviceScopeFactory;
@ -38,6 +40,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
ILogger<HlsSessionWorker> sessionWorkerLogger, ILogger<HlsSessionWorker> sessionWorkerLogger,
IFFmpegSegmenterService ffmpegSegmenterService, IFFmpegSegmenterService ffmpegSegmenterService,
IConfigElementRepository configElementRepository, IConfigElementRepository configElementRepository,
IHostApplicationLifetime hostApplicationLifetime,
ChannelWriter<IBackgroundServiceRequest> workerChannel) ChannelWriter<IBackgroundServiceRequest> workerChannel)
{ {
_hlsPlaylistFilter = hlsPlaylistFilter; _hlsPlaylistFilter = hlsPlaylistFilter;
@ -49,6 +52,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
_sessionWorkerLogger = sessionWorkerLogger; _sessionWorkerLogger = sessionWorkerLogger;
_ffmpegSegmenterService = ffmpegSegmenterService; _ffmpegSegmenterService = ffmpegSegmenterService;
_configElementRepository = configElementRepository; _configElementRepository = configElementRepository;
_hostApplicationLifetime = hostApplicationLifetime;
_workerChannel = workerChannel; _workerChannel = workerChannel;
} }
@ -81,7 +85,7 @@ public class StartFFmpegSessionHandler : IRequestHandler<StartFFmpegSession, Eit
_ffmpegSegmenterService.SessionWorkers.AddOrUpdate(request.ChannelNumber, _ => worker, (_, _) => worker); _ffmpegSegmenterService.SessionWorkers.AddOrUpdate(request.ChannelNumber, _ => worker, (_, _) => worker);
// fire and forget worker // fire and forget worker
_ = worker.Run(request.ChannelNumber, idleTimeout, cancellationToken) _ = worker.Run(request.ChannelNumber, idleTimeout, _hostApplicationLifetime.ApplicationStopping)
.ContinueWith( .ContinueWith(
_ => _ =>
{ {

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

@ -7,6 +7,7 @@ using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Health; using ErsatzTV.Core.Health;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.FFmpeg.Capabilities; using ErsatzTV.FFmpeg.Capabilities;
using ErsatzTV.FFmpeg.Capabilities.Qsv;
using ErsatzTV.FFmpeg.Runtime; using ErsatzTV.FFmpeg.Runtime;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -74,6 +75,7 @@ public class GetTroubleshootingInfoHandler : IRequestHandler<GetTroubleshootingI
.ToList(); .ToList();
string nvidiaCapabilities = null; string nvidiaCapabilities = null;
string qsvCapabilities = null;
string vaapiCapabilities = null; string vaapiCapabilities = null;
Option<ConfigElement> maybeFFmpegPath = Option<ConfigElement> maybeFFmpegPath =
await _configElementRepository.GetConfigElement(ConfigElementKey.FFmpegPath); await _configElementRepository.GetConfigElement(ConfigElementKey.FFmpegPath);
@ -87,6 +89,20 @@ public class GetTroubleshootingInfoHandler : IRequestHandler<GetTroubleshootingI
{ {
nvidiaCapabilities = await _hardwareCapabilitiesFactory.GetNvidiaOutput(ffmpegPath.Value); 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)) if (_runtimeInfo.IsOSPlatform(OSPlatform.Linux))
{ {
var allDrivers = new List<VaapiDriver> var allDrivers = new List<VaapiDriver>
@ -94,11 +110,6 @@ public class GetTroubleshootingInfoHandler : IRequestHandler<GetTroubleshootingI
foreach (VaapiDriver activeDriver in allDrivers) 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 vaapiDevice in vaapiDevices)
{ {
foreach (string output in await _hardwareCapabilitiesFactory.GetVaapiOutput( foreach (string output in await _hardwareCapabilitiesFactory.GetVaapiOutput(
@ -123,6 +134,7 @@ public class GetTroubleshootingInfoHandler : IRequestHandler<GetTroubleshootingI
activeFFmpegProfiles, activeFFmpegProfiles,
channels, channels,
nvidiaCapabilities, nvidiaCapabilities,
qsvCapabilities,
vaapiCapabilities); vaapiCapabilities);
} }

1
ErsatzTV.Application/Troubleshooting/TroubleshootingInfo.cs

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

4
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -480,11 +480,13 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
var videoInputFile = new VideoInputFile(videoPath, new List<VideoStream> { ffmpegVideoStream }); var videoInputFile = new VideoInputFile(videoPath, new List<VideoStream> { ffmpegVideoStream });
// TODO: ignore accel if this already failed once
HardwareAccelerationMode hwAccel = GetHardwareAccelerationMode(playbackSettings, FillerKind.None); HardwareAccelerationMode hwAccel = GetHardwareAccelerationMode(playbackSettings, FillerKind.None);
_logger.LogDebug("HW accel mode: {HwAccel}", hwAccel);
var ffmpegState = new FFmpegState( var ffmpegState = new FFmpegState(
false, false,
hwAccel, HardwareAccelerationMode.None, // no hw accel decode since errors loop
hwAccel, hwAccel,
VaapiDriverName(hwAccel, vaapiDriver), VaapiDriverName(hwAccel, vaapiDriver),
VaapiDeviceName(hwAccel, vaapiDevice), VaapiDeviceName(hwAccel, vaapiDevice),

3
ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs

@ -175,8 +175,7 @@ public static class FFmpegPlaybackSettingsCalculator
bool hlsRealtime) => bool hlsRealtime) =>
new() new()
{ {
// HardwareAcceleration = ffmpegProfile.HardwareAcceleration, HardwareAcceleration = ffmpegProfile.HardwareAcceleration,
HardwareAcceleration = HardwareAccelerationKind.None,
FormatFlags = CommonFormatFlags, FormatFlags = CommonFormatFlags,
VideoFormat = ffmpegProfile.VideoFormat, VideoFormat = ffmpegProfile.VideoFormat,
VideoBitrate = ffmpegProfile.VideoBitrate, VideoBitrate = ffmpegProfile.VideoBitrate,

1
ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs

@ -494,6 +494,7 @@ public class PipelineBuilderBaseTests
{ {
public DefaultFFmpegCapabilities() public DefaultFFmpegCapabilities()
: base( : 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>(), 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;
public class FFmpegCapabilities : IFFmpegCapabilities public class FFmpegCapabilities : IFFmpegCapabilities
{ {
private readonly IReadOnlySet<string> _ffmpegHardwareAccelerations;
private readonly IReadOnlySet<string> _ffmpegDecoders; private readonly IReadOnlySet<string> _ffmpegDecoders;
private readonly IReadOnlySet<string> _ffmpegEncoders; private readonly IReadOnlySet<string> _ffmpegEncoders;
private readonly IReadOnlySet<string> _ffmpegFilters; private readonly IReadOnlySet<string> _ffmpegFilters;
public FFmpegCapabilities( public FFmpegCapabilities(
IReadOnlySet<string> ffmpegHardwareAccelerations,
IReadOnlySet<string> ffmpegDecoders, IReadOnlySet<string> ffmpegDecoders,
IReadOnlySet<string> ffmpegFilters, IReadOnlySet<string> ffmpegFilters,
IReadOnlySet<string> ffmpegEncoders) IReadOnlySet<string> ffmpegEncoders)
{ {
_ffmpegHardwareAccelerations = ffmpegHardwareAccelerations;
_ffmpegDecoders = ffmpegDecoders; _ffmpegDecoders = ffmpegDecoders;
_ffmpegFilters = ffmpegFilters; _ffmpegFilters = ffmpegFilters;
_ffmpegEncoders = ffmpegEncoders; _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 HasDecoder(string decoder) => _ffmpegDecoders.Contains(decoder);
public bool HasEncoder(string encoder) => _ffmpegEncoders.Contains(encoder); public bool HasEncoder(string encoder) => _ffmpegEncoders.Contains(encoder);

158
ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs

@ -1,10 +1,14 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Globalization; using System.Globalization;
using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using CliWrap; using CliWrap;
using CliWrap.Buffered; using CliWrap.Buffered;
using ErsatzTV.FFmpeg.Capabilities.Qsv;
using ErsatzTV.FFmpeg.Capabilities.Vaapi; using ErsatzTV.FFmpeg.Capabilities.Vaapi;
using ErsatzTV.FFmpeg.GlobalOption.HardwareAcceleration;
using ErsatzTV.FFmpeg.Runtime;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -15,24 +19,35 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
private const string ArchitectureCacheKey = "ffmpeg.hardware.nvidia.architecture"; private const string ArchitectureCacheKey = "ffmpeg.hardware.nvidia.architecture";
private const string ModelCacheKey = "ffmpeg.hardware.nvidia.model"; private const string ModelCacheKey = "ffmpeg.hardware.nvidia.model";
private const string VaapiCacheKeyFormat = "ffmpeg.hardware.vaapi.{0}.{1}"; private const string VaapiCacheKeyFormat = "ffmpeg.hardware.vaapi.{0}.{1}";
private const string QsvCacheKeyFormat = "ffmpeg.hardware.qsv.{0}";
private const string FFmpegCapabilitiesCacheKeyFormat = "ffmpeg.{0}"; private const string FFmpegCapabilitiesCacheKeyFormat = "ffmpeg.{0}";
private readonly ILogger<HardwareCapabilitiesFactory> _logger; private readonly ILogger<HardwareCapabilitiesFactory> _logger;
private readonly IMemoryCache _memoryCache; 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; _memoryCache = memoryCache;
_runtimeInfo = runtimeInfo;
_logger = logger; _logger = logger;
} }
public async Task<IFFmpegCapabilities> GetFFmpegCapabilities(string ffmpegPath) public async Task<IFFmpegCapabilities> GetFFmpegCapabilities(string ffmpegPath)
{ {
IReadOnlySet<string> ffmpegDecoders = await GetFFmpegCapabilities(ffmpegPath, "decoders"); // TODO: validate videotoolbox somehow
IReadOnlySet<string> ffmpegFilters = await GetFFmpegCapabilities(ffmpegPath, "filters"); // TODO: validate amf somehow
IReadOnlySet<string> ffmpegEncoders = await GetFFmpegCapabilities(ffmpegPath, "encoders");
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( public async Task<IHardwareCapabilities> GetHardwareCapabilities(
@ -40,14 +55,31 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
string ffmpegPath, string ffmpegPath,
HardwareAccelerationMode hardwareAccelerationMode, HardwareAccelerationMode hardwareAccelerationMode,
Option<string> vaapiDriver, Option<string> vaapiDriver,
Option<string> vaapiDevice) => Option<string> vaapiDevice)
hardwareAccelerationMode switch {
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.Nvenc => await GetNvidiaCapabilities(ffmpegPath, ffmpegCapabilities),
HardwareAccelerationMode.Qsv => await GetQsvCapabilities(ffmpegPath, vaapiDevice),
HardwareAccelerationMode.Vaapi => await GetVaapiCapabilities(vaapiDriver, vaapiDevice), HardwareAccelerationMode.Vaapi => await GetVaapiCapabilities(vaapiDriver, vaapiDevice),
HardwareAccelerationMode.Amf => new AmfHardwareCapabilities(), HardwareAccelerationMode.Amf => new AmfHardwareCapabilities(),
_ => new DefaultHardwareCapabilities() _ => new DefaultHardwareCapabilities()
}; };
}
public async Task<string> GetNvidiaOutput(string ffmpegPath) public async Task<string> GetNvidiaOutput(string ffmpegPath)
{ {
@ -71,6 +103,33 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
return output; 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) public async Task<Option<string>> GetVaapiOutput(Option<string> vaapiDriver, string vaapiDevice)
{ {
@ -99,13 +158,16 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
return result.StandardOutput; 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); var cacheKey = string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, capabilities);
if (_memoryCache.TryGetValue(cacheKey, out IReadOnlySet<string>? cachedDecoders) && if (_memoryCache.TryGetValue(cacheKey, out IReadOnlySet<string>? cachedCapabilities) &&
cachedDecoders is not null) cachedCapabilities is not null)
{ {
return cachedDecoders; return cachedCapabilities;
} }
string[] arguments = { "-hide_banner", $"-{capabilities}" }; string[] arguments = { "-hide_banner", $"-{capabilities}" };
@ -120,10 +182,17 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
: result.StandardOutput; : result.StandardOutput;
return output.Split("\n").Map(s => s.Trim()) return output.Split("\n").Map(s => s.Trim())
.Bind(l => ParseFFmpegLine(l)) .Bind(l => parseLine(l))
.ToImmutableHashSet(); .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) private static Option<string> ParseFFmpegLine(string input)
{ {
const string PATTERN = @"^\s*?[A-Z\.]+\s+(\w+).*"; const string PATTERN = @"^\s*?[A-Z\.]+\s+(\w+).*";
@ -195,6 +264,71 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory
return new NoHardwareCapabilities(); 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( private async Task<IHardwareCapabilities> GetNvidiaCapabilities(
string ffmpegPath, string ffmpegPath,
IFFmpegCapabilities ffmpegCapabilities) IFFmpegCapabilities ffmpegCapabilities)

1
ErsatzTV.FFmpeg/Capabilities/IFFmpegCapabilities.cs

@ -4,6 +4,7 @@ namespace ErsatzTV.FFmpeg.Capabilities;
public interface IFFmpegCapabilities public interface IFFmpegCapabilities
{ {
bool HasHardwareAcceleration(HardwareAccelerationMode hardwareAccelerationMode);
bool HasDecoder(string decoder); bool HasDecoder(string decoder);
bool HasEncoder(string encoder); bool HasEncoder(string encoder);
bool HasFilter(string filter); bool HasFilter(string filter);

4
ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilitiesFactory.cs

@ -1,3 +1,5 @@
using ErsatzTV.FFmpeg.Capabilities.Qsv;
namespace ErsatzTV.FFmpeg.Capabilities; namespace ErsatzTV.FFmpeg.Capabilities;
public interface IHardwareCapabilitiesFactory public interface IHardwareCapabilitiesFactory
@ -13,5 +15,7 @@ public interface IHardwareCapabilitiesFactory
Task<string> GetNvidiaOutput(string ffmpegPath); Task<string> GetNvidiaOutput(string ffmpegPath);
Task<QsvOutput> GetQsvOutput(string ffmpegPath, Option<string> qsvDevice);
Task<Option<string>> GetVaapiOutput(Option<string> vaapiDriver, string vaapiDevice); Task<Option<string>> GetVaapiOutput(Option<string> vaapiDriver, string vaapiDevice);
} }

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

@ -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
private readonly FrameState _desiredState; private readonly FrameState _desiredState;
private readonly Option<string> _mediaFrameRate; private readonly Option<string> _mediaFrameRate;
private readonly string _playlistPath; private readonly string _playlistPath;
private readonly bool _oneSecondGop;
private readonly string _segmentTemplate; private readonly string _segmentTemplate;
public OutputFormatHls( public OutputFormatHls(
FrameState desiredState, FrameState desiredState,
Option<string> mediaFrameRate, Option<string> mediaFrameRate,
string segmentTemplate, string segmentTemplate,
string playlistPath) string playlistPath,
bool oneSecondGop = false)
{ {
_desiredState = desiredState; _desiredState = desiredState;
_mediaFrameRate = mediaFrameRate; _mediaFrameRate = mediaFrameRate;
_segmentTemplate = segmentTemplate; _segmentTemplate = segmentTemplate;
_playlistPath = playlistPath; _playlistPath = playlistPath;
_oneSecondGop = oneSecondGop;
} }
public IList<EnvironmentVariable> EnvironmentVariables => Array.Empty<EnvironmentVariable>(); public IList<EnvironmentVariable> EnvironmentVariables => Array.Empty<EnvironmentVariable>();
@ -33,9 +36,11 @@ public class OutputFormatHls : IPipelineStep
const int SEGMENT_SECONDS = 4; const int SEGMENT_SECONDS = 4;
int frameRate = _desiredState.FrameRate.IfNone(GetFrameRateFromMedia); int frameRate = _desiredState.FrameRate.IfNone(GetFrameRateFromMedia);
int gop = _oneSecondGop ? frameRate : frameRate * SEGMENT_SECONDS;
return new List<string> return new List<string>
{ {
"-g", $"{frameRate * SEGMENT_SECONDS}", "-g", $"{gop}",
"-keyint_min", $"{frameRate * SEGMENT_SECONDS}", "-keyint_min", $"{frameRate * SEGMENT_SECONDS}",
"-force_key_frames", $"expr:gte(t,n_forced*{SEGMENT_SECONDS})", "-force_key_frames", $"expr:gte(t,n_forced*{SEGMENT_SECONDS})",
"-f", "hls", "-f", "hls",

3
ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs

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

6
ErsatzTV.FFmpeg/Pipeline/PipelineBuilderFactory.cs

@ -65,7 +65,7 @@ public class PipelineBuilderFactory : IPipelineBuilderFactory
reportsFolder, reportsFolder,
fontsFolder, fontsFolder,
_logger), _logger),
HardwareAccelerationMode.Qsv => new QsvPipelineBuilder( HardwareAccelerationMode.Qsv when capabilities is not NoHardwareCapabilities => new QsvPipelineBuilder(
ffmpegCapabilities, ffmpegCapabilities,
capabilities, capabilities,
hardwareAccelerationMode, hardwareAccelerationMode,
@ -76,7 +76,7 @@ public class PipelineBuilderFactory : IPipelineBuilderFactory
reportsFolder, reportsFolder,
fontsFolder, fontsFolder,
_logger), _logger),
HardwareAccelerationMode.VideoToolbox => new VideoToolboxPipelineBuilder( HardwareAccelerationMode.VideoToolbox when capabilities is not NoHardwareCapabilities => new VideoToolboxPipelineBuilder(
ffmpegCapabilities, ffmpegCapabilities,
capabilities, capabilities,
hardwareAccelerationMode, hardwareAccelerationMode,
@ -87,7 +87,7 @@ public class PipelineBuilderFactory : IPipelineBuilderFactory
reportsFolder, reportsFolder,
fontsFolder, fontsFolder,
_logger), _logger),
HardwareAccelerationMode.Amf => new AmfPipelineBuilder( HardwareAccelerationMode.Amf when capabilities is not NoHardwareCapabilities => new AmfPipelineBuilder(
ffmpegCapabilities, ffmpegCapabilities,
capabilities, capabilities,
hardwareAccelerationMode, hardwareAccelerationMode,

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

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

20
ErsatzTV/Pages/FFmpegEditor.razor

@ -69,7 +69,7 @@
</MudElement> </MudElement>
<MudElement HtmlTag="div" Class="mt-3"> <MudElement HtmlTag="div" Class="mt-3">
<MudSelect Label="Hardware Acceleration" @bind-Value="_model.HardwareAcceleration" For="@(() => _model.HardwareAcceleration)"> <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> <MudSelectItem Value="@hwAccel">@hwAccel</MudSelectItem>
} }
@ -88,7 +88,7 @@
</MudSelect> </MudSelect>
</MudElement> </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"> <MudElement HtmlTag="div" Class="mt-3">
<MudSelect Disabled="@(_model.HardwareAcceleration != HardwareAccelerationKind.Vaapi && _model.HardwareAcceleration != HardwareAccelerationKind.Qsv)" <MudSelect Disabled="@(_model.HardwareAcceleration != HardwareAccelerationKind.Vaapi && _model.HardwareAcceleration != HardwareAccelerationKind.Qsv)"
@ -167,6 +167,7 @@
private ValidationMessageStore _messageStore; private ValidationMessageStore _messageStore;
private List<ResolutionViewModel> _resolutions = new(); private List<ResolutionViewModel> _resolutions = new();
private List<HardwareAccelerationKind> _hardwareAccelerationKinds = new();
private List<string> _vaapiDevices = new(); private List<string> _vaapiDevices = new();
private PersistingComponentStateSubscription _persistingSubscription; private PersistingComponentStateSubscription _persistingSubscription;
@ -195,6 +196,15 @@
{ {
_resolutions = restoredResolutions; _resolutions = restoredResolutions;
} }
if (!ApplicationState.TryTakeFromJson("_hardwareAccelerationKinds", out List<HardwareAccelerationKind> restoredHardwareAccelerationKinds))
{
_hardwareAccelerationKinds = await _mediator.Send(new GetSupportedHardwareAccelerationKinds(), _cts.Token);
}
else
{
_hardwareAccelerationKinds = restoredHardwareAccelerationKinds;
}
if (IsEdit) if (IsEdit)
{ {
@ -221,6 +231,11 @@
_model = new FFmpegProfileEditViewModel(await _mediator.Send(new NewFFmpegProfile(), _cts.Token)); _model = new FFmpegProfileEditViewModel(await _mediator.Send(new NewFFmpegProfile(), _cts.Token));
} }
if (!_hardwareAccelerationKinds.Contains(_model.HardwareAcceleration))
{
_model.HardwareAcceleration = HardwareAccelerationKind.None;
}
_editContext = new EditContext(_model); _editContext = new EditContext(_model);
_messageStore = new ValidationMessageStore(_editContext); _messageStore = new ValidationMessageStore(_editContext);
@ -236,6 +251,7 @@
{ {
ApplicationState.PersistAsJson("_model", _model); ApplicationState.PersistAsJson("_model", _model);
ApplicationState.PersistAsJson("_resolutions", _resolutions); ApplicationState.PersistAsJson("_resolutions", _resolutions);
ApplicationState.PersistAsJson("_hardwareAccelerationKinds", _hardwareAccelerationKinds);
return Task.CompletedTask; return Task.CompletedTask;
} }

13
ErsatzTV/Pages/Troubleshooting.razor

@ -29,6 +29,16 @@
Copy Copy
</MudButton> </MudButton>
</MudExpansionPanel> </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"> <MudExpansionPanel Text="VAAPI Capabilities">
<div class="overflow-y-scroll" style="max-height: 500px"> <div class="overflow-y-scroll" style="max-height: 500px">
<pre> <pre>
@ -46,9 +56,11 @@
private readonly CancellationTokenSource _cts = new(); private readonly CancellationTokenSource _cts = new();
private string _troubleshootingInfo; private string _troubleshootingInfo;
private string _nvidiaCapabilities; private string _nvidiaCapabilities;
private string _qsvCapabilities;
private string _vaapiCapabilities; private string _vaapiCapabilities;
private ElementReference _troubleshootingView; private ElementReference _troubleshootingView;
private ElementReference _nvidiaView; private ElementReference _nvidiaView;
private ElementReference _qsvView;
private ElementReference _vaapiView; private ElementReference _vaapiView;
public void Dispose() public void Dispose()
@ -73,6 +85,7 @@
}); });
_nvidiaCapabilities = info.NvidiaCapabilities; _nvidiaCapabilities = info.NvidiaCapabilities;
_qsvCapabilities = info.QsvCapabilities;
_vaapiCapabilities = info.VaapiCapabilities; _vaapiCapabilities = info.VaapiCapabilities;
} }
catch (Exception ex) catch (Exception ex)

Loading…
Cancel
Save