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() diff --git a/FFMpegCore/FFMetadataInputArgument.cs b/FFMpegCore/FFMetadataInputArgument.cs new file mode 100644 index 0000000..f2fce59 --- /dev/null +++ b/FFMpegCore/FFMetadataInputArgument.cs @@ -0,0 +1,62 @@ +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; + } +} 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);