Merge pull request #582 from rosenbjerg/main
Some checks failed
NuGet release / release (push) Has been cancelled

V.5.3.0
This commit is contained in:
Malte Rosenbjerg 2025-10-17 20:49:31 +02:00 committed by GitHub
commit 4651252427
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
173 changed files with 8688 additions and 7569 deletions

View file

@ -18,7 +18,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [windows-latest, ubuntu-latest, macos-13] os: [windows-latest, ubuntu-latest, macos-latest]
timeout-minutes: 7 timeout-minutes: 7
steps: steps:
@ -30,14 +30,15 @@ jobs:
with: with:
dotnet-version: '8.0.x' dotnet-version: '8.0.x'
- name: Lint with dotnet - if: matrix.os == 'ubuntu-latest'
name: Lint with dotnet
run: dotnet format FFMpegCore.sln --severity warn --verify-no-changes run: dotnet format FFMpegCore.sln --severity warn --verify-no-changes
- name: Prepare FFMpeg - name: Setup FFmpeg
uses: FedericoCarboni/setup-ffmpeg@v3 uses: AnimMouse/setup-ffmpeg@ae28d57dabbb148eff63170b6bf7f2b60062cbae # 1.1.0
with: with:
ffmpeg-version: 6.0.1 version: ${{ matrix.os != 'macos-latest' && '7.1' || '711' }}
github-token: ${{ secrets.GITHUB_TOKEN }} token: ${{ github.token }}
- name: Test with dotnet - name: Test with dotnet
run: dotnet test FFMpegCore.sln --collect "XPlat Code Coverage" --logger GitHubActions run: dotnet test FFMpegCore.sln --collect "XPlat Code Coverage" --logger GitHubActions

View file

@ -1,26 +1,26 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<NeutralLanguage>en</NeutralLanguage> <NeutralLanguage>en</NeutralLanguage>
<AssemblyVersion>5.0.0.0</AssemblyVersion> <AssemblyVersion>5.0.0.0</AssemblyVersion>
<LangVersion>default</LangVersion> <LangVersion>default</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<RepositoryType>GitHub</RepositoryType> <RepositoryType>GitHub</RepositoryType>
<RepositoryUrl>https://github.com/rosenbjerg/FFMpegCore</RepositoryUrl> <RepositoryUrl>https://github.com/rosenbjerg/FFMpegCore</RepositoryUrl>
<PackageProjectUrl>https://github.com/rosenbjerg/FFMpegCore</PackageProjectUrl> <PackageProjectUrl>https://github.com/rosenbjerg/FFMpegCore</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<NeutralLanguage>en</NeutralLanguage> <NeutralLanguage>en</NeutralLanguage>
<PublishRepositoryUrl>true</PublishRepositoryUrl> <PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols> <IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat> <SymbolPackageFormat>snupkg</SymbolPackageFormat>
<EmbedUntrackedSources>true</EmbedUntrackedSources> <EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'"> <PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild> <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View file

@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\FFMpegCore.Extensions.SkiaSharp\FFMpegCore.Extensions.SkiaSharp.csproj" /> <ProjectReference Include="..\FFMpegCore.Extensions.SkiaSharp\FFMpegCore.Extensions.SkiaSharp.csproj"/>
<ProjectReference Include="..\FFMpegCore.Extensions.System.Drawing.Common\FFMpegCore.Extensions.System.Drawing.Common.csproj"/> <ProjectReference Include="..\FFMpegCore.Extensions.System.Drawing.Common\FFMpegCore.Extensions.System.Drawing.Common.csproj"/>
<ProjectReference Include="..\FFMpegCore\FFMpegCore.csproj"/> <ProjectReference Include="..\FFMpegCore\FFMpegCore.csproj"/>
</ItemGroup> </ItemGroup>

View file

@ -61,7 +61,7 @@ var outputStream = new MemoryStream();
} }
{ {
FFMpeg.JoinImageSequence(@"..\joined_video.mp4", frameRate: 1, @"..\1.png", @"..\2.png", @"..\3.png"); FFMpeg.JoinImageSequence(@"..\joined_video.mp4", 1, @"..\1.png", @"..\2.png", @"..\3.png");
} }
{ {
@ -90,7 +90,11 @@ var inputImagePath = "/path/to/input/image";
skiaSharpImage.AddAudio(inputAudioPath, outputPath); skiaSharpImage.AddAudio(inputAudioPath, outputPath);
} }
IVideoFrame GetNextFrame() => throw new NotImplementedException(); IVideoFrame GetNextFrame()
{
throw new NotImplementedException();
}
{ {
IEnumerable<IVideoFrame> CreateFrames(int count) IEnumerable<IVideoFrame> CreateFrames(int count)
{ {
@ -100,10 +104,11 @@ IVideoFrame GetNextFrame() => throw new NotImplementedException();
} }
} }
var videoFramesSource = new RawVideoPipeSource(CreateFrames(64)) //pass IEnumerable<IVideoFrame> or IEnumerator<IVideoFrame> to constructor of RawVideoPipeSource var videoFramesSource =
{ new RawVideoPipeSource(CreateFrames(64)) //pass IEnumerable<IVideoFrame> or IEnumerator<IVideoFrame> to constructor of RawVideoPipeSource
FrameRate = 30 //set source frame rate {
}; FrameRate = 30 //set source frame rate
};
await FFMpegArguments await FFMpegArguments
.FromPipeInput(videoFramesSource) .FromPipeInput(videoFramesSource)
.OutputToFile(outputPath, false, options => options .OutputToFile(outputPath, false, options => options

View file

@ -0,0 +1,9 @@
namespace FFMpegCore.Extensions.Downloader.Enums;
[Flags]
public enum FFMpegBinaries : ushort
{
FFMpeg = 1,
FFProbe = 2,
FFPlay = 4
}

View file

@ -0,0 +1,39 @@
using System.ComponentModel;
namespace FFMpegCore.Extensions.Downloader.Enums;
public enum FFMpegVersions : ushort
{
[Description("https://ffbinaries.com/api/v1/version/latest")]
LatestAvailable,
[Description("https://ffbinaries.com/api/v1/version/6.1")]
V6_1,
[Description("https://ffbinaries.com/api/v1/version/5.1")]
V5_1,
[Description("https://ffbinaries.com/api/v1/version/4.4.1")]
V4_4_1,
[Description("https://ffbinaries.com/api/v1/version/4.2.1")]
V4_2_1,
[Description("https://ffbinaries.com/api/v1/version/4.2")]
V4_2,
[Description("https://ffbinaries.com/api/v1/version/4.1")]
V4_1,
[Description("https://ffbinaries.com/api/v1/version/4.0")]
V4_0,
[Description("https://ffbinaries.com/api/v1/version/3.4")]
V3_4,
[Description("https://ffbinaries.com/api/v1/version/3.3")]
V3_3,
[Description("https://ffbinaries.com/api/v1/version/3.2")]
V3_2
}

View file

@ -0,0 +1,13 @@
namespace FFMpegCore.Extensions.Downloader.Enums;
public enum SupportedPlatforms : ushort
{
Windows64,
Windows32,
Linux64,
Linux32,
LinuxArmhf,
LinuxArmel,
LinuxArm64,
Osx64
}

View file

@ -0,0 +1,18 @@
namespace FFMpegCore.Extensions.Downloader.Exceptions;
/// <summary>
/// Custom exception for FFMpegDownloader
/// </summary>
public class FFMpegDownloaderException : Exception
{
public readonly string Detail = "";
public FFMpegDownloaderException(string message) : base(message)
{
}
public FFMpegDownloaderException(string message, string detail) : base(message)
{
Detail = detail;
}
}

View file

@ -0,0 +1,31 @@
using System.ComponentModel;
namespace FFMpegCore.Extensions.Downloader.Extensions;
public static class EnumExtensions
{
internal static string GetDescription(this Enum enumValue)
{
var field = enumValue.GetType().GetField(enumValue.ToString());
if (field == null)
{
return enumValue.ToString();
}
if (Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) is DescriptionAttribute attribute)
{
return attribute.Description;
}
return enumValue.ToString();
}
public static TEnum[] GetFlags<TEnum>(this TEnum input) where TEnum : Enum
{
return Enum.GetValues(input.GetType())
.Cast<Enum>()
.Where(input.HasFlag)
.Cast<TEnum>()
.ToArray();
}
}

View file

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPackable>true</IsPackable>
<Description>FFMpeg downloader extension for FFMpegCore</Description>
<PackageVersion>5.0.0</PackageVersion>
<PackageOutputPath>../nupkg</PackageOutputPath>
<PackageReleaseNotes>
- Updated dependencies
</PackageReleaseNotes>
<PackageTags>ffmpeg ffprobe convert video audio mediafile resize analyze download install</PackageTags>
<Authors>Kerry Cao, Malte Rosenbjerg</Authors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\FFMpegCore\FFMpegCore.csproj"/>
</ItemGroup>
</Project>

View file

@ -0,0 +1,83 @@
using System.IO.Compression;
using System.Text.Json;
using FFMpegCore.Extensions.Downloader.Enums;
using FFMpegCore.Extensions.Downloader.Exceptions;
using FFMpegCore.Extensions.Downloader.Extensions;
using FFMpegCore.Extensions.Downloader.Models;
namespace FFMpegCore.Extensions.Downloader;
public static class FFMpegDownloader
{
/// <summary>
/// Download the latest FFMpeg suite binaries for current platform
/// </summary>
/// <param name="version">used to explicitly state the version of binary you want to download</param>
/// <param name="binaries">used to explicitly state the binaries you want to download (ffmpeg, ffprobe, ffplay)</param>
/// <param name="options">used for specifying binary folder to download binaries into. If not provided, GlobalFFOptions are used</param>
/// <param name="platformOverride">used to explicitly state the os and architecture you want to download</param>
/// <returns>a list of the binaries that have been successfully downloaded</returns>
public static async Task<List<string>> DownloadBinaries(
FFMpegVersions version = FFMpegVersions.LatestAvailable,
FFMpegBinaries binaries = FFMpegBinaries.FFMpeg | FFMpegBinaries.FFProbe,
FFOptions? options = null,
SupportedPlatforms? platformOverride = null)
{
using var httpClient = new HttpClient();
var versionInfo = await httpClient.GetVersionInfo(version);
var binariesDictionary = versionInfo.BinaryInfo?.GetCompatibleDownloadInfo(platformOverride) ??
throw new FFMpegDownloaderException("Failed to get compatible download info");
var successList = new List<string>();
var relevantOptions = options ?? GlobalFFOptions.Current;
if (string.IsNullOrEmpty(relevantOptions.BinaryFolder))
{
throw new FFMpegDownloaderException("Binary folder not specified");
}
var binaryFlags = binaries.GetFlags();
foreach (var binaryFlag in binaryFlags)
{
if (binariesDictionary.TryGetValue(binaryFlag.ToString().ToLowerInvariant(), out var binaryUrl))
{
using var zipStream = await httpClient.GetStreamAsync(new Uri(binaryUrl));
var extracted = ExtractZipAndSave(zipStream, relevantOptions.BinaryFolder);
successList.AddRange(extracted);
}
}
return successList;
}
private static async Task<VersionInfo> GetVersionInfo(this HttpClient client, FFMpegVersions version)
{
var versionUri = version.GetDescription();
var response = await client.GetAsync(versionUri);
if (!response.IsSuccessStatusCode)
{
throw new FFMpegDownloaderException($"Failed to get version info from {versionUri}", "network error");
}
var jsonString = await response.Content.ReadAsStringAsync();
var versionInfo = JsonSerializer.Deserialize<VersionInfo>(jsonString);
return versionInfo ??
throw new FFMpegDownloaderException($"Failed to deserialize version info from {versionUri}", jsonString);
}
private static IEnumerable<string> ExtractZipAndSave(Stream zipStream, string binaryFolder)
{
using var archive = new ZipArchive(zipStream, ZipArchiveMode.Read);
foreach (var entry in archive.Entries)
{
if (entry.Name is "ffmpeg" or "ffmpeg.exe" or "ffprobe.exe" or "ffprobe" or "ffplay.exe" or "ffplay")
{
var filePath = Path.Combine(binaryFolder, entry.Name);
entry.ExtractToFile(filePath, true);
yield return filePath;
}
}
}
}

View file

@ -0,0 +1,74 @@
using System.Runtime.InteropServices;
using System.Text.Json.Serialization;
using FFMpegCore.Extensions.Downloader.Enums;
namespace FFMpegCore.Extensions.Downloader.Models;
internal class BinaryInfo
{
[JsonPropertyName("windows-64")] public Dictionary<string, string>? Windows64 { get; set; }
[JsonPropertyName("windows-32")] public Dictionary<string, string>? Windows32 { get; set; }
[JsonPropertyName("linux-32")] public Dictionary<string, string>? Linux32 { get; set; }
[JsonPropertyName("linux-64")] public Dictionary<string, string>? Linux64 { get; set; }
[JsonPropertyName("linux-armhf")] public Dictionary<string, string>? LinuxArmhf { get; set; }
[JsonPropertyName("linux-armel")] public Dictionary<string, string>? LinuxArmel { get; set; }
[JsonPropertyName("linux-arm64")] public Dictionary<string, string>? LinuxArm64 { get; set; }
[JsonPropertyName("osx-64")] public Dictionary<string, string>? Osx64 { get; set; }
/// <summary>
/// Automatically get the compatible download info for current os and architecture
/// </summary>
/// <param name="platformOverride"></param>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
/// <exception cref="PlatformNotSupportedException"></exception>
public Dictionary<string, string>? GetCompatibleDownloadInfo(SupportedPlatforms? platformOverride = null)
{
if (platformOverride is not null)
{
return platformOverride switch
{
SupportedPlatforms.Windows64 => Windows64,
SupportedPlatforms.Windows32 => Windows32,
SupportedPlatforms.Linux64 => Linux64,
SupportedPlatforms.Linux32 => Linux32,
SupportedPlatforms.LinuxArmhf => LinuxArmhf,
SupportedPlatforms.LinuxArmel => LinuxArmel,
SupportedPlatforms.LinuxArm64 => LinuxArm64,
SupportedPlatforms.Osx64 => Osx64,
_ => throw new ArgumentOutOfRangeException(nameof(platformOverride), platformOverride, null)
};
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return RuntimeInformation.OSArchitecture == Architecture.X64 ? Windows64 : Windows32;
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return RuntimeInformation.OSArchitecture switch
{
Architecture.X86 => Linux32,
Architecture.X64 => Linux64,
Architecture.Arm => LinuxArmhf,
Architecture.Arm64 => LinuxArm64,
_ => LinuxArmel
};
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && RuntimeInformation.OSArchitecture == Architecture.X64)
{
return Osx64;
}
throw new PlatformNotSupportedException("Unsupported OS or Architecture");
}
}

View file

@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace FFMpegCore.Extensions.Downloader.Models;
internal record VersionInfo
{
[JsonPropertyName("version")] public string? Version { get; set; }
[JsonPropertyName("permalink")] public string? Permalink { get; set; }
[JsonPropertyName("bin")] public BinaryInfo? BinaryInfo { get; set; }
}

View file

@ -1,27 +1,26 @@
using SkiaSharp; using SkiaSharp;
namespace FFMpegCore.Extensions.SkiaSharp namespace FFMpegCore.Extensions.SkiaSharp;
{
public static class BitmapExtensions
{
public static bool AddAudio(this SKBitmap poster, string audio, string output)
{
var destination = $"{Environment.TickCount}.png";
using (var fileStream = File.OpenWrite(destination))
{
poster.Encode(fileStream, SKEncodedImageFormat.Png, default); // PNG does not respect the quality parameter
}
try public static class BitmapExtensions
{
public static bool AddAudio(this SKBitmap poster, string audio, string output)
{
var destination = $"{Environment.TickCount}.png";
using (var fileStream = File.OpenWrite(destination))
{
poster.Encode(fileStream, SKEncodedImageFormat.Png, default); // PNG does not respect the quality parameter
}
try
{
return FFMpeg.PosterWithAudio(destination, audio, output);
}
finally
{
if (File.Exists(destination))
{ {
return FFMpeg.PosterWithAudio(destination, audio, output); File.Delete(destination);
}
finally
{
if (File.Exists(destination))
{
File.Delete(destination);
}
} }
} }
} }

View file

@ -1,59 +1,58 @@
using FFMpegCore.Pipes; using FFMpegCore.Pipes;
using SkiaSharp; using SkiaSharp;
namespace FFMpegCore.Extensions.SkiaSharp namespace FFMpegCore.Extensions.SkiaSharp;
public class BitmapVideoFrameWrapper : IVideoFrame, IDisposable
{ {
public class BitmapVideoFrameWrapper : IVideoFrame, IDisposable public BitmapVideoFrameWrapper(SKBitmap bitmap)
{ {
public int Width => Source.Width; Source = bitmap ?? throw new ArgumentNullException(nameof(bitmap));
Format = ConvertStreamFormat(bitmap.ColorType);
}
public int Height => Source.Height; public SKBitmap Source { get; }
public string Format { get; private set; } public void Dispose()
{
Source.Dispose();
}
public SKBitmap Source { get; private set; } public int Width => Source.Width;
public BitmapVideoFrameWrapper(SKBitmap bitmap) public int Height => Source.Height;
public string Format { get; }
public void Serialize(Stream stream)
{
var data = Source.Bytes;
stream.Write(data, 0, data.Length);
}
public async Task SerializeAsync(Stream stream, CancellationToken token)
{
var data = Source.Bytes;
await stream.WriteAsync(data, 0, data.Length, token).ConfigureAwait(false);
}
private static string ConvertStreamFormat(SKColorType fmt)
{
// TODO: Add support for additional formats
switch (fmt)
{ {
Source = bitmap ?? throw new ArgumentNullException(nameof(bitmap)); case SKColorType.Gray8:
Format = ConvertStreamFormat(bitmap.ColorType); return "gray8";
} case SKColorType.Bgra8888:
return "bgra";
public void Serialize(Stream stream) case SKColorType.Rgb888x:
{ return "rgb";
var data = Source.Bytes; case SKColorType.Rgba8888:
stream.Write(data, 0, data.Length); return "rgba";
} case SKColorType.Rgb565:
return "rgb565";
public async Task SerializeAsync(Stream stream, CancellationToken token) default:
{ throw new NotSupportedException($"Not supported pixel format {fmt}");
var data = Source.Bytes;
await stream.WriteAsync(data, 0, data.Length, token).ConfigureAwait(false);
}
public void Dispose()
{
Source.Dispose();
}
private static string ConvertStreamFormat(SKColorType fmt)
{
// TODO: Add support for additional formats
switch (fmt)
{
case SKColorType.Gray8:
return "gray8";
case SKColorType.Bgra8888:
return "bgra";
case SKColorType.Rgb888x:
return "rgb";
case SKColorType.Rgba8888:
return "rgba";
case SKColorType.Rgb565:
return "rgb565";
default:
throw new NotSupportedException($"Not supported pixel format {fmt}");
}
} }
} }
} }

View file

@ -1,23 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
<Description>Image extension for FFMpegCore using SkiaSharp</Description> <Description>Image extension for FFMpegCore using SkiaSharp</Description>
<PackageVersion>5.0.2</PackageVersion> <PackageVersion>5.0.3</PackageVersion>
<PackageOutputPath>../nupkg</PackageOutputPath> <PackageOutputPath>../nupkg</PackageOutputPath>
<PackageReleaseNotes>Bump dependencies</PackageReleaseNotes> <PackageReleaseNotes>
<PackageTags>ffmpeg ffprobe convert video audio mediafile resize analyze muxing skiasharp</PackageTags> - Updated dependencies
<Authors>Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev, Dimitri Vranken</Authors> </PackageReleaseNotes>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly> <PackageTags>ffmpeg ffprobe convert video audio image mediafile resize analyze muxing skia skiasharp</PackageTags>
</PropertyGroup> <Authors>Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev, Dimitri Vranken</Authors>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="SkiaSharp" Version="3.116.1" /> <PackageReference Include="SkiaSharp" Version="3.119.1"/>
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.116.1" /> <PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.119.1"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\FFMpegCore\FFMpegCore.csproj" /> <ProjectReference Include="..\FFMpegCore\FFMpegCore.csproj"/>
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -2,56 +2,57 @@
using FFMpegCore.Pipes; using FFMpegCore.Pipes;
using SkiaSharp; using SkiaSharp;
namespace FFMpegCore.Extensions.SkiaSharp namespace FFMpegCore.Extensions.SkiaSharp;
public static class FFMpegImage
{ {
public static class FFMpegImage /// <summary>
/// Saves a 'png' thumbnail to an in-memory bitmap
/// </summary>
/// <param name="input">Source video file.</param>
/// <param name="captureTime">Seek position where the thumbnail should be taken.</param>
/// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param>
/// <param name="streamIndex">Selected video stream index.</param>
/// <param name="inputFileIndex">Input file index</param>
/// <returns>Bitmap with the requested snapshot.</returns>
public static SKBitmap Snapshot(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0)
{ {
/// <summary> var source = FFProbe.Analyse(input);
/// Saves a 'png' thumbnail to an in-memory bitmap var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
/// </summary> using var ms = new MemoryStream();
/// <param name="input">Source video file.</param>
/// <param name="captureTime">Seek position where the thumbnail should be taken.</param>
/// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param>
/// <param name="streamIndex">Selected video stream index.</param>
/// <param name="inputFileIndex">Input file index</param>
/// <returns>Bitmap with the requested snapshot.</returns>
public static SKBitmap Snapshot(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0)
{
var source = FFProbe.Analyse(input);
var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
using var ms = new MemoryStream();
arguments arguments
.OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options
.ForceFormat("rawvideo"))) .ForceFormat("rawvideo")))
.ProcessSynchronously(); .ProcessSynchronously();
ms.Position = 0; ms.Position = 0;
using var bitmap = SKBitmap.Decode(ms); using var bitmap = SKBitmap.Decode(ms);
return bitmap.Copy(); return bitmap.Copy();
} }
/// <summary>
/// Saves a 'png' thumbnail to an in-memory bitmap
/// </summary>
/// <param name="input">Source video file.</param>
/// <param name="captureTime">Seek position where the thumbnail should be taken.</param>
/// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param>
/// <param name="streamIndex">Selected video stream index.</param>
/// <param name="inputFileIndex">Input file index</param>
/// <returns>Bitmap with the requested snapshot.</returns>
public static async Task<SKBitmap> SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0)
{
var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false);
var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
using var ms = new MemoryStream();
await arguments /// <summary>
.OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options /// Saves a 'png' thumbnail to an in-memory bitmap
.ForceFormat("rawvideo"))) /// </summary>
.ProcessAsynchronously(); /// <param name="input">Source video file.</param>
/// <param name="captureTime">Seek position where the thumbnail should be taken.</param>
/// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param>
/// <param name="streamIndex">Selected video stream index.</param>
/// <param name="inputFileIndex">Input file index</param>
/// <returns>Bitmap with the requested snapshot.</returns>
public static async Task<SKBitmap> SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null,
int inputFileIndex = 0)
{
var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false);
var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
using var ms = new MemoryStream();
ms.Position = 0; await arguments
return SKBitmap.Decode(ms); .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options
} .ForceFormat("rawvideo")))
.ProcessAsynchronously();
ms.Position = 0;
return SKBitmap.Decode(ms);
} }
} }

View file

@ -1,23 +1,22 @@
using System.Drawing; using System.Drawing;
namespace FFMpegCore.Extensions.System.Drawing.Common namespace FFMpegCore.Extensions.System.Drawing.Common;
public static class BitmapExtensions
{ {
public static class BitmapExtensions public static bool AddAudio(this Image poster, string audio, string output)
{ {
public static bool AddAudio(this Image poster, string audio, string output) var destination = $"{Environment.TickCount}.png";
poster.Save(destination);
try
{ {
var destination = $"{Environment.TickCount}.png"; return FFMpeg.PosterWithAudio(destination, audio, output);
poster.Save(destination); }
try finally
{
if (File.Exists(destination))
{ {
return FFMpeg.PosterWithAudio(destination, audio, output); File.Delete(destination);
}
finally
{
if (File.Exists(destination))
{
File.Delete(destination);
}
} }
} }
} }

View file

@ -3,85 +3,84 @@ using System.Drawing.Imaging;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using FFMpegCore.Pipes; using FFMpegCore.Pipes;
namespace FFMpegCore.Extensions.System.Drawing.Common namespace FFMpegCore.Extensions.System.Drawing.Common;
public class BitmapVideoFrameWrapper : IVideoFrame, IDisposable
{ {
public class BitmapVideoFrameWrapper : IVideoFrame, IDisposable public BitmapVideoFrameWrapper(Bitmap bitmap)
{ {
public int Width => Source.Width; Source = bitmap ?? throw new ArgumentNullException(nameof(bitmap));
Format = ConvertStreamFormat(bitmap.PixelFormat);
}
public int Height => Source.Height; public Bitmap Source { get; }
public string Format { get; private set; } public void Dispose()
{
Source.Dispose();
}
public Bitmap Source { get; private set; } public int Width => Source.Width;
public BitmapVideoFrameWrapper(Bitmap bitmap) public int Height => Source.Height;
public string Format { get; }
public void Serialize(Stream stream)
{
var data = Source.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.ReadOnly, Source.PixelFormat);
try
{ {
Source = bitmap ?? throw new ArgumentNullException(nameof(bitmap)); var buffer = new byte[data.Stride * data.Height];
Format = ConvertStreamFormat(bitmap.PixelFormat); Marshal.Copy(data.Scan0, buffer, 0, buffer.Length);
stream.Write(buffer, 0, buffer.Length);
} }
finally
public void Serialize(Stream stream)
{ {
var data = Source.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.ReadOnly, Source.PixelFormat); Source.UnlockBits(data);
try
{
var buffer = new byte[data.Stride * data.Height];
Marshal.Copy(data.Scan0, buffer, 0, buffer.Length);
stream.Write(buffer, 0, buffer.Length);
}
finally
{
Source.UnlockBits(data);
}
} }
}
public async Task SerializeAsync(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);
try
{ {
var data = Source.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.ReadOnly, Source.PixelFormat); var buffer = new byte[data.Stride * data.Height];
Marshal.Copy(data.Scan0, buffer, 0, buffer.Length);
try await stream.WriteAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false);
{
var buffer = new byte[data.Stride * data.Height];
Marshal.Copy(data.Scan0, buffer, 0, buffer.Length);
await stream.WriteAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false);
}
finally
{
Source.UnlockBits(data);
}
} }
finally
public void Dispose()
{ {
Source.Dispose(); Source.UnlockBits(data);
} }
}
private static string ConvertStreamFormat(PixelFormat fmt) private static string ConvertStreamFormat(PixelFormat fmt)
{
switch (fmt)
{ {
switch (fmt) case PixelFormat.Format16bppGrayScale:
{ return "gray16le";
case PixelFormat.Format16bppGrayScale: case PixelFormat.Format16bppRgb555:
return "gray16le"; return "bgr555le";
case PixelFormat.Format16bppRgb555: case PixelFormat.Format16bppRgb565:
return "bgr555le"; return "bgr565le";
case PixelFormat.Format16bppRgb565: case PixelFormat.Format24bppRgb:
return "bgr565le"; return "bgr24";
case PixelFormat.Format24bppRgb: case PixelFormat.Format32bppArgb:
return "bgr24"; return "bgra";
case PixelFormat.Format32bppArgb: case PixelFormat.Format32bppPArgb:
return "bgra"; //This is not really same as argb32
case PixelFormat.Format32bppPArgb: return "argb";
//This is not really same as argb32 case PixelFormat.Format32bppRgb:
return "argb"; return "rgba";
case PixelFormat.Format32bppRgb: case PixelFormat.Format48bppRgb:
return "rgba"; return "rgb48le";
case PixelFormat.Format48bppRgb: default:
return "rgb48le"; throw new NotSupportedException($"Not supported pixel format {fmt}");
default:
throw new NotSupportedException($"Not supported pixel format {fmt}");
}
} }
} }
} }

