From dfd4490b373405303061de0d059986a40da6a1b1 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Wed, 3 Nov 2021 18:53:38 +0100 Subject: [PATCH 01/17] Added VideoCodec.LibX265 (#276) Former-commit-id: 0f82509c41de15fd29d2c061c5bd5555f7308dd8 --- FFMpegCore.Test/VideoTest.cs | 13 +++++++++++++ FFMpegCore/FFMpeg/Enums/Enums.cs | 1 + 2 files changed, 14 insertions(+) diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 27ec79e..0f806d6 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -72,6 +72,19 @@ public void Video_ToMP4_Args() Assert.IsTrue(success); } + [TestMethod, Timeout(10000)] + public void Video_ToH265_MKV_Args() + { + using var outputFile = new TemporaryFile($"out.mkv"); + + var success = FFMpegArguments + .FromFileInput(TestResources.WebmVideo) + .OutputToFile(outputFile, false, opt => opt + .WithVideoCodec(VideoCodec.LibX265)) + .ProcessSynchronously(); + Assert.IsTrue(success); + } + [DataTestMethod, Timeout(10000)] [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] diff --git a/FFMpegCore/FFMpeg/Enums/Enums.cs b/FFMpegCore/FFMpeg/Enums/Enums.cs index 31a5f1e..7520fea 100644 --- a/FFMpegCore/FFMpeg/Enums/Enums.cs +++ b/FFMpegCore/FFMpeg/Enums/Enums.cs @@ -12,6 +12,7 @@ public enum CodecType public static class VideoCodec { public static Codec LibX264 => FFMpeg.GetCodec("libx264"); + public static Codec LibX265 => FFMpeg.GetCodec("libx265"); public static Codec LibVpx => FFMpeg.GetCodec("libvpx"); public static Codec LibTheora => FFMpeg.GetCodec("libtheora"); public static Codec Png => FFMpeg.GetCodec("png"); From 4608b590e16d3f44b870c3a724baa6c382e752b3 Mon Sep 17 00:00:00 2001 From: Alex Zhukov Date: Mon, 8 Nov 2021 06:28:16 -0800 Subject: [PATCH 02/17] parse ffprobes -show_packets output Former-commit-id: 239e2aef4208db7bda06a500c1b0fde5cb295e40 --- FFMpegCore.Test/FFProbeTests.cs | 38 ++++++++++++++++++++++ FFMpegCore/FFProbe/FFProbe.cs | 39 +++++++++++++++++++++++ FFMpegCore/FFProbe/PacketAnalysis.cs | 47 ++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 FFMpegCore/FFProbe/PacketAnalysis.cs diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs index f990c7f..c2e6e5a 100644 --- a/FFMpegCore.Test/FFProbeTests.cs +++ b/FFMpegCore.Test/FFProbeTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -50,6 +51,43 @@ public async Task FrameAnalysis_Async() Assert.IsTrue(frameAnalysis.Frames.All(f => f.MediaType == "video")); } + [TestMethod] + public async Task PacketAnalysis_Async() + { + var packetAnalysis = await FFProbe.GetPacketsAsync(TestResources.WebmVideo); + var packets = packetAnalysis.Packets; + Assert.AreEqual(96, packets.Count); + Assert.IsTrue(packets.All(f => f.CodecType == "video")); + Assert.AreEqual("K_", packets[0].Flags); + Assert.AreEqual(1362, packets.Last().Size); + } + + + [TestMethod] + public void PacketAnalysis_Sync() + { + var packets = FFProbe.GetPackets(TestResources.WebmVideo).Packets; + + Assert.AreEqual(96, packets.Count); + Assert.IsTrue(packets.All(f => f.CodecType == "video")); + Assert.AreEqual("K_", packets[0].Flags); + Assert.AreEqual(1362, packets.Last().Size); + } + + [TestMethod] + public void PacketAnalysisAudioVideo_Sync() + { + var packets = FFProbe.GetPackets(TestResources.Mp4Video).Packets; + + Assert.AreEqual(216, packets.Count); + var actual = packets.Select(f => f.CodecType).Distinct().ToList(); + var expected = new List {"audio", "video"}; + CollectionAssert.AreEquivalent(expected, actual); + Assert.IsTrue(packets.Where(t=>t.CodecType == "audio").All(f => f.Flags == "K_")); + Assert.AreEqual(75, packets.Count(t => t.CodecType == "video")); + Assert.AreEqual(141, packets.Count(t => t.CodecType == "audio")); + } + [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 d0e8ea8..36f050c 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -37,6 +37,20 @@ public static FFProbeFrames GetFrames(string filePath, int outputCapacity = int. return ParseFramesOutput(instance); } + + public static FFProbePackets GetPackets(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 = PreparePacketAnalysisInstance(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 ParsePacketsOutput(instance); + } + public static IMediaAnalysis Analyse(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) { using var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current); @@ -91,6 +105,17 @@ public static async Task GetFramesAsync(string filePath, int outp await instance.FinishedRunning().ConfigureAwait(false); return ParseFramesOutput(instance); } + + public static async Task GetPacketsAsync(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 = PreparePacketAnalysisInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); + await instance.FinishedRunning().ConfigureAwait(false); + return ParsePacketsOutput(instance); + } + public static async Task AnalyseAsync(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) { using var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current); @@ -152,11 +177,25 @@ private static FFProbeFrames ParseFramesOutput(Instance instance) return ffprobeAnalysis; } + private static FFProbePackets ParsePacketsOutput(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 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 PreparePacketAnalysisInstance(string filePath, int outputCapacity, FFOptions ffOptions) + => PrepareInstance($"-loglevel error -print_format json -show_packets -v quiet -sexagesimal \"{filePath}\"", outputCapacity, ffOptions); private static Instance PrepareInstance(string arguments, int outputCapacity, FFOptions ffOptions) { diff --git a/FFMpegCore/FFProbe/PacketAnalysis.cs b/FFMpegCore/FFProbe/PacketAnalysis.cs new file mode 100644 index 0000000..d4da0f5 --- /dev/null +++ b/FFMpegCore/FFProbe/PacketAnalysis.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace FFMpegCore +{ + public class FFProbePacketAnalysis + { + [JsonPropertyName("codec_type")] + public string CodecType { get; set; } + + [JsonPropertyName("stream_index")] + public int StreamIndex { get; set; } + + [JsonPropertyName("pts")] + public long Pts { get; set; } + + [JsonPropertyName("pts_time")] + public string PtsTime { get; set; } + + [JsonPropertyName("dts")] + public long Dts { get; set; } + + [JsonPropertyName("dts_time")] + public string DtsTime { get; set; } + + [JsonPropertyName("duration")] + public int Duration { get; set; } + + [JsonPropertyName("duration_time")] + public string DurationTime { get; set; } + + [JsonPropertyName("size")] + public int Size { get; set; } + + [JsonPropertyName("pos")] + public long Pos { get; set; } + + [JsonPropertyName("flags")] + public string Flags { get; set; } + } + + public class FFProbePackets + { + [JsonPropertyName("packets")] + public List Packets { get; set; } + } +} From 28891eb2428eb6a48146231b73d1d8735f616662 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sun, 14 Nov 2021 01:25:04 +0100 Subject: [PATCH 03/17] Add documentation comments to NotifyOn* methods Former-commit-id: a0399b361a08c52a443c70c4bfaeafb43e38b6d2 --- FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs index 97ea94f..68738be 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs @@ -29,17 +29,32 @@ internal FFMpegArgumentProcessor(FFMpegArguments ffMpegArguments) private event EventHandler CancelEvent = null!; + /// + /// Register action that will be invoked during the ffmpeg processing, when a progress time is output and parsed and progress percentage is calculated. + /// Total time is needed to calculate the percentage that has been processed of the full file. + /// + /// Action to invoke when progress percentage is updated + /// The total timespan of the mediafile being processed public FFMpegArgumentProcessor NotifyOnProgress(Action onPercentageProgress, TimeSpan totalTimeSpan) { _totalTimespan = totalTimeSpan; _onPercentageProgress = onPercentageProgress; return this; } + /// + /// Register action that will be invoked during the ffmpeg processing, when a progress time is output and parsed + /// + /// Action that will be invoked with the parsed timestamp as argument public FFMpegArgumentProcessor NotifyOnProgress(Action onTimeProgress) { _onTimeProgress = onTimeProgress; return this; } + + /// + /// Register action that will be invoked during the ffmpeg processing, when a line is output + /// + /// public FFMpegArgumentProcessor NotifyOnOutput(Action onOutput) { _onOutput = onOutput; From a5b59659240873638c0a44fd2000f4172c2371d7 Mon Sep 17 00:00:00 2001 From: BobSilent Date: Tue, 2 Nov 2021 10:40:05 +0100 Subject: [PATCH 04/17] Add Configure on FFMpegArgumentProcessor to fuently configure ffoptions per run. Former-commit-id: 965e756dc4e7d80c5ed487368b129bb675467396 --- FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs | 37 +++++++++++++++----- FFMpegCore/FFOptions.cs | 17 ++++++--- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs index 68738be..209cc36 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs @@ -14,6 +14,7 @@ namespace FFMpegCore public class FFMpegArgumentProcessor { private static readonly Regex ProgressRegex = new Regex(@"time=(\d\d:\d\d:\d\d.\d\d?)", RegexOptions.Compiled); + private readonly List> _configurations; private readonly FFMpegArguments _ffMpegArguments; private Action? _onPercentageProgress; private Action? _onTimeProgress; @@ -22,12 +23,13 @@ public class FFMpegArgumentProcessor internal FFMpegArgumentProcessor(FFMpegArguments ffMpegArguments) { + _configurations = new List>(); _ffMpegArguments = ffMpegArguments; } public string Arguments => _ffMpegArguments.Text; - private event EventHandler CancelEvent = null!; + private event EventHandler CancelEvent = null!; /// /// Register action that will be invoked during the ffmpeg processing, when a progress time is output and parsed and progress percentage is calculated. @@ -70,10 +72,15 @@ public FFMpegArgumentProcessor CancellableThrough(CancellationToken token, int t token.Register(() => CancelEvent?.Invoke(this, timeout)); return this; } + public FFMpegArgumentProcessor Configure(Action configureOptions) + { + _configurations.Add(configureOptions); + return this; + } public bool ProcessSynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null) { - using var instance = PrepareInstance(ffMpegOptions ?? GlobalFFOptions.Current, out var cancellationTokenSource); - var errorCode = -1; + var options = GetConfiguredOptions(ffMpegOptions); + using var instance = PrepareInstance(options, out var cancellationTokenSource); void OnCancelEvent(object sender, int timeout) { @@ -87,7 +94,8 @@ void OnCancelEvent(object sender, int timeout) } CancelEvent += OnCancelEvent; instance.Exited += delegate { cancellationTokenSource.Cancel(); }; - + + var errorCode = -1; try { errorCode = Process(instance, cancellationTokenSource).ConfigureAwait(false).GetAwaiter().GetResult(); @@ -100,14 +108,14 @@ void OnCancelEvent(object sender, int timeout) { CancelEvent -= OnCancelEvent; } - + return HandleCompletion(throwOnError, errorCode, instance.ErrorData); } public async Task ProcessAsynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null) { - using var instance = PrepareInstance(ffMpegOptions ?? GlobalFFOptions.Current, out var cancellationTokenSource); - var errorCode = -1; + var options = GetConfiguredOptions(ffMpegOptions); + using var instance = PrepareInstance(options, out var cancellationTokenSource); void OnCancelEvent(object sender, int timeout) { @@ -120,7 +128,8 @@ void OnCancelEvent(object sender, int timeout) } } CancelEvent += OnCancelEvent; - + + var errorCode = -1; try { errorCode = await Process(instance, cancellationTokenSource).ConfigureAwait(false); @@ -163,6 +172,18 @@ private bool HandleCompletion(bool throwOnError, int exitCode, IReadOnlyList /// Working directory for the ffmpeg/ffprobe instance @@ -27,16 +28,24 @@ public class FFOptions public Encoding Encoding { get; set; } = Encoding.Default; /// - /// + /// /// public Dictionary ExtensionOverrides { get; set; } = new Dictionary { { "mpegts", ".ts" }, }; - + /// /// Whether to cache calls to get ffmpeg codec, pixel- and container-formats /// public bool UseCache { get; set; } = true; + + /// + object ICloneable.Clone() => Clone(); + + /// + /// Creates a new object that is a copy of the current instance. + /// + public FFOptions Clone() => (FFOptions)MemberwiseClone(); } } \ No newline at end of file From 084f8b3a58d8e64d9bfd21c740e72958736b39fa Mon Sep 17 00:00:00 2001 From: BobSilent Date: Thu, 18 Nov 2021 14:00:18 +0100 Subject: [PATCH 05/17] Update Docu Former-commit-id: f7ad3394596b159c2786ffe01198ef3c6ff74098 --- FFMpegCore.Examples/Program.cs | 14 ++++++++++++-- README.md | 12 +++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/FFMpegCore.Examples/Program.cs b/FFMpegCore.Examples/Program.cs index 256ef3c..a718a21 100644 --- a/FFMpegCore.Examples/Program.cs +++ b/FFMpegCore.Examples/Program.cs @@ -98,7 +98,7 @@ IEnumerable CreateFrames(int count) yield return GetNextFrame(); //method of generating new frames } } - + var videoFramesSource = new RawVideoPipeSource(CreateFrames(64)) //pass IEnumerable or IEnumerator to constructor of RawVideoPipeSource { FrameRate = 30 //set source frame rate @@ -115,10 +115,20 @@ await FFMpegArguments GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" }); // or GlobalFFOptions.Configure(options => options.BinaryFolder = "./bin"); - + // or individual, per-run options await FFMpegArguments .FromFileInput(inputPath) .OutputToFile(outputPath) .ProcessAsynchronously(true, new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" }); + + // or combined, setting global defaults and adapting per-run options + GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "./globalTmp", WorkingDirectory = "./" }); + + await FFMpegArguments + .FromFileInput(inputPath) + .OutputToFile(outputPath) + .Configure(options => options.WorkingDirectory = "./CurrentRunWorkingDir") + .Configure(options => options.TemporaryFilesFolder = "./CurrentRunTmpFolder") + .ProcessAsynchronously(); } \ No newline at end of file diff --git a/README.md b/README.md index a8ab510..2c55520 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,17 @@ await FFMpegArguments .FromFileInput(inputPath) .OutputToFile(outputPath) .ProcessAsynchronously(true, new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" }); -``` + +// or combined, setting global defaults and adapting per-run options +GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "./globalTmp", WorkingDirectory = "./" }); + +await FFMpegArguments + .FromFileInput(inputPath) + .OutputToFile(outputPath) + .Configure(options => options.WorkingDirectory = "./CurrentRunWorkingDir") + .Configure(options => options.TemporaryFilesFolder = "./CurrentRunTmpFolder") + .ProcessAsynchronously(); + ``` ### Option 2 From c04546a1560ecbfd49acc5ed03ef2d6c96c4a224 Mon Sep 17 00:00:00 2001 From: BobSilent Date: Thu, 18 Nov 2021 14:01:38 +0100 Subject: [PATCH 06/17] Added Tests Former-commit-id: 47a6c23b2d55c20f0f046bb418f09f4e4bbc4c2d --- .../FFMpegArgumentProcessorTest.cs | 84 +++++++++++++++++++ FFMpegCore.Test/FFMpegCore.Test.csproj | 1 + FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs | 4 +- 3 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 FFMpegCore.Test/FFMpegArgumentProcessorTest.cs diff --git a/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs b/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs new file mode 100644 index 0000000..08de91c --- /dev/null +++ b/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs @@ -0,0 +1,84 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using FluentAssertions; + +namespace FFMpegCore.Test +{ + [TestClass] + public class FFMpegArgumentProcessorTest + { + + private static FFMpegArgumentProcessor CreateArgumentProcessor() => FFMpegArguments + .FromFileInput("") + .OutputToFile(""); + + + [TestMethod] + public void Processor_GlobalOptions_GetUsed() + { + + var globalWorkingDir = "Whatever"; + GlobalFFOptions.Configure(new FFOptions { WorkingDirectory = globalWorkingDir }); + + var processor = CreateArgumentProcessor(); + var options2 = processor.GetConfiguredOptions(null); + options2.WorkingDirectory.Should().Be(globalWorkingDir); + } + + [TestMethod] + public void Processor_SessionOptions_GetUsed() + { + + var sessionWorkingDir = "./CurrentRunWorkingDir"; + + var processor = CreateArgumentProcessor(); + processor.Configure(options => options.WorkingDirectory = sessionWorkingDir); + var options = processor.GetConfiguredOptions(null); + + options.WorkingDirectory.Should().Be(sessionWorkingDir); + } + + + [TestMethod] + public void Processor_Options_CanBeOverridden_And_Configured() + { + var globalConfig = "Whatever"; + GlobalFFOptions.Configure(new FFOptions { WorkingDirectory = globalConfig, TemporaryFilesFolder = globalConfig, BinaryFolder = globalConfig }); + + + var processor = CreateArgumentProcessor(); + + var sessionTempDir = "./CurrentRunWorkingDir"; + processor.Configure(options => options.TemporaryFilesFolder = sessionTempDir); + + var overrideOptions = new FFOptions() { WorkingDirectory = "override" }; + var options = processor.GetConfiguredOptions(overrideOptions); + + options.Should().BeEquivalentTo(overrideOptions); + options.TemporaryFilesFolder.Should().BeEquivalentTo(sessionTempDir); + options.BinaryFolder.Should().NotBeEquivalentTo(globalConfig); + } + + + [TestMethod] + public void Options_Global_And_Session_Options_Can_Differ() + { + FFMpegArgumentProcessor CreateArgumentProcessor() => FFMpegArguments + .FromFileInput("") + .OutputToFile(""); + + var globalWorkingDir = "Whatever"; + GlobalFFOptions.Configure(new FFOptions { WorkingDirectory = globalWorkingDir }); + + var processor1 = CreateArgumentProcessor(); + var sessionWorkingDir = "./CurrentRunWorkingDir"; + processor1.Configure(options => options.WorkingDirectory = sessionWorkingDir); + var options1 = processor1.GetConfiguredOptions(null); + options1.WorkingDirectory.Should().Be(sessionWorkingDir); + + + var processor2 = CreateArgumentProcessor(); + var options2 = processor2.GetConfiguredOptions(null); + options2.WorkingDirectory.Should().Be(globalWorkingDir); + } + } +} \ No newline at end of file diff --git a/FFMpegCore.Test/FFMpegCore.Test.csproj b/FFMpegCore.Test/FFMpegCore.Test.csproj index e6831e6..6388724 100644 --- a/FFMpegCore.Test/FFMpegCore.Test.csproj +++ b/FFMpegCore.Test/FFMpegCore.Test.csproj @@ -39,6 +39,7 @@ + diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs index 209cc36..fdbdcc8 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs @@ -172,7 +172,7 @@ private bool HandleCompletion(bool throwOnError, int exitCode, IReadOnlyList errorData) { if (!throwOnError) From ce71aaab787b89d85d83ca689fbdffae3a07a702 Mon Sep 17 00:00:00 2001 From: BobSilent Date: Thu, 18 Nov 2021 23:59:15 +0100 Subject: [PATCH 07/17] reset GlobalFFOptions to null after testing Former-commit-id: 8cacf074fd830032279df9f05d88b0472e5ac011 --- FFMpegCore.Test/FFMpegArgumentProcessorTest.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs b/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs index 08de91c..dc65489 100644 --- a/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs +++ b/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs @@ -1,11 +1,19 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using FluentAssertions; +using System.Reflection; namespace FFMpegCore.Test { [TestClass] public class FFMpegArgumentProcessorTest { + [TestCleanup] + public void TestInitialize() + + { + // After testing reset global configuration to null, to be not wrong for other test relying on configuration + typeof(GlobalFFOptions).GetField("_current", BindingFlags.NonPublic | BindingFlags.Static).SetValue(GlobalFFOptions.Current, null); + } private static FFMpegArgumentProcessor CreateArgumentProcessor() => FFMpegArguments .FromFileInput("") @@ -15,7 +23,6 @@ private static FFMpegArgumentProcessor CreateArgumentProcessor() => FFMpegArgume [TestMethod] public void Processor_GlobalOptions_GetUsed() { - var globalWorkingDir = "Whatever"; GlobalFFOptions.Configure(new FFOptions { WorkingDirectory = globalWorkingDir }); @@ -27,7 +34,6 @@ public void Processor_GlobalOptions_GetUsed() [TestMethod] public void Processor_SessionOptions_GetUsed() { - var sessionWorkingDir = "./CurrentRunWorkingDir"; var processor = CreateArgumentProcessor(); From 82fa9b56e154a3cd22737ecc96186acc45a18011 Mon Sep 17 00:00:00 2001 From: Weirdo Date: Tue, 21 Dec 2021 00:10:59 +0100 Subject: [PATCH 08/17] Added MetaDataArgument Former-commit-id: e3eb2f2056ec7ccb0b36d4c2136e4fac4743ccbf --- .../FFMpeg/Arguments/MetaDataArgument.cs | 27 +++++++++++++++++++ FFMpegCore/FFMpeg/FFMpegArguments.cs | 1 + 2 files changed, 28 insertions(+) create mode 100644 FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs diff --git a/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs b/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs new file mode 100644 index 0000000..7e9ffc6 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs @@ -0,0 +1,27 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace FFMpegCore.Arguments +{ + public class MetaDataArgument : IInputArgument + { + private readonly string _metaDataContent; + private readonly string _tempFileName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, $"metadata_{Guid.NewGuid()}.txt"); + + public MetaDataArgument(string metaDataContent) + { + _metaDataContent = metaDataContent; + } + + public string Text => $"-i \"{_tempFileName}\" -map_metadata 1"; + + public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask; + + + public void Pre() => File.WriteAllText(_tempFileName, _metaDataContent); + + public void Post() => File.Delete(_tempFileName); + } +} diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs index 847e68c..45750c6 100644 --- a/FFMpegCore/FFMpeg/FFMpegArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs @@ -38,6 +38,7 @@ public FFMpegArguments WithGlobalOptions(Action configure public FFMpegArguments AddFileInput(FileInfo fileInfo, Action? addArguments = null) => WithInput(new InputArgument(fileInfo.FullName, false), addArguments); public FFMpegArguments AddUrlInput(Uri uri, Action? addArguments = null) => WithInput(new InputArgument(uri.AbsoluteUri, false), addArguments); public FFMpegArguments AddPipeInput(IPipeSource sourcePipe, Action? addArguments = null) => WithInput(new InputPipeArgument(sourcePipe), addArguments); + public FFMpegArguments AddMetaData(string content, Action? addArguments = null) => WithInput(new MetaDataArgument(content), addArguments); private FFMpegArguments WithInput(IInputArgument inputArgument, Action? addArguments) { From 5c8597670c0d565b7c325055b0ff6d5a784d8594 Mon Sep 17 00:00:00 2001 From: Weirdo Date: Tue, 21 Dec 2021 00:34:33 +0100 Subject: [PATCH 09/17] Added MetadataBuilder Former-commit-id: 2605ac1a54dcc5b76325bcc4045bc20e4b64fba4 --- .../FFMpeg/Builders/MetaData/ChapterData.cs | 18 ++++ .../Builders/MetaData/IReadOnlyMetaData.cs | 11 +++ .../FFMpeg/Builders/MetaData/MetaData.cs | 33 ++++++++ .../Builders/MetaData/MetaDataBuilder.cs | 82 +++++++++++++++++++ .../Builders/MetaData/MetaDataSerializer.cs | 38 +++++++++ .../Builders/MetaData/ReadOnlyMetaData.cs | 25 ++++++ FFMpegCore/FFMpeg/FFMpegArguments.cs | 2 + 7 files changed, 209 insertions(+) create mode 100644 FFMpegCore/FFMpeg/Builders/MetaData/ChapterData.cs create mode 100644 FFMpegCore/FFMpeg/Builders/MetaData/IReadOnlyMetaData.cs create mode 100644 FFMpegCore/FFMpeg/Builders/MetaData/MetaData.cs create mode 100644 FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs create mode 100644 FFMpegCore/FFMpeg/Builders/MetaData/MetaDataSerializer.cs create mode 100644 FFMpegCore/FFMpeg/Builders/MetaData/ReadOnlyMetaData.cs diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/ChapterData.cs b/FFMpegCore/FFMpeg/Builders/MetaData/ChapterData.cs new file mode 100644 index 0000000..24ad2b6 --- /dev/null +++ b/FFMpegCore/FFMpeg/Builders/MetaData/ChapterData.cs @@ -0,0 +1,18 @@ +using System; + +namespace FFMpegCore.Builders.MetaData +{ + public class ChapterData + { + public string Title { get; private set; } + public TimeSpan Start { get; private set; } + public TimeSpan End { get; private set; } + + public ChapterData(string title, TimeSpan start, TimeSpan end) + { + Title = title; + Start = start; + End = end; + } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/IReadOnlyMetaData.cs b/FFMpegCore/FFMpeg/Builders/MetaData/IReadOnlyMetaData.cs new file mode 100644 index 0000000..fd55ea7 --- /dev/null +++ b/FFMpegCore/FFMpeg/Builders/MetaData/IReadOnlyMetaData.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace FFMpegCore.Builders.MetaData +{ + + public interface IReadOnlyMetaData + { + IReadOnlyList Chapters { get; } + IReadOnlyDictionary Entries { get; } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/MetaData.cs b/FFMpegCore/FFMpeg/Builders/MetaData/MetaData.cs new file mode 100644 index 0000000..2efc696 --- /dev/null +++ b/FFMpegCore/FFMpeg/Builders/MetaData/MetaData.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; + +namespace FFMpegCore.Builders.MetaData +{ + public class MetaData : IReadOnlyMetaData + { + public Dictionary Entries { get; private set; } + public List Chapters { get; private set; } + + IReadOnlyList IReadOnlyMetaData.Chapters => this.Chapters; + IReadOnlyDictionary IReadOnlyMetaData.Entries => this.Entries; + + public MetaData() + { + Entries = new Dictionary(); + Chapters = new List(); + } + + public MetaData(MetaData cloneSource) + { + Entries = new Dictionary(cloneSource.Entries); + Chapters = cloneSource.Chapters + .Select(x => new ChapterData + ( + start: x.Start, + end: x.End, + title: x.Title + )) + .ToList(); + } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs new file mode 100644 index 0000000..90513ac --- /dev/null +++ b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs @@ -0,0 +1,82 @@ +using System; +using System.Linq; + +namespace FFMpegCore.Builders.MetaData +{ + public class MetaDataBuilder + { + private MetaData _metaData = new MetaData(); + + public MetaDataBuilder WithEntry(string key, string value) + { + _metaData.Entries[key] = value; + return this; + } + + public MetaDataBuilder AddChapter(ChapterData chapterData) + { + _metaData.Chapters.Add(chapterData); + return this; + } + + public MetaDataBuilder AddChapter(TimeSpan duration, string? title = null) + { + var start = _metaData.Chapters.LastOrDefault()?.End ?? TimeSpan.Zero; + var end = start + duration; + title = String.IsNullOrEmpty(title) ? $"Chapter {_metaData.Chapters.Count + 1}" : title; + + _metaData.Chapters.Add(new ChapterData + ( + start: start, + end: end, + title: title + )); + + return this; + } + + //major_brand=M4A + public MetaDataBuilder WithMajorBrand(string value) => WithEntry("major_brand", value); + + //minor_version=512 + public MetaDataBuilder WithMinorVersion(string value) => WithEntry("minor_version", value); + + //compatible_brands=M4A isomiso2 + public MetaDataBuilder WithCompatibleBrands(string value) => WithEntry("compatible_brands", value); + + //copyright=©2017 / 2019 Dennis E. Taylor / Random House Audio / Wilhelm Heyne Verlag. Übersetzung von Urban Hofstetter (P)2019 Random House Audio + public MetaDataBuilder WithCopyright(string value) => WithEntry("copyright", value); + + //title=Alle diese Welten: Bobiverse 3 + public MetaDataBuilder WithTitle(string value) => WithEntry("title", value); + + //artist=Dennis E. Taylor + public MetaDataBuilder WithArtist(string value) => WithEntry("artist", value); + + //composer=J. K. Rowling + public MetaDataBuilder WithComposer(string value) => WithEntry("composer", value); + + //album_artist=Dennis E. Taylor + public MetaDataBuilder WithAlbumArtist(string value) => WithEntry("album_artist", value); + + //album=Alle diese Welten: Bobiverse 3 + public MetaDataBuilder WithAlbum(string value) => WithEntry("album", value); + + //date=2019 + public MetaDataBuilder WithDate(string value) => WithEntry("date", value); + + //genre=Hörbuch + public MetaDataBuilder WithGenre(string value) => WithEntry("genre", value); + + //comment=Chapter 200 + public MetaDataBuilder WithComment(string value) => WithEntry("comment", value); + + //encoder=Lavf58.47.100 + public MetaDataBuilder WithEncoder(string value) => WithEntry("encoder", value); + + public ReadOnlyMetaData Build() + { + return new MetaData(_metaData); + } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataSerializer.cs b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataSerializer.cs new file mode 100644 index 0000000..1a6f176 --- /dev/null +++ b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataSerializer.cs @@ -0,0 +1,38 @@ +using System.Linq; +using System.Text; + +namespace FFMpegCore.Builders.MetaData +{ + public class MetaDataSerializer + { + public static readonly MetaDataSerializer Instance = new MetaDataSerializer(); + + public string Serialize(IReadOnlyMetaData metaData) + { + var sb = new StringBuilder() + .AppendLine(";FFMETADATA1"); + + foreach (var value in metaData.Entries) + { + sb.AppendLine($"{value.Key}={value.Value}"); + } + + int chapterNumber = 0; + foreach (var chapter in metaData.Chapters ?? Enumerable.Empty()) + { + chapterNumber++; + var title = string.IsNullOrEmpty(chapter.Title) ? $"Chapter {chapterNumber}" : chapter.Title; + + sb + .AppendLine("[CHAPTER]") + .AppendLine($"TIMEBASE=1/1000") + .AppendLine($"START={(int)chapter.Start.TotalMilliseconds}") + .AppendLine($"END={(int)chapter.End.TotalMilliseconds}") + .AppendLine($"title={title}") + ; + } + + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/ReadOnlyMetaData.cs b/FFMpegCore/FFMpeg/Builders/MetaData/ReadOnlyMetaData.cs new file mode 100644 index 0000000..ff9bae9 --- /dev/null +++ b/FFMpegCore/FFMpeg/Builders/MetaData/ReadOnlyMetaData.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; + +namespace FFMpegCore.Builders.MetaData +{ + public class ReadOnlyMetaData : IReadOnlyMetaData + { + public IReadOnlyDictionary Entries { get; private set; } + public IReadOnlyList Chapters { get; private set; } + + public ReadOnlyMetaData(MetaData metaData) + { + Entries = new Dictionary(metaData.Entries); + Chapters = metaData.Chapters + .Select(x => new ChapterData + ( + start: x.Start, + end: x.End, + title: x.Title + )) + .ToList() + .AsReadOnly(); + } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs index 45750c6..6c9784d 100644 --- a/FFMpegCore/FFMpeg/FFMpegArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using FFMpegCore.Arguments; +using FFMpegCore.Builders.MetaData; using FFMpegCore.Pipes; namespace FFMpegCore @@ -39,6 +40,7 @@ public FFMpegArguments WithGlobalOptions(Action configure public FFMpegArguments AddUrlInput(Uri uri, Action? addArguments = null) => WithInput(new InputArgument(uri.AbsoluteUri, false), addArguments); public FFMpegArguments AddPipeInput(IPipeSource sourcePipe, Action? addArguments = null) => WithInput(new InputPipeArgument(sourcePipe), addArguments); public FFMpegArguments AddMetaData(string content, Action? addArguments = null) => WithInput(new MetaDataArgument(content), addArguments); + public FFMpegArguments AddMetaData(IReadOnlyMetaData metaData, Action? addArguments = null) => WithInput(new MetaDataArgument(MetaDataSerializer.Instance.Serialize(metaData)), addArguments); private FFMpegArguments WithInput(IInputArgument inputArgument, Action? addArguments) { From f38562aa68bb359685da50dcb37b0009da86074a Mon Sep 17 00:00:00 2001 From: Weirdo Date: Tue, 21 Dec 2021 00:40:45 +0100 Subject: [PATCH 10/17] Implemented AddChapters for convenience Former-commit-id: e2dc91660cef004cba6ca3ccba9ef9c822f874d3 --- .../FFMpeg/Builders/MetaData/MetaDataBuilder.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs index 90513ac..7409543 100644 --- a/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs +++ b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; namespace FFMpegCore.Builders.MetaData @@ -19,6 +20,17 @@ public MetaDataBuilder AddChapter(ChapterData chapterData) return this; } + public MetaDataBuilder AddChapters(IEnumerable values, Func chapterGetter) + { + foreach (T value in values) + { + var (duration, title) = chapterGetter(value); + AddChapter(duration, title); + } + + return this; + } + public MetaDataBuilder AddChapter(TimeSpan duration, string? title = null) { var start = _metaData.Chapters.LastOrDefault()?.End ?? TimeSpan.Zero; From 532c4abcb3f959842f2416604aface521cb4998a Mon Sep 17 00:00:00 2001 From: Weirdo Date: Tue, 21 Dec 2021 00:42:20 +0100 Subject: [PATCH 11/17] Returning ReadOnlyMetaData instead of MetaData Former-commit-id: 6194dc4e4b3eb80875b0d2331b7a1c230f0cda9f --- FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs index 7409543..8c7360b 100644 --- a/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs +++ b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs @@ -88,7 +88,7 @@ public MetaDataBuilder AddChapter(TimeSpan duration, string? title = null) public ReadOnlyMetaData Build() { - return new MetaData(_metaData); + return new ReadOnlyMetaData(_metaData); } } } \ No newline at end of file From 65bb8ecd2bafa8a7a13997b83868717a38bed728 Mon Sep 17 00:00:00 2001 From: Jonas Kamsker <11245306+JKamsker@users.noreply.github.com> Date: Tue, 4 Jan 2022 16:05:40 +0100 Subject: [PATCH 12/17] Added string escape for DemuxConcatArgument Fixes issue when source files have a ``'`` in their path Former-commit-id: 21d1df824f54d311d2ce9e3b9e60d208687f370b --- FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs b/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs index c672c74..47564f9 100644 --- a/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs @@ -16,8 +16,17 @@ public class DemuxConcatArgument : IInputArgument public readonly IEnumerable Values; public DemuxConcatArgument(IEnumerable values) { - Values = values.Select(value => $"file '{value}'"); + Values = values.Select(value => $"file '{Escape(value)}'"); } + + /// + /// Thanks slhck + /// https://superuser.com/a/787651/1089628 + /// + /// + /// + private string Escape(string value) => value.Replace("'", @"'\''"); + private readonly string _tempFileName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, $"concat_{Guid.NewGuid()}.txt"); public void Pre() => File.WriteAllLines(_tempFileName, Values); From e950cdf0686d3fb1368d4a05719948acc61f2b53 Mon Sep 17 00:00:00 2001 From: Jonas Kamsker Date: Tue, 4 Jan 2022 17:47:10 +0100 Subject: [PATCH 13/17] Added unit test Former-commit-id: e79448df485b017a7d5fde49efd17b38e98df4c2 --- FFMpegCore.Test/FFMpegArgumentProcessorTest.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs b/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs index dc65489..8443d0d 100644 --- a/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs +++ b/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs @@ -1,6 +1,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using FluentAssertions; using System.Reflection; +using FFMpegCore.Arguments; namespace FFMpegCore.Test { @@ -86,5 +87,12 @@ FFMpegArgumentProcessor CreateArgumentProcessor() => FFMpegArguments var options2 = processor2.GetConfiguredOptions(null); options2.WorkingDirectory.Should().Be(globalWorkingDir); } + + [TestMethod] + public void Concat_Escape() + { + var arg = new DemuxConcatArgument(new[] { @"Heaven's River\05 - Investigation.m4b" }); + arg.Values.Should().BeEquivalentTo(new[] { @"file 'Heaven'\''s River\05 - Investigation.m4b'" }); + } } } \ No newline at end of file From e1a5c8aea288e802ad416e5a825630381f62bdeb Mon Sep 17 00:00:00 2001 From: Weirdo Date: Tue, 4 Jan 2022 16:02:47 +0100 Subject: [PATCH 14/17] Revert "Added string escape for DemuxConcatArgument" This reverts commit ecbdae40af53d99581204342f6f44318774b495e. Former-commit-id: 9b670f11b28705831375a927ca78687d257fb274 From 1f0a4f2a0e94df8141f01aaeabb6663632828401 Mon Sep 17 00:00:00 2001 From: Jonas Kamsker Date: Thu, 6 Jan 2022 19:38:37 +0100 Subject: [PATCH 15/17] Refactored, added unit test Former-commit-id: f560244628a147dd24a8a257f2e15cd4b5833881 --- FFMpegCore.Test/MetaDataBuilderTests.cs | 54 +++++++++++++++++++ .../Builders/MetaData/MetaDataBuilder.cs | 39 +++++++++----- 2 files changed, 81 insertions(+), 12 deletions(-) create mode 100644 FFMpegCore.Test/MetaDataBuilderTests.cs diff --git a/FFMpegCore.Test/MetaDataBuilderTests.cs b/FFMpegCore.Test/MetaDataBuilderTests.cs new file mode 100644 index 0000000..5f0a144 --- /dev/null +++ b/FFMpegCore.Test/MetaDataBuilderTests.cs @@ -0,0 +1,54 @@ +using FFMpegCore.Builders.MetaData; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.Test +{ + [TestClass] + public class MetaDataBuilderTests + { + [TestMethod] + public void TestMetaDataBuilderIntegrity() + { + var source = new + { + Album = "Kanon und Gigue", + Artist = "Pachelbel", + Title = "Kanon und Gigue in D-Dur", + Copyright = "Copyright Lol", + Composer = "Pachelbel", + Genres = new[] { "Synthwave", "Classics" }, + Tracks = new[] + { + new { Duration = TimeSpan.FromSeconds(10), Title = "Chapter 01" }, + new { Duration = TimeSpan.FromSeconds(10), Title = "Chapter 02" }, + new { Duration = TimeSpan.FromSeconds(10), Title = "Chapter 03" }, + new { Duration = TimeSpan.FromSeconds(10), Title = "Chapter 04" }, + } + }; + + var builder = new MetaDataBuilder() + .WithTitle(source.Title) + .WithArtists(source.Artist) + .WithComposers(source.Composer) + .WithAlbumArtists(source.Artist) + .WithGenres(source.Genres) + .WithCopyright(source.Copyright) + .AddChapters(source.Tracks, x => (x.Duration, x.Title)); + + var metadata = builder.Build(); + var serialized = MetaDataSerializer.Instance.Serialize(metadata); + + Assert.IsTrue(serialized.StartsWith(";FFMETADATA1", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(serialized.Contains("genre=Synthwave; Classics", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(serialized.Contains("title=Chapter 01", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(serialized.Contains("album_artist=Pachelbel", StringComparison.OrdinalIgnoreCase)); + } + } +} diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs index 8c7360b..29c13c2 100644 --- a/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs +++ b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs @@ -8,12 +8,23 @@ public class MetaDataBuilder { private MetaData _metaData = new MetaData(); - public MetaDataBuilder WithEntry(string key, string value) + public MetaDataBuilder WithEntry(string key, string entry) { - _metaData.Entries[key] = value; + if (_metaData.Entries.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value)) + { + entry = String.Concat(value, "; ", entry); + } + + _metaData.Entries[key] = entry; return this; } + public MetaDataBuilder WithEntry(string key, params string[] values) + => this.WithEntry(key, String.Join("; ", values)); + + public MetaDataBuilder WithEntry(string key, IEnumerable values) + => this.WithEntry(key, String.Join("; ", values)); + public MetaDataBuilder AddChapter(ChapterData chapterData) { _metaData.Chapters.Add(chapterData); @@ -41,7 +52,7 @@ public MetaDataBuilder AddChapter(TimeSpan duration, string? title = null) ( start: start, end: end, - title: title + title: title ?? String.Empty )); return this; @@ -63,13 +74,16 @@ public MetaDataBuilder AddChapter(TimeSpan duration, string? title = null) public MetaDataBuilder WithTitle(string value) => WithEntry("title", value); //artist=Dennis E. Taylor - public MetaDataBuilder WithArtist(string value) => WithEntry("artist", value); + public MetaDataBuilder WithArtists(params string[] value) => WithEntry("artist", value); + public MetaDataBuilder WithArtists(IEnumerable value) => WithEntry("artist", value); //composer=J. K. Rowling - public MetaDataBuilder WithComposer(string value) => WithEntry("composer", value); + public MetaDataBuilder WithComposers(params string[] value) => WithEntry("composer", value); + public MetaDataBuilder WithComposers(IEnumerable value) => WithEntry("composer", value); //album_artist=Dennis E. Taylor - public MetaDataBuilder WithAlbumArtist(string value) => WithEntry("album_artist", value); + public MetaDataBuilder WithAlbumArtists(params string[] value) => WithEntry("album_artist", value); + public MetaDataBuilder WithAlbumArtists(IEnumerable value) => WithEntry("album_artist", value); //album=Alle diese Welten: Bobiverse 3 public MetaDataBuilder WithAlbum(string value) => WithEntry("album", value); @@ -78,17 +92,18 @@ public MetaDataBuilder AddChapter(TimeSpan duration, string? title = null) public MetaDataBuilder WithDate(string value) => WithEntry("date", value); //genre=Hörbuch - public MetaDataBuilder WithGenre(string value) => WithEntry("genre", value); + public MetaDataBuilder WithGenres(params string[] value) => WithEntry("genre", value); + public MetaDataBuilder WithGenres(IEnumerable value) => WithEntry("genre", value); //comment=Chapter 200 - public MetaDataBuilder WithComment(string value) => WithEntry("comment", value); + public MetaDataBuilder WithComments(params string[] value) => WithEntry("comment", value); + public MetaDataBuilder WithComments(IEnumerable value) => WithEntry("comment", value); //encoder=Lavf58.47.100 public MetaDataBuilder WithEncoder(string value) => WithEntry("encoder", value); - public ReadOnlyMetaData Build() - { - return new ReadOnlyMetaData(_metaData); - } + + + public ReadOnlyMetaData Build() => new ReadOnlyMetaData(_metaData); } } \ No newline at end of file From 81ee9b98726b677ee9eb4d62aaa082ba8b09a70d Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sat, 8 Jan 2022 12:27:14 +0100 Subject: [PATCH 16/17] Update packages Former-commit-id: 1c65502b90bbe9ad5ba2a5e5b38ee2faf2d2453a --- FFMpegCore.Test/FFMpegCore.Test.csproj | 8 ++++---- FFMpegCore/FFMpegCore.csproj | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/FFMpegCore.Test/FFMpegCore.Test.csproj b/FFMpegCore.Test/FFMpegCore.Test.csproj index 6388724..5d49065 100644 --- a/FFMpegCore.Test/FFMpegCore.Test.csproj +++ b/FFMpegCore.Test/FFMpegCore.Test.csproj @@ -39,11 +39,11 @@ - + - - - + + + diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index fbf7ea4..2273075 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -37,7 +37,7 @@ - + From 40a28f92e74ca0bb2d882ebed3d007a24f6ae55c Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sat, 8 Jan 2022 12:35:10 +0100 Subject: [PATCH 17/17] Update nuget meta Former-commit-id: 0502b415328e6169a44917bcca198182a99a94ef --- FFMpegCore/FFMpegCore.csproj | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index 2273075..afabd90 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -8,18 +8,13 @@ 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 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 + - Added libx265 static codec prop +- Support for reading Packets from mediafile through ffprobe (thanks zhuker) +- Support for fluent configuration of FFOptinos per-run (thanks BobSilent) +- Support for adding metadata (thanks Weirdo) +- Automatically escape single quotes in filenames for DemuxConcatArgument (thanks JKamsker) 8 - 4.6.0 + 4.7.0 MIT Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev ffmpeg ffprobe convert video audio mediafile resize analyze muxing