235 lines
8.0 KiB
C#
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>());
|
|
}
|