diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs
index 8e8fc59..cd43a06 100644
--- a/FFMpegCore.Test/VideoTest.cs
+++ b/FFMpegCore.Test/VideoTest.cs
@@ -19,8 +19,25 @@ public class VideoTest
{
private const int BaseTimeoutMilliseconds = 60_000;
+ private string _segmentPathSource = "";
+
public TestContext TestContext { get; set; }
+ [TestInitialize]
+ public void Setup()
+ {
+ _segmentPathSource = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}-");
+ }
+
+ [TestCleanup]
+ public void Cleanup()
+ {
+ foreach (var file in Directory.EnumerateFiles(Path.GetDirectoryName(_segmentPathSource), Path.GetFileName(_segmentPathSource) + "*"))
+ {
+ File.Delete(file);
+ }
+ }
+
[TestMethod]
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
public void Video_ToOGV()
@@ -1275,4 +1292,99 @@ public class VideoTest
Assert.AreEqual("h264", outputInfo.PrimaryVideoStream.CodecName);
Assert.AreEqual("aac", outputInfo.PrimaryAudioStream!.CodecName);
}
+
+ [TestMethod]
+ [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
+ public void Video_Segmented_File_Output()
+ {
+ using var input = File.OpenRead(TestResources.WebmVideo);
+ var success = FFMpegArguments
+ .FromPipeInput(new StreamPipeSource(input))
+ .OutPutToSegmentedFiles(
+ new SegmentArgument($"{_segmentPathSource}%Y-%m-%d_%H-%M-%S.mkv", true, segmentOptions => segmentOptions
+ .Strftime(true)
+ .Wrap()
+ .Time()
+ .ResetTimeStamps()),
+ options => options
+ .CopyChannel()
+ .WithVideoCodec("h264")
+ .ForceFormat("matroska")
+ .WithConstantRateFactor(21)
+ .WithVideoBitrate(3000)
+ .WithFastStart()
+ .WithVideoFilters(filterOptions => filterOptions
+ .Scale(VideoSize.Hd)
+ .DrawText(DrawTextOptions.Create(@"'%{localtime}.%{eif\:1M*t-1K*trunc(t*1K)\:d\:3}'",
+ @"C:/Users/yan.gauthier/AppData/Local/Microsoft/Windows/Fonts/Roboto-Regular.ttf")
+ .WithParameter("fontcolor", "yellow")
+ .WithParameter("fontsize", "40")
+ .WithParameter("x", "(w-text_w)")
+ .WithParameter("y", "(h - text_h)")
+ .WithParameter("rate", "19")
+ )
+ )
+ )
+ .CancellableThrough(TestContext.CancellationToken)
+ .ProcessSynchronously(false);
+ Assert.IsTrue(success);
+ }
+
+ [TestMethod]
+ [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
+ public void Video_MultiOutput_With_Segmented_File_Output()
+ {
+ using var input = File.OpenRead(TestResources.WebmVideo);
+ var success = FFMpegArguments
+ .FromPipeInput(new StreamPipeSource(input))
+ .MultiOutput(args => args
+ .OutputToFile($"{_segmentPathSource}2", true, options => options
+ .CopyChannel()
+ .WithVideoCodec("mjpeg")
+ .ForceFormat("matroska")
+ .WithConstantRateFactor(21)
+ .WithVideoBitrate(4000)
+ .WithFastStart()
+ .WithVideoFilters(filterOptions => filterOptions
+ .Scale(VideoSize.Hd)
+ .DrawText(DrawTextOptions.Create(@"'%{localtime}.%{eif\:1M*t-1K*trunc(t*1K)\:d\:3}'",
+ @"C:/Users/yan.gauthier/AppData/Local/Microsoft/Windows/Fonts/Roboto-Regular.ttf")
+ .WithParameter("fontcolor", "yellow")
+ .WithParameter("fontsize", "40")
+ .WithParameter("x", "(w-text_w)")
+ .WithParameter("y", "(h - text_h)")
+ .WithParameter("rate", "19")
+ )
+ )
+ )
+ .OutPutToSegmentedFiles(
+ new SegmentArgument($"{_segmentPathSource}%Y-%m-%d_%H-%M-%S.mkv", true, segmentOptions => segmentOptions
+ .Strftime(true)
+ .Wrap()
+ .Time()
+ .ResetTimeStamps()),
+ options => options
+ .CopyChannel()
+ .WithVideoCodec("h264")
+ .ForceFormat("matroska")
+ .WithConstantRateFactor(21)
+ .WithVideoBitrate(3000)
+ .WithFastStart()
+ .WithVideoFilters(filterOptions => filterOptions
+ .Scale(VideoSize.Hd)
+ .DrawText(DrawTextOptions.Create(@"'%{localtime}.%{eif\:1M*t-1K*trunc(t*1K)\:d\:3}'",
+ @"C:/Users/yan.gauthier/AppData/Local/Microsoft/Windows/Fonts/Roboto-Regular.ttf")
+ .WithParameter("fontcolor", "yellow")
+ .WithParameter("fontsize", "40")
+ .WithParameter("x", "(w-text_w)")
+ .WithParameter("y", "(h - text_h)")
+ .WithParameter("rate", "19")
+ )
+ )
+ )
+ )
+ .CancellableThrough(TestContext.CancellationToken)
+ .ProcessSynchronously(false);
+ Assert.IsTrue(success);
+ }
}
diff --git a/FFMpegCore/FFMpeg/Arguments/OutputSegmentArgument.cs b/FFMpegCore/FFMpeg/Arguments/OutputSegmentArgument.cs
new file mode 100644
index 0000000..b979c62
--- /dev/null
+++ b/FFMpegCore/FFMpeg/Arguments/OutputSegmentArgument.cs
@@ -0,0 +1,114 @@
+using FFMpegCore.Exceptions;
+
+namespace FFMpegCore.Arguments;
+
+///
+/// Represents output parameter
+///
+public class OutputSegmentArgument : IOutputArgument
+{
+ public readonly SegmentArgumentOptions Options;
+ public readonly bool Overwrite;
+ public readonly string SegmentPattern;
+
+ public OutputSegmentArgument(SegmentArgument segmentArgument)
+ {
+ SegmentPattern = segmentArgument.SegmentPattern;
+ Overwrite = segmentArgument.Overwrite;
+ var segmentArgumentobj = new SegmentArgumentOptions();
+ segmentArgument.Options?.Invoke(segmentArgumentobj);
+ Options = segmentArgumentobj;
+ }
+
+ public void Pre()
+ {
+ if (int.TryParse(Options.Arguments.FirstOrDefault(x => x.Key == "segment_time").Value, out var result) && result < 1)
+ {
+ throw new FFMpegException(FFMpegExceptionType.Process, "Parameter SegmentTime cannot be negative or equal to zero");
+ }
+
+ if (Options.Arguments.FirstOrDefault(x => x.Key == "segment_time").Value == "0")
+ {
+ throw new FFMpegException(FFMpegExceptionType.Process, "Parameter SegmentWrap cannot equal to zero");
+ }
+ }
+
+ public Task During(CancellationToken cancellationToken = default)
+ {
+ return Task.CompletedTask;
+ }
+
+ public void Post()
+ {
+ }
+
+ public string Text => GetText();
+
+ private string GetText()
+ {
+ var arguments = Options.Arguments
+ .Where(arg => !string.IsNullOrWhiteSpace(arg.Value) && !string.IsNullOrWhiteSpace(arg.Key))
+ .Select(arg =>
+ {
+ return arg.Value;
+ });
+
+ return $"-f segment {string.Join(" ", arguments)} \"{SegmentPattern}\"{(Overwrite ? " -y" : string.Empty)}";
+ }
+}
+
+public interface ISegmentArgument
+{
+ string Key { get; }
+ string Value { get; }
+}
+
+public class SegmentArgumentOptions
+{
+ public List Arguments { get; } = new();
+
+ public SegmentArgumentOptions ResetTimeStamps(bool resetTimestamps = true)
+ {
+ return WithArgument(new SegmentResetTimeStampsArgument(resetTimestamps));
+ }
+
+ public SegmentArgumentOptions Strftime(bool enable = false)
+ {
+ return WithArgument(new SegmentStrftimeArgument(enable));
+ }
+
+ public SegmentArgumentOptions Time(int time = 60)
+ {
+ return WithArgument(new SegmentTimeArgument(time));
+ }
+
+ public SegmentArgumentOptions Wrap(int limit = -1)
+ {
+ return WithArgument(new SegmentWrapArgument(limit));
+ }
+
+ public SegmentArgumentOptions WithCustomArgument(string argument)
+ {
+ return WithArgument(new SegmentCustomArgument(argument));
+ }
+
+ private SegmentArgumentOptions WithArgument(ISegmentArgument argument)
+ {
+ Arguments.Add(argument);
+ return this;
+ }
+}
+
+public class SegmentArgument
+{
+ public readonly Action Options;
+ public readonly bool Overwrite;
+ public readonly string SegmentPattern;
+
+ public SegmentArgument(string segmentPattern, bool overwrite, Action options)
+ {
+ SegmentPattern = segmentPattern;
+ Overwrite = overwrite;
+ Options = options;
+ }
+}
diff --git a/FFMpegCore/FFMpeg/Arguments/SegmentCustomArgument.cs b/FFMpegCore/FFMpeg/Arguments/SegmentCustomArgument.cs
new file mode 100644
index 0000000..f90503c
--- /dev/null
+++ b/FFMpegCore/FFMpeg/Arguments/SegmentCustomArgument.cs
@@ -0,0 +1,14 @@
+namespace FFMpegCore.Arguments;
+
+public class SegmentCustomArgument : ISegmentArgument
+{
+ public readonly string Argument;
+
+ public SegmentCustomArgument(string argument)
+ {
+ Argument = argument;
+ }
+
+ public string Key => "custom";
+ public string Value => Argument ?? string.Empty;
+}
diff --git a/FFMpegCore/FFMpeg/Arguments/SegmentResetTimestampsArgument.cs b/FFMpegCore/FFMpeg/Arguments/SegmentResetTimestampsArgument.cs
new file mode 100644
index 0000000..34a95e7
--- /dev/null
+++ b/FFMpegCore/FFMpeg/Arguments/SegmentResetTimestampsArgument.cs
@@ -0,0 +1,21 @@
+namespace FFMpegCore.Arguments;
+
+///
+/// Represents reset_timestamps parameter
+///
+public class SegmentResetTimeStampsArgument : ISegmentArgument
+{
+ public readonly bool ResetTimestamps;
+
+ ///
+ /// Represents reset_timestamps parameter
+ ///
+ /// true if files timestamps are to be reset
+ public SegmentResetTimeStampsArgument(bool resetTimestamps)
+ {
+ ResetTimestamps = resetTimestamps;
+ }
+
+ public string Key { get; } = "reset_timestamps";
+ public string Value => ResetTimestamps ? "-reset_timestamps 1" : string.Empty;
+}
diff --git a/FFMpegCore/FFMpeg/Arguments/SegmentStrftimeArgument.cs b/FFMpegCore/FFMpeg/Arguments/SegmentStrftimeArgument.cs
new file mode 100644
index 0000000..57dfc9d
--- /dev/null
+++ b/FFMpegCore/FFMpeg/Arguments/SegmentStrftimeArgument.cs
@@ -0,0 +1,23 @@
+namespace FFMpegCore.Arguments;
+
+///
+/// Use the strftime function to define the name of the new segments to write. If this is selected, the output segment name must contain a
+/// strftime function template. Default value is 0.
+///
+public class SegmentStrftimeArgument : ISegmentArgument
+{
+ public readonly bool Enable;
+
+ ///
+ /// Use the strftime function to define the name of the new segments to write. If this is selected, the output segment name must contain a
+ /// strftime function template. Default value is 0.
+ ///
+ /// true to enable strftime
+ public SegmentStrftimeArgument(bool enable)
+ {
+ Enable = enable;
+ }
+
+ public string Key { get; } = "strftime";
+ public string Value => Enable ? "-strftime 1" : string.Empty;
+}
diff --git a/FFMpegCore/FFMpeg/Arguments/SegmentTimeArgument.cs b/FFMpegCore/FFMpeg/Arguments/SegmentTimeArgument.cs
new file mode 100644
index 0000000..c791f5f
--- /dev/null
+++ b/FFMpegCore/FFMpeg/Arguments/SegmentTimeArgument.cs
@@ -0,0 +1,21 @@
+namespace FFMpegCore.Arguments;
+
+///
+/// Represents segment_time parameter
+///
+public class SegmentTimeArgument : ISegmentArgument
+{
+ public readonly int Time;
+
+ ///
+ /// Represents segment_time parameter
+ ///
+ /// time in seconds of the segment
+ public SegmentTimeArgument(int time)
+ {
+ Time = time;
+ }
+
+ public string Key { get; } = "segment_time";
+ public string Value => Time <= 0 ? string.Empty : $"-segment_time {Time}";
+}
diff --git a/FFMpegCore/FFMpeg/Arguments/SegmentWrapArgument.cs b/FFMpegCore/FFMpeg/Arguments/SegmentWrapArgument.cs
new file mode 100644
index 0000000..811e47c
--- /dev/null
+++ b/FFMpegCore/FFMpeg/Arguments/SegmentWrapArgument.cs
@@ -0,0 +1,21 @@
+namespace FFMpegCore.Arguments;
+
+///
+/// Represents segment_wrap parameter
+///
+public class SegmentWrapArgument : ISegmentArgument
+{
+ public readonly int Limit;
+
+ ///
+ /// Represents segment_wrap parameter
+ ///
+ /// limit value after which segment index will wrap around
+ public SegmentWrapArgument(int limit)
+ {
+ Limit = limit;
+ }
+
+ public string Key { get; } = "segment_wrap";
+ public string Value => Limit <= 0 ? string.Empty : $"-segment_wrap {Limit}";
+}
diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs
index 927442d..13d6c2f 100644
--- a/FFMpegCore/FFMpeg/FFMpegArguments.cs
+++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs
@@ -157,6 +157,11 @@ public sealed class FFMpegArguments : FFMpegArgumentsBase
return ToProcessor(new OutputPipeArgument(reader), addArguments);
}
+ public FFMpegArgumentProcessor OutPutToSegmentedFiles(SegmentArgument segmentArgument, Action? addArguments = null)
+ {
+ return ToProcessor(new OutputSegmentArgument(segmentArgument), addArguments);
+ }
+
private FFMpegArgumentProcessor ToProcessor(IOutputArgument argument, Action? addArguments)
{
var args = new FFMpegArgumentOptions();
diff --git a/FFMpegCore/FFMpeg/FFMpegMultiOutputOptions.cs b/FFMpegCore/FFMpeg/FFMpegMultiOutputOptions.cs
index 170c2e5..4e086f8 100644
--- a/FFMpegCore/FFMpeg/FFMpegMultiOutputOptions.cs
+++ b/FFMpegCore/FFMpeg/FFMpegMultiOutputOptions.cs
@@ -29,6 +29,11 @@ public class FFMpegMultiOutputOptions
return AddOutput(new OutputPipeArgument(reader), addArguments);
}
+ public FFMpegMultiOutputOptions OutPutToSegmentedFiles(SegmentArgument segmentArgument, Action? addArguments = null)
+ {
+ return AddOutput(new OutputSegmentArgument(segmentArgument), addArguments);
+ }
+
public FFMpegMultiOutputOptions AddOutput(IOutputArgument argument, Action? addArguments)
{
var args = new FFMpegArgumentOptions();