FFMpegLoader.cs 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. using MediaBrowser.Common.Configuration;
  2. using MediaBrowser.Common.Net;
  3. using MediaBrowser.Model.IO;
  4. using MediaBrowser.Model.Logging;
  5. using Mono.Unix.Native;
  6. using System;
  7. using System.Collections.Generic;
  8. using System.IO;
  9. using System.Linq;
  10. using System.Threading;
  11. using System.Threading.Tasks;
  12. using MediaBrowser.Common.IO;
  13. using MediaBrowser.Controller.IO;
  14. using MediaBrowser.Model.IO;
  15. namespace MediaBrowser.Server.Startup.Common.FFMpeg
  16. {
  17. public class FFMpegLoader
  18. {
  19. private readonly IHttpClient _httpClient;
  20. private readonly IApplicationPaths _appPaths;
  21. private readonly ILogger _logger;
  22. private readonly IZipClient _zipClient;
  23. private readonly IFileSystem _fileSystem;
  24. private readonly NativeEnvironment _environment;
  25. private readonly FFMpegInstallInfo _ffmpegInstallInfo;
  26. public FFMpegLoader(ILogger logger, IApplicationPaths appPaths, IHttpClient httpClient, IZipClient zipClient, IFileSystem fileSystem, NativeEnvironment environment, FFMpegInstallInfo ffmpegInstallInfo)
  27. {
  28. _logger = logger;
  29. _appPaths = appPaths;
  30. _httpClient = httpClient;
  31. _zipClient = zipClient;
  32. _fileSystem = fileSystem;
  33. _environment = environment;
  34. _ffmpegInstallInfo = ffmpegInstallInfo;
  35. }
  36. public async Task<FFMpegInfo> GetFFMpegInfo(NativeEnvironment environment, StartupOptions options, IProgress<double> progress)
  37. {
  38. var customffMpegPath = options.GetOption("-ffmpeg");
  39. var customffProbePath = options.GetOption("-ffprobe");
  40. if (!string.IsNullOrWhiteSpace(customffMpegPath) && !string.IsNullOrWhiteSpace(customffProbePath))
  41. {
  42. return new FFMpegInfo
  43. {
  44. ProbePath = customffProbePath,
  45. EncoderPath = customffMpegPath,
  46. Version = "external"
  47. };
  48. }
  49. var downloadInfo = _ffmpegInstallInfo;
  50. var version = downloadInfo.Version;
  51. if (string.Equals(version, "path", StringComparison.OrdinalIgnoreCase))
  52. {
  53. return new FFMpegInfo
  54. {
  55. ProbePath = downloadInfo.FFProbeFilename,
  56. EncoderPath = downloadInfo.FFMpegFilename,
  57. Version = version
  58. };
  59. }
  60. if (string.Equals(version, "0", StringComparison.OrdinalIgnoreCase))
  61. {
  62. return new FFMpegInfo();
  63. }
  64. var rootEncoderPath = Path.Combine(_appPaths.ProgramDataPath, "ffmpeg");
  65. var versionedDirectoryPath = Path.Combine(rootEncoderPath, version);
  66. var info = new FFMpegInfo
  67. {
  68. ProbePath = Path.Combine(versionedDirectoryPath, downloadInfo.FFProbeFilename),
  69. EncoderPath = Path.Combine(versionedDirectoryPath, downloadInfo.FFMpegFilename),
  70. Version = version
  71. };
  72. _fileSystem.CreateDirectory(versionedDirectoryPath);
  73. var excludeFromDeletions = new List<string> { versionedDirectoryPath };
  74. if (!_fileSystem.FileExists(info.ProbePath) || !_fileSystem.FileExists(info.EncoderPath))
  75. {
  76. // ffmpeg not present. See if there's an older version we can start with
  77. var existingVersion = GetExistingVersion(info, rootEncoderPath);
  78. // No older version. Need to download and block until complete
  79. if (existingVersion == null)
  80. {
  81. var success = await DownloadFFMpeg(downloadInfo, versionedDirectoryPath, progress).ConfigureAwait(false);
  82. if (!success)
  83. {
  84. return new FFMpegInfo();
  85. }
  86. }
  87. else
  88. {
  89. info = existingVersion;
  90. versionedDirectoryPath = Path.GetDirectoryName(info.EncoderPath);
  91. excludeFromDeletions.Add(versionedDirectoryPath);
  92. }
  93. }
  94. // Allow just one of these to be overridden, if desired.
  95. if (!string.IsNullOrWhiteSpace(customffMpegPath))
  96. {
  97. info.EncoderPath = customffMpegPath;
  98. }
  99. if (!string.IsNullOrWhiteSpace(customffProbePath))
  100. {
  101. info.EncoderPath = customffProbePath;
  102. }
  103. return info;
  104. }
  105. private FFMpegInfo GetExistingVersion(FFMpegInfo info, string rootEncoderPath)
  106. {
  107. var encoderFilename = Path.GetFileName(info.EncoderPath);
  108. var probeFilename = Path.GetFileName(info.ProbePath);
  109. foreach (var directory in Directory.EnumerateDirectories(rootEncoderPath, "*", SearchOption.TopDirectoryOnly)
  110. .ToList())
  111. {
  112. var allFiles = Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories).ToList();
  113. var encoder = allFiles.FirstOrDefault(i => string.Equals(Path.GetFileName(i), encoderFilename, StringComparison.OrdinalIgnoreCase));
  114. var probe = allFiles.FirstOrDefault(i => string.Equals(Path.GetFileName(i), probeFilename, StringComparison.OrdinalIgnoreCase));
  115. if (!string.IsNullOrWhiteSpace(encoder) &&
  116. !string.IsNullOrWhiteSpace(probe))
  117. {
  118. return new FFMpegInfo
  119. {
  120. EncoderPath = encoder,
  121. ProbePath = probe,
  122. Version = Path.GetFileName(Path.GetDirectoryName(probe))
  123. };
  124. }
  125. }
  126. return null;
  127. }
  128. private async Task<bool> DownloadFFMpeg(FFMpegInstallInfo downloadinfo, string directory, IProgress<double> progress)
  129. {
  130. foreach (var url in downloadinfo.DownloadUrls)
  131. {
  132. progress.Report(0);
  133. try
  134. {
  135. var tempFile = await _httpClient.GetTempFile(new HttpRequestOptions
  136. {
  137. Url = url,
  138. CancellationToken = CancellationToken.None,
  139. Progress = progress
  140. }).ConfigureAwait(false);
  141. ExtractFFMpeg(downloadinfo, tempFile, directory);
  142. return true;
  143. }
  144. catch (Exception ex)
  145. {
  146. _logger.ErrorException("Error downloading {0}", ex, url);
  147. }
  148. }
  149. return false;
  150. }
  151. private void ExtractFFMpeg(FFMpegInstallInfo downloadinfo, string tempFile, string targetFolder)
  152. {
  153. _logger.Info("Extracting ffmpeg from {0}", tempFile);
  154. var tempFolder = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString());
  155. _fileSystem.CreateDirectory(tempFolder);
  156. try
  157. {
  158. ExtractArchive(downloadinfo, tempFile, tempFolder);
  159. var files = Directory.EnumerateFiles(tempFolder, "*", SearchOption.AllDirectories)
  160. .ToList();
  161. foreach (var file in files.Where(i =>
  162. {
  163. var filename = Path.GetFileName(i);
  164. return
  165. string.Equals(filename, downloadinfo.FFProbeFilename, StringComparison.OrdinalIgnoreCase) ||
  166. string.Equals(filename, downloadinfo.FFMpegFilename, StringComparison.OrdinalIgnoreCase);
  167. }))
  168. {
  169. var targetFile = Path.Combine(targetFolder, Path.GetFileName(file));
  170. _fileSystem.CopyFile(file, targetFile, true);
  171. SetFilePermissions(targetFile);
  172. }
  173. }
  174. finally
  175. {
  176. DeleteFile(tempFile);
  177. }
  178. }
  179. private void SetFilePermissions(string path)
  180. {
  181. // Linux: File permission to 666, and user's execute bit
  182. if (_environment.OperatingSystem == OperatingSystem.Bsd || _environment.OperatingSystem == OperatingSystem.Linux || _environment.OperatingSystem == OperatingSystem.Osx)
  183. {
  184. _logger.Info("Syscall.chmod {0} FilePermissions.DEFFILEMODE | FilePermissions.S_IRWXU | FilePermissions.S_IXGRP | FilePermissions.S_IXOTH", path);
  185. Syscall.chmod(path, FilePermissions.DEFFILEMODE | FilePermissions.S_IRWXU | FilePermissions.S_IXGRP | FilePermissions.S_IXOTH);
  186. }
  187. }
  188. private void ExtractArchive(FFMpegInstallInfo downloadinfo, string archivePath, string targetPath)
  189. {
  190. _logger.Info("Extracting {0} to {1}", archivePath, targetPath);
  191. if (string.Equals(downloadinfo.ArchiveType, "7z", StringComparison.OrdinalIgnoreCase))
  192. {
  193. _zipClient.ExtractAllFrom7z(archivePath, targetPath, true);
  194. }
  195. else if (string.Equals(downloadinfo.ArchiveType, "gz", StringComparison.OrdinalIgnoreCase))
  196. {
  197. _zipClient.ExtractAllFromTar(archivePath, targetPath, true);
  198. }
  199. }
  200. private void DeleteFile(string path)
  201. {
  202. try
  203. {
  204. _fileSystem.DeleteFile(path);
  205. }
  206. catch (IOException ex)
  207. {
  208. _logger.ErrorException("Error deleting temp file {0}", ex, path);
  209. }
  210. }
  211. }
  212. }