From 65d605b17dfc978ebc089b7500a5aacd8fcd863f Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 1 Feb 2023 14:58:04 +0100 Subject: [PATCH] Improve ffprobe json parsing and don't log error for Codec Type attachment --- .../TunerHosts/HdHomerun/HdHomerunHost.cs | 4 +- .../Encoder/MediaEncoder.cs | 5 +- .../Probing/CodecType.cs | 32 ++++++ .../Probing/MediaStreamInfo.cs | 6 +- .../Probing/ProbeResultNormalizer.cs | 99 +++++++------------ .../Converters/JsonBoolStringConverter.cs | 34 +++++++ src/Jellyfin.Extensions/Json/JsonDefaults.cs | 1 - .../Json/Converters/JsonBoolStringTests.cs | 37 +++++++ .../FFprobeParserTests.cs | 25 ----- .../Probing/ProbeResultNormalizerTests.cs | 22 ++++- .../{ffprobe1.json => Probing/video_ts.json} | 0 11 files changed, 169 insertions(+), 96 deletions(-) create mode 100644 MediaBrowser.MediaEncoding/Probing/CodecType.cs create mode 100644 src/Jellyfin.Extensions/Json/Converters/JsonBoolStringConverter.cs create mode 100644 tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolStringTests.cs delete mode 100644 tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs rename tests/Jellyfin.MediaEncoding.Tests/Test Data/{ffprobe1.json => Probing/video_ts.json} (100%) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 5327b3d74..98bbc1540 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/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"; diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index d2240b5af..cef02d5f8 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/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(Controller.Extensions.ConfigurationExtensions.FfmpegPathKey) ?? string.Empty; - _jsonSerializerOptions = JsonDefaults.Options; + + _jsonSerializerOptions = new JsonSerializerOptions(JsonDefaults.Options); + _jsonSerializerOptions.Converters.Add(new JsonBoolStringConverter()); } /// diff --git a/MediaBrowser.MediaEncoding/Probing/CodecType.cs b/MediaBrowser.MediaEncoding/Probing/CodecType.cs new file mode 100644 index 000000000..d7c68e5f3 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Probing/CodecType.cs @@ -0,0 +1,32 @@ +namespace MediaBrowser.MediaEncoding.Probing; + +/// +/// FFmpeg Codec Type. +/// +public enum CodecType +{ + /// + /// Video. + /// + Video, + + /// + /// Audio. + /// + Audio, + + /// + /// Opaque data information usually continuous. + /// + Data, + + /// + /// Subtitles. + /// + Subtitle, + + /// + /// Opaque data information usually sparse. + /// + Attachment +} diff --git a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs index eab8f79bb..294442324 100644 --- a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs +++ b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs @@ -43,7 +43,7 @@ namespace MediaBrowser.MediaEncoding.Probing /// /// The codec_type. [JsonPropertyName("codec_type")] - public string CodecType { get; set; } + public CodecType CodecType { get; set; } /// /// Gets or sets the sample_rate. @@ -228,11 +228,11 @@ namespace MediaBrowser.MediaEncoding.Probing public long StartPts { get; set; } /// - /// Gets or sets the is_avc. + /// Gets or sets a value indicating whether the stream is AVC. /// /// The is_avc. [JsonPropertyName("is_avc")] - public string IsAvc { get; set; } + public bool IsAvc { get; set; } /// /// Gets or sets the nal_length_size. diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index c667f5f57..99310a75d 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -107,9 +107,9 @@ namespace MediaBrowser.MediaEncoding.Probing } var tags = new Dictionary(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 /// MediaAttachments. 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; } diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonBoolStringConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonBoolStringConverter.cs new file mode 100644 index 000000000..2936fe4d6 --- /dev/null +++ b/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; + +/// +/// Converts a string to a boolean. +/// This is needed for FFprobe. +/// +public class JsonBoolStringConverter : JsonConverter +{ + /// + public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + ReadOnlySpan utf8Span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; + if (Utf8Parser.TryParse(utf8Span, out bool val, out _, 'l')) + { + return val; + } + } + + return reader.GetBoolean(); + } + + /// + public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) + => writer.WriteBooleanValue(value); +} diff --git a/src/Jellyfin.Extensions/Json/JsonDefaults.cs b/src/Jellyfin.Extensions/Json/JsonDefaults.cs index 97cbee971..4d56ca615 100644 --- a/src/Jellyfin.Extensions/Json/JsonDefaults.cs +++ b/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() } diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolStringTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolStringTests.cs new file mode 100644 index 000000000..be256da2e --- /dev/null +++ b/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(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); + } +} diff --git a/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs deleted file mode 100644 index 97dbb3be0..000000000 --- a/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs +++ /dev/null @@ -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(stream, JsonDefaults.Options).ConfigureAwait(false); - Assert.NotNull(res); - } - } - } -} diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs index a64604e99..6cb98b2b8 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs +++ b/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(), 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(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() { diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/ffprobe1.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_ts.json similarity index 100% rename from tests/Jellyfin.MediaEncoding.Tests/Test Data/ffprobe1.json rename to tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_ts.json