View file

@ -3,16 +3,18 @@
<PropertyGroup> <PropertyGroup>
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
<Description>Image extension for FFMpegCore using System.Common.Drawing</Description> <Description>Image extension for FFMpegCore using System.Common.Drawing</Description>
<PackageVersion>5.0.2</PackageVersion> <PackageVersion>5.0.3</PackageVersion>
<PackageOutputPath>../nupkg</PackageOutputPath> <PackageOutputPath>../nupkg</PackageOutputPath>
<PackageReleaseNotes>Bump dependencies</PackageReleaseNotes> <PackageReleaseNotes>
<PackageTags>ffmpeg ffprobe convert video audio mediafile resize analyze muxing</PackageTags> - Updated dependencies
</PackageReleaseNotes>
<PackageTags>ffmpeg ffprobe convert video audio image mediafile resize analyze muxing</PackageTags>
<Authors>Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev</Authors> <Authors>Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev</Authors>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly> <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="System.Drawing.Common" Version="9.0.2" /> <PackageReference Include="System.Drawing.Common" Version="9.0.10"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -1,57 +1,57 @@
using System.Drawing; using System.Drawing;
using FFMpegCore.Pipes; using FFMpegCore.Pipes;
namespace FFMpegCore.Extensions.System.Drawing.Common namespace FFMpegCore.Extensions.System.Drawing.Common;
public static class FFMpegImage
{ {
public static class FFMpegImage /// <summary>
/// Saves a 'png' thumbnail to an in-memory bitmap
/// </summary>
/// <param name="input">Source video file.</param>
/// <param name="captureTime">Seek position where the thumbnail should be taken.</param>
/// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param>
/// <param name="streamIndex">Selected video stream index.</param>
/// <param name="inputFileIndex">Input file index</param>
/// <returns>Bitmap with the requested snapshot.</returns>
public static Bitmap Snapshot(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0)
{ {
/// <summary> var source = FFProbe.Analyse(input);
/// Saves a 'png' thumbnail to an in-memory bitmap var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
/// </summary> using var ms = new MemoryStream();
/// <param name="input">Source video file.</param>
/// <param name="captureTime">Seek position where the thumbnail should be taken.</param>
/// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param>
/// <param name="streamIndex">Selected video stream index.</param>
/// <param name="inputFileIndex">Input file index</param>
/// <returns>Bitmap with the requested snapshot.</returns>
public static Bitmap Snapshot(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0)
{
var source = FFProbe.Analyse(input);
var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
using var ms = new MemoryStream();
arguments arguments
.OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options
.ForceFormat("rawvideo"))) .ForceFormat("rawvideo")))
.ProcessSynchronously(); .ProcessSynchronously();
ms.Position = 0; ms.Position = 0;
using var bitmap = new Bitmap(ms); using var bitmap = new Bitmap(ms);
return bitmap.Clone(new Rectangle(0, 0, bitmap.Width, bitmap.Height), bitmap.PixelFormat); return bitmap.Clone(new Rectangle(0, 0, bitmap.Width, bitmap.Height), bitmap.PixelFormat);
} }
/// <summary> /// <summary>
/// Saves a 'png' thumbnail to an in-memory bitmap /// Saves a 'png' thumbnail to an in-memory bitmap
/// </summary> /// </summary>
/// <param name="input">Source video file.</param> /// <param name="input">Source video file.</param>
/// <param name="captureTime">Seek position where the thumbnail should be taken.</param> /// <param name="captureTime">Seek position where the thumbnail should be taken.</param>
/// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param> /// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param>
/// <param name="streamIndex">Selected video stream index.</param> /// <param name="streamIndex">Selected video stream index.</param>
/// <param name="inputFileIndex">Input file index</param> /// <param name="inputFileIndex">Input file index</param>
/// <returns>Bitmap with the requested snapshot.</returns> /// <returns>Bitmap with the requested snapshot.</returns>
public static async Task<Bitmap> SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) public static async Task<Bitmap> SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null,
{ int inputFileIndex = 0)
var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false); {
var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false);
using var ms = new MemoryStream(); var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
using var ms = new MemoryStream();
await arguments await arguments
.OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options
.ForceFormat("rawvideo"))) .ForceFormat("rawvideo")))
.ProcessAsynchronously(); .ProcessAsynchronously();
ms.Position = 0; ms.Position = 0;
return new Bitmap(ms); return new Bitmap(ms);
}
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]

View file

@ -3,326 +3,297 @@ using FFMpegCore.Exceptions;
using FFMpegCore.Extend; using FFMpegCore.Extend;
using FFMpegCore.Pipes; using FFMpegCore.Pipes;
using FFMpegCore.Test.Resources; using FFMpegCore.Test.Resources;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace FFMpegCore.Test namespace FFMpegCore.Test;
[TestClass]
public class AudioTest
{ {
[TestClass] [TestMethod]
public class AudioTest public void Audio_Remove()
{ {
[TestMethod] using var outputFile = new TemporaryFile("out.mp4");
public void Audio_Remove()
{
using var outputFile = new TemporaryFile("out.mp4");
FFMpeg.Mute(TestResources.Mp4Video, outputFile); FFMpeg.Mute(TestResources.Mp4Video, outputFile);
var analysis = FFProbe.Analyse(outputFile); var analysis = FFProbe.Analyse(outputFile);
Assert.IsTrue(analysis.VideoStreams.Any()); Assert.IsNotEmpty(analysis.VideoStreams);
Assert.IsTrue(!analysis.AudioStreams.Any()); Assert.IsEmpty(analysis.AudioStreams);
} }
[TestMethod] [TestMethod]
public void Audio_Save() public void Audio_Save()
{ {
using var outputFile = new TemporaryFile("out.mp3"); using var outputFile = new TemporaryFile("out.mp3");
FFMpeg.ExtractAudio(TestResources.Mp4Video, outputFile); FFMpeg.ExtractAudio(TestResources.Mp4Video, outputFile);
var analysis = FFProbe.Analyse(outputFile); var analysis = FFProbe.Analyse(outputFile);
Assert.IsTrue(!analysis.VideoStreams.Any()); Assert.IsNotEmpty(analysis.AudioStreams);
Assert.IsTrue(analysis.AudioStreams.Any()); Assert.IsEmpty(analysis.VideoStreams);
} }
[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] [TestMethod]
public void Audio_Add() public async Task Audio_FromRaw()
{ {
using var outputFile = new TemporaryFile("out.mp4"); 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();
}
var success = FFMpeg.ReplaceAudio(TestResources.Mp4WithoutAudio, TestResources.Mp3Audio, outputFile); [TestMethod]
var videoAnalysis = FFProbe.Analyse(TestResources.Mp4WithoutAudio); public void Audio_Add()
var audioAnalysis = FFProbe.Analyse(TestResources.Mp3Audio); {
var outputAnalysis = FFProbe.Analyse(outputFile); using var outputFile = new TemporaryFile("out.mp4");
Assert.IsTrue(success); var success = FFMpeg.ReplaceAudio(TestResources.Mp4WithoutAudio, TestResources.Mp3Audio, outputFile);
Assert.AreEqual(Math.Max(videoAnalysis.Duration.TotalSeconds, audioAnalysis.Duration.TotalSeconds), outputAnalysis.Duration.TotalSeconds, 0.15); var videoAnalysis = FFProbe.Analyse(TestResources.Mp4WithoutAudio);
Assert.IsTrue(File.Exists(outputFile)); var audioAnalysis = FFProbe.Analyse(TestResources.Mp3Audio);
} var outputAnalysis = FFProbe.Analyse(outputFile);
[TestMethod] Assert.IsTrue(success);
public void Image_AddAudio() Assert.AreEqual(Math.Max(videoAnalysis.Duration.TotalSeconds, audioAnalysis.Duration.TotalSeconds), outputAnalysis.Duration.TotalSeconds, 0.15);
{ Assert.IsTrue(File.Exists(outputFile));
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));
}
[TestMethod, Timeout(10000)] [TestMethod]
public void Audio_ToAAC_Args_Pipe() public void Image_AddAudio()
{ {
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); using var outputFile = new TemporaryFile("out.mp4");
FFMpeg.PosterWithAudio(TestResources.PngImage, TestResources.Mp3Audio, outputFile);
var analysis = FFProbe.Analyse(TestResources.Mp3Audio);
Assert.IsGreaterThan(0, analysis.Duration.TotalSeconds);
Assert.IsTrue(File.Exists(outputFile));
}
var samples = new List<IAudioSample> [TestMethod]
{ [Timeout(10000, CooperativeCancellation = true)]
new PcmAudioSampleWrapper(new byte[] { 0, 0 }), public void Audio_ToAAC_Args_Pipe()
new PcmAudioSampleWrapper(new byte[] { 0, 0 }), {
}; using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
var audioSamplesSource = new RawAudioPipeSource(samples) var samples = new List<IAudioSample> { new PcmAudioSampleWrapper([0, 0]), new PcmAudioSampleWrapper([0, 0]) };
{
Channels = 2,
Format = "s8",
SampleRate = 8000,
};
var success = FFMpegArguments var audioSamplesSource = new RawAudioPipeSource(samples) { Channels = 2, Format = "s8", SampleRate = 8000 };
.FromPipeInput(audioSamplesSource)
.OutputToFile(outputFile, false, opt => opt
.WithAudioCodec(AudioCodec.Aac))
.ProcessSynchronously();
Assert.IsTrue(success);
}
[TestMethod, Timeout(10000)] var success = FFMpegArguments
public void Audio_ToLibVorbis_Args_Pipe() .FromPipeInput(audioSamplesSource)
{ .OutputToFile(outputFile, false, opt => opt
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); .WithAudioCodec(AudioCodec.Aac))
.ProcessSynchronously();
Assert.IsTrue(success);
}
var samples = new List<IAudioSample> [TestMethod]
{ [Timeout(10000, CooperativeCancellation = true)]
new PcmAudioSampleWrapper(new byte[] { 0, 0 }), public void Audio_ToLibVorbis_Args_Pipe()
new PcmAudioSampleWrapper(new byte[] { 0, 0 }), {
}; using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
var audioSamplesSource = new RawAudioPipeSource(samples) var samples = new List<IAudioSample> { new PcmAudioSampleWrapper([0, 0]), new PcmAudioSampleWrapper([0, 0]) };
{
Channels = 2,
Format = "s8",
SampleRate = 8000,
};
var success = FFMpegArguments var audioSamplesSource = new RawAudioPipeSource(samples) { Channels = 2, Format = "s8", SampleRate = 8000 };
.FromPipeInput(audioSamplesSource)
.OutputToFile(outputFile, false, opt => opt
.WithAudioCodec(AudioCodec.LibVorbis))
.ProcessSynchronously();
Assert.IsTrue(success);
}
[TestMethod, Timeout(10000)] var success = FFMpegArguments
public async Task Audio_ToAAC_Args_Pipe_Async() .FromPipeInput(audioSamplesSource)
{ .OutputToFile(outputFile, false, opt => opt
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); .WithAudioCodec(AudioCodec.LibVorbis))
.ProcessSynchronously();
Assert.IsTrue(success);
}
var samples = new List<IAudioSample> [TestMethod]
{ [Timeout(10000, CooperativeCancellation = true)]
new PcmAudioSampleWrapper(new byte[] { 0, 0 }), public async Task Audio_ToAAC_Args_Pipe_Async()
new PcmAudioSampleWrapper(new byte[] { 0, 0 }), {
}; using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
var audioSamplesSource = new RawAudioPipeSource(samples) var samples = new List<IAudioSample> { new PcmAudioSampleWrapper([0, 0]), new PcmAudioSampleWrapper([0, 0]) };
{
Channels = 2,
Format = "s8",
SampleRate = 8000,
};
var success = await FFMpegArguments var audioSamplesSource = new RawAudioPipeSource(samples) { Channels = 2, Format = "s8", SampleRate = 8000 };
.FromPipeInput(audioSamplesSource)
.OutputToFile(outputFile, false, opt => opt
.WithAudioCodec(AudioCodec.Aac))
.ProcessAsynchronously();
Assert.IsTrue(success);
}
[TestMethod, Timeout(10000)] var success = await FFMpegArguments
public void Audio_ToAAC_Args_Pipe_ValidDefaultConfiguration() .FromPipeInput(audioSamplesSource)
{ .OutputToFile(outputFile, false, opt => opt
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); .WithAudioCodec(AudioCodec.Aac))
.ProcessAsynchronously();
Assert.IsTrue(success);
}
var samples = new List<IAudioSample> [TestMethod]
{ [Timeout(10000, CooperativeCancellation = true)]
new PcmAudioSampleWrapper(new byte[] { 0, 0 }), public void Audio_ToAAC_Args_Pipe_ValidDefaultConfiguration()
new PcmAudioSampleWrapper(new byte[] { 0, 0 }), {
}; using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
var audioSamplesSource = new RawAudioPipeSource(samples); var samples = new List<IAudioSample> { new PcmAudioSampleWrapper([0, 0]), new PcmAudioSampleWrapper([0, 0]) };
var success = FFMpegArguments var audioSamplesSource = new RawAudioPipeSource(samples);
.FromPipeInput(audioSamplesSource)
.OutputToFile(outputFile, false, opt => opt
.WithAudioCodec(AudioCodec.Aac))
.ProcessSynchronously();
Assert.IsTrue(success);
}
[TestMethod, Timeout(10000)] var success = FFMpegArguments
public void Audio_ToAAC_Args_Pipe_InvalidChannels() .FromPipeInput(audioSamplesSource)
{ .OutputToFile(outputFile, false, opt => opt
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); .WithAudioCodec(AudioCodec.Aac))
.ProcessSynchronously();
Assert.IsTrue(success);
}
var audioSamplesSource = new RawAudioPipeSource(new List<IAudioSample>()) [TestMethod]
{ [Timeout(10000, CooperativeCancellation = true)]
Channels = 0, public void Audio_ToAAC_Args_Pipe_InvalidChannels()
}; {
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
var ex = Assert.ThrowsException<FFMpegException>(() => FFMpegArguments var audioSamplesSource = new RawAudioPipeSource(new List<IAudioSample>()) { Channels = 0 };
.FromPipeInput(audioSamplesSource)
.OutputToFile(outputFile, false, opt => opt
.WithAudioCodec(AudioCodec.Aac))
.ProcessSynchronously());
}
[TestMethod, Timeout(10000)] Assert.ThrowsExactly<FFMpegException>(() => FFMpegArguments
public void Audio_ToAAC_Args_Pipe_InvalidFormat() .FromPipeInput(audioSamplesSource)
{ .OutputToFile(outputFile, false, opt => opt
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); .WithAudioCodec(AudioCodec.Aac))
.ProcessSynchronously());
}
var audioSamplesSource = new RawAudioPipeSource(new List<IAudioSample>()) [TestMethod]
{ [Timeout(10000, CooperativeCancellation = true)]
Format = "s8le", public void Audio_ToAAC_Args_Pipe_InvalidFormat()
}; {
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
var ex = Assert.ThrowsException<FFMpegException>(() => FFMpegArguments var audioSamplesSource = new RawAudioPipeSource(new List<IAudioSample>()) { Format = "s8le" };
.FromPipeInput(audioSamplesSource)
.OutputToFile(outputFile, false, opt => opt
.WithAudioCodec(AudioCodec.Aac))
.ProcessSynchronously());
}
[TestMethod, Timeout(10000)] Assert.ThrowsExactly<FFMpegException>(() => FFMpegArguments
public void Audio_ToAAC_Args_Pipe_InvalidSampleRate() .FromPipeInput(audioSamplesSource)
{ .OutputToFile(outputFile, false, opt => opt
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); .WithAudioCodec(AudioCodec.Aac))
.ProcessSynchronously());
}
var audioSamplesSource = new RawAudioPipeSource(new List<IAudioSample>()) [TestMethod]
{ [Timeout(10000, CooperativeCancellation = true)]
SampleRate = 0, public void Audio_ToAAC_Args_Pipe_InvalidSampleRate()
}; {
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
var ex = Assert.ThrowsException<FFMpegException>(() => FFMpegArguments var audioSamplesSource = new RawAudioPipeSource(new List<IAudioSample>()) { SampleRate = 0 };
.FromPipeInput(audioSamplesSource)
.OutputToFile(outputFile, false, opt => opt
.WithAudioCodec(AudioCodec.Aac))
.ProcessSynchronously());
}
[TestMethod, Timeout(10000)] Assert.ThrowsExactly<FFMpegException>(() => FFMpegArguments
public void Audio_Pan_ToMono() .FromPipeInput(audioSamplesSource)
{ .OutputToFile(outputFile, false, opt => opt
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); .WithAudioCodec(AudioCodec.Aac))
.ProcessSynchronously());
}
var success = FFMpegArguments.FromFileInput(TestResources.Mp3Audio) [TestMethod]
.OutputToFile(outputFile, true, [Timeout(10000, CooperativeCancellation = true)]
argumentOptions => argumentOptions public void Audio_Pan_ToMono()
.WithAudioFilters(filter => filter.Pan(1, "c0 < 0.9 * c0 + 0.1 * c1"))) {
.ProcessSynchronously(); using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
var mediaAnalysis = FFProbe.Analyse(outputFile); var success = FFMpegArguments.FromFileInput(TestResources.Mp3Audio)
.OutputToFile(outputFile, true,
argumentOptions => argumentOptions
.WithAudioFilters(filter => filter.Pan(1, "c0 < 0.9 * c0 + 0.1 * c1")))
.ProcessSynchronously();
Assert.IsTrue(success); var mediaAnalysis = FFProbe.Analyse(outputFile);
Assert.AreEqual(1, mediaAnalysis.AudioStreams.Count);
Assert.AreEqual("mono", mediaAnalysis.PrimaryAudioStream!.ChannelLayout);
}
[TestMethod, Timeout(10000)] Assert.IsTrue(success);
public void Audio_Pan_ToMonoNoDefinitions() Assert.HasCount(1, mediaAnalysis.AudioStreams);
{ Assert.AreEqual("mono", mediaAnalysis.PrimaryAudioStream!.ChannelLayout);
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); }
var success = FFMpegArguments.FromFileInput(TestResources.Mp3Audio) [TestMethod]
.OutputToFile(outputFile, true, [Timeout(10000, CooperativeCancellation = true)]
argumentOptions => argumentOptions public void Audio_Pan_ToMonoNoDefinitions()
.WithAudioFilters(filter => filter.Pan(1))) {
.ProcessSynchronously(); using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
var mediaAnalysis = FFProbe.Analyse(outputFile); var success = FFMpegArguments.FromFileInput(TestResources.Mp3Audio)
.OutputToFile(outputFile, true,
argumentOptions => argumentOptions
.WithAudioFilters(filter => filter.Pan(1)))
.ProcessSynchronously();
Assert.IsTrue(success); var mediaAnalysis = FFProbe.Analyse(outputFile);
Assert.AreEqual(1, mediaAnalysis.AudioStreams.Count);
Assert.AreEqual("mono", mediaAnalysis.PrimaryAudioStream!.ChannelLayout);
}
[TestMethod, Timeout(10000)] Assert.IsTrue(success);
public void Audio_Pan_ToMonoChannelsToOutputDefinitionsMismatch() Assert.HasCount(1, mediaAnalysis.AudioStreams);
{ Assert.AreEqual("mono", mediaAnalysis.PrimaryAudioStream!.ChannelLayout);
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); }
var ex = Assert.ThrowsException<ArgumentException>(() => FFMpegArguments.FromFileInput(TestResources.Mp3Audio) [TestMethod]
.OutputToFile(outputFile, true, [Timeout(10000, CooperativeCancellation = true)]
argumentOptions => argumentOptions public void Audio_Pan_ToMonoChannelsToOutputDefinitionsMismatch()
.WithAudioFilters(filter => filter.Pan(1, "c0=c0", "c1=c1"))) {
.ProcessSynchronously()); using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
}
[TestMethod, Timeout(10000)] Assert.ThrowsExactly<ArgumentException>(() => FFMpegArguments.FromFileInput(TestResources.Mp3Audio)
public void Audio_Pan_ToMonoChannelsLayoutToOutputDefinitionsMismatch() .OutputToFile(outputFile, true,
{ argumentOptions => argumentOptions
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); .WithAudioFilters(filter => filter.Pan(1, "c0=c0", "c1=c1")))
.ProcessSynchronously());
}
var ex = Assert.ThrowsException<FFMpegException>(() => FFMpegArguments.FromFileInput(TestResources.Mp3Audio) [TestMethod]
.OutputToFile(outputFile, true, [Timeout(10000, CooperativeCancellation = true)]
argumentOptions => argumentOptions public void Audio_Pan_ToMonoChannelsLayoutToOutputDefinitionsMismatch()
.WithAudioFilters(filter => filter.Pan("mono", "c0=c0", "c1=c1"))) {
.ProcessSynchronously()); using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
}
[TestMethod, Timeout(10000)] Assert.ThrowsExactly<FFMpegException>(() => FFMpegArguments.FromFileInput(TestResources.Mp3Audio)
public void Audio_DynamicNormalizer_WithDefaultValues() .OutputToFile(outputFile, true,
{ argumentOptions => argumentOptions
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); .WithAudioFilters(filter => filter.Pan("mono", "c0=c0", "c1=c1")))
.ProcessSynchronously());
}
var success = FFMpegArguments.FromFileInput(TestResources.Mp3Audio) [TestMethod]
.OutputToFile(outputFile, true, [Timeout(10000, CooperativeCancellation = true)]
argumentOptions => argumentOptions public void Audio_DynamicNormalizer_WithDefaultValues()
.WithAudioFilters(filter => filter.DynamicNormalizer())) {
.ProcessSynchronously(); using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
Assert.IsTrue(success); var success = FFMpegArguments.FromFileInput(TestResources.Mp3Audio)
} .OutputToFile(outputFile, true,
argumentOptions => argumentOptions
.WithAudioFilters(filter => filter.DynamicNormalizer()))
.ProcessSynchronously();
[TestMethod, Timeout(10000)] Assert.IsTrue(success);
public void Audio_DynamicNormalizer_WithNonDefaultValues() }
{
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
var success = FFMpegArguments.FromFileInput(TestResources.Mp3Audio) [TestMethod]
.OutputToFile(outputFile, true, [Timeout(10000, CooperativeCancellation = true)]
argumentOptions => argumentOptions public void Audio_DynamicNormalizer_WithNonDefaultValues()
.WithAudioFilters( {
filter => filter.DynamicNormalizer(250, 7, 0.9, 2, 1, false, true, true, 0.5))) using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
.ProcessSynchronously();
Assert.IsTrue(success); var success = FFMpegArguments.FromFileInput(TestResources.Mp3Audio)
} .OutputToFile(outputFile, true,
argumentOptions => argumentOptions
.WithAudioFilters(filter => filter.DynamicNormalizer(250, 7, 0.9, 2, 1, false, true, true, 0.5)))
.ProcessSynchronously();
[DataTestMethod, Timeout(10000)] Assert.IsTrue(success);
[DataRow(2)] }
[DataRow(32)]
[DataRow(8)]
public void Audio_DynamicNormalizer_FilterWindow(int filterWindow)
{
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
var ex = Assert.ThrowsException<ArgumentOutOfRangeException>(() => FFMpegArguments [TestMethod]
.FromFileInput(TestResources.Mp3Audio) [Timeout(10000, CooperativeCancellation = true)]
.OutputToFile(outputFile, true, [DataRow(2)]
argumentOptions => argumentOptions [DataRow(32)]
.WithAudioFilters( [DataRow(8)]
filter => filter.DynamicNormalizer(filterWindow: filterWindow))) public void Audio_DynamicNormalizer_FilterWindow(int filterWindow)
.ProcessSynchronously()); {
} using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => FFMpegArguments
.FromFileInput(TestResources.Mp3Audio)
.OutputToFile(outputFile, true,
argumentOptions => argumentOptions
.WithAudioFilters(filter => filter.DynamicNormalizer(filterWindow: filterWindow)))
.ProcessSynchronously());
} }
} }

View file

@ -0,0 +1,53 @@
using FFMpegCore.Extensions.Downloader;
using FFMpegCore.Extensions.Downloader.Enums;
using FFMpegCore.Test.Utilities;
namespace FFMpegCore.Test;
[TestClass]
public class DownloaderTests
{
private FFOptions _ffOptions;
[TestInitialize]
public void InitializeTestFolder()
{
var tempDownloadFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDownloadFolder);
_ffOptions = new FFOptions { BinaryFolder = tempDownloadFolder };
}
[TestCleanup]
public void DeleteTestFolder()
{
Directory.Delete(_ffOptions.BinaryFolder, true);
}
[OsSpecificTestMethod(OsPlatforms.Windows | OsPlatforms.Linux)]
public async Task GetSpecificVersionTest()
{
var binaries = await FFMpegDownloader.DownloadBinaries(FFMpegVersions.V6_1, options: _ffOptions);
try
{
Assert.HasCount(2, binaries);
}
finally
{
binaries.ForEach(File.Delete);
}
}
[OsSpecificTestMethod(OsPlatforms.Windows | OsPlatforms.Linux)]
public async Task GetAllLatestSuiteTest()
{
var binaries = await FFMpegDownloader.DownloadBinaries(options: _ffOptions);
try
{
Assert.HasCount(2, binaries);
}
finally
{
binaries.ForEach(File.Delete);
}
}
}

View file

