Files
ersatztv/ErsatzTV.FFmpeg.Tests/Pipeline/QsvPipelineBuilderTests.cs
T

235 lines
8.0 KiB
C#

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<ILogger>();
[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<GraphicsEngineInput>.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<GraphicsEngineInput>.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<VideoStream>
{
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<AudioStream> { new(1, AudioFormat.Aac, 2) },
new AudioState(
AudioFormat.Aac,
2,
320,
640,
48,
false,
AudioFilter.None,
Option<double>.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<FrameSize>.None,
FFmpegFilterMode.Software,
false,
Option<FrameRate>.None,
2000,
4000,
90_000,
false,
deinterlace);
var ffmpegState = new FFmpegState(
false,
HardwareAccelerationMode.Qsv,
HardwareAccelerationMode.Qsv,
Option<string>.None,
"/dev/dri/renderD128",
Option<TimeSpan>.None,
Option<TimeSpan>.None,
false,
Option<string>.None,
Option<string>.None,
Option<string>.None,
Option<string>.None,
Option<string>.None,
OutputFormatKind.MpegTs,
Option<string>.None,
Option<string>.None,
Option<string>.None,
Option<string>.None,
TimeSpan.Zero,
Option<int>.None,
Option<int>.None,
false,
false,
"linear",
false,
preferNativeDecoder);
return (videoInputFile, audioInputFile, ffmpegState, desiredState);
}
private static string PrintCommand(
Option<VideoInputFile> videoInputFile,
Option<AudioInputFile> audioInputFile,
Option<WatermarkInputFile> watermarkInputFile,
Option<ConcatInputFile> concatInputFile,
Option<GraphicsEngineInput> graphicsEngineInput,
FFmpegPipeline pipeline)
{
IList<string> 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<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>());
}