From dd5fd1ef8f9defe6f306a1bf085e1019a67c984d Mon Sep 17 00:00:00 2001
From: Jason Dove <1695733+jasongdove@users.noreply.github.com>
Date: Thu, 2 Oct 2025 20:13:40 -0500
Subject: [PATCH] fix cropping jellyfin and emby content that is too small
(#2481)
* fix cropping jellyfin and emby content that is too small
* fix transcoding tests with nvidia
* update dependencies
---
.config/dotnet-tools.json | 2 +-
CHANGELOG.md | 1 +
.../ErsatzTV.Core.Tests.csproj | 2 +-
.../FFmpegPlaybackSettingsCalculatorTests.cs | 31 ++++++++++++
.../FFmpegPlaybackSettingsCalculator.cs | 47 ++++++++++++-------
.../ErsatzTV.FFmpeg.Tests.csproj | 2 +-
ErsatzTV.FFmpeg/AspectRatio.cs | 30 ++++++++++++
.../ErsatzTV.Infrastructure.Tests.csproj | 2 +-
.../Core/FFmpeg/TranscodingTests.cs | 23 +++++----
.../ErsatzTV.Scanner.Tests.csproj | 2 +-
ErsatzTV/ErsatzTV.csproj | 4 +-
11 files changed, 111 insertions(+), 35 deletions(-)
create mode 100644 ErsatzTV.FFmpeg/AspectRatio.cs
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 150ea5616..0b7967ddb 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"jetbrains.resharper.globaltools": {
- "version": "2025.2.1",
+ "version": "2025.2.2.1",
"commands": [
"jb"
],
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 35d9a2b81..5b386abb6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -101,6 +101,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix some more hls.js warnings by adding codec information to multi-variant playlists
- Fix hardware decode of h264 constrained baseline content using VAAPI accel
- Custom stream selector: ignore embedded text subtitles that have not been extracted
+- Fix cropping Jellyfin and Emby content that is smaller than the crop resolution
### Changed
- Filler presets: use separate text fields for `hours`, `minutes` and `seconds` duration
diff --git a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj
index 5cca821be..47444e74c 100644
--- a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj
+++ b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj
@@ -15,7 +15,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsCalculatorTests.cs b/ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsCalculatorTests.cs
index cf63a1930..36ee3f5b8 100644
--- a/ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsCalculatorTests.cs
+++ b/ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsCalculatorTests.cs
@@ -430,6 +430,37 @@ public class FFmpegPlaybackSettingsCalculatorTests
actual.PadToDesiredResolution.ShouldBeFalse();
}
+ [Test]
+ public void Should_ScaleBeyondMinSize_ForCrop_ForTransportStream_UnknownSAR()
+ {
+ FFmpegProfile ffmpegProfile = TestProfile() with
+ {
+ Resolution = new Resolution { Width = 640, Height = 411 },
+ ScalingBehavior = ScalingBehavior.Crop
+ };
+
+ var version = new MediaVersion
+ { Width = 626, Height = 476, SampleAspectRatio = "0:0", DisplayAspectRatio = "4:3" };
+
+ FFmpegPlaybackSettings actual = FFmpegPlaybackSettingsCalculator.CalculateSettings(
+ StreamingMode.TransportStream,
+ ffmpegProfile,
+ version,
+ new MediaStream(),
+ DateTimeOffset.Now,
+ DateTimeOffset.Now,
+ TimeSpan.Zero,
+ TimeSpan.Zero,
+ false,
+ StreamInputKind.Vod,
+ None);
+
+ IDisplaySize scaledSize = actual.ScaledSize.IfNone(new MediaVersion { Width = 0, Height = 0 });
+ scaledSize.Width.ShouldBe(640);
+ scaledSize.Height.ShouldBe(480);
+ actual.PadToDesiredResolution.ShouldBeFalse();
+ }
+
[Test]
public void Should_ScaleDownToMinSize_ForCrop_ForTransportStream()
{
diff --git a/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs b/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs
index 1539ddd52..25470eed4 100644
--- a/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs
+++ b/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs
@@ -92,9 +92,18 @@ public static class FFmpegPlaybackSettingsCalculator
case StreamingMode.TransportStream:
result.HardwareAcceleration = ffmpegProfile.HardwareAcceleration;
- if (NeedToScale(ffmpegProfile, videoVersion) && videoVersion.SampleAspectRatio != "0:0")
+ string sampleAspectRatio = videoVersion.SampleAspectRatio;
+ if (sampleAspectRatio == "0:0")
{
- DisplaySize scaledSize = CalculateScaledSize(ffmpegProfile, videoVersion);
+ sampleAspectRatio = AspectRatio.CalculateSAR(
+ videoVersion.Width,
+ videoVersion.Height,
+ videoVersion.DisplayAspectRatio);
+ }
+
+ if (NeedToScale(ffmpegProfile, videoVersion, sampleAspectRatio))
+ {
+ DisplaySize scaledSize = CalculateScaledSize(ffmpegProfile, videoVersion, sampleAspectRatio);
if (!scaledSize.IsSameSizeAs(videoVersion))
{
int fixedHeight = scaledSize.Height + scaledSize.Height % 2;
@@ -229,11 +238,11 @@ public static class FFmpegPlaybackSettingsCalculator
FrameRate = 24
};
- private static bool NeedToScale(FFmpegProfile ffmpegProfile, MediaVersion version) =>
- IsIncorrectSize(ffmpegProfile.Resolution, version) ||
- IsTooLarge(ffmpegProfile.Resolution, version) ||
- IsOddSize(version) ||
- TooSmallToCrop(ffmpegProfile, version);
+ private static bool NeedToScale(FFmpegProfile ffmpegProfile, MediaVersion version, string sampleAspectRatio) =>
+ (IsIncorrectSize(ffmpegProfile.Resolution, version) ||
+ IsTooLarge(ffmpegProfile.Resolution, version) ||
+ IsOddSize(version) ||
+ TooSmallToCrop(ffmpegProfile, version)) && sampleAspectRatio != "0:0";
private static bool IsIncorrectSize(Resolution desiredResolution, MediaVersion version) =>
IsAnamorphic(version) ||
@@ -257,14 +266,17 @@ public static class FFmpegPlaybackSettingsCalculator
return version.Height < ffmpegProfile.Resolution.Height || version.Width < ffmpegProfile.Resolution.Width;
}
- private static DisplaySize CalculateScaledSize(FFmpegProfile ffmpegProfile, MediaVersion version)
+ private static DisplaySize CalculateScaledSize(
+ FFmpegProfile ffmpegProfile,
+ MediaVersion version,
+ string sampleAspectRatio)
{
- DisplaySize sarSize = SARSize(version);
- int p = version.Width * sarSize.Width;
- int q = version.Height * sarSize.Height;
+ (double sarWidth, double sarHeight) = SARSize(sampleAspectRatio);
+ var p = (int)Math.Round(version.Width * sarWidth);
+ var q = (int)Math.Round(version.Height * sarHeight);
int g = Gcd(q, p);
- p = p / g;
- q = q / g;
+ p /= g;
+ q /= g;
Resolution targetSize = ffmpegProfile.Resolution;
int hw1 = targetSize.Width;
int hh1 = hw1 * q / p;
@@ -323,11 +335,10 @@ public static class FFmpegPlaybackSettingsCalculator
return version.DisplayAspectRatio != $"{version.Width}:{version.Height}";
}
- private static DisplaySize SARSize(MediaVersion version)
+ private static (double, double) SARSize(string sampleAspectRatio)
{
- string[] split = version.SampleAspectRatio.Split(":");
- return new DisplaySize(
- int.Parse(split[0], CultureInfo.InvariantCulture),
- int.Parse(split[1], CultureInfo.InvariantCulture));
+ string[] split = sampleAspectRatio.Split(":");
+ return (double.Parse(split[0], CultureInfo.InvariantCulture),
+ double.Parse(split[1], CultureInfo.InvariantCulture));
}
}
diff --git a/ErsatzTV.FFmpeg.Tests/ErsatzTV.FFmpeg.Tests.csproj b/ErsatzTV.FFmpeg.Tests/ErsatzTV.FFmpeg.Tests.csproj
index b39dd78b8..459b99776 100644
--- a/ErsatzTV.FFmpeg.Tests/ErsatzTV.FFmpeg.Tests.csproj
+++ b/ErsatzTV.FFmpeg.Tests/ErsatzTV.FFmpeg.Tests.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/ErsatzTV.FFmpeg/AspectRatio.cs b/ErsatzTV.FFmpeg/AspectRatio.cs
new file mode 100644
index 000000000..0ba7ce3d0
--- /dev/null
+++ b/ErsatzTV.FFmpeg/AspectRatio.cs
@@ -0,0 +1,30 @@
+using System.Globalization;
+
+namespace ErsatzTV.FFmpeg;
+
+public static class AspectRatio
+{
+ public static string CalculateSAR(int width, int height, string displayAspectRatio)
+ {
+ // first check for decimal DAR
+ if (!double.TryParse(displayAspectRatio, out double dar))
+ {
+ // if not, assume it's a ratio
+ string[] split = displayAspectRatio.Split(':');
+ var num = double.Parse(split[0], CultureInfo.InvariantCulture);
+ var den = double.Parse(split[1], CultureInfo.InvariantCulture);
+ dar = num / den;
+ }
+
+ double res = width / (double)height;
+ var formattedDar = string.Format(
+ CultureInfo.InvariantCulture,
+ dar % 1 == 0 ? "{0:F0}" : "{0:0.############}",
+ dar);
+ var formattedRes = string.Format(
+ CultureInfo.InvariantCulture,
+ res % 1 == 0 ? "{0:F0}" : "{0:0.############}",
+ res);
+ return $"{formattedDar}:{formattedRes}";
+ }
+}
diff --git a/ErsatzTV.Infrastructure.Tests/ErsatzTV.Infrastructure.Tests.csproj b/ErsatzTV.Infrastructure.Tests/ErsatzTV.Infrastructure.Tests.csproj
index 2d105aa4f..a6d91391c 100644
--- a/ErsatzTV.Infrastructure.Tests/ErsatzTV.Infrastructure.Tests.csproj
+++ b/ErsatzTV.Infrastructure.Tests/ErsatzTV.Infrastructure.Tests.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs
index 810a65ecb..9bfbba551 100644
--- a/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs
+++ b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs
@@ -17,6 +17,7 @@ using ErsatzTV.Core.Interfaces.Streaming;
using ErsatzTV.Core.Metadata;
using ErsatzTV.FFmpeg;
using ErsatzTV.FFmpeg.Capabilities;
+using ErsatzTV.FFmpeg.Capabilities.Nvidia;
using ErsatzTV.FFmpeg.Filter;
using ErsatzTV.FFmpeg.Filter.Cuda;
using ErsatzTV.FFmpeg.Filter.Qsv;
@@ -112,7 +113,7 @@ public class TranscodingTests
public static Watermark[] Watermarks =
[
Watermark.None,
- Watermark.PermanentOpaqueScaled,
+ //Watermark.PermanentOpaqueScaled,
// Watermark.PermanentOpaqueActualSize,
// Watermark.PermanentTransparentScaled,
// Watermark.PermanentTransparentActualSize
@@ -121,7 +122,7 @@ public class TranscodingTests
public static Subtitle[] Subtitles =
[
Subtitle.None,
- Subtitle.Picture,
+ //Subtitle.Picture,
// Subtitle.Text
];
@@ -133,8 +134,8 @@ public class TranscodingTests
public static ScalingBehavior[] ScalingBehaviors =
[
- ScalingBehavior.ScaleAndPad
- //ScalingBehavior.Crop,
+ ScalingBehavior.ScaleAndPad,
+ ScalingBehavior.Crop,
//ScalingBehavior.Stretch
];
@@ -150,20 +151,20 @@ public class TranscodingTests
new("libx264", "yuv420p", "tv", "smpte170m", "bt709", "smpte170m"),
// // //
// // // // 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", "yuv420p10le"),
// // // // new("libx264", "yuv444p10le"),
// // //
// // // // new("mpeg1video", "yuv420p"),
// // // //
- new("mpeg2video", "yuv420p"),
+ //new("mpeg2video", "yuv420p"),
// //
//new InputFormat("libx265", "yuv420p"),
- new("libx265", "yuv420p10le"),
+ //new("libx265", "yuv420p10le"),
//
- new("mpeg4", "yuv420p")
+ //new("mpeg4", "yuv420p")
//
// new("libvpx-vp9", "yuv420p"),
// new("libvpx-vp9", "yuv420p10le"),
@@ -187,7 +188,7 @@ public class TranscodingTests
public static FFmpegProfileBitDepth[] BitDepths =
[
FFmpegProfileBitDepth.EightBit,
- FFmpegProfileBitDepth.TenBit
+ //FFmpegProfileBitDepth.TenBit
];
public static FFmpegProfileVideoFormat[] VideoFormats =
@@ -443,6 +444,8 @@ public class TranscodingTests
[ValueSource(typeof(TestData), nameof(TestData.StreamingModes))]
StreamingMode streamingMode)
{
+ NvEncSharpRedirector.Init();
+
string file = fileToTest;
if (string.IsNullOrWhiteSpace(file))
{
diff --git a/ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj b/ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj
index 5717300b7..9551d8c08 100644
--- a/ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj
+++ b/ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj
@@ -10,7 +10,7 @@
-
+
diff --git a/ErsatzTV/ErsatzTV.csproj b/ErsatzTV/ErsatzTV.csproj
index 2698b65d1..19b190976 100644
--- a/ErsatzTV/ErsatzTV.csproj
+++ b/ErsatzTV/ErsatzTV.csproj
@@ -54,10 +54,10 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
+