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()