@ -1,103 +1,98 @@
using System.Reflection; namespace FFMpegCore.Test;
using FFMpegCore.Arguments;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace FFMpegCore.Test [TestClass]
public class FFMpegArgumentProcessorTest
{ {
[TestClass] private static FFMpegArgumentProcessor CreateArgumentProcessor()
public class FFMpegArgumentProcessorTest
{ {
[TestCleanup] return FFMpegArguments
public void TestInitialize() .FromFileInput("")
.OutputToFile("");
}
[TestMethod]
public void ZZZ_Processor_GlobalOptions_GetUsed()
{
var globalWorkingDir = "Whatever";
var processor = CreateArgumentProcessor();
try
{ {
// After testing reset global configuration to null, to be not wrong for other test relying on configuration
typeof(GlobalFFOptions).GetField("_current", BindingFlags.NonPublic | BindingFlags.Static)!.SetValue(GlobalFFOptions.Current, null);
}
private static FFMpegArgumentProcessor CreateArgumentProcessor() => FFMpegArguments
.FromFileInput("")
.OutputToFile("");
[TestMethod]
public void Processor_GlobalOptions_GetUsed()
{
var globalWorkingDir = "Whatever";
GlobalFFOptions.Configure(new FFOptions { WorkingDirectory = globalWorkingDir }); GlobalFFOptions.Configure(new FFOptions { WorkingDirectory = globalWorkingDir });
var processor = CreateArgumentProcessor();
var options2 = processor.GetConfiguredOptions(null);
options2.WorkingDirectory.Should().Be(globalWorkingDir);
}
[TestMethod]
public void Processor_SessionOptions_GetUsed()
{
var sessionWorkingDir = "./CurrentRunWorkingDir";
var processor = CreateArgumentProcessor();
processor.Configure(options => options.WorkingDirectory = sessionWorkingDir);
var options = processor.GetConfiguredOptions(null); var options = processor.GetConfiguredOptions(null);
options.WorkingDirectory.Should().Be(sessionWorkingDir); Assert.AreEqual(globalWorkingDir, options.WorkingDirectory);
} }
finally
[TestMethod]
public void Processor_Options_CanBeOverridden_And_Configured()
{ {
var globalConfig = "Whatever"; GlobalFFOptions.Configure(new FFOptions());
GlobalFFOptions.Configure(new FFOptions { WorkingDirectory = globalConfig, TemporaryFilesFolder = globalConfig, BinaryFolder = globalConfig }); }
}
[TestMethod]
public void Processor_SessionOptions_GetUsed()
{
var sessionWorkingDir = "./CurrentRunWorkingDir";
var processor = CreateArgumentProcessor();
processor.Configure(options => options.WorkingDirectory = sessionWorkingDir);
var options = processor.GetConfiguredOptions(null);
Assert.AreEqual(sessionWorkingDir, options.WorkingDirectory);
}
[TestMethod]
public void ZZZ_Processor_Options_CanBeOverridden_And_Configured()
{
var globalConfig = "Whatever";
try
{
var processor = CreateArgumentProcessor(); var processor = CreateArgumentProcessor();
var sessionTempDir = "./CurrentRunWorkingDir"; var sessionTempDir = "./CurrentRunWorkingDir";
processor.Configure(options => options.TemporaryFilesFolder = sessionTempDir); processor.Configure(options => options.TemporaryFilesFolder = sessionTempDir);
var overrideOptions = new FFOptions() { WorkingDirectory = "override" }; var overrideOptions = new FFOptions { WorkingDirectory = "override" };
GlobalFFOptions.Configure(new FFOptions { WorkingDirectory = globalConfig, TemporaryFilesFolder = globalConfig, BinaryFolder = globalConfig });
var options = processor.GetConfiguredOptions(overrideOptions); var options = processor.GetConfiguredOptions(overrideOptions);
options.Should().BeEquivalentTo(overrideOptions); Assert.AreEqual(options.WorkingDirectory, overrideOptions.WorkingDirectory);
options.TemporaryFilesFolder.Should().BeEquivalentTo(sessionTempDir); Assert.AreEqual(options.TemporaryFilesFolder, overrideOptions.TemporaryFilesFolder);
options.BinaryFolder.Should().NotBeEquivalentTo(globalConfig); Assert.AreEqual(options.BinaryFolder, overrideOptions.BinaryFolder);
Assert.AreEqual(sessionTempDir, options.TemporaryFilesFolder);
Assert.AreNotEqual(globalConfig, options.BinaryFolder);
} }
finally
[TestMethod]
public void Options_Global_And_Session_Options_Can_Differ()
{ {
var globalWorkingDir = "Whatever"; GlobalFFOptions.Configure(new FFOptions());
GlobalFFOptions.Configure(new FFOptions { WorkingDirectory = globalWorkingDir }); }
}
[TestMethod]
public void ZZZ_Options_Global_And_Session_Options_Can_Differ()
{
var globalWorkingDir = "Whatever";
try
{
var processor1 = CreateArgumentProcessor(); var processor1 = CreateArgumentProcessor();
var sessionWorkingDir = "./CurrentRunWorkingDir"; var sessionWorkingDir = "./CurrentRunWorkingDir";
processor1.Configure(options => options.WorkingDirectory = sessionWorkingDir); processor1.Configure(options => options.WorkingDirectory = sessionWorkingDir);
var options1 = processor1.GetConfiguredOptions(null); var options1 = processor1.GetConfiguredOptions(null);
options1.WorkingDirectory.Should().Be(sessionWorkingDir); Assert.AreEqual(sessionWorkingDir, options1.WorkingDirectory);
var processor2 = CreateArgumentProcessor(); var processor2 = CreateArgumentProcessor();
GlobalFFOptions.Configure(new FFOptions { WorkingDirectory = globalWorkingDir });
var options2 = processor2.GetConfiguredOptions(null); var options2 = processor2.GetConfiguredOptions(null);
options2.WorkingDirectory.Should().Be(globalWorkingDir); Assert.AreEqual(globalWorkingDir, options2.WorkingDirectory);
} }
finally
[TestMethod]
public void Concat_Escape()
{ {
var arg = new DemuxConcatArgument(new[] { @"Heaven's River\05 - Investigation.m4b" }); GlobalFFOptions.Configure(new FFOptions());
arg.Values.Should().BeEquivalentTo(new[] { @"file 'Heaven'\''s River\05 - Investigation.m4b'" });
}
[TestMethod]
public void Audible_Aaxc_Test()
{
var arg = new AudibleEncryptionKeyArgument("123", "456");
arg.Text.Should().Be($"-audible_key 123 -audible_iv 456");
}
[TestMethod]
public void Audible_Aax_Test()
{
var arg = new AudibleEncryptionKeyArgument("62689101");
arg.Text.Should().Be($"-activation_bytes 62689101");
} }
} }
} }

View file

@ -5,6 +5,7 @@
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<Nullable>disable</Nullable> <Nullable>disable</Nullable>
<LangVersion>default</LangVersion> <LangVersion>default</LangVersion>
<OrderTestsByNameInClass>true</OrderTestsByNameInClass>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -12,21 +13,20 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="FluentAssertions" Version="8.0.1" />
<PackageReference Include="GitHubActionsTestLogger" Version="2.4.1"> <PackageReference Include="GitHubActionsTestLogger" Version="2.4.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0"/>
<PackageReference Include="MSTest.TestAdapter" Version="3.8.0" /> <PackageReference Include="MSTest.TestAdapter" Version="4.0.1"/>
<PackageReference Include="MSTest.TestFramework" Version="3.8.0" /> <PackageReference Include="MSTest.TestFramework" Version="4.0.1"/>
<PackageReference Include="SkiaSharp" Version="3.116.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\FFMpegCore.Extensions.SkiaSharp\FFMpegCore.Extensions.SkiaSharp.csproj" /> <ProjectReference Include="..\FFMpegCore.Extensions.Downloader\FFMpegCore.Extensions.Downloader.csproj"/>
<ProjectReference Include="..\FFMpegCore.Extensions.System.Drawing.Common\FFMpegCore.Extensions.System.Drawing.Common.csproj" /> <ProjectReference Include="..\FFMpegCore.Extensions.SkiaSharp\FFMpegCore.Extensions.SkiaSharp.csproj"/>
<ProjectReference Include="..\FFMpegCore\FFMpegCore.csproj" /> <ProjectReference Include="..\FFMpegCore.Extensions.System.Drawing.Common\FFMpegCore.Extensions.System.Drawing.Common.csproj"/>
<ProjectReference Include="..\FFMpegCore\FFMpegCore.csproj"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -1,48 +1,42 @@
using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json;
using Newtonsoft.Json;
namespace FFMpegCore.Test namespace FFMpegCore.Test;
[TestClass]
public class FFMpegOptionsTest
{ {
[TestClass] [TestMethod]
public class FFMpegOptionsTest public void Options_Initialized()
{ {
[TestMethod] Assert.IsNotNull(GlobalFFOptions.Current);
public void Options_Initialized() }
{
Assert.IsNotNull(GlobalFFOptions.Current);
}
[TestMethod] [TestMethod]
public void Options_Defaults_Configured() public void Options_Defaults_Configured()
{ {
Assert.AreEqual(new FFOptions().BinaryFolder, $""); Assert.AreEqual("", new FFOptions().BinaryFolder);
} }
[TestMethod] [TestMethod]
public void Options_Loaded_From_File() public void Options_Loaded_From_File()
{ {
Assert.AreEqual( Assert.AreEqual(
GlobalFFOptions.Current.BinaryFolder, GlobalFFOptions.Current.BinaryFolder,
JsonConvert.DeserializeObject<FFOptions>(File.ReadAllText("ffmpeg.config.json")).BinaryFolder JsonConvert.DeserializeObject<FFOptions>(File.ReadAllText("ffmpeg.config.json")).BinaryFolder
); );
} }
[TestMethod] [TestMethod]
public void Options_Set_Programmatically() public void ZZZ_Options_Set_Programmatically()
{
try
{ {
var original = GlobalFFOptions.Current; GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "Whatever" });
try Assert.AreEqual("Whatever", GlobalFFOptions.Current.BinaryFolder);
{ }
GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "Whatever" }); finally
Assert.AreEqual( {
GlobalFFOptions.Current.BinaryFolder, GlobalFFOptions.Configure(new FFOptions());
"Whatever"
);
}
finally
{
GlobalFFOptions.Configure(original);
}
} }
} }
} }

View file

@ -1,267 +1,288 @@
using FFMpegCore.Test.Resources; using FFMpegCore.Test.Resources;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace FFMpegCore.Test namespace FFMpegCore.Test;
[TestClass]
public class FFProbeTests
{ {
[TestClass] public TestContext TestContext { get; set; }
public class FFProbeTests
[TestMethod]
public async Task Audio_FromStream_Duration()
{ {
[TestMethod] var fileAnalysis = await FFProbe.AnalyseAsync(TestResources.WebmVideo, cancellationToken: TestContext.CancellationToken);
public async Task Audio_FromStream_Duration() await using var inputStream = File.OpenRead(TestResources.WebmVideo);
{ var streamAnalysis = await FFProbe.AnalyseAsync(inputStream, cancellationToken: TestContext.CancellationToken);
var fileAnalysis = await FFProbe.AnalyseAsync(TestResources.WebmVideo); Assert.IsTrue(fileAnalysis.Duration == streamAnalysis.Duration);
await using var inputStream = File.OpenRead(TestResources.WebmVideo); }
var streamAnalysis = await FFProbe.AnalyseAsync(inputStream);
Assert.IsTrue(fileAnalysis.Duration == streamAnalysis.Duration);
}
[TestMethod] [TestMethod]
public void FrameAnalysis_Sync() public void FrameAnalysis_Sync()
{ {
var frameAnalysis = FFProbe.GetFrames(TestResources.WebmVideo); var frameAnalysis = FFProbe.GetFrames(TestResources.WebmVideo);
Assert.AreEqual(90, frameAnalysis.Frames.Count); Assert.HasCount(90, frameAnalysis.Frames);
Assert.IsTrue(frameAnalysis.Frames.All(f => f.PixelFormat == "yuv420p")); Assert.IsTrue(frameAnalysis.Frames.All(f => f.PixelFormat == "yuv420p"));
Assert.IsTrue(frameAnalysis.Frames.All(f => f.Height == 360)); Assert.IsTrue(frameAnalysis.Frames.All(f => f.Height == 360));
Assert.IsTrue(frameAnalysis.Frames.All(f => f.Width == 640)); Assert.IsTrue(frameAnalysis.Frames.All(f => f.Width == 640));
Assert.IsTrue(frameAnalysis.Frames.All(f => f.MediaType == "video")); Assert.IsTrue(frameAnalysis.Frames.All(f => f.MediaType == "video"));
} }
[TestMethod] [TestMethod]
public async Task FrameAnalysis_Async() public async Task FrameAnalysis_Async()
{ {
var frameAnalysis = await FFProbe.GetFramesAsync(TestResources.WebmVideo); var frameAnalysis = await FFProbe.GetFramesAsync(TestResources.WebmVideo, cancellationToken: TestContext.CancellationToken);
Assert.AreEqual(90, frameAnalysis.Frames.Count); Assert.HasCount(90, frameAnalysis.Frames);
Assert.IsTrue(frameAnalysis.Frames.All(f => f.PixelFormat == "yuv420p")); Assert.IsTrue(frameAnalysis.Frames.All(f => f.PixelFormat == "yuv420p"));
Assert.IsTrue(frameAnalysis.Frames.All(f => f.Height == 360)); Assert.IsTrue(frameAnalysis.Frames.All(f => f.Height == 360));
Assert.IsTrue(frameAnalysis.Frames.All(f => f.Width == 640)); Assert.IsTrue(frameAnalysis.Frames.All(f => f.Width == 640));
Assert.IsTrue(frameAnalysis.Frames.All(f => f.MediaType == "video")); Assert.IsTrue(frameAnalysis.Frames.All(f => f.MediaType == "video"));
} }
[TestMethod] [TestMethod]
public async Task PacketAnalysis_Async() public async Task PacketAnalysis_Async()
{ {
var packetAnalysis = await FFProbe.GetPacketsAsync(TestResources.WebmVideo); var packetAnalysis = await FFProbe.GetPacketsAsync(TestResources.WebmVideo, cancellationToken: TestContext.CancellationToken);
var packets = packetAnalysis.Packets; var packets = packetAnalysis.Packets;
Assert.AreEqual(96, packets.Count); Assert.HasCount(96, packets);
Assert.IsTrue(packets.All(f => f.CodecType == "video")); Assert.IsTrue(packets.All(f => f.CodecType == "video"));
Assert.IsTrue(packets[0].Flags.StartsWith("K_")); Assert.StartsWith("K_", packets[0].Flags);
Assert.AreEqual(1362, packets.Last().Size); Assert.AreEqual(1362, packets.Last().Size);
} }
[TestMethod] [TestMethod]
public void PacketAnalysis_Sync() public void PacketAnalysis_Sync()
{ {
var packets = FFProbe.GetPackets(TestResources.WebmVideo).Packets; var packets = FFProbe.GetPackets(TestResources.WebmVideo).Packets;
Assert.AreEqual(96, packets.Count); Assert.HasCount(96, packets);
Assert.IsTrue(packets.All(f => f.CodecType == "video")); Assert.IsTrue(packets.All(f => f.CodecType == "video"));
Assert.IsTrue(packets[0].Flags.StartsWith("K_")); Assert.StartsWith("K_", packets[0].Flags);
Assert.AreEqual(1362, packets.Last().Size); Assert.AreEqual(1362, packets.Last().Size);
} }
[TestMethod] [TestMethod]
public void PacketAnalysisAudioVideo_Sync() public void PacketAnalysisAudioVideo_Sync()
{ {
var packets = FFProbe.GetPackets(TestResources.Mp4Video).Packets; var packets = FFProbe.GetPackets(TestResources.Mp4Video).Packets;
Assert.AreEqual(216, packets.Count); Assert.HasCount(216, packets);
var actual = packets.Select(f => f.CodecType).Distinct().ToList(); var actual = packets.Select(f => f.CodecType).Distinct().ToList();
var expected = new List<string> { "audio", "video" }; var expected = new List<string> { "audio", "video" };
CollectionAssert.AreEquivalent(expected, actual); CollectionAssert.AreEquivalent(expected, actual);
Assert.IsTrue(packets.Where(t => t.CodecType == "audio").All(f => f.Flags.StartsWith("K_"))); Assert.IsTrue(packets.Where(t => t.CodecType == "audio").All(f => f.Flags.StartsWith("K_")));
Assert.AreEqual(75, packets.Count(t => t.CodecType == "video")); Assert.AreEqual(75, packets.Count(t => t.CodecType == "video"));
Assert.AreEqual(141, packets.Count(t => t.CodecType == "audio")); Assert.AreEqual(141, packets.Count(t => t.CodecType == "audio"));
} }
[DataTestMethod] [TestMethod]
[DataRow("0:00:03.008000", 0, 0, 0, 3, 8)] [DataRow("0:00:03.008000", 0, 0, 0, 3, 8)]
[DataRow("05:12:59.177", 0, 5, 12, 59, 177)] [DataRow("05:12:59.177", 0, 5, 12, 59, 177)]
[DataRow("149:07:50.911750", 6, 5, 7, 50, 911)] [DataRow("149:07:50.911750", 6, 5, 7, 50, 911)]
[DataRow("00:00:00.83", 0, 0, 0, 0, 830)] [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) public void MediaAnalysis_ParseDuration(string duration, int expectedDays, int expectedHours, int expectedMinutes, int expectedSeconds,
{ int expectedMilliseconds)
var ffprobeStream = new FFProbeStream { Duration = duration }; {
var ffprobeStream = new FFProbeStream { Duration = duration };
var parsedDuration = MediaAnalysisUtils.ParseDuration(ffprobeStream.Duration); var parsedDuration = MediaAnalysisUtils.ParseDuration(ffprobeStream.Duration);
Assert.AreEqual(expectedDays, parsedDuration.Days); Assert.AreEqual(expectedDays, parsedDuration.Days);
Assert.AreEqual(expectedHours, parsedDuration.Hours); Assert.AreEqual(expectedHours, parsedDuration.Hours);
Assert.AreEqual(expectedMinutes, parsedDuration.Minutes); Assert.AreEqual(expectedMinutes, parsedDuration.Minutes);
Assert.AreEqual(expectedSeconds, parsedDuration.Seconds); Assert.AreEqual(expectedSeconds, parsedDuration.Seconds);
Assert.AreEqual(expectedMilliseconds, parsedDuration.Milliseconds); Assert.AreEqual(expectedMilliseconds, parsedDuration.Milliseconds);
} }
[TestMethod, Ignore("Consistently fails on GitHub Workflow ubuntu agents")] [TestMethod]
public async Task Uri_Duration() [Ignore("Consistently fails on GitHub Workflow ubuntu agents")]
{ public async Task Uri_Duration()
var fileAnalysis = await FFProbe.AnalyseAsync(new Uri("https://github.com/rosenbjerg/FFMpegCore/raw/master/FFMpegCore.Test/Resources/input_3sec.webm")); {
Assert.IsNotNull(fileAnalysis); var fileAnalysis = await FFProbe.AnalyseAsync(new Uri("https://github.com/rosenbjerg/FFMpegCore/raw/master/FFMpegCore.Test/Resources/input_3sec.webm"),
} cancellationToken: TestContext.CancellationToken);
Assert.IsNotNull(fileAnalysis);
}
[TestMethod] [TestMethod]
public void Probe_Success() public void Probe_Success()
{ {
var info = FFProbe.Analyse(TestResources.Mp4Video); var info = FFProbe.Analyse(TestResources.Mp4Video);
Assert.AreEqual(3, info.Duration.Seconds); Assert.AreEqual(3, info.Duration.Seconds);
Assert.AreEqual(0, info.Chapters.Count); Assert.IsEmpty(info.Chapters);
Assert.AreEqual("5.1", info.PrimaryAudioStream!.ChannelLayout); Assert.AreEqual("5.1", info.PrimaryAudioStream!.ChannelLayout);
Assert.AreEqual(6, info.PrimaryAudioStream.Channels); Assert.AreEqual(6, info.PrimaryAudioStream.Channels);
Assert.AreEqual("AAC (Advanced Audio Coding)", info.PrimaryAudioStream.CodecLongName); Assert.AreEqual("AAC (Advanced Audio Coding)", info.PrimaryAudioStream.CodecLongName);
Assert.AreEqual("aac", info.PrimaryAudioStream.CodecName); Assert.AreEqual("aac", info.PrimaryAudioStream.CodecName);
Assert.AreEqual("LC", info.PrimaryAudioStream.Profile); Assert.AreEqual("LC", info.PrimaryAudioStream.Profile);
Assert.AreEqual(377351, info.PrimaryAudioStream.BitRate); Assert.AreEqual(377351, info.PrimaryAudioStream.BitRate);
Assert.AreEqual(48000, info.PrimaryAudioStream.SampleRateHz); Assert.AreEqual(48000, info.PrimaryAudioStream.SampleRateHz);
Assert.AreEqual("mp4a", info.PrimaryAudioStream.CodecTagString); Assert.AreEqual("mp4a", info.PrimaryAudioStream.CodecTagString);
Assert.AreEqual("0x6134706d", info.PrimaryAudioStream.CodecTag); Assert.AreEqual("0x6134706d", info.PrimaryAudioStream.CodecTag);
Assert.AreEqual(1471810, info.PrimaryVideoStream!.BitRate); Assert.AreEqual(1471810, info.PrimaryVideoStream!.BitRate);
Assert.AreEqual(16, info.PrimaryVideoStream.DisplayAspectRatio.Width); Assert.AreEqual(16, info.PrimaryVideoStream.DisplayAspectRatio.Width);
Assert.AreEqual(9, info.PrimaryVideoStream.DisplayAspectRatio.Height); Assert.AreEqual(9, info.PrimaryVideoStream.DisplayAspectRatio.Height);
Assert.AreEqual(1, info.PrimaryVideoStream.SampleAspectRatio.Width); Assert.AreEqual(1, info.PrimaryVideoStream.SampleAspectRatio.Width);
Assert.AreEqual(1, info.PrimaryVideoStream.SampleAspectRatio.Height); Assert.AreEqual(1, info.PrimaryVideoStream.SampleAspectRatio.Height);
Assert.AreEqual("yuv420p", info.PrimaryVideoStream.PixelFormat); Assert.AreEqual("yuv420p", info.PrimaryVideoStream.PixelFormat);
Assert.AreEqual(31, info.PrimaryVideoStream.Level); Assert.AreEqual(31, info.PrimaryVideoStream.Level);
Assert.AreEqual(1280, info.PrimaryVideoStream.Width); Assert.AreEqual(1280, info.PrimaryVideoStream.Width);
Assert.AreEqual(720, info.PrimaryVideoStream.Height); Assert.AreEqual(720, info.PrimaryVideoStream.Height);
Assert.AreEqual(25, info.PrimaryVideoStream.AvgFrameRate); Assert.AreEqual(25, info.PrimaryVideoStream.AvgFrameRate);
Assert.AreEqual(25, info.PrimaryVideoStream.FrameRate); Assert.AreEqual(25, info.PrimaryVideoStream.FrameRate);
Assert.AreEqual("H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", info.PrimaryVideoStream.CodecLongName); Assert.AreEqual("H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", info.PrimaryVideoStream.CodecLongName);
Assert.AreEqual("h264", info.PrimaryVideoStream.CodecName); Assert.AreEqual("h264", info.PrimaryVideoStream.CodecName);
Assert.AreEqual(8, info.PrimaryVideoStream.BitsPerRawSample); Assert.AreEqual(8, info.PrimaryVideoStream.BitsPerRawSample);
Assert.AreEqual("Main", info.PrimaryVideoStream.Profile); Assert.AreEqual("Main", info.PrimaryVideoStream.Profile);
Assert.AreEqual("avc1", info.PrimaryVideoStream.CodecTagString); Assert.AreEqual("avc1", info.PrimaryVideoStream.CodecTagString);
Assert.AreEqual("0x31637661", info.PrimaryVideoStream.CodecTag); Assert.AreEqual("0x31637661", info.PrimaryVideoStream.CodecTag);
} }
[TestMethod] [TestMethod]
public void Probe_Rotation() public void Probe_Rotation()
{ {
var info = FFProbe.Analyse(TestResources.Mp4Video); var info = FFProbe.Analyse(TestResources.Mp4Video);
Assert.AreEqual(0, info.PrimaryVideoStream.Rotation); Assert.IsNotNull(info.PrimaryVideoStream);
Assert.AreEqual(0, info.PrimaryVideoStream.Rotation);
info = FFProbe.Analyse(TestResources.Mp4VideoRotation); info = FFProbe.Analyse(TestResources.Mp4VideoRotation);
Assert.AreEqual(90, info.PrimaryVideoStream.Rotation); Assert.IsNotNull(info.PrimaryVideoStream);
} Assert.AreEqual(90, info.PrimaryVideoStream.Rotation);
}
[TestMethod] [TestMethod]
public void Probe_Rotation_Negative_Value() public void Probe_Rotation_Negative_Value()
{ {
var info = FFProbe.Analyse(TestResources.Mp4VideoRotationNegative); var info = FFProbe.Analyse(TestResources.Mp4VideoRotationNegative);
Assert.AreEqual(-90, info.PrimaryVideoStream.Rotation); Assert.IsNotNull(info.PrimaryVideoStream);
} Assert.AreEqual(-90, info.PrimaryVideoStream.Rotation);
}
[TestMethod, Timeout(10000)] [TestMethod]
public async Task Probe_Async_Success() [Timeout(10000, CooperativeCancellation = true)]
{ public async Task Probe_Async_Success()
var info = await FFProbe.AnalyseAsync(TestResources.Mp4Video); {
Assert.AreEqual(3, info.Duration.Seconds); var info = await FFProbe.AnalyseAsync(TestResources.Mp4Video, cancellationToken: TestContext.CancellationToken);
Assert.AreEqual(8, info.PrimaryVideoStream.BitDepth); Assert.AreEqual(3, info.Duration.Seconds);
// This video's audio stream is AAC, which is lossy, so bit depth is meaningless. Assert.IsNotNull(info.PrimaryVideoStream);
Assert.IsNull(info.PrimaryAudioStream.BitDepth); Assert.AreEqual(8, info.PrimaryVideoStream.BitDepth);
} // This video's audio stream is AAC, which is lossy, so bit depth is meaningless.
Assert.IsNotNull(info.PrimaryAudioStream);
Assert.IsNull(info.PrimaryAudioStream.BitDepth);
}
[TestMethod, Timeout(10000)] [TestMethod]
public void Probe_Success_FromStream() [Timeout(10000, CooperativeCancellation = true)]
{ public void Probe_Success_FromStream()
using var stream = File.OpenRead(TestResources.WebmVideo); {
var info = FFProbe.Analyse(stream); using var stream = File.OpenRead(TestResources.WebmVideo);
Assert.AreEqual(3, info.Duration.Seconds); var info = FFProbe.Analyse(stream);
// This video has no audio stream. Assert.AreEqual(3, info.Duration.Seconds);
Assert.IsNull(info.PrimaryAudioStream); // This video has no audio stream.
} Assert.IsNull(info.PrimaryAudioStream);
}
[TestMethod, Timeout(10000)] [TestMethod]
public async Task Probe_Success_FromStream_Async() [Timeout(10000, CooperativeCancellation = true)]
{ public async Task Probe_Success_FromStream_Async()
await using var stream = File.OpenRead(TestResources.WebmVideo); {
var info = await FFProbe.AnalyseAsync(stream); await using var stream = File.OpenRead(TestResources.WebmVideo);
Assert.AreEqual(3, info.Duration.Seconds); var info = await FFProbe.AnalyseAsync(stream, cancellationToken: TestContext.CancellationToken);
} Assert.AreEqual(3, info.Duration.Seconds);
}
[TestMethod, Timeout(10000)] [TestMethod]
public void Probe_HDR() [Timeout(10000, CooperativeCancellation = true)]
{ public void Probe_HDR()
var info = FFProbe.Analyse(TestResources.HdrVideo); {
var info = FFProbe.Analyse(TestResources.HdrVideo);
Assert.IsNotNull(info.PrimaryVideoStream); Assert.IsNotNull(info.PrimaryVideoStream);
Assert.AreEqual("tv", info.PrimaryVideoStream.ColorRange); Assert.AreEqual("tv", info.PrimaryVideoStream.ColorRange);
Assert.AreEqual("bt2020nc", info.PrimaryVideoStream.ColorSpace); Assert.AreEqual("bt2020nc", info.PrimaryVideoStream.ColorSpace);
Assert.AreEqual("arib-std-b67", info.PrimaryVideoStream.ColorTransfer); Assert.AreEqual("arib-std-b67", info.PrimaryVideoStream.ColorTransfer);
Assert.AreEqual("bt2020", info.PrimaryVideoStream.ColorPrimaries); Assert.AreEqual("bt2020", info.PrimaryVideoStream.ColorPrimaries);
} }
[TestMethod, Timeout(10000)] [TestMethod]
public async Task Probe_Success_Subtitle_Async() [Timeout(10000, CooperativeCancellation = true)]
{ public async Task Probe_Success_Subtitle_Async()
var info = await FFProbe.AnalyseAsync(TestResources.SrtSubtitle); {
Assert.IsNotNull(info.PrimarySubtitleStream); var info = await FFProbe.AnalyseAsync(TestResources.SrtSubtitle, cancellationToken: TestContext.CancellationToken);
Assert.AreEqual(1, info.SubtitleStreams.Count); Assert.IsNotNull(info.PrimarySubtitleStream);
Assert.AreEqual(0, info.AudioStreams.Count); Assert.HasCount(1, info.SubtitleStreams);
Assert.AreEqual(0, info.VideoStreams.Count); Assert.IsEmpty(info.AudioStreams);
// BitDepth is meaningless for subtitles Assert.IsEmpty(info.VideoStreams);
Assert.IsNull(info.SubtitleStreams[0].BitDepth); // BitDepth is meaningless for subtitles
} Assert.IsNull(info.SubtitleStreams[0].BitDepth);
}
[TestMethod, Timeout(10000)] [TestMethod]
public async Task Probe_Success_Disposition_Async() [Timeout(10000, CooperativeCancellation = true)]
{ public async Task Probe_Success_Disposition_Async()
var info = await FFProbe.AnalyseAsync(TestResources.Mp4Video); {
Assert.IsNotNull(info.PrimaryAudioStream); var info = await FFProbe.AnalyseAsync(TestResources.Mp4Video, cancellationToken: TestContext.CancellationToken);
Assert.IsNotNull(info.PrimaryAudioStream.Disposition); Assert.IsNotNull(info.PrimaryAudioStream);
Assert.AreEqual(true, info.PrimaryAudioStream.Disposition["default"]); Assert.IsNotNull(info.PrimaryAudioStream.Disposition);
Assert.AreEqual(false, info.PrimaryAudioStream.Disposition["forced"]); Assert.IsTrue(info.PrimaryAudioStream.Disposition["default"]);
} Assert.IsFalse(info.PrimaryAudioStream.Disposition["forced"]);
}
[TestMethod, Timeout(10000)] [TestMethod]
public async Task Probe_Success_Mp3AudioBitDepthNull_Async() [Timeout(10000, CooperativeCancellation = true)]
{ public async Task Probe_Success_Mp3AudioBitDepthNull_Async()
var info = await FFProbe.AnalyseAsync(TestResources.Mp3Audio); {
Assert.IsNotNull(info.PrimaryAudioStream); var info = await FFProbe.AnalyseAsync(TestResources.Mp3Audio, cancellationToken: TestContext.CancellationToken);
// mp3 is lossy, so bit depth is meaningless. Assert.IsNotNull(info.PrimaryAudioStream);
Assert.IsNull(info.PrimaryAudioStream.BitDepth); // mp3 is lossy, so bit depth is meaningless.
} Assert.IsNull(info.PrimaryAudioStream.BitDepth);
}
[TestMethod, Timeout(10000)] [TestMethod]
public async Task Probe_Success_VocAudioBitDepth_Async() [Timeout(10000, CooperativeCancellation = true)]
{ public async Task Probe_Success_VocAudioBitDepth_Async()
var info = await FFProbe.AnalyseAsync(TestResources.AiffAudio); {
Assert.IsNotNull(info.PrimaryAudioStream); var info = await FFProbe.AnalyseAsync(TestResources.AiffAudio, cancellationToken: TestContext.CancellationToken);
Assert.AreEqual(16, info.PrimaryAudioStream.BitDepth); Assert.IsNotNull(info.PrimaryAudioStream);
} Assert.AreEqual(16, info.PrimaryAudioStream.BitDepth);
}
[TestMethod, Timeout(10000)] [TestMethod]
public async Task Probe_Success_MkvVideoBitDepth_Async() [Timeout(10000, CooperativeCancellation = true)]
{ public async Task Probe_Success_MkvVideoBitDepth_Async()
var info = await FFProbe.AnalyseAsync(TestResources.MkvVideo); {
Assert.IsNotNull(info.PrimaryAudioStream); var info = await FFProbe.AnalyseAsync(TestResources.MkvVideo, cancellationToken: TestContext.CancellationToken);
Assert.AreEqual(8, info.PrimaryVideoStream.BitDepth); Assert.IsNotNull(info.PrimaryVideoStream);
Assert.IsNull(info.PrimaryAudioStream.BitDepth); Assert.AreEqual(8, info.PrimaryVideoStream.BitDepth);
}
[TestMethod, Timeout(10000)] Assert.IsNotNull(info.PrimaryAudioStream);
public async Task Probe_Success_24BitWavBitDepth_Async() Assert.IsNull(info.PrimaryAudioStream.BitDepth);
{ }
var info = await FFProbe.AnalyseAsync(TestResources.Wav24Bit);
Assert.IsNotNull(info.PrimaryAudioStream);
Assert.AreEqual(24, info.PrimaryAudioStream.BitDepth);
}
[TestMethod, Timeout(10000)] [TestMethod]
public async Task Probe_Success_32BitWavBitDepth_Async() [Timeout(10000, CooperativeCancellation = true)]
{ public async Task Probe_Success_24BitWavBitDepth_Async()
var info = await FFProbe.AnalyseAsync(TestResources.Wav32Bit); {
Assert.IsNotNull(info.PrimaryAudioStream); var info = await FFProbe.AnalyseAsync(TestResources.Wav24Bit, cancellationToken: TestContext.CancellationToken);
Assert.AreEqual(32, info.PrimaryAudioStream.BitDepth); Assert.IsNotNull(info.PrimaryAudioStream);
} Assert.AreEqual(24, info.PrimaryAudioStream.BitDepth);
}
[TestMethod] [TestMethod]
public void Probe_Success_Custom_Arguments() [Timeout(10000, CooperativeCancellation = true)]
{ public async Task Probe_Success_32BitWavBitDepth_Async()
var info = FFProbe.Analyse(TestResources.Mp4Video, customArguments: "-headers \"Hello: World\""); {
Assert.AreEqual(3, info.Duration.Seconds); var info = await FFProbe.AnalyseAsync(TestResources.Wav32Bit, cancellationToken: TestContext.CancellationToken);
} Assert.IsNotNull(info.PrimaryAudioStream);
Assert.AreEqual(32, info.PrimaryAudioStream.BitDepth);
}
[TestMethod]
public void Probe_Success_Custom_Arguments()
{
var info = FFProbe.Analyse(TestResources.Mp4Video, customArguments: "-headers \"Hello: World\"");
Assert.AreEqual(3, info.Duration.Seconds);
} }
} }

