Merge pull request #219 from rosenbjerg/master

v.4.3.0

Former-commit-id: ef1f40b182
This commit is contained in:
Malte Rosenbjerg 2021-06-08 22:20:00 +02:00 committed by GitHub
commit b044577a36
18 changed files with 275 additions and 32 deletions

View file

@ -18,12 +18,12 @@ jobs:
timeout-minutes: 6 timeout-minutes: 6
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v1 uses: actions/checkout@v2
- name: Prepare .NET - name: Prepare .NET
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v1
with: with:
dotnet-version: '5.0.x' dotnet-version: '5.0.x'
- name: Prepare FFMpeg - name: Prepare FFMpeg
uses: FedericoCarboni/setup-ffmpeg@v1-beta uses: FedericoCarboni/setup-ffmpeg@v1
- name: Test with dotnet - name: Test with dotnet
run: dotnet test --logger GitHubActions run: dotnet test --logger GitHubActions

View file

@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v1 uses: actions/checkout@v2
- name: Prepare .NET - name: Prepare .NET
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v1
with: with:

View file

@ -1,11 +1,13 @@
using System; using FFMpegCore.Enums;
using FFMpegCore.Enums; using FFMpegCore.Exceptions;
using FFMpegCore.Pipes;
using FFMpegCore.Test.Resources; using FFMpegCore.Test.Resources;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using FFMpegCore.Pipes;
namespace FFMpegCore.Test namespace FFMpegCore.Test
{ {
@ -70,5 +72,155 @@ public void Image_AddAudio()
Assert.IsTrue(analysis.Duration.TotalSeconds > 0); Assert.IsTrue(analysis.Duration.TotalSeconds > 0);
Assert.IsTrue(File.Exists(outputFile)); 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<IAudioSample>
{
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<IAudioSample>
{
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<IAudioSample>
{
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<IAudioSample>
{
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<IAudioSample>())
{
Channels = 0,
};
var ex = Assert.ThrowsException<FFMpegException>(() => 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<IAudioSample>())
{
Format = "s8le",
};
var ex = Assert.ThrowsException<FFMpegException>(() => 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<IAudioSample>())
{
SampleRate = 0,
};
var ex = Assert.ThrowsException<FFMpegException>(() => FFMpegArguments
.FromPipeInput(audioSamplesSource)
.OutputToFile(outputFile, false, opt => opt
.WithAudioCodec(AudioCodec.Aac))
.ProcessSynchronously());
}
} }
} }

View file

@ -1,8 +1,4 @@
using System; namespace FFMpegCore.Test.Resources
using System.IO;
using FFMpegCore.Enums;
namespace FFMpegCore.Test.Resources
{ {
public enum AudioType public enum AudioType
{ {

View file

@ -1,6 +1,7 @@
using System; using System;
using System.Drawing; using System.Drawing;
using System.Drawing.Imaging; using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -24,7 +25,7 @@ public BitmapVideoFrameWrapper(Bitmap bitmap)
Format = ConvertStreamFormat(bitmap.PixelFormat); 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); 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); 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: case PixelFormat.Format16bppGrayScale:
return "gray16le"; return "gray16le";
case PixelFormat.Format16bppRgb555:
return "bgr555le";
case PixelFormat.Format16bppRgb565: case PixelFormat.Format16bppRgb565:
return "bgr565le"; return "bgr565le";
case PixelFormat.Format24bppRgb: case PixelFormat.Format24bppRgb:

View file

@ -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);
}
}

View file

