diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6fe6cb..6946920 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,12 +18,12 @@ jobs: timeout-minutes: 6 steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v2 - name: Prepare .NET uses: actions/setup-dotnet@v1 with: dotnet-version: '5.0.x' - name: Prepare FFMpeg - uses: FedericoCarboni/setup-ffmpeg@v1-beta + uses: FedericoCarboni/setup-ffmpeg@v1 - name: Test with dotnet run: dotnet test --logger GitHubActions diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0f10b27..5ef0a4c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v2 - name: Prepare .NET uses: actions/setup-dotnet@v1 with: diff --git a/FFMpegCore.Test/AudioTest.cs b/FFMpegCore.Test/AudioTest.cs index bd53541..f1abb72 100644 --- a/FFMpegCore.Test/AudioTest.cs +++ b/FFMpegCore.Test/AudioTest.cs @@ -1,11 +1,13 @@ -using System; -using FFMpegCore.Enums; +using FFMpegCore.Enums; +using FFMpegCore.Exceptions; +using FFMpegCore.Pipes; using FFMpegCore.Test.Resources; using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using FFMpegCore.Pipes; namespace FFMpegCore.Test { @@ -70,5 +72,155 @@ public void Image_AddAudio() Assert.IsTrue(analysis.Duration.TotalSeconds > 0); Assert.IsTrue(File.Exists(outputFile)); } + + [TestMethod, Timeout(10000)] + public void Audio_ToAAC_Args_Pipe() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var samples = new List + { + new PcmAudioSampleWrapper(new byte[] { 0, 0 }), + new PcmAudioSampleWrapper(new byte[] { 0, 0 }), + }; + + var audioSamplesSource = new RawAudioPipeSource(samples) + { + Channels = 2, + Format = "s8", + SampleRate = 8000, + }; + + var success = FFMpegArguments + .FromPipeInput(audioSamplesSource) + .OutputToFile(outputFile, false, opt => opt + .WithAudioCodec(AudioCodec.Aac)) + .ProcessSynchronously(); + Assert.IsTrue(success); + } + + [TestMethod, Timeout(10000)] + public void Audio_ToLibVorbis_Args_Pipe() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var samples = new List + { + new PcmAudioSampleWrapper(new byte[] { 0, 0 }), + new PcmAudioSampleWrapper(new byte[] { 0, 0 }), + }; + + var audioSamplesSource = new RawAudioPipeSource(samples) + { + Channels = 2, + Format = "s8", + SampleRate = 8000, + }; + + var success = FFMpegArguments + .FromPipeInput(audioSamplesSource) + .OutputToFile(outputFile, false, opt => opt + .WithAudioCodec(AudioCodec.LibVorbis)) + .ProcessSynchronously(); + Assert.IsTrue(success); + } + + [TestMethod, Timeout(10000)] + public async Task Audio_ToAAC_Args_Pipe_Async() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var samples = new List + { + new PcmAudioSampleWrapper(new byte[] { 0, 0 }), + new PcmAudioSampleWrapper(new byte[] { 0, 0 }), + }; + + var audioSamplesSource = new RawAudioPipeSource(samples) + { + Channels = 2, + Format = "s8", + SampleRate = 8000, + }; + + var success = await FFMpegArguments + .FromPipeInput(audioSamplesSource) + .OutputToFile(outputFile, false, opt => opt + .WithAudioCodec(AudioCodec.Aac)) + .ProcessAsynchronously(); + Assert.IsTrue(success); + } + + [TestMethod, Timeout(10000)] + public void Audio_ToAAC_Args_Pipe_ValidDefaultConfiguration() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var samples = new List + { + new PcmAudioSampleWrapper(new byte[] { 0, 0 }), + new PcmAudioSampleWrapper(new byte[] { 0, 0 }), + }; + + var audioSamplesSource = new RawAudioPipeSource(samples); + + var success = FFMpegArguments + .FromPipeInput(audioSamplesSource) + .OutputToFile(outputFile, false, opt => opt + .WithAudioCodec(AudioCodec.Aac)) + .ProcessSynchronously(); + Assert.IsTrue(success); + } + + [TestMethod, Timeout(10000)] + public void Audio_ToAAC_Args_Pipe_InvalidChannels() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var audioSamplesSource = new RawAudioPipeSource(new List()) + { + Channels = 0, + }; + + var ex = Assert.ThrowsException(() => FFMpegArguments + .FromPipeInput(audioSamplesSource) + .OutputToFile(outputFile, false, opt => opt + .WithAudioCodec(AudioCodec.Aac)) + .ProcessSynchronously()); + } + + [TestMethod, Timeout(10000)] + public void Audio_ToAAC_Args_Pipe_InvalidFormat() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var audioSamplesSource = new RawAudioPipeSource(new List()) + { + Format = "s8le", + }; + + var ex = Assert.ThrowsException(() => FFMpegArguments + .FromPipeInput(audioSamplesSource) + .OutputToFile(outputFile, false, opt => opt + .WithAudioCodec(AudioCodec.Aac)) + .ProcessSynchronously()); + } + + [TestMethod, Timeout(10000)] + public void Audio_ToAAC_Args_Pipe_InvalidSampleRate() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var audioSamplesSource = new RawAudioPipeSource(new List()) + { + SampleRate = 0, + }; + + var ex = Assert.ThrowsException(() => FFMpegArguments + .FromPipeInput(audioSamplesSource) + .OutputToFile(outputFile, false, opt => opt + .WithAudioCodec(AudioCodec.Aac)) + .ProcessSynchronously()); + } } } \ No newline at end of file diff --git a/FFMpegCore.Test/Resources/TestResources.cs b/FFMpegCore.Test/Resources/TestResources.cs index f37ed0c..6277dd3 100644 --- a/FFMpegCore.Test/Resources/TestResources.cs +++ b/FFMpegCore.Test/Resources/TestResources.cs @@ -1,8 +1,4 @@ -using System; -using System.IO; -using FFMpegCore.Enums; - -namespace FFMpegCore.Test.Resources +namespace FFMpegCore.Test.Resources { public enum AudioType { diff --git a/FFMpegCore/Extend/BitmapVideoFrameWrapper.cs b/FFMpegCore/Extend/BitmapVideoFrameWrapper.cs index e2f0737..678bdcb 100644 --- a/FFMpegCore/Extend/BitmapVideoFrameWrapper.cs +++ b/FFMpegCore/Extend/BitmapVideoFrameWrapper.cs @@ -1,6 +1,7 @@ using System; using System.Drawing; using System.Drawing.Imaging; +using System.IO; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -24,7 +25,7 @@ public BitmapVideoFrameWrapper(Bitmap bitmap) Format = ConvertStreamFormat(bitmap.PixelFormat); } - public void Serialize(System.IO.Stream stream) + public void Serialize(Stream stream) { var data = Source.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.ReadOnly, Source.PixelFormat); @@ -40,7 +41,7 @@ public void Serialize(System.IO.Stream stream) } } - public async Task SerializeAsync(System.IO.Stream stream, CancellationToken token) + public async Task SerializeAsync(Stream stream, CancellationToken token) { var data = Source.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.ReadOnly, Source.PixelFormat); @@ -67,6 +68,8 @@ private static string ConvertStreamFormat(PixelFormat fmt) { case PixelFormat.Format16bppGrayScale: return "gray16le"; + case PixelFormat.Format16bppRgb555: + return "bgr555le"; case PixelFormat.Format16bppRgb565: return "bgr565le"; case PixelFormat.Format24bppRgb: diff --git a/FFMpegCore/Extend/PcmAudioSampleWrapper.cs b/FFMpegCore/Extend/PcmAudioSampleWrapper.cs new file mode 100644 index 0000000..d67038b --- /dev/null +++ b/FFMpegCore/Extend/PcmAudioSampleWrapper.cs @@ -0,0 +1,27 @@ +using FFMpegCore.Pipes; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +public class PcmAudioSampleWrapper : IAudioSample +{ + //This could actually be short or int, but copies would be inefficient. + //Handling bytes lets the user decide on the conversion, and abstract the library + //from handling shorts, unsigned shorts, integers, unsigned integers and floats. + private readonly byte[] _sample; + + public PcmAudioSampleWrapper(byte[] sample) + { + _sample = sample; + } + + public void Serialize(Stream stream) + { + stream.Write(_sample, 0, _sample.Length); + } + + public async Task SerializeAsync(Stream stream, CancellationToken token) + { + await stream.WriteAsync(_sample, 0, _sample.Length, token); + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs b/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs index 479fa90..199d324 100644 --- a/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs @@ -17,7 +17,7 @@ public InputPipeArgument(IPipeSource writer) : base(PipeDirection.Out) Writer = writer; } - public override string Text => $"-y {Writer.GetStreamArguments()} -i \"{PipePath}\""; + public override string Text => $"{Writer.GetStreamArguments()} -i \"{PipePath}\""; protected override async Task ProcessDataAsync(CancellationToken token) { diff --git a/FFMpegCore/FFMpeg/Arguments/SetMirroringArgument.cs b/FFMpegCore/FFMpeg/Arguments/SetMirroringArgument.cs index f042f77..fff98f3 100644 --- a/FFMpegCore/FFMpeg/Arguments/SetMirroringArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/SetMirroringArgument.cs @@ -1,7 +1,5 @@ using FFMpegCore.Enums; using System; -using System.Collections.Generic; -using System.Text; namespace FFMpegCore.Arguments { diff --git a/FFMpegCore/FFMpeg/Arguments/SizeArgument.cs b/FFMpegCore/FFMpeg/Arguments/SizeArgument.cs index 04fe615..924c0a0 100644 --- a/FFMpegCore/FFMpeg/Arguments/SizeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/SizeArgument.cs @@ -1,5 +1,4 @@ using System.Drawing; -using FFMpegCore.Enums; namespace FFMpegCore.Arguments { diff --git a/FFMpegCore/FFMpeg/Pipes/IAudioSample.cs b/FFMpegCore/FFMpeg/Pipes/IAudioSample.cs new file mode 100644 index 0000000..c7dea65 --- /dev/null +++ b/FFMpegCore/FFMpeg/Pipes/IAudioSample.cs @@ -0,0 +1,16 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace FFMpegCore.Pipes +{ + /// + /// Interface for Audio sample + /// + public interface IAudioSample + { + void Serialize(Stream stream); + + Task SerializeAsync(Stream stream, CancellationToken token); + } +} diff --git a/FFMpegCore/FFMpeg/Pipes/IPipeSink.cs b/FFMpegCore/FFMpeg/Pipes/IPipeSink.cs index 875407e..e5f2bf4 100644 --- a/FFMpegCore/FFMpeg/Pipes/IPipeSink.cs +++ b/FFMpegCore/FFMpeg/Pipes/IPipeSink.cs @@ -1,11 +1,12 @@ -using System.Threading; +using System.IO; +using System.Threading; using System.Threading.Tasks; namespace FFMpegCore.Pipes { public interface IPipeSink { - Task ReadAsync(System.IO.Stream inputStream, CancellationToken cancellationToken); + Task ReadAsync(Stream inputStream, CancellationToken cancellationToken); string GetFormat(); } } diff --git a/FFMpegCore/FFMpeg/Pipes/IPipeSource.cs b/FFMpegCore/FFMpeg/Pipes/IPipeSource.cs index cdd5139..c250421 100644 --- a/FFMpegCore/FFMpeg/Pipes/IPipeSource.cs +++ b/FFMpegCore/FFMpeg/Pipes/IPipeSource.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System.IO; +using System.Threading; using System.Threading.Tasks; namespace FFMpegCore.Pipes @@ -9,6 +10,6 @@ namespace FFMpegCore.Pipes public interface IPipeSource { string GetStreamArguments(); - Task WriteAsync(System.IO.Stream outputStream, CancellationToken cancellationToken); + Task WriteAsync(Stream outputStream, CancellationToken cancellationToken); } } diff --git a/FFMpegCore/FFMpeg/Pipes/IVideoFrame.cs b/FFMpegCore/FFMpeg/Pipes/IVideoFrame.cs index 094040b..dd583d9 100644 --- a/FFMpegCore/FFMpeg/Pipes/IVideoFrame.cs +++ b/FFMpegCore/FFMpeg/Pipes/IVideoFrame.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System.IO; +using System.Threading; using System.Threading.Tasks; namespace FFMpegCore.Pipes @@ -12,7 +13,7 @@ public interface IVideoFrame int Height { get; } string Format { get; } - void Serialize(System.IO.Stream pipe); - Task SerializeAsync(System.IO.Stream pipe, CancellationToken token); + void Serialize(Stream pipe); + Task SerializeAsync(Stream pipe, CancellationToken token); } } diff --git a/FFMpegCore/FFMpeg/Pipes/RawAudioPipeSource.cs b/FFMpegCore/FFMpeg/Pipes/RawAudioPipeSource.cs new file mode 100644 index 0000000..8797694 --- /dev/null +++ b/FFMpegCore/FFMpeg/Pipes/RawAudioPipeSource.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace FFMpegCore.Pipes +{ + /// + /// Implementation of for a raw audio stream that is gathered from . + /// It is the user's responbility to make sure the enumerated samples match the configuration provided to this pipe. + /// + public class RawAudioPipeSource : IPipeSource + { + private readonly IEnumerator _sampleEnumerator; + + public string Format { get; set; } = "s16le"; + public uint SampleRate { get; set; } = 8000; + public uint Channels { get; set; } = 1; + + public RawAudioPipeSource(IEnumerator sampleEnumerator) + { + _sampleEnumerator = sampleEnumerator; + } + + public RawAudioPipeSource(IEnumerable sampleEnumerator) + : this(sampleEnumerator.GetEnumerator()) { } + + public string GetStreamArguments() + { + return $"-f {Format} -ar {SampleRate} -ac {Channels}"; + } + + public async Task WriteAsync(Stream outputStream, CancellationToken cancellationToken) + { + if (_sampleEnumerator.Current != null) + { + await _sampleEnumerator.Current.SerializeAsync(outputStream, cancellationToken).ConfigureAwait(false); + } + + while (_sampleEnumerator.MoveNext()) + { + await _sampleEnumerator.Current!.SerializeAsync(outputStream, cancellationToken).ConfigureAwait(false); + } + } + } +} diff --git a/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs b/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs index 65f622e..0e3ab61 100644 --- a/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs +++ b/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Threading; using System.Threading.Tasks; using FFMpegCore.Exceptions; @@ -46,7 +47,7 @@ public string GetStreamArguments() return $"-f rawvideo -r {FrameRate.ToString(CultureInfo.InvariantCulture)} -pix_fmt {StreamFormat} -s {Width}x{Height}"; } - public async Task WriteAsync(System.IO.Stream outputStream, CancellationToken cancellationToken) + public async Task WriteAsync(Stream outputStream, CancellationToken cancellationToken) { if (_framesEnumerator.Current != null) { diff --git a/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs b/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs index cd13f40..addc14e 100644 --- a/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs +++ b/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs @@ -20,7 +20,7 @@ public StreamPipeSink(Stream destination) Writer = (inputStream, cancellationToken) => inputStream.CopyToAsync(destination, BlockSize, cancellationToken); } - public Task ReadAsync(System.IO.Stream inputStream, CancellationToken cancellationToken) + public Task ReadAsync(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 404029f..99bc081 100644 --- a/FFMpegCore/FFMpeg/Pipes/StreamPipeSource.cs +++ b/FFMpegCore/FFMpeg/Pipes/StreamPipeSource.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System.IO; +using System.Threading; using System.Threading.Tasks; namespace FFMpegCore.Pipes @@ -8,17 +9,17 @@ namespace FFMpegCore.Pipes /// public class StreamPipeSource : IPipeSource { - public System.IO.Stream Source { get; } + public Stream Source { get; } public int BlockSize { get; } = 4096; public string StreamFormat { get; } = string.Empty; - public StreamPipeSource(System.IO.Stream source) + public StreamPipeSource(Stream source) { Source = source; } public string GetStreamArguments() => StreamFormat; - public Task WriteAsync(System.IO.Stream outputStream, CancellationToken cancellationToken) => Source.CopyToAsync(outputStream, BlockSize, cancellationToken); + public Task WriteAsync(Stream outputStream, CancellationToken cancellationToken) => Source.CopyToAsync(outputStream, BlockSize, cancellationToken); } } diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index bf9e682..c8fa692 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -9,9 +9,10 @@ 3.0.0.0 3.0.0.0 3.0.0.0 - - Added support for mirroring video filter (thanks gorobvictor) + - Added support for PCM audio through RawAudioPipeSource (thanks to Namaneo) +- Removed -y in InputPipeArgument due to reported problems 8 - 4.2.0 + 4.3.0 MIT Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev ffmpeg ffprobe convert video audio mediafile resize analyze muxing