View file

@ -1,70 +1,66 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using FFMpegCore.Builders.MetaData; using FFMpegCore.Builders.MetaData;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace FFMpegCore.Test namespace FFMpegCore.Test;
[TestClass]
public class MetaDataBuilderTests
{ {
[TestClass] [TestMethod]
public class MetaDataBuilderTests public void TestMetaDataBuilderIntegrity()
{ {
[TestMethod] var source = new
public void TestMetaDataBuilderIntegrity()
{ {
var source = new Album = "Kanon und Gigue",
Artist = "Pachelbel",
Title = "Kanon und Gigue in D-Dur",
Copyright = "Copyright Lol",
Composer = "Pachelbel",
Genres = new[] { "Synthwave", "Classics" },
Tracks = new[]
{ {
Album = "Kanon und Gigue", new { Duration = TimeSpan.FromSeconds(10), Title = "Chapter 01" }, new { Duration = TimeSpan.FromSeconds(10), Title = "Chapter 02" },
Artist = "Pachelbel", new { Duration = TimeSpan.FromSeconds(10), Title = "Chapter 03" }, new { Duration = TimeSpan.FromSeconds(10), Title = "Chapter 04" }
Title = "Kanon und Gigue in D-Dur", }
Copyright = "Copyright Lol", };
Composer = "Pachelbel",
Genres = new[] { "Synthwave", "Classics" },
Tracks = new[]
{
new { Duration = TimeSpan.FromSeconds(10), Title = "Chapter 01" },
new { Duration = TimeSpan.FromSeconds(10), Title = "Chapter 02" },
new { Duration = TimeSpan.FromSeconds(10), Title = "Chapter 03" },
new { Duration = TimeSpan.FromSeconds(10), Title = "Chapter 04" },
}
};
var builder = new MetaDataBuilder() var builder = new MetaDataBuilder()
.WithTitle(source.Title) .WithTitle(source.Title)
.WithArtists(source.Artist) .WithArtists(source.Artist)
.WithComposers(source.Composer) .WithComposers(source.Composer)
.WithAlbumArtists(source.Artist) .WithAlbumArtists(source.Artist)
.WithGenres(source.Genres) .WithGenres(source.Genres)
.WithCopyright(source.Copyright) .WithCopyright(source.Copyright)
.AddChapters(source.Tracks, x => (x.Duration, x.Title)); .AddChapters(source.Tracks, x => (x.Duration, x.Title));
var metadata = builder.Build(); var metadata = builder.Build();
var serialized = MetaDataSerializer.Instance.Serialize(metadata); var serialized = MetaDataSerializer.Instance.Serialize(metadata);
Assert.IsTrue(serialized.StartsWith(";FFMETADATA1", StringComparison.OrdinalIgnoreCase)); Assert.IsTrue(serialized.StartsWith(";FFMETADATA1", StringComparison.OrdinalIgnoreCase));
Assert.IsTrue(serialized.Contains("genre=Synthwave; Classics", StringComparison.OrdinalIgnoreCase)); Assert.IsTrue(serialized.Contains("genre=Synthwave; Classics", StringComparison.OrdinalIgnoreCase));
Assert.IsTrue(serialized.Contains("title=Chapter 01", StringComparison.OrdinalIgnoreCase)); Assert.IsTrue(serialized.Contains("title=Chapter 01", StringComparison.OrdinalIgnoreCase));
Assert.IsTrue(serialized.Contains("album_artist=Pachelbel", StringComparison.OrdinalIgnoreCase)); Assert.IsTrue(serialized.Contains("album_artist=Pachelbel", StringComparison.OrdinalIgnoreCase));
} }
[TestMethod] [TestMethod]
public void TestMapMetadata() public void TestMapMetadata()
{ {
//-i "whaterver0" // index: 0 //-i "whaterver0" // index: 0
//-f concat -safe 0 //-f concat -safe 0
//-i "\AppData\Local\Temp\concat_b511f2bf-c4af-4f71-b9bd-24d706bf4861.txt" // index: 1 //-i "\AppData\Local\Temp\concat_b511f2bf-c4af-4f71-b9bd-24d706bf4861.txt" // index: 1
//-i "\AppData\Local\Temp\metadata_210d3259-3d5c-43c8-9786-54b5c414fa70.txt" // index: 2 //-i "\AppData\Local\Temp\metadata_210d3259-3d5c-43c8-9786-54b5c414fa70.txt" // index: 2
//-map_metadata 2 //-map_metadata 2
var text0 = FFMpegArguments.FromFileInput("whaterver0") var text0 = FFMpegArguments.FromFileInput("whaterver0")
.AddMetaData("WhatEver3") .AddMetaData("WhatEver3")
.Text; .Text;
var text1 = FFMpegArguments.FromFileInput("whaterver0") var text1 = FFMpegArguments.FromFileInput("whaterver0")
.AddDemuxConcatInput(new[] { "whaterver", "whaterver1" }) .AddDemuxConcatInput(new[] { "whaterver", "whaterver1" })
.AddMetaData("WhatEver3") .AddMetaData("WhatEver3")
.Text; .Text;
Assert.IsTrue(Regex.IsMatch(text0, "metadata_[0-9a-f-]+\\.txt\" -map_metadata 1"), "map_metadata index is calculated incorrectly."); Assert.IsTrue(Regex.IsMatch(text0, "metadata_[0-9a-f-]+\\.txt\" -map_metadata 1"), "map_metadata index is calculated incorrectly.");
Assert.IsTrue(Regex.IsMatch(text1, "metadata_[0-9a-f-]+\\.txt\" -map_metadata 2"), "map_metadata index is calculated incorrectly."); Assert.IsTrue(Regex.IsMatch(text1, "metadata_[0-9a-f-]+\\.txt\" -map_metadata 2"), "map_metadata index is calculated incorrectly.");
}
} }
} }

View file

@ -1,41 +1,39 @@
using FFMpegCore.Exceptions; using FFMpegCore.Exceptions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace FFMpegCore.Test namespace FFMpegCore.Test;
[TestClass]
public class PixelFormatTests
{ {
[TestClass] [TestMethod]
public class PixelFormatTests public void PixelFormats_Enumerate()
{ {
[TestMethod] var formats = FFMpeg.GetPixelFormats();
public void PixelFormats_Enumerate() Assert.IsNotEmpty(formats);
{ }
var formats = FFMpeg.GetPixelFormats();
Assert.IsTrue(formats.Count > 0);
}
[TestMethod] [TestMethod]
public void PixelFormats_TryGetExisting() public void PixelFormats_TryGetExisting()
{ {
Assert.IsTrue(FFMpeg.TryGetPixelFormat("yuv420p", out _)); Assert.IsTrue(FFMpeg.TryGetPixelFormat("yuv420p", out _));
} }
[TestMethod] [TestMethod]
public void PixelFormats_TryGetNotExisting() public void PixelFormats_TryGetNotExisting()
{ {
Assert.IsFalse(FFMpeg.TryGetPixelFormat("yuv420pppUnknown", out _)); Assert.IsFalse(FFMpeg.TryGetPixelFormat("yuv420pppUnknown", out _));
} }
[TestMethod] [TestMethod]
public void PixelFormats_GetExisting() public void PixelFormats_GetExisting()
{ {
var fmt = FFMpeg.GetPixelFormat("yuv420p"); var fmt = FFMpeg.GetPixelFormat("yuv420p");
Assert.IsTrue(fmt.Components == 3 && fmt.BitsPerPixel == 12); Assert.IsTrue(fmt.Components == 3 && fmt.BitsPerPixel == 12);
} }
[TestMethod] [TestMethod]
public void PixelFormats_GetNotExisting() public void PixelFormats_GetNotExisting()
{ {
Assert.ThrowsException<FFMpegException>(() => FFMpeg.GetPixelFormat("yuv420pppUnknown")); Assert.ThrowsExactly<FFMpegException>(() => FFMpeg.GetPixelFormat("yuv420pppUnknown"));
}
} }
} }

View file

@ -1,22 +1,21 @@
namespace FFMpegCore.Test.Resources namespace FFMpegCore.Test.Resources;
public static class TestResources
{ {
public static class TestResources public static readonly string Mp4Video = "./Resources/input_3sec.mp4";
{ public static readonly string Mp4VideoRotation = "./Resources/input_3sec_rotation_90deg.mp4";
public static readonly string Mp4Video = "./Resources/input_3sec.mp4"; public static readonly string Mp4VideoRotationNegative = "./Resources/input_3sec_rotation_negative_90deg.mp4";
public static readonly string Mp4VideoRotation = "./Resources/input_3sec_rotation_90deg.mp4"; public static readonly string WebmVideo = "./Resources/input_3sec.webm";
public static readonly string Mp4VideoRotationNegative = "./Resources/input_3sec_rotation_negative_90deg.mp4"; public static readonly string HdrVideo = "./Resources/input_hdr.mov";
public static readonly string WebmVideo = "./Resources/input_3sec.webm"; public static readonly string Mp4WithoutVideo = "./Resources/input_audio_only_10sec.mp4";
public static readonly string HdrVideo = "./Resources/input_hdr.mov"; public static readonly string Mp4WithoutAudio = "./Resources/input_video_only_3sec.mp4";
public static readonly string Mp4WithoutVideo = "./Resources/input_audio_only_10sec.mp4"; public static readonly string RawAudio = "./Resources/audio.raw";
public static readonly string Mp4WithoutAudio = "./Resources/input_video_only_3sec.mp4"; public static readonly string Mp3Audio = "./Resources/audio.mp3";
public static readonly string RawAudio = "./Resources/audio.raw"; public static readonly string PngImage = "./Resources/cover.png";
public static readonly string Mp3Audio = "./Resources/audio.mp3"; public static readonly string ImageCollection = "./Resources/images";
public static readonly string PngImage = "./Resources/cover.png"; public static readonly string SrtSubtitle = "./Resources/sample.srt";
public static readonly string ImageCollection = "./Resources/images"; public static readonly string AiffAudio = "./Resources/sample3aiff.aiff";
public static readonly string SrtSubtitle = "./Resources/sample.srt"; public static readonly string MkvVideo = "./Resources/sampleMKV.mkv";
public static readonly string AiffAudio = "./Resources/sample3aiff.aiff"; public static readonly string Wav24Bit = "./Resources/24_bit_fixed.WAV";
public static readonly string MkvVideo = "./Resources/sampleMKV.mkv"; public static readonly string Wav32Bit = "./Resources/32_bit_float.WAV";
public static readonly string Wav24Bit = "./Resources/24_bit_fixed.WAV";
public static readonly string Wav32Bit = "./Resources/32_bit_float.WAV";
}
} }

View file

@ -1,21 +1,24 @@
namespace FFMpegCore.Test namespace FFMpegCore.Test;
public class TemporaryFile : IDisposable
{ {
public class TemporaryFile : IDisposable private readonly string _path;
public TemporaryFile(string filename)
{ {
private readonly string _path; _path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}-{filename}");
}
public TemporaryFile(string filename) public void Dispose()
{
if (File.Exists(_path))
{ {
_path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}-{filename}"); File.Delete(_path);
}
public static implicit operator string(TemporaryFile temporaryFile) => temporaryFile._path;
public void Dispose()
{
if (File.Exists(_path))
{
File.Delete(_path);
}
} }
} }
public static implicit operator string(TemporaryFile temporaryFile)
{
return temporaryFile._path;
}
} }

View file

@ -2,254 +2,251 @@
using System.Drawing.Imaging; using System.Drawing.Imaging;
using System.Numerics; using System.Numerics;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using FFMpegCore.Extensions.System.Drawing.Common;
using FFMpegCore.Pipes; using FFMpegCore.Pipes;
using SkiaSharp; using SkiaSharp;
namespace FFMpegCore.Test.Utilities namespace FFMpegCore.Test.Utilities;
internal static class BitmapSource
{ {
internal static class BitmapSource [SupportedOSPlatform("windows")]
public static IEnumerable<IVideoFrame> CreateBitmaps(int count, PixelFormat fmt, int w, int h)
{ {
[SupportedOSPlatform("windows")] for (var i = 0; i < count; i++)
public static IEnumerable<IVideoFrame> CreateBitmaps(int count, PixelFormat fmt, int w, int h)
{ {
for (var i = 0; i < count; i++) using (var frame = CreateVideoFrame(i, fmt, w, h, 0.025f, 0.025f * w * 0.03f))
{ {
using (var frame = CreateVideoFrame(i, fmt, w, h, 0.025f, 0.025f * w * 0.03f)) yield return frame;
{
yield return frame;
}
} }
} }
}
public static IEnumerable<IVideoFrame> CreateBitmaps(int count, SKColorType fmt, int w, int h)
{ public static IEnumerable<IVideoFrame> CreateBitmaps(int count, SKColorType fmt, int w, int h)
for (var i = 0; i < count; i++) {
{ for (var i = 0; i < count; i++)
using (var frame = CreateVideoFrame(i, fmt, w, h, 0.025f, 0.025f * w * 0.03f)) {
{ using (var frame = CreateVideoFrame(i, fmt, w, h, 0.025f, 0.025f * w * 0.03f))
yield return frame; {
} yield return frame;
} }
} }
}
[SupportedOSPlatform("windows")]
public static Extensions.System.Drawing.Common.BitmapVideoFrameWrapper CreateVideoFrame(int index, PixelFormat fmt, int w, int h, float scaleNoise, float offset) [SupportedOSPlatform("windows")]
{ public static BitmapVideoFrameWrapper CreateVideoFrame(int index, PixelFormat fmt, int w, int h, float scaleNoise, float offset)
var bitmap = new Bitmap(w, h, fmt); {
var bitmap = new Bitmap(w, h, fmt);
foreach (var (x, y, red, green, blue) in GenerateVideoFramePixels(index, w, h, scaleNoise, offset))
{ foreach (var (x, y, red, green, blue) in GenerateVideoFramePixels(index, w, h, scaleNoise, offset))
var color = Color.FromArgb(red, blue, green); {
bitmap.SetPixel(x, y, color); var color = Color.FromArgb(red, blue, green);
} bitmap.SetPixel(x, y, color);
}
return new Extensions.System.Drawing.Common.BitmapVideoFrameWrapper(bitmap);
} return new BitmapVideoFrameWrapper(bitmap);
}
public static Extensions.SkiaSharp.BitmapVideoFrameWrapper CreateVideoFrame(int index, SKColorType fmt, int w, int h, float scaleNoise, float offset)
{ public static Extensions.SkiaSharp.BitmapVideoFrameWrapper CreateVideoFrame(int index, SKColorType fmt, int w, int h, float scaleNoise, float offset)
var bitmap = new SKBitmap(w, h, fmt, SKAlphaType.Opaque); {
var bitmap = new SKBitmap(w, h, fmt, SKAlphaType.Opaque);
bitmap.Pixels = GenerateVideoFramePixels(index, w, h, scaleNoise, offset)
.Select(args => new SKColor(args.red, args.blue, args.green)) bitmap.Pixels = GenerateVideoFramePixels(index, w, h, scaleNoise, offset)
.ToArray(); .Select(args => new SKColor(args.red, args.blue, args.green))
.ToArray();
return new Extensions.SkiaSharp.BitmapVideoFrameWrapper(bitmap);
} return new Extensions.SkiaSharp.BitmapVideoFrameWrapper(bitmap);
}
private static IEnumerable<(int x, int y, byte red, byte green, byte blue)> GenerateVideoFramePixels(int index, int w, int h, float scaleNoise, float offset)
{ private static IEnumerable<(int x, int y, byte red, byte green, byte blue)> GenerateVideoFramePixels(int index, int w, int h, float scaleNoise,
offset = offset * index; float offset)
{
for (var y = 0; y < h; y++) offset = offset * index;
{
for (var x = 0; x < w; x++) for (var y = 0; y < h; y++)
{ {
var xf = x / (float)w; for (var x = 0; x < w; x++)
var yf = y / (float)h; {
var nx = x * scaleNoise + offset; var xf = x / (float)w;
var ny = y * scaleNoise + offset; var yf = y / (float)h;
var nx = x * scaleNoise + offset;
var value = (byte)((Perlin.Noise(nx, ny) + 1.0f) / 2.0f * 255); var ny = y * scaleNoise + offset;
yield return ((x, y, (byte)(value * xf), (byte)(value * yf), value)); var value = (byte)((Perlin.Noise(nx, ny) + 1.0f) / 2.0f * 255);
}
} yield return (x, y, (byte)(value * xf), (byte)(value * yf), value);
} }
}
// }
// Perlin noise generator for Unity
// Keijiro Takahashi, 2013, 2015 //
// https://github.com/keijiro/PerlinNoise // Perlin noise generator for Unity
// // Keijiro Takahashi, 2013, 2015
// Based on the original implementation by Ken Perlin // https://github.com/keijiro/PerlinNoise
// http://mrl.nyu.edu/~perlin/noise/ //
// // Based on the original implementation by Ken Perlin
private static class Perlin // http://mrl.nyu.edu/~perlin/noise/
{ //
#region Noise functions private static class Perlin
{
public static float Noise(float x) #region Noise functions
{
var X = (int)MathF.Floor(x) & 0xff; public static float Noise(float x)
x -= MathF.Floor(x); {
var u = Fade(x); var X = (int)MathF.Floor(x) & 0xff;
return Lerp(u, Grad(perm[X], x), Grad(perm[X + 1], x - 1)) * 2; x -= MathF.Floor(x);
} var u = Fade(x);
return Lerp(u, Grad(perm[X], x), Grad(perm[X + 1], x - 1)) * 2;
public static float Noise(float x, float y) }
{
var X = (int)MathF.Floor(x) & 0xff; public static float Noise(float x, float y)
var Y = (int)MathF.Floor(y) & 0xff; {
x -= MathF.Floor(x); var X = (int)MathF.Floor(x) & 0xff;
y -= MathF.Floor(y); var Y = (int)MathF.Floor(y) & 0xff;
var u = Fade(x); x -= MathF.Floor(x);
var v = Fade(y); y -= MathF.Floor(y);
var A = (perm[X] + Y) & 0xff; var u = Fade(x);
var B = (perm[X + 1] + Y) & 0xff; var v = Fade(y);
return Lerp(v, Lerp(u, Grad(perm[A], x, y), Grad(perm[B], x - 1, y)), var A = (perm[X] + Y) & 0xff;
Lerp(u, Grad(perm[A + 1], x, y - 1), Grad(perm[B + 1], x - 1, y - 1))); var B = (perm[X + 1] + Y) & 0xff;
} return Lerp(v, Lerp(u, Grad(perm[A], x, y), Grad(perm[B], x - 1, y)),
Lerp(u, Grad(perm[A + 1], x, y - 1), Grad(perm[B + 1], x - 1, y - 1)));
public static float Noise(Vector2 coord) }
{
return Noise(coord.X, coord.Y); public static float Noise(Vector2 coord)
} {
return Noise(coord.X, coord.Y);
public static float Noise(float x, float y, float z) }
{
var X = (int)MathF.Floor(x) & 0xff; public static float Noise(float x, float y, float z)
var Y = (int)MathF.Floor(y) & 0xff; {
var Z = (int)MathF.Floor(z) & 0xff; var X = (int)MathF.Floor(x) & 0xff;
x -= MathF.Floor(x); var Y = (int)MathF.Floor(y) & 0xff;
y -= MathF.Floor(y); var Z = (int)MathF.Floor(z) & 0xff;
z -= MathF.Floor(z); x -= MathF.Floor(x);
var u = Fade(x); y -= MathF.Floor(y);
var v = Fade(y); z -= MathF.Floor(z);
var w = Fade(z); var u = Fade(x);
var A = (perm[X] + Y) & 0xff; var v = Fade(y);
var B = (perm[X + 1] + Y) & 0xff; var w = Fade(z);
var AA = (perm[A] + Z) & 0xff; var A = (perm[X] + Y) & 0xff;
var BA = (perm[B] + Z) & 0xff; var B = (perm[X + 1] + Y) & 0xff;
var AB = (perm[A + 1] + Z) & 0xff; var AA = (perm[A] + Z) & 0xff;
var BB = (perm[B + 1] + Z) & 0xff; var BA = (perm[B] + Z) & 0xff;
return Lerp(w, Lerp(v, Lerp(u, Grad(perm[AA], x, y, z), Grad(perm[BA], x - 1, y, z)), var AB = (perm[A + 1] + Z) & 0xff;
Lerp(u, Grad(perm[AB], x, y - 1, z), Grad(perm[BB], x - 1, y - 1, z))), var BB = (perm[B + 1] + Z) & 0xff;
Lerp(v, Lerp(u, Grad(perm[AA + 1], x, y, z - 1), Grad(perm[BA + 1], x - 1, y, z - 1)), return Lerp(w, Lerp(v, Lerp(u, Grad(perm[AA], x, y, z), Grad(perm[BA], x - 1, y, z)),
Lerp(u, Grad(perm[AB + 1], x, y - 1, z - 1), Grad(perm[BB + 1], x - 1, y - 1, z - 1)))); Lerp(u, Grad(perm[AB], x, y - 1, z), Grad(perm[BB], x - 1, y - 1, z))),
} Lerp(v, Lerp(u, Grad(perm[AA + 1], x, y, z - 1), Grad(perm[BA + 1], x - 1, y, z - 1)),
Lerp(u, Grad(perm[AB + 1], x, y - 1, z - 1), Grad(perm[BB + 1], x - 1, y - 1, z - 1))));
public static float Noise(Vector3 coord) }
{
return Noise(coord.X, coord.Y, coord.Z); public static float Noise(Vector3 coord)
} {
return Noise(coord.X, coord.Y, coord.Z);
#endregion }
#region fBm functions #endregion
public static float Fbm(float x, int octave) #region fBm functions
{
var f = 0.0f; public static float Fbm(float x, int octave)
var w = 0.5f; {
for (var i = 0; i < octave; i++) var f = 0.0f;
{ var w = 0.5f;
f += w * Noise(x); for (var i = 0; i < octave; i++)
x *= 2.0f; {
w *= 0.5f; f += w * Noise(x);
} x *= 2.0f;
w *= 0.5f;
return f; }
}
return f;
public static float Fbm(Vector2 coord, int octave) }
{
var f = 0.0f; public static float Fbm(Vector2 coord, int octave)
var w = 0.5f; {
for (var i = 0; i < octave; i++) var f = 0.0f;
{ var w = 0.5f;
f += w * Noise(coord); for (var i = 0; i < octave; i++)
coord *= 2.0f; {
w *= 0.5f; f += w * Noise(coord);
} coord *= 2.0f;
w *= 0.5f;
return f; }
}
return f;
public static float Fbm(float x, float y, int octave) }
{
return Fbm(new Vector2(x, y), octave); public static float Fbm(float x, float y, int octave)
} {
return Fbm(new Vector2(x, y), octave);
public static float Fbm(Vector3 coord, int octave) }
{
var f = 0.0f; public static float Fbm(Vector3 coord, int octave)
var w = 0.5f; {
for (var i = 0; i < octave; i++) var f = 0.0f;
{ var w = 0.5f;
f += w * Noise(coord); for (var i = 0; i < octave; i++)
coord *= 2.0f; {
w *= 0.5f; f += w * Noise(coord);
} coord *= 2.0f;
w *= 0.5f;
return f; }
}
return f;
public static float Fbm(float x, float y, float z, int octave) }
{
return Fbm(new Vector3(x, y, z), octave); public static float Fbm(float x, float y, float z, int octave)
} {
return Fbm(new Vector3(x, y, z), octave);
#endregion }
#region Private functions #endregion
private static float Fade(float t) #region Private functions
{
return t * t * t * (t * (t * 6 - 15) + 10); private static float Fade(float t)
} {
return t * t * t * (t * (t * 6 - 15) + 10);
private static float Lerp(float t, float a, float b) }
{
return a + t * (b - a); private static float Lerp(float t, float a, float b)
} {
return a + t * (b - a);
private static float Grad(int hash, float x) }
{
return (hash & 1) == 0 ? x : -x; private static float Grad(int hash, float x)
} {
return (hash & 1) == 0 ? x : -x;
private static float Grad(int hash, float x, float y) }
{
return ((hash & 1) == 0 ? x : -x) + ((hash & 2) == 0 ? y : -y); private static float Grad(int hash, float x, float y)
} {
return ((hash & 1) == 0 ? x : -x) + ((hash & 2) == 0 ? y : -y);
private static float Grad(int hash, float x, float y, float z) }
{
var h = hash & 15; private static float Grad(int hash, float x, float y, float z)
var u = h < 8 ? x : y; {
var v = h < 4 ? y : (h == 12 || h == 14 ? x : z); var h = hash & 15;
return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v); var u = h < 8 ? x : y;
} var v = h < 4 ? y : h == 12 || h == 14 ? x : z;
return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v);
private static readonly int[] perm = { }
151,160,137,91,90,15,
131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23, private static readonly int[] perm =
190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33, {
88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166, 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247,
77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74,
102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54,
135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3,
5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183,
223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178,
129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214,
251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195,
49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254, 78, 66, 215, 61, 156, 180, 151
138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180, };
151
}; #endregion
#endregion
}
} }
} }

View file

@ -0,0 +1,42 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using FFMpegCore.Extensions.Downloader.Extensions;
namespace FFMpegCore.Test.Utilities;
[Flags]
internal enum OsPlatforms : ushort
{
Windows = 1,
Linux = 2,
MacOS = 4
}
internal class OsSpecificTestMethod : TestMethodAttribute
{
private readonly IEnumerable<OSPlatform> _supportedOsPlatforms;
public OsSpecificTestMethod(OsPlatforms supportedOsPlatforms, [CallerFilePath] string callerFilePath = "",
[CallerLineNumber] int callerLineNumber = -1) : base(callerFilePath, callerLineNumber)
{
_supportedOsPlatforms = supportedOsPlatforms.GetFlags()
.Select(flag => OSPlatform.Create(flag.ToString().ToUpperInvariant()))
.ToArray();
}
public override async Task<TestResult[]> ExecuteAsync(ITestMethod testMethod)
{
if (_supportedOsPlatforms.Any(RuntimeInformation.IsOSPlatform))
{
return await base.ExecuteAsync(testMethod);
}
var message = $"Test only executed on specific platforms: {string.Join(", ", _supportedOsPlatforms.Select(platform => platform.ToString()))}";
{
return
[
new TestResult { Outcome = UnitTestOutcome.Inconclusive, TestFailureException = new AssertInconclusiveException(message) }
];
}
}
}

View file

@ -1,23 +0,0 @@
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace FFMpegCore.Test.Utilities;
public class WindowsOnlyDataTestMethod : DataTestMethodAttribute
{
public override TestResult[] Execute(ITestMethod testMethod)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var message = $"Test not executed on other platforms than Windows";
{
return new[]
{
new TestResult { Outcome = UnitTestOutcome.Inconclusive, TestFailureException = new AssertInconclusiveException(message) }
};
}
}
return base.Execute(testMethod);
}
}

