From 83c94780078d8af82d11b7535344120607d25d94 Mon Sep 17 00:00:00 2001 From: crypton Date: Sat, 6 Feb 2021 16:50:12 -0800 Subject: [PATCH 01/18] ffprobe duration parsing - on large recordings (e.g. radio transmissions), ffprobe might return number of hours which is too large for TimeSpan.Parse (exception: The TimeSpan string '149:07:50.911750' could not be parsed because at least one of the numeric components is out of range or contains too many digits.) - use regex groups to extract components (hours/minutes/seconds/millis) then parse/create new timespan from that - NOTICE: this will discard microseconds provided by ffprobe, not sure if this is significant - ffprobe has inconsitencies with how it represents millisecond component. Sometimes it may return just `82` for 820 milliseconds, so padding with 0s is required on the left. Likewise, sometimes it might return microseconds past milliseconds (first 3 significant figures); this is currently discarded - Added InternalsVisibleTo to help with unit testing *just* the duration parsing function Former-commit-id: 35ca34c0b00a9453a26010a41a41e127d66b0413 --- FFMpegCore.Test/FFProbeTests.cs | 18 ++++++++++++- FFMpegCore/Assembly.cs | 3 +++ FFMpegCore/FFProbe/MediaAnalysis.cs | 42 ++++++++++++++++++++++------- 3 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 FFMpegCore/Assembly.cs diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs index 21d9d34..8dc675b 100644 --- a/FFMpegCore.Test/FFProbeTests.cs +++ b/FFMpegCore.Test/FFProbeTests.cs @@ -23,7 +23,23 @@ public async Task Audio_FromStream_Duration() var streamAnalysis = await FFProbe.AnalyseAsync(inputStream); Assert.IsTrue(fileAnalysis.Duration == streamAnalysis.Duration); } - + + [TestMethod] + public void MediaAnalysis_ParseDuration() + { + var durationHHMMSS = new FFProbeStream { Duration = "05:12:59.177" }; + var longDuration = new FFProbeStream { Duration = "149:07:50.911750" }; + var shortDuration = new FFProbeStream { Duration = "00:00:00.83" }; + + var testdurationHHMMSS = MediaAnalysis.ParseDuration(durationHHMMSS); + var testlongDuration = MediaAnalysis.ParseDuration(longDuration); + var testshortDuration = MediaAnalysis.ParseDuration(shortDuration); + + Assert.IsTrue(testdurationHHMMSS.Days == 0 && testdurationHHMMSS.Hours == 5 && testdurationHHMMSS.Minutes == 12 && testdurationHHMMSS.Seconds == 59 && testdurationHHMMSS.Milliseconds == 177); + Assert.IsTrue(testlongDuration.Days == 6 && testlongDuration.Hours == 5 && testlongDuration.Minutes == 7 && testlongDuration.Seconds == 50 && testlongDuration.Milliseconds == 911); + Assert.IsTrue(testdurationHHMMSS.Days == 0 && testshortDuration.Hours == 0 && testshortDuration.Minutes == 0 && testshortDuration.Seconds == 0 && testshortDuration.Milliseconds == 830); + } + [TestMethod] public void Probe_Success() { 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/FFProbe/MediaAnalysis.cs b/FFMpegCore/FFProbe/MediaAnalysis.cs index 5a43aa2..011a8db 100644 --- a/FFMpegCore/FFProbe/MediaAnalysis.cs +++ b/FFMpegCore/FFProbe/MediaAnalysis.cs @@ -7,7 +7,7 @@ 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); + private static readonly Regex DurationRegex = new Regex(@"^(\d+):(\d{1,2}):(\d{1,2})\.(\d{1,3})", RegexOptions.Compiled); internal MediaAnalysis(string path, FFProbeAnalysis analysis) { @@ -23,7 +23,7 @@ private MediaFormat ParseFormat(Format analysisFormat) { return new MediaFormat { - Duration = TimeSpan.Parse(analysisFormat.Duration ?? "0"), + Duration = ParseDuration(analysisFormat.Duration), FormatName = analysisFormat.FormatName, FormatLongName = analysisFormat.FormatLongName, StreamCount = analysisFormat.NbStreams, @@ -74,17 +74,41 @@ private VideoStream ParseVideoStream(FFProbeStream stream) }; } - private static TimeSpan ParseDuration(FFProbeStream ffProbeStream) + internal static TimeSpan ParseDuration(string duration) { - return !string.IsNullOrEmpty(ffProbeStream.Duration) - ? TimeSpan.Parse(ffProbeStream.Duration) - : TimeSpan.Parse(TrimTimeSpan(ffProbeStream.GetDuration()) ?? "0"); + 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; + } } - private static string? TrimTimeSpan(string? durationTag) + internal static TimeSpan ParseDuration(FFProbeStream ffProbeStream) { - var durationMatch = DurationRegex.Match(durationTag ?? ""); - return durationMatch.Success ? durationMatch.Groups[1].Value : null; + return ParseDuration(ffProbeStream.Duration); } private AudioStream ParseAudioStream(FFProbeStream stream) From bff34065455328cbe5a404e89558258709d0a917 Mon Sep 17 00:00:00 2001 From: crypton Date: Fri, 12 Feb 2021 22:16:55 -0800 Subject: [PATCH 02/18] Format -ss timespan argument to calculate hours Former-commit-id: bb08076db4afdef77680303e578fe28bbcf83617 --- FFMpegCore.Test/ArgumentBuilderTest.cs | 2 +- FFMpegCore/FFMpeg/Arguments/SeekArgument.cs | 21 +++++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs index de625e2..aa1c878 100644 --- a/FFMpegCore.Test/ArgumentBuilderTest.cs +++ b/FFMpegCore.Test/ArgumentBuilderTest.cs @@ -197,7 +197,7 @@ public void Builder_BuildString_Loop() 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); + Assert.AreEqual("-ss 00:00:10.000 -i \"input.mp4\" -ss 00:00:10.000 \"output.mp4\"", str); } [TestMethod] 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; + } + } + } } } From 93e56e523c0eefde9ba4c5db9b581a638bf1cbbe Mon Sep 17 00:00:00 2001 From: Thierry Fleury Date: Sun, 28 Feb 2021 19:28:35 +0100 Subject: [PATCH 03/18] Add OutputStreamArgument (cherry picked from commit 0c64c4d81d7055a582d8377123dbc3b7ba86e444) Former-commit-id: 157a53690f92550ef3852c73dab239a25bcc979d --- .../FFMpeg/Arguments/OutputStreamArgument.cs | 26 +++++++++++++++++++ FFMpegCore/FFMpeg/FFMpegArguments.cs | 2 ++ 2 files changed, 28 insertions(+) create mode 100644 FFMpegCore/FFMpeg/Arguments/OutputStreamArgument.cs diff --git a/FFMpegCore/FFMpeg/Arguments/OutputStreamArgument.cs b/FFMpegCore/FFMpeg/Arguments/OutputStreamArgument.cs new file mode 100644 index 0000000..5581929 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/OutputStreamArgument.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace FFMpegCore.Arguments +{ + /// + /// Represents output stream parameter + /// + public class OutputStreamArgument : IOutputArgument + { + public readonly string Stream; + + public OutputStreamArgument(string stream) + { + Stream = stream; + } + + public void Post() { } + + public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask; + + public void Pre() { } + + public string Text => Stream; + } +} diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs index 44e20d2..d2609eb 100644 --- a/FFMpegCore/FFMpeg/FFMpegArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs @@ -51,6 +51,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 OutputToStream(string uri, Action? addArguments = null) => ToProcessor(new OutputStreamArgument(uri), addArguments); + public FFMpegArgumentProcessor OutputToStream(Uri uri, Action? addArguments = null) => ToProcessor(new OutputStreamArgument(uri.ToString()), addArguments); public FFMpegArgumentProcessor OutputToPipe(IPipeSink reader, Action? addArguments = null) => ToProcessor(new OutputPipeArgument(reader), addArguments); private FFMpegArgumentProcessor ToProcessor(IOutputArgument argument, Action? addArguments) From 64e31eb5788e9ff38469da95ee45cc8d1a018efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20K=C3=BChner?= Date: Wed, 10 Mar 2021 07:15:36 +0100 Subject: [PATCH 04/18] Update README.md Former-commit-id: 928ef40f21848a1544ce205b2fc6bcfa8e36e6ad --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6a9fc35..e4abc3e 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,7 @@ The default value (`\\FFMPEG\\bin`) can be overwritten via the `FFMpegOptions` c ```c# public Startup() { - FFMpegOptions.Configure(new FFMpegOptions { RootDirectory = "./bin", TempDirectory = "/tmp" }); + GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/tmp" }); } ``` @@ -194,8 +194,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" } ``` From ba8904429de42cd1c1b69c44ad0d5fc42eb10ddd Mon Sep 17 00:00:00 2001 From: Maxim Bagryantsev Date: Mon, 15 Mar 2021 20:35:19 +0300 Subject: [PATCH 05/18] Fixed process hang on pipe images format mismatch Former-commit-id: fe646752d366a5a9c33fcc30ec457991241e6d41 --- FFMpegCore.Test/Utilities/BitmapSources.cs | 2 +- FFMpegCore.Test/VideoTest.cs | 88 +++++++++++++++++++ FFMpegCore/FFMpeg/Arguments/PipeArgument.cs | 7 +- .../FFMpeg/Exceptions/FFMpegException.cs | 8 ++ FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs | 2 +- 5 files changed, 103 insertions(+), 4 deletions(-) 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/FFMpeg/Arguments/PipeArgument.cs b/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs index 428f21b..e169400 100644 --- a/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs @@ -41,13 +41,16 @@ public async Task During(CancellationToken cancellationToken = default) try { await ProcessDataAsync(cancellationToken); - Debug.WriteLine($"Disconnecting NamedPipeServerStream on {GetType().Name}"); - Pipe?.Disconnect(); + Debug.WriteLine($"Disconnecting NamedPipeServerStream on {GetType().Name}"); } catch (TaskCanceledException) { Debug.WriteLine($"ProcessDataAsync on {GetType().Name} cancelled"); } + finally + { + Pipe?.Disconnect(); + } } protected abstract Task ProcessDataAsync(CancellationToken token); 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/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}"); } From fdc524d5165d4cc39c5e3f9e783c971d19da6417 Mon Sep 17 00:00:00 2001 From: Maxim Bagryantsev Date: Mon, 15 Mar 2021 20:44:48 +0300 Subject: [PATCH 06/18] Moved Debug.WriteLine to Pipe disconnect Former-commit-id: bbd9b7f55cc95b3b648e7a67fff31057b3e2d62b --- FFMpegCore/FFMpeg/Arguments/PipeArgument.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs b/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs index e169400..fcb944a 100644 --- a/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs @@ -40,8 +40,7 @@ public async Task During(CancellationToken cancellationToken = default) { try { - await ProcessDataAsync(cancellationToken); - Debug.WriteLine($"Disconnecting NamedPipeServerStream on {GetType().Name}"); + await ProcessDataAsync(cancellationToken); } catch (TaskCanceledException) { @@ -49,6 +48,7 @@ public async Task During(CancellationToken cancellationToken = default) } finally { + Debug.WriteLine($"Disconnecting NamedPipeServerStream on {GetType().Name}"); Pipe?.Disconnect(); } } From 16d2c8a6ff1dfe40d41a2f28f11cd77d5b409665 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 15 Mar 2021 18:49:20 +0100 Subject: [PATCH 07/18] Update README.md Former-commit-id: eba1dac0b975328bb5dc13e61eebf211d30f0278 --- README.md | 52 +++++++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index e4abc3e..89b8f35 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() -{ - GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "./bin", TemporaryFilesFolder = "/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 From 5fd475210d687c55df47fb0503e91eecbfdfb9db Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 15 Mar 2021 18:49:31 +0100 Subject: [PATCH 08/18] Update README.md Former-commit-id: d44863747a57ba871105e1d1976cd99201c8e0d8 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 89b8f35..9dce345 100644 --- a/README.md +++ b/README.md @@ -223,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) From fdd2d2b6c38998a37e60db5b7e8a1377ba6e86fd Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 15 Mar 2021 22:45:30 +0100 Subject: [PATCH 09/18] Add examples from readme Former-commit-id: cbf241ca3c8f2825a66282ad05f904769e565386 --- .../FFMpegCore.Examples.csproj | 12 ++ FFMpegCore.Examples/Program.cs | 124 ++++++++++++++++++ FFMpegCore.sln | 6 + 3 files changed, 142 insertions(+) create mode 100644 FFMpegCore.Examples/FFMpegCore.Examples.csproj create mode 100644 FFMpegCore.Examples/Program.cs 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.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 From d2ed98ee26b744fc4e01b7f723598b7b209b7a16 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 15 Mar 2021 22:48:43 +0100 Subject: [PATCH 10/18] Move extension method from Bitmap to Image Former-commit-id: 68822845933343f554866373dd4462bbc7c72f54 --- FFMpegCore/Extend/BitmapExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); From ffbce8fba94709d24fdf4c82ab8d3501689c13b5 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 15 Mar 2021 22:48:53 +0100 Subject: [PATCH 11/18] Bump nuget dependencies Former-commit-id: 0face0b6e4b36429ea85e1c4a8405d4d7875f6a2 --- FFMpegCore.Test/FFMpegCore.Test.csproj | 8 ++++---- FFMpegCore/FFMpegCore.csproj | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) 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/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index 69a8e61..615b71a 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -32,7 +32,7 @@ - + From aec737bbc54251f2633607b4f0ed45cc967f023c Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 15 Mar 2021 23:04:59 +0100 Subject: [PATCH 12/18] Renaming to OutputUrlArgument Former-commit-id: fc2802d5fbe7cffc8ea1d4e3ced145abc85c634e --- ...OutputStreamArgument.cs => OutputUrlArgument.cs} | 13 +++++++------ FFMpegCore/FFMpeg/FFMpegArguments.cs | 5 ++--- 2 files changed, 9 insertions(+), 9 deletions(-) rename FFMpegCore/FFMpeg/Arguments/{OutputStreamArgument.cs => OutputUrlArgument.cs} (51%) diff --git a/FFMpegCore/FFMpeg/Arguments/OutputStreamArgument.cs b/FFMpegCore/FFMpeg/Arguments/OutputUrlArgument.cs similarity index 51% rename from FFMpegCore/FFMpeg/Arguments/OutputStreamArgument.cs rename to FFMpegCore/FFMpeg/Arguments/OutputUrlArgument.cs index 5581929..15cbef9 100644 --- a/FFMpegCore/FFMpeg/Arguments/OutputStreamArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/OutputUrlArgument.cs @@ -4,15 +4,16 @@ namespace FFMpegCore.Arguments { /// - /// Represents output stream parameter + /// Represents outputting to url using supported protocols + /// See http://ffmpeg.org/ffmpeg-protocols.html /// - public class OutputStreamArgument : IOutputArgument + public class OutputUrlArgument : IOutputArgument { - public readonly string Stream; + public readonly string Url; - public OutputStreamArgument(string stream) + public OutputUrlArgument(string url) { - Stream = stream; + Url = url; } public void Post() { } @@ -21,6 +22,6 @@ public void Post() { } public void Pre() { } - public string Text => Stream; + public string Text => Url; } } diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs index cdce6be..847e68c 100644 --- a/FFMpegCore/FFMpeg/FFMpegArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs @@ -49,9 +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 OutputToStream(string uri, Action? addArguments = null) => ToProcessor(new OutputStreamArgument(uri), addArguments); - public FFMpegArgumentProcessor OutputToStream(Uri uri, Action? addArguments = null) => ToProcessor(new OutputStreamArgument(uri.ToString()), 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) From 7340380bd06e7134871f80d5c4be1530646d74f7 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 15 Mar 2021 23:06:34 +0100 Subject: [PATCH 13/18] Update nuget meta Former-commit-id: 0a146251e78a786d5a58b75eac0504309670f7a3 --- FFMpegCore/FFMpegCore.csproj | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index 615b71a..0f43758 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -5,18 +5,16 @@ 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) 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 From e1f319e074e851b32503e38acbcae9d21ac4944b Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 15 Mar 2021 23:17:56 +0100 Subject: [PATCH 14/18] Move MediaAnalysis parsing helper methods to static class Former-commit-id: 8a314f02ae9b0ce037998b66764c150ed0a1afc8 --- FFMpegCore/FFProbe/MediaAnalysis.cs | 105 +++++++++++++++------------- 1 file changed, 56 insertions(+), 49 deletions(-) diff --git a/FFMpegCore/FFProbe/MediaAnalysis.cs b/FFMpegCore/FFProbe/MediaAnalysis.cs index f1b2f82..1ee7b18 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); @@ -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,7 +66,56 @@ private VideoStream ParseVideoStream(FFProbeStream stream) }; } - private static TimeSpan ParseDuration(FFProbeStream ffProbeStream) + + private AudioStream ParseAudioStream(FFProbeStream stream) + { + return new AudioStream + { + Index = stream.Index, + BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitRate) : default, + CodecName = stream.CodecName, + CodecLongName = stream.CodecLongName, + Channels = stream.Channels ?? default, + ChannelLayout = stream.ChannelLayout, + Duration = MediaAnalysisUtils.ParseDuration(stream), + SampleRateHz = !string.IsNullOrEmpty(stream.SampleRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.SampleRate) : default, + Profile = stream.Profile, + Language = stream.GetLanguage(), + Tags = stream.Tags, + }; + } + + + } + + public static class MediaAnalysisUtils + { + private static readonly Regex DurationRegex = new Regex("^(\\d{1,5}:\\d{1,2}:\\d{1,2}(.\\d{1,7})?)", 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])); + } + + 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); + } + + public static double ParseDoubleInvariant(string line) => + double.Parse(line, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture); + + public static int ParseIntInvariant(string line) => + int.Parse(line, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture); + + + public static TimeSpan ParseDuration(FFProbeStream ffProbeStream) { return !string.IsNullOrEmpty(ffProbeStream.Duration) ? TimeSpan.Parse(ffProbeStream.Duration) @@ -80,45 +127,5 @@ private static TimeSpan ParseDuration(FFProbeStream ffProbeStream) 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, - 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, - 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) - { - 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) - { - 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) => - double.Parse(line, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture); - - private static int ParseIntInvariant(string line) => - int.Parse(line, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture); } } \ No newline at end of file From 263645f83bf28b6c008c281c4592b3789557d1a3 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 15 Mar 2021 23:43:22 +0100 Subject: [PATCH 15/18] Fix tests Former-commit-id: 1d6517796f5cd2732befaa552a7266cb98e803f1 --- FFMpegCore.Test/FFProbeTests.cs | 24 +++++++++++++----------- FFMpegCore/FFMpeg/FFMpeg.cs | 4 ++-- FFMpegCore/FFProbe/MediaAnalysis.cs | 10 ++-------- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs index a056fd9..cba5c52 100644 --- a/FFMpegCore.Test/FFProbeTests.cs +++ b/FFMpegCore.Test/FFProbeTests.cs @@ -25,20 +25,22 @@ public async Task Audio_FromStream_Duration() Assert.IsTrue(fileAnalysis.Duration == streamAnalysis.Duration); } - [TestMethod] - public void MediaAnalysis_ParseDuration() + [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, 911750)] + [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 durationHHMMSS = new FFProbeStream { Duration = "05:12:59.177" }; - var longDuration = new FFProbeStream { Duration = "149:07:50.911750" }; - var shortDuration = new FFProbeStream { Duration = "00:00:00.83" }; + var ffprobeStream = new FFProbeStream { Duration = duration }; - var testdurationHHMMSS = MediaAnalysis.ParseDuration(durationHHMMSS); - var testlongDuration = MediaAnalysis.ParseDuration(longDuration); - var testshortDuration = MediaAnalysis.ParseDuration(shortDuration); + var parsedDuration = MediaAnalysisUtils.ParseDuration(ffprobeStream); - Assert.IsTrue(testdurationHHMMSS.Days == 0 && testdurationHHMMSS.Hours == 5 && testdurationHHMMSS.Minutes == 12 && testdurationHHMMSS.Seconds == 59 && testdurationHHMMSS.Milliseconds == 177); - Assert.IsTrue(testlongDuration.Days == 6 && testlongDuration.Hours == 5 && testlongDuration.Minutes == 7 && testlongDuration.Seconds == 50 && testlongDuration.Milliseconds == 911); - Assert.IsTrue(testdurationHHMMSS.Days == 0 && testshortDuration.Hours == 0 && testshortDuration.Minutes == 0 && testshortDuration.Seconds == 0 && testshortDuration.Milliseconds == 830); + Assert.AreEqual(parsedDuration.Days, expectedDays); + Assert.AreEqual(parsedDuration.Hours, expectedHours); + Assert.AreEqual(parsedDuration.Minutes, expectedMinutes); + Assert.AreEqual(parsedDuration.Seconds, expectedSeconds); + Assert.AreEqual(parsedDuration.Milliseconds, expectedMilliseconds); } [TestMethod] 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/FFProbe/MediaAnalysis.cs b/FFMpegCore/FFProbe/MediaAnalysis.cs index 772997a..2602f86 100644 --- a/FFMpegCore/FFProbe/MediaAnalysis.cs +++ b/FFMpegCore/FFProbe/MediaAnalysis.cs @@ -18,7 +18,7 @@ private MediaFormat ParseFormat(Format analysisFormat) { return new MediaFormat { - Duration = ParseDuration(analysisFormat.Duration), + Duration = MediaAnalysisUtils.ParseDuration(analysisFormat.Duration), FormatName = analysisFormat.FormatName, FormatLongName = analysisFormat.FormatLongName, StreamCount = analysisFormat.NbStreams, @@ -89,7 +89,7 @@ private AudioStream ParseAudioStream(FFProbeStream stream) public static class MediaAnalysisUtils { - private static readonly Regex DurationRegex = new Regex("^(\\d{1,5}:\\d{1,2}:\\d{1,2}(.\\d{1,7})?)", RegexOptions.Compiled); + 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; @@ -150,11 +150,5 @@ public static TimeSpan ParseDuration(FFProbeStream ffProbeStream) { return ParseDuration(ffProbeStream.Duration); } - - private static string? TrimTimeSpan(string? durationTag) - { - var durationMatch = DurationRegex.Match(durationTag ?? ""); - return durationMatch.Success ? durationMatch.Groups[1].Value : null; - } } } \ No newline at end of file From eed36f579bcc9d1b463856e54e29738ab64c5dcb Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 15 Mar 2021 23:43:28 +0100 Subject: [PATCH 16/18] Update nuget meta Former-commit-id: 277f11d06ebd99f1497835b960e8056f28ee16c0 --- FFMpegCore/FFMpegCore.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index 0f43758..187ad09 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -10,7 +10,8 @@ 3.0.0.0 3.0.0.0 - Fixes for RawVideoPipeSource hanging (thanks to max619) -- Added .OutputToUrl(..) method for outputting to url using supported protocol (thanks to TFleury) +- Added .OutputToUrl(..) method for outputting to url using supported protocol (thanks to TFleury) +Improved timespan parsing (thanks to test-in-prod) 8 4.1.0 MIT From 90f642d61d41ccd3b04fba290628e18e0779751e Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 15 Mar 2021 23:44:31 +0100 Subject: [PATCH 17/18] Update FFMpegCore.csproj Former-commit-id: 97fe2cce6018f554331c841221546ce6c9370960 --- FFMpegCore/FFMpegCore.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index 187ad09..47b41d8 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -11,7 +11,7 @@ 3.0.0.0 - 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) +- Improved timespan parsing (thanks to test-in-prod) 8 4.1.0 MIT From 59dc8e6b94173107c07339962fd213518ab202d4 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Mon, 15 Mar 2021 23:50:11 +0100 Subject: [PATCH 18/18] Fix test Former-commit-id: cce6c6983c812feed041026e610373ca1d5e08ff --- FFMpegCore.Test/FFProbeTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs index cba5c52..5cabc4e 100644 --- a/FFMpegCore.Test/FFProbeTests.cs +++ b/FFMpegCore.Test/FFProbeTests.cs @@ -28,7 +28,7 @@ public async Task Audio_FromStream_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, 911750)] + [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) { @@ -36,11 +36,11 @@ public void MediaAnalysis_ParseDuration(string duration, int expectedDays, int e var parsedDuration = MediaAnalysisUtils.ParseDuration(ffprobeStream); - Assert.AreEqual(parsedDuration.Days, expectedDays); - Assert.AreEqual(parsedDuration.Hours, expectedHours); - Assert.AreEqual(parsedDuration.Minutes, expectedMinutes); - Assert.AreEqual(parsedDuration.Seconds, expectedSeconds); - Assert.AreEqual(parsedDuration.Milliseconds, expectedMilliseconds); + 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]