From e1035ca88e873afc5db20267327ffe65c1d74d82 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 12 Aug 2021 23:06:07 +0200 Subject: [PATCH 01/26] Update README.md Former-commit-id: a76ec851c8305f67257b96b2874abaecaf4c7895 --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d58e3fe..56bc001 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) From 57e2585f52df1103fe1c7502503e074b50bdcbf8 Mon Sep 17 00:00:00 2001 From: alex6dj Date: Mon, 6 Sep 2021 23:08:36 -0400 Subject: [PATCH 02/26] Get extra disposition data in MediaStream Former-commit-id: 41ec1a10dd913bba9371b88500a7a78179546aa9 --- FFMpegCore.Test/FFProbeTests.cs | 9 +++++++++ FFMpegCore/FFProbe/FFProbeAnalysis.cs | 27 ++++++++++++++++++++++++--- FFMpegCore/FFProbe/MediaAnalysis.cs | 3 +++ FFMpegCore/FFProbe/MediaStream.cs | 3 ++- 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs index 7af92cd..91411d4 100644 --- a/FFMpegCore.Test/FFProbeTests.cs +++ b/FFMpegCore.Test/FFProbeTests.cs @@ -114,5 +114,14 @@ 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.AreEqual(1, info.PrimaryAudioStream.Disposition["default"]); + Assert.AreEqual(0, info.PrimaryAudioStream.Disposition["forced"]); + } } } \ No newline at end of file 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/MediaAnalysis.cs b/FFMpegCore/FFProbe/MediaAnalysis.cs index aea714c..174eb7e 100644 --- a/FFMpegCore/FFProbe/MediaAnalysis.cs +++ b/FFMpegCore/FFProbe/MediaAnalysis.cs @@ -67,6 +67,7 @@ private VideoStream ParseVideoStream(FFProbeStream stream) PixelFormat = stream.PixelFormat, Rotation = (int)float.Parse(stream.GetRotate() ?? "0"), Language = stream.GetLanguage(), + Disposition = stream.Disposition, Tags = stream.Tags, }; } @@ -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 = stream.Disposition, Tags = stream.Tags, }; } @@ -101,6 +103,7 @@ private SubtitleStream ParseSubtitleStream(FFProbeStream stream) CodecLongName = stream.CodecLongName, Duration = MediaAnalysisUtils.ParseDuration(stream), Language = stream.GetLanguage(), + Disposition = stream.Disposition, Tags = stream.Tags, }; } diff --git a/FFMpegCore/FFProbe/MediaStream.cs b/FFMpegCore/FFProbe/MediaStream.cs index 22186c5..9681ac9 100644 --- a/FFMpegCore/FFProbe/MediaStream.cs +++ b/FFMpegCore/FFProbe/MediaStream.cs @@ -15,8 +15,9 @@ public class MediaStream public int 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 From 2b6a74dd7e09f0b8f88b49bde9914f65ef008144 Mon Sep 17 00:00:00 2001 From: alex6dj Date: Tue, 7 Sep 2021 12:48:05 -0400 Subject: [PATCH 03/26] Use boolean instead on generic int for disposition state value Former-commit-id: d79bbaef97f7bc4b043a4b4266db5d4d395b5852 --- FFMpegCore.Test/FFProbeTests.cs | 5 +++-- FFMpegCore/FFProbe/MediaAnalysis.cs | 31 ++++++++++++++++++++++++++--- FFMpegCore/FFProbe/MediaStream.cs | 2 +- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs index 91411d4..aaadd4c 100644 --- a/FFMpegCore.Test/FFProbeTests.cs +++ b/FFMpegCore.Test/FFProbeTests.cs @@ -120,8 +120,9 @@ public async Task Probe_Success_Disposition_Async() { var info = await FFProbe.AnalyseAsync(TestResources.Mp4Video); Assert.IsNotNull(info.PrimaryAudioStream); - Assert.AreEqual(1, info.PrimaryAudioStream.Disposition["default"]); - Assert.AreEqual(0, info.PrimaryAudioStream.Disposition["forced"]); + 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/FFProbe/MediaAnalysis.cs b/FFMpegCore/FFProbe/MediaAnalysis.cs index 174eb7e..f7338cc 100644 --- a/FFMpegCore/FFProbe/MediaAnalysis.cs +++ b/FFMpegCore/FFProbe/MediaAnalysis.cs @@ -67,7 +67,7 @@ private VideoStream ParseVideoStream(FFProbeStream stream) PixelFormat = stream.PixelFormat, Rotation = (int)float.Parse(stream.GetRotate() ?? "0"), Language = stream.GetLanguage(), - Disposition = stream.Disposition, + Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition), Tags = stream.Tags, }; } @@ -88,7 +88,7 @@ private AudioStream ParseAudioStream(FFProbeStream stream) SampleRateHz = !string.IsNullOrEmpty(stream.SampleRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.SampleRate) : default, Profile = stream.Profile, Language = stream.GetLanguage(), - Disposition = stream.Disposition, + Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition), Tags = stream.Tags, }; } @@ -103,7 +103,7 @@ private SubtitleStream ParseSubtitleStream(FFProbeStream stream) CodecLongName = stream.CodecLongName, Duration = MediaAnalysisUtils.ParseDuration(stream), Language = stream.GetLanguage(), - Disposition = stream.Disposition, + Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition), Tags = stream.Tags, }; } @@ -172,5 +172,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 9681ac9..7d6ad20 100644 --- a/FFMpegCore/FFProbe/MediaStream.cs +++ b/FFMpegCore/FFProbe/MediaStream.cs @@ -15,7 +15,7 @@ public class MediaStream public int BitRate { get; internal set; } public TimeSpan Duration { get; internal set; } public string? Language { get; internal set; } - public Dictionary? Disposition { get; internal set; } + public Dictionary? Disposition { get; internal set; } public Dictionary? Tags { get; internal set; } public Codec GetCodecInfo() => FFMpeg.GetCodec(CodecName); From feaeb7478e061a9f849c2f2885ca4ad7542b04fe Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sun, 19 Sep 2021 20:49:10 +0200 Subject: [PATCH 04/26] Update README.md Former-commit-id: e6d85eea11ab3fbe2eabd945130e6d568ea47bea --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 56bc001..a8ab510 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,7 @@ Older versions of ffmpeg might not support all ffmpeg arguments available throug -## Non-code contributors +## Other contributors From 849eb3ce544793bf7b68af615a9bdb6d810462dd Mon Sep 17 00:00:00 2001 From: Stanislau Krashynski Date: Mon, 20 Sep 2021 16:06:43 +0300 Subject: [PATCH 05/26] fix stream index for passing to ffmpeg while making a snapshot Former-commit-id: 26fcfcc206ad4a2ff841c1e0ad43ab0e76107d45 --- FFMpegCore/FFMpeg/FFMpeg.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index a345160..ed3d390 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -116,7 +116,12 @@ 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; + if (streamIndex == null) + { + streamIndex = source.PrimaryVideoStream?.Index + ?? source.VideoStreams.First()?.Index + ?? 0; + } return (FFMpegArguments .FromFileInput(input, false, options => options From dbccc116fe05db82863d7107b68f2d2e1072c385 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Fri, 24 Sep 2021 10:23:59 +0100 Subject: [PATCH 06/26] Adding support for ffprobe show frames Former-commit-id: 93131a7cd09f95e5958d46179a56c3edd32a026e --- FFMpegCore/FFProbe/FFProbe.cs | 46 +++++++++++++++++++++++++++++ FFMpegCore/FFProbe/FrameAnalysis.cs | 39 ++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 FFMpegCore/FFProbe/FrameAnalysis.cs diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs index 7d043a6..fe1e1fe 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -25,6 +25,18 @@ public static IMediaAnalysis Analyse(string filePath, int outputCapacity = int.M 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 = PrepareProbeInstance(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); @@ -66,6 +78,16 @@ public static async Task AnalyseAsync(string filePath, int outpu await instance.FinishedRunning().ConfigureAwait(false); 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 = PrepareProbeInstance(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); @@ -112,6 +134,17 @@ 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 + }) ; + + return ffprobeAnalysis; + } private static Instance PrepareInstance(string filePath, int outputCapacity, FFOptions ffOptions) { @@ -126,5 +159,18 @@ private static Instance PrepareInstance(string filePath, int outputCapacity, FFO var instance = new Instance(startInfo) { DataBufferCapacity = outputCapacity }; return instance; } + private static Instance PrepareProbeInstance(string filePath, int outputCapacity, FFOptions ffOptions) + { + FFProbeHelper.RootExceptionCheck(); + FFProbeHelper.VerifyFFProbeExists(ffOptions); + var arguments = $"-loglevel error -print_format json -show_frames -v quiet -sexagesimal \"{filePath}\""; + var startInfo = new ProcessStartInfo(GlobalFFOptions.GetFFProbeBinaryPath(), arguments) + { + StandardOutputEncoding = ffOptions.Encoding, + StandardErrorEncoding = ffOptions.Encoding + }; + var instance = new Instance(startInfo) { DataBufferCapacity = outputCapacity }; + return instance; + } } } diff --git a/FFMpegCore/FFProbe/FrameAnalysis.cs b/FFMpegCore/FFProbe/FrameAnalysis.cs new file mode 100644 index 0000000..6ac2b19 --- /dev/null +++ b/FFMpegCore/FFProbe/FrameAnalysis.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json.Serialization; + +namespace FFMpegCore +{ + public class Frame + { + public string media_type { get; set; } + public int stream_index { get; set; } + public int key_frame { get; set; } + public long pkt_pts { get; set; } + public string pkt_pts_time { get; set; } + public long pkt_dts { get; set; } + public string pkt_dts_time { get; set; } + public long best_effort_timestamp { get; set; } + public string best_effort_timestamp_time { get; set; } + public int pkt_duration { get; set; } + public string pkt_duration_time { get; set; } + public long pkt_pos { get; set; } + public int pkt_size { get; set; } + public long width { get; set; } + public long height { get; set; } + public string pix_fmt { get; set; } + public string pict_type { get; set; } + public long coded_picture_number { get; set; } + public long display_picture_number { get; set; } + public int interlaced_frame { get; set; } + public int top_field_first { get; set; } + public int repeat_pict { get; set; } + public string chroma_location { get; set; } + } + + public class FFProbeFrames + { + public List frames { get; set; } + } +} From 21da31c4c1608b89df3ccaefc6b844a47c5f9f77 Mon Sep 17 00:00:00 2001 From: alex6dj Date: Fri, 1 Oct 2021 23:34:56 -0400 Subject: [PATCH 07/26] Fixed error when burning subtitle with some special charaters in path Former-commit-id: e6e07fc2fe0e5c480d31e211f4048818781a909c --- FFMpegCore.Test/ArgumentBuilderTest.cs | 17 +++++- FFMpegCore/Extend/StringExtensions.cs | 57 ++++++++++++++++++- .../Arguments/SubtitleHardBurnArgument.cs | 2 +- 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs index 082d9bf..5f2ce5c 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); } diff --git a/FFMpegCore/Extend/StringExtensions.cs b/FFMpegCore/Extend/StringExtensions.cs index ddcf54b..7afcc34 100644 --- a/FFMpegCore/Extend/StringExtensions.cs +++ b/FFMpegCore/Extend/StringExtensions.cs @@ -1,7 +1,19 @@ -namespace FFMpegCore.Extend +using System.Collections.Generic; +using System.Text; + +namespace FFMpegCore.Extend { internal static class StringExtensions { + private static Dictionary CharactersSubstitution { get; } = new Dictionary + { + {'\\', @"\\"}, + {':', @"\:"}, + {'[', @"\["}, + {']', @"\]"}, + // {'\'', @"\'"} TODO: Quotes need to be escaped but i failed miserably + }; + /// /// Enclose string between quotes if contains an space character /// @@ -11,5 +23,48 @@ public static string EncloseIfContainsSpace(this string input) { return input.Contains(" ") ? $"'{input}'" : input; } + + /// + /// Enclose an string in quotes + /// + /// + /// + public static string EncloseInQuotes(this 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(this 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/SubtitleHardBurnArgument.cs b/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs index a48f845..d7ec3aa 100644 --- a/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs @@ -103,7 +103,7 @@ 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[] { _subtitle.ToFFmpegLibavfilterPath().EncloseInQuotes() }.Concat(Parameters.Select(parameter => parameter.FormatArgumentPair(enclose: true)))); } public class StyleOptions From 0c74351d83bbe010e28eb0fb0a320e039c411d16 Mon Sep 17 00:00:00 2001 From: alex6dj Date: Sun, 3 Oct 2021 11:07:15 -0400 Subject: [PATCH 08/26] Fixed single quotes escape in subtitle file path Former-commit-id: 975bd75c5de76f5ae0dea8c468f216d38d02f5be --- FFMpegCore.Test/ArgumentBuilderTest.cs | 4 ++-- FFMpegCore/Extend/StringExtensions.cs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs index 5f2ce5c..9a358ae 100644 --- a/FFMpegCore.Test/ArgumentBuilderTest.cs +++ b/FFMpegCore.Test/ArgumentBuilderTest.cs @@ -353,10 +353,10 @@ public void Builder_BuildString_SubtitleHardBurnFilterFixedPaths() .OutputToFile("output.mp4", false, opt => opt .WithVideoFilters(filterOptions => filterOptions .HardBurnSubtitle(SubtitleHardBurnOptions - .Create(subtitlePath: @"sample( \ : [ ] , ).srt")))) + .Create(subtitlePath: @"sample( \ : [ ] , ' ).srt")))) .Arguments; - Assert.AreEqual(@"-i ""input.mp4"" -vf ""subtitles='sample( \\ \: \[ \] \, ).srt'"" ""output.mp4""", + Assert.AreEqual(@"-i ""input.mp4"" -vf ""subtitles='sample( \\ \: \[ \] \, '\\\'' ).srt'"" ""output.mp4""", str); } diff --git a/FFMpegCore/Extend/StringExtensions.cs b/FFMpegCore/Extend/StringExtensions.cs index 7afcc34..2c33681 100644 --- a/FFMpegCore/Extend/StringExtensions.cs +++ b/FFMpegCore/Extend/StringExtensions.cs @@ -7,11 +7,11 @@ internal static class StringExtensions { private static Dictionary CharactersSubstitution { get; } = new Dictionary { - {'\\', @"\\"}, - {':', @"\:"}, - {'[', @"\["}, - {']', @"\]"}, - // {'\'', @"\'"} TODO: Quotes need to be escaped but i failed miserably + { '\\', @"\\" }, + { ':', @"\:" }, + { '[', @"\[" }, + { ']', @"\]" }, + { '\'', @"'\\\''" } }; /// From c6f1d94a74112d8d4949e1b6d35295b5ebb83bbf Mon Sep 17 00:00:00 2001 From: alex6dj Date: Mon, 4 Oct 2021 09:54:38 -0400 Subject: [PATCH 09/26] Initial audio filter implementation Former-commit-id: 78a703fc938c3c44e9bf967c5ce8bf74758580ea --- .../FFMpeg/Arguments/AudioFiltersArgument.cs | 53 +++++++++++++++++++ FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs | 7 +++ 2 files changed, 60 insertions(+) create mode 100644 FFMpegCore/FFMpeg/Arguments/AudioFiltersArgument.cs diff --git a/FFMpegCore/FFMpeg/Arguments/AudioFiltersArgument.cs b/FFMpegCore/FFMpeg/Arguments/AudioFiltersArgument.cs new file mode 100644 index 0000000..834b784 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/AudioFiltersArgument.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using FFMpegCore.Enums; +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(); + + private AudioFilterOptions WithArgument(IAudioFilterArgument argument) + { + Arguments.Add(argument); + return this; + } + } +} \ No newline at end of file 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)); From b5dd8600a447dea9595335955048f9f235cc0db0 Mon Sep 17 00:00:00 2001 From: alex6dj Date: Mon, 4 Oct 2021 12:27:24 -0400 Subject: [PATCH 10/26] Pan filter implementation and testing Former-commit-id: 7a661b6ab37e4dc90d20901b7c98939d21241e4d --- FFMpegCore.Test/ArgumentBuilderTest.cs | 33 +++++++++++ FFMpegCore.Test/AudioTest.cs | 58 +++++++++++++++++++ .../FFMpeg/Arguments/AudioFiltersArgument.cs | 7 ++- FFMpegCore/FFMpeg/Arguments/PanArgument.cs | 43 ++++++++++++++ 4 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 FFMpegCore/FFMpeg/Arguments/PanArgument.cs diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs index 082d9bf..140e033 100644 --- a/FFMpegCore.Test/ArgumentBuilderTest.cs +++ b/FFMpegCore.Test/ArgumentBuilderTest.cs @@ -414,5 +414,38 @@ 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); + } } } \ No newline at end of file diff --git a/FFMpegCore.Test/AudioTest.cs b/FFMpegCore.Test/AudioTest.cs index b6fde77..1231b6f 100644 --- a/FFMpegCore.Test/AudioTest.cs +++ b/FFMpegCore.Test/AudioTest.cs @@ -223,5 +223,63 @@ 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.Mp4Video) + .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); + } + + [TestMethod, Timeout(10000)] + public void Audio_Pan_ToMonoNoDefinitions() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var success = FFMpegArguments.FromFileInput(TestResources.Mp4Video) + .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); + } + + [TestMethod, Timeout(10000)] + public void Audio_Pan_ToMonoChannelsToOutputDefinitionsMismatch() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var ex = Assert.ThrowsException(() => FFMpegArguments.FromFileInput(TestResources.Mp4Video) + .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.Mp4Video) + .OutputToFile(outputFile, true, + argumentOptions => argumentOptions + .WithAudioFilters(filter => filter.Pan("mono", "c0=c0", "c1=c1"))) + .ProcessSynchronously()); + } } } \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/AudioFiltersArgument.cs b/FFMpegCore/FFMpeg/Arguments/AudioFiltersArgument.cs index 834b784..4776d81 100644 --- a/FFMpegCore/FFMpeg/Arguments/AudioFiltersArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/AudioFiltersArgument.cs @@ -1,7 +1,5 @@ using System.Collections.Generic; -using System.Drawing; using System.Linq; -using FFMpegCore.Enums; using FFMpegCore.Exceptions; namespace FFMpegCore.Arguments @@ -43,7 +41,10 @@ public interface IAudioFilterArgument 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)); + private AudioFilterOptions WithArgument(IAudioFilterArgument argument) { Arguments.Add(argument); diff --git a/FFMpegCore/FFMpeg/Arguments/PanArgument.cs b/FFMpegCore/FFMpeg/Arguments/PanArgument.cs new file mode 100644 index 0000000..74d5699 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/PanArgument.cs @@ -0,0 +1,43 @@ +using System; +using System.Linq; + +namespace FFMpegCore.Arguments +{ + /// + /// Represents scale parameter + /// + public class PanArgument : IAudioFilterArgument + { + public readonly string ChannelLayout; + private readonly string[] _outputDefinitions; + + 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; + } + + 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)); + } +} From d7be7aa5ac50051a7f25060e80bab13bbe34772d Mon Sep 17 00:00:00 2001 From: alex6dj Date: Mon, 4 Oct 2021 13:05:44 -0400 Subject: [PATCH 11/26] Document PanArgument Former-commit-id: 571cc88a3966f6e845af9babe60b1c8e9373ac9f --- FFMpegCore/FFMpeg/Arguments/PanArgument.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/FFMpegCore/FFMpeg/Arguments/PanArgument.cs b/FFMpegCore/FFMpeg/Arguments/PanArgument.cs index 74d5699..013fbf6 100644 --- a/FFMpegCore/FFMpeg/Arguments/PanArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/PanArgument.cs @@ -4,13 +4,22 @@ namespace FFMpegCore.Arguments { /// - /// Represents scale parameter + /// 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)) @@ -23,6 +32,13 @@ public PanArgument(string channelLayout, params string[] outputDefinitions) _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)); From 152648275fe02f9bdd7cc49c5dae971e162e917d Mon Sep 17 00:00:00 2001 From: alex6dj Date: Mon, 4 Oct 2021 19:10:00 -0400 Subject: [PATCH 12/26] Implemented Dynamic Audio Normalizer Former-commit-id: 40c14b573ab5aaa42851b5b4e8b170efb7744f54 --- FFMpegCore.Test/ArgumentBuilderTest.cs | 22 +++++++++ FFMpegCore.Test/AudioTest.cs | 48 +++++++++++++++++- .../FFMpeg/Arguments/AudioFiltersArgument.cs | 6 +++ .../Arguments/DynamicNormalizerArgument.cs | 49 +++++++++++++++++++ 4 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 FFMpegCore/FFMpeg/Arguments/DynamicNormalizerArgument.cs diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs index 140e033..a06a951 100644 --- a/FFMpegCore.Test/ArgumentBuilderTest.cs +++ b/FFMpegCore.Test/ArgumentBuilderTest.cs @@ -447,5 +447,27 @@ public void Builder_BuildString_PanAudioFilterChannelNoOutputDefinition() 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 1231b6f..9c3d074 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 { @@ -281,5 +281,51 @@ public void Audio_Pan_ToMonoChannelsLayoutToOutputDefinitionsMismatch() .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/FFMpeg/Arguments/AudioFiltersArgument.cs b/FFMpegCore/FFMpeg/Arguments/AudioFiltersArgument.cs index 4776d81..50b26b3 100644 --- a/FFMpegCore/FFMpeg/Arguments/AudioFiltersArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/AudioFiltersArgument.cs @@ -44,6 +44,12 @@ public class AudioFilterOptions 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) { 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 From 56abd747d3d16fd1efe6770b913ddc6395303f2a Mon Sep 17 00:00:00 2001 From: alex6dj Date: Mon, 4 Oct 2021 19:10:58 -0400 Subject: [PATCH 13/26] Updated MSTest to fix failling test using DataTestMethod Former-commit-id: f3db0f5d78cc754fbe05269224faddf792794ec0 --- FFMpegCore.Test/FFMpegCore.Test.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 @@ - - - + + + From fb71e1a0624661dc6867541ec7a5c07e12545e00 Mon Sep 17 00:00:00 2001 From: alex6dj Date: Mon, 4 Oct 2021 19:14:04 -0400 Subject: [PATCH 14/26] Use only audio files for test to speedup tests run Former-commit-id: 6045d1534e2065b2c582e96f28614977665fff47 --- FFMpegCore.Test/AudioTest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/FFMpegCore.Test/AudioTest.cs b/FFMpegCore.Test/AudioTest.cs index 9c3d074..aaf25b0 100644 --- a/FFMpegCore.Test/AudioTest.cs +++ b/FFMpegCore.Test/AudioTest.cs @@ -229,7 +229,7 @@ public void Audio_Pan_ToMono() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); - var success = FFMpegArguments.FromFileInput(TestResources.Mp4Video) + var success = FFMpegArguments.FromFileInput(TestResources.Mp3Audio) .OutputToFile(outputFile, true, argumentOptions => argumentOptions .WithAudioFilters(filter => filter.Pan(1, "c0 < 0.9 * c0 + 0.1 * c1"))) @@ -246,7 +246,7 @@ public void Audio_Pan_ToMonoNoDefinitions() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); - var success = FFMpegArguments.FromFileInput(TestResources.Mp4Video) + var success = FFMpegArguments.FromFileInput(TestResources.Mp3Audio) .OutputToFile(outputFile, true, argumentOptions => argumentOptions .WithAudioFilters(filter => filter.Pan(1))) @@ -263,7 +263,7 @@ public void Audio_Pan_ToMonoChannelsToOutputDefinitionsMismatch() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); - var ex = Assert.ThrowsException(() => FFMpegArguments.FromFileInput(TestResources.Mp4Video) + var ex = Assert.ThrowsException(() => FFMpegArguments.FromFileInput(TestResources.Mp3Audio) .OutputToFile(outputFile, true, argumentOptions => argumentOptions .WithAudioFilters(filter => filter.Pan(1, "c0=c0", "c1=c1"))) @@ -275,7 +275,7 @@ public void Audio_Pan_ToMonoChannelsLayoutToOutputDefinitionsMismatch() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); - var ex = Assert.ThrowsException(() => FFMpegArguments.FromFileInput(TestResources.Mp4Video) + var ex = Assert.ThrowsException(() => FFMpegArguments.FromFileInput(TestResources.Mp3Audio) .OutputToFile(outputFile, true, argumentOptions => argumentOptions .WithAudioFilters(filter => filter.Pan("mono", "c0=c0", "c1=c1"))) From 67030dda50a06a7a70b30e596f3478880ef34035 Mon Sep 17 00:00:00 2001 From: alex6dj Date: Tue, 5 Oct 2021 00:48:33 -0400 Subject: [PATCH 15/26] Finish PanArgument tests behaviour Former-commit-id: 45b8486a7296fb8e8919c3b56750b9234305a8c5 --- FFMpegCore.Test/AudioTest.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/FFMpegCore.Test/AudioTest.cs b/FFMpegCore.Test/AudioTest.cs index aaf25b0..795fedf 100644 --- a/FFMpegCore.Test/AudioTest.cs +++ b/FFMpegCore.Test/AudioTest.cs @@ -239,6 +239,7 @@ public void Audio_Pan_ToMono() Assert.IsTrue(success); Assert.AreEqual(1, mediaAnalysis.AudioStreams.Count); + Assert.AreEqual("mono", mediaAnalysis.PrimaryAudioStream.ChannelLayout); } [TestMethod, Timeout(10000)] @@ -256,6 +257,7 @@ public void Audio_Pan_ToMonoNoDefinitions() Assert.IsTrue(success); Assert.AreEqual(1, mediaAnalysis.AudioStreams.Count); + Assert.AreEqual("mono", mediaAnalysis.PrimaryAudioStream.ChannelLayout); } [TestMethod, Timeout(10000)] From 9f3debb9f187662a1e7f1cb476d044ccd341ff51 Mon Sep 17 00:00:00 2001 From: Konstantin Samburov Date: Mon, 11 Oct 2021 12:25:41 +0900 Subject: [PATCH 16/26] Fix throwing exception for AnalyseAsync if ExitCode not 0. Former-commit-id: 9ff82337f6cca721d6fb7325136fb62d8e4eac0f --- FFMpegCore/FFProbe/FFProbe.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs index 7d043a6..7c3e9ca 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -63,13 +63,19 @@ public static async Task AnalyseAsync(string filePath, int outpu throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); using var instance = PrepareInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); - await instance.FinishedRunning().ConfigureAwait(false); + 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(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) { using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current); - await instance.FinishedRunning().ConfigureAwait(false); + 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) From 390cd00005f074a7b63a47a3e20802cf89509b4e Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 21 Oct 2021 19:43:13 +0200 Subject: [PATCH 17/26] Minor fixes Former-commit-id: 1b67ea76f0b9fef26fbb9cb22c42b693c8d81d6d --- FFMpegCore/Extend/KeyValuePairExtensions.cs | 2 +- FFMpegCore/Extend/StringExtensions.cs | 6 +++--- FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs | 4 +++- 3 files changed, 7 insertions(+), 5 deletions(-) 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/StringExtensions.cs b/FFMpegCore/Extend/StringExtensions.cs index 2c33681..29c8d42 100644 --- a/FFMpegCore/Extend/StringExtensions.cs +++ b/FFMpegCore/Extend/StringExtensions.cs @@ -19,7 +19,7 @@ internal static class StringExtensions /// /// The input /// The enclosed string - public static string EncloseIfContainsSpace(this string input) + public static string EncloseIfContainsSpace(string input) { return input.Contains(" ") ? $"'{input}'" : input; } @@ -29,7 +29,7 @@ public static string EncloseIfContainsSpace(this string input) /// /// /// - public static string EncloseInQuotes(this string input) + public static string EncloseInQuotes(string input) { return $"'{input}'"; } @@ -43,7 +43,7 @@ public static string EncloseInQuotes(this string input) /// /// /// Scaped path - public static string ToFFmpegLibavfilterPath(this string source) + public static string ToFFmpegLibavfilterPath(string source) { return source.Replace(CharactersSubstitution); } diff --git a/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs b/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs index d7ec3aa..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.ToFFmpegLibavfilterPath().EncloseInQuotes() }.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 From 2de12dd61df3ecd85e1b9b315133d5ae193f459a Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 21 Oct 2021 19:44:22 +0200 Subject: [PATCH 18/26] Minor fixes Former-commit-id: 06da927c7e8552ae124a76e72bb72af0ddc428ce --- FFMpegCore/FFMpeg/FFMpeg.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index ed3d390..13563ce 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -116,12 +116,9 @@ private static (FFMpegArguments, Action outputOptions) Bu { captureTime ??= TimeSpan.FromSeconds(source.Duration.TotalSeconds / 3); size = PrepareSnapshotSize(source, size); - if (streamIndex == null) - { - streamIndex = source.PrimaryVideoStream?.Index - ?? source.VideoStreams.First()?.Index - ?? 0; - } + streamIndex ??= source.PrimaryVideoStream?.Index + ?? source.VideoStreams.FirstOrDefault()?.Index + ?? 0; return (FFMpegArguments .FromFileInput(input, false, options => options From 75fa1db64f0026b10213385133cb7f7e175f7bc1 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 21 Oct 2021 20:36:54 +0200 Subject: [PATCH 19/26] Minor improvements Former-commit-id: 79bc9189175c7998b738bf26ca8d0dee7fe4a08d --- FFMpegCore.Test/FFProbeTests.cs | 25 +++++++ FFMpegCore/FFProbe/FFProbe.cs | 38 +++++------ FFMpegCore/FFProbe/FrameAnalysis.cs | 100 ++++++++++++++++++++-------- 3 files changed, 112 insertions(+), 51 deletions(-) diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs index aaadd4c..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)] diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs index fe1e1fe..09dde4b 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -18,7 +18,7 @@ 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)); @@ -30,7 +30,7 @@ public static FFProbeFrames GetFrames(string filePath, int outputCapacity = int. if (!File.Exists(filePath)) throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); - using var instance = PrepareProbeInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); + 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)); @@ -39,7 +39,7 @@ public static FFProbeFrames GetFrames(string filePath, int outputCapacity = int. } 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)); @@ -50,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(); @@ -74,7 +74,7 @@ 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); + using var instance = PrepareStreamAnalysisInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); await instance.FinishedRunning().ConfigureAwait(false); return ParseOutput(instance); } @@ -84,13 +84,13 @@ public static async Task GetFramesAsync(string filePath, int outp if (!File.Exists(filePath)) throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); - using var instance = PrepareProbeInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); + 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); + using var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current); await instance.FinishedRunning().ConfigureAwait(false); return ParseOutput(instance); } @@ -98,7 +98,7 @@ public static async Task AnalyseAsync(Stream stream, int outputC { 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(); @@ -146,24 +146,16 @@ private static FFProbeFrames ParseFramesOutput(Instance instance) return ffprobeAnalysis; } - private static Instance PrepareInstance(string filePath, int outputCapacity, FFOptions ffOptions) + + 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 - }; - var instance = new Instance(startInfo) { DataBufferCapacity = outputCapacity }; - return instance; - } - private static Instance PrepareProbeInstance(string filePath, int outputCapacity, FFOptions ffOptions) - { - FFProbeHelper.RootExceptionCheck(); - FFProbeHelper.VerifyFFProbeExists(ffOptions); - var arguments = $"-loglevel error -print_format json -show_frames -v quiet -sexagesimal \"{filePath}\""; var startInfo = new ProcessStartInfo(GlobalFFOptions.GetFFProbeBinaryPath(), arguments) { StandardOutputEncoding = ffOptions.Encoding, diff --git a/FFMpegCore/FFProbe/FrameAnalysis.cs b/FFMpegCore/FFProbe/FrameAnalysis.cs index 6ac2b19..a22cd24 100644 --- a/FFMpegCore/FFProbe/FrameAnalysis.cs +++ b/FFMpegCore/FFProbe/FrameAnalysis.cs @@ -1,39 +1,83 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System.Collections.Generic; using System.Text.Json.Serialization; namespace FFMpegCore { - public class Frame + public class FFProbeFrameAnalysis { - public string media_type { get; set; } - public int stream_index { get; set; } - public int key_frame { get; set; } - public long pkt_pts { get; set; } - public string pkt_pts_time { get; set; } - public long pkt_dts { get; set; } - public string pkt_dts_time { get; set; } - public long best_effort_timestamp { get; set; } - public string best_effort_timestamp_time { get; set; } - public int pkt_duration { get; set; } - public string pkt_duration_time { get; set; } - public long pkt_pos { get; set; } - public int pkt_size { get; set; } - public long width { get; set; } - public long height { get; set; } - public string pix_fmt { get; set; } - public string pict_type { get; set; } - public long coded_picture_number { get; set; } - public long display_picture_number { get; set; } - public int interlaced_frame { get; set; } - public int top_field_first { get; set; } - public int repeat_pict { get; set; } - public string chroma_location { get; set; } + [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 { - public List frames { get; set; } + [JsonPropertyName("frames")] + public List Frames { get; set; } } } From f47e64789b9917b76faf7e8df6d3640770a6ce24 Mon Sep 17 00:00:00 2001 From: Rich Gerber Date: Tue, 26 Oct 2021 11:47:46 -0700 Subject: [PATCH 20/26] Change Int32 to long for BitRate Former-commit-id: b8fefc6a0dd7d4bebba385d57a7ae43313d7ccdc --- FFMpegCore/FFProbe/MediaAnalysis.cs | 9 ++++++--- FFMpegCore/FFProbe/MediaStream.cs | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/FFMpegCore/FFProbe/MediaAnalysis.cs b/FFMpegCore/FFProbe/MediaAnalysis.cs index f7338cc..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, @@ -77,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, @@ -98,7 +98,7 @@ 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), @@ -135,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) { diff --git a/FFMpegCore/FFProbe/MediaStream.cs b/FFMpegCore/FFProbe/MediaStream.cs index 7d6ad20..68bc78f 100644 --- a/FFMpegCore/FFProbe/MediaStream.cs +++ b/FFMpegCore/FFProbe/MediaStream.cs @@ -12,7 +12,7 @@ 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; } From 079760d0447627909c0024c8b1e5e3bde58294ff Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 1 Nov 2021 18:13:23 +0100 Subject: [PATCH 21/26] Support for specyfying process working directory through FFOptions (#272) Former-commit-id: c2d28055c1e9f11334b9a079221ef6e43f43b63c --- FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs | 11 ++++++----- FFMpegCore/FFOptions.cs | 5 +++++ FFMpegCore/FFProbe/FFProbe.cs | 3 ++- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs index 060ffc3..0fac769 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs @@ -145,17 +145,18 @@ private bool HandleCompletion(bool throwOnError, int exitCode, IReadOnlyList + /// 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 89f905d..d0e8ea8 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -165,7 +165,8 @@ private static Instance PrepareInstance(string arguments, int outputCapacity, FF 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; From f5745b9670b0f2643df10f267870ac33bada081b Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 1 Nov 2021 18:14:06 +0100 Subject: [PATCH 22/26] Ensure disposal of opened Image instances (#249) Former-commit-id: b217ca4af0a450448eb5347b74616b444b784689 --- FFMpegCore/FFMpeg/FFMpeg.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index 13563ce..8ee59b8 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -303,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(); From 8ba0d7deefa6529bca0eab3251946f46db944280 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 1 Nov 2021 18:53:33 +0100 Subject: [PATCH 23/26] Add .ConfigureAwait(false) in StreamPipeSink Writer function Former-commit-id: e0385dcfdb3fe9dd678c5044c57fcdb8de3f31a7 --- FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs b/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs index addc14e..f03c1db 100644 --- a/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs +++ b/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs @@ -17,7 +17,7 @@ public StreamPipeSink(Func writer) } public StreamPipeSink(Stream destination) { - Writer = (inputStream, cancellationToken) => inputStream.CopyToAsync(destination, BlockSize, cancellationToken); + Writer = async (inputStream, cancellationToken) => await inputStream.CopyToAsync(destination, BlockSize, cancellationToken).ConfigureAwait(false); } public Task ReadAsync(Stream inputStream, CancellationToken cancellationToken) From aa30c82985f3fb611329076d253e1556228ad4fc Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 1 Nov 2021 19:15:00 +0100 Subject: [PATCH 24/26] Ensure ConfigureAwait(false) is set (#246) Former-commit-id: e8df465ffa6e70277f0f29f093aef65cdd11a9f1 --- FFMpegCore/Extend/BitmapVideoFrameWrapper.cs | 2 +- FFMpegCore/Extend/PcmAudioSampleWrapper.cs | 2 +- FFMpegCore/FFMpeg/Arguments/PipeArgument.cs | 2 +- FFMpegCore/FFMpeg/FFMpeg.cs | 4 +-- FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs | 31 +++++++++++--------- 5 files changed, 22 insertions(+), 19 deletions(-) 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/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/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/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index 8ee59b8..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(); diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs index 0fac769..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) From 5ceebfba599fd69ad140173531d69ef336bd4242 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 1 Nov 2021 19:15:39 +0100 Subject: [PATCH 25/26] Update nuget info Former-commit-id: 6377c50de7aac95f5a6beb4278c58a890e2ddac5 --- FFMpegCore/FFMpegCore.csproj | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) 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 From 4c54ff18b97e278a6c24af2bc66212c9649231c1 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 1 Nov 2021 19:21:34 +0100 Subject: [PATCH 26/26] Move .ConfigureAwait(false) in StreamPipeSink to inside ReadAsync to fix failure in ubuntu CI Former-commit-id: 936da48ca2e14b8e1fe70a3d6d12f2dc8e421e92 --- FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs b/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs index f03c1db..289c0ea 100644 --- a/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs +++ b/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs @@ -17,11 +17,11 @@ public StreamPipeSink(Func writer) } public StreamPipeSink(Stream destination) { - Writer = async (inputStream, cancellationToken) => await inputStream.CopyToAsync(destination, BlockSize, cancellationToken).ConfigureAwait(false); + Writer = (inputStream, cancellationToken) => 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; }