Browse Source

fix scale behavior crop with qsv (#1546)

pull/1551/head
Jason Dove 2 years ago committed by GitHub
parent
commit
5e530b9301
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      CHANGELOG.md
  2. 10
      ErsatzTV.FFmpeg/Filter/Qsv/ScaleQsvFilter.cs
  3. 3
      ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs
  4. 150
      ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs
  5. 2
      ErsatzTV.Scanner/Core/Metadata/LocalFolderScanner.cs

2
CHANGELOG.md

@ -27,7 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Unlock playout when playout build fails - Unlock playout when playout build fails
- Ignore errors deleting old HLS segments; this should improve stream reliability - Ignore errors deleting old HLS segments; this should improve stream reliability
- Update show year when changed within Plex - Update show year when changed within Plex
- Fix crop scale mode with NVIDIA acceleration - Fix crop scale behavior with NVIDIA, QSV acceleration
### Changed ### Changed
- Upgrade from .NET 7 to .NET 8 - Upgrade from .NET 7 to .NET 8

10
ErsatzTV.FFmpeg/Filter/Qsv/ScaleQsvFilter.cs

@ -9,16 +9,22 @@ public class ScaleQsvFilter : BaseFilter
private readonly bool _isAnamorphicEdgeCase; private readonly bool _isAnamorphicEdgeCase;
private readonly string _sampleAspectRatio; private readonly string _sampleAspectRatio;
private readonly FrameSize _scaledSize; private readonly FrameSize _scaledSize;
private readonly FrameSize _paddedSize;
private readonly Option<FrameSize> _croppedSize;
public ScaleQsvFilter( public ScaleQsvFilter(
FrameState currentState, FrameState currentState,
FrameSize scaledSize, FrameSize scaledSize,
FrameSize paddedSize,
Option<FrameSize> croppedSize,
int extraHardwareFrames, int extraHardwareFrames,
bool isAnamorphicEdgeCase, bool isAnamorphicEdgeCase,
string sampleAspectRatio) string sampleAspectRatio)
{ {
_currentState = currentState; _currentState = currentState;
_scaledSize = scaledSize; _scaledSize = scaledSize;
_paddedSize = paddedSize;
_croppedSize = croppedSize;
_extraHardwareFrames = extraHardwareFrames; _extraHardwareFrames = extraHardwareFrames;
_isAnamorphicEdgeCase = isAnamorphicEdgeCase; _isAnamorphicEdgeCase = isAnamorphicEdgeCase;
_sampleAspectRatio = sampleAspectRatio; _sampleAspectRatio = sampleAspectRatio;
@ -43,7 +49,9 @@ public class ScaleQsvFilter : BaseFilter
else else
{ {
string squareScale = string.Empty; string squareScale = string.Empty;
var targetSize = $"w={_scaledSize.Width}:h={_scaledSize.Height}"; var targetSize = _croppedSize.IsSome
? $"w={_paddedSize.Width}:h={_paddedSize.Height}"
: $"w={_scaledSize.Width}:h={_scaledSize.Height}";
string format = string.Empty; string format = string.Empty;
foreach (IPixelFormat pixelFormat in _currentState.PixelFormat) foreach (IPixelFormat pixelFormat in _currentState.PixelFormat)
{ {

3
ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs

@ -165,6 +165,7 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder
// _logger.LogDebug("After scale: {PixelFormat}", currentState.PixelFormat); // _logger.LogDebug("After scale: {PixelFormat}", currentState.PixelFormat);
currentState = SetPad(videoInputFile, videoStream, desiredState, currentState); currentState = SetPad(videoInputFile, videoStream, desiredState, currentState);
// _logger.LogDebug("After pad: {PixelFormat}", currentState.PixelFormat); // _logger.LogDebug("After pad: {PixelFormat}", currentState.PixelFormat);
currentState = SetCrop(videoInputFile, desiredState, currentState);
// need to download for any sort of overlay // need to download for any sort of overlay
if (currentState.FrameDataLocation == FrameDataLocation.Hardware && if (currentState.FrameDataLocation == FrameDataLocation.Hardware &&
@ -572,6 +573,8 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder
: Option<IPixelFormat>.None : Option<IPixelFormat>.None
}, },
desiredState.ScaledSize, desiredState.ScaledSize,
desiredState.PaddedSize,
desiredState.CroppedSize,
ffmpegState.QsvExtraHardwareFrames, ffmpegState.QsvExtraHardwareFrames,
VideoStream.IsAnamorphicEdgeCase, VideoStream.IsAnamorphicEdgeCase,
videoStream.SampleAspectRatio); videoStream.SampleAspectRatio);

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

@ -110,51 +110,58 @@ public class TranscodingTests
private class TestData private class TestData
{ {
public static Watermark[] Watermarks = public static Watermark[] Watermarks =
{ [
Watermark.None, Watermark.None
Watermark.PermanentOpaqueScaled, //Watermark.PermanentOpaqueScaled,
// Watermark.PermanentOpaqueActualSize, // Watermark.PermanentOpaqueActualSize,
Watermark.PermanentTransparentScaled //Watermark.PermanentTransparentScaled
// Watermark.PermanentTransparentActualSize // Watermark.PermanentTransparentActualSize
}; ];
public static Subtitle[] Subtitles = public static Subtitle[] Subtitles =
{ [
Subtitle.None, Subtitle.None
Subtitle.Picture, //Subtitle.Picture,
Subtitle.Text //Subtitle.Text
}; ];
public static Padding[] Paddings = public static Padding[] Paddings =
{ [
Padding.NoPadding, Padding.NoPadding,
Padding.WithPadding Padding.WithPadding
}; ];
public static ScalingBehavior[] ScalingBehaviors =
[
ScalingBehavior.ScaleAndPad,
ScalingBehavior.Crop,
//ScalingBehavior.Stretch
];
public static VideoScanKind[] VideoScanKinds = public static VideoScanKind[] VideoScanKinds =
{ [
VideoScanKind.Progressive, VideoScanKind.Progressive
VideoScanKind.Interlaced //VideoScanKind.Interlaced
}; ];
public static InputFormat[] InputFormats = public static InputFormat[] InputFormats =
{ [
// // // example format that requires colorspace filter // // // example format that requires colorspace filter
new("libx264", "yuv420p", "tv", "smpte170m", "bt709", "smpte170m"), // new("libx264", "yuv420p", "tv", "smpte170m", "bt709", "smpte170m"),
// // // // //
// // // example format that requires setparams filter // // // // example format that requires setparams filter
new("libx264", "yuv420p", string.Empty, string.Empty, string.Empty, string.Empty), // new("libx264", "yuv420p", string.Empty, string.Empty, string.Empty, string.Empty),
// //
// // // new("libx264", "yuvj420p"),
new("libx264", "yuv420p10le"),
// // // new("libx264", "yuv444p10le"),
// //
// // // new("mpeg1video", "yuv420p"),
// // // // // //
new("mpeg2video", "yuv420p"), // // // // new("libx264", "yuvj420p"),
// new("libx264", "yuv420p10le"),
// // // // new("libx264", "yuv444p10le"),
// // //
// // // // new("mpeg1video", "yuv420p"),
// // // //
// new("mpeg2video", "yuv420p"),
// // // //
new("libx265", "yuv420p"), new InputFormat("libx265", "yuv420p")
new("libx265", "yuv420p10le") // new("libx265", "yuv420p10le")
// //
// new("mpeg4", "yuv420p"), // new("mpeg4", "yuv420p"),
// //
@ -168,38 +175,39 @@ public class TranscodingTests
// new("msmpeg4v3", "yuv420p") // new("msmpeg4v3", "yuv420p")
// //
// // wmv3 yuv420p 1 // // wmv3 yuv420p 1
}; ];
public static Resolution[] Resolutions = public static Resolution[] Resolutions =
{ [
new() { Width = 1920, Height = 1080 }, new Resolution { Width = 1920, Height = 1080 },
new() { Width = 1280, Height = 720 } new Resolution { Width = 1280, Height = 720 },
}; new Resolution { Width = 640, Height = 480 }
];
public static FFmpegProfileBitDepth[] BitDepths = public static FFmpegProfileBitDepth[] BitDepths =
{ [
FFmpegProfileBitDepth.EightBit, FFmpegProfileBitDepth.EightBit
FFmpegProfileBitDepth.TenBit //FFmpegProfileBitDepth.TenBit
}; ];
public static FFmpegProfileVideoFormat[] VideoFormats = public static FFmpegProfileVideoFormat[] VideoFormats =
{ [
FFmpegProfileVideoFormat.H264, FFmpegProfileVideoFormat.H264,
FFmpegProfileVideoFormat.Hevc FFmpegProfileVideoFormat.Hevc
// FFmpegProfileVideoFormat.Mpeg2Video // FFmpegProfileVideoFormat.Mpeg2Video
}; ];
public static HardwareAccelerationKind[] TestAccelerations = public static HardwareAccelerationKind[] TestAccelerations =
{ [
HardwareAccelerationKind.None, HardwareAccelerationKind.None,
// HardwareAccelerationKind.Nvenc, HardwareAccelerationKind.Nvenc,
HardwareAccelerationKind.Vaapi //HardwareAccelerationKind.Vaapi,
// HardwareAccelerationKind.Qsv HardwareAccelerationKind.Qsv,
// HardwareAccelerationKind.VideoToolbox, // HardwareAccelerationKind.VideoToolbox,
// HardwareAccelerationKind.Amf // HardwareAccelerationKind.Amf
}; ];
public static string[] FilesToTest => new[] { string.Empty }; public static string[] FilesToTest => [string.Empty];
} }
[Test] [Test]
@ -388,6 +396,8 @@ public class TranscodingTests
FFmpegProfileBitDepth profileBitDepth, FFmpegProfileBitDepth profileBitDepth,
[ValueSource(typeof(TestData), nameof(TestData.Paddings))] [ValueSource(typeof(TestData), nameof(TestData.Paddings))]
Padding padding, Padding padding,
[ValueSource(typeof(TestData), nameof(TestData.ScalingBehaviors))]
ScalingBehavior scalingBehavior,
[ValueSource(typeof(TestData), nameof(TestData.VideoScanKinds))] [ValueSource(typeof(TestData), nameof(TestData.VideoScanKinds))]
VideoScanKind videoScanKind, VideoScanKind videoScanKind,
[ValueSource(typeof(TestData), nameof(TestData.Watermarks))] [ValueSource(typeof(TestData), nameof(TestData.Watermarks))]
@ -415,22 +425,19 @@ public class TranscodingTests
} }
} }
string name = GetStringSha256Hash($"{inputFormat}_{videoScanKind}_{padding}_{subtitle}"); string name = GetStringSha256Hash($"{inputFormat}_{videoScanKind}_{padding}_{scalingBehavior}_{subtitle}");
file = Path.Combine(TestContext.CurrentContext.TestDirectory, $"{name}.mkv"); file = Path.Combine(TestContext.CurrentContext.TestDirectory, $"{name}.mkv");
if (!File.Exists(file)) if (!File.Exists(file))
{ {
await GenerateTestFile(inputFormat, padding, videoScanKind, subtitle, file); await GenerateTestFile(inputFormat, padding, scalingBehavior, videoScanKind, subtitle, file);
} }
} }
var v = new MediaVersion var v = new MediaVersion
{ {
MediaFiles = new List<MediaFile> MediaFiles = [new MediaFile { Path = file }],
{ Streams = []
new() { Path = file }
},
Streams = new List<MediaStream>()
}; };
IMetadataRepository? metadataRepository = Substitute.For<IMetadataRepository>(); IMetadataRepository? metadataRepository = Substitute.For<IMetadataRepository>();
@ -552,7 +559,7 @@ public class TranscodingTests
.Any(); .Any();
// TODO: sometimes scaling is used for pixel format, so this is harder to assert the absence // TODO: sometimes scaling is used for pixel format, so this is harder to assert the absence
if (profileResolution.Width != 1920) if (profileResolution.Width != 1920 && profileResolution.Width != 640)
{ {
hasScaling.Should().BeTrue(); hasScaling.Should().BeTrue();
} }
@ -563,11 +570,17 @@ public class TranscodingTests
// TODO: optimize out padding // TODO: optimize out padding
// hasPadding.Should().Be(padding == Padding.WithPadding); // hasPadding.Should().Be(padding == Padding.WithPadding);
if (padding == Padding.WithPadding) if (padding is Padding.WithPadding && scalingBehavior is not ScalingBehavior.Crop)
{ {
hasPadding.Should().BeTrue(); hasPadding.Should().BeTrue();
} }
bool hasCrop = filterChain.VideoFilterSteps.Any(s => s is CropFilter);
if (scalingBehavior is ScalingBehavior.Crop)
{
hasCrop.Should().BeTrue();
}
bool hasSubtitleFilters = bool hasSubtitleFilters =
filterChain.VideoFilterSteps.Any(s => s is SubtitlesFilter) || filterChain.VideoFilterSteps.Any(s => s is SubtitlesFilter) ||
filterChain.SubtitleOverlayFilterSteps.Any( filterChain.SubtitleOverlayFilterSteps.Any(
@ -599,7 +612,8 @@ public class TranscodingTests
VideoFormat = profileVideoFormat, VideoFormat = profileVideoFormat,
AudioFormat = FFmpegProfileAudioFormat.Aac, AudioFormat = FFmpegProfileAudioFormat.Aac,
DeinterlaceVideo = true, DeinterlaceVideo = true,
BitDepth = profileBitDepth BitDepth = profileBitDepth,
ScalingBehavior = scalingBehavior
}, },
StreamingMode = StreamingMode.TransportStream, StreamingMode = StreamingMode.TransportStream,
SubtitleMode = subtitleMode SubtitleMode = subtitleMode
@ -711,11 +725,18 @@ public class TranscodingTests
private static async Task GenerateTestFile( private static async Task GenerateTestFile(
InputFormat inputFormat, InputFormat inputFormat,
Padding padding, Padding padding,
ScalingBehavior scalingBehavior,
VideoScanKind videoScanKind, VideoScanKind videoScanKind,
Subtitle subtitle, Subtitle subtitle,
string file) string file)
{ {
string resolution = padding == Padding.WithPadding ? "1920x1060" : "1920x1080"; string resolution = (scalingBehavior, padding) switch
{
(ScalingBehavior.Crop, Padding.NoPadding) => "1920x1080",
// TODO: (ScalingBehavior.Crop, Padding.WithPadding) => "632x480",
(ScalingBehavior.Stretch or ScalingBehavior.ScaleAndPad, Padding.WithPadding) => "1920x1060",
_ => "1920x1080"
};
string videoFilter = videoScanKind == VideoScanKind.Interlaced string videoFilter = videoScanKind == VideoScanKind.Interlaced
? "-vf interlace=scan=tff:lowpass=complex" ? "-vf interlace=scan=tff:lowpass=complex"
@ -954,16 +975,13 @@ public class TranscodingTests
ExecutableName("ffprobe"), ExecutableName("ffprobe"),
new Movie new Movie
{ {
MediaVersions = new List<MediaVersion> MediaVersions =
{ [
new() new MediaVersion
{ {
MediaFiles = new List<MediaFile> MediaFiles = [new MediaFile { Path = tempFile }]
{
new() { Path = tempFile }
}
} }
} ]
}); });
MediaVersion v = getFinalMediaVersion(); MediaVersion v = getFinalMediaVersion();

2
ErsatzTV.Scanner/Core/Metadata/LocalFolderScanner.cs

@ -21,7 +21,7 @@ public abstract class LocalFolderScanner
public static readonly ImmutableHashSet<string> VideoFileExtensions = new[] public static readonly ImmutableHashSet<string> VideoFileExtensions = new[]
{ {
".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".ogg", ".mp4", ".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".ogg", ".mp4",
".m4p", ".m4v", ".avi", ".wmv", ".mov", ".mkv", ".ts", ".webm" ".m4p", ".m4v", ".avi", ".wmv", ".mov", ".mkv", ".m2ts", ".ts", ".webm"
}.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); }.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
public static readonly ImmutableHashSet<string> AudioFileExtensions = new[] public static readonly ImmutableHashSet<string> AudioFileExtensions = new[]

Loading…
Cancel
Save