FFMpegManager.cs 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068
  1. using MediaBrowser.Common.IO;
  2. using MediaBrowser.Controller.Entities;
  3. using MediaBrowser.Controller.Entities.Audio;
  4. using MediaBrowser.Model.Entities;
  5. using MediaBrowser.Model.IO;
  6. using MediaBrowser.Model.Logging;
  7. using MediaBrowser.Model.Serialization;
  8. using System;
  9. using System.Collections.Generic;
  10. using System.ComponentModel;
  11. using System.Diagnostics;
  12. using System.IO;
  13. using System.Linq;
  14. using System.Reflection;
  15. using System.Runtime.InteropServices;
  16. using System.Threading;
  17. using System.Threading.Tasks;
  18. namespace MediaBrowser.Controller.MediaInfo
  19. {
  20. /// <summary>
  21. /// Class FFMpegManager
  22. /// </summary>
  23. public class FFMpegManager : IDisposable
  24. {
  25. /// <summary>
  26. /// Gets or sets the video image cache.
  27. /// </summary>
  28. /// <value>The video image cache.</value>
  29. internal FileSystemRepository VideoImageCache { get; set; }
  30. /// <summary>
  31. /// Gets or sets the image cache.
  32. /// </summary>
  33. /// <value>The image cache.</value>
  34. internal FileSystemRepository AudioImageCache { get; set; }
  35. /// <summary>
  36. /// Gets or sets the subtitle cache.
  37. /// </summary>
  38. /// <value>The subtitle cache.</value>
  39. internal FileSystemRepository SubtitleCache { get; set; }
  40. /// <summary>
  41. /// Gets or sets the zip client.
  42. /// </summary>
  43. /// <value>The zip client.</value>
  44. private readonly IZipClient _zipClient;
  45. /// <summary>
  46. /// The _logger
  47. /// </summary>
  48. private readonly Kernel _kernel;
  49. /// <summary>
  50. /// The _logger
  51. /// </summary>
  52. private readonly ILogger _logger;
  53. /// <summary>
  54. /// Gets the json serializer.
  55. /// </summary>
  56. /// <value>The json serializer.</value>
  57. private readonly IJsonSerializer _jsonSerializer;
  58. /// <summary>
  59. /// The _protobuf serializer
  60. /// </summary>
  61. private readonly IProtobufSerializer _protobufSerializer;
  62. private readonly IServerApplicationPaths _appPaths;
  63. /// <summary>
  64. /// Initializes a new instance of the <see cref="FFMpegManager" /> class.
  65. /// </summary>
  66. /// <param name="kernel">The kernel.</param>
  67. /// <param name="zipClient">The zip client.</param>
  68. /// <param name="jsonSerializer">The json serializer.</param>
  69. /// <param name="protobufSerializer">The protobuf serializer.</param>
  70. /// <param name="logger">The logger.</param>
  71. /// <exception cref="System.ArgumentNullException">zipClient</exception>
  72. public FFMpegManager(Kernel kernel, IZipClient zipClient, IJsonSerializer jsonSerializer, IProtobufSerializer protobufSerializer, ILogManager logManager, IServerApplicationPaths appPaths)
  73. {
  74. if (kernel == null)
  75. {
  76. throw new ArgumentNullException("kernel");
  77. }
  78. if (zipClient == null)
  79. {
  80. throw new ArgumentNullException("zipClient");
  81. }
  82. if (jsonSerializer == null)
  83. {
  84. throw new ArgumentNullException("jsonSerializer");
  85. }
  86. if (protobufSerializer == null)
  87. {
  88. throw new ArgumentNullException("protobufSerializer");
  89. }
  90. _kernel = kernel;
  91. _zipClient = zipClient;
  92. _jsonSerializer = jsonSerializer;
  93. _protobufSerializer = protobufSerializer;
  94. _appPaths = appPaths;
  95. _logger = logManager.GetLogger("FFMpegManager");
  96. // Not crazy about this but it's the only way to suppress ffmpeg crash dialog boxes
  97. SetErrorMode(ErrorModes.SEM_FAILCRITICALERRORS | ErrorModes.SEM_NOALIGNMENTFAULTEXCEPT | ErrorModes.SEM_NOGPFAULTERRORBOX | ErrorModes.SEM_NOOPENFILEERRORBOX);
  98. VideoImageCache = new FileSystemRepository(VideoImagesDataPath);
  99. AudioImageCache = new FileSystemRepository(AudioImagesDataPath);
  100. SubtitleCache = new FileSystemRepository(SubtitleCachePath);
  101. Task.Run(() => VersionedDirectoryPath = GetVersionedDirectoryPath());
  102. }
  103. public void Dispose()
  104. {
  105. Dispose(true);
  106. }
  107. /// <summary>
  108. /// Releases unmanaged and - optionally - managed resources.
  109. /// </summary>
  110. /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
  111. protected void Dispose(bool dispose)
  112. {
  113. if (dispose)
  114. {
  115. SetErrorMode(ErrorModes.SYSTEM_DEFAULT);
  116. AudioImageCache.Dispose();
  117. VideoImageCache.Dispose();
  118. }
  119. }
  120. /// <summary>
  121. /// The FF probe resource pool count
  122. /// </summary>
  123. private const int FFProbeResourcePoolCount = 3;
  124. /// <summary>
  125. /// The audio image resource pool count
  126. /// </summary>
  127. private const int AudioImageResourcePoolCount = 3;
  128. /// <summary>
  129. /// The video image resource pool count
  130. /// </summary>
  131. private const int VideoImageResourcePoolCount = 1;
  132. /// <summary>
  133. /// The FF probe resource pool
  134. /// </summary>
  135. private readonly SemaphoreSlim FFProbeResourcePool = new SemaphoreSlim(FFProbeResourcePoolCount, FFProbeResourcePoolCount);
  136. /// <summary>
  137. /// The audio image resource pool
  138. /// </summary>
  139. private readonly SemaphoreSlim AudioImageResourcePool = new SemaphoreSlim(AudioImageResourcePoolCount, AudioImageResourcePoolCount);
  140. /// <summary>
  141. /// The video image resource pool
  142. /// </summary>
  143. private readonly SemaphoreSlim VideoImageResourcePool = new SemaphoreSlim(VideoImageResourcePoolCount, VideoImageResourcePoolCount);
  144. /// <summary>
  145. /// Gets or sets the versioned directory path.
  146. /// </summary>
  147. /// <value>The versioned directory path.</value>
  148. private string VersionedDirectoryPath { get; set; }
  149. /// <summary>
  150. /// Gets the FFMPEG version.
  151. /// </summary>
  152. /// <value>The FFMPEG version.</value>
  153. public string FFMpegVersion
  154. {
  155. get { return Path.GetFileNameWithoutExtension(VersionedDirectoryPath); }
  156. }
  157. /// <summary>
  158. /// The _ FF MPEG path
  159. /// </summary>
  160. private string _FFMpegPath;
  161. /// <summary>
  162. /// Gets the path to ffmpeg.exe
  163. /// </summary>
  164. /// <value>The FF MPEG path.</value>
  165. public string FFMpegPath
  166. {
  167. get
  168. {
  169. return _FFMpegPath ?? (_FFMpegPath = Path.Combine(VersionedDirectoryPath, "ffmpeg.exe"));
  170. }
  171. }
  172. /// <summary>
  173. /// The _ FF probe path
  174. /// </summary>
  175. private string _FFProbePath;
  176. /// <summary>
  177. /// Gets the path to ffprobe.exe
  178. /// </summary>
  179. /// <value>The FF probe path.</value>
  180. public string FFProbePath
  181. {
  182. get
  183. {
  184. return _FFProbePath ?? (_FFProbePath = Path.Combine(VersionedDirectoryPath, "ffprobe.exe"));
  185. }
  186. }
  187. /// <summary>
  188. /// The _video images data path
  189. /// </summary>
  190. private string _videoImagesDataPath;
  191. /// <summary>
  192. /// Gets the video images data path.
  193. /// </summary>
  194. /// <value>The video images data path.</value>
  195. public string VideoImagesDataPath
  196. {
  197. get
  198. {
  199. if (_videoImagesDataPath == null)
  200. {
  201. _videoImagesDataPath = Path.Combine(_appPaths.DataPath, "ffmpeg-video-images");
  202. if (!Directory.Exists(_videoImagesDataPath))
  203. {
  204. Directory.CreateDirectory(_videoImagesDataPath);
  205. }
  206. }
  207. return _videoImagesDataPath;
  208. }
  209. }
  210. /// <summary>
  211. /// The _audio images data path
  212. /// </summary>
  213. private string _audioImagesDataPath;
  214. /// <summary>
  215. /// Gets the audio images data path.
  216. /// </summary>
  217. /// <value>The audio images data path.</value>
  218. public string AudioImagesDataPath
  219. {
  220. get
  221. {
  222. if (_audioImagesDataPath == null)
  223. {
  224. _audioImagesDataPath = Path.Combine(_appPaths.DataPath, "ffmpeg-audio-images");
  225. if (!Directory.Exists(_audioImagesDataPath))
  226. {
  227. Directory.CreateDirectory(_audioImagesDataPath);
  228. }
  229. }
  230. return _audioImagesDataPath;
  231. }
  232. }
  233. /// <summary>
  234. /// The _subtitle cache path
  235. /// </summary>
  236. private string _subtitleCachePath;
  237. /// <summary>
  238. /// Gets the subtitle cache path.
  239. /// </summary>
  240. /// <value>The subtitle cache path.</value>
  241. public string SubtitleCachePath
  242. {
  243. get
  244. {
  245. if (_subtitleCachePath == null)
  246. {
  247. _subtitleCachePath = Path.Combine(_appPaths.CachePath, "ffmpeg-subtitles");
  248. if (!Directory.Exists(_subtitleCachePath))
  249. {
  250. Directory.CreateDirectory(_subtitleCachePath);
  251. }
  252. }
  253. return _subtitleCachePath;
  254. }
  255. }
  256. /// <summary>
  257. /// Gets the versioned directory path.
  258. /// </summary>
  259. /// <returns>System.String.</returns>
  260. private string GetVersionedDirectoryPath()
  261. {
  262. var assembly = GetType().Assembly;
  263. const string prefix = "MediaBrowser.Controller.MediaInfo.";
  264. const string srch = prefix + "ffmpeg";
  265. var resource = assembly.GetManifestResourceNames().First(r => r.StartsWith(srch));
  266. var filename = resource.Substring(resource.IndexOf(prefix, StringComparison.OrdinalIgnoreCase) + prefix.Length);
  267. var versionedDirectoryPath = Path.Combine(_appPaths.MediaToolsPath, Path.GetFileNameWithoutExtension(filename));
  268. if (!Directory.Exists(versionedDirectoryPath))
  269. {
  270. Directory.CreateDirectory(versionedDirectoryPath);
  271. }
  272. ExtractTools(assembly, resource, versionedDirectoryPath);
  273. return versionedDirectoryPath;
  274. }
  275. /// <summary>
  276. /// Extracts the tools.
  277. /// </summary>
  278. /// <param name="assembly">The assembly.</param>
  279. /// <param name="zipFileResourcePath">The zip file resource path.</param>
  280. /// <param name="targetPath">The target path.</param>
  281. private void ExtractTools(Assembly assembly, string zipFileResourcePath, string targetPath)
  282. {
  283. using (var resourceStream = assembly.GetManifestResourceStream(zipFileResourcePath))
  284. {
  285. _zipClient.ExtractAll(resourceStream, targetPath, false);
  286. }
  287. }
  288. /// <summary>
  289. /// Gets the probe size argument.
  290. /// </summary>
  291. /// <param name="item">The item.</param>
  292. /// <returns>System.String.</returns>
  293. public string GetProbeSizeArgument(BaseItem item)
  294. {
  295. var video = item as Video;
  296. return video != null ? GetProbeSizeArgument(video.VideoType, video.IsoType) : string.Empty;
  297. }
  298. /// <summary>
  299. /// Gets the probe size argument.
  300. /// </summary>
  301. /// <param name="videoType">Type of the video.</param>
  302. /// <param name="isoType">Type of the iso.</param>
  303. /// <returns>System.String.</returns>
  304. public string GetProbeSizeArgument(VideoType videoType, IsoType? isoType)
  305. {
  306. if (videoType == VideoType.Dvd || (isoType.HasValue && isoType.Value == IsoType.Dvd))
  307. {
  308. return "-probesize 1G -analyzeduration 200M";
  309. }
  310. return string.Empty;
  311. }
  312. /// <summary>
  313. /// Runs FFProbe against a BaseItem
  314. /// </summary>
  315. /// <param name="item">The item.</param>
  316. /// <param name="inputPath">The input path.</param>
  317. /// <param name="lastDateModified">The last date modified.</param>
  318. /// <param name="cache">The cache.</param>
  319. /// <param name="cancellationToken">The cancellation token.</param>
  320. /// <returns>Task{FFProbeResult}.</returns>
  321. /// <exception cref="System.ArgumentNullException">item</exception>
  322. public Task<FFProbeResult> RunFFProbe(BaseItem item, string inputPath, DateTime lastDateModified, FileSystemRepository cache, CancellationToken cancellationToken)
  323. {
  324. if (string.IsNullOrEmpty(inputPath))
  325. {
  326. throw new ArgumentNullException("inputPath");
  327. }
  328. if (cache == null)
  329. {
  330. throw new ArgumentNullException("cache");
  331. }
  332. // Put the ffmpeg version into the cache name so that it's unique per-version
  333. // We don't want to try and deserialize data based on an old version, which could potentially fail
  334. var resourceName = item.Id + "_" + lastDateModified.Ticks + "_" + FFMpegVersion;
  335. // Forumulate the cache file path
  336. var cacheFilePath = cache.GetResourcePath(resourceName, ".pb");
  337. cancellationToken.ThrowIfCancellationRequested();
  338. // Avoid File.Exists by just trying to deserialize
  339. try
  340. {
  341. return Task.FromResult(_protobufSerializer.DeserializeFromFile<FFProbeResult>(cacheFilePath));
  342. }
  343. catch (FileNotFoundException)
  344. {
  345. var extractChapters = false;
  346. var video = item as Video;
  347. var probeSizeArgument = string.Empty;
  348. if (video != null)
  349. {
  350. extractChapters = true;
  351. probeSizeArgument = GetProbeSizeArgument(video.VideoType, video.IsoType);
  352. }
  353. return RunFFProbeInternal(inputPath, extractChapters, cacheFilePath, probeSizeArgument, cancellationToken);
  354. }
  355. }
  356. /// <summary>
  357. /// Runs FFProbe against a BaseItem
  358. /// </summary>
  359. /// <param name="inputPath">The input path.</param>
  360. /// <param name="extractChapters">if set to <c>true</c> [extract chapters].</param>
  361. /// <param name="cacheFile">The cache file.</param>
  362. /// <param name="probeSizeArgument">The probe size argument.</param>
  363. /// <param name="cancellationToken">The cancellation token.</param>
  364. /// <returns>Task{FFProbeResult}.</returns>
  365. /// <exception cref="System.ApplicationException"></exception>
  366. private async Task<FFProbeResult> RunFFProbeInternal(string inputPath, bool extractChapters, string cacheFile, string probeSizeArgument, CancellationToken cancellationToken)
  367. {
  368. var process = new Process
  369. {
  370. StartInfo = new ProcessStartInfo
  371. {
  372. CreateNoWindow = true,
  373. UseShellExecute = false,
  374. // Must consume both or ffmpeg may hang due to deadlocks. See comments below.
  375. RedirectStandardOutput = true,
  376. RedirectStandardError = true,
  377. FileName = FFProbePath,
  378. Arguments = string.Format("{0} -i {1} -threads 0 -v info -print_format json -show_streams -show_format", probeSizeArgument, inputPath).Trim(),
  379. WindowStyle = ProcessWindowStyle.Hidden,
  380. ErrorDialog = false
  381. },
  382. EnableRaisingEvents = true
  383. };
  384. _logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
  385. process.Exited += ProcessExited;
  386. await FFProbeResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
  387. FFProbeResult result;
  388. string standardError = null;
  389. try
  390. {
  391. process.Start();
  392. Task<string> standardErrorReadTask = null;
  393. // MUST read both stdout and stderr asynchronously or a deadlock may occurr
  394. if (extractChapters)
  395. {
  396. standardErrorReadTask = process.StandardError.ReadToEndAsync();
  397. }
  398. else
  399. {
  400. process.BeginErrorReadLine();
  401. }
  402. result = _jsonSerializer.DeserializeFromStream<FFProbeResult>(process.StandardOutput.BaseStream);
  403. if (extractChapters)
  404. {
  405. standardError = await standardErrorReadTask.ConfigureAwait(false);
  406. }
  407. }
  408. catch
  409. {
  410. // Hate having to do this
  411. try
  412. {
  413. process.Kill();
  414. }
  415. catch (InvalidOperationException ex1)
  416. {
  417. _logger.ErrorException("Error killing ffprobe", ex1);
  418. }
  419. catch (Win32Exception ex1)
  420. {
  421. _logger.ErrorException("Error killing ffprobe", ex1);
  422. }
  423. throw;
  424. }
  425. finally
  426. {
  427. FFProbeResourcePool.Release();
  428. }
  429. if (result == null)
  430. {
  431. throw new ApplicationException(string.Format("FFProbe failed for {0}", inputPath));
  432. }
  433. cancellationToken.ThrowIfCancellationRequested();
  434. if (extractChapters && !string.IsNullOrEmpty(standardError))
  435. {
  436. AddChapters(result, standardError);
  437. }
  438. _protobufSerializer.SerializeToFile(result, cacheFile);
  439. return result;
  440. }
  441. /// <summary>
  442. /// Adds the chapters.
  443. /// </summary>
  444. /// <param name="result">The result.</param>
  445. /// <param name="standardError">The standard error.</param>
  446. private void AddChapters(FFProbeResult result, string standardError)
  447. {
  448. var lines = standardError.Split('\n').Select(l => l.TrimStart());
  449. var chapters = new List<ChapterInfo> { };
  450. ChapterInfo lastChapter = null;
  451. foreach (var line in lines)
  452. {
  453. if (line.StartsWith("Chapter", StringComparison.OrdinalIgnoreCase))
  454. {
  455. // Example:
  456. // Chapter #0.2: start 400.534, end 4565.435
  457. const string srch = "start ";
  458. var start = line.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
  459. if (start == -1)
  460. {
  461. continue;
  462. }
  463. var subString = line.Substring(start + srch.Length);
  464. subString = subString.Substring(0, subString.IndexOf(','));
  465. double seconds;
  466. if (double.TryParse(subString, out seconds))
  467. {
  468. lastChapter = new ChapterInfo
  469. {
  470. StartPositionTicks = TimeSpan.FromSeconds(seconds).Ticks
  471. };
  472. chapters.Add(lastChapter);
  473. }
  474. }
  475. else if (line.StartsWith("title", StringComparison.OrdinalIgnoreCase))
  476. {
  477. if (lastChapter != null && string.IsNullOrEmpty(lastChapter.Name))
  478. {
  479. var index = line.IndexOf(':');
  480. if (index != -1)
  481. {
  482. lastChapter.Name = line.Substring(index + 1).Trim().TrimEnd('\r');
  483. }
  484. }
  485. }
  486. }
  487. result.Chapters = chapters;
  488. }
  489. /// <summary>
  490. /// The first chapter ticks
  491. /// </summary>
  492. private static long FirstChapterTicks = TimeSpan.FromSeconds(15).Ticks;
  493. /// <summary>
  494. /// Extracts the chapter images.
  495. /// </summary>
  496. /// <param name="video">The video.</param>
  497. /// <param name="cancellationToken">The cancellation token.</param>
  498. /// <param name="extractImages">if set to <c>true</c> [extract images].</param>
  499. /// <param name="saveItem">if set to <c>true</c> [save item].</param>
  500. /// <returns>Task.</returns>
  501. /// <exception cref="System.ArgumentNullException"></exception>
  502. public async Task PopulateChapterImages(Video video, CancellationToken cancellationToken, bool extractImages, bool saveItem)
  503. {
  504. if (video.Chapters == null)
  505. {
  506. throw new ArgumentNullException();
  507. }
  508. // Can't extract images if there are no video streams
  509. if (video.MediaStreams == null || video.MediaStreams.All(m => m.Type != MediaStreamType.Video))
  510. {
  511. return;
  512. }
  513. var changesMade = false;
  514. foreach (var chapter in video.Chapters)
  515. {
  516. var filename = video.Id + "_" + video.DateModified.Ticks + "_" + chapter.StartPositionTicks;
  517. var path = VideoImageCache.GetResourcePath(filename, ".jpg");
  518. if (!VideoImageCache.ContainsFilePath(path))
  519. {
  520. if (extractImages)
  521. {
  522. // Disable for now on folder rips
  523. if (video.VideoType != VideoType.VideoFile)
  524. {
  525. continue;
  526. }
  527. // Add some time for the first chapter to make sure we don't end up with a black image
  528. var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(FirstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks);
  529. var success = await ExtractImage(GetInputArgument(video), time, path, cancellationToken).ConfigureAwait(false);
  530. if (success)
  531. {
  532. chapter.ImagePath = path;
  533. changesMade = true;
  534. }
  535. }
  536. }
  537. else if (!string.Equals(path, chapter.ImagePath, StringComparison.OrdinalIgnoreCase))
  538. {
  539. chapter.ImagePath = path;
  540. changesMade = true;
  541. }
  542. }
  543. if (saveItem && changesMade)
  544. {
  545. await _kernel.ItemRepository.SaveItem(video, CancellationToken.None).ConfigureAwait(false);
  546. }
  547. }
  548. /// <summary>
  549. /// Extracts an image from an Audio file and returns a Task whose result indicates whether it was successful or not
  550. /// </summary>
  551. /// <param name="input">The input.</param>
  552. /// <param name="outputPath">The output path.</param>
  553. /// <param name="cancellationToken">The cancellation token.</param>
  554. /// <returns>Task{System.Boolean}.</returns>
  555. /// <exception cref="System.ArgumentNullException">input</exception>
  556. public async Task<bool> ExtractImage(Audio input, string outputPath, CancellationToken cancellationToken)
  557. {
  558. if (input == null)
  559. {
  560. throw new ArgumentNullException("input");
  561. }
  562. if (string.IsNullOrEmpty(outputPath))
  563. {
  564. throw new ArgumentNullException("outputPath");
  565. }
  566. var process = new Process
  567. {
  568. StartInfo = new ProcessStartInfo
  569. {
  570. CreateNoWindow = true,
  571. UseShellExecute = false,
  572. FileName = FFMpegPath,
  573. Arguments = string.Format("-i {0} -threads 0 -v quiet -f image2 \"{1}\"", GetInputArgument(input), outputPath),
  574. WindowStyle = ProcessWindowStyle.Hidden,
  575. ErrorDialog = false
  576. }
  577. };
  578. await AudioImageResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
  579. await RunAsync(process).ConfigureAwait(false);
  580. AudioImageResourcePool.Release();
  581. var exitCode = process.ExitCode;
  582. process.Dispose();
  583. if (exitCode != -1 && File.Exists(outputPath))
  584. {
  585. return true;
  586. }
  587. _logger.Error("ffmpeg audio image extraction failed for {0}", input.Path);
  588. return false;
  589. }
  590. /// <summary>
  591. /// Determines whether [is subtitle cached] [the specified input].
  592. /// </summary>
  593. /// <param name="input">The input.</param>
  594. /// <param name="subtitleStreamIndex">Index of the subtitle stream.</param>
  595. /// <param name="outputExtension">The output extension.</param>
  596. /// <returns><c>true</c> if [is subtitle cached] [the specified input]; otherwise, <c>false</c>.</returns>
  597. public bool IsSubtitleCached(Video input, int subtitleStreamIndex, string outputExtension)
  598. {
  599. return SubtitleCache.ContainsFilePath(GetSubtitleCachePath(input, subtitleStreamIndex, outputExtension));
  600. }
  601. /// <summary>
  602. /// Gets the subtitle cache path.
  603. /// </summary>
  604. /// <param name="input">The input.</param>
  605. /// <param name="subtitleStreamIndex">Index of the subtitle stream.</param>
  606. /// <param name="outputExtension">The output extension.</param>
  607. /// <returns>System.String.</returns>
  608. public string GetSubtitleCachePath(Video input, int subtitleStreamIndex, string outputExtension)
  609. {
  610. return SubtitleCache.GetResourcePath(input.Id + "_" + subtitleStreamIndex + "_" + input.DateModified.Ticks, outputExtension);
  611. }
  612. /// <summary>
  613. /// Extracts the text subtitle.
  614. /// </summary>
  615. /// <param name="input">The input.</param>
  616. /// <param name="subtitleStreamIndex">Index of the subtitle stream.</param>
  617. /// <param name="outputPath">The output path.</param>
  618. /// <param name="cancellationToken">The cancellation token.</param>
  619. /// <returns>Task{System.Boolean}.</returns>
  620. /// <exception cref="System.ArgumentNullException">input</exception>
  621. public async Task<bool> ExtractTextSubtitle(Video input, int subtitleStreamIndex, string outputPath, CancellationToken cancellationToken)
  622. {
  623. if (input == null)
  624. {
  625. throw new ArgumentNullException("input");
  626. }
  627. if (cancellationToken == null)
  628. {
  629. throw new ArgumentNullException("cancellationToken");
  630. }
  631. var process = new Process
  632. {
  633. StartInfo = new ProcessStartInfo
  634. {
  635. CreateNoWindow = true,
  636. UseShellExecute = false,
  637. FileName = FFMpegPath,
  638. Arguments = string.Format("-i {0} -map 0:{1} -an -vn -c:s ass \"{2}\"", GetInputArgument(input), subtitleStreamIndex, outputPath),
  639. WindowStyle = ProcessWindowStyle.Hidden,
  640. ErrorDialog = false
  641. }
  642. };
  643. _logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
  644. await AudioImageResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
  645. await RunAsync(process).ConfigureAwait(false);
  646. AudioImageResourcePool.Release();
  647. var exitCode = process.ExitCode;
  648. process.Dispose();
  649. if (exitCode != -1 && File.Exists(outputPath))
  650. {
  651. return true;
  652. }
  653. _logger.Error("ffmpeg subtitle extraction failed for {0}", input.Path);
  654. return false;
  655. }
  656. /// <summary>
  657. /// Converts the text subtitle.
  658. /// </summary>
  659. /// <param name="mediaStream">The media stream.</param>
  660. /// <param name="outputPath">The output path.</param>
  661. /// <param name="cancellationToken">The cancellation token.</param>
  662. /// <returns>Task{System.Boolean}.</returns>
  663. /// <exception cref="System.ArgumentNullException">mediaStream</exception>
  664. /// <exception cref="System.ArgumentException">The given MediaStream is not an external subtitle stream</exception>
  665. public async Task<bool> ConvertTextSubtitle(MediaStream mediaStream, string outputPath, CancellationToken cancellationToken)
  666. {
  667. if (mediaStream == null)
  668. {
  669. throw new ArgumentNullException("mediaStream");
  670. }
  671. if (!mediaStream.IsExternal || string.IsNullOrEmpty(mediaStream.Path))
  672. {
  673. throw new ArgumentException("The given MediaStream is not an external subtitle stream");
  674. }
  675. var process = new Process
  676. {
  677. StartInfo = new ProcessStartInfo
  678. {
  679. CreateNoWindow = true,
  680. UseShellExecute = false,
  681. FileName = FFMpegPath,
  682. Arguments = string.Format("-i \"{0}\" \"{1}\"", mediaStream.Path, outputPath),
  683. WindowStyle = ProcessWindowStyle.Hidden,
  684. ErrorDialog = false
  685. }
  686. };
  687. _logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
  688. await AudioImageResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
  689. await RunAsync(process).ConfigureAwait(false);
  690. AudioImageResourcePool.Release();
  691. var exitCode = process.ExitCode;
  692. process.Dispose();
  693. if (exitCode != -1 && File.Exists(outputPath))
  694. {
  695. return true;
  696. }
  697. _logger.Error("ffmpeg subtitle conversion failed for {0}", mediaStream.Path);
  698. return false;
  699. }
  700. /// <summary>
  701. /// Extracts an image from a Video and returns a Task whose result indicates whether it was successful or not
  702. /// </summary>
  703. /// <param name="inputPath">The input path.</param>
  704. /// <param name="offset">The offset.</param>
  705. /// <param name="outputPath">The output path.</param>
  706. /// <param name="cancellationToken">The cancellation token.</param>
  707. /// <returns>Task{System.Boolean}.</returns>
  708. /// <exception cref="System.ArgumentNullException">video</exception>
  709. public async Task<bool> ExtractImage(string inputPath, TimeSpan offset, string outputPath, CancellationToken cancellationToken)
  710. {
  711. if (string.IsNullOrEmpty(inputPath))
  712. {
  713. throw new ArgumentNullException("inputPath");
  714. }
  715. if (string.IsNullOrEmpty(outputPath))
  716. {
  717. throw new ArgumentNullException("outputPath");
  718. }
  719. var process = new Process
  720. {
  721. StartInfo = new ProcessStartInfo
  722. {
  723. CreateNoWindow = true,
  724. UseShellExecute = false,
  725. FileName = FFMpegPath,
  726. Arguments = string.Format("-ss {0} -i {1} -threads 0 -v quiet -t 1 -f image2 \"{2}\"", Convert.ToInt32(offset.TotalSeconds), inputPath, outputPath),
  727. WindowStyle = ProcessWindowStyle.Hidden,
  728. ErrorDialog = false
  729. }
  730. };
  731. await VideoImageResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
  732. process.Start();
  733. var ranToCompletion = process.WaitForExit(10000);
  734. if (!ranToCompletion)
  735. {
  736. try
  737. {
  738. _logger.Info("Killing ffmpeg process");
  739. process.Kill();
  740. process.WaitForExit(1000);
  741. }
  742. catch (Win32Exception ex)
  743. {
  744. _logger.ErrorException("Error killing process", ex);
  745. }
  746. catch (InvalidOperationException ex)
  747. {
  748. _logger.ErrorException("Error killing process", ex);
  749. }
  750. catch (NotSupportedException ex)
  751. {
  752. _logger.ErrorException("Error killing process", ex);
  753. }
  754. }
  755. VideoImageResourcePool.Release();
  756. var exitCode = ranToCompletion ? process.ExitCode : -1;
  757. process.Dispose();
  758. if (exitCode == -1)
  759. {
  760. if (File.Exists(outputPath))
  761. {
  762. try
  763. {
  764. _logger.Info("Deleting extracted image due to failure: ", outputPath);
  765. File.Delete(outputPath);
  766. }
  767. catch (IOException ex)
  768. {
  769. _logger.ErrorException("Error deleting extracted image {0}", ex, outputPath);
  770. }
  771. }
  772. }
  773. else
  774. {
  775. if (File.Exists(outputPath))
  776. {
  777. return true;
  778. }
  779. }
  780. _logger.Error("ffmpeg video image extraction failed for {0}", inputPath);
  781. return false;
  782. }
  783. /// <summary>
  784. /// Gets the input argument.
  785. /// </summary>
  786. /// <param name="item">The item.</param>
  787. /// <returns>System.String.</returns>
  788. public string GetInputArgument(BaseItem item)
  789. {
  790. var video = item as Video;
  791. if (video != null)
  792. {
  793. if (video.VideoType == VideoType.BluRay)
  794. {
  795. return GetBlurayInputArgument(video.Path);
  796. }
  797. if (video.VideoType == VideoType.Dvd)
  798. {
  799. return GetDvdInputArgument(video.GetPlayableStreamFiles());
  800. }
  801. }
  802. return string.Format("file:\"{0}\"", item.Path);
  803. }
  804. /// <summary>
  805. /// Gets the input argument.
  806. /// </summary>
  807. /// <param name="item">The item.</param>
  808. /// <param name="mount">The mount.</param>
  809. /// <returns>System.String.</returns>
  810. public string GetInputArgument(Video item, IIsoMount mount)
  811. {
  812. if (item.VideoType == VideoType.Iso && item.IsoType.HasValue)
  813. {
  814. if (item.IsoType.Value == IsoType.BluRay)
  815. {
  816. return GetBlurayInputArgument(mount.MountedPath);
  817. }
  818. if (item.IsoType.Value == IsoType.Dvd)
  819. {
  820. return GetDvdInputArgument(item.GetPlayableStreamFiles(mount.MountedPath));
  821. }
  822. }
  823. return GetInputArgument(item);
  824. }
  825. /// <summary>
  826. /// Gets the bluray input argument.
  827. /// </summary>
  828. /// <param name="blurayRoot">The bluray root.</param>
  829. /// <returns>System.String.</returns>
  830. public string GetBlurayInputArgument(string blurayRoot)
  831. {
  832. return string.Format("bluray:\"{0}\"", blurayRoot);
  833. }
  834. /// <summary>
  835. /// Gets the DVD input argument.
  836. /// </summary>
  837. /// <param name="playableStreamFiles">The playable stream files.</param>
  838. /// <returns>System.String.</returns>
  839. public string GetDvdInputArgument(IEnumerable<string> playableStreamFiles)
  840. {
  841. // Get all streams
  842. var streamFilePaths = (playableStreamFiles ?? new string[] { }).ToArray();
  843. // If there's more than one we'll need to use the concat command
  844. if (streamFilePaths.Length > 1)
  845. {
  846. var files = string.Join("|", streamFilePaths);
  847. return string.Format("concat:\"{0}\"", files);
  848. }
  849. // Determine the input path for video files
  850. return string.Format("file:\"{0}\"", streamFilePaths[0]);
  851. }
  852. /// <summary>
  853. /// Processes the exited.
  854. /// </summary>
  855. /// <param name="sender">The sender.</param>
  856. /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
  857. void ProcessExited(object sender, EventArgs e)
  858. {
  859. ((Process)sender).Dispose();
  860. }
  861. /// <summary>
  862. /// Provides a non-blocking method to start a process and wait asynchronously for it to exit
  863. /// </summary>
  864. /// <param name="process">The process.</param>
  865. /// <returns>Task{System.Boolean}.</returns>
  866. private static Task<bool> RunAsync(Process process)
  867. {
  868. var tcs = new TaskCompletionSource<bool>();
  869. process.EnableRaisingEvents = true;
  870. process.Exited += (sender, args) => tcs.SetResult(true);
  871. process.Start();
  872. return tcs.Task;
  873. }
  874. /// <summary>
  875. /// Sets the error mode.
  876. /// </summary>
  877. /// <param name="uMode">The u mode.</param>
  878. /// <returns>ErrorModes.</returns>
  879. [DllImport("kernel32.dll")]
  880. static extern ErrorModes SetErrorMode(ErrorModes uMode);
  881. /// <summary>
  882. /// Enum ErrorModes
  883. /// </summary>
  884. [Flags]
  885. public enum ErrorModes : uint
  886. {
  887. /// <summary>
  888. /// The SYSTE m_ DEFAULT
  889. /// </summary>
  890. SYSTEM_DEFAULT = 0x0,
  891. /// <summary>
  892. /// The SE m_ FAILCRITICALERRORS
  893. /// </summary>
  894. SEM_FAILCRITICALERRORS = 0x0001,
  895. /// <summary>
  896. /// The SE m_ NOALIGNMENTFAULTEXCEPT
  897. /// </summary>
  898. SEM_NOALIGNMENTFAULTEXCEPT = 0x0004,
  899. /// <summary>
  900. /// The SE m_ NOGPFAULTERRORBOX
  901. /// </summary>
  902. SEM_NOGPFAULTERRORBOX = 0x0002,
  903. /// <summary>
  904. /// The SE m_ NOOPENFILEERRORBOX
  905. /// </summary>
  906. SEM_NOOPENFILEERRORBOX = 0x8000
  907. }
  908. }
  909. }