View file

@ -1,23 +0,0 @@
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace FFMpegCore.Test.Utilities;
public class WindowsOnlyTestMethod : TestMethodAttribute
{
public override TestResult[] Execute(ITestMethod testMethod)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var message = $"Test not executed on other platforms than Windows";
{
return new[]
{
new TestResult { Outcome = UnitTestOutcome.Inconclusive, TestFailureException = new AssertInconclusiveException(message) }
};
}
}
return base.Execute(testMethod);
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,3 @@
{ {
"RootDirectory": "" "BinaryFolder": ""
} }

View file

@ -1,7 +1,7 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16 # Visual Studio Version 17
VisualStudioVersion = 16.0.31005.135 VisualStudioVersion = 17.7.34003.232
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore", "FFMpegCore\FFMpegCore.csproj", "{19DE2EC2-9955-4712-8096-C22EF6713E4F}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore", "FFMpegCore\FFMpegCore.csproj", "{19DE2EC2-9955-4712-8096-C22EF6713E4F}"
EndProject EndProject
@ -13,6 +13,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore.Extensions.Syste
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore.Extensions.SkiaSharp", "FFMpegCore.Extensions.SkiaSharp\FFMpegCore.Extensions.SkiaSharp.csproj", "{5A76F9B7-3681-4551-A9B6-8D3AC5DA1090}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore.Extensions.SkiaSharp", "FFMpegCore.Extensions.SkiaSharp\FFMpegCore.Extensions.SkiaSharp.csproj", "{5A76F9B7-3681-4551-A9B6-8D3AC5DA1090}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FFMpegCore.Extensions.Downloader", "FFMpegCore.Extensions.Downloader\FFMpegCore.Extensions.Downloader.csproj", "{5FA30158-CAB0-44FD-AD98-C31F5E3D5A56}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -39,6 +41,10 @@ Global
{5A76F9B7-3681-4551-A9B6-8D3AC5DA1090}.Debug|Any CPU.Build.0 = Debug|Any CPU {5A76F9B7-3681-4551-A9B6-8D3AC5DA1090}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5A76F9B7-3681-4551-A9B6-8D3AC5DA1090}.Release|Any CPU.ActiveCfg = Release|Any CPU {5A76F9B7-3681-4551-A9B6-8D3AC5DA1090}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5A76F9B7-3681-4551-A9B6-8D3AC5DA1090}.Release|Any CPU.Build.0 = Release|Any CPU {5A76F9B7-3681-4551-A9B6-8D3AC5DA1090}.Release|Any CPU.Build.0 = Release|Any CPU
{5FA30158-CAB0-44FD-AD98-C31F5E3D5A56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5FA30158-CAB0-44FD-AD98-C31F5E3D5A56}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5FA30158-CAB0-44FD-AD98-C31F5E3D5A56}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5FA30158-CAB0-44FD-AD98-C31F5E3D5A56}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View file

@ -1,22 +1,21 @@
namespace FFMpegCore.Extend namespace FFMpegCore.Extend;
{
internal static class KeyValuePairExtensions
{
/// <summary>
/// Concat the two members of a <see cref="KeyValuePair{TKey,TValue}" />
/// </summary>
/// <param name="pair">Input object</param>
/// <param name="enclose">
/// If true encloses the value part between quotes if contains an space character. If false use the
/// value unmodified
/// </param>
/// <returns>The formatted string</returns>
public static string FormatArgumentPair(this KeyValuePair<string, string> pair, bool enclose)
{
var key = pair.Key;
var value = enclose ? StringExtensions.EncloseIfContainsSpace(pair.Value) : pair.Value;
return $"{key}={value}"; internal static class KeyValuePairExtensions
} {
/// <summary>
/// Concat the two members of a <see cref="KeyValuePair{TKey,TValue}" />
/// </summary>
/// <param name="pair">Input object</param>
/// <param name="enclose">
/// If true encloses the value part between quotes if contains an space character. If false use the
/// value unmodified
/// </param>
/// <returns>The formatted string</returns>
public static string FormatArgumentPair(this KeyValuePair<string, string> pair, bool enclose)
{
var key = pair.Key;
var value = enclose ? StringExtensions.EncloseIfContainsSpace(pair.Value) : pair.Value;
return $"{key}={value}";
} }
} }

View file

@ -1,27 +1,26 @@
using FFMpegCore.Pipes; using FFMpegCore.Pipes;
namespace FFMpegCore.Extend namespace FFMpegCore.Extend;
public class PcmAudioSampleWrapper : IAudioSample
{ {
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)
{ {
//This could actually be short or int, but copies would be inefficient. _sample = sample;
//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) public void Serialize(Stream stream)
{ {
_sample = sample; stream.Write(_sample, 0, _sample.Length);
} }
public void Serialize(Stream stream) public async Task SerializeAsync(Stream stream, CancellationToken token)
{ {
stream.Write(_sample, 0, _sample.Length); await stream.WriteAsync(_sample, 0, _sample.Length, token).ConfigureAwait(false);
}
public async Task SerializeAsync(Stream stream, CancellationToken token)
{
await stream.WriteAsync(_sample, 0, _sample.Length, token).ConfigureAwait(false);
}
} }
} }

View file

@ -1,69 +1,68 @@
using System.Text; using System.Text;
namespace FFMpegCore.Extend namespace FFMpegCore.Extend;
internal static class StringExtensions
{ {
internal static class StringExtensions private static Dictionary<char, string> CharactersSubstitution { get; } = new()
{ {
private static Dictionary<char, string> CharactersSubstitution { get; } = new() { '\\', @"\\" },
{ { ':', @"\:" },
{ '\\', @"\\" }, { '[', @"\[" },
{ ':', @"\:" }, { ']', @"\]" },
{ '[', @"\[" }, { '\'', @"'\\\''" }
{ ']', @"\]" }, };
{ '\'', @"'\\\''" }
};
/// <summary> /// <summary>
/// Enclose string between quotes if contains an space character /// Enclose string between quotes if contains an space character
/// </summary> /// </summary>
/// <param name="input">The input</param> /// <param name="input">The input</param>
/// <returns>The enclosed string</returns> /// <returns>The enclosed string</returns>
public static string EncloseIfContainsSpace(string input) public static string EncloseIfContainsSpace(string input)
{ {
return input.Contains(" ") ? $"'{input}'" : input; return input.Contains(" ") ? $"'{input}'" : input;
} }
/// <summary> /// <summary>
/// Enclose an string in quotes /// Enclose an string in quotes
/// </summary> /// </summary>
/// <param name="input"></param> /// <param name="input"></param>
/// <returns></returns> /// <returns></returns>
public static string EncloseInQuotes(string input) public static string EncloseInQuotes(string input)
{ {
return $"'{input}'"; return $"'{input}'";
} }
/// <summary> /// <summary>
/// Scape several characters in subtitle path used by FFmpeg /// Scape several characters in subtitle path used by FFmpeg
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This is needed because internally FFmpeg use Libav Filters /// This is needed because internally FFmpeg use Libav Filters
/// and the info send to it must be in an specific format /// and the info send to it must be in an specific format
/// </remarks> /// </remarks>
/// <param name="source"></param> /// <param name="source"></param>
/// <returns>Scaped path</returns> /// <returns>Scaped path</returns>
public static string ToFFmpegLibavfilterPath(string source) public static string ToFFmpegLibavfilterPath(string source)
{ {
return source.Replace(CharactersSubstitution); return source.Replace(CharactersSubstitution);
} }
public static string Replace(this string str, Dictionary<char, string> replaceList) public static string Replace(this string str, Dictionary<char, string> replaceList)
{ {
var parsedString = new StringBuilder(); var parsedString = new StringBuilder();
foreach (var l in str) foreach (var l in str)
{
if (replaceList.ContainsKey(l))
{ {
if (replaceList.ContainsKey(l)) parsedString.Append(replaceList[l]);
{ }
parsedString.Append(replaceList[l]); else
} {
else parsedString.Append(l);
{
parsedString.Append(l);
}
} }
return parsedString.ToString();
} }
return parsedString.ToString();
} }
} }

View file

@ -1,10 +1,9 @@
namespace FFMpegCore.Extend namespace FFMpegCore.Extend;
public static class UriExtensions
{ {
public static class UriExtensions public static bool SaveStream(this Uri uri, string output)
{ {
public static bool SaveStream(this Uri uri, string output) return FFMpeg.SaveM3U8Stream(uri, output);
{
return FFMpeg.SaveM3U8Stream(uri, output);
}
} }
} }

View file

@ -1,27 +1,26 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
public class AudibleEncryptionKeyArgument : IArgument
{ {
public class AudibleEncryptionKeyArgument : IArgument private readonly bool _aaxcMode;
private readonly string? _activationBytes;
private readonly string? _iv;
private readonly string? _key;
public AudibleEncryptionKeyArgument(string activationBytes)
{ {
private readonly bool _aaxcMode; _activationBytes = activationBytes;
private readonly string? _key;
private readonly string? _iv;
private readonly string? _activationBytes;
public AudibleEncryptionKeyArgument(string activationBytes)
{
_activationBytes = activationBytes;
}
public AudibleEncryptionKeyArgument(string key, string iv)
{
_aaxcMode = true;
_key = key;
_iv = iv;
}
public string Text => _aaxcMode ? $"-audible_key {_key} -audible_iv {_iv}" : $"-activation_bytes {_activationBytes}";
} }
public AudibleEncryptionKeyArgument(string key, string iv)
{
_aaxcMode = true;
_key = key;
_iv = iv;
}
public string Text => _aaxcMode ? $"-audible_key {_key} -audible_iv {_iv}" : $"-activation_bytes {_activationBytes}";
} }

View file

@ -1,19 +1,19 @@
using FFMpegCore.Enums; using FFMpegCore.Enums;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
{
/// <summary>
/// Represents parameter of audio codec and it's quality
/// </summary>
public class AudioBitrateArgument : IArgument
{
public readonly int Bitrate;
public AudioBitrateArgument(AudioQuality value) : this((int)value) { }
public AudioBitrateArgument(int bitrate)
{
Bitrate = bitrate;
}
public string Text => $"-b:a {Bitrate}k"; /// <summary>
/// Represents parameter of audio codec and it's quality
/// </summary>
public class AudioBitrateArgument : IArgument
{
public readonly int Bitrate;
public AudioBitrateArgument(AudioQuality value) : this((int)value) { }
public AudioBitrateArgument(int bitrate)
{
Bitrate = bitrate;
} }
public string Text => $"-b:a {Bitrate}k";
} }

View file

@ -1,30 +1,29 @@
using FFMpegCore.Enums; using FFMpegCore.Enums;
using FFMpegCore.Exceptions; using FFMpegCore.Exceptions;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
/// <summary>
/// Represents parameter of audio codec and it's quality
/// </summary>
public class AudioCodecArgument : IArgument
{ {
/// <summary> public readonly string AudioCodec;
/// Represents parameter of audio codec and it's quality
/// </summary> public AudioCodecArgument(Codec audioCodec)
public class AudioCodecArgument : IArgument
{ {
public readonly string AudioCodec; if (audioCodec.Type != CodecType.Audio)
public AudioCodecArgument(Codec audioCodec)
{ {
if (audioCodec.Type != CodecType.Audio) throw new FFMpegException(FFMpegExceptionType.Operation, $"Codec \"{audioCodec.Name}\" is not an audio codec");
{
throw new FFMpegException(FFMpegExceptionType.Operation, $"Codec \"{audioCodec.Name}\" is not an audio codec");
}
AudioCodec = audioCodec.Name;
} }
public AudioCodecArgument(string audioCodec) AudioCodec = audioCodec.Name;
{
AudioCodec = audioCodec;
}
public string Text => $"-c:a {AudioCodec.ToString().ToLowerInvariant()}";
} }
public AudioCodecArgument(string audioCodec)
{
AudioCodec = audioCodec;
}
public string Text => $"-c:a {AudioCodec.ToLowerInvariant()}";
} }

View file

@ -1,71 +1,97 @@
using FFMpegCore.Exceptions; using FFMpegCore.Exceptions;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
public class AudioFiltersArgument : IArgument
{ {
public class AudioFiltersArgument : IArgument public readonly AudioFilterOptions Options;
{
public readonly AudioFilterOptions Options;
public AudioFiltersArgument(AudioFilterOptions options) public AudioFiltersArgument(AudioFilterOptions options)
{
Options = options;
}
public string Text => GetText();
private string GetText()
{
if (!Options.Arguments.Any())
{ {
Options = options; throw new FFMpegArgumentException("No audio-filter arguments provided");
} }
public string Text => GetText(); var arguments = Options.Arguments
.Where(arg => !string.IsNullOrEmpty(arg.Value))
private string GetText() .Select(arg =>
{
if (!Options.Arguments.Any())
{ {
throw new FFMpegArgumentException("No audio-filter arguments provided"); var escapedValue = arg.Value.Replace(",", "\\,");
} return string.IsNullOrEmpty(arg.Key) ? escapedValue : $"{arg.Key}={escapedValue}";
});
var arguments = Options.Arguments return $"-af \"{string.Join(", ", arguments)}\"";
.Where(arg => !string.IsNullOrEmpty(arg.Value)) }
.Select(arg => }
{
var escapedValue = arg.Value.Replace(",", "\\,"); public interface IAudioFilterArgument
return string.IsNullOrEmpty(arg.Key) ? escapedValue : $"{arg.Key}={escapedValue}"; {
}); string Key { get; }
string Value { get; }
return $"-af \"{string.Join(", ", arguments)}\""; }
}
} public class AudioFilterOptions
{
public interface IAudioFilterArgument public List<IAudioFilterArgument> Arguments { get; } = new();
{
string Key { get; } public AudioFilterOptions Pan(string channelLayout, params string[] outputDefinitions)
string Value { get; } {
} return WithArgument(new PanArgument(channelLayout, outputDefinitions));
}
public class AudioFilterOptions
{ public AudioFilterOptions Pan(int channels, params string[] outputDefinitions)
public List<IAudioFilterArgument> Arguments { get; } = new(); {
return WithArgument(new PanArgument(channels, outputDefinitions));
public AudioFilterOptions Pan(string channelLayout, params string[] outputDefinitions) => WithArgument(new PanArgument(channelLayout, outputDefinitions)); }
public AudioFilterOptions Pan(int channels, params string[] outputDefinitions) => WithArgument(new PanArgument(channels, outputDefinitions));
public AudioFilterOptions DynamicNormalizer(int frameLength = 500, int filterWindow = 31, double targetPeak = 0.95, public AudioFilterOptions DynamicNormalizer(int frameLength = 500, int filterWindow = 31, double targetPeak = 0.95,
double gainFactor = 10.0, double targetRms = 0.0, bool channelCoupling = true, double gainFactor = 10.0, double targetRms = 0.0, bool channelCoupling = true,
bool enableDcBiasCorrection = false, bool enableAlternativeBoundary = false, bool enableDcBiasCorrection = false, bool enableAlternativeBoundary = false,
double compressorFactor = 0.0) => WithArgument(new DynamicNormalizerArgument(frameLength, filterWindow, double compressorFactor = 0.0)
targetPeak, gainFactor, targetRms, channelCoupling, enableDcBiasCorrection, enableAlternativeBoundary, {
compressorFactor)); return WithArgument(new DynamicNormalizerArgument(frameLength, filterWindow,
public AudioFilterOptions HighPass(double frequency = 3000, int poles = 2, string width_type = "q", double width = 0.707, targetPeak, gainFactor, targetRms, channelCoupling, enableDcBiasCorrection, enableAlternativeBoundary,
double mix = 1, string channels = "", bool normalize = false, string transform = "", string precision = "auto", compressorFactor));
int? blocksize = null) => WithArgument(new HighPassFilterArgument(frequency, poles, width_type, width, mix, channels, normalize, transform, precision, blocksize)); }
public AudioFilterOptions LowPass(double frequency = 3000, int poles = 2, string width_type = "q", double width = 0.707,
double mix = 1, string channels = "", bool normalize = false, string transform = "", string precision = "auto", public AudioFilterOptions HighPass(double frequency = 3000, int poles = 2, string width_type = "q", double width = 0.707,
int? blocksize = null) => WithArgument(new LowPassFilterArgument(frequency, poles, width_type, width, mix, channels, normalize, transform, precision, blocksize)); double mix = 1, string channels = "", bool normalize = false, string transform = "", string precision = "auto",
public AudioFilterOptions AudioGate(double level_in = 1, string mode = "downward", double range = 0.06125, double threshold = 0.125, int? blocksize = null)
int ratio = 2, double attack = 20, double release = 250, int makeup = 1, double knee = 2.828427125, string detection = "rms", {
string link = "average") => WithArgument(new AudioGateArgument(level_in, mode, range, threshold, ratio, attack, release, makeup, knee, detection, link)); return WithArgument(new HighPassFilterArgument(frequency, poles, width_type, width, mix, channels, normalize, transform, precision, blocksize));
public AudioFilterOptions SilenceDetect(string noise_type = "db", double noise = 60, double duration = 2, }
bool mono = false) => WithArgument(new SilenceDetectArgument(noise_type, noise, duration, mono));
public AudioFilterOptions LowPass(double frequency = 3000, int poles = 2, string width_type = "q", double width = 0.707,
private AudioFilterOptions WithArgument(IAudioFilterArgument argument) double mix = 1, string channels = "", bool normalize = false, string transform = "", string precision = "auto",
{ int? blocksize = null)
Arguments.Add(argument); {
return this; return WithArgument(new LowPassFilterArgument(frequency, poles, width_type, width, mix, channels, normalize, transform, precision, blocksize));
} }
public AudioFilterOptions AudioGate(double level_in = 1, string mode = "downward", double range = 0.06125, double threshold = 0.125,
int ratio = 2, double attack = 20, double release = 250, int makeup = 1, double knee = 2.828427125, string detection = "rms",
string link = "average")
{
return WithArgument(new AudioGateArgument(level_in, mode, range, threshold, ratio, attack, release, makeup, knee, detection, link));
}
public AudioFilterOptions SilenceDetect(string noise_type = "db", double noise = 60, double duration = 2,
bool mono = false)
{
return WithArgument(new SilenceDetectArgument(noise_type, noise, duration, mono));
}
private AudioFilterOptions WithArgument(IAudioFilterArgument argument)
{
Arguments.Add(argument);
return this;
} }
} }

View file

