From a6e90e20789b95fce34261103bda44981b10a213 Mon Sep 17 00:00:00 2001 From: Yan Gauthier Date: Thu, 15 Aug 2024 10:08:18 -0400 Subject: [PATCH 1/7] Updated System.Text.Json because of vulnerabilities --- FFMpegCore/FFMpegCore.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index ed3b71c..7e279fb 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -18,7 +18,7 @@ - + From f71e172f662f27424c2c3001403a07dfc39d3729 Mon Sep 17 00:00:00 2001 From: Yan Gauthier Date: Thu, 15 Aug 2024 15:26:03 -0400 Subject: [PATCH 2/7] Added option to use the segment muxer allowing output to file to be segmented. There's a SegmentCustomArgument for the arguments I did not implement. --- FFMpegCore.Test/VideoTest.cs | 106 ++++++++++++++++++ .../FFMpeg/Arguments/OutputSegmentArgument.cs | 92 +++++++++++++++ .../FFMpeg/Arguments/SegmentCustomArgument.cs | 14 +++ .../SegmentResetTimestampsArgument.cs | 20 ++++ .../Arguments/SegmentStrftimeArgument.cs | 20 ++++ .../FFMpeg/Arguments/SegmentTimeArgument.cs | 20 ++++ .../FFMpeg/Arguments/SegmentWrapArgument.cs | 20 ++++ FFMpegCore/FFMpeg/FFMpegArguments.cs | 2 +- FFMpegCore/FFMpeg/FFMpegMultiOutputOptions.cs | 2 +- 9 files changed, 294 insertions(+), 2 deletions(-) create mode 100644 FFMpegCore/FFMpeg/Arguments/OutputSegmentArgument.cs create mode 100644 FFMpegCore/FFMpeg/Arguments/SegmentCustomArgument.cs create mode 100644 FFMpegCore/FFMpeg/Arguments/SegmentResetTimestampsArgument.cs create mode 100644 FFMpegCore/FFMpeg/Arguments/SegmentStrftimeArgument.cs create mode 100644 FFMpegCore/FFMpeg/Arguments/SegmentTimeArgument.cs create mode 100644 FFMpegCore/FFMpeg/Arguments/SegmentWrapArgument.cs diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 5071a48..1de87e0 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -16,6 +16,22 @@ namespace FFMpegCore.Test public class VideoTest { private const int BaseTimeoutMilliseconds = 15_000; + private string _segmentPathSource = ""; + + [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)] public void Video_ToOGV() @@ -895,5 +911,95 @@ namespace FFMpegCore.Test Assert.AreEqual("h264", outputInfo.PrimaryVideoStream.CodecName); Assert.AreEqual("aac", outputInfo.PrimaryAudioStream!.CodecName); } + + [TestMethod, Timeout(BaseTimeoutMilliseconds)] + 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(-1) + .Time(60) + .ResetTimeStamps(true)), + 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(new CancellationTokenSource().Token, 8000) + .ProcessSynchronously(false); + Assert.IsTrue(success); + } + + [TestMethod, Timeout(BaseTimeoutMilliseconds)] + 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(-1) + .Time(60) + .ResetTimeStamps(true)), + 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(new CancellationTokenSource().Token, 8000) + .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..f6113cb --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/OutputSegmentArgument.cs @@ -0,0 +1,92 @@ +using FFMpegCore.Exceptions; + +namespace FFMpegCore.Arguments +{ + /// + /// Represents output parameter + /// + public class OutputSegmentArgument : IOutputArgument + { + public readonly string SegmentPattern; + public readonly bool Overwrite; + public readonly SegmentArgumentOptions Options; + + 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) => 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 + { + public string Key { get; } + public string Value { get; } + } + + public class SegmentArgumentOptions + { + public List Arguments { get; } = new(); + + public SegmentArgumentOptions ResetTimeStamps(bool resetTimestamps = true) => WithArgument(new SegmentResetTimeStampsArgument(resetTimestamps)); + public SegmentArgumentOptions Strftime(bool enable = false) => WithArgument(new SegmentStrftimeArgument(enable)); + public SegmentArgumentOptions Time(int time = 60) => WithArgument(new SegmentTimeArgument(time)); + public SegmentArgumentOptions Wrap(int limit = -1) => WithArgument(new SegmentWrapArgument(limit)); + public SegmentArgumentOptions WithCustomArgument(string argument) => WithArgument(new SegmentCustomArgument(argument)); + private SegmentArgumentOptions WithArgument(ISegmentArgument argument) + { + Arguments.Add(argument); + return this; + } + } + + public class SegmentArgument + { + public readonly string SegmentPattern; + public readonly bool Overwrite; + public readonly Action Options; + + 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..359e529 --- /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..f9a048f --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/SegmentResetTimestampsArgument.cs @@ -0,0 +1,20 @@ +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..b50552a --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/SegmentStrftimeArgument.cs @@ -0,0 +1,20 @@ +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..5a42e29 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/SegmentTimeArgument.cs @@ -0,0 +1,20 @@ +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..be38208 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/SegmentWrapArgument.cs @@ -0,0 +1,20 @@ +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 cfc6d9d..4cb2c19 100644 --- a/FFMpegCore/FFMpeg/FFMpegArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs @@ -61,7 +61,7 @@ namespace FFMpegCore public FFMpegArgumentProcessor OutputToUrl(string uri, Action? addArguments = null) => ToProcessor(new OutputUrlArgument(uri), addArguments); public FFMpegArgumentProcessor OutputToUrl(Uri uri, Action? addArguments = null) => ToProcessor(new OutputUrlArgument(uri.ToString()), addArguments); public FFMpegArgumentProcessor OutputToPipe(IPipeSink reader, Action? addArguments = null) => ToProcessor(new OutputPipeArgument(reader), addArguments); - + public FFMpegArgumentProcessor OutPutToSegmentedFiles(SegmentArgument segmentArgument, Action? addArguments = null) => 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 594413b..b0d5359 100644 --- a/FFMpegCore/FFMpeg/FFMpegMultiOutputOptions.cs +++ b/FFMpegCore/FFMpeg/FFMpegMultiOutputOptions.cs @@ -16,7 +16,7 @@ namespace FFMpegCore public FFMpegMultiOutputOptions OutputToUrl(Uri uri, Action? addArguments = null) => AddOutput(new OutputUrlArgument(uri.ToString()), addArguments); public FFMpegMultiOutputOptions OutputToPipe(IPipeSink reader, Action? addArguments = null) => AddOutput(new OutputPipeArgument(reader), addArguments); - + public FFMpegMultiOutputOptions OutPutToSegmentedFiles(SegmentArgument segmentArgument, Action? addArguments = null) => AddOutput(new OutputSegmentArgument(segmentArgument), addArguments); public FFMpegMultiOutputOptions AddOutput(IOutputArgument argument, Action? addArguments) { var args = new FFMpegArgumentOptions(); From 91e8e1e18da36f2b6bb3a750f6e2b48bc8065c03 Mon Sep 17 00:00:00 2001 From: Yan Gauthier Date: Thu, 15 Aug 2024 15:28:01 -0400 Subject: [PATCH 3/7] Added JsonIgnore to Encoding on FFOptions since a breacking change in System.Text.Json introduced in .NET8.0 prevent us from deserializing this type https://github.com/dotnet/docs/issues/37041 --- FFMpegCore/FFOptions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/FFMpegCore/FFOptions.cs b/FFMpegCore/FFOptions.cs index 3194874..52a3a0a 100644 --- a/FFMpegCore/FFOptions.cs +++ b/FFMpegCore/FFOptions.cs @@ -1,4 +1,5 @@ using System.Text; +using System.Text.Json.Serialization; using FFMpegCore.Enums; namespace FFMpegCore @@ -20,6 +21,7 @@ namespace FFMpegCore /// public string TemporaryFilesFolder { get; set; } = Path.GetTempPath(); + [JsonIgnore] /// /// Encoding used for parsing stdout/stderr on ffmpeg and ffprobe processes /// From 935980568cf86f9c3f4df4b482f9e762d2564fdb Mon Sep 17 00:00:00 2001 From: Yan Gauthier Date: Thu, 15 Aug 2024 16:32:12 -0400 Subject: [PATCH 4/7] Removed excessive whitespace --- FFMpegCore.Test/VideoTest.cs | 3 --- FFMpegCore/FFMpeg/Arguments/OutputSegmentArgument.cs | 10 +++------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 1de87e0..b54c557 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -17,13 +17,11 @@ namespace FFMpegCore.Test { private const int BaseTimeoutMilliseconds = 15_000; private string _segmentPathSource = ""; - [TestInitialize] public void Setup() { _segmentPathSource = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}-"); } - [TestCleanup] public void Cleanup() { @@ -32,7 +30,6 @@ namespace FFMpegCore.Test File.Delete(file); } } - [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_ToOGV() { diff --git a/FFMpegCore/FFMpeg/Arguments/OutputSegmentArgument.cs b/FFMpegCore/FFMpeg/Arguments/OutputSegmentArgument.cs index f6113cb..73b63f7 100644 --- a/FFMpegCore/FFMpeg/Arguments/OutputSegmentArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/OutputSegmentArgument.cs @@ -10,12 +10,10 @@ namespace FFMpegCore.Arguments public readonly string SegmentPattern; public readonly bool Overwrite; public readonly SegmentArgumentOptions Options; - public OutputSegmentArgument(SegmentArgument segmentArgument) { SegmentPattern = segmentArgument.SegmentPattern; Overwrite = segmentArgument.Overwrite; - var segmentArgumentobj = new SegmentArgumentOptions(); segmentArgument.Options?.Invoke(segmentArgumentobj); Options = segmentArgumentobj; @@ -24,12 +22,11 @@ namespace FFMpegCore.Arguments 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"); + { + 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"); } @@ -40,7 +37,6 @@ namespace FFMpegCore.Arguments } public string Text => GetText(); - private string GetText() { var arguments = Options.Arguments @@ -82,7 +78,7 @@ namespace FFMpegCore.Arguments public readonly bool Overwrite; public readonly Action Options; - public SegmentArgument(string segmentPattern, bool overwrite, Action options) + public SegmentArgument(string segmentPattern, bool overwrite, Action options) { SegmentPattern = segmentPattern; Overwrite = overwrite; From c60e217a2f2dfffe8c10d2f810bc38f3df9cf648 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 16 Oct 2025 15:00:43 +0200 Subject: [PATCH 5/7] Update code after update from main --- FFMpegCore.Test/VideoTest.cs | 138 +++++++++++++++++++++++---- FFMpegCore/FFMpeg/FFMpegArguments.cs | 7 +- 2 files changed, 124 insertions(+), 21 deletions(-) diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index c13ef64..0d17cb6 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -18,22 +18,26 @@ namespace FFMpegCore.Test; public class VideoTest { private const int BaseTimeoutMilliseconds = 15_000; - private string _segmentPathSource = ""; - [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); - } - } + + 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() @@ -1081,11 +1085,105 @@ public class VideoTest var outputInfo = await FFProbe.AnalyseAsync(outputFile, cancellationToken: TestContext.CancellationToken); - Assert.IsNotNull(outputInfo); - Assert.AreEqual(320, outputInfo.PrimaryVideoStream!.Width); - Assert.AreEqual(240, outputInfo.PrimaryVideoStream.Height); - Assert.AreEqual("h264", outputInfo.PrimaryVideoStream.CodecName); - Assert.AreEqual("aac", outputInfo.PrimaryAudioStream!.CodecName); - } + Assert.IsNotNull(outputInfo); + Assert.AreEqual(320, outputInfo.PrimaryVideoStream!.Width); + Assert.AreEqual(240, outputInfo.PrimaryVideoStream.Height); + 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/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs index 20df625..355d938 100644 --- a/FFMpegCore/FFMpeg/FFMpegArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs @@ -151,7 +151,12 @@ public sealed class FFMpegArguments : FFMpegArgumentsBase { return ToProcessor(new OutputPipeArgument(reader), addArguments); } - public FFMpegArgumentProcessor OutPutToSegmentedFiles(SegmentArgument segmentArgument, Action? addArguments = null) => ToProcessor(new OutputSegmentArgument(segmentArgument), 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(); From 095687087567d35e842c2e14c59a10d278798e96 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 16 Oct 2025 15:14:14 +0200 Subject: [PATCH 6/7] Run code cleanup --- .../FFMpeg/Arguments/OutputSegmentArgument.cs | 178 ++++++++++-------- .../FFMpeg/Arguments/SegmentCustomArgument.cs | 22 +-- .../SegmentResetTimestampsArgument.cs | 29 +-- .../Arguments/SegmentStrftimeArgument.cs | 31 +-- .../FFMpeg/Arguments/SegmentTimeArgument.cs | 29 +-- .../FFMpeg/Arguments/SegmentWrapArgument.cs | 29 +-- FFMpegCore/FFOptions.cs | 113 +++++------ 7 files changed, 232 insertions(+), 199 deletions(-) diff --git a/FFMpegCore/FFMpeg/Arguments/OutputSegmentArgument.cs b/FFMpegCore/FFMpeg/Arguments/OutputSegmentArgument.cs index 73b63f7..a3f5843 100644 --- a/FFMpegCore/FFMpeg/Arguments/OutputSegmentArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/OutputSegmentArgument.cs @@ -1,88 +1,114 @@ using FFMpegCore.Exceptions; -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents output parameter +/// +public class OutputSegmentArgument : IOutputArgument { - /// - /// Represents output parameter - /// - public class OutputSegmentArgument : IOutputArgument + public readonly SegmentArgumentOptions Options; + public readonly bool Overwrite; + public readonly string SegmentPattern; + + public OutputSegmentArgument(SegmentArgument segmentArgument) { - public readonly string SegmentPattern; - public readonly bool Overwrite; - public readonly SegmentArgumentOptions Options; - 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) { - SegmentPattern = segmentArgument.SegmentPattern; - Overwrite = segmentArgument.Overwrite; - var segmentArgumentobj = new SegmentArgumentOptions(); - segmentArgument.Options?.Invoke(segmentArgumentobj); - Options = segmentArgumentobj; + throw new FFMpegException(FFMpegExceptionType.Process, "Parameter SegmentTime cannot be negative or equal to zero"); } - public void Pre() + if (Options.Arguments.FirstOrDefault(x => x.Key == "segment_time").Value == "0") { - if (int.TryParse(Options.Arguments.FirstOrDefault(x => x.Key == "segment_time").Value, out var result) && result < 1) + 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 => { - throw new FFMpegException(FFMpegExceptionType.Process, "Parameter SegmentTime cannot be negative or equal to zero"); - } + return arg.Value; + }); - 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) => 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 - { - public string Key { get; } - public string Value { get; } - } - - public class SegmentArgumentOptions - { - public List Arguments { get; } = new(); - - public SegmentArgumentOptions ResetTimeStamps(bool resetTimestamps = true) => WithArgument(new SegmentResetTimeStampsArgument(resetTimestamps)); - public SegmentArgumentOptions Strftime(bool enable = false) => WithArgument(new SegmentStrftimeArgument(enable)); - public SegmentArgumentOptions Time(int time = 60) => WithArgument(new SegmentTimeArgument(time)); - public SegmentArgumentOptions Wrap(int limit = -1) => WithArgument(new SegmentWrapArgument(limit)); - public SegmentArgumentOptions WithCustomArgument(string argument) => WithArgument(new SegmentCustomArgument(argument)); - private SegmentArgumentOptions WithArgument(ISegmentArgument argument) - { - Arguments.Add(argument); - return this; - } - } - - public class SegmentArgument - { - public readonly string SegmentPattern; - public readonly bool Overwrite; - public readonly Action Options; - - public SegmentArgument(string segmentPattern, bool overwrite, Action options) - { - SegmentPattern = segmentPattern; - Overwrite = overwrite; - Options = options; - } + return $"-f segment {string.Join(" ", arguments)} \"{SegmentPattern}\"{(Overwrite ? " -y" : string.Empty)}"; + } +} + +public interface ISegmentArgument +{ + public string Key { get; } + public 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 index 359e529..f90503c 100644 --- a/FFMpegCore/FFMpeg/Arguments/SegmentCustomArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/SegmentCustomArgument.cs @@ -1,14 +1,14 @@ -namespace FFMpegCore.Arguments -{ - public class SegmentCustomArgument : ISegmentArgument - { - public readonly string Argument; +namespace FFMpegCore.Arguments; - public SegmentCustomArgument(string argument) - { - Argument = argument; - } - public string Key => "custom"; - public string Value => Argument ?? string.Empty; +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 index f9a048f..34a95e7 100644 --- a/FFMpegCore/FFMpeg/Arguments/SegmentResetTimestampsArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/SegmentResetTimestampsArgument.cs @@ -1,20 +1,21 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents reset_timestamps parameter +/// +public class SegmentResetTimeStampsArgument : ISegmentArgument { + public readonly bool ResetTimestamps; + /// - /// Represents reset_timestamps parameter + /// Represents reset_timestamps parameter /// - public class SegmentResetTimeStampsArgument : ISegmentArgument + /// true if files timestamps are to be reset + public SegmentResetTimeStampsArgument(bool resetTimestamps) { - 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; + 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 index b50552a..57dfc9d 100644 --- a/FFMpegCore/FFMpeg/Arguments/SegmentStrftimeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/SegmentStrftimeArgument.cs @@ -1,20 +1,23 @@ -namespace FFMpegCore.Arguments +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. + /// 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 + /// true to enable strftime + public SegmentStrftimeArgument(bool enable) { - 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; + 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 index 5a42e29..c791f5f 100644 --- a/FFMpegCore/FFMpeg/Arguments/SegmentTimeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/SegmentTimeArgument.cs @@ -1,20 +1,21 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents segment_time parameter +/// +public class SegmentTimeArgument : ISegmentArgument { + public readonly int Time; + /// - /// Represents segment_time parameter + /// Represents segment_time parameter /// - public class SegmentTimeArgument : ISegmentArgument + /// time in seconds of the segment + public SegmentTimeArgument(int time) { - 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}"; + 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 index be38208..811e47c 100644 --- a/FFMpegCore/FFMpeg/Arguments/SegmentWrapArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/SegmentWrapArgument.cs @@ -1,20 +1,21 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents segment_wrap parameter +/// +public class SegmentWrapArgument : ISegmentArgument { + public readonly int Limit; + /// - /// Represents segment_wrap parameter + /// Represents segment_wrap parameter /// - public class SegmentWrapArgument : ISegmentArgument + /// limit value after which segment index will wrap around + public SegmentWrapArgument(int limit) { - 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}"; + Limit = limit; } + + public string Key { get; } = "segment_wrap"; + public string Value => Limit <= 0 ? string.Empty : $"-segment_wrap {Limit}"; } diff --git a/FFMpegCore/FFOptions.cs b/FFMpegCore/FFOptions.cs index 2d4e4c9..ea784e5 100644 --- a/FFMpegCore/FFOptions.cs +++ b/FFMpegCore/FFOptions.cs @@ -2,68 +2,69 @@ using System.Text.Json.Serialization; using FFMpegCore.Enums; -namespace FFMpegCore +namespace FFMpegCore; + +public class FFOptions : ICloneable { - public class FFOptions : ICloneable + /// + /// Working directory for the ffmpeg/ffprobe instance + /// + public string WorkingDirectory { get; set; } = string.Empty; + + /// + /// Folder container ffmpeg and ffprobe binaries. Leave empty if ffmpeg and ffprobe are present in PATH + /// + public string BinaryFolder { get; set; } = string.Empty; + + /// + /// Folder used for temporary files necessary for static methods on FFMpeg class + /// + public string TemporaryFilesFolder { get; set; } = Path.GetTempPath(); + + /// + /// Encoding web name used to persist encoding + /// + public string EncodingWebName { get; set; } = Encoding.Default.WebName; + + /// + /// Encoding used for parsing stdout/stderr on ffmpeg and ffprobe processes + /// + [JsonIgnore] + public Encoding Encoding { - /// - /// Working directory for the ffmpeg/ffprobe instance - /// - public string WorkingDirectory { get; set; } = string.Empty; + get => Encoding.GetEncoding(EncodingWebName); + set => EncodingWebName = value?.WebName ?? Encoding.Default.WebName; + } - /// - /// Folder container ffmpeg and ffprobe binaries. Leave empty if ffmpeg and ffprobe are present in PATH - /// - public string BinaryFolder { get; set; } = string.Empty; + /// + /// The log level to use when calling of the ffmpeg executable. + /// + /// This option can be overridden before an execution of a Process command + /// to set the log level for that command. + /// + /// + public FFMpegLogLevel? LogLevel { get; set; } - /// - /// Folder used for temporary files necessary for static methods on FFMpeg class - /// - public string TemporaryFilesFolder { get; set; } = Path.GetTempPath(); + /// + /// + public Dictionary ExtensionOverrides { get; set; } = new() { { "mpegts", ".ts" } }; - /// - /// Encoding web name used to persist encoding - /// - public string EncodingWebName { get; set; } = Encoding.Default.WebName; + /// + /// Whether to cache calls to get ffmpeg codec, pixel- and container-formats + /// + public bool UseCache { get; set; } = true; - /// - /// Encoding used for parsing stdout/stderr on ffmpeg and ffprobe processes - /// - [JsonIgnore] - public Encoding Encoding - { - get => Encoding.GetEncoding(EncodingWebName); - set => EncodingWebName = value?.WebName ?? Encoding.Default.WebName; - } + /// + object ICloneable.Clone() + { + return Clone(); + } - /// - /// The log level to use when calling of the ffmpeg executable. - /// - /// This option can be overridden before an execution of a Process command - /// to set the log level for that command. - /// - /// - public FFMpegLogLevel? LogLevel { get; set; } - - /// - /// - /// - public Dictionary ExtensionOverrides { get; set; } = new() - { - { "mpegts", ".ts" }, - }; - - /// - /// Whether to cache calls to get ffmpeg codec, pixel- and container-formats - /// - public bool UseCache { get; set; } = true; - - /// - object ICloneable.Clone() => Clone(); - - /// - /// Creates a new object that is a copy of the current instance. - /// - public FFOptions Clone() => (FFOptions)MemberwiseClone(); + /// + /// Creates a new object that is a copy of the current instance. + /// + public FFOptions Clone() + { + return (FFOptions)MemberwiseClone(); } } From b10cf5fd761b5231b86a5e8d8a94c272c32bb273 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 16 Oct 2025 15:17:19 +0200 Subject: [PATCH 7/7] Remove accessibility modifiers on interface --- FFMpegCore/FFMpeg/Arguments/OutputSegmentArgument.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FFMpegCore/FFMpeg/Arguments/OutputSegmentArgument.cs b/FFMpegCore/FFMpeg/Arguments/OutputSegmentArgument.cs index a3f5843..b979c62 100644 --- a/FFMpegCore/FFMpeg/Arguments/OutputSegmentArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/OutputSegmentArgument.cs @@ -59,8 +59,8 @@ public class OutputSegmentArgument : IOutputArgument public interface ISegmentArgument { - public string Key { get; } - public string Value { get; } + string Key { get; } + string Value { get; } } public class SegmentArgumentOptions