diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f5b67151..4814860f7 100644 --- a/CHANGELOG.md +++ b/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 - 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 diff --git a/ErsatzTV.Application/ErsatzTV.Application.csproj b/ErsatzTV.Application/ErsatzTV.Application.csproj index 495d72b5e..11f101c74 100644 --- a/ErsatzTV.Application/ErsatzTV.Application.csproj +++ b/ErsatzTV.Application/ErsatzTV.Application.csproj @@ -14,6 +14,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/ErsatzTV.Application/FFmpegProfiles/Queries/GetSupportedHardwareAccelerationKinds.cs b/ErsatzTV.Application/FFmpegProfiles/Queries/GetSupportedHardwareAccelerationKinds.cs new file mode 100644 index 000000000..7489a4865 --- /dev/null +++ b/ErsatzTV.Application/FFmpegProfiles/Queries/GetSupportedHardwareAccelerationKinds.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Core.Domain; + +namespace ErsatzTV.Application.FFmpegProfiles; + +public record GetSupportedHardwareAccelerationKinds : IRequest>; diff --git a/ErsatzTV.Application/FFmpegProfiles/Queries/GetSupportedHardwareAccelerationKindsHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Queries/GetSupportedHardwareAccelerationKindsHandler.cs new file mode 100644 index 000000000..318bc3701 --- /dev/null +++ b/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> +{ + private readonly IDbContextFactory _dbContextFactory; + private readonly IHardwareCapabilitiesFactory _hardwareCapabilitiesFactory; + + public GetSupportedHardwareAccelerationKindsHandler( + IDbContextFactory dbContextFactory, + IHardwareCapabilitiesFactory hardwareCapabilitiesFactory) + { + _dbContextFactory = dbContextFactory; + _hardwareCapabilitiesFactory = hardwareCapabilitiesFactory; + } + + public async Task> Handle( + GetSupportedHardwareAccelerationKinds request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + Validation validation = await Validate(dbContext); + + return await validation.Match( + GetHardwareAccelerationKinds, + _ => Task.FromResult(new List { HardwareAccelerationKind.None })); + } + + private async Task> GetHardwareAccelerationKinds(string ffmpegPath) + { + var result = new List { 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> Validate(TvContext dbContext) => + await FFmpegPathMustExist(dbContext); + + private static Task> FFmpegPathMustExist(TvContext dbContext) => + dbContext.ConfigElements.GetValue(ConfigElementKey.FFmpegPath) + .FilterT(File.Exists) + .Map(maybePath => maybePath.ToValidation("FFmpeg path does not exist on filesystem")); +} diff --git a/ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs b/ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs index 721c68bd7..a8a614ebd 100644 --- a/ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs +++ b/ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs @@ -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> { 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 sessionWorkerLogger, IFFmpegSegmenterService ffmpegSegmenterService, IConfigElementRepository configElementRepository, + IHostApplicationLifetime hostApplicationLifetime, ChannelWriter workerChannel) { _hlsPlaylistFilter = hlsPlaylistFilter; @@ -49,6 +52,7 @@ public class StartFFmpegSessionHandler : IRequestHandler worker, (_, _) => worker); // fire and forget worker - _ = worker.Run(request.ChannelNumber, idleTimeout, cancellationToken) + _ = worker.Run(request.ChannelNumber, idleTimeout, _hostApplicationLifetime.ApplicationStopping) .ContinueWith( _ => { diff --git a/ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs b/ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs index 262797b87..36950021e 100644 --- a/ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs +++ b/ErsatzTV.Application/Troubleshooting/Queries/GetTroubleshootingInfoHandler.cs @@ -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 maybeFFmpegPath = await _configElementRepository.GetConfigElement(ConfigElementKey.FFmpegPath); @@ -87,6 +89,20 @@ public class GetTroubleshootingInfoHandler : IRequestHandler vaapiDevices)) + { + vaapiDevices = new List { "/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 @@ -94,11 +110,6 @@ public class GetTroubleshootingInfoHandler : IRequestHandler vaapiDevices)) - { - vaapiDevices = new List { "/dev/dri/renderD128" }; - } - foreach (string vaapiDevice in vaapiDevices) { foreach (string output in await _hardwareCapabilitiesFactory.GetVaapiOutput( @@ -123,6 +134,7 @@ public class GetTroubleshootingInfoHandler : IRequestHandler FFmpegProfiles, IEnumerable Channels, string NvidiaCapabilities, + string QsvCapabilities, string VaapiCapabilities); diff --git a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs index 29895b6c8..ce69c60d1 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs @@ -480,11 +480,13 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService var videoInputFile = new VideoInputFile(videoPath, new List { 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), diff --git a/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs b/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs index 25b5354d3..3b2524e5d 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs @@ -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, diff --git a/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs b/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs index 62de86f76..063a7e9b4 100644 --- a/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs +++ b/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs @@ -494,6 +494,7 @@ public class PipelineBuilderBaseTests { public DefaultFFmpegCapabilities() : base( + new System.Collections.Generic.HashSet(), new System.Collections.Generic.HashSet(), new System.Collections.Generic.HashSet(), new System.Collections.Generic.HashSet()) diff --git a/ErsatzTV.FFmpeg/Capabilities/FFmpegCapabilities.cs b/ErsatzTV.FFmpeg/Capabilities/FFmpegCapabilities.cs index aafc8edb3..49380f799 100644 --- a/ErsatzTV.FFmpeg/Capabilities/FFmpegCapabilities.cs +++ b/ErsatzTV.FFmpeg/Capabilities/FFmpegCapabilities.cs @@ -5,20 +5,38 @@ namespace ErsatzTV.FFmpeg.Capabilities; public class FFmpegCapabilities : IFFmpegCapabilities { + private readonly IReadOnlySet _ffmpegHardwareAccelerations; private readonly IReadOnlySet _ffmpegDecoders; private readonly IReadOnlySet _ffmpegEncoders; private readonly IReadOnlySet _ffmpegFilters; public FFmpegCapabilities( + IReadOnlySet ffmpegHardwareAccelerations, IReadOnlySet ffmpegDecoders, IReadOnlySet ffmpegFilters, IReadOnlySet 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); diff --git a/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs b/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs index d8ac792d5..27f3fa889 100644 --- a/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs +++ b/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs @@ -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 _logger; private readonly IMemoryCache _memoryCache; + private readonly IRuntimeInfo _runtimeInfo; - public HardwareCapabilitiesFactory(IMemoryCache memoryCache, ILogger logger) + public HardwareCapabilitiesFactory( + IMemoryCache memoryCache, + IRuntimeInfo runtimeInfo, + ILogger logger) { _memoryCache = memoryCache; + _runtimeInfo = runtimeInfo; _logger = logger; } public async Task GetFFmpegCapabilities(string ffmpegPath) { - IReadOnlySet ffmpegDecoders = await GetFFmpegCapabilities(ffmpegPath, "decoders"); - IReadOnlySet ffmpegFilters = await GetFFmpegCapabilities(ffmpegPath, "filters"); - IReadOnlySet ffmpegEncoders = await GetFFmpegCapabilities(ffmpegPath, "encoders"); + // TODO: validate videotoolbox somehow + // TODO: validate amf somehow - return new FFmpegCapabilities(ffmpegDecoders, ffmpegFilters, ffmpegEncoders); + IReadOnlySet ffmpegHardwareAccelerations = + await GetFFmpegCapabilities(ffmpegPath, "hwaccels", ParseFFmpegAccelLine); + IReadOnlySet ffmpegDecoders = await GetFFmpegCapabilities(ffmpegPath, "decoders", ParseFFmpegLine); + IReadOnlySet ffmpegFilters = await GetFFmpegCapabilities(ffmpegPath, "filters", ParseFFmpegLine); + IReadOnlySet ffmpegEncoders = await GetFFmpegCapabilities(ffmpegPath, "encoders", ParseFFmpegLine); + + return new FFmpegCapabilities(ffmpegHardwareAccelerations, ffmpegDecoders, ffmpegFilters, ffmpegEncoders); } public async Task GetHardwareCapabilities( @@ -40,14 +55,31 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory string ffmpegPath, HardwareAccelerationMode hardwareAccelerationMode, Option vaapiDriver, - Option vaapiDevice) => - hardwareAccelerationMode switch + Option 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 GetNvidiaOutput(string ffmpegPath) { @@ -71,6 +103,33 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory return output; } + + public async Task GetQsvOutput(string ffmpegPath, Option 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> GetVaapiOutput(Option vaapiDriver, string vaapiDevice) { @@ -99,13 +158,16 @@ public class HardwareCapabilitiesFactory : IHardwareCapabilitiesFactory return result.StandardOutput; } - private async Task> GetFFmpegCapabilities(string ffmpegPath, string capabilities) + private async Task> GetFFmpegCapabilities( + string ffmpegPath, + string capabilities, + Func> parseLine) { var cacheKey = string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, capabilities); - if (_memoryCache.TryGetValue(cacheKey, out IReadOnlySet? cachedDecoders) && - cachedDecoders is not null) + if (_memoryCache.TryGetValue(cacheKey, out IReadOnlySet? 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 ParseFFmpegAccelLine(string input) + { + const string PATTERN = @"^([\w]+)$"; + Match match = Regex.Match(input, PATTERN); + return match.Success ? match.Groups[1].Value : Option.None; + } + private static Option 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 GetQsvCapabilities(string ffmpegPath, Option 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? 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 vaapiOutput = await GetVaapiOutput(Option.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 GetNvidiaCapabilities( string ffmpegPath, IFFmpegCapabilities ffmpegCapabilities) diff --git a/ErsatzTV.FFmpeg/Capabilities/IFFmpegCapabilities.cs b/ErsatzTV.FFmpeg/Capabilities/IFFmpegCapabilities.cs index 3d310c8fb..fd63d9aaf 100644 --- a/ErsatzTV.FFmpeg/Capabilities/IFFmpegCapabilities.cs +++ b/ErsatzTV.FFmpeg/Capabilities/IFFmpegCapabilities.cs @@ -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); diff --git a/ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilitiesFactory.cs b/ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilitiesFactory.cs index a770bbfb0..eb6a71486 100644 --- a/ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilitiesFactory.cs +++ b/ErsatzTV.FFmpeg/Capabilities/IHardwareCapabilitiesFactory.cs @@ -1,3 +1,5 @@ +using ErsatzTV.FFmpeg.Capabilities.Qsv; + namespace ErsatzTV.FFmpeg.Capabilities; public interface IHardwareCapabilitiesFactory @@ -13,5 +15,7 @@ public interface IHardwareCapabilitiesFactory Task GetNvidiaOutput(string ffmpegPath); + Task GetQsvOutput(string ffmpegPath, Option qsvDevice); + Task> GetVaapiOutput(Option vaapiDriver, string vaapiDevice); } diff --git a/ErsatzTV.FFmpeg/Capabilities/Qsv/QsvOutput.cs b/ErsatzTV.FFmpeg/Capabilities/Qsv/QsvOutput.cs new file mode 100644 index 000000000..e26512165 --- /dev/null +++ b/ErsatzTV.FFmpeg/Capabilities/Qsv/QsvOutput.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.FFmpeg.Capabilities.Qsv; + +public record QsvOutput(int ExitCode, string Output); diff --git a/ErsatzTV.FFmpeg/OutputFormat/OutputFormatHls.cs b/ErsatzTV.FFmpeg/OutputFormat/OutputFormatHls.cs index b34337df0..d5bed4153 100644 --- a/ErsatzTV.FFmpeg/OutputFormat/OutputFormatHls.cs +++ b/ErsatzTV.FFmpeg/OutputFormat/OutputFormatHls.cs @@ -7,18 +7,21 @@ public class OutputFormatHls : IPipelineStep private readonly FrameState _desiredState; private readonly Option _mediaFrameRate; private readonly string _playlistPath; + private readonly bool _oneSecondGop; private readonly string _segmentTemplate; public OutputFormatHls( FrameState desiredState, Option mediaFrameRate, string segmentTemplate, - string playlistPath) + string playlistPath, + bool oneSecondGop = false) { _desiredState = desiredState; _mediaFrameRate = mediaFrameRate; _segmentTemplate = segmentTemplate; _playlistPath = playlistPath; + _oneSecondGop = oneSecondGop; } public IList EnvironmentVariables => Array.Empty(); @@ -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 { - "-g", $"{frameRate * SEGMENT_SECONDS}", + "-g", $"{gop}", "-keyint_min", $"{frameRate * SEGMENT_SECONDS}", "-force_key_frames", $"expr:gte(t,n_forced*{SEGMENT_SECONDS})", "-f", "hls", diff --git a/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs b/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs index 5becbdf87..2ad19651f 100644 --- a/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs +++ b/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs @@ -274,7 +274,8 @@ public abstract class PipelineBuilderBase : IPipelineBuilder desiredState, videoStream.FrameRate, segmentTemplate, - playlistPath)); + playlistPath, + ffmpegState.EncoderHardwareAccelerationMode is HardwareAccelerationMode.Qsv)); } } diff --git a/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderFactory.cs b/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderFactory.cs index ecda9c1e0..2adbeeb50 100644 --- a/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderFactory.cs +++ b/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderFactory.cs @@ -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, diff --git a/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs index 01cfd25ef..3ed1d9a45 100644 --- a/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs +++ b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs @@ -245,6 +245,7 @@ public class TranscodingTests //new FakeNvidiaCapabilitiesFactory(), new HardwareCapabilitiesFactory( MemoryCache, + new RuntimeInfo(), LoggerFactory.CreateLogger()), LoggerFactory.CreateLogger()), Substitute.For(), @@ -861,6 +862,7 @@ public class TranscodingTests //new FakeNvidiaCapabilitiesFactory(), new HardwareCapabilitiesFactory( MemoryCache, + new RuntimeInfo(), LoggerFactory.CreateLogger()), LoggerFactory.CreateLogger()), Substitute.For(), diff --git a/ErsatzTV/Pages/FFmpegEditor.razor b/ErsatzTV/Pages/FFmpegEditor.razor index 5c2960799..e74563edc 100644 --- a/ErsatzTV/Pages/FFmpegEditor.razor +++ b/ErsatzTV/Pages/FFmpegEditor.razor @@ -69,7 +69,7 @@ - @foreach (HardwareAccelerationKind hwAccel in Enum.GetValues()) + @foreach (HardwareAccelerationKind hwAccel in _hardwareAccelerationKinds) { @hwAccel } @@ -88,7 +88,7 @@ } - @if (_model.HardwareAcceleration == HardwareAccelerationKind.Vaapi || _model.HardwareAcceleration == HardwareAccelerationKind.Qsv) + @if (_model.HardwareAcceleration is HardwareAccelerationKind.Vaapi or HardwareAccelerationKind.Qsv) { _resolutions = new(); + private List _hardwareAccelerationKinds = new(); private List _vaapiDevices = new(); private PersistingComponentStateSubscription _persistingSubscription; @@ -195,6 +196,15 @@ { _resolutions = restoredResolutions; } + + if (!ApplicationState.TryTakeFromJson("_hardwareAccelerationKinds", out List restoredHardwareAccelerationKinds)) + { + _hardwareAccelerationKinds = await _mediator.Send(new GetSupportedHardwareAccelerationKinds(), _cts.Token); + } + else + { + _hardwareAccelerationKinds = restoredHardwareAccelerationKinds; + } if (IsEdit) { @@ -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; } diff --git a/ErsatzTV/Pages/Troubleshooting.razor b/ErsatzTV/Pages/Troubleshooting.razor index c35d83daa..ed49a922e 100644 --- a/ErsatzTV/Pages/Troubleshooting.razor +++ b/ErsatzTV/Pages/Troubleshooting.razor @@ -29,6 +29,16 @@ Copy + +
+
+                    @_qsvCapabilities
+                
+
+ + Copy + +
@@ -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)