using MediaBrowser.Common.MediaInfo; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.MediaInfo; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Providers.MediaInfo { /// /// Provides a base class for extracting media information through ffprobe /// /// public abstract class BaseFFProbeProvider : BaseMetadataProvider where T : BaseItem, IHasMediaStreams { protected BaseFFProbeProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IMediaEncoder mediaEncoder, IJsonSerializer jsonSerializer) : base(logManager, configurationManager) { JsonSerializer = jsonSerializer; MediaEncoder = mediaEncoder; } protected readonly IMediaEncoder MediaEncoder; protected readonly IJsonSerializer JsonSerializer; /// /// Gets the priority. /// /// The priority. public override MetadataProviderPriority Priority { get { return MetadataProviderPriority.First; } } protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); /// /// Supportses the specified item. /// /// The item. /// true if XXXX, false otherwise public override bool Supports(BaseItem item) { return item.LocationType == LocationType.FileSystem && item is T; } /// /// Override this to return the date that should be compared to the last refresh date /// to determine if this provider should be re-fetched. /// /// The item. /// DateTime. protected override DateTime CompareDate(BaseItem item) { return item.DateModified; } /// /// The null mount task result /// protected readonly Task NullMountTaskResult = Task.FromResult(null); /// /// Gets the provider version. /// /// The provider version. protected override string ProviderVersion { get { return MediaEncoder.Version; } } /// /// Gets a value indicating whether [refresh on version change]. /// /// true if [refresh on version change]; otherwise, false. protected override bool RefreshOnVersionChange { get { return true; } } /// /// Gets the media info. /// /// The item. /// The iso mount. /// The cancellation token. /// Task{MediaInfoResult}. /// inputPath /// or /// cache protected async Task GetMediaInfo(BaseItem item, IIsoMount isoMount, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); var type = InputType.AudioFile; var inputPath = isoMount == null ? new[] { item.Path } : new[] { isoMount.MountedPath }; var video = item as Video; if (video != null) { inputPath = MediaEncoderHelpers.GetInputArgument(video.Path, video.LocationType == LocationType.Remote, video.VideoType, video.IsoType, isoMount, video.PlayableStreamFileNames, out type); } return await MediaEncoder.GetMediaInfo(inputPath, type, cancellationToken).ConfigureAwait(false); } /// /// Mounts the iso if needed. /// /// The item. /// The cancellation token. /// IsoMount. protected virtual Task MountIsoIfNeeded(T item, CancellationToken cancellationToken) { return NullMountTaskResult; } /// /// Called when [pre fetch]. /// /// The item. /// The mount. protected virtual void OnPreFetch(T item, IIsoMount mount) { } /// /// Normalizes the FF probe result. /// /// The result. protected void NormalizeFFProbeResult(MediaInfoResult result) { if (result.format != null && result.format.tags != null) { result.format.tags = ConvertDictionaryToCaseInSensitive(result.format.tags); } if (result.streams != null) { // Convert all dictionaries to case insensitive foreach (var stream in result.streams) { if (stream.tags != null) { stream.tags = ConvertDictionaryToCaseInSensitive(stream.tags); } if (stream.disposition != null) { stream.disposition = ConvertDictionaryToCaseInSensitive(stream.disposition); } } } } /// /// Converts ffprobe stream info to our MediaStream class /// /// The stream info. /// The format info. /// MediaStream. protected MediaStream GetMediaStream(MediaStreamInfo streamInfo, MediaFormatInfo formatInfo) { var stream = new MediaStream { Codec = streamInfo.codec_name, Profile = streamInfo.profile, Level = streamInfo.level, Index = streamInfo.index }; if (streamInfo.tags != null) { stream.Language = GetDictionaryValue(streamInfo.tags, "language"); } if (string.Equals(streamInfo.codec_type, "audio", StringComparison.OrdinalIgnoreCase)) { stream.Type = MediaStreamType.Audio; stream.Channels = streamInfo.channels; if (!string.IsNullOrEmpty(streamInfo.sample_rate)) { stream.SampleRate = int.Parse(streamInfo.sample_rate, UsCulture); } stream.ChannelLayout = ParseChannelLayout(streamInfo.channel_layout); } else if (string.Equals(streamInfo.codec_type, "subtitle", StringComparison.OrdinalIgnoreCase)) { stream.Type = MediaStreamType.Subtitle; } else if (string.Equals(streamInfo.codec_type, "video", StringComparison.OrdinalIgnoreCase)) { stream.Type = MediaStreamType.Video; stream.Width = streamInfo.width; stream.Height = streamInfo.height; stream.AspectRatio = GetAspectRatio(streamInfo); stream.AverageFrameRate = GetFrameRate(streamInfo.avg_frame_rate); stream.RealFrameRate = GetFrameRate(streamInfo.r_frame_rate); } else { return null; } // Get stream bitrate if (stream.Type != MediaStreamType.Subtitle) { var bitrate = 0; if (!string.IsNullOrEmpty(streamInfo.bit_rate)) { bitrate = int.Parse(streamInfo.bit_rate, UsCulture); } else if (formatInfo != null && !string.IsNullOrEmpty(formatInfo.bit_rate)) { // If the stream info doesn't have a bitrate get the value from the media format info bitrate = int.Parse(formatInfo.bit_rate, UsCulture); } if (bitrate > 0) { stream.BitRate = bitrate; } } if (streamInfo.disposition != null) { var isDefault = GetDictionaryValue(streamInfo.disposition, "default"); var isForced = GetDictionaryValue(streamInfo.disposition, "forced"); stream.IsDefault = string.Equals(isDefault, "1", StringComparison.OrdinalIgnoreCase); stream.IsForced = string.Equals(isForced, "1", StringComparison.OrdinalIgnoreCase); } return stream; } private string ParseChannelLayout(string input) { if (string.IsNullOrEmpty(input)) { return input; } return input.Split('(').FirstOrDefault(); } private string GetAspectRatio(MediaStreamInfo info) { var original = info.display_aspect_ratio; int height; int width; var parts = (original ?? string.Empty).Split(':'); if (!(parts.Length == 2 && int.TryParse(parts[0], NumberStyles.Any, UsCulture, out width) && int.TryParse(parts[1], NumberStyles.Any, UsCulture, out height) && width > 0 && height > 0)) { width = info.width; height = info.height; } if (width > 0 && height > 0) { double ratio = width; ratio /= height; if (IsClose(ratio, 1.777777778, .03)) { return "16:9"; } if (IsClose(ratio, 1.3333333333, .05)) { return "4:3"; } if (IsClose(ratio, 1.41)) { return "1.41:1"; } if (IsClose(ratio, 1.5)) { return "1.5:1"; } if (IsClose(ratio, 1.6)) { return "1.6:1"; } if (IsClose(ratio, 1.66666666667)) { return "5:3"; } if (IsClose(ratio, 1.85, .02)) { return "1.85:1"; } if (IsClose(ratio, 2.35, .025)) { return "2.35:1"; } if (IsClose(ratio, 2.4, .025)) { return "2.40:1"; } } return original; } private bool IsClose(double d1, double d2, double variance = .005) { return Math.Abs(d1 - d2) <= variance; } /// /// Gets a frame rate from a string value in ffprobe output /// This could be a number or in the format of 2997/125. /// /// The value. /// System.Nullable{System.Single}. private float? GetFrameRate(string value) { if (!string.IsNullOrEmpty(value)) { var parts = value.Split('/'); float result; if (parts.Length == 2) { result = float.Parse(parts[0], UsCulture) / float.Parse(parts[1], UsCulture); } else { result = float.Parse(parts[0], UsCulture); } return float.IsNaN(result) ? (float?)null : result; } return null; } /// /// Gets a string from an FFProbeResult tags dictionary /// /// The tags. /// The key. /// System.String. protected string GetDictionaryValue(Dictionary tags, string key) { if (tags == null) { return null; } string val; tags.TryGetValue(key, out val); return val; } /// /// Gets an int from an FFProbeResult tags dictionary /// /// The tags. /// The key. /// System.Nullable{System.Int32}. protected int? GetDictionaryNumericValue(Dictionary tags, string key) { var val = GetDictionaryValue(tags, key); if (!string.IsNullOrEmpty(val)) { int i; if (int.TryParse(val, out i)) { return i; } } return null; } /// /// Gets a DateTime from an FFProbeResult tags dictionary /// /// The tags. /// The key. /// System.Nullable{DateTime}. protected DateTime? GetDictionaryDateTime(Dictionary tags, string key) { var val = GetDictionaryValue(tags, key); if (!string.IsNullOrEmpty(val)) { DateTime i; if (DateTime.TryParse(val, out i)) { return i.ToUniversalTime(); } } return null; } /// /// Converts a dictionary to case insensitive /// /// The dict. /// Dictionary{System.StringSystem.String}. private Dictionary ConvertDictionaryToCaseInSensitive(Dictionary dict) { return new Dictionary(dict, StringComparer.OrdinalIgnoreCase); } } }