diff --git a/Hudl.FFmpeg.Core/Filters/Attributes/FilterParameterAttribute.cs b/Hudl.FFmpeg.Core/Filters/Attributes/FilterParameterAttribute.cs
index dc1e354..19c3bc4 100644
--- a/Hudl.FFmpeg.Core/Filters/Attributes/FilterParameterAttribute.cs
+++ b/Hudl.FFmpeg.Core/Filters/Attributes/FilterParameterAttribute.cs
@@ -24,5 +24,7 @@ public FilterParameterAttribute(string name)
public Type Formatter { get; set; }
public Type Binding { get; set; }
+
+ public int Order { get; set; }
}
}
diff --git a/Hudl.FFmpeg.Core/Filters/Serialization/FilterSerializerAttributeParser.cs b/Hudl.FFmpeg.Core/Filters/Serialization/FilterSerializerAttributeParser.cs
index 937d90d..2d4a791 100644
--- a/Hudl.FFmpeg.Core/Filters/Serialization/FilterSerializerAttributeParser.cs
+++ b/Hudl.FFmpeg.Core/Filters/Serialization/FilterSerializerAttributeParser.cs
@@ -69,6 +69,11 @@ private static void FillFilterParameterAttributes(FilterSerializerData filterSer
IsDefault = filterPropertyIsDefault
});
}
+
+ if (filterSerializerData.Parameters.Any(f => f.Parameter.Order > 0))
+ {
+ filterSerializerData.Parameters = filterSerializerData.Parameters.OrderBy(f => f.Parameter.Order).ToList();
+ }
}
private static object GetFilterSerializationBindingValue(FilterParameterAttribute filterParameterAttribute, PropertyInfo propertyInfo, FilterBindingContext context, IFilter filter)
diff --git a/Hudl.FFmpeg.Core/Formatters/SingleQuoteExpressionFormatter.cs b/Hudl.FFmpeg.Core/Formatters/SingleQuoteExpressionFormatter.cs
new file mode 100644
index 0000000..35b2a57
--- /dev/null
+++ b/Hudl.FFmpeg.Core/Formatters/SingleQuoteExpressionFormatter.cs
@@ -0,0 +1,17 @@
+using Hudl.FFmpeg.Interfaces;
+using System.Text.RegularExpressions;
+
+namespace Hudl.FFmpeg.Formatters
+{
+ public class SingleQuoteExpressionFormatter : IFormatter
+ {
+ public string Format(object value)
+ {
+ var valueAsString = value.ToString();
+ var valueIsNumeric = Regex.IsMatch(valueAsString, @"^\d+$");
+ return (!valueIsNumeric)
+ ? $"'{valueAsString}'"
+ : valueAsString;
+ }
+ }
+}
diff --git a/Hudl.FFmpeg.Core/Formatters/TimeSpanSecondsFormatter.cs b/Hudl.FFmpeg.Core/Formatters/TimeSpanSecondsFormatter.cs
new file mode 100644
index 0000000..6d9e52c
--- /dev/null
+++ b/Hudl.FFmpeg.Core/Formatters/TimeSpanSecondsFormatter.cs
@@ -0,0 +1,14 @@
+using System;
+using Hudl.FFmpeg.Formatters.Utility;
+using Hudl.FFmpeg.Interfaces;
+
+namespace Hudl.FFmpeg.Formatters
+{
+ public class TimeSpanSecondsFormatter : IFormatter
+ {
+ public string Format(object value)
+ {
+ return ((TimeSpan)value).TotalSeconds.ToString();
+ }
+ }
+}
diff --git a/Hudl.FFmpeg.Core/Hudl.FFmpeg.Core.csproj b/Hudl.FFmpeg.Core/Hudl.FFmpeg.Core.csproj
index 3384587..05d942f 100644
--- a/Hudl.FFmpeg.Core/Hudl.FFmpeg.Core.csproj
+++ b/Hudl.FFmpeg.Core/Hudl.FFmpeg.Core.csproj
@@ -111,10 +111,12 @@
+
+
diff --git a/Hudl.FFmpeg/Filters/AReverse.cs b/Hudl.FFmpeg/Filters/AReverse.cs
new file mode 100644
index 0000000..a17f254
--- /dev/null
+++ b/Hudl.FFmpeg/Filters/AReverse.cs
@@ -0,0 +1,18 @@
+using Hudl.FFmpeg.Attributes;
+using Hudl.FFmpeg.Filters.Attributes;
+using Hudl.FFmpeg.Filters.Interfaces;
+using Hudl.FFmpeg.Resources.BaseTypes;
+
+namespace Hudl.FFmpeg.Filters
+{
+ ///
+ /// Reverses an audio clip. Note that it will buffer the entire clip,
+ /// so it's recommented to use trimming.
+ ///
+ [ForStream(Type = typeof(AudioStream))]
+ [Filter(Name ="areverse", MinInputs = 1, MaxInputs = 1)]
+ public class AReverse : IFilter
+ {
+ //This filter accepts no parameters
+ }
+}
diff --git a/Hudl.FFmpeg/Filters/AlphaExtract.cs b/Hudl.FFmpeg/Filters/AlphaExtract.cs
new file mode 100644
index 0000000..bfb3b97
--- /dev/null
+++ b/Hudl.FFmpeg/Filters/AlphaExtract.cs
@@ -0,0 +1,17 @@
+using Hudl.FFmpeg.Attributes;
+using Hudl.FFmpeg.Filters.Attributes;
+using Hudl.FFmpeg.Filters.Interfaces;
+using Hudl.FFmpeg.Resources.BaseTypes;
+
+namespace Hudl.FFmpeg.Filters
+{
+ ///
+ /// Extracts the alpha component from the input as a grayscale video.
+ ///
+ [ForStream(Type = typeof(VideoStream))]
+ [Filter(Name ="alphaextract", MinInputs = 1, MaxInputs = 1)]
+ public class AlphaExtract : IFilter
+ {
+ //This filter accepts no parameters
+ }
+}
diff --git a/Hudl.FFmpeg/Filters/AlphaMerge.cs b/Hudl.FFmpeg/Filters/AlphaMerge.cs
new file mode 100644
index 0000000..a520ab3
--- /dev/null
+++ b/Hudl.FFmpeg/Filters/AlphaMerge.cs
@@ -0,0 +1,18 @@
+using Hudl.FFmpeg.Attributes;
+using Hudl.FFmpeg.Filters.Attributes;
+using Hudl.FFmpeg.Filters.Interfaces;
+using Hudl.FFmpeg.Resources.BaseTypes;
+
+namespace Hudl.FFmpeg.Filters
+{
+ ///
+ /// Adds or replaces the alpha component of the primary input with
+ /// the grayscale value of a second input.
+ ///
+ [ForStream(Type = typeof(VideoStream))]
+ [Filter(Name ="alphamerge", MinInputs = 2, MaxInputs = 2)]
+ public class AlphaMerge : IFilter
+ {
+ //This filter accepts no parameters
+ }
+}
diff --git a/Hudl.FFmpeg/Filters/Blend.cs b/Hudl.FFmpeg/Filters/Blend.cs
index a398dd3..e57fb22 100644
--- a/Hudl.FFmpeg/Filters/Blend.cs
+++ b/Hudl.FFmpeg/Filters/Blend.cs
@@ -24,6 +24,7 @@ public Blend()
{
Mode = BlendVideoModeType.and;
Option = BlendVideoOptionType.all_expr;
+ RepeatLast = true;
}
public Blend(string expression)
: this()
@@ -31,18 +32,24 @@ public Blend(string expression)
Expression = expression;
}
- [FilterParameter]
+ [FilterParameter(Order = 1)]
public BlendVideoOptionType? Option { get; set; }
- [FilterParameter(Default = BlendVideoModeType.and)]
+ [FilterParameter(Order = 2, Default = BlendVideoModeType.and)]
public BlendVideoModeType? Mode { get; set; }
///
/// the blend expression details can be found at http://ffmpeg.org/ffmpeg-all.html#blend.
///
- [FilterParameter(Formatter = typeof(SingleQuoteFormatter))]
+ [FilterParameter(Order = 3, Formatter = typeof(SingleQuoteFormatter))]
public string Expression { get; set; }
+ [FilterParameter(Order = 4, Name = "repeatlast", Default = true, Formatter = typeof(BoolToInt32Formatter))]
+ public bool RepeatLast { get; set; }
+
+ [FilterParameter(Order = 5, Name = "shortest", Default = false, Formatter = typeof(BoolToInt32Formatter))]
+ public bool Shortest { get; set; }
+
public virtual MetadataInfoTreeContainer EditInfo(MetadataInfoTreeContainer infoToUpdate, List suppliedInfo)
{
infoToUpdate.VideoStream.VideoMetadata.Duration = suppliedInfo.Min(r => r.VideoStream.VideoMetadata.Duration);
diff --git a/Hudl.FFmpeg/Filters/Color.cs b/Hudl.FFmpeg/Filters/Color.cs
index 9f2a30f..346fb10 100644
--- a/Hudl.FFmpeg/Filters/Color.cs
+++ b/Hudl.FFmpeg/Filters/Color.cs
@@ -32,7 +32,7 @@ public class Color : IFilter, IMetadataManipulation
[FilterParameter(Name = "r", Default = 25)]
public int? FrameRate { get; set; }
- [FilterParameter(Name = "d", Formatter = typeof(TimeSpanFormatter))]
+ [FilterParameter(Name = "d", Formatter = typeof(TimeSpanSecondsFormatter))]
[Validate(typeof(TimeSpanGreterThanZeroValidator))]
public TimeSpan? Duration { get; set; }
diff --git a/Hudl.FFmpeg/Filters/Pad.cs b/Hudl.FFmpeg/Filters/Pad.cs
index eaeb568..27d0ba0 100644
--- a/Hudl.FFmpeg/Filters/Pad.cs
+++ b/Hudl.FFmpeg/Filters/Pad.cs
@@ -45,11 +45,11 @@ public Pad(string expression)
public int? Height { get; set; }
[FilterParameter(Name = "x")]
- [Validate(LogicalOperators.GreaterThan, 0)]
+ [Validate(LogicalOperators.GreaterThanOrEqual, 0)]
public int? X { get; set; }
[FilterParameter(Name = "y")]
- [Validate(LogicalOperators.GreaterThan, 0)]
+ [Validate(LogicalOperators.GreaterThanOrEqual, 0)]
public int? Y { get; set; }
[FilterParameter(Name = "color")]
diff --git a/Hudl.FFmpeg/Filters/Reverse.cs b/Hudl.FFmpeg/Filters/Reverse.cs
new file mode 100644
index 0000000..30bd946
--- /dev/null
+++ b/Hudl.FFmpeg/Filters/Reverse.cs
@@ -0,0 +1,18 @@
+using Hudl.FFmpeg.Attributes;
+using Hudl.FFmpeg.Filters.Attributes;
+using Hudl.FFmpeg.Filters.Interfaces;
+using Hudl.FFmpeg.Resources.BaseTypes;
+
+namespace Hudl.FFmpeg.Filters
+{
+ ///
+ /// Reverses a video clip. Note that it will buffer the entire clip,
+ /// so it's recommented to use trimming.
+ ///
+ [ForStream(Type = typeof(VideoStream))]
+ [Filter(Name ="reverse", MinInputs = 1, MaxInputs = 1)]
+ public class Reverse : IFilter
+ {
+ //This filter accepts no parameters
+ }
+}
diff --git a/Hudl.FFmpeg/Filters/ZoomPan.cs b/Hudl.FFmpeg/Filters/ZoomPan.cs
new file mode 100644
index 0000000..24cad75
--- /dev/null
+++ b/Hudl.FFmpeg/Filters/ZoomPan.cs
@@ -0,0 +1,63 @@
+using Hudl.FFmpeg.Attributes;
+using Hudl.FFmpeg.Common;
+using Hudl.FFmpeg.Enums;
+using Hudl.FFmpeg.Filters.Attributes;
+using Hudl.FFmpeg.Filters.Interfaces;
+using Hudl.FFmpeg.Formatters;
+using Hudl.FFmpeg.Metadata;
+using Hudl.FFmpeg.Resources.BaseTypes;
+using System.Collections.Generic;
+using System.Drawing;
+
+namespace Hudl.FFmpeg.Filters
+{
+ ///
+ /// Applies Zoom & Pan effect.
+ ///
+ [ForStream(Type = typeof(VideoStream))]
+ [Filter(Name ="zoompan", MinInputs = 1, MaxInputs = 1)]
+ public class ZoomPan : IFilter
+ {
+ public ZoomPan()
+ {
+ }
+
+ public ZoomPan(string zoom, string x, string y, string d, double? fps, Size? size)
+ {
+ Zoom = zoom;
+ X = x;
+ Y = y;
+ D = d;
+ S = size;
+ Fps = fps;
+ }
+
+ public ZoomPan(string zoom, string x, string y, string d, double? fps, ScalePresetType? preset)
+ : this(zoom, x, y, d, fps, (Size?)null)
+ {
+ Size sizeValue = default(Size);
+ if (preset.HasValue && Helpers.ScalingPresets.TryGetValue(preset.Value, out sizeValue))
+ {
+ S = sizeValue;
+ }
+ }
+
+ [FilterParameter(Name = "zoom", Formatter = typeof(SingleQuoteExpressionFormatter))]
+ public string Zoom { get; set; }
+
+ [FilterParameter(Name = "x", Formatter = typeof(SingleQuoteExpressionFormatter))]
+ public string X { get; set; }
+
+ [FilterParameter(Name = "y", Formatter = typeof(SingleQuoteExpressionFormatter))]
+ public string Y { get; set; }
+
+ [FilterParameter(Name = "d")]
+ public string D { get; set; }
+
+ [FilterParameter(Name = "s", Formatter = typeof(SizeFormatter))]
+ public Size? S { get; set; }
+
+ [FilterParameter(Name = "fps")]
+ public double? Fps { get; set; }
+ }
+}
diff --git a/Hudl.FFmpeg/Hudl.Ffmpeg.csproj b/Hudl.FFmpeg/Hudl.Ffmpeg.csproj
index 6e97d7d..0611fd6 100644
--- a/Hudl.FFmpeg/Hudl.Ffmpeg.csproj
+++ b/Hudl.FFmpeg/Hudl.Ffmpeg.csproj
@@ -59,8 +59,13 @@
+
+
+
+
+
diff --git a/Hudl.Ffmpeg.Tests/Filter/FilterTests.cs b/Hudl.Ffmpeg.Tests/Filter/FilterTests.cs
index 648eede..e579943 100644
--- a/Hudl.Ffmpeg.Tests/Filter/FilterTests.cs
+++ b/Hudl.Ffmpeg.Tests/Filter/FilterTests.cs
@@ -330,6 +330,14 @@ public void Blend_Verify()
filter.Expression = "A*(if(gte(T,{0}),1,T/{0}))+B*(1-(if(gte(T,{0}),1,T/{0})))";
filterValue.Append("blend=all_expr='A*(if(gte(T,{0}),1,T/{0}))+B*(1-(if(gte(T,{0}),1,T/{0})))'");
Assert.Equal(filterValue.ToString(), FilterSerializer.Serialize(filter));
+
+ filter.RepeatLast = false;
+ filterValue.Append(":repeatlast=0");
+ Assert.Equal(filterValue.ToString(), FilterSerializer.Serialize(filter));
+
+ filter.Shortest = true;
+ filterValue.Append(":shortest=1");
+ Assert.Equal(filterValue.ToString(), FilterSerializer.Serialize(filter));
}
[Fact]
@@ -692,7 +700,7 @@ public void Color_Verify()
Assert.Equal(filterValue.ToString(), FilterSerializer.Serialize(filter));
filter.Duration = TimeSpan.FromSeconds(3.222);
- filterValue.Append(":d=00:00:03.222");
+ filterValue.Append(":d=3.222");
Assert.Equal(filterValue.ToString(), FilterSerializer.Serialize(filter));
filter.SampleAspectRatio = Ratio.Create(1, 1);
@@ -702,6 +710,66 @@ public void Color_Verify()
}
+ [Fact]
+ public void ZoomPan_Verify()
+ {
+ var filter = FilterFactory.CreateEmpty();
+ var filterValue = new StringBuilder(100);
+ Assert.DoesNotThrow(() => FilterSerializer.Serialize(filter));
+
+ filter.Zoom = "2";
+ filterValue.Append("zoompan=zoom=2");
+ Assert.Equal(filterValue.ToString(), FilterSerializer.Serialize(filter));
+
+ filter.X = "100";
+ filterValue.Append(":x=100");
+ Assert.Equal(filterValue.ToString(), FilterSerializer.Serialize(filter));
+
+ filter.Y = "200";
+ filterValue.Append(":y=200");
+ Assert.Equal(filterValue.ToString(), FilterSerializer.Serialize(filter));
+
+ filter.D = "1";
+ filterValue.Append(":d=1");
+ Assert.Equal(filterValue.ToString(), FilterSerializer.Serialize(filter));
+
+ filter.S = new Size(1280, 720);
+ filterValue.Append(":s=1280x720");
+ Assert.Equal(filterValue.ToString(), FilterSerializer.Serialize(filter));
+
+ filter.Fps = 30;
+ filterValue.Append(":fps=30");
+ Assert.Equal(filterValue.ToString(), FilterSerializer.Serialize(filter));
+
+ var filter2 = FilterFactory.CreateEmpty();
+ filterValue = new StringBuilder(100);
+ Assert.DoesNotThrow(() => FilterSerializer.Serialize(filter2));
+
+ filter2.Zoom = "in_w";
+ filterValue.Append("zoompan=zoom='in_w'");
+ Assert.Equal(filterValue.ToString(), FilterSerializer.Serialize(filter2));
+
+ filter2.X = "in_w+2";
+ filterValue.Append(":x='in_w+2'");
+ Assert.Equal(filterValue.ToString(), FilterSerializer.Serialize(filter2));
+
+ filter2.Y = "out_w+3";
+ filterValue.Append(":y='out_w+3'");
+ Assert.Equal(filterValue.ToString(), FilterSerializer.Serialize(filter2));
+
+ filter2.D = "1";
+ filterValue.Append(":d=1");
+ Assert.Equal(filterValue.ToString(), FilterSerializer.Serialize(filter2));
+
+ filter2.S = new Size(1280, 720);
+ filterValue.Append(":s=1280x720");
+ Assert.Equal(filterValue.ToString(), FilterSerializer.Serialize(filter2));
+
+ filter2.Fps = 30;
+ filterValue.Append(":fps=30");
+ Assert.Equal(filterValue.ToString(), FilterSerializer.Serialize(filter2));
+ }
+
private class FilterFactory
{
internal static TFilter CreateEmpty()