BaseFFProbeProvider.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. using MediaBrowser.Common.IO;
  2. using MediaBrowser.Common.MediaInfo;
  3. using MediaBrowser.Controller.Configuration;
  4. using MediaBrowser.Controller.Entities;
  5. using MediaBrowser.Model.Entities;
  6. using MediaBrowser.Model.Logging;
  7. using MediaBrowser.Model.Serialization;
  8. using System;
  9. using System.Collections.Generic;
  10. using System.Globalization;
  11. using System.IO;
  12. using System.Threading;
  13. using System.Threading.Tasks;
  14. namespace MediaBrowser.Controller.Providers.MediaInfo
  15. {
  16. /// <summary>
  17. /// Provides a base class for extracting media information through ffprobe
  18. /// </summary>
  19. /// <typeparam name="T"></typeparam>
  20. public abstract class BaseFFProbeProvider<T> : BaseFFMpegProvider<T>
  21. where T : BaseItem
  22. {
  23. protected BaseFFProbeProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IMediaEncoder mediaEncoder, IJsonSerializer jsonSerializer)
  24. : base(logManager, configurationManager, mediaEncoder)
  25. {
  26. JsonSerializer = jsonSerializer;
  27. }
  28. protected readonly IJsonSerializer JsonSerializer;
  29. /// <summary>
  30. /// Gets or sets the FF probe cache.
  31. /// </summary>
  32. /// <value>The FF probe cache.</value>
  33. protected FileSystemRepository FFProbeCache { get; set; }
  34. /// <summary>
  35. /// Initializes this instance.
  36. /// </summary>
  37. protected override void Initialize()
  38. {
  39. base.Initialize();
  40. FFProbeCache = new FileSystemRepository(Path.Combine(ConfigurationManager.ApplicationPaths.CachePath, CacheDirectoryName));
  41. }
  42. /// <summary>
  43. /// Gets the name of the cache directory.
  44. /// </summary>
  45. /// <value>The name of the cache directory.</value>
  46. protected virtual string CacheDirectoryName
  47. {
  48. get
  49. {
  50. return "ffmpeg-video-info";
  51. }
  52. }
  53. /// <summary>
  54. /// Gets the priority.
  55. /// </summary>
  56. /// <value>The priority.</value>
  57. public override MetadataProviderPriority Priority
  58. {
  59. // Give this second priority
  60. // Give metadata xml providers a chance to fill in data first, so that we can skip this whenever possible
  61. get { return MetadataProviderPriority.Second; }
  62. }
  63. protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
  64. /// <summary>
  65. /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
  66. /// </summary>
  67. /// <param name="item">The item.</param>
  68. /// <param name="force">if set to <c>true</c> [force].</param>
  69. /// <param name="cancellationToken">The cancellation token.</param>
  70. /// <returns>Task{System.Boolean}.</returns>
  71. public override async Task<bool> FetchAsync(BaseItem item, bool force, CancellationToken cancellationToken)
  72. {
  73. var myItem = (T)item;
  74. var isoMount = await MountIsoIfNeeded(myItem, cancellationToken).ConfigureAwait(false);
  75. try
  76. {
  77. OnPreFetch(myItem, isoMount);
  78. var result = await GetMediaInfo(item, isoMount, item.DateModified, FFProbeCache, cancellationToken).ConfigureAwait(false);
  79. cancellationToken.ThrowIfCancellationRequested();
  80. NormalizeFFProbeResult(result);
  81. cancellationToken.ThrowIfCancellationRequested();
  82. Fetch(myItem, cancellationToken, result, isoMount);
  83. cancellationToken.ThrowIfCancellationRequested();
  84. SetLastRefreshed(item, DateTime.UtcNow);
  85. }
  86. finally
  87. {
  88. if (isoMount != null)
  89. {
  90. isoMount.Dispose();
  91. }
  92. }
  93. return true;
  94. }
  95. /// <summary>
  96. /// Gets the media info.
  97. /// </summary>
  98. /// <param name="item">The item.</param>
  99. /// <param name="isoMount">The iso mount.</param>
  100. /// <param name="lastDateModified">The last date modified.</param>
  101. /// <param name="cache">The cache.</param>
  102. /// <param name="cancellationToken">The cancellation token.</param>
  103. /// <returns>Task{MediaInfoResult}.</returns>
  104. /// <exception cref="System.ArgumentNullException">inputPath
  105. /// or
  106. /// cache</exception>
  107. private async Task<MediaInfoResult> GetMediaInfo(BaseItem item, IIsoMount isoMount, DateTime lastDateModified, FileSystemRepository cache, CancellationToken cancellationToken)
  108. {
  109. if (cache == null)
  110. {
  111. throw new ArgumentNullException("cache");
  112. }
  113. // Put the ffmpeg version into the cache name so that it's unique per-version
  114. // We don't want to try and deserialize data based on an old version, which could potentially fail
  115. var resourceName = item.Id + "_" + lastDateModified.Ticks + "_" + MediaEncoder.Version;
  116. // Forumulate the cache file path
  117. var cacheFilePath = cache.GetResourcePath(resourceName, ".js");
  118. cancellationToken.ThrowIfCancellationRequested();
  119. // Avoid File.Exists by just trying to deserialize
  120. try
  121. {
  122. return JsonSerializer.DeserializeFromFile<MediaInfoResult>(cacheFilePath);
  123. }
  124. catch (FileNotFoundException)
  125. {
  126. // Cache file doesn't exist
  127. }
  128. var type = InputType.AudioFile;
  129. var inputPath = isoMount == null ? new[] { item.Path } : new[] { isoMount.MountedPath };
  130. var video = item as Video;
  131. if (video != null)
  132. {
  133. inputPath = MediaEncoderHelpers.GetInputArgument(video, isoMount, out type);
  134. }
  135. var info = await MediaEncoder.GetMediaInfo(inputPath, type, cancellationToken).ConfigureAwait(false);
  136. JsonSerializer.SerializeToFile(info, cacheFilePath);
  137. return info;
  138. }
  139. /// <summary>
  140. /// Gets a value indicating whether [refresh on version change].
  141. /// </summary>
  142. /// <value><c>true</c> if [refresh on version change]; otherwise, <c>false</c>.</value>
  143. protected override bool RefreshOnVersionChange
  144. {
  145. get
  146. {
  147. return true;
  148. }
  149. }
  150. /// <summary>
  151. /// Mounts the iso if needed.
  152. /// </summary>
  153. /// <param name="item">The item.</param>
  154. /// <param name="cancellationToken">The cancellation token.</param>
  155. /// <returns>IsoMount.</returns>
  156. protected virtual Task<IIsoMount> MountIsoIfNeeded(T item, CancellationToken cancellationToken)
  157. {
  158. return NullMountTaskResult;
  159. }
  160. /// <summary>
  161. /// Called when [pre fetch].
  162. /// </summary>
  163. /// <param name="item">The item.</param>
  164. /// <param name="mount">The mount.</param>
  165. protected virtual void OnPreFetch(T item, IIsoMount mount)
  166. {
  167. }
  168. /// <summary>
  169. /// Normalizes the FF probe result.
  170. /// </summary>
  171. /// <param name="result">The result.</param>
  172. private void NormalizeFFProbeResult(MediaInfoResult result)
  173. {
  174. if (result.format != null && result.format.tags != null)
  175. {
  176. result.format.tags = ConvertDictionaryToCaseInSensitive(result.format.tags);
  177. }
  178. if (result.streams != null)
  179. {
  180. // Convert all dictionaries to case insensitive
  181. foreach (var stream in result.streams)
  182. {
  183. if (stream.tags != null)
  184. {
  185. stream.tags = ConvertDictionaryToCaseInSensitive(stream.tags);
  186. }
  187. if (stream.disposition != null)
  188. {
  189. stream.disposition = ConvertDictionaryToCaseInSensitive(stream.disposition);
  190. }
  191. }
  192. }
  193. }
  194. /// <summary>
  195. /// Subclasses must set item values using this
  196. /// </summary>
  197. /// <param name="item">The item.</param>
  198. /// <param name="cancellationToken">The cancellation token.</param>
  199. /// <param name="result">The result.</param>
  200. /// <param name="isoMount">The iso mount.</param>
  201. /// <returns>Task.</returns>
  202. protected abstract void Fetch(T item, CancellationToken cancellationToken, MediaInfoResult result, IIsoMount isoMount);
  203. /// <summary>
  204. /// Converts ffprobe stream info to our MediaStream class
  205. /// </summary>
  206. /// <param name="streamInfo">The stream info.</param>
  207. /// <param name="formatInfo">The format info.</param>
  208. /// <returns>MediaStream.</returns>
  209. protected MediaStream GetMediaStream(MediaStreamInfo streamInfo, MediaFormatInfo formatInfo)
  210. {
  211. var stream = new MediaStream
  212. {
  213. Codec = streamInfo.codec_name,
  214. Language = GetDictionaryValue(streamInfo.tags, "language"),
  215. Profile = streamInfo.profile,
  216. Level = streamInfo.level,
  217. Index = streamInfo.index
  218. };
  219. if (streamInfo.codec_type.Equals("audio", StringComparison.OrdinalIgnoreCase))
  220. {
  221. stream.Type = MediaStreamType.Audio;
  222. stream.Channels = streamInfo.channels;
  223. if (!string.IsNullOrEmpty(streamInfo.sample_rate))
  224. {
  225. stream.SampleRate = int.Parse(streamInfo.sample_rate, UsCulture);
  226. }
  227. }
  228. else if (streamInfo.codec_type.Equals("subtitle", StringComparison.OrdinalIgnoreCase))
  229. {
  230. stream.Type = MediaStreamType.Subtitle;
  231. }
  232. else if (streamInfo.codec_type.Equals("data", StringComparison.OrdinalIgnoreCase))
  233. {
  234. stream.Type = MediaStreamType.Data;
  235. }
  236. else
  237. {
  238. stream.Type = MediaStreamType.Video;
  239. stream.Width = streamInfo.width;
  240. stream.Height = streamInfo.height;
  241. stream.PixelFormat = streamInfo.pix_fmt;
  242. stream.AspectRatio = streamInfo.display_aspect_ratio;
  243. stream.AverageFrameRate = GetFrameRate(streamInfo.avg_frame_rate);
  244. stream.RealFrameRate = GetFrameRate(streamInfo.r_frame_rate);
  245. }
  246. // Get stream bitrate
  247. if (stream.Type != MediaStreamType.Subtitle)
  248. {
  249. if (!string.IsNullOrEmpty(streamInfo.bit_rate))
  250. {
  251. stream.BitRate = int.Parse(streamInfo.bit_rate, UsCulture);
  252. }
  253. else if (formatInfo != null && !string.IsNullOrEmpty(formatInfo.bit_rate))
  254. {
  255. // If the stream info doesn't have a bitrate get the value from the media format info
  256. stream.BitRate = int.Parse(formatInfo.bit_rate, UsCulture);
  257. }
  258. }
  259. if (streamInfo.disposition != null)
  260. {
  261. var isDefault = GetDictionaryValue(streamInfo.disposition, "default");
  262. var isForced = GetDictionaryValue(streamInfo.disposition, "forced");
  263. stream.IsDefault = string.Equals(isDefault, "1", StringComparison.OrdinalIgnoreCase);
  264. stream.IsForced = string.Equals(isForced, "1", StringComparison.OrdinalIgnoreCase);
  265. }
  266. return stream;
  267. }
  268. /// <summary>
  269. /// Gets a frame rate from a string value in ffprobe output
  270. /// This could be a number or in the format of 2997/125.
  271. /// </summary>
  272. /// <param name="value">The value.</param>
  273. /// <returns>System.Nullable{System.Single}.</returns>
  274. private float? GetFrameRate(string value)
  275. {
  276. if (!string.IsNullOrEmpty(value))
  277. {
  278. var parts = value.Split('/');
  279. float result;
  280. if (parts.Length == 2)
  281. {
  282. result = float.Parse(parts[0], UsCulture) / float.Parse(parts[1], UsCulture);
  283. }
  284. else
  285. {
  286. result = float.Parse(parts[0], UsCulture);
  287. }
  288. return float.IsNaN(result) ? (float?)null : result;
  289. }
  290. return null;
  291. }
  292. /// <summary>
  293. /// Gets a string from an FFProbeResult tags dictionary
  294. /// </summary>
  295. /// <param name="tags">The tags.</param>
  296. /// <param name="key">The key.</param>
  297. /// <returns>System.String.</returns>
  298. protected string GetDictionaryValue(Dictionary<string, string> tags, string key)
  299. {
  300. if (tags == null)
  301. {
  302. return null;
  303. }
  304. string val;
  305. tags.TryGetValue(key, out val);
  306. return val;
  307. }
  308. /// <summary>
  309. /// Gets an int from an FFProbeResult tags dictionary
  310. /// </summary>
  311. /// <param name="tags">The tags.</param>
  312. /// <param name="key">The key.</param>
  313. /// <returns>System.Nullable{System.Int32}.</returns>
  314. protected int? GetDictionaryNumericValue(Dictionary<string, string> tags, string key)
  315. {
  316. var val = GetDictionaryValue(tags, key);
  317. if (!string.IsNullOrEmpty(val))
  318. {
  319. int i;
  320. if (int.TryParse(val, out i))
  321. {
  322. return i;
  323. }
  324. }
  325. return null;
  326. }
  327. /// <summary>
  328. /// Gets a DateTime from an FFProbeResult tags dictionary
  329. /// </summary>
  330. /// <param name="tags">The tags.</param>
  331. /// <param name="key">The key.</param>
  332. /// <returns>System.Nullable{DateTime}.</returns>
  333. protected DateTime? GetDictionaryDateTime(Dictionary<string, string> tags, string key)
  334. {
  335. var val = GetDictionaryValue(tags, key);
  336. if (!string.IsNullOrEmpty(val))
  337. {
  338. DateTime i;
  339. if (DateTime.TryParse(val, out i))
  340. {
  341. return i.ToUniversalTime();
  342. }
  343. }
  344. return null;
  345. }
  346. /// <summary>
  347. /// Converts a dictionary to case insensitive
  348. /// </summary>
  349. /// <param name="dict">The dict.</param>
  350. /// <returns>Dictionary{System.StringSystem.String}.</returns>
  351. private Dictionary<string, string> ConvertDictionaryToCaseInSensitive(Dictionary<string, string> dict)
  352. {
  353. return new Dictionary<string, string>(dict, StringComparer.OrdinalIgnoreCase);
  354. }
  355. }
  356. }