From f71e172f662f27424c2c3001403a07dfc39d3729 Mon Sep 17 00:00:00 2001 From: Yan Gauthier Date: Thu, 15 Aug 2024 15:26:03 -0400 Subject: [PATCH] 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 @@ public async Task Video_Cancel_CancellationToken_Async_With_Timeout() 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 @@ private FFMpegArguments WithInput(IInputArgument inputArgument, 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 @@ public class FFMpegMultiOutputOptions 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();