From b01bdc6110f1e6a96a32920ced1648b5fceb2140 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 20 Oct 2025 16:37:38 +0300 Subject: [PATCH 1/2] Bump FFMpegCore to 5.4.0 --- src/NuGet.Config | 1 - src/NzbDrone.Common/Sonarr.Common.csproj | 2 +- .../MediaInfo/VideoFileInfoReaderFixture.cs | 65 ++++++++-------- .../MediaInfo/MediaInfoFormatter.cs | 5 ++ .../MediaFiles/MediaInfo/MediaInfoModel.cs | 5 +- .../MediaInfo/VideoFileInfoReader.cs | 78 ++++++++++--------- src/NzbDrone.Core/Sonarr.Core.csproj | 6 +- 7 files changed, 83 insertions(+), 79 deletions(-) diff --git a/src/NuGet.Config b/src/NuGet.Config index e07e77531..4bf5263ac 100644 --- a/src/NuGet.Config +++ b/src/NuGet.Config @@ -5,6 +5,5 @@ - diff --git a/src/NzbDrone.Common/Sonarr.Common.csproj b/src/NzbDrone.Common/Sonarr.Common.csproj index d9ab36fd5..e4cbd0558 100644 --- a/src/NzbDrone.Common/Sonarr.Common.csproj +++ b/src/NzbDrone.Common/Sonarr.Common.csproj @@ -17,7 +17,7 @@ - + diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs index 49a3b06d9..126399b51 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs @@ -1,12 +1,11 @@ +using System.Collections.Generic; using System.IO; using System.Linq; -using System.Reflection; -using FFMpegCore; +using System.Text.Json.Nodes; using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Common.Disk; -using NzbDrone.Common.Extensions; using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.Test.Framework; using NzbDrone.Test.Common.Categories; @@ -103,39 +102,39 @@ public void get_info_unicode() info.Title.Should().Be("Sample Title"); } - [TestCase(8, "", "", "", null, HdrFormat.None)] - [TestCase(10, "", "", "", null, HdrFormat.None)] - [TestCase(10, "bt709", "bt709", "", null, HdrFormat.None)] - [TestCase(8, "bt2020", "smpte2084", "", null, HdrFormat.None)] - [TestCase(10, "bt2020", "bt2020-10", "", null, HdrFormat.None)] - [TestCase(10, "bt2020", "arib-std-b67", "", null, HdrFormat.Hlg10)] - [TestCase(10, "bt2020", "smpte2084", "", null, HdrFormat.Pq10)] - [TestCase(10, "bt2020", "smpte2084", "FFMpegCore.SideData", null, HdrFormat.Pq10)] - [TestCase(10, "bt2020", "smpte2084", "FFMpegCore.MasteringDisplayMetadata", null, HdrFormat.Hdr10)] - [TestCase(10, "bt2020", "smpte2084", "FFMpegCore.ContentLightLevelMetadata", null, HdrFormat.Hdr10)] - [TestCase(10, "bt2020", "smpte2084", "FFMpegCore.HdrDynamicMetadataSpmte2094", null, HdrFormat.Hdr10Plus)] - [TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", null, HdrFormat.DolbyVision)] - [TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", 1, HdrFormat.DolbyVisionHdr10)] - [TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData,FFMpegCore.HdrDynamicMetadataSpmte2094", 1, HdrFormat.DolbyVisionHdr10Plus)] - [TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData,FFMpegCore.HdrDynamicMetadataSpmte2094", 6, HdrFormat.DolbyVisionHdr10Plus)] - [TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", 2, HdrFormat.DolbyVisionSdr)] - [TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", 4, HdrFormat.DolbyVisionHlg)] - public void should_detect_hdr_correctly(int bitDepth, string colourPrimaries, string transferFunction, string sideDataTypes, int? doviConfigId, HdrFormat expected) + [TestCase(8, "", "", null, null, HdrFormat.None)] + [TestCase(10, "", "", null, null, HdrFormat.None)] + [TestCase(10, "bt709", "bt709", null, null, HdrFormat.None)] + [TestCase(8, "bt2020", "smpte2084", null, null, HdrFormat.None)] + [TestCase(10, "bt2020", "bt2020-10", null, null, HdrFormat.None)] + [TestCase(10, "bt2020", "arib-std-b67", null, null, HdrFormat.Hlg10)] + [TestCase(10, "bt2020", "smpte2084", null, null, HdrFormat.Pq10)] + [TestCase(10, "bt2020", "smpte2084", new[] { "" }, null, HdrFormat.Pq10)] + [TestCase(10, "bt2020", "smpte2084", new[] { FFMpegCoreSideDataTypes.MasteringDisplayMetadata }, null, HdrFormat.Hdr10)] + [TestCase(10, "bt2020", "smpte2084", new[] { FFMpegCoreSideDataTypes.ContentLightLevelMetadata }, null, HdrFormat.Hdr10)] + [TestCase(10, "bt2020", "smpte2084", new[] { FFMpegCoreSideDataTypes.HdrDynamicMetadataSpmte2094 }, null, HdrFormat.Hdr10Plus)] + [TestCase(10, "bt2020", "smpte2084", new[] { FFMpegCoreSideDataTypes.DoviConfigurationRecordSideData }, null, HdrFormat.DolbyVision)] + [TestCase(10, "bt2020", "smpte2084", new[] { FFMpegCoreSideDataTypes.DoviConfigurationRecordSideData }, 1, HdrFormat.DolbyVisionHdr10)] + [TestCase(10, "bt2020", "smpte2084", new[] { FFMpegCoreSideDataTypes.DoviConfigurationRecordSideData, FFMpegCoreSideDataTypes.HdrDynamicMetadataSpmte2094 }, 1, HdrFormat.DolbyVisionHdr10Plus)] + [TestCase(10, "bt2020", "smpte2084", new[] { FFMpegCoreSideDataTypes.DoviConfigurationRecordSideData, FFMpegCoreSideDataTypes.HdrDynamicMetadataSpmte2094 }, 6, HdrFormat.DolbyVisionHdr10Plus)] + [TestCase(10, "bt2020", "smpte2084", new[] { FFMpegCoreSideDataTypes.DoviConfigurationRecordSideData }, 2, HdrFormat.DolbyVisionSdr)] + [TestCase(10, "bt2020", "smpte2084", new[] { FFMpegCoreSideDataTypes.DoviConfigurationRecordSideData }, 4, HdrFormat.DolbyVisionHlg)] + public void should_detect_hdr_correctly(int bitDepth, string colourPrimaries, string transferFunction, string[] sideDataTypes, int? doviConfigId, HdrFormat expected) { - var assembly = Assembly.GetAssembly(typeof(FFProbe)); - var types = sideDataTypes.Split(",").Select(x => x.Trim()).ToList(); - var sideData = types.Where(x => x.IsNotNullOrWhiteSpace()).Select(x => assembly.CreateInstance(x)).Cast().ToList(); - - if (doviConfigId.HasValue) + var sideData = sideDataTypes?.Select(sideDataType => { - sideData.ForEach(x => + var sideData = new Dictionary { - if (x.GetType().Name == "DoviConfigurationRecordSideData") - { - ((DoviConfigurationRecordSideData)x).DvBlSignalCompatibilityId = doviConfigId.Value; - } - }); - } + { "side_data_type", JsonValue.Create(sideDataType) } + }; + + if (doviConfigId.HasValue) + { + sideData.Add("dv_bl_signal_compatibility_id", JsonValue.Create(doviConfigId.Value)); + } + + return sideData; + }).ToList(); var result = VideoFileInfoReader.GetHdrFormat(bitDepth, colourPrimaries, transferFunction, sideData); diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs index 203bd6714..68d77752f 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs @@ -117,6 +117,11 @@ public static string FormatAudioCodec(MediaInfoModel mediaInfo, string sceneName return "HE-AAC"; } + if (audioProfile == "xHE-AAC") + { + return "xHE-AAC"; + } + return "AAC"; } diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs index 41b40596d..197c9b88c 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; -using FFMpegCore; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.MediaFiles.MediaInfo @@ -9,7 +8,7 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo public class MediaInfoModel : IEmbeddedDocument { public string RawStreamData { get; set; } - public string RawFrameData { get; set; } + public int SchemaRevision { get; set; } public string ContainerFormat { get; set; } @@ -27,8 +26,6 @@ public class MediaInfoModel : IEmbeddedDocument public string VideoTransferCharacteristics { get; set; } - public DoviConfigurationRecordSideData DoviConfigurationRecord { get; set; } - public HdrFormat VideoHdrFormat { get; set; } public int Height { get; set; } diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs index 2b9379db7..deef4daca 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.Json.Nodes; using FFMpegCore; using NLog; using NzbDrone.Common.Disk; @@ -19,7 +20,6 @@ public class VideoFileInfoReader : IVideoFileInfoReader { private readonly IDiskProvider _diskProvider; private readonly Logger _logger; - private readonly List _pixelFormats; public const int MINIMUM_MEDIA_INFO_SCHEMA_REVISION = 12; public const int CURRENT_MEDIA_INFO_SCHEMA_REVISION = 12; @@ -36,16 +36,6 @@ public VideoFileInfoReader(IDiskProvider diskProvider, Logger logger) // We bundle ffprobe for all platforms GlobalFFOptions.Configure(options => options.BinaryFolder = AppDomain.CurrentDomain.BaseDirectory); - - try - { - _pixelFormats = FFProbe.GetPixelFormats(); - } - catch (Exception e) - { - _logger.Error(e, "Failed to get supported pixel formats from ffprobe"); - _pixelFormats = new List(); - } } public MediaInfoModel GetMediaInfo(string filename) @@ -65,15 +55,13 @@ public MediaInfoModel GetMediaInfo(string filename) try { _logger.Debug("Getting media info from {0}", filename); - var ffprobeOutput = FFProbe.GetStreamJson(filename, ffOptions: new FFOptions { ExtraArguments = "-probesize 50000000" }); - var analysis = FFProbe.AnalyseStreamJson(ffprobeOutput); + var analysis = FFProbe.Analyse(filename, customArguments: "-probesize 50000000"); var primaryVideoStream = GetPrimaryVideoStream(analysis); if (analysis.PrimaryAudioStream?.ChannelLayout.IsNullOrWhiteSpace() ?? true) { - ffprobeOutput = FFProbe.GetStreamJson(filename, ffOptions: new FFOptions { ExtraArguments = "-probesize 150000000 -analyzeduration 150000000" }); - analysis = FFProbe.AnalyseStreamJson(ffprobeOutput); + analysis = FFProbe.Analyse(filename, customArguments: "-probesize 150000000 -analyzeduration 150000000"); } var mediaInfoModel = new MediaInfoModel(); @@ -85,7 +73,6 @@ public MediaInfoModel GetMediaInfo(string filename) mediaInfoModel.VideoBitDepth = GetPixelFormat(primaryVideoStream?.PixelFormat)?.Components.Min(x => x.BitDepth) ?? 8; mediaInfoModel.VideoColourPrimaries = primaryVideoStream?.ColorPrimaries; mediaInfoModel.VideoTransferCharacteristics = primaryVideoStream?.ColorTransfer; - mediaInfoModel.DoviConfigurationRecord = primaryVideoStream?.SideDataList?.Find(x => x.GetType().Name == nameof(DoviConfigurationRecordSideData)) as DoviConfigurationRecordSideData; mediaInfoModel.Height = primaryVideoStream?.Height ?? 0; mediaInfoModel.Width = primaryVideoStream?.Width ?? 0; mediaInfoModel.AudioFormat = analysis.PrimaryAudioStream?.CodecName; @@ -103,8 +90,12 @@ public MediaInfoModel GetMediaInfo(string filename) mediaInfoModel.Subtitles = analysis.SubtitleStreams?.Select(x => x.Language) .Where(l => l.IsNotNullOrWhiteSpace()) .ToList(); - mediaInfoModel.ScanType = "Progressive"; - mediaInfoModel.RawStreamData = ffprobeOutput; + mediaInfoModel.ScanType = primaryVideoStream?.FieldOrder switch + { + "tt" or "bb" or "tb" or "bt" => "Interlaced", + _ => "Progressive" + }; + mediaInfoModel.RawStreamData = string.Concat(analysis.OutputData); mediaInfoModel.SchemaRevision = CURRENT_MEDIA_INFO_SCHEMA_REVISION; if (analysis.Format.Tags?.TryGetValue("title", out var title) ?? false) @@ -117,14 +108,11 @@ public MediaInfoModel GetMediaInfo(string filename) // if it looks like PQ10 or similar HDR, do a frame analysis to figure out which type it is if (PqTransferFunctions.Contains(mediaInfoModel.VideoTransferCharacteristics)) { - var frameOutput = FFProbe.GetFrameJson(filename, ffOptions: new() { ExtraArguments = $"-read_intervals \"%+#1\" -select_streams v:{primaryVideoStream?.Index ?? 0}" }); - mediaInfoModel.RawFrameData = frameOutput; - - frames = FFProbe.AnalyseFrameJson(frameOutput); + frames = FFProbe.GetFrames(filename, customArguments: $"-read_intervals \"%+#1\" -select_streams v:{primaryVideoStream?.Index ?? 0}"); } - var streamSideData = primaryVideoStream?.SideDataList ?? new(); - var framesSideData = frames?.Frames?.Count > 0 ? frames?.Frames[0]?.SideDataList ?? new() : new(); + var streamSideData = primaryVideoStream?.SideData ?? new(); + var framesSideData = frames?.Frames.FirstOrDefault()?.SideData ?? new(); var sideData = streamSideData.Concat(framesSideData).ToList(); mediaInfoModel.VideoHdrFormat = GetHdrFormat(mediaInfoModel.VideoBitDepth, mediaInfoModel.VideoColourPrimaries, mediaInfoModel.VideoTransferCharacteristics, sideData); @@ -176,7 +164,7 @@ private static long GetBitrate(MediaStream mediaStream) return 0; } - private VideoStream GetPrimaryVideoStream(IMediaAnalysis mediaAnalysis) + private static VideoStream GetPrimaryVideoStream(IMediaAnalysis mediaAnalysis) { if (mediaAnalysis.VideoStreams.Count <= 1) { @@ -189,23 +177,30 @@ private VideoStream GetPrimaryVideoStream(IMediaAnalysis mediaAnalysis) return mediaAnalysis.VideoStreams.FirstOrDefault(s => !codecFilter.Contains(s.CodecName)) ?? mediaAnalysis.PrimaryVideoStream; } - private FFProbePixelFormat GetPixelFormat(string format) + private static FFProbePixelFormat GetPixelFormat(string format) { - return _pixelFormats.Find(x => x.Name == format); + if (format.IsNullOrWhiteSpace()) + { + return null; + } + + return FFProbe.TryGetPixelFormat(format, out var pixelFormat) ? pixelFormat : null; } - public static HdrFormat GetHdrFormat(int bitDepth, string colorPrimaries, string transferFunction, List sideData) + public static HdrFormat GetHdrFormat(int bitDepth, string colorPrimaries, string transferFunction, List> sideData) { if (bitDepth < 10) { return HdrFormat.None; } - if (TryGetSideData(sideData, out var dovi)) + if (TryGetSideData(sideData, FFMpegCoreSideDataTypes.DoviConfigurationRecordSideData, out var dovi)) { - var hasHdr10Plus = TryGetSideData(sideData, out _); + var hasHdr10Plus = TryGetSideData(sideData, FFMpegCoreSideDataTypes.HdrDynamicMetadataSpmte2094, out _); - return dovi.DvBlSignalCompatibilityId switch + dovi.TryGetValue("dv_bl_signal_compatibility_id", out var dvBlSignalCompatibilityId); + + return dvBlSignalCompatibilityId?.GetValue() switch { 1 => hasHdr10Plus ? HdrFormat.DolbyVisionHdr10Plus : HdrFormat.DolbyVisionHdr10, 2 => HdrFormat.DolbyVisionSdr, @@ -227,13 +222,13 @@ public static HdrFormat GetHdrFormat(int bitDepth, string colorPrimaries, string if (PqTransferFunctions.Contains(transferFunction)) { - if (TryGetSideData(sideData, out _)) + if (TryGetSideData(sideData, FFMpegCoreSideDataTypes.HdrDynamicMetadataSpmte2094, out _)) { return HdrFormat.Hdr10Plus; } - if (TryGetSideData(sideData, out _) || - TryGetSideData(sideData, out _)) + if (TryGetSideData(sideData, FFMpegCoreSideDataTypes.MasteringDisplayMetadata, out _) || + TryGetSideData(sideData, FFMpegCoreSideDataTypes.ContentLightLevelMetadata, out _)) { return HdrFormat.Hdr10; } @@ -244,12 +239,21 @@ public static HdrFormat GetHdrFormat(int bitDepth, string colorPrimaries, string return HdrFormat.None; } - private static bool TryGetSideData(List list, out T result) - where T : SideData + private static bool TryGetSideData(IReadOnlyList> list, string name, out Dictionary result) { - result = (T)list?.FirstOrDefault(x => x.GetType().Name == typeof(T).Name); + result = list?.FirstOrDefault(item => + item.TryGetValue("side_data_type", out var rawSideDataType) && + rawSideDataType.GetValue().Equals(name, StringComparison.OrdinalIgnoreCase)); return result != null; } } + + public sealed class FFMpegCoreSideDataTypes + { + public const string DoviConfigurationRecordSideData = "DOVI configuration record"; + public const string HdrDynamicMetadataSpmte2094 = "HDR Dynamic Metadata SMPTE2094-40 (HDR10+)"; + public const string MasteringDisplayMetadata = "Mastering display metadata"; + public const string ContentLightLevelMetadata = "Content light level metadata"; + } } diff --git a/src/NzbDrone.Core/Sonarr.Core.csproj b/src/NzbDrone.Core/Sonarr.Core.csproj index 95d383318..12fd12bb7 100644 --- a/src/NzbDrone.Core/Sonarr.Core.csproj +++ b/src/NzbDrone.Core/Sonarr.Core.csproj @@ -9,9 +9,9 @@ + + - - @@ -25,7 +25,7 @@ - + From e77d949efade76d67e5537425b28c9eba1df3b02 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 5 Dec 2025 21:14:09 +0200 Subject: [PATCH 2/2] fixup! Bump FFMpegCore to 5.4.0 --- .../MediaFiles/MediaInfo/MediaInfoFormatter.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs index 68d77752f..fff7f11bb 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs @@ -52,7 +52,11 @@ public static string FormatAudioCodec(MediaInfoModel mediaInfo, string sceneName if (audioFormat == "truehd") { - return "TrueHD"; + return audioProfile switch + { + "Dolby TrueHD + Dolby Atmos" => "TrueHD Atmos", + _ => "TrueHD" + }; } if (audioFormat == "flac") @@ -62,7 +66,7 @@ public static string FormatAudioCodec(MediaInfoModel mediaInfo, string sceneName if (audioFormat == "dts") { - if (audioProfile == "DTS:X") + if (audioProfile is "DTS:X" or "DTS-HD MA + DTS:X IMAX") { return "DTS-X"; } @@ -102,7 +106,11 @@ public static string FormatAudioCodec(MediaInfoModel mediaInfo, string sceneName if (audioFormat == "eac3") { - return "EAC3"; + return audioProfile switch + { + "Dolby Digital Plus + Dolby Atmos" => "EAC3 Atmos", + _ => "EAC3" + }; } if (audioFormat == "ac3")