From bc316b3dc855e93d4d11e2c0d73d70326c38b889 Mon Sep 17 00:00:00 2001
From: NoFear0411 <9083405+NoFear0411@users.noreply.github.com>
Date: Sun, 1 Mar 2026 00:00:05 +0400
Subject: Fix near-1:1 SAR values falsely flagged as anamorphic
Encoders sometimes produce sample aspect ratios like 3201:3200
(0.03% off square) for content that has effectively square pixels.
The exact string comparison against "1:1" marks these as anamorphic,
which triggers unnecessary transcoding on clients that require
non-anamorphic video.
Parse the SAR ratio numerically and treat values within 1% of 1:1
as square pixels. This threshold is well clear of the nearest real
anamorphic SAR (PAL 4:3 at 16:15 = 6.67% off).
---
.../Probing/ProbeResultNormalizer.cs | 28 +++++++++++++++++++++-
1 file changed, 27 insertions(+), 1 deletion(-)
(limited to 'MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs')
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index dbe5322897..471df369ba 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -863,7 +863,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
stream.IsAnamorphic = false;
}
- else if (string.Equals(streamInfo.SampleAspectRatio, "1:1", StringComparison.Ordinal))
+ else if (IsNearSquarePixelSar(streamInfo.SampleAspectRatio))
{
stream.IsAnamorphic = false;
}
@@ -1154,6 +1154,32 @@ namespace MediaBrowser.MediaEncoding.Probing
return Math.Abs(d1 - d2) <= variance;
}
+ ///
+ /// Determines whether a sample aspect ratio represents square (or near-square) pixels.
+ /// Some encoders produce SARs like 3201:3200 for content that is effectively 1:1,
+ /// which would be falsely classified as anamorphic by an exact string comparison.
+ /// A 1% tolerance safely covers encoder rounding artifacts while preserving detection
+ /// of genuine anamorphic content (closest standard is PAL 4:3 at 16:15 = 6.67% off).
+ ///
+ internal static bool IsNearSquarePixelSar(string sar)
+ {
+ if (string.IsNullOrEmpty(sar))
+ {
+ return false;
+ }
+
+ var parts = sar.Split(':');
+ if (parts.Length == 2
+ && double.TryParse(parts[0], CultureInfo.InvariantCulture, out var num)
+ && double.TryParse(parts[1], CultureInfo.InvariantCulture, out var den)
+ && den > 0)
+ {
+ return IsClose(num / den, 1.0, 0.01);
+ }
+
+ return string.Equals(sar, "1:1", StringComparison.Ordinal);
+ }
+
///
/// Gets a frame rate from a string value in ffprobe output
/// This could be a number or in the format of 2997/125.
--
cgit v1.2.3
From d87fe973f3516e20ae4c4ecd8036286deeb4b51d Mon Sep 17 00:00:00 2001
From: NoFear0411 <9083405+NoFear0411@users.noreply.github.com>
Date: Sun, 1 Mar 2026 18:51:27 +0400
Subject: Fix StyleCop and xUnit analyzer errors
- Add missing param and returns XML doc tags (SA1611, SA1615)
- Remove trailing alignment whitespace in test attributes (SA1025)
- Use nullable string parameter for null test case (xUnit1012)
---
.../Probing/ProbeResultNormalizer.cs | 2 ++
.../Probing/ProbeResultNormalizerTests.cs | 28 +++++++++++-----------
2 files changed, 16 insertions(+), 14 deletions(-)
(limited to 'MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs')
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 471df369ba..127bdd380d 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -1161,6 +1161,8 @@ namespace MediaBrowser.MediaEncoding.Probing
/// A 1% tolerance safely covers encoder rounding artifacts while preserving detection
/// of genuine anamorphic content (closest standard is PAL 4:3 at 16:15 = 6.67% off).
///
+ /// The sample aspect ratio string in "N:D" format.
+ /// true if the SAR is within 1% of 1:1; otherwise false.
internal static bool IsNearSquarePixelSar(string sar)
{
if (string.IsNullOrEmpty(sar))
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
index 40f853699b..8ebbd029ac 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
@@ -40,20 +40,20 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
=> Assert.Equal(expected, ProbeResultNormalizer.GetFrameRate(value));
[Theory]
- [InlineData("1:1", true)] // exact square pixels
- [InlineData("3201:3200", true)] // 0.03% off — encoder rounding artifact (4K HEVC)
- [InlineData("1215:1216", true)] // 0.08% off — encoder rounding artifact
- [InlineData("1001:1000", true)] // 0.1% off — encoder rounding artifact
- [InlineData("16:15", false)] // 6.67% off — PAL DVD 4:3, genuinely anamorphic
- [InlineData("8:9", false)] // 11.1% off — NTSC DVD 4:3
- [InlineData("32:27", false)] // 18.5% off — NTSC DVD 16:9
- [InlineData("10:11", false)] // 9.1% off — DV NTSC
- [InlineData("64:45", false)] // 42.2% off — PAL DVD 16:9
- [InlineData("4:3", false)] // 33.3% off — classic anamorphic
- [InlineData("0:1", false)] // invalid/unknown SAR
- [InlineData("", false)] // empty
- [InlineData(null, false)] // null
- public void IsNearSquarePixelSar_DetectsCorrectly(string sar, bool expected)
+ [InlineData("1:1", true)]
+ [InlineData("3201:3200", true)]
+ [InlineData("1215:1216", true)]
+ [InlineData("1001:1000", true)]
+ [InlineData("16:15", false)]
+ [InlineData("8:9", false)]
+ [InlineData("32:27", false)]
+ [InlineData("10:11", false)]
+ [InlineData("64:45", false)]
+ [InlineData("4:3", false)]
+ [InlineData("0:1", false)]
+ [InlineData("", false)]
+ [InlineData(null, false)]
+ public void IsNearSquarePixelSar_DetectsCorrectly(string? sar, bool expected)
=> Assert.Equal(expected, ProbeResultNormalizer.IsNearSquarePixelSar(sar));
[Fact]
--
cgit v1.2.3
From a6da575785e678e64ed03978d1f4f60a80423121 Mon Sep 17 00:00:00 2001
From: Bond_009
Date: Sun, 29 Mar 2026 14:16:26 +0200
Subject: Only set IsAvc for video streams
Also enables nullable for MediaStreamInfo
Makes more properties nullable that aren't always present
---
.../Probing/FFProbeHelpers.cs | 4 +-
.../Probing/MediaStreamInfo.cs | 82 +++++++++++-----------
.../Probing/ProbeResultNormalizer.cs | 23 ++----
MediaBrowser.Model/Entities/MediaStream.cs | 1 -
.../Probing/ProbeResultNormalizerTests.cs | 4 +-
5 files changed, 50 insertions(+), 64 deletions(-)
(limited to 'MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs')
diff --git a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
index 6f51e1a6ab..975c2b8161 100644
--- a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
+++ b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
@@ -74,9 +74,9 @@ namespace MediaBrowser.MediaEncoding.Probing
///
/// The dict.
/// Dictionary{System.StringSystem.String}.
- private static Dictionary ConvertDictionaryToCaseInsensitive(IReadOnlyDictionary dict)
+ private static Dictionary ConvertDictionaryToCaseInsensitive(IReadOnlyDictionary dict)
{
- return new Dictionary(dict, StringComparer.OrdinalIgnoreCase);
+ return new Dictionary(dict, StringComparer.OrdinalIgnoreCase);
}
}
}
diff --git a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
index 2944423248..f631c471f6 100644
--- a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
+++ b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Collections.Generic;
using System.Text.Json.Serialization;
@@ -22,21 +20,21 @@ namespace MediaBrowser.MediaEncoding.Probing
///
/// The profile.
[JsonPropertyName("profile")]
- public string Profile { get; set; }
+ public string? Profile { get; set; }
///
/// Gets or sets the codec_name.
///
/// The codec_name.
[JsonPropertyName("codec_name")]
- public string CodecName { get; set; }
+ public string? CodecName { get; set; }
///
/// Gets or sets the codec_long_name.
///
/// The codec_long_name.
[JsonPropertyName("codec_long_name")]
- public string CodecLongName { get; set; }
+ public string? CodecLongName { get; set; }
///
/// Gets or sets the codec_type.
@@ -50,49 +48,49 @@ namespace MediaBrowser.MediaEncoding.Probing
///
/// The sample_rate.
[JsonPropertyName("sample_rate")]
- public string SampleRate { get; set; }
+ public string? SampleRate { get; set; }
///
/// Gets or sets the channels.
///
/// The channels.
[JsonPropertyName("channels")]
- public int Channels { get; set; }
+ public int? Channels { get; set; }
///
/// Gets or sets the channel_layout.
///
/// The channel_layout.
[JsonPropertyName("channel_layout")]
- public string ChannelLayout { get; set; }
+ public string? ChannelLayout { get; set; }
///
/// Gets or sets the avg_frame_rate.
///
/// The avg_frame_rate.
[JsonPropertyName("avg_frame_rate")]
- public string AverageFrameRate { get; set; }
+ public string? AverageFrameRate { get; set; }
///
/// Gets or sets the duration.
///
/// The duration.
[JsonPropertyName("duration")]
- public string Duration { get; set; }
+ public string? Duration { get; set; }
///
/// Gets or sets the bit_rate.
///
/// The bit_rate.
[JsonPropertyName("bit_rate")]
- public string BitRate { get; set; }
+ public string? BitRate { get; set; }
///
/// Gets or sets the width.
///
/// The width.
[JsonPropertyName("width")]
- public int Width { get; set; }
+ public int? Width { get; set; }
///
/// Gets or sets the refs.
@@ -106,21 +104,21 @@ namespace MediaBrowser.MediaEncoding.Probing
///
/// The height.
[JsonPropertyName("height")]
- public int Height { get; set; }
+ public int? Height { get; set; }
///
/// Gets or sets the display_aspect_ratio.
///
/// The display_aspect_ratio.
[JsonPropertyName("display_aspect_ratio")]
- public string DisplayAspectRatio { get; set; }
+ public string? DisplayAspectRatio { get; set; }
///
/// Gets or sets the tags.
///
/// The tags.
[JsonPropertyName("tags")]
- public IReadOnlyDictionary Tags { get; set; }
+ public IReadOnlyDictionary? Tags { get; set; }
///
/// Gets or sets the bits_per_sample.
@@ -141,7 +139,7 @@ namespace MediaBrowser.MediaEncoding.Probing
///
/// The r_frame_rate.
[JsonPropertyName("r_frame_rate")]
- public string RFrameRate { get; set; }
+ public string? RFrameRate { get; set; }
///
/// Gets or sets the has_b_frames.
@@ -155,70 +153,70 @@ namespace MediaBrowser.MediaEncoding.Probing
///
/// The sample_aspect_ratio.
[JsonPropertyName("sample_aspect_ratio")]
- public string SampleAspectRatio { get; set; }
+ public string? SampleAspectRatio { get; set; }
///
/// Gets or sets the pix_fmt.
///
/// The pix_fmt.
[JsonPropertyName("pix_fmt")]
- public string PixelFormat { get; set; }
+ public string? PixelFormat { get; set; }
///
/// Gets or sets the level.
///
/// The level.
[JsonPropertyName("level")]
- public int Level { get; set; }
+ public int? Level { get; set; }
///
/// Gets or sets the time_base.
///
/// The time_base.
[JsonPropertyName("time_base")]
- public string TimeBase { get; set; }
+ public string? TimeBase { get; set; }
///
/// Gets or sets the start_time.
///
/// The start_time.
[JsonPropertyName("start_time")]
- public string StartTime { get; set; }
+ public string? StartTime { get; set; }
///
/// Gets or sets the codec_time_base.
///
/// The codec_time_base.
[JsonPropertyName("codec_time_base")]
- public string CodecTimeBase { get; set; }
+ public string? CodecTimeBase { get; set; }
///
/// Gets or sets the codec_tag.
///
/// The codec_tag.
[JsonPropertyName("codec_tag")]
- public string CodecTag { get; set; }
+ public string? CodecTag { get; set; }
///
- /// Gets or sets the codec_tag_string.
+ /// Gets or sets the codec_tag_string?.
///
- /// The codec_tag_string.
- [JsonPropertyName("codec_tag_string")]
- public string CodecTagString { get; set; }
+ /// The codec_tag_string?.
+ [JsonPropertyName("codec_tag_string?")]
+ public string? CodecTagString { get; set; }
///
/// Gets or sets the sample_fmt.
///
/// The sample_fmt.
[JsonPropertyName("sample_fmt")]
- public string SampleFmt { get; set; }
+ public string? SampleFmt { get; set; }
///
/// Gets or sets the dmix_mode.
///
/// The dmix_mode.
[JsonPropertyName("dmix_mode")]
- public string DmixMode { get; set; }
+ public string? DmixMode { get; set; }
///
/// Gets or sets the start_pts.
@@ -232,90 +230,90 @@ namespace MediaBrowser.MediaEncoding.Probing
///
/// The is_avc.
[JsonPropertyName("is_avc")]
- public bool IsAvc { get; set; }
+ public bool? IsAvc { get; set; }
///
/// Gets or sets the nal_length_size.
///
/// The nal_length_size.
[JsonPropertyName("nal_length_size")]
- public string NalLengthSize { get; set; }
+ public string? NalLengthSize { get; set; }
///
/// Gets or sets the ltrt_cmixlev.
///
/// The ltrt_cmixlev.
[JsonPropertyName("ltrt_cmixlev")]
- public string LtrtCmixlev { get; set; }
+ public string? LtrtCmixlev { get; set; }
///
/// Gets or sets the ltrt_surmixlev.
///
/// The ltrt_surmixlev.
[JsonPropertyName("ltrt_surmixlev")]
- public string LtrtSurmixlev { get; set; }
+ public string? LtrtSurmixlev { get; set; }
///
/// Gets or sets the loro_cmixlev.
///
/// The loro_cmixlev.
[JsonPropertyName("loro_cmixlev")]
- public string LoroCmixlev { get; set; }
+ public string? LoroCmixlev { get; set; }
///
/// Gets or sets the loro_surmixlev.
///
/// The loro_surmixlev.
[JsonPropertyName("loro_surmixlev")]
- public string LoroSurmixlev { get; set; }
+ public string? LoroSurmixlev { get; set; }
///
/// Gets or sets the field_order.
///
/// The field_order.
[JsonPropertyName("field_order")]
- public string FieldOrder { get; set; }
+ public string? FieldOrder { get; set; }
///
/// Gets or sets the disposition.
///
/// The disposition.
[JsonPropertyName("disposition")]
- public IReadOnlyDictionary Disposition { get; set; }
+ public IReadOnlyDictionary? Disposition { get; set; }
///
/// Gets or sets the color range.
///
/// The color range.
[JsonPropertyName("color_range")]
- public string ColorRange { get; set; }
+ public string? ColorRange { get; set; }
///
/// Gets or sets the color space.
///
/// The color space.
[JsonPropertyName("color_space")]
- public string ColorSpace { get; set; }
+ public string? ColorSpace { get; set; }
///
/// Gets or sets the color transfer.
///
/// The color transfer.
[JsonPropertyName("color_transfer")]
- public string ColorTransfer { get; set; }
+ public string? ColorTransfer { get; set; }
///
/// Gets or sets the color primaries.
///
/// The color primaries.
[JsonPropertyName("color_primaries")]
- public string ColorPrimaries { get; set; }
+ public string? ColorPrimaries { get; set; }
///
/// Gets or sets the side_data_list.
///
/// The side_data_list.
[JsonPropertyName("side_data_list")]
- public IReadOnlyList SideDataList { get; set; }
+ public IReadOnlyList? SideDataList { get; set; }
}
}
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 127bdd380d..d3e7b52315 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -697,24 +697,18 @@ namespace MediaBrowser.MediaEncoding.Probing
/// MediaStream.
private MediaStream GetMediaStream(bool isAudio, MediaStreamInfo streamInfo, MediaFormatInfo formatInfo, IReadOnlyList frameInfoList)
{
- // These are mp4 chapters
- if (string.Equals(streamInfo.CodecName, "mov_text", StringComparison.OrdinalIgnoreCase))
- {
- // Edit: but these are also sometimes subtitles?
- // return null;
- }
-
var stream = new MediaStream
{
Codec = streamInfo.CodecName,
Profile = streamInfo.Profile,
+ Width = streamInfo.Width,
+ Height = streamInfo.Height,
Level = streamInfo.Level,
Index = streamInfo.Index,
PixelFormat = streamInfo.PixelFormat,
NalLengthSize = streamInfo.NalLengthSize,
TimeBase = streamInfo.TimeBase,
- CodecTimeBase = streamInfo.CodecTimeBase,
- IsAVC = streamInfo.IsAvc
+ CodecTimeBase = streamInfo.CodecTimeBase
};
// Filter out junk
@@ -774,10 +768,6 @@ namespace MediaBrowser.MediaEncoding.Probing
stream.LocalizedExternal = _localization.GetLocalizedString("External");
stream.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired");
- // Graphical subtitle may have width and height info
- stream.Width = streamInfo.Width;
- stream.Height = streamInfo.Height;
-
if (string.IsNullOrEmpty(stream.Title))
{
// mp4 missing track title workaround: fall back to handler_name if populated and not the default "SubtitleHandler"
@@ -790,6 +780,7 @@ namespace MediaBrowser.MediaEncoding.Probing
}
else if (streamInfo.CodecType == CodecType.Video)
{
+ stream.IsAVC = streamInfo.IsAvc;
stream.AverageFrameRate = GetFrameRate(streamInfo.AverageFrameRate);
stream.RealFrameRate = GetFrameRate(streamInfo.RFrameRate);
@@ -822,8 +813,6 @@ namespace MediaBrowser.MediaEncoding.Probing
stream.Type = MediaStreamType.Video;
}
- stream.Width = streamInfo.Width;
- stream.Height = streamInfo.Height;
stream.AspectRatio = GetAspectRatio(streamInfo);
if (streamInfo.BitsPerSample > 0)
@@ -1091,8 +1080,8 @@ namespace MediaBrowser.MediaEncoding.Probing
&& width > 0
&& height > 0))
{
- width = info.Width;
- height = info.Height;
+ width = info.Width.Value;
+ height = info.Height.Value;
}
if (width > 0 && height > 0)
diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs
index c443af32cf..11f81ff7d8 100644
--- a/MediaBrowser.Model/Entities/MediaStream.cs
+++ b/MediaBrowser.Model/Entities/MediaStream.cs
@@ -2,7 +2,6 @@
#pragma warning disable CS1591
using System;
-using System.Collections.Frozen;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
index 8ebbd029ac..3369af0e84 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
@@ -209,8 +209,8 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
Assert.Equal("mkv,webm", res.Container);
Assert.Equal(2, res.MediaStreams.Count);
-
- Assert.False(res.MediaStreams[0].IsAVC);
+ Assert.Equal(540, res.MediaStreams[0].Width);
+ Assert.Equal(360, res.MediaStreams[0].Height);
}
[Fact]
--
cgit v1.2.3