using MediaBrowser.Common.IO; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.MediaInfo; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; using System; using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Logging; namespace MediaBrowser.Controller.Providers.MediaInfo { /// /// Provides a base class for extracting media information through ffprobe /// /// public abstract class BaseFFProbeProvider : BaseFFMpegProvider where T : BaseItem { protected BaseFFProbeProvider(ILogManager logManager) : base(logManager) { } /// /// Gets or sets the FF probe cache. /// /// The FF probe cache. protected FileSystemRepository FFProbeCache { get; set; } /// /// Initializes this instance. /// protected override void Initialize() { base.Initialize(); FFProbeCache = new FileSystemRepository(Path.Combine(Kernel.Instance.ApplicationPaths.CachePath, CacheDirectoryName)); } /// /// Gets the name of the cache directory. /// /// The name of the cache directory. protected virtual string CacheDirectoryName { get { return "ffmpeg-video-info"; } } /// /// Gets the priority. /// /// The priority. public override MetadataProviderPriority Priority { // Give this second priority // Give metadata xml providers a chance to fill in data first, so that we can skip this whenever possible get { return MetadataProviderPriority.Second; } } /// /// Fetches metadata and returns true or false indicating if any work that requires persistence was done /// /// The item. /// if set to true [force]. /// The cancellation token. /// Task{System.Boolean}. protected override async Task FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken) { var myItem = (T)item; var isoMount = await MountIsoIfNeeded(myItem, cancellationToken).ConfigureAwait(false); try { OnPreFetch(myItem, isoMount); var inputPath = isoMount == null ? Kernel.Instance.FFMpegManager.GetInputArgument(myItem) : Kernel.Instance.FFMpegManager.GetInputArgument((Video)item, isoMount); var result = await Kernel.Instance.FFMpegManager.RunFFProbe(item, inputPath, item.DateModified, FFProbeCache, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); NormalizeFFProbeResult(result); cancellationToken.ThrowIfCancellationRequested(); await Fetch(myItem, cancellationToken, result, isoMount).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); SetLastRefreshed(item, DateTime.UtcNow); } finally { if (isoMount != null) { isoMount.Dispose(); } } return true; } /// /// Gets a value indicating whether [refresh on version change]. /// /// true if [refresh on version change]; otherwise, false. protected override bool RefreshOnVersionChange { get { return true; } } /// /// 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. private void NormalizeFFProbeResult(FFProbeResult 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); } } } } /// /// Subclasses must set item values using this /// /// The item. /// The cancellation token. /// The result. /// The iso mount. /// Task. protected abstract Task Fetch(T item, CancellationToken cancellationToken, FFProbeResult result, IIsoMount isoMount); /// /// Converts ffprobe stream info to our MediaStream class /// /// The stream info. /// The format info. /// MediaStream. protected MediaStream GetMediaStream(FFProbeMediaStreamInfo streamInfo, FFProbeMediaFormatInfo formatInfo) { var stream = new MediaStream { Codec = streamInfo.codec_name, Language = GetDictionaryValue(streamInfo.tags, "language"), Profile = streamInfo.profile, Index = streamInfo.index }; if (streamInfo.codec_type.Equals("audio", StringComparison.OrdinalIgnoreCase)) { stream.Type = MediaStreamType.Audio; stream.Channels = streamInfo.channels; if (!string.IsNullOrEmpty(streamInfo.sample_rate)) { stream.SampleRate = int.Parse(streamInfo.sample_rate); } } else if (streamInfo.codec_type.Equals("subtitle", StringComparison.OrdinalIgnoreCase)) { stream.Type = MediaStreamType.Subtitle; } else { stream.Type = MediaStreamType.Video; stream.Width = streamInfo.width; stream.Height = streamInfo.height; stream.AspectRatio = streamInfo.display_aspect_ratio; stream.AverageFrameRate = GetFrameRate(streamInfo.avg_frame_rate); stream.RealFrameRate = GetFrameRate(streamInfo.r_frame_rate); } // Get stream bitrate if (stream.Type != MediaStreamType.Subtitle) { if (!string.IsNullOrEmpty(streamInfo.bit_rate)) { stream.BitRate = int.Parse(streamInfo.bit_rate); } 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 stream.BitRate = int.Parse(formatInfo.bit_rate); } } 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; } /// /// 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('/'); if (parts.Length == 2) { return float.Parse(parts[0]) / float.Parse(parts[1]); } return float.Parse(parts[0]); } 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); } /// /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected override void Dispose(bool dispose) { if (dispose) { FFProbeCache.Dispose(); } base.Dispose(dispose); } } }