aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Server.Implementations/MediaEncoder
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Server.Implementations/MediaEncoder')
-rw-r--r--MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs930
-rw-r--r--MediaBrowser.Server.Implementations/MediaEncoder/ffmpeg20130405.zip.REMOVED.git-id1
-rw-r--r--MediaBrowser.Server.Implementations/MediaEncoder/fonts/fonts.conf9
-rw-r--r--MediaBrowser.Server.Implementations/MediaEncoder/readme.txt5
4 files changed, 945 insertions, 0 deletions
diff --git a/MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs b/MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs
new file mode 100644
index 000000000..fce88e3d8
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs
@@ -0,0 +1,930 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.MediaInfo;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.MediaEncoder
+{
+ /// <summary>
+ /// Class MediaEncoder
+ /// </summary>
+ public class MediaEncoder : IMediaEncoder, IDisposable
+ {
+ /// <summary>
+ /// Gets or sets the zip client.
+ /// </summary>
+ /// <value>The zip client.</value>
+ private readonly IZipClient _zipClient;
+
+ /// <summary>
+ /// The _logger
+ /// </summary>
+ private readonly ILogger _logger;
+
+ /// <summary>
+ /// The _app paths
+ /// </summary>
+ private readonly IApplicationPaths _appPaths;
+
+ /// <summary>
+ /// Gets the json serializer.
+ /// </summary>
+ /// <value>The json serializer.</value>
+ private readonly IJsonSerializer _jsonSerializer;
+
+ /// <summary>
+ /// The video image resource pool
+ /// </summary>
+ private readonly SemaphoreSlim _videoImageResourcePool = new SemaphoreSlim(2, 2);
+
+ /// <summary>
+ /// The audio image resource pool
+ /// </summary>
+ private readonly SemaphoreSlim _audioImageResourcePool = new SemaphoreSlim(3, 3);
+
+ /// <summary>
+ /// The _subtitle extraction resource pool
+ /// </summary>
+ private readonly SemaphoreSlim _subtitleExtractionResourcePool = new SemaphoreSlim(2, 2);
+
+ /// <summary>
+ /// The FF probe resource pool
+ /// </summary>
+ private readonly SemaphoreSlim _ffProbeResourcePool = new SemaphoreSlim(3, 3);
+
+ /// <summary>
+ /// Gets or sets the versioned directory path.
+ /// </summary>
+ /// <value>The versioned directory path.</value>
+ private string VersionedDirectoryPath { get; set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MediaEncoder" /> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="zipClient">The zip client.</param>
+ /// <param name="appPaths">The app paths.</param>
+ /// <param name="jsonSerializer">The json serializer.</param>
+ public MediaEncoder(ILogger logger, IZipClient zipClient, IApplicationPaths appPaths, IJsonSerializer jsonSerializer)
+ {
+ _logger = logger;
+ _zipClient = zipClient;
+ _appPaths = appPaths;
+ _jsonSerializer = jsonSerializer;
+
+ // Not crazy about this but it's the only way to suppress ffmpeg crash dialog boxes
+ SetErrorMode(ErrorModes.SEM_FAILCRITICALERRORS | ErrorModes.SEM_NOALIGNMENTFAULTEXCEPT | ErrorModes.SEM_NOGPFAULTERRORBOX | ErrorModes.SEM_NOOPENFILEERRORBOX);
+
+ Task.Run(() => VersionedDirectoryPath = GetVersionedDirectoryPath());
+ }
+
+ /// <summary>
+ /// The _media tools path
+ /// </summary>
+ private string _mediaToolsPath;
+ /// <summary>
+ /// Gets the folder path to tools
+ /// </summary>
+ /// <value>The media tools path.</value>
+ private string MediaToolsPath
+ {
+ get
+ {
+ if (_mediaToolsPath == null)
+ {
+ _mediaToolsPath = Path.Combine(_appPaths.ProgramDataPath, "ffmpeg");
+
+ if (!Directory.Exists(_mediaToolsPath))
+ {
+ Directory.CreateDirectory(_mediaToolsPath);
+ }
+ }
+
+ return _mediaToolsPath;
+ }
+ }
+
+ /// <summary>
+ /// Gets the encoder path.
+ /// </summary>
+ /// <value>The encoder path.</value>
+ public string EncoderPath
+ {
+ get { return FFMpegPath; }
+ }
+
+ /// <summary>
+ /// The _ FF MPEG path
+ /// </summary>
+ private string _FFMpegPath;
+ /// <summary>
+ /// Gets the path to ffmpeg.exe
+ /// </summary>
+ /// <value>The FF MPEG path.</value>
+ public string FFMpegPath
+ {
+ get
+ {
+ return _FFMpegPath ?? (_FFMpegPath = Path.Combine(VersionedDirectoryPath, "ffmpeg.exe"));
+ }
+ }
+
+ /// <summary>
+ /// The _ FF probe path
+ /// </summary>
+ private string _FFProbePath;
+ /// <summary>
+ /// Gets the path to ffprobe.exe
+ /// </summary>
+ /// <value>The FF probe path.</value>
+ private string FFProbePath
+ {
+ get
+ {
+ return _FFProbePath ?? (_FFProbePath = Path.Combine(VersionedDirectoryPath, "ffprobe.exe"));
+ }
+ }
+
+ /// <summary>
+ /// Gets the version.
+ /// </summary>
+ /// <value>The version.</value>
+ public string Version
+ {
+ get { return Path.GetFileNameWithoutExtension(VersionedDirectoryPath); }
+ }
+
+ /// <summary>
+ /// Gets the versioned directory path.
+ /// </summary>
+ /// <returns>System.String.</returns>
+ private string GetVersionedDirectoryPath()
+ {
+ var assembly = GetType().Assembly;
+
+ var prefix = GetType().Namespace + ".";
+
+ var srch = prefix + "ffmpeg";
+
+ var resource = assembly.GetManifestResourceNames().First(r => r.StartsWith(srch));
+
+ var filename = resource.Substring(resource.IndexOf(prefix, StringComparison.OrdinalIgnoreCase) + prefix.Length);
+
+ var versionedDirectoryPath = Path.Combine(MediaToolsPath, Path.GetFileNameWithoutExtension(filename));
+
+ if (!Directory.Exists(versionedDirectoryPath))
+ {
+ Directory.CreateDirectory(versionedDirectoryPath);
+ }
+
+ ExtractTools(assembly, resource, versionedDirectoryPath);
+
+ return versionedDirectoryPath;
+ }
+
+ /// <summary>
+ /// Extracts the tools.
+ /// </summary>
+ /// <param name="assembly">The assembly.</param>
+ /// <param name="zipFileResourcePath">The zip file resource path.</param>
+ /// <param name="targetPath">The target path.</param>
+ private void ExtractTools(Assembly assembly, string zipFileResourcePath, string targetPath)
+ {
+ using (var resourceStream = assembly.GetManifestResourceStream(zipFileResourcePath))
+ {
+ _zipClient.ExtractAll(resourceStream, targetPath, false);
+ }
+
+ ExtractFonts(assembly, targetPath);
+ }
+
+ /// <summary>
+ /// Extracts the fonts.
+ /// </summary>
+ /// <param name="assembly">The assembly.</param>
+ /// <param name="targetPath">The target path.</param>
+ private async void ExtractFonts(Assembly assembly, string targetPath)
+ {
+ var fontsDirectory = Path.Combine(targetPath, "fonts");
+
+ if (!Directory.Exists(fontsDirectory))
+ {
+ Directory.CreateDirectory(fontsDirectory);
+ }
+
+ const string fontFilename = "ARIALUNI.TTF";
+
+ var fontFile = Path.Combine(fontsDirectory, fontFilename);
+
+ if (!File.Exists(fontFile))
+ {
+ using (var stream = assembly.GetManifestResourceStream(GetType().Namespace + ".fonts." + fontFilename))
+ {
+ using (var fileStream = new FileStream(fontFile, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
+ {
+ await stream.CopyToAsync(fileStream).ConfigureAwait(false);
+ }
+ }
+ }
+
+ await ExtractFontConfigFile(assembly, fontsDirectory).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Extracts the font config file.
+ /// </summary>
+ /// <param name="assembly">The assembly.</param>
+ /// <param name="fontsDirectory">The fonts directory.</param>
+ /// <returns>Task.</returns>
+ private async Task ExtractFontConfigFile(Assembly assembly, string fontsDirectory)
+ {
+ const string fontConfigFilename = "fonts.conf";
+ var fontConfigFile = Path.Combine(fontsDirectory, fontConfigFilename);
+
+ if (!File.Exists(fontConfigFile))
+ {
+ using (var stream = assembly.GetManifestResourceStream(GetType().Namespace + ".fonts." + fontConfigFilename))
+ {
+ using (var streamReader = new StreamReader(stream))
+ {
+ var contents = await streamReader.ReadToEndAsync().ConfigureAwait(false);
+
+ contents = contents.Replace("<dir></dir>", "<dir>" + fontsDirectory + "</dir>");
+
+ var bytes = Encoding.UTF8.GetBytes(contents);
+
+ using (var fileStream = new FileStream(fontConfigFile, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
+ {
+ await fileStream.WriteAsync(bytes, 0, bytes.Length);
+ }
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the media info.
+ /// </summary>
+ /// <param name="inputFiles">The input files.</param>
+ /// <param name="type">The type.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task<MediaInfoResult> GetMediaInfo(string[] inputFiles, InputType type, CancellationToken cancellationToken)
+ {
+ return GetMediaInfoInternal(GetInputArgument(inputFiles, type), type != InputType.AudioFile, GetProbeSizeArgument(type), cancellationToken);
+ }
+
+ /// <summary>
+ /// Gets the input argument.
+ /// </summary>
+ /// <param name="inputFiles">The input files.</param>
+ /// <param name="type">The type.</param>
+ /// <returns>System.String.</returns>
+ /// <exception cref="System.ArgumentException">Unrecognized InputType</exception>
+ public string GetInputArgument(string[] inputFiles, InputType type)
+ {
+ string inputPath = null;
+
+ switch (type)
+ {
+ case InputType.Dvd:
+ case InputType.VideoFile:
+ case InputType.AudioFile:
+ inputPath = GetConcatInputArgument(inputFiles);
+ break;
+ case InputType.Bluray:
+ inputPath = GetBlurayInputArgument(inputFiles[0]);
+ break;
+ default:
+ throw new ArgumentException("Unrecognized InputType");
+ }
+
+ return inputPath;
+ }
+
+ /// <summary>
+ /// Gets the probe size argument.
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <returns>System.String.</returns>
+ public string GetProbeSizeArgument(InputType type)
+ {
+ return type == InputType.Dvd ? "-probesize 1G -analyzeduration 200M" : string.Empty;
+ }
+
+ /// <summary>
+ /// Gets the media info internal.
+ /// </summary>
+ /// <param name="inputPath">The input path.</param>
+ /// <param name="extractChapters">if set to <c>true</c> [extract chapters].</param>
+ /// <param name="probeSizeArgument">The probe size argument.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{MediaInfoResult}.</returns>
+ /// <exception cref="System.ApplicationException"></exception>
+ private async Task<MediaInfoResult> GetMediaInfoInternal(string inputPath, bool extractChapters, string probeSizeArgument, CancellationToken cancellationToken)
+ {
+ var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ CreateNoWindow = true,
+ UseShellExecute = false,
+
+ // Must consume both or ffmpeg may hang due to deadlocks. See comments below.
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ FileName = FFProbePath,
+ Arguments = string.Format("{0} -i {1} -threads 0 -v info -print_format json -show_streams -show_format", probeSizeArgument, inputPath).Trim(),
+
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false
+ },
+
+ EnableRaisingEvents = true
+ };
+
+ _logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
+
+ process.Exited += ProcessExited;
+
+ await _ffProbeResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ MediaInfoResult result;
+ string standardError = null;
+
+ try
+ {
+ process.Start();
+
+ Task<string> standardErrorReadTask = null;
+
+ // MUST read both stdout and stderr asynchronously or a deadlock may occurr
+ if (extractChapters)
+ {
+ standardErrorReadTask = process.StandardError.ReadToEndAsync();
+ }
+ else
+ {
+ process.BeginErrorReadLine();
+ }
+
+ result = _jsonSerializer.DeserializeFromStream<MediaInfoResult>(process.StandardOutput.BaseStream);
+
+ if (extractChapters)
+ {
+ standardError = await standardErrorReadTask.ConfigureAwait(false);
+ }
+ }
+ catch
+ {
+ // Hate having to do this
+ try
+ {
+ process.Kill();
+ }
+ catch (InvalidOperationException ex1)
+ {
+ _logger.ErrorException("Error killing ffprobe", ex1);
+ }
+ catch (Win32Exception ex1)
+ {
+ _logger.ErrorException("Error killing ffprobe", ex1);
+ }
+
+ throw;
+ }
+ finally
+ {
+ _ffProbeResourcePool.Release();
+ }
+
+ if (result == null)
+ {
+ throw new ApplicationException(string.Format("FFProbe failed for {0}", inputPath));
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (extractChapters && !string.IsNullOrEmpty(standardError))
+ {
+ AddChapters(result, standardError);
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Adds the chapters.
+ /// </summary>
+ /// <param name="result">The result.</param>
+ /// <param name="standardError">The standard error.</param>
+ private void AddChapters(MediaInfoResult result, string standardError)
+ {
+ var lines = standardError.Split('\n').Select(l => l.TrimStart());
+
+ var chapters = new List<ChapterInfo> { };
+
+ ChapterInfo lastChapter = null;
+
+ foreach (var line in lines)
+ {
+ if (line.StartsWith("Chapter", StringComparison.OrdinalIgnoreCase))
+ {
+ // Example:
+ // Chapter #0.2: start 400.534, end 4565.435
+ const string srch = "start ";
+ var start = line.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
+
+ if (start == -1)
+ {
+ continue;
+ }
+
+ var subString = line.Substring(start + srch.Length);
+ subString = subString.Substring(0, subString.IndexOf(','));
+
+ double seconds;
+
+ if (double.TryParse(subString, out seconds))
+ {
+ lastChapter = new ChapterInfo
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(seconds).Ticks
+ };
+
+ chapters.Add(lastChapter);
+ }
+ }
+
+ else if (line.StartsWith("title", StringComparison.OrdinalIgnoreCase))
+ {
+ if (lastChapter != null && string.IsNullOrEmpty(lastChapter.Name))
+ {
+ var index = line.IndexOf(':');
+
+ if (index != -1)
+ {
+ lastChapter.Name = line.Substring(index + 1).Trim().TrimEnd('\r');
+ }
+ }
+ }
+ }
+
+ result.Chapters = chapters;
+ }
+
+ /// <summary>
+ /// Processes the exited.
+ /// </summary>
+ /// <param name="sender">The sender.</param>
+ /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
+ void ProcessExited(object sender, EventArgs e)
+ {
+ ((Process)sender).Dispose();
+ }
+
+ /// <summary>
+ /// Converts the text subtitle to ass.
+ /// </summary>
+ /// <param name="inputPath">The input path.</param>
+ /// <param name="outputPath">The output path.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException">inputPath
+ /// or
+ /// outputPath</exception>
+ /// <exception cref="System.ApplicationException"></exception>
+ public async Task ConvertTextSubtitleToAss(string inputPath, string outputPath, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(inputPath))
+ {
+ throw new ArgumentNullException("inputPath");
+ }
+
+ if (string.IsNullOrEmpty(outputPath))
+ {
+ throw new ArgumentNullException("outputPath");
+ }
+
+ var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ FileName = FFMpegPath,
+ Arguments = string.Format("-i \"{0}\" \"{1}\"", inputPath, outputPath),
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false
+ }
+ };
+
+ _logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
+
+ await _subtitleExtractionResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ var ranToCompletion = StartAndWaitForProcess(process);
+
+ _subtitleExtractionResourcePool.Release();
+
+ var exitCode = ranToCompletion ? process.ExitCode : -1;
+
+ process.Dispose();
+
+ var failed = false;
+
+ if (exitCode == -1)
+ {
+ failed = true;
+
+ if (File.Exists(outputPath))
+ {
+ try
+ {
+ _logger.Info("Deleting converted subtitle due to failure: ", outputPath);
+ File.Delete(outputPath);
+ }
+ catch (IOException ex)
+ {
+ _logger.ErrorException("Error deleting converted subtitle {0}", ex, outputPath);
+ }
+ }
+ }
+ else if (!File.Exists(outputPath))
+ {
+ failed = true;
+ }
+
+ if (failed)
+ {
+ var msg = string.Format("ffmpeg subtitle conversion failed for {0}", inputPath);
+
+ _logger.Error(msg);
+
+ throw new ApplicationException(msg);
+ }
+ }
+
+ /// <summary>
+ /// Extracts the text subtitle.
+ /// </summary>
+ /// <param name="inputFiles">The input files.</param>
+ /// <param name="type">The type.</param>
+ /// <param name="subtitleStreamIndex">Index of the subtitle stream.</param>
+ /// <param name="outputPath">The output path.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentException">Must use inputPath list overload</exception>
+ public Task ExtractTextSubtitle(string[] inputFiles, InputType type, int subtitleStreamIndex, string outputPath, CancellationToken cancellationToken)
+ {
+ return ExtractTextSubtitleInternal(GetInputArgument(inputFiles, type), subtitleStreamIndex, outputPath, cancellationToken);
+ }
+
+ /// <summary>
+ /// Extracts the text subtitle.
+ /// </summary>
+ /// <param name="inputPath">The input path.</param>
+ /// <param name="subtitleStreamIndex">Index of the subtitle stream.</param>
+ /// <param name="outputPath">The output path.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException">inputPath
+ /// or
+ /// outputPath
+ /// or
+ /// cancellationToken</exception>
+ /// <exception cref="System.ApplicationException"></exception>
+ private async Task ExtractTextSubtitleInternal(string inputPath, int subtitleStreamIndex, string outputPath, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(inputPath))
+ {
+ throw new ArgumentNullException("inputPath");
+ }
+
+ if (string.IsNullOrEmpty(outputPath))
+ {
+ throw new ArgumentNullException("outputPath");
+ }
+
+ if (cancellationToken == null)
+ {
+ throw new ArgumentNullException("cancellationToken");
+ }
+
+ var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ FileName = FFMpegPath,
+ Arguments = string.Format("-i {0} -map 0:{1} -an -vn -c:s ass \"{2}\"", inputPath, subtitleStreamIndex, outputPath),
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false
+ }
+ };
+
+ _logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
+
+ await _subtitleExtractionResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ var ranToCompletion = StartAndWaitForProcess(process);
+
+ _subtitleExtractionResourcePool.Release();
+
+ var exitCode = ranToCompletion ? process.ExitCode : -1;
+
+ process.Dispose();
+
+ var failed = false;
+
+ if (exitCode == -1)
+ {
+ failed = true;
+
+ if (File.Exists(outputPath))
+ {
+ try
+ {
+ _logger.Info("Deleting extracted subtitle due to failure: ", outputPath);
+ File.Delete(outputPath);
+ }
+ catch (IOException ex)
+ {
+ _logger.ErrorException("Error deleting extracted subtitle {0}", ex, outputPath);
+ }
+ }
+ }
+ else if (!File.Exists(outputPath))
+ {
+ failed = true;
+ }
+
+ if (failed)
+ {
+ var msg = string.Format("ffmpeg subtitle extraction failed for {0}", inputPath);
+
+ _logger.Error(msg);
+
+ throw new ApplicationException(msg);
+ }
+ }
+
+ /// <summary>
+ /// Extracts the image.
+ /// </summary>
+ /// <param name="inputFiles">The input files.</param>
+ /// <param name="type">The type.</param>
+ /// <param name="offset">The offset.</param>
+ /// <param name="outputPath">The output path.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentException">Must use inputPath list overload</exception>
+ public Task ExtractImage(string[] inputFiles, InputType type, TimeSpan? offset, string outputPath, CancellationToken cancellationToken)
+ {
+ var resourcePool = type == InputType.AudioFile ? _audioImageResourcePool : _videoImageResourcePool;
+
+ return ExtractImageInternal(GetInputArgument(inputFiles, type), offset, outputPath, resourcePool, cancellationToken);
+ }
+
+ /// <summary>
+ /// Extracts the image.
+ /// </summary>
+ /// <param name="inputPath">The input path.</param>
+ /// <param name="offset">The offset.</param>
+ /// <param name="outputPath">The output path.</param>
+ /// <param name="resourcePool">The resource pool.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException">inputPath
+ /// or
+ /// outputPath</exception>
+ /// <exception cref="System.ApplicationException"></exception>
+ private async Task ExtractImageInternal(string inputPath, TimeSpan? offset, string outputPath, SemaphoreSlim resourcePool, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(inputPath))
+ {
+ throw new ArgumentNullException("inputPath");
+ }
+
+ if (string.IsNullOrEmpty(outputPath))
+ {
+ throw new ArgumentNullException("outputPath");
+ }
+
+
+ var args = string.Format("-i {0} -threads 0 -v quiet -vframes 1 -filter:v select=\\'eq(pict_type\\,I)\\' -f image2 \"{1}\"", inputPath, outputPath);
+
+ if (offset.HasValue)
+ {
+ args = string.Format("-ss {0} ", Convert.ToInt32(offset.Value.TotalSeconds)) + args;
+ }
+
+ var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ FileName = FFMpegPath,
+ Arguments = args,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false
+ }
+ };
+
+ await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ var ranToCompletion = StartAndWaitForProcess(process);
+
+ resourcePool.Release();
+
+ var exitCode = ranToCompletion ? process.ExitCode : -1;
+
+ process.Dispose();
+
+ var failed = false;
+
+ if (exitCode == -1)
+ {
+ failed = true;
+
+ if (File.Exists(outputPath))
+ {
+ try
+ {
+ _logger.Info("Deleting extracted image due to failure: ", outputPath);
+ File.Delete(outputPath);
+ }
+ catch (IOException ex)
+ {
+ _logger.ErrorException("Error deleting extracted image {0}", ex, outputPath);
+ }
+ }
+ }
+ else if (!File.Exists(outputPath))
+ {
+ failed = true;
+ }
+
+ if (failed)
+ {
+ var msg = string.Format("ffmpeg image extraction failed for {0}", inputPath);
+
+ _logger.Error(msg);
+
+ throw new ApplicationException(msg);
+ }
+ }
+
+ /// <summary>
+ /// Starts the and wait for process.
+ /// </summary>
+ /// <param name="process">The process.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ private bool StartAndWaitForProcess(Process process)
+ {
+ process.Start();
+
+ var ranToCompletion = process.WaitForExit(10000);
+
+ if (!ranToCompletion)
+ {
+ try
+ {
+ _logger.Info("Killing ffmpeg process");
+
+ process.Kill();
+
+ process.WaitForExit(1000);
+ }
+ catch (Win32Exception ex)
+ {
+ _logger.ErrorException("Error killing process", ex);
+ }
+ catch (InvalidOperationException ex)
+ {
+ _logger.ErrorException("Error killing process", ex);
+ }
+ catch (NotSupportedException ex)
+ {
+ _logger.ErrorException("Error killing process", ex);
+ }
+ }
+
+ return ranToCompletion;
+ }
+
+ /// <summary>
+ /// Gets the file input argument.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>System.String.</returns>
+ public string GetFileInputArgument(string path)
+ {
+ return string.Format("file:\"{0}\"", path);
+ }
+
+ /// <summary>
+ /// Gets the concat input argument.
+ /// </summary>
+ /// <param name="playableStreamFiles">The playable stream files.</param>
+ /// <returns>System.String.</returns>
+ public string GetConcatInputArgument(string[] playableStreamFiles)
+ {
+ // Get all streams
+ // If there's more than one we'll need to use the concat command
+ if (playableStreamFiles.Length > 1)
+ {
+ var files = string.Join("|", playableStreamFiles);
+
+ return string.Format("concat:\"{0}\"", files);
+ }
+
+ // Determine the input path for video files
+ return string.Format("file:\"{0}\"", playableStreamFiles[0]);
+ }
+
+ /// <summary>
+ /// Gets the bluray input argument.
+ /// </summary>
+ /// <param name="blurayRoot">The bluray root.</param>
+ /// <returns>System.String.</returns>
+ public string GetBlurayInputArgument(string blurayRoot)
+ {
+ return string.Format("bluray:\"{0}\"", blurayRoot);
+ }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources.
+ /// </summary>
+ /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool dispose)
+ {
+ if (dispose)
+ {
+ _videoImageResourcePool.Dispose();
+ }
+
+ SetErrorMode(ErrorModes.SYSTEM_DEFAULT);
+ }
+
+ /// <summary>
+ /// Sets the error mode.
+ /// </summary>
+ /// <param name="uMode">The u mode.</param>
+ /// <returns>ErrorModes.</returns>
+ [DllImport("kernel32.dll")]
+ static extern ErrorModes SetErrorMode(ErrorModes uMode);
+
+ /// <summary>
+ /// Enum ErrorModes
+ /// </summary>
+ [Flags]
+ public enum ErrorModes : uint
+ {
+ /// <summary>
+ /// The SYSTE m_ DEFAULT
+ /// </summary>
+ SYSTEM_DEFAULT = 0x0,
+ /// <summary>
+ /// The SE m_ FAILCRITICALERRORS
+ /// </summary>
+ SEM_FAILCRITICALERRORS = 0x0001,
+ /// <summary>
+ /// The SE m_ NOALIGNMENTFAULTEXCEPT
+ /// </summary>
+ SEM_NOALIGNMENTFAULTEXCEPT = 0x0004,
+ /// <summary>
+ /// The SE m_ NOGPFAULTERRORBOX
+ /// </summary>
+ SEM_NOGPFAULTERRORBOX = 0x0002,
+ /// <summary>
+ /// The SE m_ NOOPENFILEERRORBOX
+ /// </summary>
+ SEM_NOOPENFILEERRORBOX = 0x8000
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/MediaEncoder/ffmpeg20130405.zip.REMOVED.git-id b/MediaBrowser.Server.Implementations/MediaEncoder/ffmpeg20130405.zip.REMOVED.git-id
new file mode 100644
index 000000000..363d8552f
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/MediaEncoder/ffmpeg20130405.zip.REMOVED.git-id
@@ -0,0 +1 @@
+33054d71c54e6c262d24d16153c05d45718aeb26 \ No newline at end of file
diff --git a/MediaBrowser.Server.Implementations/MediaEncoder/fonts/fonts.conf b/MediaBrowser.Server.Implementations/MediaEncoder/fonts/fonts.conf
new file mode 100644
index 000000000..648bdb7b2
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/MediaEncoder/fonts/fonts.conf
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<fontconfig>
+
+<dir></dir>
+ <alias>
+ <family>Arial</family>
+ <prefer>Arial Unicode MS</prefer>
+ </alias>
+</fontconfig> \ No newline at end of file
diff --git a/MediaBrowser.Server.Implementations/MediaEncoder/readme.txt b/MediaBrowser.Server.Implementations/MediaEncoder/readme.txt
new file mode 100644
index 000000000..b32dd9aec
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/MediaEncoder/readme.txt
@@ -0,0 +1,5 @@
+This is the 32-bit static build of ffmpeg, located at:
+
+http://ffmpeg.zeranoe.com/builds/
+
+The zip file contains both ffmpeg and ffprobe, and is suffixed with the date of the build. \ No newline at end of file