Browse Source

Improve ffprobe json parsing and don't log error for Codec Type attachment

Bond_009 2 years ago
parent
commit
65d605b17d

+ 3 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs

@@ -14,6 +14,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Extensions;
 using Jellyfin.Extensions.Json;
+using Jellyfin.Extensions.Json.Converters;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
@@ -58,7 +59,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             _socketFactory = socketFactory;
             _streamHelper = streamHelper;
 
-            _jsonOptions = JsonDefaults.Options;
+            _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options);
+            _jsonOptions.Converters.Add(new JsonBoolNumberConverter());
         }
 
         public string Name => "HD Homerun";

+ 4 - 1
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs

@@ -12,6 +12,7 @@ using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Extensions.Json;
+using Jellyfin.Extensions.Json.Converters;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
@@ -105,7 +106,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
             _config = config;
             _serverConfig = serverConfig;
             _startupOptionFFmpegPath = config.GetValue<string>(Controller.Extensions.ConfigurationExtensions.FfmpegPathKey) ?? string.Empty;
-            _jsonSerializerOptions = JsonDefaults.Options;
+
+            _jsonSerializerOptions = new JsonSerializerOptions(JsonDefaults.Options);
+            _jsonSerializerOptions.Converters.Add(new JsonBoolStringConverter());
         }
 
         /// <inheritdoc />

+ 32 - 0
MediaBrowser.MediaEncoding/Probing/CodecType.cs

@@ -0,0 +1,32 @@
+namespace MediaBrowser.MediaEncoding.Probing;
+
+/// <summary>
+/// FFmpeg Codec Type.
+/// </summary>
+public enum CodecType
+{
+    /// <summary>
+    /// Video.
+    /// </summary>
+    Video,
+
+    /// <summary>
+    /// Audio.
+    /// </summary>
+    Audio,
+
+    /// <summary>
+    /// Opaque data information usually continuous.
+    /// </summary>
+    Data,
+
+    /// <summary>
+    /// Subtitles.
+    /// </summary>
+    Subtitle,
+
+    /// <summary>
+    /// Opaque data information usually sparse.
+    /// </summary>
+    Attachment
+}

+ 3 - 3
MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs

@@ -43,7 +43,7 @@ namespace MediaBrowser.MediaEncoding.Probing
         /// </summary>
         /// <value>The codec_type.</value>
         [JsonPropertyName("codec_type")]
-        public string CodecType { get; set; }
+        public CodecType CodecType { get; set; }
 
         /// <summary>
         /// Gets or sets the sample_rate.
@@ -228,11 +228,11 @@ namespace MediaBrowser.MediaEncoding.Probing
         public long StartPts { get; set; }
 
         /// <summary>
-        /// Gets or sets the is_avc.
+        /// Gets or sets a value indicating whether the stream is AVC.
         /// </summary>
         /// <value>The is_avc.</value>
         [JsonPropertyName("is_avc")]
-        public string IsAvc { get; set; }
+        public bool IsAvc { get; set; }
 
         /// <summary>
         /// Gets or sets the nal_length_size.

+ 35 - 64
MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs

@@ -107,9 +107,9 @@ namespace MediaBrowser.MediaEncoding.Probing
             }
 
             var tags = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-            var tagStreamType = isAudio ? "audio" : "video";
+            var tagStreamType = isAudio ? CodecType.Audio : CodecType.Video;
 
