diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 994a569..a6fe6cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,8 +2,8 @@ name: CI on: push: - branches-ignore: - - release + branches: + - master pull_request: branches: - master @@ -11,15 +11,19 @@ on: jobs: ci: - runs-on: ubuntu-latest - timeout-minutes: 7 + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest, ubuntu-latest] + timeout-minutes: 6 steps: - - uses: actions/checkout@v1 - - name: Prepare FFMpeg - run: sudo apt-get update && sudo apt-get install -y ffmpeg - - name: Setup .NET Core + - name: Checkout + uses: actions/checkout@v1 + - name: Prepare .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 3.1.101 + dotnet-version: '5.0.x' + - name: Prepare FFMpeg + uses: FedericoCarboni/setup-ffmpeg@v1-beta - name: Test with dotnet run: dotnet test --logger GitHubActions diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5f7efcd..0f10b27 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,13 +7,14 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - name: Setup .NET Core + - name: Checkout + uses: actions/checkout@v1 + - name: Prepare .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 3.1 + dotnet-version: '5.0.x' - name: Build solution - run: dotnet build --output build + run: dotnet build --output build -c Release - name: Publish NuGet package - run: dotnet nuget push "build/*.nupkg" --skip-duplicate --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }} + run: dotnet nuget push "build/*.nupkg" --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }} diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs index 4092ad6..f56c4d3 100644 --- a/FFMpegCore.Test/ArgumentBuilderTest.cs +++ b/FFMpegCore.Test/ArgumentBuilderTest.cs @@ -6,7 +6,7 @@ namespace FFMpegCore.Test { [TestClass] - public class ArgumentBuilderTest : BaseTest + public class ArgumentBuilderTest { private readonly string[] _concatFiles = { "1.mp4", "2.mp4", "3.mp4", "4.mp4"}; diff --git a/FFMpegCore.Test/AudioTest.cs b/FFMpegCore.Test/AudioTest.cs index 552ca24..bd53541 100644 --- a/FFMpegCore.Test/AudioTest.cs +++ b/FFMpegCore.Test/AudioTest.cs @@ -3,80 +3,72 @@ using FFMpegCore.Test.Resources; using Microsoft.VisualStudio.TestTools.UnitTesting; using System.IO; +using System.Linq; +using System.Threading.Tasks; +using FFMpegCore.Pipes; namespace FFMpegCore.Test { [TestClass] - public class AudioTest : BaseTest + public class AudioTest { [TestMethod] public void Audio_Remove() { - var output = Input.OutputLocation(VideoType.Mp4); - - try - { - FFMpeg.Mute(Input.FullName, output); - Assert.IsTrue(File.Exists(output)); - } - finally - { - if (File.Exists(output)) File.Delete(output); - } + using var outputFile = new TemporaryFile("out.mp4"); + + FFMpeg.Mute(TestResources.Mp4Video, outputFile); + var analysis = FFProbe.Analyse(outputFile); + + Assert.IsTrue(analysis.VideoStreams.Any()); + Assert.IsTrue(!analysis.AudioStreams.Any()); } [TestMethod] public void Audio_Save() { - var output = Input.OutputLocation(AudioType.Mp3); - - try - { - FFMpeg.ExtractAudio(Input.FullName, output); - Assert.IsTrue(File.Exists(output)); - } - finally - { - if (File.Exists(output)) File.Delete(output); - } + using var outputFile = new TemporaryFile("out.mp3"); + + FFMpeg.ExtractAudio(TestResources.Mp4Video, outputFile); + var analysis = FFProbe.Analyse(outputFile); + + Assert.IsTrue(!analysis.VideoStreams.Any()); + Assert.IsTrue(analysis.AudioStreams.Any()); } - + [TestMethod] + public async Task Audio_FromRaw() + { + await using var file = File.Open(TestResources.RawAudio, FileMode.Open); + var memoryStream = new MemoryStream(); + await FFMpegArguments + .FromPipeInput(new StreamPipeSource(file), options => options.ForceFormat("s16le")) + .OutputToPipe(new StreamPipeSink(memoryStream), options => options.ForceFormat("mp3")) + .ProcessAsynchronously(); + } + [TestMethod] public void Audio_Add() { - var output = Input.OutputLocation(VideoType.Mp4); - try - { - var success = FFMpeg.ReplaceAudio(VideoLibrary.LocalVideoNoAudio.FullName, VideoLibrary.LocalAudio.FullName, output); - Assert.IsTrue(success); - var audioAnalysis = FFProbe.Analyse(VideoLibrary.LocalVideoNoAudio.FullName); - var videoAnalysis = FFProbe.Analyse(VideoLibrary.LocalAudio.FullName); - var outputAnalysis = FFProbe.Analyse(output); - Assert.AreEqual(Math.Max(videoAnalysis.Duration.TotalSeconds, audioAnalysis.Duration.TotalSeconds), outputAnalysis.Duration.TotalSeconds, 0.15); - Assert.IsTrue(File.Exists(output)); - } - finally - { - if (File.Exists(output)) File.Delete(output); - } + using var outputFile = new TemporaryFile("out.mp4"); + + var success = FFMpeg.ReplaceAudio(TestResources.Mp4WithoutAudio, TestResources.Mp3Audio, outputFile); + var videoAnalysis = FFProbe.Analyse(TestResources.Mp4WithoutAudio); + var audioAnalysis = FFProbe.Analyse(TestResources.Mp3Audio); + var outputAnalysis = FFProbe.Analyse(outputFile); + + Assert.IsTrue(success); + Assert.AreEqual(Math.Max(videoAnalysis.Duration.TotalSeconds, audioAnalysis.Duration.TotalSeconds), outputAnalysis.Duration.TotalSeconds, 0.15); + Assert.IsTrue(File.Exists(outputFile)); } [TestMethod] public void Image_AddAudio() { - var output = Input.OutputLocation(VideoType.Mp4); - - try - { - FFMpeg.PosterWithAudio(VideoLibrary.LocalCover.FullName, VideoLibrary.LocalAudio.FullName, output); - var analysis = FFProbe.Analyse(VideoLibrary.LocalAudio.FullName); - Assert.IsTrue(analysis.Duration.TotalSeconds > 0); - Assert.IsTrue(File.Exists(output)); - } - finally - { - if (File.Exists(output)) File.Delete(output); - } + using var outputFile = new TemporaryFile("out.mp4"); + FFMpeg.PosterWithAudio(TestResources.PngImage, TestResources.Mp3Audio, outputFile); + var analysis = FFProbe.Analyse(TestResources.Mp3Audio); + Assert.IsTrue(analysis.Duration.TotalSeconds > 0); + Assert.IsTrue(File.Exists(outputFile)); } } } \ No newline at end of file diff --git a/FFMpegCore.Test/BaseTest.cs b/FFMpegCore.Test/BaseTest.cs deleted file mode 100644 index 38a5bcc..0000000 --- a/FFMpegCore.Test/BaseTest.cs +++ /dev/null @@ -1,15 +0,0 @@ -using FFMpegCore.Test.Resources; -using System.IO; - -namespace FFMpegCore.Test -{ - public class BaseTest - { - protected FileInfo Input; - - public BaseTest() - { - Input = VideoLibrary.LocalVideo; - } - } -} \ No newline at end of file diff --git a/FFMpegCore.Test/FFMpegCore.Test.csproj b/FFMpegCore.Test/FFMpegCore.Test.csproj index 971b098..2cd97bb 100644 --- a/FFMpegCore.Test/FFMpegCore.Test.csproj +++ b/FFMpegCore.Test/FFMpegCore.Test.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net5.0 false @@ -27,6 +27,9 @@ PreserveNewest + + Always + @@ -36,8 +39,8 @@ - - + + diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs index 7fe928e..21d9d34 100644 --- a/FFMpegCore.Test/FFProbeTests.cs +++ b/FFMpegCore.Test/FFProbeTests.cs @@ -11,16 +11,26 @@ public class FFProbeTests [TestMethod] public void Probe_TooLongOutput() { - Assert.ThrowsException(() => FFProbe.Analyse(VideoLibrary.LocalVideo.FullName, 5)); + Assert.ThrowsException(() => FFProbe.Analyse(TestResources.Mp4Video, 5)); + } + + + [TestMethod] + public async Task Audio_FromStream_Duration() + { + var fileAnalysis = await FFProbe.AnalyseAsync(TestResources.WebmVideo); + await using var inputStream = File.OpenRead(TestResources.WebmVideo); + var streamAnalysis = await FFProbe.AnalyseAsync(inputStream); + Assert.IsTrue(fileAnalysis.Duration == streamAnalysis.Duration); } [TestMethod] public void Probe_Success() { - var info = FFProbe.Analyse(VideoLibrary.LocalVideo.FullName); + var info = FFProbe.Analyse(TestResources.Mp4Video); Assert.AreEqual(3, info.Duration.Seconds); Assert.AreEqual(".mp4", info.Extension); - Assert.AreEqual(VideoLibrary.LocalVideo.FullName, info.Path); + Assert.AreEqual(TestResources.Mp4Video, info.Path); Assert.AreEqual("5.1", info.PrimaryAudioStream.ChannelLayout); Assert.AreEqual(6, info.PrimaryAudioStream.Channels); @@ -47,14 +57,14 @@ public void Probe_Success() [TestMethod, Timeout(10000)] public async Task Probe_Async_Success() { - var info = await FFProbe.AnalyseAsync(VideoLibrary.LocalVideo.FullName); + var info = await FFProbe.AnalyseAsync(TestResources.Mp4Video); Assert.AreEqual(3, info.Duration.Seconds); } [TestMethod, Timeout(10000)] public void Probe_Success_FromStream() { - using var stream = File.OpenRead(VideoLibrary.LocalVideoWebm.FullName); + using var stream = File.OpenRead(TestResources.WebmVideo); var info = FFProbe.Analyse(stream); Assert.AreEqual(3, info.Duration.Seconds); } @@ -62,7 +72,7 @@ public void Probe_Success_FromStream() [TestMethod, Timeout(10000)] public async Task Probe_Success_FromStream_Async() { - await using var stream = File.OpenRead(VideoLibrary.LocalVideoWebm.FullName); + await using var stream = File.OpenRead(TestResources.WebmVideo); var info = await FFProbe.AnalyseAsync(stream); Assert.AreEqual(3, info.Duration.Seconds); } diff --git a/FFMpegCore.Test/Resources/TestResources.cs b/FFMpegCore.Test/Resources/TestResources.cs new file mode 100644 index 0000000..f37ed0c --- /dev/null +++ b/FFMpegCore.Test/Resources/TestResources.cs @@ -0,0 +1,28 @@ +using System; +using System.IO; +using FFMpegCore.Enums; + +namespace FFMpegCore.Test.Resources +{ + public enum AudioType + { + Mp3 + } + + public enum ImageType + { + Png + } + + public static class TestResources + { + public static readonly string Mp4Video = "./Resources/input_3sec.mp4"; + public static readonly string WebmVideo = "./Resources/input_3sec.webm"; + public static readonly string Mp4WithoutVideo = "./Resources/input_audio_only_10sec.mp4"; + public static readonly string Mp4WithoutAudio = "./Resources/input_video_only_3sec.mp4"; + public static readonly string RawAudio = "./Resources/audio.raw"; + public static readonly string Mp3Audio = "./Resources/audio.mp3"; + public static readonly string PngImage = "./Resources/cover.png"; + public static readonly string ImageCollection = "./Resources/images"; + } +} diff --git a/FFMpegCore.Test/Resources/VideoLibrary.cs b/FFMpegCore.Test/Resources/VideoLibrary.cs deleted file mode 100644 index 8bb0139..0000000 --- a/FFMpegCore.Test/Resources/VideoLibrary.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.IO; -using FFMpegCore.Enums; - -namespace FFMpegCore.Test.Resources -{ - public enum AudioType - { - Mp3 - } - - public enum ImageType - { - Png - } - - public static class VideoLibrary - { - public static readonly FileInfo LocalVideo = new FileInfo($".{Path.DirectorySeparatorChar}Resources{Path.DirectorySeparatorChar}input_3sec.mp4"); - public static readonly FileInfo LocalVideoWebm = new FileInfo($".{Path.DirectorySeparatorChar}Resources{Path.DirectorySeparatorChar}input_3sec.webm"); - public static readonly FileInfo LocalVideoAudioOnly = new FileInfo($".{Path.DirectorySeparatorChar}Resources{Path.DirectorySeparatorChar}input_audio_only_10sec.mp4"); - public static readonly FileInfo LocalVideoNoAudio = new FileInfo($".{Path.DirectorySeparatorChar}Resources{Path.DirectorySeparatorChar}input_video_only_3sec.mp4"); - public static readonly FileInfo LocalAudio = new FileInfo($".{Path.DirectorySeparatorChar}Resources{Path.DirectorySeparatorChar}audio.mp3"); - public static readonly FileInfo LocalCover = new FileInfo($".{Path.DirectorySeparatorChar}Resources{Path.DirectorySeparatorChar}cover.png"); - public static readonly FileInfo ImageDirectory = new FileInfo($".{Path.DirectorySeparatorChar}Resources{Path.DirectorySeparatorChar}images"); - public static readonly FileInfo ImageJoinOutput = new FileInfo($".{Path.DirectorySeparatorChar}Resources{Path.DirectorySeparatorChar}images{Path.DirectorySeparatorChar}output.mp4"); - - public static string OutputLocation(this FileInfo file, ContainerFormat type) - { - return OutputLocation(file, type.Extension, "_converted"); - } - - public static string OutputLocation(this FileInfo file, AudioType type) - { - return OutputLocation(file, type.ToString(), "_audio"); - } - - public static string OutputLocation(this FileInfo file, ImageType type) - { - return OutputLocation(file, type.ToString(), "_screenshot"); - } - - public static string OutputLocation(this FileInfo file, string type, string keyword) - { - string originalLocation = file.Directory.FullName, - outputFile = file.Name.Replace(file.Extension, keyword + "." + type.ToLowerInvariant()); - - return $"{originalLocation}{Path.DirectorySeparatorChar}{Guid.NewGuid()}_{outputFile}"; - } - } -} diff --git a/FFMpegCore.Test/Resources/audio.raw b/FFMpegCore.Test/Resources/audio.raw new file mode 100644 index 0000000..e131095 Binary files /dev/null and b/FFMpegCore.Test/Resources/audio.raw differ diff --git a/FFMpegCore.Test/TemporaryFile.cs b/FFMpegCore.Test/TemporaryFile.cs new file mode 100644 index 0000000..f64f5fe --- /dev/null +++ b/FFMpegCore.Test/TemporaryFile.cs @@ -0,0 +1,22 @@ +using System; +using System.IO; + +namespace FFMpegCore.Test +{ + public class TemporaryFile : IDisposable + { + private readonly string _path; + + public TemporaryFile(string filename) + { + _path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}-{filename}"); + } + + public static implicit operator string(TemporaryFile temporaryFile) => temporaryFile._path; + public void Dispose() + { + if (File.Exists(_path)) + File.Delete(_path); + } + } +} \ No newline at end of file diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index e4f750d..b2bff02 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -15,105 +15,90 @@ namespace FFMpegCore.Test { [TestClass] - public class VideoTest : BaseTest + public class VideoTest { public bool Convert(ContainerFormat type, bool multithreaded = false, VideoSize size = VideoSize.Original) { - var output = Input.OutputLocation(type); + using var outputFile = new TemporaryFile($"out{type.Extension}"); - try + var input = FFProbe.Analyse(TestResources.Mp4Video); + FFMpeg.Convert(input, outputFile, type, size: size, multithreaded: multithreaded); + var outputVideo = FFProbe.Analyse(outputFile); + + Assert.IsTrue(File.Exists(outputFile)); + Assert.AreEqual(outputVideo.Duration.TotalSeconds, input.Duration.TotalSeconds, 0.1); + if (size == VideoSize.Original) { - var input = FFProbe.Analyse(Input.FullName); - FFMpeg.Convert(input, output, type, size: size, multithreaded: multithreaded); - var outputVideo = FFProbe.Analyse(output); + Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); + Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); + } + else + { + Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); + Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); + Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, (int)size); + } - Assert.IsTrue(File.Exists(output)); - Assert.AreEqual(outputVideo.Duration.TotalSeconds, input.Duration.TotalSeconds, 0.1); - if (size == VideoSize.Original) - { - Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); - Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); - } - else - { - Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); - Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); - Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, (int)size); - } - return File.Exists(output) && - outputVideo.Duration == input.Duration && + return File.Exists(outputFile) && + outputVideo.Duration == input.Duration && + ( ( - ( size == VideoSize.Original && outputVideo.PrimaryVideoStream.Width == input.PrimaryVideoStream.Width && outputVideo.PrimaryVideoStream.Height == input.PrimaryVideoStream.Height - ) || - ( + ) || + ( size != VideoSize.Original && outputVideo.PrimaryVideoStream.Width != input.PrimaryVideoStream.Width && outputVideo.PrimaryVideoStream.Height != input.PrimaryVideoStream.Height && outputVideo.PrimaryVideoStream.Height == (int)size - ) - ); - } - finally - { - if (File.Exists(output)) - File.Delete(output); - } + ) + ); } private void ConvertFromStreamPipe(ContainerFormat type, params IArgument[] arguments) { - var output = Input.OutputLocation(type); + using var outputFile = new TemporaryFile($"out{type.Extension}"); + + var input = FFProbe.Analyse(TestResources.WebmVideo); + using var inputStream = File.OpenRead(input.Path); + var processor = FFMpegArguments + .FromPipeInput(new StreamPipeSource(inputStream)) + .OutputToFile(outputFile, false, opt => + { + foreach (var arg in arguments) + opt.WithArgument(arg); + }); - try + var scaling = arguments.OfType().FirstOrDefault(); + + var success = processor.ProcessSynchronously(); + + var outputVideo = FFProbe.Analyse(outputFile); + + Assert.IsTrue(success); + Assert.IsTrue(File.Exists(outputFile)); + Assert.IsTrue(Math.Abs((outputVideo.Duration - input.Duration).TotalMilliseconds) < 1000.0 / input.PrimaryVideoStream.FrameRate); + + if (scaling?.Size == null) { - var input = FFProbe.Analyse(VideoLibrary.LocalVideoWebm.FullName); - using var inputStream = File.OpenRead(input.Path); - var processor = FFMpegArguments - .FromPipeInput(new StreamPipeSource(inputStream)) - .OutputToFile(output, false, opt => - { - foreach (var arg in arguments) - opt.WithArgument(arg); - }); - - var scaling = arguments.OfType().FirstOrDefault(); - - var success = processor.ProcessSynchronously(); - - var outputVideo = FFProbe.Analyse(output); - - Assert.IsTrue(success); - Assert.IsTrue(File.Exists(output)); - Assert.IsTrue(Math.Abs((outputVideo.Duration - input.Duration).TotalMilliseconds) < 1000.0 / input.PrimaryVideoStream.FrameRate); - - if (scaling?.Size == null) - { - Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); - Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); - } - else - { - if (scaling.Size.Value.Width != -1) - { - Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, scaling.Size.Value.Width); - } - - if (scaling.Size.Value.Height != -1) - { - Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, scaling.Size.Value.Height); - } - - Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); - Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); - } + Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); + Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); } - finally + else { - if (File.Exists(output)) - File.Delete(output); + if (scaling.Size.Value.Width != -1) + { + Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, scaling.Size.Value.Width); + } + + if (scaling.Size.Value.Height != -1) + { + Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, scaling.Size.Value.Height); + } + + Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); + Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); } } @@ -121,7 +106,7 @@ private void ConvertToStreamPipe(params IArgument[] arguments) { using var ms = new MemoryStream(); var processor = FFMpegArguments - .FromFileInput(VideoLibrary.LocalVideo) + .FromFileInput(TestResources.Mp4Video) .OutputToPipe(new StreamPipeSink(ms), opt => { foreach (var arg in arguments) @@ -135,7 +120,7 @@ private void ConvertToStreamPipe(params IArgument[] arguments) ms.Position = 0; var outputVideo = FFProbe.Analyse(ms); - var input = FFProbe.Analyse(VideoLibrary.LocalVideo.FullName); + var input = FFProbe.Analyse(TestResources.Mp4Video); // Assert.IsTrue(Math.Abs((outputVideo.Duration - input.Duration).TotalMilliseconds) < 1000.0 / input.PrimaryVideoStream.FrameRate); if (scaling?.Size == null) @@ -162,53 +147,45 @@ private void ConvertToStreamPipe(params IArgument[] arguments) public void Convert(ContainerFormat type, Action validationMethod, params IArgument[] arguments) { - var output = Input.OutputLocation(type); + using var outputFile = new TemporaryFile($"out{type.Extension}"); - try - { - var input = FFProbe.Analyse(Input.FullName); + var input = FFProbe.Analyse(TestResources.Mp4Video); - var processor = FFMpegArguments - .FromFileInput(VideoLibrary.LocalVideo) - .OutputToFile(output, false, opt => + var processor = FFMpegArguments + .FromFileInput(TestResources.Mp4Video) + .OutputToFile(outputFile, false, opt => { foreach (var arg in arguments) opt.WithArgument(arg); }); - var scaling = arguments.OfType().FirstOrDefault(); - processor.ProcessSynchronously(); + var scaling = arguments.OfType().FirstOrDefault(); + processor.ProcessSynchronously(); - var outputVideo = FFProbe.Analyse(output); + var outputVideo = FFProbe.Analyse(outputFile); - Assert.IsTrue(File.Exists(output)); - Assert.AreEqual(outputVideo.Duration.TotalSeconds, input.Duration.TotalSeconds, 0.1); - validationMethod?.Invoke(outputVideo); - if (scaling?.Size == null) - { - Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); - Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); - } - else - { - if (scaling.Size.Value.Width != -1) - { - Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, scaling.Size.Value.Width); - } - - if (scaling.Size.Value.Height != -1) - { - Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, scaling.Size.Value.Height); - } - - Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); - Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); - } - } - finally + Assert.IsTrue(File.Exists(outputFile)); + Assert.AreEqual(outputVideo.Duration.TotalSeconds, input.Duration.TotalSeconds, 0.1); + validationMethod?.Invoke(outputVideo); + if (scaling?.Size == null) { - if (File.Exists(output)) - File.Delete(output); + Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); + Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); + } + else + { + if (scaling.Size.Value.Width != -1) + { + Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, scaling.Size.Value.Width); + } + + if (scaling.Size.Value.Height != -1) + { + Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, scaling.Size.Value.Height); + } + + Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Width, input.PrimaryVideoStream.Width); + Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Height, input.PrimaryVideoStream.Height); } } @@ -219,50 +196,41 @@ public void Convert(ContainerFormat type, params IArgument[] inputArguments) public void ConvertFromPipe(ContainerFormat type, System.Drawing.Imaging.PixelFormat fmt, params IArgument[] arguments) { - var output = Input.OutputLocation(type); + using var outputFile = new TemporaryFile($"out{type.Extension}"); - try + var videoFramesSource = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, fmt, 256, 256)); + var processor = FFMpegArguments.FromPipeInput(videoFramesSource).OutputToFile(outputFile, false, opt => { - var videoFramesSource = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, fmt, 256, 256)); - var processor = FFMpegArguments.FromPipeInput(videoFramesSource).OutputToFile(output, false, opt => - { - foreach (var arg in arguments) - opt.WithArgument(arg); - }); - var scaling = arguments.OfType().FirstOrDefault(); - processor.ProcessSynchronously(); + foreach (var arg in arguments) + opt.WithArgument(arg); + }); + var scaling = arguments.OfType().FirstOrDefault(); + processor.ProcessSynchronously(); - var outputVideo = FFProbe.Analyse(output); + var outputVideo = FFProbe.Analyse(outputFile); - Assert.IsTrue(File.Exists(output)); + Assert.IsTrue(File.Exists(outputFile)); - if (scaling?.Size == null) - { - Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, videoFramesSource.Width); - Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, videoFramesSource.Height); - } - else - { - if (scaling.Size.Value.Width != -1) - { - Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, scaling.Size.Value.Width); - } - - if (scaling.Size.Value.Height != -1) - { - Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, scaling.Size.Value.Height); - } - - Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Width, videoFramesSource.Width); - Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Height, videoFramesSource.Height); - } - } - finally + if (scaling?.Size == null) { - if (File.Exists(output)) - File.Delete(output); + Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, videoFramesSource.Width); + Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, videoFramesSource.Height); } + else + { + if (scaling.Size.Value.Width != -1) + { + Assert.AreEqual(outputVideo.PrimaryVideoStream.Width, scaling.Size.Value.Width); + } + if (scaling.Size.Value.Height != -1) + { + Assert.AreEqual(outputVideo.PrimaryVideoStream.Height, scaling.Size.Value.Height); + } + + Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Width, videoFramesSource.Width); + Assert.AreNotEqual(outputVideo.PrimaryVideoStream.Height, videoFramesSource.Height); + } } [TestMethod, Timeout(10000)] @@ -287,7 +255,6 @@ public void Video_ToMP4_Args() [DataTestMethod, Timeout(10000)] [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] - // [DataRow(PixelFormat.Format48bppRgb)] public void Video_ToMP4_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat) { ConvertFromPipe(VideoType.Mp4, pixelFormat, new VideoCodecArgument(VideoCodec.LibX264)); @@ -307,11 +274,26 @@ await Assert.ThrowsExceptionAsync(async () => await using var ms = new MemoryStream(); var pipeSource = new StreamPipeSink(ms); await FFMpegArguments - .FromFileInput(VideoLibrary.LocalVideo) + .FromFileInput(TestResources.Mp4Video) .OutputToPipe(pipeSource, opt => opt.ForceFormat("mkv")) .ProcessAsynchronously(); }); } + [TestMethod, Timeout(10000)] + public void Video_StreamFile_OutputToMemoryStream() + { + var output = new MemoryStream(); + + FFMpegArguments + .FromPipeInput(new StreamPipeSource(File.OpenRead(TestResources.WebmVideo)), options => options.ForceFormat("webm")) + .OutputToPipe(new StreamPipeSink(output), options => options + .ForceFormat("mpegts")) + .ProcessSynchronously(); + + output.Position = 0; + var result = FFProbe.Analyse(output); + Console.WriteLine(result.Duration); + } [TestMethod, Timeout(10000)] public void Video_ToMP4_Args_StreamOutputPipe_Failure() @@ -326,7 +308,7 @@ public void Video_ToMP4_Args_StreamOutputPipe_Async() using var ms = new MemoryStream(); var pipeSource = new StreamPipeSink(ms); FFMpegArguments - .FromFileInput(VideoLibrary.LocalVideo) + .FromFileInput(TestResources.Mp4Video) .OutputToPipe(pipeSource, opt => opt .WithVideoCodec(VideoCodec.LibX264) .ForceFormat("matroska")) @@ -337,11 +319,11 @@ public void Video_ToMP4_Args_StreamOutputPipe_Async() [TestMethod, Timeout(10000)] public async Task TestDuplicateRun() { - FFMpegArguments.FromFileInput(VideoLibrary.LocalVideo) + FFMpegArguments.FromFileInput(TestResources.Mp4Video) .OutputToFile("temporary.mp4") .ProcessSynchronously(); - await FFMpegArguments.FromFileInput(VideoLibrary.LocalVideo) + await FFMpegArguments.FromFileInput(TestResources.Mp4Video) .OutputToFile("temporary.mp4") .ProcessAsynchronously(); @@ -372,7 +354,6 @@ public void Video_ToTS_Args() [DataTestMethod, Timeout(10000)] [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] - // [DataRow(PixelFormat.Format48bppRgb)] public void Video_ToTS_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat) { ConvertFromPipe(VideoType.Ts, pixelFormat, new ForceFormatArgument(VideoType.Ts)); @@ -447,190 +428,126 @@ public void Video_ToOGV_MultiThread() [TestMethod, Timeout(10000)] public void Video_Snapshot_InMemory() { - var output = Input.OutputLocation(ImageType.Png); - - try - { - var input = FFProbe.Analyse(Input.FullName); - - using var bitmap = FFMpeg.Snapshot(input); - Assert.AreEqual(input.PrimaryVideoStream.Width, bitmap.Width); - Assert.AreEqual(input.PrimaryVideoStream.Height, bitmap.Height); - Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png); - } - finally - { - if (File.Exists(output)) - File.Delete(output); - } + var input = FFProbe.Analyse(TestResources.Mp4Video); + using var bitmap = FFMpeg.Snapshot(input); + + Assert.AreEqual(input.PrimaryVideoStream.Width, bitmap.Width); + Assert.AreEqual(input.PrimaryVideoStream.Height, bitmap.Height); + Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png); } [TestMethod, Timeout(10000)] public void Video_Snapshot_PersistSnapshot() { - var output = Input.OutputLocation(ImageType.Png); - try - { - var input = FFProbe.Analyse(Input.FullName); + var outputPath = new TemporaryFile("out.png"); + var input = FFProbe.Analyse(TestResources.Mp4Video); - FFMpeg.Snapshot(input, output); + FFMpeg.Snapshot(input, outputPath); - var bitmap = Image.FromFile(output); - Assert.AreEqual(input.PrimaryVideoStream.Width, bitmap.Width); - Assert.AreEqual(input.PrimaryVideoStream.Height, bitmap.Height); - Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png); - bitmap.Dispose(); - } - finally - { - if (File.Exists(output)) - File.Delete(output); - } + using var bitmap = Image.FromFile(outputPath); + Assert.AreEqual(input.PrimaryVideoStream.Width, bitmap.Width); + Assert.AreEqual(input.PrimaryVideoStream.Height, bitmap.Height); + Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png); } [TestMethod, Timeout(10000)] public void Video_Join() { - var output = Input.OutputLocation(VideoType.Mp4); - var newInput = Input.OutputLocation(VideoType.Mp4.Name, "duplicate"); - try - { - var input = FFProbe.Analyse(Input.FullName); - File.Copy(Input.FullName, newInput); - - var success = FFMpeg.Join(output, Input.FullName, newInput); - Assert.IsTrue(success); - - Assert.IsTrue(File.Exists(output)); - var expectedDuration = input.Duration * 2; - var result = FFProbe.Analyse(output); - Assert.AreEqual(expectedDuration.Days, result.Duration.Days); - Assert.AreEqual(expectedDuration.Hours, result.Duration.Hours); - Assert.AreEqual(expectedDuration.Minutes, result.Duration.Minutes); - Assert.AreEqual(expectedDuration.Seconds, result.Duration.Seconds); - Assert.AreEqual(input.PrimaryVideoStream.Height, result.PrimaryVideoStream.Height); - Assert.AreEqual(input.PrimaryVideoStream.Width, result.PrimaryVideoStream.Width); - } - finally - { - if (File.Exists(output)) - File.Delete(output); - - if (File.Exists(newInput)) - File.Delete(newInput); - } + var inputCopy = new TemporaryFile("copy-input.mp4"); + File.Copy(TestResources.Mp4Video, inputCopy); + var outputPath = new TemporaryFile("out.mp4"); + var input = FFProbe.Analyse(TestResources.Mp4Video); + var success = FFMpeg.Join(outputPath, TestResources.Mp4Video, inputCopy); + Assert.IsTrue(success); + Assert.IsTrue(File.Exists(outputPath)); + + var expectedDuration = input.Duration * 2; + var result = FFProbe.Analyse(outputPath); + Assert.AreEqual(expectedDuration.Days, result.Duration.Days); + Assert.AreEqual(expectedDuration.Hours, result.Duration.Hours); + Assert.AreEqual(expectedDuration.Minutes, result.Duration.Minutes); + Assert.AreEqual(expectedDuration.Seconds, result.Duration.Seconds); + Assert.AreEqual(input.PrimaryVideoStream.Height, result.PrimaryVideoStream.Height); + Assert.AreEqual(input.PrimaryVideoStream.Width, result.PrimaryVideoStream.Width); } [TestMethod, Timeout(10000)] public void Video_Join_Image_Sequence() { - try - { - var imageSet = new List(); - Directory.EnumerateFiles(VideoLibrary.ImageDirectory.FullName) - .Where(file => file.ToLower().EndsWith(".png")) - .ToList() - .ForEach(file => - { - for (var i = 0; i < 15; i++) - { - imageSet.Add(new ImageInfo(file)); - } - }); - - var success = FFMpeg.JoinImageSequence(VideoLibrary.ImageJoinOutput.FullName, images: imageSet.ToArray()); - Assert.IsTrue(success); - var result = FFProbe.Analyse(VideoLibrary.ImageJoinOutput.FullName); - - VideoLibrary.ImageJoinOutput.Refresh(); - - Assert.IsTrue(VideoLibrary.ImageJoinOutput.Exists); - Assert.AreEqual(3, result.Duration.Seconds); - Assert.AreEqual(imageSet.First().Width, result.PrimaryVideoStream.Width); - Assert.AreEqual(imageSet.First().Height, result.PrimaryVideoStream.Height); - } - finally - { - VideoLibrary.ImageJoinOutput.Refresh(); - if (VideoLibrary.ImageJoinOutput.Exists) + var imageSet = new List(); + Directory.EnumerateFiles(TestResources.ImageCollection) + .Where(file => file.ToLower().EndsWith(".png")) + .ToList() + .ForEach(file => { - VideoLibrary.ImageJoinOutput.Delete(); - } - } + for (var i = 0; i < 15; i++) + { + imageSet.Add(new ImageInfo(file)); + } + }); + + var outputFile = new TemporaryFile("out.mp4"); + var success = FFMpeg.JoinImageSequence(outputFile, images: imageSet.ToArray()); + Assert.IsTrue(success); + var result = FFProbe.Analyse(outputFile); + Assert.AreEqual(3, result.Duration.Seconds); + Assert.AreEqual(imageSet.First().Width, result.PrimaryVideoStream.Width); + Assert.AreEqual(imageSet.First().Height, result.PrimaryVideoStream.Height); } [TestMethod, Timeout(10000)] public void Video_With_Only_Audio_Should_Extract_Metadata() { - var video = FFProbe.Analyse(VideoLibrary.LocalVideoAudioOnly.FullName); + var video = FFProbe.Analyse(TestResources.Mp4WithoutVideo); Assert.AreEqual(null, video.PrimaryVideoStream); Assert.AreEqual("aac", video.PrimaryAudioStream.CodecName); Assert.AreEqual(10, video.Duration.TotalSeconds, 0.5); - // Assert.AreEqual(1.25, video.Size); } [TestMethod, Timeout(10000)] public void Video_Duration() { - var video = FFProbe.Analyse(VideoLibrary.LocalVideo.FullName); - var output = Input.OutputLocation(VideoType.Mp4); + var video = FFProbe.Analyse(TestResources.Mp4Video); + var outputFile = new TemporaryFile("out.mp4"); - try - { - FFMpegArguments - .FromFileInput(VideoLibrary.LocalVideo) - .OutputToFile(output, false, opt => opt.WithDuration(TimeSpan.FromSeconds(video.Duration.TotalSeconds - 2))) - .ProcessSynchronously(); + FFMpegArguments + .FromFileInput(TestResources.Mp4Video) + .OutputToFile(outputFile, false, opt => opt.WithDuration(TimeSpan.FromSeconds(video.Duration.TotalSeconds - 2))) + .ProcessSynchronously(); - Assert.IsTrue(File.Exists(output)); - var outputVideo = FFProbe.Analyse(output); + Assert.IsTrue(File.Exists(outputFile)); + var outputVideo = FFProbe.Analyse(outputFile); - Assert.AreEqual(video.Duration.Days, outputVideo.Duration.Days); - Assert.AreEqual(video.Duration.Hours, outputVideo.Duration.Hours); - Assert.AreEqual(video.Duration.Minutes, outputVideo.Duration.Minutes); - Assert.AreEqual(video.Duration.Seconds - 2, outputVideo.Duration.Seconds); - } - finally - { - if (File.Exists(output)) - File.Delete(output); - } + Assert.AreEqual(video.Duration.Days, outputVideo.Duration.Days); + Assert.AreEqual(video.Duration.Hours, outputVideo.Duration.Hours); + Assert.AreEqual(video.Duration.Minutes, outputVideo.Duration.Minutes); + Assert.AreEqual(video.Duration.Seconds - 2, outputVideo.Duration.Seconds); } [TestMethod, Timeout(10000)] public void Video_UpdatesProgress() { - var output = Input.OutputLocation(VideoType.Mp4); + var outputFile = new TemporaryFile("out.mp4"); var percentageDone = 0.0; var timeDone = TimeSpan.Zero; void OnPercentageProgess(double percentage) => percentageDone = percentage; void OnTimeProgess(TimeSpan time) => timeDone = time; - var analysis = FFProbe.Analyse(VideoLibrary.LocalVideo.FullName); + var analysis = FFProbe.Analyse(TestResources.Mp4Video); + var success = FFMpegArguments + .FromFileInput(TestResources.Mp4Video) + .OutputToFile(outputFile, false, opt => opt + .WithDuration(TimeSpan.FromSeconds(2))) + .NotifyOnProgress(OnPercentageProgess, analysis.Duration) + .NotifyOnProgress(OnTimeProgess) + .ProcessSynchronously(); - - try - { - var success = FFMpegArguments - .FromFileInput(VideoLibrary.LocalVideo) - .OutputToFile(output, false, opt => opt - .WithDuration(TimeSpan.FromSeconds(2))) - .NotifyOnProgress(OnPercentageProgess, analysis.Duration) - .NotifyOnProgress(OnTimeProgess) - .ProcessSynchronously(); - - Assert.IsTrue(success); - Assert.IsTrue(File.Exists(output)); - Assert.AreNotEqual(0.0, percentageDone); - Assert.AreNotEqual(TimeSpan.Zero, timeDone); - } - finally - { - if (File.Exists(output)) - File.Delete(output); - } + Assert.IsTrue(success); + Assert.IsTrue(File.Exists(outputFile)); + Assert.AreNotEqual(0.0, percentageDone); + Assert.AreNotEqual(TimeSpan.Zero, timeDone); } [TestMethod, Timeout(10000)] @@ -656,11 +573,11 @@ public void Video_TranscodeInMemory() [TestMethod, Timeout(10000)] public async Task Video_Cancel_Async() { - var output = Input.OutputLocation(VideoType.Mp4); + var outputFile = new TemporaryFile("out.mp4"); var task = FFMpegArguments - .FromFileInput(VideoLibrary.LocalVideo) - .OutputToFile(output, false, opt => opt + .FromFileInput(TestResources.Mp4Video) + .OutputToFile(outputFile, false, opt => opt .Resize(new Size(1000, 1000)) .WithAudioCodec(AudioCodec.Aac) .WithVideoCodec(VideoCodec.LibX264) @@ -670,19 +587,11 @@ public async Task Video_Cancel_Async() .CancellableThrough(out var cancel) .ProcessAsynchronously(false); - try - { - await Task.Delay(300); - cancel(); + await Task.Delay(300); + cancel(); - var result = await task; - Assert.IsFalse(result); - } - finally - { - if (File.Exists(output)) - File.Delete(output); - } + var result = await task; + Assert.IsFalse(result); } } } diff --git a/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs b/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs index 17d0372..479fa90 100644 --- a/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs @@ -17,14 +17,14 @@ public InputPipeArgument(IPipeSource writer) : base(PipeDirection.Out) Writer = writer; } - public override string Text => $"-y {Writer.GetFormat()} -i \"{PipePath}\""; + public override string Text => $"-y {Writer.GetStreamArguments()} -i \"{PipePath}\""; protected override async Task ProcessDataAsync(CancellationToken token) { await Pipe.WaitForConnectionAsync(token).ConfigureAwait(false); if (!Pipe.IsConnected) throw new TaskCanceledException(); - await Writer.CopyAsync(Pipe, token).ConfigureAwait(false); + await Writer.WriteAsync(Pipe, token).ConfigureAwait(false); } } } diff --git a/FFMpegCore/FFMpeg/Arguments/OutputPipeArgument.cs b/FFMpegCore/FFMpeg/Arguments/OutputPipeArgument.cs index ebf1e7f..f089a1e 100644 --- a/FFMpegCore/FFMpeg/Arguments/OutputPipeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/OutputPipeArgument.cs @@ -21,7 +21,7 @@ protected override async Task ProcessDataAsync(CancellationToken token) await Pipe.WaitForConnectionAsync(token).ConfigureAwait(false); if (!Pipe.IsConnected) throw new TaskCanceledException(); - await Reader.CopyAsync(Pipe, token).ConfigureAwait(false); + await Reader.ReadAsync(Pipe, token).ConfigureAwait(false); } } } diff --git a/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs b/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs index 4a6113a..428f21b 100644 --- a/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.IO.Pipes; using System.Threading; using System.Threading.Tasks; @@ -30,6 +31,7 @@ public void Pre() public void Post() { + Debug.WriteLine($"Disposing NamedPipeServerStream on {GetType().Name}"); Pipe?.Dispose(); Pipe = null!; } @@ -39,11 +41,13 @@ public async Task During(CancellationToken cancellationToken = default) try { await ProcessDataAsync(cancellationToken); + Debug.WriteLine($"Disconnecting NamedPipeServerStream on {GetType().Name}"); + Pipe?.Disconnect(); } catch (TaskCanceledException) { + Debug.WriteLine($"ProcessDataAsync on {GetType().Name} cancelled"); } - Pipe.Disconnect(); } protected abstract Task ProcessDataAsync(CancellationToken token); diff --git a/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs b/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs index 8c046ac..37c2bda 100644 --- a/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs +++ b/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs @@ -4,7 +4,7 @@ namespace FFMpegCore.Enums { public class ContainerFormat { - private static readonly Regex _formatRegex = new Regex(@"([D ])([E ])\s+([a-z0-9_]+)\s+(.+)"); + private static readonly Regex FormatRegex = new Regex(@"([D ])([E ])\s+([a-z0-9_]+)\s+(.+)"); public string Name { get; private set; } public bool DemuxingSupported { get; private set; } @@ -27,17 +27,19 @@ internal ContainerFormat(string name) internal static bool TryParse(string line, out ContainerFormat fmt) { - var match = _formatRegex.Match(line); + var match = FormatRegex.Match(line); if (!match.Success) { fmt = null!; return false; } - fmt = new ContainerFormat(match.Groups[3].Value); - fmt.DemuxingSupported = match.Groups[1].Value == " "; - fmt.MuxingSupported = match.Groups[2].Value == " "; - fmt.Description = match.Groups[4].Value; + fmt = new ContainerFormat(match.Groups[3].Value) + { + DemuxingSupported = match.Groups[1].Value == " ", + MuxingSupported = match.Groups[2].Value == " ", + Description = match.Groups[4].Value + }; return true; } } diff --git a/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs b/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs index 6bd608d..fc154ac 100644 --- a/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs +++ b/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs @@ -13,15 +13,16 @@ public enum FFMpegExceptionType public class FFMpegException : Exception { - - public FFMpegException(FFMpegExceptionType type, string? message = null, Exception? innerException = null, string ffMpegErrorOutput = "") + public FFMpegException(FFMpegExceptionType type, string? message = null, Exception? innerException = null, string ffmpegErrorOutput = "", string ffmpegOutput = "") : base(message, innerException) { - FFMpegErrorOutput = ffMpegErrorOutput; + FfmpegOutput = ffmpegOutput; + FfmpegErrorOutput = ffmpegErrorOutput; Type = type; } public FFMpegExceptionType Type { get; } - public string FFMpegErrorOutput { get; } + public string FfmpegOutput { get; } + public string FfmpegErrorOutput { get; } } } \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index 8a442ed..cc611e3 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -72,7 +72,8 @@ public static Bitmap Snapshot(IMediaAnalysis source, Size? size = null, TimeSpan .ProcessSynchronously(); ms.Position = 0; - return new Bitmap(ms); + using var bitmap = new Bitmap(ms); + return bitmap.Clone(new Rectangle(0, 0, bitmap.Width, bitmap.Height), bitmap.PixelFormat); } /// /// Saves a 'png' thumbnail to an in-memory bitmap diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs index 92fa6fe..5961ed3 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs @@ -70,20 +70,20 @@ void OnCancelEvent(object sender, EventArgs args) } catch (Exception e) { - if (!HandleException(throwOnError, e, instance.ErrorData)) return false; + if (!HandleException(throwOnError, e, instance.ErrorData, instance.OutputData)) return false; } finally { CancelEvent -= OnCancelEvent; } - return HandleCompletion(throwOnError, errorCode, instance.ErrorData); + return HandleCompletion(throwOnError, errorCode, instance.ErrorData, instance.OutputData); } - private bool HandleCompletion(bool throwOnError, int errorCode, IReadOnlyList errorData) + private bool HandleCompletion(bool throwOnError, int errorCode, IReadOnlyList errorData, IReadOnlyList outputData) { if (throwOnError && errorCode != 0) - throw new FFMpegException(FFMpegExceptionType.Conversion, string.Join("\n", errorData)); + throw new FFMpegException(FFMpegExceptionType.Conversion, "FFMpeg exited with non-zero exitcode.", null, string.Join("\n", errorData), string.Join("\n", outputData)); _onPercentageProgress?.Invoke(100.0); if (_totalTimespan.HasValue) _onTimeProgress?.Invoke(_totalTimespan.Value); @@ -98,7 +98,7 @@ public async Task ProcessAsynchronously(bool throwOnError = true) void OnCancelEvent(object sender, EventArgs args) { - instance?.SendInput("q"); + instance.SendInput("q"); cancellationTokenSource.Cancel(); instance.Started = false; } @@ -116,14 +116,14 @@ await Task.WhenAll(instance.FinishedRunning().ContinueWith(t => } catch (Exception e) { - if (!HandleException(throwOnError, e, instance.ErrorData)) return false; + if (!HandleException(throwOnError, e, instance.ErrorData, instance.OutputData)) return false; } finally { CancelEvent -= OnCancelEvent; } - return HandleCompletion(throwOnError, errorCode, instance.ErrorData); + return HandleCompletion(throwOnError, errorCode, instance.ErrorData, instance.OutputData); } private Instance PrepareInstance(out CancellationTokenSource cancellationTokenSource) @@ -140,13 +140,12 @@ private Instance PrepareInstance(out CancellationTokenSource cancellationTokenSo } - private static bool HandleException(bool throwOnError, Exception e, IReadOnlyList errorData) + private static bool HandleException(bool throwOnError, Exception e, IReadOnlyList errorData, IReadOnlyList outputData) { if (!throwOnError) return false; - throw new FFMpegException(FFMpegExceptionType.Process, "Exception thrown during processing", e, - string.Join("\n", errorData)); + throw new FFMpegException(FFMpegExceptionType.Process, "Exception thrown during processing", e, string.Join("\n", errorData), string.Join("\n", outputData)); } private void OutputData(object sender, (DataType Type, string Data) msg) diff --git a/FFMpegCore/FFMpeg/FFMpegOptions.cs b/FFMpegCore/FFMpeg/FFMpegOptions.cs index 8a98a0a..947f942 100644 --- a/FFMpegCore/FFMpeg/FFMpegOptions.cs +++ b/FFMpegCore/FFMpeg/FFMpegOptions.cs @@ -32,7 +32,7 @@ static FFMpegOptions() { if (File.Exists(ConfigFile)) { - Options = JsonSerializer.Deserialize(File.ReadAllText(ConfigFile)); + Options = JsonSerializer.Deserialize(File.ReadAllText(ConfigFile))!; foreach (var pair in DefaultExtensionsOverrides) if (!Options.ExtensionOverrides.ContainsKey(pair.Key)) Options.ExtensionOverrides.Add(pair.Key, pair.Value); } diff --git a/FFMpegCore/FFMpeg/Pipes/IPipeSink.cs b/FFMpegCore/FFMpeg/Pipes/IPipeSink.cs index 8010d87..875407e 100644 --- a/FFMpegCore/FFMpeg/Pipes/IPipeSink.cs +++ b/FFMpegCore/FFMpeg/Pipes/IPipeSink.cs @@ -5,7 +5,7 @@ namespace FFMpegCore.Pipes { public interface IPipeSink { - Task CopyAsync(System.IO.Stream inputStream, CancellationToken cancellationToken); + Task ReadAsync(System.IO.Stream inputStream, CancellationToken cancellationToken); string GetFormat(); } } diff --git a/FFMpegCore/FFMpeg/Pipes/IPipeSource.cs b/FFMpegCore/FFMpeg/Pipes/IPipeSource.cs index 35766d0..cdd5139 100644 --- a/FFMpegCore/FFMpeg/Pipes/IPipeSource.cs +++ b/FFMpegCore/FFMpeg/Pipes/IPipeSource.cs @@ -8,7 +8,7 @@ namespace FFMpegCore.Pipes /// public interface IPipeSource { - string GetFormat(); - Task CopyAsync(System.IO.Stream outputStream, CancellationToken cancellationToken); + string GetStreamArguments(); + Task WriteAsync(System.IO.Stream outputStream, CancellationToken cancellationToken); } } diff --git a/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs b/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs index 8739a40..f61bb7c 100644 --- a/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs +++ b/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs @@ -25,7 +25,7 @@ public RawVideoPipeSource(IEnumerator framesEnumerator) public RawVideoPipeSource(IEnumerable framesEnumerator) : this(framesEnumerator.GetEnumerator()) { } - public string GetFormat() + public string GetStreamArguments() { if (!_formatInitialized) { @@ -45,7 +45,7 @@ public string GetFormat() return $"-f rawvideo -r {FrameRate} -pix_fmt {StreamFormat} -s {Width}x{Height}"; } - public async Task CopyAsync(System.IO.Stream outputStream, CancellationToken cancellationToken) + public async Task WriteAsync(System.IO.Stream outputStream, CancellationToken cancellationToken) { if (_framesEnumerator.Current != null) { diff --git a/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs b/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs index ca2246f..cd13f40 100644 --- a/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs +++ b/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs @@ -1,21 +1,27 @@ -using System.Threading; +using System; +using System.IO; +using System.Threading; using System.Threading.Tasks; namespace FFMpegCore.Pipes { public class StreamPipeSink : IPipeSink { - public System.IO.Stream Destination { get; } + public Func Writer { get; } public int BlockSize { get; set; } = 4096; public string Format { get; set; } = string.Empty; - public StreamPipeSink(System.IO.Stream destination) + public StreamPipeSink(Func writer) { - Destination = destination; + Writer = writer; + } + public StreamPipeSink(Stream destination) + { + Writer = (inputStream, cancellationToken) => inputStream.CopyToAsync(destination, BlockSize, cancellationToken); } - public Task CopyAsync(System.IO.Stream inputStream, CancellationToken cancellationToken) => - inputStream.CopyToAsync(Destination, BlockSize, cancellationToken); + public Task ReadAsync(System.IO.Stream inputStream, CancellationToken cancellationToken) + => Writer(inputStream, cancellationToken); public string GetFormat() => Format; } diff --git a/FFMpegCore/FFMpeg/Pipes/StreamPipeSource.cs b/FFMpegCore/FFMpeg/Pipes/StreamPipeSource.cs index db41eb7..404029f 100644 --- a/FFMpegCore/FFMpeg/Pipes/StreamPipeSource.cs +++ b/FFMpegCore/FFMpeg/Pipes/StreamPipeSource.cs @@ -17,8 +17,8 @@ public StreamPipeSource(System.IO.Stream source) Source = source; } - public Task CopyAsync(System.IO.Stream outputStream, CancellationToken cancellationToken) => Source.CopyToAsync(outputStream, BlockSize, cancellationToken); + public string GetStreamArguments() => StreamFormat; - public string GetFormat() => StreamFormat; + public Task WriteAsync(System.IO.Stream outputStream, CancellationToken cancellationToken) => Source.CopyToAsync(outputStream, BlockSize, cancellationToken); } } diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index 057f605..43247f7 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -9,10 +9,10 @@ 3.0.0.0 3.0.0.0 3.0.0.0 - - Fix hanging pipes on unix sockets -- Internal API cleanup + - Also include ffmpeg output data on non-zero exit code 8 - 3.1.0 + 3.2.4 + MIT Malte Rosenbjerg, Vlad Jerca ffmpeg ffprobe convert video audio mediafile resize analyze muxing GitHub @@ -29,8 +29,8 @@ - - + + diff --git a/FFMpegCore/FFMpegCore.csproj.DotSettings b/FFMpegCore/FFMpegCore.csproj.DotSettings new file mode 100644 index 0000000..69be7ec --- /dev/null +++ b/FFMpegCore/FFMpegCore.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs index f650371..076bbfd 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -46,7 +46,7 @@ public static IMediaAnalysis Analyse(Stream stream, int outputCapacity = int.Max } var exitCode = task.ConfigureAwait(false).GetAwaiter().GetResult(); if (exitCode != 0) - throw new FFMpegException(FFMpegExceptionType.Process, $"FFProbe process returned exit status {exitCode}: {string.Join("\n", instance.OutputData)} {string.Join("\n", instance.ErrorData)}"); + throw new FFMpegException(FFMpegExceptionType.Process, $"FFProbe process returned exit status {exitCode}", null, string.Join("\n", instance.ErrorData), string.Join("\n", instance.OutputData)); return ParseOutput(pipeArgument.PipePath, instance); } @@ -86,7 +86,7 @@ public static async Task AnalyseAsync(Stream stream, int outputC } var exitCode = await task; if (exitCode != 0) - throw new FFMpegException(FFMpegExceptionType.Process, $"FFProbe process returned exit status {exitCode}: {string.Join("\n", instance.OutputData)} {string.Join("\n", instance.ErrorData)}"); + throw new FFMpegException(FFMpegExceptionType.Process, $"FFProbe process returned exit status {exitCode}", null, string.Join("\n", instance.ErrorData), string.Join("\n", instance.OutputData)); pipeArgument.Post(); return ParseOutput(pipeArgument.PipePath, instance); @@ -98,7 +98,7 @@ private static IMediaAnalysis ParseOutput(string filePath, Instance instance) var ffprobeAnalysis = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true - }); + })!; return new MediaAnalysis(filePath, ffprobeAnalysis); } @@ -106,7 +106,7 @@ private static Instance PrepareInstance(string filePath, int outputCapacity) { FFProbeHelper.RootExceptionCheck(); FFProbeHelper.VerifyFFProbeExists(); - var arguments = $"-print_format json -show_format -sexagesimal -show_streams \"{filePath}\""; + var arguments = $"-loglevel error -print_format json -show_format -sexagesimal -show_streams \"{filePath}\""; var instance = new Instance(FFMpegOptions.Options.FFProbeBinary(), arguments) {DataBufferCapacity = outputCapacity}; return instance; }