Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions FFMpegCore.Test/VideoTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,25 @@ public class VideoTest
{
private const int BaseTimeoutMilliseconds = 15_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()
Expand Down Expand Up @@ -1074,4 +1091,99 @@ public async Task Video_Cancel_CancellationToken_Async_With_Timeout()
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);
}
}
114 changes: 114 additions & 0 deletions FFMpegCore/FFMpeg/Arguments/OutputSegmentArgument.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using FFMpegCore.Exceptions;

namespace FFMpegCore.Arguments;

/// <summary>
/// Represents output parameter
/// </summary>
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<ISegmentArgument> 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<SegmentArgumentOptions> Options;
public readonly bool Overwrite;
public readonly string SegmentPattern;

public SegmentArgument(string segmentPattern, bool overwrite, Action<SegmentArgumentOptions> options)
{
SegmentPattern = segmentPattern;
Overwrite = overwrite;
Options = options;
}
}
14 changes: 14 additions & 0 deletions FFMpegCore/FFMpeg/Arguments/SegmentCustomArgument.cs
Original file line number Diff line number Diff line change
@@ -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;
}
21 changes: 21 additions & 0 deletions FFMpegCore/FFMpeg/Arguments/SegmentResetTimestampsArgument.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace FFMpegCore.Arguments;

/// <summary>
/// Represents reset_timestamps parameter
/// </summary>
public class SegmentResetTimeStampsArgument : ISegmentArgument
{
public readonly bool ResetTimestamps;

/// <summary>
/// Represents reset_timestamps parameter
/// </summary>
/// <param name="resetTimestamps">true if files timestamps are to be reset</param>
public SegmentResetTimeStampsArgument(bool resetTimestamps)
{
ResetTimestamps = resetTimestamps;
}

public string Key { get; } = "reset_timestamps";
public string Value => ResetTimestamps ? "-reset_timestamps 1" : string.Empty;
}
23 changes: 23 additions & 0 deletions FFMpegCore/FFMpeg/Arguments/SegmentStrftimeArgument.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace FFMpegCore.Arguments;

/// <summary>
/// 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.
/// </summary>
public class SegmentStrftimeArgument : ISegmentArgument
{
public readonly bool Enable;

/// <summary>
/// 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.
/// </summary>
/// <param name="enable">true to enable strftime</param>
public SegmentStrftimeArgument(bool enable)
{
Enable = enable;
}

public string Key { get; } = "strftime";
public string Value => Enable ? "-strftime 1" : string.Empty;
}
21 changes: 21 additions & 0 deletions FFMpegCore/FFMpeg/Arguments/SegmentTimeArgument.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace FFMpegCore.Arguments;

/// <summary>
/// Represents segment_time parameter
/// </summary>
public class SegmentTimeArgument : ISegmentArgument
{
public readonly int Time;

/// <summary>
/// Represents segment_time parameter
/// </summary>
/// <param name="time">time in seconds of the segment</param>
public SegmentTimeArgument(int time)
{
Time = time;
}

public string Key { get; } = "segment_time";
public string Value => Time <= 0 ? string.Empty : $"-segment_time {Time}";
}
21 changes: 21 additions & 0 deletions FFMpegCore/FFMpeg/Arguments/SegmentWrapArgument.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace FFMpegCore.Arguments;

/// <summary>
/// Represents segment_wrap parameter
/// </summary>
public class SegmentWrapArgument : ISegmentArgument
{
public readonly int Limit;

/// <summary>
/// Represents segment_wrap parameter
/// </summary>
/// <param name="limit">limit value after which segment index will wrap around</param>
public SegmentWrapArgument(int limit)
{
Limit = limit;
}

public string Key { get; } = "segment_wrap";
public string Value => Limit <= 0 ? string.Empty : $"-segment_wrap {Limit}";
}
5 changes: 5 additions & 0 deletions FFMpegCore/FFMpeg/FFMpegArguments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@ public FFMpegArgumentProcessor OutputToPipe(IPipeSink reader, Action<FFMpegArgum
return ToProcessor(new OutputPipeArgument(reader), addArguments);
}

public FFMpegArgumentProcessor OutPutToSegmentedFiles(SegmentArgument segmentArgument, Action<FFMpegArgumentOptions>? addArguments = null)
{
return ToProcessor(new OutputSegmentArgument(segmentArgument), addArguments);
}

private FFMpegArgumentProcessor ToProcessor(IOutputArgument argument, Action<FFMpegArgumentOptions>? addArguments)
{
var args = new FFMpegArgumentOptions();
Expand Down
5 changes: 5 additions & 0 deletions FFMpegCore/FFMpeg/FFMpegMultiOutputOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ public FFMpegMultiOutputOptions OutputToPipe(IPipeSink reader, Action<FFMpegArgu
return AddOutput(new OutputPipeArgument(reader), addArguments);
}

public FFMpegMultiOutputOptions OutPutToSegmentedFiles(SegmentArgument segmentArgument, Action<FFMpegArgumentOptions>? addArguments = null)
{
return AddOutput(new OutputSegmentArgument(segmentArgument), addArguments);
}

public FFMpegMultiOutputOptions AddOutput(IOutputArgument argument, Action<FFMpegArgumentOptions>? addArguments)
{
var args = new FFMpegArgumentOptions();
Expand Down
Loading