-            var tagStream = data.Streams?.FirstOrDefault(i => string.Equals(i.CodecType, tagStreamType, StringComparison.OrdinalIgnoreCase));
+            var tagStream = data.Streams?.FirstOrDefault(i => i.CodecType == tagStreamType);
 
             if (tagStream?.Tags is not null)
             {
@@ -599,7 +599,7 @@ namespace MediaBrowser.MediaEncoding.Probing
         /// <returns>MediaAttachments.</returns>
         private MediaAttachment GetMediaAttachment(MediaStreamInfo streamInfo)
         {
-            if (!string.Equals(streamInfo.CodecType, "attachment", StringComparison.OrdinalIgnoreCase)
+            if (streamInfo.CodecType != CodecType.Attachment
                 && streamInfo.Disposition?.GetValueOrDefault("attached_pic") != 1)
             {
                 return null;
@@ -651,20 +651,10 @@ namespace MediaBrowser.MediaEncoding.Probing
                 PixelFormat = streamInfo.PixelFormat,
                 NalLengthSize = streamInfo.NalLengthSize,
                 TimeBase = streamInfo.TimeBase,
-                CodecTimeBase = streamInfo.CodecTimeBase
+                CodecTimeBase = streamInfo.CodecTimeBase,
+                IsAVC = streamInfo.IsAvc
             };
 
-            if (string.Equals(streamInfo.IsAvc, "true", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(streamInfo.IsAvc, "1", StringComparison.OrdinalIgnoreCase))
-            {
-                stream.IsAVC = true;
-            }
-            else if (string.Equals(streamInfo.IsAvc, "false", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(streamInfo.IsAvc, "0", StringComparison.OrdinalIgnoreCase))
-            {
-                stream.IsAVC = false;
-            }
-
             // Filter out junk
             if (!string.IsNullOrWhiteSpace(streamInfo.CodecTagString) && !streamInfo.CodecTagString.Contains("[0]", StringComparison.OrdinalIgnoreCase))
             {
@@ -678,18 +668,15 @@ namespace MediaBrowser.MediaEncoding.Probing
                 stream.Title = GetDictionaryValue(streamInfo.Tags, "title");
             }
 
-            if (string.Equals(streamInfo.CodecType, "audio", StringComparison.OrdinalIgnoreCase))
+            if (streamInfo.CodecType == CodecType.Audio)
             {
                 stream.Type = MediaStreamType.Audio;
 
                 stream.Channels = streamInfo.Channels;
 
-                if (!string.IsNullOrEmpty(streamInfo.SampleRate))
+                if (int.TryParse(streamInfo.SampleRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
                 {
-                    if (int.TryParse(streamInfo.SampleRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
-                    {
-                        stream.SampleRate = value;
-                    }
+                    stream.SampleRate = value;
                 }
 
                 stream.ChannelLayout = ParseChannelLayout(streamInfo.ChannelLayout);
@@ -713,7 +700,7 @@ namespace MediaBrowser.MediaEncoding.Probing
                     }
                 }
             }
-            else if (string.Equals(streamInfo.CodecType, "subtitle", StringComparison.OrdinalIgnoreCase))
+            else if (streamInfo.CodecType == CodecType.Subtitle)
             {
                 stream.Type = MediaStreamType.Subtitle;
                 stream.Codec = NormalizeSubtitleCodec(stream.Codec);
@@ -733,7 +720,7 @@ namespace MediaBrowser.MediaEncoding.Probing
                     }
                 }
             }
-            else if (string.Equals(streamInfo.CodecType, "video", StringComparison.OrdinalIgnoreCase))
+            else if (streamInfo.CodecType == CodecType.Video)
             {
                 stream.AverageFrameRate = GetFrameRate(streamInfo.AverageFrameRate);
                 stream.RealFrameRate = GetFrameRate(streamInfo.RFrameRate);
@@ -854,13 +841,12 @@ namespace MediaBrowser.MediaEncoding.Probing
                     }
                 }
             }
-            else if (string.Equals(streamInfo.CodecType, "data", StringComparison.OrdinalIgnoreCase))
+            else if (streamInfo.CodecType == CodecType.Data)
             {
                 stream.Type = MediaStreamType.Data;
             }
             else
             {
-                _logger.LogError("Codec Type {CodecType} unknown. The stream (index: {Index}) will be ignored. Warning: Subsequential streams will have a wrong stream specifier!", streamInfo.CodecType, streamInfo.Index);
                 return null;
             }
 
@@ -895,29 +881,26 @@ namespace MediaBrowser.MediaEncoding.Probing
 
             // Extract bitrate info from tag "BPS" if possible.
             if (!stream.BitRate.HasValue
-                && (string.Equals(streamInfo.CodecType, "audio", StringComparison.OrdinalIgnoreCase)
-                    || string.Equals(streamInfo.CodecType, "video", StringComparison.OrdinalIgnoreCase)))
+                && (streamInfo.CodecType == CodecType.Audio
+                    || streamInfo.CodecType == CodecType.Video))
             {
                 var bps = GetBPSFromTags(streamInfo);
                 if (bps > 0)
                 {
                     stream.BitRate = bps;
                 }
-            }
-
-            // Get average bitrate info from tag "NUMBER_OF_BYTES" and "DURATION" if possible.
-            if (!stream.BitRate.HasValue
-                && (string.Equals(streamInfo.CodecType, "audio", StringComparison.OrdinalIgnoreCase)
-                    || string.Equals(streamInfo.CodecType, "video", StringComparison.OrdinalIgnoreCase)))
-            {
-                var durationInSeconds = GetRuntimeSecondsFromTags(streamInfo);
-                var bytes = GetNumberOfBytesFromTags(streamInfo);
-                if (durationInSeconds is not null && bytes is not null)
+                else
                 {
-                    var bps = Convert.ToInt32(bytes * 8 / durationInSeconds, CultureInfo.InvariantCulture);
-                    if (bps > 0)
+                    // Get average bitrate info from tag "NUMBER_OF_BYTES" and "DURATION" if possible.
+                    var durationInSeconds = GetRuntimeSecondsFromTags(streamInfo);
+                    var bytes = GetNumberOfBytesFromTags(streamInfo);
+                    if (durationInSeconds is not null && bytes is not null)
                     {
-                        stream.BitRate = bps;
+                        bps = Convert.ToInt32(bytes * 8 / durationInSeconds, CultureInfo.InvariantCulture);
+                        if (bps > 0)
+                        {
+                            stream.BitRate = bps;
+                        }
                     }
                 }
             }
@@ -948,12 +931,8 @@ namespace MediaBrowser.MediaEncoding.Probing
 
         private void NormalizeStreamTitle(MediaStream stream)
         {
-            if (string.Equals(stream.Title, "cc", StringComparison.OrdinalIgnoreCase))
-            {
-                stream.Title = null;
-            }
-
-            if (stream.Type == MediaStreamType.EmbeddedImage)
+            if (string.Equals(stream.Title, "cc", StringComparison.OrdinalIgnoreCase)
+                || stream.Type == MediaStreamType.EmbeddedImage)
             {
                 stream.Title = null;
             }
@@ -984,7 +963,7 @@ namespace MediaBrowser.MediaEncoding.Probing
                 return null;
             }
 
-            return input.Split('(').FirstOrDefault();
+            return input.AsSpan().LeftPart('(').ToString();
         }
 
         private string GetAspectRatio(MediaStreamInfo info)
@@ -992,11 +971,11 @@ namespace MediaBrowser.MediaEncoding.Probing
             var original = info.DisplayAspectRatio;
 
             var parts = (original ?? string.Empty).Split(':');
-            if (!(parts.Length == 2 &&
-                int.TryParse(parts[0], NumberStyles.Any, CultureInfo.InvariantCulture, out var width) &&
-                int.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out var height) &&
-                width > 0 &&
-                height > 0))
+            if (!(parts.Length == 2
+                    && int.TryParse(parts[0], NumberStyles.Any, CultureInfo.InvariantCulture, out var width)
+                    && int.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out var height)
+                    && width > 0
+                    && height > 0))
             {
                 width = info.Width;
                 height = info.Height;
@@ -1077,12 +1056,6 @@ namespace MediaBrowser.MediaEncoding.Probing
             int index = value.IndexOf('/');
             if (index == -1)
             {
-                // REVIEW: is this branch actually required? (i.e. does ffprobe ever output something other than a fraction?)
-                if (float.TryParse(value, NumberStyles.AllowThousands | NumberStyles.Float, CultureInfo.InvariantCulture, out var result))
-                {
-                    return result;
-                }
-
                 return null;
             }
 
@@ -1098,7 +1071,7 @@ namespace MediaBrowser.MediaEncoding.Probing
         private void SetAudioRuntimeTicks(InternalMediaInfoResult result, MediaInfo data)
         {
             // Get the first info stream
-            var stream = result.Streams?.FirstOrDefault(s => string.Equals(s.CodecType, "audio", StringComparison.OrdinalIgnoreCase));
+            var stream = result.Streams?.FirstOrDefault(s => s.CodecType == CodecType.Audio);
             if (stream is null)
             {
                 return;
@@ -1128,8 +1101,7 @@ namespace MediaBrowser.MediaEncoding.Probing
             }
 
             var bps = GetDictionaryValue(streamInfo.Tags, "BPS-eng") ?? GetDictionaryValue(streamInfo.Tags, "BPS");
-            if (!string.IsNullOrEmpty(bps)
-                && int.TryParse(bps, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedBps))
+            if (int.TryParse(bps, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedBps))
             {
                 return parsedBps;
             }
@@ -1162,8 +1134,7 @@ namespace MediaBrowser.MediaEncoding.Probing
 
             var numberOfBytes = GetDictionaryValue(streamInfo.Tags, "NUMBER_OF_BYTES-eng")
                                 ?? GetDictionaryValue(streamInfo.Tags, "NUMBER_OF_BYTES");
-            if (!string.IsNullOrEmpty(numberOfBytes)
-                && long.TryParse(numberOfBytes, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedBytes))
+            if (long.TryParse(numberOfBytes, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedBytes))
             {
                 return parsedBytes;
             }
@@ -1455,7 +1426,7 @@ namespace MediaBrowser.MediaEncoding.Probing
         {
             var disc = tags.GetValueOrDefault(tagName);
 
-            if (!string.IsNullOrEmpty(disc) && int.TryParse(disc.AsSpan().LeftPart('/'), out var discNum))
+            if (int.TryParse(disc.AsSpan().LeftPart('/'), out var discNum))
             {
                 return discNum;
             }

+ 34 - 0
src/Jellyfin.Extensions/Json/Converters/JsonBoolStringConverter.cs

@@ -0,0 +1,34 @@
+using System;
+using System.Buffers;
+using System.Buffers.Text;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Jellyfin.Extensions.Json.Converters;
+
+/// <summary>
+/// Converts a string to a boolean.
+/// This is needed for FFprobe.
+/// </summary>
+public class JsonBoolStringConverter : JsonConverter<bool>
+{
+    /// <inheritdoc />
+    public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+    {
+        if (reader.TokenType == JsonTokenType.String)
+        {
+            ReadOnlySpan<byte> utf8Span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
+            if (Utf8Parser.TryParse(utf8Span, out bool val, out _, 'l'))
+            {
+                return val;
+            }
+        }
+
+        return reader.GetBoolean();
+    }
+
+    /// <inheritdoc />
+    public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
+        => writer.WriteBooleanValue(value);
+}

+ 0 - 1
src/Jellyfin.Extensions/Json/JsonDefaults.cs

@@ -39,7 +39,6 @@ namespace Jellyfin.Extensions.Json
                 new JsonFlagEnumConverterFactory(),
                 new JsonStringEnumConverter(),
                 new JsonNullableStructConverterFactory(),
-                new JsonBoolNumberConverter(),
                 new JsonDateTimeConverter(),
                 new JsonStringConverter()
             }

+ 37 - 0
tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolStringTests.cs

@@ -0,0 +1,37 @@
+using System.Text.Json;
+using Jellyfin.Extensions.Json.Converters;
+using Xunit;
+
+namespace Jellyfin.Extensions.Tests.Json.Converters
+{
+    public class JsonBoolStringTests
+    {
+        private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions()
+        {
+            Converters =
+            {
+                new JsonBoolStringConverter()
+            }
+        };
+
+        [Theory]
+        [InlineData(@"{ ""Value"": ""true"" }", true)]
+        [InlineData(@"{ ""Value"": ""false"" }", false)]
+        public void Deserialize_String_Valid_Success(string input, bool output)
+        {
+            var s = JsonSerializer.Deserialize<TestStruct>(input, _jsonOptions);
+            Assert.Equal(s.Value, output);
+        }
+
+        [Theory]
+        [InlineData(true, "true")]
+        [InlineData(false, "false")]
+        public void Serialize_Bool_Success(bool input, string output)
+        {
+            var value = JsonSerializer.Serialize(input, _jsonOptions);
+            Assert.Equal(value, output);
+        }
+
+        private readonly record struct TestStruct(bool Value);
+    }
+}

+ 0 - 25
tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs

@@ -1,25 +0,0 @@
-using System.IO;
-using System.Text.Json;
-using System.Threading.Tasks;
-using Jellyfin.Extensions.Json;
-using MediaBrowser.MediaEncoding.Probing;
-using MediaBrowser.Model.IO;
-using Xunit;
-
-namespace Jellyfin.MediaEncoding.Tests
-{
-    public class FFprobeParserTests
-    {
-        [Theory]
-        [InlineData("ffprobe1.json")]
-        public async Task Test(string fileName)
-        {
-            var path = Path.Join("Test Data", fileName);
-            await using (var stream = AsyncFile.OpenRead(path))
-            {
-                var res = await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(stream, JsonDefaults.Options).ConfigureAwait(false);
-                Assert.NotNull(res);
-            }
-        }
-    }
-}

+ 21 - 1
tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs

@@ -3,6 +3,7 @@ using System.Globalization;
 using System.IO;
 using System.Text.Json;
 using Jellyfin.Extensions.Json;
+using Jellyfin.Extensions.Json.Converters;
 using MediaBrowser.MediaEncoding.Probing;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Globalization;
@@ -15,9 +16,15 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
 {
     public class ProbeResultNormalizerTests
     {
-        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+        private readonly JsonSerializerOptions _jsonOptions;
         private readonly ProbeResultNormalizer _probeResultNormalizer = new ProbeResultNormalizer(new NullLogger<EncoderValidatorTests>(), null);
 
+        public ProbeResultNormalizerTests()
+        {
+            _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options);
+            _jsonOptions.Converters.Add(new JsonBoolStringConverter());
+        }
+
         [Theory]
         [InlineData("2997/125", 23.976f)]
         [InlineData("1/50", 0.02f)]
@@ -148,6 +155,19 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
             Assert.False(res.MediaStreams[5].IsHearingImpaired);
         }
 
+        [Fact]
+        public void GetMediaInfo_TS_Success()
+        {
+            var bytes = File.ReadAllBytes("Test Data/Probing/video_ts.json");
+            var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions);
+
+            MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_metadata.mkv", MediaProtocol.File);
+
+            Assert.Equal(2, res.MediaStreams.Count);
+
+            Assert.False(res.MediaStreams[0].IsAVC);
+        }
+
         [Fact]
         public void GetMediaInfo_ProgressiveVideoNoFieldOrder_Success()
         {

+ 0 - 0
tests/Jellyfin.MediaEncoding.Tests/Test Data/ffprobe1.json → tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_ts.json