From 96ec0613d367a851386bd8409d540be35269924d Mon Sep 17 00:00:00 2001 From: alex6dj Date: Thu, 5 Aug 2021 14:37:32 -0400 Subject: [PATCH] Subtitle hard-burn implementation. Former-commit-id: 3a890623846eb5a0e978882afbda4c3e1d5b1508 --- FFMpegCore.Test/ArgumentBuilderTest.cs | 21 +++++ FFMpegCore/Extend/KeyValuePairExtensions.cs | 15 ++++ FFMpegCore/Extend/StringExtensions.cs | 10 +++ .../Arguments/SubtitleHardBurnArgument.cs | 89 +++++++++++++++++++ .../FFMpeg/Arguments/VideoFiltersArgument.cs | 1 + 5 files changed, 136 insertions(+) create mode 100644 FFMpegCore/Extend/KeyValuePairExtensions.cs create mode 100644 FFMpegCore/Extend/StringExtensions.cs create mode 100644 FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs index daa3eda..71bc90b 100644 --- a/FFMpegCore.Test/ArgumentBuilderTest.cs +++ b/FFMpegCore.Test/ArgumentBuilderTest.cs @@ -317,6 +317,27 @@ public void Builder_BuildString_DrawtextFilter_Alt() str); } + [TestMethod] + public void Builder_BuildString_SubtitleHardBurnFilter() + { + var str = FFMpegArguments + .FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt + .WithVideoFilters(filterOptions => filterOptions + .HardBurnSubtitle(SubtitleHardBurnOptions + .Create(subtitlePath: "sample.srt") + .SetCharacterEncoding("UTF-8") + .SetOriginalSize(1366,768) + .SetSubtitleIndex(0) + .WithStyle(StyleOptions.Create() + .WithParameter("FontName", "DejaVu Serif") + .WithParameter("PrimaryColour", "&HAA00FF00"))))) + .Arguments; + + Assert.AreEqual("-i \"input.mp4\" -vf \"subtitles=sample.srt:charenc=UTF-8:original_size=1366x768:si=0:force_style='FontName=DejaVu Serif\\,PrimaryColour=&HAA00FF00'\" \"output.mp4\"", + str); + } + [TestMethod] public void Builder_BuildString_StartNumber() { diff --git a/FFMpegCore/Extend/KeyValuePairExtensions.cs b/FFMpegCore/Extend/KeyValuePairExtensions.cs new file mode 100644 index 0000000..92dbf6d --- /dev/null +++ b/FFMpegCore/Extend/KeyValuePairExtensions.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace FFMpegCore.Extend +{ + 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; + + return $"{key}={value}"; + } + } +} \ No newline at end of file diff --git a/FFMpegCore/Extend/StringExtensions.cs b/FFMpegCore/Extend/StringExtensions.cs new file mode 100644 index 0000000..f4e0169 --- /dev/null +++ b/FFMpegCore/Extend/StringExtensions.cs @@ -0,0 +1,10 @@ +namespace FFMpegCore.Extend +{ + internal static class StringExtensions + { + public static string EncloseIfContainsSpace(this string input) + { + return input.Contains(" ") ? $"'{input}'" : input; + } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs b/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs new file mode 100644 index 0000000..552a87b --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs @@ -0,0 +1,89 @@ +using FFMpegCore.Extend; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; + +namespace FFMpegCore.Arguments +{ + public class SubtitleHardBurnArgument : IVideoFilterArgument + { + private readonly SubtitleHardBurnOptions _subtitleHardBurnOptions; + + public SubtitleHardBurnArgument(SubtitleHardBurnOptions subtitleHardBurnOptions) + { + _subtitleHardBurnOptions = subtitleHardBurnOptions; + } + + public string Key => "subtitles"; + + public string Value => _subtitleHardBurnOptions.TextInternal; + } + + public class SubtitleHardBurnOptions + { + private readonly string _subtitle; + + public readonly Dictionary Parameters = new Dictionary(); + + public static SubtitleHardBurnOptions Create(string subtitlePath) + { + return new SubtitleHardBurnOptions(subtitlePath); + } + + private SubtitleHardBurnOptions(string subtitle) + { + _subtitle = subtitle; + } + + public SubtitleHardBurnOptions SetOriginalSize(int width, int height) + { + return WithParameter("original_size", $"{width}x{height}"); + } + + public SubtitleHardBurnOptions SetOriginalSize(Size size) + { + return SetOriginalSize(size.Width, size.Height); + } + + public SubtitleHardBurnOptions SetSubtitleIndex(int index) + { + return WithParameter("si", index.ToString()); + } + + public SubtitleHardBurnOptions SetCharacterEncoding(string encode) + { + return WithParameter("charenc", encode); + } + + public SubtitleHardBurnOptions WithStyle(StyleOptions styleOptions) + { + return WithParameter("force_style", styleOptions.TextInternal); + } + + public SubtitleHardBurnOptions WithParameter(string key, string value) + { + Parameters.Add(key, value); + return this; + } + + internal string TextInternal => string.Join(":", new[] { _subtitle.EncloseIfContainsSpace() }.Concat(Parameters.Select(parameter => parameter.FormatArgumentPair(enclose: true)))); + } + + public class StyleOptions + { + public readonly Dictionary Parameters = new Dictionary(); + + public static StyleOptions Create() + { + return new StyleOptions(); + } + + public StyleOptions WithParameter(string key, string value) + { + Parameters.Add(key, value); + return this; + } + + internal string TextInternal => string.Join(",", Parameters.Select(parameter => parameter.FormatArgumentPair(enclose: false))); + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs b/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs index fa4ae1e..4d0dfde 100644 --- a/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs @@ -50,6 +50,7 @@ public class VideoFilterOptions public VideoFilterOptions Transpose(Transposition transposition) => WithArgument(new TransposeArgument(transposition)); public VideoFilterOptions Mirror(Mirroring mirroring) => WithArgument(new SetMirroringArgument(mirroring)); public VideoFilterOptions DrawText(DrawTextOptions drawTextOptions) => WithArgument(new DrawTextArgument(drawTextOptions)); + public VideoFilterOptions HardBurnSubtitle(SubtitleHardBurnOptions subtitleHardBurnOptions) => WithArgument(new SubtitleHardBurnArgument(subtitleHardBurnOptions)); private VideoFilterOptions WithArgument(IVideoFilterArgument argument) {