From 3a890623846eb5a0e978882afbda4c3e1d5b1508 Mon Sep 17 00:00:00 2001 From: alex6dj Date: Thu, 5 Aug 2021 14:37:32 -0400 Subject: [PATCH 1/3] Subtitle hard-burn implementation. --- 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) { From 6247bf6ea4a0ce4bc74e1f93d4192419c27f3e7a Mon Sep 17 00:00:00 2001 From: alex6dj Date: Thu, 5 Aug 2021 15:11:23 -0400 Subject: [PATCH 2/3] Document parameters --- FFMpegCore/Extend/KeyValuePairExtensions.cs | 9 ++++ FFMpegCore/Extend/StringExtensions.cs | 5 +++ .../Arguments/SubtitleHardBurnArgument.cs | 43 +++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/FFMpegCore/Extend/KeyValuePairExtensions.cs b/FFMpegCore/Extend/KeyValuePairExtensions.cs index 92dbf6d..28cc087 100644 --- a/FFMpegCore/Extend/KeyValuePairExtensions.cs +++ b/FFMpegCore/Extend/KeyValuePairExtensions.cs @@ -4,6 +4,15 @@ namespace FFMpegCore.Extend { internal static class KeyValuePairExtensions { + /// + /// Concat the two members of a + /// + /// Input object + /// + /// If true encloses the value part between quotes if contains an space character. If false use the + /// value unmodified + /// + /// The formatted string public static string FormatArgumentPair(this KeyValuePair pair, bool enclose) { var key = pair.Key; diff --git a/FFMpegCore/Extend/StringExtensions.cs b/FFMpegCore/Extend/StringExtensions.cs index f4e0169..ddcf54b 100644 --- a/FFMpegCore/Extend/StringExtensions.cs +++ b/FFMpegCore/Extend/StringExtensions.cs @@ -2,6 +2,11 @@ { internal static class StringExtensions { + /// + /// Enclose string between quotes if contains an space character + /// + /// The input + /// The enclosed string public static string EncloseIfContainsSpace(this string input) { return input.Contains(" ") ? $"'{input}'" : input; diff --git a/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs b/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs index 552a87b..1186ae2 100644 --- a/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs @@ -25,6 +25,13 @@ public class SubtitleHardBurnOptions public readonly Dictionary Parameters = new Dictionary(); + /// + /// Create a new using a provided subtitle file or a video file + /// containing one. + /// + /// + /// + /// Only support .srt and .ass files, and subrip and ssa subtitle streams public static SubtitleHardBurnOptions Create(string subtitlePath) { return new SubtitleHardBurnOptions(subtitlePath); @@ -35,26 +42,56 @@ private SubtitleHardBurnOptions(string subtitle) _subtitle = subtitle; } + /// + /// Specify the size of the original video, the video for which the ASS file was composed. + /// + /// + /// + /// public SubtitleHardBurnOptions SetOriginalSize(int width, int height) { return WithParameter("original_size", $"{width}x{height}"); } + /// + /// Specify the size of the original video, the video for which the ASS file was composed. + /// + /// + /// public SubtitleHardBurnOptions SetOriginalSize(Size size) { return SetOriginalSize(size.Width, size.Height); } + /// + /// Set subtitles stream index. + /// + /// + /// + /// + /// Used when the provided subtitle is an stream of a video file (ex. .mkv) with multiple subtitles. + /// Represent the index of the subtitle not the stream, them the first subtitle index is 0 and second is 1 + /// public SubtitleHardBurnOptions SetSubtitleIndex(int index) { return WithParameter("si", index.ToString()); } + /// + /// Set subtitles input character encoding. Only useful if not UTF-8 + /// + /// Charset encoding + /// public SubtitleHardBurnOptions SetCharacterEncoding(string encode) { return WithParameter("charenc", encode); } + /// + /// Override default style or script info parameters of the subtitles + /// + /// + /// public SubtitleHardBurnOptions WithStyle(StyleOptions styleOptions) { return WithParameter("force_style", styleOptions.TextInternal); @@ -78,6 +115,12 @@ public static StyleOptions Create() return new StyleOptions(); } + /// + /// Used to override default style or script info parameters of the subtitles. It accepts ASS style format + /// + /// + /// + /// public StyleOptions WithParameter(string key, string value) { Parameters.Add(key, value); From 14d457b94661bcc481f246c1f0472b30874cd12a Mon Sep 17 00:00:00 2001 From: alex6dj Date: Thu, 5 Aug 2021 15:14:37 -0400 Subject: [PATCH 3/3] Use long option for stream index for better clarity --- FFMpegCore.Test/ArgumentBuilderTest.cs | 2 +- FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs index 71bc90b..6fd108d 100644 --- a/FFMpegCore.Test/ArgumentBuilderTest.cs +++ b/FFMpegCore.Test/ArgumentBuilderTest.cs @@ -334,7 +334,7 @@ public void Builder_BuildString_SubtitleHardBurnFilter() .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\"", + 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); } diff --git a/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs b/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs index 1186ae2..a48f845 100644 --- a/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs @@ -74,7 +74,7 @@ public SubtitleHardBurnOptions SetOriginalSize(Size size) /// public SubtitleHardBurnOptions SetSubtitleIndex(int index) { - return WithParameter("si", index.ToString()); + return WithParameter("stream_index", index.ToString()); } ///