@ -1,98 +1,115 @@
using System.Globalization; using System.Globalization;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
public class AudioGateArgument : IAudioFilterArgument
{ {
public class AudioGateArgument : IAudioFilterArgument private readonly Dictionary<string, string> _arguments = new();
/// <summary>
/// Audio Gate. <see href="https://ffmpeg.org/ffmpeg-filters.html#agate" />
/// </summary>
/// <param name="levelIn">Set input level before filtering. Default is 1. Allowed range is from 0.015625 to 64.</param>
/// <param name="mode">
/// Set the mode of operation. Can be upward or downward. Default is downward. If set to upward mode, higher parts of signal
/// will be amplified, expanding dynamic range in upward direction. Otherwise, in case of downward lower parts of signal will be reduced.
/// </param>
/// <param name="range">
/// Set the level of gain reduction when the signal is below the threshold. Default is 0.06125. Allowed range is from 0 to
/// 1. Setting this to 0 disables reduction and then filter behaves like expander.
/// </param>
/// <param name="threshold">If a signal rises above this level the gain reduction is released. Default is 0.125. Allowed range is from 0 to 1.</param>
/// <param name="ratio">Set a ratio by which the signal is reduced. Default is 2. Allowed range is from 1 to 9000.</param>
/// <param name="attack">
/// Amount of milliseconds the signal has to rise above the threshold before gain reduction stops. Default is 20
/// milliseconds. Allowed range is from 0.01 to 9000.
/// </param>
/// <param name="release">
/// Amount of milliseconds the signal has to fall below the threshold before the reduction is increased again. Default is
/// 250 milliseconds. Allowed range is from 0.01 to 9000.
/// </param>
/// <param name="makeup">Set amount of amplification of signal after processing. Default is 1. Allowed range is from 1 to 64.</param>
/// <param name="knee">
/// Curve the sharp knee around the threshold to enter gain reduction more softly. Default is 2.828427125. Allowed range is
/// from 1 to 8.
/// </param>
/// <param name="detection">Choose if exact signal should be taken for detection or an RMS like one. Default is rms. Can be peak or rms.</param>
/// <param name="link">
/// Choose if the average level between all channels or the louder channel affects the reduction. Default is average. Can be
/// average or maximum.
/// </param>
public AudioGateArgument(double levelIn = 1, string mode = "downward", double range = 0.06125, double threshold = 0.125, int ratio = 2,
double attack = 20, double release = 250, int makeup = 1, double knee = 2.828427125, string detection = "rms", string link = "average")
{ {
private readonly Dictionary<string, string> _arguments = new(); if (levelIn is < 0.015625 or > 64)
/// <summary>
/// Audio Gate. <see href="https://ffmpeg.org/ffmpeg-filters.html#agate"/>
/// </summary>
/// <param name="levelIn">Set input level before filtering. Default is 1. Allowed range is from 0.015625 to 64.</param>
/// <param name="mode">Set the mode of operation. Can be upward or downward. Default is downward. If set to upward mode, higher parts of signal will be amplified, expanding dynamic range in upward direction. Otherwise, in case of downward lower parts of signal will be reduced.</param>
/// <param name="range">Set the level of gain reduction when the signal is below the threshold. Default is 0.06125. Allowed range is from 0 to 1. Setting this to 0 disables reduction and then filter behaves like expander.</param>
/// <param name="threshold">If a signal rises above this level the gain reduction is released. Default is 0.125. Allowed range is from 0 to 1.</param>
/// <param name="ratio">Set a ratio by which the signal is reduced. Default is 2. Allowed range is from 1 to 9000.</param>
/// <param name="attack">Amount of milliseconds the signal has to rise above the threshold before gain reduction stops. Default is 20 milliseconds. Allowed range is from 0.01 to 9000.</param>
/// <param name="release">Amount of milliseconds the signal has to fall below the threshold before the reduction is increased again. Default is 250 milliseconds. Allowed range is from 0.01 to 9000.</param>
/// <param name="makeup">Set amount of amplification of signal after processing. Default is 1. Allowed range is from 1 to 64.</param>
/// <param name="knee">Curve the sharp knee around the threshold to enter gain reduction more softly. Default is 2.828427125. Allowed range is from 1 to 8.</param>
/// <param name="detection">Choose if exact signal should be taken for detection or an RMS like one. Default is rms. Can be peak or rms.</param>
/// <param name="link">Choose if the average level between all channels or the louder channel affects the reduction. Default is average. Can be average or maximum.</param>
public AudioGateArgument(double levelIn = 1, string mode = "downward", double range = 0.06125, double threshold = 0.125, int ratio = 2,
double attack = 20, double release = 250, int makeup = 1, double knee = 2.828427125, string detection = "rms", string link = "average")
{ {
if (levelIn is < 0.015625 or > 64) throw new ArgumentOutOfRangeException(nameof(levelIn), "Level in must be between 0.015625 to 64");
{
throw new ArgumentOutOfRangeException(nameof(levelIn), "Level in must be between 0.015625 to 64");
}
if (mode != "upward" && mode != "downward")
{
throw new ArgumentOutOfRangeException(nameof(mode), "Mode must be either upward or downward");
}
if (range is <= 0 or > 1)
{
throw new ArgumentOutOfRangeException(nameof(range));
}
if (threshold is < 0 or > 1)
{
throw new ArgumentOutOfRangeException(nameof(threshold), "Threshold must be between 0 and 1");
}
if (ratio is < 1 or > 9000)
{
throw new ArgumentOutOfRangeException(nameof(ratio), "Ratio must be between 1 and 9000");
}
if (attack is < 0.01 or > 9000)
{
throw new ArgumentOutOfRangeException(nameof(attack), "Attack must be between 0.01 and 9000");
}
if (release is < 0.01 or > 9000)
{
throw new ArgumentOutOfRangeException(nameof(release), "Release must be between 0.01 and 9000");
}
if (makeup is < 1 or > 64)
{
throw new ArgumentOutOfRangeException(nameof(makeup), "Makeup Gain must be between 1 and 64");
}
if (knee is < 1 or > 64)
{
throw new ArgumentOutOfRangeException(nameof(makeup), "Knee must be between 1 and 8");
}
if (detection != "peak" && detection != "rms")
{
throw new ArgumentOutOfRangeException(nameof(detection), "Detection must be either peak or rms");
}
if (link != "average" && link != "maximum")
{
throw new ArgumentOutOfRangeException(nameof(link), "Link must be either average or maximum");
}
_arguments.Add("level_in", levelIn.ToString("0.00", CultureInfo.InvariantCulture));
_arguments.Add("mode", mode);
_arguments.Add("range", range.ToString("0.00", CultureInfo.InvariantCulture));
_arguments.Add("threshold", threshold.ToString("0.00", CultureInfo.InvariantCulture));
_arguments.Add("ratio", ratio.ToString());
_arguments.Add("attack", attack.ToString("0.00", CultureInfo.InvariantCulture));
_arguments.Add("release", release.ToString("0.00", CultureInfo.InvariantCulture));
_arguments.Add("makeup", makeup.ToString());
_arguments.Add("knee", knee.ToString("0.00", CultureInfo.InvariantCulture));
_arguments.Add("detection", detection);
_arguments.Add("link", link);
} }
public string Key { get; } = "agate"; if (mode != "upward" && mode != "downward")
{
throw new ArgumentOutOfRangeException(nameof(mode), "Mode must be either upward or downward");
}
public string Value => string.Join(":", _arguments.Select(pair => $"{pair.Key}={pair.Value}")); if (range is <= 0 or > 1)
{
throw new ArgumentOutOfRangeException(nameof(range));
}
if (threshold is < 0 or > 1)
{
throw new ArgumentOutOfRangeException(nameof(threshold), "Threshold must be between 0 and 1");
}
if (ratio is < 1 or > 9000)
{
throw new ArgumentOutOfRangeException(nameof(ratio), "Ratio must be between 1 and 9000");
}
if (attack is < 0.01 or > 9000)
{
throw new ArgumentOutOfRangeException(nameof(attack), "Attack must be between 0.01 and 9000");
}
if (release is < 0.01 or > 9000)
{
throw new ArgumentOutOfRangeException(nameof(release), "Release must be between 0.01 and 9000");
}
if (makeup is < 1 or > 64)
{
throw new ArgumentOutOfRangeException(nameof(makeup), "Makeup Gain must be between 1 and 64");
}
if (knee is < 1 or > 64)
{
throw new ArgumentOutOfRangeException(nameof(makeup), "Knee must be between 1 and 8");
}
if (detection != "peak" && detection != "rms")
{
throw new ArgumentOutOfRangeException(nameof(detection), "Detection must be either peak or rms");
}
if (link != "average" && link != "maximum")
{
throw new ArgumentOutOfRangeException(nameof(link), "Link must be either average or maximum");
}
_arguments.Add("level_in", levelIn.ToString("0.00", CultureInfo.InvariantCulture));
_arguments.Add("mode", mode);
_arguments.Add("range", range.ToString("0.00", CultureInfo.InvariantCulture));
_arguments.Add("threshold", threshold.ToString("0.00", CultureInfo.InvariantCulture));
_arguments.Add("ratio", ratio.ToString());
_arguments.Add("attack", attack.ToString("0.00", CultureInfo.InvariantCulture));
_arguments.Add("release", release.ToString("0.00", CultureInfo.InvariantCulture));
_arguments.Add("makeup", makeup.ToString());
_arguments.Add("knee", knee.ToString("0.00", CultureInfo.InvariantCulture));
_arguments.Add("detection", detection);
_arguments.Add("link", link);
} }
public string Key { get; } = "agate";
public string Value => string.Join(":", _arguments.Select(pair => $"{pair.Key}={pair.Value}"));
} }

View file

@ -1,16 +1,16 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
{
/// <summary>
/// Audio sampling rate argument. Defaults to 48000 (Hz)
/// </summary>
public class AudioSamplingRateArgument : IArgument
{
public readonly int SamplingRate;
public AudioSamplingRateArgument(int samplingRate = 48000)
{
SamplingRate = samplingRate;
}
public string Text => $"-ar {SamplingRate}"; /// <summary>
/// Audio sampling rate argument. Defaults to 48000 (Hz)
/// </summary>
public class AudioSamplingRateArgument : IArgument
{
public readonly int SamplingRate;
public AudioSamplingRateArgument(int samplingRate = 48000)
{
SamplingRate = samplingRate;
} }
public string Text => $"-ar {SamplingRate}";
} }

View file

@ -1,26 +1,25 @@
using FFMpegCore.Enums; using FFMpegCore.Enums;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
/// <summary>
/// Represents parameter of bitstream filter
/// </summary>
public class BitStreamFilterArgument : IArgument
{ {
/// <summary> public readonly Channel Channel;
/// Represents parameter of bitstream filter public readonly Filter Filter;
/// </summary>
public class BitStreamFilterArgument : IArgument public BitStreamFilterArgument(Channel channel, Filter filter)
{ {
public readonly Channel Channel; Channel = channel;
public readonly Filter Filter; Filter = filter;
public BitStreamFilterArgument(Channel channel, Filter filter)
{
Channel = channel;
Filter = filter;
}
public string Text => Channel switch
{
Channel.Audio => $"-bsf:a {Filter.ToString().ToLowerInvariant()}",
Channel.Video => $"-bsf:v {Filter.ToString().ToLowerInvariant()}",
_ => string.Empty
};
} }
public string Text => Channel switch
{
Channel.Audio => $"-bsf:a {Filter.ToString().ToLowerInvariant()}",
Channel.Video => $"-bsf:v {Filter.ToString().ToLowerInvariant()}",
_ => string.Empty
};
} }

View file

@ -1,14 +1,13 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
public class BlackDetectArgument : IVideoFilterArgument
{ {
public class BlackDetectArgument : IVideoFilterArgument public BlackDetectArgument(double minimumDuration = 2.0, double pictureBlackRatioThreshold = 0.98, double pixelBlackThreshold = 0.1)
{ {
public string Key => "blackdetect"; Value = $"d={minimumDuration}:pic_th={pictureBlackRatioThreshold}:pix_th={pixelBlackThreshold}";
public string Value { get; }
public BlackDetectArgument(double minimumDuration = 2.0, double pictureBlackRatioThreshold = 0.98, double pixelBlackThreshold = 0.1)
{
Value = $"d={minimumDuration}:pic_th={pictureBlackRatioThreshold}:pix_th={pixelBlackThreshold}";
}
} }
public string Key => "blackdetect";
public string Value { get; }
} }

View file

@ -1,14 +1,13 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
internal class BlackFrameArgument : IVideoFilterArgument
{ {
internal class BlackFrameArgument : IVideoFilterArgument public BlackFrameArgument(int amount = 98, int threshold = 32)
{ {
public string Key => "blackframe"; Value = $"amount={amount}:threshold={threshold}";
public string Value { get; }
public BlackFrameArgument(int amount = 98, int threshold = 32)
{
Value = $"amount={amount}:threshold={threshold}";
}
} }
public string Key => "blackframe";
public string Value { get; }
} }

View file

