/Plugins/FFmpeg/FFmpegManager.cs

https://github.com/cchitsiang/resizer · C# · 278 lines · 205 code · 49 blank · 24 comment · 31 complexity · 94c2bd4a75281f1070435355724f83fa MD5 · raw file

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using System.IO;
  5. using System.Reflection;
  6. using ImageResizer.Util;
  7. using System.Drawing;
  8. using System.Diagnostics;
  9. using System.Web;
  10. using System.Threading;
  11. using System.Globalization;
  12. using ImageResizer.ExtensionMethods;
  13. using System.Xml.Linq;
  14. using System.Xml;
  15. using System.Web.Hosting;
  16. using ImageResizer.Configuration;
  17. using AForge.Imaging.Filters;
  18. using System.Linq;
  19. namespace ImageResizer.Plugins.FFmpeg
  20. {
  21. class FFmpegManager
  22. {
  23. public FFmpegManager()
  24. {
  25. MaxConcurrentExecutions = 0;
  26. MaxConcurrentWaitingThreads = 0;
  27. }
  28. private string ffmpegPath;
  29. private string ffprobePath;
  30. public string GetFFmpegPath()
  31. {
  32. if (ffmpegPath == null) LocateExeFiles();
  33. return ffmpegPath;
  34. }
  35. public string GetFFprobePath()
  36. {
  37. if (ffprobePath == null) LocateExeFiles();
  38. return ffprobePath;
  39. }
  40. private object _locateLock = new object();
  41. private void LocateExeFiles()
  42. {
  43. lock (_locateLock)
  44. {
  45. List<string> searchFolders = new List<string>() { };
  46. var a = this.GetType().Assembly;
  47. //Use CodeBase if it is physical; this means we don't re-download each time we recycle.
  48. //If it's a URL, we fall back to Location, which is often the shadow-copied version.
  49. var searchFolder = a.CodeBase.StartsWith("file:///", StringComparison.OrdinalIgnoreCase)
  50. ? a.CodeBase
  51. : a.Location;
  52. //Convert UNC paths
  53. searchFolder = Path.GetDirectoryName(searchFolder.Replace("file:///", "").Replace("/", "\\"));
  54. searchFolders.Add(searchFolder);
  55. foreach (string basePath in searchFolders)
  56. {
  57. if (ffmpegPath == null)
  58. {
  59. string m = basePath.TrimEnd('\\') + '\\' + "ffmpeg.exe";
  60. if (File.Exists(Path.GetFullPath(m))) ffmpegPath = Path.GetFullPath(m);
  61. }
  62. if (ffprobePath == null)
  63. {
  64. string p = basePath.TrimEnd('\\') + '\\' + "ffprobe.exe";
  65. if (File.Exists(Path.GetFullPath(p))) ffprobePath = Path.GetFullPath(p);
  66. }
  67. }
  68. if (ffmpegPath == null) throw new FileNotFoundException("Failed to locate ffmpeg.exe in the bin folder");
  69. if (ffprobePath == null) throw new FileNotFoundException("Failed to locate ffprobe.exe in the bin folder");
  70. }
  71. }
  72. public int MaxConcurrentExecutions { get; set; }
  73. public int MaxConcurrentWaitingThreads { get; set; }
  74. protected string cairPath = null;
  75. protected object cairLock = new object();
  76. /// <summary>
  77. /// Number of executing CAIR.exe processes
  78. /// </summary>
  79. private int _concurrentExecutions = 0;
  80. /// <summary>
  81. /// Number of threads waiting for a CAIR.exe process.
  82. /// </summary>
  83. private int _concurrentWaitingThreads = 0;
  84. /// <summary>
  85. /// Used for efficient thread waiting
  86. /// </summary>
  87. private AutoResetEvent turnstile = new AutoResetEvent(true);
  88. public bool Execute(FFmpegJob job)
  89. {
  90. //If we have too many threads waiting to run CAIR, just kill the request.
  91. if (MaxConcurrentWaitingThreads > 0 &&
  92. _concurrentWaitingThreads > MaxConcurrentWaitingThreads)
  93. throw new Exception("FFmpeg failed - too many threads waiting. Try again later.");
  94. //If there are any threads waiting in line, or if the permitted number of CAIR.exe instances has been reached, get in line
  95. if (_concurrentWaitingThreads > 0 || (MaxConcurrentExecutions > 0 &&
  96. _concurrentExecutions > MaxConcurrentExecutions))
  97. {
  98. try
  99. {
  100. Interlocked.Increment(ref _concurrentWaitingThreads);
  101. //Wait for a free slot
  102. while (MaxConcurrentExecutions > 0 &&
  103. _concurrentExecutions > MaxConcurrentExecutions)
  104. {
  105. turnstile.WaitOne(1000);
  106. }
  107. }
  108. finally
  109. {
  110. Interlocked.Decrement(ref _concurrentWaitingThreads);
  111. }
  112. }
  113. //Ok, there should be a free slot now.
  114. try
  115. {
  116. //Register, we have our own process slot now.
  117. Interlocked.Increment(ref _concurrentExecutions);
  118. return InnerExecute(job);
  119. }
  120. finally
  121. {
  122. Interlocked.Decrement(ref _concurrentExecutions);
  123. turnstile.Set();
  124. }
  125. }
  126. private XElement GetVideoInfo(FFmpegJob job)
  127. {
  128. //*ffprobe.exe -loglevel error -show_format -show_streams inputFile.extension -print_format json*/
  129. string result = RunExecutable(GetFFprobePath(), " -loglevel error -show_format -print_format xml -i \"" + job.SourcePath + "\"", job.Timeout /2);
  130. return XElement.Parse(result);
  131. }
  132. private bool InnerExecute(FFmpegJob job)
  133. {
  134. if (job.Seconds == null){
  135. var xml = GetVideoInfo(job);
  136. var duration = (double)xml.Descendants("format").FirstOrDefault().Attributes("duration").FirstOrDefault();
  137. job.Seconds = duration * job.Percent / 100;
  138. }
  139. bool failedTest = false;
  140. var tries = 0;
  141. do
  142. {
  143. if (tries > 4) throw new Exception("Failed to locate a non-blank frame");
  144. var path = Path.GetTempFileName();
  145. byte[] result;
  146. try{
  147. string message = RunExecutable(GetFFmpegPath(), " -ss " + job.Seconds.ToString() + " -y -i \"" + job.SourcePath + "\" -an -vframes 1 -r 1 -f image2 -pix_fmt rgb24 \"" + path + "\"", job.Timeout);
  148. if (message.Contains("Output file is empty, nothing was encoded"))
  149. {
  150. throw new Exception("You are outside the bounds of the video");
  151. }
  152. result = File.ReadAllBytes(path);
  153. job.Result = new MemoryStream(result);
  154. }finally{
  155. File.Delete(path);
  156. }
  157. failedTest = (job.SkipBlankFrames == true && IsBlank(result,10));
  158. if (failedTest) job.Seconds += job.IncrementWhenBlank ?? 5;
  159. tries++;
  160. } while (failedTest);
  161. return true;
  162. }
  163. /// <summary>
  164. /// Returns true if the average energy of the image is below the given threshold
  165. /// </summary>
  166. /// <param name="image"></param>
  167. /// <param name="threshold"></param>
  168. /// <returns></returns>
  169. private bool IsBlank(byte[] image, byte threshold)
  170. {
  171. var ms = new MemoryStream(image);
  172. using (var b = new Bitmap(ms))
  173. {
  174. using (var gray = Grayscale.CommonAlgorithms.BT709.Apply(b))
  175. {
  176. new SobelEdgeDetector().ApplyInPlace(gray);
  177. var p = new SimplePosterization(SimplePosterization.PosterizationFillingType.Average);
  178. p.PosterizationInterval = 128;
  179. p.ApplyInPlace(gray);
  180. return gray.GetPixel(0, 0).R < threshold;
  181. }
  182. }
  183. }
  184. public Stream GetFrameStream(Config c,string virtualPath, System.Collections.Specialized.NameValueCollection queryString)
  185. {
  186. var job = new FFmpegJob(queryString);
  187. bool bufferToTemp = !File.Exists(HostingEnvironment.MapPath(virtualPath));
  188. job.SourcePath = !bufferToTemp ? HostingEnvironment.MapPath(virtualPath) : Path.GetTempFileName();
  189. try
  190. {
  191. if (bufferToTemp)
  192. {
  193. using (Stream input = c.Pipeline.GetFile(virtualPath, new System.Collections.Specialized.NameValueCollection()).Open())
  194. using (Stream output = File.Create(job.SourcePath))
  195. {
  196. StreamExtensions.CopyToStream(input, output);
  197. }
  198. }
  199. this.Execute(job);
  200. return job.Result;
  201. }
  202. finally
  203. {
  204. if (bufferToTemp) File.Delete(job.SourcePath);
  205. }
  206. }
  207. private string RunExecutable(string filename, string arguments, int timeout)
  208. {
  209. ProcessStartInfo info = new ProcessStartInfo(filename, arguments);
  210. info.UseShellExecute = false;
  211. info.RedirectStandardError = true;
  212. info.RedirectStandardOutput = true;
  213. info.CreateNoWindow = true;
  214. using (Process p = Process.Start(info))
  215. {
  216. bool result = p.WaitForExit(timeout);
  217. if (!result)
  218. {
  219. p.Kill(); //Kill the process if it times out.
  220. throw new Exception("FFmpeg failed due to timeout.");
  221. }
  222. string messages = p.StandardError.ReadToEnd() + p.StandardOutput.ReadToEnd();
  223. if (p.ExitCode != 0)
  224. throw new Exception("FFmpeg failed: " + messages);
  225. return messages;
  226. }
  227. }
  228. }
  229. }