diff --git a/FFMpegCore.Examples/FFMpegCore.Examples.csproj b/FFMpegCore.Examples/FFMpegCore.Examples.csproj
new file mode 100644
index 0000000..f9daae7
--- /dev/null
+++ b/FFMpegCore.Examples/FFMpegCore.Examples.csproj
@@ -0,0 +1,12 @@
+
+
+
+ Exe
+ net5.0
+
+
+
+
+
+
+
diff --git a/FFMpegCore.Examples/Program.cs b/FFMpegCore.Examples/Program.cs
new file mode 100644
index 0000000..256ef3c
--- /dev/null
+++ b/FFMpegCore.Examples/Program.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.IO;
+using FFMpegCore;
+using FFMpegCore.Enums;
+using FFMpegCore.Pipes;
+using FFMpegCore.Extend;
+
+var inputPath = "/path/to/input";
+var outputPath = "/path/to/output";
+
+{
+ var mediaInfo = FFProbe.Analyse(inputPath);
+}
+
+{
+ var mediaInfo = await FFProbe.AnalyseAsync(inputPath);
+}
+
+{
+ FFMpegArguments
+ .FromFileInput(inputPath)
+ .OutputToFile(outputPath, false, options => options
+ .WithVideoCodec(VideoCodec.LibX264)
+ .WithConstantRateFactor(21)
+ .WithAudioCodec(AudioCodec.Aac)
+ .WithVariableBitrate(4)
+ .WithVideoFilters(filterOptions => filterOptions
+ .Scale(VideoSize.Hd))
+ .WithFastStart())
+ .ProcessSynchronously();
+}
+
+{
+ // process the snapshot in-memory and use the Bitmap directly
+ var bitmap = FFMpeg.Snapshot(inputPath, new Size(200, 400), TimeSpan.FromMinutes(1));
+
+ // or persists the image on the drive
+ FFMpeg.Snapshot(inputPath, outputPath, new Size(200, 400), TimeSpan.FromMinutes(1));
+}
+
+var inputStream = new MemoryStream();
+var outputStream = new MemoryStream();
+
+{
+ await FFMpegArguments
+ .FromPipeInput(new StreamPipeSource(inputStream))
+ .OutputToPipe(new StreamPipeSink(outputStream), options => options
+ .WithVideoCodec("vp9")
+ .ForceFormat("webm"))
+ .ProcessAsynchronously();
+}
+
+{
+ FFMpeg.Join(@"..\joined_video.mp4",
+ @"..\part1.mp4",
+ @"..\part2.mp4",
+ @"..\part3.mp4"
+ );
+}
+
+{
+ FFMpeg.JoinImageSequence(@"..\joined_video.mp4", frameRate: 1,
+ ImageInfo.FromPath(@"..\1.png"),
+ ImageInfo.FromPath(@"..\2.png"),
+ ImageInfo.FromPath(@"..\3.png")
+ );
+}
+
+{
+ FFMpeg.Mute(inputPath, outputPath);
+}
+
+{
+ FFMpeg.ExtractAudio(inputPath, outputPath);
+}
+
+var inputAudioPath = "/path/to/input/audio";
+{
+ FFMpeg.ReplaceAudio(inputPath, inputAudioPath, outputPath);
+}
+
+var inputImagePath = "/path/to/input/image";
+{
+ FFMpeg.PosterWithAudio(inputPath, inputAudioPath, outputPath);
+ // or
+ var image = Image.FromFile(inputImagePath);
+ image.AddAudio(inputAudioPath, outputPath);
+}
+
+IVideoFrame GetNextFrame() => throw new NotImplementedException();
+{
+ IEnumerable CreateFrames(int count)
+ {
+ for(int i = 0; i < count; i++)
+ {
+ yield return GetNextFrame(); //method of generating new frames
+ }
+ }
+
+ var videoFramesSource = new RawVideoPipeSource(CreateFrames(64)) //pass IEnumerable or IEnumerator to constructor of RawVideoPipeSource
+ {
+ FrameRate = 30 //set source frame rate
+ };
+ await FFMpegArguments
+ .FromPipeInput(videoFramesSource)
+ .OutputToFile(outputPath, false, options => options
+ .WithVideoCodec(VideoCodec.LibVpx))
+ .ProcessAsynchronously();
+}
+
+{
+ // setting global options
+ GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" });
+ // or
+ GlobalFFOptions.Configure(options => options.BinaryFolder = "./bin");
+
+ // or individual, per-run options
+ await FFMpegArguments
+ .FromFileInput(inputPath)
+ .OutputToFile(outputPath)
+ .ProcessAsynchronously(true, new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" });
+}
\ No newline at end of file
diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs
index f0792ef..18a8c7d 100644
--- a/FFMpegCore.Test/ArgumentBuilderTest.cs
+++ b/FFMpegCore.Test/ArgumentBuilderTest.cs
@@ -239,9 +239,8 @@ public void Builder_BuildString_Loop()
[TestMethod]
public void Builder_BuildString_Seek()
{
- var str = FFMpegArguments.FromFileInput("input.mp4", false, opt => opt.Seek(TimeSpan.FromSeconds(10)))
- .OutputToFile("output.mp4", false, opt => opt.Seek(TimeSpan.FromSeconds(10))).Arguments;
- Assert.AreEqual("-ss 00:00:10 -i \"input.mp4\" -ss 00:00:10 \"output.mp4\"", str);
+ var str = FFMpegArguments.FromFileInput("input.mp4", false, opt => opt.Seek(TimeSpan.FromSeconds(10))).OutputToFile("output.mp4", false, opt => opt.Seek(TimeSpan.FromSeconds(10))).Arguments;
+ Assert.AreEqual("-ss 00:00:10.000 -i \"input.mp4\" -ss 00:00:10.000 \"output.mp4\"", str);
}
[TestMethod]
diff --git a/FFMpegCore.Test/FFMpegCore.Test.csproj b/FFMpegCore.Test/FFMpegCore.Test.csproj
index 2cd97bb..98c9274 100644
--- a/FFMpegCore.Test/FFMpegCore.Test.csproj
+++ b/FFMpegCore.Test/FFMpegCore.Test.csproj
@@ -39,10 +39,10 @@
-
-
-
-
+
+
+
+
diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs
index 5ac83a1..5cabc4e 100644
--- a/FFMpegCore.Test/FFProbeTests.cs
+++ b/FFMpegCore.Test/FFProbeTests.cs
@@ -25,6 +25,24 @@ public async Task Audio_FromStream_Duration()
Assert.IsTrue(fileAnalysis.Duration == streamAnalysis.Duration);
}
+ [DataTestMethod]
+ [DataRow("0:00:03.008000", 0, 0, 0, 3, 8)]
+ [DataRow("05:12:59.177", 0, 5, 12, 59, 177)]
+ [DataRow("149:07:50.911750", 6, 5, 7, 50, 911)]
+ [DataRow("00:00:00.83", 0, 0, 0, 0, 830)]
+ public void MediaAnalysis_ParseDuration(string duration, int expectedDays, int expectedHours, int expectedMinutes, int expectedSeconds, int expectedMilliseconds)
+ {
+ var ffprobeStream = new FFProbeStream { Duration = duration };
+
+ var parsedDuration = MediaAnalysisUtils.ParseDuration(ffprobeStream);
+
+ Assert.AreEqual(expectedDays, parsedDuration.Days);
+ Assert.AreEqual(expectedHours, parsedDuration.Hours);
+ Assert.AreEqual(expectedMinutes, parsedDuration.Minutes);
+ Assert.AreEqual(expectedSeconds, parsedDuration.Seconds);
+ Assert.AreEqual(expectedMilliseconds, parsedDuration.Milliseconds);
+ }
+
[TestMethod]
public async Task Uri_Duration()
{
diff --git a/FFMpegCore.Test/Utilities/BitmapSources.cs b/FFMpegCore.Test/Utilities/BitmapSources.cs
index c3e8d40..8ea02e8 100644
--- a/FFMpegCore.Test/Utilities/BitmapSources.cs
+++ b/FFMpegCore.Test/Utilities/BitmapSources.cs
@@ -21,7 +21,7 @@ public static IEnumerable CreateBitmaps(int count, PixelFormat fmt,
}
}
- private static BitmapVideoFrameWrapper CreateVideoFrame(int index, PixelFormat fmt, int w, int h, float scaleNoise, float offset)
+ public static BitmapVideoFrameWrapper CreateVideoFrame(int index, PixelFormat fmt, int w, int h, float scaleNoise, float offset)
{
var bitmap = new Bitmap(w, h, fmt);
diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs
index 30e6a9a..149dabd 100644
--- a/FFMpegCore.Test/VideoTest.cs
+++ b/FFMpegCore.Test/VideoTest.cs
@@ -87,6 +87,92 @@ public void Video_ToMP4_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat
Assert.IsTrue(success);
}
+ [TestMethod, Timeout(10000)]
+ public void Video_ToMP4_Args_Pipe_DifferentImageSizes()
+ {
+ using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
+
+ var frames = new List
+ {
+ BitmapSource.CreateVideoFrame(0, System.Drawing.Imaging.PixelFormat.Format24bppRgb, 255, 255, 1, 0),
+ BitmapSource.CreateVideoFrame(0, System.Drawing.Imaging.PixelFormat.Format24bppRgb, 256, 256, 1, 0)
+ };
+
+ var videoFramesSource = new RawVideoPipeSource(frames);
+ var ex = Assert.ThrowsException(() => FFMpegArguments
+ .FromPipeInput(videoFramesSource)
+ .OutputToFile(outputFile, false, opt => opt
+ .WithVideoCodec(VideoCodec.LibX264))
+ .ProcessSynchronously());
+
+ Assert.IsInstanceOfType(ex.GetBaseException(), typeof(FFMpegStreamFormatException));
+ }
+
+
+ [TestMethod, Timeout(10000)]
+ public async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_Async()
+ {
+ using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
+
+ var frames = new List
+ {
+ BitmapSource.CreateVideoFrame(0, System.Drawing.Imaging.PixelFormat.Format24bppRgb, 255, 255, 1, 0),
+ BitmapSource.CreateVideoFrame(0, System.Drawing.Imaging.PixelFormat.Format24bppRgb, 256, 256, 1, 0)
+ };
+
+ var videoFramesSource = new RawVideoPipeSource(frames);
+ var ex = await Assert.ThrowsExceptionAsync(() => FFMpegArguments
+ .FromPipeInput(videoFramesSource)
+ .OutputToFile(outputFile, false, opt => opt
+ .WithVideoCodec(VideoCodec.LibX264))
+ .ProcessAsynchronously());
+
+ Assert.IsInstanceOfType(ex.GetBaseException(), typeof(FFMpegStreamFormatException));
+ }
+
+ [TestMethod, Timeout(10000)]
+ public void Video_ToMP4_Args_Pipe_DifferentPixelFormats()
+ {
+ using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
+
+ var frames = new List
+ {
+ BitmapSource.CreateVideoFrame(0, System.Drawing.Imaging.PixelFormat.Format24bppRgb, 255, 255, 1, 0),
+ BitmapSource.CreateVideoFrame(0, System.Drawing.Imaging.PixelFormat.Format32bppRgb, 255, 255, 1, 0)
+ };
+
+ var videoFramesSource = new RawVideoPipeSource(frames);
+ var ex = Assert.ThrowsException(() => FFMpegArguments
+ .FromPipeInput(videoFramesSource)
+ .OutputToFile(outputFile, false, opt => opt
+ .WithVideoCodec(VideoCodec.LibX264))
+ .ProcessSynchronously());
+
+ Assert.IsInstanceOfType(ex.GetBaseException(), typeof(FFMpegStreamFormatException));
+ }
+
+
+ [TestMethod, Timeout(10000)]
+ public async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_Async()
+ {
+ using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
+
+ var frames = new List
+ {
+ BitmapSource.CreateVideoFrame(0, System.Drawing.Imaging.PixelFormat.Format24bppRgb, 255, 255, 1, 0),
+ BitmapSource.CreateVideoFrame(0, System.Drawing.Imaging.PixelFormat.Format32bppRgb, 255, 255, 1, 0)
+ };
+
+ var videoFramesSource = new RawVideoPipeSource(frames);
+ var ex = await Assert.ThrowsExceptionAsync(() => FFMpegArguments
+ .FromPipeInput(videoFramesSource)
+ .OutputToFile(outputFile, false, opt => opt
+ .WithVideoCodec(VideoCodec.LibX264))
+ .ProcessAsynchronously());
+
+ Assert.IsInstanceOfType(ex.GetBaseException(), typeof(FFMpegStreamFormatException));
+ }
+
[TestMethod, Timeout(10000)]
public void Video_ToMP4_Args_StreamPipe()
{
@@ -114,6 +200,8 @@ await FFMpegArguments
.ProcessAsynchronously();
});
}
+
+
[TestMethod, Timeout(10000)]
public void Video_StreamFile_OutputToMemoryStream()
{
diff --git a/FFMpegCore.sln b/FFMpegCore.sln
index eab20fd..27eab0a 100644
--- a/FFMpegCore.sln
+++ b/FFMpegCore.sln
@@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FFMpegCore", "FFMpegCore\FF
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore.Test", "FFMpegCore.Test\FFMpegCore.Test.csproj", "{F20C8353-72D9-454B-9F16-3624DBAD2328}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FFMpegCore.Examples", "FFMpegCore.Examples\FFMpegCore.Examples.csproj", "{3125CF91-FFBD-4E4E-8930-247116AFE772}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -21,6 +23,10 @@ Global
{F20C8353-72D9-454B-9F16-3624DBAD2328}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F20C8353-72D9-454B-9F16-3624DBAD2328}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F20C8353-72D9-454B-9F16-3624DBAD2328}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3125CF91-FFBD-4E4E-8930-247116AFE772}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3125CF91-FFBD-4E4E-8930-247116AFE772}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3125CF91-FFBD-4E4E-8930-247116AFE772}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3125CF91-FFBD-4E4E-8930-247116AFE772}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/FFMpegCore/Assembly.cs b/FFMpegCore/Assembly.cs
new file mode 100644
index 0000000..0117671
--- /dev/null
+++ b/FFMpegCore/Assembly.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("FFMpegCore.Test")]
\ No newline at end of file
diff --git a/FFMpegCore/Extend/BitmapExtensions.cs b/FFMpegCore/Extend/BitmapExtensions.cs
index bf10336..e2f5505 100644
--- a/FFMpegCore/Extend/BitmapExtensions.cs
+++ b/FFMpegCore/Extend/BitmapExtensions.cs
@@ -6,7 +6,7 @@ namespace FFMpegCore.Extend
{
public static class BitmapExtensions
{
- public static bool AddAudio(this Bitmap poster, string audio, string output)
+ public static bool AddAudio(this Image poster, string audio, string output)
{
var destination = $"{Environment.TickCount}.png";
poster.Save(destination);
diff --git a/FFMpegCore/FFMpeg/Arguments/OutputUrlArgument.cs b/FFMpegCore/FFMpeg/Arguments/OutputUrlArgument.cs
new file mode 100644
index 0000000..15cbef9
--- /dev/null
+++ b/FFMpegCore/FFMpeg/Arguments/OutputUrlArgument.cs
@@ -0,0 +1,27 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace FFMpegCore.Arguments
+{
+ ///
+ /// Represents outputting to url using supported protocols
+ /// See http://ffmpeg.org/ffmpeg-protocols.html
+ ///
+ public class OutputUrlArgument : IOutputArgument
+ {
+ public readonly string Url;
+
+ public OutputUrlArgument(string url)
+ {
+ Url = url;
+ }
+
+ public void Post() { }
+
+ public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask;
+
+ public void Pre() { }
+
+ public string Text => Url;
+ }
+}
diff --git a/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs b/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs
index 428f21b..fcb944a 100644
--- a/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs
+++ b/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs
@@ -40,14 +40,17 @@ public async Task During(CancellationToken cancellationToken = default)
{
try
{
- await ProcessDataAsync(cancellationToken);
- Debug.WriteLine($"Disconnecting NamedPipeServerStream on {GetType().Name}");
- Pipe?.Disconnect();
+ await ProcessDataAsync(cancellationToken);
}
catch (TaskCanceledException)
{
Debug.WriteLine($"ProcessDataAsync on {GetType().Name} cancelled");
}
+ finally
+ {
+ Debug.WriteLine($"Disconnecting NamedPipeServerStream on {GetType().Name}");
+ Pipe?.Disconnect();
+ }
}
protected abstract Task ProcessDataAsync(CancellationToken token);
diff --git a/FFMpegCore/FFMpeg/Arguments/SeekArgument.cs b/FFMpegCore/FFMpeg/Arguments/SeekArgument.cs
index 1057b88..1b58890 100644
--- a/FFMpegCore/FFMpeg/Arguments/SeekArgument.cs
+++ b/FFMpegCore/FFMpeg/Arguments/SeekArgument.cs
@@ -8,11 +8,28 @@ namespace FFMpegCore.Arguments
public class SeekArgument : IArgument
{
public readonly TimeSpan? SeekTo;
+
public SeekArgument(TimeSpan? seekTo)
{
SeekTo = seekTo;
}
-
- public string Text => !SeekTo.HasValue ? string.Empty : $"-ss {SeekTo.Value}";
+
+ public string Text {
+ get {
+ if(SeekTo.HasValue)
+ {
+ int hours = SeekTo.Value.Hours;
+ if(SeekTo.Value.Days > 0)
+ {
+ hours += SeekTo.Value.Days * 24;
+ }
+ return $"-ss {hours.ToString("00")}:{SeekTo.Value.Minutes.ToString("00")}:{SeekTo.Value.Seconds.ToString("00")}.{SeekTo.Value.Milliseconds.ToString("000")}";
+ }
+ else
+ {
+ return string.Empty;
+ }
+ }
+ }
}
}
diff --git a/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs b/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs
index dad6ef1..485cf20 100644
--- a/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs
+++ b/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs
@@ -49,4 +49,12 @@ public FFMpegArgumentException(string? message = null, Exception? innerException
{
}
}
+
+ public class FFMpegStreamFormatException : FFMpegException
+ {
+ public FFMpegStreamFormatException(FFMpegExceptionType type, string message, Exception? innerException = null)
+ : base(type, message, innerException)
+ {
+ }
+ }
}
\ No newline at end of file
diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs
index 3f3d162..42c344b 100644
--- a/FFMpegCore/FFMpeg/FFMpeg.cs
+++ b/FFMpegCore/FFMpeg/FFMpeg.cs
@@ -163,8 +163,8 @@ public static bool Convert(
var source = FFProbe.Analyse(input);
FFMpegHelper.ConversionSizeExceptionCheck(source);
- var scale = VideoSize.Original == size ? 1 : (double)source.PrimaryVideoStream.Height / (int)size;
- var outputSize = new Size((int)(source.PrimaryVideoStream.Width / scale), (int)(source.PrimaryVideoStream.Height / scale));
+ var scale = VideoSize.Original == size ? 1 : (double)source.PrimaryVideoStream!.Height / (int)size;
+ var outputSize = new Size((int)(source.PrimaryVideoStream!.Width / scale), (int)(source.PrimaryVideoStream.Height / scale));
if (outputSize.Width % 2 != 0)
outputSize.Width += 1;
diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs
index 57ff68c..847e68c 100644
--- a/FFMpegCore/FFMpeg/FFMpegArguments.cs
+++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs
@@ -49,7 +49,8 @@ private FFMpegArguments WithInput(IInputArgument inputArgument, Action? addArguments = null) => ToProcessor(new OutputArgument(file, overwrite), addArguments);
- public FFMpegArgumentProcessor OutputToFile(Uri uri, bool overwrite = true, Action? addArguments = null) => ToProcessor(new OutputArgument(uri.AbsolutePath, overwrite), addArguments);
+ 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);
private FFMpegArgumentProcessor ToProcessor(IOutputArgument argument, Action? addArguments)
diff --git a/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs b/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs
index 378cead..65f622e 100644
--- a/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs
+++ b/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs
@@ -64,7 +64,7 @@ public async Task WriteAsync(System.IO.Stream outputStream, CancellationToken ca
private void CheckFrameAndThrow(IVideoFrame frame)
{
if (frame.Width != Width || frame.Height != Height || frame.Format != StreamFormat)
- throw new FFMpegException(FFMpegExceptionType.Operation, "Video frame is not the same format as created raw video stream\r\n" +
+ throw new FFMpegStreamFormatException(FFMpegExceptionType.Operation, "Video frame is not the same format as created raw video stream\r\n" +
$"Frame format: {frame.Width}x{frame.Height} pix_fmt: {frame.Format}\r\n" +
$"Stream format: {Width}x{Height} pix_fmt: {StreamFormat}");
}
diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj
index 69a8e61..47b41d8 100644
--- a/FFMpegCore/FFMpegCore.csproj
+++ b/FFMpegCore/FFMpegCore.csproj
@@ -5,18 +5,17 @@
https://github.com/rosenbjerg/FFMpegCore
https://github.com/rosenbjerg/FFMpegCore
- A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your C# applications
+ A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications
3.0.0.0
3.0.0.0
3.0.0.0
- - Video filter args refactored to support multiple arguments
-- Cancel improved with timeout (thanks TFleury)
-- Basic support for webcam/mic input through InputDeviceArgument (thanks TFleury)
-- Other fixes and improvements
+ - Fixes for RawVideoPipeSource hanging (thanks to max619)
+- Added .OutputToUrl(..) method for outputting to url using supported protocol (thanks to TFleury)
+- Improved timespan parsing (thanks to test-in-prod)
8
- 4.0.0
+ 4.1.0
MIT
- Malte Rosenbjerg, Vlad Jerca
+ Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev
ffmpeg ffprobe convert video audio mediafile resize analyze muxing
GitHub
true
@@ -32,7 +31,7 @@
-
+
diff --git a/FFMpegCore/FFProbe/MediaAnalysis.cs b/FFMpegCore/FFProbe/MediaAnalysis.cs
index f1b2f82..2602f86 100644
--- a/FFMpegCore/FFProbe/MediaAnalysis.cs
+++ b/FFMpegCore/FFProbe/MediaAnalysis.cs
@@ -7,8 +7,6 @@ namespace FFMpegCore
{
internal class MediaAnalysis : IMediaAnalysis
{
- private static readonly Regex DurationRegex = new Regex("^(\\d{1,2}:\\d{1,2}:\\d{1,2}(.\\d{1,7})?)", RegexOptions.Compiled);
-
internal MediaAnalysis(FFProbeAnalysis analysis)
{
Format = ParseFormat(analysis.Format);
@@ -20,7 +18,7 @@ private MediaFormat ParseFormat(Format analysisFormat)
{
return new MediaFormat
{
- Duration = TimeSpan.Parse(analysisFormat.Duration ?? "0"),
+ Duration = MediaAnalysisUtils.ParseDuration(analysisFormat.Duration),
FormatName = analysisFormat.FormatName,
FormatLongName = analysisFormat.FormatLongName,
StreamCount = analysisFormat.NbStreams,
@@ -50,14 +48,14 @@ private VideoStream ParseVideoStream(FFProbeStream stream)
return new VideoStream
{
Index = stream.Index,
- AvgFrameRate = DivideRatio(ParseRatioDouble(stream.AvgFrameRate, '/')),
- BitRate = !string.IsNullOrEmpty(stream.BitRate) ? ParseIntInvariant(stream.BitRate) : default,
- BitsPerRawSample = !string.IsNullOrEmpty(stream.BitsPerRawSample) ? ParseIntInvariant(stream.BitsPerRawSample) : default,
+ AvgFrameRate = MediaAnalysisUtils.DivideRatio(MediaAnalysisUtils.ParseRatioDouble(stream.AvgFrameRate, '/')),
+ BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitRate) : default,
+ BitsPerRawSample = !string.IsNullOrEmpty(stream.BitsPerRawSample) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitsPerRawSample) : default,
CodecName = stream.CodecName,
CodecLongName = stream.CodecLongName,
- DisplayAspectRatio = ParseRatioInt(stream.DisplayAspectRatio, ':'),
- Duration = ParseDuration(stream),
- FrameRate = DivideRatio(ParseRatioDouble(stream.FrameRate, '/')),
+ DisplayAspectRatio = MediaAnalysisUtils.ParseRatioInt(stream.DisplayAspectRatio, ':'),
+ Duration = MediaAnalysisUtils.ParseDuration(stream),
+ FrameRate = MediaAnalysisUtils.DivideRatio(MediaAnalysisUtils.ParseRatioDouble(stream.FrameRate, '/')),
Height = stream.Height ?? 0,
Width = stream.Width ?? 0,
Profile = stream.Profile,
@@ -68,57 +66,89 @@ private VideoStream ParseVideoStream(FFProbeStream stream)
};
}
- private static TimeSpan ParseDuration(FFProbeStream ffProbeStream)
- {
- return !string.IsNullOrEmpty(ffProbeStream.Duration)
- ? TimeSpan.Parse(ffProbeStream.Duration)
- : TimeSpan.Parse(TrimTimeSpan(ffProbeStream.GetDuration()) ?? "0");
- }
-
- private static string? TrimTimeSpan(string? durationTag)
- {
- var durationMatch = DurationRegex.Match(durationTag ?? "");
- return durationMatch.Success ? durationMatch.Groups[1].Value : null;
- }
-
private AudioStream ParseAudioStream(FFProbeStream stream)
{
return new AudioStream
{
Index = stream.Index,
- BitRate = !string.IsNullOrEmpty(stream.BitRate) ? ParseIntInvariant(stream.BitRate) : default,
+ BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitRate) : default,
CodecName = stream.CodecName,
CodecLongName = stream.CodecLongName,
Channels = stream.Channels ?? default,
ChannelLayout = stream.ChannelLayout,
- Duration = ParseDuration(stream),
- SampleRateHz = !string.IsNullOrEmpty(stream.SampleRate) ? ParseIntInvariant(stream.SampleRate) : default,
+ Duration = MediaAnalysisUtils.ParseDuration(stream),
+ SampleRateHz = !string.IsNullOrEmpty(stream.SampleRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.SampleRate) : default,
Profile = stream.Profile,
Language = stream.GetLanguage(),
Tags = stream.Tags,
};
}
- private static double DivideRatio((double, double) ratio) => ratio.Item1 / ratio.Item2;
- private static (int, int) ParseRatioInt(string input, char separator)
+ }
+
+ public static class MediaAnalysisUtils
+ {
+ private static readonly Regex DurationRegex = new Regex(@"^(\d+):(\d{1,2}):(\d{1,2})\.(\d{1,3})", RegexOptions.Compiled);
+
+ public static double DivideRatio((double, double) ratio) => ratio.Item1 / ratio.Item2;
+
+ public static (int, int) ParseRatioInt(string input, char separator)
{
if (string.IsNullOrEmpty(input)) return (0, 0);
var ratio = input.Split(separator);
return (ParseIntInvariant(ratio[0]), ParseIntInvariant(ratio[1]));
}
- private static (double, double) ParseRatioDouble(string input, char separator)
+ public static (double, double) ParseRatioDouble(string input, char separator)
{
if (string.IsNullOrEmpty(input)) return (0, 0);
var ratio = input.Split(separator);
return (ratio.Length > 0 ? ParseDoubleInvariant(ratio[0]) : 0, ratio.Length > 1 ? ParseDoubleInvariant(ratio[1]) : 0);
}
- private static double ParseDoubleInvariant(string line) =>
+ public static double ParseDoubleInvariant(string line) =>
double.Parse(line, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
- private static int ParseIntInvariant(string line) =>
+ public static int ParseIntInvariant(string line) =>
int.Parse(line, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
+
+
+ public static TimeSpan ParseDuration(string duration)
+ {
+ if (!string.IsNullOrEmpty(duration))
+ {
+ var match = DurationRegex.Match(duration);
+ if (match.Success)
+ {
+ // ffmpeg may provide < 3-digit number of milliseconds (omitting trailing zeros), which won't simply parse correctly
+ // e.g. 00:12:02.11 -> 12 minutes 2 seconds and 110 milliseconds
+ var millisecondsPart = match.Groups[4].Value;
+ if (millisecondsPart.Length < 3)
+ {
+ millisecondsPart = millisecondsPart.PadRight(3, '0');
+ }
+
+ var hours = int.Parse(match.Groups[1].Value);
+ var minutes = int.Parse(match.Groups[2].Value);
+ var seconds = int.Parse(match.Groups[3].Value);
+ var milliseconds = int.Parse(millisecondsPart);
+ return new TimeSpan(0, hours, minutes, seconds, milliseconds);
+ }
+ else
+ {
+ return TimeSpan.Zero;
+ }
+ }
+ else
+ {
+ return TimeSpan.Zero;
+ }
+ }
+
+ public static TimeSpan ParseDuration(FFProbeStream ffProbeStream)
+ {
+ return ParseDuration(ffProbeStream.Duration);
+ }
}
}
\ No newline at end of file
diff --git a/README.md b/README.md
index 6a9fc35..9dce345 100644
--- a/README.md
+++ b/README.md
@@ -22,11 +22,11 @@ A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and
FFProbe is used to gather media information:
```csharp
-var mediaInfo = FFProbe.Analyse(inputFile);
+var mediaInfo = FFProbe.Analyse(inputPath);
```
or
```csharp
-var mediaInfo = await FFProbe.AnalyseAsync(inputFile);
+var mediaInfo = await FFProbe.AnalyseAsync(inputPath);
```
@@ -43,20 +43,19 @@ FFMpegArguments
.WithConstantRateFactor(21)
.WithAudioCodec(AudioCodec.Aac)
.WithVariableBitrate(4)
- .WithFastStart()
- .Scale(VideoSize.Hd))
+ .WithVideoFilters(filterOptions => filterOptions
+ .Scale(VideoSize.Hd))
+ .WithFastStart())
.ProcessSynchronously();
```
Easily capture screens from your videos:
```csharp
-var mediaFileAnalysis = FFProbe.Analyse(inputPath);
-
// process the snapshot in-memory and use the Bitmap directly
-var bitmap = FFMpeg.Snapshot(mediaFileAnalysis, new Size(200, 400), TimeSpan.FromMinutes(1));
+var bitmap = FFMpeg.Snapshot(inputPath, new Size(200, 400), TimeSpan.FromMinutes(1));
// or persists the image on the drive
-FFMpeg.Snapshot(mediaFileAnalysis, outputPath, new Size(200, 400), TimeSpan.FromMinutes(1))
+FFMpeg.Snapshot(inputPath, outputPath, new Size(200, 400), TimeSpan.FromMinutes(1));
```
Convert to and/or from streams
@@ -89,25 +88,25 @@ FFMpeg.JoinImageSequence(@"..\joined_video.mp4", frameRate: 1,
Mute videos:
```csharp
-FFMpeg.Mute(inputFilePath, outputFilePath);
+FFMpeg.Mute(inputPath, outputPath);
```
Save audio track from video:
```csharp
-FFMpeg.ExtractAudio(inputVideoFilePath, outputAudioFilePath);
+FFMpeg.ExtractAudio(inputPath, outputPath);
```
Add or replace audio track on video:
```csharp
-FFMpeg.ReplaceAudio(inputVideoFilePath, inputAudioFilePath, outputVideoFilePath);
+FFMpeg.ReplaceAudio(inputPath, inputAudioPath, outputPath);
```
Add poster image to audio file (good for youtube videos):
```csharp
-FFMpeg.PosterWithAudio(inputImageFilePath, inputAudioFilePath, outputVideoFilePath);
+FFMpeg.PosterWithAudio(inputPath, inputAudioPath, outputPath);
// or
-var image = Image.FromFile(inputImageFile);
-image.AddAudio(inputAudioFilePath, outputVideoFilePath);
+var image = Image.FromFile(inputImagePath);
+image.AddAudio(inputAudioPath, outputPath);
```
Other available arguments could be found in `FFMpegCore.Arguments` namespace.
@@ -135,10 +134,11 @@ var videoFramesSource = new RawVideoPipeSource(CreateFrames(64)) //pass IEnumera
{
FrameRate = 30 //set source frame rate
};
-FFMpegArguments
- .FromPipeInput(videoFramesSource, )
- .OutputToFile("temporary.mp4", false, )
- .ProcessSynchronously();
+await FFMpegArguments
+ .FromPipeInput(videoFramesSource)
+ .OutputToFile(outputPath, false, options => options
+ .WithVideoCodec(VideoCodec.LibVpx))
+ .ProcessAsynchronously();
```
if you want to use `System.Drawing.Bitmap` as `IVideoFrame`, there is a `BitmapVideoFrameWrapper` wrapper class.
@@ -179,13 +179,19 @@ If these folders are not defined, it will try to find the binaries in `/root/(ff
#### Option 1
-The default value (`\\FFMPEG\\bin`) can be overwritten via the `FFMpegOptions` class:
+The default value of an empty string (expecting ffmpeg to be found through PATH) can be overwritten via the `FFOptions` class:
```c#
-public Startup()
-{
- FFMpegOptions.Configure(new FFMpegOptions { RootDirectory = "./bin", TempDirectory = "/tmp" });
-}
+// setting global options
+GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" });
+// or
+GlobalFFOptions.Configure(options => options.BinaryFolder = "./bin");
+
+// or individual, per-run options
+await FFMpegArguments
+ .FromFileInput(inputPath)
+ .OutputToFile(outputPath)
+ .ProcessAsynchronously(true, new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" });
```
#### Option 2
@@ -194,8 +200,8 @@ The root and temp directory for the ffmpeg binaries can be configured via the `f
```json
{
- "RootDirectory": "./bin",
- "TempDirectory": "/tmp"
+ "BinaryFolder": "./bin",
+ "TemporaryFilesFolder": "/tmp"
}
```
@@ -217,6 +223,6 @@ The root and temp directory for the ffmpeg binaries can be configured via the `f
### License
-Copyright © 2020
+Copyright © 2021
Released under [MIT license](https://github.com/rosenbjerg/FFMpegCore/blob/master/LICENSE)