using MediaBrowser.Common.IO; using MediaBrowser.Common.MediaInfo; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Threading; using System.Threading.Tasks; 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, IServerConfigurationManager configurationManager, IMediaEncoder mediaEncoder, IJsonSerializer jsonSerializer) : base(logManager, configurationManager, mediaEncoder) { JsonSerializer = jsonSerializer; } protected readonly IJsonSerializer JsonSerializer; /// /// 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(ConfigurationManager.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; } } protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); /// /// 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}. public override async Task FetchAsync(BaseItem item, bool force, CancellationToken cancellationToken) { var myItem = (T)item; var isoMount = await MountIsoIfNeeded(myItem, cancellationToken).ConfigureAwait(false); try { OnPreFetch(myItem, isoMount); var result = await GetMediaInfo(item, isoMount, item.DateModified, FFProbeCache, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); NormalizeFFProbeResult(result); cancellationToken.ThrowIfCancellationRequested(); Fetch(myItem, cancellationToken, result, isoMount); cancellationToken.ThrowIfCancellationRequested(); SetLastRefreshed(item, DateTime.UtcNow); } finally { if (isoMount != null) { isoMount.Dispose(); } } return true; } /// /// Gets the media info. /// /// The item. /// The iso mount. /// The last date modified. /// The cache. /// The cancellation token. /// Task{MediaInfoResult}. /// inputPath /// or /// cache private async Task GetMediaInfo(BaseItem item, IIsoMount isoMount, DateTime lastDateModified, FileSystemRepository cache, CancellationToken cancellationToken) { if (cache == null) { throw new ArgumentNullException("cache"); } // Put the ffmpeg version into the cache name so that it's unique per-version // We don't want to try and deserialize data based on an old version, which could potentially fail var resourceName = item.Id + "_" + lastDateModified.Ticks + "_" + MediaEncoder.Version; // Forumulate the cache file path var cacheFilePath = cache.GetResourcePath(resourceName, ".js"); cancellationToken.ThrowIfCancellationRequested(); // Avoid File.Exists by just trying to deserialize try { return JsonSerializer.DeserializeFromFile(cacheFilePath); } catch (FileNotFoundException) { // Cache file doesn't exist } 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, isoMount, out type); } var info = await MediaEncoder.GetMediaInfo(inputPath, type, cancellationToken).ConfigureAwait(false); JsonSerializer.SerializeToFile(info, cacheFilePath); return info; } /// /// 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(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); } } } } /// /// Subclasses must set item values using this /// /// The item. /// The cancellation token. /// The result. /// The iso mount. /// Task. protected abstract void Fetch(T item, CancellationToken cancellationToken, MediaInfoResult result, IIsoMount isoMount); /// /// 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, Language = GetDictionaryValue(streamInfo.tags, "language"), Profile = streamInfo.profile, Level = streamInfo.level, 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, UsCulture); } } else if (streamInfo.codec_type.Equals("subtitle", StringComparison.OrdinalIgnoreCase)) { stream.Type = MediaStreamType.Subtitle; } else if (streamInfo.codec_type.Equals("data", StringComparison.OrdinalIgnoreCase)) { stream.Type = MediaStreamType.Data; } 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, 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 stream.BitRate = int.Parse(formatInfo.bit_rate, UsCulture); } } 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('/'); 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); } } }