diff --git a/CHANGELOG.md b/CHANGELOG.md index c3dd7202..333a3855 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - This bug made it difficult to "stop" a channel after previewing it - Fix bug where deco default filler would never use hardware acceleration - Fix deleting local libraries with MySql backend +- Fix `Scaling Behavior` `Crop` when content is smaller than FFmpeg Profile resolution + - Now, content will properly scale beyond the desired resolution before cropping ## [0.8.8-beta] - 2024-09-19 ### Added diff --git a/ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsCalculatorTests.cs b/ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsCalculatorTests.cs index 3a052ea4..97413780 100644 --- a/ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsCalculatorTests.cs +++ b/ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsCalculatorTests.cs @@ -356,6 +356,66 @@ public class FFmpegPlaybackSettingsCalculatorTests actual.PadToDesiredResolution.Should().BeTrue(); } + [Test] + public void Should_ScaleBeyondMinSize_ForCrop_ForTransportStream() + { + FFmpegProfile ffmpegProfile = TestProfile() with + { + Resolution = new Resolution { Width = 1280, Height = 720 }, + ScalingBehavior = ScalingBehavior.Crop + }; + + var version = new MediaVersion { Width = 944, Height = 720, SampleAspectRatio = "1:1" }; + + FFmpegPlaybackSettings actual = FFmpegPlaybackSettingsCalculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream(), + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + IDisplaySize scaledSize = actual.ScaledSize.IfNone(new MediaVersion { Width = 0, Height = 0 }); + scaledSize.Width.Should().Be(1280); + scaledSize.Height.Should().Be(976); + actual.PadToDesiredResolution.Should().BeFalse(); + } + + [Test] + public void Should_ScaleDownToMinSize_ForCrop_ForTransportStream() + { + FFmpegProfile ffmpegProfile = TestProfile() with + { + Resolution = new Resolution { Width = 1280, Height = 720 }, + ScalingBehavior = ScalingBehavior.Crop + }; + + var version = new MediaVersion { Width = 1920, Height = 816, SampleAspectRatio = "1:1" }; + + FFmpegPlaybackSettings actual = FFmpegPlaybackSettingsCalculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream(), + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + IDisplaySize scaledSize = actual.ScaledSize.IfNone(new MediaVersion { Width = 0, Height = 0 }); + scaledSize.Width.Should().Be(1694); + scaledSize.Height.Should().Be(720); + actual.PadToDesiredResolution.Should().BeFalse(); + } + [Test] public void Should_NotPadToDesiredResolution_When_UnscaledContentIsUnderSized_ForHttpLiveStreaming() { diff --git a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs index 02f0f3c8..3721080f 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs @@ -335,8 +335,24 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService if (channel.FFmpegProfile.ScalingBehavior is ScalingBehavior.Crop) { - paddedSize = ffmpegVideoStream.SquarePixelFrameSizeForCrop( - new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height)); + bool isTooSmallToCrop = videoVersion.Height < channel.FFmpegProfile.Resolution.Height || + videoVersion.Width < channel.FFmpegProfile.Resolution.Width; + + // if any dimension is smaller than the crop, scale beyond the crop (beyond the target resolution) + if (isTooSmallToCrop) + { + foreach (IDisplaySize size in playbackSettings.ScaledSize) + { + scaledSize = new FrameSize(size.Width, size.Height); + } + + paddedSize = scaledSize; + } + else + { + paddedSize = ffmpegVideoStream.SquarePixelFrameSizeForCrop( + new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height)); + } cropSize = new FrameSize( channel.FFmpegProfile.Resolution.Width, diff --git a/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs b/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs index 65d747f1..addffcb0 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs @@ -99,7 +99,7 @@ public static class FFmpegPlaybackSettingsCalculator } IDisplaySize sizeAfterScaling = result.ScaledSize.IfNone(videoVersion); - if (!sizeAfterScaling.IsSameSizeAs(ffmpegProfile.Resolution)) + if (!sizeAfterScaling.IsSameSizeAs(ffmpegProfile.Resolution) && ffmpegProfile.ScalingBehavior is not ScalingBehavior.Crop) { result.PadToDesiredResolution = true; } @@ -229,7 +229,8 @@ public static class FFmpegPlaybackSettingsCalculator private static bool NeedToScale(FFmpegProfile ffmpegProfile, MediaVersion version) => IsIncorrectSize(ffmpegProfile.Resolution, version) || IsTooLarge(ffmpegProfile.Resolution, version) || - IsOddSize(version); + IsOddSize(version) || + TooSmallToCrop(ffmpegProfile, version); private static bool IsIncorrectSize(IDisplaySize desiredResolution, MediaVersion version) => IsAnamorphic(version) || @@ -243,6 +244,16 @@ public static class FFmpegPlaybackSettingsCalculator private static bool IsOddSize(MediaVersion version) => version.Height % 2 == 1 || version.Width % 2 == 1; + private static bool TooSmallToCrop(FFmpegProfile ffmpegProfile, MediaVersion version) + { + if (ffmpegProfile.ScalingBehavior is not ScalingBehavior.Crop) + { + return false; + } + + return version.Height < ffmpegProfile.Resolution.Height || version.Width < ffmpegProfile.Resolution.Width; + } + private static DisplaySize CalculateScaledSize(FFmpegProfile ffmpegProfile, MediaVersion version) { IDisplaySize sarSize = SARSize(version); @@ -256,12 +267,20 @@ public static class FFmpegPlaybackSettingsCalculator int hh1 = hw1 * q / p; int hh2 = targetSize.Height; int hw2 = targetSize.Height * p / q; - if (hh1 <= targetSize.Height) + + // crop needs to return whichever version has *both* dimensions >= required + // because it will never pad + if (ffmpegProfile.ScalingBehavior is ScalingBehavior.Crop) { - return new DisplaySize(hw1, hh1); + if (hw1 >= targetSize.Width && hh1 >= targetSize.Height) + { + return new DisplaySize(hw1, hh1); + } + + return new DisplaySize(hw2, hh2); } - return new DisplaySize(hw2, hh2); + return hh1 <= targetSize.Height ? new DisplaySize(hw1, hh1) : new DisplaySize(hw2, hh2); } private static int Gcd(int a, int b)