@ -1,22 +1,26 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
/// <summary>
/// Represents parameter of concat argument
/// Used for creating video from multiple images or videos
/// </summary>
public class ConcatArgument : IInputArgument
{ {
public readonly IEnumerable<string> Values;
/// <summary> public ConcatArgument(IEnumerable<string> values)
/// Represents parameter of concat argument
/// Used for creating video from multiple images or videos
/// </summary>
public class ConcatArgument : IInputArgument
{ {
public readonly IEnumerable<string> Values; Values = values;
public ConcatArgument(IEnumerable<string> values)
{
Values = values;
}
public void Pre() { }
public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask;
public void Post() { }
public string Text => $"-i \"concat:{string.Join(@"|", Values)}\"";
} }
public void Pre() { }
public Task During(CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public void Post() { }
public string Text => $"-i \"concat:{string.Join(@"|", Values)}\"";
} }

View file

@ -1,22 +1,21 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
/// <summary>
/// Constant Rate Factor (CRF) argument
/// </summary>
public class ConstantRateFactorArgument : IArgument
{ {
/// <summary> public readonly int Crf;
/// Constant Rate Factor (CRF) argument
/// </summary> public ConstantRateFactorArgument(int crf)
public class ConstantRateFactorArgument : IArgument
{ {
public readonly int Crf; if (crf < 0 || crf > 63)
public ConstantRateFactorArgument(int crf)
{ {
if (crf < 0 || crf > 63) throw new ArgumentException("Argument is outside range (0 - 63)", nameof(crf));
{
throw new ArgumentException("Argument is outside range (0 - 63)", nameof(crf));
}
Crf = crf;
} }
public string Text => $"-crf {Crf}"; Crf = crf;
} }
public string Text => $"-crf {Crf}";
} }

View file

@ -1,23 +1,23 @@
using FFMpegCore.Enums; using FFMpegCore.Enums;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
{
/// <summary>
/// Represents parameter of copy parameter
/// Defines if channel (audio, video or both) should be copied to output file
/// </summary>
public class CopyArgument : IArgument
{
public readonly Channel Channel;
public CopyArgument(Channel channel = Channel.Both)
{
Channel = channel;
}
public string Text => Channel switch /// <summary>
{ /// Represents parameter of copy parameter
Channel.Both => "-c:a copy -c:v copy", /// Defines if channel (audio, video or both) should be copied to output file
_ => $"-c{Channel.StreamType()} copy" /// </summary>
}; public class CopyArgument : IArgument
{
public readonly Channel Channel;
public CopyArgument(Channel channel = Channel.Both)
{
Channel = channel;
} }
public string Text => Channel switch
{
Channel.Both => "-c:a copy -c:v copy",
_ => $"-c{Channel.StreamType()} copy"
};
} }

View file

@ -1,10 +1,9 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
/// <summary>
/// Represents a copy codec parameter
/// </summary>
public class CopyCodecArgument : IArgument
{ {
/// <summary> public string Text => "-codec copy";
/// Represents a copy codec parameter
/// </summary>
public class CopyCodecArgument : IArgument
{
public string Text => $"-codec copy";
}
} }

View file

@ -1,22 +1,21 @@
using System.Drawing; using System.Drawing;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
public class CropArgument : IArgument
{ {
public class CropArgument : IArgument public readonly int Left;
public readonly Size? Size;
public readonly int Top;
public CropArgument(Size? size, int top, int left)
{ {
public readonly Size? Size; Size = size;
public readonly int Top; Top = top;
public readonly int Left; Left = left;
public CropArgument(Size? size, int top, int left)
{
Size = size;
Top = top;
Left = left;
}
public CropArgument(int width, int height, int top, int left) : this(new Size(width, height), top, left) { }
public string Text => Size == null ? string.Empty : $"-vf crop={Size.Value.Width}:{Size.Value.Height}:{Left}:{Top}";
} }
public CropArgument(int width, int height, int top, int left) : this(new Size(width, height), top, left) { }
public string Text => Size == null ? string.Empty : $"-vf crop={Size.Value.Width}:{Size.Value.Height}:{Left}:{Top}";
} }

View file

@ -1,14 +1,13 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
public class CustomArgument : IArgument
{ {
public class CustomArgument : IArgument public readonly string Argument;
public CustomArgument(string argument)
{ {
public readonly string Argument; Argument = argument;
public CustomArgument(string argument)
{
Argument = argument;
}
public string Text => Argument ?? string.Empty;
} }
public string Text => Argument ?? string.Empty;
} }

View file

@ -1,31 +1,44 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
/// <summary>
/// Represents parameter of concat argument
/// Used for creating video from multiple images or videos
/// </summary>
public class DemuxConcatArgument : IInputArgument
{ {
/// <summary> private readonly string _tempFileName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, $"concat_{Guid.NewGuid()}.txt");
/// Represents parameter of concat argument public readonly IEnumerable<string> Values;
/// Used for creating video from multiple images or videos
/// </summary> public DemuxConcatArgument(IEnumerable<string> values)
public class DemuxConcatArgument : IInputArgument
{ {
public readonly IEnumerable<string> Values; Values = values.Select(value => $"file '{Escape(value)}'");
public DemuxConcatArgument(IEnumerable<string> values) }
{
Values = values.Select(value => $"file '{Escape(value)}'");
}
/// <summary> public void Pre()
/// Thanks slhck {
/// https://superuser.com/a/787651/1089628 File.WriteAllLines(_tempFileName, Values);
/// </summary> }
/// <param name="value"></param>
/// <returns></returns>
private string Escape(string value) => value.Replace("'", @"'\''");
private readonly string _tempFileName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, $"concat_{Guid.NewGuid()}.txt"); public Task During(CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public void Pre() => File.WriteAllLines(_tempFileName, Values); public void Post()
public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask; {
public void Post() => File.Delete(_tempFileName); File.Delete(_tempFileName);
}
public string Text => $"-f concat -safe 0 -i \"{_tempFileName}\""; public string Text => $"-f concat -safe 0 -i \"{_tempFileName}\"";
/// <summary>
/// Thanks slhck
/// https://superuser.com/a/787651/1089628
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
private string Escape(string value)
{
return value.Replace("'", @"'\''");
} }
} }

View file

@ -1,30 +1,29 @@
using FFMpegCore.Enums; using FFMpegCore.Enums;
using FFMpegCore.Exceptions; using FFMpegCore.Exceptions;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
/// <summary>
/// Represents cpu speed parameter
/// </summary>
public class DisableChannelArgument : IArgument
{ {
/// <summary> public readonly Channel Channel;
/// Represents cpu speed parameter
/// </summary> public DisableChannelArgument(Channel channel)
public class DisableChannelArgument : IArgument
{ {
public readonly Channel Channel; if (channel == Channel.Both)
public DisableChannelArgument(Channel channel)
{ {
if (channel == Channel.Both) throw new FFMpegException(FFMpegExceptionType.Conversion, "Cannot disable both channels");
{
throw new FFMpegException(FFMpegExceptionType.Conversion, "Cannot disable both channels");
}
Channel = channel;
} }
public string Text => Channel switch Channel = channel;
{
Channel.Video => "-vn",
Channel.Audio => "-an",
_ => string.Empty
};
} }
public string Text => Channel switch
{
Channel.Video => "-vn",
Channel.Audio => "-an",
_ => string.Empty
};
} }

View file

@ -1,59 +1,59 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
/// <summary>
/// Drawtext video filter argument
/// </summary>
public class DrawTextArgument : IVideoFilterArgument
{ {
/// <summary> public readonly DrawTextOptions Options;
/// Drawtext video filter argument
/// </summary> public DrawTextArgument(DrawTextOptions options)
public class DrawTextArgument : IVideoFilterArgument
{ {
public readonly DrawTextOptions Options; Options = options;
public DrawTextArgument(DrawTextOptions options)
{
Options = options;
}
public string Key { get; } = "drawtext";
public string Value => Options.TextInternal;
} }
public class DrawTextOptions public string Key { get; } = "drawtext";
public string Value => Options.TextInternal;
}
public class DrawTextOptions
{
public readonly string Font;
public readonly List<(string key, string value)> Parameters;
public readonly string Text;
private DrawTextOptions(string text, string font, IEnumerable<(string, string)> parameters)
{ {
public readonly string Text; Text = text;
public readonly string Font; Font = font;
public readonly List<(string key, string value)> Parameters; Parameters = parameters.ToList();
}
public static DrawTextOptions Create(string text, string font) internal string TextInternal => string.Join(":", new[] { ("text", Text), ("fontfile", Font) }.Concat(Parameters).Select(FormatArgumentPair));
{
return new DrawTextOptions(text, font, new List<(string, string)>());
}
public static DrawTextOptions Create(string text, string font, params (string key, string value)[] parameters)
{
return new DrawTextOptions(text, font, parameters);
}
internal string TextInternal => string.Join(":", new[] { ("text", Text), ("fontfile", Font) }.Concat(Parameters).Select(FormatArgumentPair)); public static DrawTextOptions Create(string text, string font)
{
return new DrawTextOptions(text, font, new List<(string, string)>());
}
private static string FormatArgumentPair((string key, string value) pair) public static DrawTextOptions Create(string text, string font, params (string key, string value)[] parameters)
{ {
return $"{pair.key}={EncloseIfContainsSpace(pair.value)}"; return new DrawTextOptions(text, font, parameters);
} }
private static string EncloseIfContainsSpace(string input) private static string FormatArgumentPair((string key, string value) pair)
{ {
return input.Contains(" ") ? $"'{input}'" : input; return $"{pair.key}={EncloseIfContainsSpace(pair.value)}";
} }
private DrawTextOptions(string text, string font, IEnumerable<(string, string)> parameters) private static string EncloseIfContainsSpace(string input)
{ {
Text = text; return input.Contains(" ") ? $"'{input}'" : input;
Font = font; }
Parameters = parameters.ToList();
}
public DrawTextOptions WithParameter(string key, string value) public DrawTextOptions WithParameter(string key, string value)
{ {
Parameters.Add((key, value)); Parameters.Add((key, value));
return this; return this;
}
} }
} }

View file

@ -1,16 +1,16 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
{
/// <summary>
/// Represents duration parameter
/// </summary>
public class DurationArgument : IArgument
{
public readonly TimeSpan? Duration;
public DurationArgument(TimeSpan? duration)
{
Duration = duration;
}
public string Text => !Duration.HasValue ? string.Empty : $"-t {Duration.Value}"; /// <summary>
/// Represents duration parameter
/// </summary>
public class DurationArgument : IArgument
{
public readonly TimeSpan? Duration;
public DurationArgument(TimeSpan? duration)
{
Duration = duration;
} }
public string Text => !Duration.HasValue ? string.Empty : $"-t {Duration.Value}";
} }

View file

@ -1,73 +1,73 @@
using System.Globalization; using System.Globalization;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
public class DynamicNormalizerArgument : IAudioFilterArgument
{ {
public class DynamicNormalizerArgument : IAudioFilterArgument private readonly Dictionary<string, string> _arguments = new();
/// <summary>
/// Dynamic Audio Normalizer. <see href="https://ffmpeg.org/ffmpeg-filters.html#dynaudnorm" />
/// </summary>
/// <param name="frameLength">Set the frame length in milliseconds. Must be between 10 to 8000. The default value is 500</param>
/// <param name="filterWindow">Set the Gaussian filter window size. In range from 3 to 301, must be odd number. The default value is 31</param>
/// <param name="targetPeak">Set the target peak value. The default value is 0.95</param>
/// <param name="gainFactor">Set the maximum gain factor. In range from 1.0 to 100.0. Default is 10.0.</param>
/// <param name="targetRms">Set the target RMS. In range from 0.0 to 1.0. Default to 0.0 (disabled)</param>
/// <param name="channelCoupling">Enable channels coupling. By default is enabled.</param>
/// <param name="enableDcBiasCorrection">Enable DC bias correction. By default is disabled.</param>
/// <param name="enableAlternativeBoundary">Enable alternative boundary mode. By default is disabled.</param>
/// <param name="compressorFactor">Set the compress factor. In range from 0.0 to 30.0. Default is 0.0 (disabled).</param>
public DynamicNormalizerArgument(int frameLength = 500, int filterWindow = 31, double targetPeak = 0.95, double gainFactor = 10.0, double targetRms = 0.0,
bool channelCoupling = true, bool enableDcBiasCorrection = false, bool enableAlternativeBoundary = false, double compressorFactor = 0.0)
{ {
private readonly Dictionary<string, string> _arguments = new(); if (frameLength < 10 || frameLength > 8000)
/// <summary>
/// Dynamic Audio Normalizer. <see href="https://ffmpeg.org/ffmpeg-filters.html#dynaudnorm"/>
/// </summary>
/// <param name="frameLength">Set the frame length in milliseconds. Must be between 10 to 8000. The default value is 500</param>
/// <param name="filterWindow">Set the Gaussian filter window size. In range from 3 to 301, must be odd number. The default value is 31</param>
/// <param name="targetPeak">Set the target peak value. The default value is 0.95</param>
/// <param name="gainFactor">Set the maximum gain factor. In range from 1.0 to 100.0. Default is 10.0.</param>
/// <param name="targetRms">Set the target RMS. In range from 0.0 to 1.0. Default to 0.0 (disabled)</param>
/// <param name="channelCoupling">Enable channels coupling. By default is enabled.</param>
/// <param name="enableDcBiasCorrection">Enable DC bias correction. By default is disabled.</param>
/// <param name="enableAlternativeBoundary">Enable alternative boundary mode. By default is disabled.</param>
/// <param name="compressorFactor">Set the compress factor. In range from 0.0 to 30.0. Default is 0.0 (disabled).</param>
public DynamicNormalizerArgument(int frameLength = 500, int filterWindow = 31, double targetPeak = 0.95, double gainFactor = 10.0, double targetRms = 0.0, bool channelCoupling = true, bool enableDcBiasCorrection = false, bool enableAlternativeBoundary = false, double compressorFactor = 0.0)
{ {
if (frameLength < 10 || frameLength > 8000) throw new ArgumentOutOfRangeException(nameof(frameLength), "Frame length must be between 10 to 8000");
{
throw new ArgumentOutOfRangeException(nameof(frameLength), "Frame length must be between 10 to 8000");
}
if (filterWindow < 3 || filterWindow > 31)
{
throw new ArgumentOutOfRangeException(nameof(filterWindow), "Gaussian filter window size must be between 3 to 31");
}
if (filterWindow % 2 == 0)
{
throw new ArgumentOutOfRangeException(nameof(filterWindow), "Gaussian filter window size must be an odd number");
}
if (targetPeak <= 0 || targetPeak > 1)
{
throw new ArgumentOutOfRangeException(nameof(targetPeak));
}
if (gainFactor < 1 || gainFactor > 100)
{
throw new ArgumentOutOfRangeException(nameof(gainFactor), "Gain factor must be between 1.0 to 100.0");
}
if (targetRms < 0 || targetRms > 1)
{
throw new ArgumentOutOfRangeException(nameof(targetRms), "Target RMS must be between 0.0 and 1.0");
}
if (compressorFactor < 0 || compressorFactor > 30)
{
throw new ArgumentOutOfRangeException(nameof(compressorFactor), "Compressor factor must be between 0.0 and 30.0");
}
_arguments.Add("f", frameLength.ToString());
_arguments.Add("g", filterWindow.ToString());
_arguments.Add("p", targetPeak.ToString("0.00", CultureInfo.InvariantCulture));
_arguments.Add("m", gainFactor.ToString("0.0", CultureInfo.InvariantCulture));
_arguments.Add("r", targetRms.ToString("0.0", CultureInfo.InvariantCulture));
_arguments.Add("n", (channelCoupling ? 1 : 0).ToString());
_arguments.Add("c", (enableDcBiasCorrection ? 1 : 0).ToString());
_arguments.Add("b", (enableAlternativeBoundary ? 1 : 0).ToString());
_arguments.Add("s", compressorFactor.ToString("0.0", CultureInfo.InvariantCulture));
} }
public string Key { get; } = "dynaudnorm"; if (filterWindow < 3 || filterWindow > 31)
{
throw new ArgumentOutOfRangeException(nameof(filterWindow), "Gaussian filter window size must be between 3 to 31");
}
public string Value => string.Join(":", _arguments.Select(pair => $"{pair.Key}={pair.Value}")); if (filterWindow % 2 == 0)
{
throw new ArgumentOutOfRangeException(nameof(filterWindow), "Gaussian filter window size must be an odd number");
}
if (targetPeak <= 0 || targetPeak > 1)
{
throw new ArgumentOutOfRangeException(nameof(targetPeak));
}
if (gainFactor < 1 || gainFactor > 100)
{
throw new ArgumentOutOfRangeException(nameof(gainFactor), "Gain factor must be between 1.0 to 100.0");
}
if (targetRms < 0 || targetRms > 1)
{
throw new ArgumentOutOfRangeException(nameof(targetRms), "Target RMS must be between 0.0 and 1.0");
}
if (compressorFactor < 0 || compressorFactor > 30)
{
throw new ArgumentOutOfRangeException(nameof(compressorFactor), "Compressor factor must be between 0.0 and 30.0");
}
_arguments.Add("f", frameLength.ToString());
_arguments.Add("g", filterWindow.ToString());
_arguments.Add("p", targetPeak.ToString("0.00", CultureInfo.InvariantCulture));
_arguments.Add("m", gainFactor.ToString("0.0", CultureInfo.InvariantCulture));
_arguments.Add("r", targetRms.ToString("0.0", CultureInfo.InvariantCulture));
_arguments.Add("n", (channelCoupling ? 1 : 0).ToString());
_arguments.Add("c", (enableDcBiasCorrection ? 1 : 0).ToString());
_arguments.Add("b", (enableAlternativeBoundary ? 1 : 0).ToString());
_arguments.Add("s", compressorFactor.ToString("0.0", CultureInfo.InvariantCulture));
} }
public string Key { get; } = "dynaudnorm";
public string Value => string.Join(":", _arguments.Select(pair => $"{pair.Key}={pair.Value}"));
} }

View file

@ -1,19 +1,18 @@
using FFMpegCore.Extend; using FFMpegCore.Extend;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
/// <summary>
/// Represents seek parameter
/// </summary>
public class EndSeekArgument : IArgument
{ {
/// <summary> public readonly TimeSpan? SeekTo;
/// Represents seek parameter
/// </summary> public EndSeekArgument(TimeSpan? seekTo)
public class EndSeekArgument : IArgument
{ {
public readonly TimeSpan? SeekTo; SeekTo = seekTo;
public EndSeekArgument(TimeSpan? seekTo)
{
SeekTo = seekTo;
}
public string Text => SeekTo.HasValue ? $"-to {SeekTo.Value.ToLongString()}" : string.Empty;
} }
public string Text => SeekTo.HasValue ? $"-to {SeekTo.Value.ToLongString()}" : string.Empty;
} }

View file

@ -1,10 +1,9 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
/// <summary>
/// Faststart argument - for moving moov atom to the start of file
/// </summary>
public class FaststartArgument : IArgument
{ {
/// <summary> public string Text => "-movflags faststart";
/// Faststart argument - for moving moov atom to the start of file
/// </summary>
public class FaststartArgument : IArgument
{
public string Text => "-movflags faststart";
}
} }

View file

@ -1,23 +1,23 @@
using FFMpegCore.Enums; using FFMpegCore.Enums;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
/// <summary>
/// Represents force format parameter
/// </summary>
public class ForceFormatArgument : IArgument
{ {
/// <summary> private readonly string _format;
/// Represents force format parameter
/// </summary> public ForceFormatArgument(string format)
public class ForceFormatArgument : IArgument
{ {
private readonly string _format; _format = format;
public ForceFormatArgument(string format)
{
_format = format;
}
public ForceFormatArgument(ContainerFormat format)
{
_format = format.Name;
}
public string Text => $"-f {_format}";
} }
public ForceFormatArgument(ContainerFormat format)
{
_format = format.Name;
}
public string Text => $"-f {_format}";
} }

View file

@ -1,17 +1,15 @@
using FFMpegCore.Enums; using FFMpegCore.Enums;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
public class ForcePixelFormat : IArgument
{ {
public class ForcePixelFormat : IArgument public ForcePixelFormat(string format)
{ {
public string PixelFormat { get; } PixelFormat = format;
public string Text => $"-pix_fmt {PixelFormat}";
public ForcePixelFormat(string format)
{
PixelFormat = format;
}
public ForcePixelFormat(PixelFormat format) : this(format.Name) { }
} }
public ForcePixelFormat(PixelFormat format) : this(format.Name) { }
public string PixelFormat { get; }
public string Text => $"-pix_fmt {PixelFormat}";
} }

View file

@ -1,16 +1,16 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
{
/// <summary>
/// Represents frame output count parameter
/// </summary>
public class FrameOutputCountArgument : IArgument
{
public readonly int Frames;
public FrameOutputCountArgument(int frames)
{
Frames = frames;
}
public string Text => $"-vframes {Frames}"; /// <summary>
/// Represents frame output count parameter
/// </summary>
public class FrameOutputCountArgument : IArgument
{
public readonly int Frames;
public FrameOutputCountArgument(int frames)
{
Frames = frames;
} }
public string Text => $"-vframes {Frames}";
} }

View file

@ -1,17 +1,18 @@
namespace FFMpegCore.Arguments using System.Globalization;
namespace FFMpegCore.Arguments;
/// <summary>
/// Represents frame rate parameter
/// </summary>
public class FrameRateArgument : IArgument
{ {
/// <summary> public readonly double Framerate;
/// Represents frame rate parameter
/// </summary> public FrameRateArgument(double framerate)
public class FrameRateArgument : IArgument
{ {
public readonly double Framerate; Framerate = framerate;
public FrameRateArgument(double framerate)
{
Framerate = framerate;
}
public string Text => $"-r {Framerate.ToString(System.Globalization.CultureInfo.InvariantCulture)}";
} }
public string Text => $"-r {Framerate.ToString(CultureInfo.InvariantCulture)}";
} }

View file

@ -1,24 +1,23 @@
using System.Drawing; using System.Drawing;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
public class GifPaletteArgument : IArgument
{ {
public class GifPaletteArgument : IArgument private readonly int _fps;
private readonly Size? _size;
private readonly int _streamIndex;
public GifPaletteArgument(int streamIndex, int fps, Size? size)
{ {
private readonly int _streamIndex; _streamIndex = streamIndex;
_fps = fps;
private readonly int _fps; _size = size;
private readonly Size? _size;
public GifPaletteArgument(int streamIndex, int fps, Size? size)
{
_streamIndex = streamIndex;
_fps = fps;
_size = size;
}
private string ScaleText => _size.HasValue ? $"scale=w={_size.Value.Width}:h={_size.Value.Height}," : string.Empty;
public string Text => $"-filter_complex \"[{_streamIndex}:v] fps={_fps},{ScaleText}split [a][b];[a] palettegen=max_colors=32 [p];[b][p] paletteuse=dither=bayer\"";
} }
private string ScaleText => _size.HasValue ? $"scale=w={_size.Value.Width}:h={_size.Value.Height}," : string.Empty;
public string Text =>
$"-filter_complex \"[{_streamIndex}:v] fps={_fps},{ScaleText}split [a][b];[a] palettegen=max_colors=32 [p];[b][p] paletteuse=dither=bayer\"";
} }

View file

@ -1,16 +1,15 @@
using FFMpegCore.Enums; using FFMpegCore.Enums;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
public class HardwareAccelerationArgument : IArgument
{ {
public class HardwareAccelerationArgument : IArgument public HardwareAccelerationArgument(HardwareAccelerationDevice hardwareAccelerationDevice)
{ {
public HardwareAccelerationDevice HardwareAccelerationDevice { get; } HardwareAccelerationDevice = hardwareAccelerationDevice;
public HardwareAccelerationArgument(HardwareAccelerationDevice hardwareAccelerationDevice)
{
HardwareAccelerationDevice = hardwareAccelerationDevice;
}
public string Text => $"-hwaccel {HardwareAccelerationDevice.ToString().ToLower()}";
} }
public HardwareAccelerationDevice HardwareAccelerationDevice { get; }
public string Text => $"-hwaccel {HardwareAccelerationDevice.ToString().ToLower()}";
} }

View file

@ -1,78 +1,112 @@
using System.Globalization; using System.Globalization;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
public class HighPassFilterArgument : IAudioFilterArgument
{ {
public class HighPassFilterArgument : IAudioFilterArgument private readonly Dictionary<string, string> _arguments = new();
private readonly List<string> _precision = new()
{ {
private readonly Dictionary<string, string> _arguments = new(); "auto",
private readonly List<string> _widthTypes = new() { "h", "q", "o", "s", "k" }; "s16",
private readonly List<string> _transformTypes = new() { "di", "dii", "tdi", "tdii", "latt", "svf", "zdf" }; "s32",
private readonly List<string> _precision = new() { "auto", "s16", "s32", "f32", "f64" }; "f32",
/// <summary> "f64"
/// HighPass Filter. <see href="https://ffmpeg.org/ffmpeg-filters.html#highpass"/> };
/// </summary>
/// <param name="frequency">Set frequency in Hz. Default is 3000.</param> private readonly List<string> _transformTypes = new()
/// <param name="poles">Set number of poles. Default is 2.</param> {
/// <param name="width_type">Set method to specify band-width of filter, possible values are: h, q, o, s, k</param> "di",
/// <param name="width">Specify the band-width of a filter in width_type units. Applies only to double-pole filter. The default is 0.707q and gives a Butterworth response.</param> "dii",
/// <param name="mix">How much to use filtered signal in output. Default is 1. Range is between 0 and 1.</param> "tdi",
/// <param name="channels">Specify which channels to filter, by default all available are filtered.</param> "tdii",
/// <param name="normalize">Normalize biquad coefficients, by default is disabled. Enabling it will normalize magnitude response at DC to 0dB.</param> "latt",
/// <param name="transform">Set transform type of IIR filter, possible values are: di, dii, tdi, tdii, latt, svf, zdf</param> "svf",
/// <param name="precision">Set precison of filtering, possible values are: auto, s16, s32, f32, f64.</param> "zdf"
/// <param name="block_size">Set block size used for reverse IIR processing. If this value is set to high enough value (higher than impulse response length truncated when reaches near zero values) filtering will become linear phase otherwise if not big enough it will just produce nasty artifacts.</param> };
public HighPassFilterArgument(double frequency = 3000, int poles = 2, string width_type = "q", double width = 0.707, double mix = 1, string channels = "", bool normalize = false, string transform = "", string precision = "auto", int? block_size = null)
private readonly List<string> _widthTypes = new()
{
"h",
"q",
"o",
"s",
"k"
};
/// <summary>
/// HighPass Filter. <see href="https://ffmpeg.org/ffmpeg-filters.html#highpass" />
/// </summary>
/// <param name="frequency">Set frequency in Hz. Default is 3000.</param>
/// <param name="poles">Set number of poles. Default is 2.</param>
/// <param name="width_type">Set method to specify band-width of filter, possible values are: h, q, o, s, k</param>
/// <param name="width">
/// Specify the band-width of a filter in width_type units. Applies only to double-pole filter. The default is 0.707q and
/// gives a Butterworth response.
/// </param>
/// <param name="mix">How much to use filtered signal in output. Default is 1. Range is between 0 and 1.</param>
/// <param name="channels">Specify which channels to filter, by default all available are filtered.</param>
/// <param name="normalize">Normalize biquad coefficients, by default is disabled. Enabling it will normalize magnitude response at DC to 0dB.</param>
/// <param name="transform">Set transform type of IIR filter, possible values are: di, dii, tdi, tdii, latt, svf, zdf</param>
/// <param name="precision">Set precison of filtering, possible values are: auto, s16, s32, f32, f64.</param>
/// <param name="block_size">
/// Set block size used for reverse IIR processing. If this value is set to high enough value (higher than impulse
/// response length truncated when reaches near zero values) filtering will become linear phase otherwise if not big enough it will just
/// produce nasty artifacts.
/// </param>
public HighPassFilterArgument(double frequency = 3000, int poles = 2, string width_type = "q", double width = 0.707, double mix = 1, string channels = "",
bool normalize = false, string transform = "", string precision = "auto", int? block_size = null)
{
if (frequency < 0)
{ {
if (frequency < 0) throw new ArgumentOutOfRangeException(nameof(frequency), "Frequency must be a positive number");
{
throw new ArgumentOutOfRangeException(nameof(frequency), "Frequency must be a positive number");
}
if (poles < 1 || poles > 2)
{
throw new ArgumentOutOfRangeException(nameof(poles), "Poles must be either 1 or 2");
}
if (!_widthTypes.Contains(width_type))
{
throw new ArgumentOutOfRangeException(nameof(width_type), "Width type must be either " + _widthTypes.ToString());
}
if (mix < 0 || mix > 1)
{
throw new ArgumentOutOfRangeException(nameof(mix), "Mix must be between 0 and 1");
}
if (!_precision.Contains(precision))
{
throw new ArgumentOutOfRangeException(nameof(precision), "Precision must be either " + _precision.ToString());
}
_arguments.Add("f", frequency.ToString("0.00", CultureInfo.InvariantCulture));
_arguments.Add("p", poles.ToString());
_arguments.Add("t", width_type);
_arguments.Add("w", width.ToString("0.00", CultureInfo.InvariantCulture));
_arguments.Add("m", mix.ToString("0.00", CultureInfo.InvariantCulture));
if (channels != "")
{
_arguments.Add("c", channels);
}
_arguments.Add("n", (normalize ? 1 : 0).ToString());
if (transform != "" && _transformTypes.Contains(transform))
{
_arguments.Add("a", transform);
}
_arguments.Add("r", precision);
if (block_size != null && block_size >= 0)
{
_arguments.Add("b", block_size.ToString());
}
} }
public string Key { get; } = "highpass"; if (poles < 1 || poles > 2)
{
throw new ArgumentOutOfRangeException(nameof(poles), "Poles must be either 1 or 2");
}
public string Value => string.Join(":", _arguments.Select(pair => $"{pair.Key}={pair.Value}")); if (!_widthTypes.Contains(width_type))
{
throw new ArgumentOutOfRangeException(nameof(width_type), "Width type must be either " + _widthTypes);
}
if (mix < 0 || mix > 1)
{
throw new ArgumentOutOfRangeException(nameof(mix), "Mix must be between 0 and 1");
}
if (!_precision.Contains(precision))
{
throw new ArgumentOutOfRangeException(nameof(precision), "Precision must be either " + _precision);
}
_arguments.Add("f", frequency.ToString("0.00", CultureInfo.InvariantCulture));
_arguments.Add("p", poles.ToString());
_arguments.Add("t", width_type);
_arguments.Add("w", width.ToString("0.00", CultureInfo.InvariantCulture));
_arguments.Add("m", mix.ToString("0.00", CultureInfo.InvariantCulture));
if (channels != "")
{
_arguments.Add("c", channels);
}
_arguments.Add("n", (normalize ? 1 : 0).ToString());
if (transform != "" && _transformTypes.Contains(transform))
{
_arguments.Add("a", transform);
}
_arguments.Add("r", precision);
if (block_size != null && block_size >= 0)
{
_arguments.Add("b", block_size.ToString());
}
} }
public string Key { get; } = "highpass";
public string Value => string.Join(":", _arguments.Select(pair => $"{pair.Key}={pair.Value}"));
} }

View file

@ -1,10 +1,9 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
public interface IArgument
{ {
public interface IArgument /// <summary>
{ /// The textual representation of the argument
/// <summary> /// </summary>
/// The textual representation of the argument string Text { get; }
/// </summary>
string Text { get; }
}
} }

View file

@ -1,14 +1,13 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
public class ID3V2VersionArgument : IArgument
{ {
public class ID3V2VersionArgument : IArgument private readonly int _version;
public ID3V2VersionArgument(int version)
{ {
private readonly int _version; _version = version;
public ID3V2VersionArgument(int version)
{
_version = version;
}
public string Text => $"-id3v2_version {_version}";
} }
public string Text => $"-id3v2_version {_version}";
} }

View file

@ -1,13 +1,12 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
public interface IDynamicArgument
{ {
public interface IDynamicArgument /// <summary>
{ /// Same as <see cref="IArgument.Text" />, but this receives the arguments generated before as parameter
/// <summary> /// </summary>
/// Same as <see cref="IArgument.Text"/>, but this receives the arguments generated before as parameter /// <param name="context"></param>
/// </summary> /// <returns></returns>
/// <param name="context"></param> //public string GetText(StringBuilder context);
/// <returns></returns> string GetText(IEnumerable<IArgument> context);
//public string GetText(StringBuilder context);
public string GetText(IEnumerable<IArgument> context);
}
} }

View file

@ -1,6 +1,5 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
public interface IInputArgument : IInputOutputArgument
{ {
public interface IInputArgument : IInputOutputArgument
{
}
} }

View file

@ -1,9 +1,8 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
public interface IInputOutputArgument : IArgument
{ {
public interface IInputOutputArgument : IArgument void Pre();
{ Task During(CancellationToken cancellationToken = default);
void Pre(); void Post();
Task During(CancellationToken cancellationToken = default);
void Post();
}
} }

View file

@ -1,6 +1,5 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
public interface IOutputArgument : IInputOutputArgument
{ {
public interface IOutputArgument : IInputOutputArgument
{
}
} }

View file

@ -1,32 +1,35 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
/// <summary>
/// Represents input parameter
/// </summary>
public class InputArgument : IInputArgument
{ {
/// <summary> public readonly string FilePath;
/// Represents input parameter public readonly bool VerifyExists;
/// </summary>
public class InputArgument : IInputArgument public InputArgument(bool verifyExists, string filePaths)
{ {
public readonly bool VerifyExists; VerifyExists = verifyExists;
public readonly string FilePath; FilePath = filePaths;
public InputArgument(bool verifyExists, string filePaths)
{
VerifyExists = verifyExists;
FilePath = filePaths;
}
public InputArgument(string path, bool verifyExists) : this(verifyExists, path) { }
public void Pre()
{
if (VerifyExists && !File.Exists(FilePath))
{
throw new FileNotFoundException("Input file not found", FilePath);
}
}
public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask;
public void Post() { }
public string Text => $"-i \"{FilePath}\"";
} }
public InputArgument(string path, bool verifyExists) : this(verifyExists, path) { }
public void Pre()
{
if (VerifyExists && !File.Exists(FilePath))
{
throw new FileNotFoundException("Input file not found", FilePath);
}
}
public Task During(CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public void Post() { }
public string Text => $"-i \"{FilePath}\"";
} }

View file

@ -1,23 +1,25 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
/// <summary>
/// Represents an input device parameter
/// </summary>
public class InputDeviceArgument : IInputArgument
{ {
/// <summary> private readonly string Device;
/// Represents an input device parameter
/// </summary> public InputDeviceArgument(string device)
public class InputDeviceArgument : IInputArgument
{ {
private readonly string Device; Device = device;
public InputDeviceArgument(string device)
{
Device = device;
}
public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask;
public void Pre() { }
public void Post() { }
public string Text => $"-i {Device}";
} }
public Task During(CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public void Pre() { }
public void Post() { }
public string Text => $"-i {Device}";
} }

View file

@ -1,31 +1,30 @@
using System.IO.Pipes; using System.IO.Pipes;
using FFMpegCore.Pipes; using FFMpegCore.Pipes;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
/// <summary>
/// Represents input parameter for a named pipe
/// </summary>
public class InputPipeArgument : PipeArgument, IInputArgument
{ {
/// <summary> public readonly IPipeSource Writer;
/// Represents input parameter for a named pipe
/// </summary> public InputPipeArgument(IPipeSource writer) : base(PipeDirection.Out)
public class InputPipeArgument : PipeArgument, IInputArgument
{ {
public readonly IPipeSource Writer; Writer = writer;
}
public InputPipeArgument(IPipeSource writer) : base(PipeDirection.Out) public override string Text => $"{Writer.GetStreamArguments()} -i \"{PipePath}\"";
protected override async Task ProcessDataAsync(CancellationToken token)
{
await Pipe.WaitForConnectionAsync(token).ConfigureAwait(false);
if (!Pipe.IsConnected)
{ {
Writer = writer; throw new OperationCanceledException();
} }
public override string Text => $"{Writer.GetStreamArguments()} -i \"{PipePath}\""; await Writer.WriteAsync(Pipe, token).ConfigureAwait(false);
protected override async Task ProcessDataAsync(CancellationToken token)
{
await Pipe.WaitForConnectionAsync(token).ConfigureAwait(false);
if (!Pipe.IsConnected)
{
throw new OperationCanceledException();
}
await Writer.WriteAsync(Pipe, token).ConfigureAwait(false);
}
} }
} }

View file

@ -1,16 +1,16 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
{
/// <summary>
/// Represents loop parameter
/// </summary>
public class LoopArgument : IArgument
{
public readonly int Times;
public LoopArgument(int times)
{
Times = times;
}
public string Text => $"-loop {Times}"; /// <summary>
/// Represents loop parameter
/// </summary>
public class LoopArgument : IArgument
{
public readonly int Times;
public LoopArgument(int times)
{
Times = times;
} }
public string Text => $"-loop {Times}";
} }

View file

@ -1,78 +1,112 @@
using System.Globalization; using System.Globalization;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
public class LowPassFilterArgument : IAudioFilterArgument
{ {
public class LowPassFilterArgument : IAudioFilterArgument private readonly Dictionary<string, string> _arguments = new();
private readonly List<string> _precision = new()
{ {
private readonly Dictionary<string, string> _arguments = new(); "auto",
private readonly List<string> _widthTypes = new() { "h", "q", "o", "s", "k" }; "s16",
private readonly List<string> _transformTypes = new() { "di", "dii", "tdi", "tdii", "latt", "svf", "zdf" }; "s32",
private readonly List<string> _precision = new() { "auto", "s16", "s32", "f32", "f64" }; "f32",
/// <summary> "f64"
/// LowPass Filter. <see href="https://ffmpeg.org/ffmpeg-filters.html#lowpass"/> };
/// </summary>
/// <param name="frequency">Set frequency in Hz. Default is 3000.</param> private readonly List<string> _transformTypes = new()
/// <param name="poles">Set number of poles. Default is 2.</param> {
/// <param name="width_type">Set method to specify band-width of filter, possible values are: h, q, o, s, k</param> "di",
/// <param name="width">Specify the band-width of a filter in width_type units. Applies only to double-pole filter. The default is 0.707q and gives a Butterworth response.</param> "dii",
/// <param name="mix">How much to use filtered signal in output. Default is 1. Range is between 0 and 1.</param> "tdi",
/// <param name="channels">Specify which channels to filter, by default all available are filtered.</param> "tdii",
/// <param name="normalize">Normalize biquad coefficients, by default is disabled. Enabling it will normalize magnitude response at DC to 0dB.</param> "latt",
/// <param name="transform">Set transform type of IIR filter, possible values are: di, dii, tdi, tdii, latt, svf, zdf</param> "svf",
/// <param name="precision">Set precison of filtering, possible values are: auto, s16, s32, f32, f64.</param> "zdf"
/// <param name="block_size">Set block size used for reverse IIR processing. If this value is set to high enough value (higher than impulse response length truncated when reaches near zero values) filtering will become linear phase otherwise if not big enough it will just produce nasty artifacts.</param> };
public LowPassFilterArgument(double frequency = 3000, int poles = 2, string width_type = "q", double width = 0.707, double mix = 1, string channels = "", bool normalize = false, string transform = "", string precision = "auto", int? block_size = null)
private readonly List<string> _widthTypes = new()
{
"h",
"q",
"o",
"s",
"k"
};
/// <summary>
/// LowPass Filter. <see href="https://ffmpeg.org/ffmpeg-filters.html#lowpass" />
/// </summary>
/// <param name="frequency">Set frequency in Hz. Default is 3000.</param>
/// <param name="poles">Set number of poles. Default is 2.</param>
/// <param name="width_type">Set method to specify band-width of filter, possible values are: h, q, o, s, k</param>
/// <param name="width">
/// Specify the band-width of a filter in width_type units. Applies only to double-pole filter. The default is 0.707q and
/// gives a Butterworth response.
/// </param>
/// <param name="mix">How much to use filtered signal in output. Default is 1. Range is between 0 and 1.</param>
/// <param name="channels">Specify which channels to filter, by default all available are filtered.</param>
/// <param name="normalize">Normalize biquad coefficients, by default is disabled. Enabling it will normalize magnitude response at DC to 0dB.</param>
/// <param name="transform">Set transform type of IIR filter, possible values are: di, dii, tdi, tdii, latt, svf, zdf</param>
/// <param name="precision">Set precison of filtering, possible values are: auto, s16, s32, f32, f64.</param>
/// <param name="block_size">
/// Set block size used for reverse IIR processing. If this value is set to high enough value (higher than impulse
/// response length truncated when reaches near zero values) filtering will become linear phase otherwise if not big enough it will just
/// produce nasty artifacts.
/// </param>
public LowPassFilterArgument(double frequency = 3000, int poles = 2, string width_type = "q", double width = 0.707, double mix = 1, string channels = "",
bool normalize = false, string transform = "", string precision = "auto", int? block_size = null)
{
if (frequency < 0)
{ {
if (frequency < 0) throw new ArgumentOutOfRangeException(nameof(frequency), "Frequency must be a positive number");
{
throw new ArgumentOutOfRangeException(nameof(frequency), "Frequency must be a positive number");
}
if (poles < 1 || poles > 2)
{
throw new ArgumentOutOfRangeException(nameof(poles), "Poles must be either 1 or 2");
}
if (!_widthTypes.Contains(width_type))
{
throw new ArgumentOutOfRangeException(nameof(width_type), "Width type must be either " + _widthTypes.ToString());
}
if (mix < 0 || mix > 1)
{
throw new ArgumentOutOfRangeException(nameof(mix), "Mix must be between 0 and 1");
}
if (!_precision.Contains(precision))
{
throw new ArgumentOutOfRangeException(nameof(precision), "Precision must be either " + _precision.ToString());
}
_arguments.Add("f", frequency.ToString("0.00", CultureInfo.InvariantCulture));
_arguments.Add("p", poles.ToString());
_arguments.Add("t", width_type);
_arguments.Add("w", width.ToString("0.00", CultureInfo.InvariantCulture));
_arguments.Add("m", mix.ToString("0.00", CultureInfo.InvariantCulture));
if (channels != "")
{
_arguments.Add("c", channels);
}
_arguments.Add("n", (normalize ? 1 : 0).ToString());
if (transform != "" && _transformTypes.Contains(transform))
{
_arguments.Add("a", transform);
}
_arguments.Add("r", precision);
if (block_size != null && block_size >= 0)
{
_arguments.Add("b", block_size.ToString());
}
} }
public string Key { get; } = "lowpass"; if (poles < 1 || poles > 2)
{
throw new ArgumentOutOfRangeException(nameof(poles), "Poles must be either 1 or 2");
}
public string Value => string.Join(":", _arguments.Select(pair => $"{pair.Key}={pair.Value}")); if (!_widthTypes.Contains(width_type))
{
throw new ArgumentOutOfRangeException(nameof(width_type), "Width type must be either " + _widthTypes);
}
if (mix < 0 || mix > 1)
{
throw new ArgumentOutOfRangeException(nameof(mix), "Mix must be between 0 and 1");
}
if (!_precision.Contains(precision))
{
throw new ArgumentOutOfRangeException(nameof(precision), "Precision must be either " + _precision);
}
_arguments.Add("f", frequency.ToString("0.00", CultureInfo.InvariantCulture));
_arguments.Add("p", poles.ToString());
_arguments.Add("t", width_type);
_arguments.Add("w", width.ToString("0.00", CultureInfo.InvariantCulture));
_arguments.Add("m", mix.ToString("0.00", CultureInfo.InvariantCulture));
if (channels != "")
{
_arguments.Add("c", channels);
}
_arguments.Add("n", (normalize ? 1 : 0).ToString());
if (transform != "" && _transformTypes.Contains(transform))
{
_arguments.Add("a", transform);
}
_arguments.Add("r", precision);
if (block_size != null && block_size >= 0)
{
_arguments.Add("b", block_size.ToString());
}
} }
public string Key { get; } = "lowpass";
public string Value => string.Join(":", _arguments.Select(pair => $"{pair.Key}={pair.Value}"));
} }

View file

@ -1,53 +1,52 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
public class MapMetadataArgument : IInputArgument, IDynamicArgument
{ {
public class MapMetadataArgument : IInputArgument, IDynamicArgument private readonly int? _inputIndex;
/// <summary>
/// Null means it takes the last input used before this argument
/// </summary>
/// <param name="inputIndex"></param>
public MapMetadataArgument(int? inputIndex = null)
{ {
private readonly int? _inputIndex; _inputIndex = inputIndex;
}
public string Text => GetText(null); public string GetText(IEnumerable<IArgument>? arguments)
{
arguments ??= Enumerable.Empty<IArgument>();
/// <summary> var index = 0;
/// Null means it takes the last input used before this argument if (_inputIndex is null)
/// </summary>
/// <param name="inputIndex"></param>
public MapMetadataArgument(int? inputIndex = null)
{ {
_inputIndex = inputIndex; index = arguments
.TakeWhile(x => x != this)
.OfType<IInputArgument>()
.Count();
index = Math.Max(index - 1, 0);
}
else
{
index = _inputIndex.Value;
} }
public string GetText(IEnumerable<IArgument>? arguments) return $"-map_metadata {index}";
{ }
arguments ??= Enumerable.Empty<IArgument>();
var index = 0; public string Text => GetText(null);
if (_inputIndex is null)
{
index = arguments
.TakeWhile(x => x != this)
.OfType<IInputArgument>()
.Count();
index = Math.Max(index - 1, 0); public Task During(CancellationToken cancellationToken = default)
} {
else return Task.CompletedTask;
{ }
index = _inputIndex.Value;
}
return $"-map_metadata {index}"; public void Post()
} {
}
public Task During(CancellationToken cancellationToken = default) public void Pre()
{ {
return Task.CompletedTask;
}
public void Post()
{
}
public void Pre()
{
}
} }
} }

View file

@ -1,31 +1,30 @@
using FFMpegCore.Enums; using FFMpegCore.Enums;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
/// <summary>
/// Represents choice of stream by the stream specifier
/// </summary>
public class MapStreamArgument : IArgument
{ {
/// <summary> private readonly Channel _channel;
/// Represents choice of stream by the stream specifier private readonly int _inputFileIndex;
/// </summary> private readonly bool _negativeMap;
public class MapStreamArgument : IArgument private readonly int _streamIndex;
public MapStreamArgument(int streamIndex, int inputFileIndex, Channel channel = Channel.All, bool negativeMap = false)
{ {
private readonly int _inputFileIndex; if (channel == Channel.Both)
private readonly int _streamIndex;
private readonly Channel _channel;
private readonly bool _negativeMap;
public MapStreamArgument(int streamIndex, int inputFileIndex, Channel channel = Channel.All, bool negativeMap = false)
{ {
if (channel == Channel.Both) // "Both" is not valid in this case and probably means all stream types
{ channel = Channel.All;
// "Both" is not valid in this case and probably means all stream types
channel = Channel.All;
}
_inputFileIndex = inputFileIndex;
_streamIndex = streamIndex;
_channel = channel;
_negativeMap = negativeMap;
} }
public string Text => $"-map {(_negativeMap ? "-" : "")}{_inputFileIndex}{_channel.StreamType()}:{_streamIndex}"; _inputFileIndex = inputFileIndex;
_streamIndex = streamIndex;
_channel = channel;
_negativeMap = negativeMap;
} }
public string Text => $"-map {(_negativeMap ? "-" : "")}{_inputFileIndex}{_channel.StreamType()}:{_streamIndex}";
} }

View file

@ -1,33 +1,41 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
public class MetaDataArgument : IInputArgument, IDynamicArgument
{ {
public class MetaDataArgument : IInputArgument, IDynamicArgument private readonly string _metaDataContent;
private readonly string _tempFileName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, $"metadata_{Guid.NewGuid()}.txt");
public MetaDataArgument(string metaDataContent)
{ {
private readonly string _metaDataContent; _metaDataContent = metaDataContent;
private readonly string _tempFileName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, $"metadata_{Guid.NewGuid()}.txt"); }
public MetaDataArgument(string metaDataContent) public string GetText(IEnumerable<IArgument>? arguments)
{ {
_metaDataContent = metaDataContent; arguments ??= Enumerable.Empty<IArgument>();
}
public string Text => GetText(null); var index = arguments
.TakeWhile(x => x != this)
.OfType<IInputArgument>()
.Count();
public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask; return $"-i \"{_tempFileName}\" -map_metadata {index}";
}
public void Pre() => File.WriteAllText(_tempFileName, _metaDataContent); public string Text => GetText(null);
public void Post() => File.Delete(_tempFileName); public Task During(CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public string GetText(IEnumerable<IArgument>? arguments) public void Pre()
{ {
arguments ??= Enumerable.Empty<IArgument>(); File.WriteAllText(_tempFileName, _metaDataContent);
}
var index = arguments public void Post()
.TakeWhile(x => x != this) {
.OfType<IInputArgument>() File.Delete(_tempFileName);
.Count();
return $"-i \"{_tempFileName}\" -map_metadata {index}";
}
} }
} }

View file

@ -1,47 +1,50 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
/// <summary>
/// Represents input parameters for multiple files
/// </summary>
public class MultiInputArgument : IInputArgument
{ {
/// <summary> public readonly IEnumerable<string> FilePaths;
/// Represents input parameters for multiple files public readonly bool VerifyExists;
/// </summary>
public class MultiInputArgument : IInputArgument public MultiInputArgument(bool verifyExists, IEnumerable<string> filePaths)
{ {
public readonly bool VerifyExists; VerifyExists = verifyExists;
public readonly IEnumerable<string> FilePaths; FilePaths = filePaths;
}
public MultiInputArgument(bool verifyExists, IEnumerable<string> filePaths) public MultiInputArgument(IEnumerable<string> filePaths, bool verifyExists) : this(verifyExists, filePaths) { }
public void Pre()
{
if (VerifyExists)
{ {
VerifyExists = verifyExists; var missingFiles = new List<string>();
FilePaths = filePaths; foreach (var filePath in FilePaths)
}
public MultiInputArgument(IEnumerable<string> filePaths, bool verifyExists) : this(verifyExists, filePaths) { }
public void Pre()
{
if (VerifyExists)
{ {
var missingFiles = new List<string>(); if (!File.Exists(filePath))
foreach (var filePath in FilePaths)
{ {
if (!File.Exists(filePath)) missingFiles.Add(filePath);
{
missingFiles.Add(filePath);
}
}
if (missingFiles.Any())
{
throw new FileNotFoundException($"The following input files were not found: {string.Join(", ", missingFiles)}");
} }
} }
if (missingFiles.Any())
{
throw new FileNotFoundException($"The following input files were not found: {string.Join(", ", missingFiles)}");
}
} }
public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask;
public void Post() { }
/// <summary>
/// Generates a combined input argument text for all file paths
/// </summary>
public string Text => string.Join(" ", FilePaths.Select(filePath => $"-i \"{filePath}\""));
} }
public Task During(CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public void Post() { }
/// <summary>
/// Generates a combined input argument text for all file paths
/// </summary>
public string Text => string.Join(" ", FilePaths.Select(filePath => $"-i \"{filePath}\""));
} }

View file

@ -1,37 +1,41 @@
using FFMpegCore.Exceptions; using FFMpegCore.Exceptions;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
/// <summary>
/// Represents output parameter
/// </summary>
public class OutputArgument : IOutputArgument
{ {
/// <summary> public readonly bool Overwrite;
/// Represents output parameter public readonly string Path;
/// </summary>
public class OutputArgument : IOutputArgument public OutputArgument(string path, bool overwrite = true)
{ {
public readonly string Path; Path = path;
public readonly bool Overwrite; Overwrite = overwrite;
public OutputArgument(string path, bool overwrite = true)
{
Path = path;
Overwrite = overwrite;
}
public void Pre()
{
if (!Overwrite && File.Exists(Path))
{
throw new FFMpegException(FFMpegExceptionType.File, "Output file already exists and overwrite is disabled");
}
}
public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask;
public void Post()
{
}
public OutputArgument(FileInfo value) : this(value.FullName) { }
public OutputArgument(Uri value) : this(value.AbsolutePath) { }
public string Text => $"\"{Path}\"{(Overwrite ? " -y" : string.Empty)}";
} }
public OutputArgument(FileInfo value) : this(value.FullName) { }
public OutputArgument(Uri value) : this(value.AbsolutePath) { }
public void Pre()
{
if (!Overwrite && File.Exists(Path))
{
throw new FFMpegException(FFMpegExceptionType.File, "Output file already exists and overwrite is disabled");
}
}
public Task During(CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public void Post()
{
}
public string Text => $"\"{Path}\"{(Overwrite ? " -y" : string.Empty)}";
} }

View file

@ -1,28 +1,27 @@
using System.IO.Pipes; using System.IO.Pipes;
using FFMpegCore.Pipes; using FFMpegCore.Pipes;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
public class OutputPipeArgument : PipeArgument, IOutputArgument
{ {
public class OutputPipeArgument : PipeArgument, IOutputArgument public readonly IPipeSink Reader;
public OutputPipeArgument(IPipeSink reader) : base(PipeDirection.In)
{ {
public readonly IPipeSink Reader; Reader = reader;
}
public OutputPipeArgument(IPipeSink reader) : base(PipeDirection.In) public override string Text => $"\"{PipePath}\" -y";
protected override async Task ProcessDataAsync(CancellationToken token)
{
await Pipe.WaitForConnectionAsync(token).ConfigureAwait(false);
if (!Pipe.IsConnected)
{ {
Reader = reader; throw new TaskCanceledException();
} }
public override string Text => $"\"{PipePath}\" -y"; await Reader.ReadAsync(Pipe, token).ConfigureAwait(false);
protected override async Task ProcessDataAsync(CancellationToken token)
{
await Pipe.WaitForConnectionAsync(token).ConfigureAwait(false);
if (!Pipe.IsConnected)
{
throw new TaskCanceledException();
}
await Reader.ReadAsync(Pipe, token).ConfigureAwait(false);
}
} }
} }

View file

@ -1,57 +1,59 @@
 namespace FFMpegCore.Arguments;
namespace FFMpegCore.Arguments
internal class OutputTeeArgument : IOutputArgument
{ {
internal class OutputTeeArgument : IOutputArgument private readonly FFMpegMultiOutputOptions _options;
public OutputTeeArgument(FFMpegMultiOutputOptions options)
{ {
private readonly FFMpegMultiOutputOptions _options; if (options.Outputs.Count == 0)
public OutputTeeArgument(FFMpegMultiOutputOptions options)
{ {
if (options.Outputs.Count == 0) throw new ArgumentException("Atleast one output must be specified.", nameof(options));
{
throw new ArgumentException("Atleast one output must be specified.", nameof(options));
}
_options = options;
} }
public string Text => $"-f tee \"{string.Join("|", _options.Outputs.Select(MapOptions))}\""; _options = options;
}
public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask; public string Text => $"-f tee \"{string.Join("|", _options.Outputs.Select(MapOptions))}\"";
public void Post() public Task During(CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public void Post()
{
}
public void Pre()
{
}
private static string MapOptions(FFMpegArgumentOptions option)
{
var optionPrefix = string.Empty;
if (option.Arguments.Count > 1)
{ {
var options = option.Arguments.Take(option.Arguments.Count - 1);
optionPrefix = $"[{string.Join(":", options.Select(MapArgument))}]";
} }
public void Pre() var output = option.Arguments.OfType<IOutputArgument>().Single();
return $"{optionPrefix}{output.Text.Trim('"')}";
}
private static string MapArgument(IArgument argument)
{
if (argument is MapStreamArgument map)
{ {
return map.Text.Replace("-map ", "select=\\'") + "\\'";
} }
private static string MapOptions(FFMpegArgumentOptions option) if (argument is BitStreamFilterArgument bitstreamFilter)
{ {
var optionPrefix = string.Empty; return bitstreamFilter.Text.Replace("-bsf:", "bsfs/").Replace(' ', '=');
if (option.Arguments.Count > 1)
{
var options = option.Arguments.Take(option.Arguments.Count - 1);
optionPrefix = $"[{string.Join(":", options.Select(MapArgument))}]";
}
var output = option.Arguments.OfType<IOutputArgument>().Single();
return $"{optionPrefix}{output.Text.Trim('"')}";
} }
private static string MapArgument(IArgument argument) return argument.Text.TrimStart('-').Replace(' ', '=');
{
if (argument is MapStreamArgument map)
{
return map.Text.Replace("-map ", "select=\\'") + "\\'";
}
else if (argument is BitStreamFilterArgument bitstreamFilter)
{
return bitstreamFilter.Text.Replace("-bsf:", "bsfs/").Replace(' ', '=');
}
return argument.Text.TrimStart('-').Replace(' ', '=');
}
} }
} }

View file

@ -1,24 +1,26 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
/// <summary>
/// Represents outputting to url using supported protocols
/// See http://ffmpeg.org/ffmpeg-protocols.html
/// </summary>
public class OutputUrlArgument : IOutputArgument
{ {
/// <summary> public readonly string Url;
/// Represents outputting to url using supported protocols
/// See http://ffmpeg.org/ffmpeg-protocols.html public OutputUrlArgument(string url)
/// </summary>
public class OutputUrlArgument : IOutputArgument
{ {
public readonly string Url; Url = url;
public OutputUrlArgument(string url)
{
Url = url;
}
public void Post() { }
public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask;
public void Pre() { }
public string Text => Url;
} }
public void Post() { }
public Task During(CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public void Pre() { }
public string Text => Url;
} }

View file

@ -1,11 +1,10 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
/// <summary>
/// Represents overwrite parameter
/// If output file should be overwritten if exists
/// </summary>
public class OverwriteArgument : IArgument
{ {
/// <summary> public string Text => "-y";
/// Represents overwrite parameter
/// If output file should be overwritten if exists
/// </summary>
public class OverwriteArgument : IArgument
{
public string Text => "-y";
}
} }

View file

@ -1,64 +1,62 @@
using FFMpegCore.Extend; using FFMpegCore.Extend;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
public class PadArgument : IVideoFilterArgument
{ {
public class PadArgument : IVideoFilterArgument private readonly PadOptions _options;
public PadArgument(PadOptions options)
{ {
private readonly PadOptions _options; _options = options;
public PadArgument(PadOptions options)
{
_options = options;
}
public string Key => "pad";
public string Value => _options.TextInternal;
} }
public class PadOptions public string Key => "pad";
public string Value => _options.TextInternal;
}
public class PadOptions
{
public readonly Dictionary<string, string> Parameters = new();
private PadOptions(string? width, string? height)
{ {
public readonly Dictionary<string, string> Parameters = new(); if (width == null && height == null)
internal string TextInternal => string.Join(":", Parameters.Select(parameter => parameter.FormatArgumentPair(true)));
public static PadOptions Create(string? width, string? height)
{ {
return new PadOptions(width, height); throw new Exception("At least one of the parameters must be not null");
} }
public static PadOptions Create(string aspectRatio) if (width != null)
{ {
return new PadOptions(aspectRatio); Parameters.Add("width", width);
} }
public PadOptions WithParameter(string key, string value) if (height != null)
{ {
Parameters.Add(key, value); Parameters.Add("height", height);
return this;
} }
}
private PadOptions(string? width, string? height) private PadOptions(string aspectRatio)
{ {
if (width == null && height == null) Parameters.Add("aspect", aspectRatio);
{ }
throw new Exception("At least one of the parameters must be not null");
}
if (width != null) internal string TextInternal => string.Join(":", Parameters.Select(parameter => parameter.FormatArgumentPair(true)));
{
Parameters.Add("width", width);
}
if (height != null) public static PadOptions Create(string? width, string? height)
{ {
Parameters.Add("height", height); return new PadOptions(width, height);
} }
}
private PadOptions(string aspectRatio) public static PadOptions Create(string aspectRatio)
{ {
Parameters.Add("aspect", aspectRatio); return new PadOptions(aspectRatio);
} }
public PadOptions WithParameter(string key, string value)
{
Parameters.Add(key, value);
return this;
} }
} }

View file

@ -1,61 +1,60 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
/// <summary>
/// Mix channels with specific gain levels.
/// </summary>
public class PanArgument : IAudioFilterArgument
{ {
private readonly string[] _outputDefinitions;
public readonly string ChannelLayout;
/// <summary> /// <summary>
/// Mix channels with specific gain levels. /// Mix channels with specific gain levels <see href="https://ffmpeg.org/ffmpeg-filters.html#toc-pan-1" />
/// </summary> /// </summary>
public class PanArgument : IAudioFilterArgument /// <param name="channelLayout">
/// Represent the output channel layout. Like "stereo", "mono", "2.1", "5.1"
/// </param>
/// <param name="outputDefinitions">
/// Output channel specification, of the form: "out_name=[gain*]in_name[(+-)[gain*]in_name...]"
/// </param>
public PanArgument(string channelLayout, params string[] outputDefinitions)
{ {
public readonly string ChannelLayout; if (string.IsNullOrWhiteSpace(channelLayout))
private readonly string[] _outputDefinitions;
/// <summary>
/// Mix channels with specific gain levels <see href="https://ffmpeg.org/ffmpeg-filters.html#toc-pan-1"/>
/// </summary>
/// <param name="channelLayout">
/// Represent the output channel layout. Like "stereo", "mono", "2.1", "5.1"
/// </param>
/// <param name="outputDefinitions">
/// Output channel specification, of the form: "out_name=[gain*]in_name[(+-)[gain*]in_name...]"
/// </param>
public PanArgument(string channelLayout, params string[] outputDefinitions)
{ {
if (string.IsNullOrWhiteSpace(channelLayout)) throw new ArgumentException("The channel layout must be set", nameof(channelLayout));
{
throw new ArgumentException("The channel layout must be set", nameof(channelLayout));
}
ChannelLayout = channelLayout;
_outputDefinitions = outputDefinitions;
} }
/// <summary> ChannelLayout = channelLayout;
/// Mix channels with specific gain levels <see href="https://ffmpeg.org/ffmpeg-filters.html#toc-pan-1"/>
/// </summary>
/// <param name="channels">Number of channels in output file</param>
/// <param name="outputDefinitions">
/// Output channel specification, of the form: "out_name=[gain*]in_name[(+-)[gain*]in_name...]"
/// </param>
public PanArgument(int channels, params string[] outputDefinitions)
{
if (channels <= 0)
{
throw new ArgumentOutOfRangeException(nameof(channels));
}
if (outputDefinitions.Length > channels) _outputDefinitions = outputDefinitions;
{
throw new ArgumentException("The number of output definitions must be equal or lower than number of channels", nameof(outputDefinitions));
}
ChannelLayout = $"{channels}c";
_outputDefinitions = outputDefinitions;
}
public string Key { get; } = "pan";
public string Value =>
string.Join("|", Enumerable.Empty<string>().Append(ChannelLayout).Concat(_outputDefinitions));
} }
/// <summary>
/// Mix channels with specific gain levels <see href="https://ffmpeg.org/ffmpeg-filters.html#toc-pan-1" />
/// </summary>
/// <param name="channels">Number of channels in output file</param>
/// <param name="outputDefinitions">
/// Output channel specification, of the form: "out_name=[gain*]in_name[(+-)[gain*]in_name...]"
/// </param>
public PanArgument(int channels, params string[] outputDefinitions)
{
if (channels <= 0)
{
throw new ArgumentOutOfRangeException(nameof(channels));
}
if (outputDefinitions.Length > channels)
{
throw new ArgumentException("The number of output definitions must be equal or lower than number of channels", nameof(outputDefinitions));
}
ChannelLayout = $"{channels}c";
_outputDefinitions = outputDefinitions;
}
public string Key { get; } = "pan";
public string Value =>
string.Join("|", Enumerable.Empty<string>().Append(ChannelLayout).Concat(_outputDefinitions));
} }

View file

@ -2,23 +2,28 @@
using System.IO.Pipes; using System.IO.Pipes;
using FFMpegCore.Pipes; using FFMpegCore.Pipes;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
public abstract class PipeArgument
{ {
public abstract class PipeArgument private readonly PipeDirection _direction;
private readonly object _pipeLock = new();
protected PipeArgument(PipeDirection direction)
{ {
private string PipeName { get; } PipeName = PipeHelpers.GetUniquePipeName();
public string PipePath => PipeHelpers.GetPipePath(PipeName); _direction = direction;
}
protected NamedPipeServerStream Pipe { get; private set; } = null!; private string PipeName { get; }
private readonly PipeDirection _direction; public string PipePath => PipeHelpers.GetPipePath(PipeName);
protected PipeArgument(PipeDirection direction) protected NamedPipeServerStream Pipe { get; private set; } = null!;
{ public abstract string Text { get; }
PipeName = PipeHelpers.GetUnqiuePipeName();
_direction = direction;
}
public void Pre() public void Pre()
{
lock (_pipeLock)
{ {
if (Pipe != null) if (Pipe != null)
{ {
@ -27,35 +32,40 @@ namespace FFMpegCore.Arguments
Pipe = new NamedPipeServerStream(PipeName, _direction, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); Pipe = new NamedPipeServerStream(PipeName, _direction, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
} }
}
public void Post() public void Post()
{
Debug.WriteLine($"Disposing NamedPipeServerStream on {GetType().Name}");
lock (_pipeLock)
{ {
Debug.WriteLine($"Disposing NamedPipeServerStream on {GetType().Name}");
Pipe?.Dispose(); Pipe?.Dispose();
Pipe = null!; Pipe = null!;
} }
}
public async Task During(CancellationToken cancellationToken = default) public async Task During(CancellationToken cancellationToken = default)
{
try
{ {
try await ProcessDataAsync(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
Debug.WriteLine($"ProcessDataAsync on {GetType().Name} cancelled");
}
finally
{
Debug.WriteLine($"Disconnecting NamedPipeServerStream on {GetType().Name}");
lock (_pipeLock)
{ {
await ProcessDataAsync(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
Debug.WriteLine($"ProcessDataAsync on {GetType().Name} cancelled");
}
finally
{
Debug.WriteLine($"Disconnecting NamedPipeServerStream on {GetType().Name}");
if (Pipe is { IsConnected: true }) if (Pipe is { IsConnected: true })
{ {
Pipe.Disconnect(); Pipe.Disconnect();
} }
} }
} }
protected abstract Task ProcessDataAsync(CancellationToken token);
public abstract string Text { get; }
} }
protected abstract Task ProcessDataAsync(CancellationToken token);
} }

View file

@ -1,10 +1,9 @@
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
/// <summary>
/// Remove metadata argument
/// </summary>
public class RemoveMetadataArgument : IArgument
{ {
/// <summary> public string Text => "-map_metadata -1";
/// Remove metadata argument
/// </summary>
public class RemoveMetadataArgument : IArgument
{
public string Text => "-map_metadata -1";
}
} }

View file

@ -1,27 +1,27 @@
using System.Drawing; using System.Drawing;
using FFMpegCore.Enums; using FFMpegCore.Enums;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
/// <summary>
/// Represents scale parameter
/// </summary>
public class ScaleArgument : IVideoFilterArgument
{ {
/// <summary> public readonly Size? Size;
/// Represents scale parameter
/// </summary> public ScaleArgument(Size? size)
public class ScaleArgument : IVideoFilterArgument
{ {
public readonly Size? Size; Size = size;
public ScaleArgument(Size? size)
{
Size = size;
}
public ScaleArgument(int width, int height) : this(new Size(width, height)) { }
public ScaleArgument(VideoSize videosize)
{
Size = videosize == VideoSize.Original ? null : (Size?)new Size(-1, (int)videosize);
}
public string Key { get; } = "scale";
public string Value => Size == null ? string.Empty : $"{Size.Value.Width}:{Size.Value.Height}";
} }
public ScaleArgument(int width, int height) : this(new Size(width, height)) { }
public ScaleArgument(VideoSize videosize)
{
Size = videosize == VideoSize.Original ? null : new Size(-1, (int)videosize);
}
public string Key { get; } = "scale";
public string Value => Size == null ? string.Empty : $"{Size.Value.Width}:{Size.Value.Height}";
} }

View file

@ -1,19 +1,18 @@
using FFMpegCore.Extend; using FFMpegCore.Extend;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
/// <summary>
/// Represents seek parameter
/// </summary>
public class SeekArgument : IArgument
{ {
/// <summary> public readonly TimeSpan? SeekTo;
/// Represents seek parameter
/// </summary> public SeekArgument(TimeSpan? seekTo)
public class SeekArgument : IArgument
{ {
public readonly TimeSpan? SeekTo; SeekTo = seekTo;
public SeekArgument(TimeSpan? seekTo)
{
SeekTo = seekTo;
}
public string Text => SeekTo.HasValue ? $"-ss {SeekTo.Value.ToLongString()}" : string.Empty;
} }
public string Text => SeekTo.HasValue ? $"-ss {SeekTo.Value.ToLongString()}" : string.Empty;
} }

View file

@ -1,24 +1,23 @@
using FFMpegCore.Enums; using FFMpegCore.Enums;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments;
public class SetMirroringArgument : IVideoFilterArgument
{ {
public class SetMirroringArgument : IVideoFilterArgument public SetMirroringArgument(Mirroring mirroring)
{ {
public SetMirroringArgument(Mirroring mirroring) Mirroring = mirroring;
{
Mirroring = mirroring;
}
public Mirroring Mirroring { get; set; }
public string Key => string.Empty;
public string Value =>
Mirroring switch
{
Mirroring.Horizontal => "hflip",
Mirroring.Vertical => "vflip",
_ => throw new ArgumentOutOfRangeException(nameof(Mirroring))
};
} }
public Mirroring Mirroring { get; set; }
public string Key => string.Empty;
public string Value =>
Mirroring switch
{
Mirroring.Horizontal => "hflip",
Mirroring.Vertical => "vflip",
_ => throw new ArgumentOutOfRangeException(nameof(Mirroring))
};
} }

Some files were not shown because too many files have changed in this diff Show more