diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs
index dda6c84..082d9bf 100644
--- a/FFMpegCore.Test/ArgumentBuilderTest.cs
+++ b/FFMpegCore.Test/ArgumentBuilderTest.cs
@@ -324,6 +324,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:stream_index=0:force_style='FontName=DejaVu Serif\\,PrimaryColour=&HAA00FF00'\" \"output.mp4\"",
+ str);
+ }
+
[TestMethod]
public void Builder_BuildString_StartNumber()
{
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
diff --git a/FFMpegCore/Extend/KeyValuePairExtensions.cs b/FFMpegCore/Extend/KeyValuePairExtensions.cs
new file mode 100644
index 0000000..28cc087
--- /dev/null
+++ b/FFMpegCore/Extend/KeyValuePairExtensions.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+
+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;
+ 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..ddcf54b
--- /dev/null
+++ b/FFMpegCore/Extend/StringExtensions.cs
@@ -0,0 +1,15 @@
+namespace FFMpegCore.Extend
+{
+ 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;
+ }
+ }
+}
\ 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..a48f845
--- /dev/null
+++ b/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs
@@ -0,0 +1,132 @@
+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();
+
+ ///
+ /// 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);
+ }
+
+ 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("stream_index", 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);
+ }
+
+ 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();
+ }
+
+ ///
+ /// 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);
+ 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)
{
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
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