diff options
| author | Luke Pulverenti <luke.pulverenti@gmail.com> | 2014-11-09 13:24:57 -0500 |
|---|---|---|
| committer | Luke Pulverenti <luke.pulverenti@gmail.com> | 2014-11-09 13:24:57 -0500 |
| commit | 1a80362a0f04c3cc571456af64f9de19c0c30d2a (patch) | |
| tree | 31916eea422d8ac861aa36a47cae832eee168ad8 /MediaBrowser.Server.Startup.Common/FFMpeg | |
| parent | 40897bac1494791e1ec6abcfe85cda27d4664a32 (diff) | |
created common startup project for mono & windows
Diffstat (limited to 'MediaBrowser.Server.Startup.Common/FFMpeg')
3 files changed, 589 insertions, 0 deletions
diff --git a/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegDownloadInfo.cs b/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegDownloadInfo.cs new file mode 100644 index 0000000000..1777e1bd26 --- /dev/null +++ b/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegDownloadInfo.cs @@ -0,0 +1,148 @@ + +namespace MediaBrowser.Server.Startup.Common.FFMpeg +{ + public class FFMpegDownloadInfo + { + public string Version { get; set; } + public string FFMpegFilename { get; set; } + public string FFProbeFilename { get; set; } + public string ArchiveType { get; set; } + public string[] DownloadUrls { get; set; } + + public FFMpegDownloadInfo() + { + DownloadUrls = new string[] { }; + Version = "Path"; + FFMpegFilename = "ffmpeg"; + FFProbeFilename = "ffprobe"; + } + + public static FFMpegDownloadInfo GetInfo(NativeEnvironment environment) + { + var info = new FFMpegDownloadInfo(); + + // Windows builds: http://ffmpeg.zeranoe.com/builds/ + // Linux builds: http://ffmpeg.gusari.org/static/ + // OS X builds: http://ffmpegmac.net/ + // OS X x64: http://www.evermeet.cx/ffmpeg/ + + switch (environment.OperatingSystem) + { + case OperatingSystem.Bsd: + break; + case OperatingSystem.Linux: + + info.ArchiveType = "gz"; + + switch (environment.SystemArchitecture) + { + case Architecture.X86_X64: + info.Version = "20140716"; + break; + case Architecture.X86: + info.Version = "20140923"; + break; + } + break; + case OperatingSystem.Osx: + + info.ArchiveType = "7z"; + + switch (environment.SystemArchitecture) + { + case Architecture.X86_X64: + info.Version = "20140923"; + break; + case Architecture.X86: + info.Version = "20140716"; + break; + } + break; + + case OperatingSystem.Windows: + + info.FFMpegFilename = "ffmpeg.exe"; + info.FFProbeFilename = "ffprobe.exe"; + info.Version = "20141005"; + info.ArchiveType = "7z"; + + switch (environment.SystemArchitecture) + { + case Architecture.X86_X64: + break; + case Architecture.X86: + break; + } + break; + } + + info.DownloadUrls = GetDownloadUrls(environment); + + return info; + } + + private static string[] GetDownloadUrls(NativeEnvironment environment) + { + switch (environment.OperatingSystem) + { + case OperatingSystem.Windows: + + switch (environment.SystemArchitecture) + { + case Architecture.X86_X64: + return new[] + { + "http://ffmpeg.zeranoe.com/builds/win64/static/ffmpeg-20141005-git-e079d43-win64-static.7z", + "https://github.com/MediaBrowser/MediaBrowser.Resources/raw/master/ffmpeg/windows/ffmpeg-20141005-git-e079d43-win64-static.7z" + }; + case Architecture.X86: + return new[] + { + "http://ffmpeg.zeranoe.com/builds/win32/static/ffmpeg-20141005-git-e079d43-win32-static.7z", + "https://github.com/MediaBrowser/MediaBrowser.Resources/raw/master/ffmpeg/windows/ffmpeg-20141005-git-e079d43-win32-static.7z" + }; + } + break; + + case OperatingSystem.Osx: + + switch (environment.SystemArchitecture) + { + case Architecture.X86_X64: + return new[] + { + "https://github.com/MediaBrowser/MediaBrowser.Resources/raw/master/ffmpeg/osx/ffmpeg-x64-2.4.1.7z" + }; + case Architecture.X86: + return new[] + { + "https://github.com/MediaBrowser/MediaBrowser.Resources/raw/master/ffmpeg/osx/ffmpeg-x86-2.4.2.7z" + }; + } + break; + + case OperatingSystem.Linux: + + switch (environment.SystemArchitecture) + { + case Architecture.X86_X64: + return new[] + { + "http://ffmpeg.gusari.org/static/64bit/ffmpeg.static.64bit.latest.tar.gz", + "https://github.com/MediaBrowser/MediaBrowser.Resources/raw/master/ffmpeg/linux/ffmpeg.static.64bit.2014-07-16.tar.gz" + }; + case Architecture.X86: + return new[] + { + "http://ffmpeg.gusari.org/static/32bit/ffmpeg.static.32bit.latest.tar.gz", + "https://github.com/MediaBrowser/MediaBrowser.Resources/raw/master/ffmpeg/linux/ffmpeg.static.32bit.2014-07-16.tar.gz" + }; + } + break; + } + + // No version available + return new string[] { }; + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegDownloader.cs b/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegDownloader.cs new file mode 100644 index 0000000000..2e58e75d87 --- /dev/null +++ b/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegDownloader.cs @@ -0,0 +1,417 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using Mono.Unix.Native; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Startup.Common.FFMpeg +{ + public class FFMpegDownloader + { + private readonly IHttpClient _httpClient; + private readonly IApplicationPaths _appPaths; + private readonly ILogger _logger; + private readonly IZipClient _zipClient; + private readonly IFileSystem _fileSystem; + + private readonly string[] _fontUrls = + { + "https://github.com/MediaBrowser/MediaBrowser.Resources/raw/master/ffmpeg/ARIALUNI.7z" + }; + + public FFMpegDownloader(ILogger logger, IApplicationPaths appPaths, IHttpClient httpClient, IZipClient zipClient, IFileSystem fileSystem) + { + _logger = logger; + _appPaths = appPaths; + _httpClient = httpClient; + _zipClient = zipClient; + _fileSystem = fileSystem; + } + + public async Task<FFMpegInfo> GetFFMpegInfo(NativeEnvironment environment, StartupOptions options, IProgress<double> progress) + { + var customffMpegPath = options.GetOption("-ffmpeg"); + var customffProbePath = options.GetOption("-ffprobe"); + + if (!string.IsNullOrWhiteSpace(customffMpegPath) && !string.IsNullOrWhiteSpace(customffProbePath)) + { + return new FFMpegInfo + { + ProbePath = customffProbePath, + EncoderPath = customffMpegPath, + Version = "custom" + }; + } + + var downloadInfo = FFMpegDownloadInfo.GetInfo(environment); + + var version = downloadInfo.Version; + + if (string.Equals(version, "path", StringComparison.OrdinalIgnoreCase)) + { + return new FFMpegInfo + { + ProbePath = downloadInfo.FFProbeFilename, + EncoderPath = downloadInfo.FFMpegFilename, + Version = version + }; + } + + var rootEncoderPath = Path.Combine(_appPaths.ProgramDataPath, "ffmpeg"); + var versionedDirectoryPath = Path.Combine(rootEncoderPath, version); + + var info = new FFMpegInfo + { + ProbePath = Path.Combine(versionedDirectoryPath, downloadInfo.FFProbeFilename), + EncoderPath = Path.Combine(versionedDirectoryPath, downloadInfo.FFMpegFilename), + Version = version + }; + + Directory.CreateDirectory(versionedDirectoryPath); + + var excludeFromDeletions = new List<string> { versionedDirectoryPath }; + + if (!File.Exists(info.ProbePath) || !File.Exists(info.EncoderPath)) + { + // ffmpeg not present. See if there's an older version we can start with + var existingVersion = GetExistingVersion(info, rootEncoderPath); + + // No older version. Need to download and block until complete + if (existingVersion == null) + { + await DownloadFFMpeg(downloadInfo, versionedDirectoryPath, progress).ConfigureAwait(false); + } + else + { + // Older version found. + // Start with that. Download new version in the background. + var newPath = versionedDirectoryPath; + Task.Run(() => DownloadFFMpegInBackground(downloadInfo, newPath)); + + info = existingVersion; + versionedDirectoryPath = Path.GetDirectoryName(info.EncoderPath); + + excludeFromDeletions.Add(versionedDirectoryPath); + } + } + + await DownloadFonts(versionedDirectoryPath).ConfigureAwait(false); + + DeleteOlderFolders(Path.GetDirectoryName(versionedDirectoryPath), excludeFromDeletions); + + return info; + } + + private void DeleteOlderFolders(string path, IEnumerable<string> excludeFolders ) + { + var folders = Directory.GetDirectories(path) + .Where(i => !excludeFolders.Contains(i, StringComparer.OrdinalIgnoreCase)) + .ToList(); + + foreach (var folder in folders) + { + DeleteFolder(folder); + } + } + + private void DeleteFolder(string path) + { + try + { + Directory.Delete(path, true); + } + catch (Exception ex) + { + _logger.ErrorException("Error deleting {0}", ex, path); + } + } + + private FFMpegInfo GetExistingVersion(FFMpegInfo info, string rootEncoderPath) + { + var encoderFilename = Path.GetFileName(info.EncoderPath); + var probeFilename = Path.GetFileName(info.ProbePath); + + foreach (var directory in Directory.EnumerateDirectories(rootEncoderPath, "*", SearchOption.TopDirectoryOnly) + .ToList()) + { + var allFiles = Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories).ToList(); + + var encoder = allFiles.FirstOrDefault(i => string.Equals(Path.GetFileName(i), encoderFilename, StringComparison.OrdinalIgnoreCase)); + var probe = allFiles.FirstOrDefault(i => string.Equals(Path.GetFileName(i), probeFilename, StringComparison.OrdinalIgnoreCase)); + + if (!string.IsNullOrWhiteSpace(encoder) && + !string.IsNullOrWhiteSpace(probe)) + { + return new FFMpegInfo + { + EncoderPath = encoder, + ProbePath = probe, + Version = Path.GetFileName(Path.GetDirectoryName(probe)) + }; + } + } + + return null; + } + + private async void DownloadFFMpegInBackground(FFMpegDownloadInfo downloadinfo, string directory) + { + try + { + await DownloadFFMpeg(downloadinfo, directory, new Progress<double>()).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error downloading ffmpeg", ex); + } + } + + private async Task DownloadFFMpeg(FFMpegDownloadInfo downloadinfo, string directory, IProgress<double> progress) + { + foreach (var url in downloadinfo.DownloadUrls) + { + progress.Report(0); + + try + { + var tempFile = await _httpClient.GetTempFile(new HttpRequestOptions + { + Url = url, + CancellationToken = CancellationToken.None, + Progress = progress + + }).ConfigureAwait(false); + + ExtractFFMpeg(downloadinfo, tempFile, directory); + return; + } + catch (Exception ex) + { + _logger.ErrorException("Error downloading {0}", ex, url); + } + } + + throw new ApplicationException("Unable to download required components. Please try again later."); + } + + private void ExtractFFMpeg(FFMpegDownloadInfo downloadinfo, string tempFile, string targetFolder) + { + _logger.Info("Extracting ffmpeg from {0}", tempFile); + + var tempFolder = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString()); + + Directory.CreateDirectory(tempFolder); + + try + { + ExtractArchive(downloadinfo, tempFile, tempFolder); + + var files = Directory.EnumerateFiles(tempFolder, "*", SearchOption.AllDirectories).ToList(); + + foreach (var file in files.Where(i => + { + var filename = Path.GetFileName(i); + + return + string.Equals(filename, downloadinfo.FFProbeFilename, StringComparison.OrdinalIgnoreCase) || + string.Equals(filename, downloadinfo.FFMpegFilename, StringComparison.OrdinalIgnoreCase); + })) + { + File.Copy(file, Path.Combine(targetFolder, Path.GetFileName(file)), true); + + SetFilePermissions(targetFolder, file); + } + } + finally + { + DeleteFile(tempFile); + } + } + + private void SetFilePermissions(string targetFolder, string file) + { + // Linux: File permission to 666, and user's execute bit + if (Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX) + { + Syscall.chmod(Path.Combine(targetFolder, Path.GetFileName(file)), FilePermissions.DEFFILEMODE | FilePermissions.S_IXUSR); + } + } + + private void ExtractArchive(FFMpegDownloadInfo downloadinfo, string archivePath, string targetPath) + { + _logger.Info("Extracting {0} to {1}", archivePath, targetPath); + + if (string.Equals(downloadinfo.ArchiveType, "7z", StringComparison.OrdinalIgnoreCase)) + { + _zipClient.ExtractAllFrom7z(archivePath, targetPath, true); + } + else if (string.Equals(downloadinfo.ArchiveType, "gz", StringComparison.OrdinalIgnoreCase)) + { + _zipClient.ExtractAllFromTar(archivePath, targetPath, true); + } + } + private void Extract7zArchive(string archivePath, string targetPath) + { + _logger.Info("Extracting {0} to {1}", archivePath, targetPath); + + _zipClient.ExtractAllFrom7z(archivePath, targetPath, true); + } + + private void DeleteFile(string path) + { + try + { + File.Delete(path); + } + catch (IOException ex) + { + _logger.ErrorException("Error deleting temp file {0}", ex, path); + } + } + + /// <summary> + /// Extracts the fonts. + /// </summary> + /// <param name="targetPath">The target path.</param> + /// <returns>Task.</returns> + private async Task DownloadFonts(string targetPath) + { + try + { + var fontsDirectory = Path.Combine(targetPath, "fonts"); + + Directory.CreateDirectory(fontsDirectory); + + const string fontFilename = "ARIALUNI.TTF"; + + var fontFile = Path.Combine(fontsDirectory, fontFilename); + + if (File.Exists(fontFile)) + { + await WriteFontConfigFile(fontsDirectory).ConfigureAwait(false); + } + else + { + // Kick this off, but no need to wait on it + Task.Run(async () => + { + await DownloadFontFile(fontsDirectory, fontFilename, new Progress<double>()).ConfigureAwait(false); + + await WriteFontConfigFile(fontsDirectory).ConfigureAwait(false); + }); + } + } + catch (HttpException ex) + { + // Don't let the server crash because of this + _logger.ErrorException("Error downloading ffmpeg font files", ex); + } + catch (Exception ex) + { + // Don't let the server crash because of this + _logger.ErrorException("Error writing ffmpeg font files", ex); + } + } + + /// <summary> + /// Downloads the font file. + /// </summary> + /// <param name="fontsDirectory">The fonts directory.</param> + /// <param name="fontFilename">The font filename.</param> + /// <returns>Task.</returns> + private async Task DownloadFontFile(string fontsDirectory, string fontFilename, IProgress<double> progress) + { + var existingFile = Directory + .EnumerateFiles(_appPaths.ProgramDataPath, fontFilename, SearchOption.AllDirectories) + .FirstOrDefault(); + + if (existingFile != null) + { + try + { + File.Copy(existingFile, Path.Combine(fontsDirectory, fontFilename), true); + return; + } + catch (IOException ex) + { + // Log this, but don't let it fail the operation + _logger.ErrorException("Error copying file", ex); + } + } + + string tempFile = null; + + foreach (var url in _fontUrls) + { + progress.Report(0); + + try + { + tempFile = await _httpClient.GetTempFile(new HttpRequestOptions + { + Url = url, + Progress = progress + + }).ConfigureAwait(false); + + break; + } + catch (Exception ex) + { + // The core can function without the font file, so handle this + _logger.ErrorException("Failed to download ffmpeg font file from {0}", ex, url); + } + } + + if (string.IsNullOrEmpty(tempFile)) + { + return; + } + + Extract7zArchive(tempFile, fontsDirectory); + + try + { + File.Delete(tempFile); + } + catch (IOException ex) + { + // Log this, but don't let it fail the operation + _logger.ErrorException("Error deleting temp file {0}", ex, tempFile); + } + } + + /// <summary> + /// Writes the font config file. + /// </summary> + /// <param name="fontsDirectory">The fonts directory.</param> + /// <returns>Task.</returns> + private async Task WriteFontConfigFile(string fontsDirectory) + { + const string fontConfigFilename = "fonts.conf"; + var fontConfigFile = Path.Combine(fontsDirectory, fontConfigFilename); + + if (!File.Exists(fontConfigFile)) + { + var contents = string.Format("<?xml version=\"1.0\"?><fontconfig><dir>{0}</dir><alias><family>Arial</family><prefer>Arial Unicode MS</prefer></alias></fontconfig>", fontsDirectory); + + var bytes = Encoding.UTF8.GetBytes(contents); + + using (var fileStream = _fileSystem.GetFileStream(fontConfigFile, FileMode.Create, FileAccess.Write, + FileShare.Read, true)) + { + await fileStream.WriteAsync(bytes, 0, bytes.Length); + } + } + } + } +} diff --git a/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegInfo.cs b/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegInfo.cs new file mode 100644 index 0000000000..d33b12a9a7 --- /dev/null +++ b/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegInfo.cs @@ -0,0 +1,24 @@ +namespace MediaBrowser.Server.Startup.Common.FFMpeg +{ + /// <summary> + /// Class FFMpegInfo + /// </summary> + public class FFMpegInfo + { + /// <summary> + /// Gets or sets the path. + /// </summary> + /// <value>The path.</value> + public string EncoderPath { get; set; } + /// <summary> + /// Gets or sets the probe path. + /// </summary> + /// <value>The probe path.</value> + public string ProbePath { get; set; } + /// <summary> + /// Gets or sets the version. + /// </summary> + /// <value>The version.</value> + public string Version { get; set; } + } +}
\ No newline at end of file |
