diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs index 082d9bf..54620a5 100644 --- a/FFMpegCore.Test/ArgumentBuilderTest.cs +++ b/FFMpegCore.Test/ArgumentBuilderTest.cs @@ -341,7 +341,22 @@ public void Builder_BuildString_SubtitleHardBurnFilter() .WithParameter("PrimaryColour", "&HAA00FF00"))))) .Arguments; - Assert.AreEqual("-i \"input.mp4\" -vf \"subtitles=sample.srt:charenc=UTF-8:original_size=1366x768:stream_index=0:force_style='FontName=DejaVu Serif\\,PrimaryColour=&HAA00FF00'\" \"output.mp4\"", + Assert.AreEqual("-i \"input.mp4\" -vf \"subtitles='sample.srt':charenc=UTF-8:original_size=1366x768:stream_index=0:force_style='FontName=DejaVu Serif\\,PrimaryColour=&HAA00FF00'\" \"output.mp4\"", + str); + } + + [TestMethod] + public void Builder_BuildString_SubtitleHardBurnFilterFixedPaths() + { + var str = FFMpegArguments + .FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt + .WithVideoFilters(filterOptions => filterOptions + .HardBurnSubtitle(SubtitleHardBurnOptions + .Create(subtitlePath: @"sample( \ : [ ] , ' ).srt")))) + .Arguments; + + Assert.AreEqual(@"-i ""input.mp4"" -vf ""subtitles='sample( \\ \: \[ \] \, '\\\'' ).srt'"" ""output.mp4""", str); } @@ -414,5 +429,60 @@ public void Builder_BuildString_ForcePixelFormat() .OutputToFile("output.mp4", false, opt => opt.ForcePixelFormat("yuv444p")).Arguments; Assert.AreEqual("-i \"input.mp4\" -pix_fmt yuv444p \"output.mp4\"", str); } + + [TestMethod] + public void Builder_BuildString_PanAudioFilterChannelNumber() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, + opt => opt.WithAudioFilters(filterOptions => filterOptions.Pan(2, "c0=c1", "c1=c1"))) + .Arguments; + + Assert.AreEqual("-i \"input.mp4\" -af \"pan=2c|c0=c1|c1=c1\" \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_PanAudioFilterChannelLayout() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, + opt => opt.WithAudioFilters(filterOptions => filterOptions.Pan("stereo", "c0=c0", "c1=c1"))) + .Arguments; + + Assert.AreEqual("-i \"input.mp4\" -af \"pan=stereo|c0=c0|c1=c1\" \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_PanAudioFilterChannelNoOutputDefinition() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, + opt => opt.WithAudioFilters(filterOptions => filterOptions.Pan("stereo"))) + .Arguments; + + Assert.AreEqual("-i \"input.mp4\" -af \"pan=stereo\" \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_DynamicAudioNormalizerDefaultFormat() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, + opt => opt.WithAudioFilters(filterOptions => filterOptions.DynamicNormalizer())) + .Arguments; + + Assert.AreEqual("-i \"input.mp4\" -af \"dynaudnorm=f=500:g=31:p=0.95:m=10.0:r=0.0:n=1:c=0:b=0:s=0.0\" \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_DynamicAudioNormalizerWithValuesFormat() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, + opt => opt.WithAudioFilters(filterOptions => filterOptions.DynamicNormalizer(125, 13, 0.9215, 5.124, 0.5458,false,true,true, 0.3333333))) + .Arguments; + + Assert.AreEqual("-i \"input.mp4\" -af \"dynaudnorm=f=125:g=13:p=0.92:m=5.1:r=0.5:n=0:c=1:b=1:s=0.3\" \"output.mp4\"", str); + } } } \ No newline at end of file diff --git a/FFMpegCore.Test/AudioTest.cs b/FFMpegCore.Test/AudioTest.cs index b6fde77..795fedf 100644 --- a/FFMpegCore.Test/AudioTest.cs +++ b/FFMpegCore.Test/AudioTest.cs @@ -1,5 +1,6 @@ using FFMpegCore.Enums; using FFMpegCore.Exceptions; +using FFMpegCore.Extend; using FFMpegCore.Pipes; using FFMpegCore.Test.Resources; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -8,7 +9,6 @@ using System.IO; using System.Linq; using System.Threading.Tasks; -using FFMpegCore.Extend; namespace FFMpegCore.Test { @@ -223,5 +223,111 @@ public void Audio_ToAAC_Args_Pipe_InvalidSampleRate() .WithAudioCodec(AudioCodec.Aac)) .ProcessSynchronously()); } + + [TestMethod, Timeout(10000)] + public void Audio_Pan_ToMono() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var success = FFMpegArguments.FromFileInput(TestResources.Mp3Audio) + .OutputToFile(outputFile, true, + argumentOptions => argumentOptions + .WithAudioFilters(filter => filter.Pan(1, "c0 < 0.9 * c0 + 0.1 * c1"))) + .ProcessSynchronously(); + + var mediaAnalysis = FFProbe.Analyse(outputFile); + + Assert.IsTrue(success); + Assert.AreEqual(1, mediaAnalysis.AudioStreams.Count); + Assert.AreEqual("mono", mediaAnalysis.PrimaryAudioStream.ChannelLayout); + } + + [TestMethod, Timeout(10000)] + public void Audio_Pan_ToMonoNoDefinitions() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var success = FFMpegArguments.FromFileInput(TestResources.Mp3Audio) + .OutputToFile(outputFile, true, + argumentOptions => argumentOptions + .WithAudioFilters(filter => filter.Pan(1))) + .ProcessSynchronously(); + + var mediaAnalysis = FFProbe.Analyse(outputFile); + + Assert.IsTrue(success); + Assert.AreEqual(1, mediaAnalysis.AudioStreams.Count); + Assert.AreEqual("mono", mediaAnalysis.PrimaryAudioStream.ChannelLayout); + } + + [TestMethod, Timeout(10000)] + public void Audio_Pan_ToMonoChannelsToOutputDefinitionsMismatch() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var ex = Assert.ThrowsException(() => FFMpegArguments.FromFileInput(TestResources.Mp3Audio) + .OutputToFile(outputFile, true, + argumentOptions => argumentOptions + .WithAudioFilters(filter => filter.Pan(1, "c0=c0", "c1=c1"))) + .ProcessSynchronously()); + } + + [TestMethod, Timeout(10000)] + public void Audio_Pan_ToMonoChannelsLayoutToOutputDefinitionsMismatch() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var ex = Assert.ThrowsException(() => FFMpegArguments.FromFileInput(TestResources.Mp3Audio) + .OutputToFile(outputFile, true, + argumentOptions => argumentOptions + .WithAudioFilters(filter => filter.Pan("mono", "c0=c0", "c1=c1"))) + .ProcessSynchronously()); + } + + [TestMethod, Timeout(10000)] + public void Audio_DynamicNormalizer_WithDefaultValues() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var success = FFMpegArguments.FromFileInput(TestResources.Mp3Audio) + .OutputToFile(outputFile, true, + argumentOptions => argumentOptions + .WithAudioFilters(filter => filter.DynamicNormalizer())) + .ProcessSynchronously(); + + Assert.IsTrue(success); + } + + [TestMethod, Timeout(10000)] + public void Audio_DynamicNormalizer_WithNonDefaultValues() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var success = FFMpegArguments.FromFileInput(TestResources.Mp3Audio) + .OutputToFile(outputFile, true, + argumentOptions => argumentOptions + .WithAudioFilters( + filter => filter.DynamicNormalizer(250, 7, 0.9, 2, 1, false, true, true, 0.5))) + .ProcessSynchronously(); + + Assert.IsTrue(success); + } + + [DataTestMethod, Timeout(10000)] + [DataRow(2)] + [DataRow(32)] + [DataRow(8)] + public void Audio_DynamicNormalizer_FilterWindow(int filterWindow) + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var ex = Assert.ThrowsException(() => FFMpegArguments + .FromFileInput(TestResources.Mp3Audio) + .OutputToFile(outputFile, true, + argumentOptions => argumentOptions + .WithAudioFilters( + filter => filter.DynamicNormalizer(filterWindow: filterWindow))) + .ProcessSynchronously()); + } } } \ No newline at end of file diff --git a/FFMpegCore.Test/FFMpegCore.Test.csproj b/FFMpegCore.Test/FFMpegCore.Test.csproj index 2505545..e6831e6 100644 --- a/FFMpegCore.Test/FFMpegCore.Test.csproj +++ b/FFMpegCore.Test/FFMpegCore.Test.csproj @@ -40,9 +40,9 @@ - - - + + + diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs index 7af92cd..f990c7f 100644 --- a/FFMpegCore.Test/FFProbeTests.cs +++ b/FFMpegCore.Test/FFProbeTests.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; using System.Threading.Tasks; using FFMpegCore.Test.Resources; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -25,6 +26,30 @@ public async Task Audio_FromStream_Duration() Assert.IsTrue(fileAnalysis.Duration == streamAnalysis.Duration); } + [TestMethod] + public void FrameAnalysis_Sync() + { + var frameAnalysis = FFProbe.GetFrames(TestResources.WebmVideo); + + Assert.AreEqual(90, frameAnalysis.Frames.Count); + Assert.IsTrue(frameAnalysis.Frames.All(f => f.PixelFormat == "yuv420p")); + Assert.IsTrue(frameAnalysis.Frames.All(f => f.Height == 360)); + Assert.IsTrue(frameAnalysis.Frames.All(f => f.Width == 640)); + Assert.IsTrue(frameAnalysis.Frames.All(f => f.MediaType == "video")); + } + + [TestMethod] + public async Task FrameAnalysis_Async() + { + var frameAnalysis = await FFProbe.GetFramesAsync(TestResources.WebmVideo); + + Assert.AreEqual(90, frameAnalysis.Frames.Count); + Assert.IsTrue(frameAnalysis.Frames.All(f => f.PixelFormat == "yuv420p")); + Assert.IsTrue(frameAnalysis.Frames.All(f => f.Height == 360)); + Assert.IsTrue(frameAnalysis.Frames.All(f => f.Width == 640)); + Assert.IsTrue(frameAnalysis.Frames.All(f => f.MediaType == "video")); + } + [DataTestMethod] [DataRow("0:00:03.008000", 0, 0, 0, 3, 8)] [DataRow("05:12:59.177", 0, 5, 12, 59, 177)] @@ -114,5 +139,15 @@ public async Task Probe_Success_Subtitle_Async() Assert.AreEqual(0, info.AudioStreams.Count); Assert.AreEqual(0, info.VideoStreams.Count); } + + [TestMethod, Timeout(10000)] + public async Task Probe_Success_Disposition_Async() + { + var info = await FFProbe.AnalyseAsync(TestResources.Mp4Video); + Assert.IsNotNull(info.PrimaryAudioStream); + Assert.IsNotNull(info.PrimaryAudioStream.Disposition); + Assert.AreEqual(true, info.PrimaryAudioStream.Disposition["default"]); + Assert.AreEqual(false, info.PrimaryAudioStream.Disposition["forced"]); + } } } \ No newline at end of file diff --git a/FFMpegCore/Extend/BitmapVideoFrameWrapper.cs b/FFMpegCore/Extend/BitmapVideoFrameWrapper.cs index 678bdcb..2222db6 100644 --- a/FFMpegCore/Extend/BitmapVideoFrameWrapper.cs +++ b/FFMpegCore/Extend/BitmapVideoFrameWrapper.cs @@ -49,7 +49,7 @@ public async Task SerializeAsync(Stream stream, CancellationToken token) { var buffer = new byte[data.Stride * data.Height]; Marshal.Copy(data.Scan0, buffer, 0, buffer.Length); - await stream.WriteAsync(buffer, 0, buffer.Length, token); + await stream.WriteAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false); } finally { diff --git a/FFMpegCore/Extend/KeyValuePairExtensions.cs b/FFMpegCore/Extend/KeyValuePairExtensions.cs index 28cc087..c2c6813 100644 --- a/FFMpegCore/Extend/KeyValuePairExtensions.cs +++ b/FFMpegCore/Extend/KeyValuePairExtensions.cs @@ -16,7 +16,7 @@ internal static class KeyValuePairExtensions public static string FormatArgumentPair(this KeyValuePair pair, bool enclose) { var key = pair.Key; - var value = enclose ? pair.Value.EncloseIfContainsSpace() : pair.Value; + var value = enclose ? StringExtensions.EncloseIfContainsSpace(pair.Value) : pair.Value; return $"{key}={value}"; } diff --git a/FFMpegCore/Extend/PcmAudioSampleWrapper.cs b/FFMpegCore/Extend/PcmAudioSampleWrapper.cs index 503a23f..d6c1d2f 100644 --- a/FFMpegCore/Extend/PcmAudioSampleWrapper.cs +++ b/FFMpegCore/Extend/PcmAudioSampleWrapper.cs @@ -24,7 +24,7 @@ public void Serialize(Stream stream) public async Task SerializeAsync(Stream stream, CancellationToken token) { - await stream.WriteAsync(_sample, 0, _sample.Length, token); + await stream.WriteAsync(_sample, 0, _sample.Length, token).ConfigureAwait(false); } } } diff --git a/FFMpegCore/Extend/StringExtensions.cs b/FFMpegCore/Extend/StringExtensions.cs index ddcf54b..29c8d42 100644 --- a/FFMpegCore/Extend/StringExtensions.cs +++ b/FFMpegCore/Extend/StringExtensions.cs @@ -1,15 +1,70 @@ -namespace FFMpegCore.Extend +using System.Collections.Generic; +using System.Text; + +namespace FFMpegCore.Extend { internal static class StringExtensions { + private static Dictionary CharactersSubstitution { get; } = new Dictionary + { + { '\\', @"\\" }, + { ':', @"\:" }, + { '[', @"\[" }, + { ']', @"\]" }, + { '\'', @"'\\\''" } + }; + /// /// Enclose string between quotes if contains an space character /// /// The input /// The enclosed string - public static string EncloseIfContainsSpace(this string input) + public static string EncloseIfContainsSpace(string input) { return input.Contains(" ") ? $"'{input}'" : input; } + + /// + /// Enclose an string in quotes + /// + /// + /// + public static string EncloseInQuotes(string input) + { + return $"'{input}'"; + } + + /// + /// Scape several characters in subtitle path used by FFmpeg + /// + /// + /// This is needed because internally FFmpeg use Libav Filters + /// and the info send to it must be in an specific format + /// + /// + /// Scaped path + public static string ToFFmpegLibavfilterPath(string source) + { + return source.Replace(CharactersSubstitution); + } + + public static string Replace(this string str, Dictionary replaceList) + { + var parsedString = new StringBuilder(); + + foreach (var l in str) + { + if (replaceList.ContainsKey(l)) + { + parsedString.Append(replaceList[l]); + } + else + { + parsedString.Append(l); + } + } + + return parsedString.ToString(); + } } } \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/AudioFiltersArgument.cs b/FFMpegCore/FFMpeg/Arguments/AudioFiltersArgument.cs new file mode 100644 index 0000000..50b26b3 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/AudioFiltersArgument.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Linq; +using FFMpegCore.Exceptions; + +namespace FFMpegCore.Arguments +{ + public class AudioFiltersArgument : IArgument + { + public readonly AudioFilterOptions Options; + + public AudioFiltersArgument(AudioFilterOptions options) + { + Options = options; + } + + public string Text => GetText(); + + private string GetText() + { + if (!Options.Arguments.Any()) + throw new FFMpegArgumentException("No audio-filter arguments provided"); + + var arguments = Options.Arguments + .Where(arg => !string.IsNullOrEmpty(arg.Value)) + .Select(arg => + { + var escapedValue = arg.Value.Replace(",", "\\,"); + return string.IsNullOrEmpty(arg.Key) ? escapedValue : $"{arg.Key}={escapedValue}"; + }); + + return $"-af \"{string.Join(", ", arguments)}\""; + } + } + + public interface IAudioFilterArgument + { + public string Key { get; } + public string Value { get; } + } + + public class AudioFilterOptions + { + public List Arguments { get; } = new List(); + + public AudioFilterOptions Pan(string channelLayout, params string[] outputDefinitions) => WithArgument(new PanArgument(channelLayout, outputDefinitions)); + public AudioFilterOptions Pan(int channels, params string[] outputDefinitions) => WithArgument(new PanArgument(channels, outputDefinitions)); + public AudioFilterOptions DynamicNormalizer(int frameLength = 500, int filterWindow = 31, double targetPeak = 0.95, + double gainFactor = 10.0, double targetRms = 0.0, bool channelCoupling = true, + bool enableDcBiasCorrection = false, bool enableAlternativeBoundary = false, + double compressorFactor = 0.0) => WithArgument(new DynamicNormalizerArgument(frameLength, filterWindow, + targetPeak, gainFactor, targetRms, channelCoupling, enableDcBiasCorrection, enableAlternativeBoundary, + compressorFactor)); + + private AudioFilterOptions WithArgument(IAudioFilterArgument argument) + { + Arguments.Add(argument); + return this; + } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/DynamicNormalizerArgument.cs b/FFMpegCore/FFMpeg/Arguments/DynamicNormalizerArgument.cs new file mode 100644 index 0000000..d1c948c --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/DynamicNormalizerArgument.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace FFMpegCore.Arguments +{ + public class DynamicNormalizerArgument : IAudioFilterArgument + { + private readonly Dictionary _arguments = new Dictionary(); + + /// + /// Dynamic Audio Normalizer. + /// + /// Set the frame length in milliseconds. Must be between 10 to 8000. The default value is 500 + /// Set the Gaussian filter window size. In range from 3 to 301, must be odd number. The default value is 31 + /// Set the target peak value. The default value is 0.95 + /// Set the maximum gain factor. In range from 1.0 to 100.0. Default is 10.0. + /// Set the target RMS. In range from 0.0 to 1.0. Default to 0.0 (disabled) + /// Enable channels coupling. By default is enabled. + /// Enable DC bias correction. By default is disabled. + /// Enable alternative boundary mode. By default is disabled. + /// Set the compress factor. In range from 0.0 to 30.0. Default is 0.0 (disabled). + public DynamicNormalizerArgument(int frameLength = 500, int filterWindow = 31, double targetPeak = 0.95, double gainFactor = 10.0, double targetRms = 0.0, bool channelCoupling = true, bool enableDcBiasCorrection = false, bool enableAlternativeBoundary = false, double compressorFactor = 0.0) + { + if (frameLength < 10 || frameLength > 8000) throw new ArgumentOutOfRangeException(nameof(frameLength),"Frame length must be between 10 to 8000"); + if (filterWindow < 3 || filterWindow > 31) throw new ArgumentOutOfRangeException(nameof(filterWindow), "Gaussian filter window size must be between 3 to 31"); + if (filterWindow % 2 == 0) throw new ArgumentOutOfRangeException(nameof(filterWindow), "Gaussian filter window size must be an odd number"); + if (targetPeak <= 0 || targetPeak > 1) throw new ArgumentOutOfRangeException(nameof(targetPeak)); + if (gainFactor < 1 || gainFactor > 100) throw new ArgumentOutOfRangeException(nameof(gainFactor), "Gain factor must be between 1.0 to 100.0"); + if (targetRms < 0 || targetRms > 1) throw new ArgumentOutOfRangeException(nameof(targetRms), "Target RMS must be between 0.0 and 1.0"); + if (compressorFactor < 0 || compressorFactor > 30) throw new ArgumentOutOfRangeException(nameof(compressorFactor), "Compressor factor must be between 0.0 and 30.0"); + + _arguments.Add("f", frameLength.ToString()); + _arguments.Add("g", filterWindow.ToString()); + _arguments.Add("p", targetPeak.ToString("0.00", CultureInfo.InvariantCulture)); + _arguments.Add("m", gainFactor.ToString("0.0", CultureInfo.InvariantCulture)); + _arguments.Add("r", targetRms.ToString("0.0", CultureInfo.InvariantCulture)); + _arguments.Add("n", (channelCoupling ? 1 : 0).ToString()); + _arguments.Add("c", (enableDcBiasCorrection ? 1 : 0).ToString()); + _arguments.Add("b", (enableAlternativeBoundary ? 1 : 0).ToString()); + _arguments.Add("s", compressorFactor.ToString("0.0", CultureInfo.InvariantCulture)); + } + + public string Key { get; } = "dynaudnorm"; + + public string Value => string.Join(":", _arguments.Select(pair => $"{pair.Key}={pair.Value}")); + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/PanArgument.cs b/FFMpegCore/FFMpeg/Arguments/PanArgument.cs new file mode 100644 index 0000000..013fbf6 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/PanArgument.cs @@ -0,0 +1,59 @@ +using System; +using System.Linq; + +namespace FFMpegCore.Arguments +{ + /// + /// Mix channels with specific gain levels. + /// + public class PanArgument : IAudioFilterArgument + { + public readonly string ChannelLayout; + private readonly string[] _outputDefinitions; + + /// + /// Mix channels with specific gain levels + /// + /// + /// Represent the output channel layout. Like "stereo", "mono", "2.1", "5.1" + /// + /// + /// Output channel specification, of the form: "out_name=[gain*]in_name[(+-)[gain*]in_name...]" + /// + public PanArgument(string channelLayout, params string[] outputDefinitions) + { + if (string.IsNullOrWhiteSpace(channelLayout)) + { + throw new ArgumentException("The channel layout must be set" ,nameof(channelLayout)); + } + + ChannelLayout = channelLayout; + + _outputDefinitions = outputDefinitions; + } + + /// + /// Mix channels with specific gain levels + /// + /// Number of channels in output file + /// + /// Output channel specification, of the form: "out_name=[gain*]in_name[(+-)[gain*]in_name...]" + /// + public PanArgument(int channels, params string[] outputDefinitions) + { + if (channels <= 0) throw new ArgumentOutOfRangeException(nameof(channels)); + + if (outputDefinitions.Length > channels) + throw new ArgumentException("The number of output definitions must be equal or lower than number of channels", nameof(outputDefinitions)); + + ChannelLayout = $"{channels}c"; + + _outputDefinitions = outputDefinitions; + } + + public string Key { get; } = "pan"; + + public string Value => + string.Join("|", Enumerable.Empty().Append(ChannelLayout).Concat(_outputDefinitions)); + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs b/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs index fcb944a..c25df04 100644 --- a/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs @@ -40,7 +40,7 @@ public async Task During(CancellationToken cancellationToken = default) { try { - await ProcessDataAsync(cancellationToken); + await ProcessDataAsync(cancellationToken).ConfigureAwait(false); } catch (TaskCanceledException) { diff --git a/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs b/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs index a48f845..2acd7ca 100644 --- a/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs @@ -103,7 +103,9 @@ public SubtitleHardBurnOptions WithParameter(string key, string value) return this; } - internal string TextInternal => string.Join(":", new[] { _subtitle.EncloseIfContainsSpace() }.Concat(Parameters.Select(parameter => parameter.FormatArgumentPair(enclose: true)))); + internal string TextInternal => string + .Join(":", new[] { StringExtensions.EncloseInQuotes(StringExtensions.ToFFmpegLibavfilterPath(_subtitle)) } + .Concat(Parameters.Select(parameter => parameter.FormatArgumentPair(enclose: true)))); } public class StyleOptions diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index a345160..6f0ede3 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -50,7 +50,7 @@ public static async Task SnapshotAsync(string input, string output, Size? if (Path.GetExtension(output) != FileExtension.Png) output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png; - var source = await FFProbe.AnalyseAsync(input); + var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false); var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); return await arguments @@ -93,7 +93,7 @@ public static Bitmap Snapshot(string input, Size? size = null, TimeSpan? capture /// Bitmap with the requested snapshot. public static async Task SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) { - var source = await FFProbe.AnalyseAsync(input); + var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false); var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); using var ms = new MemoryStream(); @@ -116,7 +116,9 @@ private static (FFMpegArguments, Action outputOptions) Bu { captureTime ??= TimeSpan.FromSeconds(source.Duration.TotalSeconds / 3); size = PrepareSnapshotSize(source, size); - streamIndex = streamIndex == null ? 0 : source.VideoStreams.FirstOrDefault(videoStream => videoStream.Index == streamIndex).Index; + streamIndex ??= source.PrimaryVideoStream?.Index + ?? source.VideoStreams.FirstOrDefault()?.Index + ?? 0; return (FFMpegArguments .FromFileInput(input, false, options => options @@ -301,12 +303,13 @@ public static bool Join(string output, params string[] videos) public static bool JoinImageSequence(string output, double frameRate = 30, params ImageInfo[] images) { var tempFolderName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, Guid.NewGuid().ToString()); - var temporaryImageFiles = images.Select((image, index) => + var temporaryImageFiles = images.Select((imageInfo, index) => { - FFMpegHelper.ConversionSizeExceptionCheck(Image.FromFile(image.FullName)); - var destinationPath = Path.Combine(tempFolderName, $"{index.ToString().PadLeft(9, '0')}{image.Extension}"); + using var image = Image.FromFile(imageInfo.FullName); + FFMpegHelper.ConversionSizeExceptionCheck(image); + var destinationPath = Path.Combine(tempFolderName, $"{index.ToString().PadLeft(9, '0')}{imageInfo.Extension}"); Directory.CreateDirectory(tempFolderName); - File.Copy(image.FullName, destinationPath); + File.Copy(imageInfo.FullName, destinationPath); return destinationPath; }).ToArray(); diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs index 41ac38c..ca6628a 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs @@ -43,6 +43,13 @@ public FFMpegArgumentOptions WithVideoFilters(Action videoFi return WithArgument(new VideoFiltersArgument(videoFilterOptionsObj)); } + public FFMpegArgumentOptions WithAudioFilters(Action audioFilterOptions) + { + var audioFilterOptionsObj = new AudioFilterOptions(); + audioFilterOptions(audioFilterOptionsObj); + return WithArgument(new AudioFiltersArgument(audioFilterOptionsObj)); + } + public FFMpegArgumentOptions WithFramerate(double framerate) => WithArgument(new FrameRateArgument(framerate)); public FFMpegArgumentOptions WithoutMetadata() => WithArgument(new RemoveMetadataArgument()); public FFMpegArgumentOptions WithSpeedPreset(Speed speed) => WithArgument(new SpeedPresetArgument(speed)); diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs index 060ffc3..97ea94f 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs @@ -75,13 +75,7 @@ void OnCancelEvent(object sender, int timeout) try { - _ffMpegArguments.Pre(); - Task.WaitAll(instance.FinishedRunning().ContinueWith(t => - { - errorCode = t.Result; - cancellationTokenSource.Cancel(); - _ffMpegArguments.Post(); - }), _ffMpegArguments.During(cancellationTokenSource.Token)); + errorCode = Process(instance, cancellationTokenSource).ConfigureAwait(false).GetAwaiter().GetResult(); } catch (Exception e) { @@ -114,13 +108,7 @@ void OnCancelEvent(object sender, int timeout) try { - _ffMpegArguments.Pre(); - await Task.WhenAll(instance.FinishedRunning().ContinueWith(t => - { - errorCode = t.Result; - cancellationTokenSource.Cancel(); - _ffMpegArguments.Post(); - }), _ffMpegArguments.During(cancellationTokenSource.Token)).ConfigureAwait(false); + errorCode = await Process(instance, cancellationTokenSource).ConfigureAwait(false); } catch (Exception e) { @@ -134,6 +122,21 @@ await Task.WhenAll(instance.FinishedRunning().ContinueWith(t => return HandleCompletion(throwOnError, errorCode, instance.ErrorData); } + private async Task Process(Instance instance, CancellationTokenSource cancellationTokenSource) + { + var errorCode = -1; + + _ffMpegArguments.Pre(); + await Task.WhenAll(instance.FinishedRunning().ContinueWith(t => + { + errorCode = t.Result; + cancellationTokenSource.Cancel(); + _ffMpegArguments.Post(); + }), _ffMpegArguments.During(cancellationTokenSource.Token)).ConfigureAwait(false); + + return errorCode; + } + private bool HandleCompletion(bool throwOnError, int exitCode, IReadOnlyList errorData) { if (throwOnError && exitCode != 0) @@ -145,17 +148,18 @@ private bool HandleCompletion(bool throwOnError, int exitCode, IReadOnlyList inputStream.CopyToAsync(destination, BlockSize, cancellationToken); } - public Task ReadAsync(Stream inputStream, CancellationToken cancellationToken) - => Writer(inputStream, cancellationToken); + public async Task ReadAsync(Stream inputStream, CancellationToken cancellationToken) + => await Writer(inputStream, cancellationToken).ConfigureAwait(false); public string GetFormat() => Format; } diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index 231df98..fbf7ea4 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -8,13 +8,18 @@ A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications 4.0.0.0 README.md - - Support for PCM audio samples (thanks to Namaneo) -- Support for subtitle streams in MediaAnalysis (thanks to alex6dj) -- Support for subtitle hard burning (thanks to alex6dj) -- Additional codec* properties on MediaAnalysis object (thanks to GuyWithDogs) -- SelectStream method for mapping/specifyíng specific streams from input files (thanks to Feodoros) + - Support for reading Disposition metadata on streams in FFProbe output (thanks alex6dj) +- Fix for stream index in Snapshot(Async) (thanks stasokrosh) +- Add support for frame analysis through FFProbe,GetFrames(Async) (thanks andyjmorgan) +- Fix for subtitle burning error, when subtitle path contains special characters (thanks alex6dj) +- Support for Audio filters (thanks alex6dj) +- Fix FFProbe.AnalyseAsync not throwing if non-zero exit-code (thanks samburovkv) +- Change bit_rate from int to long to support analysis of high bit-rate files (thanks zhuker) +- Support for specifying working directory for ffmpeg and ffprobe processes through FFOptions +- Ensure Image instances in JoinImageSequence are disposed +- Added ConfigureAwait(false) to prevent hanging with certain frameworks 8 - 4.5.0 + 4.6.0 MIT Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev ffmpeg ffprobe convert video audio mediafile resize analyze muxing diff --git a/FFMpegCore/FFOptions.cs b/FFMpegCore/FFOptions.cs index 1f7e497..94ce212 100644 --- a/FFMpegCore/FFOptions.cs +++ b/FFMpegCore/FFOptions.cs @@ -6,6 +6,11 @@ namespace FFMpegCore { public class FFOptions { + /// + /// Working directory for the ffmpeg/ffprobe instance + /// + public string WorkingDirectory { get; set; } = string.Empty; + /// /// Folder container ffmpeg and ffprobe binaries. Leave empty if ffmpeg and ffprobe are present in PATH /// diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs index 7d043a6..d0e8ea8 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -18,16 +18,28 @@ public static IMediaAnalysis Analyse(string filePath, int outputCapacity = int.M if (!File.Exists(filePath)) throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); - using var instance = PrepareInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); + using var instance = PrepareStreamAnalysisInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); var exitCode = instance.BlockUntilFinished(); if (exitCode != 0) 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 FFProbeFrames GetFrames(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 = PrepareFrameAnalysisInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); + var exitCode = instance.BlockUntilFinished(); + if (exitCode != 0) + 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 ParseFramesOutput(instance); + } public static IMediaAnalysis Analyse(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) { - using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current); + using var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current); var exitCode = instance.BlockUntilFinished(); if (exitCode != 0) throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData)); @@ -38,7 +50,7 @@ public static IMediaAnalysis Analyse(Stream stream, int outputCapacity = int.Max { var streamPipeSource = new StreamPipeSource(stream); var pipeArgument = new InputPipeArgument(streamPipeSource); - using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); + using var instance = PrepareStreamAnalysisInstance(pipeArgument.PipePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); pipeArgument.Pre(); var task = instance.FinishedRunning(); @@ -62,21 +74,37 @@ public static async Task AnalyseAsync(string filePath, int outpu if (!File.Exists(filePath)) throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); - using var instance = PrepareInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); - await instance.FinishedRunning().ConfigureAwait(false); + using var instance = PrepareStreamAnalysisInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); + var exitCode = await instance.FinishedRunning().ConfigureAwait(false); + if (exitCode != 0) + 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 GetFramesAsync(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 = PrepareFrameAnalysisInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); + await instance.FinishedRunning().ConfigureAwait(false); + return ParseFramesOutput(instance); + } public static async Task AnalyseAsync(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) { - using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current); - await instance.FinishedRunning().ConfigureAwait(false); + using var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current); + var exitCode = await instance.FinishedRunning().ConfigureAwait(false); + if (exitCode != 0) + 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 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, ffOptions ?? GlobalFFOptions.Current); + using var instance = PrepareStreamAnalysisInstance(pipeArgument.PipePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); pipeArgument.Pre(); var task = instance.FinishedRunning(); @@ -112,16 +140,33 @@ private static IMediaAnalysis ParseOutput(Instance instance) return new MediaAnalysis(ffprobeAnalysis); } + private static FFProbeFrames ParseFramesOutput(Instance instance) + { + var json = string.Join(string.Empty, instance.OutputData); + var ffprobeAnalysis = JsonSerializer.Deserialize(json, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString | System.Text.Json.Serialization.JsonNumberHandling.WriteAsString + }) ; - private static Instance PrepareInstance(string filePath, int outputCapacity, FFOptions ffOptions) + return ffprobeAnalysis; + } + + + private static Instance PrepareStreamAnalysisInstance(string filePath, int outputCapacity, FFOptions ffOptions) + => PrepareInstance($"-loglevel error -print_format json -show_format -sexagesimal -show_streams \"{filePath}\"", outputCapacity, ffOptions); + private static Instance PrepareFrameAnalysisInstance(string filePath, int outputCapacity, FFOptions ffOptions) + => PrepareInstance($"-loglevel error -print_format json -show_frames -v quiet -sexagesimal \"{filePath}\"", outputCapacity, ffOptions); + + private static Instance PrepareInstance(string arguments, int outputCapacity, FFOptions ffOptions) { FFProbeHelper.RootExceptionCheck(); FFProbeHelper.VerifyFFProbeExists(ffOptions); - var arguments = $"-loglevel error -print_format json -show_format -sexagesimal -show_streams \"{filePath}\""; var startInfo = new ProcessStartInfo(GlobalFFOptions.GetFFProbeBinaryPath(), arguments) { StandardOutputEncoding = ffOptions.Encoding, - StandardErrorEncoding = ffOptions.Encoding + StandardErrorEncoding = ffOptions.Encoding, + WorkingDirectory = ffOptions.WorkingDirectory }; var instance = new Instance(startInfo) { DataBufferCapacity = outputCapacity }; return instance; diff --git a/FFMpegCore/FFProbe/FFProbeAnalysis.cs b/FFMpegCore/FFProbe/FFProbeAnalysis.cs index 1997cc3..2177307 100644 --- a/FFMpegCore/FFProbe/FFProbeAnalysis.cs +++ b/FFMpegCore/FFProbe/FFProbeAnalysis.cs @@ -12,7 +12,7 @@ public class FFProbeAnalysis public Format Format { get; set; } = null!; } - public class FFProbeStream : ITagsContainer + public class FFProbeStream : ITagsContainer, IDispositionContainer { [JsonPropertyName("index")] public int Index { get; set; } @@ -71,9 +71,13 @@ public class FFProbeStream : ITagsContainer [JsonPropertyName("sample_rate")] public string SampleRate { get; set; } = null!; + [JsonPropertyName("disposition")] + public Dictionary Disposition { get; set; } = null!; + [JsonPropertyName("tags")] public Dictionary Tags { get; set; } = null!; } + public class Format : ITagsContainer { [JsonPropertyName("filename")] @@ -110,10 +114,16 @@ public class Format : ITagsContainer public Dictionary Tags { get; set; } = null!; } + public interface IDispositionContainer + { + Dictionary Disposition { get; set; } + } + public interface ITagsContainer { Dictionary Tags { get; set; } } + public static class TagExtensions { private static string? TryGetTagValue(ITagsContainer tagsContainer, string key) @@ -127,7 +137,18 @@ public static class TagExtensions public static string? GetCreationTime(this ITagsContainer tagsContainer) => TryGetTagValue(tagsContainer, "creation_time "); public static string? GetRotate(this ITagsContainer tagsContainer) => TryGetTagValue(tagsContainer, "rotate"); public static string? GetDuration(this ITagsContainer tagsContainer) => TryGetTagValue(tagsContainer, "duration"); - - + } + + public static class DispositionExtensions + { + private static int? TryGetDispositionValue(IDispositionContainer dispositionContainer, string key) + { + if (dispositionContainer.Disposition != null && dispositionContainer.Disposition.TryGetValue(key, out var dispositionValue)) + return dispositionValue; + return null; + } + + public static int? GetDefault(this IDispositionContainer tagsContainer) => TryGetDispositionValue(tagsContainer, "default"); + public static int? GetForced(this IDispositionContainer tagsContainer) => TryGetDispositionValue(tagsContainer, "forced"); } } diff --git a/FFMpegCore/FFProbe/FrameAnalysis.cs b/FFMpegCore/FFProbe/FrameAnalysis.cs new file mode 100644 index 0000000..a22cd24 --- /dev/null +++ b/FFMpegCore/FFProbe/FrameAnalysis.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace FFMpegCore +{ + public class FFProbeFrameAnalysis + { + [JsonPropertyName("media_type")] + public string MediaType { get; set; } + + [JsonPropertyName("stream_index")] + public int StreamIndex { get; set; } + + [JsonPropertyName("key_frame")] + public int KeyFrame { get; set; } + + [JsonPropertyName("pkt_pts")] + public long PacketPts { get; set; } + + [JsonPropertyName("pkt_pts_time")] + public string PacketPtsTime { get; set; } + + [JsonPropertyName("pkt_dts")] + public long PacketDts { get; set; } + + [JsonPropertyName("pkt_dts_time")] + public string PacketDtsTime { get; set; } + + [JsonPropertyName("best_effort_timestamp")] + public long BestEffortTimestamp { get; set; } + + [JsonPropertyName("best_effort_timestamp_time")] + public string BestEffortTimestampTime { get; set; } + + [JsonPropertyName("pkt_duration")] + public int PacketDuration { get; set; } + + [JsonPropertyName("pkt_duration_time")] + public string PacketDurationTime { get; set; } + + [JsonPropertyName("pkt_pos")] + public long PacketPos { get; set; } + + [JsonPropertyName("pkt_size")] + public int PacketSize { get; set; } + + [JsonPropertyName("width")] + public long Width { get; set; } + + [JsonPropertyName("height")] + public long Height { get; set; } + + [JsonPropertyName("pix_fmt")] + public string PixelFormat { get; set; } + + [JsonPropertyName("pict_type")] + public string PictureType { get; set; } + + [JsonPropertyName("coded_picture_number")] + public long CodedPictureNumber { get; set; } + + [JsonPropertyName("display_picture_number")] + public long DisplayPictureNumber { get; set; } + + [JsonPropertyName("interlaced_frame")] + public int InterlacedFrame { get; set; } + + [JsonPropertyName("top_field_first")] + public int TopFieldFirst { get; set; } + + [JsonPropertyName("repeat_pict")] + public int RepeatPicture { get; set; } + + [JsonPropertyName("chroma_location")] + public string ChromaLocation { get; set; } + } + + public class FFProbeFrames + { + [JsonPropertyName("frames")] + public List Frames { get; set; } + } +} diff --git a/FFMpegCore/FFProbe/MediaAnalysis.cs b/FFMpegCore/FFProbe/MediaAnalysis.cs index aea714c..a2db068 100644 --- a/FFMpegCore/FFProbe/MediaAnalysis.cs +++ b/FFMpegCore/FFProbe/MediaAnalysis.cs @@ -52,7 +52,7 @@ private VideoStream ParseVideoStream(FFProbeStream stream) { Index = stream.Index, AvgFrameRate = MediaAnalysisUtils.DivideRatio(MediaAnalysisUtils.ParseRatioDouble(stream.AvgFrameRate, '/')), - BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitRate) : default, + BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseLongInvariant(stream.BitRate) : default, BitsPerRawSample = !string.IsNullOrEmpty(stream.BitsPerRawSample) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitsPerRawSample) : default, CodecName = stream.CodecName, CodecLongName = stream.CodecLongName, @@ -67,6 +67,7 @@ private VideoStream ParseVideoStream(FFProbeStream stream) PixelFormat = stream.PixelFormat, Rotation = (int)float.Parse(stream.GetRotate() ?? "0"), Language = stream.GetLanguage(), + Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition), Tags = stream.Tags, }; } @@ -76,7 +77,7 @@ private AudioStream ParseAudioStream(FFProbeStream stream) return new AudioStream { Index = stream.Index, - BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitRate) : default, + BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseLongInvariant(stream.BitRate) : default, CodecName = stream.CodecName, CodecLongName = stream.CodecLongName, CodecTag = stream.CodecTag, @@ -87,6 +88,7 @@ private AudioStream ParseAudioStream(FFProbeStream stream) SampleRateHz = !string.IsNullOrEmpty(stream.SampleRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.SampleRate) : default, Profile = stream.Profile, Language = stream.GetLanguage(), + Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition), Tags = stream.Tags, }; } @@ -96,11 +98,12 @@ private SubtitleStream ParseSubtitleStream(FFProbeStream stream) return new SubtitleStream { Index = stream.Index, - BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitRate) : default, + BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseLongInvariant(stream.BitRate) : default, CodecName = stream.CodecName, CodecLongName = stream.CodecLongName, Duration = MediaAnalysisUtils.ParseDuration(stream), Language = stream.GetLanguage(), + Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition), Tags = stream.Tags, }; } @@ -132,6 +135,9 @@ public static double ParseDoubleInvariant(string line) => public static int ParseIntInvariant(string line) => int.Parse(line, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture); + public static long ParseLongInvariant(string line) => + long.Parse(line, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture); + public static TimeSpan ParseDuration(string duration) { @@ -169,5 +175,30 @@ public static TimeSpan ParseDuration(FFProbeStream ffProbeStream) { return ParseDuration(ffProbeStream.Duration); } + + public static Dictionary? FormatDisposition(Dictionary? disposition) + { + if (disposition == null) + { + return null; + } + + var result = new Dictionary(disposition.Count); + + foreach (var pair in disposition) + { + result.Add(pair.Key, ToBool(pair.Value)); + } + + static bool ToBool(int value) => value switch + { + 0 => false, + 1 => true, + _ => throw new ArgumentOutOfRangeException(nameof(value), + $"Not expected disposition state value: {value}") + }; + + return result; + } } } \ No newline at end of file diff --git a/FFMpegCore/FFProbe/MediaStream.cs b/FFMpegCore/FFProbe/MediaStream.cs index 22186c5..68bc78f 100644 --- a/FFMpegCore/FFProbe/MediaStream.cs +++ b/FFMpegCore/FFProbe/MediaStream.cs @@ -12,11 +12,12 @@ public class MediaStream public string CodecLongName { get; internal set; } = null!; public string CodecTagString { get; set; } = null!; public string CodecTag { get; set; } = null!; - public int BitRate { get; internal set; } + public long BitRate { get; internal set; } public TimeSpan Duration { get; internal set; } public string? Language { get; internal set; } + public Dictionary? Disposition { get; internal set; } public Dictionary? Tags { get; internal set; } - + public Codec GetCodecInfo() => FFMpeg.GetCodec(CodecName); } } \ No newline at end of file diff --git a/README.md b/README.md index d58e3fe..a8ab510 100644 --- a/README.md +++ b/README.md @@ -196,12 +196,12 @@ The root and temp directory for the ffmpeg binaries can be configured via the `f ``` ### Supporting both 32 and 64 bit processes -If you wish to support multiple client processor architectures, you can do so by creating a folder `x64` and `x86` in the `root` directory. -Both folders should contain the binaries (`ffmpeg.exe` and `ffprobe.exe`) for build for the respective architectures. +If you wish to support multiple client processor architectures, you can do so by creating two folders, `x64` and `x86`, in the `BinaryFolder` directory. +Both folders should contain the binaries (`ffmpeg.exe` and `ffprobe.exe`) built for the respective architectures. -By doing so, the library will attempt to use either `/root/{ARCH}/(ffmpeg|ffprobe).exe`. +By doing so, the library will attempt to use either `/{BinaryFolder}/{ARCH}/(ffmpeg|ffprobe).exe`. -If these folders are not defined, it will try to find the binaries in `/root/(ffmpeg|ffprobe.exe)`. +If these folders are not defined, it will try to find the binaries in `/{BinaryFolder}/(ffmpeg|ffprobe.exe)`. (`.exe` is only appended on Windows) @@ -215,7 +215,7 @@ Older versions of ffmpeg might not support all ffmpeg arguments available throug -## Non-code contributors +## Other contributors