From 97053929a9d6eebf758e3ae121a0b82fcabca9a4 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sat, 25 Oct 2025 11:25:16 +0200 Subject: [PATCH 1/5] Add FFMetadataBuilder for easily constructing metadata text --- FFMpegCore/FFMetadataInputArgument.cs | 63 +++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 FFMpegCore/FFMetadataInputArgument.cs diff --git a/FFMpegCore/FFMetadataInputArgument.cs b/FFMpegCore/FFMetadataInputArgument.cs new file mode 100644 index 0000000..68a2c2e --- /dev/null +++ b/FFMpegCore/FFMetadataInputArgument.cs @@ -0,0 +1,63 @@ +using System.Text; + +namespace FFMpegCore; + +public class FFMetadataBuilder +{ + private Dictionary Tags { get; } = new(); + private List Chapters { get; } = []; + + public static FFMetadataBuilder Empty() + { + return new FFMetadataBuilder(); + } + + public FFMetadataBuilder WithTag(string key, string value) + { + Tags.Add(key, value); + return this; + } + + public FFMetadataBuilder WithChapter(string title, long durationMs) + { + Chapters.Add(new FFMetadataChapter(title, durationMs)); + return this; + } + + public FFMetadataBuilder WithChapter(string title, double durationSeconds) + { + Chapters.Add(new FFMetadataChapter(title, Convert.ToInt64(durationSeconds * 1000))); + return this; + } + + public string GetMetadataFileContent() + { + var sb = new StringBuilder(); + sb.AppendLine(";FFMETADATA1"); + + foreach (var tag in Tags) + { + sb.AppendLine($"{tag.Key}={tag.Value}"); + } + + long totalDurationMs = 0; + foreach (var chapter in Chapters) + { + sb.AppendLine("[CHAPTER]"); + sb.AppendLine("TIMEBASE=1/1000"); + sb.AppendLine($"START={totalDurationMs}"); + sb.AppendLine($"END={totalDurationMs + chapter.DurationMs}"); + sb.AppendLine($"title={chapter.Title}"); + totalDurationMs += chapter.DurationMs; + } + + return sb.ToString(); + } + + + private class FFMetadataChapter(string title, long durationMs) + { + public string Title { get; } = title; + public long DurationMs { get; } = durationMs; + } +} From 62e829d9b484052ffea58a1b3302df72ac36b3d7 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sat, 25 Oct 2025 11:25:43 +0200 Subject: [PATCH 2/5] Add AddMetaData overload accepting FFMetadataBuilder instance --- FFMpegCore/FFMpeg/FFMpegArguments.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs index 4c3b7bf..927442d 100644 --- a/FFMpegCore/FFMpeg/FFMpegArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs @@ -109,6 +109,11 @@ public sealed class FFMpegArguments : FFMpegArgumentsBase return WithInput(new MetaDataArgument(content), addArguments); } + public FFMpegArguments AddMetaData(FFMetadataBuilder metaDataBuilder, Action? addArguments = null) + { + return WithInput(new MetaDataArgument(metaDataBuilder.GetMetadataFileContent()), addArguments); + } + public FFMpegArguments AddMetaData(IReadOnlyMetaData metaData, Action? addArguments = null) { return WithInput(new MetaDataArgument(MetaDataSerializer.Instance.Serialize(metaData)), addArguments); From ef313ea411f5600a5d87ee5f5a494a20f9d6b12f Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sat, 25 Oct 2025 11:25:52 +0200 Subject: [PATCH 3/5] Add test verifying functionality --- FFMpegCore.Test/VideoTest.cs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 1f54d9f..accba60 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -82,6 +82,40 @@ public class VideoTest Assert.IsTrue(success); } + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public async Task Video_MetadataBuilder() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + await FFMpegArguments + .FromFileInput(TestResources.WebmVideo) + .AddMetaData(FFMetadataBuilder.Empty() + .WithTag("title", "noname") + .WithTag("artist", "unknown") + .WithChapter("Chapter 1", 1.1) + .WithChapter("Chapter 2", 1.23)) + .OutputToFile(outputFile, false, opt => opt + .WithVideoCodec(VideoCodec.LibX264)) + .CancellableThrough(TestContext.CancellationToken) + .ProcessAsynchronously(); + + var analysis = await FFProbe.AnalyseAsync(outputFile, cancellationToken: TestContext.CancellationToken); + Assert.IsTrue(analysis.Format.Tags!.TryGetValue("title", out var title)); + Assert.IsTrue(analysis.Format.Tags!.TryGetValue("artist", out var artist)); + Assert.AreEqual("noname", title); + Assert.AreEqual("unknown", artist); + + Assert.HasCount(2, analysis.Chapters); + Assert.AreEqual("Chapter 1", analysis.Chapters.First().Title); + Assert.AreEqual(1.1, analysis.Chapters.First().Duration.TotalSeconds); + Assert.AreEqual(1.1, analysis.Chapters.First().End.TotalSeconds); + + Assert.AreEqual("Chapter 2", analysis.Chapters.Last().Title); + Assert.AreEqual(1.23, analysis.Chapters.Last().Duration.TotalSeconds); + Assert.AreEqual(1.1 + 1.23, analysis.Chapters.Last().End.TotalSeconds); + } + [TestMethod] [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] public void Video_ToH265_MKV_Args() From 15acd9f0dac97f5795f7215611400d30cf748145 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sat, 25 Oct 2025 11:28:47 +0200 Subject: [PATCH 4/5] Add BOM --- FFMpegCore/FFMetadataInputArgument.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFMpegCore/FFMetadataInputArgument.cs b/FFMpegCore/FFMetadataInputArgument.cs index 68a2c2e..cfd16c9 100644 --- a/FFMpegCore/FFMetadataInputArgument.cs +++ b/FFMpegCore/FFMetadataInputArgument.cs @@ -1,4 +1,4 @@ -using System.Text; +using System.Text; namespace FFMpegCore; From 53445322e41d341513ce3afc9eb5cc6cc575f2a1 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sat, 25 Oct 2025 11:36:40 +0200 Subject: [PATCH 5/5] Fix linting --- FFMpegCore/FFMetadataInputArgument.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/FFMpegCore/FFMetadataInputArgument.cs b/FFMpegCore/FFMetadataInputArgument.cs index cfd16c9..f2fce59 100644 --- a/FFMpegCore/FFMetadataInputArgument.cs +++ b/FFMpegCore/FFMetadataInputArgument.cs @@ -54,7 +54,6 @@ public class FFMetadataBuilder return sb.ToString(); } - private class FFMetadataChapter(string title, long durationMs) { public string Title { get; } = title;