diff --git a/ErsatzTV.FFmpeg.Tests/Pipeline/QsvPipelineBuilderTests.cs b/ErsatzTV.FFmpeg.Tests/Pipeline/QsvPipelineBuilderTests.cs new file mode 100644 index 000000000..f33df1243 --- /dev/null +++ b/ErsatzTV.FFmpeg.Tests/Pipeline/QsvPipelineBuilderTests.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; +using ErsatzTV.FFmpeg.Capabilities; +using ErsatzTV.FFmpeg.Format; +using ErsatzTV.FFmpeg.OutputFormat; +using ErsatzTV.FFmpeg.Pipeline; +using ErsatzTV.FFmpeg.Preset; +using ErsatzTV.FFmpeg.State; +using LanguageExt; +using Microsoft.Extensions.Logging; +using NSubstitute; +using NUnit.Framework; +using Shouldly; +using static LanguageExt.Prelude; + +namespace ErsatzTV.FFmpeg.Tests.Pipeline; + +[TestFixture] +public class QsvPipelineBuilderTests +{ + private readonly ILogger _logger = Substitute.For(); + + [Test] + public void Qsv_PreferNativeDecoder_Should_Decode_Via_Vaapi_To_Software_Then_Qsv_Encode() + { + string command = BuildAndPrint(preferNativeDecoder: true); + + // VA-API decode, frames downloaded to software (NO hwaccel_output_format) + command.ShouldContain("-hwaccel vaapi"); + command.ShouldNotContain("-hwaccel_output_format"); + command.ShouldNotContain("-hwaccel qsv"); + // no QSV *decoder* input option (decoder input options sit directly before "-readrate"/"-i"; + // "-c:v h264_qsv -" alone would also match the encoder's "-c:v h264_qsv -low_power ..." output option) + command.ShouldNotContain("-c:v h264_qsv -readrate"); + // derived-device chain retained for the QSV encoder + command.ShouldContain("-init_hw_device vaapi=va:/dev/dri/renderD128"); + command.ShouldContain("-init_hw_device qsv=hw@va"); + // software frames re-uploaded before QSV filters/encoder (proves NO bare vpp_qsv on VA-API frames) + command.ShouldContain("hwupload=extra_hw_frames"); + // QSV encoder still used + command.ShouldContain("h264_qsv"); + } + + [Test] + public void Qsv_Default_Should_Decode_And_Encode_With_Qsv() + { + string command = BuildAndPrint(preferNativeDecoder: false); + + command.ShouldContain("-hwaccel qsv"); + command.ShouldContain("-hwaccel_output_format qsv"); + command.ShouldContain("h264_qsv"); + command.ShouldNotContain("-hwaccel vaapi"); + } + + [Test] + public void Qsv_PreferNativeDecoder_Interlaced_Should_Hwupload_Before_Deinterlace_Qsv() + { + (VideoInputFile videoInputFile, AudioInputFile audioInputFile, FFmpegState ffmpegState, FrameState desiredState) = + BuildQsvH264Pipeline(preferNativeDecoder: true, scanKind: ScanKind.Interlaced, deinterlace: true); + + var builder = new QsvPipelineBuilder( + new DefaultFFmpegCapabilities(), + new DefaultHardwareCapabilities(), + HardwareAccelerationMode.Qsv, + videoInputFile, + audioInputFile, + None, + None, + None, + Option.None, + "", + "", + _logger); + + FFmpegPipeline result = builder.Build(ffmpegState, desiredState); + + string command = PrintCommand(videoInputFile, audioInputFile, None, None, None, result); + + // VA-API decode, software frames + command.ShouldContain("-hwaccel vaapi"); + command.ShouldNotContain("-hwaccel_output_format"); + + // software frames re-uploaded BEFORE deinterlace_qsv (never a bare deinterlace_qsv on VA-API frames) + command.ShouldContain("hwupload=extra_hw_frames"); + command.ShouldContain("hwupload=extra_hw_frames=64,deinterlace_qsv"); + command.ShouldNotContain(" deinterlace_qsv"); // no leading-space bare occurrence (i.e. always preceded by hwupload=...,) + + command.ShouldContain("h264_qsv"); + } + + private string BuildAndPrint(bool preferNativeDecoder) + { + (VideoInputFile videoInputFile, AudioInputFile audioInputFile, FFmpegState ffmpegState, FrameState desiredState) = + BuildQsvH264Pipeline(preferNativeDecoder, ScanKind.Progressive, false); + + var builder = new QsvPipelineBuilder( + new DefaultFFmpegCapabilities(), + new DefaultHardwareCapabilities(), + HardwareAccelerationMode.Qsv, + videoInputFile, + audioInputFile, + None, + None, + None, + Option.None, + "", + "", + _logger); + + FFmpegPipeline result = builder.Build(ffmpegState, desiredState); + + return PrintCommand(videoInputFile, audioInputFile, None, None, None, result); + } + + private static (VideoInputFile, AudioInputFile, FFmpegState, FrameState) BuildQsvH264Pipeline( + bool preferNativeDecoder, + ScanKind scanKind, + bool deinterlace) + { + var videoInputFile = new VideoInputFile( + "/tmp/whatever.mkv", + new List + { + new( + 0, + VideoFormat.H264, + VideoProfile.Main, + new PixelFormatYuv420P(), + ColorParams.Default, + new FrameSize(1920, 1080), + "1:1", + "16:9", + FrameRate.DefaultFrameRate, + false, + scanKind) + }); + + var audioInputFile = new AudioInputFile( + "/tmp/whatever.mkv", + new List { new(1, AudioFormat.Aac, 2) }, + new AudioState( + AudioFormat.Aac, + 2, + 320, + 640, + 48, + false, + AudioFilter.None, + Option.None)); + + var desiredState = new FrameState( + true, + false, + VideoFormat.H264, + VideoProfile.Main, + VideoPreset.Unset, + false, + new PixelFormatYuv420P(), + new FrameSize(1280, 720), + new FrameSize(1280, 720), + Option.None, + FFmpegFilterMode.Software, + false, + Option.None, + 2000, + 4000, + 90_000, + false, + deinterlace); + + var ffmpegState = new FFmpegState( + false, + HardwareAccelerationMode.Qsv, + HardwareAccelerationMode.Qsv, + Option.None, + "/dev/dri/renderD128", + Option.None, + Option.None, + false, + Option.None, + Option.None, + Option.None, + Option.None, + Option.None, + OutputFormatKind.MpegTs, + Option.None, + Option.None, + Option.None, + Option.None, + TimeSpan.Zero, + Option.None, + Option.None, + false, + false, + "linear", + false, + preferNativeDecoder); + + return (videoInputFile, audioInputFile, ffmpegState, desiredState); + } + + private static string PrintCommand( + Option videoInputFile, + Option audioInputFile, + Option watermarkInputFile, + Option concatInputFile, + Option graphicsEngineInput, + FFmpegPipeline pipeline) + { + IList arguments = CommandGenerator.GenerateArguments( + videoInputFile, + audioInputFile, + watermarkInputFile, + concatInputFile, + graphicsEngineInput, + pipeline.PipelineSteps, + pipeline.IsIntelVaapiOrQsv); + + var command = string.Join(" ", arguments); + + Console.WriteLine($"Generated command: ffmpeg {string.Join(" ", arguments)}"); + + return command; + } + + public class DefaultFFmpegCapabilities() : FFmpegCapabilities( + string.Empty, + new System.Collections.Generic.HashSet(), + new System.Collections.Generic.HashSet(), + new System.Collections.Generic.HashSet(), + new System.Collections.Generic.HashSet(), + new System.Collections.Generic.HashSet(), + new System.Collections.Generic.HashSet()); +} diff --git a/ErsatzTV.FFmpeg/Decoder/DecoderVaapiToSoftware.cs b/ErsatzTV.FFmpeg/Decoder/DecoderVaapiToSoftware.cs new file mode 100644 index 000000000..aaf3fb320 --- /dev/null +++ b/ErsatzTV.FFmpeg/Decoder/DecoderVaapiToSoftware.cs @@ -0,0 +1,19 @@ +using ErsatzTV.FFmpeg.Format; + +namespace ErsatzTV.FFmpeg.Decoder; + +// VA-API-accelerated decode that downloads frames to system memory (no +// -hwaccel_output_format). Pairs with `-hwaccel vaapi` from +// QsvHardwareAccelerationOption on the "prefer native decoder" QSV path: the +// error-tolerant VA-API decoder feeds software frames into the QSV builder's +// format=nv12,hwupload,vpp_qsv branch, which re-uploads for the QSV encoder. +public class DecoderVaapiToSoftware : DecoderBase +{ + protected override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Software; + + public override string Name => "implicit_vaapi"; + + // no -c:v (implicit decoder; `-hwaccel vaapi` drives VA-API) and no + // -hwaccel_output_format (frames download to software) + public override string[] InputOptions(InputFile inputFile) => []; +} diff --git a/ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs b/ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs index 2e3970f81..635a45ffc 100644 --- a/ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs +++ b/ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs @@ -102,13 +102,18 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder // give a bogus value so no cuda devices are visible to ffmpeg pipelineSteps.Add(new CudaVisibleDevicesVariable("999")); - pipelineSteps.Add(new QsvHardwareAccelerationOption(ffmpegState.VaapiDevice, decodeCapability)); + pipelineSteps.Add(new QsvHardwareAccelerationOption( + ffmpegState.VaapiDevice, + decodeCapability, + ffmpegState.QsvPreferNativeDecoder != false)); // disable hw accel if decoder/encoder isn't supported return ffmpegState with { DecoderHardwareAccelerationMode = decodeCapability == FFmpegCapability.Hardware - ? HardwareAccelerationMode.Qsv + ? ffmpegState.QsvPreferNativeDecoder != false + ? HardwareAccelerationMode.Vaapi + : HardwareAccelerationMode.Qsv : HardwareAccelerationMode.None, EncoderHardwareAccelerationMode = encodeCapability == FFmpegCapability.Hardware ? HardwareAccelerationMode.Qsv @@ -130,6 +135,7 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder (HardwareAccelerationMode.Qsv, VideoFormat.Vc1) => new DecoderVc1Qsv(), (HardwareAccelerationMode.Qsv, VideoFormat.Vp9) => new DecoderVp9Qsv(), (HardwareAccelerationMode.Qsv, VideoFormat.Av1) => new DecoderAv1Qsv(), + (HardwareAccelerationMode.Vaapi, _) => new DecoderVaapiToSoftware(), _ => GetSoftwareDecoder(videoStream) };