FFMpegManager.cs 38 KB

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