|
@@ -6,17 +6,15 @@ using MediaBrowser.Model.Logging;
|
|
|
using MediaBrowser.Model.Serialization;
|
|
|
using System;
|
|
|
using System.Collections.Concurrent;
|
|
|
-using System.Collections.Generic;
|
|
|
using System.ComponentModel;
|
|
|
using System.Diagnostics;
|
|
|
using System.Globalization;
|
|
|
using System.IO;
|
|
|
-using System.Linq;
|
|
|
using System.Text;
|
|
|
using System.Threading;
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
-namespace MediaBrowser.Server.Implementations.MediaEncoder
|
|
|
+namespace MediaBrowser.MediaEncoding.Encoder
|
|
|
{
|
|
|
/// <summary>
|
|
|
/// Class MediaEncoder
|
|
@@ -53,6 +51,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
|
|
/// The FF probe resource pool
|
|
|
/// </summary>
|
|
|
private readonly SemaphoreSlim _ffProbeResourcePool = new SemaphoreSlim(2, 2);
|
|
|
+
|
|
|
private readonly IFileSystem _fileSystem;
|
|
|
|
|
|
public string FFMpegPath { get; private set; }
|
|
@@ -62,7 +61,8 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
|
|
public string Version { get; private set; }
|
|
|
|
|
|
public MediaEncoder(ILogger logger, IApplicationPaths appPaths,
|
|
|
- IJsonSerializer jsonSerializer, string ffMpegPath, string ffProbePath, string version, IFileSystem fileSystem)
|
|
|
+ IJsonSerializer jsonSerializer, string ffMpegPath, string ffProbePath, string version,
|
|
|
+ IFileSystem fileSystem)
|
|
|
{
|
|
|
_logger = logger;
|
|
|
_appPaths = appPaths;
|
|
@@ -85,7 +85,8 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
|
|
/// <summary>
|
|
|
/// The _semaphoreLocks
|
|
|
/// </summary>
|
|
|
- private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks = new ConcurrentDictionary<string, SemaphoreSlim>();
|
|
|
+ private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
|
|
|
+ new ConcurrentDictionary<string, SemaphoreSlim>();
|
|
|
|
|
|
/// <summary>
|
|
|
/// Gets the lock.
|
|
@@ -106,10 +107,10 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
|
/// <returns>Task.</returns>
|
|
|
public Task<InternalMediaInfoResult> GetMediaInfo(string[] inputFiles, InputType type, bool isAudio,
|
|
|
- CancellationToken cancellationToken)
|
|
|
+ CancellationToken cancellationToken)
|
|
|
{
|
|
|
return GetMediaInfoInternal(GetInputArgument(inputFiles, type), !isAudio,
|
|
|
- GetProbeSizeArgument(type), cancellationToken);
|
|
|
+ GetProbeSizeArgument(type), cancellationToken);
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
@@ -172,8 +173,8 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
|
|
/// <returns>Task{MediaInfoResult}.</returns>
|
|
|
/// <exception cref="System.ApplicationException"></exception>
|
|
|
private async Task<InternalMediaInfoResult> GetMediaInfoInternal(string inputPath, bool extractChapters,
|
|
|
- string probeSizeArgument,
|
|
|
- CancellationToken cancellationToken)
|
|
|
+ string probeSizeArgument,
|
|
|
+ CancellationToken cancellationToken)
|
|
|
{
|
|
|
var args = extractChapters
|
|
|
? "{0} -i {1} -threads 0 -v info -print_format json -show_streams -show_chapters -show_format"
|
|
@@ -191,7 +192,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
|
|
RedirectStandardError = true,
|
|
|
FileName = FFProbePath,
|
|
|
Arguments = string.Format(args,
|
|
|
- probeSizeArgument, inputPath).Trim(),
|
|
|
+ probeSizeArgument, inputPath).Trim(),
|
|
|
|
|
|
WindowStyle = ProcessWindowStyle.Hidden,
|
|
|
ErrorDialog = false
|
|
@@ -225,7 +226,8 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
|
|
{
|
|
|
process.BeginErrorReadLine();
|
|
|
|
|
|
- result = _jsonSerializer.DeserializeFromStream<InternalMediaInfoResult>(process.StandardOutput.BaseStream);
|
|
|
+ result =
|
|
|
+ _jsonSerializer.DeserializeFromStream<InternalMediaInfoResult>(process.StandardOutput.BaseStream);
|
|
|
}
|
|
|
catch
|
|
|
{
|
|
@@ -295,7 +297,8 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
|
|
/// <param name="language">The language.</param>
|
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
|
/// <returns>Task.</returns>
|
|
|
- public async Task ConvertTextSubtitleToAss(string inputPath, string outputPath, string language, CancellationToken cancellationToken)
|
|
|
+ public async Task ConvertTextSubtitleToAss(string inputPath, string outputPath, string language,
|
|
|
+ CancellationToken cancellationToken)
|
|
|
{
|
|
|
var semaphore = GetLock(outputPath);
|
|
|
|
|
@@ -340,33 +343,35 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
|
|
}
|
|
|
|
|
|
|
|
|
- var encodingParam = string.IsNullOrEmpty(language) ? string.Empty :
|
|
|
- GetSubtitleLanguageEncodingParam(language) + " ";
|
|
|
+ var encodingParam = string.IsNullOrEmpty(language)
|
|
|
+ ? string.Empty
|
|
|
+ : GetSubtitleLanguageEncodingParam(language) + " ";
|
|
|
|
|
|
var process = new Process
|
|
|
+ {
|
|
|
+ StartInfo = new ProcessStartInfo
|
|
|
{
|
|
|
- StartInfo = new ProcessStartInfo
|
|
|
- {
|
|
|
- RedirectStandardOutput = false,
|
|
|
- RedirectStandardError = true,
|
|
|
-
|
|
|
- CreateNoWindow = true,
|
|
|
- UseShellExecute = false,
|
|
|
- FileName = FFMpegPath,
|
|
|
- Arguments =
|
|
|
- string.Format("{0} -i \"{1}\" -c:s ass \"{2}\"", encodingParam, inputPath, outputPath),
|
|
|
-
|
|
|
- WindowStyle = ProcessWindowStyle.Hidden,
|
|
|
- ErrorDialog = false
|
|
|
- }
|
|
|
- };
|
|
|
+ RedirectStandardOutput = false,
|
|
|
+ RedirectStandardError = true,
|
|
|
+
|
|
|
+ CreateNoWindow = true,
|
|
|
+ UseShellExecute = false,
|
|
|
+ FileName = FFMpegPath,
|
|
|
+ Arguments =
|
|
|
+ string.Format("{0} -i \"{1}\" -c:s ass \"{2}\"", encodingParam, inputPath, outputPath),
|
|
|
+
|
|
|
+ WindowStyle = ProcessWindowStyle.Hidden,
|
|
|
+ ErrorDialog = false
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
_logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
|
|
|
|
|
var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "ffmpeg-sub-convert-" + Guid.NewGuid() + ".txt");
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
|
|
|
|
|
|
- var logFileStream = _fileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, true);
|
|
|
+ var logFileStream = _fileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read,
|
|
|
+ true);
|
|
|
|
|
|
try
|
|
|
{
|
|
@@ -525,7 +530,8 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
|
/// <returns>Task.</returns>
|
|
|
/// <exception cref="System.ArgumentException">Must use inputPath list overload</exception>
|
|
|
- public async Task ExtractTextSubtitle(string[] inputFiles, InputType type, int subtitleStreamIndex, bool copySubtitleStream, string outputPath, CancellationToken cancellationToken)
|
|
|
+ public async Task ExtractTextSubtitle(string[] inputFiles, InputType type, int subtitleStreamIndex,
|
|
|
+ bool copySubtitleStream, string outputPath, CancellationToken cancellationToken)
|
|
|
{
|
|
|
var semaphore = GetLock(outputPath);
|
|
|
|
|
@@ -535,7 +541,9 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
|
|
{
|
|
|
if (!File.Exists(outputPath))
|
|
|
{
|
|
|
- await ExtractTextSubtitleInternal(GetInputArgument(inputFiles, type), subtitleStreamIndex, copySubtitleStream, outputPath, cancellationToken).ConfigureAwait(false);
|
|
|
+ await
|
|
|
+ ExtractTextSubtitleInternal(GetInputArgument(inputFiles, type), subtitleStreamIndex,
|
|
|
+ copySubtitleStream, outputPath, cancellationToken).ConfigureAwait(false);
|
|
|
}
|
|
|
}
|
|
|
finally
|
|
@@ -559,7 +567,8 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
|
|
/// or
|
|
|
/// cancellationToken</exception>
|
|
|
/// <exception cref="System.ApplicationException"></exception>
|
|
|
- private async Task ExtractTextSubtitleInternal(string inputPath, int subtitleStreamIndex, bool copySubtitleStream, string outputPath, CancellationToken cancellationToken)
|
|
|
+ private async Task ExtractTextSubtitleInternal(string inputPath, int subtitleStreamIndex,
|
|
|
+ bool copySubtitleStream, string outputPath, CancellationToken cancellationToken)
|
|
|
{
|
|
|
if (string.IsNullOrEmpty(inputPath))
|
|
|
{
|
|
@@ -571,11 +580,13 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
|
|
throw new ArgumentNullException("outputPath");
|
|
|
}
|
|
|
|
|
|
- string processArgs = string.Format("-i {0} -map 0:{1} -an -vn -c:s ass \"{2}\"", inputPath, subtitleStreamIndex, outputPath);
|
|
|
+ string processArgs = string.Format("-i {0} -map 0:{1} -an -vn -c:s ass \"{2}\"", inputPath,
|
|
|
+ subtitleStreamIndex, outputPath);
|
|
|
|
|
|
if (copySubtitleStream)
|
|
|
{
|
|
|
- processArgs = string.Format("-i {0} -map 0:{1} -an -vn -c:s copy \"{2}\"", inputPath, subtitleStreamIndex, outputPath);
|
|
|
+ processArgs = string.Format("-i {0} -map 0:{1} -an -vn -c:s copy \"{2}\"", inputPath,
|
|
|
+ subtitleStreamIndex, outputPath);
|
|
|
}
|
|
|
|
|
|
var process = new Process
|
|
@@ -600,7 +611,8 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
|
|
var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "ffmpeg-sub-extract-" + Guid.NewGuid() + ".txt");
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
|
|
|
|
|
|
- var logFileStream = _fileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, true);
|
|
|
+ var logFileStream = _fileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read,
|
|
|
+ true);
|
|
|
|
|
|
try
|
|
|
{
|
|
@@ -715,7 +727,18 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- public async Task<Stream> ExtractImage(string[] inputFiles, InputType type, bool isAudio,
|
|
|
+ public Task<Stream> ExtractAudioImage(string path, CancellationToken cancellationToken)
|
|
|
+ {
|
|
|
+ return ExtractImage(new[] { path }, InputType.File, true, null, null, cancellationToken);
|
|
|
+ }
|
|
|
+
|
|
|
+ public Task<Stream> ExtractVideoImage(string[] inputFiles, InputType type, Video3DFormat? threedFormat,
|
|
|
+ TimeSpan? offset, CancellationToken cancellationToken)
|
|
|
+ {
|
|
|
+ return ExtractImage(inputFiles, type, false, threedFormat, offset, cancellationToken);
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<Stream> ExtractImage(string[] inputFiles, InputType type, bool isAudio,
|
|
|
Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken)
|
|
|
{
|
|
|
var resourcePool = isAudio ? _audioImageResourcePool : _videoImageResourcePool;
|
|
@@ -773,7 +796,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
|
|
}
|
|
|
|
|
|
// Use ffmpeg to sample 100 (we can drop this if required using thumbnail=50 for 50 frames) frames and pick the best thumbnail. Have a fall back just in case.
|
|
|
- var args = useIFrame ? string.Format("-i {0} -threads 0 -v quiet -vframes 1 -vf \"{2},thumbnail\" -f image2 \"{1}\"", inputPath, "-", vf) :
|
|
|
+ var args = useIFrame ? string.Format("-i {0} -threads 0 -v quiet -vframes 1 -vf \"{2},thumbnail=80\" -f image2 \"{1}\"", inputPath, "-", vf) :
|
|
|
string.Format("-i {0} -threads 0 -v quiet -vframes 1 -vf \"{2}\" -f image2 \"{1}\"", inputPath, "-", vf);
|
|
|
|
|
|
var probeSize = GetProbeSizeArgument(type);
|
|
@@ -834,7 +857,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
|
|
_logger.ErrorException("Error killing process", ex);
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
resourcePool.Release();
|
|
|
|
|
|
var exitCode = ranToCompletion ? process.ExitCode : -1;
|