diff options
Diffstat (limited to 'MediaBrowser.MediaEncoding/Probing')
3 files changed, 163 insertions, 116 deletions
diff --git a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs index 6f51e1a6ab..fa2085ca6f 100644 --- a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs +++ b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs @@ -74,9 +74,15 @@ namespace MediaBrowser.MediaEncoding.Probing /// </summary> /// <param name="dict">The dict.</param> /// <returns>Dictionary{System.StringSystem.String}.</returns> - private static Dictionary<string, string> ConvertDictionaryToCaseInsensitive(IReadOnlyDictionary<string, string> dict) + private static Dictionary<string, string?> ConvertDictionaryToCaseInsensitive(IReadOnlyDictionary<string, string?> dict) { - return new Dictionary<string, string>(dict, StringComparer.OrdinalIgnoreCase); + var result = new Dictionary<string, string?>(dict.Count, StringComparer.OrdinalIgnoreCase); + foreach (var (key, value) in dict) + { + result.TryAdd(key, value); + } + + return result; } } } 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 /// </summary> /// <value>The profile.</value> [JsonPropertyName("profile")] - public string Profile { get; set; } + public string? Profile { get; set; } /// <summary> /// Gets or sets the codec_name. /// </summary> /// <value>The codec_name.</value> [JsonPropertyName("codec_name")] - public string CodecName { get; set; } + public string? CodecName { get; set; } /// <summary> /// Gets or sets the codec_long_name. /// </summary> /// <value>The codec_long_name.</value> [JsonPropertyName("codec_long_name")] - public string CodecLongName { get; set; } + public string? CodecLongName { get; set; } /// <summary> /// Gets or sets the codec_type. @@ -50,49 +48,49 @@ namespace MediaBrowser.MediaEncoding.Probing /// </summary> /// <value>The sample_rate.</value> [JsonPropertyName("sample_rate")] - public string SampleRate { get; set; } + public string? SampleRate { get; set; } /// <summary> /// Gets or sets the channels. /// </summary> /// <value>The channels.</value> [JsonPropertyName("channels")] - public int Channels { get; set; } + public int? Channels { get; set; } /// <summary> /// Gets or sets the channel_layout. /// </summary> /// <value>The channel_layout.</value> [JsonPropertyName("channel_layout")] - public string ChannelLayout { get; set; } + public string? ChannelLayout { get; set; } /// <summary> /// Gets or sets the avg_frame_rate. /// </summary> /// <value>The avg_frame_rate.</value> [JsonPropertyName("avg_frame_rate")] - public string AverageFrameRate { get; set; } + public string? AverageFrameRate { get; set; } /// <summary> /// Gets or sets the duration. /// </summary> /// <value>The duration.</value> [JsonPropertyName("duration")] - public string Duration { get; set; } + public string? Duration { get; set; } /// <summary> /// Gets or sets the bit_rate. /// </summary> /// <value>The bit_rate.</value> [JsonPropertyName("bit_rate")] - public string BitRate { get; set; } + public string? BitRate { get; set; } /// <summary> /// Gets or sets the width. /// </summary> /// <value>The width.</value> [JsonPropertyName("width")] - public int Width { get; set; } + public int? Width { get; set; } /// <summary> /// Gets or sets the refs. @@ -106,21 +104,21 @@ namespace MediaBrowser.MediaEncoding.Probing /// </summary> /// <value>The height.</value> [JsonPropertyName("height")] - public int Height { get; set; } + public int? Height { get; set; } /// <summary> /// Gets or sets the display_aspect_ratio. /// </summary> /// <value>The display_aspect_ratio.</value> [JsonPropertyName("display_aspect_ratio")] - public string DisplayAspectRatio { get; set; } + public string? DisplayAspectRatio { get; set; } /// <summary> /// Gets or sets the tags. /// </summary> /// <value>The tags.</value> [JsonPropertyName("tags")] - public IReadOnlyDictionary<string, string> Tags { get; set; } + public IReadOnlyDictionary<string, string?>? Tags { get; set; } /// <summary> /// Gets or sets the bits_per_sample. @@ -141,7 +139,7 @@ namespace MediaBrowser.MediaEncoding.Probing /// </summary> /// <value>The r_frame_rate.</value> [JsonPropertyName("r_frame_rate")] - public string RFrameRate { get; set; } + public string? RFrameRate { get; set; } /// <summary> /// Gets or sets the has_b_frames. @@ -155,70 +153,70 @@ namespace MediaBrowser.MediaEncoding.Probing /// </summary> /// <value>The sample_aspect_ratio.</value> [JsonPropertyName("sample_aspect_ratio")] - public string SampleAspectRatio { get; set; } + public string? SampleAspectRatio { get; set; } /// <summary> /// Gets or sets the pix_fmt. /// </summary> /// <value>The pix_fmt.</value> [JsonPropertyName("pix_fmt")] - public string PixelFormat { get; set; } + public string? PixelFormat { get; set; } /// <summary> /// Gets or sets the level. /// </summary> /// <value>The level.</value> [JsonPropertyName("level")] - public int Level { get; set; } + public int? Level { get; set; } /// <summary> /// Gets or sets the time_base. /// </summary> /// <value>The time_base.</value> [JsonPropertyName("time_base")] - public string TimeBase { get; set; } + public string? TimeBase { get; set; } /// <summary> /// Gets or sets the start_time. /// </summary> /// <value>The start_time.</value> [JsonPropertyName("start_time")] - public string StartTime { get; set; } + public string? StartTime { get; set; } /// <summary> /// Gets or sets the codec_time_base. /// </summary> /// <value>The codec_time_base.</value> [JsonPropertyName("codec_time_base")] - public string CodecTimeBase { get; set; } + public string? CodecTimeBase { get; set; } /// <summary> /// Gets or sets the codec_tag. /// </summary> /// <value>The codec_tag.</value> [JsonPropertyName("codec_tag")] - public string CodecTag { get; set; } + public string? CodecTag { get; set; } /// <summary> - /// Gets or sets the codec_tag_string. + /// Gets or sets the codec_tag_string?. /// </summary> - /// <value>The codec_tag_string.</value> - [JsonPropertyName("codec_tag_string")] - public string CodecTagString { get; set; } + /// <value>The codec_tag_string?.</value> + [JsonPropertyName("codec_tag_string?")] + public string? CodecTagString { get; set; } /// <summary> /// Gets or sets the sample_fmt. /// </summary> /// <value>The sample_fmt.</value> [JsonPropertyName("sample_fmt")] - public string SampleFmt { get; set; } + public string? SampleFmt { get; set; } /// <summary> /// Gets or sets the dmix_mode. /// </summary> /// <value>The dmix_mode.</value> [JsonPropertyName("dmix_mode")] - public string DmixMode { get; set; } + public string? DmixMode { get; set; } /// <summary> /// Gets or sets the start_pts. @@ -232,90 +230,90 @@ namespace MediaBrowser.MediaEncoding.Probing /// </summary> /// <value>The is_avc.</value> [JsonPropertyName("is_avc")] - public bool IsAvc { get; set; } + public bool? IsAvc { get; set; } /// <summary> /// Gets or sets the nal_length_size. /// </summary> /// <value>The nal_length_size.</value> [JsonPropertyName("nal_length_size")] - public string NalLengthSize { get; set; } + public string? NalLengthSize { get; set; } /// <summary> /// Gets or sets the ltrt_cmixlev. /// </summary> /// <value>The ltrt_cmixlev.</value> [JsonPropertyName("ltrt_cmixlev")] - public string LtrtCmixlev { get; set; } + public string? LtrtCmixlev { get; set; } /// <summary> /// Gets or sets the ltrt_surmixlev. /// </summary> /// <value>The ltrt_surmixlev.</value> [JsonPropertyName("ltrt_surmixlev")] - public string LtrtSurmixlev { get; set; } + public string? LtrtSurmixlev { get; set; } /// <summary> /// Gets or sets the loro_cmixlev. /// </summary> /// <value>The loro_cmixlev.</value> [JsonPropertyName("loro_cmixlev")] - public string LoroCmixlev { get; set; } + public string? LoroCmixlev { get; set; } /// <summary> /// Gets or sets the loro_surmixlev. /// </summary> /// <value>The loro_surmixlev.</value> [JsonPropertyName("loro_surmixlev")] - public string LoroSurmixlev { get; set; } + public string? LoroSurmixlev { get; set; } /// <summary> /// Gets or sets the field_order. /// </summary> /// <value>The field_order.</value> [JsonPropertyName("field_order")] - public string FieldOrder { get; set; } + public string? FieldOrder { get; set; } /// <summary> /// Gets or sets the disposition. /// </summary> /// <value>The disposition.</value> [JsonPropertyName("disposition")] - public IReadOnlyDictionary<string, int> Disposition { get; set; } + public IReadOnlyDictionary<string, int>? Disposition { get; set; } /// <summary> /// Gets or sets the color range. /// </summary> /// <value>The color range.</value> [JsonPropertyName("color_range")] - public string ColorRange { get; set; } + public string? ColorRange { get; set; } /// <summary> /// Gets or sets the color space. /// </summary> /// <value>The color space.</value> [JsonPropertyName("color_space")] - public string ColorSpace { get; set; } + public string? ColorSpace { get; set; } /// <summary> /// Gets or sets the color transfer. /// </summary> /// <value>The color transfer.</value> [JsonPropertyName("color_transfer")] - public string ColorTransfer { get; set; } + public string? ColorTransfer { get; set; } /// <summary> /// Gets or sets the color primaries. /// </summary> /// <value>The color primaries.</value> [JsonPropertyName("color_primaries")] - public string ColorPrimaries { get; set; } + public string? ColorPrimaries { get; set; } /// <summary> /// Gets or sets the side_data_list. /// </summary> /// <value>The side_data_list.</value> [JsonPropertyName("side_data_list")] - public IReadOnlyList<MediaStreamInfoSideData> SideDataList { get; set; } + public IReadOnlyList<MediaStreamInfoSideData>? SideDataList { get; set; } } } diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index e27195ec70..203e565ac7 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -194,6 +194,11 @@ namespace MediaBrowser.MediaEncoding.Probing info.ProductionYear = info.PremiereDate.Value.Year; } + if (data.Chapters is not null) + { + info.Chapters = data.Chapters.Select(GetChapterInfo).ToArray(); + } + // Set mediaType-specific metadata if (isAudio) { @@ -238,11 +243,6 @@ namespace MediaBrowser.MediaEncoding.Probing FetchWtvInfo(info, data); - if (data.Chapters is not null) - { - info.Chapters = data.Chapters.Select(GetChapterInfo).ToArray(); - } - ExtractTimestamp(info); if (tags.TryGetValue("stereo_mode", out var stereoMode) && string.Equals(stereoMode, "left_right", StringComparison.OrdinalIgnoreCase)) @@ -254,16 +254,38 @@ namespace MediaBrowser.MediaEncoding.Probing { if (mediaStream.Type == MediaStreamType.Audio && !mediaStream.BitRate.HasValue) { - mediaStream.BitRate = GetEstimatedAudioBitrate(mediaStream.Codec, mediaStream.Channels); + mediaStream.BitRate = GetEstimatedAudioBitrate(mediaStream.Codec, mediaStream.Profile, mediaStream.Channels); } } - var videoStreamsBitrate = info.MediaStreams.Where(i => i.Type == MediaStreamType.Video).Select(i => i.BitRate ?? 0).Sum(); - // If ffprobe reported the container bitrate as being the same as the video stream bitrate, then it's wrong - if (videoStreamsBitrate == (info.Bitrate ?? 0)) + // ffprobe frequently omits the per-stream video bitrate (common in MP4/MKV containers). + // Estimate the missing video bitrate as the container bitrate minus the combined stream bitrates. + var videoStreams = info.MediaStreams.Where(i => i.Type == MediaStreamType.Video).ToList(); + if (info.Bitrate.HasValue + && videoStreams.Count == 1 + && !videoStreams[0].BitRate.HasValue) { - info.InferTotalBitrate(true); + var otherStreams = info.MediaStreams + .Where(i => i.Type != MediaStreamType.Video && !i.IsExternal) + .ToList(); + + // Only attribute the leftover bitrate to the video stream if every audio stream's bitrate is known. + var audioBitratesKnown = otherStreams + .Where(i => i.Type == MediaStreamType.Audio) + .All(i => i.BitRate.HasValue); + + if (audioBitratesKnown) + { + var estimatedVideoBitrate = info.Bitrate.Value - otherStreams.Sum(i => i.BitRate ?? 0); + if (estimatedVideoBitrate > 0) + { + videoStreams[0].BitRate = estimatedVideoBitrate; + } + } } + + // If the container bitrate is still unknown, infer it from the sum of the streams. + info.InferTotalBitrate(); } return info; @@ -316,54 +338,34 @@ namespace MediaBrowser.MediaEncoding.Probing return string.Join(',', splitFormat.Where(s => !string.IsNullOrEmpty(s))); } - private static int? GetEstimatedAudioBitrate(string codec, int? channels) + internal static int? GetEstimatedAudioBitrate(string codec, string profile, int? channels) { - if (!channels.HasValue) + if (!channels.HasValue || channels.Value < 1 || string.IsNullOrEmpty(codec)) { return null; } - var channelsValue = channels.Value; + // Rough typical bitrates used only as a fallback when ffprobe doesn't report a stream bitrate. + var channelCount = channels.Value; + var isMultichannel = channelCount > 2; - if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase)) + return codec.ToLowerInvariant() switch { - switch (channelsValue) - { - case <= 2: - return 192000; - case >= 5: - return 320000; - } - } - - if (string.Equals(codec, "ac3", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "eac3", StringComparison.OrdinalIgnoreCase)) - { - switch (channelsValue) - { - case <= 2: - return 192000; - case >= 5: - return 640000; - } - } - - if (string.Equals(codec, "flac", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "alac", StringComparison.OrdinalIgnoreCase)) - { - switch (channelsValue) - { - case <= 2: - return 960000; - case >= 5: - return 2880000; - } - } - - return null; + "aac" or "mp3" or "mp2" => isMultichannel ? 320000 : 192000, + "ac3" or "eac3" => isMultichannel ? 640000 : 192000, + "dts" or "dca" => IsDtsLossless(profile) ? channelCount * 700000 : (isMultichannel ? 1509000 : 768000), + "opus" => isMultichannel ? 256000 : 128000, + "vorbis" => isMultichannel ? 320000 : 160000, + "wmav1" or "wmav2" or "wmapro" => isMultichannel ? 384000 : 192000, + "flac" or "alac" => channelCount * 480000, + "truehd" or "mlp" => channelCount * 700000, + _ => null + }; } + private static bool IsDtsLossless(string profile) + => profile is not null && profile.Contains("HD MA", StringComparison.OrdinalIgnoreCase); + private void FetchFromItunesInfo(string xml, MediaInfo info) { // Make things simpler and strip out the dtd @@ -697,24 +699,18 @@ namespace MediaBrowser.MediaEncoding.Probing /// <returns>MediaStream.</returns> private MediaStream GetMediaStream(bool isAudio, MediaStreamInfo streamInfo, MediaFormatInfo formatInfo, IReadOnlyList<MediaFrameInfo> 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 @@ -735,6 +731,10 @@ namespace MediaBrowser.MediaEncoding.Probing stream.Type = MediaStreamType.Audio; stream.LocalizedDefault = _localization.GetLocalizedString("Default"); stream.LocalizedExternal = _localization.GetLocalizedString("External"); + stream.LocalizedOriginal = _localization.GetLocalizedString("Original"); + stream.LocalizedLanguage = string.IsNullOrEmpty(stream.Language) + ? null + : _localization.FindLanguageInfo(stream.Language)?.DisplayName; stream.Channels = streamInfo.Channels; @@ -773,10 +773,9 @@ namespace MediaBrowser.MediaEncoding.Probing stream.LocalizedForced = _localization.GetLocalizedString("Forced"); 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; + stream.LocalizedLanguage = string.IsNullOrEmpty(stream.Language) + ? null + : _localization.FindLanguageInfo(stream.Language)?.DisplayName; if (string.IsNullOrEmpty(stream.Title)) { @@ -790,6 +789,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 +822,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) @@ -863,7 +861,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; } @@ -976,10 +974,12 @@ namespace MediaBrowser.MediaEncoding.Probing bitrate = value; } - // The bitrate info of FLAC musics and some videos is included in formatInfo. + // The bitrate info of FLAC audio is included in formatInfo. + // Don't do this for video streams: formatInfo.BitRate is the overall container + // bitrate (video + audio + subtitles + overhead), not the video bitrate. if (bitrate == 0 && formatInfo is not null - && (stream.Type == MediaStreamType.Video || (isAudio && stream.Type == MediaStreamType.Audio))) + && isAudio && stream.Type == MediaStreamType.Audio) { // If the stream info doesn't have a bitrate get the value from the media format info if (int.TryParse(formatInfo.BitRate, CultureInfo.InvariantCulture, out value)) @@ -1036,6 +1036,11 @@ namespace MediaBrowser.MediaEncoding.Probing { stream.IsHearingImpaired = true; } + + if (disposition.GetValueOrDefault("original") == 1) + { + stream.IsOriginal = true; + } } NormalizeStreamTitle(stream); @@ -1091,8 +1096,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) @@ -1155,6 +1160,34 @@ namespace MediaBrowser.MediaEncoding.Probing } /// <summary> + /// 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). + /// </summary> + /// <param name="sar">The sample aspect ratio string in "N:D" format.</param> + /// <returns><c>true</c> if the SAR is within 1% of 1:1; otherwise <c>false</c>.</returns> + 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); + } + + /// <summary> /// Gets a frame rate from a string value in ffprobe output /// This could be a number or in the format of 2997/125. /// </summary> @@ -1231,9 +1264,16 @@ namespace MediaBrowser.MediaEncoding.Probing } var duration = GetDictionaryValue(streamInfo.Tags, "DURATION-eng") ?? GetDictionaryValue(streamInfo.Tags, "DURATION"); - if (TimeSpan.TryParse(duration, out var parsedDuration)) + if (!string.IsNullOrEmpty(duration)) { - return parsedDuration.TotalSeconds; + // Matroska DURATION tags use nanosecond precision (e.g. "00:00:05.023000000"), but + // TimeSpan only supports up to 7 fractional digits (ticks). Trim the surplus digits so + // these durations parse instead of being silently dropped. + duration = DurationOverPrecisionRegex().Replace(duration, "$1"); + if (TimeSpan.TryParse(duration, CultureInfo.InvariantCulture, out var parsedDuration)) + { + return parsedDuration.TotalSeconds; + } } return null; @@ -1601,7 +1641,7 @@ namespace MediaBrowser.MediaEncoding.Probing // Credit to MCEBuddy: https://mcebuddy2x.codeplex.com/ // DateTime is reported along with timezone info (typically Z i.e. UTC hence assume None) - if (tags.TryGetValue("WM/MediaOriginalBroadcastDateTime", out var premiereDateString) && DateTime.TryParse(year, null, DateTimeStyles.AdjustToUniversal, out var parsedDate)) + if (tags.TryGetValue("WM/MediaOriginalBroadcastDateTime", out var premiereDateString) && DateTime.TryParse(year, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out var parsedDate)) { video.PremiereDate = parsedDate; } @@ -1735,5 +1775,8 @@ namespace MediaBrowser.MediaEncoding.Probing [GeneratedRegex("(?<name>.*) \\((?<instrument>.*)\\)")] private static partial Regex PerformerRegex(); + + [GeneratedRegex(@"(\.\d{7})\d+")] + private static partial Regex DurationOverPrecisionRegex(); } } |
