From a2b37cb920cf346847e556c5dbfd0965053e1ada Mon Sep 17 00:00:00 2001 From: chaitanyabd Date: Sat, 31 Jul 2021 00:05:38 +0530 Subject: [PATCH 1/6] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9dce345..298489e 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,8 @@ The default value of an empty string (expecting ffmpeg to be found through PATH) GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" }); // or GlobalFFOptions.Configure(options => options.BinaryFolder = "./bin"); +// on some systems the absolute path may be required, in which case +GlobalFFOptions.Configure(new FFOptions { BinaryFolder = Server.MapPath("./bin"), TemporaryFilesFolder = Server.MapPath("/tmp") }); // or individual, per-run options await FFMpegArguments From f80d1fa3a66b9779a5d4353a2f6a784cba117b7a Mon Sep 17 00:00:00 2001 From: alex6dj Date: Sat, 31 Jul 2021 15:34:47 -0400 Subject: [PATCH 2/6] Basic ffprobe subtitle support --- FFMpegCore/FFProbe/IMediaAnalysis.cs | 2 ++ FFMpegCore/FFProbe/MediaAnalysis.cs | 21 ++++++++++++++++++--- FFMpegCore/FFProbe/SubtitleStream.cs | 7 +++++++ 3 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 FFMpegCore/FFProbe/SubtitleStream.cs diff --git a/FFMpegCore/FFProbe/IMediaAnalysis.cs b/FFMpegCore/FFProbe/IMediaAnalysis.cs index 4e67d4f..7be3b20 100644 --- a/FFMpegCore/FFProbe/IMediaAnalysis.cs +++ b/FFMpegCore/FFProbe/IMediaAnalysis.cs @@ -9,7 +9,9 @@ public interface IMediaAnalysis MediaFormat Format { get; } AudioStream? PrimaryAudioStream { get; } VideoStream? PrimaryVideoStream { get; } + SubtitleStream? PrimarySubtitleStream { get; } List VideoStreams { get; } List AudioStreams { get; } + List SubtitleStreams { get; } } } diff --git a/FFMpegCore/FFProbe/MediaAnalysis.cs b/FFMpegCore/FFProbe/MediaAnalysis.cs index 2602f86..d021813 100644 --- a/FFMpegCore/FFProbe/MediaAnalysis.cs +++ b/FFMpegCore/FFProbe/MediaAnalysis.cs @@ -12,8 +12,9 @@ internal MediaAnalysis(FFProbeAnalysis analysis) Format = ParseFormat(analysis.Format); VideoStreams = analysis.Streams.Where(stream => stream.CodecType == "video").Select(ParseVideoStream).ToList(); AudioStreams = analysis.Streams.Where(stream => stream.CodecType == "audio").Select(ParseAudioStream).ToList(); + SubtitleStreams = analysis.Streams.Where(stream => stream.CodecType == "subtitle").Select(ParseSubtitleStream).ToList(); } - + private MediaFormat ParseFormat(Format analysisFormat) { return new MediaFormat @@ -36,12 +37,14 @@ private MediaFormat ParseFormat(Format analysisFormat) }.Max(); public MediaFormat Format { get; } + public AudioStream? PrimaryAudioStream => AudioStreams.OrderBy(stream => stream.Index).FirstOrDefault(); - public VideoStream? PrimaryVideoStream => VideoStreams.OrderBy(stream => stream.Index).FirstOrDefault(); + public SubtitleStream? PrimarySubtitleStream => SubtitleStreams.OrderBy(stream => stream.Index).FirstOrDefault(); public List VideoStreams { get; } public List AudioStreams { get; } + public List SubtitleStreams { get; } private VideoStream ParseVideoStream(FFProbeStream stream) { @@ -84,7 +87,19 @@ private AudioStream ParseAudioStream(FFProbeStream stream) }; } - + private SubtitleStream ParseSubtitleStream(FFProbeStream stream) + { + return new SubtitleStream + { + Index = stream.Index, + BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitRate) : default, + CodecName = stream.CodecName, + CodecLongName = stream.CodecLongName, + Duration = MediaAnalysisUtils.ParseDuration(stream), + Language = stream.GetLanguage(), + Tags = stream.Tags, + }; + } } public static class MediaAnalysisUtils diff --git a/FFMpegCore/FFProbe/SubtitleStream.cs b/FFMpegCore/FFProbe/SubtitleStream.cs new file mode 100644 index 0000000..80493f4 --- /dev/null +++ b/FFMpegCore/FFProbe/SubtitleStream.cs @@ -0,0 +1,7 @@ +namespace FFMpegCore +{ + public class SubtitleStream : MediaStream + { + + } +} \ No newline at end of file From 47916eac29fef9a7f86de79522e1eed0ae6d6a62 Mon Sep 17 00:00:00 2001 From: alex6dj Date: Sat, 31 Jul 2021 16:46:21 -0400 Subject: [PATCH 3/6] Basic ffprobe test --- FFMpegCore.Test/FFMpegCore.Test.csproj | 3 +++ FFMpegCore.Test/FFProbeTests.cs | 10 ++++++++++ FFMpegCore.Test/Resources/TestResources.cs | 1 + FFMpegCore.Test/Resources/sample.srt | 12 ++++++++++++ 4 files changed, 26 insertions(+) create mode 100644 FFMpegCore.Test/Resources/sample.srt diff --git a/FFMpegCore.Test/FFMpegCore.Test.csproj b/FFMpegCore.Test/FFMpegCore.Test.csproj index 98c9274..2505545 100644 --- a/FFMpegCore.Test/FFMpegCore.Test.csproj +++ b/FFMpegCore.Test/FFMpegCore.Test.csproj @@ -83,6 +83,9 @@ Always + + PreserveNewest + diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs index 5cabc4e..897848d 100644 --- a/FFMpegCore.Test/FFProbeTests.cs +++ b/FFMpegCore.Test/FFProbeTests.cs @@ -100,5 +100,15 @@ public async Task Probe_Success_FromStream_Async() var info = await FFProbe.AnalyseAsync(stream); Assert.AreEqual(3, info.Duration.Seconds); } + + [TestMethod, Timeout(10000)] + public async Task Probe_Success_Subtitle_Async() + { + var info = await FFProbe.AnalyseAsync(TestResources.SrtSubtitle); + Assert.IsNotNull(info.PrimarySubtitleStream); + Assert.AreEqual(1, info.SubtitleStreams.Count); + Assert.AreEqual(0, info.AudioStreams.Count); + Assert.AreEqual(0, info.VideoStreams.Count); + } } } \ No newline at end of file diff --git a/FFMpegCore.Test/Resources/TestResources.cs b/FFMpegCore.Test/Resources/TestResources.cs index 6277dd3..14f8abe 100644 --- a/FFMpegCore.Test/Resources/TestResources.cs +++ b/FFMpegCore.Test/Resources/TestResources.cs @@ -20,5 +20,6 @@ public static class TestResources public static readonly string Mp3Audio = "./Resources/audio.mp3"; public static readonly string PngImage = "./Resources/cover.png"; public static readonly string ImageCollection = "./Resources/images"; + public static readonly string SrtSubtitle = "./Resources/sample.srt"; } } diff --git a/FFMpegCore.Test/Resources/sample.srt b/FFMpegCore.Test/Resources/sample.srt new file mode 100644 index 0000000..b08f594 --- /dev/null +++ b/FFMpegCore.Test/Resources/sample.srt @@ -0,0 +1,12 @@ +1 +00:00:00,000 --> 00:00:01,500 +For www.forom.com + +2 +00:00:01,500 --> 00:00:02,500 +Tonight's the night. + +3 +00:00:03,000 --> 00:00:15,000 +And it's going to happen +again and again -- \ No newline at end of file From 3a890623846eb5a0e978882afbda4c3e1d5b1508 Mon Sep 17 00:00:00 2001 From: alex6dj Date: Thu, 5 Aug 2021 14:37:32 -0400 Subject: [PATCH 4/6] 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 5/6] 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 6/6] 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()); } ///