@ -17,7 +17,7 @@ public InputPipeArgument(IPipeSource writer) : base(PipeDirection.Out)
Writer = writer; 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) protected override async Task ProcessDataAsync(CancellationToken token)
{ {

View file

@ -1,7 +1,5 @@
using FFMpegCore.Enums; using FFMpegCore.Enums;
using System; using System;
using System.Collections.Generic;
using System.Text;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments
{ {

View file

@ -1,5 +1,4 @@
using System.Drawing; using System.Drawing;
using FFMpegCore.Enums;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments
{ {

View file

@ -0,0 +1,16 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace FFMpegCore.Pipes
{
/// <summary>
/// Interface for Audio sample
/// </summary>
public interface IAudioSample
{
void Serialize(Stream stream);
Task SerializeAsync(Stream stream, CancellationToken token);
}
}

View file

@ -1,11 +1,12 @@
using System.Threading; using System.IO;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace FFMpegCore.Pipes namespace FFMpegCore.Pipes
{ {
public interface IPipeSink public interface IPipeSink
{ {
Task ReadAsync(System.IO.Stream inputStream, CancellationToken cancellationToken); Task ReadAsync(Stream inputStream, CancellationToken cancellationToken);
string GetFormat(); string GetFormat();
} }
} }

View file

@ -1,4 +1,5 @@
using System.Threading; using System.IO;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace FFMpegCore.Pipes namespace FFMpegCore.Pipes
@ -9,6 +10,6 @@ namespace FFMpegCore.Pipes
public interface IPipeSource public interface IPipeSource
{ {
string GetStreamArguments(); string GetStreamArguments();
Task WriteAsync(System.IO.Stream outputStream, CancellationToken cancellationToken); Task WriteAsync(Stream outputStream, CancellationToken cancellationToken);
} }
} }

View file

@ -1,4 +1,5 @@
using System.Threading; using System.IO;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace FFMpegCore.Pipes namespace FFMpegCore.Pipes
@ -12,7 +13,7 @@ public interface IVideoFrame
int Height { get; } int Height { get; }
string Format { get; } string Format { get; }
void Serialize(System.IO.Stream pipe); void Serialize(Stream pipe);
Task SerializeAsync(System.IO.Stream pipe, CancellationToken token); Task SerializeAsync(Stream pipe, CancellationToken token);
} }
} }

View file

@ -0,0 +1,46 @@
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace FFMpegCore.Pipes
{
/// <summary>
/// Implementation of <see cref="IPipeSource"/> for a raw audio stream that is gathered from <see cref="IEnumerator{IAudioFrame}"/>.
/// It is the user's responbility to make sure the enumerated samples match the configuration provided to this pipe.
/// </summary>
public class RawAudioPipeSource : IPipeSource
{
private readonly IEnumerator<IAudioSample> _sampleEnumerator;
public string Format { get; set; } = "s16le";
public uint SampleRate { get; set; } = 8000;
public uint Channels { get; set; } = 1;
public RawAudioPipeSource(IEnumerator<IAudioSample> sampleEnumerator)
{
_sampleEnumerator = sampleEnumerator;
}
public RawAudioPipeSource(IEnumerable<IAudioSample> 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);
}
}
}
}

View file

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using FFMpegCore.Exceptions; 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}"; 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) if (_framesEnumerator.Current != null)
{ {

View file

@ -20,7 +20,7 @@ public StreamPipeSink(Stream destination)
Writer = (inputStream, cancellationToken) => inputStream.CopyToAsync(destination, BlockSize, cancellationToken); 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); => Writer(inputStream, cancellationToken);
public string GetFormat() => Format; public string GetFormat() => Format;

View file

@ -1,4 +1,5 @@
using System.Threading; using System.IO;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace FFMpegCore.Pipes namespace FFMpegCore.Pipes
@ -8,17 +9,17 @@ namespace FFMpegCore.Pipes
/// </summary> /// </summary>
public class StreamPipeSource : IPipeSource public class StreamPipeSource : IPipeSource
{ {
public System.IO.Stream Source { get; } public Stream Source { get; }
public int BlockSize { get; } = 4096; public int BlockSize { get; } = 4096;
public string StreamFormat { get; } = string.Empty; public string StreamFormat { get; } = string.Empty;
public StreamPipeSource(System.IO.Stream source) public StreamPipeSource(Stream source)
{ {
Source = source; Source = source;
} }
public string GetStreamArguments() => StreamFormat; 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);
} }
} }

View file

@ -9,9 +9,10 @@
<Version>3.0.0.0</Version> <Version>3.0.0.0</Version>
<AssemblyVersion>3.0.0.0</AssemblyVersion> <AssemblyVersion>3.0.0.0</AssemblyVersion>
<FileVersion>3.0.0.0</FileVersion> <FileVersion>3.0.0.0</FileVersion>
<PackageReleaseNotes>- Added support for mirroring video filter (thanks gorobvictor)</PackageReleaseNotes> <PackageReleaseNotes>- Added support for PCM audio through RawAudioPipeSource (thanks to Namaneo)
- Removed -y in InputPipeArgument due to reported problems</PackageReleaseNotes>
<LangVersion>8</LangVersion> <LangVersion>8</LangVersion>
<PackageVersion>4.2.0</PackageVersion> <PackageVersion>4.3.0</PackageVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<Authors>Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev</Authors> <Authors>Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev</Authors>
<PackageTags>ffmpeg ffprobe convert video audio mediafile resize analyze muxing</PackageTags> <PackageTags>ffmpeg ffprobe convert video audio mediafile resize analyze muxing</PackageTags>