mirror of
https://github.com/rosenbjerg/FFMpegCore.git
synced 2024-11-10 00:24:14 +01:00
Cleanup
This commit is contained in:
parent
7444899106
commit
df0205fb11
18 changed files with 203 additions and 190 deletions
|
@ -10,39 +10,39 @@ public class FFMpegOptionsTest
|
|||
[TestMethod]
|
||||
public void Options_Initialized()
|
||||
{
|
||||
Assert.IsNotNull(FFMpegOptions.Options);
|
||||
Assert.IsNotNull(GlobalFFOptions.Current);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Options_Defaults_Configured()
|
||||
{
|
||||
Assert.AreEqual(new FFMpegOptions().RootDirectory, $"");
|
||||
Assert.AreEqual(new FFOptions().BinaryFolder, $"");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Options_Loaded_From_File()
|
||||
{
|
||||
Assert.AreEqual(
|
||||
FFMpegOptions.Options.RootDirectory,
|
||||
JsonConvert.DeserializeObject<FFMpegOptions>(File.ReadAllText("ffmpeg.config.json")).RootDirectory
|
||||
GlobalFFOptions.Current.BinaryFolder,
|
||||
JsonConvert.DeserializeObject<FFOptions>(File.ReadAllText("ffmpeg.config.json")).BinaryFolder
|
||||
);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Options_Set_Programmatically()
|
||||
{
|
||||
var original = FFMpegOptions.Options;
|
||||
var original = GlobalFFOptions.Current;
|
||||
try
|
||||
{
|
||||
FFMpegOptions.Configure(new FFMpegOptions { RootDirectory = "Whatever" });
|
||||
GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "Whatever" });
|
||||
Assert.AreEqual(
|
||||
FFMpegOptions.Options.RootDirectory,
|
||||
GlobalFFOptions.Current.BinaryFolder,
|
||||
"Whatever"
|
||||
);
|
||||
}
|
||||
finally
|
||||
{
|
||||
FFMpegOptions.Configure(original);
|
||||
GlobalFFOptions.Configure(original);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,7 +104,7 @@ public void Video_ToMP4_Args_StreamPipe()
|
|||
[TestMethod, Timeout(10000)]
|
||||
public async Task Video_ToMP4_Args_StreamOutputPipe_Async_Failure()
|
||||
{
|
||||
await Assert.ThrowsExceptionAsync<FFMpegProcessException>(async () =>
|
||||
await Assert.ThrowsExceptionAsync<FFMpegException>(async () =>
|
||||
{
|
||||
await using var ms = new MemoryStream();
|
||||
var pipeSource = new StreamPipeSink(ms);
|
||||
|
@ -134,7 +134,7 @@ public void Video_StreamFile_OutputToMemoryStream()
|
|||
[TestMethod, Timeout(10000)]
|
||||
public void Video_ToMP4_Args_StreamOutputPipe_Failure()
|
||||
{
|
||||
Assert.ThrowsException<FFMpegProcessException>(() =>
|
||||
Assert.ThrowsException<FFMpegException>(() =>
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
FFMpegArguments
|
||||
|
@ -435,7 +435,7 @@ public void Video_OutputsData()
|
|||
var outputFile = new TemporaryFile("out.mp4");
|
||||
var dataReceived = false;
|
||||
|
||||
FFMpegOptions.Configure(opt => opt.Encoding = Encoding.UTF8);
|
||||
GlobalFFOptions.Configure(opt => opt.Encoding = Encoding.UTF8);
|
||||
var success = FFMpegArguments
|
||||
.FromFileInput(TestResources.Mp4Video)
|
||||
.WithGlobalOptions(options => options
|
||||
|
|
|
@ -18,7 +18,7 @@ public DemuxConcatArgument(IEnumerable<string> values)
|
|||
{
|
||||
Values = values.Select(value => $"file '{value}'");
|
||||
}
|
||||
private readonly string _tempFileName = Path.Combine(FFMpegOptions.Options.TempDirectory, Guid.NewGuid() + ".txt");
|
||||
private readonly string _tempFileName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, $"concat_{Guid.NewGuid()}.txt");
|
||||
|
||||
public void Pre() => File.WriteAllLines(_tempFileName, Values);
|
||||
public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
|
|
|
@ -15,8 +15,8 @@ public string Extension
|
|||
{
|
||||
get
|
||||
{
|
||||
if (FFMpegOptions.Options.ExtensionOverrides.ContainsKey(Name))
|
||||
return FFMpegOptions.Options.ExtensionOverrides[Name];
|
||||
if (GlobalFFOptions.Current.ExtensionOverrides.ContainsKey(Name))
|
||||
return GlobalFFOptions.Current.ExtensionOverrides[Name];
|
||||
return "." + Name;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,53 +4,43 @@ namespace FFMpegCore.Exceptions
|
|||
{
|
||||
public enum FFMpegExceptionType
|
||||
{
|
||||
Dependency,
|
||||
Conversion,
|
||||
File,
|
||||
Operation,
|
||||
Process
|
||||
}
|
||||
|
||||
public abstract class FFException : Exception
|
||||
{
|
||||
protected FFException(string message) : base(message) { }
|
||||
protected FFException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
public abstract class FFProcessException : FFException
|
||||
{
|
||||
protected FFProcessException(string process, int exitCode, string errorOutput)
|
||||
: base($"{process} exited with non-zero exit-code {exitCode}\n{errorOutput}")
|
||||
{
|
||||
ExitCode = exitCode;
|
||||
ErrorOutput = errorOutput;
|
||||
}
|
||||
|
||||
public int ExitCode { get; }
|
||||
public string ErrorOutput { get; }
|
||||
}
|
||||
public class FFMpegProcessException : FFProcessException
|
||||
{
|
||||
public FFMpegProcessException(int exitCode, string errorOutput)
|
||||
: base("ffmpeg", exitCode, errorOutput) { }
|
||||
}
|
||||
public class FFProbeProcessException : FFProcessException
|
||||
{
|
||||
public FFProbeProcessException(int exitCode, string errorOutput)
|
||||
: base("ffprobe", exitCode, errorOutput) { }
|
||||
}
|
||||
|
||||
public class FFMpegException : Exception
|
||||
{
|
||||
public FFMpegException(FFMpegExceptionType type, string? message = null, Exception? innerException = null, string ffMpegErrorOutput = "")
|
||||
public FFMpegException(FFMpegExceptionType type, string message, Exception? innerException = null, string ffMpegErrorOutput = "")
|
||||
: base(message, innerException)
|
||||
{
|
||||
FFMpegErrorOutput = ffMpegErrorOutput;
|
||||
Type = type;
|
||||
}
|
||||
|
||||
public FFMpegException(FFMpegExceptionType type, string message, string ffMpegErrorOutput = "")
|
||||
: base(message)
|
||||
{
|
||||
FFMpegErrorOutput = ffMpegErrorOutput;
|
||||
Type = type;
|
||||
}
|
||||
public FFMpegException(FFMpegExceptionType type, string message)
|
||||
: base(message)
|
||||
{
|
||||
FFMpegErrorOutput = string.Empty;
|
||||
Type = type;
|
||||
}
|
||||
|
||||
public FFMpegExceptionType Type { get; }
|
||||
public string FFMpegErrorOutput { get; }
|
||||
}
|
||||
public class FFOptionsException : Exception
|
||||
{
|
||||
public FFOptionsException(string message, Exception? innerException = null)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class FFMpegArgumentException : Exception
|
||||
{
|
||||
|
|
|
@ -254,8 +254,8 @@ public static bool Join(string output, params string[] videos)
|
|||
{
|
||||
var video = FFProbe.Analyse(videoPath);
|
||||
FFMpegHelper.ConversionSizeExceptionCheck(video);
|
||||
var destinationPath = Path.Combine(FFMpegOptions.Options.TempDirectory, $"{Path.GetFileNameWithoutExtension(videoPath)}{FileExtension.Ts}");
|
||||
Directory.CreateDirectory(FFMpegOptions.Options.TempDirectory);
|
||||
var destinationPath = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, $"{Path.GetFileNameWithoutExtension(videoPath)}{FileExtension.Ts}");
|
||||
Directory.CreateDirectory(GlobalFFOptions.Current.TemporaryFilesFolder);
|
||||
Convert(videoPath, destinationPath, VideoType.Ts);
|
||||
return destinationPath;
|
||||
}).ToArray();
|
||||
|
@ -284,7 +284,7 @@ public static bool Join(string output, params string[] videos)
|
|||
/// <returns>Output video information.</returns>
|
||||
public static bool JoinImageSequence(string output, double frameRate = 30, params ImageInfo[] images)
|
||||
{
|
||||
var tempFolderName = Path.Combine(FFMpegOptions.Options.TempDirectory, Guid.NewGuid().ToString());
|
||||
var tempFolderName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, Guid.NewGuid().ToString());
|
||||
var temporaryImageFiles = images.Select((image, index) =>
|
||||
{
|
||||
FFMpegHelper.ConversionSizeExceptionCheck(Image.FromFile(image.FullName));
|
||||
|
@ -398,7 +398,7 @@ internal static IReadOnlyList<PixelFormat> GetPixelFormatsInternal()
|
|||
FFMpegHelper.RootExceptionCheck();
|
||||
|
||||
var list = new List<PixelFormat>();
|
||||
using var instance = new Instances.Instance(FFMpegOptions.Options.FFMpegBinary(), "-pix_fmts");
|
||||
using var instance = new Instances.Instance(GlobalFFOptions.GetFFMpegBinaryPath(), "-pix_fmts");
|
||||
instance.DataReceived += (e, args) =>
|
||||
{
|
||||
if (PixelFormat.TryParse(args.Data, out var format))
|
||||
|
@ -413,14 +413,14 @@ internal static IReadOnlyList<PixelFormat> GetPixelFormatsInternal()
|
|||
|
||||
public static IReadOnlyList<PixelFormat> GetPixelFormats()
|
||||
{
|
||||
if (!FFMpegOptions.Options.UseCache)
|
||||
if (!GlobalFFOptions.Current.UseCache)
|
||||
return GetPixelFormatsInternal();
|
||||
return FFMpegCache.PixelFormats.Values.ToList().AsReadOnly();
|
||||
}
|
||||
|
||||
public static bool TryGetPixelFormat(string name, out PixelFormat fmt)
|
||||
{
|
||||
if (!FFMpegOptions.Options.UseCache)
|
||||
if (!GlobalFFOptions.Current.UseCache)
|
||||
{
|
||||
fmt = GetPixelFormatsInternal().FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim());
|
||||
return fmt != null;
|
||||
|
@ -443,7 +443,7 @@ private static void ParsePartOfCodecs(Dictionary<string, Codec> codecs, string a
|
|||
{
|
||||
FFMpegHelper.RootExceptionCheck();
|
||||
|
||||
using var instance = new Instances.Instance(FFMpegOptions.Options.FFMpegBinary(), arguments);
|
||||
using var instance = new Instances.Instance(GlobalFFOptions.GetFFMpegBinaryPath(), arguments);
|
||||
instance.DataReceived += (e, args) =>
|
||||
{
|
||||
var codec = parser(args.Data);
|
||||
|
@ -485,14 +485,14 @@ internal static Dictionary<string, Codec> GetCodecsInternal()
|
|||
|
||||
public static IReadOnlyList<Codec> GetCodecs()
|
||||
{
|
||||
if (!FFMpegOptions.Options.UseCache)
|
||||
if (!GlobalFFOptions.Current.UseCache)
|
||||
return GetCodecsInternal().Values.ToList().AsReadOnly();
|
||||
return FFMpegCache.Codecs.Values.ToList().AsReadOnly();
|
||||
}
|
||||
|
||||
public static IReadOnlyList<Codec> GetCodecs(CodecType type)
|
||||
{
|
||||
if (!FFMpegOptions.Options.UseCache)
|
||||
if (!GlobalFFOptions.Current.UseCache)
|
||||
return GetCodecsInternal().Values.Where(x => x.Type == type).ToList().AsReadOnly();
|
||||
return FFMpegCache.Codecs.Values.Where(x=>x.Type == type).ToList().AsReadOnly();
|
||||
}
|
||||
|
@ -504,7 +504,7 @@ public static IReadOnlyList<Codec> GetCodecs(CodecType type)
|
|||
|
||||
public static bool TryGetCodec(string name, out Codec codec)
|
||||
{
|
||||
if (!FFMpegOptions.Options.UseCache)
|
||||
if (!GlobalFFOptions.Current.UseCache)
|
||||
{
|
||||
codec = GetCodecsInternal().Values.FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim());
|
||||
return codec != null;
|
||||
|
@ -527,7 +527,7 @@ internal static IReadOnlyList<ContainerFormat> GetContainersFormatsInternal()
|
|||
FFMpegHelper.RootExceptionCheck();
|
||||
|
||||
var list = new List<ContainerFormat>();
|
||||
using var instance = new Instances.Instance(FFMpegOptions.Options.FFMpegBinary(), "-formats");
|
||||
using var instance = new Instances.Instance(GlobalFFOptions.GetFFMpegBinaryPath(), "-formats");
|
||||
instance.DataReceived += (e, args) =>
|
||||
{
|
||||
if (ContainerFormat.TryParse(args.Data, out var fmt))
|
||||
|
@ -542,14 +542,14 @@ internal static IReadOnlyList<ContainerFormat> GetContainersFormatsInternal()
|
|||
|
||||
public static IReadOnlyList<ContainerFormat> GetContainerFormats()
|
||||
{
|
||||
if (!FFMpegOptions.Options.UseCache)
|
||||
if (!GlobalFFOptions.Current.UseCache)
|
||||
return GetContainersFormatsInternal();
|
||||
return FFMpegCache.ContainerFormats.Values.ToList().AsReadOnly();
|
||||
}
|
||||
|
||||
public static bool TryGetContainerFormat(string name, out ContainerFormat fmt)
|
||||
{
|
||||
if (!FFMpegOptions.Options.UseCache)
|
||||
if (!GlobalFFOptions.Current.UseCache)
|
||||
{
|
||||
fmt = GetContainersFormatsInternal().FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim());
|
||||
return fmt != null;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
namespace FFMpegCore
|
||||
{
|
||||
public class FFMpegArgumentOptions : FFMpegOptionsBase
|
||||
public class FFMpegArgumentOptions : FFMpegArgumentsBase
|
||||
{
|
||||
internal FFMpegArgumentOptions() { }
|
||||
|
||||
|
|
|
@ -50,9 +50,9 @@ public FFMpegArgumentProcessor CancellableThrough(out Action cancel, int timeout
|
|||
cancel = () => CancelEvent?.Invoke(this, timeout);
|
||||
return this;
|
||||
}
|
||||
public bool ProcessSynchronously(bool throwOnError = true)
|
||||
public bool ProcessSynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null)
|
||||
{
|
||||
using var instance = PrepareInstance(out var cancellationTokenSource);
|
||||
using var instance = PrepareInstance(ffMpegOptions ?? GlobalFFOptions.Current, out var cancellationTokenSource);
|
||||
var errorCode = -1;
|
||||
|
||||
void OnCancelEvent(object sender, int timeout)
|
||||
|
@ -90,9 +90,9 @@ void OnCancelEvent(object sender, int timeout)
|
|||
return HandleCompletion(throwOnError, errorCode, instance.ErrorData);
|
||||
}
|
||||
|
||||
public async Task<bool> ProcessAsynchronously(bool throwOnError = true)
|
||||
public async Task<bool> ProcessAsynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null)
|
||||
{
|
||||
using var instance = PrepareInstance(out var cancellationTokenSource);
|
||||
using var instance = PrepareInstance(ffMpegOptions ?? GlobalFFOptions.Current, out var cancellationTokenSource);
|
||||
var errorCode = -1;
|
||||
|
||||
void OnCancelEvent(object sender, int timeout)
|
||||
|
@ -132,7 +132,7 @@ await Task.WhenAll(instance.FinishedRunning().ContinueWith(t =>
|
|||
private bool HandleCompletion(bool throwOnError, int exitCode, IReadOnlyList<string> errorData)
|
||||
{
|
||||
if (throwOnError && exitCode != 0)
|
||||
throw new FFMpegProcessException(exitCode, string.Join("\n", errorData));
|
||||
throw new FFMpegException(FFMpegExceptionType.Process, $"ffmpeg exited with non-zero exit-code ({exitCode} - {string.Join("\n", errorData)})", null, string.Join("\n", errorData));
|
||||
|
||||
_onPercentageProgress?.Invoke(100.0);
|
||||
if (_totalTimespan.HasValue) _onTimeProgress?.Invoke(_totalTimespan.Value);
|
||||
|
@ -140,16 +140,17 @@ private bool HandleCompletion(bool throwOnError, int exitCode, IReadOnlyList<str
|
|||
return exitCode == 0;
|
||||
}
|
||||
|
||||
private Instance PrepareInstance(out CancellationTokenSource cancellationTokenSource)
|
||||
private Instance PrepareInstance(FFOptions ffMpegOptions,
|
||||
out CancellationTokenSource cancellationTokenSource)
|
||||
{
|
||||
FFMpegHelper.RootExceptionCheck();
|
||||
FFMpegHelper.VerifyFFMpegExists();
|
||||
FFMpegHelper.VerifyFFMpegExists(ffMpegOptions);
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = FFMpegOptions.Options.FFMpegBinary(),
|
||||
FileName = GlobalFFOptions.GetFFMpegBinaryPath(ffMpegOptions),
|
||||
Arguments = _ffMpegArguments.Text,
|
||||
StandardOutputEncoding = FFMpegOptions.Options.Encoding,
|
||||
StandardErrorEncoding = FFMpegOptions.Options.Encoding,
|
||||
StandardOutputEncoding = ffMpegOptions.Encoding,
|
||||
StandardErrorEncoding = ffMpegOptions.Encoding,
|
||||
};
|
||||
var instance = new Instance(startInfo);
|
||||
cancellationTokenSource = new CancellationTokenSource();
|
||||
|
|
|
@ -9,13 +9,13 @@
|
|||
|
||||
namespace FFMpegCore
|
||||
{
|
||||
public sealed class FFMpegArguments : FFMpegOptionsBase
|
||||
public sealed class FFMpegArguments : FFMpegArgumentsBase
|
||||
{
|
||||
private readonly FFMpegGlobalOptions _globalOptions = new FFMpegGlobalOptions();
|
||||
private readonly FFMpegGlobalArguments _globalArguments = new FFMpegGlobalArguments();
|
||||
|
||||
private FFMpegArguments() { }
|
||||
|
||||
public string Text => string.Join(" ", _globalOptions.Arguments.Concat(Arguments).Select(arg => arg.Text));
|
||||
public string Text => string.Join(" ", _globalArguments.Arguments.Concat(Arguments).Select(arg => arg.Text));
|
||||
|
||||
public static FFMpegArguments FromConcatInput(IEnumerable<string> filePaths, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new ConcatArgument(filePaths), addArguments);
|
||||
public static FFMpegArguments FromDemuxConcatInput(IEnumerable<string> filePaths, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new DemuxConcatArgument(filePaths), addArguments);
|
||||
|
@ -26,9 +26,9 @@ private FFMpegArguments() { }
|
|||
public static FFMpegArguments FromPipeInput(IPipeSource sourcePipe, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new InputPipeArgument(sourcePipe), addArguments);
|
||||
|
||||
|
||||
public FFMpegArguments WithGlobalOptions(Action<FFMpegGlobalOptions> configureOptions)
|
||||
public FFMpegArguments WithGlobalOptions(Action<FFMpegGlobalArguments> configureOptions)
|
||||
{
|
||||
configureOptions(_globalOptions);
|
||||
configureOptions(_globalArguments);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
namespace FFMpegCore
|
||||
{
|
||||
public abstract class FFMpegOptionsBase
|
||||
public abstract class FFMpegArgumentsBase
|
||||
{
|
||||
internal readonly List<IArgument> Arguments = new List<IArgument>();
|
||||
}
|
18
FFMpegCore/FFMpeg/FFMpegGlobalArguments.cs
Normal file
18
FFMpegCore/FFMpeg/FFMpegGlobalArguments.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
using FFMpegCore.Arguments;
|
||||
|
||||
namespace FFMpegCore
|
||||
{
|
||||
public sealed class FFMpegGlobalArguments : FFMpegArgumentsBase
|
||||
{
|
||||
internal FFMpegGlobalArguments() { }
|
||||
|
||||
public FFMpegGlobalArguments WithVerbosityLevel(VerbosityLevel verbosityLevel = VerbosityLevel.Error) => WithOption(new VerbosityLevelArgument(verbosityLevel));
|
||||
|
||||
private FFMpegGlobalArguments WithOption(IArgument argument)
|
||||
{
|
||||
Arguments.Add(argument);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
using FFMpegCore.Arguments;
|
||||
|
||||
namespace FFMpegCore
|
||||
{
|
||||
public sealed class FFMpegGlobalOptions : FFMpegOptionsBase
|
||||
{
|
||||
internal FFMpegGlobalOptions() { }
|
||||
|
||||
public FFMpegGlobalOptions WithVerbosityLevel(VerbosityLevel verbosityLevel = VerbosityLevel.Error) => WithOption(new VerbosityLevelArgument(verbosityLevel));
|
||||
|
||||
private FFMpegGlobalOptions WithOption(IArgument argument)
|
||||
{
|
||||
Arguments.Add(argument);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace FFMpegCore
|
||||
{
|
||||
public class FFMpegOptions
|
||||
{
|
||||
private static readonly string ConfigFile = "ffmpeg.config.json";
|
||||
private static readonly string DefaultRoot = "";
|
||||
private static readonly string DefaultTemp = Path.GetTempPath();
|
||||
private static readonly Dictionary<string, string> DefaultExtensionsOverrides = new Dictionary<string, string>
|
||||
{
|
||||
{ "mpegts", ".ts" },
|
||||
};
|
||||
|
||||
public static FFMpegOptions Options { get; private set; } = new FFMpegOptions();
|
||||
|
||||
public static void Configure(Action<FFMpegOptions> optionsAction)
|
||||
{
|
||||
optionsAction?.Invoke(Options);
|
||||
}
|
||||
|
||||
public static void Configure(FFMpegOptions options)
|
||||
{
|
||||
Options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
static FFMpegOptions()
|
||||
{
|
||||
if (File.Exists(ConfigFile))
|
||||
{
|
||||
Options = JsonSerializer.Deserialize<FFMpegOptions>(File.ReadAllText(ConfigFile))!;
|
||||
foreach (var pair in DefaultExtensionsOverrides)
|
||||
if (!Options.ExtensionOverrides.ContainsKey(pair.Key)) Options.ExtensionOverrides.Add(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public string RootDirectory { get; set; } = DefaultRoot;
|
||||
public string TempDirectory { get; set; } = DefaultTemp;
|
||||
public bool UseCache { get; set; } = true;
|
||||
public Encoding Encoding { get; set; } = Encoding.Default;
|
||||
|
||||
public string FFMpegBinary() => FFBinary("FFMpeg");
|
||||
|
||||
public string FFProbeBinary() => FFBinary("FFProbe");
|
||||
|
||||
public Dictionary<string, string> ExtensionOverrides { get; private set; } = new Dictionary<string, string>();
|
||||
|
||||
private static string FFBinary(string name)
|
||||
{
|
||||
var ffName = name.ToLowerInvariant();
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
ffName += ".exe";
|
||||
|
||||
var target = Environment.Is64BitProcess ? "x64" : "x86";
|
||||
if (Directory.Exists(Path.Combine(Options.RootDirectory, target)))
|
||||
ffName = Path.Combine(target, ffName);
|
||||
|
||||
return Path.Combine(Options.RootDirectory, ffName);
|
||||
}
|
||||
}
|
||||
}
|
37
FFMpegCore/FFOptions.cs
Normal file
37
FFMpegCore/FFOptions.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace FFMpegCore
|
||||
{
|
||||
public class FFOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Folder container ffmpeg and ffprobe binaries. Leave empty if ffmpeg and ffprobe are present in PATH
|
||||
/// </summary>
|
||||
public string BinaryFolder { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Folder used for temporary files necessary for static methods on FFMpeg class
|
||||
/// </summary>
|
||||
public string TemporaryFilesFolder { get; set; } = Path.GetTempPath();
|
||||
|
||||
/// <summary>
|
||||
/// Encoding used for parsing stdout/stderr on ffmpeg and ffprobe processes
|
||||
/// </summary>
|
||||
public Encoding Encoding { get; set; } = Encoding.Default;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public Dictionary<string, string> ExtensionOverrides { get; set; } = new Dictionary<string, string>
|
||||
{
|
||||
{ "mpegts", ".ts" },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Whether to cache calls to get ffmpeg codec, pixel- and container-formats
|
||||
/// </summary>
|
||||
public bool UseCache { get; set; } = true;
|
||||
}
|
||||
}
|
|
@ -12,32 +12,32 @@ namespace FFMpegCore
|
|||
{
|
||||
public static class FFProbe
|
||||
{
|
||||
public static IMediaAnalysis Analyse(string filePath, int outputCapacity = int.MaxValue)
|
||||
public static IMediaAnalysis Analyse(string filePath, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'");
|
||||
|
||||
using var instance = PrepareInstance(filePath, outputCapacity);
|
||||
using var instance = PrepareInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
|
||||
var exitCode = instance.BlockUntilFinished();
|
||||
if (exitCode != 0)
|
||||
throw new FFProbeProcessException(exitCode, string.Join("\n", instance.ErrorData));
|
||||
throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData));
|
||||
|
||||
return ParseOutput(instance);
|
||||
}
|
||||
public static IMediaAnalysis Analyse(Uri uri, int outputCapacity = int.MaxValue)
|
||||
public static IMediaAnalysis Analyse(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
|
||||
{
|
||||
using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity);
|
||||
using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
|
||||
var exitCode = instance.BlockUntilFinished();
|
||||
if (exitCode != 0)
|
||||
throw new FFProbeProcessException(exitCode, string.Join("\n", instance.ErrorData));
|
||||
throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData));
|
||||
|
||||
return ParseOutput(instance);
|
||||
}
|
||||
public static IMediaAnalysis Analyse(Stream stream, int outputCapacity = int.MaxValue)
|
||||
public static IMediaAnalysis Analyse(Stream stream, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
|
||||
{
|
||||
var streamPipeSource = new StreamPipeSource(stream);
|
||||
var pipeArgument = new InputPipeArgument(streamPipeSource);
|
||||
using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity);
|
||||
using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
|
||||
pipeArgument.Pre();
|
||||
|
||||
var task = instance.FinishedRunning();
|
||||
|
@ -52,30 +52,30 @@ public static IMediaAnalysis Analyse(Stream stream, int outputCapacity = int.Max
|
|||
}
|
||||
var exitCode = task.ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
if (exitCode != 0)
|
||||
throw new FFProbeProcessException(exitCode, string.Join("\n", instance.ErrorData));
|
||||
throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData));
|
||||
|
||||
return ParseOutput(instance);
|
||||
}
|
||||
public static async Task<IMediaAnalysis> AnalyseAsync(string filePath, int outputCapacity = int.MaxValue)
|
||||
public static async Task<IMediaAnalysis> AnalyseAsync(string filePath, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'");
|
||||
|
||||
using var instance = PrepareInstance(filePath, outputCapacity);
|
||||
using var instance = PrepareInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
|
||||
await instance.FinishedRunning().ConfigureAwait(false);
|
||||
return ParseOutput(instance);
|
||||
}
|
||||
public static async Task<IMediaAnalysis> AnalyseAsync(Uri uri, int outputCapacity = int.MaxValue)
|
||||
public static async Task<IMediaAnalysis> AnalyseAsync(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
|
||||
{
|
||||
using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity);
|
||||
using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
|
||||
await instance.FinishedRunning().ConfigureAwait(false);
|
||||
return ParseOutput(instance);
|
||||
}
|
||||
public static async Task<IMediaAnalysis> AnalyseAsync(Stream stream, int outputCapacity = int.MaxValue)
|
||||
public static async Task<IMediaAnalysis> AnalyseAsync(Stream stream, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
|
||||
{
|
||||
var streamPipeSource = new StreamPipeSource(stream);
|
||||
var pipeArgument = new InputPipeArgument(streamPipeSource);
|
||||
using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity);
|
||||
using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
|
||||
pipeArgument.Pre();
|
||||
|
||||
var task = instance.FinishedRunning();
|
||||
|
@ -112,12 +112,12 @@ private static IMediaAnalysis ParseOutput(Instance instance)
|
|||
return new MediaAnalysis(ffprobeAnalysis);
|
||||
}
|
||||
|
||||
private static Instance PrepareInstance(string filePath, int outputCapacity)
|
||||
private static Instance PrepareInstance(string filePath, int outputCapacity, FFOptions ffOptions)
|
||||
{
|
||||
FFProbeHelper.RootExceptionCheck();
|
||||
FFProbeHelper.VerifyFFProbeExists();
|
||||
FFProbeHelper.VerifyFFProbeExists(ffOptions);
|
||||
var arguments = $"-loglevel error -print_format json -show_format -sexagesimal -show_streams \"{filePath}\"";
|
||||
var instance = new Instance(FFMpegOptions.Options.FFProbeBinary(), arguments) {DataBufferCapacity = outputCapacity};
|
||||
var instance = new Instance(GlobalFFOptions.GetFFProbeBinaryPath(), arguments) {DataBufferCapacity = outputCapacity};
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
|
52
FFMpegCore/GlobalFFOptions.cs
Normal file
52
FFMpegCore/GlobalFFOptions.cs
Normal file
|
@ -0,0 +1,52 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace FFMpegCore
|
||||
{
|
||||
public static class GlobalFFOptions
|
||||
{
|
||||
private static readonly string ConfigFile = "ffmpeg.config.json";
|
||||
|
||||
public static FFOptions Current { get; private set; }
|
||||
static GlobalFFOptions()
|
||||
{
|
||||
if (File.Exists(ConfigFile))
|
||||
{
|
||||
Current = JsonSerializer.Deserialize<FFOptions>(File.ReadAllText(ConfigFile))!;
|
||||
}
|
||||
else
|
||||
{
|
||||
Current = new FFOptions();
|
||||
}
|
||||
}
|
||||
|
||||
public static void Configure(Action<FFOptions> optionsAction)
|
||||
{
|
||||
optionsAction?.Invoke(Current);
|
||||
}
|
||||
public static void Configure(FFOptions ffOptions)
|
||||
{
|
||||
Current = ffOptions ?? throw new ArgumentNullException(nameof(ffOptions));
|
||||
}
|
||||
|
||||
|
||||
public static string GetFFMpegBinaryPath(FFOptions? ffOptions = null) => GetFFBinaryPath("FFMpeg", ffOptions ?? Current);
|
||||
|
||||
public static string GetFFProbeBinaryPath(FFOptions? ffOptions = null) => GetFFBinaryPath("FFProbe", ffOptions ?? Current);
|
||||
|
||||
private static string GetFFBinaryPath(string name, FFOptions ffOptions)
|
||||
{
|
||||
var ffName = name.ToLowerInvariant();
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
ffName += ".exe";
|
||||
|
||||
var target = Environment.Is64BitProcess ? "x64" : "x86";
|
||||
if (Directory.Exists(Path.Combine(ffOptions.BinaryFolder, target)))
|
||||
ffName = Path.Combine(target, ffName);
|
||||
|
||||
return Path.Combine(ffOptions.BinaryFolder, ffName);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -31,17 +31,17 @@ public static void ExtensionExceptionCheck(string filename, string extension)
|
|||
|
||||
public static void RootExceptionCheck()
|
||||
{
|
||||
if (FFMpegOptions.Options.RootDirectory == null)
|
||||
throw new FFMpegException(FFMpegExceptionType.Dependency,
|
||||
"FFMpeg root is not configured in app config. Missing key 'ffmpegRoot'.");
|
||||
if (GlobalFFOptions.Current.BinaryFolder == null)
|
||||
throw new FFOptionsException("FFMpeg root is not configured in app config. Missing key 'BinaryFolder'.");
|
||||
}
|
||||
|
||||
public static void VerifyFFMpegExists()
|
||||
public static void VerifyFFMpegExists(FFOptions ffMpegOptions)
|
||||
{
|
||||
if (_ffmpegVerified) return;
|
||||
var (exitCode, _) = Instance.Finish(FFMpegOptions.Options.FFMpegBinary(), "-version");
|
||||
var (exitCode, _) = Instance.Finish(GlobalFFOptions.GetFFMpegBinaryPath(ffMpegOptions), "-version");
|
||||
_ffmpegVerified = exitCode == 0;
|
||||
if (!_ffmpegVerified) throw new FFMpegException(FFMpegExceptionType.Operation, "ffmpeg was not found on your system");
|
||||
if (!_ffmpegVerified)
|
||||
throw new FFMpegException(FFMpegExceptionType.Operation, "ffmpeg was not found on your system");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,18 +20,17 @@ public static int Gcd(int first, int second)
|
|||
|
||||
public static void RootExceptionCheck()
|
||||
{
|
||||
if (FFMpegOptions.Options.RootDirectory == null)
|
||||
throw new FFMpegException(FFMpegExceptionType.Dependency,
|
||||
"FFProbe root is not configured in app config. Missing key 'ffmpegRoot'.");
|
||||
|
||||
if (GlobalFFOptions.Current.BinaryFolder == null)
|
||||
throw new FFOptionsException("FFProbe root is not configured in app config. Missing key 'BinaryFolder'.");
|
||||
}
|
||||
|
||||
public static void VerifyFFProbeExists()
|
||||
public static void VerifyFFProbeExists(FFOptions ffMpegOptions)
|
||||
{
|
||||
if (_ffprobeVerified) return;
|
||||
var (exitCode, _) = Instance.Finish(FFMpegOptions.Options.FFProbeBinary(), "-version");
|
||||
var (exitCode, _) = Instance.Finish(GlobalFFOptions.GetFFProbeBinaryPath(ffMpegOptions), "-version");
|
||||
_ffprobeVerified = exitCode == 0;
|
||||
if (!_ffprobeVerified) throw new FFMpegException(FFMpegExceptionType.Operation, "ffprobe was not found on your system");
|
||||
if (!_ffprobeVerified)
|
||||
throw new FFMpegException(FFMpegExceptionType.Operation, "ffprobe was not found on your system");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue